feat: add group chat and mention-gating to channel adapters
Slack: add requireMention option, resolve bot user ID on connect. Telegram: add group chat mention/reply-to-bot detection, strip @mention from message text, default requireMention=true for groups. WhatsApp: add allowedGroupIds for group chat support, mention detection via mentionedIds and body text, strip bot mention from messages.
This commit is contained in:
@@ -7,6 +7,9 @@ const mockPostMessage = vi.fn();
|
||||
const mockStart = vi.fn();
|
||||
const mockStop = vi.fn();
|
||||
|
||||
const mockAuthTest = vi.fn().mockResolvedValue({ user_id: 'UBOT123' });
|
||||
const mockUsersInfo = vi.fn().mockResolvedValue({ user: { real_name: 'Test User', name: 'testuser' } });
|
||||
|
||||
/** Create a fresh mock Bolt App instance. */
|
||||
function createMockApp() {
|
||||
capturedMessageHandler = null;
|
||||
@@ -20,6 +23,12 @@ function createMockApp() {
|
||||
chat: {
|
||||
postMessage: mockPostMessage,
|
||||
},
|
||||
auth: {
|
||||
test: mockAuthTest,
|
||||
},
|
||||
users: {
|
||||
info: mockUsersInfo,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -172,7 +181,7 @@ describe('SlackAdapter', () => {
|
||||
const msg: InboundMessage = handler.mock.calls[0][0];
|
||||
expect(msg.channel).toBe('slack');
|
||||
expect(msg.senderId).toBe('C123:1111.0000');
|
||||
expect(msg.senderName).toBe('U456');
|
||||
expect(msg.senderName).toBe('Test User');
|
||||
expect(msg.text).toBe('Hello Flynn');
|
||||
expect(msg.id).toBe('1234.5678');
|
||||
});
|
||||
@@ -374,4 +383,66 @@ describe('SlackAdapter', () => {
|
||||
const msg: InboundMessage = handler.mock.calls[0][0];
|
||||
expect(msg.text).toBe('Hello');
|
||||
});
|
||||
|
||||
// ── Mention gating ─────────────────────────────────────────────
|
||||
|
||||
it('ignores messages without mention when requireMention is true', async () => {
|
||||
const mentionAdapter = new SlackAdapter({
|
||||
...baseConfig,
|
||||
requireMention: true,
|
||||
});
|
||||
const handler = vi.fn();
|
||||
mentionAdapter.onMessage(handler);
|
||||
|
||||
await mentionAdapter.connect();
|
||||
|
||||
await simulateMessage({
|
||||
ts: '1234.5678',
|
||||
channel: 'C123',
|
||||
user: 'U456',
|
||||
text: 'Hello everyone',
|
||||
});
|
||||
|
||||
expect(handler).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('processes messages with bot mention when requireMention is true', async () => {
|
||||
const mentionAdapter = new SlackAdapter({
|
||||
...baseConfig,
|
||||
requireMention: true,
|
||||
});
|
||||
const handler = vi.fn();
|
||||
mentionAdapter.onMessage(handler);
|
||||
|
||||
await mentionAdapter.connect();
|
||||
|
||||
await simulateMessage({
|
||||
ts: '1234.5678',
|
||||
channel: 'C123',
|
||||
user: 'U456',
|
||||
text: '<@UBOT123> What is the weather?',
|
||||
});
|
||||
|
||||
expect(handler).toHaveBeenCalledTimes(1);
|
||||
const msg: InboundMessage = handler.mock.calls[0][0];
|
||||
expect(msg.text).toBe('What is the weather?');
|
||||
});
|
||||
|
||||
it('processes all messages when requireMention is false (default)', async () => {
|
||||
const handler = vi.fn();
|
||||
adapter.onMessage(handler);
|
||||
|
||||
await adapter.connect();
|
||||
|
||||
await simulateMessage({
|
||||
ts: '1234.5678',
|
||||
channel: 'C123',
|
||||
user: 'U456',
|
||||
text: 'Hello without mention',
|
||||
});
|
||||
|
||||
expect(handler).toHaveBeenCalledTimes(1);
|
||||
const msg: InboundMessage = handler.mock.calls[0][0];
|
||||
expect(msg.text).toBe('Hello without mention');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -22,6 +22,8 @@ export interface SlackAdapterConfig {
|
||||
signingSecret: string;
|
||||
/** Channel IDs to respond in. Empty = all channels. */
|
||||
allowedChannelIds?: string[];
|
||||
/** Require bot mention to respond (default: false). */
|
||||
requireMention?: boolean;
|
||||
}
|
||||
|
||||
/** Minimal shape of a Slack message event from Bolt. */
|
||||
@@ -49,6 +51,7 @@ export class SlackAdapter implements ChannelAdapter {
|
||||
private messageHandler?: (msg: InboundMessage) => void;
|
||||
private config: SlackAdapterConfig;
|
||||
private userNameCache: Map<string, string> = new Map();
|
||||
private botUserId?: string;
|
||||
|
||||
get status(): ChannelStatus {
|
||||
return this._status;
|
||||
@@ -81,6 +84,15 @@ export class SlackAdapter implements ChannelAdapter {
|
||||
});
|
||||
|
||||
await this.app.start();
|
||||
|
||||
// Resolve bot user ID for mention detection
|
||||
try {
|
||||
const authResult = await this.app.client.auth.test();
|
||||
this.botUserId = authResult.user_id as string | undefined;
|
||||
} catch {
|
||||
console.warn('Slack: could not resolve bot user ID for mention detection');
|
||||
}
|
||||
|
||||
this._status = 'connected';
|
||||
console.log('Slack bot connected via Socket Mode');
|
||||
} catch (error) {
|
||||
@@ -167,6 +179,15 @@ export class SlackAdapter implements ChannelAdapter {
|
||||
return;
|
||||
}
|
||||
|
||||
// Mention requirement
|
||||
const requireMention = this.config.requireMention ?? false;
|
||||
if (requireMention && this.botUserId) {
|
||||
const mentionPattern = `<@${this.botUserId}>`;
|
||||
if (!(message.text ?? '').includes(mentionPattern)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Build peer ID: channelId:threadTs (thread-aware)
|
||||
const threadTs = message.thread_ts ?? message.ts ?? '';
|
||||
const peerId = `${channelId}:${threadTs}`;
|
||||
|
||||
Reference in New Issue
Block a user