Files
dragonsstash/bot/src/tdlib/client.ts
admin f4488a079f fix: add getChat before forwardMessages and debug logging for bot sends
The bot may not have the source channel loaded in TDLib's internal
state. Calling getChat first ensures it's resolved. Also added result
logging to diagnose silent send failures.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 12:39:42 +01:00

239 lines
6.2 KiB
TypeScript

import tdl from "tdl";
import { getTdjson } from "prebuilt-tdlib";
import { config } from "../util/config.js";
import { childLogger } from "../util/logger.js";
import { withFloodWait } from "../util/flood-wait.js";
const log = childLogger("tdlib-bot");
tdl.configure({ tdjson: getTdjson() });
let client: tdl.Client | null = null;
/**
* Create and authenticate a TDLib client using the bot token.
* Bot accounts have different capabilities from user accounts —
* they can't read channel history but can send/forward/copy messages
* to users who have interacted with them.
*/
export async function createBotClient(): Promise<tdl.Client> {
if (client) return client;
log.info("Creating TDLib bot client");
client = tdl.createClient({
apiId: config.telegramApiId,
apiHash: config.telegramApiHash,
databaseDirectory: `${config.tdlibStateDir}/bot`,
filesDirectory: `${config.tdlibStateDir}/bot_files`,
});
client.on("error", (err) => {
log.error({ err }, "TDLib client error");
});
await client.login(() => ({
type: "bot",
getToken: () => Promise.resolve(config.botToken),
}));
log.info("Bot client authenticated successfully");
// Load chat list so TDLib knows about channels the bot has access to.
// Without this, forwardMessages/copyMessage will fail with "Chat not found".
try {
await client.invoke({
_: "getChats",
chat_list: { _: "chatListMain" },
limit: 200,
});
log.info("Chat list loaded");
} catch (err) {
log.warn({ err }, "Failed to load chat list — forwarding may fail");
}
return client;
}
export async function closeBotClient(): Promise<void> {
if (client) {
try {
await client.close();
} catch {
// Ignore close errors
}
client = null;
log.info("Bot client closed");
}
}
/**
* Copy a message from a channel to a user's DM.
* Uses forwardMessages with send_copy so it appears sent by the bot.
*
* The fromChatId is the Telegram chat ID from the DB (e.g. -1003767441152).
* The messageId is the TDLib message ID stored in the DB.
*/
export async function copyMessageToUser(
fromChatId: bigint,
messageId: bigint,
toUserId: bigint
): Promise<void> {
if (!client) throw new Error("Bot client not initialized");
const c = client;
log.info(
{ fromChatId: fromChatId.toString(), messageId: messageId.toString(), toUserId: toUserId.toString() },
"Copying message to user"
);
// First, ensure TDLib knows about the source chat by opening it
try {
await c.invoke({ _: "getChat", chat_id: Number(fromChatId) });
} catch (err) {
log.warn({ err, chatId: fromChatId.toString() }, "getChat failed for source channel");
}
const result = await withFloodWait(
() =>
c.invoke({
_: "forwardMessages",
chat_id: Number(toUserId),
from_chat_id: Number(fromChatId),
message_ids: [Number(messageId)],
send_copy: true,
remove_caption: false,
}),
"copyMessageToUser"
);
// forwardMessages returns immediately with temp messages — check result
const messages = (result as { messages?: unknown[] })?.messages;
log.info(
{ messageCount: messages?.length ?? 0, result: JSON.stringify(result).slice(0, 500) },
"forwardMessages result"
);
}
/**
* Send a text message to a user.
*/
export async function sendTextMessage(
chatId: bigint,
text: string,
parseMode: "textParseModeMarkdown" | "textParseModeHTML" = "textParseModeMarkdown"
): Promise<void> {
if (!client) throw new Error("Bot client not initialized");
const c = client;
// Parse the text first
const parsed = await withFloodWait(
() =>
c.invoke({
_: "parseTextEntities",
text,
parse_mode: { _: parseMode, version: parseMode === "textParseModeMarkdown" ? 2 : 0 },
}),
"parseTextEntities"
);
await withFloodWait(
() =>
c.invoke({
_: "sendMessage",
chat_id: Number(chatId),
input_message_content: {
_: "inputMessageText",
text: parsed,
},
}),
"sendTextMessage"
);
}
/**
* Send a photo with caption to a user (for preview images).
*/
export async function sendPhotoMessage(
chatId: bigint,
photoData: Buffer,
caption: string
): Promise<void> {
if (!client) throw new Error("Bot client not initialized");
const c = client;
// Write the photo to a temp file
const { writeFile, unlink } = await import("fs/promises");
const path = await import("path");
const tempPath = path.join(config.tdlibStateDir, `preview_${Date.now()}.jpg`);
try {
await writeFile(tempPath, photoData);
const parsedCaption = await withFloodWait(
() =>
c.invoke({
_: "parseTextEntities",
text: caption,
parse_mode: { _: "textParseModeMarkdown", version: 2 },
}),
"parsePhotoCaption"
);
await withFloodWait(
() =>
c.invoke({
_: "sendMessage",
chat_id: Number(chatId),
input_message_content: {
_: "inputMessagePhoto",
photo: { _: "inputFileLocal", path: tempPath },
caption: parsedCaption,
width: 0,
height: 0,
},
}),
"sendPhotoMessage"
);
} finally {
await unlink(tempPath).catch(() => {});
}
}
/**
* Get basic info about a Telegram user (name, username).
*/
export async function getUser(
userId: number
): Promise<{ firstName: string; lastName?: string; username?: string }> {
if (!client) throw new Error("Bot client not initialized");
const c = client;
const user = (await withFloodWait(
() =>
c.invoke({
_: "getUser",
user_id: userId,
}),
"getUser"
)) as {
first_name?: string;
last_name?: string;
usernames?: { editable_username?: string };
};
return {
firstName: user.first_name ?? "User",
lastName: user.last_name || undefined,
username: user.usernames?.editable_username || undefined,
};
}
/**
* Get updates from TDLib. The bot listens for new messages this way.
*/
export function onBotUpdate(
handler: (update: Record<string, unknown>) => void
): void {
if (!client) throw new Error("Bot client not initialized");
client.on("update", handler);
}