Первый 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()
Один раз в секунду контекст переключается. Работает все стабильно.
Полезные ссылки
-
Уроки по телеграм ботам "Пишем ботов для Telegram на языке Python"
-
Все методы API c официального сайта
-
Репозиторий pyTelegramBotApi с полным описанием