Skip to content

Commit 9bae3c8

Browse files
authored
Merge pull request #105 from odefun/feat/slack-router-refactor-366959
refactor: harden Slack message router hot path
2 parents b56949d + c196cdb commit 9bae3c8

File tree

3 files changed

+373
-106
lines changed

3 files changed

+373
-106
lines changed

packages/ims/slack/commands.ts

Lines changed: 115 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,35 @@ type AgentProvider = "opencode" | "claudecode" | "codex" | "kimi" | "kiro" | "ki
5858
type StatusMessageFormat = "aggressive" | "medium" | "minimum";
5959
type GitStrategy = "default" | "worktree";
6060

61+
type SlackActionBody = {
62+
actions?: Array<{
63+
value?: string;
64+
selected_option?: {
65+
value?: string;
66+
};
67+
}>;
68+
channel?: {
69+
id?: string;
70+
};
71+
user?: {
72+
id?: string;
73+
};
74+
trigger_id?: string;
75+
view?: {
76+
private_metadata?: string;
77+
id?: string;
78+
hash?: string;
79+
state?: {
80+
values?: Record<string, Record<string, { value?: string; selected_option?: { value?: string } }>>;
81+
};
82+
};
83+
message?: {
84+
thread_ts?: string;
85+
ts?: string;
86+
text?: string;
87+
};
88+
};
89+
6190
const AGENT_PROVIDERS: AgentProvider[] = ["opencode", "claudecode", "codex", "kimi", "kiro", "kilo", "qwen", "goose"];
6291

6392
const AGENT_PROVIDER_LABELS: Record<AgentProvider, string> = {
@@ -109,6 +138,38 @@ function toSelectableProvider(provider: "opencode" | "claudecode" | "codex" | "k
109138
return parseAgentProvider(provider);
110139
}
111140

141+
function getActionChannelId(body: SlackActionBody): string | undefined {
142+
return body.actions?.[0]?.value ?? body.channel?.id;
143+
}
144+
145+
function getActionUserId(body: SlackActionBody): string | undefined {
146+
return body.user?.id;
147+
}
148+
149+
function getActionTriggerId(body: SlackActionBody): string | undefined {
150+
return body.trigger_id;
151+
}
152+
153+
function getActionSelectedOptionValue(body: SlackActionBody): string | undefined {
154+
return body.actions?.[0]?.selected_option?.value;
155+
}
156+
157+
function getActionViewMetadata(body: SlackActionBody): string | undefined {
158+
return body.view?.private_metadata;
159+
}
160+
161+
function getActionThreadTs(body: SlackActionBody): string | undefined {
162+
return body.message?.thread_ts || body.message?.ts;
163+
}
164+
165+
function getActionMessageTs(body: SlackActionBody): string | undefined {
166+
return body.message?.ts;
167+
}
168+
169+
function getActionMessageText(body: SlackActionBody): string {
170+
return body.message?.text || "Question";
171+
}
172+
112173
function buildSettingsModal(params: {
113174
channelId: string;
114175
enabledProviders: AgentProvider[];
@@ -417,9 +478,11 @@ export function setupInteractiveHandlers(): void {
417478
slackApp.action(SETTINGS_LAUNCH_ACTION, async ({ ack, body, client }) => {
418479
await ack();
419480

420-
const channelId = (body as any).actions?.[0]?.value
421-
?? (body as any).channel?.id;
422-
if (!channelId) return;
481+
const actionBody = body as SlackActionBody;
482+
483+
const channelId = getActionChannelId(actionBody);
484+
const triggerId = getActionTriggerId(actionBody);
485+
if (!channelId || !triggerId) return;
423486

424487
try {
425488
await startOpenCodeServer();
@@ -448,18 +511,20 @@ export function setupInteractiveHandlers(): void {
448511
});
449512

450513
await client.views.open({
451-
trigger_id: (body as any).trigger_id,
514+
trigger_id: triggerId,
452515
view,
453516
});
454517
});
455518

456519
slackApp.action(GITHUB_LAUNCH_ACTION, async ({ ack, body, client }) => {
457520
await ack();
458521

459-
const channelId = (body as any).actions?.[0]?.value
460-
?? (body as any).channel?.id;
461-
const userId = (body as any).user?.id;
462-
if (!channelId || !userId) return;
522+
const actionBody = body as SlackActionBody;
523+
524+
const channelId = getActionChannelId(actionBody);
525+
const userId = getActionUserId(actionBody);
526+
const triggerId = getActionTriggerId(actionBody);
527+
if (!channelId || !userId || !triggerId) return;
463528

464529
const info = getGitHubInfoForUser(userId);
465530
const view = buildGitHubTokenModal({
@@ -471,16 +536,19 @@ export function setupInteractiveHandlers(): void {
471536
});
472537

473538
await client.views.open({
474-
trigger_id: (body as any).trigger_id,
539+
trigger_id: triggerId,
475540
view,
476541
});
477542
});
478543

479544
slackApp.action(GENERAL_SETTINGS_LAUNCH_ACTION, async ({ ack, body, client }) => {
480545
await ack();
481546

482-
const channelId = (body as any).actions?.[0]?.value
483-
?? (body as any).channel?.id
547+
const actionBody = body as SlackActionBody;
548+
const triggerId = getActionTriggerId(actionBody);
549+
if (!triggerId) return;
550+
551+
const channelId = getActionChannelId(actionBody)
484552
?? "";
485553
const generalSettings = getUserGeneralSettings();
486554
const view = buildGeneralSettingsModal({
@@ -490,18 +558,20 @@ export function setupInteractiveHandlers(): void {
490558
});
491559

492560
await client.views.open({
493-
trigger_id: (body as any).trigger_id,
561+
trigger_id: triggerId,
494562
view,
495563
});
496564
});
497565

498566
slackApp.action(GENERAL_SYNC_WORKSPACE_ACTION, async ({ ack, body, client }) => {
499567
await ack();
500568

501-
const channelId = (body as any).actions?.[0]?.value
502-
?? (body as any).view?.private_metadata
503-
?? (body as any).channel?.id;
504-
const userId = (body as any).user?.id;
569+
const actionBody = body as SlackActionBody;
570+
571+
const channelId = getActionChannelId(actionBody)
572+
?? getActionViewMetadata(actionBody)
573+
?? actionBody.channel?.id;
574+
const userId = getActionUserId(actionBody);
505575
if (!channelId || !userId) return;
506576

507577
const workspaces = getWorkspaces();
@@ -532,11 +602,14 @@ export function setupInteractiveHandlers(): void {
532602
slackApp.action(PROVIDER_ACTION, async ({ ack, body, client }) => {
533603
await ack();
534604

535-
const view = (body as any).view;
536-
if (!view) return;
605+
const actionBody = body as SlackActionBody;
606+
607+
const view = actionBody.view;
608+
if (!view || !view.id || !view.hash) return;
537609

538610
const channelId = view.private_metadata;
539-
const selectedOption = (body as any).actions?.[0]?.selected_option?.value;
611+
if (!channelId) return;
612+
const selectedOption = getActionSelectedOptionValue(actionBody);
540613
const selectedProvider = parseAgentProvider(selectedOption);
541614
if (selectedProvider === "opencode") {
542615
try {
@@ -656,7 +729,8 @@ export function setupInteractiveHandlers(): void {
656729
setChannelBaseBranch(channelId, baseBranch);
657730
setChannelSystemMessage(channelId, channelSystemMessage);
658731
} catch (err) {
659-
const userId = (body as any).user?.id;
732+
const actionBody = body as SlackActionBody;
733+
const userId = getActionUserId(actionBody);
660734
if (userId) {
661735
await client.chat.postEphemeral({
662736
channel: channelId,
@@ -667,7 +741,8 @@ export function setupInteractiveHandlers(): void {
667741
return;
668742
}
669743

670-
const userId = (body as any).user?.id;
744+
const actionBody = body as SlackActionBody;
745+
const userId = getActionUserId(actionBody);
671746
if (userId) {
672747
await client.chat.postEphemeral({
673748
channel: channelId,
@@ -686,9 +761,11 @@ export function setupInteractiveHandlers(): void {
686761

687762
await ack();
688763

689-
const userId = (body as any).user?.id;
690-
const channelId = (body as any).view?.private_metadata
691-
?? (body as any).channel?.id;
764+
const actionBody = body as SlackActionBody;
765+
766+
const userId = getActionUserId(actionBody);
767+
const channelId = getActionViewMetadata(actionBody)
768+
?? actionBody.channel?.id;
692769
if (!userId || !channelId) return;
693770

694771
try {
@@ -734,8 +811,9 @@ export function setupInteractiveHandlers(): void {
734811
gitStrategy,
735812
});
736813
} catch (err) {
737-
const userId = (body as any).user?.id;
738-
const channelId = view.private_metadata || (body as any).channel?.id;
814+
const actionBody = body as SlackActionBody;
815+
const userId = getActionUserId(actionBody);
816+
const channelId = view.private_metadata || actionBody.channel?.id;
739817
if (userId && channelId) {
740818
await client.chat.postEphemeral({
741819
channel: channelId,
@@ -746,8 +824,9 @@ export function setupInteractiveHandlers(): void {
746824
return;
747825
}
748826

749-
const userId = (body as any).user?.id;
750-
const channelId = view.private_metadata || (body as any).channel?.id;
827+
const actionBody = body as SlackActionBody;
828+
const userId = getActionUserId(actionBody);
829+
const channelId = view.private_metadata || actionBody.channel?.id;
751830
if (userId && channelId) {
752831
await client.chat.postEphemeral({
753832
channel: channelId,
@@ -761,18 +840,20 @@ export function setupInteractiveHandlers(): void {
761840
slackApp.action(/^user_choice_\d+$/, async ({ ack, body, client }) => {
762841
await ack();
763842

764-
const action = (body as any).actions?.[0];
843+
const actionBody = body as SlackActionBody;
844+
845+
const action = actionBody.actions?.[0];
765846
const value = action?.value;
766-
const channel = (body as any).channel?.id;
767-
const threadTs = (body as any).message?.thread_ts || (body as any).message?.ts;
768-
const userId = (body as any).user?.id;
769-
const messageTs = (body as any).message?.ts;
847+
const channel = actionBody.channel?.id;
848+
const threadTs = getActionThreadTs(actionBody);
849+
const userId = getActionUserId(actionBody);
850+
const messageTs = getActionMessageTs(actionBody);
770851

771852
if (!value || !channel || !threadTs) return;
772853

773854
// Update the original message to remove buttons (keep question text only)
774855
if (messageTs) {
775-
const originalText = (body as any).message?.text || "Question";
856+
const originalText = getActionMessageText(actionBody);
776857
await client.chat.update({
777858
channel,
778859
ts: messageTs,
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import { describe, expect, it, mock } from "bun:test";
2+
import { registerSlackMessageRouter } from "./message-router";
3+
4+
function createDeps(overrides: Partial<Parameters<typeof registerSlackMessageRouter>[0]> = {}) {
5+
const handleIncomingMessage = mock(async () => {});
6+
7+
return {
8+
app: {
9+
message: (handler: unknown) => handler,
10+
},
11+
isAuthorizedChannel: () => true,
12+
resolveWorkspaceAuth: () => undefined,
13+
syncWorkspaceForChannel: async () => false,
14+
getChannelWorkspaceName: () => "workspace",
15+
setChannelWorkspaceName: () => {},
16+
setChannelWorkspaceAuth: () => {},
17+
isThreadActive: () => false,
18+
markThreadActive: () => {},
19+
isGeneralSettingsCommand: () => false,
20+
postGeneralSettingsLauncher: async () => {},
21+
describeSettingsIssues: () => [],
22+
getChannelAgentProvider: () => "opencode" as const,
23+
handleStopCommand: async () => false,
24+
handleIncomingMessage,
25+
...overrides,
26+
};
27+
}
28+
29+
describe("registerSlackMessageRouter", () => {
30+
it("caches bot identity and avoids auth.test on every message", async () => {
31+
let registeredHandler: ((args: any) => Promise<void>) | undefined;
32+
const authTest = mock(async () => ({ user_id: "U_BOT", team_id: "T1", enterprise_id: "E1" }));
33+
const handleIncomingMessage = mock(async () => {});
34+
const deps = createDeps({
35+
app: {
36+
message: (handler: (args: any) => Promise<void>) => {
37+
registeredHandler = handler;
38+
},
39+
},
40+
handleIncomingMessage,
41+
});
42+
43+
registerSlackMessageRouter(deps);
44+
expect(registeredHandler).toBeDefined();
45+
46+
const say = mock(async () => {});
47+
48+
await registeredHandler!({
49+
message: {
50+
channel: "C1",
51+
user: "U1",
52+
text: "<@U_BOT> hello",
53+
ts: "1710000000.000001",
54+
},
55+
client: {
56+
auth: {
57+
test: authTest,
58+
},
59+
},
60+
context: { teamId: "T1", enterpriseId: "E1" },
61+
body: { team_id: "T1", enterprise_id: "E1" },
62+
say,
63+
});
64+
65+
await registeredHandler!({
66+
message: {
67+
channel: "C1",
68+
user: "U2",
69+
text: "<@U_BOT> again",
70+
ts: "1710000000.000002",
71+
},
72+
client: {
73+
auth: {
74+
test: authTest,
75+
},
76+
},
77+
context: { teamId: "T1", enterpriseId: "E1" },
78+
body: { team_id: "T1", enterprise_id: "E1" },
79+
say,
80+
});
81+
82+
expect(authTest).toHaveBeenCalledTimes(1);
83+
expect(handleIncomingMessage).toHaveBeenCalledTimes(2);
84+
});
85+
86+
it("handles message processing errors with a thread reply", async () => {
87+
let registeredHandler: ((args: any) => Promise<void>) | undefined;
88+
const deps = createDeps({
89+
app: {
90+
message: (handler: (args: any) => Promise<void>) => {
91+
registeredHandler = handler;
92+
},
93+
},
94+
handleIncomingMessage: async () => {
95+
throw new Error("boom");
96+
},
97+
});
98+
99+
registerSlackMessageRouter(deps);
100+
expect(registeredHandler).toBeDefined();
101+
102+
const say = mock(async () => {});
103+
104+
await registeredHandler!({
105+
message: {
106+
channel: "C1",
107+
user: "U1",
108+
text: "<@U_BOT> fail",
109+
ts: "1710000000.000003",
110+
},
111+
client: {
112+
auth: {
113+
test: async () => ({ user_id: "U_BOT", team_id: "T1" }),
114+
},
115+
},
116+
context: { teamId: "T1" },
117+
body: { team_id: "T1" },
118+
say,
119+
});
120+
121+
expect(say).toHaveBeenCalledWith({
122+
text: "I hit an internal error while handling that message. Please try again.",
123+
thread_ts: "1710000000.000003",
124+
});
125+
});
126+
});

0 commit comments

Comments
 (0)