fix(telegram): normalize chat ids and improve errors
This commit is contained in:
@@ -360,7 +360,13 @@ export async function agentCommand(
|
||||
const telegramTarget = opts.to?.trim() || undefined;
|
||||
|
||||
const logDeliveryError = (err: unknown) => {
|
||||
const message = `Delivery failed (${deliveryProvider}): ${String(err)}`;
|
||||
const deliveryTarget =
|
||||
deliveryProvider === "telegram"
|
||||
? telegramTarget
|
||||
: deliveryProvider === "whatsapp"
|
||||
? whatsappTarget
|
||||
: undefined;
|
||||
const message = `Delivery failed (${deliveryProvider}${deliveryTarget ? ` to ${deliveryTarget}` : ""}): ${String(err)}`;
|
||||
runtime.error?.(message);
|
||||
if (!runtime.error) runtime.log(message);
|
||||
};
|
||||
|
||||
@@ -40,8 +40,6 @@ describe("wide-area DNS-SD zone rendering", () => {
|
||||
instanceLabel: "studio-london",
|
||||
});
|
||||
|
||||
expect(txt).toContain(
|
||||
`tailnetDns=peters-mac-studio-1.sheep-coho.ts.net`,
|
||||
);
|
||||
expect(txt).toContain(`tailnetDns=peters-mac-studio-1.sheep-coho.ts.net`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -32,4 +32,39 @@ describe("sendMessageTelegram", () => {
|
||||
expect(res.chatId).toBe(chatId);
|
||||
expect(res.messageId).toBe("42");
|
||||
});
|
||||
|
||||
it("normalizes chat ids with internal prefixes", async () => {
|
||||
const sendMessage = vi.fn().mockResolvedValue({
|
||||
message_id: 1,
|
||||
chat: { id: "123" },
|
||||
});
|
||||
const api = { sendMessage } as unknown as {
|
||||
sendMessage: typeof sendMessage;
|
||||
};
|
||||
|
||||
await sendMessageTelegram("telegram:123", "hi", {
|
||||
token: "tok",
|
||||
api,
|
||||
});
|
||||
|
||||
expect(sendMessage).toHaveBeenCalledWith("123", "hi", {
|
||||
parse_mode: "Markdown",
|
||||
});
|
||||
});
|
||||
|
||||
it("wraps chat-not-found with actionable context", async () => {
|
||||
const chatId = "123";
|
||||
const err = new Error("400: Bad Request: chat not found");
|
||||
const sendMessage = vi.fn().mockRejectedValue(err);
|
||||
const api = { sendMessage } as unknown as {
|
||||
sendMessage: typeof sendMessage;
|
||||
};
|
||||
|
||||
await expect(
|
||||
sendMessageTelegram(chatId, "hi", { token: "tok", api }),
|
||||
).rejects.toThrow(/chat not found/i);
|
||||
await expect(
|
||||
sendMessageTelegram(chatId, "hi", { token: "tok", api }),
|
||||
).rejects.toThrow(/chat_id=123/);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -33,8 +33,27 @@ function resolveToken(explicit?: string): string {
|
||||
function normalizeChatId(to: string): string {
|
||||
const trimmed = to.trim();
|
||||
if (!trimmed) throw new Error("Recipient is required for Telegram sends");
|
||||
if (trimmed.startsWith("@")) return trimmed;
|
||||
return trimmed;
|
||||
|
||||
// Common internal prefixes that sometimes leak into outbound sends.
|
||||
// - ctx.To uses `telegram:<id>`
|
||||
// - group sessions often use `group:<id>`
|
||||
let normalized = trimmed.replace(/^(telegram|tg|group):/i, "").trim();
|
||||
|
||||
// Accept t.me links for public chats/channels.
|
||||
// (Invite links like `t.me/+...` are not resolvable via Bot API.)
|
||||
const m =
|
||||
/^https?:\/\/t\.me\/([A-Za-z0-9_]+)$/i.exec(normalized) ??
|
||||
/^t\.me\/([A-Za-z0-9_]+)$/i.exec(normalized);
|
||||
if (m?.[1]) normalized = `@${m[1]}`;
|
||||
|
||||
if (!normalized) throw new Error("Recipient is required for Telegram sends");
|
||||
if (normalized.startsWith("@")) return normalized;
|
||||
if (/^-?\d+$/.test(normalized)) return normalized;
|
||||
|
||||
// If the user passed a username without `@`, assume they meant a public chat/channel.
|
||||
if (/^[A-Za-z0-9_]{5,}$/i.test(normalized)) return `@${normalized}`;
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
export async function sendMessageTelegram(
|
||||
@@ -75,6 +94,18 @@ export async function sendMessageTelegram(
|
||||
throw lastErr ?? new Error(`Telegram send failed (${label})`);
|
||||
};
|
||||
|
||||
const wrapChatNotFound = (err: unknown) => {
|
||||
if (!/400: Bad Request: chat not found/i.test(String(err ?? "")))
|
||||
return err;
|
||||
return new Error(
|
||||
[
|
||||
`Telegram send failed: chat not found (chat_id=${chatId}).`,
|
||||
"Likely: bot not started in DM, bot removed from group/channel, group migrated (new -100… id), or wrong bot token.",
|
||||
`Input was: ${JSON.stringify(to)}.`,
|
||||
].join(" "),
|
||||
);
|
||||
};
|
||||
|
||||
if (mediaUrl) {
|
||||
const media = await loadWebMedia(mediaUrl, opts.maxBytes);
|
||||
const kind = mediaKindFromMime(media.contentType ?? undefined);
|
||||
@@ -92,22 +123,30 @@ export async function sendMessageTelegram(
|
||||
result = await sendWithRetry(
|
||||
() => api.sendPhoto(chatId, file, { caption }),
|
||||
"photo",
|
||||
);
|
||||
).catch((err) => {
|
||||
throw wrapChatNotFound(err);
|
||||
});
|
||||
} else if (kind === "video") {
|
||||
result = await sendWithRetry(
|
||||
() => api.sendVideo(chatId, file, { caption }),
|
||||
"video",
|
||||
);
|
||||
).catch((err) => {
|
||||
throw wrapChatNotFound(err);
|
||||
});
|
||||
} else if (kind === "audio") {
|
||||
result = await sendWithRetry(
|
||||
() => api.sendAudio(chatId, file, { caption }),
|
||||
"audio",
|
||||
);
|
||||
).catch((err) => {
|
||||
throw wrapChatNotFound(err);
|
||||
});
|
||||
} else {
|
||||
result = await sendWithRetry(
|
||||
() => api.sendDocument(chatId, file, { caption }),
|
||||
"document",
|
||||
);
|
||||
).catch((err) => {
|
||||
throw wrapChatNotFound(err);
|
||||
});
|
||||
}
|
||||
const messageId = String(result?.message_id ?? "unknown");
|
||||
return { messageId, chatId: String(result?.chat?.id ?? chatId) };
|
||||
@@ -131,9 +170,11 @@ export async function sendMessageTelegram(
|
||||
return await sendWithRetry(
|
||||
() => api.sendMessage(chatId, text),
|
||||
"message-plain",
|
||||
);
|
||||
).catch((err2) => {
|
||||
throw wrapChatNotFound(err2);
|
||||
});
|
||||
}
|
||||
throw err;
|
||||
throw wrapChatNotFound(err);
|
||||
});
|
||||
const messageId = String(res?.message_id ?? "unknown");
|
||||
return { messageId, chatId: String(res?.chat?.id ?? chatId) };
|
||||
|
||||
Reference in New Issue
Block a user