mirror of
https://github.com/xCyanGrizzly/DragonsStash.git
synced 2026-05-11 06:11:15 +00:00
add TG skill
This commit is contained in:
@@ -8,6 +8,7 @@ import {
|
||||
getGlobalDestinationChannel,
|
||||
} from "./db/queries.js";
|
||||
import { copyMessageToUser, sendTextMessage, sendPhotoMessage } from "./tdlib/client.js";
|
||||
import { sleep } from "./util/flood-wait.js";
|
||||
|
||||
const log = childLogger("send-listener");
|
||||
|
||||
@@ -200,6 +201,9 @@ async function handleNewPackage(payload: string): Promise<void> {
|
||||
"Failed to notify subscriber"
|
||||
);
|
||||
});
|
||||
|
||||
// Rate limit delay between notifications (~20 msgs/sec, under 30 msgs/sec bot limit)
|
||||
await sleep(50);
|
||||
}
|
||||
} catch (err) {
|
||||
log.error({ err, payload }, "Failed to process new_package notification");
|
||||
|
||||
@@ -2,6 +2,7 @@ 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");
|
||||
|
||||
@@ -66,14 +67,18 @@ export async function copyMessageToUser(
|
||||
): Promise<void> {
|
||||
if (!client) throw new Error("Bot client not initialized");
|
||||
|
||||
await client.invoke({
|
||||
_: "forwardMessages",
|
||||
chat_id: Number(toUserId),
|
||||
from_chat_id: Number(fromChatId),
|
||||
message_ids: [Number(messageId)],
|
||||
send_copy: true,
|
||||
remove_caption: false,
|
||||
});
|
||||
await withFloodWait(
|
||||
() =>
|
||||
client.invoke({
|
||||
_: "forwardMessages",
|
||||
chat_id: Number(toUserId),
|
||||
from_chat_id: Number(fromChatId),
|
||||
message_ids: [Number(messageId)],
|
||||
send_copy: true,
|
||||
remove_caption: false,
|
||||
}),
|
||||
"copyMessageToUser"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -87,20 +92,28 @@ export async function sendTextMessage(
|
||||
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 },
|
||||
});
|
||||
const parsed = await withFloodWait(
|
||||
() =>
|
||||
client.invoke({
|
||||
_: "parseTextEntities",
|
||||
text,
|
||||
parse_mode: { _: parseMode, version: parseMode === "textParseModeMarkdown" ? 2 : 0 },
|
||||
}),
|
||||
"parseTextEntities"
|
||||
);
|
||||
|
||||
await client.invoke({
|
||||
_: "sendMessage",
|
||||
chat_id: Number(chatId),
|
||||
input_message_content: {
|
||||
_: "inputMessageText",
|
||||
text: parsed,
|
||||
},
|
||||
});
|
||||
await withFloodWait(
|
||||
() =>
|
||||
client.invoke({
|
||||
_: "sendMessage",
|
||||
chat_id: Number(chatId),
|
||||
input_message_content: {
|
||||
_: "inputMessageText",
|
||||
text: parsed,
|
||||
},
|
||||
}),
|
||||
"sendTextMessage"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -121,23 +134,31 @@ export async function sendPhotoMessage(
|
||||
try {
|
||||
await writeFile(tempPath, photoData);
|
||||
|
||||
const parsedCaption = await client.invoke({
|
||||
_: "parseTextEntities",
|
||||
text: caption,
|
||||
parse_mode: { _: "textParseModeMarkdown", version: 2 },
|
||||
});
|
||||
const parsedCaption = await withFloodWait(
|
||||
() =>
|
||||
client.invoke({
|
||||
_: "parseTextEntities",
|
||||
text: caption,
|
||||
parse_mode: { _: "textParseModeMarkdown", version: 2 },
|
||||
}),
|
||||
"parsePhotoCaption"
|
||||
);
|
||||
|
||||
await client.invoke({
|
||||
_: "sendMessage",
|
||||
chat_id: Number(chatId),
|
||||
input_message_content: {
|
||||
_: "inputMessagePhoto",
|
||||
photo: { _: "inputFileLocal", path: tempPath },
|
||||
caption: parsedCaption,
|
||||
width: 0,
|
||||
height: 0,
|
||||
},
|
||||
});
|
||||
await withFloodWait(
|
||||
() =>
|
||||
client.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(() => {});
|
||||
}
|
||||
@@ -150,10 +171,14 @@ export async function getUser(
|
||||
userId: number
|
||||
): Promise<{ firstName: string; lastName?: string; username?: string }> {
|
||||
if (!client) throw new Error("Bot client not initialized");
|
||||
const user = (await client.invoke({
|
||||
_: "getUser",
|
||||
user_id: userId,
|
||||
})) as {
|
||||
const user = (await withFloodWait(
|
||||
() =>
|
||||
client.invoke({
|
||||
_: "getUser",
|
||||
user_id: userId,
|
||||
}),
|
||||
"getUser"
|
||||
)) as {
|
||||
first_name?: string;
|
||||
last_name?: string;
|
||||
usernames?: { editable_username?: string };
|
||||
|
||||
60
bot/src/util/flood-wait.ts
Normal file
60
bot/src/util/flood-wait.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { childLogger } from "./logger.js";
|
||||
|
||||
const log = childLogger("flood-wait");
|
||||
|
||||
function sleep(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the mandatory wait duration (in seconds) from a Telegram
|
||||
* FLOOD_WAIT error. Returns null when the error is not rate-limit related.
|
||||
*/
|
||||
export function extractFloodWaitSeconds(err: unknown): number | null {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
|
||||
// Pattern 1: FLOOD_WAIT_30
|
||||
const flood = message.match(/FLOOD_WAIT_(\d+)/i);
|
||||
if (flood) return parseInt(flood[1], 10);
|
||||
|
||||
// Pattern 2: "retry after 30"
|
||||
const retry = message.match(/retry after (\d+)/i);
|
||||
if (retry) return parseInt(retry[1], 10);
|
||||
|
||||
// Pattern 3: HTTP 429 without explicit seconds
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
if (String((err as any)?.code) === "429") return 30;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap any async Telegram operation with automatic FLOOD_WAIT retry.
|
||||
* Adds random jitter (1-5s) to prevent thundering-herd retries.
|
||||
*
|
||||
* Non-rate-limit errors are re-thrown immediately (fail-fast).
|
||||
*/
|
||||
export async function withFloodWait<T>(
|
||||
fn: () => Promise<T>,
|
||||
context?: string,
|
||||
maxRetries = 5
|
||||
): Promise<T> {
|
||||
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
||||
try {
|
||||
return await fn();
|
||||
} catch (err) {
|
||||
const wait = extractFloodWaitSeconds(err);
|
||||
if (wait === null || attempt >= maxRetries) throw err;
|
||||
|
||||
const jitter = 1000 + Math.random() * 4000;
|
||||
log.warn(
|
||||
{ context, wait, attempt: attempt + 1, maxRetries, jitter: Math.round(jitter) },
|
||||
"FLOOD_WAIT received — backing off"
|
||||
);
|
||||
await sleep(wait * 1000 + jitter);
|
||||
}
|
||||
}
|
||||
throw new Error("Unreachable");
|
||||
}
|
||||
|
||||
export { sleep };
|
||||
Reference in New Issue
Block a user