Как создать чат-бота для Telegram с помощью Python

22031

Это пошаговое руководство по созданию бота для Telegram. Бот будет показывать курсы валют, разницу между курсом раньше и сейчас, а также использовать современные встроенные клавиатуры.

Время переходить к делу и узнать наконец, как создавать ботов в Telegram.

Шаг №0: немного теории об API Telegram-ботов

Начать руководство стоит с простого вопроса: как создавать чат-ботов в Telegram?

Ответ очень простой: для чтения сообщений отправленных пользователями и для отправки сообщений назад используется API HTML. Это требует использования URL:

https://api.telegram.org/bot/METHOD_NAME

Токен — уникальная строка из символов, которая нужна для того, чтобы установить подлинность бота в системе. Токен генерируется при создании бота. METHOD_NAME — это метод, например, getUpdates, sendMessage, getChat и так далее.

Токен выглядит приблизительно так:

123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11

Для выполнения запросов используются как GET, так и POST запросы. Многие методы требуют дополнительных параметров (методу sendMessage, например, нужно передать chat_id и текст). Эти параметры могут быть переданы как строка запроса URL, application/x-www-form-urlencoded и application-json (кроме загрузки файлов). Еще одно требование — кодировка UTF-8.

После отправки запроса к API, вы получаете ответ в формате JSON. Например, если извлечь данные с помощью метода getME, ответ будет такой:

GET https://api.telegram.org/bot<token>/getMe
{
   ok: true,
   result: {
       id: 231757398,
       first_name: "Exchange Rate Bot",
       username: "exchangetestbot"
   }
}

Если значение ‘ok’ — true, значит запрос был успешным и результат отобразится в поле ‘field’. Если false — в поле ‘description’ будет сообщение об ошибке.

Список всех типов данных и методов API Telegram-бота можно найти здесь (ENG) или с переводом здесь (ру) .

Следующий вопрос: как получать пользовательские сообщения?

Есть два варианта.

Первый — вручную создавать запросы с помощью метода getUpdates. В качестве объекта вы получите массив объектов Update. Этот метод работает как технология длинных опросов (long polling), когда вы отправляете запрос, обрабатываете данные и начинаете повторяете процесс. Чтобы избежать повторной обработки одних и тех же данных рекомендуется использовать параметр offset.

Второй вариант — использовать webhooks. Метод setWebhook нужно будет применить только один раз. После этого Telegram будет отправлять все обновления на конкретный URL-адрес, как только они появятся. Единственное ограничение — необходим HTTPS, но можно использовать и сертификаты, заверенные самостоятельно.

Как выбрать оптимальный метод? Метод getUpdates лучше всего подходит, если:

  1. Вы не хотите или не можете настраивать HTTPS во время разработки.
  2. Вы работаете со скриптовыми языками, которые сложно интегрировать в веб-сервер.
  3. У бота высокая нагрузка.
  4. Вы меняете сервер бота время от времени.

Метод с Webhook лучше подойдет в таких случаях:

  1. Вы используете веб-языки (например, PHP).
  2. У бота низкая нагрузка, и нет смысла делать запросы вручную.
  3. Бот на постоянной основе интегрирован в веб-сервер.

В этом руководстве будет использоваться метод getUpdates.

Еще один вопрос: как создать зарегистрировать бота?

@BotFather используется для создания ботов в Telegram. Он также отвечает за базовую настройку (описание, фото профиля, встроенная поддержка и так далее).

Существует масса библиотек, которые облегчают процесс работы с API Telegram-бота. Вот некоторые из них:

По своей сути, все эти библиотеки — оболочки HTML-запросов. Большая часть из них написана с помощью принципов ООП. Типы данных Telegram Bot API представлены в виде классов.

В этом руководстве будет использоваться библиотека pyTelegramBotApi.

Шаг №1: реализовать запросы курсов валют

Весь код был проверен на версии Python==3.7 c использование библиотек:
pyTelegramBotAPI==3.6.6
pytz==2019.1
requests==2.7.0

Полезно: Краткое руководство по библиотеке Python Requests

Начать стоит с написания Python-скрипта, который будет реализовывать логику конкретных запросов курсов валют. Использовать будем PrivatBank API. URL: https://api.privatbank.ua/p24api/pubinfo?json&exchange&coursid=5.

Пример ответа:

[
    {
        ccy:"USD",
	base_ccy:"UAH",
        buy:"25.90000",
        sale:"26.25000"
    },
    {
	ccy:"EUR",
        base_ccy:"UAH",
        buy:"29.10000",
        sale:"29.85000"
    },
    {
        ccy:"RUR",
        base_ccy:"UAH",
        buy:"0.37800",
        sale:"0.41800"
    },
    {
        ccy:"BTC",
        base_ccy:"USD",
        buy:"11220.0384",
        sale:"12401.0950"
    }
]

Создадим файл pb.py со следующим кодом:

import re  
import requests  
import json  
  
  
URL = 'https://api.privatbank.ua/p24api/pubinfo?json&exchange&coursid=5'  
  
  
def load_exchange():  
    return json.loads(requests.get(URL).text)  
  
  
def get_exchange(ccy_key):  
    for exc in load_exchange():  
        if ccy_key == exc['ccy']:  
            return exc  
    return False  
  
  
def get_exchanges(ccy_pattern):  
    result = []  
    ccy_pattern = re.escape(ccy_pattern) + '.*'  
  for exc in load_exchange():  
        if re.match(ccy_pattern, exc['ccy'], re.IGNORECASE) is not None:  
            result.append(exc)  
    return result

Были реализованы три метода:

  • load_exchange: загружает курсы валют по указанному URL-адресу и возвращает их в формате словаря(dict).
  • get_exchange: возвращает курсы валют по запрошенной валюте.
  • get_exchanges: возвращает список валют в соответствии с шаблоном (требуется для поиска валют во встроенных запросах).

Шаг №2: создать Telegram-бота с помощью @BotFather

Необходимо подключиться к боту @BotFather, чтобы получить список чат-команд в Telegram. Далее нужно набрать команду /newbot для инструкций выбора название и имени бота. После успешного создания бота вы получите следующее сообщение:

Done! Congratulations on your new bot. You will find it at telegram.me/<username>. 
You can now add a description, about section and profile picture for your bot, see /help for a list of commands. 
By the way, when you've finished creating your cool bot, ping our Bot Support if you want a better username for it. 
Just make sure the bot is fully operational before you do this.

Use this token to access the HTTP API:
<token> (here goes the bot’s token)

For a description of the Bot API, see this page: https://core.telegram.org/bots/api

Его нужно сразу настроить. Необходимо добавить описание и текст о боте (команды /setdescription и /setabouttext), фото профиля (/setuserpic), включить встроенный режим (/setinline), добавить описания команд (/setcommands). Потребуется использовать две команды: /help и /exchange. Стоит описать их в /setcommands.

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

Шаг №3: настроить и запустить бота

Начнем с создания файла config.py для настройки:

TOKEN = '<bot token>'  # заменить на токен своего бота
TIMEZONE = 'Europe/Kiev'
TIMEZONE_COMMON_NAME = 'Kiev'

В этом файле указаны: токен бота и часовой пояс, в котором тот будет работать (это понадобится в будущем для определения времени обновления сообщений. API Telegram не позволяет видеть временную зону пользователя, поэтому время обновления должно отображаться с подсказкой о часовом поясе).

Создадим файл bot.py. Нужно импортировать все необходимые библиотеки, файлы с настройками и предварительно созданный pb.py. Если каких-то библиотек не хватает, их можно установить с помощью pip.

import telebot
import config
import pb
import datetime
import pytz
import json
import traceback


P_TIMEZONE = pytz.timezone(config.TIMEZONE)
TIMEZONE_COMMON_NAME = config.TIMEZONE_COMMON_NAME

Создадим бота с помощью библиотеки pyTelegramBotAPI. Для этого конструктору нужно передать токен:

bot.py

bot = telebot.TeleBot(config.TOKEN)
bot.polling(none_stop=True)

Шаг №4: написать обработчик команды /start

Теперь чат-бот на Python работает и постоянно посылает запросы с помощью метода getUpdates. Параметр none_stop отвечает за то, чтобы запросы отправлялись, даже если API возвращает ошибку при выполнении метода.

Из переменной бота возможно вызывать любые методы API Telegram-бота.

Начнем с написания обработчика команды /start и добавим его перед строкой bot.polling(none_stop=True):

@bot.message_handler(commands=['start'])  
def start_command(message):  
    bot.send_message(  
        message.chat.id,  
        'Greetings! I can show you exchange rates.\n' +  
        'To get the exchange rates press /exchange.\n' +  
        'To get help press /help.'  
  )

Как можно видеть, pyTelegramBotApi использует декораторы Python для запуска обработчиков разных команд Telegram. Также можно перехватывать сообщения с помощью регулярных выражений, узнавать тип содержимого в них и лямбда-функции.

В нашем случае если условие commands=['start'] равно True, тогда будет вызвана функция start_command. Объект сообщения (десериализованный тип Message) будет передан функции. После этого вы просто запускаете send_message в том же чате с конкретным сообщением.

Это было просто, не так ли?

Шаг №5: создать обработчик команды /help

Давайте оживим обработчик команды /help с помощью встроенной кнопки со ссылкой на ваш аккаунт в Telegram. Кнопку можно озаглавить “Message the developer”.

@bot.message_handler(commands=['help'])  
def help_command(message):  
    keyboard = telebot.types.InlineKeyboardMarkup()  
    keyboard.add(  
        telebot.types.InlineKeyboardButton(  
            'Message the developer', url='telegram.me/artiomtb'  
  )  
    )  
    bot.send_message(  
        message.chat.id,  
        '1) To receive a list of available currencies press /exchange.\n' +  
        '2) Click on the currency you are interested in.\n' +  
        '3) You will receive a message containing information regarding the source and the target currencies, ' +  
        'buying rates and selling rates.\n' +  
        '4) Click “Update” to receive the current information regarding the request. ' +  
        'The bot will also show the difference between the previous and the current exchange rates.\n' +  
        '5) The bot supports inline. Type @<botusername> in any chat and the first letters of a currency.',  
        reply_markup=keyboard  
    )

Как видно в примере выше, был использован дополнительный параметр (reply_markup) для метода send_message. Метод получил встроенную клавиатуру (InlineKeyboardMarkup) с одной кнопкой (InlineKeyboardButton) и следующим текстом: “Message the developer” и url='telegram.me/artiomtb'.

Код выше выглядит вот так:

обработчик команды /help

Шаг №6: добавить обработчик команды /exchange

Обработчик команды /exchange отображает меню выбора валюты и встроенную клавиатуру с 3 кнопками: USD, EUR и RUR (это валюты, поддерживаемые API банка).

@bot.message_handler(commands=['exchange'])  
def exchange_command(message):  
    keyboard = telebot.types.InlineKeyboardMarkup()  
    keyboard.row(  
        telebot.types.InlineKeyboardButton('USD', callback_data='get-USD')  
    )  
    keyboard.row(  
        telebot.types.InlineKeyboardButton('EUR', callback_data='get-EUR'),  
        telebot.types.InlineKeyboardButton('RUR', callback_data='get-RUR')  
    )  
  
    bot.send_message(  
        message.chat.id,   
        'Click on the currency of choice:',  
        reply_markup=keyboard  
    )

Вот как работает InlineKeyboardButton. Когда пользователь нажимает на кнопку, вы получаете CallbackQuery (в параметре data содержится callback-data) в getUpdates. Таким образом вы знаете, какую именно кнопку нажал пользователь, и как ее правильно обработать.

Вот как работает ответ /exchange:

обработчик команды /exchange

Шаг №7: написать обработчик для кнопок встроенной клавиатуры

В библиотеке pyTelegramBot Api есть декоратор @bot.callback_query_handler, который передает объект CallbackQuery во вложенную функцию.

@bot.callback_query_handler(func=lambda call: True)  
def iq_callback(query):  
    data = query.data  
    if data.startswith('get-'):  
        get_ex_callback(query)

Давайте реализуем метод get_ex_callback:

def get_ex_callback(query):  
    bot.answer_callback_query(query.id)  
    send_exchange_result(query.message, query.data[4:])

Метод answer_callback_query нужен, чтобы убрать состояние загрузки, к которому переходит бот после нажатия кнопки. Отправим сообщение send_exchange_query. Ему нужно передать Message и код валюты (получить ее можно из query.data. Если это, например, get-USD, передавайте USD).

Реализуем send_exchange_result:

def send_exchange_result(message, ex_code):  
    bot.send_chat_action(message.chat.id, 'typing')  
    ex = pb.get_exchange(ex_code)  
    bot.send_message(  
        message.chat.id, serialize_ex(ex),  
        reply_markup=get_update_keyboard(ex),  
	parse_mode='HTML'  
    )

Все довольно просто.

Сперва отправим состояние ввода в чат, так чтобы бот показывал индикатор «набора текста», пока API банка получает запрос. Теперь вызовем метод get_exchange из файла pb.py, который получит код валюты (например, USD). Также нужно вызвать два новых метода в send_message: serialize_ex, сериализатор валюты и get_update_keyboard (который возвращает клавиатуре кнопки “Update” и “Share”).

def get_update_keyboard(ex):  
    keyboard = telebot.types.InlineKeyboardMarkup()  
    keyboard.row(  
        telebot.types.InlineKeyboardButton(  
            'Update',  
	    callback_data=json.dumps({  
                't': 'u',  
		'e': {  
                    'b': ex['buy'],  
		    's': ex['sale'],  
		    'c': ex['ccy']  
                }  
            }).replace(' ', '')  
        ),  
	telebot.types.InlineKeyboardButton('Share', switch_inline_query=ex['ccy'])  
    )  
    return keyboard

Запишем в get_update_keyboard текущий курс валют в callback_data в форме JSON. JSON сжимается, потому что максимальный разрешенный размер файла равен 64 байтам.

Кнопка t значит тип, а e — обмен. Остальное выполнено по тому же принципу.

У кнопки Share есть параметр switch_inline_query. После нажатия кнопки пользователю будет предложено выбрать один из чатов, открыть этот чат и ввести имя бота и определенный запрос в поле ввода.

Методы serialize_ex и дополнительный serialize_exchange_diff нужны, чтобы показывать разницу между текущим и старыми курсами валют после нажатия кнопки Update.

def serialize_ex(ex_json, diff=None):  
    result = '<b>' + ex_json['base_ccy'] + ' -> ' + ex_json['ccy'] + ':</b>\n\n' + \  
             'Buy: ' + ex_json['buy']  
    if diff:  
        result += ' ' + serialize_exchange_diff(diff['buy_diff']) + '\n' + \  
                  'Sell: ' + ex_json['sale'] + \  
                  ' ' + serialize_exchange_diff(diff['sale_diff']) + '\n'  
    else:  
        result += '\nSell: ' + ex_json['sale'] + '\n'  
    return result


def serialize_exchange_diff(diff):  
    result = ''  
    if diff > 0:  
        result = '(' + str(diff) + ' <img draggable="false" data-mce-resize="false" data-mce-placeholder="1" data-wp-emoji="1" class="emoji" alt="<img draggable="false" data-mce-resize="false" data-mce-placeholder="1" data-wp-emoji="1" class="emoji" alt="<img draggable="false" data-mce-resize="false" data-mce-placeholder="1" data-wp-emoji="1" class="emoji" alt="<img draggable="false" data-mce-resize="false" data-mce-placeholder="1" data-wp-emoji="1" class="emoji" alt="<img draggable="false" data-mce-resize="false" data-mce-placeholder="1" data-wp-emoji="1" class="emoji" alt="↗️" src="https://s.w.org/images/core/emoji/2.3/svg/2197.svg">" src="https://s.w.org/images/core/emoji/2.3/svg/2197.svg">" src="https://s.w.org/images/core/emoji/2.3/svg/2197.svg">" src="https://s.w.org/images/core/emoji/72x72/2197.png">" src="https://s.w.org/images/core/emoji/72x72/2197.png">)'  
    elif diff < 0:  
        result = '(' + str(diff)[1:] + ' <img draggable="false" data-mce-resize="false" data-mce-placeholder="1" data-wp-emoji="1" class="emoji" alt="<img draggable="false" data-mce-resize="false" data-mce-placeholder="1" data-wp-emoji="1" class="emoji" alt="<img draggable="false" data-mce-resize="false" data-mce-placeholder="1" data-wp-emoji="1" class="emoji" alt="<img draggable="false" data-mce-resize="false" data-mce-placeholder="1" data-wp-emoji="1" class="emoji" alt="<img draggable="false" data-mce-resize="false" data-mce-placeholder="1" data-wp-emoji="1" class="emoji" alt="↘️" src="https://s.w.org/images/core/emoji/2.3/svg/2198.svg">" src="https://s.w.org/images/core/emoji/2.3/svg/2198.svg">" src="https://s.w.org/images/core/emoji/2.3/svg/2198.svg">" src="https://s.w.org/images/core/emoji/72x72/2198.png">" src="https://s.w.org/images/core/emoji/72x72/2198.png">)'  
    return result

Как видно, метод serialize_ex получает необязательный параметр diff. Ему будет передаваться разница между курсами обмена в формате {'buy_diff': <float>, 'sale_diff': <float>}. Это будет происходить во время сериализации после нажатия кнопки Update. Когда курсы валют отображаются первый раз, он нам не нужен.

Вот как будет выглядеть бот после нажатия кнопки USD:

отображение курса валют ботом

Шаг №8: реализовать обработчик кнопки обновления

Теперь можно создать обработчик кнопки Update. После дополнения метода iq_callback_method он будет выглядеть следующим образом:

@bot.callback_query_handler(func=lambda call: True)  
def iq_callback(query):  
    data = query.data  
    if data.startswith('get-'):  
        get_ex_callback(query)  
    else:  
        try:  
            if json.loads(data)['t'] == 'u':  
                edit_message_callback(query)  
        except ValueError:  
            pass

Если данные обратного вызова начинаются с get- (get-USD, get-EUR и так далее), тогда нужно вызывать get_ex_callback, как раньше. В противном случае стоит попробовать разобрать строку JSON и получить ее ключ t. Если его значение равно u, тогда нужно вызвать метод edit_message_callback. Реализуем это:

def edit_message_callback(query):  
    data = json.loads(query.data)['e']  
    exchange_now = pb.get_exchange(data['c'])  
    text = serialize_ex(  
        exchange_now,  
	get_exchange_diff(  
            get_ex_from_iq_data(data),  
	    exchange_now  
        )  
    ) + '\n' + get_edited_signature()  
    if query.message:  
        bot.edit_message_text(  
            text,  
	    query.message.chat.id,  
	    query.message.message_id,  
	    reply_markup=get_update_keyboard(exchange_now),  
	    parse_mode='HTML'  
	)  
    elif query.inline_message_id:  
        bot.edit_message_text(  
            text,  
	    inline_message_id=query.inline_message_id,  
	    reply_markup=get_update_keyboard(exchange_now),  
	    parse_mode='HTML'  
	)

Как это работает? Очень просто:

  1. Загружаем текущий курс валюты (exchange_now = pb.get_exchange(data['c'])).
  2. Генерируем текст нового сообщения путем сериализации текущего курса валют с параметром diff, который можно получить с помощью новых методов (о них дальше). Также нужно добавить подпись — get_edited_signature.
  3. Вызываем метод edit_message_text, если оригинальное сообщение не изменилось. Если это ответ на встроенный запрос, передаем другие параметры.

Метод get_ex_from_iq_data разбирает JSON из callback_data:

def get_ex_from_iq_data(exc_json):  
    return {  
        'buy': exc_json['b'],  
	'sale': exc_json['s']  
    }

Метод get_exchange_diff получает старое и текущее значение курсов валют и возвращает разницу в формате {'buy_diff': <float>, 'sale_diff': <float>}:

def get_exchange_diff(last, now):  
    return {  
        'sale_diff': float("%.6f" % (float(now['sale']) - float(last['sale']))),  
	'buy_diff': float("%.6f" % (float(now['buy']) - float(last['buy'])))  
    }

get_edited_signature генерирует текст “Updated…”:

def get_edited_signature():  
    return '<i>Updated ' + \  
           str(datetime.datetime.now(P_TIMEZONE).strftime('%H:%M:%S')) + \  
           ' (' + TIMEZONE_COMMON_NAME + ')</i>'

Вот как выглядит сообщение после обновления, если курсы валют не изменились:

отображение курса валют ботом

И вот так — если изменились:

обновление курса валют ботом

Шаг №9: реализовать встроенный режим

Реализация встроенного режима значит, что если пользователь введет @ + имя бота в любом чате, это активирует поиск введенного текста и выведет результаты. После нажатия на один из них бот отправит результат от вашего имени (с пометкой “via bot”).

@bot.inline_handler(func=lambda query: True)  
def query_text(inline_query):  
    bot.answer_inline_query(  
        inline_query.id,  
        get_iq_articles(pb.get_exchanges(inline_query.query))  
    )

Обработчик встроенных запросов реализован.

Библиотека передаст объект InlineQuery в функцию query_text. Внутри используется функция answer_line, которая должна получить inline_query_id и массив объектов (результаты поиска).

Используем get_exchanges для поиска нескольких валют, подходящих под запрос. Нужно передать этот массив методу get_iq_articles, который вернет массив из InlineQueryResultArticle:

def get_iq_articles(exchanges):  
    result = []  
    for exc in exchanges:  
        result.append(  
            telebot.types.InlineQueryResultArticle(  
                id=exc['ccy'],  
	        title=exc['ccy'],  
	        input_message_content=telebot.types.InputTextMessageContent(  
                    serialize_ex(exc),  
		    parse_mode='HTML'  
		),  
	        reply_markup=get_update_keyboard(exc),  
	        description='Convert ' + exc['base_ccy'] + ' -> ' + exc['ccy'],  
	        thumb_height=1  
	    )  
        )   
    return result

Теперь при вводе “@exchangetestbost + пробел” вы увидите следующее:

инлайн режим бота

Попробуем набрать usd, и результат мгновенно отфильтруется:

инлайн режим бота: ввод параметра

Проверим предложенный результат:

инлайн режим бота: результат

Кнопка “Update” тоже работает:

инлайн режим бота: работа

Отличная работа! Вы реализовали встроенный режим!

Выводы

Поздравляем! Теперь вы знаете, как сделать бота для Telegram, добавить встроенную клавиатуру, обновление сообщений и встроенный режим. Можете похлопать себя по спине и поднять тост за нового бота.

Тест на знание python

Что выведет этот код?
Что выведет этот код?
Что нужно вcтавить после "if", для вывода "x четное число"
Как узнать длину списка?
Какая из следующих функций проверяет, что все символы строки в верхнем регистре?