FALCON BLACK Русский

Bots for Falcon Black

A bot is a program that answers people in Falcon Black. You create it in a chat with @botnest (Bot Nest), get a key on the website and connect your program to the Bot API: it receives people's messages and replies. The API shape will feel familiar if you have written bots for other platforms: same method and field names.

Bots work in one-to-one chats, in groups and in channels (below); updates arrive by long polling (getUpdates) or a webhook (setWebhook). Mini apps are below. Inline mode comes next; its calls get a clear refusal.

Quick start

  1. Open a chat with @botnest in Falcon Black and send /newbot.
  2. Send the bot's name, then a username in Latin letters ending in bot.
  3. Send /token — Bot Nest replies with a one-time code (10 minutes). The key itself is never sent in a chat.
  4. Open falconblack.org/bots/key, enter the code and your Falcon Black password — the key is shown once. Put it into the FB_BOT_TOKEN environment variable.
  5. Run an example below and write to your bot.

Address and key

All methods: https://falconblack.org/bot-api/v1/<method>, GET or POST (JSON, form or multipart/form-data). The key goes only in the header Authorization: Bearer <key>. A key inside a request address is refused and disabled at once (query string or a /bot<key>/method path): addresses end up in proxy logs. In the app, a key is issued the same way — My bots → bot → New key, with your account password — and is also shown once: we don't store the key itself, only its fingerprint. Revoke is there too.

import zlib from "node:zlib";   // Node 20.15+ / 22
const B62 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
export function keyLooksRight(key) {
  const m = /^(\d{10,16}:fb1_[0-9A-Za-z]{43})([0-9A-Za-z]{6})$/.exec(key);
  if (!m) return false;
  let n = zlib.crc32(m[1]), sum = "";
  for (let i = 0; i < 6; i++) { sum = B62[n % 62] + sum; n = Math.floor(n / 62); }
  return sum === m[2];
}

Example: echo bot

No libraries, long polling, tested against the Falcon Black server. echo.mjs (Node.js 20+), echo.py (Python 3.9+ with requests); full examples: full.mjs, full.py. Comments in the examples are in Russian.

// Эхо-бот 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}`));
  }
}

Bot profile

What people see in the bot's profile, in search and in an empty chat: the name (up to 64 characters), the picture, About (up to 120 characters — in the profile and in search) and “What can this bot do” (up to 512 characters — in an empty chat, before Start). Change them in @botnest or in the app: My bots → bot → Info. Names and texts must not make the bot look like Falcon Black.

Commands

A command is a word after “/” that people pick in the bot's menu: up to 100 commands, each 1–32 characters (Latin letters, digits and “_”, or Cyrillic), with a description up to 256 characters. Set the menu in @botnest, in the app (My bots → bot → Commands) or from your program with setMyCommands — also per language (language_code) and for groups (scope).

Methods

getMe, getUpdates (offset, limit 1–100, timeout 0–50 s), sendMessage, sendPhoto (upload up to 10 MB or file_id), sendDocument (upload up to 20 MB or file_id), editMessageText, editMessageReplyMarkup, deleteMessage (own messages under 48 h), answerCallbackQuery, sendChatAction, setMyCommands / getMyCommands / deleteMyCommands, getChat, sendVideo (MP4/MOV), sendAudio (MP3, M4A, WAV, FLAC), sendVoice (M4A or MP3 — no OGG/Opus yet), sendSticker (only from Falcon Black sticker sets: a received sticker's file_id or getStickerSet), sendLocation (no live location yet), sendContact (delivered as text), setMessageReaction (one emoji, including a flag or a keycap digit; one reaction per message. In a channel — only the reactions its owner allows, and no more different reactions per post than set: getChat → available_reactions, max_reaction_count), setWebhook / deleteWebhook / getWebhookInfo, setChatMenuButton / getChatMenuButton, answerWebAppQuery (mini apps; getMe.has_main_web_app).

Webhook

https only, ports 443, 88 or 8443, a host name (not an IP), no user info; internal addresses and falconblack.org are refused (bad_webhook). The host is checked at setWebhook and before every delivery. Each update is a POST with Falcon-Update-Id and, if you set secret_token, the X-Falcon-Bot-Api-Secret-Token header — the name differs from other platforms, so check it yourself. 2xx within 10 s means delivered; network failures are retried (5 s … then hourly, for a day); an update answered with 4xx/5xx five times in a row is dropped. One person's updates go strictly in order; different people in parallel, up to max_connections (max 10). A new URL gets one update at a time, at most every 5 s, until it answers 2xx once. 15 minutes without success — falcon_state: "failing" and a message from @botnest. While a webhook is set, getUpdates returns 409. A new key or revoking the key removes the webhook.

// Node.js 20+: приёмник вебхука. Свой https-сервер или прокси перед ним (порт 443, 88 или 8443).
import http from "node:http";
const SECRET = process.env.FB_WEBHOOK_SECRET;            // тот же, что в setWebhook
http.createServer((req, res) => {
  // Сверяйте НАШ заголовок сами: готовые библиотеки ищут заголовок с другим именем.
  if (req.headers["x-falcon-bot-api-secret-token"] !== SECRET) { res.writeHead(401).end(); return; }
  let body = "";
  req.on("data", (c) => (body += c));
  req.on("end", () => {
    const update = JSON.parse(body);                     // update_id — и в заголовке Falcon-Update-Id
    res.writeHead(200).end();                            // 2xx за 10 с — доставлено; работу — после ответа
    handle(update);
  });
}).listen(8080);                                         // за прокси с https

// Один раз, при запуске программы:
// curl -H "Authorization: Bearer $FB_BOT_TOKEN" -d url=https://bot.example.org/hook \
//      -d secret_token=$FB_WEBHOOK_SECRET https://falconblack.org/bot-api/v1/setWebhook

Events and buttons

What a bot knows about a person

A number unique to your bot, the profile name and the app language. No username, phone or photo. A bot can only write to people who wrote to it first and haven't blocked it. Everything a person writes to a bot goes to the bot's author.

Errors and limits

{"ok": true, "result": {...}}
{"ok": false, "error_code": 403, "description": "Forbidden: bot was blocked by the user",
 "error_key": "blocked"}
{"ok": false, "error_code": 429, "description": "Too Many Requests: retry after 3",
 "error_key": "too_many", "parameters": {"retry_after": 3}}

HTTP status equals error_code; error_key is a stable machine code; every response has X-Request-Id. Sending: new bots (first 3 days) 10/s and 300/min, others 30/s and 1200/min; one chat — 1/s, burst 5; text 4096 UTF-16 units, caption 1024; photo 10 MB, file, video, audio and voice 20 MB, at most 2 uploads at a time per key; getUpdates 30/s for new bots, 50/s for others. New bots may send links only to their owner. Limits are explicit: 429 with retry_after.

Groups and channels

A group or channel admin adds the bot — from the bot's profile or by a link. The bot's author can forbid this with /setjoingroups in @botnest or in My bots; that doesn't remove the bot from where it already is. In a group the bot is an ordinary member (everyone sees it, older app versions too); in a channel a bot is always an admin.

A bot is a program run by another person: everything the bot sees in a group goes to its author.

Mini apps

A mini app is your website opened inside Falcon Black: as a sheet over the chat, full height, or full screen for games. People open it from the bot's profile, a button under a message, the button next to the input field, a keyboard cell or a link. On first launch the person allows the site to get their name and language; the site doesn't get their username, phone number, contacts or chats. Falcon Black always shows the app name and the site address on top; the page can't cover them.

<script src="https://falconblack.org/sdk/mini-app-1.0.0.js" integrity="sha384-iPxcV8KtBhjKcb6hdeKa5FnZRrlW+kJU+AYoR4BKChP9HgNjye3Giaz2muZbzZ8B" crossorigin="anonymous"></script>

initData

Send WebApp.initData to your server in the request body (never in a URL) and validate it there. Fields: user (JSON: id — the person's number for your bot, first_name, language_code if known, allows_write_to_pm), auth_date, query_id, chat_type and chat_instance (only when opened from the chat with the bot), start_param, falcon_app (short name), signature, hash. Never a username, photo, phone number, real id or chat titles.

Ready-made validators: validate-initdata.mjs (Node.js 18+), validate-initdata.py (Python 3.9+); test vectors: initdata-vectors.json.

import { validateInitData, fetchPublicKeys } from "./validate-initdata.mjs";

const keys = await fetchPublicKeys();      // /sdk/keys.json: держите в памяти, обновляйте раз в час
// initData — из ТЕЛА запроса страницы, не из адреса
const r = validateInitData(initData, { botToken: process.env.FB_BOT_TOKEN, publicKeys: keys });
if (!r.ok) { res.writeHead(401).end(); return; }
const person = r.data.user;                 // {id, first_name, language_code?, allows_write_to_pm}

web_app buttons, menu button, data

{"chat_id": 4815162342108, "text": "Каталог открыт",
 "reply_markup": {"inline_keyboard": [[{"text": "Открыть магазин", "web_app": {"url": "https://shop.example.com/item/5"}}]]}}

{"chat_id": 4815162342108, "text": "Что посмотрим?",
 "reply_markup": {"keyboard": [[{"text": "Каталог", "web_app": {"url": "https://shop.example.com/catalog"}}, "Помощь"]],
                  "resize_keyboard": true}}

Rules for mini app authors

  1. Don't imitate the Falcon Black interface; never ask for a Falcon Black password or login code.
  2. Don't sell digital goods or services to iPhone users (App Store rule 3.1.1).
  3. A privacy policy is required; delete a person's data on request; state the age rating honestly.
  4. Every app can be reported from inside it. Break the rules — the app is turned off; again — the bot too.

Rules for bot authors

  1. A bot does not pretend to be Falcon Black, another person or organization.
  2. A bot does not send ads or write to people who didn't ask.
  3. A bot does not trick people into giving passwords, login codes, card details or money.
  4. What people write to a bot belongs to them. Keep it safe, don't show it to anyone, delete it on request.
  5. The key is like a password. We never ask for it.
  6. The owner is responsible for the bot. Break the rules — the bot is disabled; again — no more bots.

In full — falconblack.org/bots/rules.

A bot's privacy policy: add its address in My bots → bot → Bot settings; without it, the standard Falcon Black privacy policy for bots applies.