mirror of
https://github.com/xCyanGrizzly/DragonsStash.git
synced 2026-05-11 06:11:15 +00:00
feat: Docker audit + Telegram bot service + send UI
Docker:
- Harden docker-compose.yml: parameterized DB creds, required AUTH_SECRET,
health checks, resource limits, network isolation, removed exposed DB port
- Add profiles (telegram/bot/full) so base 'docker compose up' needs only AUTH_SECRET
- Fix docker-entrypoint.sh: AUTH_SECRET startup guard
- Fix Dockerfile: copy prisma.config.ts + dotenv into production image
- Update .env.example with all new variables
- Update .dockerignore
Telegram Bot Service (bot/):
- TDLib-based bot using bot token auth (not HTTP Bot API)
- Commands: /search, /latest, /package, /link, /unlink, /subscribe, /unsubscribe
- pg_notify listener for send requests (bot_send) and new packages (new_package)
- Subscription-based notifications when matching packages arrive
- Dockerfile with multi-stage build (bookworm-slim for glibc/TDLib)
API & Database:
- Prisma: TelegramLink, BotSendRequest, BotSubscription models + migration
- POST /api/telegram/bot/send - queue package delivery to linked TG account
- GET /api/telegram/bot/send/[id] - poll send request status
- Server actions: generateTelegramLinkCode, unlinkTelegram, getBotSendHistory
- Worker: emit pg_notify('new_package') after creating packages
Frontend:
- Settings: TelegramLinkCard for account linking via one-time code
- STL table + drawer: SendToTelegramButton with send dialog and status polling
- Telegram admin: Bot Sends tab with delivery history table
- Shared SendHistoryRow type
README: Updated with bot docs, profiles, config vars, project structure
This commit is contained in:
155
bot/src/tdlib/client.ts
Normal file
155
bot/src/tdlib/client.ts
Normal file
@@ -0,0 +1,155 @@
|
||||
import tdl from "tdl";
|
||||
import { getTdjson } from "prebuilt-tdlib";
|
||||
import { config } from "../util/config.js";
|
||||
import { childLogger } from "../util/logger.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",
|
||||
token: config.botToken,
|
||||
}));
|
||||
|
||||
log.info("Bot client authenticated successfully");
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Forward a message from a channel to a user's DM.
|
||||
* Uses copyMessage to make it appear as sent by the bot.
|
||||
*/
|
||||
export async function copyMessageToUser(
|
||||
fromChatId: bigint,
|
||||
messageId: bigint,
|
||||
toUserId: bigint
|
||||
): Promise<void> {
|
||||
if (!client) throw new Error("Bot client not initialized");
|
||||
|
||||
// TDLib uses negative chat IDs for channels/supergroups
|
||||
// The telegramId from the DB is the raw Telegram ID; for channels it needs -100 prefix
|
||||
const fromChatIdNum = Number(-100n * 1n) + Number(fromChatId);
|
||||
|
||||
await client.invoke({
|
||||
_: "forwardMessages",
|
||||
chat_id: Number(toUserId),
|
||||
from_chat_id: Number(fromChatId) > 0 ? -Number(fromChatId) : Number(fromChatId),
|
||||
message_ids: [Number(messageId)],
|
||||
send_copy: true,
|
||||
remove_caption: false,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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");
|
||||
|
||||
// Parse the text first
|
||||
const parsed = await client.invoke({
|
||||
_: "parseTextEntities",
|
||||
text,
|
||||
parse_mode: { _: parseMode, version: parseMode === "textParseModeMarkdown" ? 2 : 0 },
|
||||
});
|
||||
|
||||
await client.invoke({
|
||||
_: "sendMessage",
|
||||
chat_id: Number(chatId),
|
||||
input_message_content: {
|
||||
_: "inputMessageText",
|
||||
text: parsed,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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");
|
||||
|
||||
// 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 client.invoke({
|
||||
_: "parseTextEntities",
|
||||
text: caption,
|
||||
parse_mode: { _: "textParseModeMarkdown", version: 2 },
|
||||
});
|
||||
|
||||
await client.invoke({
|
||||
_: "sendMessage",
|
||||
chat_id: Number(chatId),
|
||||
input_message_content: {
|
||||
_: "inputMessagePhoto",
|
||||
photo: { _: "inputFileLocal", path: tempPath },
|
||||
caption: parsedCaption,
|
||||
width: 0,
|
||||
height: 0,
|
||||
},
|
||||
});
|
||||
} finally {
|
||||
await unlink(tempPath).catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
Reference in New Issue
Block a user