Урок 9. Bot API v2: Специальные кнопки, опять редактирование сообщений, кэшированный инлайн.

⚠️ Основная и более полная версия учебника «переехала» на Github рядом к исходникам ботов: https://mastergroosha.github.io/telegram-tutorial/

Продолжаю рассказывать о нововведениях в Bot API версии 2. Я не буду рассказывать о методах getChat, getChatMember и т.д., которые появились в обновлении 2.1: они интуитивно понятны и особых проблем не вызывают. Вопросы могут возникнуть при изучении специальных обычных кнопок, вроде тех, что запрашивают у вас номер телефона и геолокацию, при попытке получить отредактированное сообщение, а также при работе с уже загруженными в облако объектами с инлайн-режимом. Обо всём по порядку.

Специальные кнопки

Некоторым ботам жизненно необходим ваш номер телефона или местоположение, например, для привязки к учётным записям на других сайтах или же поиска близлежащих объектов на карте. Разработчики Telegram прислушались к мнению ботоводов и добавили особые свойства обычным (не инлайновым) кнопкам. Итак, чтобы запросить номер телефона, нужно помимо аргумента text передать аргумент request_contact=True, а для геолокации, соответственно, request_location=True. Обратите внимание, что одновременно у кнопки может быть не больше одного особого свойства (можно не указывать никакой), а также что специальные кнопки могут быть отправлены только в диалоги (бот-человек). Напишем код, который на команду /geophone отправит нам клавиатуру с этими кнопками.

# не забудьте про from telebot import types
@bot.message_handler(commands=["geophone"])
def geophone(message):
# Эти параметры для клавиатуры необязательны, просто для удобства
keyboard = types.ReplyKeyboardMarkup(row_width=1, resize_keyboard=True)
button_phone = types.KeyboardButton(text="Отправить номер телефона", request_contact=True)
button_geo = types.KeyboardButton(text="Отправить местоположение", request_location=True)
keyboard.add(button_phone, button_geo)
bot.send_message(message.chat.id, "Отправь мне свой номер телефона или поделись местоположением, жалкий человечишка!", reply_markup=keyboard)

При нажатии на кнопку отправки номера телефона сервер вернёт объект Message с непустым типом Contact, а при нажатии на кнопку отправки геолокации – с непустым типом Location.

Важно: если вы используете конечные автоматы или любой другой механизм состояний, который будет ждать от пользователя его телефон в объекте Contact, помните, что ушлый юзер может попробовать обмануть бота и скинуть любой другой контакт из записной книжки. Чтобы убедиться, что номер телефона принадлежит именно этому конкретному пользователю, сравните user_id в объекте from с user_id в объекте Contact, они должны совпадать.

Редактирование сообщений пользователями

Начиная с мая 2016 года, пользователи могут редактировать свои сообщения, а боты могут видеть исправления. Как им в этом помочь, давайте разберёмся вместе. В качестве примера заставим нашего бота отвечать на ругательства. К примеру, если пользователь пишет "дурак", бот ответит "сам дурак". Хитрые люди могут попробовать отредактировать своё сообщение и выставить бота в дурном свете, но мы будем изменять ответ бота под пользовательский текст.

Для отслеживания изменений, у нас в копилке появился новый тип хэндлеров – edited_message_handler, который настраивается точно так же, как и message_handler, просто "ловит" он только те сообщения, которые отредактированы. Что ж, ничего сложного, пишем!

@bot.message_handler(func=lambda message: True)
def any_message(message):
bot.reply_to(message, "Сам {!s}".format(message.text))
@bot.edited_message_handler(func=lambda message: True)
def edit_message(message):
bot.edit_message_text(chat_id=message.chat.id,
text= "Сам {!s}".format(message.text),
message_id=message.message_id + 1)

Заметили? Да, при вызове edit_message_text надо указать message_id на единицу бОльший, чем тот, который прислан сервером, потому что сервер сообщает о сообщении от пользователя, а нам нужно редактировать сообщение бота, которое шло за ним следом. И вот как это будет выглядеть (это одни и те же сообщения, что видно по метке "изм." около моего)

обмен любезностями

Кэшированный инлайн

Когда инлайн-боты только появились, то в качестве источника данных для ответов надо было указывать внешние ссылки, причем с ограничениями по размеру указываемого файла. Очевидно, такой подход мог быть не очень быстрым, а чем дольше пользователь ждёт, тем он менее доволен результатами работы бота :) В итоге, в Bot API v2 инлайн-режиму разрешили в качестве источника для медиа использовать file_id уже имеющихся на сервере файлов (напомню, что file_id для одного и того же файла будут разниться от бота к боту) Итак, у меня есть file_id двух фотографий с капибарами (как получить file_id загружаемых боту фотографий, считайте это заданием для самоподготовки), надо на любой инлайн-запрос (даже пустой) предложить эти 2 изображения. По сути, всё сводится к замене типа InlineQueryResultPhoto на тип InlineQueryResultCachedPhoto

@bot.inline_handler(func=lambda query: True)
def inline_mode(query):
capibara1 = types.InlineQueryResultCachedPhoto(
id="1",
photo_file_id="AgADAgAD6rMxGyBnGwABgBmcoHgy01IENAAQSYK_1gyoAAU-5aQACAg",
caption="Это капибара №1"
)
capibara2 = types.InlineQueryResultCachedPhoto(
id="2",
photo_file_id="AgADAgAD67MxGyBnGwABCvqPIYxMoNHENAAS51HjO88y_Z0ffAQABAg",
caption="Это капибара №2"
)
bot.answer_inline_query(query.id, [capibara1, capibara2])

Запускаем бота. Ура, теперь мы умеем очень быстро предлагать разных капибар нашим пользователям :)

каталог капибар? о_О

Помимо фотографий, из "кэша" можно показывать любые типы, поддерживаемые мессенджером: видео, аудио, стикеры, файлы (пока что только pdf и zip).

На этом всё. Хороших ботов!