![](https://habrastorage.org/webt/wu/gu/te/wugutergqh4fjjgtj5i0ycuoxre.jpeg)
Я часто взаимодействую с ботами в Telegram. Чаще как пользователь, но создать собственного бота или потрогать чужого я не боюсь. При разработке собственного решения чувствуется, что бот не похож на GUI- или веб-приложение, но программисты тщательно превозмогают это чувство и делают так, как проще с точки зрения программирования.
В этой статье я расскажу про некоторые способы взаимодействия человека и бота в личных сообщениях и группах. Текст рассчитан на тех, кто только начинает изучать тему создания ботов, но, возможно, будет полезен и профессионалам.
Дисклеймер. Автор не является специалистом по UX. Изложенные тезисы не претендуют на звание лучших практик, а скорее показывают опыт автора, приобретенный на практике.
Шаблон
Статья практическая, поэтому предполагает фрагменты кода, которые наглядно продемонстрируют описанный подход. Для демонстрации я буду использовать свой основной язык программирования — Python. Итак, список требований:
- Python 3.9.
- Пакет python-telegram-bot версии 20.0a2 (python -m pip install python-telegram-bot==20.0a2).
- Созданный бот в Telegram и токен доступа. Для создания обратитесь к BotFather.
Хотя примеры будут на конкретном языке программирования с использованием специфической библиотеки, идея не зависит от деталей реализации. Примеры можно перенести на другие языки программирования.
Фреймворк python-telegram-bot основан на обработчиках. Ядро получает обновления (Update) от Telegram Bot API и вызывает соответствующий обработчик из списка зарегистрированных. Если подходящего обработчика нет, то событие игнорируется.
Рассмотрим шаблон на примере простого echo-бота, который отвечает вашим же текстом.
import logging
from telegram import Update
from telegram.ext import *
# Логирование
logging.basicConfig(
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO
)
logger = logging.getLogger(__name__)
# Функция-обработчик
async def echo(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
await update.message.reply_text(update.message.text)
# Создание объекта Бот
application = Application.builder().token("здесь ваш токен").build()
# Регистрация обработчика на текстовые сообщения, но не команды
application.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, echo))
# Запуск бота
application.run_polling()
Далее в примерах я буду приводить только функцию-обработчик и строку для регистрации обработчика.
После краткого введения приступим к обзору возможных взаимодействий с ботом. Бот — это программа, а программы должны исполнять команды. С них и начнем.
Команды
Команды в Telegram — это сообщения, начинающиеся со слэша (/). Примеры команд:
/start
/subscribe@ExampleArticleBot
Первый вид команд используется в личных сообщениях. Имя бота добавляется в группах, чтобы явно указать, какому боту дана команда. Если в групповом чате написать команду без имени бота, то команда отправится всем ботам.
![](https://habrastorage.org/webt/f2/mn/-c/f2mn-cjs-lhwxwfmnx_rf2g8ovc.png)
Команды — это хороший способ инициировать действие, так как список команд перечисляется в выпадающем меню чата с краткой справкой. При выборе команды сообщение отправляется незамедлительно. Это значит, что у команд не должно быть аргументов. Допустим, у нас бот в групповом чате с командой как на скриншоте выше, а команда принимает имя города через пробел. Таким образом, для получения погоды в Москве придется полностью напечатать следующий текст:
/weather@ExampleArticleBot Москва
Неудобно и отбивает всякое желание пользоваться ботом. Единственная команда, которая может получать аргументы, — это /start, и только при переходе по ссылке, которая выглядит следующим образом:
https://t.me/<имя_бота>?start=<строка>
В этом случае у пользователя появится кнопка START, даже если пользователь уже активировал бота. При нажатии кнопки в чат отправится сообщение
/start
, но бот получит сообщение /start <строка>
. Создадим обработчик аргументов команды start:async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
if update.message.text == "/start":
await update.message.reply_text("Start without arguments")
return
# Удаляем /start
arg = update.message.text[7:]
await update.message.reply_text(arg)
# Регистрация обработчика
application.add_handler(CommandHandler("start", start))
![](https://habrastorage.org/webt/_3/ro/yc/_3roycs2hc6-ftvdecgqpiqafvg.png)
Подобный подход позволяет боту задать первоначальный контекст обращения или помочь вести аналитику переходов, почти как UTM-метки.
Существенный минус команд проявляется в групповых чатах. Команды в отправленных сообщениях кликабельны, поэтому одна неосторожная команда может привести к лавине сообщений. Возможное решение — использование текста.
Текстовые сообщения
Специфичные команды можно представить в виде кодовых слов. Например, вместо /start запрограммировать бота реагировать на «Поехали!». Это отличное решение для ботов, которые в группах реагируют только на сообщения администраторов. Но есть в ложке меда бочка дегтя:
- Документация по командам бота должна распространяться отдельно.
- Программист должен учесть возможную вариативность сообщений.
- Бот должен иметь модификатор «имеет доступ к сообщениям», что может снизить доверие к боту.
Неожиданный сюжетный поворот: бот способен получать ответы на свои сообщения даже если в группе он «не имеет доступ к сообщениям». В python-telegram-bot для этого есть абстракция ConversationHandler.
![](https://habrastorage.org/webt/27/sd/u9/27sdu9qdcejwabnynwvgdx4se9q.png)
Получается хорошая комбинация из команды, которая инициирует запрос, и текстовых аргументов. При этом бот не получает доступ ко всем сообщениям в чате, что несколько повышает безопасность.
# Точка входа в диалог
async def weather(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
await update.message.reply_text("В каком городе хотите посмотреть погоду?")
return 1
# Обработка ответа
async def show_weather(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
city = update.message.text
await update.message.reply_text(
f"Вы хотите посмотреть погоду в городе {city}.\n"
f"\n"
f"Но я не умею показывать погоду, извините :("
)
return ConversationHandler.END
# Задаем точки входа и ветви диалога
handler = ConversationHandler(
entry_points=[CommandHandler("weather", weather)],
states={
1: [MessageHandler(filters.TEXT & ~filters.COMMAND, show_weather)]
},
fallbacks=[]
)
application.add_handler(handler)
В некоторых случаях бот предлагает несколько вариантов на выбор вместо свободного ввода. В этом случае уместно использовать кнопки.
Если вас интересует тема Telegram-ботов, посмотрите, что у нас есть еще на эту тему:
→ Как сделать бота для Telegram на облачных функциях
→ Как сгенерировать стикеры из сообщений в Telegram
Кнопки
![](https://habrastorage.org/webt/oq/g_/op/oqg_opng9ym6d0yw4aqh5keqpcy.png)
ReplyKeyboard в действии
В Telegram существует два вида кнопок, которые могут быть созданы сообщением от бота. Первый вид — ReplyKeyboard, заменяющий клавиатуру на сенсорных устройствах. Нажатие на эту кнопку отправляет в чат текст кнопки.
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
keyboard = [
["Кнопка 1", "Кнопка 2"],
["Большая привлекательная кнопка кнопка"]
]
await update.message.reply_text(
"Какую кнопку будем нажимать?",
reply_markup=ReplyKeyboardMarkup(
keyboard,
one_time_keyboard=False,
input_field_placeholder="Ваш выбор?"
)
)
application.add_handler(CommandHandler("start", start))
Такие кнопки можно использовать в том числе в групповых чатах. Современные клиенты Telegram автоматически отправляют сообщение в виде ответа на сообщение бота. При локализации бота придется менять обработчики для поддержки сообщений на разных языках. Также кнопки этой клавиатуры отвечают на последнее сообщение, которое создало клавиатуру.
![](https://habrastorage.org/webt/vz/ff/qf/vzffqf46eiz8wbfard7pehhkmv8.png)
Если хочется разные действия для нескольких сообщений одновременно, то на помощь приходит InlineKeyboard — клавиатура под сообщением.
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
keyboard = [
[
InlineKeyboardButton("Кнопка 1", callback_data="button-1"),
InlineKeyboardButton("Кнопка 2", callback_data="button-2")
],
[InlineKeyboardButton("Большая привлекательная кнопка кнопка", url="https://habr.com/")]
]
await update.message.reply_text(
"Какую кнопку будем нажимать?",
reply_markup=InlineKeyboardMarkup(keyboard)
)
async def weather(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
keyboard = [
[
InlineKeyboardButton("Санкт-Петербург", callback_data="LED"),
InlineKeyboardButton("Москва", callback_data="SVO"),
InlineKeyboardButton("Иркутск", callback_data="IKT")
]
]
await update.message.reply_text(
"Где хотите посмотреть погоду?",
reply_markup=InlineKeyboardMarkup(keyboard)
)
application.add_handler(CommandHandler("start", start))
application.add_handler(CommandHandler("weather", weather))
Кнопки встроенной клавиатуры разнообразны и могут содержать один из следующих элементов:
- callback_data — строка для специальных обработчиков, рассмотрим подробнее позже.
- url — ссылка на любой ресурс. Кнопка со ссылкой отмечается стрелкой в верхнем правом углу.
- inline_query — запускает inline-режим в указанном чате с текущим ботом. Наиболее известный бот с inline-режимом — gif.
- callback_game — ссылка на игру.
- web_app — ссылка на WebApp-приложение, доступно только в личных сообщениях.
- login_url — ссылка на аутентификацию в сервисе через Telegram.
- pay — ссылка на оплату счета через кошелек в Telegram.
![](https://habrastorage.org/webt/ck/yb/qx/ckybqxsnbtvbnxyctjdpi952crg.png)
Кнопки с действием, если это не callback_data, открывает новое окно с указанным действием. Если у сообщения одна кнопка, то она будет продублирована в закреплении, что может быть полезно в групповых чатах.
Максимальное количество кнопок под сообщением — 100, вне зависимости от компоновки. При превышении этого числа Telegram не выводит ошибку, но «лишние» кнопки не отображает.
Вернемся к обработке действия с callback_data. Нажатие на кнопку генерирует событие callback_query.
Обработка нажатия кнопки
![](https://habrastorage.org/webt/wq/x_/ef/wqx_efg1ozvwvelaf67joc3mqbm.png)
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
keyboard = [
[InlineKeyboardButton("❤️", callback_data="like-trex")]
]
await update.message.reply_text(
"Нажми лайк, чтобы поддержать Тирекса!",
reply_markup=InlineKeyboardMarkup(keyboard)
)
async def query(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
# Убираем кнопки
await update.callback_query.message.edit_reply_markup(None)
# Отмечаем, что мы обработали событие и выводим текст
text = f"Спасибо, {update.callback_query.from_user.full_name}, что поддержал Тирекса!"
await update.callback_query.answer(text, show_alert=True)
application.add_handler(CommandHandler("start", start))
application.add_handler(CallbackQueryHandler(query))
При нажатии на кнопку в верхнем правом углу появляется пиктограмма часов. Это значит, что событие передано боту. Обработчик callback_query может ответить разными способами:
- Обработать событие «тихо». На кнопке исчезнет пиктограмма часов.
- Ответить всплывающим текстом. Этот способ варьируется в зависимости от клиента, но идея заключается в появлении текста поверх чата на короткий промежуток времени.
- Ответить всплывающим окном. Текст отображается всплывающим окном с кнопкой «ОК».
- Открыть чат с пользователем по ссылке или запустить игру в Telegram по ссылке.
Не пытайтесь сделать в обработчике много действий с сообщением подряд. В одном из своих проектов я выяснил, что открепление сообщения и удаление кнопок под сообщением в одно время «роняет» Telegram Desktop на Windows и Linux. Я оставил сообщение об ошибке для разработчиков Telegram.
В одной из следующих статей я расскажу о том, как сделал бота для массового заказа шавермы в Selectel. А пока вы можете подписать на бота компании — он рассказывает о предстоящих мероприятиях компании.
![](https://habrastorage.org/webt/bi/ii/e9/biiie9qllcck6xqyg02-ic41apq.png)