REPOSITORY / ScuroNeko/Laniakea

Wiki

KNOWLEDGE REPOSITORY
2
Rich Messages
ScuroNeko edited this page 2026-08-19 14:59:10 +03:00

Rich Messages

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.

Packages and data flow

  • 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.

Outgoing and incoming block types are intentionally separate. InputRichBlock* describes data accepted by Telegram; RichBlock* describes the normalized tree returned by Telegram.

Sending from a handler

MessageContext.RichAnswer accepts input blocks and builds a validated HTML rich message:

import (
	"git.scuroneko.dev/scuroneko/laniakea/tgrich"
)

func report(ctx *laniakea.MessageContext, _ struct{}) error {
	ctx.RichAnswer(
		tgrich.H1(tgrich.Text("Daily report")),
		tgrich.P(tgrich.Concat(
			tgrich.Text("Status: "),
			tgrich.Bold(tgrich.Text("all systems go")),
		)),
		tgrich.Ul(
			tgrich.NewListItem(tgrich.P(tgrich.Text("backups"))).
				SetCheckbox().SetChecked().Build(),
			tgrich.NewListItem(tgrich.P(tgrich.Text("migration"))).
				SetCheckbox().Build(),
		),
	)
	return nil
}

RichAnswerKeyboard(keyboard, blocks...) sends the same content with an inline keyboard.

Building text and blocks

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.

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.

List items and tables use small builders:

ordered := tgrich.Ol(
	tgrich.OlOpts{Start: 3, Type: tgapi.InputRichBlockListItemTypeLower},
	tgrich.NewListItem(tgrich.P(tgrich.Text("third"))).Build(),
)

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 payloads and HTML conversion

Send the input tree directly when no conversion is needed:

rich := tgapi.InputRichMessage{
	Blocks: []tgapi.InputRichBlock{
		tgrich.H1(tgrich.Text("Report")),
		tgrich.P(tgrich.Text("Ready")),
	},
}

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.

BuildHTML enables SkipEntityDetection, escapes text and attributes, and enforces Telegram's rich-message limits:

  • 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:

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:

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

Message.RichMessage is parsed automatically. Walk the received tree with type switches:

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 {
			// Label is the server-rendered marker: "1.", "c.", "vii.", or "•".
		}
	}
}

Unknown future nodes with a text field are preserved as RichTextWrap or RichBlockWrap, allowing parsers to tolerate compatible Bot API additions.

Pitfalls

  • InputRichBlock* and received RichBlock* are different type families.
  • Thinking can be sent only through sendRichMessageDraft.
  • A checked list item must also have a checkbox.
  • 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