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";
|
||||||
import { Prisma } from "../../prisma/generated/client";
|
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 = {
|
export type TrafficAlertEvent = {
|
||||||
level: "info" | "warning" | "error";
|
level: "info" | "warning" | "error";
|
||||||
action: string;
|
action: string;
|
||||||
@@ -26,7 +31,95 @@ const TELEGRAM_CHAT_ID = (
|
|||||||
process.env.VIBEWORKAIAGENTBOT_CHAT_ID ||
|
process.env.VIBEWORKAIAGENTBOT_CHAT_ID ||
|
||||||
process.env.VIBEWORK_AI_BOT_CHAT_ID
|
process.env.VIBEWORK_AI_BOT_CHAT_ID
|
||||||
)?.trim();
|
)?.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 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) {
|
function escapeMarkdown(value: unknown) {
|
||||||
if (value === null || value === undefined) return "";
|
if (value === null || value === undefined) return "";
|
||||||
@@ -55,43 +148,34 @@ function buildTelegramMessage(event: TrafficAlertEvent) {
|
|||||||
async function sendViaHttps(url: string, body: Record<string, unknown>) {
|
async function sendViaHttps(url: string, body: Record<string, unknown>) {
|
||||||
return new Promise<void>((resolve, reject) => {
|
return new Promise<void>((resolve, reject) => {
|
||||||
try {
|
try {
|
||||||
const parsed = new URL(url);
|
|
||||||
const payload = JSON.stringify(body);
|
const payload = JSON.stringify(body);
|
||||||
|
let responseBody = "";
|
||||||
|
|
||||||
const requestHandle = request(
|
fetch(url, {
|
||||||
{
|
method: "POST",
|
||||||
method: "POST",
|
headers: {
|
||||||
hostname: parsed.hostname,
|
"content-type": "application/json",
|
||||||
path: `${parsed.pathname}${parsed.search}`,
|
"content-length": String(Buffer.byteLength(payload)),
|
||||||
port: 443,
|
|
||||||
protocol: "https:",
|
|
||||||
headers: {
|
|
||||||
"content-type": "application/json",
|
|
||||||
"content-length": Buffer.byteLength(payload),
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
(response) => {
|
body: payload,
|
||||||
let responseBody = "";
|
signal: AbortSignal.timeout(TELEGRAM_NOTIFY_TIMEOUT_MS),
|
||||||
response.on("data", (chunk) => {
|
})
|
||||||
responseBody += chunk;
|
.then(async (response) => {
|
||||||
});
|
responseBody = await response.text();
|
||||||
|
if (response.status >= 200 && response.status < 300) {
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
response.on("end", () => {
|
reject(new Error(`Telegram API ${response.status}: ${responseBody.slice(0, 200)}`));
|
||||||
if (response.statusCode && response.statusCode >= 200 && response.statusCode < 300) {
|
})
|
||||||
return resolve();
|
.catch((error) => {
|
||||||
}
|
if (error && error.name === "TimeoutError") {
|
||||||
|
reject(new Error(`Telegram API timeout ${TELEGRAM_NOTIFY_TIMEOUT_MS}ms`));
|
||||||
reject(new Error(`Telegram API ${response.statusCode}: ${responseBody.slice(0, 200)}`));
|
return;
|
||||||
});
|
}
|
||||||
}
|
reject(error);
|
||||||
);
|
});
|
||||||
|
|
||||||
requestHandle.on("error", reject);
|
|
||||||
requestHandle.setTimeout(3000, () => {
|
|
||||||
requestHandle.destroy(new Error("Telegram request timeout"));
|
|
||||||
});
|
|
||||||
requestHandle.write(payload);
|
|
||||||
requestHandle.end();
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
reject(error);
|
reject(error);
|
||||||
}
|
}
|
||||||
@@ -153,6 +237,8 @@ export async function sendTrafficAlert(event: TrafficAlertEvent): Promise<void>
|
|||||||
...event,
|
...event,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const resolvedTelegramChatId = await resolveTelegramChatId();
|
||||||
|
|
||||||
const notifyTargets = [
|
const notifyTargets = [
|
||||||
TRAFFIC_WEBHOOK_URL && {
|
TRAFFIC_WEBHOOK_URL && {
|
||||||
kind: "generic",
|
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",
|
kind: "telegram",
|
||||||
url: `https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage`,
|
url: `https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage`,
|
||||||
init: {
|
init: {
|
||||||
@@ -187,11 +274,11 @@ export async function sendTrafficAlert(event: TrafficAlertEvent): Promise<void>
|
|||||||
headers: {
|
headers: {
|
||||||
"content-type": "application/json",
|
"content-type": "application/json",
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
chat_id: TELEGRAM_CHAT_ID,
|
chat_id: resolvedTelegramChatId,
|
||||||
text: buildTelegramMessage(event),
|
text: buildTelegramMessage(event),
|
||||||
parse_mode: "MarkdownV2",
|
parse_mode: "MarkdownV2",
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
].filter(Boolean) as Array<{ kind: string; url: string; init: RequestInit }>;
|
].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
|
const payload = target.init.body
|
||||||
? JSON.parse(typeof target.init.body === "string" ? 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 {
|
} else {
|
||||||
const response = await fetch(target.url, { ...target.init, signal: AbortSignal.timeout(3000) });
|
const response = await fetch(target.url, { ...target.init, signal: AbortSignal.timeout(3000) });
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
|||||||
Reference in New Issue
Block a user