REPOSITORY / ScuroNeko/Laniakea

Wiki

KNOWLEDGE REPOSITORY

(doc): v1.2 wiki update

2026-08-19 14:59:10 +03:00
parent 3f7422e47c
commit 3ec127f090
13 changed files with 515 additions and 100 deletions
+1 -1
@@ -6,7 +6,7 @@ English version: [[Getting-Started]]
## Что нужно сначала
- Go 1.24 или новее
- Go 1.26 или новее
- токен Telegram-бота от `@BotFather`
- Go-модуль, который может импортировать `git.scuroneko.dev/scuroneko/laniakea`
+1 -1
@@ -7,7 +7,7 @@ Start here if you are integrating Laniakea into a new bot for the first time.
This page covers the shortest path to a working bot, the minimum concepts you need to understand, and the most important defaults that affect startup and runtime behavior.
## What you need first
- Go 1.24 or newer
- Go 1.26 or newer
- a Telegram bot token from `@BotFather`
- a module that can import `git.scuroneko.dev/scuroneko/laniakea`
+1
@@ -40,6 +40,7 @@ English version: [[Home]]
## Миграция и сопровождение
- [[Migration-RU]]
- [[V2-Migration-Plan-RU]] — DRAFT; предполагаемые breaking changes и compatibility bridges v1.2
- [[Semver-and-Releases-RU]]
- [[Framework-Backlog-RU]]
+1
@@ -42,5 +42,6 @@ Use this wiki as the structured companion to the README: start with setup, then
## Migration and Maintenance
- [[Migration]]
- [[V2-Migration-Plan]] — DRAFT; proposed breaking changes and v1.2 compatibility bridges
- [[Semver-and-Releases]]
- [[Framework-Backlog]]
+4 -4
@@ -107,16 +107,16 @@ English version: [[MessageContext]]
## Rich-сообщения
`RichAnswer(...)` и `RichAnswerKeyboard(...)` отправляют структурированные rich-сообщения Bot API 10.1, собранные из фрагментов `tgfmt`:
`RichAnswer(...)` и `RichAnswerKeyboard(...)` валидируют и отправляют input rich-блоки Bot API 10.2, собранные через `tgrich`:
```go
ctx.RichAnswer(
tgfmt.H1(tgfmt.NewRich("Отчёт")),
tgfmt.P(tgfmt.NewRich("всё работает").Bold()),
tgrich.H1(tgrich.Text("Отчёт")),
tgrich.P(tgrich.Bold(tgrich.Text("всё работает"))),
)
```
Полный DSL и приёмная сторона описаны на странице [[Rich-Messages-RU]].
Все конструкторы, загрузка медиа, валидация и приёмная сторона описаны на странице [[Rich-Messages-RU]].
## Drafts и localization
+4 -4
@@ -113,16 +113,16 @@ This is useful for:
## Rich messages
Use `RichAnswer(...)` and `RichAnswerKeyboard(...)` to send structured Bot API 10.1 rich messages built from `tgfmt` fragments:
Use `RichAnswer(...)` and `RichAnswerKeyboard(...)` to validate and send Bot API 10.2 input rich blocks built with `tgrich`:
```go
ctx.RichAnswer(
tgfmt.H1(tgfmt.NewRich("Report")),
tgfmt.P(tgfmt.NewRich("all systems go").Bold()),
tgrich.H1(tgrich.Text("Report")),
tgrich.P(tgrich.Bold(tgrich.Text("all systems go"))),
)
```
See [[Rich-Messages]] for the full DSL and the receive side.
See [[Rich-Messages]] for all constructors, media uploads, validation, and the receive side.
## Markdown helpers
+1
@@ -12,6 +12,7 @@ English version: [[Migration]]
- [[Bot-Lifecycle-RU]]
- [[Commands-and-Plugins-RU]]
- [[Inline-Keyboards-and-Payloads-RU]]
- [[V2-Migration-Plan-RU]] — DRAFT-план cleanup для v2 и compatibility bridges v1.2
- [[Semver-and-Releases-RU]]
## Что важно помнить при апгрейде
+1
@@ -12,6 +12,7 @@ Also see:
- [[Bot-Lifecycle]] for the current startup and shutdown model;
- [[Commands-and-Plugins]] for handler registration patterns;
- [[Inline-Keyboards-and-Payloads]] for payload-type behavior;
- [[V2-Migration-Plan]] for the DRAFT v2 cleanup plan and v1.2 compatibility bridges;
- [[Semver-and-Releases]] for the project's versioning intent.
## Recommended upgrade strategy
+117 -45
@@ -1,71 +1,141 @@
# Rich-сообщения
Rich-сообщения — механизм Bot API 10.1 для сильно структурированного текста: заголовки, абзацы, списки с чекбоксами, таблицы, медиа-коллажи, раскрывающиеся секции, математика и другое. Laniakea поддерживает их целиком: типизированный HTML-DSL для отправки, типизированные wire-структуры для приёма и стриминг черновиков.
Rich-сообщения поддерживают заголовки, абзацы, списки, таблицы, медиа, цитаты, раскрывающиеся секции и формулы. Laniakea поддерживает исходящие деревья `InputRichBlock` из Bot API 10.2, входящие деревья `RichBlock`, преобразование в HTML, multipart-загрузки и потоковые черновики.
## Ключевая асимметрия
## Пакеты и поток данных
Telegram сделал отправку и приём намеренно разными, и Laniakea повторяет это разделение:
- `tgapi` содержит wire-типы Telegram и методы API.
- `tgrich` содержит конструкторы `tgapi.RichText` и `tgapi.InputRichBlock`, валидацию и преобразование в HTML.
- Исходящие сообщения используют `tgapi.InputRichMessage`. Должно быть задано ровно одно из полей `Blocks`, `HTML` или `Markdown`.
- Входящие сообщения используют `Message.RichMessage` с узлами `tgapi.RichBlock` и `tgapi.RichText`.
- **Отправляется разметка, а не дерево.** Единственный способ отправить rich-сообщение — `tgapi.InputRichMessage` со строкой `HTML` или `Markdown`. API для отправки дерева блоков не существует.
- **Принимается дерево, а не разметка.** Входящее rich-сообщение приходит как `Message.RichMessage` — полностью типизированное дерево узлов `RichBlock` и `RichText`, собранное сервером.
Поэтому в Laniakea:
- **out**-сторона живёт в `tgfmt` (`rich.go`): типизированные HTML-фрагменты и конструкторы с чистыми именами (`P`, `H1`, `Bold`, `Photo`, ...);
- **in**-сторона живёт в `tgapi` (`richtext.go`, `richblock.go`): wire-типы с именами официальных объектов API (`RichBlockParagraph` — это `RichBlockWrap{Tag: "paragraph"}`, `RichTextBold``RichTextWrap{Tag: "bold"}` и т.д.) плюс `UnmarshalRichMessage`.
Исходящие и входящие типы блоков намеренно разделены. `InputRichBlock*` описывает данные, принимаемые Telegram, а `RichBlock*` — нормализованное дерево, возвращаемое Telegram.
## Отправка из обработчика
`MessageContext.RichAnswer(...)` принимает фрагменты `tgfmt` и отправляет их через `sendRichMessage`:
`MessageContext.RichAnswer` принимает input-блоки и собирает валидированное HTML rich-сообщение:
```go
import "git.scuroneko.dev/scuroneko/laniakea/tgfmt"
import (
"git.scuroneko.dev/scuroneko/laniakea/tgrich"
)
func report(ctx *laniakea.MessageContext, _ struct{}) error {
ctx.RichAnswer(
tgfmt.H1(tgfmt.NewRich("Дневной отчёт")),
tgfmt.P(
tgfmt.NewRich("Статус: "),
tgfmt.NewRich("всё работает").Bold(),
),
tgfmt.Ul(
tgfmt.LiCheckbox(true, tgfmt.NewRich("бэкапы")),
tgfmt.LiCheckbox(false, tgfmt.NewRich("миграция")),
tgrich.H1(tgrich.Text("Дневной отчёт")),
tgrich.P(tgrich.Concat(
tgrich.Text("Статус: "),
tgrich.Bold(tgrich.Text("всё работает")),
)),
tgrich.Ul(
tgrich.NewListItem(tgrich.P(tgrich.Text("бэкапы"))).
SetCheckbox().SetChecked().Build(),
tgrich.NewListItem(tgrich.P(tgrich.Text("миграция"))).
SetCheckbox().Build(),
),
)
return nil
}
```
`RichAnswerKeyboard(kb, items...)` прикрепляет inline-клавиатуру.
`RichAnswerKeyboard(keyboard, blocks...)` отправляет те же блоки с inline-клавиатурой.
## DSL в `tgfmt`
## Сборка текста и блоков
Два типа фрагментов превращают невалидную вложенность в ошибку компиляции:
Inline-конструкторы: `Text`, `Concat`, `Bold`, `Italic`, `Underline`, `Strikethrough`, `Spoiler`, `Code`, `Marked`, `Subscript`, `Superscript`, `URL`, `Email`, `Phone`, `TextMention`, `Mention`, `Hashtag`, `Cashtag`, `BotCommand`, `Emoji`, `DateTime`, `MathExpression`, anchors и references.
- `tgfmt.Rich` — inline-содержимое (жирный, ссылки, эмодзи, время, математика, ...);
- `tgfmt.RichBlock` — блочное содержимое (заголовки, абзацы, списки, таблицы, медиа, ...).
Блочные конструкторы: `P`, `H1`-`H6`, `Pre`, `CodeBlock`, `Footer`, `Hr`, `Math`, `Anchor`, `Ul`, `Ol`, цитаты, коллажи, слайд-шоу, таблицы, details, карты, анимации, аудио, фото, видео, голосовые сообщения и доступный только в черновиках блок `Thinking`.
Блочные конструкторы принимают только `Rich`-аргументы, поэтому таблица внутри абзаца не скомпилируется. На *верхнем уровне* допустимы оба (`RichItem`): соседний inline-контент Telegram сам собирает в абзацы.
Для элементов списков и таблиц используются небольшие builders:
Сырой текст попадает в DSL ровно одним способом — `tgfmt.NewRich("...")`, который экранирует HTML. Всё остальное — композиция уже безопасных фрагментов.
```go
ordered := tgrich.Ol(
tgrich.OlOpts{Start: 3, Type: tgapi.InputRichBlockListItemTypeLower},
tgrich.NewListItem(tgrich.P(tgrich.Text("третий"))).Build(),
)
Inline-хелперы (методы `Rich`): `Bold`, `Italic`, `Underline`, `Strike`, `Code`, `Mark`, `Sub`, `Sup`, `Spoiler`, `Link`, `Email`, `Phone`, `Mention`, `Anchor`, `AnchorLink`, `Ref`, `Time`, `TimeFormat`, `Math`; свободные функции `Emoji`, `Br`.
table := tgrich.NewTable(
tgrich.Row(
tgrich.CellWithText(tgrich.Text("Имя")).SetHeader().Build(),
tgrich.CellWithText(tgrich.Text("Значение")).SetHeader().Build(),
),
tgrich.Row(
tgrich.CellWithText(tgrich.Text("Статус")).Build(),
tgrich.CellWithText(tgrich.Bold(tgrich.Text("ОК"))).Build(),
),
).SetBordered(true).Build()
```
Блочные конструкторы: `H1``H6`, `P`, `Pre`, `PreCode`, `Footer`, `Hr`, `AnchorBlock`, `Ul`/`Ol` с `Li`/`LiCheckbox`, `Blockquote`, `Aside`, `Photo`/`Video`/`Audio` (медиа-билдеры с `.Block()`, `.Caption(...)`, `.SetSpoiler()`), `Map`/`MapCaption`, `Collage`/`Slideshow` (+варианты `...Caption`), `Table`/`Row`/`Cell`, `Details`, `MathBlock`.
## Block payload и преобразование в HTML
Чтобы собрать payload без отправки, используйте `tgfmt.RichHTML(items...)` (строка) или `tgfmt.RichMessage(items...)` (готовый `tgapi.InputRichMessage` с включённым `skip_entity_detection`, чтобы сервер не добавлял авто-entities).
Если преобразование не требуется, отправляйте input-дерево напрямую:
## Прямой доступ через `tgapi`
```go
rich := tgapi.InputRichMessage{
Blocks: []tgapi.InputRichBlock{
tgrich.H1(tgrich.Text("Отчёт")),
tgrich.P(tgrich.Text("Готово")),
},
}
```
- `API.SendRichMessage(params)` — отправка; `params.RichMessage` — это `InputRichMessage`.
- `API.SendRichMessageDraft(params)` — стриминг частичного сообщения во время генерации. Черновик — эфемерное превью на ~30 секунд с ключом `DraftID` (обновления с тем же ID анимируются); в конце вызовите `SendRichMessage` с полным сообщением. Черновики — единственное место, где встречается блок `thinking`.
- `EditMessageText.RichMessage` — редактирование rich-сообщения на месте (`Text` оставьте пустым).
- `InputRichMessageContent` — rich-контент для результатов inline-запросов (`input_message_content`).
`tgrich.BuildHTML(blocks...)` валидирует целое дерево и преобразует его в HTML-вариант `InputRichMessage`. `tgrich.ToHTML(block)` — сокращённая форма для одного блока. Преобразование полезно для логов, превью и API вроде `MessageContext.RichAnswer`, которые отправляют отрендеренный HTML.
`BuildHTML` включает `SkipEntityDetection`, экранирует текст и атрибуты и проверяет официальные лимиты Telegram:
- 32768 UTF-8 символов;
- 500 блоков, включая вложенные блоки, элементы списков и строки таблиц;
- 16 общих уровней вложенных блоков и форматирования;
- 50 media attachments;
- 20 колонок таблицы с учётом `colspan`.
Также проверяются discriminators блоков, размеры заголовков, типы маркеров и checkbox-состояние списков, выравнивание и spans таблиц, координаты/zoom/размеры карт и типы медиа.
## Медиа и multipart-загрузки
Медиа-конструкторы принимают `tgapi.InputMedia`, поэтому поле `Media` может содержать HTTP URL, Telegram `file_id` или ссылку `attach://name`:
```go
block := tgrich.PhotoWithCaption(
tgapi.InputMedia{Media: "telegram-file-id"},
tgrich.CaptionWithCredit(
tgrich.Text("Запуск"),
tgrich.Text("Эксплуатация"),
),
)
```
При преобразовании в HTML медиа-блоки превращаются в ссылки `tg://photo?id=...`, `tg://video?id=...` или `tg://audio?id=...`. Исходные значения `InputMedia` собираются в `InputRichMessage.Media` со стабильными идентификаторами `media_1`, `media_2`, ...
Для multipart-загрузки имя после `attach://` должно совпадать с именем multipart-поля:
```go
rich, err := tgrich.BuildHTML(
tgrich.Photo(tgapi.InputMedia{Media: "attach://report"}),
)
if err != nil {
return err
}
_, err = uploader.SendRichMessage(
tgapi.SendRichMessage{ChatID: chatID, RichMessage: rich},
tgapi.NewUploaderFile("report.jpg", data).SetAttachName("report"),
)
```
Та же схема загрузки доступна через `Uploader.SendRichMessageDraft`.
## Прямой доступ через API
- `API.SendRichMessage` отправляет завершённое rich-сообщение.
- `API.SendRichMessageDraft` обновляет эфемерное превью во время генерации. `Thinking` допустим только в черновиках.
- `Uploader.SendRichMessage` и `Uploader.SendRichMessageDraft` загружают файлы, указанные через `attach://`.
- `EditMessageText.RichMessage` редактирует существующее rich-сообщение; оставьте `Text` пустым.
- `InputRichMessageContent` задаёт rich-контент для inline-результатов.
## Приём
`Message.RichMessage` (`*tgapi.RichMessage`) разбирается автоматически. Обходите его type switch-ами:
`Message.RichMessage` разбирается автоматически. Обходите принятое дерево через type switch:
```go
for _, block := range msg.RichMessage.Blocks {
@@ -74,23 +144,25 @@ for _, block := range msg.RichMessage.Blocks {
handleText(b.Text)
case tgapi.RichBlockList:
for _, item := range b.Items {
// item.Label — готовый видимый маркер: "1.", "c.", "vii.", "•"
// Label — готовый серверный маркер: "1.", "c.", "vii." или "•".
}
}
}
```
Неизвестные будущие типы узлов с полем `text` сохраняются как `RichTextWrap`/`RichBlockWrap`, а не роняют разбор — парсинг переживает расширения API.
Неизвестные будущие узлы с полем `text` сохраняются как `RichTextWrap` или `RichBlockWrap`, поэтому parser переносит совместимые расширения Bot API.
## Подводные камни
- **Медиа — только по URL.** В HTML-режиме `<img>`/`<video>`/`<audio>` принимают только HTTP(S)-URL; `file_id` не работает. Сигнатуры DSL (`Photo(url string)`) делают это явным.
- **Маркеры списков рендерит сервер.** Отправляйте семантику `<ol type start>`/`<li value>` через `OlOpts`/`SetValue`/`SetType`; видимый маркер («c.», «vii.») возвращается уже вычисленным в `RichBlockListItem.Label`. Не генерируйте маркеры сами.
- **Один тег `<video>`/`<audio>` — два типа блоков.** Сервер различает видео/анимацию и аудио/голосовое по расширению URL (`.gif` → animation, `.ogg` → voice note).
- **`skip_entity_detection`** в `tgfmt.RichMessage(...)` по умолчанию `true`; без него сервер добавит авто-entities, и принятое дерево разойдётся с отправленным.
- `InputRichBlock*` и входящие `RichBlock*` разные семейства типов.
- `Thinking` можно отправлять только через `sendRichMessageDraft`.
- У отмеченного элемента списка обязательно должен быть checkbox.
- Ordered items требуют поддерживаемого типа маркера; unordered items не должны задавать `Value`.
- Ширина таблицы учитывает column spans.
- `BuildHTML` возвращает ошибки валидации до API-запроса; для их классификации используйте `errors.Is` с экспортируемыми значениями `tgrich.ErrRich*`.
## Что читать дальше
- [[MessageContext]] — остальные вспомогательные методы ответа.
- [[tgapi-Overview]] — низкоуровневый клиент, на котором живут rich-методы.
- [[Drafts]] — поэтапная сборка обычных (не rich) сообщений.
- [[MessageContext]] — вспомогательные методы ответа.
- [[tgapi-Overview]] — низкоуровневый API-клиент.
- [[Drafts]] — поэтапная генерация сообщений.
+117 -45
@@ -1,71 +1,141 @@
# Rich Messages
Rich messages are the Bot API 10.1 feature for highly structured text: headings, paragraphs, lists with checkboxes, tables, media collages, expandable sections, math, and more. Laniakea supports them end to end: a typed HTML DSL for sending, typed wire structures for receiving, and streaming drafts.
Rich messages provide headings, paragraphs, lists, tables, media, quotations, expandable sections, and formulas. Laniakea supports outgoing `InputRichBlock` trees from Bot API 10.2, incoming `RichBlock` trees, HTML conversion, multipart uploads, and streaming drafts.
## The core asymmetry
## Packages and data flow
Telegram made sending and receiving deliberately different, and Laniakea mirrors that split:
- `tgapi` contains Telegram wire types and API methods.
- `tgrich` contains constructors for `tgapi.RichText` and `tgapi.InputRichBlock`, validation, and HTML conversion.
- Outgoing messages use `tgapi.InputRichMessage`. Exactly one of `Blocks`, `HTML`, or `Markdown` must be set.
- Incoming messages use `Message.RichMessage` with `tgapi.RichBlock` and `tgapi.RichText` nodes.
- **You send markup, not trees.** The only way to send a rich message is `tgapi.InputRichMessage` with an `HTML` or `Markdown` string. There is no API to submit a block tree.
- **You receive a tree, not markup.** An incoming rich message arrives as `Message.RichMessage` — a fully typed tree of `RichBlock` and `RichText` nodes rendered by the server.
So in Laniakea:
- the **out** side lives in `tgfmt` (`rich.go`): typed HTML fragments and constructors with clean names (`P`, `H1`, `Bold`, `Photo`, ...);
- the **in** side lives in `tgapi` (`richtext.go`, `richblock.go`): wire types named after the official API objects (`RichBlockParagraph` is `RichBlockWrap{Tag: "paragraph"}`, `RichTextBold` is `RichTextWrap{Tag: "bold"}`, and so on) plus `UnmarshalRichMessage`.
Outgoing and incoming block types are intentionally separate. `InputRichBlock*` describes data accepted by Telegram; `RichBlock*` describes the normalized tree returned by Telegram.
## Sending from a handler
`MessageContext.RichAnswer(...)` accepts `tgfmt` fragments and sends them through `sendRichMessage`:
`MessageContext.RichAnswer` accepts input blocks and builds a validated HTML rich message:
```go
import "git.scuroneko.dev/scuroneko/laniakea/tgfmt"
import (
"git.scuroneko.dev/scuroneko/laniakea/tgrich"
)
func report(ctx *laniakea.MessageContext, _ struct{}) error {
ctx.RichAnswer(
tgfmt.H1(tgfmt.NewRich("Daily report")),
tgfmt.P(
tgfmt.NewRich("Status: "),
tgfmt.NewRich("all systems go").Bold(),
),
tgfmt.Ul(
tgfmt.LiCheckbox(true, tgfmt.NewRich("backups")),
tgfmt.LiCheckbox(false, tgfmt.NewRich("migration")),
tgrich.H1(tgrich.Text("Daily report")),
tgrich.P(tgrich.Concat(
tgrich.Text("Status: "),
tgrich.Bold(tgrich.Text("all systems go")),
)),
tgrich.Ul(
tgrich.NewListItem(tgrich.P(tgrich.Text("backups"))).
SetCheckbox().SetChecked().Build(),
tgrich.NewListItem(tgrich.P(tgrich.Text("migration"))).
SetCheckbox().Build(),
),
)
return nil
}
```
`RichAnswerKeyboard(kb, items...)` attaches an inline keyboard.
`RichAnswerKeyboard(keyboard, blocks...)` sends the same content with an inline keyboard.
## The `tgfmt` DSL
## Building text and blocks
Two fragment types make invalid nesting a compile error:
Inline text constructors include `Text`, `Concat`, `Bold`, `Italic`, `Underline`, `Strikethrough`, `Spoiler`, `Code`, `Marked`, `Subscript`, `Superscript`, `URL`, `Email`, `Phone`, `TextMention`, `Mention`, `Hashtag`, `Cashtag`, `BotCommand`, `Emoji`, `DateTime`, `MathExpression`, anchors, and references.
- `tgfmt.Rich` — inline content (bold, links, emoji, time, math, ...);
- `tgfmt.RichBlock` — block content (headings, paragraphs, lists, tables, media, ...).
Block constructors include `P`, `H1`-`H6`, `Pre`, `CodeBlock`, `Footer`, `Hr`, `Math`, `Anchor`, `Ul`, `Ol`, quotations, collages, slideshows, tables, details, maps, animation, audio, photo, video, voice notes, and the draft-only `Thinking` block.
Block constructors accept only `Rich` arguments, so a table inside a paragraph does not compile. At the *top level* both are allowed (`RichItem`): Telegram merges adjacent inline content into paragraphs.
List items and tables use small builders:
Raw text enters the DSL exactly one way — `tgfmt.NewRich("...")` — which HTML-escapes it. Everything else composes already-safe fragments.
```go
ordered := tgrich.Ol(
tgrich.OlOpts{Start: 3, Type: tgapi.InputRichBlockListItemTypeLower},
tgrich.NewListItem(tgrich.P(tgrich.Text("third"))).Build(),
)
Inline helpers (methods on `Rich`): `Bold`, `Italic`, `Underline`, `Strike`, `Code`, `Mark`, `Sub`, `Sup`, `Spoiler`, `Link`, `Email`, `Phone`, `Mention`, `Anchor`, `AnchorLink`, `Ref`, `Time`, `TimeFormat`, `Math`; free functions `Emoji`, `Br`.
table := tgrich.NewTable(
tgrich.Row(
tgrich.CellWithText(tgrich.Text("Name")).SetHeader().Build(),
tgrich.CellWithText(tgrich.Text("Value")).SetHeader().Build(),
),
tgrich.Row(
tgrich.CellWithText(tgrich.Text("Status")).Build(),
tgrich.CellWithText(tgrich.Bold(tgrich.Text("OK"))).Build(),
),
).SetBordered(true).Build()
```
Block constructors: `H1``H6`, `P`, `Pre`, `PreCode`, `Footer`, `Hr`, `AnchorBlock`, `Ul`/`Ol` with `Li`/`LiCheckbox`, `Blockquote`, `Aside`, `Photo`/`Video`/`Audio` (media builders with `.Block()`, `.Caption(...)`, `.SetSpoiler()`), `Map`/`MapCaption`, `Collage`/`Slideshow` (+`...Caption` variants), `Table`/`Row`/`Cell`, `Details`, `MathBlock`.
## Block payloads and HTML conversion
To build the payload without sending, use `tgfmt.RichHTML(items...)` (plain string) or `tgfmt.RichMessage(items...)` (a ready `tgapi.InputRichMessage` with `skip_entity_detection` enabled so the server does not inject auto-detected entities).
Send the input tree directly when no conversion is needed:
## Direct `tgapi` access
```go
rich := tgapi.InputRichMessage{
Blocks: []tgapi.InputRichBlock{
tgrich.H1(tgrich.Text("Report")),
tgrich.P(tgrich.Text("Ready")),
},
}
```
- `API.SendRichMessage(params)` — send; `params.RichMessage` is the `InputRichMessage`.
- `API.SendRichMessageDraft(params)` — stream a partial message while it is being generated. The draft is an ephemeral ~30-second preview keyed by `DraftID` (updates with the same ID animate); finish by calling `SendRichMessage` with the complete message. Drafts are the only place the `thinking` block appears.
- `EditMessageText.RichMessage` — edit a rich message in place (leave `Text` empty).
- `InputRichMessageContent` — rich content for inline query results (`input_message_content`).
Use `tgrich.BuildHTML(blocks...)` to validate a whole tree and convert it to an HTML-based `InputRichMessage`. `tgrich.ToHTML(block)` is the single-block convenience form. Conversion is useful for logging, previews, or APIs such as `MessageContext.RichAnswer` that send rendered HTML.
`BuildHTML` enables `SkipEntityDetection`, escapes text and attributes, and enforces Telegram's rich-message limits:
- 32768 UTF-8 characters;
- 500 blocks, including nested blocks, list items, and table rows;
- 16 combined levels of nested blocks and formatting;
- 50 media attachments;
- 20 table columns, including `colspan`.
It also validates block discriminators, heading sizes, list marker and checkbox state, table alignment and spans, map coordinates/zoom/dimensions, and media types.
## Media and multipart uploads
Media constructors accept `tgapi.InputMedia`, so `Media` can contain an HTTP URL, Telegram `file_id`, or an `attach://name` reference:
```go
block := tgrich.PhotoWithCaption(
tgapi.InputMedia{Media: "telegram-file-id"},
tgrich.CaptionWithCredit(
tgrich.Text("Launch"),
tgrich.Text("Operations"),
),
)
```
During HTML conversion, media blocks become `tg://photo?id=...`, `tg://video?id=...`, or `tg://audio?id=...` references. The original `InputMedia` values are collected in `InputRichMessage.Media` under stable `media_1`, `media_2`, ... identifiers.
For multipart uploads, the `attach://` name and multipart field name must match:
```go
rich, err := tgrich.BuildHTML(
tgrich.Photo(tgapi.InputMedia{Media: "attach://report"}),
)
if err != nil {
return err
}
_, err = uploader.SendRichMessage(
tgapi.SendRichMessage{ChatID: chatID, RichMessage: rich},
tgapi.NewUploaderFile("report.jpg", data).SetAttachName("report"),
)
```
The same upload model is available through `Uploader.SendRichMessageDraft`.
## Direct API access
- `API.SendRichMessage` sends a completed rich message.
- `API.SendRichMessageDraft` updates an ephemeral preview while content is generated. `Thinking` is valid only in drafts.
- `Uploader.SendRichMessage` and `Uploader.SendRichMessageDraft` upload files referenced through `attach://`.
- `EditMessageText.RichMessage` edits an existing rich message; leave `Text` empty.
- `InputRichMessageContent` supplies rich content for inline results.
## Receiving
`Message.RichMessage` (`*tgapi.RichMessage`) is parsed automatically. Walk it with type switches:
`Message.RichMessage` is parsed automatically. Walk the received tree with type switches:
```go
for _, block := range msg.RichMessage.Blocks {
@@ -74,23 +144,25 @@ for _, block := range msg.RichMessage.Blocks {
handleText(b.Text)
case tgapi.RichBlockList:
for _, item := range b.Items {
// item.Label is the ready-made visible marker: "1.", "c.", "vii.", "•"
// Label is the server-rendered marker: "1.", "c.", "vii.", or "•".
}
}
}
```
Unknown future node types that carry a `text` field are preserved as `RichTextWrap`/`RichBlockWrap` instead of failing, so parsing survives API additions.
Unknown future nodes with a `text` field are preserved as `RichTextWrap` or `RichBlockWrap`, allowing parsers to tolerate compatible Bot API additions.
## Pitfalls
- **Media is URL-only.** In HTML mode `<img>`/`<video>`/`<audio>` accept only HTTP(S) URLs; `file_id` does not work. The DSL signatures (`Photo(url string)`) make this explicit.
- **List markers are server-rendered.** Send `<ol type start>`/`<li value>` semantics via `OlOpts`/`SetValue`/`SetType`; the visible label ("c.", "vii.") comes back computed in `RichBlockListItem.Label`. Do not generate labels yourself.
- **One `<video>`/`<audio>` tag, two block types.** The server distinguishes video vs animation and audio vs voice note by the URL extension (`.gif` → animation, `.ogg` → voice note).
- **`skip_entity_detection`** defaults to `true` in `tgfmt.RichMessage(...)`; without it the server adds auto-detected entities and the received tree diverges from what you sent.
- `InputRichBlock*` and received `RichBlock*` are different type families.
- `Thinking` can be sent only through `sendRichMessageDraft`.
- A checked list item must also have a checkbox.
- Ordered items require one of the supported marker types; unordered items must not set `Value`.
- Table width includes column spans.
- `BuildHTML` returns validation errors before making an API request; use `errors.Is` with the exported `tgrich.ErrRich*` values when callers need to classify them.
## Where to go next
- [[MessageContext]] — the rest of the reply helpers.
- [[tgapi-Overview]] — the low-level client the rich methods live on.
- [[Drafts]] staged assembly of regular (non-rich) messages.
- [[MessageContext]] - handler response helpers.
- [[tgapi-Overview]] - low-level API client.
- [[Drafts]] - staged message generation.
+133
@@ -0,0 +1,133 @@
# DRAFT: план миграции Laniakea v2
English version: [[V2-Migration-Plan]]
> **Статус: DRAFT.** Эта страница описывает возможное направление v2, а не реализованный или утверждённый публичный API. Имена, сигнатуры, значения по умолчанию и состав релиза могут измениться.
Laniakea `v1.2.0` сохраняет публичный API v1 и одновременно добавляет совместимые мосты к более строгому, context-aware и ограниченному поведению. Будущая v2 сможет использовать эти мосты, чтобы убрать неоднозначные контракты и устаревшие aliases через в основном механическую миграцию, не смешивая breaking cleanup с minor-релизом.
## Уже принятые решения
- `v1.2.0` должна сохранять source compatibility с v1 везде, где существующая возможность работает.
- Перечисленные здесь breaking changes откладываются до новой major-версии.
- В новом коде на v1.2 стоит предпочитать показанные ниже compatibility API, чтобы сократить будущую миграцию.
- Совместимость с Telegram на уровне wire format намеренно не ломается; исправленные JSON-имена полей уже входят в v1.2 как bug fix.
- Этот черновик не назначает дату выхода v2 и не гарантирует реализацию каждого предложения.
## Подготовка совместимости в v1.2.0
| Область | Предпочтительный API v1.2 | Что сохраняется для совместимости в v1.2 |
| --- | --- | --- |
| Участники опроса | `PollAnswer.VoterUser()` и `VoterChatInfo()` | `User` и `VoterChat` остаются value-полями |
| Runners | `NewContextRunner(...)` | `RunnerFn` и `NewRunner(...)` остаются доступны |
| Callback payloads | `CallbackData.EncodeValidated(...)` | `Encode(...)` сохраняет сигнатуру без `error` |
| Построение клавиатур | `Build()`/`Validate()` кнопки и `GetValidated()`/`Validate()` клавиатуры | Существующие fluent builders и `Get()` остаются доступны |
| Rich messages | `UnmarshalRichMessageStrict(...)` для недоверенного ввода | `UnmarshalRichMessage(...)` остаётся permissive для пустого root |
| Загрузка файлов | `GetFileByLinkLimit(...)` или `OpenFileByLink(...)` | Неограниченный `GetFileByLink(...)` остаётся доступен |
| Очистка реакций | `DeleteAllMessageReactionsWithContext(...)` | Singular alias с опечаткой остаётся deprecated |
| Поля Telegram | Существующие Go-поля с исправленными wire keys | Имена полей на уровне исходного кода не меняются |
Переход на предпочтительный столбец уже в v1.2 должен сделать большинство изменений v2 видимыми при компиляции и простыми в применении.
## Предлагаемые breaking changes
### 1. Явно представить optional-участника опроса
Изменить `PollAnswer.User` и `PollAnswer.VoterChat` с value-полей на pointers. Telegram присылает ровно один вариант участника, а pointers выражают отсутствие без проверки нулевого ID.
Направление миграции:
```go
// совместимая подготовка на v1.2
user, ok := answer.VoterUser()
if ok {
use(user)
}
// возможная форма v2
if answer.User != nil {
use(answer.User)
}
```
### 2. Сделать cancellation обязательной частью runner
Включить `context.Context` в основной контракт callback для runner. Длительные и I/O-задачи должны останавливаться при завершении polling- или webhook-runtime.
Только иллюстрация API:
```go
runner := laniakea.NewRunner("sync", func(ctx context.Context, bot *laniakea.Bot[*App]) error {
return app.Sync(ctx)
})
```
Финальные имена могут сохранить `NewContextRunner`: черновик фиксирует context-aware поведение, но не конкретное имя конструктора.
### 3. Не позволять по умолчанию создать невалидную клавиатуру
Перенести проверку длины callback data, типа payload и размера строки в обычный путь построения. Fluent-методы без `error` могут быть заменены или дополнены builder-ом, возвращающим ошибку. Неограниченные строки должны включаться явной опцией вместо перегрузки `maxRow <= 0`.
Точная форма builder-а ещё не выбрана. Текущим мостом для миграции служат методы v1.2 `Build`, `Validate`, `EncodeValidated` и `GetValidated`.
### 4. Использовать строгие и ограниченные defaults
- Сделать так, чтобы `UnmarshalRichMessage(...)` по умолчанию отклонял `null`, `{}`, а также отсутствующий или null `blocks`.
- Сохранять неизвестные rich block objects без потерь вместо сведения к известной текстовой обёртке.
- Удалить или заменить неограниченный `GetFileByLink(...)`; предпочесть обязательный лимит байт или streaming body.
Эти изменения делают недоверенный ввод и удалённые загрузки безопаснее, но точные типы ошибок и fallback ещё не выбраны.
### 5. Удалить deprecated compatibility names
- Удалить `DeleteAllMessageReactionWithContext(...)` в пользу `DeleteAllMessageReactionsWithContext(...)`.
- Рассмотреть следующие переименования экспортированных полей:
- `ChatFullInfo.AvailableReaction``AvailableReactions`;
- `ReplyParameters.QuoteParsingMode``QuoteParseMode`;
- `InputChecklist.OtherCanAddTasks``OthersCanAddTasks`;
- `InputChecklist.OtherCanMarkTasksAsDone``OthersCanMarkTasksAsDone`.
Wire keys уже исправлены в v1.2. Эти предложения затрагивают только имена в исходном Go-коде.
## Ожидаемая работа при миграции
| Вызов или поле v1 | Предпочтительная подготовка сейчас | Возможная миграция на v2 |
| --- | --- | --- |
| `answer.User.ID != 0` | `answer.VoterUser()` | nil-check `answer.User` |
| `NewRunner(name, func(*Bot) error)` | `NewContextRunner(name, func(context.Context, *Bot) error)` | перейти на основной context-aware конструктор |
| `keyboard.Get()` | `keyboard.GetValidated()` | обрабатывать ошибку из основного пути построения |
| `data.Encode(kind)` | `data.EncodeValidated(kind)` | обрабатывать возвращаемую ошибку валидации |
| `UnmarshalRichMessage(data)` для внешних данных | `UnmarshalRichMessageStrict(data)` | strict-поведение становится default |
| `GetFileByLink(link)` | `GetFileByLinkLimit(link, max)` или `OpenFileByLink(link)` | использовать только bounded- или streaming-загрузку |
| Singular alias для реакций | Plural-метод | дополнительных изменений не потребуется |
## Открытые решения дизайна
- Какой публичный тип должен сохранять неизвестные rich objects для lossless round trip?
- Должна ли buffered-загрузка требовать лимит от caller, иметь документированный default или быть удалена в пользу streaming?
- Нужно ли сохранять какие-либо deprecated aliases ещё на один major-цикл?
- Должна ли валидация клавиатур использовать immutable builders, mutation с `error` или финальный шаг валидации?
- Нужны ли временные accessors для переименованных полей в prerelease-версиях v2?
- Какие предложения должны войти в первый релиз v2, а какие можно перенести в последующий minor v2?
## Критерии готовности релиза
До первого стабильного релиза v2 нужно:
1. Зафиксировать предлагаемый публичный API и опубликовать парные English/Russian инструкции по миграции.
2. Сгенерировать и проверить полный diff публичного API относительно последнего релиза v1.
3. Добавить compile-focused примеры миграции и regression tests для каждого удаляемого compatibility path.
4. Проверить Telegram JSON fixtures и lossless round trip неизвестных rich objects.
5. Определить лимиты загрузки и правила владения streaming response bodies.
6. Решить или явно отложить каждый открытый вопрос этой страницы.
7. Согласованно отметить завершённые framework backlog items в `Framework-Backlog` и `CHANGELOG.md` основного репозитория.
## Связанные страницы
- [[Migration-RU]]
- [[Semver-and-Releases-RU]]
- [[Framework-Backlog-RU]]
- [[Runners-RU]]
- [[Inline-Keyboards-and-Payloads-RU]]
- [[Rich-Messages-RU]]
- [[tgapi-Overview-RU]]
+133
@@ -0,0 +1,133 @@
# DRAFT: Laniakea v2 Migration Plan
Russian version: [[V2-Migration-Plan-RU]]
> **Status: DRAFT.** This page describes a possible v2 direction, not an implemented or committed public API. Names, signatures, defaults, and release scope are subject to change.
Laniakea `v1.2.0` preserves the v1 public API while adding compatibility bridges for stricter, context-aware, and bounded behavior. A future v2 can use those bridges to remove ambiguous contracts and legacy aliases through a mostly mechanical migration instead of mixing breaking cleanup into a minor release.
## Decisions already made
- `v1.2.0` must remain source-compatible with v1 wherever the existing feature works.
- Breaking changes listed here are deferred to a new major version.
- New v1.2 code should prefer the compatibility APIs shown below to reduce future migration work.
- Telegram wire compatibility is not intentionally broken; corrected JSON field names are already a v1.2 bug fix.
- This draft does not define a v2 release date or guarantee that every proposal will ship.
## Compatibility preparation in v1.2.0
| Area | Preferred v1.2 API | Compatibility retained in v1.2 |
| --- | --- | --- |
| Poll voters | `PollAnswer.VoterUser()` and `VoterChatInfo()` | `User` and `VoterChat` remain value fields |
| Runners | `NewContextRunner(...)` | `RunnerFn` and `NewRunner(...)` remain available |
| Callback payloads | `CallbackData.EncodeValidated(...)` | `Encode(...)` keeps its error-free signature |
| Keyboard construction | button `Build()`/`Validate()` and keyboard `GetValidated()`/`Validate()` | Existing fluent builders and `Get()` remain available |
| Rich messages | `UnmarshalRichMessageStrict(...)` for untrusted input | `UnmarshalRichMessage(...)` remains permissive for an empty root |
| File downloads | `GetFileByLinkLimit(...)` or `OpenFileByLink(...)` | Unbounded `GetFileByLink(...)` remains available |
| Reaction cleanup | `DeleteAllMessageReactionsWithContext(...)` | Misspelled singular alias remains deprecated |
| Telegram field names | Use the existing Go fields with corrected wire keys | Source-level field names remain unchanged |
Adopting the preferred column in v1.2 should make most v2 changes compile-time-visible and straightforward to apply.
## Proposed breaking changes
### 1. Make optional poll voters explicit
Change `PollAnswer.User` and `PollAnswer.VoterChat` from value fields to pointers. Telegram sends exactly one voter variant, and pointers represent absence without relying on a zero ID.
Migration direction:
```go
// v1.2-compatible preparation
user, ok := answer.VoterUser()
if ok {
use(user)
}
// possible v2 form
if answer.User != nil {
use(answer.User)
}
```
### 2. Make runner cancellation mandatory
Make `context.Context` part of the primary runner callback contract. Long-running and I/O-bound work should stop with polling or webhook runtime cancellation.
Illustrative API only:
```go
runner := laniakea.NewRunner("sync", func(ctx context.Context, bot *laniakea.Bot[*App]) error {
return app.Sync(ctx)
})
```
The final names may instead retain `NewContextRunner`; this draft commits to context-aware behavior, not constructor spelling.
### 3. Make invalid keyboards unrepresentable by default
Move callback-data length checks, payload validation, and row-size validation into the normal construction path. Error-free fluent methods may be replaced or complemented by a builder that returns an error. Unlimited rows should require an explicit option rather than overloading `maxRow <= 0`.
The exact builder shape is still open. Existing v1.2 `Build`, `Validate`, `EncodeValidated`, and `GetValidated` methods are the migration bridge.
### 4. Use strict and bounded defaults
- Make `UnmarshalRichMessage(...)` reject `null`, `{}`, and a missing or null `blocks` field by default.
- Preserve unknown rich block objects losslessly instead of reducing them to a known text wrapper.
- Remove or replace unbounded `GetFileByLink(...)`; prefer a required byte limit or a streaming body.
These changes make untrusted input and remote downloads safer, but their exact error and fallback types remain open.
### 5. Remove deprecated compatibility names
- Remove `DeleteAllMessageReactionWithContext(...)` in favor of `DeleteAllMessageReactionsWithContext(...)`.
- Consider the following exported field renames:
- `ChatFullInfo.AvailableReaction``AvailableReactions`;
- `ReplyParameters.QuoteParsingMode``QuoteParseMode`;
- `InputChecklist.OtherCanAddTasks``OthersCanAddTasks`;
- `InputChecklist.OtherCanMarkTasksAsDone``OthersCanMarkTasksAsDone`.
The wire keys are already correct in v1.2. These proposals affect Go source names only.
## Expected migration work
| v1 call or field | Preferred preparation now | Possible v2 migration |
| --- | --- | --- |
| `answer.User.ID != 0` | `answer.VoterUser()` | nil-check `answer.User` |
| `NewRunner(name, func(*Bot) error)` | `NewContextRunner(name, func(context.Context, *Bot) error)` | use the primary context-aware constructor |
| `keyboard.Get()` | `keyboard.GetValidated()` | handle construction error from the default path |
| `data.Encode(kind)` | `data.EncodeValidated(kind)` | handle the returned validation error |
| `UnmarshalRichMessage(data)` for external data | `UnmarshalRichMessageStrict(data)` | strict behavior becomes the default |
| `GetFileByLink(link)` | `GetFileByLinkLimit(link, max)` or `OpenFileByLink(link)` | use bounded or streaming download only |
| singular reaction alias | plural method | no further source change |
## Open design decisions
- What public type should preserve unknown rich objects for lossless round trips?
- Should buffered file downloads require a caller-provided limit, provide a documented default, or be removed in favor of streaming?
- Should any deprecated aliases survive one additional major cycle?
- Should keyboard validation use immutable builders, error-returning mutation, or a final validation step?
- Should renamed fields receive temporary accessors during v2 prereleases?
- Which proposals belong in the first v2 release rather than a later v2 minor release?
## Release gates
Before the first stable v2 release:
1. Freeze the proposed public API and publish paired English/Russian migration guidance.
2. Generate and review a complete public API diff against the latest v1 release.
3. Add compile-focused migration examples and regression tests for every removed compatibility path.
4. Verify Telegram JSON fixtures and unknown rich-object round trips.
5. Define bounded download defaults and ownership rules for streaming response bodies.
6. Resolve or explicitly defer every open decision on this page.
7. Mark completed framework backlog items consistently in `Framework-Backlog` and the main repository `CHANGELOG.md`.
## Related pages
- [[Migration]]
- [[Semver-and-Releases]]
- [[Framework-Backlog]]
- [[Runners]]
- [[Inline-Keyboards-and-Payloads]]
- [[Rich-Messages]]
- [[tgapi-Overview]]
+1
@@ -35,5 +35,6 @@
## Maintenance
- [[Migration]]
- [[V2-Migration-Plan]] (DRAFT)
- [[Semver-and-Releases]]
- [[Framework-Backlog]]