(new): Telegram API 10.1
Golang lint / lint (push) Successful in 2m5s

(new): rich text
This commit is contained in:
2026-07-01 09:55:02 +03:00
parent 38309e74f6
commit b563e695df
4 changed files with 1362 additions and 0 deletions
+648
View File
@@ -0,0 +1,648 @@
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
}