Telegram: exec approvals for OpenCode/Codex (#37233)

Merged via squash.

Prepared head SHA: f243379094
Co-authored-by: huntharo <5617868+huntharo@users.noreply.github.com>
Co-authored-by: huntharo <5617868+huntharo@users.noreply.github.com>
Reviewed-by: @huntharo
This commit is contained in:
Harold Hunt
2026-03-09 23:04:35 -04:00
committed by GitHub
parent 9432a8bb3f
commit de49a8b72c
78 changed files with 4058 additions and 524 deletions

View File

@@ -179,6 +179,41 @@ describe("telegramPlugin duplicate token guard", () => {
expect(result).toMatchObject({ channel: "telegram", messageId: "tg-1" });
});
it("preserves buttons for outbound text payload sends", async () => {
const sendMessageTelegram = vi.fn(async () => ({ messageId: "tg-2" }));
setTelegramRuntime({
channel: {
telegram: {
sendMessageTelegram,
},
},
} as unknown as PluginRuntime);
const result = await telegramPlugin.outbound!.sendPayload!({
cfg: createCfg(),
to: "12345",
text: "",
payload: {
text: "Approval required",
channelData: {
telegram: {
buttons: [[{ text: "Allow Once", callback_data: "/approve abc allow-once" }]],
},
},
},
accountId: "ops",
});
expect(sendMessageTelegram).toHaveBeenCalledWith(
"12345",
"Approval required",
expect.objectContaining({
buttons: [[{ text: "Allow Once", callback_data: "/approve abc allow-once" }]],
}),
);
expect(result).toMatchObject({ channel: "telegram", messageId: "tg-2" });
});
it("ignores accounts with missing tokens during duplicate-token checks", async () => {
const cfg = createCfg();
cfg.channels!.telegram!.accounts!.ops = {} as never;

View File

@@ -91,6 +91,10 @@ const telegramMessageActions: ChannelMessageActionAdapter = {
},
};
type TelegramInlineButtons = ReadonlyArray<
ReadonlyArray<{ text: string; callback_data: string; style?: "danger" | "success" | "primary" }>
>;
const telegramConfigAccessors = createScopedAccountConfigAccessors({
resolveAccount: ({ cfg, accountId }) => resolveTelegramAccount({ cfg, accountId }),
resolveAllowFrom: (account: ResolvedTelegramAccount) => account.config.allowFrom,
@@ -317,6 +321,62 @@ export const telegramPlugin: ChannelPlugin<ResolvedTelegramAccount, TelegramProb
chunkerMode: "markdown",
textChunkLimit: 4000,
pollMaxOptions: 10,
sendPayload: async ({
cfg,
to,
payload,
mediaLocalRoots,
accountId,
deps,
replyToId,
threadId,
silent,
}) => {
const send = deps?.sendTelegram ?? getTelegramRuntime().channel.telegram.sendMessageTelegram;
const replyToMessageId = parseTelegramReplyToMessageId(replyToId);
const messageThreadId = parseTelegramThreadId(threadId);
const telegramData = payload.channelData?.telegram as
| { buttons?: TelegramInlineButtons; quoteText?: string }
| undefined;
const quoteText =
typeof telegramData?.quoteText === "string" ? telegramData.quoteText : undefined;
const text = payload.text ?? "";
const mediaUrls = payload.mediaUrls?.length
? payload.mediaUrls
: payload.mediaUrl
? [payload.mediaUrl]
: [];
const baseOpts = {
verbose: false,
cfg,
mediaLocalRoots,
messageThreadId,
replyToMessageId,
quoteText,
accountId: accountId ?? undefined,
silent: silent ?? undefined,
};
if (mediaUrls.length === 0) {
const result = await send(to, text, {
...baseOpts,
buttons: telegramData?.buttons,
});
return { channel: "telegram", ...result };
}
let finalResult: Awaited<ReturnType<typeof send>> | undefined;
for (let i = 0; i < mediaUrls.length; i += 1) {
const mediaUrl = mediaUrls[i];
const isFirst = i === 0;
finalResult = await send(to, isFirst ? text : "", {
...baseOpts,
mediaUrl,
...(isFirst ? { buttons: telegramData?.buttons } : {}),
});
}
return { channel: "telegram", ...(finalResult ?? { messageId: "unknown", chatId: to }) };
},
sendText: async ({ cfg, to, text, accountId, deps, replyToId, threadId, silent }) => {
const send = deps?.sendTelegram ?? getTelegramRuntime().channel.telegram.sendMessageTelegram;
const replyToMessageId = parseTelegramReplyToMessageId(replyToId);