Форматированный текст, replies и inline-кнопки
Результат
Вы ответите на конкретное сообщение, покажете inline-кнопку, получите
callback_query, подтвердите нажатие и отредактируете отправленное ботом
сообщение.
Что понадобится
- пройденный быстрый старт;
- запущенный gateway профиля
telegram-v1-core-preview-4на127.0.0.1:8081; - открытый диалог пользователя с ботом;
- Bash, Python 3,
curlиjq.
export SKREPA_BOT_API='http://127.0.0.1:8081'
export SKREPA_BOT_TOKEN='ваш-token-из-BotFather'
set -o pipefail
Верните runtime в polling mode, если ранее настраивали webhook:
curl --fail-with-body -sS -X POST \
"$SKREPA_BOT_API/bot${SKREPA_BOT_TOKEN}/deleteWebhook" | jq .
1. Получите сообщение, на которое ответит бот
Отправьте боту Покажи меню из клиента, затем выполните:
wait_for_update() {
local kind="$1" payload="$2" expected_data="${3-}" response kinds
while :; do
if ! response="$(curl --fail-with-body -sS \
-H 'content-type: application/json' \
-d "$payload" \
"$SKREPA_BOT_API/bot${SKREPA_BOT_TOKEN}/getUpdates")"
then
printf 'getUpdates завершился ошибкой; остановите сценарий.\n' >&2
return 1
fi
if printf '%s\n' "$response" | jq -e '.result | length == 0' >/dev/null
then
printf 'Нужное событие пока не пришло; продолжаю ждать.\n' >&2
continue
fi
if printf '%s\n' "$response" \
| jq -e --arg kind "$kind" \
'.ok == true and (.result | length == 1)
and (.result[0][$kind] != null)' >/dev/null
then
if [ -n "$expected_data" ] && ! printf '%s\n' "$response" \
| jq -e --arg data "$expected_data" \
'.result[0].callback_query.data == $data' >/dev/null
then
printf 'В начале очереди другой callback; offset не изменён. Обработайте его отдельным consumer.\n' >&2
return 2
fi
printf '%s\n' "$response"
return 0
fi
kinds="$(printf '%s\n' "$response" \
| jq -r '.result[0] | keys - ["update_id"] | join(", ")')"
printf 'В начале очереди update другого типа (%s); offset не изменён. Обработайте его отдельным consumer.\n' "$kinds" >&2
return 2
done
}
if UPDATES="$(wait_for_update message \
'{"limit":1,"timeout":30,"allowed_updates":["message","callback_query"]}')"
then
CHAT_ID="$(printf '%s\n' "$UPDATES" | jq -er '.result[0].message.chat.id')"
INCOMING_MESSAGE_ID="$(printf '%s\n' "$UPDATES" \
| jq -er '.result[0].message.message_id')"
LAST_UPDATE_ID="$(printf '%s\n' "$UPDATES" | jq -er '.result[0].update_id')"
else
printf 'Устраните ошибку выше, затем повторите шаг 1.\n' >&2
fi
Не переходите к следующему шагу, если функция сообщила об ошибке или о старом update другого типа.
2. Покажите действие и меню
Индикатор набора текста не создаёт сообщения в истории:
curl --fail-with-body -sS \
-H 'content-type: application/json' \
-d "{\"chat_id\":${CHAT_ID},\"action\":\"typing\"}" \
"$SKREPA_BOT_API/bot${SKREPA_BOT_TOKEN}/sendChatAction" | jq .
Отправьте HTML-текст как reply и добавьте callback-кнопку:
SENT="$(curl --fail-with-body -sS \
-H 'content-type: application/json' \
-d "$(jq -n \
--argjson chat_id "$CHAT_ID" \
--argjson reply_to "$INCOMING_MESSAGE_ID" \
'{chat_id:$chat_id,
text:"<b>Меню готово</b>",
parse_mode:"HTML",
reply_parameters:{message_id:$reply_to},
reply_markup:{inline_keyboard:[[
{text:"Выполнить", callback_data:"run"}
]]}}')" \
"$SKREPA_BOT_API/bot${SKREPA_BOT_TOKEN}/sendMessage")"
printf '%s\n' "$SENT" | jq .
BOT_MESSAGE_ID="$(printf '%s\n' "$SENT" | jq -er '.result.message_id')"
В клиенте появится сообщение с жирным текстом, ссылкой на исходное сообщение и кнопкой «Выполнить».
3. Получите и подтвердите callback
Запустите long poll, затем нажмите кнопку в клиенте:
NEXT_OFFSET="$(python3 -c 'import sys; print(int(sys.argv[1]) + 1)' "$LAST_UPDATE_ID")"
if CALLBACKS="$(wait_for_update callback_query \
"{\"offset\":${NEXT_OFFSET},\"limit\":1,\"timeout\":30,
\"allowed_updates\":[\"message\",\"callback_query\"]}" run)"
then
printf '%s\n' "$CALLBACKS" | jq .
CALLBACK_ID="$(printf '%s\n' "$CALLBACKS" \
| jq -er '.result[0].callback_query.id')"
CALLBACK_UPDATE_ID="$(printf '%s\n' "$CALLBACKS" \
| jq -er '.result[0].update_id')"
else
printf 'Устраните ошибку выше, затем повторите шаг 3 с тем же offset.\n' >&2
fi
Подтвердите нажатие. Клиент покажет короткое уведомление:
curl --fail-with-body -sS \
-H 'content-type: application/json' \
-d "$(jq -n --arg id "$CALLBACK_ID" \
'{callback_query_id:$id, text:"Готово"}')" \
"$SKREPA_BOT_API/bot${SKREPA_BOT_TOKEN}/answerCallbackQuery" | jq .
4. Отредактируйте сообщение
curl --fail-with-body -sS \
-H 'content-type: application/json' \
-d "$(jq -n \
--argjson chat_id "$CHAT_ID" \
--argjson message_id "$BOT_MESSAGE_ID" \
'{chat_id:$chat_id, message_id:$message_id,
text:"<i>Действие выполнено</i>", parse_mode:"HTML"}')" \
"$SKREPA_BOT_API/bot${SKREPA_BOT_TOKEN}/editMessageText" | jq .
Клиент заменит текст исходного сообщения.
После успешной обработки подтвердите callback update:
ACK_OFFSET="$(python3 -c 'import sys; print(int(sys.argv[1]) + 1)' "$CALLBACK_UPDATE_ID")"
curl --fail-with-body -sS \
-H 'content-type: application/json' \
-d "{\"offset\":${ACK_OFFSET},\"limit\":100,\"timeout\":0}" \
"$SKREPA_BOT_API/bot${SKREPA_BOT_TOKEN}/getUpdates" | jq .
При желании удалите сообщение бота:
curl --fail-with-body -sS \
-H 'content-type: application/json' \
-d "{\"chat_id\":${CHAT_ID},\"message_id\":${BOT_MESSAGE_ID}}" \
"$SKREPA_BOT_API/bot${SKREPA_BOT_TOKEN}/deleteMessage" | jq .
Как понять, что всё получилось
- ответ связан с сообщением «Покажи меню», а HTML отображается как форматированный текст;
- нажатие кнопки породило
callback_queryсdata: "run"; answerCallbackQueryпоказал уведомление и вернулok: true;editMessageTextизменил уже существующее сообщение в клиенте.
Типичные ошибки
| Симптом | Что проверить |
|---|---|
unknown private chat | chat_id взят из update этого runtime, gateway не перезапущен с чужим каталогом состояния |
| reply отклонён | message_id существует в известной истории этого чата; для необязательного reply можно использовать allow_sending_without_reply |
| ошибка форматирования | используйте HTML или MarkdownV2; не передавайте одновременно parse_mode и entities |
| callback не приходит | polling включает callback_query, пользователь нажал актуальную кнопку |
| edit/delete отклонён | сообщение создано этим ботом и ещё присутствует в durable history gateway |
jq не находит callback | long poll завершился раньше нажатия; повторите запрос с тем же offset |
Что гарантирует gateway
- entities используют Telegram-семантику UTF-16 offsets; границы проверяются до отправки;
- replies ссылаются на настоящий идентификатор сообщения, известный gateway;
- inline keyboard и callback data входят в защищённое сообщение, контекст кнопки проверяется при нажатии;
- callback попадает в ту же durable ordered queue и повторяется до подтверждения через polling или webhook;
- повтор одного transport-события подавляется, а
answerCallbackQueryидемпотентно закрывает известный callback; - edit/delete разрешены лишь для сообщений этого бота из доступной истории.