feat: add discord dm/guild allowlists
This commit is contained in:
@@ -18,6 +18,7 @@
|
|||||||
- UI: add optional `ui.seamColor` accent to tint the Talk Mode side bubble (macOS/iOS/Android).
|
- UI: add optional `ui.seamColor` accent to tint the Talk Mode side bubble (macOS/iOS/Android).
|
||||||
- Nix mode: opt-in declarative config + read-only settings UI when `CLAWDIS_NIX_MODE=1` (thanks @joshp123 for the persistence — earned my trust; I'll merge these going forward).
|
- Nix mode: opt-in declarative config + read-only settings UI when `CLAWDIS_NIX_MODE=1` (thanks @joshp123 for the persistence — earned my trust; I'll merge these going forward).
|
||||||
- Agent runtime: accept legacy `Z_AI_API_KEY` for Z.AI provider auth (maps to `ZAI_API_KEY`).
|
- Agent runtime: accept legacy `Z_AI_API_KEY` for Z.AI provider auth (maps to `ZAI_API_KEY`).
|
||||||
|
- Discord: add DM enable/allowlist plus guild channel/user/guild allowlists with id/name matching.
|
||||||
- Signal: add `signal-cli` JSON-RPC support for send/receive via the Signal provider.
|
- Signal: add `signal-cli` JSON-RPC support for send/receive via the Signal provider.
|
||||||
- iMessage: add imsg JSON-RPC integration (stdio), chat_id routing, and group chat support.
|
- iMessage: add imsg JSON-RPC integration (stdio), chat_id routing, and group chat support.
|
||||||
- Chat UI: add recent-session dropdown switcher (main first) in macOS/iOS/Android + Control UI.
|
- Chat UI: add recent-session dropdown switcher (main first) in macOS/iOS/Android + Control UI.
|
||||||
|
|||||||
@@ -173,15 +173,21 @@ Configure the Discord bot by setting the bot token and optional gating:
|
|||||||
discord: {
|
discord: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
token: "your-bot-token",
|
token: "your-bot-token",
|
||||||
allowFrom: ["discord:1234567890", "*"], // optional DM allowlist (user ids)
|
|
||||||
guildAllowFrom: {
|
|
||||||
guilds: ["123456789012345678"], // optional guild allowlist (ids)
|
|
||||||
users: ["987654321098765432"] // optional user allowlist (ids)
|
|
||||||
},
|
|
||||||
requireMention: true, // require @bot mentions in guilds
|
|
||||||
mediaMaxMb: 8, // clamp inbound media size
|
mediaMaxMb: 8, // clamp inbound media size
|
||||||
historyLimit: 20, // include last N guild messages as context
|
enableReactions: true, // allow agent-triggered reactions
|
||||||
enableReactions: true // allow agent-triggered reactions
|
dm: {
|
||||||
|
enabled: true, // disable all DMs when false
|
||||||
|
allowFrom: ["1234567890", "steipete"] // optional DM allowlist (ids or names)
|
||||||
|
},
|
||||||
|
guild: {
|
||||||
|
channels: ["general", "help"], // optional channel allowlist (ids or names)
|
||||||
|
allowFrom: {
|
||||||
|
guilds: ["123456789012345678"], // optional guild allowlist (ids or names)
|
||||||
|
users: ["987654321098765432"] // optional user allowlist (ids or names)
|
||||||
|
},
|
||||||
|
requireMention: true, // require @bot mentions in guilds
|
||||||
|
historyLimit: 20 // include last N guild messages as context
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -21,11 +21,12 @@ Status: ready for DM and guild text channels via the official Discord bot gatewa
|
|||||||
3. Configure Clawdis with `DISCORD_BOT_TOKEN` (or `discord.token` in `~/.clawdis/clawdis.json`).
|
3. Configure Clawdis with `DISCORD_BOT_TOKEN` (or `discord.token` in `~/.clawdis/clawdis.json`).
|
||||||
4. Run the gateway; it auto-starts the Discord provider when the token is set (unless `discord.enabled = false`).
|
4. Run the gateway; it auto-starts the Discord provider when the token is set (unless `discord.enabled = false`).
|
||||||
5. Direct chats: use `user:<id>` (or a `<@id>` mention) when delivering; all turns land in the shared `main` session.
|
5. Direct chats: use `user:<id>` (or a `<@id>` mention) when delivering; all turns land in the shared `main` session.
|
||||||
6. Guild channels: use `channel:<channelId>` for delivery. Mentions are required by default; disable with `discord.requireMention = false`.
|
6. Guild channels: use `channel:<channelId>` for delivery. Mentions are required by default; disable with `discord.guild.requireMention = false` (legacy: `discord.requireMention`).
|
||||||
7. Optional DM allowlist: reuse `discord.allowFrom` with user ids (`1234567890` or `discord:1234567890`). Use `"*"` to allow all DMs.
|
7. Optional DM control: set `discord.dm.enabled = false` to ignore all DMs, or `discord.dm.allowFrom` to allow specific users (ids or names). Legacy: `discord.allowFrom`.
|
||||||
8. Optional guild allowlist: set `discord.guildAllowFrom` with `guilds` and/or `users` to gate who can invoke the bot in servers.
|
8. Optional guild allowlist: set `discord.guild.allowFrom` with `guilds` and/or `users` (ids or names) to gate who can invoke the bot in servers. Legacy: `discord.guildAllowFrom`.
|
||||||
9. Optional guild context history: set `discord.historyLimit` (default 20) to include the last N guild messages as context when replying to a mention. Set `0` to disable.
|
9. Optional guild channel allowlist: set `discord.guild.channels` with channel ids or names to restrict where the bot listens.
|
||||||
10. Reactions (default on): set `discord.enableReactions = false` to disable agent-triggered reactions via the `clawdis_discord` tool.
|
10. Optional guild context history: set `discord.guild.historyLimit` (default 20) to include the last N guild messages as context when replying to a mention. Set `0` to disable (legacy: `discord.historyLimit`).
|
||||||
|
11. Reactions (default on): set `discord.enableReactions = false` to disable agent-triggered reactions via the `clawdis_discord` tool.
|
||||||
|
|
||||||
Note: Discord does not provide a simple username → id lookup without extra guild context, so prefer ids or `<@id>` mentions for DM delivery targets.
|
Note: Discord does not provide a simple username → id lookup without extra guild context, so prefer ids or `<@id>` mentions for DM delivery targets.
|
||||||
|
|
||||||
@@ -42,24 +43,32 @@ Note: Discord does not provide a simple username → id lookup without extra gui
|
|||||||
discord: {
|
discord: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
token: "abc.123",
|
token: "abc.123",
|
||||||
allowFrom: ["123456789012345678"],
|
|
||||||
guildAllowFrom: {
|
|
||||||
guilds: ["123456789012345678"],
|
|
||||||
users: ["987654321098765432"]
|
|
||||||
},
|
|
||||||
requireMention: true,
|
|
||||||
mediaMaxMb: 8,
|
mediaMaxMb: 8,
|
||||||
historyLimit: 20,
|
enableReactions: true,
|
||||||
enableReactions: true
|
dm: {
|
||||||
|
enabled: true,
|
||||||
|
allowFrom: ["123456789012345678", "steipete"]
|
||||||
|
},
|
||||||
|
guild: {
|
||||||
|
channels: ["general", "help"],
|
||||||
|
allowFrom: {
|
||||||
|
guilds: ["123456789012345678", "My Server"],
|
||||||
|
users: ["987654321098765432", "steipete"]
|
||||||
|
},
|
||||||
|
requireMention: true,
|
||||||
|
historyLimit: 20
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
- `allowFrom`: DM allowlist (user ids). Omit or set to `["*"]` to allow any DM sender.
|
- `dm.enabled`: set `false` to ignore all DMs (default `true`).
|
||||||
- `guildAllowFrom`: Optional allowlist for guild messages. Set `guilds` and/or `users` (ids). When both are set, both must match.
|
- `dm.allowFrom`: DM allowlist (user ids or names). Omit or set to `["*"]` to allow any DM sender.
|
||||||
- `requireMention`: when `true`, messages in guild channels must mention the bot.
|
- `guild.allowFrom`: Optional allowlist for guild messages. Set `guilds` and/or `users` (ids or names). When both are set, both must match.
|
||||||
|
- `guild.channels`: Optional allowlist for channel ids or names.
|
||||||
|
- `guild.requireMention`: when `true`, messages in guild channels must mention the bot.
|
||||||
- `mediaMaxMb`: clamp inbound media saved to disk.
|
- `mediaMaxMb`: clamp inbound media saved to disk.
|
||||||
- `historyLimit`: number of recent guild messages to include as context when replying to a mention (default 20, `0` disables).
|
- `guild.historyLimit`: number of recent guild messages to include as context when replying to a mention (default 20, `0` disables).
|
||||||
- `enableReactions`: allow agent-triggered reactions via the `clawdis_discord` tool (default `true`).
|
- `enableReactions`: allow agent-triggered reactions via the `clawdis_discord` tool (default `true`).
|
||||||
|
|
||||||
## Reactions
|
## Reactions
|
||||||
|
|||||||
@@ -164,21 +164,47 @@ export type TelegramConfig = {
|
|||||||
webhookPath?: string;
|
webhookPath?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type DiscordDmConfig = {
|
||||||
|
/** If false, ignore all incoming Discord DMs. Default: true. */
|
||||||
|
enabled?: boolean;
|
||||||
|
/** Allowlist for DM senders (ids or names). */
|
||||||
|
allowFrom?: Array<string | number>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DiscordGuildConfig = {
|
||||||
|
/** Allowlist for guild messages (guilds/users by id or name). */
|
||||||
|
allowFrom?: {
|
||||||
|
guilds?: Array<string | number>;
|
||||||
|
users?: Array<string | number>;
|
||||||
|
};
|
||||||
|
/** Allowlist for guild channels (ids or names). */
|
||||||
|
channels?: Array<string | number>;
|
||||||
|
/** Require @bot mention to respond in guilds. Default: true. */
|
||||||
|
requireMention?: boolean;
|
||||||
|
/** Number of recent guild messages to include for context. */
|
||||||
|
historyLimit?: number;
|
||||||
|
};
|
||||||
|
|
||||||
export type DiscordConfig = {
|
export type DiscordConfig = {
|
||||||
/** If false, do not start the Discord provider. Default: true. */
|
/** If false, do not start the Discord provider. Default: true. */
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
token?: string;
|
token?: string;
|
||||||
|
/** Legacy DM allowlist (ids). Prefer discord.dm.allowFrom. */
|
||||||
allowFrom?: Array<string | number>;
|
allowFrom?: Array<string | number>;
|
||||||
|
/** Legacy guild allowlist (ids). Prefer discord.guild.allowFrom. */
|
||||||
guildAllowFrom?: {
|
guildAllowFrom?: {
|
||||||
guilds?: Array<string | number>;
|
guilds?: Array<string | number>;
|
||||||
users?: Array<string | number>;
|
users?: Array<string | number>;
|
||||||
};
|
};
|
||||||
|
/** Legacy mention requirement. Prefer discord.guild.requireMention. */
|
||||||
requireMention?: boolean;
|
requireMention?: boolean;
|
||||||
mediaMaxMb?: number;
|
mediaMaxMb?: number;
|
||||||
/** Number of recent guild messages to include for context (default: 20). */
|
/** Legacy history limit. Prefer discord.guild.historyLimit. */
|
||||||
historyLimit?: number;
|
historyLimit?: number;
|
||||||
/** Allow agent-triggered Discord reactions (default: true). */
|
/** Allow agent-triggered Discord reactions (default: true). */
|
||||||
enableReactions?: boolean;
|
enableReactions?: boolean;
|
||||||
|
dm?: DiscordDmConfig;
|
||||||
|
guild?: DiscordGuildConfig;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SignalConfig = {
|
export type SignalConfig = {
|
||||||
@@ -919,6 +945,25 @@ const ClawdisSchema = z.object({
|
|||||||
mediaMaxMb: z.number().positive().optional(),
|
mediaMaxMb: z.number().positive().optional(),
|
||||||
historyLimit: z.number().int().min(0).optional(),
|
historyLimit: z.number().int().min(0).optional(),
|
||||||
enableReactions: z.boolean().optional(),
|
enableReactions: z.boolean().optional(),
|
||||||
|
dm: z
|
||||||
|
.object({
|
||||||
|
enabled: z.boolean().optional(),
|
||||||
|
allowFrom: z.array(z.union([z.string(), z.number()])).optional(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
guild: z
|
||||||
|
.object({
|
||||||
|
allowFrom: z
|
||||||
|
.object({
|
||||||
|
guilds: z.array(z.union([z.string(), z.number()])).optional(),
|
||||||
|
users: z.array(z.union([z.string(), z.number()])).optional(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
channels: z.array(z.union([z.string(), z.number()])).optional(),
|
||||||
|
requireMention: z.boolean().optional(),
|
||||||
|
historyLimit: z.number().int().min(0).optional(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
signal: z
|
signal: z
|
||||||
|
|||||||
@@ -48,6 +48,12 @@ type DiscordHistoryEntry = {
|
|||||||
messageId?: string;
|
messageId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type DiscordAllowList = {
|
||||||
|
allowAll: boolean;
|
||||||
|
ids: Set<string>;
|
||||||
|
names: Set<string>;
|
||||||
|
};
|
||||||
|
|
||||||
export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
|
export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
|
||||||
const cfg = loadConfig();
|
const cfg = loadConfig();
|
||||||
const token = normalizeDiscordToken(
|
const token = normalizeDiscordToken(
|
||||||
@@ -70,16 +76,28 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const allowFrom = opts.allowFrom ?? cfg.discord?.allowFrom;
|
const dmConfig = cfg.discord?.dm;
|
||||||
const guildAllowFrom = opts.guildAllowFrom ?? cfg.discord?.guildAllowFrom;
|
const guildConfig = cfg.discord?.guild;
|
||||||
|
const allowFrom =
|
||||||
|
opts.allowFrom ?? dmConfig?.allowFrom ?? cfg.discord?.allowFrom;
|
||||||
|
const guildAllowFrom =
|
||||||
|
opts.guildAllowFrom ?? guildConfig?.allowFrom ?? cfg.discord?.guildAllowFrom;
|
||||||
|
const guildChannels = guildConfig?.channels;
|
||||||
const requireMention =
|
const requireMention =
|
||||||
opts.requireMention ?? cfg.discord?.requireMention ?? true;
|
opts.requireMention ??
|
||||||
|
guildConfig?.requireMention ??
|
||||||
|
cfg.discord?.requireMention ??
|
||||||
|
true;
|
||||||
const mediaMaxBytes =
|
const mediaMaxBytes =
|
||||||
(opts.mediaMaxMb ?? cfg.discord?.mediaMaxMb ?? 8) * 1024 * 1024;
|
(opts.mediaMaxMb ?? cfg.discord?.mediaMaxMb ?? 8) * 1024 * 1024;
|
||||||
const historyLimit = Math.max(
|
const historyLimit = Math.max(
|
||||||
0,
|
0,
|
||||||
opts.historyLimit ?? cfg.discord?.historyLimit ?? 20,
|
opts.historyLimit ??
|
||||||
|
guildConfig?.historyLimit ??
|
||||||
|
cfg.discord?.historyLimit ??
|
||||||
|
20,
|
||||||
);
|
);
|
||||||
|
const dmEnabled = dmConfig?.enabled ?? true;
|
||||||
|
|
||||||
const client = new Client({
|
const client = new Client({
|
||||||
intents: [
|
intents: [
|
||||||
@@ -111,6 +129,8 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
|
|||||||
const isGroupDm = channelType === ChannelType.GroupDM;
|
const isGroupDm = channelType === ChannelType.GroupDM;
|
||||||
const isDirectMessage = channelType === ChannelType.DM;
|
const isDirectMessage = channelType === ChannelType.DM;
|
||||||
const isGuildMessage = Boolean(message.guild);
|
const isGuildMessage = Boolean(message.guild);
|
||||||
|
if (isGroupDm) return;
|
||||||
|
if (isDirectMessage && !dmEnabled) return;
|
||||||
const botId = client.user?.id;
|
const botId = client.user?.id;
|
||||||
const wasMentioned =
|
const wasMentioned =
|
||||||
!isDirectMessage && Boolean(botId && message.mentions.has(botId));
|
!isDirectMessage && Boolean(botId && message.mentions.has(botId));
|
||||||
@@ -121,7 +141,7 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
|
|||||||
message.embeds[0]?.description ||
|
message.embeds[0]?.description ||
|
||||||
"";
|
"";
|
||||||
|
|
||||||
if (!isDirectMessage && historyLimit > 0 && baseText) {
|
if (isGuildMessage && historyLimit > 0 && baseText) {
|
||||||
const history = guildHistories.get(message.channelId) ?? [];
|
const history = guildHistories.get(message.channelId) ?? [];
|
||||||
history.push({
|
history.push({
|
||||||
sender: message.member?.displayName ?? message.author.tag,
|
sender: message.member?.displayName ?? message.author.tag,
|
||||||
@@ -133,7 +153,7 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
|
|||||||
guildHistories.set(message.channelId, history);
|
guildHistories.set(message.channelId, history);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isDirectMessage && requireMention) {
|
if (isGuildMessage && requireMention) {
|
||||||
if (botId && !wasMentioned) {
|
if (botId && !wasMentioned) {
|
||||||
logger.info(
|
logger.info(
|
||||||
{
|
{
|
||||||
@@ -146,7 +166,27 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isDirectMessage && isGuildMessage && guildAllowFrom) {
|
if (isGuildMessage) {
|
||||||
|
const channelAllow = normalizeDiscordAllowList(guildChannels, [
|
||||||
|
"channel:",
|
||||||
|
]);
|
||||||
|
if (channelAllow) {
|
||||||
|
const channelName =
|
||||||
|
"name" in message.channel ? message.channel.name : undefined;
|
||||||
|
const channelOk = allowListMatches(channelAllow, {
|
||||||
|
id: message.channelId,
|
||||||
|
name: channelName,
|
||||||
|
});
|
||||||
|
if (!channelOk) {
|
||||||
|
logVerbose(
|
||||||
|
`Blocked discord channel ${message.channelId} not in guild.channels`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isGuildMessage && guildAllowFrom) {
|
||||||
const guilds = normalizeDiscordAllowList(guildAllowFrom.guilds, [
|
const guilds = normalizeDiscordAllowList(guildAllowFrom.guilds, [
|
||||||
"guild:",
|
"guild:",
|
||||||
]);
|
]);
|
||||||
@@ -158,8 +198,18 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
|
|||||||
const guildId = message.guild?.id ?? "";
|
const guildId = message.guild?.id ?? "";
|
||||||
const userId = message.author.id;
|
const userId = message.author.id;
|
||||||
const guildOk =
|
const guildOk =
|
||||||
!guilds || guilds.allowAll || (guildId && guilds.ids.has(guildId));
|
!guilds ||
|
||||||
const userOk = !users || users.allowAll || users.ids.has(userId);
|
allowListMatches(guilds, {
|
||||||
|
id: guildId,
|
||||||
|
name: message.guild?.name,
|
||||||
|
});
|
||||||
|
const userOk =
|
||||||
|
!users ||
|
||||||
|
allowListMatches(users, {
|
||||||
|
id: userId,
|
||||||
|
name: message.author.username,
|
||||||
|
tag: message.author.tag,
|
||||||
|
});
|
||||||
if (!guildOk || !userOk) {
|
if (!guildOk || !userOk) {
|
||||||
logVerbose(
|
logVerbose(
|
||||||
`Blocked discord guild sender ${userId} (guild ${guildId || "unknown"}) not in guildAllowFrom`,
|
`Blocked discord guild sender ${userId} (guild ${guildId || "unknown"}) not in guildAllowFrom`,
|
||||||
@@ -170,22 +220,20 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isDirectMessage && Array.isArray(allowFrom) && allowFrom.length > 0) {
|
if (isDirectMessage && Array.isArray(allowFrom) && allowFrom.length > 0) {
|
||||||
const allowed = allowFrom
|
const allowList = normalizeDiscordAllowList(allowFrom, [
|
||||||
.map((entry) => String(entry).trim())
|
"discord:",
|
||||||
.filter(Boolean);
|
"user:",
|
||||||
const candidate = message.author.id;
|
]);
|
||||||
const normalized = new Set(
|
|
||||||
allowed
|
|
||||||
.filter((entry) => entry !== "*")
|
|
||||||
.map((entry) => entry.replace(/^discord:/i, "")),
|
|
||||||
);
|
|
||||||
const permitted =
|
const permitted =
|
||||||
allowed.includes("*") ||
|
allowList &&
|
||||||
normalized.has(candidate) ||
|
allowListMatches(allowList, {
|
||||||
allowed.includes(candidate);
|
id: message.author.id,
|
||||||
|
name: message.author.username,
|
||||||
|
tag: message.author.tag,
|
||||||
|
});
|
||||||
if (!permitted) {
|
if (!permitted) {
|
||||||
logVerbose(
|
logVerbose(
|
||||||
`Blocked unauthorized discord sender ${candidate} (not in allowFrom)`,
|
`Blocked unauthorized discord sender ${message.author.id} (not in allowFrom)`,
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -300,7 +348,7 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
|
|||||||
token,
|
token,
|
||||||
runtime,
|
runtime,
|
||||||
});
|
});
|
||||||
if (!isDirectMessage && shouldClearHistory && historyLimit > 0) {
|
if (isGuildMessage && shouldClearHistory && historyLimit > 0) {
|
||||||
guildHistories.set(message.channelId, []);
|
guildHistories.set(message.channelId, []);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -384,22 +432,67 @@ function buildGuildLabel(message: import("discord.js").Message) {
|
|||||||
function normalizeDiscordAllowList(
|
function normalizeDiscordAllowList(
|
||||||
raw: Array<string | number> | undefined,
|
raw: Array<string | number> | undefined,
|
||||||
prefixes: string[],
|
prefixes: string[],
|
||||||
): { allowAll: boolean; ids: Set<string> } | null {
|
): DiscordAllowList | null {
|
||||||
if (!raw || raw.length === 0) return null;
|
if (!raw || raw.length === 0) return null;
|
||||||
const cleaned = raw
|
const ids = new Set<string>();
|
||||||
.map((entry) => String(entry).trim())
|
const names = new Set<string>();
|
||||||
.filter(Boolean)
|
let allowAll = false;
|
||||||
.map((entry) => {
|
|
||||||
for (const prefix of prefixes) {
|
for (const rawEntry of raw) {
|
||||||
if (entry.toLowerCase().startsWith(prefix)) {
|
let entry = String(rawEntry).trim();
|
||||||
return entry.slice(prefix.length);
|
if (!entry) continue;
|
||||||
}
|
if (entry === "*") {
|
||||||
|
allowAll = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (const prefix of prefixes) {
|
||||||
|
if (entry.toLowerCase().startsWith(prefix)) {
|
||||||
|
entry = entry.slice(prefix.length);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
return entry;
|
}
|
||||||
});
|
const mentionMatch = entry.match(/^<[@#][!]?(\d+)>$/);
|
||||||
const allowAll = cleaned.includes("*");
|
if (mentionMatch?.[1]) {
|
||||||
const ids = new Set(cleaned.filter((entry) => entry !== "*"));
|
ids.add(mentionMatch[1]);
|
||||||
return { allowAll, ids };
|
continue;
|
||||||
|
}
|
||||||
|
entry = entry.trim();
|
||||||
|
if (entry.startsWith("@") || entry.startsWith("#")) {
|
||||||
|
entry = entry.slice(1);
|
||||||
|
}
|
||||||
|
if (/^\d+$/.test(entry)) {
|
||||||
|
ids.add(entry);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const normalized = normalizeDiscordName(entry);
|
||||||
|
if (normalized) names.add(normalized);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!allowAll && ids.size === 0 && names.size === 0) return null;
|
||||||
|
return { allowAll, ids, names };
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeDiscordName(value?: string | null) {
|
||||||
|
if (!value) return "";
|
||||||
|
return value.trim().toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
function allowListMatches(
|
||||||
|
allowList: DiscordAllowList,
|
||||||
|
candidates: {
|
||||||
|
id?: string;
|
||||||
|
name?: string | null;
|
||||||
|
tag?: string | null;
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
if (allowList.allowAll) return true;
|
||||||
|
const { id, name, tag } = candidates;
|
||||||
|
if (id && allowList.ids.has(id)) return true;
|
||||||
|
const normalizedName = normalizeDiscordName(name);
|
||||||
|
if (normalizedName && allowList.names.has(normalizedName)) return true;
|
||||||
|
const normalizedTag = normalizeDiscordName(tag);
|
||||||
|
if (normalizedTag && allowList.names.has(normalizedTag)) return true;
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function sendTyping(message: Message) {
|
async function sendTyping(message: Message) {
|
||||||
|
|||||||
Reference in New Issue
Block a user