diff --git a/Getting-Started-RU.md b/Getting-Started-RU.md index c73cf0e..2a7371d 100644 --- a/Getting-Started-RU.md +++ b/Getting-Started-RU.md @@ -6,7 +6,7 @@ English version: [[Getting-Started]] ## Что нужно сначала -- Go 1.24 или новее +- Go 1.26 или новее - токен Telegram-бота от `@BotFather` - Go-модуль, который может импортировать `git.scuroneko.dev/scuroneko/laniakea` diff --git a/Getting-Started.md b/Getting-Started.md index 4e42dc1..9edc1c8 100644 --- a/Getting-Started.md +++ b/Getting-Started.md @@ -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` diff --git a/Home-RU.md b/Home-RU.md index ba66efc..396add4 100644 --- a/Home-RU.md +++ b/Home-RU.md @@ -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]] diff --git a/Home.md b/Home.md index ea95b6b..0064bc9 100644 --- a/Home.md +++ b/Home.md @@ -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]] diff --git a/MessageContext-RU.md b/MessageContext-RU.md index f7779b8..1cb4495 100644 --- a/MessageContext-RU.md +++ b/MessageContext-RU.md @@ -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 diff --git a/MessageContext.md b/MessageContext.md index a995e58..f52d4a1 100644 --- a/MessageContext.md +++ b/MessageContext.md @@ -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 diff --git a/Migration-RU.md b/Migration-RU.md index 715a668..f440fcc 100644 --- a/Migration-RU.md +++ b/Migration-RU.md @@ -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]] ## Что важно помнить при апгрейде diff --git a/Migration.md b/Migration.md index c10b416..a613e3d 100644 --- a/Migration.md +++ b/Migration.md @@ -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 diff --git a/Rich-Messages-RU.md b/Rich-Messages-RU.md index 7b740a0..64412eb 100644 --- a/Rich-Messages-RU.md +++ b/Rich-Messages-RU.md @@ -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-режиме ``/`