Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
72 lines
2.4 KiB
TypeScript
72 lines
2.4 KiB
TypeScript
import { isLocale, type Locale } from './types.ts';
|
|
|
|
/**
|
|
* Payload для t.me/<bot>?start=<payload> (LND-3, ONB-1).
|
|
* Telegram ограничивает его 64 символами из [A-Za-z0-9_-], поэтому формат компактный:
|
|
*
|
|
* l-ru_a-AGENCY1 язык и код агентства
|
|
* s-k3j9x2 короткий токен; язык, агентство и UTM лежат в БД (StartToken)
|
|
*
|
|
* Части разделены "_", ключ и значение — "-". Неизвестные части игнорируются.
|
|
*/
|
|
export interface StartPayload {
|
|
locale?: Locale;
|
|
agency?: string;
|
|
token?: string;
|
|
}
|
|
|
|
export const START_PAYLOAD_MAX = 64;
|
|
const SAFE = /^[A-Za-z0-9]+$/;
|
|
|
|
export function normalizeAgencyCode(code: string): string | null {
|
|
const c = code.trim().toUpperCase();
|
|
return SAFE.test(c) && c.length <= 32 ? c : null;
|
|
}
|
|
|
|
export function encodeStartPayload(p: StartPayload): string {
|
|
const parts: string[] = [];
|
|
if (p.token) {
|
|
if (!SAFE.test(p.token)) throw new Error('Bad token');
|
|
parts.push(`s-${p.token}`);
|
|
}
|
|
if (p.locale) parts.push(`l-${p.locale}`);
|
|
if (p.agency) {
|
|
const a = normalizeAgencyCode(p.agency);
|
|
if (a) parts.push(`a-${a}`);
|
|
}
|
|
const out = parts.join('_');
|
|
if (out.length > START_PAYLOAD_MAX) throw new Error('Start payload too long');
|
|
return out;
|
|
}
|
|
|
|
export function decodeStartPayload(raw: string | undefined | null): StartPayload {
|
|
const out: StartPayload = {};
|
|
if (!raw) return out;
|
|
for (const part of raw.slice(0, START_PAYLOAD_MAX).split('_')) {
|
|
const [key, value] = [part.slice(0, 1), part.slice(2)];
|
|
if (part[1] !== '-' || !value) continue;
|
|
if (key === 'l' && isLocale(value)) out.locale = value;
|
|
else if (key === 'a') {
|
|
const a = normalizeAgencyCode(value);
|
|
if (a) out.agency = a;
|
|
} else if (key === 's' && SAFE.test(value)) out.token = value;
|
|
}
|
|
return out;
|
|
}
|
|
|
|
export const UTM_KEYS = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_content', 'utm_term'] as const;
|
|
export type Utm = Partial<Record<(typeof UTM_KEYS)[number], string>>;
|
|
|
|
export function pickUtm(params: URLSearchParams): Utm {
|
|
const utm: Utm = {};
|
|
for (const k of UTM_KEYS) {
|
|
const v = params.get(k);
|
|
if (v) utm[k] = v.slice(0, 120);
|
|
}
|
|
return utm;
|
|
}
|
|
|
|
export function botLink(username: string, payload: string): string {
|
|
return `https://t.me/${username}${payload ? `?start=${payload}` : ''}`;
|
|
}
|