feat(chat): @everyone mention
CI / Build & test (Node 20) (push) Canceled after 0s
CI / Build & test (Node 24) (push) Canceled after 0s
CI / Build & test (push) Canceled after 0s
CodeQL / Analyze (javascript-typescript) (push) Canceled after 0s
Security / Secret scan (gitleaks) (push) Canceled after 0s
Security / Dependency scan (OSV-Scanner) (push) Canceled after 0s
Security / IaC/config scan (Trivy) (push) Canceled after 0s
Security / License compliance scan (Trivy) (push) Canceled after 0s
OpenSSF Scorecard / Scorecard analysis (push) Canceled after 0s
CI / Build & test (Node 20) (push) Canceled after 0s
CI / Build & test (Node 24) (push) Canceled after 0s
CI / Build & test (push) Canceled after 0s
CodeQL / Analyze (javascript-typescript) (push) Canceled after 0s
Security / Secret scan (gitleaks) (push) Canceled after 0s
Security / Dependency scan (OSV-Scanner) (push) Canceled after 0s
Security / IaC/config scan (Trivy) (push) Canceled after 0s
Security / License compliance scan (Trivy) (push) Canceled after 0s
OpenSSF Scorecard / Scorecard analysis (push) Canceled after 0s
Rides the existing `<@id>` token: `everyone` already matches the parser's
`[a-zA-Z0-9_-]+`, so the markdown pre-processor, the inline renderer and the
badge pick it up with no second syntax. User ids are snowflakes, so the
literal `everyone` cannot collide with one.
Gated on a new MENTION_EVERYONE permission rather than being open to all —
otherwise a role can never take it away. Granted to @everyone by default: on
a small instance the ping is a convenience, not a weapon, and an owner who
disagrees can deny it and grant it to a role instead.
Two things that would each have made the gate a no-op:
- DEFAULT_EVERYONE_PERMISSIONS only applies at space creation, so spaces
that predate the bit would have had every @everyone mention silently
stripped. A boot-time backfill ORs the bit into @everyone roles only
(`id = space_id`), leaving other roles alone so a later denial is not
overruled on the next restart. Verified against a copy of the production
database: three roles moved by exactly 2^15, non-@everyone roles
untouched, second pass a no-op.
- Enforcement covers edit as well as create; without that, posting a plain
message and editing it is the way around the permission.
A message that lacks the permission is not rejected — losing what someone
wrote over one token is worse than posting it. The token is demoted to the
literal `@everyone` text they typed: the message reads the same and rings
nobody.
Code spans and fences never ping. People paste payloads and snippets into
chat, and a paste should not ring everyone's client. The same rule decides
the row highlight and the notification sound, so the three cannot disagree.
Verified: web build, server typecheck, 504 web tests, 804 server tests.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -16,7 +16,9 @@
|
||||
"./src/constants": "./src/constants.ts",
|
||||
"./src/constants.js": "./src/constants.ts",
|
||||
"./src/audit": "./src/audit.ts",
|
||||
"./src/audit.js": "./src/audit.ts"
|
||||
"./src/audit.js": "./src/audit.ts",
|
||||
"./src/mentions": "./src/mentions.ts",
|
||||
"./src/mentions.js": "./src/mentions.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* The @everyone mention.
|
||||
*
|
||||
* It rides the existing `<@id>` token shape — `everyone` matches the same
|
||||
* `[a-zA-Z0-9_-]+` the user-mention parser already accepts — so the markdown
|
||||
* pre-processor, the inline renderer and the badge all pick it up without a
|
||||
* second syntax. User ids are snowflakes (digits only), so the literal
|
||||
* `everyone` can never collide with a real one.
|
||||
*/
|
||||
export const EVERYONE_MENTION_ID = 'everyone';
|
||||
export const EVERYONE_MENTION_TOKEN = `<@${EVERYONE_MENTION_ID}>`;
|
||||
|
||||
/** Matches code spans and fences first, so their contents are never a ping. */
|
||||
const CODE_OR_EVERYONE = /(```[\s\S]*?```|`[^`\n]*`)|<@everyone>/g;
|
||||
|
||||
/**
|
||||
* Whether a message pings everyone.
|
||||
*
|
||||
* A token inside a code span or fenced block does not count: people paste
|
||||
* message payloads and snippets into chat, and a paste should not ring
|
||||
* everybody's client.
|
||||
*/
|
||||
export function mentionsEveryone(content: string | null | undefined): boolean {
|
||||
if (!content) return false;
|
||||
CODE_OR_EVERYONE.lastIndex = 0;
|
||||
for (let m = CODE_OR_EVERYONE.exec(content); m; m = CODE_OR_EVERYONE.exec(content)) {
|
||||
if (!m[1]) return true; // matched the token itself, not a code span
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Neutralises the token into plain text, leaving code spans untouched.
|
||||
*
|
||||
* The server applies this when the author lacks MENTION_EVERYONE. Rejecting the
|
||||
* message instead would lose what they wrote over a formatting detail; this way
|
||||
* the message still posts, and reads as the literal text they typed.
|
||||
*/
|
||||
export function stripEveryoneMention(content: string): string {
|
||||
return content.replace(CODE_OR_EVERYONE, (match, codeBlock: string | undefined) =>
|
||||
codeBlock ? codeBlock : '@everyone',
|
||||
);
|
||||
}
|
||||
@@ -16,6 +16,7 @@ export const PermissionBits = {
|
||||
ATTACH_FILES: 1n << 12n,
|
||||
READ_MESSAGE_HISTORY: 1n << 13n,
|
||||
ADD_REACTIONS: 1n << 14n,
|
||||
MENTION_EVERYONE: 1n << 15n,
|
||||
CONNECT: 1n << 20n,
|
||||
SPEAK: 1n << 21n,
|
||||
MUTE_MEMBERS: 1n << 22n,
|
||||
@@ -38,6 +39,10 @@ export const DEFAULT_EVERYONE_PERMISSIONS =
|
||||
PermissionBits.ATTACH_FILES |
|
||||
PermissionBits.READ_MESSAGE_HISTORY |
|
||||
PermissionBits.ADD_REACTIONS |
|
||||
// Granted by default: on a small instance the ping is a convenience, not a
|
||||
// weapon. An owner who disagrees can deny it on @everyone and grant it to a
|
||||
// role instead — which is why it is a bit rather than being ungated.
|
||||
PermissionBits.MENTION_EVERYONE |
|
||||
PermissionBits.STREAM;
|
||||
|
||||
// ─── Helpers ────────────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user