Как нужно и не нужно писать чат ботов на примере моего бота для игры в «Тайный Санта»

в 21:40, , рубрики: botman, laravel, php, Чат-боты, чат-боты в телеграм

image

Предыстория

Год назад решил я создать телеграм бот для того, чтобы поиграть в достаточно популярную новогоднюю игру «Тайный Санта». Вдохновился я тем, что пару лет назад мы на работе компанией решили сыграть в эту игру (это показалось очень круто), и плюс я давно следил за клубом АДМ на Хабре. В октябре-ноябре прошлого года, я понял, что нужно сыграть между своей же компанией в этом году снова, но в этот раз не вытягивая имена написанные на листочке с шапки Деда Мороза, а более технологично, что ли. Поскольку все сидели в телеграме и мне было очень интересно написать туда бота, я решил это сделать именно на этой платформе

Кстати, год назад я уже писал об этом проекте статью на Хабре, но я ничего не говорил о реализации. Не говорил не зря, потому что было стыдно, что ли :) Проект готовился только для компании на работе (человек 15-20 на максималках) а вышло так, что проект «стрельнул» в других кругах после пары статей на ресурсах. Далее, более популярные ресурсы сами начали меня рекламировать (я об этом даже не знал, до того как пошел из ниоткуда большой приток людей)
За месяц без единого вложения в рекламу я собрал 5000+ довольных игроков и очень сильно полюбил этот проект. Но помимо всех его бенефитов, в нем был один огромнейший минус, и это реализация.

Как это было год назад

Было забавно, что у меня была кнопка в боте «присоедениться к комнате». Да, именно присоедЕниться. Мне писали пофиксить этот грамматический баг, но я не рисковал, и вот почему :) Далее, прикладываю кусок кода из прошлогодней версии бота.

elseif ($user['state'] == 7) {
        if (mb_stripos($textMessage, 'присоедениться') !== false) {
            if (!empty($user['santa_for_user_id'])) {
                $text = 'Нельзя присоедениться к комнате, если у тебя уже есть Санта';
            } else {
                $text = "Отлично! Если у Вас уже есть комната - напиши мне ее номер сюда, и ты присоеденишься к этой комнате";
                $db->updateState($userId, 8);
            }
        }

Весь бот — это был один огромнейший index.php файлик, который жил за счет функции mb_stripos, по сути. При чем было очень много одинаковых «ифчиков». Т.е. mb_stripos($textMessage, 'присоедениться') !== false мог встретиться не единажды. Если ты поменяешь в кнопке слово «присоедениться» на «присоединиться», и забудешь поменять какой то ифчик (которых, опять же, много) всё может посыпаться. При чем это может быть не сразу заметно (просто бот на определенных сценариях будет не отвечать как надо). Разок я поменял текст, начали писать юзеры, что на определенном сценарии бот отвечает не как надо. Дальше я рисковать не хотел, и подумал, что ошибка не такая уж и критичная :) В принципе, вы поняли. Если была кнопка, например «Найти случайного Санту», я зацеплялся на слово «случайного» через mb_stripos. Весело было, когда появлялась подобная кнопка, с подобным текстом, и когда не надо оно все попадало в не нужный if (если например и там и там есть слово «случайного») :)
Кстати, заметили $user['state']? На то время я ввёл «состояния», чтобы понимать, на каком состоянии сейчас находится пользователь. Захотел ли он присоединиться к комнате, например, или создать, или может он захотел в одиночную игру поиграть? И для каждого стейта свой набор ифчиков шел, который важно было также не сломать.
Крон файлик, кстати, лежал рядом с index.php, его можно было запустить на прямую из под браузера (видимо тогда меня это не особо волновало). Далее, когда вдруг хотелось добавить какой-то «стейт» (лучше бы я этого не хотел) мне приходилось окунаться в это г.., и с первой же попытки ничего конечно же не получалось. Все это еще и лежало на самом дешевом хостинге за 1$ в месяц, который мог меня послать куда подальше, когда начало писать в «час пик» достаточно большое количество человек.

Это был конечно сущий ад для программиста :)

Что я решил делать в этом году
В этом году я понятное дело решил переписать бота (раз был немалый спрос в прошлом году), захотел зайти в старый код и разобраться, как это было в том году, чтобы перенести бизнес-логику. К сожалению у меня не получилось на 70% даже разобраться в старом коде, даже при том, что тогда я пытался оставлять себе комментарии в коде, чтобы помочь себе через год :)

Решил я просто вспоминать основные сценарии, а там и добавлять что-то новое по ходу дела. Начал с вопроса: «а что же использовать для написания архитектуры так, чтобы потом не плакать?». После долгих исследований — выбор пал на Botman. У нас есть небольшие статьи на Хабре о нем. Если коротко, то Botman это очень классная вещь. Его можно поставить как на «чистую», так и сразу установить его сборку вместе с Laravel (да, есть сразу ботман установленный сверху Laravel). Я решил и остановиться на этой версии, поскольку Laravel это явно лучше, чем то, что было год назад :) У него есть возможность кэширования из «коробки», удобный роутинг, artisan, миддлвары, удобность возможность работать с БД и прочие бенефиты. Если вдруг вам не нравится Laravel, Вы можете использовать любой другой фреймворк, и сверху него установить Botman, либо же не использовать фреймворк вовсе. Кстати, Botman построен поверх ReactPHP, что как бы круто :)

Далее, я буду описывать бенефиты Botman:
Есть единый файлик botman.php, в котором Вы можете описать все команды. Пример:

$botman->hears('/start', function (BotMan $bot) {
    $bot->startConversation(new StartConversation());
})->stopsConversation();

При написании команды /start, запустится StartConversation (который должен наследоваться от абстрактного класса Conversation) и реализовывать метод run().
Вопросы задаются достаточно удобно, пример:

$question = Question::create("Ты хочешь создать комнату, или присоединиться в существующую?")->addButtons([Button::create('Создать')->value('create'), Button::create('Присоединиться')->value('join')]);
$this->ask($question, function (Answer $answer) {
            if ($answer->isInteractiveMessageReply()) {
            if ($answer->getValue() == 'join') {

Заметили, что у Button мы можем задать value, и в последствии цепляться потом на него? То есть, на ваших глазах баг с «присоедЕниться» пофикшен, за счет того, что я цепляюсь на value() :) Кстати, еще можно юзать isInteractiveMessageReply метод, который ответит на вопрос, написали ли вам текст, или нажали интерактивную кнопку, при ответе на вопрос заданный юзеру.
Ботман помог избавиться от стейтов, я могу в методе ask сделать еще один метод ask, например если человек нажал «присоединиться», я просто делаю еще один ask внутри этого if.

Вот еще несколько методов (из очень большого количества), которые предоставляет ботман, которые можно легко понять из названия:

$this->repeat($question);

$this->bot->typesAndWaits($secondsToWait);

$this->bot->reply($reply);

Киллер же фичей ботмана является то, что один код может запускаться на многих платформах. То есть, Вы можете написать свой код, изначально запустить его только для Telegram. Потом, решить, что Вы хотите еще перейти в Facebook Manager, и вам совсем не надо начинать разбираться с SDK Facebook, разработчики Botman уже это сделали за вас. Просто нужно будет установить драйвер и задать API Token вашего Facebook Messenger бота в .env. Весь функционал автоматически заработает в фейсбук мессенджере.
Botman поддерживает не только Facebook Messenger и Telegram, в этот список входят также Slack, Skype, WeChat (с полным списком можно ознакомиться у них на сайте).

Также «виновник торжества» славится тем, что с ним уже идет папочка tests/Botman (можете писать юнит тесты, ваш кэп), а также хорошей документацией. Все бенефиты тяжело назвать, поскольку я явно не со всеми поработал, не все помню, но думаю того, что я описал, должно уже хватить, чтобы им как минимум заинтересоваться :)

Ну ок, а хостить опять на хостинге за $1 будем?

Не, в этом году всё серьезно. Хостинг за $10 в месяц и бесплатным доменом с ссл. Шучу :)
Решил подтянуть знания по докеру, купил VPS на DigitalOcean, и развернул проект в докере. Вышло достаточно неплохо, при том что я это всё делал чуть ли не в первый раз. На удивление докер ни разу не падал
С VPS конечно круче :)
При докере было сильно удобнее вести разработку (версионность на деве и на проде сохранилась)

Самое забавное, что когда я вводил платный функционал в бота, мне нужно было получить апрув от платежной системы. Платежная система постоянно возвращала мне мою заявку на апрув, и говорила «сайт не работает». У меня работал, у друзей работал (мы из Украины), а вот у ребят из РФ не работал. Не долго думая, я увидел, что Роскомнадзор еще год назад забанил айпишник моего дроплета (очень много серверов DigitalOcean пострадало на то время от руки РКН). С этим потом тоже порешали.

На чем написан же все таки твой бот?

  • PHP 7.3
  • Laravel
  • Botman

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

Что хоть нового в боте появилось?

Санта научился звонить

У Санты можно заказать звонок! Он даже будет Вас понимать и слушать :)
Санта звонит на указанный номер (с номеров USA), задает вопросы, например «как ты себя вёл в году?», «что хочешь на Новый Год?», «Стишок знаешь?» и т.д. Если пользователь говорит, что стишок не знает, то Санта пойдет по другому сценарию вопросов, если скажет, что знает, то Санта любезно попросит рассказать стишок :) Еще: когда человек говорит свой список желаний на Новый Год Санте, Санта слушает, и присылает потом этот список желаний пользователю, который заказал звонок (вдруг, ребёнок закрылся от родителей в комнате, а им как-то надо узнать что же он попросил у Санты?). Также Санта присылает саму аудиозапись звонка с Сантой на память :)

Теперь можно узнать кто твой Санта

Зрада? Это же противоречит названию игры «Тайный Санта», разве нет? В принципе — да. НО в прошлом году от количества желающих узнать своего Санту мои ЛС разрывались. «Мне будет дарить подарок босс?», «Нам одной девочке не подарил подарок кто-то, можете сказать, кто ей должен был дарить?» ну и всё в таком роде. Сейчас есть такая возможность, но что бы было не повадно — такое удовольствие выйдет в $5.99 :)

Выводы

Не стоит надеяться на то, что ваш проект всегда будет маленьким. Не нужно создавать один index.php с кучкой ифов, даже если проект начинается с всего лишь пары тройки ифов (я достаточно таких проектов знаю)). Лучше сделать сразу хорошо. Даже если вы потратите на это больше времени, это будет давать вам бенефиты в том, что когда в проект срочно нужно будет изменять/добавлять логику, не пришлось думать как менять ифы так чтобы бот не упал, как это вышло у меня :). Также этот подход (с ифами) учит вас делать все другие решения костыльно (не лучший навык, согласитесь). Ну и конечно подумайте о себе, вам же придется по позже зайти в этот код, и придется с ним разбираться, и тогда будет очень не сладко :(

Всем хорошего кода, написания своих чат-ботов, а также пишите свои проекты. Это кайфово!

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

Если захотелось поиграть — велкам :)

Автор: driverx18

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js