diff --git a/src/handlers/__tests__/message.test.ts b/src/handlers/__tests__/message.test.ts index 56606c2..cde03c1 100644 --- a/src/handlers/__tests__/message.test.ts +++ b/src/handlers/__tests__/message.test.ts @@ -5,7 +5,6 @@ import { createMessageHandler } from '../message.ts'; describe('createMessageHandler', () => { const timesAllChannelId = 'C_TIMES_ALL'; const workspaceUrl = 'https://workspace.slack.com'; - const handler = createMessageHandler(timesAllChannelId, workspaceUrl); function makeArgs( message: Record, @@ -29,6 +28,7 @@ describe('createMessageHandler', () => { } it('timesチャンネルのメッセージをtimes-allに転送する', async () => { + const handler = createMessageHandler(timesAllChannelId, workspaceUrl); const message = { type: 'message', subtype: undefined, @@ -53,6 +53,7 @@ describe('createMessageHandler', () => { }); it('times-以外のチャンネルは転送しない', async () => { + const handler = createMessageHandler(timesAllChannelId, workspaceUrl); const message = { type: 'message', subtype: undefined, @@ -70,6 +71,7 @@ describe('createMessageHandler', () => { }); it('subtypeを持つメッセージは転送しない', async () => { + const handler = createMessageHandler(timesAllChannelId, workspaceUrl); const message = { type: 'message', subtype: 'channel_join', @@ -86,6 +88,7 @@ describe('createMessageHandler', () => { }); it('DMメッセージは転送しない', async () => { + const handler = createMessageHandler(timesAllChannelId, workspaceUrl); const message = { type: 'message', subtype: undefined, @@ -102,4 +105,56 @@ describe('createMessageHandler', () => { assert.equal(args.client.conversations.info.mock.callCount(), 0); assert.equal(args.client.chat.postMessage.mock.callCount(), 0); }); + + it('同一チャンネルの2回目以降はconversations.infoを呼ばない', async () => { + const handler = createMessageHandler(timesAllChannelId, workspaceUrl); + const message = { + type: 'message', + subtype: undefined, + channel: 'C_CACHED', + channel_type: 'channel', + text: 'first', + ts: '1234567890.000001', + user: 'U12345', + }; + // biome-ignore lint/suspicious/noExplicitAny: テスト用のモック + const args1 = makeArgs(message) as any; + await handler(args1); + + const message2 = { ...message, text: 'second', ts: '1234567890.000002' }; + // biome-ignore lint/suspicious/noExplicitAny: テスト用のモック + const args2 = makeArgs(message2) as any; + await handler(args2); + + assert.equal(args1.client.conversations.info.mock.callCount(), 1); + assert.equal(args2.client.conversations.info.mock.callCount(), 0); + }); + + it('conversations.infoが例外をスローした場合は転送しない', async () => { + const handler = createMessageHandler(timesAllChannelId, workspaceUrl); + const message = { + type: 'message', + subtype: undefined, + channel: 'C_ERROR', + channel_type: 'channel', + text: 'hello', + ts: '1234567890.123456', + user: 'U12345', + }; + const args = { + message, + client: { + conversations: { + info: mock.fn(() => Promise.reject(new Error('platform_error'))), + }, + chat: { + postMessage: mock.fn(() => Promise.resolve()), + }, + }, + }; + // biome-ignore lint/suspicious/noExplicitAny: テスト用のモック + await handler(args as any); + + assert.equal(args.client.chat.postMessage.mock.callCount(), 0); + }); }); diff --git a/src/handlers/message.ts b/src/handlers/message.ts index 7470ce7..2e456fd 100644 --- a/src/handlers/message.ts +++ b/src/handlers/message.ts @@ -4,25 +4,75 @@ import { isForwardableMessage } from '../lib/messageFilter.ts'; import { buildMessageLink } from '../lib/messageLink.ts'; import { isTimesChannel } from '../lib/timesChannel.ts'; +type SlackClient = (AllMiddlewareArgs & + SlackEventMiddlewareArgs<'message'>)['client']; + +async function getChannelName( + client: SlackClient, + channelId: string, + cache: Map, +): Promise { + const cached = cache.get(channelId); + if (cached !== undefined) { + return cached; + } + try { + const channelInfo = await client.conversations.info({ + channel: channelId, + }); + const channelName = channelInfo.channel?.name; + if (!channelName) { + console.error('channel name not found', { + channel: channelId, + ok: channelInfo.ok, + error: channelInfo.error, + }); + return undefined; + } + cache.set(channelId, channelName); + return channelName; + } catch (error) { + console.error('conversations.info failed', { + channel: channelId, + error: + error instanceof Error + ? { name: error.name, message: error.message, stack: error.stack } + : error, + }); + return undefined; + } +} + export function createMessageHandler( timesAllChannelId: string, workspaceUrl: string, ) { + const channelNameCache = new Map(); + return async ({ message, client, }: AllMiddlewareArgs & SlackEventMiddlewareArgs<'message'>) => { - console.log('received message'); + console.log('received message', { + channel: message.channel, + subtype: message.subtype, + ts: message.ts, + }); if (!isForwardableMessage(message, timesAllChannelId)) { + console.log('skipped: not forwardable', { subtype: message.subtype }); return; } - const channelInfo = await client.conversations.info({ - channel: message.channel, - }); - const channelName = channelInfo.channel?.name; - if (!channelName || !isTimesChannel(channelName)) { - console.log(channelName); + const channelName = await getChannelName( + client, + message.channel, + channelNameCache, + ); + if (!channelName) { + return; + } + if (!isTimesChannel(channelName)) { + console.log('skipped: not a times channel', { channelName }); return; }