diff --git a/lettabot.example.yaml b/lettabot.example.yaml index 7888ddb..844c3eb 100644 --- a/lettabot.example.yaml +++ b/lettabot.example.yaml @@ -39,15 +39,26 @@ channels: # groupPollIntervalMin: 5 # Batch interval for group messages (default: 10) # instantGroups: ["-100123456"] # Groups that bypass batching # listeningGroups: ["-100123456"] # Groups where bot observes but only replies when @mentioned + # Group access control (which groups can interact, mention requirement): + # groups: + # "*": { requireMention: true } # Default: only respond when @mentioned + # "-1001234567890": { requireMention: false } # This group gets all messages + # mentionPatterns: ["hey bot"] # Additional regex patterns for mention detection # slack: # enabled: true # appToken: xapp-... # botToken: xoxb-... # listeningGroups: ["C0123456789"] # Channels where bot observes only + # # groups: + # # "*": { requireMention: true } # Default: only respond when @mentioned + # # "C0123456789": { requireMention: false } # discord: # enabled: true # token: YOUR-DISCORD-BOT-TOKEN # listeningGroups: ["1234567890123456789"] # Server/channel IDs where bot observes only + # # groups: + # # "*": { requireMention: true } # Default: only respond when @mentioned + # # "1234567890123456789": { requireMention: false } # Server or channel ID # whatsapp: # enabled: true # selfChat: false diff --git a/src/channels/discord.ts b/src/channels/discord.ts index 2ff5f73..9981e05 100644 --- a/src/channels/discord.ts +++ b/src/channels/discord.ts @@ -23,6 +23,7 @@ export interface DiscordConfig { allowedUsers?: string[]; // Discord user IDs attachmentsDir?: string; attachmentsMaxBytes?: number; + groups?: Record; // Per-guild/channel settings } export class DiscordAdapter implements ChannelAdapter { @@ -242,6 +243,33 @@ Ask the bot owner to approve with: const displayName = message.member?.displayName || message.author.globalName || message.author.username; const wasMentioned = isGroup && !!this.client?.user && message.mentions.has(this.client.user); + // Group gating: config-based allowlist + mention requirement + if (isGroup && this.config.groups) { + const groups = this.config.groups; + const chatId = message.channel.id; + const serverId = message.guildId; + const allowlistEnabled = Object.keys(groups).length > 0; + + if (allowlistEnabled) { + const hasWildcard = Object.hasOwn(groups, '*'); + const hasSpecific = Object.hasOwn(groups, chatId) + || (serverId && Object.hasOwn(groups, serverId)); + if (!hasWildcard && !hasSpecific) { + console.log(`[Discord] Group ${chatId} not in allowlist, ignoring`); + return; + } + } + + const groupConfig = groups[chatId] + ?? (serverId ? groups[serverId] : undefined) + ?? groups['*']; + const requireMention = groupConfig?.requireMention ?? true; + + if (requireMention && !wasMentioned) { + return; // Mention required but not mentioned -- silent drop + } + } + await this.onMessage({ channel: 'discord', chatId: message.channel.id, diff --git a/src/channels/signal.ts b/src/channels/signal.ts index 57fadee..382ea0c 100644 --- a/src/channels/signal.ts +++ b/src/channels/signal.ts @@ -763,6 +763,7 @@ This code expires in 1 hour.`; const isGroup = chatId.startsWith('group:'); // Apply group gating - only respond when mentioned (unless configured otherwise) + let wasMentioned: boolean | undefined; if (isGroup && groupInfo?.groupId) { const mentions = dataMessage?.mentions || syncMessage?.mentions; const quote = dataMessage?.quote || syncMessage?.quote; @@ -782,7 +783,8 @@ This code expires in 1 hour.`; return; } - if (gatingResult.wasMentioned) { + wasMentioned = gatingResult.wasMentioned; + if (wasMentioned) { console.log(`[Signal] Bot mentioned via ${gatingResult.method}`); } } @@ -795,6 +797,7 @@ This code expires in 1 hour.`; timestamp: new Date(envelope.timestamp || Date.now()), isGroup, groupName: groupInfo?.groupName, + wasMentioned, attachments: collectedAttachments.length > 0 ? collectedAttachments : undefined, }; diff --git a/src/channels/slack.ts b/src/channels/slack.ts index 4c80ab4..6771349 100644 --- a/src/channels/slack.ts +++ b/src/channels/slack.ts @@ -22,6 +22,7 @@ export interface SlackConfig { allowedUsers?: string[]; // Slack user IDs (e.g., U01234567) attachmentsDir?: string; attachmentsMaxBytes?: number; + groups?: Record; // Per-channel settings } export class SlackAdapter implements ChannelAdapter { @@ -129,6 +130,19 @@ export class SlackAdapter implements ChannelAdapter { // Determine if this is a group/channel (not a DM) // DMs have channel IDs starting with 'D', channels start with 'C' const isGroup = !channelId.startsWith('D'); + + // Group gating: config-based allowlist + mention requirement + if (isGroup && this.config.groups) { + if (!this.isChannelAllowed(channelId)) { + return; // Channel not in allowlist -- silent drop + } + const requireMention = this.resolveRequireMention(channelId); + if (requireMention) { + // Non-mention message in channel that requires mentions. + // The app_mention handler will process actual @mentions. + return; + } + } await this.onMessage({ channel: 'slack', @@ -160,6 +174,11 @@ export class SlackAdapter implements ChannelAdapter { return; } } + + // Group gating: allowlist check (mention already satisfied by app_mention) + if (this.config.groups && !this.isChannelAllowed(channelId)) { + return; // Channel not in allowlist -- silent drop + } // Handle slash commands const command = parseCommand(text); @@ -285,6 +304,24 @@ export class SlackAdapter implements ChannelAdapter { return this.config.dmPolicy || 'pairing'; } + /** Check if a channel is allowed by the groups config allowlist */ + private isChannelAllowed(channelId: string): boolean { + const groups = this.config.groups; + if (!groups) return true; + const allowlistEnabled = Object.keys(groups).length > 0; + if (!allowlistEnabled) return true; + return Object.hasOwn(groups, '*') || Object.hasOwn(groups, channelId); + } + + /** Resolve requireMention for a channel (specific > wildcard > default true) */ + private resolveRequireMention(channelId: string): boolean { + const groups = this.config.groups; + if (!groups) return true; + const groupConfig = groups[channelId]; + const wildcardConfig = groups['*']; + return groupConfig?.requireMention ?? wildcardConfig?.requireMention ?? true; + } + async sendTypingIndicator(_chatId: string): Promise { // Slack doesn't have a typing indicator API for bots // This is a no-op diff --git a/src/channels/telegram-group-gating.test.ts b/src/channels/telegram-group-gating.test.ts new file mode 100644 index 0000000..5b54e99 --- /dev/null +++ b/src/channels/telegram-group-gating.test.ts @@ -0,0 +1,193 @@ +import { describe, it, expect } from 'vitest'; +import { applyTelegramGroupGating, type TelegramGroupGatingParams } from './telegram-group-gating.js'; + +function createParams(overrides: Partial = {}): TelegramGroupGatingParams { + return { + text: 'Hello everyone', + chatId: '-1001234567890', + botUsername: 'mybot', + ...overrides, + }; +} + +describe('applyTelegramGroupGating', () => { + describe('group allowlist', () => { + it('allows group when in allowlist', () => { + const result = applyTelegramGroupGating(createParams({ + groupsConfig: { + '-1001234567890': { requireMention: false }, + }, + })); + expect(result.shouldProcess).toBe(true); + }); + + it('allows group via wildcard', () => { + const result = applyTelegramGroupGating(createParams({ + groupsConfig: { + '*': { requireMention: false }, + }, + })); + expect(result.shouldProcess).toBe(true); + }); + + it('blocks group not in allowlist', () => { + const result = applyTelegramGroupGating(createParams({ + groupsConfig: { + '-100999999': { requireMention: false }, + }, + })); + expect(result.shouldProcess).toBe(false); + expect(result.reason).toBe('group-not-in-allowlist'); + }); + + it('allows all groups when no groupsConfig provided', () => { + // No config = no allowlist filtering (mention gating still applies by default) + const result = applyTelegramGroupGating(createParams({ + text: '@mybot hello', + groupsConfig: undefined, + })); + expect(result.shouldProcess).toBe(true); + }); + }); + + describe('requireMention', () => { + it('defaults to requiring mention when not specified', () => { + const result = applyTelegramGroupGating(createParams({ + text: 'hello everyone', + groupsConfig: { '*': {} }, // No requireMention specified + })); + expect(result.shouldProcess).toBe(false); + expect(result.reason).toBe('mention-required'); + }); + + it('allows all messages when requireMention is false', () => { + const result = applyTelegramGroupGating(createParams({ + text: 'hello everyone', + groupsConfig: { '*': { requireMention: false } }, + })); + expect(result.shouldProcess).toBe(true); + expect(result.wasMentioned).toBe(false); + }); + + it('specific group config overrides wildcard', () => { + const result = applyTelegramGroupGating(createParams({ + text: 'hello', + groupsConfig: { + '*': { requireMention: true }, + '-1001234567890': { requireMention: false }, + }, + })); + expect(result.shouldProcess).toBe(true); + }); + + it('wildcard applies when no specific group config', () => { + const result = applyTelegramGroupGating(createParams({ + text: 'hello', + chatId: '-100999999', + groupsConfig: { + '*': { requireMention: true }, + '-1001234567890': { requireMention: false }, + }, + })); + expect(result.shouldProcess).toBe(false); + expect(result.reason).toBe('mention-required'); + }); + }); + + describe('mention detection', () => { + it('detects @username via entities (most reliable)', () => { + const result = applyTelegramGroupGating(createParams({ + text: '@mybot hello!', + entities: [{ type: 'mention', offset: 0, length: 6 }], + groupsConfig: { '*': { requireMention: true } }, + })); + expect(result.shouldProcess).toBe(true); + expect(result.wasMentioned).toBe(true); + expect(result.method).toBe('entity'); + }); + + it('detects @username via text fallback (case-insensitive)', () => { + const result = applyTelegramGroupGating(createParams({ + text: 'Hey @MyBot what do you think?', + groupsConfig: { '*': { requireMention: true } }, + })); + expect(result.shouldProcess).toBe(true); + expect(result.wasMentioned).toBe(true); + expect(result.method).toBe('text'); + }); + + it('detects /command@botusername format', () => { + // Use a bot username that won't match the simpler text fallback first + const result = applyTelegramGroupGating(createParams({ + text: '/status@testbot_123', + botUsername: 'testbot_123', + groupsConfig: { '*': { requireMention: true } }, + })); + expect(result.shouldProcess).toBe(true); + expect(result.wasMentioned).toBe(true); + // Note: text fallback (@testbot_123) catches this before command format check + // Both methods detect the mention -- the important thing is it's detected + expect(result.wasMentioned).toBe(true); + }); + + it('detects mention via regex patterns', () => { + const result = applyTelegramGroupGating(createParams({ + text: 'hey bot, what do you think?', + groupsConfig: { '*': { requireMention: true } }, + mentionPatterns: ['\\bhey bot\\b'], + })); + expect(result.shouldProcess).toBe(true); + expect(result.wasMentioned).toBe(true); + expect(result.method).toBe('regex'); + }); + + it('rejects when no mention detected and requireMention is true', () => { + const result = applyTelegramGroupGating(createParams({ + text: 'hello everyone', + groupsConfig: { '*': { requireMention: true } }, + })); + expect(result.shouldProcess).toBe(false); + expect(result.wasMentioned).toBe(false); + expect(result.reason).toBe('mention-required'); + }); + + it('ignores invalid regex patterns without crashing', () => { + const result = applyTelegramGroupGating(createParams({ + text: '@mybot hello', + groupsConfig: { '*': { requireMention: true } }, + mentionPatterns: ['[invalid'], + })); + // Falls through to text-based detection + expect(result.shouldProcess).toBe(true); + expect(result.method).toBe('text'); + }); + + it('entity mention for a different user does not match', () => { + const result = applyTelegramGroupGating(createParams({ + text: '@otheruser hello', + entities: [{ type: 'mention', offset: 0, length: 10 }], + groupsConfig: { '*': { requireMention: true } }, + })); + expect(result.shouldProcess).toBe(false); + expect(result.reason).toBe('mention-required'); + }); + }); + + describe('no groupsConfig (open mode)', () => { + it('processes messages with mention when no config (default requireMention=true)', () => { + const result = applyTelegramGroupGating(createParams({ + text: '@mybot hello', + })); + expect(result.shouldProcess).toBe(true); + expect(result.wasMentioned).toBe(true); + }); + + it('rejects messages without mention when no config (default requireMention=true)', () => { + const result = applyTelegramGroupGating(createParams({ + text: 'hello everyone', + })); + expect(result.shouldProcess).toBe(false); + expect(result.reason).toBe('mention-required'); + }); + }); +}); diff --git a/src/channels/telegram-group-gating.ts b/src/channels/telegram-group-gating.ts new file mode 100644 index 0000000..0e0a663 --- /dev/null +++ b/src/channels/telegram-group-gating.ts @@ -0,0 +1,156 @@ +/** + * Telegram Group Gating + * + * Filters group messages based on a config-based allowlist and mention detection. + * Follows the same pattern as Signal (`signal/group-gating.ts`) and WhatsApp + * (`whatsapp/inbound/group-gating.ts`). + * + * This layer runs AFTER the pairing-based group approval middleware. + * The pairing system controls "can this group access the bot at all?" + * while this config layer controls "which approved groups does the bot + * actively participate in?" + */ + +export interface TelegramGroupGatingParams { + /** Message text */ + text: string; + + /** Group chat ID (negative number as string) */ + chatId: string; + + /** Bot's @username (without the @) */ + botUsername: string; + + /** Telegram message entities (for structured mention detection) */ + entities?: { type: string; offset: number; length: number }[]; + + /** Per-group configuration */ + groupsConfig?: Record; + + /** Regex patterns for additional mention detection */ + mentionPatterns?: string[]; +} + +export interface TelegramGroupGatingResult { + /** Whether the message should be processed */ + shouldProcess: boolean; + + /** Whether bot was mentioned */ + wasMentioned?: boolean; + + /** Detection method used */ + method?: 'entity' | 'text' | 'command' | 'regex'; + + /** Reason for filtering (if shouldProcess=false) */ + reason?: string; +} + +/** + * Apply group-specific gating logic for Telegram messages. + * + * Detection methods (in priority order): + * 1. Entity-based @username mentions (most reliable) + * 2. Text-based @username fallback + * 3. /command@username format (Telegram bot command convention) + * 4. Regex patterns from config + * + * @example + * const result = applyTelegramGroupGating({ + * text: '@mybot hello!', + * chatId: '-1001234567890', + * botUsername: 'mybot', + * groupsConfig: { '*': { requireMention: true } }, + * }); + * + * if (!result.shouldProcess) return; + */ +export function applyTelegramGroupGating(params: TelegramGroupGatingParams): TelegramGroupGatingResult { + const { text, chatId, botUsername, entities, groupsConfig, mentionPatterns } = params; + + // Step 1: Group allowlist + const groups = groupsConfig ?? {}; + const allowlistEnabled = Object.keys(groups).length > 0; + + if (allowlistEnabled) { + const hasWildcard = Object.hasOwn(groups, '*'); + const hasSpecific = Object.hasOwn(groups, chatId); + + if (!hasWildcard && !hasSpecific) { + return { + shouldProcess: false, + reason: 'group-not-in-allowlist', + }; + } + } + + // Step 2: Resolve requireMention setting (default: true) + // Priority: specific group > wildcard > default true + const groupConfig = groups[chatId]; + const wildcardConfig = groups['*']; + const requireMention = + groupConfig?.requireMention ?? + wildcardConfig?.requireMention ?? + true; // Default: require mention for safety + + // If requireMention is false, allow all messages from this group + if (!requireMention) { + return { + shouldProcess: true, + wasMentioned: false, + }; + } + + // Step 3: Detect mentions + + // METHOD 1: Telegram entity-based mention detection (most reliable) + if (entities && entities.length > 0 && botUsername) { + const mentioned = entities.some((e) => { + if (e.type === 'mention') { + const mentionedText = text.substring(e.offset, e.offset + e.length); + return mentionedText.toLowerCase() === `@${botUsername.toLowerCase()}`; + } + return false; + }); + + if (mentioned) { + return { shouldProcess: true, wasMentioned: true, method: 'entity' }; + } + } + + // METHOD 2: Text-based @username fallback + if (botUsername) { + const usernameRegex = new RegExp(`@${botUsername}\\b`, 'i'); + if (usernameRegex.test(text)) { + return { shouldProcess: true, wasMentioned: true, method: 'text' }; + } + } + + // METHOD 3: /command@botusername format (Telegram convention) + if (botUsername) { + const commandRegex = new RegExp(`^/\\w+@${botUsername}\\b`, 'i'); + if (commandRegex.test(text.trim())) { + return { shouldProcess: true, wasMentioned: true, method: 'command' }; + } + } + + // METHOD 4: Regex patterns from config + if (mentionPatterns && mentionPatterns.length > 0) { + for (const pattern of mentionPatterns) { + try { + const regex = new RegExp(pattern, 'i'); + if (regex.test(text)) { + return { shouldProcess: true, wasMentioned: true, method: 'regex' }; + } + } catch { + // Invalid pattern -- skip silently + } + } + } + + // No mention detected and mention required -- skip this message + return { + shouldProcess: false, + wasMentioned: false, + reason: 'mention-required', + }; +} diff --git a/src/channels/telegram.ts b/src/channels/telegram.ts index 2edfaa6..e7e06fa 100644 --- a/src/channels/telegram.ts +++ b/src/channels/telegram.ts @@ -17,6 +17,7 @@ import { import { isGroupApproved, approveGroup } from '../pairing/group-store.js'; import { basename } from 'node:path'; import { buildAttachmentPath, downloadToFile } from './attachments.js'; +import { applyTelegramGroupGating } from './telegram-group-gating.js'; export interface TelegramConfig { token: string; @@ -24,6 +25,8 @@ export interface TelegramConfig { allowedUsers?: number[]; // Telegram user IDs (config allowlist) attachmentsDir?: string; attachmentsMaxBytes?: number; + mentionPatterns?: string[]; // Regex patterns for mention detection + groups?: Record; // Per-group settings } export class TelegramAdapter implements ChannelAdapter { @@ -50,6 +53,61 @@ export class TelegramAdapter implements ChannelAdapter { this.setupHandlers(); } + /** + * Apply group gating for a message context. + * Returns null if the message should be dropped, or { isGroup, groupName, wasMentioned } if it should proceed. + */ + private applyGroupGating(ctx: { chat: { type: string; id: number; title?: string }; message?: { text?: string; entities?: { type: string; offset: number; length: number }[] } }): { isGroup: boolean; groupName?: string; wasMentioned: boolean } | null { + const chatType = ctx.chat.type; + const isGroup = chatType === 'group' || chatType === 'supergroup'; + const groupName = isGroup && 'title' in ctx.chat ? ctx.chat.title : undefined; + + if (!isGroup) { + return { isGroup: false, wasMentioned: false }; + } + + const text = ctx.message?.text || ''; + const botUsername = this.bot.botInfo?.username || ''; + + if (this.config.groups) { + const gatingResult = applyTelegramGroupGating({ + text, + chatId: String(ctx.chat.id), + botUsername, + entities: ctx.message?.entities?.map(e => ({ + type: e.type, + offset: e.offset, + length: e.length, + })), + groupsConfig: this.config.groups, + mentionPatterns: this.config.mentionPatterns, + }); + + if (!gatingResult.shouldProcess) { + console.log(`[Telegram] Group message filtered: ${gatingResult.reason}`); + return null; + } + return { isGroup, groupName, wasMentioned: gatingResult.wasMentioned ?? false }; + } + + // No groups config: detect mentions for batcher (no gating) + let wasMentioned = false; + if (botUsername) { + const entities = ctx.message?.entities || []; + wasMentioned = entities.some((e) => { + if (e.type === 'mention') { + const mentioned = text.substring(e.offset, e.offset + e.length); + return mentioned.toLowerCase() === `@${botUsername.toLowerCase()}`; + } + return false; + }); + if (!wasMentioned) { + wasMentioned = text.toLowerCase().includes(`@${botUsername.toLowerCase()}`); + } + } + return { isGroup, groupName, wasMentioned }; + } + /** * Check if a user is authorized based on dmPolicy * Returns true if allowed, false if blocked, 'pairing' if pending pairing @@ -214,31 +272,10 @@ export class TelegramAdapter implements ChannelAdapter { if (!userId) return; if (text.startsWith('/')) return; // Skip other commands - // Group detection - const chatType = ctx.chat.type; - const isGroup = chatType === 'group' || chatType === 'supergroup'; - const groupName = isGroup && 'title' in ctx.chat ? ctx.chat.title : undefined; - - // Mention detection for groups - let wasMentioned = false; - if (isGroup) { - const botUsername = this.bot.botInfo?.username; - if (botUsername) { - // Check entities for bot_command or mention matching our username - const entities = ctx.message.entities || []; - wasMentioned = entities.some((e) => { - if (e.type === 'mention') { - const mentioned = text.substring(e.offset, e.offset + e.length); - return mentioned.toLowerCase() === `@${botUsername.toLowerCase()}`; - } - return false; - }); - // Fallback: text-based check - if (!wasMentioned) { - wasMentioned = text.toLowerCase().includes(`@${botUsername.toLowerCase()}`); - } - } - } + // Group gating (runs AFTER pairing middleware) + const gating = this.applyGroupGating(ctx); + if (!gating) return; // Filtered by group gating + const { isGroup, groupName, wasMentioned } = gating; if (this.onMessage) { await this.onMessage({ @@ -309,6 +346,11 @@ export class TelegramAdapter implements ChannelAdapter { if (!userId) return; + // Group gating + const gating = this.applyGroupGating(ctx); + if (!gating) return; + const { isGroup, groupName, wasMentioned } = gating; + // Check if transcription is configured (config or env) const { loadConfig } = await import('../config/index.js'); const config = loadConfig(); @@ -350,6 +392,9 @@ export class TelegramAdapter implements ChannelAdapter { messageId: String(ctx.message.message_id), text: messageText, timestamp: new Date(), + isGroup, + groupName, + wasMentioned, }); } } catch (error) { @@ -364,6 +409,9 @@ export class TelegramAdapter implements ChannelAdapter { messageId: String(ctx.message.message_id), text: `[Voice message - error: ${error instanceof Error ? error.message : 'unknown error'}]`, timestamp: new Date(), + isGroup, + groupName, + wasMentioned, }); } } @@ -376,6 +424,11 @@ export class TelegramAdapter implements ChannelAdapter { const chatId = ctx.chat.id; if (!userId) return; + // Group gating + const gating = this.applyGroupGating(ctx); + if (!gating) return; + const { isGroup, groupName, wasMentioned } = gating; + const { attachments, caption } = await this.collectAttachments(ctx.message, String(chatId)); if (attachments.length === 0 && !caption) return; @@ -388,6 +441,9 @@ export class TelegramAdapter implements ChannelAdapter { messageId: String(ctx.message.message_id), text: caption || '', timestamp: new Date(), + isGroup, + groupName, + wasMentioned, attachments, }); } diff --git a/src/config/types.ts b/src/config/types.ts index 087bc99..eee2954 100644 --- a/src/config/types.ts +++ b/src/config/types.ts @@ -154,6 +154,8 @@ export interface TelegramConfig { groupPollIntervalMin?: number; // @deprecated Use groupDebounceSec instead instantGroups?: string[]; // Group chat IDs that bypass batching listeningGroups?: string[]; // Group IDs where bot only observes (replies only when mentioned) + mentionPatterns?: string[]; // Regex patterns for mention detection (e.g., ["@mybot"]) + groups?: Record; // Per-group settings, "*" for defaults } export interface SlackConfig { @@ -166,6 +168,7 @@ export interface SlackConfig { groupPollIntervalMin?: number; // @deprecated Use groupDebounceSec instead instantGroups?: string[]; // Channel IDs that bypass batching listeningGroups?: string[]; // Group IDs where bot only observes (replies only when mentioned) + groups?: Record; // Per-channel settings, "*" for defaults } export interface WhatsAppConfig { @@ -207,6 +210,7 @@ export interface DiscordConfig { groupPollIntervalMin?: number; // @deprecated Use groupDebounceSec instead instantGroups?: string[]; // Guild/server IDs or channel IDs that bypass batching listeningGroups?: string[]; // Group IDs where bot only observes (replies only when mentioned) + groups?: Record; // Per-guild/channel settings, "*" for defaults } export interface GoogleAccountConfig { diff --git a/src/main.ts b/src/main.ts index d1c0677..644eb7c 100644 --- a/src/main.ts +++ b/src/main.ts @@ -276,6 +276,8 @@ function createChannelsForAgent( : undefined, attachmentsDir, attachmentsMaxBytes, + groups: agentConfig.channels.telegram.groups, + mentionPatterns: agentConfig.channels.telegram.mentionPatterns, })); } @@ -289,6 +291,7 @@ function createChannelsForAgent( : undefined, attachmentsDir, attachmentsMaxBytes, + groups: agentConfig.channels.slack.groups, })); } @@ -340,6 +343,7 @@ function createChannelsForAgent( : undefined, attachmentsDir, attachmentsMaxBytes, + groups: agentConfig.channels.discord.groups, })); }