Как можно упростить себе жизнь с помощью Telegram-бота

в 10:02, , рубрики: python, python-telegram-bot, requests, telegram bots, urllib2, метки:

О чём эта статья?

Эта статья — краткий рассказ о том, как с помощью подручных средств (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 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.

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 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. Она позволяет выполнять отложенные и повторяемые задачи.

Как можно использовать возможность выполнять периодически запускаемые задачи? Видятся следующие возможные улучшения:

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

Тут потребуется добавить к пользователю поле, хранящее эту пороговую величину: threshold. Для задания значения для этого поля потребуется отдельная команда для бота.

Реализацию варианта №3 можно увидеть в репозитории бота на Github.

Резюме

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

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

Автор: spitty

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js