refactor(telegram): centralize target parsing
This commit is contained in:
@@ -302,6 +302,31 @@ describe("sendMessageTelegram", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("parses message_thread_id from recipient string (telegram:group:...:topic:...)", async () => {
|
||||
const chatId = "-1001234567890";
|
||||
const sendMessage = vi.fn().mockResolvedValue({
|
||||
message_id: 55,
|
||||
chat: { id: chatId },
|
||||
});
|
||||
const api = { sendMessage } as unknown as {
|
||||
sendMessage: typeof sendMessage;
|
||||
};
|
||||
|
||||
await sendMessageTelegram(
|
||||
`telegram:group:${chatId}:topic:271`,
|
||||
"hello forum",
|
||||
{
|
||||
token: "tok",
|
||||
api,
|
||||
},
|
||||
);
|
||||
|
||||
expect(sendMessage).toHaveBeenCalledWith(chatId, "hello forum", {
|
||||
parse_mode: "HTML",
|
||||
message_thread_id: 271,
|
||||
});
|
||||
});
|
||||
|
||||
it("includes reply_to_message_id for threaded replies", async () => {
|
||||
const chatId = "123";
|
||||
const sendMessage = vi.fn().mockResolvedValue({
|
||||
|
||||
@@ -11,6 +11,10 @@ import { loadWebMedia } from "../web/media.js";
|
||||
import { resolveTelegramAccount } from "./accounts.js";
|
||||
import { resolveTelegramFetch } from "./fetch.js";
|
||||
import { markdownToTelegramHtml } from "./format.js";
|
||||
import {
|
||||
parseTelegramTarget,
|
||||
stripTelegramInternalPrefixes,
|
||||
} from "./targets.js";
|
||||
|
||||
type TelegramSendOpts = {
|
||||
token?: string;
|
||||
@@ -65,7 +69,7 @@ function normalizeChatId(to: string): string {
|
||||
// Common internal prefixes that sometimes leak into outbound sends.
|
||||
// - ctx.To uses `telegram:<id>`
|
||||
// - group sessions often use `telegram:group:<id>`
|
||||
let normalized = trimmed.replace(/^(telegram|tg|group):/i, "").trim();
|
||||
let normalized = stripTelegramInternalPrefixes(trimmed);
|
||||
|
||||
// Accept t.me links for public chats/channels.
|
||||
// (Invite links like `t.me/+...` are not resolvable via Bot API.)
|
||||
@@ -110,7 +114,8 @@ export async function sendMessageTelegram(
|
||||
accountId: opts.accountId,
|
||||
});
|
||||
const token = resolveToken(opts.token, account);
|
||||
const chatId = normalizeChatId(to);
|
||||
const target = parseTelegramTarget(to);
|
||||
const chatId = normalizeChatId(target.chatId);
|
||||
// Use provided api or create a new Bot instance. The nullish coalescing
|
||||
// operator ensures api is always defined (Bot.api is always non-null).
|
||||
const fetchImpl = resolveTelegramFetch();
|
||||
@@ -123,8 +128,12 @@ export async function sendMessageTelegram(
|
||||
// Build optional params for forum topics and reply threading.
|
||||
// Only include these if actually provided to keep API calls clean.
|
||||
const threadParams: Record<string, number> = {};
|
||||
if (opts.messageThreadId != null) {
|
||||
threadParams.message_thread_id = Math.trunc(opts.messageThreadId);
|
||||
const messageThreadId =
|
||||
opts.messageThreadId != null
|
||||
? opts.messageThreadId
|
||||
: target.messageThreadId;
|
||||
if (messageThreadId != null) {
|
||||
threadParams.message_thread_id = Math.trunc(messageThreadId);
|
||||
}
|
||||
if (opts.replyToMessageId != null) {
|
||||
threadParams.reply_to_message_id = Math.trunc(opts.replyToMessageId);
|
||||
|
||||
72
src/telegram/targets.test.ts
Normal file
72
src/telegram/targets.test.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
parseTelegramTarget,
|
||||
stripTelegramInternalPrefixes,
|
||||
} from "./targets.js";
|
||||
|
||||
describe("stripTelegramInternalPrefixes", () => {
|
||||
it("strips telegram prefix", () => {
|
||||
expect(stripTelegramInternalPrefixes("telegram:123")).toBe("123");
|
||||
});
|
||||
|
||||
it("strips telegram+group prefixes", () => {
|
||||
expect(stripTelegramInternalPrefixes("telegram:group:-100123")).toBe(
|
||||
"-100123",
|
||||
);
|
||||
});
|
||||
|
||||
it("is idempotent", () => {
|
||||
expect(stripTelegramInternalPrefixes("@mychannel")).toBe("@mychannel");
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseTelegramTarget", () => {
|
||||
it("parses plain chatId", () => {
|
||||
expect(parseTelegramTarget("-1001234567890")).toEqual({
|
||||
chatId: "-1001234567890",
|
||||
});
|
||||
});
|
||||
|
||||
it("parses @username", () => {
|
||||
expect(parseTelegramTarget("@mychannel")).toEqual({
|
||||
chatId: "@mychannel",
|
||||
});
|
||||
});
|
||||
|
||||
it("parses chatId:topicId format", () => {
|
||||
expect(parseTelegramTarget("-1001234567890:123")).toEqual({
|
||||
chatId: "-1001234567890",
|
||||
messageThreadId: 123,
|
||||
});
|
||||
});
|
||||
|
||||
it("parses chatId:topic:topicId format", () => {
|
||||
expect(parseTelegramTarget("-1001234567890:topic:456")).toEqual({
|
||||
chatId: "-1001234567890",
|
||||
messageThreadId: 456,
|
||||
});
|
||||
});
|
||||
|
||||
it("trims whitespace", () => {
|
||||
expect(parseTelegramTarget(" -1001234567890:99 ")).toEqual({
|
||||
chatId: "-1001234567890",
|
||||
messageThreadId: 99,
|
||||
});
|
||||
});
|
||||
|
||||
it("does not treat non-numeric suffix as topicId", () => {
|
||||
expect(parseTelegramTarget("-1001234567890:abc")).toEqual({
|
||||
chatId: "-1001234567890:abc",
|
||||
});
|
||||
});
|
||||
|
||||
it("strips internal prefixes before parsing", () => {
|
||||
expect(
|
||||
parseTelegramTarget("telegram:group:-1001234567890:topic:456"),
|
||||
).toEqual({
|
||||
chatId: "-1001234567890",
|
||||
messageThreadId: 456,
|
||||
});
|
||||
});
|
||||
});
|
||||
43
src/telegram/targets.ts
Normal file
43
src/telegram/targets.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
export type TelegramTarget = {
|
||||
chatId: string;
|
||||
messageThreadId?: number;
|
||||
};
|
||||
|
||||
export function stripTelegramInternalPrefixes(to: string): string {
|
||||
let trimmed = to.trim();
|
||||
while (true) {
|
||||
const next = trimmed.replace(/^(telegram|tg|group):/i, "").trim();
|
||||
if (next === trimmed) return trimmed;
|
||||
trimmed = next;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a Telegram delivery target into chatId and optional topic/thread ID.
|
||||
*
|
||||
* Supported formats:
|
||||
* - `chatId` (plain chat ID, t.me link, @username, or internal prefixes like `telegram:...`)
|
||||
* - `chatId:topicId` (numeric topic/thread ID)
|
||||
* - `chatId:topic:topicId` (explicit topic marker; preferred)
|
||||
*/
|
||||
export function parseTelegramTarget(to: string): TelegramTarget {
|
||||
const normalized = stripTelegramInternalPrefixes(to);
|
||||
|
||||
const topicMatch = /^(.+?):topic:(\d+)$/.exec(normalized);
|
||||
if (topicMatch) {
|
||||
return {
|
||||
chatId: topicMatch[1],
|
||||
messageThreadId: Number.parseInt(topicMatch[2], 10),
|
||||
};
|
||||
}
|
||||
|
||||
const colonMatch = /^(.+):(\d+)$/.exec(normalized);
|
||||
if (colonMatch) {
|
||||
return {
|
||||
chatId: colonMatch[1],
|
||||
messageThreadId: Number.parseInt(colonMatch[2], 10),
|
||||
};
|
||||
}
|
||||
|
||||
return { chatId: normalized };
|
||||
}
|
||||
Reference in New Issue
Block a user