feat: add group gating to Telegram, Discord, and Slack (#258) (#265)

WhatsApp and Signal already had groups config with requireMention and
group allowlists. This brings the same pattern to the remaining three
channels, giving operators consistent control over which groups the bot
participates in and whether mentions are required.

- New `groups` config for Telegram, Discord, Slack (per-group allowlist
  with requireMention, wildcard support)
- Telegram: standalone gating module with entity, text, command, and
  regex mention detection; applied to text, voice, and attachment handlers
- Discord: inline gating using native message.mentions API
- Slack: gating in message handler (drops non-mentions) and app_mention
  handler (allowlist check); helper methods on adapter
- Signal: pass wasMentioned through to InboundMessage (was detected but
  never forwarded)
- Config wiring: groups/mentionPatterns forwarded from main.ts to all
  adapter constructors
- 17 new tests for Telegram gating

Written by Cameron ◯ Letta Code

"Even in the group chat of life, sometimes you gotta be @mentioned
to know the universe is talking to you." — Ancient Internet Proverb
This commit is contained in:
Cameron
2026-02-10 14:47:19 -08:00
committed by GitHub
parent df43091d21
commit f5371a9ba7
9 changed files with 518 additions and 26 deletions

View File

@@ -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

View File

@@ -23,6 +23,7 @@ export interface DiscordConfig {
allowedUsers?: string[]; // Discord user IDs
attachmentsDir?: string;
attachmentsMaxBytes?: number;
groups?: Record<string, { requireMention?: boolean }>; // 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,

View File

@@ -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,
};

View File

@@ -22,6 +22,7 @@ export interface SlackConfig {
allowedUsers?: string[]; // Slack user IDs (e.g., U01234567)
attachmentsDir?: string;
attachmentsMaxBytes?: number;
groups?: Record<string, { requireMention?: boolean }>; // Per-channel settings
}
export class SlackAdapter implements ChannelAdapter {
@@ -130,6 +131,19 @@ export class SlackAdapter implements ChannelAdapter {
// 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',
chatId: channelId,
@@ -161,6 +175,11 @@ export class SlackAdapter implements ChannelAdapter {
}
}
// 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);
if (command) {
@@ -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<void> {
// Slack doesn't have a typing indicator API for bots
// This is a no-op

View File

@@ -0,0 +1,193 @@
import { describe, it, expect } from 'vitest';
import { applyTelegramGroupGating, type TelegramGroupGatingParams } from './telegram-group-gating.js';
function createParams(overrides: Partial<TelegramGroupGatingParams> = {}): 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');
});
});
});

View File

@@ -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<string, { requireMention?: boolean }>;
/** 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',
};
}

View File

@@ -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<string, { requireMention?: boolean }>; // 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,
});
}

View File

@@ -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<string, { requireMention?: boolean }>; // 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<string, { requireMention?: boolean }>; // 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<string, { requireMention?: boolean }>; // Per-guild/channel settings, "*" for defaults
}
export interface GoogleAccountConfig {

View File

@@ -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,
}));
}