NE İŞE YARARLAR
Örnek uygulamalar
Bir sipariş ekranı, bir arama, bir rezervasyon, bir ücret. Aşağıdaki her kart bu sayfanın dağıttığı düğümlerden kurulmuştur ve hepsi canlıdır — bas onlara. Bir ekranın, sözlükte olmayan bir şeye ihtiyacı olduğunda, eksik olan sözlüğün bir düğümüydü: place, time, item ve total, bunları kurarken ortaya çıkan dört düğüm.
Bir menünün iki satırı, boyu, üstüne ne geldiği ve ne tuttuğu. Bütün sipariş, sürekli düzenlenen tek bir mesaj.
itemitemchoicechecklisttotalbutton
MargheritaSan Marzano, fior di latte, basil£11.50−1+
Diavola'Nduja, chilli, honey£13.00−1+FİKİR
Widget nedir
Bir klavye, sıra sıra düğmedir; dolayısıyla bu düğmelerden hangisi olmayan her soru bir konuşmaya dönüşür: her değer için bir mesaj, ve bir kümeyi, bir adedi ya da bir günü yerinde toplamanın hiçbir yolu yok. Widget bunun cevabıdır: uygulamanın, botun gönderdiği JSON’dan çizdiği bir kart — aynı mesajın içinde, altında aynı callback kanalıyla.
Durum modeli, bir klavyenin zaten sahip olduğu modeldir, keskinleştirilmiş hâli: durum botundur, istemci kendisine verilmemiş hiçbir şeyi tutmaz. Her dokunuş sıradan bir olay dizgisidir; bot kendi kopyasını günceller ve bir sonraki tanımı editMessageWidget ile iter, kart da yerinde yeniden çizilir. sendMessage bir widget parametresi alır, yani tanım gönderimle birlikte yolculuk eder ve mesajın text’i onu çizemeyen bir istemci için geri düşüş noktasıdır.
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… } }
KURUCU
Bir tane kur
Bir denetim seç, düğmelerini çevir ve karta bas. JSON, botunun sendMessage’a gönderdiği şeydir; altındaki dizgi ise birisi senin çizdiğine bastığında geri gelen şeyin tamamıdır.
stepper kaç tane, sınırlar içinde ister.
Telegram’ın cevabı iki düğme ve metnin içine yazılmış bir sayaçtır.
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:" } ] } }'
Telefondaki karta bas. Denetim ne yaparsa yapsın, bota ulaşan şey cbq üzerinde tek bir dizgidir — o kanala başka bir şey sığmaz.
AYNI ANDA BİRKAÇI
Bir kart oluştur
Bir denetim bir soruya cevap verir, ve bir botun sorduğu şeylerin neredeyse hiçbiri tek bir soru değildir. Bir rezervasyon bir sayı, bir gün, bir ayar ve bir düğmedir. card onları tutan düğümdür, ve çocukları bir listedir — yani liste, yerleşimin ta kendisidir. Bunlardan biriyle başla, ya da ona satır ekle.
stepper düzenleniyor — kaç tane, sınırlar içinde ister.
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" } ] } }'
Telefondaki kartın herhangi bir satırına bas. Bir kart kaç düğüm tutarsa tutsun, bota ulaşan şey tek bir dizgidir — adreslenebilir her düğümün kendi tap’ini taşımasının sebebi de bu.
SÖZLÜK
Sözlük bir beyaz listedir
- yerleşim —
column,row,wrap,box,card,space,divider - gösterim —
text,heading,note,banner,icon,badge,progress,markdown,code,spinner,glossary,total - giriş —
field,check,switch,choice,select,checklist,stepper,slider,rating,date,time,place,button,item,gallery,pagination,tree - bileşikler —
checkbox_group,payment_request,swap_panel,ticket_offer: uygulamanın, botun verdiği durumdan çizdiği bütün kartlar - pencere —
mini_app: bu uygulamanın yazmadığı bir şeyi çizen tek düğüm, ve bilerek tablonun sonuncusu. Konuşmanın yerine değil, üstüne açılır.
Listede olmayan bir ad, hiçbir şey değil, onu adlandıran bir kutu çizer — sessizce bir satırı eksik olan bir form, birinin tamam sanarak gönderdiği formdur. Görünüm bir token’dır ("tone": "accent", "size": "lg") ve uygulamanın paletine karşı çözülür: renk özelliği yok, piksel özelliği yok, beyaz listenin dışında simge yok — ve bir botun, birinin zaten güvendiği bir konuşmanın içine bu uygulamanın kendi onay sayfasının biçiminde bir şey çizmesini engelleyen tam da budur. Ham bir "color" yok sayılır, ve bunu söyleyen bir test var.
Tek bir dizgi taşıyan bir kanaldan bir değer nasıl geri döner
- Değeri düğümün kendisi olan bir denetim —
button,check,switch—tapdizgisini olduğu gibi gönderir. Eski durumu bot tutar ve yenisinin ne olması gerektiğini bilir. - Bir değer taşıyan bir denetim —
select,choice,checklist,stepper,slider,rating,date,field—tap’i değer sonuna eklenmiş hâlde gönderir:"tap": "form:seats"ile dördü seçmekform:seats:4olarak gelir. Serbest metin son parçadır (todo:rename:2:Buy oat milk), yani her iki noktadan bölmek yerine onu sondan ayır. tap’i olmayan bir düğüm hareketsiz çizilir. Bir tanım “bu gösteriyor, sormuyor” demeyi böyle yapar — ve bitmiş bir form bir sonraki tanımını hiçtapdizgisi olmadan gönderir, basılabilir olmaktan da böyle çıkar.callback_dataBot API’de 64 bayttır, yani seçilen bir değer kısa olmak zorundadır: varlık kodlarından bir liste olur, cümlelerden bir liste olmaz.
Her denetim ve her birinden ne döner
Tek bir tablo, çünkü protokolün tamamı onun içinde. Soldaki düğümü gönder; sağdaki dokunuş callback_query.data olarak gelen şeydir. Bunların her biri, çalıştırabileceğin bir bot olan examples/widgetbot.rs dosyasından alınmıştır.
| Gönderdiğin düğüm | Dokunuşta geri gelen |
|---|---|
{"type": "button", "label": "Book it", "primary": true, "tap": "form:place"} | form:placedeğer, düğümün kendisi |
{"type": "switch", "id": "remind", "label": "Remind me", "on": false, "tap": "form:remind"} | form:remindçıplak — eski biti sen tutuyorsun ve yenisini biliyorsun |
{"type": "stepper", "id": "seats", "label": "Seats", "value": 2, "min": 1, "max": 8, "tap": "form:seats"} | form:seats:4değer sona eklenir |
{"type": "select", "id": "sitting", "value": "main", "options": [{"value": "early", "label": "Early — 18:00"}, …], "tap": "form:sitting"} | form:sitting:earlyseçeneğin value’su, asla etiketi değil |
{"type": "checklist", "id": "extras", "values": ["window"], "options": […], "tap": "form:extras"} | form:extras:cakedokunulan öğe, yeni küme değil — onu sen çevirirsin |
{"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.comserbest metin son parçadır |
{"type": "text", "text": "Order #4821"} — tap yok | hiçbir şey. Hareketsiz çizilmiş: bu gösteriyor, sormuyor |
Yani bir işleyici iki kez böler, daha fazla değil: bir kez fiil için, bir kez alan için, ve geriye kalan da değerdir.
# 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.
Döngü, baştan sona
Üç çağrı, ve ortadaki uygulamanın. Hepsi bu — bir şey soran bir kart, bir dokunuş, ve cevabı içinde olacak şekilde yeniden çizilmiş kart.
Aşağıdaki bloktaki aynı üç çağrı, oynatılmış hâli. Artıya bas ve sayacın yerinde kaldığını izle: uygulama seats:3’ü göndermiş ve dayanacak başka bir şeyi yok. Bir sonraki tanım geldiğinde değişir — ki bu, tek bir denetimde bütün durum modelidir, ve bu protokolde hiçbir yerde delta olmamasının sebebidir.
# 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… } }
Reddetmek, üçüncüsü olmadan aynı üç çağrıdır: "One to eight." diye cevap ver ve yeni tanım gönderme, kart olduğu gibi kalır. Sessizce başarısız olmanın yolu yok, çünkü denetim cevap gelene kadar bekliyor olarak çizilir.
Yazılmış bir şey istemek
field, kelime taşıyan tek denetimdir. Geri gelen, yazılan şeydir, son parça olarak — ve bunu dört mesaj yerine bir form yapan şey, biçimin de sana ait olmasıdır: bir tanım, az önce orada olmayan bir satırı büyütebilir.
# 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." }Bir alan hakkında, birini yazmadan önce bilmeye değer iki şey var. Her tuşta değil, alandan çıkıldığında haber verir — yani dokunuş bir kez gelir, yazılanla birlikte. Ve value’su yalnızca kutunun başladığı şeydir: bir alanı temizlemek ya da ikinci bir deneme için boş geri getirmek için ona yeni bir id ver. Yukarıdaki id’lerin tur numarasını taşımasının sebebi bu (email‑1, email‑2) — bir kutunun boş dönmesinin yolu yeni bir kimliktir.
Üç bütün kart, bir durum makinesi fiyatına
todo_list, swap_panel ve ticket_offer bileşiktir: kartı uygulama çizer, durumu bot verir. Dokunuş sözlüğü kartındır.
"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"Doğrulama senin işin, ve düzenlilikle ilgili bir kural değil
Her giriş düğümü hint ve error alır, ve ikisi de botun sözleridir. Çizicideki hiçbir şey bir e-posta adresinin neye benzediğini ya da bir işaret listesinin bir üst sınırı olduğunu bilmez: değer yukarı gider, bot hükmeder, ve bir sonraki tanım hükmü taşır. Kendi başına hükmeden bir istemci, başka bir yerde yaşayan kurallar hakkında ikinci bir görüş olurdu, ve seninkileri değiştirdiğin gün ikisi ayrışırdı — sessizce, bir şeyi geçirme yönünde. Girişte de yeniden denetle: bir düğmeyi saklamak, yeniden oynatılan bir olayı durdurmaz.
BOT YAZMADAN GÖRMEK
Uygulamadaki Ayarlar → Dinamik widget’lar, öteki uçta bir taklitle bütün döngüyü çalıştırır: aynı tanımlar, aynı olay dizgileri, ve her kartın altında telin iki yarısını olduğu gibi gösteren bir panel. Sonra cargo run -p botapi --example widgetbot, aynı altı kartın gerçek bir botla sürülmüş hâlidir — reddedilmiş, iki kez gelmiş ya da zaman aşımına uğramış bir dokunuşu da gösterebilen sürüm budur.