Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 16 из 25 ~18 мин +20 XP

Редактирование сообщений и многостраничное меню

Редактирование сообщений в telegram-боте на Go строится на EditMessageText: бот не шлёт новую порцию текста в чат, а переписывает уже отправленное сообщение по паре chatID и messageID. Здесь разберём правку текста из callback-нажатия, дизайн callback_data для многостраничного меню, пагинацию списка по кнопке «Ещё» и ошибку message is not modified, которая ловит каждого, кто делает меню с кнопками.

Содержание урока · 9 разделов

Меню бота-дневника из урока про inline-клавиатуры устроено честно, но расточительно: нажатие кнопки «Статистика» отправляет новое сообщение с цифрами, нажатие «Настройки» — ещё одно, справка — третье. Через неделю диалог выглядит как стопка карточек, где актуальна только верхняя, а пользователь скроллит назад в поисках кнопок, которые уже утонули в истории. Так ведут себя боты, которые не умеют редактировать: каждое действие порождает новый объект в чате вместо перерисовки старого. Telegram давно придумал механизм получше — правку уже отправленного сообщения на месте. Чат с таким ботом занимает один экран: кнопки при каждом нажатии перерисовывают тот же блок текста, как страницы приложения. Так работают меню самого BotFather, пагинация каталогов и настройки аккуратных ботов. На Go механика держится на трёх вещах: адресе сообщения из двух полей, конструкторе NewEditMessageText и схеме callback_data, которая понимает, какую страницу рисовать. Ниже — правка из callback-нажатия, многостраничное меню без серверного состояния, кейс со списком записей и границы метода.

EditMessageText: правка по chatID и messageID#

У каждого сообщения, отправленного ботом, есть два адресных поля: chatID — куда, и messageID — номер сообщения внутри этого чата. Метод editMessageText принимает оба и новый текст, после чего сервер Telegram заменяет содержимое исходного сообщения у всех его читателей. Сроков для правки собственных сообщений у бота нет: аккаунт человека в официальных клиентах ограничен окном в 48 часов, а бот может переписать своё сообщение и через месяц — на этом строятся живые дашборды и длинные меню. Ограничение в другом: редактировать можно только то, что отправил сам бот, плюс инлайн-сообщения по их inlineMessageID. Чужой трафик — сообщения людей — недоступен в принципе, и это правильно. В callback-нажатии оба адресных поля уже лежат готовые: update.CallbackQuery.Message содержит Chat.ID и MessageID того сообщения, к которому прикреплена клавиатура. Отдельный поиск по истории чата не нужен: нажатие кнопки само говорит, какой объект перерисовать. Если сообщение инлайн-результата из пятнадцатого урока, у него вместо пары chatID и messageID отдельный идентификатор inlineMessageID, и правка идёт через тот же метод.

Правка из callback-нажатия

GO
if update.CallbackQuery != nil {
        cq := update.CallbackQuery
        if cq.Data == "menu:stats" {
                edit := tgbotapi.NewEditMessageText(
                        cq.Message.Chat.ID,
                        cq.Message.MessageID,
                        "Записей за неделю: 12\nСерия: 4 дня",
                )
                bot.Send(edit)
        }
        bot.AnswerCallbackQuery(tgbotapi.NewCallback(cq.ID, ""))
}

Данные нажатия приходят в cq.Data, а адрес правки — в cq.Message: это то же сообщение с клавиатурой, которое надо переписать. NewEditMessageText собирает конфигурацию правки, bot.Send отдаёт её API, и текст заменяется без создания новых сообщений. AnswerCallbackQuery закрывает запрос, чтобы на кнопке не крутились часики — правило урока про inline-клавиатуры действует и здесь.

Дизайн callback_data для многостраничного меню#

Меню с несколькими экранами сводится к двум вопросам: какое действие выбрал человек и на какой странице он находится. Ответ живёт в строке callback_data, и от её схемы зависит сложность обработчика. Рабочий формат — путь с разделителем: menu:stats, help:2, settings:notifications. В обработчике строка разбирается strings.Split по двоеточию, первый сегмент выбирает экран, второй — параметр. Лимит поля — 64 байта, поэтому в callback_data кладут только адрес, а не содержимое страницы: текст и клавиатура генерируются в момент нажатия из ваших данных. Из этой разведённости следует приятное следствие — многостраничное меню обходится вообще без серверного состояния. Страница help:2 рендерится функцией от номера и клавиатурой, где Назад и Вперёд несут в callback_data адреса соседних экранов. Нажатие пришло — бот понял адрес, отрисовал страницу и перезаписал сообщение; хранить что-либо между нажатиями не требуется, и состояние диалога из десятого урока тут не нужно. Это та же логика, что и у URL в вебе: адрес описывает экран, а не результат.

Справка из трёх страниц

GO
var helpPages = map[int]string{
        1: "Команды\n/today, /stats, /export",
        2: "Формат записи\nбег 5 км, темп 6:10",
        3: "Приватность\nданные только у вас",
}

func renderHelp(page int) (string, tgbotapi.InlineKeyboardMarkup) {
        text := helpPages[page]
        row := tgbotapi.NewInlineKeyboardRow(
                tgbotapi.NewInlineKeyboardButtonData("Назад",
                        fmt.Sprintf("help:%d", page-1)),
                tgbotapi.NewInlineKeyboardButtonData("Вперёд",
                        fmt.Sprintf("help:%d", page+1)),
        )
        return text, tgbotapi.NewInlineKeyboardMarkup(row)
}

Содержимое страниц — обычная карта, клавиатура собирается из двух кнопок, в callback_data которых зашиты адреса соседних страниц. fmt.Sprintf подставляет номер в строку адреса, и обработчик help:N понимает, что рендерить. Границы проверяются до сборки кнопок: на первой странице Назад не рисуется, на последней — Вперёд, иначе клик уведёт в пустую страницу.

Что ещё умеет правка и где её границы#

Кроме текста, у сообщения можно переписать подпись медиа — editMessageCaption через NewEditMessageCaption — и клавиатуру — editMessageReplyMarkup через NewEditMessageReplyMarkup. Для замены вложения целиком есть editMessageMedia с NewEditMessageMediaConfig: карточка товара меняет фото, не теряя позиции в чате. Все варианты адресуются той же парой chatID и messageID и упираются в то же правило: правится только исходящий трафик бота. Разметка нового текста включается отдельно: у конфигурации правки есть поле ParseMode, и для жирного с кодом ставится tgbotapi.ModeHTML из урока про форматирование, а пользовательские вставки по-прежнему проходят html.EscapeString. Живую перерисовку проверяет только настоящий Telegram: локальный Go с реальным токеном, сообщение с кнопками и два нажатия подряд покажут то, чего песочница не покажет, — скорость правки и поведение часиков на кнопке.

Спорное: не всё стоит рисовать в одном сообщении#

Освоив правку, новички тащат в неё весь трафик: статистика перерисовывается на месте, список записей тоже, история диалога исчезает. Я считаю это перебором, и граница у меня такая: правка — для интерфейсов, новое сообщение — для контента. Меню, настройки, карточка записи с кнопками действий — правильные кандидаты: следующий экран отменяет предыдущий, и оставлять старый в истории незачем. А список записей за неделю человек через месяц захочет перечитать — и найдёт его в чате, только если бот отправил его отдельным сообщением: правка затирает прежнее состояние без следа, и лента диалога остаётся единственным архивом. Практическое правило: если следующий экран отменяет предыдущий — редактируем; если дополняет — отправляем новое сообщение. Тогда диалог выглядит как лента результатов, поверх которой живёт одно перерисовываемое меню, и жалобы не приходят ни на стену сообщений, ни на потерянную историю. Правило вкусовое, но после трёх ботов закрепляется намертво.

Кейс: статистика с кнопкой «Ещё»#

Бот-дневник печатает записи за неделю, а записей пятнадцать — в одно сообщение они не помещаются красиво. Решение через правку: команда /stats отправляет первые пять записей и inline-клавиатуру с кнопкой «Ещё», несущей callback_data вида stats:2. Нажатие рендерит вторую страницу — следующие пять записей — и правит сообщение, дописывая их к тексту. Третий клик дополняет третью страницу, после чего кнопка исчезает: записей больше нет. Состояние живёт в самой callback_data — там только номер страницы, — и между нажатиями запоминать нечего: каждое нажатие самодостаточно. Подвох один: если пользователь в это время добавит новую запись, страницы поедут, и кнопка «Ещё» покажет запись дважды — для учебного бота терпимо, а для товарного каталога страницы стоит фиксировать на момент первого рендера. Вариант с новыми сообщениями здесь проигрывает сразу: пять кликов — пять сообщений-заготовок, чат захламлён, а правка держит весь список в одном блоке, который скроллится пальцем.

Методы редактирования в tgbotapi#

Метод APIКонструкторЧто правит
editMessageTextNewEditMessageTextтекст сообщения
editMessageCaptionNewEditMessageCaptionподпись медиа
editMessageReplyMarkupNewEditMessageReplyMarkupклавиатуру под сообщением
editMessageMediaNewEditMessageMediaConfigвложение целиком
stopPollNewStopPollостанавливает голосование

Проверка: адрес правки

Что нужно, чтобы переписать текст уже отправленного ботом сообщения?

Проверка: повторное нажатие

Пользователь второй раз нажал кнопку, текст и клавиатура не изменились. Что вернёт API?

Что выведет разбор callback_data

Прочитайте код и выберите, что он выведет:

data := "help:2"
parts := strings.Split(data, ":")
fmt.Println(parts[0], parts[1])

Соберите правку сообщения

Впишите поле callback-запроса, конструктор правки и метод отправки.

cq := update. edit := tgbotapi.(cq.Message.Chat.ID, cq.Message.MessageID, text) bot.(edit)

Шесть строк перерисовки меню

Расставьте обработку нажатия: от чтения callback до ответа на запрос, как в меню из кейса.

Строки

Ваша программа

Нажимайте на строки слева, чтобы собрать программу

Карточки: редактирование сообщений

Термин

EditMessageText

нажмите, чтобы перевернуть
Определение

Метод Bot API: переписывает текст отправленного ботом сообщения по паре chatID и messageID. Возвращает обновлённое сообщение.

1 / 6

Меню научилось перерисовывать само себя — следующий шаг вынести бота из цикла обновлений: урок про webhook превращает бота в HTTP-сервер, а перед этим стоит привести в порядок работу с ошибками API — урок про лимиты и 429 рассказывает, что вернёт Send, когда скорость выше дозволенной. Механику callback-запросов напомнит урок про inline-клавиатуры, а правка подписей пригодится в медиа-уроке, когда карточка с фото перерисовывает описание. Работа с картами и срезами разобрана в уроке базового курса. Полный маршрут курса — на его странице.

Песочница урока

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.

Вывод

Запустите код, чтобы увидеть вывод.

Коротко: что запомнить

  • Правка строится на EditMessageText: адрес — пара chatID и messageID, новый текст приходит третьим аргументом конструктора.
  • В callback-нажатии адрес уже готов: CallbackQuery.Message несёт Chat.ID и MessageID того сообщения, где кнопка, а инлайн-сообщения адресуются inlineMessageID.
  • callback_data — это адрес экрана вида help:2, а не содержимое страницы: лимит 64 байта, рендер в момент нажатия.
  • Многостраничное меню обходится без серверного состояния: страница и её клавиатура — чистые функции от номера, тесты пишутся без Telegram.
  • message is not modified — защита от бессмысленной правки; сравнивайте тексты до отправки или глотайте ошибку по описанию.
  • Правится только исходящий трафик бота; у аккаунтов людей окно правки 48 часов, у бота срока нет.
  • Правка — для интерфейсов, новое сообщение — для контента: следующий экран отменяет предыдущий или дополняет его.

Домашнее задание

Соберите в локальном боте трёхстраничную справку: страницы в карте, кнопки Назад и Вперёд с callback_data вида help:N, правка через NewEditMessageText по адресу из CallbackQuery.Message. Проверьте три вещи: переход по страницам перерисовывает одно и то же сообщение, повторное нажатие кнопки текущей страницы возвращает message is not modified, и эта ошибка обрабатывается в логе без шума. Затем добавьте пагинацию со списком: пять записей за раз и кнопка «Ещё» с адресом stats:N, которая дописывает следующую порцию к тексту.

Личные заметкиПусто
Хранятся только в вашем браузере (Telegram · Редактирование сообщений и многостраничное меню)

Вопросы и ответы по уроку

Можно ли править сообщение пользователя, а не своё?

Нет: editMessageText работает только с исходящими сообщениями бота и инлайн-сообщениями. Чужие сообщения недоступны для правки — это ограничение платформы, а не библиотеки. Если нужно отреагировать на реплику человека, бот отвечает отдельным сообщением или реакцией из двадцать второго урока.

Куда девать содержимое страниц, если callback_data маленькая?

В саму callback_data кладётся только адрес: номер страницы или ключ экрана. Содержимое хранится в карте, базе или Redis и рендерится функцией в момент нажатия — так меню не боится длинных текстов и остаётся совместимым с любым хранилищем из одиннадцатого урока.

Почему при правке теряется ParseMode, если его не указать?

Правка заменяет содержимое целиком, включая оформление: у конфигурации свой ParseMode, и без него теги уйдут как текст. Каждый вызов правки выставляет tgbotapi.ModeHTML заново, если текст с разметкой.

Термины урока

Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.

Следующая ступень учебного пути

Prometheus25 уроков~12 чпрактика25 уроков уже доступны

Мониторинг Go с Prometheus: метрики, Grafana, алерты

Курс по метрикам в Go: Counter, Gauge, Histogram, лейблы, PromQL, Grafana-дашборды, алерты и бизнес-метрики. 25 уроков с примерами client_golang.

  • Понимать четыре типа метрик и выбирать правильный
  • Экспортировать метрики из Go через promhttp
  • Инструментировать HTTP-запросы и бизнес-события
  • Писать PromQL-запросы: rate, sum, p99