// Pure validation + templating helpers for 3x-ui's Telegram bot settings. // Grounded in internal/web/service/tgbot/tgbot.go (admin ids parsed with // strconv.ParseInt(_,10,64); token handed to telego.NewBot → api.telegram.org) // and the panel's tg* settings (tgRunTime uses robfig/cron). No React/DOM. export interface BotConfig { token: string; adminIds: string; // raw comma-separated input runTime: string; // @daily | @every 8h | 5/6-field cron } export interface TokenValidation { valid: boolean; botId?: string; error?: string; } export interface AdminIdsResult { ids: number[]; invalid: string[]; } export type CronKind = 'macro' | 'every' | 'cron' | 'invalid'; export interface CronValidation { valid: boolean; kind: CronKind; error?: string; } // BotFather tokens are :<35+ char secret of [A-Za-z0-9_-]>. const TOKEN_RE = /^(\d+):[A-Za-z0-9_-]{35,}$/; export function validateBotToken(token: string): TokenValidation { const t = token.trim(); if (!t) return { valid: false, error: 'Token is empty.' }; const m = TOKEN_RE.exec(t); if (!m) { return { valid: false, error: 'Expected the BotFather format :<35+ char secret>.' }; } return { valid: true, botId: m[1] }; } export function parseAdminIds(raw: string): AdminIdsResult { const ids: number[] = []; const invalid: string[] = []; for (const part of raw.split(',').map((s) => s.trim()).filter(Boolean)) { // Telegram chat ids are integers; group/channel ids are negative. if (/^-?\d+$/.test(part)) ids.push(Number(part)); else invalid.push(part); } return { ids, invalid }; } // robfig/cron predefined macros (note: @reboot is NOT supported). const CRON_MACROS = new Set([ '@yearly', '@annually', '@monthly', '@weekly', '@daily', '@midnight', '@hourly', ]); // Go duration: one or more chunks (ns, us/µs, ms, s, m, h). const GO_DURATION_RE = /^(\d+(\.\d+)?(ns|us|µs|ms|s|m|h))+$/; export function validateRunTime(s: string): CronValidation { const v = s.trim(); if (!v) return { valid: false, kind: 'invalid', error: 'Schedule is empty.' }; if (v.startsWith('@every ')) { const dur = v.slice('@every '.length).trim(); if (GO_DURATION_RE.test(dur)) return { valid: true, kind: 'every' }; return { valid: false, kind: 'invalid', error: `Invalid @every duration: "${dur}".` }; } if (v.startsWith('@')) { if (CRON_MACROS.has(v)) return { valid: true, kind: 'macro' }; return { valid: false, kind: 'invalid', error: `Unknown macro: "${v}".` }; } const fields = v.split(/\s+/); if (fields.length === 5 || fields.length === 6) return { valid: true, kind: 'cron' }; return { valid: false, kind: 'invalid', error: 'Use a 5/6-field cron, an @macro (e.g. @daily), or @every .', }; } export function telegramApiBase(token: string): string { return `https://api.telegram.org/bot${token.trim()}`; } export function renderMessageTemplate(tpl: string, vars: Record): string { return tpl.replace(/\{\{\s*(\w+)\s*\}\}/g, (_match, key: string) => key in vars ? vars[key] : `{{${key}}}`, ); } export function buildBotConfigSummary(c: BotConfig): { tgBotEnable: boolean; tgBotToken: string; tgBotChatId: string; tgRunTime: string; } { const { ids } = parseAdminIds(c.adminIds); return { tgBotEnable: true, tgBotToken: c.token.trim(), tgBotChatId: ids.join(','), tgRunTime: c.runTime.trim(), }; }