Blog/content/posts/2023/python/howto-make-telegram-bot.md
2023-05-28 04:36:10 +03:00

650 lines
29 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
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 bots 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="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': ..., '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::**
Отличная работа! Встроенный режим работает!