Сейчас телеграм боты крайне популярны, вкратце что они из себя представляют: чтобы создать своего бота нужно получить токен у @BotFather, а потом используя его обращаться в HTTP API для получения обновлений (Update)

Есть два способа получения апдейтов:

  • getUpdates: обновления пачками приходят через механизм long poll

  • setWebhook: обновления по одному приходят на какой-то ваш адрес в виде http запроса из телеграма

Теоретически, setWebhook должен быть эффективнее, но на практике long справляется не хуже и не создаёт лишних сложностей в реализации и запуске бота.

Когда апдейт получен, бот просто использует апи, например sendMessage, в общем выполняет свою бизнес-логику.

Требования

Казалось бы, если всё так просто и есть спрос, то наверняка уже сотни библиотек для удобного создания ботов?

Вкратце - нет, если вы захотите написать телеграм бота, то вы (были) вынуждены делать это на python. Библиотеки на других языках непопулярны и зачастую не выполняют даже минимальных требований

Кстати, про минимальные требования. Для создания чего-то серьёзного хотелось бы по крайней мере:

  • асинхронное обращение к апи

  • легкое в подключение библиотеки и лёгкость в использовании

  • http2, по причинам схожим с теми, по которым хочется иметь асинхронность (переиспользование соединения)

    Итак, по доброй традиции С++, мы не нашли под свои требования существующих библиотек и потому приступили к написанию своей.

TGBM

в TGBM (telegram-bot motherlib) нужно было создать три главных компонента:

  • генерация api методов и классов на основе документации

  • json - парсинг ответов и сериализация запросов

  • http2 и ssl (телеграм требует ssl соединение для работы)

И так как это С++, то перед тем как приступать к коду нужно решить главную проблему - как будут подключать вашу библиотеку. Конечно, CMake вне конкуренции и он точно будет. Но одного его недостаточно для подключения библиотеки с такими зависимостями как openssl (порой поражаешься, насколько сложной в подключении можно сделать библиотеку из кучки .c файлов) и boost.

Все до этого существующие библиотеки телеграм ботов на С++ требовали установить зависимости вручную.

Почему не vcpkg: эта система сборки похожа больше на шутку. Сидит какое-то количество программистов microsoft и вручную добавляет все библиотеки, потом вручную их обновляет, никакой расширяемости. Этот "пакетный менеджер" не умеет в версии библиотек. В него нельзя добавить свою библиотеку. Всё через какие-то странные костыли. И главное, последние несколько лет эта штука даже не развивается.

Они владеют github, у них целая операционная систем и они выпускают вот такие релизы:

Почему не conan: не просто так существует conan2. Очень сложная в использовании система, которая по моему личному мнению проиграет конкуренцию, так что уже сейчас использовать её не нужно. В конце концов, не для того мы пишем на С++ чтобы писать билд скрипты на питоне

Так что мы п��одолжили поиски пакетного менеджера. Оказалось, что решение есть, но оно (пока) не так популярно

CPM

CPM (cmake package manager). Этот пакетный менеджер как можно понять из названия использует cmake и основан на механизме cmake fetch content. При использовании CPM подключение библиотеки с любыми сложными зависимостями (openssl, boost, HPACK) выглядит просто:

CPMAddPackage(
  NAME TGBM
  GIT_REPOSITORY https://github.com/bot-motherlib/TGBM
  GIT_TAG        v1.0.1
  OPTIONS "TGBM_ENABLE_EXAMPLES ON"
)

target_link_libraries(MyTargetName tgbmlib)

Думаю, для многих С++ программистов станет открытием, что не обязательно круглосуточно страдать при подключении зависимостей

И только теперь, после решения главной проблемы С++ можно приступать к коду.

Генерация апи

Первой неожиданностью стало то, что телеграм не предоставляет какой-то формальной схемы своего апи. Есть по сути только человекочитаемый текст, из-за чего парсить его и генерировать что-то на его основе не просто мука, а минное поле, учитывая что меняется апи примерно раз в 2 недели.

Так или иначе, с этим можно побороться, всего пару недель парсинга html глазами для выявления закономерностей =)

После этого ещё нужно узнать то, что в документации не написано: какой формат ответа у телеграма, например он не просто присылает status + body, вместо этого там в зависимости от запроса и вероятно расположения духа того кто в тот день это писал может быть
{ "ok" : true, result: "то что ты действительно хотел получить", "description": "", "error_code" }, а иногда оно вместо json может вообще в ответ прислать html, в общем там много мест для исследования методом проб, ошибок и мечтаний о том чтобы тг разраб это написал в документации.

После того как закономерности выявлены, пайплайн генерации таков:

  • html документация телеграма

  • скрипт, который создаёт файлы на С++ с структурами и функциями

  • С++ рефлексия (boost pfr) и несколько шаблонов генерирующие из С++ структур json парсинг и сериализацию запросов

Дальше остаётся "всего лишь" сформировать корректный json, отправить его по сети, получить обратно и распарсить.

Здесь достаточно упомянуть то что вышло в итоге: json парсится потоково, насколько это возможно эффективно, внутри используется boost json. Сериализация возложена на rapid json.

Насчёт http2... В С++ огромное множество библиотек на любую ситуацию. Именно из-за этого наивного мифа в итоге для http2 есть только nghttp2, после взгляда на которую было решено, что легче будет написать с нуля. Ну и написали реализацию http2 с нуля.

echobot

наконец можно посмотреть на какого-то работающего бота. По традиции это будет echobot, который отправляет в ответ то, что ему написал пользователь:

#include <tgbm/bot.hpp>

dd::task<void> main_task(tgbm::bot& bot);

int main() {
  tgbm::bot bot{/* YOUR BOT TOKEN */};

  main_task(bot).start_and_detach();
  bot.run();

  return 0;
}

dd::task<void> main_task(tgbm::bot& bot) {
  using namespace tgbm::api;

  auto updates = bot.updates();
  while (Update* u = co_await updates.next()) {
    Message* m = u->get_message();
    if (!m || !m->text)
      continue;
    bot.api.sendMessage({.chat_id = m->chat->id, .text = *m->text})
           .start_and_detach();
  }
}

Первое что бросается в глаза - то что это даже проще в коде, чем аналогичные боты на питоне.

Разберём по строкам то что тут происходит:

  • создаём бота, передавая токен от BotFather

  • создаём цикл обработки апдейтов (тут он назван main_task) и запускаем его не блокируясь (start_and_detach)

  • и запускаем бота (bot.run())

bot.updates() возвращает асинхронный генератор апдейтов, из которого мы их получаем по одному (за этим скрыт один из способов получения апдейтов, long-poll или webHooks). Нам показалась эта схема наиболее гибкой и понятной.

В данном случае, мы просто отправляем в ответ sendMessage с тем же текстом, что прислал юзер (если это вообще был текст), при этом не блокируем ни корутину, ни поток, чтобы тут же начать обрабатывать следующий апдейт (.start_and_detach).

В целом, телеграм апи полностью повторяется в bot.api с такими же именами, причём везде использую��ся структуры для имитации именованных аргументов. В будущем планируется добавить билдеры запросов, но это лишь микрооптимизация.

Команды

Команды это важная часть телеграм бота, каждый бот (по негласной конвенции) должен поддерживать команду /start и вот как добавление команд выглядит в tgbm (это тот же самый бот, но с командой send_cat отправляющей фото кота).


#include <tgbm/bot.hpp>

dd::task<void> main_task(tgbm::bot& bot);

int main() {
  tgbm::bot bot{/* YOUR BOT TOKEN */};

  bot.commands.add("send_cat", [&bot](tgbm::api::Message msg) {
    bot.api.sendPhoto({
            .chat_id = msg.chat->id,
            .photo = tgbm::api::InputFile::from_file("path/to/cat", "image/jpeg"),
        })
        .start_and_detach();
  });

  main_task(bot).start_and_detach();
  bot.run();

  return 0;
}

dd::task<void> main_task(tgbm::bot& bot) {
  using namespace tgbm::api;

  auto updates = bot.updates();
  while (Update* u = co_await updates.next()) {
    Message* m = u->get_message();
    if (!m || !m->text)
      continue;
    bot.api.sendMessage({.chat_id = m->chat->id, .text = *m->text})
           .start_and_detach();
  }
}



Теперь неплохо бы поговорить о том, что в действительности происходит внутри. Мы старались обойтись без неявностей, так как важно понимать что твой код делает.

Во время обработки цикла (co_await updates.next()) если апдейт был командой (сообщением /send_cat), то вместо того чтобы разбудить корутину ожидающую Update, вызывается обработчик команды.

Внутри запросов api тоже всё прозрачно: формируется json запрос, в зависимости от содержимого запроса либо application/json либо multipart data по требованиям телеграма, кодируется в http2 + ssl, отправляется по сети, потом когда-то асинхронно читается и возвращает управление в этот цикл. Всё это (в данном случае) в одном потоке, никаких скрытых тредпулов.

Вот и всё, наконец-то на С++ можно просто взять и написать телеграм бота, пользуйтесь ) https://github.com/bot-motherlib/TGBM