– Пользователь на шаге N?
– Сообщение содержит изображение и смайлик?
– Текст подходит под регулярное выражению «I am [a-zA-Z]+»?
– Время получения раньше/позже заданного?
– Это было нажатие на клавиатуре/обычное сообщение/inline-кнопка?
Большинство из этих правил могут быть в зависимости друг от друга, но об этом чуть позже.
Для начала, о чем я хочу рассказать:
- Об идее проекта – зачем я это сделал, какие есть аналоги, но почему мне они не нравятся.
- Архитектурных решениях, какие возникли трудности, как были решены.
- Что получилось в итоге и стоило ли оно того, дальнейшее развитие.
Цель проекта
Это было в конце ноября, я понимал что скоро новый год и необходимо дарить подарки.
Идея с Telegram Bot–ом, в качестве карты поиска, казалась крайней простой и в должном исполнении интересной. Единственное что необходимо было сделать – взять и загуглить. Что собственно я и сделал. Основной посыл запроса – платформа для создания квестов, или же просто чат-бот c дополнительной логикой, написанный на python (желательно Django Framework)
Большинство рассмотренных приложений либо имели захордкоженные данные, либо же были своего рода викторинами. Ни то ни другое не подходило.
Интересными вариантами показались: Django Telegram Bot, PermaBot.
На первом репозитории долго задерживаться нет смысла, так как в описании недвусмысленно сказано «Try Permabots: more stable django app for bots.». Потому выбираем второй.
Стоит отметить что данная платформа сделана достаточно качественно, хоть и без соблюдения pep8 (не камень в огород, но это же де-факто стандарт :) ), однако с комментариями, тестами, а главное документацией по REST API.
При наличии всех этих плюсов были обнаружены и существенные недочеты:
- старая версия telegram API (версия 4.2.0 против 9.0.0, последний коммит Июнь 2016)
- полное отсутствие поддержки media (здесь моя идея с изображениями накрылась медным тазом)
- некоторые отсутствующие фичи (отложенная отправка уведомлений с настройками возможного ответа, гибкая настройка клавиатуры чата).
Как итог, был начат проект со следующим стеком:
Веб фреймворк – Django, для асинхронных задач – Celery, брокер сообщений –
Redis, база данных – SQLite. В дальнейшем есть возможность переехать на Postgres/MySQL, но пока тащить всю эту махину с собой бессмысленно.
Что стоит отметить, для разработки и запуска проекта нужен внешний https-адрес, на который будут прилетать веб-хуки. Для этого можно использовать туннелирование на публичный домен посредством Ngrok или LocalTunnel.
Я использовал localtunnel локально и на сервере в связке c nginx и let's encrypt сертификатами.
Архитектура
Сущности:
Bot – содержит общую информацию и token (аналог telegram.Bot в оф. документации)
Quest – модель квеста, все просто: имя, описание, бот. вынесено в отдельную сущность для логического разграничения
Step – возможные состояния переходов. если имеет флаг is_init=True, то будет выбрано в качестве начального при инициализации квеста
Handler – выступает в роли dispatcher-a, имеет логическое выражение, которое может выполняться либо нет, в зависимости от этого переводит пользователя на определенный шаг и отправляет соответствующие ответы (представлены моделью Response)
Condition – содержит возможные правила для какого–то определенного поля.
Update – объект, который прилетает с веб – хуком и содержит всю информацию
CallbackQuery – используется для inline – сообщений, отправлять таковые возможности пока нет (основной упор был на базовую клавиатуру), однако варианты расширения учитывались.
Reponse – хранит информацию о тексте сообщения, клавиатуре, ее поведении (скрыть, удалить, установить по умолчанию, показать дефолтную)
Message – полученный объект сообщения (telegram.Message)
Chat – хранит информацию о чате (telegram.Chat)
Photo – модель хранящая информацию о изображении (telegram.PhotoSize)
Event – отвечает за отправку сообщений в определенный чат, и имеет возможность одновременной установки состояния (шага) пользователя. все дополнительные данные (текст, клавиатура, настройки к ней) берет из заданной модели Reponse
User – модель пользователя унаследовал от contrib.auth.models.AbstractUser c указанием device_uid, step и еще некоторых дополнительных полей
Полную UML диаграмму можно просмотреть в draw.io, файл db-diagram.xml
Как мне кажется, один из любопытных аспектов это то, как происходит решение о выборе того или иного обработчика.
В поле «Mathematics expression» логическое выражение можно задавать двумя способами. Первый – тот что указан в примере, в этом случае условия к данному обработчику будут подставлены в порядке их добавления, во втором варианте – можно указать id условий.
Затем выполняется парсинг. Он проходит два этапа, вначале делается преобразование в булевое отображение, где 1 и 0 – истинность конкретного условия, и затем происходит разбор грамматики с использованием библиотеки pyparsing.
Другая сложность с которой пришлось столкнуться – выбор и указание клавиатуры. Было решено сделать одну по умолчанию и другую предоставлять в ответе. Таким образом появляется возможность задать дефолтную на весь чат, которая может выглядеть примерно так [[«Задать вопрос»], [«Попросить подсказку»]]
Также можно увидеть что telegram api позволяет скрывать текущую клавиатуру при нажатии на кнопку или же удалять ее совсем (в этом случае отправляется дефолтная).
Еще одна вещь связанная с клавиатурой – это попытка узнать что же это было, то ли обычное сообщение написанное пользователем или же нажатие на кнопку. Для чего это нужно?
Предположим, на каком-то шаге мы решили запросить текстовый ответ у пользователя и в то же время там отображается стандартная клавиатура с кнопкой «Поменять задание». Несмотря на то что обработчики (Handlers) содержат опциональные поля Step on success и Step on error, гораздо проще организовать логику ограничив тип действия на который они реагируют. Для этого в модели Chat содержится информация еще и о текущей клавиатуре, по ней определяется нажатие это или другой тип события. Это полезно когда необходимо узнать что творится в каком-то определенном чате.
Помимо этого, из фич, которые показались нужными – это функция redirect message. В каждом обработчике можно указать кому перенаправлять сообщение при вынесении вердикта истинности. Пригождается когда нужно проследить специфические моменты (тот же вопрос администратору) или прохождение некоторых этапов пользователем.
Стоило оно того?
Определенно. Тестирование заняло пару вечеров, после чего я настроил и запустил это на сервере. Квест состоял из 10+ состояний и включал в себя выбор вариантов ответа, рассказ истории (суть была в поиске ключевых слов), выполнение задания по карте местности, отправки фотографии года и еще некоторые.
Пожалуй, реакция человека, для которого я это делал, окупила те вечера перед зачетной неделей, которые я потратил.
Перспективы
На данный момент готов рабочий вариант, который разворачивается в docker-контейнере за пару минут, но для полноценной разработки необходимы тесты, продуманное окружение и еще некоторые настройки. Если кому-то проект покажется интересным, то я с желанием продолжу поддержку/разработку, реализация фронтенда под вопросом, но возможна (на emberJS или vueJS).
Спасибо вам за внимание!
Буду рад услышать вопросы, пожелания, критику
Комментарии (6)
Zverik
26.01.2018 16:56+1Насколько знаю, сложные квесты проще писать в TADS или Inform. Первый даже прикручивали к телеграму: github.com/ykrivopalov/ifictionbot
mikhailpavlov Автор
26.01.2018 17:18Спасибо за наводку на TADS, логично что можно сделать обертку для IF и избежать проблем с прописыванием логики где-то на стороне.
roginvs
26.01.2018 18:12Если интересно, то вы можете подключить в проект квесты из Космических рейнжеров github.com/roginvs/space-rangers-quest — src/lib/qmreader.ts и src/lib/qmplayer.ts дают достаточно внятный интерфейс, так что должно быть несложно их приспособить
jehy
26.01.2018 19:14Делал подобную штуку на node.js+mysql. Сама обвязка с телеграммом элементарная, а вот как сделать красивый интерфейс, которым сможет пользоваться сторителлер для создания сложных квестов — это проблема, которую у меня сходу не удалось решить. Хочется, чтобы была не простая логика с условиями и ветками, а скорее описание объектов мира, с которыми можно взаимодействовать. И там редактор объектов, сцен, их состояний и так далее. Что-то проще делать не хочется, а на такой степени сложности уже не хватает свободного времени.
mikhailpavlov Автор
26.01.2018 19:30Согласен, даже в случае когда идет переплетение логических условий — уже не просто продумать интерфейс чтобы показать зависимости. Сделать это в django админке почти нереально, полная кастомизация, разумней сразу писать отдельный UI.
k12th
Поделюсь своим ботом для текстовых квестов типа Lifeline в TG: https://github.com/hogart/deathline
Написан на TypeScript, требует nodejs >= 8.x. Игры пишутся в JSON, есть импортер из CSV. Тестовый бот тут: https://t.me/usidorTest2Bot