feat: ретранслятор Bot API на домашнем сервере и TELEGRAM_API_BASE
С VPS api.telegram.org недоступен. deploy/tg-relay: nginx на адресе Tailscale домашнего сервера, пропускает только запросы нашего бота из tailnet, без логов доступа (токен в URL). api: базовый URL Bot API из TELEGRAM_API_BASE; сетевые сбои polling пишутся в лог раз в 10 минут.
This commit is contained in:
@@ -3,6 +3,8 @@
|
||||
import { DEFAULT_DB_PATH } from './db/index.js';
|
||||
|
||||
export type TelegramConfig = {
|
||||
/** Базовый URL Bot API: ретранслятор, если api.telegram.org напрямую недоступен. */
|
||||
apiBase?: string;
|
||||
token?: string;
|
||||
/** id группы, у супергрупп отрицательный: -100… */
|
||||
chatId?: number;
|
||||
@@ -50,6 +52,7 @@ export function loadConfig(env: Env = process.env): Config {
|
||||
.filter(Boolean),
|
||||
telegram: {
|
||||
token,
|
||||
apiBase: optional(env, 'TELEGRAM_API_BASE')?.replace(/\/+$/, ''),
|
||||
chatId: optionalInt(env, 'TELEGRAM_CHAT_ID'),
|
||||
leadsThreadId: optionalInt(env, 'TELEGRAM_LEADS_THREAD_ID'),
|
||||
systemThreadId: optionalInt(env, 'TELEGRAM_SYSTEM_THREAD_ID'),
|
||||
|
||||
@@ -14,7 +14,7 @@ import { createSystemReporter, shortError } from './telegram/system.js';
|
||||
const config = loadConfig();
|
||||
const tg = config.telegram;
|
||||
const now = () => new Date();
|
||||
const client = tg.token ? createTelegramClient({ token: tg.token }) : undefined;
|
||||
const client = tg.token ? createTelegramClient({ token: tg.token, apiBase: tg.apiBase }) : undefined;
|
||||
const system = createSystemReporter({ client, chatId: tg.chatId, threadId: tg.systemThreadId, now });
|
||||
|
||||
let holder: DbHolder;
|
||||
|
||||
@@ -8,6 +8,7 @@ import { shortError, type SystemReporter } from './system.js';
|
||||
export const LEADS_TOPIC = 'Kvarta Cafe: Заявки';
|
||||
export const SYSTEM_TOPIC = 'Kvarta Cafe: Система';
|
||||
const POLL_TIMEOUT_SEC = 25;
|
||||
const NETWORK_WARN_EVERY_MS = 10 * 60 * 1000;
|
||||
const RETRY_PAUSE_MS = 5000;
|
||||
|
||||
export type Bot = {
|
||||
@@ -45,6 +46,7 @@ export function createBot({
|
||||
}): Bot {
|
||||
let me: TgUser | undefined;
|
||||
let offset: number | undefined;
|
||||
let lastNetworkWarnAt = 0;
|
||||
let running = false;
|
||||
let abort: AbortController | undefined;
|
||||
let loop: Promise<void> | undefined;
|
||||
@@ -156,6 +158,10 @@ export function createBot({
|
||||
if (!running) return;
|
||||
if (err instanceof TelegramError && err.kind === 'config') {
|
||||
await system.report(`polling ${err.code}`, `Бот не получает обновления: ${err.message}`);
|
||||
} else if (Date.now() - lastNetworkWarnAt >= NETWORK_WARN_EVERY_MS) {
|
||||
// Сеть до Telegram недоступна — в «Систему» не сообщить, остаётся лог контейнера.
|
||||
lastNetworkWarnAt = Date.now();
|
||||
console.warn(`[bot] getUpdates не удался: ${err instanceof Error ? err.message : String(err)}`);
|
||||
}
|
||||
await pause(err instanceof TelegramError && err.retryAfterSec ? err.retryAfterSec * 1000 : retryPauseMs);
|
||||
}
|
||||
|
||||
@@ -50,14 +50,17 @@ export type SendMessageParams = {
|
||||
|
||||
export type TelegramClient = ReturnType<typeof createTelegramClient>;
|
||||
|
||||
const API = 'https://api.telegram.org';
|
||||
// С VPS в РФ api.telegram.org недоступен — ходим через ретранслятор (TELEGRAM_API_BASE, см. deploy/tg-relay).
|
||||
export const DEFAULT_API_BASE = 'https://api.telegram.org';
|
||||
|
||||
export function createTelegramClient({
|
||||
token,
|
||||
apiBase = DEFAULT_API_BASE,
|
||||
fetch = globalThis.fetch,
|
||||
timeoutMs = 5000,
|
||||
}: {
|
||||
token: string;
|
||||
apiBase?: string;
|
||||
fetch?: FetchLike;
|
||||
timeoutMs?: number;
|
||||
}) {
|
||||
@@ -81,7 +84,7 @@ export function createTelegramClient({
|
||||
params instanceof FormData
|
||||
? { method: 'POST', body: params }
|
||||
: { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(params) };
|
||||
const res = await request(method, `${API}/bot${token}/${method}`, init, opts.timeoutMs ?? timeoutMs, opts.signal);
|
||||
const res = await request(method, `${apiBase}/bot${token}/${method}`, init, opts.timeoutMs ?? timeoutMs, opts.signal);
|
||||
|
||||
let body: { ok?: boolean; result?: T; description?: string; parameters?: { retry_after?: number } };
|
||||
try {
|
||||
@@ -133,7 +136,7 @@ export function createTelegramClient({
|
||||
call<TgMessage | boolean>('editMessageText', params),
|
||||
getFile: (file_id: string) => call<{ file_id: string; file_path?: string; file_size?: number }>('getFile', { file_id }),
|
||||
async downloadFile(filePath: string, maxBytes: number): Promise<Uint8Array> {
|
||||
const res = await request('downloadFile', `${API}/file/bot${token}/${filePath}`, {}, 60_000);
|
||||
const res = await request('downloadFile', `${apiBase}/file/bot${token}/${filePath}`, {}, 60_000);
|
||||
if (!res.ok) throw new TelegramError(`downloadFile: ${res.status}`, res.status >= 500 ? 'retryable' : 'config', res.status);
|
||||
const data = new Uint8Array(await res.arrayBuffer());
|
||||
if (data.byteLength > maxBytes) throw new TelegramError('downloadFile: файл больше лимита', 'config');
|
||||
|
||||
Reference in New Issue
Block a user