Сейчас телеграм боты крайне популярны, вкратце что они из себя представляют: чтобы создать своего бота нужно получить токен у @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

Комментарии (12)


  1. JBFW
    17.01.2025 14:00

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

    Простой бот легко пишется без всяких библиотек и питонов, да вот хотя бы на perl )

    Но на С конечно быстрее.


  1. sabudilovskiy
    17.01.2025 14:00

    Раньше хабр был весь в статьях-нативках телеграм каналов, а теперь телеграм библиотек. Прогресс?


  1. Sazonov
    17.01.2025 14:00

    Зря вы так на vcpkg. Версионность там есть, хотя немного нетривиально реализуется. Свои библиотеки подключать - тоже без проблем. И у меня такое чувство что вы путаете репозиторий с проектами который управляется Майкрософтом, и саму систему vcpkg.


    1. Kelbon Автор
      17.01.2025 14:00

      Как ни крути, оно проигрывает CPM даже если там будет всё легко и будет хорошая документация, хотя бы потому что оно не развивается уже несколько лет


  1. DjUmnik
    17.01.2025 14:00

    Насчёт Conan не понял, чем он так плох? Вполне рабочая штука. Хотя да, они сломали всё при переходе на версию 2.0. Но в целом они спрятали зоопарк разных систем сборок библиотек за одним фасадом. Собственные пакеты создаются элементарно.


    1. Kelbon Автор
      17.01.2025 14:00

      И я и мои знакомые, которые пытались пользоваться conan зачастую не могут написать рецепт к простейшей библиотеке. А эти люди вообще-то С++ смогли выучить. Это говорит о том, что conan слишком неудобен для реальности


  1. Apoheliy
    17.01.2025 14:00

    Ещё добавлю про систему сборки:

    Вы (действительно!) уверены, что при наличии уже установленного OpenSSL версии 3 будет правильным (и безопасным!) скачивать в исходниках версию 1 с репозитория некого Джанбара из Франции, собирать её из исходников, и (подозреваю) установить её в систему как одну из версий криптобиблиотек?

    Про остальные библиотеки тоже так себе решение: подтянуть ещё одну версию буста (причём не всю, только отдельные библиотеки) и др.

    Возможно, это сомнительный путь. Не находите?

    -

    Ответ на "Почему версия 1": Потому что в файле deps.cmake указан тэг 1.1.1w-20231130. Хотя, возможно, для Джанбара это самая новая версия библиотеки.


    1. Kelbon Автор
      17.01.2025 14:00

      Почему версия 1? Даже через буст и шифры в коде явно отключается возможность установить соединение по версии 1 и 2


    1. sabudilovskiy
      17.01.2025 14:00

      С OpenSSL вообще непросто, у них есть версия SSL, версия библиотеки и версия TLS, 1.1.1w-20231130 поддерживает TLS 1.2/TLS 1.3.
      Это не прям последняя версия, но с ней всё нормально


  1. Homyakin
    17.01.2025 14:00

    За старания, конечно, лайк.

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

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


    1. Kelbon Автор
      17.01.2025 14:00

      много библиотек на блокирующих вызовах или с другими фундаментальными проблемами, даже на питоне кажется всего 2 библиотеки, которые стоят рассмотрения (telegram-bot и aiogram)


      1. Homyakin
        17.01.2025 14:00

        на блокирующих вызовах

        Скорее всего мои боты пока не дошли до того, что это является проблемой (да и вряд ли дойдут).

        Сам пишу ботов на Java с использованием TelegramBots и проблем у меня с библиотекой примерно никаких. Всё апи поддерживается, если чего-то мне не хватает, то это косяк апи телеграма, а не библиотеки.

        В общем, интересно узнать, что за требования такие к библиотекам.