feat(telegram): wire replyToMode config, add forum topic support, fix messaging tool duplicates
Changes: - Default replyToMode from "off" to "first" for better threading UX - Add messageThreadId and replyToMessageId params for forum topic support - Add messaging tool duplicate detection to suppress redundant block replies - Add sendMessage action to telegram tool schema - Add @grammyjs/types devDependency for proper TypeScript typing - Remove @ts-nocheck and fix all type errors in send.ts - Add comprehensive docs/telegram.md documentation - Add PR-326-REVIEW.md with John Carmack-level code review Test coverage: - normalizeTextForComparison: 5 cases - isMessagingToolDuplicate: 7 cases - sendMessageTelegram thread params: 5 cases - handleTelegramAction sendMessage: 4 cases - Forum topic isolation: 4 cases 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
committed by
Peter Steinberger
parent
6cd32ec7f6
commit
33e2d53be3
@@ -4,15 +4,21 @@ import type { ClawdbotConfig } from "../../config/config.js";
|
||||
import { handleTelegramAction } from "./telegram-actions.js";
|
||||
|
||||
const reactMessageTelegram = vi.fn(async () => ({ ok: true }));
|
||||
const sendMessageTelegram = vi.fn(async () => ({
|
||||
messageId: "789",
|
||||
chatId: "123",
|
||||
}));
|
||||
const originalToken = process.env.TELEGRAM_BOT_TOKEN;
|
||||
|
||||
vi.mock("../../telegram/send.js", () => ({
|
||||
reactMessageTelegram: (...args: unknown[]) => reactMessageTelegram(...args),
|
||||
sendMessageTelegram: (...args: unknown[]) => sendMessageTelegram(...args),
|
||||
}));
|
||||
|
||||
describe("handleTelegramAction", () => {
|
||||
beforeEach(() => {
|
||||
reactMessageTelegram.mockClear();
|
||||
sendMessageTelegram.mockClear();
|
||||
process.env.TELEGRAM_BOT_TOKEN = "tok";
|
||||
});
|
||||
|
||||
@@ -92,4 +98,74 @@ describe("handleTelegramAction", () => {
|
||||
),
|
||||
).rejects.toThrow(/Telegram reactions are disabled/);
|
||||
});
|
||||
|
||||
it("sends a text message", async () => {
|
||||
const cfg = { telegram: { botToken: "tok" } } as ClawdbotConfig;
|
||||
const result = await handleTelegramAction(
|
||||
{
|
||||
action: "sendMessage",
|
||||
to: "@testchannel",
|
||||
content: "Hello, Telegram!",
|
||||
},
|
||||
cfg,
|
||||
);
|
||||
expect(sendMessageTelegram).toHaveBeenCalledWith(
|
||||
"@testchannel",
|
||||
"Hello, Telegram!",
|
||||
{ token: "tok", mediaUrl: undefined },
|
||||
);
|
||||
expect(result.content).toContainEqual({
|
||||
type: "text",
|
||||
text: expect.stringContaining('"ok": true'),
|
||||
});
|
||||
});
|
||||
|
||||
it("sends a message with media", async () => {
|
||||
const cfg = { telegram: { botToken: "tok" } } as ClawdbotConfig;
|
||||
await handleTelegramAction(
|
||||
{
|
||||
action: "sendMessage",
|
||||
to: "123456",
|
||||
content: "Check this image!",
|
||||
mediaUrl: "https://example.com/image.jpg",
|
||||
},
|
||||
cfg,
|
||||
);
|
||||
expect(sendMessageTelegram).toHaveBeenCalledWith(
|
||||
"123456",
|
||||
"Check this image!",
|
||||
{ token: "tok", mediaUrl: "https://example.com/image.jpg" },
|
||||
);
|
||||
});
|
||||
|
||||
it("respects sendMessage gating", async () => {
|
||||
const cfg = {
|
||||
telegram: { botToken: "tok", actions: { sendMessage: false } },
|
||||
} as ClawdbotConfig;
|
||||
await expect(
|
||||
handleTelegramAction(
|
||||
{
|
||||
action: "sendMessage",
|
||||
to: "@testchannel",
|
||||
content: "Hello!",
|
||||
},
|
||||
cfg,
|
||||
),
|
||||
).rejects.toThrow(/Telegram sendMessage is disabled/);
|
||||
});
|
||||
|
||||
it("throws on missing bot token for sendMessage", async () => {
|
||||
delete process.env.TELEGRAM_BOT_TOKEN;
|
||||
const cfg = {} as ClawdbotConfig;
|
||||
await expect(
|
||||
handleTelegramAction(
|
||||
{
|
||||
action: "sendMessage",
|
||||
to: "@testchannel",
|
||||
content: "Hello!",
|
||||
},
|
||||
cfg,
|
||||
),
|
||||
).rejects.toThrow(/Telegram bot token missing/);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
|
||||
|
||||
import type { ClawdbotConfig } from "../../config/config.js";
|
||||
import { reactMessageTelegram } from "../../telegram/send.js";
|
||||
import {
|
||||
reactMessageTelegram,
|
||||
sendMessageTelegram,
|
||||
} from "../../telegram/send.js";
|
||||
import { resolveTelegramToken } from "../../telegram/token.js";
|
||||
import {
|
||||
createActionGate,
|
||||
@@ -49,5 +52,38 @@ export async function handleTelegramAction(
|
||||
return jsonResult({ ok: true, removed: true });
|
||||
}
|
||||
|
||||
if (action === "sendMessage") {
|
||||
if (!isActionEnabled("sendMessage")) {
|
||||
throw new Error("Telegram sendMessage is disabled.");
|
||||
}
|
||||
const to = readStringParam(params, "to", { required: true });
|
||||
const content = readStringParam(params, "content", { required: true });
|
||||
const mediaUrl = readStringParam(params, "mediaUrl");
|
||||
// Optional threading parameters for forum topics and reply chains
|
||||
const replyToMessageId = readNumberParam(params, "replyToMessageId", {
|
||||
integer: true,
|
||||
});
|
||||
const messageThreadId = readNumberParam(params, "messageThreadId", {
|
||||
integer: true,
|
||||
});
|
||||
const token = resolveTelegramToken(cfg).token;
|
||||
if (!token) {
|
||||
throw new Error(
|
||||
"Telegram bot token missing. Set TELEGRAM_BOT_TOKEN or telegram.botToken.",
|
||||
);
|
||||
}
|
||||
const result = await sendMessageTelegram(to, content, {
|
||||
token,
|
||||
mediaUrl: mediaUrl || undefined,
|
||||
replyToMessageId: replyToMessageId ?? undefined,
|
||||
messageThreadId: messageThreadId ?? undefined,
|
||||
});
|
||||
return jsonResult({
|
||||
ok: true,
|
||||
messageId: result.messageId,
|
||||
chatId: result.chatId,
|
||||
});
|
||||
}
|
||||
|
||||
throw new Error(`Unsupported Telegram action: ${action}`);
|
||||
}
|
||||
|
||||
@@ -13,4 +13,22 @@ export const TelegramToolSchema = Type.Union([
|
||||
},
|
||||
includeRemove: true,
|
||||
}),
|
||||
Type.Object({
|
||||
action: Type.Literal("sendMessage"),
|
||||
to: Type.String({ description: "Chat ID, @username, or t.me/username" }),
|
||||
content: Type.String({ description: "Message text to send" }),
|
||||
mediaUrl: Type.Optional(
|
||||
Type.String({ description: "URL of image/video/audio to attach" }),
|
||||
),
|
||||
replyToMessageId: Type.Optional(
|
||||
Type.Union([Type.String(), Type.Number()], {
|
||||
description: "Message ID to reply to (for threading)",
|
||||
}),
|
||||
),
|
||||
messageThreadId: Type.Optional(
|
||||
Type.Union([Type.String(), Type.Number()], {
|
||||
description: "Forum topic thread ID (for forum supergroups)",
|
||||
}),
|
||||
),
|
||||
}),
|
||||
]);
|
||||
|
||||
@@ -7,7 +7,7 @@ export function createTelegramTool(): AnyAgentTool {
|
||||
return {
|
||||
label: "Telegram",
|
||||
name: "telegram",
|
||||
description: "Manage Telegram reactions.",
|
||||
description: "Send messages and manage reactions on Telegram.",
|
||||
parameters: TelegramToolSchema,
|
||||
execute: async (_toolCallId, args) => {
|
||||
const params = args as Record<string, unknown>;
|
||||
|
||||
Reference in New Issue
Block a user