Files
openclaw/extensions/discord/src/mentions.ts
scoootscooob 5682ec37fa refactor: move Discord channel implementation to extensions/ (#45660)
* refactor: move Discord channel implementation to extensions/discord/src/

Move all Discord source files from src/discord/ to extensions/discord/src/,
following the extension migration pattern. Source files in src/discord/ are
replaced with re-export shims. Channel-plugin files from
src/channels/plugins/*/discord* are similarly moved and shimmed.

- Copy all .ts source files preserving subdirectory structure (monitor/, voice/)
- Move channel-plugin files (actions, normalize, onboarding, outbound, status-issues)
- Fix all relative imports to use correct paths from new location
- Create re-export shims at original locations for backward compatibility
- Delete test files from shim locations (tests live in extension now)
- Update tsconfig.plugin-sdk.dts.json rootDir from "src" to "." to accommodate
  extension files outside src/
- Update write-plugin-sdk-entry-dts.ts to match new declaration output paths

* fix: add importOriginal to thread-bindings session-meta mock for extensions test

* style: fix formatting in thread-bindings lifecycle test
2026-03-14 02:53:57 -07:00

84 lines
2.8 KiB
TypeScript

import { resolveDiscordDirectoryUserId } from "./directory-cache.js";
const MARKDOWN_CODE_SEGMENT_PATTERN = /```[\s\S]*?```|`[^`\n]*`/g;
const MENTION_CANDIDATE_PATTERN = /(^|[\s([{"'.,;:!?])@([a-z0-9_.-]{2,32}(?:#[0-9]{4})?)/gi;
const DISCORD_RESERVED_MENTIONS = new Set(["everyone", "here"]);
function normalizeSnowflake(value: string | number | bigint): string | null {
const text = String(value ?? "").trim();
if (!/^\d+$/.test(text)) {
return null;
}
return text;
}
export function formatMention(params: {
userId?: string | number | bigint | null;
roleId?: string | number | bigint | null;
channelId?: string | number | bigint | null;
}): string {
const userId = params.userId == null ? null : normalizeSnowflake(params.userId);
const roleId = params.roleId == null ? null : normalizeSnowflake(params.roleId);
const channelId = params.channelId == null ? null : normalizeSnowflake(params.channelId);
const values = [
userId ? { kind: "user" as const, id: userId } : null,
roleId ? { kind: "role" as const, id: roleId } : null,
channelId ? { kind: "channel" as const, id: channelId } : null,
].filter((entry): entry is { kind: "user" | "role" | "channel"; id: string } => Boolean(entry));
if (values.length !== 1) {
throw new Error("formatMention requires exactly one of userId, roleId, or channelId");
}
const target = values[0];
if (target.kind === "user") {
return `<@${target.id}>`;
}
if (target.kind === "role") {
return `<@&${target.id}>`;
}
return `<#${target.id}>`;
}
function rewritePlainTextMentions(text: string, accountId?: string | null): string {
if (!text.includes("@")) {
return text;
}
return text.replace(MENTION_CANDIDATE_PATTERN, (match, prefix, rawHandle) => {
const handle = String(rawHandle ?? "").trim();
if (!handle) {
return match;
}
const lookup = handle.toLowerCase();
if (DISCORD_RESERVED_MENTIONS.has(lookup)) {
return match;
}
const userId = resolveDiscordDirectoryUserId({
accountId,
handle,
});
if (!userId) {
return match;
}
return `${String(prefix ?? "")}${formatMention({ userId })}`;
});
}
export function rewriteDiscordKnownMentions(
text: string,
params: { accountId?: string | null },
): string {
if (!text.includes("@")) {
return text;
}
let rewritten = "";
let offset = 0;
MARKDOWN_CODE_SEGMENT_PATTERN.lastIndex = 0;
for (const match of text.matchAll(MARKDOWN_CODE_SEGMENT_PATTERN)) {
const matchIndex = match.index ?? 0;
rewritten += rewritePlainTextMentions(text.slice(offset, matchIndex), params.accountId);
rewritten += match[0];
offset = matchIndex + match[0].length;
}
rewritten += rewritePlainTextMentions(text.slice(offset), params.accountId);
return rewritten;
}