Serverless-телеграм-бот с s3 на Python или как я научился играть в пинг-понг

TL; DR

  • Проблемы с настольным теннисом в отдельно взятом коллективе 

  • Telegram-бот, чтобы всем помочь

  • А как это все повторить?

Настольный теннис и почему это важно

Настольный теннис — самый популярный рекреационный вид спорта на сегодняшний день. Минимум экипировки, короткие партии, все «как-то давно не играли». Возможно здесь есть какая-то связь, но во всех 6 командах, где я работал с с 2012-го года, в перерывах, мы с коллегами играли именно в настольный теннис. Иногда стол стоял прямо у нас в офисе, иногда мы выходили к ближайшему уличному столу, иногда даже устраивали небольшие тимбилдинги на арендованных столах.

У меня сформировалось устойчивое мнение, что любовь к настольному теннису — повсеместное явление в этой нашей IT-сфере. Товарищи, кто в юношестве посещали математические и программистские сборы, это только подтверждали. А в какой-то момент наличие стола даже заняло место в списке преимуществ работы «именно у нас» где-то между белой зарплатой и дружным коллективом. И вообще, это индикатор финансовой успешности вашей корпорации. 

Финансовые результаты компании в отношении к расходам на настольный теннис по по мнению WSJ

Финансовые результаты компании в отношении к расходам на настольный теннис по по мнению WSJ

Предыстория 

В 2023 наш небольшой коллектив, что последние пару лет перебивался настольным футболом, переезжает в новый просторный офис, где вдали от рабочих мест, на кухне, нашлось место и для заветного стола. Из 80 офисных сотрудников большая часть находит в себе силы иногда размяться и показать соседнему отделу у кого откуда руки растут. Однако, спустя какое-то время проявились две проблемы:

  1. Стояние у стола. В часы наибольшего ажиотажа, люди, кто очень-очень хотят поиграть прямо сейчас и не очень спешат вернутся на свое рабочее место, начали сбиваться в толпы наблюдающих за играми других. Чтобы точно забрать стол, когда его освободят.

  2. Кто самый крутой в офисе? А номер два? А кто чаще побеждал? А как подобрать противника с соответствующим уровнем?

И если с первой проблемой было совершенно непонятно, что сделать, да и разве это проблема? То со второй коллеги стали активно разбираться. Начали с фиксирования результатов игр на маркерных досках, в блокнотах, а закончили таблицами в google docs с автоматическим подсчетом рейтинга Эло. Все это, конечно же, в нерабочее время, не подумайте, что мы там настолько обнаглели.

Что за рейтинг Эло?

Эдуардо забыл, что разница рейтингов должна быть показателем степени, а не множителем

Эдуардо забыл, что разница рейтингов должна быть показателем степени, а не множителем

Это система рейтингов используемая Международной Шахматной Федерацией с 1970. А еще это тот самый рейтинг, который в «Социальной Сети» сосед Цукерберга рисовал на стекле, чтобы тот использовал его для ранжирования горячести студенток Гарварда еще задолго до того, как Марк стал экстремистом.

Подсчет рейтинга по результатам некоторой игры выглядит так:

Вычисляется ожидаемое количество очков, которое получит игрок A в партии с B:

E_A = \frac 1 {1 + 10^{\frac {R_B-R_A} {400} } }

где:

  • EA — ожидаемое количество очков, которое наберёт игрок A в партии с B

  • RA и RB — рейтинги соответствующих игроков

Как вы понимаете, мы ищем некоторое число от 0 до 1 соответствующее ожидаемости победы при заданной разницы рейтингов. Где чем EA ближе к 0 тем менее мы верим в победу A.

Обновление рейтинга игрока А считается как:

R_A^\prime = R_A + K \cdot (S_A - E_A)

где:

  • K — коэффициент, значение которого равно 10 для сильнейших игроков (рейтинг 2400 и выше), 20 (было 15) — для игроков с рейтингом меньше, чем 2400 и 40 (было 30) — для новых игроков (первые 30 партий с момента получения рейтинга ФИДЕ), а также для игроков до 18 лет, рейтинг которых ниже 2300

  • SA — Фактически набранное игроком А количество очков (1 очко за победу, 0,5 — за ничью и 0 — за поражение)

  • R’A — Новый рейтинг игрока А

Таким образом, чем более ожидаема победа (EA стремится к 1) тем слабее изменится рейтинг А. Также справедливо обратное.

Нам этот рейтинг понравился, так  как:

  • Он удобно пересчитывается на основе матча двух игроков и стремится к некоторому балансу, когда очевидные результаты не приносят изменений рейтинга.

  • Он позволяет учитывать результаты игры по партиям (3–0, 2–1 и т.п.) через изменение значения в SA. Мы установили SA для победы с преимуществом в одну партию как 0.8, в две как 1 и в три как 1.2.

  • Нам не понравился оригинальный теннисный рейтинг RTTF из-за его заточенности на «турнирность» и того, что так сложилось исторически:) Впрочем, если это принципиально, это не так сложно изменить в коде.

Так и что вы предложили?

4c7e485227540872a92755a8dd630f03.jpeg

Я решил, что таблицы с коллективным доступом это очень весело, но я могу быстро написать простого Telegram-бота, чтобы всем стало хорошо. В бота можно было бы присылать результаты игр, а он бы все пересчитывал и публиковал топ.

А так как проект некоммерческий, но интересный, то тратить на работу этой штуки настоящие деньги не очень-то и хотелось. Хорошо, что у нас в 2023 есть serverless вычисления:

  • можно быстро поднять очередного бота по инструкции, коих опубликовано уже довольно много (как и эта статья)

  • привязать обработку запросов к боту к lambda-функции в каком-либо удобном для вас облаке, где вы платите только за выполнение запросов

  • боту желательно быть stateless, так как мы не сможем хранить стейт внутри lambda, а возится с серьезными базами не хотелось бы

  • однако, мы можем создать бакет в s3-сторадже этого же облака, где тарификация происходит на байты и запросы. Так, например, можно хранить статистику игроков

И тут я задумался над решением проблемы №1. Ведь вместе со статистикой игроков можно рядом  хранить состояние текущей очереди. А значит мы получим какую-никакую синхронизацию за наш уникальный ресурс — стол.

Что же вышло

Взаимодействие автора и большинства умных ботов выглядит примерно так

Взаимодействие автора и большинства умных ботов выглядит примерно так

О выборе платформы: я сижу на Яндекс.Облаке, по причине «Синдром Утенка». Однако, и проблем я с ней не наблюдал. Попытка с наскока транслировать всю конструкцию на aws быстрой не вышла. Поэтому решение было вдохновлено этим прекрасным гайдом.

Исходный код бота опубликован тут.

Для работы с s3 я использовал boto3 (put_object, delete_object, get_object, list_objects).

Для работы с апи телеграма telebot, где бот обрабатывает команды из того, что пришло в handler лямбды.

В финале, после выполнения перечисленных ниже инструкций, мы получаем tg-группу, где можно постить trash talk вашим оппонентам, а также где ваш бот позволяет бронировать стол, регистрирует результаты матчей и публикует статистику.

Об игроках в s3-бакете сохраняются только пары tg_handler + рейтинг. Они считываются и обновляются при публикации результатов матчей. Также статистика хранится для пар игроков, кто сыграл друг с другом хотя бы раз.

Ну, а очередь «к столу» реализована просто как объекты от имени создавшего их tg_handler. Кто первее создал, тот и раньше в очереди.

Как это все деплоить?

Когда собрался размотать всех своих коллег

Когда собрался размотать всех своих коллег

Итак, если вы хотите повторить наш опыт, то вот 10 простых шагов для достижения этой цели (ETA ~ 15 минут):

  1. В качестве пререквизитов, у вас есть аккаунт на Яндекс.Облаке. Все имена, которые стоит назвать вам, взяты в <>.

  2. Создаем бота в Telegram. Единственная точка входа для этого — BotFather. Проходим по его инструкциям для создания нового бота . ApiToken, который вернул BotFather, нам пригодится чуть позже. Также можно сразу установить популярные команды будущего бота (для автокомплита) во вкладке Edit Commands:

help - Show help message
start - Show help message
register_me - Register you in the competition
played - Declare game result 
my_rating - Show your rating
rating_of - Show someone’s rating
stats_vs - Show your personal score with someone
top - Show top scorers’ ratings
queue - Current waiting list
book - Add me into waiting list
leave - Leave table and waiting list
clean_queue - Clean queue if something is wrong 
  1. Создаем группу в Telegram, где будем регистрировать результаты игр. Добавляем нашего туда и в свойствах группы выдаем ему права администратора на удаление сообщений (остальное отзываем).

  2. В консоли Яндекс.Облака создаем облако (плюс в верхнем левом углу), в нем автоматически создается каталог сервисов default внутри. В него и переходим.

  3. Создаем сервисный аккаунт (вкладка «Сервисные аккаунты» справа от «Дашборда каталога», выбираем и нажимаем синюю кнопку), который будет использоваться нами в дальнейшем. Проваливаемся в созданный аккаунт и нажимаем «Создать новый ключ» на кнопке сверху. Создаем статический ключ доступа. Значения AccessKeyId и SecretAccessKey сохраняем в чем-то для использования далее.

  4. Создаем бакет Object storage . Для этого из «Дашборда каталога» из списка «Все сервисы» выбираем «Object Storage» и там нажимаем синюю кнопку «Создать бакет», где выбираем максимальным размером 0.01gb, которого нам хватит очень надолго. Далее мы проваливаемся в свежесозданный бакет. Во вкладке «Права доступа», с помощью синей кнопки «Назначить роли», выдаем нашему права editor.

  5. Создаем секрет со всей нужной нам информацией. Для этого из «Дашборда каталога» из списка «Все сервисы» выбираем «Lockbox» и нажимаем синюю кнопку «Создать секрет». Называем , а потом добавляем пары Ключ-Значение для всего, что будет нами использовано:

    1. S3_ACCESS_KEY_ID устанавливаем AccessKeyId из пункта 4

    2. S3_SECRET_ACCESS_KEY устанавливаем SecretAccessKey из пункта 4

    3. S3_BUCKET_NAME  устанавливаем в 

    4. S3_ENDPOINT_URL устанавливаем как https://storage.yandexcloud.net, (но можно перепроверить, что написано тут, как ендпоинт доступа к Object Storage)

    5. S3_REGION  устанавливаем как ru-central1, (но можно перепроверить, что написано тут для установки region)

    6. ADMIN_HANDLER устанавливаем как ваш tg_handler (без @)

    7. GROUP_NAME устанавливаем в 

    8. BOT_TOKEN устанавливаем в ApiToken, что нам выдал BotFather в переписке с ним (пункт 1)

  1. После создания секрета, проваливаемся в него и во вкладке «Права доступа» назначаем роли lockbox.payloadViewer и lockbox.viewer нашему сервисном аккаунту  .

  2. Создаем нашу лямбду. Для этого, как мы уже несколько раз делали, из «Дашборда каталога» создаем ресурс «Cloud Functions». Называем , выбираем python 3.12. Далее в редакторе мы заменяем содержимое созданного index.py на содержимое из githab, а рядом создаем requirements.txt с содержимым оттуда же. Выбираем сервисным аккаунтом нашего . В секции «Секреты Yandex Lockbox» мы прокидываем все 8 ключей-значений из созданного в 6-м пункте секрета, где названия переменных мы повторяем за ключами. Сохраняем изменения и во вкладке  «Обзор» делаем функцию публичной. 

  3. Ссылка для вызова функции из вкладки «Обзор» должна быть использована нами для привязки ее к API Telegram. Также нам опять нужен ApiToken из пункта 1. Подставляем эти значения в вызов соответствующей команды на нужной используемой вами платформе и вызываем ее.

Linux, macOS:
curl 
  --request POST 
  --url https://api.telegram.org/bot/setWebhook 
  --header 'content-type: application/json' 
  --data '{"url": ""}'

Windows (в терминале cmd):
curl ^
  --request POST ^
  --url https://api.telegram.org/bot/setWebhook ^
  --header "content-type: application/json" ^
  --data "{"url": ""}"

Windows (в терминале PowerShell):
curl.exe `
  --request POST `
  --url https://api.telegram.org/bot/setWebhook `
  --header '"content-type: application/json"' `
  --data '"{ \"url\": \"\" }"'
  1. Все должно начать работать, а команда /start отзываться в переписке с ботом. Настало время переигрывать и уничтожать.

Как этим пользоваться?

  • Собираем всех в созданной группе 

  • Желающие могут отправлять запрос /book чтобы встать в очередь и /leave для покидания очереди или стола. Когда вы станете первым в списке ожидания, бот пришлет приглашение к столу.

  • Если вы хотите провести рейтинговую игру, оба игрока должны быть зарегистрированы (/register_me) — так мы проставляем дефолтный рейтинг в 1000 новым игрокам.

  • Публикуем результаты методом /played @someone 2-1.

  • Наслаждаемся статистикой в рейтинге (/top) и результатами против отдельных оппонентов (/stats_vs @someone).

  • Радуемся растущему рейтингу или вынашиваем месть против зарвавшихся коллег.

Итоги

Для нашей скромной компании получилось так:

  • Больше тысячи зарегистрированных партий в группе из 40 человек и схожее число бронирований стола.

  • Все это за 100 потраченных рублей квоты облака и несколько часов свободного времени автора.

  • Очередь стала главной фичей, так как теперь ты уверен, что стол твой и можно спокойно прогонять тех, кто там сейчас играет.

  • Профилактика малоподвижного образа жизни.

  • Рейтинг поднял мотивацию людей доигрывать партии с полной отдачей, хоть и …

  • Стало немного душнее:)

  • Автор существенно улучшил свои top и backspin

Ну и очевидно, что вы, вообще говоря, можете использовать схему для всего, что дорого вашему сердцу:

  • Большой теннис

  • Настольный футбол

  • Армрестлинг

  • Шахматы?

  • Гитарные батлы

  • Споры в пулреквестах до победного

  • Что угодно, где есть два игрока и счет Best-Of-N:

Спасибо за внимание, всем хороших матчей!

Надеюсь, окажется кому-то полезным и не превратит ваш рабочий процесс в руины:)

P.S.

Гугля ссылки на рейтинг RTTF для этой статьи, я наткнулся на замечательную статью с похожей историей. Однако, вычисления конкретного рейтинга были нам не столь интересны, в отличии от принципиального различия в подходе: мне показалось, что наличие группы со всеми игроками является действенным способом контроля честности + дает администраторам контроль над данными и ресурсами. 

© Habrahabr.ru