feat(api): Telegram — темы, очередь повторов, «Система», /setup
telegram/client: Bot API на fetch с таймаутом 5 с; ошибки делятся на повторяемые (сеть, таймаут, 429, 5xx) и конфигурационные (400/403…), токен не попадает ни в тексты ошибок, ни в tg_last_error. format: заявка и уточнение по шаблону плана, HTML с экранированием, источник из utm / referrer, первый визит. outbox: Notifier с отправкой после ответа и тиком раз в минуту, пауза 1, 2, 4… мин, уточнение только после своей заявки, после 10 попыток — в «Систему». system: тема «Система» с троттлингом ×N за период. bot: long polling, только TELEGRAM_ADMIN_ID, /setup создаёт темы и присылает id в личку. config: всё окружение в одном месте.
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
// Всё окружение api читается здесь. Telegram-переменные необязательны: без них api
|
||||
// принимает заявки, а отправки копятся в pending до настройки.
|
||||
import { DEFAULT_DB_PATH } from './db/index.js';
|
||||
|
||||
export type TelegramConfig = {
|
||||
token?: string;
|
||||
/** id группы, у супергрупп отрицательный: -100… */
|
||||
chatId?: number;
|
||||
leadsThreadId?: number;
|
||||
systemThreadId?: number;
|
||||
adminId?: number;
|
||||
};
|
||||
|
||||
export type Config = {
|
||||
port: number;
|
||||
dbPath: string;
|
||||
version: string;
|
||||
trustedProxies: string[];
|
||||
telegram: TelegramConfig;
|
||||
};
|
||||
|
||||
type Env = Record<string, string | undefined>;
|
||||
|
||||
function optional(env: Env, name: string): string | undefined {
|
||||
const value = env[name]?.trim();
|
||||
return value ? value : undefined;
|
||||
}
|
||||
|
||||
function optionalInt(env: Env, name: string): number | undefined {
|
||||
const raw = optional(env, name);
|
||||
if (raw === undefined) return undefined;
|
||||
if (!/^-?\d+$/.test(raw) || !Number.isSafeInteger(Number(raw))) {
|
||||
throw new Error(`${name}: ожидается целое число`);
|
||||
}
|
||||
return Number(raw);
|
||||
}
|
||||
|
||||
export function loadConfig(env: Env = process.env): Config {
|
||||
const port = optionalInt(env, 'PORT') ?? 3001;
|
||||
const token = optional(env, 'TELEGRAM_BOT_TOKEN');
|
||||
// Значение токена в текст ошибки не попадает — только факт, что формат не тот.
|
||||
if (token !== undefined && !/^\d+:[\w-]+$/.test(token)) throw new Error('TELEGRAM_BOT_TOKEN: неверный формат');
|
||||
return {
|
||||
port,
|
||||
dbPath: optional(env, 'DB_PATH') ?? DEFAULT_DB_PATH,
|
||||
version: optional(env, 'APP_VERSION') ?? 'dev',
|
||||
trustedProxies: (optional(env, 'TRUSTED_PROXIES') ?? '')
|
||||
.split(',')
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean),
|
||||
telegram: {
|
||||
token,
|
||||
chatId: optionalInt(env, 'TELEGRAM_CHAT_ID'),
|
||||
leadsThreadId: optionalInt(env, 'TELEGRAM_LEADS_THREAD_ID'),
|
||||
systemThreadId: optionalInt(env, 'TELEGRAM_SYSTEM_THREAD_ID'),
|
||||
adminId: optionalInt(env, 'TELEGRAM_ADMIN_ID'),
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user