Skip to content

Commit dc493bc

Browse files
fix(slack): emit message_sent on outbound replies (#89943)
Emit terminal Slack message_sent and message:sent hooks across normal, streaming, preview, fallback, slash, failure, and TTS reply paths with canonical session/target correlation and one outcome per logical payload. Fixes #89942 Co-authored-by: Rishi Tamrakar <[email protected]>
1 parent 78c6674 commit dc493bc

11 files changed

Lines changed: 1728 additions & 108 deletions

File tree

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
/**
2+
* Slack-side emission of the `message_sent` plugin hook.
3+
*
4+
* Mirrors the Telegram pattern in `extensions/telegram/src/bot/delivery.replies.ts`
5+
* (`buildTelegramSentHookContext`, `emitMessageSentHooks`, `emitTelegramMessageSentHooks`).
6+
*
7+
* Without this, plugins observing `message_sent` see Telegram outbound but not
8+
* Slack outbound — even though `docs/plugins/hooks.md` documents the hook as
9+
* firing for all successful outbound deliveries.
10+
*/
11+
import {
12+
buildCanonicalSentMessageHookContext,
13+
createInternalHookEvent,
14+
fireAndForgetHook,
15+
toInternalMessageSentContext,
16+
toPluginMessageContext,
17+
toPluginMessageSentEvent,
18+
triggerInternalHook,
19+
} from "openclaw/plugin-sdk/hook-runtime";
20+
import { getGlobalHookRunner } from "openclaw/plugin-sdk/plugin-runtime";
21+
22+
export type EmitSlackMessageSentHookParams = {
23+
/** Optional canonical session key. When set, the internal `message:sent` hook fires too. */
24+
sessionKeyForInternalHooks?: string;
25+
/** Slack target (channel ID `C…`, DM channel ID `D…`, group `G…`, or user ID `U…`). */
26+
to: string;
27+
accountId?: string | null;
28+
/** The outbound content that was sent. Mirrors `MessageSentEvent.content`. */
29+
content: string;
30+
success: boolean;
31+
error?: string;
32+
/** Slack message `ts` returned by `chat.postMessage` on success. */
33+
messageId?: string;
34+
isGroup?: boolean;
35+
groupId?: string;
36+
};
37+
38+
function buildSlackSentHookContext(params: EmitSlackMessageSentHookParams) {
39+
return buildCanonicalSentMessageHookContext({
40+
to: params.to,
41+
content: params.content,
42+
success: params.success,
43+
error: params.error,
44+
channelId: "slack",
45+
accountId: params.accountId ?? undefined,
46+
conversationId: params.to,
47+
// Mirror the canonical session key into the `message_sent` hook context so
48+
// plugins observing both `message_sending` and `message_sent` see the same
49+
// `sessionKey` (and it matches the value the internal `message:sent` hook
50+
// fires with). This matches the shared outbound emitter in
51+
// `src/infra/outbound/deliver.ts`.
52+
sessionKey: params.sessionKeyForInternalHooks,
53+
messageId: params.messageId,
54+
isGroup: params.isGroup,
55+
groupId: params.groupId,
56+
});
57+
}
58+
59+
function emitInternalSlackMessageSentHook(params: EmitSlackMessageSentHookParams): void {
60+
if (!params.sessionKeyForInternalHooks) {
61+
return;
62+
}
63+
const canonical = buildSlackSentHookContext(params);
64+
fireAndForgetHook(
65+
triggerInternalHook(
66+
createInternalHookEvent(
67+
"message",
68+
"sent",
69+
params.sessionKeyForInternalHooks,
70+
toInternalMessageSentContext(canonical),
71+
),
72+
),
73+
"slack: message:sent internal hook failed",
74+
);
75+
}
76+
77+
function emitMessageSentHooks(
78+
params: EmitSlackMessageSentHookParams & {
79+
hookRunner: ReturnType<typeof getGlobalHookRunner>;
80+
enabled: boolean;
81+
},
82+
): void {
83+
if (!params.enabled && !params.sessionKeyForInternalHooks) {
84+
return;
85+
}
86+
const canonical = buildSlackSentHookContext(params);
87+
if (params.enabled) {
88+
fireAndForgetHook(
89+
Promise.resolve(
90+
params.hookRunner!.runMessageSent(
91+
toPluginMessageSentEvent(canonical),
92+
toPluginMessageContext(canonical),
93+
),
94+
),
95+
"slack: message_sent plugin hook failed",
96+
);
97+
}
98+
emitInternalSlackMessageSentHook(params);
99+
}
100+
101+
/**
102+
* Fire both the plugin `message_sent` hook and (if a session key is supplied)
103+
* the internal `message:sent` hook for a successful or failed Slack outbound
104+
* delivery.
105+
*
106+
* Safe to call after every `chat.postMessage` — the function self-gates on
107+
* `hookRunner.hasHooks("message_sent")` so plugins not observing the hook
108+
* incur no cost.
109+
*/
110+
export function emitSlackMessageSentHooks(params: EmitSlackMessageSentHookParams): void {
111+
const hookRunner = getGlobalHookRunner();
112+
emitMessageSentHooks({
113+
...params,
114+
hookRunner,
115+
enabled: hookRunner?.hasHooks("message_sent") ?? false,
116+
});
117+
}

0 commit comments

Comments
 (0)