COMPAX

BOTS

WIDGETS
NUM CHAT

Um teclado são fileiras de botões, então toda pergunta que não seja qual destes botões vira uma conversa. Um widget é a resposta a isso: um cartão que o aplicativo desenha a partir do JSON que o bot manda, na mesma mensagem, pelo mesmo canal de callback. Abaixo está todo controle que existe, um construtor que escreve o JSON de cada um, e o cartão que ele desenha.

PARA QUE SERVEM

Exemplos de aplicativos

Uma tela de pedido, uma busca, uma reserva, uma tarifa. Cada cartão abaixo é feito dos nós que esta página distribui, e todos estão vivos — aperte. Onde uma tela precisou de algo que o vocabulário não tinha, era o vocabulário a quem faltava um nó: place, time, item e total são os quatro que saíram de construir estes.

Duas linhas de um cardápio, o tamanho, o que vai em cima, e quanto dá. O pedido inteiro é uma mensagem que não para de ser editada.

itemitemchoicechecklisttotalbutton

9:41
‹tablebotbot
/book10:58 ✓✓
tablebot
MargheritaSan Marzano, fior di latte, basil£11.50−1+
Diavola'Nduja, chilli, honey£13.00−1+
Size
Regular+£3.00
Large
On top
Extra mozzarella+£1.50
Olives+£1.00
Anchovy+£1.50
Totaldelivery included£24.50
Send to the kitchen
+Write a message…☺🎤

A IDEIA

O que é um widget

Um teclado são fileiras de botões, então toda pergunta que não seja qual destes botões vira uma conversa: uma mensagem por valor, e jeito nenhum de recolher ali mesmo um conjunto, uma quantidade ou um dia. Um widget é a resposta a isso: um cartão que o app desenha a partir do JSON que o bot manda, na mesma mensagem, com o mesmo canal de callback por baixo.

O modelo de estado é o que um teclado já tem, afiado: o bot é dono do estado, e o cliente não guarda nada que não lhe tenham dado. Cada toque é uma string de evento comum; o bot atualiza a própria cópia e empurra a próxima especificação com editMessageWidget, e o cartão se redesenha no lugar. sendMessage aceita um parâmetro widget, então a especificação viaja junto com o envio, e o text da mensagem é o recurso para um cliente que não consiga desenhá-la.

UM CARTÃO FEITO DE NÓS GERAIS
POST /bot$KEY/sendMessage
{
  "chat_id": -1001,
  "text": "Order #4821 — packed",          // the fallback
  "widget": {
    "type": "card", "gap": "xs",
    "children": [
      { "type": "row", "gap": "sm", "children": [
          { "type": "icon",  "icon": "cart", "tone": "accent" },
          { "type": "text",  "text": "Order #4821", "weight": "bold" },
          { "type": "badge", "text": "PACKED", "tone": "muted" } ] },
      { "type": "progress", "value": 0.66 },
      { "type": "row", "gap": "sm", "children": [
          { "type": "button", "label": "Advance", "primary": true, "tap": "card:step" },
          { "type": "button", "label": "Reset", "tap": "card:reset" } ] }
    ]
  }
}

# the tap arrives as an ordinary callback_query with data "card:step",
# and the bot pushes the next state into the same message:
POST /bot$KEY/editMessageWidget
{ "chat_id": -1001, "message_id": 19, "widget": { …the next spec… } }

O CONSTRUTOR

Construa um

Escolha um controle, gire os botões dele e aperte o cartão. O JSON é o que o seu bot manda para sendMessage; a string embaixo é tudo o que volta quando alguém aperta o que você desenhou.

stepper pede quantos, dentro de limites.

A resposta do Telegram são dois botões e um contador escrito dentro do texto.

O QUE O BOT MANDA
curl -X POST "$BASE/bot$KEY/sendMessage" \
  -H 'content-type: application/json' \
  -d '{
       "chat_id": -1001,
       "text": "People — open the app to answer",
       "widget": {
         "type": "card",
         "gap": "sm",
         "children": [
           {
             "type": "stepper",
             "label": "People",
             "min": 1,
             "max": 8,
             "value": 2,
             "tap": "seats:"
           }
         ]
       }
     }'
O que volta

Aperte o cartão no telefone. Faça o controle o que fizer, o que chega ao bot é uma string em cbq — nada além disso cabe nesse canal.

9:41
‹tablebotbot
/book10:58 ✓✓
tablebot
People−2+
+Write a message…☺🎤

VÁRIOS DE UMA VEZ

Componha um cartão

Um controle responde uma pergunta, e quase nada do que um bot pede é uma pergunta só. Uma reserva é uma contagem, um dia, um ajuste e um botão. card é o nó que os segura, e os filhos dele são uma lista — então a lista é o arranjo. Comece por um destes, ou acrescente linhas a ele.

Editando stepper — ele pede quantos, dentro de limites.

O QUE O BOT MANDA
curl -X POST "$BASE/bot$KEY/sendMessage" \
  -H 'content-type: application/json' \
  -d '{
       "chat_id": -1001,
       "text": "Open the app to answer",
       "widget": {
         "type": "card",
         "gap": "sm",
         "children": [
           {
             "type": "stepper",
             "label": "People",
             "min": 1,
             "max": 8,
             "value": 2,
             "tap": "seats:"
           },
           {
             "type": "date",
             "label": "Day",
             "placeholder": "Pick a day",
             "tap": "day:"
           },
           {
             "type": "switch",
             "label": "Window seat",
             "value": false,
             "tap": "window"
           },
           {
             "type": "button",
             "label": "Book the table",
             "tap": "book"
           }
         ]
       }
     }'
O que volta

Aperte qualquer linha do cartão no telefone. Por mais nós que um cartão segure, o que chega ao bot é uma string — e é por isso que todo nó endereçável carrega o próprio tap.

9:41
‹tablebotbot
/book10:58 ✓✓
tablebot
People−2+
DayPick a day
Window seat
Book the table
+Write a message…☺🎤

O VOCABULÁRIO

O vocabulário é uma lista branca

  • arranjo — column, row, wrap, box, card, space, divider
  • exibição — text, heading, note, banner, icon, badge, progress, markdown, code, spinner, glossary, total
  • entrada — field, check, switch, choice, select, checklist, stepper, slider, rating, date, time, place, button, item, gallery, pagination, tree
  • compostos — checkbox_group, payment_request, swap_panel, ticket_offer: cartões inteiros que o app desenha a partir do estado que o bot fornece
  • a janela — mini_app: o único nó que desenha algo que este aplicativo não escreveu, e de propósito o último da tabela. Ele abre por cima da conversa, não no lugar dela.

Um nome que não esteja na lista desenha uma caixa que o nomeia, em vez de nada — um formulário a que falta uma linha em silêncio é um formulário que alguém envia acreditando estar completo. A aparência é um token ("tone": "accent", "size": "lg"), resolvido contra a paleta do app: não há propriedade de cor, nem propriedade de pixels, nem ícone fora da lista branca — e é isso que impede um bot de desenhar algo com a forma da própria folha de confirmação deste app dentro de uma conversa em que alguém já confia. Um "color" cru é ignorado, e há um teste que diz isso.

Como um valor volta por um canal que carrega uma única string

  • Um controle cujo valor é o nó — button, check, switch — manda a string tap dele sem mudança. O bot tem o estado antigo e sabe qual o novo tem de ser.
  • Um controle que carrega um valor — select, choice, checklist, stepper, slider, rating, date, field — manda tap com o valor acrescentado no fim: com "tap": "form:seats", escolher quatro chega como form:seats:4. Texto livre é o último segmento (todo:rename:2:Buy oat milk), então separe-o em vez de partir em cada dois-pontos.
  • Um nó sem tap é desenhado inerte. É assim que uma especificação diz “isto está mostrando, não perguntando” — e um formulário terminado manda a próxima especificação sem string tap nenhuma, e é assim que ele deixa de ser apertável.
  • callback_data são 64 bytes na Bot API, então um valor escolhido tem de ser curto: uma lista de códigos de ativos serve, uma lista de frases não.

Todo controle, e o que volta de cada um

Uma tabela, porque o protocolo inteiro cabe nela. Mande o nó da esquerda; o toque da direita é o que chega como callback_query.data. Cada um destes foi tirado de examples/widgetbot.rs, que é um bot que você pode rodar.

O nó que você mandaO que volta num toque
{"type": "button", "label": "Book it", "primary": true, "tap": "form:place"}form:place
o nó é o valor
{"type": "switch", "id": "remind", "label": "Remind me", "on": false, "tap": "form:remind"}form:remind
puro — você tem o bit antigo e sabe qual é o novo
{"type": "stepper", "id": "seats", "label": "Seats", "value": 2, "min": 1, "max": 8, "tap": "form:seats"}form:seats:4
o valor vem acrescentado no fim
{"type": "select", "id": "sitting", "value": "main", "options": [{"value": "early", "label": "Early — 18:00"}, …], "tap": "form:sitting"}form:sitting:early
o value da opção, nunca o rótulo dela
{"type": "checklist", "id": "extras", "values": ["window"], "options": […], "tap": "form:extras"}form:extras:cake
o item tocado, não o conjunto novo — quem inverte é você
{"type": "slider", "id": "tip", "value": 10, "min": 0, "max": 30, "step": 5, "suffix": "%", "tap": "form:tip"}form:tip:15
{"type": "rating", "id": "stars", "value": 4, "max": 5, "tap": "form:stars"}form:stars:5
{"type": "date", "id": "when", "value": "", "tap": "form:when"}form:when:2026-09-14
{"type": "field", "id": "email", "label": "E‑mail", "placeholder": "you@example.com", "tap": "signup:email"}signup:email:ada@example.com
texto livre é o último segmento
{"type": "text", "text": "Order #4821"} — sem tapnada. Desenhado inerte: isto está mostrando, não perguntando

Então um manipulador parte duas vezes e não mais: uma para o verbo, outra para o campo, e o que sobrar é o valor.

LENDO UM TOQUE
# data = "form:seats:4"
let (verb, arg)   = data.split_once(':').unwrap_or((data, ""));   # form | seats:4
let (field, value) = arg.split_once(':').unwrap_or((arg, ""));    # seats | 4

# Free text can hold colons, so it is the remainder — never a third split.
# "todo:rename:2:Buy oat milk, 2 tins" is rename, item 2, and the rest.

O ciclo, de ponta a ponta

Três chamadas, e a do meio é do app. É isso tudo — um cartão que pergunta algo, um toque, e o cartão redesenhado com a resposta dentro.

O número não se move enquanto o bot não o mover
9:41
‹tablebotbot
tablebot
Pessoas−23+
DiaSex, 20h
Reservar
editado
seats:3 → tablebot
+Escreva uma mensagem…☺

As mesmas três chamadas do bloco abaixo, tocadas. Aperte o mais e veja a contagem ficar onde está: o app mandou seats:3 e não tem mais nada em que se apoiar. Ela muda quando a próxima especificação chega — que é o modelo de estado inteiro num único controle, e a razão de não haver deltas em lugar nenhum deste protocolo.

PERGUNTA → TOQUE → REDESENHO
# 1. the bot asks. `text` is the fallback for a client that cannot draw widgets.
POST /bot$KEY/sendMessage
{
  "chat_id": -1001,
  "text": "Table for two — 2 seats, no day yet",
  "widget": {
    "type": "card", "gap": "xs",
    "children": [
      { "type": "row", "gap": "sm", "children": [
          { "type": "icon", "icon": "cart", "tone": "accent" },
          { "type": "text", "text": "Table for two", "weight": "bold" } ] },
      { "type": "stepper", "id": "seats", "label": "Seats",
        "value": 2, "min": 1, "max": 8, "tap": "form:seats" },
      { "type": "date", "id": "when", "label": "Day", "value": "", "tap": "form:when" },
      { "type": "note", "text": "2 seats, no day yet" },
      { "type": "button", "label": "Book it", "primary": true, "tap": "form:place" }
    ]
  }
}

# 2. somebody moves the stepper to four — this arrives from getUpdates
{ "update_id": 43, "callback_query": {
    "id": "cb-9", "data": "form:seats:4",
    "from": { "id": 5501, "first_name": "Ada" },
    "message": { "message_id": 19, "chat": { "id": -1001 } } } }

# 3. answer first — the control is waiting on it — then redraw with the new state
POST /bot$KEY/answerCallbackQuery
{ "callback_query_id": "cb-9", "text": "4 at the table." }

POST /bot$KEY/editMessageWidget
{ "chat_id": -1001, "message_id": 19, "widget": { …the same card, "value": 4… } }

Recusar são as mesmas três chamadas sem a terceira: responda "One to eight." e não mande especificação nova, e o cartão fica como estava. Não há como falhar em silêncio, porque o controle é desenhado esperando até a resposta chegar.

Pedir algo digitado

Um field é o único controle que carrega palavras. O que volta é o que foi digitado, como último segmento — e o que faz dele um formulário em vez de quatro mensagens é que a forma também é sua: uma especificação pode fazer crescer uma linha que um momento antes não estava lá.

UM CAMPO, E UM VEREDICTO SOBRE ELE
# the bot asks
{ "type": "field", "id": "email-1", "label": "E-mail",
  "placeholder": "you@example.com", "value": "", "tap": "signup:email" }

# what arrives
"data": "signup:email:ada@exmaple"

# the bot judges — nothing on the device knows what an address looks like —
# and sends the same field back wearing the verdict
{ "type": "field", "id": "email-2", "label": "E-mail",
  "value": "ada@exmaple", "error": "That address has no dot after the @.",
  "tap": "signup:email" }

# …with a banner above it, so the card says how many things need fixing
{ "type": "banner", "tone": "danger", "text": "One thing needs fixing." }

Duas coisas sobre um campo valem saber antes de escrever um. Ele avisa quando é deixado, não a cada tecla — então o toque chega uma vez só, com o que foi digitado. E o value dele é só aquilo com que a caixa começa: para limpar um campo, ou para trazê-lo de volta vazio numa segunda tentativa, dê a ele um id novo. É por isso que os ids acima carregam o número da rodada (email‑1, email‑2) — uma identidade nova é como uma caixa volta vazia.

Três cartões inteiros, pelo preço de uma máquina de estados

todo_list, swap_panel e ticket_offer são compostos: o app desenha o cartão, o bot fornece o estado. O vocabulário de toques é o do cartão.

UMA LISTA QUE O APP DESENHA
"widget": {
  "type": "todo_list", "title": "Shopping",
  // the two the bot decides. A full list shows no Add at all,
  // rather than an Add that answers no.
  "can_add": true, "can_edit": true,
  "items": [
    { "id": 0, "text": "Oat milk", "done": true },
    { "id": 1, "text": "Coffee", "done": false }
  ]
}

# There is no "tap" on a composite: the card's vocabulary is the card's,
# and the app sends these of its own accord.
#   ticking the second line  → "todo:toggle:1"
#   renaming it             → "todo:rename:1:Coffee, the good one"
#   the Add button          → "todo:add"      (bare — you name the new item)
#   retitling the card      → "todo:title:This week"

A validação é sua, e não é uma regra sobre arrumação

Todo nó de entrada aceita hint e error, e os dois são palavras do bot. Nada no desenhista sabe com o que se parece um endereço de e-mail nem que uma lista de marcação tem um máximo: o valor sobe, o bot julga, e a próxima especificação carrega o veredicto. Um cliente que julgasse por conta própria seria uma segunda opinião sobre regras que moram noutro lugar, e as duas discordariam no dia em que você mudasse as suas — caladamente, na direção de deixar algo passar. Confira de novo na entrada também: esconder um botão não impede um evento repetido.

VER ISSO SEM ESCREVER UM BOT

Ajustes → Widgets dinâmicos no app roda o ciclo inteiro com um simulacro do outro lado: as mesmas especificações, as mesmas strings de evento, e um painel embaixo de cada cartão mostrando as duas metades do fio literalmente. Depois, cargo run -p botapi --example widgetbot são esses mesmos seis cartões movidos por um bot de verdade — que é a versão capaz de mostrar também um toque recusado, chegando duas vezes, ou expirando.