fix: harden traffic telegram alert delivery and fallback chat id
Some checks failed
Deploy to 110 WOOO Server / deploy (push) Failing after 6s
Some checks failed
Deploy to 110 WOOO Server / deploy (push) Failing after 6s
This commit is contained in:
@@ -1,7 +1,12 @@
|
||||
import { request } from "node:https";
|
||||
import { prisma } from "./prisma";
|
||||
import { Prisma } from "../../prisma/generated/client";
|
||||
|
||||
const TELEGRAM_BOT_ID_FROM_TOKEN = (() => {
|
||||
if (!process.env.TELEGRAM_BOT_TOKEN) return undefined;
|
||||
const [candidate] = process.env.TELEGRAM_BOT_TOKEN.split(":", 1);
|
||||
return candidate?.trim() || undefined;
|
||||
})();
|
||||
|
||||
export type TrafficAlertEvent = {
|
||||
level: "info" | "warning" | "error";
|
||||
action: string;
|
||||
@@ -26,7 +31,95 @@ const TELEGRAM_CHAT_ID = (
|
||||
process.env.VIBEWORKAIAGENTBOT_CHAT_ID ||
|
||||
process.env.VIBEWORK_AI_BOT_CHAT_ID
|
||||
)?.trim();
|
||||
const TELEGRAM_FALLBACK_FROM_UPDATES =
|
||||
process.env.TELEGRAM_FALLBACK_FROM_UPDATES?.trim().toLowerCase() === "true";
|
||||
const DISCORD_WEBHOOK_URL = process.env.DISCORD_WEBHOOK_URL?.trim();
|
||||
const TELEGRAM_NOTIFY_TIMEOUT_MS = Math.max(
|
||||
1,
|
||||
Number.parseInt(process.env.TELEGRAM_NOTIFY_TIMEOUT_MS?.trim() || "3000", 10) || 3000
|
||||
);
|
||||
const TELEGRAM_NOTIFY_MAX_ATTEMPTS = Math.max(
|
||||
1,
|
||||
Number.parseInt(process.env.TELEGRAM_NOTIFY_MAX_ATTEMPTS?.trim() || "2", 10) || 2
|
||||
);
|
||||
const TELEGRAM_NOTIFY_RETRY_BASE_DELAY_MS = Math.max(
|
||||
100,
|
||||
Number.parseInt(process.env.TELEGRAM_NOTIFY_RETRY_BASE_DELAY_MS?.trim() || "400", 10) || 400
|
||||
);
|
||||
|
||||
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
let cachedTelegramFallbackChatId: string | null | undefined = undefined;
|
||||
|
||||
function normalizeChatId(rawChatId: string | undefined) {
|
||||
if (!rawChatId) return undefined;
|
||||
const normalized = rawChatId.trim();
|
||||
if (!normalized) return undefined;
|
||||
if (TELEGRAM_BOT_ID_FROM_TOKEN && normalized === TELEGRAM_BOT_ID_FROM_TOKEN) {
|
||||
return undefined;
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
async function resolveTelegramFallbackChatId(): Promise<string | undefined> {
|
||||
if (!TELEGRAM_BOT_TOKEN) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (cachedTelegramFallbackChatId !== undefined) {
|
||||
return cachedTelegramFallbackChatId || undefined;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/getUpdates?limit=20`, {
|
||||
method: "GET",
|
||||
signal: AbortSignal.timeout(TELEGRAM_NOTIFY_TIMEOUT_MS),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const data = (await response.json()) as {
|
||||
result?: Array<Record<string, unknown>>;
|
||||
};
|
||||
|
||||
const updates = Array.isArray(data.result) ? data.result : [];
|
||||
for (let index = updates.length - 1; index >= 0; index -= 1) {
|
||||
const update = updates[index];
|
||||
const chatCandidate =
|
||||
(update?.message as { chat?: { id?: number | string } } | undefined)?.chat?.id ??
|
||||
(update?.edited_message as { chat?: { id?: number | string } } | undefined)?.chat?.id ??
|
||||
(update?.callback_query as { message?: { chat?: { id?: number | string } } } | undefined)?.message?.chat?.id ??
|
||||
(update?.channel_post as { chat?: { id?: number | string } } | undefined)?.chat?.id;
|
||||
|
||||
const normalized = normalizeChatId(String(chatCandidate ?? ""));
|
||||
if (normalized) {
|
||||
cachedTelegramFallbackChatId = normalized;
|
||||
console.log(`[Traffic alert] Resolved Telegram chat_id from updates: ${normalized}`);
|
||||
return normalized;
|
||||
}
|
||||
}
|
||||
|
||||
cachedTelegramFallbackChatId = null;
|
||||
return undefined;
|
||||
} catch (error) {
|
||||
console.warn("[Traffic alert] resolveTelegramFallbackChatId failed", error);
|
||||
cachedTelegramFallbackChatId = null;
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveTelegramChatId(): Promise<string | undefined> {
|
||||
const explicitChatId = normalizeChatId(TELEGRAM_CHAT_ID);
|
||||
if (explicitChatId) return explicitChatId;
|
||||
|
||||
if (!TELEGRAM_FALLBACK_FROM_UPDATES) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return resolveTelegramFallbackChatId();
|
||||
}
|
||||
|
||||
function escapeMarkdown(value: unknown) {
|
||||
if (value === null || value === undefined) return "";
|
||||
@@ -55,43 +148,34 @@ function buildTelegramMessage(event: TrafficAlertEvent) {
|
||||
async function sendViaHttps(url: string, body: Record<string, unknown>) {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
try {
|
||||
const parsed = new URL(url);
|
||||
const payload = JSON.stringify(body);
|
||||
let responseBody = "";
|
||||
|
||||
const requestHandle = request(
|
||||
{
|
||||
method: "POST",
|
||||
hostname: parsed.hostname,
|
||||
path: `${parsed.pathname}${parsed.search}`,
|
||||
port: 443,
|
||||
protocol: "https:",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
"content-length": Buffer.byteLength(payload),
|
||||
},
|
||||
fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
"content-length": String(Buffer.byteLength(payload)),
|
||||
},
|
||||
(response) => {
|
||||
let responseBody = "";
|
||||
response.on("data", (chunk) => {
|
||||
responseBody += chunk;
|
||||
});
|
||||
body: payload,
|
||||
signal: AbortSignal.timeout(TELEGRAM_NOTIFY_TIMEOUT_MS),
|
||||
})
|
||||
.then(async (response) => {
|
||||
responseBody = await response.text();
|
||||
if (response.status >= 200 && response.status < 300) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
response.on("end", () => {
|
||||
if (response.statusCode && response.statusCode >= 200 && response.statusCode < 300) {
|
||||
return resolve();
|
||||
}
|
||||
|
||||
reject(new Error(`Telegram API ${response.statusCode}: ${responseBody.slice(0, 200)}`));
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
requestHandle.on("error", reject);
|
||||
requestHandle.setTimeout(3000, () => {
|
||||
requestHandle.destroy(new Error("Telegram request timeout"));
|
||||
});
|
||||
requestHandle.write(payload);
|
||||
requestHandle.end();
|
||||
reject(new Error(`Telegram API ${response.status}: ${responseBody.slice(0, 200)}`));
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error && error.name === "TimeoutError") {
|
||||
reject(new Error(`Telegram API timeout ${TELEGRAM_NOTIFY_TIMEOUT_MS}ms`));
|
||||
return;
|
||||
}
|
||||
reject(error);
|
||||
});
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
@@ -153,6 +237,8 @@ export async function sendTrafficAlert(event: TrafficAlertEvent): Promise<void>
|
||||
...event,
|
||||
};
|
||||
|
||||
const resolvedTelegramChatId = await resolveTelegramChatId();
|
||||
|
||||
const notifyTargets = [
|
||||
TRAFFIC_WEBHOOK_URL && {
|
||||
kind: "generic",
|
||||
@@ -179,7 +265,8 @@ export async function sendTrafficAlert(event: TrafficAlertEvent): Promise<void>
|
||||
}),
|
||||
},
|
||||
},
|
||||
TELEGRAM_BOT_TOKEN && TELEGRAM_CHAT_ID && {
|
||||
TELEGRAM_BOT_TOKEN &&
|
||||
resolvedTelegramChatId && {
|
||||
kind: "telegram",
|
||||
url: `https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage`,
|
||||
init: {
|
||||
@@ -187,11 +274,11 @@ export async function sendTrafficAlert(event: TrafficAlertEvent): Promise<void>
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
chat_id: TELEGRAM_CHAT_ID,
|
||||
text: buildTelegramMessage(event),
|
||||
parse_mode: "MarkdownV2",
|
||||
}),
|
||||
body: JSON.stringify({
|
||||
chat_id: resolvedTelegramChatId,
|
||||
text: buildTelegramMessage(event),
|
||||
parse_mode: "MarkdownV2",
|
||||
}),
|
||||
},
|
||||
},
|
||||
].filter(Boolean) as Array<{ kind: string; url: string; init: RequestInit }>;
|
||||
@@ -205,7 +292,20 @@ export async function sendTrafficAlert(event: TrafficAlertEvent): Promise<void>
|
||||
const payload = target.init.body
|
||||
? JSON.parse(typeof target.init.body === "string" ? target.init.body : "{}")
|
||||
: {};
|
||||
await sendViaHttps(target.url, payload);
|
||||
|
||||
let attempt = 0;
|
||||
while (true) {
|
||||
try {
|
||||
await sendViaHttps(target.url, payload);
|
||||
return;
|
||||
} catch (error) {
|
||||
attempt += 1;
|
||||
if (attempt >= TELEGRAM_NOTIFY_MAX_ATTEMPTS) {
|
||||
throw error;
|
||||
}
|
||||
await sleep(TELEGRAM_NOTIFY_RETRY_BASE_DELAY_MS * attempt);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const response = await fetch(target.url, { ...target.init, signal: AbortSignal.timeout(3000) });
|
||||
if (!response.ok) {
|
||||
|
||||
Reference in New Issue
Block a user