Files
3x-ui/docs/lib/xray/telegram.ts
T
MHSanaei 9b91f0f42e docs: vendor the documentation site into the monorepo
Fold the standalone 3x-ui-docs project (Next.js 16 + Fumadocs, deployed to
docs.sanaei.dev) into docs/ so the panel and its documentation share a single
source of truth, the way sing-box keeps its docs in-tree. The old repo becomes
redundant and can be retired.

- Import the full site under docs/ (app, components, content, lib, public,
  scripts, config). The self-contained pnpm project sits alongside the existing
  engineering notes with no filename collisions.
- Re-point "Edit on GitHub" links from MHSanaei/3x-ui-docs to this repo's
  docs/content/docs path (docs/lib/shared.ts, docs/app/.../page.tsx).
- Add docs-ci.yml and docs-deploy.yml under .github/workflows/, scoped to
  docs/** and run with working-directory: docs, since GitHub only runs
  workflows from the repo-root .github/. deploy-static.yml's GitHub Pages
  publish (CNAME docs.sanaei.dev) carries over unchanged.

Follow-up (outside this commit): attach the docs.sanaei.dev custom domain to
this repository's Pages (or set the Vercel project's root directory to docs),
confirm the site is live from the monorepo, then delete MHSanaei/3x-ui-docs.
2026-07-07 23:07:14 +02:00

116 lines
3.4 KiB
TypeScript

// 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 <bot-id>:<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 <bot-id>:<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 <number><unit> 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 <duration>.',
};
}
export function telegramApiBase(token: string): string {
return `https://api.telegram.org/bot${token.trim()}`;
}
export function renderMessageTemplate(tpl: string, vars: Record<string, string>): 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(),
};
}