Привет! Наверное, каждый из нас когда-то сталкивался с ситуацией, когда нужно срочно куда-то уехать, но все Ж/Д билеты уже раскуплены. В этой статье я расскажу о том, как я писал Telegram бота для отслеживания и покупки освободившихся билетов Укрзалізниці.
Как это работает
Для покупки железнодорожных билетов в Украине компания Укрзалізниця запустила ресурс http://booking.uz.gov.ua/. Ресурс удобен тем, что не нужно посещать кассы, чтобы забрать сам билет. Достаточно показать проводнику QR код с посадочного талона на экране смартфона либо распечатав на принтере.
Проблема состоит в том, что на популярные рейсы места очень быстро заканчиваются и иногда купить билет довольно проблематично. Однако, многие люди не покупают билет, а бронируют его. Бронь действует лишь 24 часа и после этого, если она не выкуплена в кассе, билет возвращается в пул свободных. Таким образом, необходимо успеть словить этот момент, когда билет доступен для покупки до того, как его снова забронируют или купят.
Было принято решение решить эту задачу с помощью скрипта, который раз в минуту проверяет свободные билеты на интересующий поезд и, в случае наличия, резервирует его на 15 минут. После чего пользователю необходимо завершить процедуру оплаты через веб браузер.
В качестве интерфейса был выбран Telegram так как это новая платформа для меня и я хотел с ней немного разобраться. В качестве бонуса сразу получаем уведомления на мобильный, не задумываясь о push нотификациях или email'ах.
В качестве языка программирования был выбран Python.
Интерфейс
И всё же, как это работает с точки зрения пользователя?
Бот распознает следующие команды:
/help
— вернёт список поддерживаемых команд/trains 2016-06-12 Kyiv Lviv
— вернёт список поездов из Киева во Львов, отправляющихся 12 июня 2016 года/scan Ivanov Ivan 2016-06-12 Kyiv Lviv 743K
— запустит мониторинг билетов на поезд 743К Киев-Львов. Возвращает ID данного сканирования/status_1234
— вернет состояние сканирования с ID 1234/abort_1234
— остановит сканирование с ID 1234
В случае успешного резервирования билета пользователь получит сообщение, содержащее Session ID. Этот ID затем необходимо вручную прописать в cookie браузера и завершить покупку билета.
UZ API
Для начала давайте разберёмся с форматом API, используемым порталом. Это не составляет большого труда, достаточно просто открыть консоль разработчика в браузере и посмотреть какие запросы выполняет скрипт на странице поиска билетов.
В API используются только POST запросы. Для защиты от использования API сторонними разработчиками почти во всех вызовах в тело включается токен. Без токена можно производить только поиск станций.
Стоит также отметить, некоторые нюансы работы с датами. Во-первых, формат даты меняется в зависимости от текущей локали API. Например, для локали en
формат будет mm.dd.yyyy
. Тогда как для ua
и ru
это будет привычный нам dd.mm.yyyy
. Во-вторых, для некоторых запросов дата представляется в виде timestamp, однако он зависит от состояния летнего/зимнего времени. Потому я решил не заморачиваться с сериализацией/десериализацией данных штампов, а использовать их в том виде, в котором API возвращает их.
Получение токена
Покопавшись в подключаемых сайтом скриптах, можно с легкостью обнаружить такой кусок:
var ajax = $v.ajax(url).header({
'GV-Ajax': 1,
'GV-Referer': encodeURI(GV.site.htcur_url + GV.site.requestUri),
'GV-Screen': screen.width + 'x' + screen.height,
'GV-Token': localStorage.getItem('gv-token') || ''
});
Здесь мы видим, что при вызовах в API токен считывается из localStorage браузера. Осталось найти где он туда записывается.
Эта часть была самой интересной, потому как простым поиском по html и js этого найти не удалось. Потратив несколько часов в гугле, я наткнулся на статью, в которой автор решает такой же вопрос с мониторингом билетов на сайте УЗ. Итак, в статье подробно описано, что токен генерируется обфусцированным с помощью JJEncode кодом. За несколько минут находим реализацию деобфускатора на питоне, который и будет использоваться в дальнейшем.
Краткий API reference
Для вызова методов API, необходимо включать следующие заголовки:
GV-Ajax: 1
GV-Referer: http://booking.uz.gov.ua/en/
GV-Token: <token>
Поиск станций
Например, для формирования подсказок автодополнения станций выполняется запрос с пустым телом по адресу http://booking.uz.gov.ua/en/purchase/station/ky/
, где ky
— это то, что пользователь вводит в текстовое поле выбора станции.
В ответ сервер отправляет примерно такой JSON:
{
"value": [
{
"title": "Kyiv",
"station_id": "2200001"
},
{
"title": "Kyivska Rusanivka",
"station_id": "2201180"
},
{
"title": "Kyj",
"station_id": "2031278"
},
{
"title": "Kykshor",
"station_id": "2011189"
}
],
"error": null,
"data": {
"req_text": [
"ky",
"лн"
]
},
"captcha": null
}
Поиск поездов
Для поиска поездов необходимо выполнить запрос на http://booking.uz.gov.ua/en/purchase/search/
с таким телом:
station_id_from=2200001 # ID станции отправления
station_id_till=2218000 # ID станции назначения
date_dep=06.12.2016 # дата отправления в формате mm.dd.yyyy
time_dep=00:00
time_dep_till=
another_ec=0
search=
В ответ мы получим список поездов, следующих по указанному маршруту. Так же, в ответ будет включена информация о количестве свободных мест в вагонах каждого типа (Люкс, Купе, Плацкарт, и т. д.):
{
"value": [
{
"num": "743Л",
"model": 1,
"category": 1,
"travel_time": "5:01",
"from": {
"station_id": 2200001,
"station": "Darnytsya",
"date": 1465741200,
"src_date": "2016-06-12 17:20:00"
},
"till": {
"station_id": 2218000,
"station": "Lviv",
"date": 1465759260,
"src_date": "2016-06-12 22:21:00"
},
"types": [
{
"title": "Seating first class",
"letter": "С1",
"places": 117
},
{
"title": "Seating second class",
"letter": "С2",
"places": 176
}
],
"reserve_error": "reserve_24h"
},
{
"num": "091К",
"model": 0,
"category": 0,
"travel_time": "7:25",
"from": {
"station_id": 2200001,
"station": "Kyiv-Pasazhyrsky",
"date": 1465760460,
"src_date": "2016-06-12 22:41:00"
},
"till": {
"station_id": 2218000,
"station": "Lviv",
"date": 1465787160,
"src_date": "2016-06-13 06:06:00"
},
"types": [
{
"title": "Suite / first-class sleeper",
"letter": "Л",
"places": 11
},
{
"title": "Coupe / coach with compartments",
"letter": "К",
"places": 50
}
],
"reserve_error": "reserve_24h"
}
],
"error": null,
"data": null,
"captcha": null
}
Просмотр вагонов
Просмотреть список вагонов и количество свободных мест можно выполнив запрос на http://booking.uz.gov.ua/en/purchase/coaches/
с таким телом:
station_id_from=2200001
station_id_till=2218000
date_dep=1462976400
train=743К # номер поезда
model=3 # модель поезда
coach_type=С2 # тип вагона (люкс, купе, и т. д.)
round_trip=0
another_ec=0
В ответ мы получим список вагонов данного типа с количеством свободных мест и ценой:
{
"coach_type_id": 10,
"coaches": [
{
"num": 1,
"type": "С",
"allow_bonus": false,
"places_cnt": 21,
"has_bedding": false,
"reserve_price": 1700,
"services": [],
"prices": {
"А": 35831
},
"coach_type_id": 10,
"coach_class": "2"
},
{
"num": 3,
"type": "С",
"allow_bonus": false,
"places_cnt": 21,
"has_bedding": false,
"reserve_price": 1700,
"services": [],
"prices": {
"А": 35831
},
"coach_type_id": 9,
"coach_class": "2"
}
],
"places_allowed": 8,
"places_max": 8
}
Просмотр свободных мест
Для просмотра свободных мест в выбранном вагоне необходимо выполнить запрос на http://booking.uz.gov.ua/en/purchase/coach/
с телом:
station_id_from=2200001
station_id_till=2218000
train=743К
coach_num=1
coach_class=2
coach_type_id=19
date_dep=1462976400
change_scheme=1
В ответ получаем список свободных мест:
{
"value": {
"places": {
"А": [
"8",
"12",
"16",
"18",
"22",
"27",
"28",
"32",
"33",
"34",
"36",
"37",
"38",
"39",
"42",
"43",
"47",
"48",
"49",
"55",
"56"
]
}
},
"error": null,
"data": null,
"captcha": null
}
Работа с корзиной
Для того, чтобы положить билет в корзину, тем самым зарезервировав его на 15 минут для оплаты, необходимо выполнить запрос на http://booking.uz.gov.ua/en/cart/add/
с телом:
code_station_from:2200007
code_station_to:2218000
train:743К
date:1463580000
round_trip:0
places[0][ord]:0
places[0][coach_num]:5
places[0][coach_class]:2
places[0][coach_type_id]:22
places[0][place_num]:37
places[0][firstname]:Name
places[0][lastname]:Surname
places[0][bedding]:0
places[0][child]:
places[0][stud]:
places[0][transp]:0
places[0][reserve]:0
Мониторинг
Итак, вот мы и добрались до самой интересной части, до мониторинга свободных билетов. Для решения этой задачи был реализован класс UZScanner
, который имеет несколько методов:
- добавить поезд для мониторинга
- удалить поезд из мониторинга
- запуск мониторинга
- остановка мониторинга
Класс мониторинга реализован таким образом, чтобы к нему с легкостью можно было подключать любые пользовательские интерфейсы, например, любой другой, отличный от Telegram, бот или веб сайт.
Мониторинг является асинхронным процессом и выполняется как корутина. В случае успешного резервирования билета, мониторинг выполняет callback, информируя пользователя о результате. Для этого в конструктор класса передается callback-функция.
class UZScanner(object):
def __init__(self, success_cb, delay=60):
self.success_cb = success_cb
self.loop = asyncio.get_event_loop()
self.delay = delay
self.session = aiohttp.ClientSession()
self.client = UZClient(self.session)
self.__state = dict()
self.__running = False
Для того, чтобы вызывающий код различал для какого именно пользователя произошел callback, помимо данных о самом поезде также передаётся callback ID:
def add_item(self, success_cb_id, firstname, lastname, date,
source, destination, train_num, ct_letter=None):
scan_id = uuid4().hex
self.__state[scan_id] = dict(
success_cb_id=success_cb_id,
firstname=firstname,
lastname=lastname,
date=date,
source=source,
destination=destination,
train_num=train_num,
ct_letter=ct_letter,
lock=asyncio.Lock(),
attempts=0,
error=None)
return scan_id
Основная функция мониторинга является циклом, в котором для каждого поезда запускается функция проверки наличия мест.
async def run(self):
self.__running = True
while self.__running:
for scan_id, data in self.__state.items():
asyncio.ensure_future(self.scan(scan_id, data))
await reliable_async_sleep(self.delay)
Сама же функция мониторинга работает по такому алгоритму:
- Получить список поездов на заданную дату по заданному маршруту
- Проверить, есть ли нужный поезд
- Для всех вагонов (либо только для указанного типа) проверить наличие мест
- Попробовать зарезервировать первое найденное свободное место
- В случае успеха, выполнить callback, удалить поезд из мониторинга
async def scan(self, scan_id, data):
if data['lock'].locked():
return
async with data['lock']:
data['attempts'] += 1
train = await self.client.fetch_train(
data['date'], data['source'], data['destination'], data['train_num'])
if train is None:
return self.handle_error(
scan_id, data, 'Train {} not found'.format(data['train_num']))
if data['ct_letter']:
coach_type = self.find_coach_type(train, data['ct_letter'])
if coach_type is None:
return self.handle_error(
scan_id, data, 'Coach type {} not found'.format(data['ct_letter']))
coach_types = [coach_type]
else:
coach_types = train.coach_types
session_id = await self.book(train, coach_types, data['firstname'], data['lastname'])
if session_id is None:
return self.handle_error(scan_id, data, 'No available seats')
await self.success_cb(data['success_cb_id'], session_id)
self.abort(scan_id)
@staticmethod
async def book(train, coach_types, firstname, lastname):
with UZClient() as client:
for coach_type in coach_types:
for coach in await client.list_coaches(train, coach_type):
try:
seats = await client.list_seats(train, coach)
except ResponseError:
continue
for seat in seats:
try:
await client.book_seat(train, coach, seat, firstname, lastname)
except ResponseError:
continue
return client.get_session_id()
Заключение
Мы разобрались с API, используемым порталом http://booking.uz.gov.ua и реализовали скрипт резервирования билета. Код доступен на GitHub. Docker image доступен на DockerHub. Также доступен Telegram бот @uz_ticket_bot
UPD УЗ заблокировала IP бота. Бот временно отключён.
Комментарии (52)
Sergey_datex
13.06.2016 23:30+3Автор, отличная работа. Вы материализовали мысль многих, кто пользуется этим сервисом
opanas
14.06.2016 00:57+2Я пользуюсь бесплатным сервисом watchmyticket.com (donate по желанию), но здорово теперь иметь еще один вариант.
almaz010891
14.06.2016 00:36+2Отличная идея! Спасибо, прочитал доку по боту, там есть следующее: /scan Firstname Lastname 2016-01-01 Kyiv Lviv 743K C2
Что такое C2 — Как я понял, с помощью этого можно указать тип места (плацкарт/купе и т.д.) Какие типы кроме C2 могут быть тогда, можно более развернуто?ketom_z80
14.06.2016 08:19Все верно, С2 — это тип вагона.
/trains
возвращает список поездов в таком виде:
Train: 013Ш Departure time: 2016-07-01 01:08:00 Travel time: 8:52 ~~~~~~~~~~ Л: 2 (Suite / first-class sleeper) К: 10 (Coupe / coach with compartments) П: 16 (Berth / third-class sleeper) ========== Train: 744Л Departure time: 2016-07-01 06:20:00 Travel time: 4:59 ~~~~~~~~~~ С1: 164 (Seating first class) С2: 403 (Seating second class) ==========
Здесь видно, что типы вагонов бывают С1, С2, Л, К, П
PEgorov
14.06.2016 01:20+9У Укрзализныци наверняка есть такой же неочевидный лайфхак, как и у РЖД, суть его в следующем: если очень-очень нужно уехать на конкретном поезде, а билетов на него в продаже нет, то можно поискать билеты с «фиктивными» пересадками. Для тех, кто знаком с понятием split ticketing все уже понятно, для остальных поясню на актуальных на момент написания комментария данных.
Допустим, есть поезд 062А, который едет из Москвы в Питер, проезжая при этом несколько промежуточных станций. Билетов на этот поезд на сегодня уже в продаже нет, но если очень хочется уехать, то можно купить два билета на одно и то же место, на два последовательных сегмента маршрута. Например, можно купить на 37 место 5 вагона из Москвы до Бологое, и на это же место от Бологое до Питера. Полного билета, из Москвы в Питер, на это место в продаже нет. Я не уверен, почему это так, скорее всего это как-то связано с квотами станций.
А еще этот милый лайфхак иногда позволяет сэкономить приличную сумму денег. Например, прямо сейчас билет на сегодня из Москвы в Питер, в купе поезда 082А стоит 4326 рублей. Но вместо одного билета можно купить два, от Москвы до Окуловки за 1451 рублей и от Окуловки до Питера, за 947. В сумме это обойдется Вам в 2398 рублей, экономия равна 1928 рублям или 45%, при этом качественно разницы никакой нет совершенно.ketom_z80
14.06.2016 08:22+1Вероятно, такая штука есть. Однако, в Украине помимо обычных поездов запустили быстрые электропоезда "интерсити". Зачастую они следуют без остановок по всему маршруту. Лично я чаще всего пользуюсь именно такими поездами, потому ваш вариант здесь вряд ли сработает.
sergey_prokofiev
14.06.2016 13:34-2Все поезда интерсити на украине идут с промежуточными остановками. В этом можно легко убедиться здесь: intercity.kiev.ua/raspisanie
rootes
14.06.2016 10:02+1Да, фиктивные «пересадки» не раз выручали. Однозначно стоит добавить в реализацию бота.
Правда у нас в украинских поездах цена билетов наоборот получается дороже процентов на 30%, вероятно из-за двойной оплаты страховки и дополнительных сборов.
Еще есть прикол, когда за неделю уже билетов на поезд нет, а в день отправления «магическим» образом появляется куча свободных мест в разных вагонах. Но это уже другая история, о куче посредников, их бизнесе и времени бронирования.oxidmod
14.06.2016 10:12+3УкрЗалізниця бронирует в каждом поезде определенный процент мест для своих сотрудников (они имеют право раз или два в год взять бесплатные билеты туда и обратно на любой поезд), для сотрудников разнообразных органов (внезапные командировки и так далее). Естественно эта бронь не выкупается в полном объеме никогда и в последний день снимается.
demimurych
14.06.2016 13:08бронь снимается дважды.
первый раз ровно за 12 часов до отправления поезда.
второй раз по разному — для некоторых маршрутов за 3 часа для некоторых за полтора. Так и не понял в каких случаях выбирается какое время. (возможно проходящий или прямой)
Этот алгоритм работает даже в сезон на курортных маршрутах. Ни раз так брал билеты.
lysk1959
14.06.2016 15:24Да не на 30% а вдвое. Пример? Интерсити Днепр-Харьков, промежуточная Лозовая.
Днепр-Харьков =133 грн, Днепр-Лозовая=113грн, Лозовая-Харьков 112 грн, итого 225 против 133,
грабеж чистой воды.
Alibobaevich
14.06.2016 20:25В УЗ на каждую станцию выделяется определенный пул билетов, и часто такое бывает, что на больших станцииях билеты расходятся быстрее чем на мелких. Например если из Запорожья в Киев нет билетов ни на один поезд, то почти всегда из соседнего Мелитополя чуть ли не десяток в каждом вагоне. Выходит чуть дороже, но зато точно доедешь куда нужно и без нервотрепок по поводу наличия или отсутсвия снятой брони в последний день перед отправкой. Ну или если в обратную сторону — взять билет с посадкой из соседнего городка и добраться туда автобусом или машиной.
madkite
15.06.2016 20:37У Укрзализныци с этим как раз наоборот. Они при ажиотаже предпочитают продавать билеты от конечной до конечной, видимо зарабатывая при этом больше денег (билет стоит дороже, а на оставшуюся часть пути найти пассажира сложнее). Потому иногда система не позволяет купить билет с промежуточной станции, при этом позволяя купить с начальной до конечной. Но тут есть другая проблема — если пассажир не сел в поезд, то на одной из следующих станций они могут как-то подать эту информацию и билет снова поступает в продажу. Я не знаю как оно технически работает, это мне рассказывали в справочной. Потому чтобы не попасть в неприятную ситуацию, если вы садитесь на промежуточной станции, нужно посылать телеграмму начальнику поезда о том, что посадка будет производиться на такой-то станции.
Chpock
14.06.2016 04:46Как-то одноразово понадобилось отловить билет на УЗ, в итоге «на коленке» родился такой tcl скрипт с проверкой билетов на направление/дату (через альтернативный api) и отправкой sms (через сервис twilio) — http://paste.tclers.tk/3842 Билет был успешно пойман и скрипт отправился в архив.
Tribunal
14.06.2016 10:30Когда-то тоже писал бот на Python. Поработал пару недель, и они добавили этот токен. Побороть его тогда не смог (.
Вещь отличная, особенно когда пользуется ей сравнительно немного людей.
lysk1959
14.06.2016 13:35На каком-то из сайтов — посредников (а билетами УЗ торгует не только booking, но и многие другие, сайты дирекций например, Приват-банк тот же) уже реализованы функции ожидания нужных билетов. В системе резервирования УЗ есть еще такая особенность, что в продажу выставляется не весь поезд одновременно, а сначала несколько вагонов, потом по их заполнению до определенного процента — добавляется еще вагон и т.д. Поэтому имеем ситуацию, что вчера например в поезде нет рядом 4х мест для семейной поездки, а сегодня они уже есть… И да, квоты между железными дорогами… Это просто пир духа… С моей станции ЮЖД например нет билета на нужный поезд, но с соседней станции ПриднЖД — есть… Приходится брать, благо разница в цене копеечная, но его надо обязательно предварительно за день регистрировать у дежурного по вокзалу, чтобы изменить место посадки, иначе билет аннулируется…
Alibobaevich
14.06.2016 20:30Десятки раз брал билет от соседнего города и никогда не регистрировал свой билет у дежурного по вокзалу — без проблем садился на своей станции(правда всегда брал в пределах одной ЖД)
ArisChik
14.06.2016 13:35Интересно, а внутренняя система, которой пользуются кассиры, смотрит на ту же БД\сервер? Я к тому, что вы только что открыли ящик Пандоры — оказывается легкое шаманство и можно тупо «положить» сервер, а если он еще и один с тем что кассиры используют…
BoDVa
14.06.2016 13:52+1Есть, давненько уже, запущенный и работающий бот
https://telegram.me/railwaybot
На днях там запустилась еще и продажа билетов, к сожалению не на прямую через букинг, но тоже интересный вариант.
BoDVa
14.06.2016 13:56Вопрос по авторской реализации. Для чего реализовано добавление в корзину, ведь корзина не привязывается к аккаунту и не получится просто зайти и купить, ожидающий билет. Это сделано просто для резервирования?
ketom_z80
14.06.2016 16:00Корзина привязана к session id. Подменив этот id в браузере, можно произвести оплату билета. Это не идеальное решение, но первое, что пришло в голову.
IgorPastukhov
14.06.2016 14:21Конечно, такие вещи давно многими реализованы «для себя». Просто не афишируются, ибо приведет к появлению captcha, как и у РЖД. Но использование telegramm безусловно интересный вариант, спасибо!
vpoplavskii
14.06.2016 15:19Запустил с Docker. Телеграм бот работает, отвечает на команду /help, но при вводе /trains или /scan возвращает 403 страницу с УЗ.
ketom_z80
14.06.2016 15:59Очевидно, немного забомбили запросами и УЗ оперативно добавили еще одну проверку.
ketom_z80
14.06.2016 20:39Попробуйте еще раз. Добавлена поддержка браузерных юзер агентов.
vpoplavskii
15.06.2016 09:25Спасибо, работает. А можно добавить бронь 2-3х мест рядом с исключением туалетов?
ketom_z80
15.06.2016 09:50Не думаю, что у меня будет возможность сделать это в ближайшее время. Но я с удовольствием просмотрю ваш PR.
VladKopanev
14.06.2016 18:48В целом не плохо, тоже в голову приходили мысли, а как же это автоматизировать. Но смущает только один момент: «вручную прописать в cookie браузера и завершить покупку билета.» — как-то не юзер френдли получается, я понимаю, что тут ограничения среды вступают в силу,, но все же.
extempl
14.06.2016 19:19Это решается простеньким js кодом упакованным в URL и запускаемым на странице УЗ. Достаточно туда впихнуть промпт который будет спрашивать sessionID и записывать его в LS.
ketom_z80
14.06.2016 20:35PR приветствуется!
extempl
14.06.2016 21:36Done. Немного коряво вышло из-за ограничения гитхаба на публикацию букмарклетов в .md, так что нужно смерджить в бранч
gh-pages
где будет открываться html с линкой букмарклета.extempl
14.06.2016 21:47Конечно, это только полуавтоматическое решение, и не для всех подходит, но самое простое. К сожалению, сделать это ссылкой из бота не выйдет, как могло показаться из моего первого комментария, речь шла именно о букмарклете.
ketom_z80
14.06.2016 22:21Увы, но данный способ не подходит, так как куки будет записана для домена github.io и не будет работать для booking.uz.gov.ua.
extempl
15.06.2016 06:25Так запускать-то его надо именно на странице booking.uz.gov.ua. Я в ридми описал. Нужно не просто линку нажать на странице, а добавить её как закладку. Оттого оно и зовётся букмарклетом.
extempl
15.06.2016 06:33+1Для наглядности скринкаст как пользоваться: http://screencast.com/t/xsqsLiE2
teddygod
15.06.2016 09:51Интересный подход, сыроват пока что, но тем немение для УЗ будет новым витком развития
wowkin
21.06.2016 14:49SCAN_DELAY_SEC лучше ставить минимум 60 секунд.
Информация на сервере все равно обновляется только каждые 20 минут.
Поэтому лишний раз DDoS-ить сервер не надо.
oxidmod
имхо, так можно сервис и заДДОСить, тем более что он до сих пор в тестовой експлуатации
ketom_z80
Заддосить можно все, что угодно. Но это же не повод не реализовывать программы, выполняющие полезную функцию, правда?
А шильдик «тестовая эксплуатация» там висит с момента запуска сервиса. Лет 5 уже как.
oxidmod
одно дело писать прораммы использующие публичное АПИ, но на сколько я знаю разработчики не открывали его. Да, они не особо то и скрывали, но обфускация важного кода какбы намекает, что АПИ не публичное. Сервис банально может быть не расчитан на нагрузки созданные всеми желающими и в итоге ни Вы не купити билетик, ни сотни других покупателей
extempl
Кроме того, запуском бота с множеством пользователей возникнет проблема с обычным нормальным пользованием сервисом, так как бот будет постоянно резервировать места и совсем не факт что с последующей покупкой.