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
MargheritaSan Marzano, fior di latte, basil£11.50−1+
Diavola'Nduja, chilli, honey£13.00−1+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.
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.
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:" } ] } }'
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.
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.
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" } ] } }'
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.
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 stringtapdele 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— mandatapcom o valor acrescentado no fim: com"tap": "form:seats", escolher quatro chega comoform: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 stringtapnenhuma, e é assim que ele deixa de ser apertável. callback_datasã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ê manda | O que volta num toque |
|---|---|
{"type": "button", "label": "Book it", "primary": true, "tap": "form:place"} | form:placeo nó é o valor |
{"type": "switch", "id": "remind", "label": "Remind me", "on": false, "tap": "form:remind"} | form:remindpuro — 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:4o valor vem acrescentado no fim |
{"type": "select", "id": "sitting", "value": "main", "options": [{"value": "early", "label": "Early — 18:00"}, …], "tap": "form:sitting"} | form:sitting:earlyo value da opção, nunca o rótulo dela |
{"type": "checklist", "id": "extras", "values": ["window"], "options": […], "tap": "form:extras"} | form:extras:cakeo 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.comtexto livre é o último segmento |
{"type": "text", "text": "Order #4821"} — sem tap | nada. 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.
# 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.
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.
# 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á.
# 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.
"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.