mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-10 00:13:28 +00:00
fix(telegram): make reaction handling soft-fail and message-id resilient (#20236)
* Telegram: soft-fail reactions and fallback to inbound message id * Telegram: soft-fail missing reaction message id * Update CHANGELOG.md --------- Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
This commit is contained in:
@@ -14,7 +14,12 @@ describe("channels dock", () => {
|
||||
|
||||
const telegramContext = telegramDock?.threading?.buildToolContext?.({
|
||||
cfg: emptyConfig(),
|
||||
context: { To: " room-1 ", MessageThreadId: 42, ReplyToId: "fallback" },
|
||||
context: {
|
||||
To: " room-1 ",
|
||||
MessageThreadId: 42,
|
||||
ReplyToId: "fallback",
|
||||
CurrentMessageId: "9001",
|
||||
},
|
||||
hasRepliedRef,
|
||||
});
|
||||
const googleChatContext = googleChatDock?.threading?.buildToolContext?.({
|
||||
@@ -26,6 +31,7 @@ describe("channels dock", () => {
|
||||
expect(telegramContext).toEqual({
|
||||
currentChannelId: "room-1",
|
||||
currentThreadTs: "42",
|
||||
currentMessageId: "9001",
|
||||
hasRepliedRef,
|
||||
});
|
||||
expect(googleChatContext).toEqual({
|
||||
@@ -35,6 +41,23 @@ describe("channels dock", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("telegram threading does not treat ReplyToId as thread id in DMs", () => {
|
||||
const hasRepliedRef = { value: false };
|
||||
const telegramDock = getChannelDock("telegram");
|
||||
const context = telegramDock?.threading?.buildToolContext?.({
|
||||
cfg: emptyConfig(),
|
||||
context: { To: " dm-1 ", ReplyToId: "12345", CurrentMessageId: "12345" },
|
||||
hasRepliedRef,
|
||||
});
|
||||
|
||||
expect(context).toEqual({
|
||||
currentChannelId: "dm-1",
|
||||
currentThreadTs: undefined,
|
||||
currentMessageId: "12345",
|
||||
hasRepliedRef,
|
||||
});
|
||||
});
|
||||
|
||||
it("irc resolveDefaultTo matches account id case-insensitively", () => {
|
||||
const ircDock = getChannelDock("irc");
|
||||
const cfg = {
|
||||
|
||||
@@ -253,8 +253,22 @@ const DOCKS: Record<ChatChannelId, ChannelDock> = {
|
||||
},
|
||||
threading: {
|
||||
resolveReplyToMode: ({ cfg }) => cfg.channels?.telegram?.replyToMode ?? "off",
|
||||
buildToolContext: ({ context, hasRepliedRef }) =>
|
||||
buildThreadToolContextFromMessageThreadOrReply({ context, hasRepliedRef }),
|
||||
buildToolContext: ({ context, hasRepliedRef }) => {
|
||||
// Telegram auto-threading should only use actual thread/topic IDs.
|
||||
// ReplyToId is a message ID and causes invalid message_thread_id in DMs.
|
||||
const threadId = context.MessageThreadId;
|
||||
const rawCurrentMessageId = context.CurrentMessageId;
|
||||
const currentMessageId =
|
||||
typeof rawCurrentMessageId === "number"
|
||||
? rawCurrentMessageId
|
||||
: rawCurrentMessageId?.trim() || undefined;
|
||||
return {
|
||||
currentChannelId: context.To?.trim() || undefined,
|
||||
currentThreadTs: threadId != null ? String(threadId) : undefined,
|
||||
currentMessageId,
|
||||
hasRepliedRef,
|
||||
};
|
||||
},
|
||||
},
|
||||
},
|
||||
whatsapp: {
|
||||
|
||||
@@ -673,6 +673,83 @@ describe("telegramMessageActions", () => {
|
||||
expect(String(callPayload.messageId)).toBe("456");
|
||||
expect(callPayload.emoji).toBe("ok");
|
||||
});
|
||||
|
||||
it("accepts snake_case message_id for reactions", async () => {
|
||||
const cfg = telegramCfg();
|
||||
|
||||
await telegramMessageActions.handleAction?.({
|
||||
channel: "telegram",
|
||||
action: "react",
|
||||
params: {
|
||||
channelId: 123,
|
||||
message_id: "456",
|
||||
emoji: "ok",
|
||||
},
|
||||
cfg,
|
||||
accountId: undefined,
|
||||
});
|
||||
|
||||
expect(handleTelegramAction).toHaveBeenCalledTimes(1);
|
||||
const call = handleTelegramAction.mock.calls[0]?.[0];
|
||||
if (!call) {
|
||||
throw new Error("missing telegram action call");
|
||||
}
|
||||
const callPayload = call as Record<string, unknown>;
|
||||
expect(callPayload.action).toBe("react");
|
||||
expect(String(callPayload.chatId)).toBe("123");
|
||||
expect(String(callPayload.messageId)).toBe("456");
|
||||
});
|
||||
|
||||
it("falls back to toolContext.currentMessageId for reactions when messageId is omitted", async () => {
|
||||
const cfg = telegramCfg();
|
||||
|
||||
await telegramMessageActions.handleAction?.({
|
||||
channel: "telegram",
|
||||
action: "react",
|
||||
params: {
|
||||
chatId: "123",
|
||||
emoji: "ok",
|
||||
},
|
||||
cfg,
|
||||
accountId: undefined,
|
||||
toolContext: { currentMessageId: "9001" },
|
||||
});
|
||||
|
||||
expect(handleTelegramAction).toHaveBeenCalledTimes(1);
|
||||
const call = handleTelegramAction.mock.calls[0]?.[0];
|
||||
if (!call) {
|
||||
throw new Error("missing telegram action call");
|
||||
}
|
||||
const callPayload = call as Record<string, unknown>;
|
||||
expect(callPayload.action).toBe("react");
|
||||
expect(String(callPayload.messageId)).toBe("9001");
|
||||
});
|
||||
|
||||
it("forwards missing reaction messageId to telegram-actions for soft-fail handling", async () => {
|
||||
const cfg = telegramCfg();
|
||||
|
||||
await expect(
|
||||
telegramMessageActions.handleAction?.({
|
||||
channel: "telegram",
|
||||
action: "react",
|
||||
params: {
|
||||
chatId: "123",
|
||||
emoji: "ok",
|
||||
},
|
||||
cfg,
|
||||
accountId: undefined,
|
||||
}),
|
||||
).resolves.toBeDefined();
|
||||
|
||||
expect(handleTelegramAction).toHaveBeenCalledTimes(1);
|
||||
const call = handleTelegramAction.mock.calls[0]?.[0];
|
||||
if (!call) {
|
||||
throw new Error("missing telegram action call");
|
||||
}
|
||||
const callPayload = call as Record<string, unknown>;
|
||||
expect(callPayload.action).toBe("react");
|
||||
expect(callPayload.messageId).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("signalMessageActions", () => {
|
||||
|
||||
@@ -107,7 +107,7 @@ export const telegramMessageActions: ChannelMessageActionAdapter = {
|
||||
extractToolSend: ({ args }) => {
|
||||
return extractToolSend(args, "sendMessage");
|
||||
},
|
||||
handleAction: async ({ action, params, cfg, accountId, mediaLocalRoots }) => {
|
||||
handleAction: async ({ action, params, cfg, accountId, mediaLocalRoots, toolContext }) => {
|
||||
if (action === "send") {
|
||||
const sendParams = readTelegramSendParams(params);
|
||||
return await handleTelegramAction(
|
||||
@@ -122,9 +122,8 @@ export const telegramMessageActions: ChannelMessageActionAdapter = {
|
||||
}
|
||||
|
||||
if (action === "react") {
|
||||
const messageId = readStringOrNumberParam(params, "messageId", {
|
||||
required: true,
|
||||
});
|
||||
const messageId =
|
||||
readStringOrNumberParam(params, "messageId") ?? toolContext?.currentMessageId;
|
||||
const emoji = readStringParam(params, "emoji", { allowEmpty: true });
|
||||
const remove = typeof params.remove === "boolean" ? params.remove : undefined;
|
||||
return await handleTelegramAction(
|
||||
|
||||
@@ -249,6 +249,7 @@ export type ChannelThreadingContext = {
|
||||
From?: string;
|
||||
To?: string;
|
||||
ChatType?: string;
|
||||
CurrentMessageId?: string | number;
|
||||
ReplyToId?: string;
|
||||
ReplyToIdFull?: string;
|
||||
ThreadLabel?: string;
|
||||
@@ -259,6 +260,7 @@ export type ChannelThreadingToolContext = {
|
||||
currentChannelId?: string;
|
||||
currentChannelProvider?: ChannelId;
|
||||
currentThreadTs?: string;
|
||||
currentMessageId?: string | number;
|
||||
replyToMode?: "off" | "first" | "all";
|
||||
hasRepliedRef?: { value: boolean };
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user