Skip to content

Commit 2fcd861

Browse files
authored
feat: add allowedChannelIds for Discord channel filtering (#29)
* feat: add allowedChannelIds config for Discord channel filtering - Add allowedChannelIds to DiscordImConfig (config.jsonl) - Propagate through runtime env as ALLOWED_CHANNEL_IDS - Filter messages and interactions in Discord index.ts - Thread messages check parentId against allowlist - Empty list = no filtering (backward compatible) - 5 new tests covering allow/deny/thread/empty scenarios * chore: bump patch version (1.1.1 / 1.2.1)
1 parent 55c8205 commit 2fcd861

File tree

10 files changed

+150
-7
lines changed

10 files changed

+150
-7
lines changed

apps/agent-inbox/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@doctorwu/agent-inbox",
3-
"version": "1.2.0",
3+
"version": "1.2.1",
44
"type": "module",
55
"main": "dist/index.mjs",
66
"types": "dist/index.d.mts",

apps/agent-inbox/src/config.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export type DiscordImConfig = {
1313
token?: string;
1414
clientId?: string;
1515
guildIds?: string[];
16+
allowedChannelIds?: string[];
1617
};
1718

1819
export type FeishuImConfig = {
@@ -57,7 +58,7 @@ export type AvailableIm =
5758
| {
5859
id: 'discord';
5960
note?: string;
60-
config: Required<Pick<DiscordImConfig, 'token' | 'clientId'>> & Pick<DiscordImConfig, 'guildIds'>;
61+
config: Required<Pick<DiscordImConfig, 'token' | 'clientId'>> & Pick<DiscordImConfig, 'guildIds' | 'allowedChannelIds'>;
6162
}
6263
| {
6364
id: 'feishu';
@@ -151,6 +152,7 @@ function normalizeDiscordImRecord(value: Record<string, unknown>): DiscordImReco
151152
token: asString(config.token),
152153
clientId: asString(config.clientId),
153154
guildIds: asStringList(config.guildIds),
155+
allowedChannelIds: asStringList(config.allowedChannelIds),
154156
},
155157
};
156158
}
@@ -292,6 +294,7 @@ export function resolveAvailableIms(records: AppConfigRecord[]): AvailableIm[] {
292294
token: record.config.token,
293295
clientId: record.config.clientId,
294296
guildIds: record.config.guildIds,
297+
allowedChannelIds: record.config.allowedChannelIds,
295298
},
296299
}];
297300
}

apps/agent-inbox/src/runtime.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export function applyRuntimeEnvironment(
3535
delete process.env['DISCORD_TOKEN'];
3636
delete process.env['DISCORD_CLIENT_ID'];
3737
delete process.env['GUILD_IDS'];
38+
delete process.env['ALLOWED_CHANNEL_IDS'];
3839
delete process.env['FEISHU_APP_ID'];
3940
delete process.env['FEISHU_APP_SECRET'];
4041
delete process.env['FEISHU_VERIFICATION_TOKEN'];
@@ -46,6 +47,7 @@ export function applyRuntimeEnvironment(
4647
process.env['DISCORD_TOKEN'] = selectedIm.config.token;
4748
process.env['DISCORD_CLIENT_ID'] = selectedIm.config.clientId;
4849
setOptionalEnv('GUILD_IDS', selectedIm.config.guildIds?.join(','));
50+
setOptionalEnv('ALLOWED_CHANNEL_IDS', selectedIm.config.allowedChannelIds?.join(','));
4951
return;
5052
}
5153

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "agent-im-relay-workspace",
3-
"version": "1.1.0",
3+
"version": "1.1.1",
44
"private": true,
55
"description": "Inbox-first IM launcher for local AI agents",
66
"type": "module",

packages/core/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@agent-im-relay/core",
3-
"version": "1.1.0",
3+
"version": "1.1.1",
44
"type": "module",
55
"main": "dist/index.mjs",
66
"types": "dist/index.d.mts",

packages/discord/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@agent-im-relay/discord",
3-
"version": "1.1.0",
3+
"version": "1.1.1",
44
"type": "module",
55
"main": "dist/index.mjs",
66
"scripts": {

packages/discord/src/__tests__/index.test.ts

Lines changed: 118 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { beforeEach, describe, expect, it, vi } from 'vitest';
1+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
22

33
const { clientMock, restMock } = vi.hoisted(() => ({
44
clientMock: {
@@ -127,6 +127,7 @@ vi.mock('../commands/thread-setup.js', () => ({
127127

128128
import { handleDiscordMessageCreate } from '../index.js';
129129
import { handleSkillAutocomplete } from '../commands/skill.js';
130+
import { config as discordConfig } from '../config.js';
130131
import {
131132
applyMessageControlDirectives,
132133
getAvailableBackendCapabilities,
@@ -469,3 +470,119 @@ describe('handleDiscordMessageCreate', () => {
469470
});
470471
});
471472
});
473+
474+
describe('allowedChannelIds filter', () => {
475+
afterEach(() => {
476+
discordConfig.allowedChannelIds = [];
477+
});
478+
479+
it('drops messages from channels not in the allowlist', async () => {
480+
discordConfig.allowedChannelIds = ['allowed-channel'];
481+
482+
const message = createBaseMessage();
483+
message.channel.id = 'other-channel';
484+
485+
await handleDiscordMessageCreate(message, {
486+
botUser: { id: 'relay-bot' },
487+
hasOpenStickyThreadSession: () => false,
488+
runThreadConversation: vi.fn(),
489+
ensureMentionThread: vi.fn(),
490+
promptThreadSetup: vi.fn(),
491+
applySetupResult: vi.fn(),
492+
});
493+
494+
expect(message.react).not.toHaveBeenCalled();
495+
});
496+
497+
it('accepts messages from allowed channels', async () => {
498+
discordConfig.allowedChannelIds = ['allowed-channel'];
499+
500+
const message = createBaseMessage();
501+
message.channel.id = 'allowed-channel';
502+
503+
const ensureMentionThread = vi.fn(async () => ({
504+
id: 'thread-1',
505+
send: vi.fn(async () => undefined),
506+
}));
507+
508+
await handleDiscordMessageCreate(message, {
509+
botUser: { id: 'relay-bot' },
510+
hasOpenStickyThreadSession: () => false,
511+
runThreadConversation: vi.fn(async () => true),
512+
ensureMentionThread,
513+
promptThreadSetup: vi.fn(async () => ({ kind: 'skip' })),
514+
applySetupResult: vi.fn(),
515+
});
516+
517+
expect(message.react).toHaveBeenCalled();
518+
});
519+
520+
it('accepts thread messages whose parent is in the allowlist', async () => {
521+
discordConfig.allowedChannelIds = ['allowed-channel'];
522+
523+
const message = createBaseMessage();
524+
message.channel = {
525+
id: 'thread-in-allowed',
526+
parentId: 'allowed-channel',
527+
isThread: () => true,
528+
send: vi.fn(async () => undefined),
529+
};
530+
531+
await handleDiscordMessageCreate(message, {
532+
botUser: { id: 'relay-bot' },
533+
hasOpenStickyThreadSession: () => true,
534+
runThreadConversation: vi.fn(async () => true),
535+
ensureMentionThread: vi.fn(),
536+
promptThreadSetup: vi.fn(),
537+
applySetupResult: vi.fn(),
538+
});
539+
540+
expect(message.react).toHaveBeenCalled();
541+
});
542+
543+
it('drops thread messages whose parent is not in the allowlist', async () => {
544+
discordConfig.allowedChannelIds = ['allowed-channel'];
545+
546+
const message = createBaseMessage();
547+
message.channel = {
548+
id: 'thread-in-other',
549+
parentId: 'other-channel',
550+
isThread: () => true,
551+
send: vi.fn(async () => undefined),
552+
};
553+
554+
await handleDiscordMessageCreate(message, {
555+
botUser: { id: 'relay-bot' },
556+
hasOpenStickyThreadSession: () => true,
557+
runThreadConversation: vi.fn(),
558+
ensureMentionThread: vi.fn(),
559+
promptThreadSetup: vi.fn(),
560+
applySetupResult: vi.fn(),
561+
});
562+
563+
expect(message.react).not.toHaveBeenCalled();
564+
});
565+
566+
it('allows all channels when allowedChannelIds is empty', async () => {
567+
discordConfig.allowedChannelIds = [];
568+
569+
const message = createBaseMessage();
570+
message.channel.id = 'any-channel';
571+
572+
const ensureMentionThread = vi.fn(async () => ({
573+
id: 'thread-1',
574+
send: vi.fn(async () => undefined),
575+
}));
576+
577+
await handleDiscordMessageCreate(message, {
578+
botUser: { id: 'relay-bot' },
579+
hasOpenStickyThreadSession: () => false,
580+
runThreadConversation: vi.fn(async () => true),
581+
ensureMentionThread,
582+
promptThreadSetup: vi.fn(async () => ({ kind: 'skip' })),
583+
applySetupResult: vi.fn(),
584+
});
585+
586+
expect(message.react).toHaveBeenCalled();
587+
});
588+
});

packages/discord/src/config.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ export const config = {
2929
guildIds: process.env['GUILD_IDS']
3030
? process.env['GUILD_IDS'].split(',').map((id) => id.trim()).filter(Boolean)
3131
: [],
32+
allowedChannelIds: process.env['ALLOWED_CHANNEL_IDS']
33+
? process.env['ALLOWED_CHANNEL_IDS'].split(',').map((id) => id.trim()).filter(Boolean)
34+
: [],
3235
streamUpdateIntervalMs: numberEnv('STREAM_UPDATE_INTERVAL_MS', 1000),
3336
discordMessageCharLimit: numberEnv('DISCORD_MESSAGE_CHAR_LIMIT', 1900),
3437
maxAttachmentSizeBytes: coreConfig.artifactMaxSizeBytes,

packages/discord/src/index.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,12 @@ import {
4545
} from './commands/skill.js';
4646
import { promptThreadSetup, applySetupResult } from './commands/thread-setup.js';
4747

48+
function isChannelAllowed(channelId: string, parentId: string | null): boolean {
49+
if (config.allowedChannelIds.length === 0) return true;
50+
return config.allowedChannelIds.includes(channelId)
51+
|| (parentId !== null && config.allowedChannelIds.includes(parentId));
52+
}
53+
4854
type CommandHandler = (interaction: ChatInputCommandInteraction) => Promise<void>;
4955
type AutocompleteHandler = (interaction: AutocompleteInteraction) => Promise<void>;
5056

@@ -164,6 +170,11 @@ export async function handleDiscordMessageCreate(
164170
const botUser = dependencies.botUser ?? client.user;
165171
if (!botUser) return;
166172

173+
// Channel allowlist filter
174+
const channelId = message.channel.id;
175+
const parentId = message.channel.isThread() ? message.channel.parentId : null;
176+
if (!isChannelAllowed(channelId, parentId)) return;
177+
167178
const isActiveThread = message.channel.isThread()
168179
&& (dependencies.hasOpenStickyThreadSession ?? hasOpenStickyThreadSession)(message.channel.id);
169180
const routedMessage = resolveInboundDiscordMessage({
@@ -302,6 +313,13 @@ client.on(Events.Error, (error) => {
302313

303314
client.on(Events.InteractionCreate, async (interaction) => {
304315
try {
316+
// Channel allowlist filter
317+
if (interaction.channel) {
318+
const channelId = interaction.channel.id;
319+
const parentId = interaction.channel.isThread() ? interaction.channel.parentId : null;
320+
if (!isChannelAllowed(channelId, parentId)) return;
321+
}
322+
305323
if (interaction.isChatInputCommand()) {
306324
const handler = commandHandlers.get(interaction.commandName);
307325
if (!handler) return;

packages/feishu/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@agent-im-relay/feishu",
3-
"version": "1.1.0",
3+
"version": "1.1.1",
44
"type": "module",
55
"main": "dist/index.mjs",
66
"types": "dist/index.d.mts",

0 commit comments

Comments
 (0)