Pull to refresh
Kokoc Group
Объединяем агентства и сервисы по всему миру

Как я писал свою библиотеку для работы с Telegram

Level of difficultyMedium
Reading time5 min
Views7.2K

В тот далекий 2019 год, когда про ковид никто не слышал и другой жести ещё не было на горизонте, я читал очередную статью как «правильно» писать очередного бота для Telegram... И в очередной раз у меня крутилась мысль: «а почему ж код так ужасно выглядит?» Где-то в то время Telegram еще был на хайпе, я упражнялся в ботописании, а каждый второй выходец из онлайн-школы программирования пытался объяснить окружающим, как делать своих ботов. Смотря на это все, у меня возник вопрос: а как оно должно выглядеть, чтобы мне понравилось ? Дальше речь пойдет про субъективщину и избавление от фатального недостатка. А я Саша, на тот момент был php разработчиком, сейчас же дожил до лида в Kokoc group и это моя маленькая исповедь в погоне за красотой.

Критерии красоты

На тот момент php 7+ уже шагал по городам и весям и основной посыл от него был такой: типизация — хорошо, не типизация — плохо. Звучит здорово, но в чем это может выражаться ?

До этого у меня был опыт работы с XML + XSD/WSDL. И довольно стандартный путь работы с этим стеком — кодогенерация классов-типов на основе XSD/WSDL. Поверх этих классов накидывается сериализация объект→XML. И получается неплохой способ работы с конечным эндпоинтом: собирать конечный XML не вручную по нодам, а запихивать объект в некоторый метод и этот метод тебе возвращает другой объект — ответ от сервера. В общем, красота.

Если уметь генерировать не только классы-типы, но и набор методов, то можно получить на выходе неплохой RPC, на котором можно вызывать конкретный набор методов. В эти методы втыкать конкретного типа аргументы и получать конкретного типа результат. А валидацию типов проведет сам php. Из чуть менее очевидных плюсов — это подсказки в IDE. Если, например, phpstorm видит класс объекта, то он может подсказывать что можно вызывать на объекте, а где ошибка и такого метода нет. Это небольшое упрощение во взаимодействии с документацией телеграм: можно не идти в доку, чтобы вспомнить, как правильно пишется метод.

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

Что этот мир предлагал мне ?

Половина статей на Хабре, да и не только, предлагали создать ресурс curl`а, воткнуть в него 15 опций и вызвать curl_exec. Ну, Бог им судья.

На тот момент, в 2019 году, я решил посмотреть популярные пакеты на packagist.org Из самого популярного нашелся и ныне здравствующий longman/telegram-bot. Глядя на него у меня всю дорогу мелькала мысль: «что это и зачем?»

Эта штука рекомендовала использовать MySQL в примерах использования. Почему MySQL-то? Про единственную ответственность тут уже речь не идет. Помимо этого, использование статических методов для вызова методов Telegram мне тоже не понравилось: для меня становится не очевидным, на каком токене будут вызываться методы. Но в мире, где Laravel научил народ глобальной функции app(), такие претензии могут кому-то показаться излишними. Ну, это ж моя субъективщина, значит я буду ее учитывать.

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

Из ныне популярных пакетов есть irazasyed/telegram-bot-sdk. Не помню чтобы видел его тогда. Этот пакет в текущем состоянии выглядит как оптимальный. Наверное, если бы сейчас уже не собрал свой велосипед, то использовал бы этот. Но в нем смущает, что все методы динамические, т.е. нужно сидеть в обнимку с документацией телеграм, чтобы знать какие методы можно вызывать. И методы вызываются с одним аргументом в виде массива. В общем, тоже не идеально, но уже неплохо.

Изобретаю свой велосипед

Было решено делать свой RPC. Нужно было где-то раздобыть свои типы-классы + генерацию сигнатур методов. Но как это сделать ?

Поискал примеры, как это делают другие люди, ничего внятного не нашел. Видел, что у некоторых библиотек есть типы, но откуда они их взяли — непонятно. То ли сами вручную писали, то ли конвертировали из других языков (может из typescript?). Вручную описывать все типы в php я точно не был готов. Блуждая по интернетам, наткнулся на библиотечку GenerateTelegramBotApiSchema, которая конвертировала страницу документации Telegram в типы. О, это уже интересней.

Чем глубже я её читал, тем больше было понятно, что в таком виде мне оно не нравится. Но сама концепция мне зашла. Решается проблема актуализации типов: запустил команду — есть обновленные типы. Самой библиотеки не было в packagist, ее писал только один человек — сам автор. Я попробовал накинуть пул реквест с улучшениями: структурирование кодовой базы, больше типизации при генерации и еще что-то по мелочи. Больше недели висел этот реквест. Стало понятно, что это может затянуться. Ну и ладно, погнали форкать.

Форк, рефактор, изменения, дополнения. В общем, в конце концов получилась приемлемая вещь. Написано не левой ногой, а уже правой. Мне подходит. Эта штука действует в два этапа: сначала из html генерится AST, а вторым этапом из полученного AST генерим классы. Из забавного: классы генерируются не через модный nette/php-generator, а тупо через twig шаблоны. Ну и теперь оно работает и выглядит нормально. Меня устраивает. Еще из наблюдений: документация телеграм вызывала великую печаль из-за своей неконсистентности и многим словоблудием, в котором лежат описания типов. В общем, некоторое количество грусти присутствовало при написании, но этот квест с честью пройден.

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

Впрочем, где-то здесь я родил себе новую проблему. В Telegram есть механика, при которой можно делать запрос к API вместе с ответом на webhook. Т.е. телеграм делает запрос на мой сервер, я в ответ на этот запрос могу отдать ответ, в котором явно будет сказано «а вызови еще вот этот метод с этими аргументами». Хотелось бы уметь в эту механику. Чтобы ее поддерживать, нужно чтобы клиент, на котором вызывается метод, возвращал массив аргументов. А у меня генерируется клиент, в котором возвращаемый тип уже определен. В угоду этого, вместе с типизированным клиентом, генерится клиент, в котором возвращаемый тип mixed.

Теперь, имея на руках кучу типов, хорошо бы уметь превращать то, что приходит от апи телеграм, в нужные объекты. Ранее сталкивался в работе с jms/serializer. Он может маппить данные в объекты. Но тут важно уметь явно разметить типы-классы. Т.е. какой сеттер вызывать, при установке значения в это поле. Генератор я контролирую, поэтому дополнительная разметка не была проблемой.

А дальше что ? Да, в общем, почти все сделано. Клиент, который генерится, является абстрактным. Реализацию передачи запросов можно сделать самому на курлах или ещё-какой-нечести, если очень хочется. А можно взять готовую штуку на обычном guzzle.

Примеры обращения к библиотеке

// Клиент, который наследуется от сгенерированного клиента,
// а в сгенеренном клиенте уже описаны все методы
$client = new \MadmagesTelegram\Client\Client('BOT_TOKEN');

// Отправка сообщения, синтаксис под php 8+
// можно и под 7+, но тогда нужно накинуть пачку null`ов
$client->sendMessage(chatId: 0, text: 'Hello world');

// Отправка сообщения + отключение уведомления от него
$client->sendMessage(chatId: 0, text: 'Тихое сообщение', disableNotification: true);

// Это файл изображения
$file = new \MadmagesTelegram\Types\Type\InputFile('/var/photos/some-photo.jpg');

// И этот файл можно отправить как фото.
$sentMessage = $client->sendPhoto($chatId, $file);
// или как документ, в ответ на сообщение выше
$sentMessage = $client->sendDocument($chatId, $file, replyToMessageId: $sentMessage->getMessageId());
// А потом вывести все поля, которые телега вернула в ответ
print_r($sentMessage->_getData());

Вместо итогов

Фатальный недостаток был преодолен, волосы стали более шелковистыми, у меня появилась библиотека для работы с Telegram, которая мне нравится. Но появилась новая проблема: нужно поддерживать эту штуку в рабочем состоянии. Но это уже отдельная история. В следующий раз напишу как поверх этой библиотечки запустил работу заявочной системы в Symfony-приложении на работе и зачем всё это нужно.

Tags:
Hubs:
Total votes 12: ↑11 and ↓1+15
Comments9

Articles

Information

Website
kokocgroup.ru
Registered
Founded
Employees
501–1,000 employees
Location
Россия