// Эхо-бот Falcon Black: Node.js 20+, без библиотек, длинный опрос. // Запуск: FB_BOT_TOKEN='ключ со страницы falconblack.org/bots/key' node echo.mjs // Ключ — только в переменной окружения и только в заголовке Authorization: Bearer // (ключ в адресе запроса сервер сразу отключает). const API = process.env.FB_API || "https://falconblack.org/bot-api/v1"; const HEADERS = { Authorization: `Bearer ${(process.env.FB_BOT_TOKEN || "").trim()}`, "Content-Type": "application/json" }; async function call(method, params = {}) { const r = await fetch(`${API}/${method}`, { method: "POST", headers: HEADERS, body: JSON.stringify(params) }); let j; try { j = await r.json(); } catch { throw new Error("ответ не JSON — сбой сети, повторим"); } if (j.ok) return j.result; throw Object.assign(new Error(j.description), { code: j.error_code, key: j.error_key, retryAfter: j.parameters?.retry_after }); } // Кнопки люди пока видят списком с номерами под сообщением: ответ «1» приходит // программе нажатием (callback_query). Ответ цифрой относится к ПОСЛЕДНЕМУ // сообщению бота — поэтому кнопка есть под каждым ответом. const DICE = { inline_keyboard: [[{ text: "Бросить кубик", callback_data: "roll" }]] }; async function handle(u) { const m = u.message; if (m?.text?.startsWith("/start")) { // «/start» приходит первым, когда человек впервые написал боту. await call("sendMessage", { chat_id: m.chat.id, text: "Здравствуйте! Напишите что-нибудь — я повторю.", reply_markup: DICE }); } else if (m?.text) { await call("sendMessage", { chat_id: m.chat.id, text: `Вы написали: ${m.text}`, reply_markup: DICE }); } else if (u.callback_query?.data === "roll") { const n = 1 + Math.floor(Math.random() * 6); await call("answerCallbackQuery", { callback_query_id: u.callback_query.id }); await call("sendMessage", { chat_id: u.callback_query.message.chat.id, text: `Выпало ${n}`, reply_markup: DICE }); } } let offset = 0; for (;;) { let updates; try { updates = await call("getUpdates", { offset, timeout: 50 }); } catch (e) { // 401 — ключ (причина — в e.key), 409 — запущена вторая копия: повтор не поможет. if ([401, 403, 409].includes(e.code)) { console.error(`${e.code} ${e.key}: ${e.message}`); process.exit(1); } await new Promise((ok) => setTimeout(ok, (e.retryAfter ?? 3) * 1000)); // сбой сети или 429 continue; } for (const u of updates) { offset = u.update_id + 1; // Ошибка одного события (человек заблокировал бота — 403, предел — 429) не роняет программу. await handle(u).catch((e) => console.warn(`событие ${u.update_id}: ${e.code ?? ""} ${e.message}`)); } }