FILE / ScuroNeko/Laniakea

tgfmt/richtext/richblock.go

Исходный файл и его история в репозитории.
FILE b563e695df50e9cdf78e1c1c90243bcca783eac5
Files
Laniakea/tgfmt/richtext/richblock.go
T
ScuroNeko b563e695df
Golang lint / lint (push) Successful in 2m5s
(new): Telegram API 10.1
(new): rich text
2026-07-01 09:55:02 +03:00

649 lines
18 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package richtext
import (
"encoding/json"
"fmt"
)
// RichBlock — блок в структурированном rich-сообщении.
type RichBlock interface {
isRichBlock()
}
// RichMessage — корневой тип структурированного сообщения (Bot API 10.1).
type RichMessage struct {
Blocks []RichBlock
}
func (m RichMessage) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Blocks []RichBlock `json:"blocks"`
}{m.Blocks})
}
func (m *RichMessage) UnmarshalJSON(data []byte) error {
msg, err := UnmarshalMessage(data)
if err != nil {
return err
}
*m = msg
return nil
}
// ---------------------------------------------------------------------------
// Вспомогательные типы
// ---------------------------------------------------------------------------
// RichBlockListItem — один элемент списка (ordered/unordered).
type RichBlockListItem struct {
Blocks []RichBlock
}
func (i RichBlockListItem) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Blocks []RichBlock `json:"blocks"`
}{i.Blocks})
}
func (i *RichBlockListItem) UnmarshalJSON(data []byte) error {
item, err := unmarshalListItem(data)
if err != nil {
return err
}
*i = item
return nil
}
// RichBlockTableCell — ячейка таблицы.
type RichBlockTableCell struct {
Content []RichBlock
ColumnSpan int
RowSpan int
}
func (c RichBlockTableCell) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Content []RichBlock `json:"content"`
ColumnSpan int `json:"column_span,omitempty"`
RowSpan int `json:"row_span,omitempty"`
}{c.Content, c.ColumnSpan, c.RowSpan})
}
func (c *RichBlockTableCell) UnmarshalJSON(data []byte) error {
cell, err := unmarshalTableCell(data)
if err != nil {
return err
}
*c = cell
return nil
}
// ---------------------------------------------------------------------------
// BlockWrap: чистые текстовые блоки — paragraph, section_heading, footer, thinking.
// ---------------------------------------------------------------------------
// BlockWrap покрывает все блоки, у которых есть только поле text.
type BlockWrap struct {
Tag string
Text RichText
}
func (BlockWrap) isRichBlock() {}
func (b BlockWrap) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Type string `json:"type"`
Text RichText `json:"text"`
}{b.Tag, b.Text})
}
var blockWrapTags = map[string]bool{
"paragraph": true, "section_heading": true,
"footer": true, "thinking": true,
}
func Paragraph(t RichText) BlockWrap { return BlockWrap{"paragraph", t} }
func SectionHeading(t RichText) BlockWrap { return BlockWrap{"section_heading", t} }
func Footer(t RichText) BlockWrap { return BlockWrap{"footer", t} }
func Thinking(t RichText) BlockWrap { return BlockWrap{"thinking", t} }
// ---------------------------------------------------------------------------
// Блок с text + language
// ---------------------------------------------------------------------------
type BlockPreformatted struct {
Text RichText
Language string
}
func (BlockPreformatted) isRichBlock() {}
func (b BlockPreformatted) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Type string `json:"type"`
Text RichText `json:"text"`
Language string `json:"language,omitempty"`
}{"preformatted", b.Text, b.Language})
}
// ---------------------------------------------------------------------------
// Блоки с text + caption
// ---------------------------------------------------------------------------
type BlockBlockQuotation struct {
Text RichText
Caption RichText
}
func (BlockBlockQuotation) isRichBlock() {}
func (b BlockBlockQuotation) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Type string `json:"type"`
Text RichText `json:"text"`
Caption RichText `json:"caption,omitempty"`
}{"block_quotation", b.Text, b.Caption})
}
type BlockPullQuotation struct {
Text RichText
Caption RichText
}
func (BlockPullQuotation) isRichBlock() {}
func (b BlockPullQuotation) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Type string `json:"type"`
Text RichText `json:"text"`
Caption RichText `json:"caption,omitempty"`
}{"pull_quotation", b.Text, b.Caption})
}
// ---------------------------------------------------------------------------
// Список
// ---------------------------------------------------------------------------
type BlockList struct {
Items []RichBlockListItem
Ordered bool
}
func (BlockList) isRichBlock() {}
func (b BlockList) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Type string `json:"type"`
Items []RichBlockListItem `json:"items"`
Ordered bool `json:"ordered"`
}{"list", b.Items, b.Ordered})
}
// ---------------------------------------------------------------------------
// Контейнеры с items []RichBlock + caption
// ---------------------------------------------------------------------------
type BlockCollage struct {
Items []RichBlock
Caption RichText
}
func (BlockCollage) isRichBlock() {}
func (b BlockCollage) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Type string `json:"type"`
Items []RichBlock `json:"items"`
Caption RichText `json:"caption,omitempty"`
}{"collage", b.Items, b.Caption})
}
type BlockSlideshow struct {
Items []RichBlock
Caption RichText
}
func (BlockSlideshow) isRichBlock() {}
func (b BlockSlideshow) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Type string `json:"type"`
Items []RichBlock `json:"items"`
Caption RichText `json:"caption,omitempty"`
}{"slideshow", b.Items, b.Caption})
}
// ---------------------------------------------------------------------------
// Details — раскрывающийся блок
// ---------------------------------------------------------------------------
type BlockDetails struct {
Title RichText
Blocks []RichBlock
Open bool
}
func (BlockDetails) isRichBlock() {}
func (b BlockDetails) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Type string `json:"type"`
Title RichText `json:"title"`
Blocks []RichBlock `json:"blocks"`
Open bool `json:"open"`
}{"details", b.Title, b.Blocks, b.Open})
}
// ---------------------------------------------------------------------------
// Таблица
// ---------------------------------------------------------------------------
type BlockTable struct {
Title RichText
Rows [][]RichBlockTableCell
Bordered bool
Striped bool
}
func (BlockTable) isRichBlock() {}
func (b BlockTable) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Type string `json:"type"`
Title RichText `json:"title,omitempty"`
Rows [][]RichBlockTableCell `json:"rows"`
Bordered bool `json:"bordered"`
Striped bool `json:"striped"`
}{"table", b.Title, b.Rows, b.Bordered, b.Striped})
}
// ---------------------------------------------------------------------------
// Карта
// ---------------------------------------------------------------------------
type BlockMap struct {
Latitude float64
Longitude float64
Zoom int
Width int
Height int
Caption RichText
}
func (BlockMap) isRichBlock() {}
func (b BlockMap) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Type string `json:"type"`
Latitude float64 `json:"latitude"`
Longitude float64 `json:"longitude"`
Zoom int `json:"zoom"`
Width int `json:"width"`
Height int `json:"height"`
Caption RichText `json:"caption,omitempty"`
}{"map", b.Latitude, b.Longitude, b.Zoom, b.Width, b.Height, b.Caption})
}
// ---------------------------------------------------------------------------
// Медиа-блоки (file_id + caption)
// ---------------------------------------------------------------------------
type BlockPhoto struct {
FileID string
Caption RichText
URL string
}
func (BlockPhoto) isRichBlock() {}
func (b BlockPhoto) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Type string `json:"type"`
FileID string `json:"file_id"`
Caption RichText `json:"caption,omitempty"`
URL string `json:"url,omitempty"`
}{"photo", b.FileID, b.Caption, b.URL})
}
type BlockVideo struct {
FileID string
Caption RichText
Autoplay bool
Loop bool
}
func (BlockVideo) isRichBlock() {}
func (b BlockVideo) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Type string `json:"type"`
FileID string `json:"file_id"`
Caption RichText `json:"caption,omitempty"`
Autoplay bool `json:"autoplay"`
Loop bool `json:"loop"`
}{"video", b.FileID, b.Caption, b.Autoplay, b.Loop})
}
type BlockAudio struct {
FileID string
Caption RichText
}
func (BlockAudio) isRichBlock() {}
func (b BlockAudio) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Type string `json:"type"`
FileID string `json:"file_id"`
Caption RichText `json:"caption,omitempty"`
}{"audio", b.FileID, b.Caption})
}
type BlockAnimation struct {
FileID string
Caption RichText
}
func (BlockAnimation) isRichBlock() {}
func (b BlockAnimation) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Type string `json:"type"`
FileID string `json:"file_id"`
Caption RichText `json:"caption,omitempty"`
}{"animation", b.FileID, b.Caption})
}
type BlockVoiceNote struct {
FileID string
Caption RichText
}
func (BlockVoiceNote) isRichBlock() {}
func (b BlockVoiceNote) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Type string `json:"type"`
FileID string `json:"file_id"`
Caption RichText `json:"caption,omitempty"`
}{"voice_note", b.FileID, b.Caption})
}
// ---------------------------------------------------------------------------
// Листья без вложенного контента
// ---------------------------------------------------------------------------
type BlockDivider struct{}
func (BlockDivider) isRichBlock() {}
func (b BlockDivider) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Type string `json:"type"`
}{"divider"})
}
type BlockMathematicalExpression struct {
Expression string
}
func (BlockMathematicalExpression) isRichBlock() {}
func (b BlockMathematicalExpression) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Type string `json:"type"`
Expression string `json:"expression"`
}{"mathematical_expression", b.Expression})
}
type BlockAnchor struct {
Name string
}
func (BlockAnchor) isRichBlock() {}
func (b BlockAnchor) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Type string `json:"type"`
Name string `json:"name"`
}{"anchor", b.Name})
}
// ---------------------------------------------------------------------------
// Разбор JSON -> RichBlock
// ---------------------------------------------------------------------------
func UnmarshalBlock(data []byte) (RichBlock, error) {
var head struct {
Type string `json:"type"`
Text json.RawMessage `json:"text"`
Caption json.RawMessage `json:"caption"`
Title json.RawMessage `json:"title"`
}
if err := json.Unmarshal(data, &head); err != nil {
return nil, fmt.Errorf("richblock: %w", err)
}
parseText := func(raw json.RawMessage) (RichText, error) {
if len(raw) == 0 || string(raw) == "null" {
return nil, nil
}
return Unmarshal(raw)
}
if blockWrapTags[head.Type] {
text, err := parseText(head.Text)
if err != nil {
return nil, fmt.Errorf("richblock %q: text: %w", head.Type, err)
}
return BlockWrap{Tag: head.Type, Text: text}, nil
}
switch head.Type {
case "preformatted":
var v struct {
Language string `json:"language"`
}
_ = json.Unmarshal(data, &v)
text, _ := parseText(head.Text)
return BlockPreformatted{text, v.Language}, nil
case "block_quotation":
text, _ := parseText(head.Text)
caption, _ := parseText(head.Caption)
return BlockBlockQuotation{text, caption}, nil
case "pull_quotation":
text, _ := parseText(head.Text)
caption, _ := parseText(head.Caption)
return BlockPullQuotation{text, caption}, nil
case "list":
var v struct {
Items []RichBlockListItem `json:"items"`
Ordered bool `json:"ordered"`
}
if err := json.Unmarshal(data, &v); err != nil {
return nil, err
}
return BlockList{v.Items, v.Ordered}, nil
case "collage":
var raw struct {
Items json.RawMessage `json:"items"`
}
_ = json.Unmarshal(data, &raw)
items, _ := unmarshalBlocks(raw.Items)
caption, _ := parseText(head.Caption)
return BlockCollage{items, caption}, nil
case "slideshow":
var raw struct {
Items json.RawMessage `json:"items"`
}
_ = json.Unmarshal(data, &raw)
items, _ := unmarshalBlocks(raw.Items)
caption, _ := parseText(head.Caption)
return BlockSlideshow{items, caption}, nil
case "details":
var raw struct {
Blocks json.RawMessage `json:"blocks"`
Open bool `json:"open"`
}
_ = json.Unmarshal(data, &raw)
title, _ := parseText(head.Title)
blocks, _ := unmarshalBlocks(raw.Blocks)
return BlockDetails{title, blocks, raw.Open}, nil
case "table":
var raw struct {
Title json.RawMessage `json:"title"`
Rows [][]RichBlockTableCell `json:"rows"`
Bordered bool `json:"bordered"`
Striped bool `json:"striped"`
}
if err := json.Unmarshal(data, &raw); err != nil {
return nil, err
}
title, _ := parseText(raw.Title)
return BlockTable{title, raw.Rows, raw.Bordered, raw.Striped}, nil
case "map":
var v struct {
Latitude float64 `json:"latitude"`
Longitude float64 `json:"longitude"`
Zoom int `json:"zoom"`
Width int `json:"width"`
Height int `json:"height"`
Caption json.RawMessage `json:"caption"`
}
_ = json.Unmarshal(data, &v)
caption, _ := parseText(v.Caption)
return BlockMap{v.Latitude, v.Longitude, v.Zoom, v.Width, v.Height, caption}, nil
case "photo":
var v struct {
FileID string `json:"file_id"`
Caption json.RawMessage `json:"caption"`
URL string `json:"url"`
}
_ = json.Unmarshal(data, &v)
caption, _ := parseText(v.Caption)
return BlockPhoto{v.FileID, caption, v.URL}, nil
case "video":
var v struct {
FileID string `json:"file_id"`
Caption json.RawMessage `json:"caption"`
Autoplay bool `json:"autoplay"`
Loop bool `json:"loop"`
}
_ = json.Unmarshal(data, &v)
caption, _ := parseText(v.Caption)
return BlockVideo{v.FileID, caption, v.Autoplay, v.Loop}, nil
case "audio":
var v struct {
FileID string `json:"file_id"`
Caption json.RawMessage `json:"caption"`
}
_ = json.Unmarshal(data, &v)
caption, _ := parseText(v.Caption)
return BlockAudio{v.FileID, caption}, nil
case "animation":
var v struct {
FileID string `json:"file_id"`
Caption json.RawMessage `json:"caption"`
}
_ = json.Unmarshal(data, &v)
caption, _ := parseText(v.Caption)
return BlockAnimation{v.FileID, caption}, nil
case "voice_note":
var v struct {
FileID string `json:"file_id"`
Caption json.RawMessage `json:"caption"`
}
_ = json.Unmarshal(data, &v)
caption, _ := parseText(v.Caption)
return BlockVoiceNote{v.FileID, caption}, nil
case "divider":
return BlockDivider{}, nil
case "mathematical_expression":
var v struct {
Expression string `json:"expression"`
}
_ = json.Unmarshal(data, &v)
return BlockMathematicalExpression{v.Expression}, nil
case "anchor":
var v struct {
Name string `json:"name"`
}
_ = json.Unmarshal(data, &v)
return BlockAnchor{v.Name}, nil
default:
// forward-compat: неизвестный тип с text → BlockWrap, без text → ошибка.
if text, err := parseText(head.Text); err == nil && text != nil {
return BlockWrap{Tag: head.Type, Text: text}, nil
}
return nil, fmt.Errorf("richblock: unknown type %q", head.Type)
}
}
// UnmarshalMessage разбирает корневой RichMessage из JSON.
func UnmarshalMessage(data []byte) (RichMessage, error) {
var raw struct {
Blocks json.RawMessage `json:"blocks"`
}
if err := json.Unmarshal(data, &raw); err != nil {
return RichMessage{}, fmt.Errorf("richmessage: %w", err)
}
blocks, err := unmarshalBlocks(raw.Blocks)
if err != nil {
return RichMessage{}, err
}
return RichMessage{blocks}, nil
}
// ---------------------------------------------------------------------------
// Внутренние хелперы
// ---------------------------------------------------------------------------
func unmarshalBlocks(raw json.RawMessage) ([]RichBlock, error) {
if len(raw) == 0 || string(raw) == "null" {
return nil, nil
}
var raws []json.RawMessage
if err := json.Unmarshal(raw, &raws); err != nil {
return nil, err
}
blocks := make([]RichBlock, len(raws))
for i, r := range raws {
b, err := UnmarshalBlock(r)
if err != nil {
return nil, err
}
blocks[i] = b
}
return blocks, nil
}
func unmarshalListItem(data []byte) (RichBlockListItem, error) {
var raw struct {
Blocks json.RawMessage `json:"blocks"`
}
if err := json.Unmarshal(data, &raw); err != nil {
return RichBlockListItem{}, err
}
blocks, err := unmarshalBlocks(raw.Blocks)
if err != nil {
return RichBlockListItem{}, err
}
return RichBlockListItem{blocks}, nil
}
func unmarshalTableCell(data []byte) (RichBlockTableCell, error) {
var raw struct {
Content json.RawMessage `json:"content"`
ColumnSpan int `json:"column_span"`
RowSpan int `json:"row_span"`
}
if err := json.Unmarshal(data, &raw); err != nil {
return RichBlockTableCell{}, err
}
content, err := unmarshalBlocks(raw.Content)
if err != nil {
return RichBlockTableCell{}, err
}
return RichBlockTableCell{content, raw.ColumnSpan, raw.RowSpan}, nil
}