|
| 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