Monorepo skeleton, Prisma schema, core domain and services with tests
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
commit
45d7512d2b
64 files changed
+9483
No files matched your search
@@ -0,0 +1,71 @@
|
||||
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}` : ''}`;
|
||||
}
|
||||
Reference in new issue
Block a user