Ctrl K
ComunidadGuías…

docs/bot-widgets.md

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# 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`.