diff --git a/src/plugins/interactive-dispatch-adapters.ts b/src/plugins/interactive-dispatch-adapters.ts new file mode 100644 index 00000000000..4050e707958 --- /dev/null +++ b/src/plugins/interactive-dispatch-adapters.ts @@ -0,0 +1,219 @@ +import { + detachPluginConversationBinding, + getCurrentPluginConversationBinding, + requestPluginConversationBinding, +} from "./conversation-binding.js"; +import type { + PluginConversationBindingRequestParams, + PluginInteractiveDiscordHandlerContext, + PluginInteractiveDiscordHandlerRegistration, + PluginInteractiveSlackHandlerContext, + PluginInteractiveSlackHandlerRegistration, + PluginInteractiveTelegramHandlerContext, + PluginInteractiveTelegramHandlerRegistration, +} from "./types.js"; + +type RegisteredInteractiveMetadata = { + pluginId: string; + pluginName?: string; + pluginRoot?: string; +}; + +type PluginBindingConversation = Parameters< + typeof requestPluginConversationBinding +>[0]["conversation"]; + +export type TelegramInteractiveDispatchContext = Omit< + PluginInteractiveTelegramHandlerContext, + | "callback" + | "respond" + | "channel" + | "requestConversationBinding" + | "detachConversationBinding" + | "getCurrentConversationBinding" +> & { + callbackMessage: { + messageId: number; + chatId: string; + messageText?: string; + }; +}; + +export type DiscordInteractiveDispatchContext = Omit< + PluginInteractiveDiscordHandlerContext, + | "interaction" + | "respond" + | "channel" + | "requestConversationBinding" + | "detachConversationBinding" + | "getCurrentConversationBinding" +> & { + interaction: Omit< + PluginInteractiveDiscordHandlerContext["interaction"], + "data" | "namespace" | "payload" + >; +}; + +export type SlackInteractiveDispatchContext = Omit< + PluginInteractiveSlackHandlerContext, + | "interaction" + | "respond" + | "channel" + | "requestConversationBinding" + | "detachConversationBinding" + | "getCurrentConversationBinding" +> & { + interaction: Omit< + PluginInteractiveSlackHandlerContext["interaction"], + "data" | "namespace" | "payload" + >; +}; + +function createConversationBindingHelpers(params: { + registration: RegisteredInteractiveMetadata; + senderId?: string; + conversation: PluginBindingConversation; +}) { + const { registration, senderId, conversation } = params; + const pluginRoot = registration.pluginRoot; + + return { + requestConversationBinding: async (binding: PluginConversationBindingRequestParams = {}) => { + if (!pluginRoot) { + return { + status: "error" as const, + message: "This interaction cannot bind the current conversation.", + }; + } + return requestPluginConversationBinding({ + pluginId: registration.pluginId, + pluginName: registration.pluginName, + pluginRoot, + requestedBySenderId: senderId, + conversation, + binding, + }); + }, + detachConversationBinding: async () => { + if (!pluginRoot) { + return { removed: false }; + } + return detachPluginConversationBinding({ + pluginRoot, + conversation, + }); + }, + getCurrentConversationBinding: async () => { + if (!pluginRoot) { + return null; + } + return getCurrentPluginConversationBinding({ + pluginRoot, + conversation, + }); + }, + }; +} + +export function dispatchTelegramInteractiveHandler(params: { + registration: PluginInteractiveTelegramHandlerRegistration & RegisteredInteractiveMetadata; + data: string; + namespace: string; + payload: string; + ctx: TelegramInteractiveDispatchContext; + respond: PluginInteractiveTelegramHandlerContext["respond"]; +}) { + const { callbackMessage, ...handlerContext } = params.ctx; + + return params.registration.handler({ + ...handlerContext, + channel: "telegram", + callback: { + data: params.data, + namespace: params.namespace, + payload: params.payload, + messageId: callbackMessage.messageId, + chatId: callbackMessage.chatId, + messageText: callbackMessage.messageText, + }, + respond: params.respond, + ...createConversationBindingHelpers({ + registration: params.registration, + senderId: handlerContext.senderId, + conversation: { + channel: "telegram", + accountId: handlerContext.accountId, + conversationId: handlerContext.conversationId, + parentConversationId: handlerContext.parentConversationId, + threadId: handlerContext.threadId, + }, + }), + }); +} + +export function dispatchDiscordInteractiveHandler(params: { + registration: PluginInteractiveDiscordHandlerRegistration & RegisteredInteractiveMetadata; + data: string; + namespace: string; + payload: string; + ctx: DiscordInteractiveDispatchContext; + respond: PluginInteractiveDiscordHandlerContext["respond"]; +}) { + const handlerContext = params.ctx; + + return params.registration.handler({ + ...handlerContext, + channel: "discord", + interaction: { + ...handlerContext.interaction, + data: params.data, + namespace: params.namespace, + payload: params.payload, + }, + respond: params.respond, + ...createConversationBindingHelpers({ + registration: params.registration, + senderId: handlerContext.senderId, + conversation: { + channel: "discord", + accountId: handlerContext.accountId, + conversationId: handlerContext.conversationId, + parentConversationId: handlerContext.parentConversationId, + }, + }), + }); +} + +export function dispatchSlackInteractiveHandler(params: { + registration: PluginInteractiveSlackHandlerRegistration & RegisteredInteractiveMetadata; + data: string; + namespace: string; + payload: string; + ctx: SlackInteractiveDispatchContext; + respond: PluginInteractiveSlackHandlerContext["respond"]; +}) { + const handlerContext = params.ctx; + + return params.registration.handler({ + ...handlerContext, + channel: "slack", + interaction: { + ...handlerContext.interaction, + data: params.data, + namespace: params.namespace, + payload: params.payload, + }, + respond: params.respond, + ...createConversationBindingHelpers({ + registration: params.registration, + senderId: handlerContext.senderId, + conversation: { + channel: "slack", + accountId: handlerContext.accountId, + conversationId: handlerContext.conversationId, + parentConversationId: handlerContext.parentConversationId, + threadId: handlerContext.threadId, + }, + }), + }); +} diff --git a/src/plugins/interactive.test.ts b/src/plugins/interactive.test.ts index 10caf6dbfa9..14980ec4545 100644 --- a/src/plugins/interactive.test.ts +++ b/src/plugins/interactive.test.ts @@ -1,13 +1,62 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi, type MockInstance } from "vitest"; +import * as conversationBinding from "./conversation-binding.js"; import { clearPluginInteractiveHandlers, dispatchPluginInteractiveHandler, registerPluginInteractiveHandler, } from "./interactive.js"; +let requestPluginConversationBindingMock: MockInstance< + typeof conversationBinding.requestPluginConversationBinding +>; +let detachPluginConversationBindingMock: MockInstance< + typeof conversationBinding.detachPluginConversationBinding +>; +let getCurrentPluginConversationBindingMock: MockInstance< + typeof conversationBinding.getCurrentPluginConversationBinding +>; + describe("plugin interactive handlers", () => { beforeEach(() => { clearPluginInteractiveHandlers(); + requestPluginConversationBindingMock = vi + .spyOn(conversationBinding, "requestPluginConversationBinding") + .mockResolvedValue({ + status: "bound", + binding: { + bindingId: "binding-1", + pluginId: "codex-plugin", + pluginName: "Codex", + pluginRoot: "/plugins/codex", + channel: "telegram", + accountId: "default", + conversationId: "-10099:topic:77", + parentConversationId: "-10099", + threadId: 77, + boundAt: 1, + }, + }); + detachPluginConversationBindingMock = vi + .spyOn(conversationBinding, "detachPluginConversationBinding") + .mockResolvedValue({ removed: true }); + getCurrentPluginConversationBindingMock = vi + .spyOn(conversationBinding, "getCurrentPluginConversationBinding") + .mockResolvedValue({ + bindingId: "binding-1", + pluginId: "codex-plugin", + pluginName: "Codex", + pluginRoot: "/plugins/codex", + channel: "telegram", + accountId: "default", + conversationId: "-10099:topic:77", + parentConversationId: "-10099", + threadId: 77, + boundAt: 1, + }); + }); + + afterEach(() => { + vi.restoreAllMocks(); }); it("routes Telegram callbacks by namespace and dedupes callback ids", async () => { @@ -213,6 +262,359 @@ describe("plugin interactive handlers", () => { ); }); + it("wires Telegram conversation binding helpers with topic context", async () => { + const requestResult = { + status: "bound" as const, + binding: { + bindingId: "binding-telegram", + pluginId: "codex-plugin", + pluginName: "Codex", + pluginRoot: "/plugins/codex", + channel: "telegram", + accountId: "default", + conversationId: "-10099:topic:77", + parentConversationId: "-10099", + threadId: 77, + boundAt: 1, + }, + }; + const currentBinding = { + ...requestResult.binding, + boundAt: 2, + }; + requestPluginConversationBindingMock.mockResolvedValueOnce(requestResult); + getCurrentPluginConversationBindingMock.mockResolvedValueOnce(currentBinding); + + const handler = vi.fn(async (ctx) => { + await expect( + ctx.requestConversationBinding({ + summary: "Bind this topic", + detachHint: "Use /new to detach", + }), + ).resolves.toEqual(requestResult); + await expect(ctx.detachConversationBinding()).resolves.toEqual({ removed: true }); + await expect(ctx.getCurrentConversationBinding()).resolves.toEqual(currentBinding); + return { handled: true }; + }); + expect( + registerPluginInteractiveHandler( + "codex-plugin", + { + channel: "telegram", + namespace: "codex", + handler, + }, + { pluginName: "Codex", pluginRoot: "/plugins/codex" }, + ), + ).toEqual({ ok: true }); + + await expect( + dispatchPluginInteractiveHandler({ + channel: "telegram", + data: "codex:bind", + callbackId: "cb-bind", + ctx: { + accountId: "default", + callbackId: "cb-bind", + conversationId: "-10099:topic:77", + parentConversationId: "-10099", + senderId: "user-1", + senderUsername: "ada", + threadId: 77, + isGroup: true, + isForum: true, + auth: { isAuthorizedSender: true }, + callbackMessage: { + messageId: 55, + chatId: "-10099", + messageText: "Pick a thread", + }, + }, + respond: { + reply: vi.fn(async () => {}), + editMessage: vi.fn(async () => {}), + editButtons: vi.fn(async () => {}), + clearButtons: vi.fn(async () => {}), + deleteMessage: vi.fn(async () => {}), + }, + }), + ).resolves.toEqual({ + matched: true, + handled: true, + duplicate: false, + }); + + expect(requestPluginConversationBindingMock).toHaveBeenCalledWith({ + pluginId: "codex-plugin", + pluginName: "Codex", + pluginRoot: "/plugins/codex", + requestedBySenderId: "user-1", + conversation: { + channel: "telegram", + accountId: "default", + conversationId: "-10099:topic:77", + parentConversationId: "-10099", + threadId: 77, + }, + binding: { + summary: "Bind this topic", + detachHint: "Use /new to detach", + }, + }); + expect(detachPluginConversationBindingMock).toHaveBeenCalledWith({ + pluginRoot: "/plugins/codex", + conversation: { + channel: "telegram", + accountId: "default", + conversationId: "-10099:topic:77", + parentConversationId: "-10099", + threadId: 77, + }, + }); + expect(getCurrentPluginConversationBindingMock).toHaveBeenCalledWith({ + pluginRoot: "/plugins/codex", + conversation: { + channel: "telegram", + accountId: "default", + conversationId: "-10099:topic:77", + parentConversationId: "-10099", + threadId: 77, + }, + }); + }); + + it("wires Discord conversation binding helpers with parent channel context", async () => { + const requestResult = { + status: "bound" as const, + binding: { + bindingId: "binding-discord", + pluginId: "codex-plugin", + pluginName: "Codex", + pluginRoot: "/plugins/codex", + channel: "discord", + accountId: "default", + conversationId: "channel-1", + parentConversationId: "parent-1", + boundAt: 1, + }, + }; + const currentBinding = { + ...requestResult.binding, + boundAt: 2, + }; + requestPluginConversationBindingMock.mockResolvedValueOnce(requestResult); + getCurrentPluginConversationBindingMock.mockResolvedValueOnce(currentBinding); + + const handler = vi.fn(async (ctx) => { + await expect(ctx.requestConversationBinding({ summary: "Bind Discord" })).resolves.toEqual( + requestResult, + ); + await expect(ctx.detachConversationBinding()).resolves.toEqual({ removed: true }); + await expect(ctx.getCurrentConversationBinding()).resolves.toEqual(currentBinding); + return { handled: true }; + }); + expect( + registerPluginInteractiveHandler( + "codex-plugin", + { + channel: "discord", + namespace: "codex", + handler, + }, + { pluginName: "Codex", pluginRoot: "/plugins/codex" }, + ), + ).toEqual({ ok: true }); + + await expect( + dispatchPluginInteractiveHandler({ + channel: "discord", + data: "codex:bind", + interactionId: "ix-bind", + ctx: { + accountId: "default", + interactionId: "ix-bind", + conversationId: "channel-1", + parentConversationId: "parent-1", + guildId: "guild-1", + senderId: "user-1", + senderUsername: "ada", + auth: { isAuthorizedSender: true }, + interaction: { + kind: "button", + messageId: "message-1", + values: ["allow"], + }, + }, + respond: { + acknowledge: vi.fn(async () => {}), + reply: vi.fn(async () => {}), + followUp: vi.fn(async () => {}), + editMessage: vi.fn(async () => {}), + clearComponents: vi.fn(async () => {}), + }, + }), + ).resolves.toEqual({ + matched: true, + handled: true, + duplicate: false, + }); + + expect(requestPluginConversationBindingMock).toHaveBeenCalledWith({ + pluginId: "codex-plugin", + pluginName: "Codex", + pluginRoot: "/plugins/codex", + requestedBySenderId: "user-1", + conversation: { + channel: "discord", + accountId: "default", + conversationId: "channel-1", + parentConversationId: "parent-1", + }, + binding: { + summary: "Bind Discord", + }, + }); + expect(detachPluginConversationBindingMock).toHaveBeenCalledWith({ + pluginRoot: "/plugins/codex", + conversation: { + channel: "discord", + accountId: "default", + conversationId: "channel-1", + parentConversationId: "parent-1", + }, + }); + expect(getCurrentPluginConversationBindingMock).toHaveBeenCalledWith({ + pluginRoot: "/plugins/codex", + conversation: { + channel: "discord", + accountId: "default", + conversationId: "channel-1", + parentConversationId: "parent-1", + }, + }); + }); + + it("wires Slack conversation binding helpers with thread context", async () => { + const requestResult = { + status: "bound" as const, + binding: { + bindingId: "binding-slack", + pluginId: "codex-plugin", + pluginName: "Codex", + pluginRoot: "/plugins/codex", + channel: "slack", + accountId: "default", + conversationId: "C123", + parentConversationId: "C123", + threadId: "1710000000.000100", + boundAt: 1, + }, + }; + const currentBinding = { + ...requestResult.binding, + boundAt: 2, + }; + requestPluginConversationBindingMock.mockResolvedValueOnce(requestResult); + getCurrentPluginConversationBindingMock.mockResolvedValueOnce(currentBinding); + + const handler = vi.fn(async (ctx) => { + await expect(ctx.requestConversationBinding({ summary: "Bind Slack" })).resolves.toEqual( + requestResult, + ); + await expect(ctx.detachConversationBinding()).resolves.toEqual({ removed: true }); + await expect(ctx.getCurrentConversationBinding()).resolves.toEqual(currentBinding); + return { handled: true }; + }); + expect( + registerPluginInteractiveHandler( + "codex-plugin", + { + channel: "slack", + namespace: "codex", + handler, + }, + { pluginName: "Codex", pluginRoot: "/plugins/codex" }, + ), + ).toEqual({ ok: true }); + + await expect( + dispatchPluginInteractiveHandler({ + channel: "slack", + data: "codex:bind", + interactionId: "slack-bind", + ctx: { + accountId: "default", + interactionId: "slack-bind", + conversationId: "C123", + parentConversationId: "C123", + threadId: "1710000000.000100", + senderId: "user-1", + senderUsername: "ada", + auth: { isAuthorizedSender: true }, + interaction: { + kind: "button", + actionId: "codex", + blockId: "codex_actions", + messageTs: "1710000000.000200", + threadTs: "1710000000.000100", + value: "bind", + selectedValues: ["bind"], + selectedLabels: ["Bind"], + triggerId: "trigger-1", + responseUrl: "https://hooks.slack.test/response", + }, + }, + respond: { + acknowledge: vi.fn(async () => {}), + reply: vi.fn(async () => {}), + followUp: vi.fn(async () => {}), + editMessage: vi.fn(async () => {}), + }, + }), + ).resolves.toEqual({ + matched: true, + handled: true, + duplicate: false, + }); + + expect(requestPluginConversationBindingMock).toHaveBeenCalledWith({ + pluginId: "codex-plugin", + pluginName: "Codex", + pluginRoot: "/plugins/codex", + requestedBySenderId: "user-1", + conversation: { + channel: "slack", + accountId: "default", + conversationId: "C123", + parentConversationId: "C123", + threadId: "1710000000.000100", + }, + binding: { + summary: "Bind Slack", + }, + }); + expect(detachPluginConversationBindingMock).toHaveBeenCalledWith({ + pluginRoot: "/plugins/codex", + conversation: { + channel: "slack", + accountId: "default", + conversationId: "C123", + parentConversationId: "C123", + threadId: "1710000000.000100", + }, + }); + expect(getCurrentPluginConversationBindingMock).toHaveBeenCalledWith({ + pluginRoot: "/plugins/codex", + conversation: { + channel: "slack", + accountId: "default", + conversationId: "C123", + parentConversationId: "C123", + threadId: "1710000000.000100", + }, + }); + }); + it("does not consume dedupe keys when a handler throws", async () => { const handler = vi .fn(async () => ({ handled: true })) diff --git a/src/plugins/interactive.ts b/src/plugins/interactive.ts index 15561a8af15..04403c80fa2 100644 --- a/src/plugins/interactive.ts +++ b/src/plugins/interactive.ts @@ -1,9 +1,12 @@ import { createDedupeCache } from "../infra/dedupe.js"; import { - detachPluginConversationBinding, - getCurrentPluginConversationBinding, - requestPluginConversationBinding, -} from "./conversation-binding.js"; + dispatchDiscordInteractiveHandler, + dispatchSlackInteractiveHandler, + dispatchTelegramInteractiveHandler, + type DiscordInteractiveDispatchContext, + type SlackInteractiveDispatchContext, + type TelegramInteractiveDispatchContext, +} from "./interactive-dispatch-adapters.js"; import type { PluginInteractiveDiscordHandlerContext, PluginInteractiveButtons, @@ -30,52 +33,6 @@ type InteractiveDispatchResult = | { matched: false; handled: false; duplicate: false } | { matched: true; handled: boolean; duplicate: boolean }; -type TelegramInteractiveDispatchContext = Omit< - PluginInteractiveTelegramHandlerContext, - | "callback" - | "respond" - | "channel" - | "requestConversationBinding" - | "detachConversationBinding" - | "getCurrentConversationBinding" -> & { - callbackMessage: { - messageId: number; - chatId: string; - messageText?: string; - }; -}; - -type DiscordInteractiveDispatchContext = Omit< - PluginInteractiveDiscordHandlerContext, - | "interaction" - | "respond" - | "channel" - | "requestConversationBinding" - | "detachConversationBinding" - | "getCurrentConversationBinding" -> & { - interaction: Omit< - PluginInteractiveDiscordHandlerContext["interaction"], - "data" | "namespace" | "payload" - >; -}; - -type SlackInteractiveDispatchContext = Omit< - PluginInteractiveSlackHandlerContext, - | "interaction" - | "respond" - | "channel" - | "requestConversationBinding" - | "detachConversationBinding" - | "getCurrentConversationBinding" -> & { - interaction: Omit< - PluginInteractiveSlackHandlerContext["interaction"], - "data" | "namespace" | "payload" - >; -}; - const interactiveHandlers = new Map(); const callbackDedupe = createDedupeCache({ ttlMs: 5 * 60_000, @@ -252,211 +209,34 @@ export async function dispatchPluginInteractiveHandler(params: { | ReturnType | ReturnType; if (params.channel === "telegram") { - const pluginRoot = match.registration.pluginRoot; - const { callbackMessage, ...handlerContext } = params.ctx as TelegramInteractiveDispatchContext; - result = ( - match.registration as RegisteredInteractiveHandler & - PluginInteractiveTelegramHandlerRegistration - ).handler({ - ...handlerContext, - channel: "telegram", - callback: { - data: params.data, - namespace: match.namespace, - payload: match.payload, - messageId: callbackMessage.messageId, - chatId: callbackMessage.chatId, - messageText: callbackMessage.messageText, - }, + result = dispatchTelegramInteractiveHandler({ + registration: match.registration as RegisteredInteractiveHandler & + PluginInteractiveTelegramHandlerRegistration, + data: params.data, + namespace: match.namespace, + payload: match.payload, + ctx: params.ctx as TelegramInteractiveDispatchContext, respond: params.respond as PluginInteractiveTelegramHandlerContext["respond"], - requestConversationBinding: async (bindingParams) => { - if (!pluginRoot) { - return { - status: "error", - message: "This interaction cannot bind the current conversation.", - }; - } - return requestPluginConversationBinding({ - pluginId: match.registration.pluginId, - pluginName: match.registration.pluginName, - pluginRoot, - requestedBySenderId: handlerContext.senderId, - conversation: { - channel: "telegram", - accountId: handlerContext.accountId, - conversationId: handlerContext.conversationId, - parentConversationId: handlerContext.parentConversationId, - threadId: handlerContext.threadId, - }, - binding: bindingParams, - }); - }, - detachConversationBinding: async () => { - if (!pluginRoot) { - return { removed: false }; - } - return detachPluginConversationBinding({ - pluginRoot, - conversation: { - channel: "telegram", - accountId: handlerContext.accountId, - conversationId: handlerContext.conversationId, - parentConversationId: handlerContext.parentConversationId, - threadId: handlerContext.threadId, - }, - }); - }, - getCurrentConversationBinding: async () => { - if (!pluginRoot) { - return null; - } - return getCurrentPluginConversationBinding({ - pluginRoot, - conversation: { - channel: "telegram", - accountId: handlerContext.accountId, - conversationId: handlerContext.conversationId, - parentConversationId: handlerContext.parentConversationId, - threadId: handlerContext.threadId, - }, - }); - }, }); } else if (params.channel === "discord") { - const pluginRoot = match.registration.pluginRoot; - result = ( - match.registration as RegisteredInteractiveHandler & - PluginInteractiveDiscordHandlerRegistration - ).handler({ - ...(params.ctx as DiscordInteractiveDispatchContext), - channel: "discord", - interaction: { - ...(params.ctx as DiscordInteractiveDispatchContext).interaction, - data: params.data, - namespace: match.namespace, - payload: match.payload, - }, + result = dispatchDiscordInteractiveHandler({ + registration: match.registration as RegisteredInteractiveHandler & + PluginInteractiveDiscordHandlerRegistration, + data: params.data, + namespace: match.namespace, + payload: match.payload, + ctx: params.ctx as DiscordInteractiveDispatchContext, respond: params.respond as PluginInteractiveDiscordHandlerContext["respond"], - requestConversationBinding: async (bindingParams) => { - if (!pluginRoot) { - return { - status: "error", - message: "This interaction cannot bind the current conversation.", - }; - } - const handlerContext = params.ctx as DiscordInteractiveDispatchContext; - return requestPluginConversationBinding({ - pluginId: match.registration.pluginId, - pluginName: match.registration.pluginName, - pluginRoot, - requestedBySenderId: handlerContext.senderId, - conversation: { - channel: "discord", - accountId: handlerContext.accountId, - conversationId: handlerContext.conversationId, - parentConversationId: handlerContext.parentConversationId, - }, - binding: bindingParams, - }); - }, - detachConversationBinding: async () => { - if (!pluginRoot) { - return { removed: false }; - } - const handlerContext = params.ctx as DiscordInteractiveDispatchContext; - return detachPluginConversationBinding({ - pluginRoot, - conversation: { - channel: "discord", - accountId: handlerContext.accountId, - conversationId: handlerContext.conversationId, - parentConversationId: handlerContext.parentConversationId, - }, - }); - }, - getCurrentConversationBinding: async () => { - if (!pluginRoot) { - return null; - } - const handlerContext = params.ctx as DiscordInteractiveDispatchContext; - return getCurrentPluginConversationBinding({ - pluginRoot, - conversation: { - channel: "discord", - accountId: handlerContext.accountId, - conversationId: handlerContext.conversationId, - parentConversationId: handlerContext.parentConversationId, - }, - }); - }, }); } else { - const pluginRoot = match.registration.pluginRoot; - const handlerContext = params.ctx as SlackInteractiveDispatchContext; - result = ( - match.registration as RegisteredInteractiveHandler & PluginInteractiveSlackHandlerRegistration - ).handler({ - ...handlerContext, - channel: "slack", - interaction: { - ...handlerContext.interaction, - data: params.data, - namespace: match.namespace, - payload: match.payload, - }, + result = dispatchSlackInteractiveHandler({ + registration: match.registration as RegisteredInteractiveHandler & + PluginInteractiveSlackHandlerRegistration, + data: params.data, + namespace: match.namespace, + payload: match.payload, + ctx: params.ctx as SlackInteractiveDispatchContext, respond: params.respond as PluginInteractiveSlackHandlerContext["respond"], - requestConversationBinding: async (bindingParams) => { - if (!pluginRoot) { - return { - status: "error", - message: "This interaction cannot bind the current conversation.", - }; - } - return requestPluginConversationBinding({ - pluginId: match.registration.pluginId, - pluginName: match.registration.pluginName, - pluginRoot, - requestedBySenderId: handlerContext.senderId, - conversation: { - channel: "slack", - accountId: handlerContext.accountId, - conversationId: handlerContext.conversationId, - parentConversationId: handlerContext.parentConversationId, - threadId: handlerContext.threadId, - }, - binding: bindingParams, - }); - }, - detachConversationBinding: async () => { - if (!pluginRoot) { - return { removed: false }; - } - return detachPluginConversationBinding({ - pluginRoot, - conversation: { - channel: "slack", - accountId: handlerContext.accountId, - conversationId: handlerContext.conversationId, - parentConversationId: handlerContext.parentConversationId, - threadId: handlerContext.threadId, - }, - }); - }, - getCurrentConversationBinding: async () => { - if (!pluginRoot) { - return null; - } - return getCurrentPluginConversationBinding({ - pluginRoot, - conversation: { - channel: "slack", - accountId: handlerContext.accountId, - conversationId: handlerContext.conversationId, - parentConversationId: handlerContext.parentConversationId, - threadId: handlerContext.threadId, - }, - }); - }, }); } const resolved = await result;