feat(worker): native TDLib forward from source to destination channel

This commit is contained in:
2026-07-31 05:06:11 +02:00
parent 960da01ec6
commit 662f5ac711
2 changed files with 112 additions and 0 deletions
+39
View File
@@ -0,0 +1,39 @@
import { describe, it, expect, vi } from "vitest";
import { forwardArchiveToChannel } from "./forward.js";
function fakeClient(response: unknown) {
return { invoke: vi.fn(async () => response) } as never;
}
describe("forwardArchiveToChannel", () => {
it("sorts message ids ascending and sends them via forwardMessages", async () => {
const invoke = vi.fn(async (req: { message_ids: number[] }) => ({
messages: req.message_ids.map((id) => ({ id: id + 1000 })),
}));
const client = { invoke } as never;
const result = await forwardArchiveToChannel(client, 111n, 222n, [30n, 10n, 20n]);
expect(invoke).toHaveBeenCalledWith(
expect.objectContaining({
_: "forwardMessages",
chat_id: 222,
from_chat_id: 111,
message_ids: [10, 20, 30],
send_copy: false,
}),
);
expect(result.messageId).toBe(1010n);
expect(result.messageIds).toEqual([1010n, 1020n, 1030n]);
});
it("throws when Telegram returns null for a message (can't be forwarded)", async () => {
const client = fakeClient({ messages: [{ id: 1001 }, null] });
await expect(forwardArchiveToChannel(client, 111n, 222n, [10n, 20n])).rejects.toThrow(/could not forward/);
});
it("throws when the response has the wrong number of messages", async () => {
const client = fakeClient({ messages: [{ id: 1001 }] });
await expect(forwardArchiveToChannel(client, 111n, 222n, [10n, 20n])).rejects.toThrow(/expected 2/);
});
});
+73
View File
@@ -0,0 +1,73 @@
import type { Client } from "tdl";
import { childLogger } from "../util/logger.js";
import { withFloodWait } from "../util/retry.js";
const log = childLogger("forward");
export interface ForwardResult {
messageId: bigint;
messageIds: bigint[];
}
/**
* Forward all parts of an archive set from the source chat directly to the
* destination chat via TDLib's forwardMessages — no download, no re-upload.
* Only usable when the source channel allows forwarding
* (TelegramChannel.allowsForwarding); the caller is responsible for that
* check. message_ids must be in strictly increasing order per the TDLib API,
* so this always sorts them regardless of the order they're passed in.
*/
export async function forwardArchiveToChannel(
client: Client,
fromChatId: bigint,
toChatId: bigint,
sourceMessageIds: bigint[],
): Promise<ForwardResult> {
const sortedIds = [...sourceMessageIds].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
const numericIds = sortedIds.map((id) => Number(id));
log.info(
{ fromChatId: Number(fromChatId), toChatId: Number(toChatId), count: numericIds.length },
"Forwarding archive to destination channel"
);
const result = (await withFloodWait(
() =>
client.invoke({
_: "forwardMessages",
chat_id: Number(toChatId),
topic_id: null,
from_chat_id: Number(fromChatId),
message_ids: numericIds,
options: null,
send_copy: false,
remove_caption: false,
} as never),
"forwardMessages"
)) as { messages: ({ id: number } | null)[] };
const forwarded = result.messages;
if (!forwarded || forwarded.length !== numericIds.length) {
throw new Error(
`forwardMessages returned ${forwarded?.length ?? 0} messages, expected ${numericIds.length}`
);
}
const messageIds: bigint[] = [];
for (let i = 0; i < forwarded.length; i++) {
const msg = forwarded[i];
if (!msg) {
throw new Error(
`forwardMessages could not forward source message ${sortedIds[i]} (Telegram returned null — message may not be forwardable)`
);
}
messageIds.push(BigInt(msg.id));
}
log.info(
{ fromChatId: Number(fromChatId), toChatId: Number(toChatId), messageIds: messageIds.map(Number) },
"Forward confirmed by Telegram"
);
return { messageId: messageIds[0], messageIds };
}