О чём эта статья?
Эта статья — краткий рассказ о том, как с помощью подручных средств (Firefox) и Python можно осуществить успешную интеграцию Telegram-бота и внешнего сервиса.
Материал будет интересен тем, кто наслышан о Telegram'ных ботах, но не знает, как к ним подступиться и какие задачи с их помощью можно решать. Предполагается знание Python.
Картинка для привлечения внимания:
TL;DR
Из статьи вы узнаете:
1. Как с помощью браузера узнать, какой запрос отправляется на сервер при клике по кнопке?
2. Как легко отправить запрос на сервер с помощью Python?
urllib2
является библиотека requests
. Подробнее на Хабре: "Библиотека для упрощения HTTP-запросов".
3. Как написать бота на Python?
python-telegram-bot
. Пока на Хабре эта библиотека не упоминалась.
Проблема
Существует единая транспортная карта "Стрелка", которая позволяет сильно сэкономить на поездках из Москвы в область. Причина экономии в том, что стоимость проезда при оплате наличными оказывается на 20-30% выше, чем при оплате упомянутой транспортной картой.
Узнать остаток на счёте при совершении оплаты нельзя. Вероятно, из-за отсутствия у считывающего устройства постоянной связи с процессинговыми серверами. Для получения информации о состоянии счёта предусмотрен сайт http://strelkacard.ru и мобильные приложения для популярных платформ.
После обновления телефона до Android 6 случилось неприятное: официальное приложение стало стабильно вылетать при запуске. Немало порядочных пользователей оставили соответствующее сообщение на странице приложения в Google Play. Но воз и ныне там.
Потребность в получении информации о балансе обычно появляется в момент оплаты поездки. Делать это с мобильного устройства через сайт не слишком удобно даже при использовании личного кабинета.
Возник вопрос: "Можно ли узнать баланс карты без лишних телодвижений?"
Исследуем интерфейсы
Известны как минимум два места, откуда можно получить информацию о балансе карты:
- Мобильное приложение
- Официальный сайт
Узнать, какие запросы генерирует приложение можно, но нетривиально. Гораздо проще использовать привычный браузер и его средства web-разработчика. Очень удобен для использования оказывается plugin для Firefox Firebug.
Зайдём на страницу http://strelkacard.ru/ и активируем панель Firebug. Перейдём на вкладку Net
. Если теперь ввести номер карты и запросить баланс, можно увидеть запрос, сгенерированный для получения этих данных:
Щёлкнем правой кнопкой по этому запросу и выберем пункт 'Copy as cURL'
. Это действие приведёт к тому, что в буффере обмена окажется команда вызова консольной утилиты curl
с параметрами, которые позволят отправить ровно тот же запрос, какой был сгенерирован браузером.
Пример:
curl 'http://strelkacard.ru/api/cards/status/?cardnum=12345678901&cardtypeid=3ae427a1-0f17-4524-acb1-a3f50090a8f3' -H 'Accept: application/json, text/plain, */*' -H 'Accept-Encoding: gzip, deflate' -H 'Accept-Language: en-US,en;q=0.5' -H 'Connection: keep-alive' -H 'Host: strelkacard.ru' -H 'Referer: http://strelkacard.ru/' -H 'User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:44.0) Gecko/20100101 Firefox/44.0'
Если ввести данную команду без изменений в консоли, то получим ожидаемый ответ в формате JSON:
{"cardactive":false,"balance":100100,"baserate":3000,"cardblocked":false,"numoftrips":0}
Отложим вопрос разбора этого ответа на потом, а сейчас попробуем понять, какие аргументы реально нужны для получения корректного ответа.
Так как данные действия доступны пользователю без авторизации, то скорее всего можно убрать из запроса header'ы, не рискуя получить некорректный ответ. Убираем все аргументы -H
и пробуем отправить простой GET запрос:
$ curl 'http://strelkacard.ru/api/cards/status/?cardnum=12345678901&cardtypeid=3ae427a1-0f17-4524-acb1-a3f50090a8f3'
{"cardactive":false,"balance":100100,"baserate":3000,"cardblocked":false,"numoftrips":0}
Ответ ещё соответствует ожиданиям. Среди параметров, передаваемых с запросом есть cardnum
, отвечающий за номер карты, и cardtypeid
, название которого подсказывает, что он отвечает за тип карты. Если в необходимости первого параметра сомнений нет, то значимость типа карты под вопросом. Особенно при условии, что пользователь никак его не выбирает. Попробуем избавиться и от него:
$ curl 'http://strelkacard.ru/api/cards/status/?cardnum=12345678901'
{"__all__":["Некорректные параметры"]}
Ожидания не оправдались. Значит, тип карты как-то участвует в формировании ответа. Беглый обзор js-скриптов выявил факт того, что данный параметр статичен, и его можно просто запомнить и использовать в дальнейшем при общении с сервером.
Описание ответа сервера
Посмотрим повнимательнее на ответ сервера:
{"cardactive":false,"balance":100100,"baserate":3000,"cardblocked":false,"numoftrips":0}
Очевидно, что нужная нам величина — balance
. Если при этом сравнить число с тем, что отобразилось в браузере после выполнения запроса, станет очевиден и тот факт, что величина эта имеет размерность копеек.
Общаемся с интерейсом из Python
curl
безусловно хорош, но нам мало отправлять запрос и получать ответ в виде текста. Нам необходимо уметь этот текст обрабатывать. Язык Python поддерживает преобразование json в собственную структуру объектов из коробки. Также он имеет в стандартной комплектации библиотеку для отправки HTTP urllib2
. Отправка запроса с помощью этой библиотеки выглядит примерно так:
>>> import urllib2
>>> f = urllib2.urlopen('http://strelkacard.ru/api/cards/status/?cardnum=12345678901&cardtypeid=3ae427a1-0f17-4524-acb1-a3f50090a8f3')
>>> print f.read()
{"cardactive":false,"balance":100100,"baserate":3000,"cardblocked":false,"numoftrips":0}
Выглядит хорошо. Но параметр cardnum
мы собираемся менять, поэтому было бы здорово подняться на один уровень абстракции повыше и не формировать строку параметров самостоятельно. Поставим библиотеку requests
и используем её:
>>> import requests
>>> CARD_TYPE_ID = '3ae427a1-0f17-4524-acb1-a3f50090a8f3'
>>> card_number='12345678901'
>>> payload = {'cardnum':card_number, 'cardtypeid': CARD_TYPE_ID}
>>> r = requests.get('http://strelkacard.ru/api/cards/status/', params=payload)
>>> print "Get info for card %s: %d %s" % (card_number, r.status_code, r.text)
Get info for card 12345678901: 200 {"cardactive":false,"balance":100100,"baserate":3000,"cardblocked":false,"numoftrips":0}
Библиотека requests
предоставляет метод json()
для преобразования json-formatted ответа сервера в Python структуру данных.
>>> r.json()
{u'cardactive': False, u'balance': 100100, u'numoftrips': 0, u'baserate': 3000, u'cardblocked': False}
>>> r.json()['balance']
100100
Создаём свою обёртку
Мы уже умеем получать необходимую нам информацию, используя пару строк кода на Python. Создадим функцию для получения информации о балансе карты. Запишем в файл checker.py
следующий код:
#!/usr/bin/python
import logging
import requests
CARD_TYPE_ID = '3ae427a1-0f17-4524-acb1-a3f50090a8f3'
logger = logging.getLogger(__name__)
def get_status(card_number):
payload = {'cardnum':card_number, 'cardtypeid': CARD_TYPE_ID}
r = requests.get('http://strelkacard.ru/api/cards/status/', params=payload)
logger.info("Get info for card %s: %d %s" % (card_number, r.status_code, r.text))
if r.status_code == requests.codes.ok:
return r.json()
raise ValueError("Can't get info about card with number %s" % card_number)
def get_balance(card_number):
r = get_status(card_number)
return r['balance']/100.
Теперь для того, чтобы получить данные о балансе, нам достаточно двух строк:
>>> import checker
>>> checker.get_balance('12345678901')
1001.0
В представленном коде также используется библиотека logging
. С примерами её использования на русском можно ознакомиться на Хабре.
Тут отмечу лишь, что для того, чтобы сообщения логгирования выводились на экран, необходимо сделать что-то вроде:
logging.basicConfig(
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
level=logging.INFO)
Обёртка есть. Что дальше?
Изначально задача стояла в том, чтобы удобно получать информацию о балансе с мобильного устройства. Если писать отдельное приложение, то у него должна быть функциональность добавить карту (текстовое поле ввода + кнопка) и получить информацию о балансе на добавленных картах (кнопка + текстовое поле вывода). Интерфейс не содержит никаких особенных элементов.
Требуемую функциональность оказывается возможным реализовать с помощью использования API Telegram для ботов.
Сложно ли написать бота?
В общем случае нет. Нам сейчас не требуется, чтобы бот умел вести светскую беседу. От него ожидается лишь поддержка ограниченного числа команд.
Подробно и с картинками о ботах Telegram можно почитать в официальном блоге: "Telegram Bot Platform". Что нам важно: мобильное приложение имеет возможность подсказать, какие команды поддерживает бот и отправить нужную буквально в пару нажатий.
Создаём бота
Получаем токен
Подробное описание ботов и их возможностей есть на странице "Bots: An introduction for developers". Там же есть информация о том, как создать своего бота. Для этого нужно обратиться за помощью к авторитету среди ботов: боту @BotFather
.
Главное, что необходимо получить в результате общения с @BotFather
— token для использования API.
Ищем подходящую библиотеку-обёртку
Мы уже начали использовать Python. И даже умеем отправлять запросы к серверу. Можно реализовать свои методы взаимодействия с Telegram API. Но если это можем сделать мы, то начерняка кто-то сделал это до нас. Поищем подходящую библиотеку для работы с Telegram bot API. Поиск [в любимом поисковике](http://ddg.gg/?q=python telegram bot) по ключевым словам "python telegram bot" показал, что есть библиотека-обёртка python-telegram-bot
. Версия библиотеки на момент публикации: 3.3.
Использование обёртки предельно простое: необходимо создать методы с правильной сигнатурой и зарегистрировать их через метод addTelegramCommandHandler
у правильного объекта. Примеры использования даны в описании библиотеки.
Реализуем логику работы бота
Нашему боту в его минимальной рабочей конфигурации необходимо поддерживать команды:
- добавление карты по её номеру
- удаление карты по её номеру
- получение информации о балансе на картах
Создадим рядом с checker.py
файл strelka_bot.py
следующего содержания:
#!/usr/bin/python
from telegram import Updater, User
import logging
import checker
TOKEN = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
# Enable Logging
logging.basicConfig(
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
level=logging.INFO)
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
# Dictionary to store users by its id
users = {}
class UserInfo:
def __init__(self, telegram_user):
self.user = telegram_user
self.cards = {}
def add_card(self, card_number):
card = CardInfo(card_number)
self.cards[card_number] = CardInfo(card_number)
class CardInfo:
def __init__(self, card_number):
self.card_number = card_number
def balance(self):
logger.info("Getting balance for card %s" % self.card_number)
json = checker.get_status(self.card_number)
return json['balance']/100.
def log_params(method_name, update):
logger.debug("Method: %snFrom: %snchat_id: %dnText: %s" %
(method_name,
update.message.from_user,
update.message.chat_id,
update.message.text))
def help(bot, update):
log_params('help', update)
bot.sendMessage(update.message.chat_id
, text="""Supported commands:
/help - Show help
/addcard - Add a card to the list of registered cards
/removecard - Remove a card to the list of registered cards
/getcards - Returns balance for all registered cards""")
def add_card(bot, update, args):
log_params('add_card', update)
if len(args) != 1:
bot.sendMessage(update.message.chat_id, text="Usage:n/addcard 1234567890")
return
card_number = args[0]
telegram_user = update.message.from_user
if not users.has_key(telegram_user.id):
users[telegram_user.id] = UserInfo(telegram_user)
user = users[telegram_user.id]
if not user.cards.has_key(card_number):
user.add_card(card_number)
bot.sendMessage(update.message.chat_id, text="Card %s successfully added" % (card_number))
else:
bot.sendMessage(update.message.chat_id, text="Card %s already added. Do nothing" % (card_number))
def remove_card(bot, update, args):
log_params('remove_card', update)
if len(args) != 1:
bot.sendMessage(update.message.chat_id, text="Usage:n/removecard 1234567890")
return
card_number = args[0]
telegram_user = update.message.from_user
if not users.has_key(telegram_user.id):
bot.sendMessage(update.message.chat_id, text="There are no cards registered for you")
return
user = users[telegram_user.id]
if user.cards.has_key(card_number):
user.cards.pop(card_number)
bot.sendMessage(update.message.chat_id, text="Card %s successfully removed" % (card_number))
else:
bot.sendMessage(update.message.chat_id, text="Card %s has not being added. Do nothing" % (card_number))
def get_cards(bot, update):
log_params('get_cards', update)
telegram_user = update.message.from_user
if not users.has_key(telegram_user.id) or len(users[telegram_user.id].cards) == 0:
bot.sendMessage(update.message.chat_id
, text="There are now saved cards for you. Please use command /addcard CARD_NUMBER")
return
user = users[telegram_user.id]
cards = user.cards
response = ""
for card in cards.values():
if len(response) != 0:
response += 'n'
response += "Card balance for %s: %.2f"%(card.card_number, card.balance())
bot.sendMessage(update.message.chat_id, text=response)
def main():
updater = Updater(TOKEN)
# Get the dispatcher to register handlers
dp = updater.dispatcher
# Add handlers for Telegram messages
dp.addTelegramCommandHandler("help", help)
dp.addTelegramCommandHandler("addcard", add_card)
dp.addTelegramCommandHandler("removecard", remove_card)
dp.addTelegramCommandHandler("getcards", get_cards)
updater.start_polling()
updater.idle()
if __name__ == '__main__':
main()
В переменную TOKEN
необходимо поместить токен, полученный от @BotFather
.
Запускаем бота
Запуск получившегося strelka_bot.py
приводит нашего бота в жизнь.
$ python simple_strelka_bot.py
2016-03-06 12:02:11,706 - telegram.dispatcher - INFO - Dispatcher started
2016-03-06 12:02:11,706 - telegram.updater - INFO - Updater thread started
2016-03-06 12:02:26,123 - telegram.bot - INFO - Getting updates: [645839879]
2016-03-06 12:02:26,173 - __main__ - DEBUG - Method: get_cards
From: {'username': u'username', 'first_name': u'Firstname', 'last_name': u'Lastname', 'id': 12345678}
chat_id: 12345678
Text: /getcards
Теперь он будет отвечать на сообщения и писать в консоли информацию о том, какие сообщения боту приходят и какие ошибки происходят. Завершить работу бота можно комбинацией Ctrl+C
, отправляющей сигнал прерывания (SIGINT
).
После добавления интересующих нас карт получить информацию об их балансах можно с помощью единственной команды /getcards
.
Можно ещё добавить сохранение переменной users
при изменении, чтобы не терять состояние при перезапуске бота. Для этого отлично подходит модуль shelve
. Примеры использования указаны в конце страницы с документацией и использование модуля не должно вызвать затруднения.
Исследуем возможности библиотеки python-telegram-bot
Посмотрим описание библиотеки python-telegram-bot
. Там есть интересная функциональность JobQueue. Она позволяет выполнять отложенные и повторяемые задачи.
Как можно использовать возможность выполнять периодически запускаемые задачи? Видятся следующие возможные улучшения:
- Периодически (раз в день) присылать пользователю сообщение с информацией о текущем балансе на зарегистрированных картах.
- Периодически (несколько раз в час) проверять баланс на карточках и сообщать о его изменении.
Для этого требуется добавить вCardInfo
поле, хранящее текущий баланс и его значение до последнего обновления. И метод для самого обновления: какой-нибудьupdate()
. - Предыдущий вариант, но с возможностью задавать пороговую величину, которая определяет, стоит ли что-то говорить пользователю или не стоит его тревожить.
Тут потребуется добавить к пользователю поле, хранящее эту пороговую величину: threshold
. Для задания значения для этого поля потребуется отдельная команда для бота.
Реализацию варианта №3 можно увидеть в репозитории бота на Github.
Резюме
С помощью использования библиотеки requests
, API для ботов в Telegram и библиотеки python-telegram-bot
удалось создать бота, который вовремя уведомляет пользователей о том, что баланс на их карточках упал до критически низких значений. Задача пополениния карты осталась за рамками, но функциональность "личного кабинета" на официальном сайте позволяет её комфортно решать с ПК.
На Github можно посмотреть более полную реализацию бота с использованием модуля shelve
и функциональности JobQueue: strelka_telegram_bot.
Автор: spitty