--- title: "🤖 Howto Make Telegram Bot" date: 2023-05-28T04:02:05+03:00 draft: true tags: [python, tutorial, telegram, bot] --- # Как создать чат-бота для Telegram с помощью Python > Статья является полной копией страницы https://pythonru.com/primery/python-telegram-bot. Это пошаговое руководство по созданию бота для Telegram. Бот будет показывать курсы валют, разницу между курсом раньше и сейчас, а также использовать современные встроенные клавиатуры. Время переходить к делу и узнать наконец, как создавать ботов в Telegram. ## Шаг № 0: немного теории об API Telegram-ботов Начать руководство стоит с простого вопроса: как создавать чат-ботов в Telegram? Ответ очень простой: для чтения сообщений отправленных пользователями и для отправки сообщений назад используется API HTML. Это требует использования URL: ```text https://api.telegram.org/bot/METHOD_NAME ``` `METHOD_NAME` — это метод, например, `getUpdates`, `sendMessage`, `getChat` и т.п. Токен — уникальная строка из символов, которая нужна для того, чтобы установить подлинность бота в системе. Токен генерируется при создании бота. Токен выглядит приблизительно так: ```text 123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11 ``` Для выполнения запросов используются как GET, так и POST запросы. Многие методы требуют дополнительных параметров (методу `sendMessage`, например, нужно передать `chat_id` и `текст`). Эти параметры могут быть переданы как строка запроса URL, `application/x-www-form-urlencoded` и `application-json` (кроме загрузки файлов). Еще одно требование — кодировка `UTF-8`. После отправки запроса к API, вы получаете ответ в формате JSON. Например, если извлечь данные с помощью метода `getME`, ответ будет такой: ```text GET https://api.telegram.org/bot/getMe ``` ```text { ok: true, result: { id: 231757398, first_name: "Exchange Rate Bot", username: "exchangetestbot" } } ``` Список всех типов данных и методов API Telegram-бота можно найти здесь https://core.telegram.org/bots/api. Следующий вопрос: как получать пользовательские сообщения? Есть два варианта. Первый — вручную создавать запросы с помощью метода `getUpdates`. В качестве объекта вы получите массив объектов `Update`. Этот метод работает как технология длинных опросов (long polling), когда вы отправляете запрос, обрабатываете данные и начинаете повторяете процесс. Чтобы избежать повторной обработки одних и тех же данных рекомендуется использовать параметр `offset`. Второй вариант — использовать `webhooks`. Метод `setWebhook` нужно будет применить только один раз. После этого Telegram будет отправлять все обновления на конкретный URL-адрес, как только они появятся. Единственное ограничение — необходим HTTPS, но можно использовать и сертификаты, заверенные самостоятельно. Как выбрать оптимальный метод? Метод `getUpdates` лучше всего подходит, если: 1. Вы не хотите или не можете настраивать HTTPS во время разработки. 2. Вы работаете со скриптовыми языками, которые сложно интегрировать в веб-сервер. 3. У бота высокая нагрузка. 4. Вы меняете сервер бота время от времени. Метод с Webhook лучше подойдет в таких случаях: 1. Вы используете веб-языки (например, PHP). 2. У бота низкая нагрузка, и нет смысла делать запросы вручную. 3. Бот на постоянной основе интегрирован в веб-сервер. 4. В этом руководстве будет использоваться метод `getUpdates`. Еще один вопрос: как создать зарегистрировать бота? `@BotFather` используется для создания ботов в Telegram. Он также отвечает за базовую настройку (описание, фото профиля, встроенная поддержка и так далее). В этом руководстве будет использоваться библиотека [pyTelegramBotAPI](https://github.com/eternnoir/pyTelegramBotAPI). ## Шаг № 1: реализовать запросы курсов валют Начать стоит с написания Python-скрипта, который будет реализовывать логику конкретных запросов курсов валют. Использовать будем `PrivatBank API`. ```text https://api.privatbank.ua/p24api/pubinfo?json&exchange&coursid=5 ``` Пример ответа: ```text [ { 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` со следующим кодом: ```python 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 ``` Были реализованы три метода: 1. `load_exchange`: загружает курсы валют по указанному URL-адресу и возвращает их в формате словаря(dict). 2. `get_exchange`: возвращает курсы валют по запрошенной валюте. 3. `get_exchanges`: возвращает список валют в соответствии с шаблоном (требуется для поиска валют во встроенных запросах). ## Шаг № 2: создать Telegram-бота с помощью `@BotFather` Необходимо подключиться к боту `@BotFather`, чтобы получить список чат-команд в Telegram. Далее нужно набрать команду `/newbot` для инструкций выбора название и имени бота. После успешного создания бота вы получите следующее сообщение: ```text Done! Congratulations on your new bot. You will find it at telegram.me/. 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: (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](https://core.telegram.org/bots/api#authorizing-your-bot) и ознакомиться с документацией библиотеки, чтобы лучше понимать то, о чем пойдет речь дальше. ## Шаг № 3: настроить и запустить бота Начнем с создания файла `config.py` для настройки: ```python TOKEN = '' # заменить на токен своего бота TIMEZONE = 'Europe/Moscow' TIMEZONE_COMMON_NAME = 'Moscow' ``` В этом файле указаны: токен бота и часовой пояс, в котором тот будет работать (это понадобится в будущем для определения времени обновления сообщений. API Telegram не позволяет видеть временную зону пользователя, поэтому время обновления должно отображаться с подсказкой о часовом поясе). Создадим файл `bot.py`. Нужно импортировать все необходимые библиотеки, файлы с настройками и предварительно созданный `pb.py`. Если каких-то библиотек не хватает, их можно установить с помощью `pip`. ```python 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`. Для этого конструктору нужно передать токен: ```python 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)`: ```python @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»**. ```python @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 @ 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'`. Результат выше выглядит вот так: **::TODO::** ## Шаг № 6: добавить обработчик команды `/exchange` Обработчик команды `/exchange` отображает меню выбора валюты и встроенную клавиатуру с 3 кнопками: `USD`, `EUR` и `RUR` (это валюты, поддерживаемые API банка). ```python @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`: **::TODO::** ## Шаг № 7: написать обработчик для кнопок встроенной клавиатуры В библиотеке pyTelegramBotAPI есть декоратор `@bot.callback_query_handler`, который передает объект `CallbackQuery` во вложенную функцию. ```python @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`: ```python 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`: ```python 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»). ```python 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`. ```python def serialize_ex(ex_json, diff=None): result = '' + ex_json['base_ccy'] + ' -> ' + ex_json['ccy'] + ':\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=" 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=" 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': ..., 'sale_diff': ...}`. Это будет происходить во время сериализации после нажатия кнопки `Update`. Когда курсы валют отображаются первый раз, он нам не нужен. Вот как будет выглядеть результат выполнения после нажатия кнопки USD: **::TODO::** ## Шаг № 8: реализовать обработчик кнопки обновления Теперь можно создать обработчик кнопки `Update`. После дополнения метода `iq_callback_method` он будет выглядеть следующим образом: ```python @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`. Реализуем это: ```python 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`: ```python def get_ex_from_iq_data(exc_json): return { 'buy': exc_json['b'], 'sale': exc_json['s'] } ``` Метод `get_exchange_diff` получает старое и текущее значение курсов валют и возвращает разницу в формате `{'buy_diff': ..., 'sale_diff': ...}`: ```python 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…»: ```python def get_edited_signature(): return 'Updated ' + \ str(datetime.datetime.now(P_TIMEZONE).strftime('%H:%M:%S')) + \ ' (' + TIMEZONE_COMMON_NAME + ')' ``` Вот как выглядит сообщение после обновления, если курсы валют не изменились: **::TODO::** И вот так — если изменились: **::TODO::** ## Шаг № 9: реализовать встроенный режим Реализация встроенного режима значит, что если пользователь введет `@ + имя бота` в любом чате, это активирует поиск введенного текста и выведет результаты. После нажатия на один из них бот отправит результат от вашего имени (с пометкой «via bot»). ```python @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`: ```python 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 + пробел` вы увидите следующее: **::TODO::** Попробуем набрать `usd`, и результат мгновенно отфильтруется: **::TODO::** Проверим предложенный результат: **::TODO::** Кнопка «Update» тоже работает: **::TODO::** Отличная работа! Встроенный режим работает!