Telegram-бот в качестве подарка
Я работаю программистом чуть более шести лет, пишу в основном на Java и 1С, звезд с неба не хватаю, но поставленные задачи выполняю.
Весной 2017 года меня заинтересовало создание ботов для различных мессенджеров. Сначала хорошей идеей казалось создание бота в Viber. В Сибири он наиболее популярен, в нем сидят практически все знакомые, корпоративные чаты ведутся тоже в нем. Кроме того, вдохновляла эта статья. Однако, создание публичного аккаунта оказалось не такой простой задачей – на все запросы приходили немногословные отказы.
Помучившись с Вайбером около недели, я обратил внимание на Телеграм, оказалось, что зарегистрировать бота там очень легко, а жизнь джава-программиста облегчается наличием библиотеки TelegramBots. В качестве пробы пера был написан бот, позволяющий получать некоторые отчеты из корпоративных систем, потом был бот со справочной информацией, а осенью уже для себя я попробовал сделать бота для предварительной записи в салон красоты. Все это были интересные поделки, которые нравились окружающим, а первые два бота даже реально использовались в работе. Благодаря этому, в голове постоянно крутились мысли о том, как еще можно применить ботов в быту и народном хозяйстве.
По работе в качестве программиста на 1С мне много приходится общаться и совместно работать с очень симпатичной коллегой, коллега кроме того, что симпатичная, еще и умна, а временами (когда настроение хорошее) достаточно глубоко вникает в вопросы разработки (а при желании может и запрограммировать что-нибудь) и вообще обладает системным мышлением. А системное мышление – венец женской привлекательности, кто бы спорил. Так вот, со столь привлекательной коллегой мы часто обсуждаем всяческие свистоперделки для 1С (обсуждаем на мою голову, так как потом они попадают в очередные требования к разрабатываемой системе), разные новые технологии и околоайтишные темы. Кстати говоря, своего первого бота, я показывал именно этой коллеге и получил тогда неплохую обратную связь и импульс к изучению темы.
Здесь необходимо заметить, что у коллеги в самом начале декабря намечался день рождения, ну и естественно не поздравить её было нельзя. Достаточно очевидно, что коллеги с системным мышлением в качестве подарков могут рассчитывать не только на конфеты, цветы и прочие ништяки, но и на что-нибудь, претендующее на остроумность. Так в 2016 году в качестве подарка была закастомлена главная страница той системы, которую мы вместе делаем, и в день рождения вместо стандартной заставки коллегу встречало поздравление. Вроде бы мелочь, но в нашей организации никого так не поздравляли и подарок, как говорится, зашел, да так зашел, что коллега посчитала это поздравление самым лучшим в тот день (надеюсь, так и было). После таких достижений было понятно, что в 2017 году надо развить тему и снова дополнить стандартный поздравительный набор чем-нибудь айтишным. В голове смутно бродили мысли о том, что «что-нибудь айтишное» могло бы быть связано с ботами, но четкой идеи не было, и я почти смирился с тем, что ничего оригинального придумать не получится. Время шло, до дня рождения оставалось 5 дней…
Я возвращался с работы домой, пробка текла вяло, и можно было углубиться в свои мысли: планы разработок, технические решения, автоматизация с помощью ботов, предвкушение пятничного пива, в общем, стандартная каша. Вдруг в голове промелькнула мысль, что коллеге все же надо подарить бота, пускай совсем простенького, например, выдающего разные памятные для коллеги фоточки. Мысль показалась совсем несвежей, однако я начал прикидывать, какие фото можно будет использовать. При этом фото было много, а вот обстоятельства, при которых они делались, вспоминались уже с трудом. Тут-то и появилась идея подарочного бота: шуточная викторина, в которой каждый вопрос сопровождался бы фотографиями коллеги и несколькими вариантами ответов, при этом на каждый ответ бот выдавал бы какой-то смешной комментарий. Эту идею я счел с одной стороны достаточно оригинальной, а с другой – осуществимой за оставшееся до дня рождения время.
Подготовка и создание базы вопросовНа выполнение всех работ оставалось всего 4 дня, а если говорить точнее, то 4 вечера, да и то неполных. Под рукой имелись исходники трех других ботов, которые можно использовать как «запчасти», и было понятно, что впереди ждет увлекательное ралли на велосипеде из костылей. В качестве языка программирования была выбрана Java, в качестве библиотеки, для работы с API Telegram – TelegramBots, для хранения базы вопросов использована СУБД H2.
Первой задачей являлось создание базы вопросов. Для этого пришлось провести большую работу по сбору фотографий с телефона, рабочего и домашнего компьютеров и соцсетей. Полученные фотографии были структурированы таким образом, что получилось 26 вопросов, к каждому из которых прилагалось от 2 до 4 фото и 4 варианта ответа. При этом заведомо правильных вариантов ответа не предусматривалось, а ответ на каждый вопрос просто сопровождался комментарием. Хотелось также сделать сохранение истории выбранных вариантов ответа, но в самый последний момент я просто забыл прикрутить эту фичу.
Раскладка фотографий и придумывание вопросов оказались очень трудоемким процессом, и на них ушло полтора вечера.
Далее была реализована база данных, хранящая вопросы. Ниже приведены описание таблиц базы данных и DDL-скрипт.
- CLS_QUEST – таблица, содержащая тексты вопросов
- CLS_QUEST_PHOTO – таблица содержащая относительные пути к фотографиям, которые связаны с задаваемым вопросом; сами фотографии лежат в файловой системе в папках, соответствующих вопросу.
- CLS_ANSWER – таблица, содержащая варианты ответов на вопрос, а также комментарии к каждому варианту ответа
После создания база была наполнена данными вручную, благо в Netbeans, который я использую как среду разработки, достаточно удобный редактор SQL-скриптов.
По истечении двух дней база вопросов и фотографий была готова, времени оставалось совсем немного, пора было переходить к созданию самого бота.
Каркас ботаНапомню, что для создания бота в Telegram необходимо написать @BotFather, пользуясь командой /newbot ввести для бота отображаемое имя и имя пользователя. После выполнения этих действий будет получен токен для доступа к API Telegram. Выглядит это примерно так.
Для красоты добавим фото профиля с помощью /setuserpic.
Теперь перейдем к созданию самого бота с помощью TelegramBots. Напомню, что Telegram позволяет создавать боты работающие с Webhooks и LongPolling-боты. Выбран был второй вариант. Для создания LongPolling-бота, необходимо реализовать собственный класс, наследующий классу org.telegram.telegrambots.bots.TelegramLongPollingBot .
TOKEN – токен для доступа к API Telegram, полученный на этапе регистрации бота. USERNAME – имя бота, полученное на этапе регистрации бота. Метод onUpdateReceived вызывается при поступлении боту «входящих обновлений». В нашем боте нас интересует обработка текстовых команд (если быть честным, то только команды /start) и обработка колбэков (обратных вызовов), возникающих при нажатии на кнопки инлайн-клавиатуры (размещается в области сообщений).
Бот проверяет, является ли входящее обновление текстовым сообщением update.hasMessage() && update.getMessage().hasText() или колбэком update.hasCallbackQuery() , после чего вызывает соответствующие методы для обработки. О содержимом этих методов поговорим немного позже.
Создаваемый бот является обычным консольным приложением и его запуск выглядит следующим образом:
Ничего сложного в инициализации бота нет, однако хочу обратить внимание, что достаточно важно предусмотреть возможность указать боту прокси. В нашем случае настройки прокси хранятся в обычном properties-файле, откуда считываются при старте программы. Также замечу, что в приложении используется собственный нехороший велосипед в виде некоего подобия глобального контекста AppEnv.getContext() . На момент написания бота исправлять это было некогда, но в новых «поделках» удалось изжить этот велосипед и использовать вместо него Google Guice.
Приветственное сообщениеРабота бота естественно начинается с обработки команды /start. Как было написано выше, эта команда обрабатывается методом processCommand . В начале метода объявим смайлы, которые будут использоваться в тексте приветственного сообщения.
Далее производится проверка введенной команды и если это команда /start, то формируется ответное сообщение answerMessage . У сообщения устанавливается текст setText() , включается поддержка некоторых html-тегов setParseMode("HTML") и устанавливается идентификатор чата, в который сообщение будет отправлено setChatId(update.getMessage().getChatId()) . Осталось только добавить кнопку «Начать». Для этого сформируем инлайн-клавиатуру и добавим ее в ответ:
Интересным моментом является установка данных колбэка. Эти данные могут использоваться при обработке нажатия кнопок. В нашем случае в данные колбэка записывается сериализованный в JSON объект. Этот способ тяжеловесен для данной задачи, но позволяет работать с данными возврата без лишних заморочек на преобразование. Данные возврата формируются в специальном билдере ActionBuilder .
Для того, чтобы ActionBuilder мог вернуть JSON ему необходимо передать маршаллер. Здесь и далее при упоминании переменной marshaller подразумевается, что она является объектом класса, реализующего интерфейс DocumentMarshaller .
Маршаллер, который используется в ActionBuilder , реализован с использованием Jackson.
И, наконец, производится отправка сообщения:
В итоге, приветственное сообщение выглядит вот так.
Задаем вопросыОставалось сделать самое интересное — реализовать логику работы бота. Для работы с базой вопросов использовалось JPA. Приведу код классов-сущностей.
Также замечу, что здесь и далее для доступа к данным используется объект, реализующий интерфейс ClassifierRepository , а при упоминании переменной classifierRepository подразумевается, что она является объектом класса, реализующего интерфейс ClassifierRepository
Теперь перейдем к тому моменту, когда нажимается кнопка «Начать!». В этот самый момент бот обрабатывает очередную порцию входящей информации и вызывает ранее упоминавшийся метод processCallbackQuery() . В начале метода обрабатывается входящее обновление, а также извлекаются данные колбэка. На основании данных колбэка определяется, было ли произведено нажатие на кнопку «Начать!» OPEN_MAIN.equals(action.getName() , либо была нажата кнопка ответа на очередной вопрос. GET_ANSWER.equals(action.getName()) .
Если викторина только начата, то необходимо проинициализировать список вопросов и задать первый вопрос.
Сейчас рассмотрим инициализацию списка вопросов initQuests() :
В методе initQuests сначала получим все 26 вопросов, а потом перемешаем в случайном порядке. После этого вопросы положим в QuestEnumeration , откуда будем получать их по одному, до тех пор, пока не будут получены все 26 вопросов. QuestEnumeration добавим в объект специального класса QuestStateHolder , хранящего соответствие пользователя и его текущей сессии вопросов. Код классов QuestStateHolder и QuestEnumeration ниже.
После инициализации задается первый вопрос. Но об этом поговорим чуть позже. А пока рассмотрим ситуацию, когда пришел ответ на уже заданный вопрос и боту необходимо отправить комментарий, относящийся к этому варианту ответа. Здесь все достаточно просто, сначала ищем ответ в базе данных (уникальный идентификатор варианта ответа сохранен в CallbackData кнопки, на которую было произведено нажатие):
Потом готовим на основе найденного ответа сообщение и отправляем его:
Теперь рассмотрим метод sendQuest , который отправляет очередной вопрос. Начинается все с получения очередного вопроса:
Если Enumeration еще содержит элементы, то готовим вопрос к отправке, в противном случае пора выводить сообщение об окончании викторины. Отправляем сам вопрос:
Теперь отправляем фотографии, относящиеся к данному вопросу:
И, наконец, варианты ответа:
Клавиатура с вариантами ответа формируется следующим образом
Вопрос отправлен. Выглядеть это будет примерно так (фотографии немного размыты, чтобы никого не смущать).
В тот момент, когда вопросы закончились, будет сформировано сообщение об окончании викторины:
И в самом конце отправим забавный стикер:
На этом работа бота завершается.
Работа над всей описанной функциональностью была завершена где-то в 11 часов вечера дня, предшествующего дню X. Замечу, что имея в виду некоторые особенности, я понимал, что запустить бота надо ровно в 12 ночи. В связи с этим я испытывал некоторый цейтнот (почему и забыл про историю ответов). Кроме того коллегу необходимо было как-то оповестить об этой викторине. По ряду причин просто скинуть ссылку я не мог, поэтому доверил оповещение другому боту (благо в процессе тестирования идентификатор пользователя был сохранен и бот мог свободно писать). На этом история написания бота заканчивается.
ЗаключениеКаких-то глубоких выводов из этой истории не будет. Сам бот был принят с интересом и понравился, так что старался я не зря. Если переходить к технической части вопроса, то работа над ботом позволила задуматься над тем, что от некоторых костылей и велосипедов пора избавляться, что я и попытался сделать в следующем боте.
Также хочу заметить, что сама идея ботов очень перспективна. Есть множество повседневных задач от заказа пиццы до вызова такси, которые предлагается решать посредством мобильных приложений, сайтов и их мобильных версий или телефонных звонков оператору. С одной стороны все эти способы доказали свою эффективность, удобны и в ближайшее время вряд ли изменятся. С другой стороны приложения для мобильных устройств хотя и богаты функциональностью, но требуют установки, обновления и изучения их интерфейса, а еще имеют свойство кушать батарею. Сайты и их мобильные версии требуют от пользователя как минимум перехода в браузер и работы с новым интерфейсом, что не всегда удобно, особенно на мобильных устройствах. Взаимодействие по телефону удобно для многих, но не подразумевает какой-либо визуализации в принципе, а кроме того оператор всегда будет узким местом системы. При решении тех же задач боты не требуют установки чего-либо кроме мессендежера, не требуют изучения новых интерфейсов и позволяют пользователю работать асинхронно (в отличие от телефонного звонка) в относительно знакомом интерфейсе мессенджера. Мессенджер в таком случае предоставляет некую среду для клиент-серверного взаимодействия, где в роли сервера выступает бот, а клиентская часть реализована средствами мессенджера. Безусловно, при работе с ботами для пользователя также возникают различные трудности, в чем-то схожие с трудностями работы с текстовыми интерфейсами, в чем-то обусловленные ограничениями самих мессенджеров. Но все равно боты, ориентированные на решение небольших повседневных задач (или на развлечения, как бот, описанный в этой статье) представляются перспективными.