Блог Синявского
  • Разделы
  • Метки
  • Все статьи

Телеграм бот, tornado и очереди

1

Первый Telegram бот

Необходимо разработать бота для мессенджера Telegram - калькулятор осаго. На самом деле это мой не первый бот. Первый мой бот был простейшим приложением для оповещений, где, для экономии денег, вместо смс транспорта использовался telegram и его приватные каналы. Считаю что боты - это очень перспективная область для развития программирования и отдельное направление будущего социальных сетей и коммуникаций. В связи с этим мне хочется написать статью в своём блоге об этом. Однако в своём повествовании я постараюсь не использовать под копирку описания с других сайтов, которые похожы друг на друга как близнецы. А напишу о своём опыте и тех проблемах с которыми столкнулся. Не считаю себя джедаем поэтому свои решения оставляю на ваш суд и не принуждаю никого проходить этот путь так и только так.

Суть решения:

  • Бот показывает приветствие с рекламной информацией предлагает посчитать стоимость полиса осаго (все тексты должны быть редактируемы).
  • Расчет полиса идет по шагам, фразы и тексты обязательно должны быть шаблонные
  • После расчета полиса бот благодарит и предлагает его оформить и привезти, сообщение должно отправится на указанный email, а также запрос номера телефона для обратного звонка
  • Есть возможность упрощенного расчёта полиса ОСАГО

Будущие возможности: - Сохраняются все расчёты текущего клиента, есть возможность их сбросить - Проверяется КБМ - Веб интерфейс для просмотра всех расчётов и отправки сообщений клиентам, редактирования текстов

Работу представлена на github в репозитории https://github.com/sinyawskiy/osagobot.git.

Структурная схема и многопоточная обработка запросов

Взаимодействие бота и базы данных. В моем случае приложение очень маленькое и я использовал базу данных sqlite которая естественно не многопоточная. Возникла проблема с многопоточностью. Решение я увидел в использовании однопоточной части программы которая работает с базой данных и многопоточной частью которая взаимодействует с Telegram API, переключением контекста и очередью между ними.

Создадим бота

Как регистрировать бота подробно описано на официальном сайте.

В кратце для начала пообщаемся с BotFather. Используя команду /newbot создадим бота и получит заветный токен для доступа к http api, выглядит он вот так 123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11.

Подробно с картинками описано вот на этом сайте https://retifrav.github.io

Пишем своего бота используя python, tornado и pyTelegramBotAPI.

nano telegram.py
# -*- coding: utf-8 -*-
import telebot
bot = telebot.TeleBot("")

@bot.message_handler(commands=['help', 'start'])
def send_welcome(message):
    msg = bot.send_message(message.chat.id, 'Привет! Я codex_bot!')

@bot.message_handler(commands=['auth'])
def send_auth(message):
    pass

bot.polling()

Этот бот описан на этом сайте https://ifmo.su/. Метод polling запускает бесконечный цикл в котором через определенные промежутки времени происходят запросы к серверам telegram и забираются новые данные.

Для того чтобы увязать telebot с tornado я эту стандартную схему немного изменил

# -*- coding: utf-8 -*-

from telebot import TeleBot
import tornado
from tornado.httpserver import HTTPServer
from tornado.ioloop import PeriodicCallback, IOLoop

# периодический запуск синхронных задач по обработке задач в очереди запросов
class CustomPeriodicCallback(PeriodicCallback):
    def __init__(self, request_queue, response_queue, callback_time, io_loop=None):
        if callback_time <= 0:
            raise ValueError("Periodic callback must have a positive callback_time")
        self.callback_time = callback_time
        self.io_loop = io_loop or IOLoop.current()
        self._running = False
        self._timeout = None
        self.request_queue = request_queue
        self.response_queue = response_queue

    # обработка очереди, однопоточная работа с базой данных
    # взяли из очереди задачу, обработали, записали результат и сказали что задача выполенена
    def queue_callback(self):
        try:
            message = self.request_queue.get_nowait()
        except QueueEmpty:
            pass
        else:
            start = False
            is_reset = False
            if message['text'] == 'telegram_cmd':
                self.response_queue.put({
                    'chat_id':message['chat_id'],
                    'wait_message_id':message['wait_message_id'],
                    'message_text': question,
                    'markup': markup
                })
            self.request_queue.task_done()

    def _run(self):
        if not self._running:
            return
        try:
            return self.queue_callback()
        except Exception:
            self.io_loop.handle_callback_exception(self.queue_callback)
        finally:
            self._schedule_next()

# периодический запуск получения запросов с серверов Telegram и отправка ответов
class BotPeriodicCallback(PeriodicCallback):
    def __init__(self, bot, callback_time, io_loop=None):
        if callback_time <= 0:
            raise ValueError("Periodic callback must have a positive callback_time")
        self.callback_time = callback_time
        self.io_loop = io_loop or IOLoop.current()
        self._running = False
        self._timeout = None
        self.bot = bot

    def bot_callback(self, timeout=1):
        #print 'bot_callback'
        if self.bot.skip_pending:
            self.bot.skip_pending = False
        updates = self.bot.get_updates(offset=(self.bot.last_update_id + 1), timeout=timeout)
        self.bot.process_new_updates(updates)
        self.bot.send_response_messages()

    def _run(self):
        if not self._running:
            return
        try:
            return self.bot_callback()
        except Exception:
            self.io_loop.handle_callback_exception(self.bot_callback)
        finally:
            self._schedule_next()

# Добавление к боту очередей запросов и результатов
class AppTeleBot(TeleBot, object):
    def __init__(self, token, request_queue, response_queue, threaded=True, skip_pending=False):
        super(AppTeleBot, self).__init__(token, threaded=True, skip_pending=False)
        self.request_queue = request_queue
        self.response_queue = response_queue

    # Отправка всех обработанных данных из очереди результатов
    def send_response_messages(self):
        try:
            message = self.response_queue.get_nowait()
        except QueueEmpty:
            pass
        else:
            self.send_chat_action(message['chat_id'], 'typing')
            if message['message_text'] == 'contact':
                self.send_contact(message['chat_id'], phone_number=PHONE_NUMBER, last_name=LAST_NAME, first_name=FIRST_NAME, reply_markup=message['markup'])
            else:
                self.send_message(message['chat_id'], message['message_text'], reply_markup=message['markup'])
            self.response_queue.task_done()

def main():
    TOKEN = 'telegram api token'

    request_queue = Queue(maxsize=0) # очередь запросов
    response_queue = Queue(maxsize=0) # очередь результатов
    bot = AppTeleBot(TOKEN, request_queue, response_queue)

    @bot.message_handler(commands=['start'])
    def send_welcome(message):
        pass

    # добавление запросов к боту в очередь запросов
    @bot.message_handler(func=lambda message: True, content_types=['text'])
    def echo_all(message):
        markup = ReplyKeyboardRemove(selective=False)
        response = bot.send_message(message.chat.id,  u'Подождите...', reply_markup=markup)
        bot.request_queue.put({
            'text': message.text,
            'chat_id': message.chat.id,
            'username': message.chat.username,
            'first_name': message.chat.first_name,
            'last_name': message.chat.last_name,
            'message_id': message.message_id,
            'wait_message_id': response.message_id
        })


    ioloop = tornado.ioloop.IOLoop.instance()

    BotPeriodicCallback(bot, 1000, ioloop).start()
    CustomPeriodicCallback(request_queue, response_queue, 1000, ioloop).start()

    ioloop.start()


if __name__ == "__main__":
    main()

Один раз в секунду контекст переключается. Работает все стабильно.

Полезные ссылки

  1. Уроки по телеграм ботам "Пишем ботов для Telegram на языке Python"

  2. Все методы API c официального сайта

  3. Репозиторий pyTelegramBotApi с полным описанием



  • ← сюда
  • туда →

comments powered by Disqus

Опубликовано

22.03.2017

Обновление

05.05.2022

Категории

python

Тэги

  • example 16
  • python 30
  • telegram 3
  • tornado 3

Всегда на связи

  • Блог Синявского - Ничего не переносить на завтра, это тоже проблема с прокастинацией?
  • © Алексей Синявский, по лицензии CC BY-SA если не указано иное.
  • С использованием Pelican. Тема: Elegant от Talha Mansoor