- Простой Telegram бот, который задаёт 1 вопрос
- Инструменты
- Как это должно работать в теории
- Для реализации взаимодействия с сервисом Telegram есть несколько API библиотек на языке PHP
- Немного о php-telegram-bot/core
- Немного кода — реализации
- Как это работает на самом деле, и какие есть оправданные сомнения
- Какие ещё были мысли перед реализацией и что остаётся за рамками сейчас
- Ссылки
Простой Telegram бот, который задаёт 1 вопрос
Основная идея бота — это противодействие спам — регистрациям в группе Telegram.
Ниже расскажу о процессе создания Telegram бота который выполняет простую функцию — задаёт новому пользователю группы вопрос и предоставляет возможностью выбрать ответ на него.
Для всех желающих увидеть код сразу и целиком — добро пожаловать в конец статьи ( там есть ссылки)
Инструменты
- PHP 7.4
- Laminas Framework (бывший Zend Framework)
- Библиотека php-telegram-bot/core
- Документация с официального сайта Telegram
- Созданный бот через @BotFather в вашем «месенджере»
- Postman — для тестирования не прибегая к смартфону
- Docker (для локального тестирования)
- Обычный бюджетный Российский хостинг (для боевого тестирования)
Всё началось с того что одна из моих групп доросла до некоторого количество пользователей и стала интересна для различных спам сервисов. Удаление спам сообщений не составляет труда, но постоянно мониторить группы на предмет таких сообщений не самый лучший способ провести время. При этом давно хотел попробовать указанные выше инструменты для реализации хоть сколько полезного бота Telegram.
Идея с тем что бы задавать вопрос пользователю при помощи бота, далеко не новая и успешно была реализована в таких сервисах — ботах как Combot, Terminator (не помню точное название, там ещё мужик похожий на Арнольда в куртке был на логотипе) и другие.
Как это должно работать в теории
- Пользователь (человек, авто-спам, злой бот) вступает в сообщество
- Бот (добрый бот) — администратор сообщества реагирует на каждое новое вступление и задаёт простой вопрос. (Кто ты?). При этом есть только два варианта ответов — «Я человек» и «Я робот». Так же бот забирает все права и разрешения у пользователя в этой группе.
- По сути можно оставить только одну кнопку — «Я человек» и предположить, то что ответить, не ткнув на кнопку пальцем не представляется возможным.
- После нажатия на кнопку с ответом, backend этого бота обрабатывает ответ и принимает дальнейшее решение.
- Если ответ принадлежит пользователю вступившему в группу — и ответ «Я человек», пользователю возвращаются права и разрешения в группе в которую он вступил.
- Бот что-нибудь пишет, например, «добро пожаловать»
Для понимания того что происходит в группе необходимо обрабатывать json-ы которые сервис Telegram будет присылать на URL адрес (webhook) закреплённый за созданным вами ботом.
Наш backend обрабатывает все сообщения приходящие от сервиса Telegram, и по мере необходимости наступления некоторых событий формирует запросы к сервису Telegram. В свою очередь последний — через вашего бота доводит эти команды до группы (где вы можете видеть результат).
Для реализации взаимодействия с сервисом Telegram есть несколько API библиотек на языке PHP
- danog/MadelineProto — Мощная, гибкая, с асинхронными «фичами».
- php-telegram-bot/core — Выбрал её, так как захотелось попробовать что то новое. Как оказалась она достаточно простая для понимания, достаточно пробежаться по ней xdebug-ом
Немного о php-telegram-bot/core
Вся «сила» библиотеки крутится возле одного крупного класса class Longman\TelegramBot\Telegram. При этом есть возможность создавать отдельные обработчики поступающих json-ов через создание файлов-классов команд.
При инициализации объекта класса Longman\TelegramBot\Telegram необходимо указать путь к папке с вашими обработчиками команд, что бы затем в этих обработчиках описать всю необходимую логику реагирования на события происходящие в вашей группе (события обновления группы).
Так же что бы лаконично внедриться в этот механизм взаимодействия с библиотекой, потребовалось расширить базовый класс библиотеки своим собственным, добавить новый метод для встраивания Interop\Container\ContainerInterface от Laminas и организовать фабрику для удовлетворения всех зависимостей.
Немного кода — реализации
Код сильно упрощён (В конце будут ссылки на репозитарий с модулем и отдельно приложением Laminas)
Пример обработчика поступающих команд
getMessage(); // Здесь какие либо действия или обработки return Request::emptyResponse(); > >
В момент поступления сообщения от сервиса Telegram — библиотека вызовет необходимый обработчик из указанной вами папки. Внутри этого обработчика можно получить данные о сообщение, отправителе, получателе, а так же сформировать новое сообщение для отправки — в группу от имени бота.
Так образом, мы можем получить уведомление о новом событие в группе. В данном случае нас интересует событие связанное с вступлением в группу нового пользователя.
Теперь необходимо сформировать сообщение которое будет иметь вопрос и две кнопки с ответом.
Ниже код, формирующий две кнопки. Поле callback_data — содержит значение ответа. В нашем случае это некоторая строка к которой добавлен (методом конкатенации) id вступившего в группу пользователя. Значение id пользователя добавляется в объект при его создании в вызывающем коде.
InlineKeyboard] */ public function getQuestion() < $keyboard = new InlineKeyboard([ ['text' =>'Я бот!', 'callback_data' => QuestionKeyboardMap::CALLBACK_ANSWER_BOT.$this->curentUserId], ['text' => 'Я человек!', 'callback_data' => QuestionKeyboardMap::CALLBACK_ANSWER_HUMAN.$this->curentUserId], ]); return ['reply_markup' => $keyboard]; > public function setCurentUserId(string $curentUserId) < $this->curentUserId = $curentUserId; return $this; > >
Пример обработки события о вступлении нового пользователя в группу. В данном случае используется триггер события framework-а Laminas, что бы выполнить всю логику обработки этого кода в другой части модуля.
telegram */ /** @var \Laminas\EventManager\EventManager $eventManager */ $eventManager = $this->telegram->getServiceManager()->get(EventManager::class); /** @var \Laminas\ServiceManager\ServiceManager $serviceManager */ /** @var \Longman\TelegramBot\Entities\Message $message */ $message = $this->getMessage(); /** @var array $members */ $members = $message->getNewChatMembers(); $eventManager->trigger( Events::NEW_USER_SENT_REQUEST_TO_JOIN_GROUP, null, ['message' => $message,'members' => $members] ); return Request::emptyResponse(); > >
Подписываемся на событие при инициализации модуля и обрабатываем его в методе processRequestToJoinGroup класса Events
getApplication()->getServiceManager()->get(Events::class); $eventManager = $e->getApplication()->getServiceManager()->get(EventManager::class); $eventManager->attach(EventsMap::NEW_USER_SENT_REQUEST_TO_JOIN_GROUP,[$eventsService,'processRequestToJoinGroup']); > >
Теперь необходимо отправить приветственное сообщение новому пользователю и добавить к сообщению наши кнопки с ответами. А так же лишить пользователя всех прав в группе до того момента пока он не предоставит ответ на вопрос.
getParam('message',null); $members = $event->getParam('members',[]); if (!($message instanceof Message)) < return; >/** @var KeybordQuestion $keybord */ $keybord = $this->serviceManager->get(KeybordQuestion::class); // Наша клавиатура $keybord->setCurentUserId((string)$message->getFrom()->getId()); // Вступивший пользователь /** @var TelegramRestrict $restrictionService */ $restrictionService = $this->serviceManager->get(TelegramRestrict::class); $question = $keybord->getQuestion(); try < // Ограничить нового пользователя в правах, до ответа на вопрос $restrictionService->setRestrict($message->getChat()->getId(),$message->getFrom()->getId()); if ($message->botAddedInChat()) < // Если это бот return Request::sendMessage([ 'chat_id' =>$message->getChat()->getId(), 'text' => "Привет бот!", 'disable_notification' => true, ]); > $member_names = []; /** @var UserEntities $member */ foreach ($members as $member) < $member_names[] = $member->tryMention(); > return Request::sendMessage( array_merge([ 'chat_id' => $message->getChat()->getId(), 'text' => 'Привет! ' . implode(', ', $member_names) . '! Скажи, кто ты?', 'disable_notification' => true, ],$question) ); > catch (\Exception $e) < $this->logger->err($e->getMessage(),$e->getTrace()); return; > > >
Дальше, нужно обработать ответ от пользователя, снять ограничения и написать ему что нибудь в ответ, например «Добро пожаловать»
getParam('message',null); $user = $event->getParam('user',null); $callback = $event->getParam('callback', null); if (!($message instanceof Message)) < return; >if (!($user instanceof User)) < return; >if (!($callback instanceof CallbackQuery)) < return; >$approved = false; if ($callback->getData() === QuestionKeyboard::CALLBACK_ANSWER_HUMAN.$user->getId()) < $approved = true; >/** @var TelegramRestrict $restrictionService */ $restrictionService = $this->serviceManager->get(TelegramRestrict::class); try < if ($approved) < $eventManager = $this->serviceManager->get(EventManager::class); $eventManager->trigger( EventsMap::THE_NEW_USER_ANSWERED_CORRECTLY, null, ['user' => $user,'message' => $message] ); // Снимаем ограничения $restrictionService->unsetRestrict($message->getChat()->getId(),$user->getId()); // Удаление сообщения Request::deleteMessage( [ 'chat_id' => $message->getChat()->getId(), 'message_id' => $message->getMessageId(), ] ); // Отправляем сообщение return Request::sendMessage([ 'chat_id' => $message->getChat()->getId(), 'text' => "Добро пожаловать!", 'disable_notification' => true, ]); > > catch (\Exception $e) < $this->logger->err($e->getMessage(),$e->getTrace()); return; > > >
Это метод так же вызывается как триггер на событие созданное при инициализации модуля Laminas. (В рамках статьи этот код здесь не указан)
Так же в этом методе есть триггер успешного прохождения проверки пользователя (здесь пример реализации не приводится) — его можно использовать для записи каких-либо данных о пользователе в базу данных.
Как это работает на самом деле, и какие есть оправданные сомнения
- «Хороший» пользователь вступает в группу, получает вопрос и отвечает «Я человек». У него есть все возможности для общения в группе и за ним больше ни кто не наблюдает и его действия ни как не обрабатываются. Хотя при желании можно и дальше отслеживать, сервис Telegram будет отправлять изменения в группе на webhook.
- «Плохой бот» вступает в группу (именно бот в понятии Telegram), эта «единица» пользователей отмечены специальным полем. Такие по задумке блокируются на «подлёте» без дополнительных вопросов.
- «Плохой» пользователь (чаще всего автоматизированный) — вступает в группу и кидает сообщение с здоровенной картинкой (сейчас модно почему то про Биткоин спамить таким способом). Вот тут возникает предположение, что наш «добрый» бот не успеет сделать всё как надо, потому — что ему потребуется время на отправку команды для лишения пользователя всех прав в группе и отправка команды с приветствием и вопросом. (два запроса, в обоих случаях это вызов Longman\TelegramBot\Request). И с большей долей вероятности «Плохой» пользователь сможет выполнить свой корыстный спам-запрос и нагадить в нашу группу между Request.
- Если групп очень крупная, популярная, тогда есть вероятность что сообщения по линии webhook от сервиса Telegram на наш backend встанут в очередь и часть пользователей не сможет дать ответ на вопрос (тупо запрос повиснет в воздухе) или по достижению 100-а запросов в единицу времени, 101-й будет проходить мимо «доброго» бота. От части можно снизить очередь сообщений об обновлениях в группе, указав при создании webhook-а какие обновления от сервиса Телеграм отправлять (через параметр allowed_updates)
Какие ещё были мысли перед реализацией и что остаётся за рамками сейчас
Для проверки отправителя ответа нужно записывать информацию в базу данных. Т.е с начало сохранить данные о попытке вступления, потом сверить эти данные с теми что придут после ответа на вопрос (если они придут).
Показалось что это слишком сложно, и как указал выше, можно просто «приплюсовать» конкатенировать id пользователя к значению ответа перед формированием кнопок с ответами. Потом сопоставить ответ с тем значением от кого пришёл ответ.
Удалять все сервисные сообщения от бота. Группа может быть просто завален вопросами от бота без ответа. Так как при нормальной логике (после верного ответа), вопрос удаляется. Для очистки нужно подключать cron, а так же вести лог с номерами сервисных сообщений в базе данных.
Что делать если бот не доступен, тогда новые пользователи в чат не попадут без ручного модерирования.
Пользователя после ответа на вопрос можно наделить «избыточными» разрешениями. Но как оказалось во время исследования, наделить пользователя полномочиями выше тех что установлены на группу глобально — не получится.
Это не будет работать. Да, действительно, решение достаточно спорное и пока оно тестируется в весьма «тепличных» условиях.
Ссылки
- Библиотека для запросов а API Telegram php-telegram-bot/core здесь
- Весь код модуля бота для Laminas здесь
- Скелетон приложения Laminas с модулем для развёртывания здесь
Благодарность за тестирование @PabloR
P.S.: Весь код в примерах на PHP, выбрать язык не позволил новый редактор, отправил баг в специальный раздел