29 KiB
title | date | draft | tags | ||||
---|---|---|---|---|---|---|---|
🤖 Howto Make Telegram Bot | 2023-05-28T04:02:05+03:00 | true |
|
Как создать чат-бота для Telegram с помощью Python
Статья является полной копией страницы https://pythonru.com/primery/python-telegram-bot.
Это пошаговое руководство по созданию бота для 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/getMe
{
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
лучше всего подходит, если:
- Вы не хотите или не можете настраивать HTTPS во время разработки.
- Вы работаете со скриптовыми языками, которые сложно интегрировать в веб-сервер.
- У бота высокая нагрузка.
- Вы меняете сервер бота время от времени.
Метод с Webhook лучше подойдет в таких случаях:
- Вы используете веб-языки (например, PHP).
- У бота низкая нагрузка, и нет смысла делать запросы вручную.
- Бот на постоянной основе интегрирован в веб-сервер.
- В этом руководстве будет использоваться метод
getUpdates
.
Еще один вопрос: как создать зарегистрировать бота?
@BotFather
используется для создания ботов в Telegram.
Он также отвечает за базовую настройку
(описание, фото профиля, встроенная поддержка и так далее).
В этом руководстве будет использоваться библиотека pyTelegramBotAPI.
Шаг № 1: реализовать запросы курсов валют
Начать стоит с написания Python-скрипта,
который будет реализовывать логику конкретных запросов курсов валют.
Использовать будем PrivatBank API
.
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/.
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 и ознакомиться с документацией библиотеки, чтобы лучше понимать то, о чем пойдет речь дальше.
Шаг № 3: настроить и запустить бота
Начнем с создания файла config.py
для настройки:
TOKEN = '' # заменить на токен своего бота
TIMEZONE = 'Europe/Moscow'
TIMEZONE_COMMON_NAME = 'Moscow'
В этом файле указаны: токен бота и часовой пояс, в котором тот будет работать (это понадобится в будущем для определения времени обновления сообщений. 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 = 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 @ 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 банка).
@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
во вложенную функцию.
@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 = '' + 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
он будет выглядеть следующим образом:
@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'
)
Как это работает? Очень просто:
- Загружаем текущий курс валюты (
exchange_now = pb.get_exchange(data['c'])
). - Генерируем текст нового сообщения путем
сериализации текущего курса валют с параметром
diff
, который можно получить с помощью новых методов (о них дальше). Также нужно добавить подпись —get_edited_signature
. - Вызываем метод
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': ..., 'sale_diff': ...}
:
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 'Updated ' + \
str(datetime.datetime.now(P_TIMEZONE).strftime('%H:%M:%S')) + \
' (' + TIMEZONE_COMMON_NAME + ')'
Вот как выглядит сообщение после обновления, если курсы валют не изменились:
::TODO::
И вот так — если изменились:
::TODO::
Шаг № 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 + пробел
вы увидите следующее:
::TODO::
Попробуем набрать usd
, и результат мгновенно отфильтруется:
::TODO::
Проверим предложенный результат:
::TODO::
Кнопка «Update» тоже работает:
::TODO::
Отличная работа! Встроенный режим работает!