REPOSITORY / ScuroNeko/Laniakea
Wiki
(doc): v1.2 wiki update
+1
-1
@@ -6,7 +6,7 @@ English version: [[Getting-Started]]
|
|||||||
|
|
||||||
## Что нужно сначала
|
## Что нужно сначала
|
||||||
|
|
||||||
- Go 1.24 или новее
|
- Go 1.26 или новее
|
||||||
- токен Telegram-бота от `@BotFather`
|
- токен Telegram-бота от `@BotFather`
|
||||||
- Go-модуль, который может импортировать `git.scuroneko.dev/scuroneko/laniakea`
|
- 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.
|
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
|
## What you need first
|
||||||
- Go 1.24 or newer
|
- Go 1.26 or newer
|
||||||
- a Telegram bot token from `@BotFather`
|
- a Telegram bot token from `@BotFather`
|
||||||
- a module that can import `git.scuroneko.dev/scuroneko/laniakea`
|
- a module that can import `git.scuroneko.dev/scuroneko/laniakea`
|
||||||
|
|
||||||
|
|||||||
+1
@@ -40,6 +40,7 @@ English version: [[Home]]
|
|||||||
## Миграция и сопровождение
|
## Миграция и сопровождение
|
||||||
|
|
||||||
- [[Migration-RU]]
|
- [[Migration-RU]]
|
||||||
|
- [[V2-Migration-Plan-RU]] — DRAFT; предполагаемые breaking changes и compatibility bridges v1.2
|
||||||
- [[Semver-and-Releases-RU]]
|
- [[Semver-and-Releases-RU]]
|
||||||
- [[Framework-Backlog-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 and Maintenance
|
||||||
- [[Migration]]
|
- [[Migration]]
|
||||||
|
- [[V2-Migration-Plan]] — DRAFT; proposed breaking changes and v1.2 compatibility bridges
|
||||||
- [[Semver-and-Releases]]
|
- [[Semver-and-Releases]]
|
||||||
- [[Framework-Backlog]]
|
- [[Framework-Backlog]]
|
||||||
|
|||||||
+4
-4
@@ -107,16 +107,16 @@ English version: [[MessageContext]]
|
|||||||
|
|
||||||
## Rich-сообщения
|
## Rich-сообщения
|
||||||
|
|
||||||
`RichAnswer(...)` и `RichAnswerKeyboard(...)` отправляют структурированные rich-сообщения Bot API 10.1, собранные из фрагментов `tgfmt`:
|
`RichAnswer(...)` и `RichAnswerKeyboard(...)` валидируют и отправляют input rich-блоки Bot API 10.2, собранные через `tgrich`:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
ctx.RichAnswer(
|
ctx.RichAnswer(
|
||||||
tgfmt.H1(tgfmt.NewRich("Отчёт")),
|
tgrich.H1(tgrich.Text("Отчёт")),
|
||||||
tgfmt.P(tgfmt.NewRich("всё работает").Bold()),
|
tgrich.P(tgrich.Bold(tgrich.Text("всё работает"))),
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
Полный DSL и приёмная сторона описаны на странице [[Rich-Messages-RU]].
|
Все конструкторы, загрузка медиа, валидация и приёмная сторона описаны на странице [[Rich-Messages-RU]].
|
||||||
|
|
||||||
## Drafts и localization
|
## Drafts и localization
|
||||||
|
|
||||||
|
|||||||
+4
-4
@@ -113,16 +113,16 @@ This is useful for:
|
|||||||
|
|
||||||
## Rich messages
|
## 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
|
```go
|
||||||
ctx.RichAnswer(
|
ctx.RichAnswer(
|
||||||
tgfmt.H1(tgfmt.NewRich("Report")),
|
tgrich.H1(tgrich.Text("Report")),
|
||||||
tgfmt.P(tgfmt.NewRich("all systems go").Bold()),
|
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
|
## Markdown helpers
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ English version: [[Migration]]
|
|||||||
- [[Bot-Lifecycle-RU]]
|
- [[Bot-Lifecycle-RU]]
|
||||||
- [[Commands-and-Plugins-RU]]
|
- [[Commands-and-Plugins-RU]]
|
||||||
- [[Inline-Keyboards-and-Payloads-RU]]
|
- [[Inline-Keyboards-and-Payloads-RU]]
|
||||||
|
- [[V2-Migration-Plan-RU]] — DRAFT-план cleanup для v2 и compatibility bridges v1.2
|
||||||
- [[Semver-and-Releases-RU]]
|
- [[Semver-and-Releases-RU]]
|
||||||
|
|
||||||
## Что важно помнить при апгрейде
|
## Что важно помнить при апгрейде
|
||||||
|
|||||||
+1
@@ -12,6 +12,7 @@ Also see:
|
|||||||
- [[Bot-Lifecycle]] for the current startup and shutdown model;
|
- [[Bot-Lifecycle]] for the current startup and shutdown model;
|
||||||
- [[Commands-and-Plugins]] for handler registration patterns;
|
- [[Commands-and-Plugins]] for handler registration patterns;
|
||||||
- [[Inline-Keyboards-and-Payloads]] for payload-type behavior;
|
- [[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.
|
- [[Semver-and-Releases]] for the project's versioning intent.
|
||||||
|
|
||||||
## Recommended upgrade strategy
|
## Recommended upgrade strategy
|
||||||
|
|||||||
+117
-45
@@ -1,71 +1,141 @@
|
|||||||
# Rich-сообщения
|
# 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 для отправки дерева блоков не существует.
|
Исходящие и входящие типы блоков намеренно разделены. `InputRichBlock*` описывает данные, принимаемые Telegram, а `RichBlock*` — нормализованное дерево, возвращаемое Telegram.
|
||||||
- **Принимается дерево, а не разметка.** Входящее 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`.
|
|
||||||
|
|
||||||
## Отправка из обработчика
|
## Отправка из обработчика
|
||||||
|
|
||||||
`MessageContext.RichAnswer(...)` принимает фрагменты `tgfmt` и отправляет их через `sendRichMessage`:
|
`MessageContext.RichAnswer` принимает input-блоки и собирает валидированное HTML rich-сообщение:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
import "git.scuroneko.dev/scuroneko/laniakea/tgfmt"
|
import (
|
||||||
|
"git.scuroneko.dev/scuroneko/laniakea/tgrich"
|
||||||
|
)
|
||||||
|
|
||||||
func report(ctx *laniakea.MessageContext, _ struct{}) error {
|
func report(ctx *laniakea.MessageContext, _ struct{}) error {
|
||||||
ctx.RichAnswer(
|
ctx.RichAnswer(
|
||||||
tgfmt.H1(tgfmt.NewRich("Дневной отчёт")),
|
tgrich.H1(tgrich.Text("Дневной отчёт")),
|
||||||
tgfmt.P(
|
tgrich.P(tgrich.Concat(
|
||||||
tgfmt.NewRich("Статус: "),
|
tgrich.Text("Статус: "),
|
||||||
tgfmt.NewRich("всё работает").Bold(),
|
tgrich.Bold(tgrich.Text("всё работает")),
|
||||||
),
|
)),
|
||||||
tgfmt.Ul(
|
tgrich.Ul(
|
||||||
tgfmt.LiCheckbox(true, tgfmt.NewRich("бэкапы")),
|
tgrich.NewListItem(tgrich.P(tgrich.Text("бэкапы"))).
|
||||||
tgfmt.LiCheckbox(false, tgfmt.NewRich("миграция")),
|
SetCheckbox().SetChecked().Build(),
|
||||||
|
tgrich.NewListItem(tgrich.P(tgrich.Text("миграция"))).
|
||||||
|
SetCheckbox().Build(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
return nil
|
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-содержимое (жирный, ссылки, эмодзи, время, математика, ...);
|
Блочные конструкторы: `P`, `H1`-`H6`, `Pre`, `CodeBlock`, `Footer`, `Hr`, `Math`, `Anchor`, `Ul`, `Ol`, цитаты, коллажи, слайд-шоу, таблицы, details, карты, анимации, аудио, фото, видео, голосовые сообщения и доступный только в черновиках блок `Thinking`.
|
||||||
- `tgfmt.RichBlock` — блочное содержимое (заголовки, абзацы, списки, таблицы, медиа, ...).
|
|
||||||
|
|
||||||
Блочные конструкторы принимают только `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`.
|
`tgrich.BuildHTML(blocks...)` валидирует целое дерево и преобразует его в HTML-вариант `InputRichMessage`. `tgrich.ToHTML(block)` — сокращённая форма для одного блока. Преобразование полезно для логов, превью и API вроде `MessageContext.RichAnswer`, которые отправляют отрендеренный HTML.
|
||||||
- `API.SendRichMessageDraft(params)` — стриминг частичного сообщения во время генерации. Черновик — эфемерное превью на ~30 секунд с ключом `DraftID` (обновления с тем же ID анимируются); в конце вызовите `SendRichMessage` с полным сообщением. Черновики — единственное место, где встречается блок `thinking`.
|
|
||||||
- `EditMessageText.RichMessage` — редактирование rich-сообщения на месте (`Text` оставьте пустым).
|
`BuildHTML` включает `SkipEntityDetection`, экранирует текст и атрибуты и проверяет официальные лимиты Telegram:
|
||||||
- `InputRichMessageContent` — rich-контент для результатов inline-запросов (`input_message_content`).
|
|
||||||
|
- 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
|
```go
|
||||||
for _, block := range msg.RichMessage.Blocks {
|
for _, block := range msg.RichMessage.Blocks {
|
||||||
@@ -74,23 +144,25 @@ for _, block := range msg.RichMessage.Blocks {
|
|||||||
handleText(b.Text)
|
handleText(b.Text)
|
||||||
case tgapi.RichBlockList:
|
case tgapi.RichBlockList:
|
||||||
for _, item := range b.Items {
|
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)`) делают это явным.
|
- `InputRichBlock*` и входящие `RichBlock*` — разные семейства типов.
|
||||||
- **Маркеры списков рендерит сервер.** Отправляйте семантику `<ol type start>`/`<li value>` через `OlOpts`/`SetValue`/`SetType`; видимый маркер («c.», «vii.») возвращается уже вычисленным в `RichBlockListItem.Label`. Не генерируйте маркеры сами.
|
- `Thinking` можно отправлять только через `sendRichMessageDraft`.
|
||||||
- **Один тег `<video>`/`<audio>` — два типа блоков.** Сервер различает видео/анимацию и аудио/голосовое по расширению URL (`.gif` → animation, `.ogg` → voice note).
|
- У отмеченного элемента списка обязательно должен быть checkbox.
|
||||||
- **`skip_entity_detection`** в `tgfmt.RichMessage(...)` по умолчанию `true`; без него сервер добавит авто-entities, и принятое дерево разойдётся с отправленным.
|
- Ordered items требуют поддерживаемого типа маркера; unordered items не должны задавать `Value`.
|
||||||
|
- Ширина таблицы учитывает column spans.
|
||||||
|
- `BuildHTML` возвращает ошибки валидации до API-запроса; для их классификации используйте `errors.Is` с экспортируемыми значениями `tgrich.ErrRich*`.
|
||||||
|
|
||||||
## Что читать дальше
|
## Что читать дальше
|
||||||
|
|
||||||
- [[MessageContext]] — остальные вспомогательные методы ответа.
|
- [[MessageContext]] — вспомогательные методы ответа.
|
||||||
- [[tgapi-Overview]] — низкоуровневый клиент, на котором живут rich-методы.
|
- [[tgapi-Overview]] — низкоуровневый API-клиент.
|
||||||
- [[Drafts]] — поэтапная сборка обычных (не rich) сообщений.
|
- [[Drafts]] — поэтапная генерация сообщений.
|
||||||
|
|||||||
+117
-45
@@ -1,71 +1,141 @@
|
|||||||
# Rich Messages
|
# 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.
|
Outgoing and incoming block types are intentionally separate. `InputRichBlock*` describes data accepted by Telegram; `RichBlock*` describes the normalized tree returned by Telegram.
|
||||||
- **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`.
|
|
||||||
|
|
||||||
## Sending from a handler
|
## 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
|
```go
|
||||||
import "git.scuroneko.dev/scuroneko/laniakea/tgfmt"
|
import (
|
||||||
|
"git.scuroneko.dev/scuroneko/laniakea/tgrich"
|
||||||
|
)
|
||||||
|
|
||||||
func report(ctx *laniakea.MessageContext, _ struct{}) error {
|
func report(ctx *laniakea.MessageContext, _ struct{}) error {
|
||||||
ctx.RichAnswer(
|
ctx.RichAnswer(
|
||||||
tgfmt.H1(tgfmt.NewRich("Daily report")),
|
tgrich.H1(tgrich.Text("Daily report")),
|
||||||
tgfmt.P(
|
tgrich.P(tgrich.Concat(
|
||||||
tgfmt.NewRich("Status: "),
|
tgrich.Text("Status: "),
|
||||||
tgfmt.NewRich("all systems go").Bold(),
|
tgrich.Bold(tgrich.Text("all systems go")),
|
||||||
),
|
)),
|
||||||
tgfmt.Ul(
|
tgrich.Ul(
|
||||||
tgfmt.LiCheckbox(true, tgfmt.NewRich("backups")),
|
tgrich.NewListItem(tgrich.P(tgrich.Text("backups"))).
|
||||||
tgfmt.LiCheckbox(false, tgfmt.NewRich("migration")),
|
SetCheckbox().SetChecked().Build(),
|
||||||
|
tgrich.NewListItem(tgrich.P(tgrich.Text("migration"))).
|
||||||
|
SetCheckbox().Build(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
return nil
|
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, ...);
|
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.
|
||||||
- `tgfmt.RichBlock` — block content (headings, paragraphs, lists, tables, media, ...).
|
|
||||||
|
|
||||||
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`.
|
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.
|
||||||
- `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).
|
`BuildHTML` enables `SkipEntityDetection`, escapes text and attributes, and enforces Telegram's rich-message limits:
|
||||||
- `InputRichMessageContent` — rich content for inline query results (`input_message_content`).
|
|
||||||
|
- 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
|
## 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
|
```go
|
||||||
for _, block := range msg.RichMessage.Blocks {
|
for _, block := range msg.RichMessage.Blocks {
|
||||||
@@ -74,23 +144,25 @@ for _, block := range msg.RichMessage.Blocks {
|
|||||||
handleText(b.Text)
|
handleText(b.Text)
|
||||||
case tgapi.RichBlockList:
|
case tgapi.RichBlockList:
|
||||||
for _, item := range b.Items {
|
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
|
## 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.
|
- `InputRichBlock*` and received `RichBlock*` are different type families.
|
||||||
- **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.
|
- `Thinking` can be sent only through `sendRichMessageDraft`.
|
||||||
- **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).
|
- A checked list item must also have a checkbox.
|
||||||
- **`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.
|
- 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
|
## Where to go next
|
||||||
|
|
||||||
- [[MessageContext]] — the rest of the reply helpers.
|
- [[MessageContext]] - handler response helpers.
|
||||||
- [[tgapi-Overview]] — the low-level client the rich methods live on.
|
- [[tgapi-Overview]] - low-level API client.
|
||||||
- [[Drafts]] — staged assembly of regular (non-rich) messages.
|
- [[Drafts]] - staged message generation.
|
||||||
|
|||||||
@@ -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
|
## Maintenance
|
||||||
- [[Migration]]
|
- [[Migration]]
|
||||||
|
- [[V2-Migration-Plan]] (DRAFT)
|
||||||
- [[Semver-and-Releases]]
|
- [[Semver-and-Releases]]
|
||||||
- [[Framework-Backlog]]
|
- [[Framework-Backlog]]
|
||||||
|
|||||||
Reference in New Issue
Block a user