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 telegramTarget = opts.to?.trim() || undefined;
|
||||||
|
|
||||||
const logDeliveryError = (err: unknown) => {
|
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);
|
runtime.error?.(message);
|
||||||
if (!runtime.error) runtime.log(message);
|
if (!runtime.error) runtime.log(message);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -40,8 +40,6 @@ describe("wide-area DNS-SD zone rendering", () => {
|
|||||||
instanceLabel: "studio-london",
|
instanceLabel: "studio-london",
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(txt).toContain(
|
expect(txt).toContain(`tailnetDns=peters-mac-studio-1.sheep-coho.ts.net`);
|
||||||
`tailnetDns=peters-mac-studio-1.sheep-coho.ts.net`,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -32,4 +32,39 @@ describe("sendMessageTelegram", () => {
|
|||||||
expect(res.chatId).toBe(chatId);
|
expect(res.chatId).toBe(chatId);
|
||||||
expect(res.messageId).toBe("42");
|
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 {
|
function normalizeChatId(to: string): string {
|
||||||
const trimmed = to.trim();
|
const trimmed = to.trim();
|
||||||
if (!trimmed) throw new Error("Recipient is required for Telegram sends");
|
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(
|
export async function sendMessageTelegram(
|
||||||
@@ -75,6 +94,18 @@ export async function sendMessageTelegram(
|
|||||||
throw lastErr ?? new Error(`Telegram send failed (${label})`);
|
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) {
|
if (mediaUrl) {
|
||||||
const media = await loadWebMedia(mediaUrl, opts.maxBytes);
|
const media = await loadWebMedia(mediaUrl, opts.maxBytes);
|
||||||
const kind = mediaKindFromMime(media.contentType ?? undefined);
|
const kind = mediaKindFromMime(media.contentType ?? undefined);
|
||||||
@@ -92,22 +123,30 @@ export async function sendMessageTelegram(
|
|||||||
result = await sendWithRetry(
|
result = await sendWithRetry(
|
||||||
() => api.sendPhoto(chatId, file, { caption }),
|
() => api.sendPhoto(chatId, file, { caption }),
|
||||||
"photo",
|
"photo",
|
||||||
);
|
).catch((err) => {
|
||||||
|
throw wrapChatNotFound(err);
|
||||||
|
});
|
||||||
} else if (kind === "video") {
|
} else if (kind === "video") {
|
||||||
result = await sendWithRetry(
|
result = await sendWithRetry(
|
||||||
() => api.sendVideo(chatId, file, { caption }),
|
() => api.sendVideo(chatId, file, { caption }),
|
||||||
"video",
|
"video",
|
||||||
);
|
).catch((err) => {
|
||||||
|
throw wrapChatNotFound(err);
|
||||||
|
});
|
||||||
} else if (kind === "audio") {
|
} else if (kind === "audio") {
|
||||||
result = await sendWithRetry(
|
result = await sendWithRetry(
|
||||||
() => api.sendAudio(chatId, file, { caption }),
|
() => api.sendAudio(chatId, file, { caption }),
|
||||||
"audio",
|
"audio",
|
||||||
);
|
).catch((err) => {
|
||||||
|
throw wrapChatNotFound(err);
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
result = await sendWithRetry(
|
result = await sendWithRetry(
|
||||||
() => api.sendDocument(chatId, file, { caption }),
|
() => api.sendDocument(chatId, file, { caption }),
|
||||||
"document",
|
"document",
|
||||||
);
|
).catch((err) => {
|
||||||
|
throw wrapChatNotFound(err);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
const messageId = String(result?.message_id ?? "unknown");
|
const messageId = String(result?.message_id ?? "unknown");
|
||||||
return { messageId, chatId: String(result?.chat?.id ?? chatId) };
|
return { messageId, chatId: String(result?.chat?.id ?? chatId) };
|
||||||
@@ -131,9 +170,11 @@ export async function sendMessageTelegram(
|
|||||||
return await sendWithRetry(
|
return await sendWithRetry(
|
||||||
() => api.sendMessage(chatId, text),
|
() => api.sendMessage(chatId, text),
|
||||||
"message-plain",
|
"message-plain",
|
||||||
);
|
).catch((err2) => {
|
||||||
|
throw wrapChatNotFound(err2);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
throw err;
|
throw wrapChatNotFound(err);
|
||||||
});
|
});
|
||||||
const messageId = String(res?.message_id ?? "unknown");
|
const messageId = String(res?.message_id ?? "unknown");
|
||||||
return { messageId, chatId: String(res?.chat?.id ?? chatId) };
|
return { messageId, chatId: String(res?.chat?.id ?? chatId) };
|
||||||
|
|||||||
Reference in New Issue
Block a user