refactor: split plugin interactive dispatch adapters

This commit is contained in:
Peter Steinberger 2026-03-16 05:28:08 +00:00
parent 9cd9c7a488
commit 3963408871
3 changed files with 650 additions and 249 deletions

View File

@ -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,
},
}),
});
}

View File

@ -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 }))

View File

@ -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<string, RegisteredInteractiveHandler>();
const callbackDedupe = createDedupeCache({
ttlMs: 5 * 60_000,
@ -252,211 +209,34 @@ export async function dispatchPluginInteractiveHandler(params: {
| ReturnType<PluginInteractiveDiscordHandlerRegistration["handler"]>
| ReturnType<PluginInteractiveSlackHandlerRegistration["handler"]>;
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;