From a5e919a9b2e7b582e4be4c6fc01c0d43da8bfb72 Mon Sep 17 00:00:00 2001 From: Alexander Popov Date: Sun, 28 May 2023 04:36:10 +0300 Subject: [PATCH] tg bot tutorial --- .../2023/python/howto-make-telegram-bot.md | 649 ++++++++++++++++++ 1 file changed, 649 insertions(+) create mode 100644 content/posts/2023/python/howto-make-telegram-bot.md diff --git a/content/posts/2023/python/howto-make-telegram-bot.md b/content/posts/2023/python/howto-make-telegram-bot.md new file mode 100644 index 0000000..e707060 --- /dev/null +++ b/content/posts/2023/python/howto-make-telegram-bot.md @@ -0,0 +1,649 @@ +--- +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::** + +Отличная работа! Встроенный режим работает!