ДЛЯ ЧОГО ВОНИ
Приклади застосунків
Екран замовлення, пошук, бронювання, тариф. Кожна картка нижче зібрана з вузлів, які роздає ця сторінка, і всі вони живі — натисни. Там, де екрану бракувало чогось, чого не було у словнику, це словнику бракувало вузла: place, time, item і total — ті чотири, що вийшли зі складання цих карток.
Два рядки меню, розмір, що зверху і на скільки виходить. Усе замовлення — одне повідомлення, яке весь час редагують.
itemitemchoicechecklisttotalbutton
MargheritaSan Marzano, fior di latte, basil£11.50−1+
Diavola'Nduja, chilli, honey£13.00−1+ІДЕЯ
Що таке віджет
Клавіатура — це ряди кнопок, тож будь-яке питання, крім яка з цих кнопок, перетворюється на розмову: одне повідомлення на значення і жодного способу зібрати на місці набір, кількість чи день. Віджет — відповідь на це: картка, яку застосунок малює з JSON, надісланого ботом, у тому самому повідомленні, з тим самим каналом callback під сподом.
Модель стану — та сама, що вже є у клавіатури, лише загострена: стан належить боту, а клієнт не тримає нічого, чого йому не дали. Кожне натискання — звичайний рядок події; бот оновлює власну копію і надсилає наступну специфікацію через editMessageWidget, а картка перемальовується на місці. sendMessage приймає параметр widget, тож специфікація їде разом із надсиланням, а text повідомлення — запасний варіант для клієнта, який її не намалює.
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… } }
КОНСТРУКТОР
Збери один
Вибери елемент керування, покрути його ручки і натисни картку. JSON — це те, що твій бот надсилає на sendMessage; рядок під ним — усе, що повертається, коли хтось натискає намальоване тобою.
stepper питає скільки, у межах.
Відповідь Telegram — це дві кнопки й лічильник, вписаний у текст.
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:" } ] } }'
Натисни картку в телефоні. Хоч би що робив елемент керування, до бота доходить один рядок на cbq — більше в цей канал не влазить.
КІЛЬКА ОДРАЗУ
Створи композицію
Один елемент керування відповідає на одне питання, а майже ніщо з того, що питає бот, не є одним питанням. Бронювання — це кількість, день, налаштування і кнопка. card — вузол, який їх тримає, а його діти — список, тож список і є розкладкою. Почни з однієї з цих або додай до неї рядки.
Редагуєш stepper — він питає скільки, у межах.
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" } ] } }'
Натисни будь-який рядок картки в телефоні. Скільки б вузлів картка не тримала, до бота доходить один рядок — саме тому кожен адресований вузол несе власний tap.
СЛОВНИК
Словник — це білий список
- розкладка —
column,row,wrap,box,card,space,divider - показ —
text,heading,note,banner,icon,badge,progress,markdown,code,spinner,glossary,total - введення —
field,check,switch,choice,select,checklist,stepper,slider,rating,date,time,place,button,item,gallery,pagination,tree - складені —
checkbox_group,payment_request,swap_panel,ticket_offer: цілі картки, які застосунок малює зі стану, що дає бот - вікно —
mini_app: єдиний вузол, що малює те, чого цей застосунок не писав, і навмисно останній у таблиці. Воно відкривається над листуванням, а не замість нього.
Ім’я, відсутнє у списку, малює коробку, яка його називає, а не порожнечу — форма, у якій тихо бракує рядка, це форма, яку хтось надсилає, вважаючи повною. Вигляд — це токен ("tone": "accent", "size": "lg"), розв’язаний проти палітри застосунку: немає властивості кольору, немає властивості в пікселях і немає піктограми поза білим списком — і саме це не дає боту намалювати щось у формі власного аркуша підтвердження цього застосунку всередині листування, якому хтось уже довіряє. Сирий "color" ігнорується, і про це є тест.
Як значення повертається каналом, що несе один рядок
- Елемент керування, значенням якого є сам вузол —
button,check,switch— надсилає свій рядокtapбез змін. Бот тримає старий стан і знає, яким має бути новий. - Елемент керування, що несе значення —
select,choice,checklist,stepper,slider,rating,date,field— надсилаєtapзі значенням, доклеєним у кінець: за"tap": "form:seats"вибір четвірки приходить якform:seats:4. Вільний текст — останній сегмент (todo:rename:2:Buy oat milk), тож відріж його, а не дроби на кожній двокрапці. - Вузол без
tapмалюється нерухомим. Так специфікація каже «це показує, а не питає» — а завершена форма надсилає наступну специфікацію взагалі без рядківtap, і саме так перестає натискатися. callback_data— це 64 байти в Bot API, тож вибране значення має бути коротким: список кодів активів годиться, список речень — ні.
Кожен елемент керування і що з нього повертається
Одна таблиця, бо в ній вміщується весь протокол. Надішли вузол ліворуч; натискання праворуч — це те, що приходить як callback_query.data. Кожен із них узятий із examples/widgetbot.rs, а це бот, якого можна запустити.
| Вузол, який ти надсилаєш | Що повертається при натисканні |
|---|---|
{"type": "button", "label": "Book it", "primary": true, "tap": "form:place"} | form:placeвузол і є значенням |
{"type": "switch", "id": "remind", "label": "Remind me", "on": false, "tap": "form:remind"} | form:remindголе — старий біт тримаєш ти і знаєш, який новий |
{"type": "stepper", "id": "seats", "label": "Seats", "value": 2, "min": 1, "max": 8, "tap": "form:seats"} | form:seats:4значення доклеєне в кінець |
{"type": "select", "id": "sitting", "value": "main", "options": [{"value": "early", "label": "Early — 18:00"}, …], "tap": "form:sitting"} | form:sitting:earlyvalue варіанта, ніколи не його підпис |
{"type": "checklist", "id": "extras", "values": ["window"], "options": […], "tap": "form:extras"} | form:extras:cakeторкнутий елемент, а не новий набір — перемикаєш його ти |
{"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вільний текст — останній сегмент |
{"type": "text", "text": "Order #4821"} — без tap | нічого. Намальовано нерухомим: це показує, а не питає |
Тож обробник ділить двічі й не більше: раз на дієслово, раз на поле, а що лишилося — це значення.
# 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.
Цикл, від початку до кінця
Три виклики, і середній належить застосунку. Це все — картка, що про щось питає, натискання, і картка, перемальована з відповіддю всередині.
Ті самі три виклики, що й у блоці нижче, програні. Натисни плюс і дивись, як лічильник стоїть на місці: застосунок надіслав seats:3 і більше не має на що спертися. Він зміниться, коли прийде наступна специфікація — а це вся модель стану в одному елементі керування і причина, чому в цьому протоколі ніде немає дельт.
# 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… } }
Відмова — ті самі три виклики без третього: відповідай "One to eight." і не надсилай нової специфікації, і картка лишиться такою, як була. Тихо провалитися неможливо, бо елемент керування малюється очікуючим, доки не прийде відповідь.
Просити щось надруковане
field — єдиний елемент керування, що несе слова. Повертається те, що надрукували, останнім сегментом — а формою, а не чотирма повідомленнями, це робить те, що форма теж твоя: специфікація може виростити рядок, якого мить тому не було.
# 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." }Про поле варто знати дві речі, перш ніж писати своє. Воно звітує, коли з нього виходять, а не на кожній клавіші — тож натискання приходить один раз, із тим, що надрукували. А його value — лише те, з чого коробка починає: щоб очистити поле або повернути його порожнім на другу спробу, дай йому новий id. Саме тому ідентифікатори вище несуть номер раунду (email‑1, email‑2) — нова особистість і є способом, яким коробка повертається порожньою.
Три цілі картки за ціною автомата станів
todo_list, swap_panel і ticket_offer — складені: картку малює застосунок, стан дає бот. Словник натискань належить картці.
"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"Перевірка — твоя, і це не правило про охайність
Кожен вузол введення приймає hint і error, і обидва — слова бота. Ніщо в малювальнику не знає, який вигляд має адреса пошти чи що в списку позначок є максимум: значення йде вгору, бот судить, а наступна специфікація несе вирок. Клієнт, який судив би сам, був би другою думкою про правила, що живуть деінде, і ці двоє розійшлися б того дня, коли ти зміниш свої — тихо і в бік того, щоб щось пропустити. Перевіряй ще раз і на вході: сховати кнопку не спиняє повторену подію.
ПОБАЧИТИ ЦЕ, НЕ ПИШУЧИ БОТА
Налаштування → Динамічні віджети у застосунку проходить увесь цикл із підробкою на іншому кінці: ті самі специфікації, ті самі рядки подій і панель під кожною карткою, що дослівно показує обидві половини дроту. Далі cargo run -p botapi --example widgetbot — ті самі шість карток, які веде справжній бот, і саме ця версія вміє показати натискання відхилене, здвоєне або протерміноване.