PO CO ONE SĄ
Przykłady aplikacji
Ekran zamówienia, wyszukiwarka, rezerwacja, taryfa. Każda karta niżej jest zbudowana z węzłów, które rozdaje ta strona, i każda z nich żyje — naciśnij ją. Tam, gdzie ekran potrzebował czegoś, czego słownik nie miał, to słownikowi brakowało węzła: place, time, item i total to te cztery, które wyszły z budowania tych kart.
Dwie pozycje z karty, rozmiar, co na wierzchu i ile to razem wychodzi. Całe zamówienie to jedna wiadomość, którą wciąż się edytuje.
itemitemchoicechecklisttotalbutton
MargheritaSan Marzano, fior di latte, basil£11.50−1+
Diavola'Nduja, chilli, honey£13.00−1+POMYSŁ
Czym jest widget
Klawiatura to rzędy przycisków, więc każde pytanie inne niż który z tych przycisków zamienia się w rozmowę: jedna wiadomość na wartość i żadnego sposobu, by zebrać na miejscu zbiór, liczbę albo dzień. Widget jest odpowiedzią na to: karta, którą aplikacja rysuje z JSON-a przysłanego przez bota, w tej samej wiadomości, z tym samym kanałem callbacku pod spodem.
Model stanu jest ten, który klawiatura już ma, tylko wyostrzony: stan należy do bota, a klient nie trzyma niczego, czego mu nie dano. Każde naciśnięcie to zwykły ciąg zdarzenia; bot aktualizuje własną kopię i dosyła następną specyfikację przez editMessageWidget, a karta przerysowuje się w miejscu. sendMessage przyjmuje parametr widget, więc specyfikacja jedzie razem z wysyłką, a text wiadomości jest zapasem dla klienta, który nie umie jej narysować.
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… } }
KONSTRUKTOR
Zbuduj jeden
Wybierz kontrolkę, pokręć jej gałkami i naciśnij kartę. JSON to jest to, co twój bot wysyła do sendMessage; ciąg pod spodem to całość tego, co wraca, gdy ktoś naciśnie to, co narysowałeś.
stepper pyta o ile, w granicach.
Odpowiedź Telegrama to dwa przyciski i licznik wpisany w tekst.
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:" } ] } }'
Naciśnij kartę w telefonie. Cokolwiek kontrolka robi, do bota dociera jeden ciąg na cbq — nic więcej się na tym kanale nie mieści.
KILKA NARAZ
Skomponuj kartę
Jedna kontrolka odpowiada na jedno pytanie, a prawie nic, o co bot pyta, nie jest jednym pytaniem. Rezerwacja to liczba, dzień, ustawienie i przycisk. card to węzeł, który je trzyma, a jego dzieci są listą — więc lista jest układem. Zacznij od jednej z tych albo dołóż do niej wiersze.
Edytujesz stepper — pyta o ile, w granicach.
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" } ] } }'
Naciśnij dowolny wiersz karty w telefonie. Ile by węzłów karta nie trzymała, do bota dociera jeden ciąg — i dlatego każdy adresowalny węzeł niesie własny tap.
SŁOWNIK
Słownik jest białą listą
- układ —
column,row,wrap,box,card,space,divider - wyświetlanie —
text,heading,note,banner,icon,badge,progress,markdown,code,spinner,glossary,total - wejście —
field,check,switch,choice,select,checklist,stepper,slider,rating,date,time,place,button,item,gallery,pagination,tree - złożone —
checkbox_group,payment_request,swap_panel,ticket_offer: całe karty, które aplikacja rysuje ze stanu dostarczonego przez bota - okno —
mini_app: jedyny węzeł, który rysuje coś, czego ta aplikacja nie napisała, i celowo ostatni w tabeli. Otwiera się nad rozmową, a nie zamiast niej.
Nazwa nieobecna na liście rysuje pudełko, które ją nazywa, a nie nic — formularz z po cichu brakującym wierszem to formularz, który ktoś wysyła w przekonaniu, że jest kompletny. Wygląd to token ("tone": "accent", "size": "lg"), rozstrzygany względem palety aplikacji: nie ma właściwości koloru, nie ma właściwości w pikselach i nie ma ikony spoza białej listy — i właśnie to powstrzymuje bota przed narysowaniem czegoś o kształcie własnej karty potwierdzenia tej aplikacji wewnątrz rozmowy, której ktoś już ufa. Surowy "color" jest ignorowany i jest test, który to mówi.
Jak wraca wartość kanałem, który niesie jeden ciąg
- Kontrolka, której wartością jest sam węzeł —
button,check,switch— wysyła swój ciągtapbez zmian. Bot trzyma stary stan i wie, jaki musi być nowy. - Kontrolka, która niesie wartość —
select,choice,checklist,stepper,slider,rating,date,field— wysyłatapz wartością doklejoną na końcu: przy"tap": "form:seats"wybór czwórki przychodzi jakoform:seats:4. Wolny tekst jest ostatnim segmentem (todo:rename:2:Buy oat milk), więc odetnij go, zamiast dzielić na każdym dwukropku. - Węzeł bez
tapjest rysowany bezwładnie. Tak specyfikacja mówi „to pokazuje, a nie pyta” — a skończony formularz wysyła następną specyfikację zupełnie bez ciągówtap, i tak przestaje dać się naciskać. callback_datato 64 bajty w Bot API, więc wybrana wartość musi być krótka: lista kodów aktywów przejdzie, lista zdań nie.
Każda kontrolka i to, co z niej wraca
Jedna tabela, bo mieści się w niej cały protokół. Wyślij węzeł z lewej; naciśnięcie z prawej to to, co przychodzi jako callback_query.data. Każdy z nich jest wzięty z examples/widgetbot.rs, czyli z bota, którego możesz uruchomić.
| Węzeł, który wysyłasz | Co wraca po naciśnięciu |
|---|---|
{"type": "button", "label": "Book it", "primary": true, "tap": "form:place"} | form:placewęzeł jest wartością |
{"type": "switch", "id": "remind", "label": "Remind me", "on": false, "tap": "form:remind"} | form:remindgoły — stary bit masz ty i wiesz, jaki jest nowy |
{"type": "stepper", "id": "seats", "label": "Seats", "value": 2, "min": 1, "max": 8, "tap": "form:seats"} | form:seats:4wartość jest doklejona na końcu |
{"type": "select", "id": "sitting", "value": "main", "options": [{"value": "early", "label": "Early — 18:00"}, …], "tap": "form:sitting"} | form:sitting:earlyvalue opcji, nigdy jej etykieta |
{"type": "checklist", "id": "extras", "values": ["window"], "options": […], "tap": "form:extras"} | form:extras:cakedotknięty element, a nie nowy zbiór — przestawiasz go ty |
{"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.comwolny tekst jest ostatnim segmentem |
{"type": "text", "text": "Order #4821"} — bez tap | nic. Narysowany bezwładnie: to pokazuje, a nie pyta |
Więc obsługa dzieli dwa razy i nie więcej: raz na czasownik, raz na pole, a co zostanie, jest wartością.
# 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.
Pętla, od początku do końca
Trzy wywołania, a środkowe należy do aplikacji. To wszystko — karta, która o coś pyta, naciśnięcie i karta przerysowana z odpowiedzią w środku.
Te same trzy wywołania co w bloku niżej, odtworzone. Naciśnij plus i patrz, jak licznik stoi w miejscu: aplikacja wysłała seats:3 i nie ma się na czym oprzeć. Zmieni się, gdy przyjdzie następna specyfikacja — a to jest cały model stanu w jednej kontrolce i powód, dla którego w tym protokole nigdzie nie ma delt.
# 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… } }
Odmowa to te same trzy wywołania bez trzeciego: odpowiedz "One to eight." i nie wysyłaj nowej specyfikacji, a karta zostanie taka, jaka była. Nie da się zawieść po cichu, bo kontrolka jest rysowana jako czekająca, dopóki odpowiedź nie dojdzie.
Prosić o coś wpisanego
field to jedyna kontrolka, która niesie słowa. Wraca to, co wpisano, jako ostatni segment — a tym, co czyni z tego formularz zamiast czterech wiadomości, jest to, że kształt też należy do ciebie: specyfikacja może wyhodować wiersz, którego chwilę wcześniej nie było.
# 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." }Dwie rzeczy o polu warto wiedzieć, zanim się je napisze. Zgłasza się, gdy się z niego wychodzi, a nie przy każdym klawiszu — więc naciśnięcie przychodzi raz, z tym, co wpisano. A jego value to tylko to, od czego pole zaczyna: żeby je wyczyścić albo przywrócić puste do drugiej próby, daj mu nowe id. Dlatego identyfikatory wyżej niosą numer rundy (email‑1, email‑2) — nowa tożsamość to sposób, w jaki pole wraca puste.
Trzy całe karty, w cenie automatu stanów
todo_list, swap_panel i ticket_offer są złożone: kartę rysuje aplikacja, stan dostarcza bot. Słownik naciśnięć należy do karty.
"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"Walidacja należy do ciebie i nie jest zasadą o schludności
Każdy węzeł wejściowy przyjmuje hint i error, i oba to słowa bota. Nic w rysowniku nie wie, jak wygląda adres e-mail ani że lista zaznaczeń ma maksimum: wartość idzie w górę, bot ocenia, a następna specyfikacja niesie wyrok. Klient, który oceniałby sam, byłby drugą opinią o regułach mieszkających gdzie indziej, a obie rozeszłyby się tego dnia, w którym zmienisz swoje — po cichu i w stronę przepuszczenia czegoś. Sprawdzaj ponownie także na wejściu: schowanie przycisku nie zatrzymuje powtórzonego zdarzenia.
ZOBACZYĆ TO BEZ PISANIA BOTA
Ustawienia → Dynamiczne widgety w aplikacji przechodzi całą pętlę z atrapą po drugiej stronie: te same specyfikacje, te same ciągi zdarzeń i panel pod każdą kartą pokazujący obie połówki drutu dosłownie. Potem cargo run -p botapi --example widgetbot to te same sześć kart napędzanych przez prawdziwego bota — czyli wersja, która potrafi też pokazać naciśnięcie odrzucone, przychodzące dwa razy albo przeterminowane.