Як можна полегшити собі життя за допомогою Telegram-бота

Про що ця стаття?
Ця стаття — короткий розповідь про те, як за допомогою підручних засобів (Firefox) і Python можна здійснити успішну інтеграцію Telegram-бота і зовнішнього сервісу.

Матеріал буде цікавий тим, хто начувся про Telegram'вих ботах, але не знає, як до них підступитися і які завдання з їх допомогою можна вирішувати. Передбачається знання Python.

Картинка для привернення уваги:

writing a twitter bot
(посилання на оригінал)

TL;DR
Із статті ви дізнаєтесь:

1. Як за допомогою браузера дізнатися, який запит відправляється на сервер при кліку по кнопці?

ВідповідьВикористовуючи web tool вашого улюбленого браузера можна побачити всі запити, які відправляються з відкритої сторінки на сервер.


2. Як легко відправити запит на сервер з допомогою Python?

ВідповідьЗручною обгорткою над стандартним модулем
urllib2
бібліотека
requests
. Детальніше на Хабре: "Бібліотека для спрощення HTTP-запитів".



3. Як написати бота на Python?

ВідповідьПовнофункціональна обгортка реалізована в бібліотеці
python-telegram-bot
. Поки на Хабре ця бібліотека не згадувалася.



Проблема
Існує єдина транспортна карта "Стрілка", яка дозволяє сильно заощадити на поїздках з Москви в область. Причина економії в тому, що вартість проїзду при оплаті готівкою виявляється на 20-30% вище, ніж при оплаті згаданої транспортної картою.

Дізнатися залишок на рахунку при здійсненні оплати не можна. Ймовірно, з-за відсутності у зчитувального пристрою постійного зв'язку з процесинговими серверами. Для отримання інформації про стан рахунку передбачений сайт http://strelkacard.ru та мобільні додатки для популярних платформ.

Після оновлення телефону до Android 6 сталося неприємне: офіційне додаток стало стабільно вилітати при запуску. Чимало порядних користувачів залишили відповідне повідомлення на сторінці програми в Google Play. Але віз і нині там.

Screenshot відгуків на 5 березнявідгуки


Потреба в отриманні інформації про баланс зазвичай з'являється в момент оплати поїздки. Робити це з мобільного пристрою через сайт не дуже зручно навіть при використанні особистого кабінету.

Виникло питання: "чи Можна дізнатися баланс карти без зайвих рухів?"

Досліджуємо інтерфейси
Відомі як мінімум два місця, звідки можна отримати інформацію про баланс карти:

  1. Мобільний додаток
  2. Офіційний сайт
Дізнатися, які генерує запити додаток можна, але нетривіально. Набагато простіше використовувати звичний браузер і його засоби web-розробника. Дуже зручний для використання виявляється plugin для Firefox Firebug.

Зайдемо на сторінку http://strelkacard.ru/ і активуємо панель Firebug. Перейдемо на вкладку
Net
. Якщо ввести номер картки і запитати баланс, можна побачити запит, згенерований для отримання цих даних:

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 card for %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 card for %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
.

The 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
у правильного об'єкта. Приклади використання дано в описі бібліотеки.

Реалізуємо логіку роботи бота
Нашу роботу в його мінімальної робочої конфігурації необхідно підтримувати команди:

  1. додавання карти по її номеру
  2. видалення карти по її номеру
  3. отримання інформації про баланс на картах
Створимо поруч з
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 card for %s" % self.card_number)
json = checker.get_status(self.card_number)
return json['balance']/100.

def log_params(method_name, update):
logger.debug("Method: %s\nFrom: %s\nchat_id: %d\n-текст: %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 removed successfully" % (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 messages Telegram
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. Вона дозволяє виконувати відкладені і повторювані завдання.

Як можна використовувати можливість виконувати періодично запускаються завдання? Бачаться наступні можливі поліпшення:

  1. Періодично (раз на день) надсилати йому повідомлення з інформацією про поточний баланс на зареєстрованих картах.
  2. Періодично (кілька разів на годину) перевіряти баланс на картках і повідомляти про його зміну.
    Для цього потрібно додати в
    CardInfo
    поле, що зберігає поточний баланс і його значення до останнього оновлення. І метод для самого оновлення: який-небудь
    update()
    .
  3. Попередній варіант, але з можливістю задавати порогову величину, яка визначає, чи варто щось говорити користувачеві чи не варто його турбувати.
Тут потрібно додати до користувачу поле, що зберігає цю порогову величину:
threshold
. Для завдання значення для цього поля знадобиться окрема команда для бота.

Реалізацію варіанта №3 можна побачити в репозиторії бота на Github.

Резюме
З допомогою використання бібліотеки
requests
, API для ботів в Telegram і бібліотеки
python-telegram-bot
вдалося створити бота, який вчасно повідомляє користувачів про те, що баланс на їх картках впав до критично низьких значень. Завдання пополениния карти залишилася за рамками, але функціональність "особистого кабінету" на офіційному сайті дозволяє комфортно вирішувати з ПК.

На Github можна подивитися більш повну реалізацію бота з використанням модуля
shelve
і функціональності JobQueue: strelka_telegram_bot.

UPD
Користувач itspers коментарях підказав, що повернути додаток "Стрілка" до життя можна явним зазначенням необхідних прав в налаштуваннях. Методом підбору було встановлено, що необхідні права 'Location' і 'Phone'.

Джерело: Хабрахабр
  • avatar
  • 0

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.