🛡️ Скрепа

Форматированный текст, replies и inline-кнопки

Результат

Вы ответите на конкретное сообщение, покажете inline-кнопку, получите callback_query, подтвердите нажатие и отредактируете отправленное ботом сообщение.

Что понадобится

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 .

Как понять, что всё получилось

Типичные ошибки

СимптомЧто проверить
unknown private chatchat_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 не находит callbacklong poll завершился раньше нажатия; повторите запрос с тем же offset

Что гарантирует gateway