diff --git a/MessageContext-RU.md b/MessageContext-RU.md index eed9dce..f7779b8 100644 --- a/MessageContext-RU.md +++ b/MessageContext-RU.md @@ -105,6 +105,19 @@ English version: [[MessageContext]] Они покрывают самые частые callback-сценарии без ручного хождения в `tgapi`. +## Rich-сообщения + +`RichAnswer(...)` и `RichAnswerKeyboard(...)` отправляют структурированные rich-сообщения Bot API 10.1, собранные из фрагментов `tgfmt`: + +```go +ctx.RichAnswer( + tgfmt.H1(tgfmt.NewRich("Отчёт")), + tgfmt.P(tgfmt.NewRich("всё работает").Bold()), +) +``` + +Полный DSL и приёмная сторона описаны на странице [[Rich-Messages-RU]]. + ## Drafts и localization У `MessageContext` есть: diff --git a/MessageContext.md b/MessageContext.md index f111dee..a995e58 100644 --- a/MessageContext.md +++ b/MessageContext.md @@ -111,6 +111,19 @@ This is useful for: - generated summaries - reports with an action button at the end +## Rich messages + +Use `RichAnswer(...)` and `RichAnswerKeyboard(...)` to send structured Bot API 10.1 rich messages built from `tgfmt` fragments: + +```go +ctx.RichAnswer( + tgfmt.H1(tgfmt.NewRich("Report")), + tgfmt.P(tgfmt.NewRich("all systems go").Bold()), +) +``` + +See [[Rich-Messages]] for the full DSL and the receive side. + ## Markdown helpers Use: diff --git a/Rich-Messages-RU.md b/Rich-Messages-RU.md new file mode 100644 index 0000000..7b740a0 --- /dev/null +++ b/Rich-Messages-RU.md @@ -0,0 +1,96 @@ +# Rich-сообщения + +Rich-сообщения — механизм Bot API 10.1 для сильно структурированного текста: заголовки, абзацы, списки с чекбоксами, таблицы, медиа-коллажи, раскрывающиеся секции, математика и другое. Laniakea поддерживает их целиком: типизированный HTML-DSL для отправки, типизированные wire-структуры для приёма и стриминг черновиков. + +## Ключевая асимметрия + +Telegram сделал отправку и приём намеренно разными, и Laniakea повторяет это разделение: + +- **Отправляется разметка, а не дерево.** Единственный способ отправить 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`. + +## Отправка из обработчика + +`MessageContext.RichAnswer(...)` принимает фрагменты `tgfmt` и отправляет их через `sendRichMessage`: + +```go +import "git.scuroneko.dev/scuroneko/laniakea/tgfmt" + +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("миграция")), + ), + ) + return nil +} +``` + +`RichAnswerKeyboard(kb, items...)` прикрепляет inline-клавиатуру. + +## DSL в `tgfmt` + +Два типа фрагментов превращают невалидную вложенность в ошибку компиляции: + +- `tgfmt.Rich` — inline-содержимое (жирный, ссылки, эмодзи, время, математика, ...); +- `tgfmt.RichBlock` — блочное содержимое (заголовки, абзацы, списки, таблицы, медиа, ...). + +Блочные конструкторы принимают только `Rich`-аргументы, поэтому таблица внутри абзаца не скомпилируется. На *верхнем уровне* допустимы оба (`RichItem`): соседний inline-контент Telegram сам собирает в абзацы. + +Сырой текст попадает в DSL ровно одним способом — `tgfmt.NewRich("...")`, который экранирует HTML. Всё остальное — композиция уже безопасных фрагментов. + +Inline-хелперы (методы `Rich`): `Bold`, `Italic`, `Underline`, `Strike`, `Code`, `Mark`, `Sub`, `Sup`, `Spoiler`, `Link`, `Email`, `Phone`, `Mention`, `Anchor`, `AnchorLink`, `Ref`, `Time`, `TimeFormat`, `Math`; свободные функции `Emoji`, `Br`. + +Блочные конструкторы: `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`. + +Чтобы собрать payload без отправки, используйте `tgfmt.RichHTML(items...)` (строка) или `tgfmt.RichMessage(items...)` (готовый `tgapi.InputRichMessage` с включённым `skip_entity_detection`, чтобы сервер не добавлял авто-entities). + +## Прямой доступ через `tgapi` + +- `API.SendRichMessage(params)` — отправка; `params.RichMessage` — это `InputRichMessage`. +- `API.SendRichMessageDraft(params)` — стриминг частичного сообщения во время генерации. Черновик — эфемерное превью на ~30 секунд с ключом `DraftID` (обновления с тем же ID анимируются); в конце вызовите `SendRichMessage` с полным сообщением. Черновики — единственное место, где встречается блок `thinking`. +- `EditMessageText.RichMessage` — редактирование rich-сообщения на месте (`Text` оставьте пустым). +- `InputRichMessageContent` — rich-контент для результатов inline-запросов (`input_message_content`). + +## Приём + +`Message.RichMessage` (`*tgapi.RichMessage`) разбирается автоматически. Обходите его type switch-ами: + +```go +for _, block := range msg.RichMessage.Blocks { + switch b := block.(type) { + case tgapi.RichBlockWrap: // paragraph, footer, thinking + handleText(b.Text) + case tgapi.RichBlockList: + for _, item := range b.Items { + // item.Label — готовый видимый маркер: "1.", "c.", "vii.", "•" + } + } +} +``` + +Неизвестные будущие типы узлов с полем `text` сохраняются как `RichTextWrap`/`RichBlockWrap`, а не роняют разбор — парсинг переживает расширения API. + +## Подводные камни + +- **Медиа — только по URL.** В HTML-режиме ``/`