tg bot tutorial

This commit is contained in:
Alexander Popov 2023-05-28 04:36:10 +03:00
parent 1e3e193d50
commit a5e919a9b2
Signed by: iiiypuk
GPG Key ID: E47FE0AB36CD5ED6

View File

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