WAAR ZE VOOR ZIJN
Voorbeeldapplicaties
Een bestelscherm, een zoekopdracht, een reservering, een tarief. Elke kaart hieronder is gebouwd uit de knopen die deze pagina uitdeelt, en ze zijn allemaal levend — druk erop. Waar een scherm iets nodig had dat het vocabulaire niet had, was het het vocabulaire dat een knoop tekortkwam: place, time, item en total zijn de vier die uit het bouwen hiervan zijn gekomen.
Twee regels van een menukaart, de maat, wat erop gaat, en waar het op uitkomt. De hele bestelling is één bericht dat steeds opnieuw bewerkt wordt.
itemitemchoicechecklisttotalbutton
MargheritaSan Marzano, fior di latte, basil£11.50−1+
Diavola'Nduja, chilli, honey£13.00−1+HET IDEE
Wat een widget is
Een toetsenbord is rijen knoppen, dus elke vraag die niet welke van deze knoppen is, wordt een gesprek: één bericht per waarde, en geen enkele manier om ter plekke een verzameling, een aantal of een dag te verzamelen. Een widget is het antwoord daarop: een kaart die de app tekent uit de JSON die de bot stuurt, in hetzelfde bericht, met hetzelfde callback-kanaal eronder.
Het toestandsmodel is dat wat een toetsenbord al heeft, aangescherpt: de bot bezit de toestand, de client houdt niets vast wat hem niet gegeven is. Elke druk is een gewone gebeurtenistekenreeks; de bot werkt zijn eigen kopie bij en duwt de volgende specificatie na met editMessageWidget, en de kaart tekent zich ter plekke opnieuw. sendMessage neemt een widget-parameter, dus de specificatie reist met het verzenden mee, en de text van het bericht is de terugval voor een client die haar niet kan tekenen.
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… } }
DE BOUWER
Bouw er een
Kies een bedieningselement, draai aan de knoppen en druk op de kaart. De JSON is wat jouw bot naar sendMessage stuurt; de tekenreeks eronder is het geheel van wat terugkomt als iemand drukt op wat je getekend hebt.
stepper vraagt om hoeveel, met grenzen.
Het antwoord van Telegram zijn twee knoppen en een teller die in de tekst geschreven staat.
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:" } ] } }'
Druk op de kaart in de telefoon. Wat het bedieningselement ook doet, wat de bot bereikt is één tekenreeks op cbq — meer past er niet op dat kanaal.
MEERDERE TEGELIJK
Stel een kaart samen
Eén bedieningselement beantwoordt één vraag, en bijna niets wat een bot vraagt is één vraag. Een reservering is een aantal, een dag, een instelling en een knop. card is de knoop die ze bij elkaar houdt, en zijn kinderen zijn een lijst — dus de lijst is de indeling. Begin met een van deze, of voeg er rijen aan toe.
Bezig met stepper — het vraagt om hoeveel, met grenzen.
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" } ] } }'
Druk op een willekeurige rij van de kaart in de telefoon. Hoeveel knopen een kaart ook bevat, wat de bot bereikt is één tekenreeks — en daarom draagt elke adresseerbare knoop zijn eigen tap.
HET VOCABULAIRE
Het vocabulaire is een witte lijst
- indeling —
column,row,wrap,box,card,space,divider - weergave —
text,heading,note,banner,icon,badge,progress,markdown,code,spinner,glossary,total - invoer —
field,check,switch,choice,select,checklist,stepper,slider,rating,date,time,place,button,item,gallery,pagination,tree - samengestelde —
checkbox_group,payment_request,swap_panel,ticket_offer: hele kaarten die de app tekent uit de toestand die de bot levert - het venster —
mini_app: de ene knoop die iets tekent wat deze applicatie niet geschreven heeft, en met opzet de laatste in de tabel. Het opent boven het gesprek in plaats van in de plaats ervan.
Een naam die niet op de lijst staat tekent een vakje dat hem noemt, in plaats van niets — een formulier waar stilzwijgend een rij aan ontbreekt is er een dat iemand verstuurt in de veronderstelling dat het compleet is. Uiterlijk is een token ("tone": "accent", "size": "lg"), opgelost tegen het palet van de app: er is geen kleureigenschap, geen pixeleigenschap en geen pictogram buiten de witte lijst — en dat is wat een bot ervan weerhoudt iets in de vorm van het eigen bevestigingsblad van deze app te tekenen binnen een gesprek dat iemand al vertrouwt. Een rauwe "color" wordt genegeerd, en er is een test die dat vastlegt.
Hoe een waarde terugkomt over een kanaal dat één tekenreeks draagt
- Een bedieningselement waarvan de waarde de knoop is —
button,check,switch— stuurt zijntap-tekenreeks onveranderd. De bot houdt de oude toestand vast en weet wat de nieuwe moet zijn. - Een bedieningselement dat een waarde draagt —
select,choice,checklist,stepper,slider,rating,date,field— stuurttapmet de waarde eraan geplakt: bij"tap": "form:seats"komt vier kiezen binnen alsform:seats:4. Vrije tekst is het laatste segment (todo:rename:2:Buy oat milk), dus haal het eraf in plaats van op elke dubbele punt te splitsen. - Een knoop zonder
tapwordt roerloos getekend. Zo zegt een specificatie “dit toont, dit vraagt niet” — en een afgerond formulier stuurt zijn volgende specificatie zonder ook maar ééntap-tekenreeks, en zo houdt het op indrukbaar te zijn. callback_datais 64 bytes in de Bot API, dus een gekozen waarde moet kort zijn: een lijst met assetcodes kan, een lijst met zinnen niet.
Elk bedieningselement, en wat er terugkomt
Eén tabel, want het hele protocol staat erin. Stuur de knoop links; de druk rechts is wat binnenkomt als callback_query.data. Elk hiervan is overgenomen uit examples/widgetbot.rs, een bot die je kunt draaien.
| De knoop die je stuurt | Wat er bij een druk terugkomt |
|---|---|
{"type": "button", "label": "Book it", "primary": true, "tap": "form:place"} | form:placede knoop is de waarde |
{"type": "switch", "id": "remind", "label": "Remind me", "on": false, "tap": "form:remind"} | form:remindkaal — jij houdt de oude bit vast en kent de nieuwe |
{"type": "stepper", "id": "seats", "label": "Seats", "value": 2, "min": 1, "max": 8, "tap": "form:seats"} | form:seats:4de waarde wordt eraan geplakt |
{"type": "select", "id": "sitting", "value": "main", "options": [{"value": "early", "label": "Early — 18:00"}, …], "tap": "form:sitting"} | form:sitting:earlyde value van de optie, nooit het label ervan |
{"type": "checklist", "id": "extras", "values": ["window"], "options": […], "tap": "form:extras"} | form:extras:cakehet aangeraakte item, niet de nieuwe verzameling — jij kantelt hem |
{"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.comvrije tekst is het laatste segment |
{"type": "text", "text": "Order #4821"} — geen tap | niets. Roerloos getekend: dit toont, dit vraagt niet |
Een afhandelaar splitst dus twee keer en niet vaker: één keer voor het werkwoord, één keer voor het veld, en wat overblijft is de waarde.
# 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.
De lus, van begin tot eind
Drie aanroepen, en die in het midden is van de app. Dit is het hele verhaal — een kaart die iets vraagt, een druk, en de kaart opnieuw getekend met het antwoord erin.
Dezelfde drie aanroepen als het blok hieronder, afgespeeld. Druk op de plus en zie de teller staan waar hij staat: de app heeft seats:3 gestuurd en heeft verder niets om op af te gaan. Hij verandert wanneer de volgende specificatie binnenkomt — en dat is het hele toestandsmodel in één bedieningselement, en de reden dat er nergens in dit protocol delta’s zijn.
# 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… } }
Weigeren zijn dezelfde drie aanroepen zonder de derde: antwoord "One to eight." en stuur geen nieuwe specificatie, dan blijft de kaart zoals hij was. Stilletjes mislukken kan niet, want het bedieningselement wordt wachtend getekend tot het antwoord binnen is.
Vragen om iets getypts
Een field is het enige bedieningselement dat woorden draagt. Wat terugkomt is wat er getypt is, als laatste segment — en wat er een formulier van maakt in plaats van vier berichten, is dat ook de vorm van jou is: een specificatie kan een rij laten groeien die er een moment eerder nog niet was.
# 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." }Twee dingen over een veld zijn het waard om te weten voor je er een schrijft. Het meldt zich wanneer je het verlaat, niet bij elke toetsaanslag — de druk komt dus één keer binnen, met wat er getypt is. En zijn value is alleen datgene waarmee het vakje begint: om een veld leeg te maken, of het voor een tweede poging leeg terug te halen, geef het een nieuwe id. Daarom dragen de ids hierboven het rondenummer (email‑1, email‑2) — een nieuwe identiteit is hoe een vakje leeg terugkomt.
Drie hele kaarten, voor de prijs van een toestandsmachine
todo_list, swap_panel en ticket_offer zijn samengesteld: de app tekent de kaart, de bot levert de toestand. Het drukvocabulaire is dat van de kaart.
"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"De controle is van jou, en het is geen regel over netheid
Elke invoerknoop neemt hint en error, en beide zijn de woorden van de bot. Niets in de tekenaar weet hoe een e-mailadres eruitziet of dat een aankruislijst een maximum heeft: de waarde gaat omhoog, de bot oordeelt, en de volgende specificatie draagt het oordeel. Een client die zelf oordeelde zou een tweede mening zijn over regels die ergens anders wonen, en de twee zouden het oneens zijn op de dag dat jij die van jou verandert — geruisloos, en in de richting van iets doorlaten. Controleer ook bij binnenkomst opnieuw: een knop verbergen houdt een herhaalde gebeurtenis niet tegen.
HET ZIEN ZONDER EEN BOT TE SCHRIJVEN
Instellingen → Dynamische widgets in de app doorloopt de hele lus met een nabootsing aan de andere kant: dezelfde specificaties, dezelfde gebeurtenistekenreeksen, en onder elke kaart een paneel dat beide helften van de draad letterlijk toont. En cargo run -p botapi --example widgetbot zijn diezelfde zes kaarten aangedreven door een echte bot — dat is de versie die ook een geweigerde, dubbel binnengekomen of verlopen druk kan laten zien.