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