Прасадный бот
Так вышло, что по понедельникам помогаю готовить прасад (обед) в вайшнавском храме. Обычно заранее неизвестно, сколько человек придёт (может, пять, а может, и пятнадцать), так что готовить приходится с большим запасом (и пока все не придут, непонятно, какого помещения будет достаточно). После пары случаев, расстроивших товарищей по кухне, пришла идея заняться записью на прасад.
Для внехрамовых мероприятий запись для расчёта прасада делается обычно через опрос в ВК, но в данном случае его простота годится не совсем:
- прасад проходит шесть раз в неделю (плюс воскресная программа, которая пока не рассматривается), опросов было бы слишком много и для подготовки, и для поиска нужного (если давать записываться не только на завтра), и для анализа результатов (сколько матаджи и сколько прабху: влияет на выбор помещения; сколько записалось и сколько пришло, чтобы учитывать соотношение);
- нужно давать записываться за несколько человек (за детей, других спутников);
- стоит учитывать случаи такого рода: "Прихожу по понедельникам (и хорошо бы не записываться каждый раз), но в ближайший не приду".
Другой вариант: чат, но ручная обработка сообщений (перенос данных в какую-нибудь таблицу) трудоёмка и неоперативна. Кроме того, поскольку запись, конечно, необязательна, иногда писать могут стесняться, в том числе когда запись нужно отменить или изменить.
Что же может совмещать простоту опроса и гибкость чата? Чат-бот. Несмотря на название, с чат-ботом необязательно взаимодействовать путём ввода текста в свободной форме: текст можно формировать через кнопки меню, это часто проще и в использовании, и в разработке.
В ВК, немного упрощая, чат-бот — это автоматический администратор сообщества, отвечающий на сообщения от пользователей. Поэтому для бота записи на прасад заведено отдельное сообщество, ссылка для написания ему сообщений (по сути ссылка на бота): https://vk.com/im?sel=-213898673.
Разработанный бот позволяет записываться так, как описано выше, и просматривать свои записи и записи на указанный день (записываться можно на несколько дней вперёд).
И для тех, кому интересно, опишу техническую сторону. Бот написан на Python, чат-бота пишу впервые. Хороший список библиотек нашёл на https://github.com/python273/vk_api/issues/356. Попробовал `vk_api`, `vkbottle`, в итоге сложилось с `vk_maria`. Библиотека новая, документации почти нет, но код написан хорошо, можно разбираться по нему. Есть даже встроенная поддержка конечных автоматов (пример использования: https://github.com/lxstvayne/vk_maria/blob/main/examples/finite_state_machine.py), но воспользоваться ей почти не пришлось: логика записи на прасад, конечно, сложнее примера, её нужно тестировать и для этого отвязать от сетевых частей библиотеки, поэтому контекст беседы с пользователем (в т.ч. состояние автомата) храню просто в словаре (а вот сохранение-восстановление контекста при перезагрузке бота сделано удобно).
Дальше выразил логику бота через функцию, отображающую старый контекст, сообщение пользователя и текущее время на новый контекст и ответ бота. И сделал юнит-тесты на разные случаи. Но у этого прямого подхода обнаружилось две проблемы. Поменьше: много дублирования в тестах при задании входных и выходных состояний и при правках строк (не каждую выразить через константу). Проблема побольше: автомат бота не является чистым автоматом ни Мура, ни Мили: есть воздействие и при каждом заходе в состояние (например, приглашение к вводу), и при переходе в другое (обработка ввода, печать результатов). И получается, что каждый вызов указанной функции автомата завершает обработку одного состояния и начинает обработку следующего. Поэтому при обычном ветвлении по текущему состоянию код начала обработки состояния дублируется (причём иногда опирается на исходный контекст, иногда на новый). Кроме того, код вывода кнопок меню оказывается отделён от кода определения того, какая из них нажата, что запутывает дальше.
В ходе поиска решения нашёл способ выражения автомата через сопрограмму (функцию с `x = yield`): https://eli.thegreenplace.net/2009/08/29/co-routines-as-an-alternative-to-state-machines/#using-co-routines-for-framing. Кода действительно намного меньше, но состояние сопрограммы не сохранить (особенно для восстановления после изменения кода) и в целом это больше для автоматов с ациклическим графом переходов (плюс возврат в начальное состояние), для текущей задачи (с разными потенциальными неприятностями почти в каждом состоянии) годится не очень.
В итоге пришёл к такому: каждому состоянию автомата соответствует сопрограмма, которая выдаёт строки приглашения и кнопки меню, считывает сообщение пользователя и текущее время, меняет контекст и выдаёт строки результата. Получается что надо: ответ для пользователя формируется по концу работы старой сопрограммы и началу работы новой, запускаемой сразу. При перезагрузке бота просто запускаем сопрограмму для последнего состояния (начальные строки проигнорируются: их или подобные пользователь уже получил в последнем сообщении от бота).
А в тестировании помогли две вещи: вывод вместо русскоязычных строк команд по их генерации (тестируемых отдельно) и spanshot-тестирование диалогов (на базе доктестов с автоматизированным обновлением вывода посредством библиотеки `pytest-accept` и системы контроля версий). Кроме того, помогло оформление базы данных просто как таблицы с логом контекстов (контексты в конечном состоянии — это и есть записи на прасад).
В общем, приходите на прасад!
За поддержку в работе благодарю Татьяну Швецову, Елену Агапову, Дарью Чиглинцеву и Кришну.