Паттерны взаимодействия с ботами в Telegram: неочевидные практики на Python и баг в мессенджере

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


Первый вид команд используется в личных сообщениях. Имя бота добавляется в группах, чтобы явно указать, какому боту дана команда. Если в групповом чате написать команду без имени бота, то команда отправится всем ботам.

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))


_3roycs2hc6-ftvdecgqpiqafvg.png


Подобный подход позволяет боту задать первоначальный контекст обращения или помочь вести аналитику переходов, почти как UTM-метки.

Существенный минус команд проявляется в групповых чатах. Команды в отправленных сообщениях кликабельны, поэтому одна неосторожная команда может привести к лавине сообщений. Возможное решение — использование текста.

Текстовые сообщения


Специфичные команды можно представить в виде кодовых слов. Например, вместо /start запрограммировать бота реагировать на «Поехали!». Это отличное решение для ботов, которые в группах реагируют только на сообщения администраторов. Но есть в ложке меда бочка дегтя:

  • Документация по командам бота должна распространяться отдельно.
  • Программист должен учесть возможную вариативность сообщений.
  • Бот должен иметь модификатор «имеет доступ к сообщениям», что может снизить доверие к боту.


Неожиданный сюжетный поворот: бот способен получать ответы на свои сообщения даже если в группе он «не имеет доступ к сообщениям». В python-telegram-bot для этого есть абстракция ConversationHandler.

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


Кнопки


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 автоматически отправляют сообщение в виде ответа на сообщение бота. При локализации бота придется менять обработчики для поддержки сообщений на разных языках. Также кнопки этой клавиатуры отвечают на последнее сообщение, которое создало клавиатуру.

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.


ckybqxsnbtvbnxyctjdpi952crg.png


Кнопки с действием, если это не callback_data, открывает новое окно с указанным действием. Если у сообщения одна кнопка, то она будет продублирована в закреплении, что может быть полезно в групповых чатах.

Максимальное количество кнопок под сообщением — 100, вне зависимости от компоновки. При превышении этого числа Telegram не выводит ошибку, но «лишние» кнопки не отображает.


Вернемся к обработке действия с callback_data. Нажатие на кнопку генерирует событие callback_query.

Обработка нажатия кнопки


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. А пока вы можете подписать на бота компании — он рассказывает о предстоящих мероприятиях компании.

biiie9qllcck6xqyg02-ic41apq.png

© Habrahabr.ru