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:
1 parent
45d7512d2b
commit
d452824c62
314 files changed
+44394
-121
No files matched your search
@@ -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, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
Reference in new issue
Block a user