docs/bot-widgets.md
Rama main · réplica de lectura del repositorio original.
# Bot web widgets (Layer 233)
`gramsrv` implements the Layer 233 bot web-widget surface. This document records
what the server does and the deliberate limits.
## Media
- `inputMediaWidget` becomes `messageMediaWidget`. The markup is stored as
`Payload` (on the message media and, for a shared widget, in its `bot_widgets`
definition), together with the box size and `allow_queries` flag.
- Widget media may only be authored by **bots**: a non-bot sender is rejected
with `MEDIA_INVALID`, so a client-rendered widget can never become a query
target.
- When `shared_widget` is set, the message gets a server-assigned random
`widget_id` and the definition is persisted in the `bot_widgets` table **in the
committed send transaction** (not while the input is parsed), so `uploadMedia`,
`setInlineBotResults` and sends that fail later leave no row.
- `inputMediaWidgetID` resolves that definition by id and attaches it to the
message as an `is_copy` widget. `chat_data` from the input is kept.
- `invert_media` from an inline widget input is carried through accept, the
`botInlineMessageWidget` projection and the sent `message.invert_media`.
- Accepted through `messages.sendMedia` / `sendMultiMedia` / `uploadMedia`, and
through inline bot results (`inputBotInlineMessageWidget` /
`inputBotInlineMessageWidgetID` → `botInlineMessageWidget`). Not accepted on
ephemeral or story media.
## Public / chat data
- `messages.getWidgetPublicData(peer, msg_id, hash)` returns
`messages.widgetPublicData{hash, data, period}` or
`messages.widgetPublicDataNotModified{period}` when the client already has the
current `hash`. Per-message data takes precedence over the shared definition;
for a shared widget the initial public data lives in the definition, so
`editWidgetData` is visible on the original message as well as on copies, and a
per-message `public_data` (including an explicit clear to `""`) always wins.
- `messages.editMessageWidgetData(peer, id, public_data?, chat_data?)` patches the
widget data **in place** for both private and channel messages: only the media
JSONB is rewritten (body/entities/edit_date untouched, the message is not marked
edited), a compact pts-bearing `updateMessageWidgetData` is recorded/dispatched
(account pts for private, channel pts + a `channel_widget_data` durable event
for channels), and unchanged data returns `WIDGET_DATA_NOT_MODIFIED`. No
filler update rides along: td drops `updateMessageWidgetData` for profiles
225-232, and those sessions see the pts gap and recover it through
`updates.getDifference` (`updates.getChannelDifference` for channels). A clear
(`public_data=""`) is pushed with the `public_data` flag set, so live clients
learn about it without polling. Only the
**bot that sent the message** (or its via bot) may patch: a recipient, channel
admin or ordinary member is rejected with `MESSAGE_ID_INVALID`.
- `messages.editInlineBotMessageWidgetData(id, public_data?, chat_data?)` does
the same for inline-bot messages owned by the calling bot, reusing the in-place
patch (channel messages use `EditInlineBotMessage` + fanout).
- `messages.editWidgetData(widget_id, public_data, cache_time?)` replaces the
shared widget's public data for messages that carry no per-message override;
only the widget's owner bot may call it, `public_data=""` clears it explicitly,
and `cache_time` is only rewritten when the optional field is present.
## Queries
- `messages.getBotWidgetQueryAnswer(peer, msg_id, query_type, data)` registers a
pending query and waits up to 25 s for the answer. The update follows the
target: `updateBotWidgetCopyQuery` for an `is_copy` widget, a forwarded/shared
widget the owner bot did not itself send, or a copy whose owner cannot be
resolved (fail closed); `updateInlineBotWidgetQuery` (routed to `ViaBotID`) for
a `via_bot` message; and `updateBotWidgetQuery` only for a message the bot
itself sent (translated to the bot's own view in private chats). A private
query's `chat_instance` hashes the querier, never the chat partner.
- `messages.setBotWidgetQueryAnswer(query_id, result?)` unblocks the waiter; only
the owning bot may resolve a query id (`QUERY_ID_INVALID` otherwise,
`BOT_RESPONSE_TIMEOUT` when unanswered).
- The pending registry is shared across nodes through a Redis store (mirroring
`BotCallbackRegistryStore`); without one it is process-local.
- `updateBotWidgetQuery.wallet_card` carries the querying user's wallet card only
when the query type opts in (`widgetQueryTypeData{with_wallet}`, or the
sign/sent-transaction types).
## Transcription files
- `messages.transcribeAudioFile(file)` bounds the summed uploaded part size to
25 MiB before it materialises a document (the check uses the stored part sizes,
not an assumed 512 KiB part), then runs the existing ASR service
(`TELESRV_ASR_URL`). A failed file job is terminal (`TRANSCRIPTION_FAILED`); a
success pushes `updateTranscribedAudioFile` to the requester.
- `messages.getTranscriptionLangs` returns an empty list (the backend exposes no
fixed language set).
## Deliberate limits
- A shared widget's `bot_widgets` row is created only by the committed send
transaction, so `uploadMedia` returns a `widget_id` whose row does not exist
until the media is actually sent. The in-memory store (tests only) materialises
nothing on send; production is the postgres send path.
- `messages.transcribeAudioFile` bounds the uploaded size and document creation
before the ASR job, but a job that fails after materialisation leaves the
document row in place (documents are shared, unrefcounted).
- `channels.toggleForum` models only the list and tabs layouts: enabling a forum
with `sidebar=true` fails closed (the author's Layer 233 decision is unchanged),
and `channel.forum_sidebar` is never emitted.
- TON widget signing is not implemented: `wallet.tonConnect*` still refuses with
`WALLET_UNAVAILABLE`.