Snapshot of uncommitted work: landing, bots, prod infra, partner cabinet, backoffice stage 1

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
zabudko-ruandClaude Opus 5.5 committed 2026-10-21 01:23:32 +04:00
1 parent 45d7512d2b
commit d452824c62
314 files changed
+44394 -121

No files matched your search

+58
View File
@@ -0,0 +1,58 @@
/**
* Отправка сообщений через Telegram Bot API без зависимостей (воркер).
* Без токена (локальная разработка) сообщение печатается в консоль.
*/
export type InlineButton = { text: string; callback_data: string } | { text: string; web_app: { url: string } } | { text: string; url: string };
export interface OutgoingMessage {
chatId: bigint | number | string;
text: string;
buttons?: InlineButton[][];
}
/** Telegram принимает web_app-кнопки только с https — в разработке такие кнопки опускаются */
export function webAppButton(text: string, url: string): InlineButton | null {
return url.startsWith('https://') ? { text, web_app: { url } } : null;
}
export function compactRows(rows: (InlineButton | null)[][]): InlineButton[][] {
return rows.map((r) => r.filter((b): b is InlineButton => !!b)).filter((r) => r.length > 0);
}
export class TelegramSender {
constructor(
private token: string,
private label: string,
) {}
async send(msg: OutgoingMessage): Promise<void> {
if (!this.token) {
const buttons = msg.buttons?.flat().map((b) => `[${b.text}]`).join(' ') ?? '';
console.log(`[${this.label} → ${msg.chatId}] ${msg.text.replace(/<[^>]+>/g, '')} ${buttons}`);
return;
}
const res = await fetch(`https://api.telegram.org/bot${this.token}/sendMessage`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
chat_id: String(msg.chatId),
text: msg.text,
parse_mode: 'HTML',
link_preview_options: { is_disabled: true },
reply_markup: msg.buttons?.length ? { inline_keyboard: msg.buttons } : undefined,
}),
});
if (!res.ok) {
const body = await res.text();
// 403 — пользователь заблокировал бота, 400 chat not found — бот ему ещё не писал
// (например, тестовые пользователи seed); повторять бессмысленно
if (res.status === 403 || (res.status === 400 && body.includes('chat not found'))) {
console.warn(`[${this.label}] chat ${msg.chatId} unreachable: ${res.status}`);
return;
}
throw new Error(`Telegram ${res.status}: ${body}`);
}
}
}
export const escapeHtml = (s: string) => s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');