diff --git a/CHANGELOG.md b/CHANGELOG.md index efaced221..8d5c765c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ - per-skill config map moved to `skills.entries` (e.g. `skills.peekaboo.enabled` → `skills.entries.peekaboo.enabled`) - new optional bundled allowlist: `skills.allowBundled` (only affects bundled skills) - Sessions: group keys now use `surface:group:` / `surface:channel:`; legacy `group:*` keys migrate on next message; `groupdm` keys are no longer recognized. +- Discord: remove legacy `discord.allowFrom`, `discord.guildAllowFrom`, and `discord.requireMention`; use `discord.dm` + `discord.guilds`. ### Features - Talk mode: continuous speech conversations (macOS/iOS/Android) with ElevenLabs TTS, reply directives, and optional interrupt-on-speech. @@ -23,6 +24,7 @@ - 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. - Discord: allow agent-triggered reactions via `clawdis_discord` when enabled, and surface message ids in context. +- Discord: revamp guild routing config with per-guild/channel rules and slugged display names; add optional group DM support (default off). - Skills: add Trello skill for board/list/card management (thanks @clawd). - Tests: add a Z.AI live test gate for smoke validation when keys are present. - macOS Debug: add app log verbosity and rolling file log toggle for swift-log-backed app logs. diff --git a/docs/configuration.md b/docs/configuration.md index 4c9f7f054..8564e98ce 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -177,22 +177,28 @@ Configure the Discord bot by setting the bot token and optional gating: enableReactions: true, // allow agent-triggered reactions dm: { enabled: true, // disable all DMs when false - allowFrom: ["1234567890", "steipete"] // optional DM allowlist (ids or names) + allowFrom: ["1234567890", "steipete"], // optional DM allowlist (ids or names) + groupEnabled: false, // enable group DMs + groupChannels: ["clawd-dm"] // optional group DM allowlist }, - 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 - } + guilds: { + "123456789012345678": { // guild id (preferred) or slug + slug: "friends-of-clawd", + requireMention: false, // per-guild default + users: ["987654321098765432"], // optional per-guild user allowlist + channels: { + general: { allow: true }, + help: { allow: true, requireMention: true } + } + } + }, + historyLimit: 20 // include last N guild messages as context } } ``` Clawdis reads `DISCORD_BOT_TOKEN` or `discord.token` to start the provider (unless `discord.enabled` is `false`). Use `user:` (DM) or `channel:` (guild channel) when specifying delivery targets for cron/CLI commands. +Guild slugs are lowercase with spaces replaced by `-`; channel keys use the slugged channel name (no leading `#`). Prefer guild ids as keys to avoid rename ambiguity. ### `imessage` (imsg CLI) diff --git a/docs/discord.md b/docs/discord.md index d914cab11..da3e4658c 100644 --- a/docs/discord.md +++ b/docs/discord.md @@ -11,8 +11,8 @@ Status: ready for DM and guild text channels via the official Discord bot gatewa ## Goals - Talk to Clawdis via Discord DMs or guild channels. -- Share the same `main` session used by WhatsApp/Telegram/WebChat; guild channels stay isolated as `discord:group:`. -- Group DMs are treated as group sessions (separate from `main`) and show up with a `discord:g-...` display label. +- Share the same `main` session used by WhatsApp/Telegram/WebChat; guild channels stay isolated as `discord:group:` (display names use `discord:#`). +- Group DMs are ignored by default; enable via `discord.dm.groupEnabled` and optionally restrict by `discord.dm.groupChannels`. - Keep routing deterministic: replies always go back to the surface they arrived on. ## How it works @@ -21,14 +21,14 @@ 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`). 4. Run the gateway; it auto-starts the Discord provider when the token is set (unless `discord.enabled = false`). 5. Direct chats: use `user:` (or a `<@id>` mention) when delivering; all turns land in the shared `main` session. -6. Guild channels: use `channel:` for delivery. Mentions are required by default; disable with `discord.guild.requireMention = false` (legacy: `discord.requireMention`). -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.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 channel allowlist: set `discord.guild.channels` with channel ids or names to restrict where the bot listens. -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. +6. Guild channels: use `channel:` for delivery. Mentions are required by default and can be set per guild or per channel. +7. Optional DM control: set `discord.dm.enabled = false` to ignore all DMs, or `discord.dm.allowFrom` to allow specific users (ids or names). Use `discord.dm.groupEnabled` + `discord.dm.groupChannels` to allow group DMs. +8. Optional guild rules: set `discord.guilds` keyed by guild id (preferred) or slug, with per-channel rules. +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. +10. 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: Slugs are lowercase with spaces replaced by `-`. Channel names are slugged without the leading `#`. ## Capabilities & limits - DMs and guild text channels (threads are treated as separate channels; voice not supported). @@ -47,16 +47,20 @@ Note: Discord does not provide a simple username → id lookup without extra gui enableReactions: true, dm: { enabled: true, - allowFrom: ["123456789012345678", "steipete"] + allowFrom: ["123456789012345678", "steipete"], + groupEnabled: false, + groupChannels: ["clawd-dm"] }, - guild: { - channels: ["general", "help"], - allowFrom: { - guilds: ["123456789012345678", "My Server"], - users: ["987654321098765432", "steipete"] - }, - requireMention: true, - historyLimit: 20 + guilds: { + "123456789012345678": { + slug: "friends-of-clawd", + requireMention: false, + users: ["987654321098765432", "steipete"], + channels: { + general: { allow: true }, + help: { allow: true, requireMention: true } + } + } } } } @@ -64,11 +68,15 @@ Note: Discord does not provide a simple username → id lookup without extra gui - `dm.enabled`: set `false` to ignore all DMs (default `true`). - `dm.allowFrom`: DM allowlist (user ids or names). Omit or set to `["*"]` to allow any DM sender. -- `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. +- `dm.groupEnabled`: enable group DMs (default `false`). +- `dm.groupChannels`: optional allowlist for group DM channel ids or slugs. +- `guilds`: per-guild rules keyed by guild id (preferred) or slug. +- `guilds..slug`: optional friendly slug used for display names. +- `guilds..users`: optional per-guild user allowlist (ids or names). +- `guilds..channels`: channel rules (keys are channel slugs or ids). +- `guilds..requireMention`: per-guild mention requirement (overridable per channel). - `mediaMaxMb`: clamp inbound media saved to disk. -- `guild.historyLimit`: number of recent guild messages to include as context when replying to a mention (default 20, `0` disables). +- `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`). ## Reactions diff --git a/docs/session.md b/docs/session.md index ca78efed8..8430819fa 100644 --- a/docs/session.md +++ b/docs/session.md @@ -24,7 +24,7 @@ All session state is **owned by the gateway** (the “master” Clawdis). UI cli ## Mapping transports → session keys - Direct chats (WhatsApp, Telegram, Discord, desktop Web Chat) all collapse to the **primary key** so they share context. - Multiple phone numbers can map to that same key; they act as transports into the same conversation. -- Group chats isolate state with `surface:group:` keys (rooms/channels use `surface:channel:`); do not reuse the primary key for groups. +- Group chats isolate state with `surface:group:` keys (rooms/channels use `surface:channel:`); do not reuse the primary key for groups. (Discord display names show `discord:#`.) - Legacy `group::` and `group:` keys are still recognized. ## Lifecyle diff --git a/src/auto-reply/reply.ts b/src/auto-reply/reply.ts index 17bc067c7..fff9a5158 100644 --- a/src/auto-reply/reply.ts +++ b/src/auto-reply/reply.ts @@ -453,14 +453,20 @@ export async function getReplyFromConfig( if (groupResolution?.surface) { const surface = groupResolution.surface; const subject = ctx.GroupSubject?.trim(); + const space = ctx.GroupSpace?.trim(); + const explicitRoom = ctx.GroupRoom?.trim(); const isRoomSurface = surface === "discord" || surface === "slack"; const nextRoom = - isRoomSurface && subject && subject.startsWith("#") ? subject : undefined; + explicitRoom ?? + (isRoomSurface && subject && subject.startsWith("#") + ? subject + : undefined); const nextSubject = nextRoom ? undefined : subject; sessionEntry.chatType = groupResolution.chatType ?? "group"; sessionEntry.surface = surface; if (nextSubject) sessionEntry.subject = nextSubject; if (nextRoom) sessionEntry.room = nextRoom; + if (space) sessionEntry.space = space; sessionEntry.displayName = buildGroupDisplayName({ surface: sessionEntry.surface, subject: sessionEntry.subject, diff --git a/src/auto-reply/templating.ts b/src/auto-reply/templating.ts index 00dc4a9a7..3e3c0ac59 100644 --- a/src/auto-reply/templating.ts +++ b/src/auto-reply/templating.ts @@ -12,6 +12,8 @@ export type MsgContext = { Transcript?: string; ChatType?: string; GroupSubject?: string; + GroupRoom?: string; + GroupSpace?: string; GroupMembers?: string; SenderName?: string; SenderE164?: string; diff --git a/src/config/config.ts b/src/config/config.ts index 7aa8b46d3..b4d1161c7 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -169,42 +169,35 @@ export type DiscordDmConfig = { enabled?: boolean; /** Allowlist for DM senders (ids or names). */ allowFrom?: Array; + /** If true, allow group DMs (default: false). */ + groupEnabled?: boolean; + /** Optional allowlist for group DM channels (ids or slugs). */ + groupChannels?: Array; }; -export type DiscordGuildConfig = { - /** Allowlist for guild messages (guilds/users by id or name). */ - allowFrom?: { - guilds?: Array; - users?: Array; - }; - /** Allowlist for guild channels (ids or names). */ - channels?: Array; - /** Require @bot mention to respond in guilds. Default: true. */ +export type DiscordGuildChannelConfig = { + allow?: boolean; requireMention?: boolean; - /** Number of recent guild messages to include for context. */ - historyLimit?: number; +}; + +export type DiscordGuildEntry = { + slug?: string; + requireMention?: boolean; + users?: Array; + channels?: Record; }; export type DiscordConfig = { /** If false, do not start the Discord provider. Default: true. */ enabled?: boolean; token?: string; - /** Legacy DM allowlist (ids). Prefer discord.dm.allowFrom. */ - allowFrom?: Array; - /** Legacy guild allowlist (ids). Prefer discord.guild.allowFrom. */ - guildAllowFrom?: { - guilds?: Array; - users?: Array; - }; - /** Legacy mention requirement. Prefer discord.guild.requireMention. */ - requireMention?: boolean; mediaMaxMb?: number; - /** Legacy history limit. Prefer discord.guild.historyLimit. */ historyLimit?: number; /** Allow agent-triggered Discord reactions (default: true). */ enableReactions?: boolean; dm?: DiscordDmConfig; - guild?: DiscordGuildConfig; + /** New per-guild config keyed by guild id or slug. */ + guilds?: Record; }; export type SignalConfig = { @@ -934,14 +927,6 @@ const ClawdisSchema = z.object({ .object({ enabled: z.boolean().optional(), token: z.string().optional(), - allowFrom: z.array(z.union([z.string(), z.number()])).optional(), - guildAllowFrom: z - .object({ - guilds: z.array(z.union([z.string(), z.number()])).optional(), - users: z.array(z.union([z.string(), z.number()])).optional(), - }) - .optional(), - requireMention: z.boolean().optional(), mediaMaxMb: z.number().positive().optional(), historyLimit: z.number().int().min(0).optional(), enableReactions: z.boolean().optional(), @@ -949,8 +934,33 @@ const ClawdisSchema = z.object({ .object({ enabled: z.boolean().optional(), allowFrom: z.array(z.union([z.string(), z.number()])).optional(), + groupEnabled: z.boolean().optional(), + groupChannels: z.array(z.union([z.string(), z.number()])).optional(), }) .optional(), + guilds: z + .record( + z.string(), + z + .object({ + slug: z.string().optional(), + requireMention: z.boolean().optional(), + users: z.array(z.union([z.string(), z.number()])).optional(), + channels: z + .record( + z.string(), + z + .object({ + allow: z.boolean().optional(), + requireMention: z.boolean().optional(), + }) + .optional(), + ) + .optional(), + }) + .optional(), + ) + .optional(), guild: z .object({ allowFrom: z diff --git a/src/config/sessions.ts b/src/config/sessions.ts index 0a4f7df60..f96de2cf9 100644 --- a/src/config/sessions.ts +++ b/src/config/sessions.ts @@ -116,11 +116,13 @@ export function buildGroupDisplayName(params: { key: string; }) { const surfaceKey = (params.surface?.trim().toLowerCase() || "group").trim(); + const room = params.room?.trim(); + const space = params.space?.trim(); + const subject = params.subject?.trim(); const detail = - params.room?.trim() || - params.subject?.trim() || - params.space?.trim() || - ""; + (room && space + ? `${space}${room.startsWith("#") ? "" : "#"}${room}` + : room || subject || space || "") || ""; const fallbackId = params.id?.trim() || params.key.replace(/^group:/, ""); const rawLabel = detail || fallbackId; let token = normalizeGroupLabel(rawLabel); @@ -130,7 +132,12 @@ export function buildGroupDisplayName(params: { if (!params.room && token.startsWith("#")) { token = token.replace(/^#+/, ""); } - if (token && !/^[@#]/.test(token) && !token.startsWith("g-")) { + if ( + token && + !/^[@#]/.test(token) && + !token.startsWith("g-") && + !token.includes("#") + ) { token = `g-${token}`; } return token ? `${surfaceKey}:${token}` : surfaceKey; diff --git a/src/discord/monitor.ts b/src/discord/monitor.ts index a7ea34e56..0ac5c3e8b 100644 --- a/src/discord/monitor.ts +++ b/src/discord/monitor.ts @@ -25,12 +25,6 @@ export type MonitorDiscordOpts = { token?: string; runtime?: RuntimeEnv; abortSignal?: AbortSignal; - allowFrom?: Array; - guildAllowFrom?: { - guilds?: Array; - users?: Array; - }; - requireMention?: boolean; mediaMaxMb?: number; historyLimit?: number; }; @@ -54,6 +48,19 @@ type DiscordAllowList = { names: Set; }; +type DiscordGuildEntryResolved = { + id?: string; + slug?: string; + requireMention?: boolean; + users?: Array; + channels?: Record; +}; + +type DiscordChannelConfigResolved = { + allowed: boolean; + requireMention?: boolean; +}; + export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) { const cfg = loadConfig(); const token = normalizeDiscordToken( @@ -77,29 +84,17 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) { }; const dmConfig = cfg.discord?.dm; - 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 = - opts.requireMention ?? - guildConfig?.requireMention ?? - cfg.discord?.requireMention ?? - true; + const guildEntries = cfg.discord?.guilds; + const allowFrom = dmConfig?.allowFrom; const mediaMaxBytes = (opts.mediaMaxMb ?? cfg.discord?.mediaMaxMb ?? 8) * 1024 * 1024; const historyLimit = Math.max( 0, - opts.historyLimit ?? - guildConfig?.historyLimit ?? - cfg.discord?.historyLimit ?? - 20, + opts.historyLimit ?? cfg.discord?.historyLimit ?? 20, ); const dmEnabled = dmConfig?.enabled ?? true; + const groupDmEnabled = dmConfig?.groupEnabled ?? false; + const groupDmChannels = dmConfig?.groupChannels; const client = new Client({ intents: [ @@ -128,8 +123,10 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) { if (!message.author) return; const channelType = message.channel.type; + const isGroupDm = channelType === ChannelType.GroupDM; const isDirectMessage = channelType === ChannelType.DM; const isGuildMessage = Boolean(message.guild); + if (isGroupDm && !groupDmEnabled) return; if (isDirectMessage && !dmEnabled) return; const botId = client.user?.id; const wasMentioned = @@ -141,6 +138,58 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) { message.embeds[0]?.description || ""; + const guildInfo = isGuildMessage + ? resolveDiscordGuildEntry({ + guild: message.guild, + guildEntries, + }) + : null; + if ( + isGuildMessage && + guildEntries && + Object.keys(guildEntries).length > 0 && + !guildInfo + ) { + logVerbose( + `Blocked discord guild ${message.guild?.id ?? "unknown"} (not in discord.guilds)`, + ); + return; + } + + const channelName = + (isGuildMessage || isGroupDm) && "name" in message.channel + ? message.channel.name + : undefined; + const channelSlug = channelName ? normalizeDiscordSlug(channelName) : ""; + const guildSlug = + guildInfo?.slug || + (message.guild?.name ? normalizeDiscordSlug(message.guild.name) : ""); + const channelConfig = isGuildMessage + ? resolveDiscordChannelConfig({ + guildInfo, + channelId: message.channelId, + channelName, + channelSlug, + }) + : null; + + const groupDmAllowed = + isGroupDm && + resolveGroupDmAllow({ + channels: groupDmChannels, + channelId: message.channelId, + channelName, + channelSlug, + }); + if (isGroupDm && !groupDmAllowed) return; + + if (isGuildMessage && channelConfig?.allowed === false) { + logVerbose( + `Blocked discord channel ${message.channelId} not in guild channel allowlist`, + ); + return; + } + if (isGuildMessage && historyLimit > 0 && baseText) { const history = guildHistories.get(message.channelId) ?? []; history.push({ @@ -153,7 +202,9 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) { guildHistories.set(message.channelId, history); } - if (isGuildMessage && requireMention) { + const resolvedRequireMention = + channelConfig?.requireMention ?? guildInfo?.requireMention ?? true; + if (isGuildMessage && resolvedRequireMention) { if (botId && !wasMentioned) { logger.info( { @@ -167,56 +218,27 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) { } 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, [ - "guild:", - ]); - const users = normalizeDiscordAllowList(guildAllowFrom.users, [ - "discord:", - "user:", - ]); - if (guilds || users) { - const guildId = message.guild?.id ?? ""; - const userId = message.author.id; - const guildOk = - !guilds || - allowListMatches(guilds, { - id: guildId, - name: message.guild?.name, - }); + const userAllow = guildInfo?.users; + if (Array.isArray(userAllow) && userAllow.length > 0) { + const users = normalizeDiscordAllowList(userAllow, [ + "discord:", + "user:", + ]); const userOk = !users || allowListMatches(users, { - id: userId, + id: message.author.id, name: message.author.username, tag: message.author.tag, }); - if (!guildOk || !userOk) { + if (!userOk) { logVerbose( - `Blocked discord guild sender ${userId} (guild ${guildId || "unknown"}) not in guildAllowFrom`, + `Blocked discord guild sender ${message.author.id} (not in guild users allowlist)`, ); return; } } + } if (isDirectMessage && Array.isArray(allowFrom) && allowFrom.length > 0) { @@ -250,13 +272,9 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) { const fromLabel = isDirectMessage ? buildDirectLabel(message) : buildGuildLabel(message); - const groupSubject = (() => { - if (isDirectMessage) return undefined; - const channelName = - "name" in message.channel ? message.channel.name : message.channelId; - if (!channelName) return undefined; - return isGuildMessage ? `#${channelName}` : channelName; - })(); + const groupRoom = + isGuildMessage && channelSlug ? `#${channelSlug}` : undefined; + const groupSubject = isDirectMessage ? undefined : groupRoom; const textWithId = `${text}\n[discord message id: ${message.id} channel: ${message.channelId}]`; let combinedBody = formatAgentEnvelope({ surface: "Discord", @@ -298,6 +316,8 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) { ChatType: isDirectMessage ? "direct" : "group", SenderName: message.member?.displayName ?? message.author.tag, GroupSubject: groupSubject, + GroupRoom: groupRoom, + GroupSpace: isGuildMessage ? guildSlug || undefined : undefined, Surface: "discord" as const, WasMentioned: wasMentioned, MessageSid: message.id, @@ -457,6 +477,8 @@ function normalizeDiscordAllowList( } const normalized = normalizeDiscordName(entry); if (normalized) names.add(normalized); + const slugged = normalizeDiscordSlug(entry); + if (slugged) names.add(slugged); } if (!allowAll && ids.size === 0 && names.size === 0) return null; @@ -468,6 +490,17 @@ function normalizeDiscordName(value?: string | null) { return value.trim().toLowerCase(); } +function normalizeDiscordSlug(value?: string | null) { + if (!value) return ""; + let text = value.trim().toLowerCase(); + if (!text) return ""; + text = text.replace(/^[@#]+/, ""); + text = text.replace(/[\s_]+/g, "-"); + text = text.replace(/[^a-z0-9-]+/g, "-"); + text = text.replace(/-{2,}/g, "-").replace(/^-+|-+$/g, ""); + return text; +} + function allowListMatches( allowList: DiscordAllowList, candidates: { @@ -483,9 +516,100 @@ function allowListMatches( if (normalizedName && allowList.names.has(normalizedName)) return true; const normalizedTag = normalizeDiscordName(tag); if (normalizedTag && allowList.names.has(normalizedTag)) return true; + const slugName = normalizeDiscordSlug(name); + if (slugName && allowList.names.has(slugName)) return true; + const slugTag = normalizeDiscordSlug(tag); + if (slugTag && allowList.names.has(slugTag)) return true; return false; } +function resolveDiscordGuildEntry(params: { + guild: import("discord.js").Guild | null; + guildEntries: Record | undefined; +}): DiscordGuildEntryResolved | null { + const { guild, guildEntries } = params; + if (!guild || !guildEntries || Object.keys(guildEntries).length === 0) { + return null; + } + const guildId = guild.id; + const guildSlug = normalizeDiscordSlug(guild.name); + const direct = guildEntries[guildId]; + if (direct) { + return { + id: guildId, + slug: direct.slug ?? guildSlug, + requireMention: direct.requireMention, + users: direct.users, + channels: direct.channels, + }; + } + if (guildSlug && guildEntries[guildSlug]) { + const entry = guildEntries[guildSlug]; + return { + id: guildId, + slug: entry.slug ?? guildSlug, + requireMention: entry.requireMention, + users: entry.users, + channels: entry.channels, + }; + } + const matchBySlug = Object.entries(guildEntries).find(([, entry]) => { + const entrySlug = normalizeDiscordSlug(entry.slug); + return entrySlug && entrySlug === guildSlug; + }); + if (matchBySlug) { + const entry = matchBySlug[1]; + return { + id: guildId, + slug: entry.slug ?? guildSlug, + requireMention: entry.requireMention, + users: entry.users, + channels: entry.channels, + }; + } + return null; +} + +function resolveDiscordChannelConfig(params: { + guildInfo: DiscordGuildEntryResolved | null; + channelId: string; + channelName?: string; + channelSlug?: string; +}): DiscordChannelConfigResolved | null { + const { guildInfo, channelId, channelName, channelSlug } = params; + const channelEntries = guildInfo?.channels; + if (channelEntries && Object.keys(channelEntries).length > 0) { + const entry = + channelEntries[channelId] ?? + (channelSlug + ? channelEntries[channelSlug] ?? + channelEntries[`#${channelSlug}`] + : undefined) ?? + (channelName + ? channelEntries[normalizeDiscordSlug(channelName)] + : undefined); + if (!entry) return { allowed: false }; + return { allowed: entry.allow !== false, requireMention: entry.requireMention }; + } + return { allowed: true }; +} + +function resolveGroupDmAllow(params: { + channels: Array | undefined; + channelId: string; + channelName?: string; + channelSlug?: string; +}) { + const { channels, channelId, channelName, channelSlug } = params; + if (!channels || channels.length === 0) return true; + const allowList = normalizeDiscordAllowList(channels, ["channel:"]); + if (!allowList) return true; + return allowListMatches(allowList, { + id: channelId, + name: channelSlug || channelName, + }); +} + async function sendTyping(message: Message) { try { const channel = message.channel; diff --git a/src/gateway/server.ts b/src/gateway/server.ts index e62841b34..73c11f5be 100644 --- a/src/gateway/server.ts +++ b/src/gateway/server.ts @@ -2210,9 +2210,6 @@ export async function startGatewayServer( token: discordToken.trim(), runtime: discordRuntimeEnv, abortSignal: discordAbort.signal, - allowFrom: cfg.discord?.allowFrom, - guildAllowFrom: cfg.discord?.guildAllowFrom, - requireMention: cfg.discord?.requireMention, mediaMaxMb: cfg.discord?.mediaMaxMb, historyLimit: cfg.discord?.historyLimit, })