Привет! Наверное, каждый из нас когда-то сталкивался с ситуацией, когда нужно срочно куда-то уехать, но все Ж/Д билеты уже раскуплены. В этой статье я расскажу о том, как я писал Telegram бота для отслеживания и покупки освободившихся билетов Укрзалізниці. Для покупки железнодорожных билетов в Украине компания Укрзалізниця запустила ресурс http://booking.uz.gov.ua/. Ресурс удобен тем, что не нужно посещать кассы, чтобы забрать сам билет. Достаточно показать проводнику QR код с посадочного талона на экране смартфона либо распечатав на принтере. Проблема состоит в том, что на популярные рейсы места очень быстро заканчиваются и иногда купить билет довольно проблематично. Однако, многие люди не покупают билет, а бронируют его. Бронь действует лишь 24 часа и после этого, если она не выкуплена в кассе, билет возвращается в пул свободных. Таким образом, необходимо успеть словить этот момент, когда билет доступен для покупки до того, как его снова забронируют или купят. Было принято решение решить эту задачу с помощью скрипта, который раз в минуту проверяет свободные билеты на интересующий поезд и, в случае наличия, резервирует его на 15 минут. После чего пользователю необходимо завершить процедуру оплаты через веб браузер.  В качестве интерфейса был выбран Telegram так как это новая платформа для меня и я хотел с ней немного разобраться. В качестве бонуса сразу получаем уведомления на мобильный, не задумываясь о push нотификациях или email'ах.
В качестве языка программирования был выбран Python. И всё же, как это работает с точки зрения пользователя?
Бот распознает следующие команды: В случае успешного резервирования билета пользователь получит сообщение, содержащее Session ID. Этот ID затем необходимо вручную прописать в cookie браузера и завершить покупку билета. Для начала давайте разберёмся с форматом API, используемым порталом. Это не составляет большого труда, достаточно просто открыть консоль разработчика в браузере и посмотреть какие запросы выполняет скрипт на странице поиска билетов.  В API используются только POST запросы. Для защиты от использования API сторонними разработчиками почти во всех вызовах в тело включается токен. Без токена можно производить только поиск станций. Стоит также отметить, некоторые нюансы работы с датами. Во-первых, формат даты меняется в зависимости от текущей локали API. Например, для локали en формат будет mm.dd.yyyy. Тогда как для ua и ru это будет привычный нам dd.mm.yyyy. Во-вторых, для некоторых запросов дата представляется в виде timestamp, однако он зависит от состояния летнего/зимнего времени. Потому я решил не заморачиваться с сериализацией/десериализацией данных штампов, а использовать их в том виде, в котором API возвращает их. Покопавшись в подключаемых сайтом скриптах, можно с легкостью обнаружить такой кусок: Здесь мы видим, что при вызовах в API токен считывается из localStorage браузера. Осталось найти где он туда записывается. Эта часть была самой интересной, потому как простым поиском по html и js этого найти не удалось. Потратив несколько часов в гугле, я наткнулся на статью, в которой автор решает такой же вопрос с мониторингом билетов на сайте УЗ. Итак, в статье подробно описано, что токен генерируется обфусцированным с помощью JJEncode кодом. За несколько минут находим реализацию деобфускатора на питоне, который и будет использоваться в дальнейшем. Для вызова методов API, необходимо включать следующие заголовки: Например, для формирования подсказок автодополнения станций выполняется запрос с пустым телом по адресу http://booking.uz.gov.ua/en/purchase/station/ky/, где ky — это то, что пользователь вводит в текстовое поле выбора станции. В ответ сервер отправляет примерно такой JSON: Для поиска поездов необходимо выполнить запрос на http://booking.uz.gov.ua/en/purchase/search/ с таким телом: В ответ мы получим список поездов, следующих по указанному маршруту. Так же, в ответ будет включена информация о количестве свободных мест в вагонах каждого типа (Люкс, Купе, Плацкарт, и т. д.): Просмотреть список вагонов и количество свободных мест можно выполнив запрос на http://booking.uz.gov.ua/en/purchase/coaches/ с таким телом: В ответ мы получим список вагонов данного типа с количеством свободных мест и ценой: Для просмотра свободных мест в выбранном вагоне необходимо выполнить запрос на http://booking.uz.gov.ua/en/purchase/coach/ с телом: В ответ получаем список свободных мест: Для того, чтобы положить билет в корзину, тем самым зарезервировав его на 15 минут для оплаты, необходимо выполнить запрос на http://booking.uz.gov.ua/en/cart/add/ с телом: Итак, вот мы и добрались до самой интересной части, до мониторинга свободных билетов. Для решения этой задачи был реализован класс UZScanner, который имеет несколько методов: Класс мониторинга реализован таким образом, чтобы к нему с легкостью можно было подключать любые пользовательские интерфейсы, например, любой другой, отличный от Telegram, бот или веб сайт. Мониторинг является асинхронным процессом и выполняется как корутина. В случае успешного резервирования билета, мониторинг выполняет callback, информируя пользователя о результате. Для этого в конструктор класса передается callback-функция. Для того, чтобы вызывающий код различал для какого именно пользователя произошел callback, помимо данных о самом поезде также передаётся callback ID: Основная функция мониторинга является циклом, в котором для каждого поезда запускается функция проверки наличия мест. Сама же функция мониторинга работает по такому алгоритму: Мы разобрались с API, используемым порталом http://booking.uz.gov.ua и реализовали скрипт резервирования билета. Код доступен на GitHub. Docker image доступен на DockerHub. Также доступен Telegram бот @uz_ticket_bot UPD УЗ заблокировала IP бота. Бот временно отключён.