Skip to content

Commit ee8708c

Browse files
committed
feat: implement bot api 10.1 rich messages
1 parent 9c28310 commit ee8708c

6 files changed

Lines changed: 657 additions & 0 deletions

File tree

‎bot.go‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -592,6 +592,13 @@ func (b *Bot) Edit(msg Editable, what interface{}, opts ...interface{}) (*Messag
592592
case string:
593593
method = "editMessageText"
594594
params["text"] = v
595+
case *InputRichMessage:
596+
method = "editMessageText"
597+
data, err := json.Marshal(v)
598+
if err != nil {
599+
return nil, err
600+
}
601+
params["rich_message"] = string(data)
595602
case Location:
596603
method = "editMessageLiveLocation"
597604
params["latitude"] = fmt.Sprintf("%f", v.Lat)
@@ -625,6 +632,12 @@ func (b *Bot) Edit(msg Editable, what interface{}, opts ...interface{}) (*Messag
625632
sendOpts := b.extractOptions(opts)
626633
b.embedSendOptions(params, sendOpts)
627634

635+
// Rich content carries its own markup; text parse options never apply.
636+
if _, ok := params["rich_message"]; ok {
637+
delete(params, "parse_mode")
638+
delete(params, "entities")
639+
}
640+
628641
data, err := b.Raw(method, params)
629642
if err != nil {
630643
return nil, err

‎message.go‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,9 @@ type Message struct {
194194
// Unique identifier for answering the guest query.
195195
GuestQueryID string `json:"guest_query_id"`
196196

197+
// (Optional) For a rich message, the rich formatted content (Bot API 10.1).
198+
RichMessage *RichMessage `json:"rich_message,omitempty"`
199+
197200
// (Optional) The bot that actually sent the message on behalf of the business account.
198201
// Available only for outgoing messages sent on behalf of the connected business account.
199202
BusinessBot *User `json:"sender_business_bot"`

‎rich.go‎

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
package telebot
2+
3+
import (
4+
"encoding/json"
5+
"strconv"
6+
)
7+
8+
// InputRichMessage describes a rich message to send with the Bot API 10.1
9+
// rich-message methods (sendRichMessage, sendRichMessageDraft, and the
10+
// rich_message parameter of editMessageText). Exactly one of HTML or Markdown
11+
// must be set; Telegram parses the source server-side into its rich block tree
12+
// — headings, tables, ordered and nested lists, blockquotes, dividers, media
13+
// blocks and so on — and renders it as a single message regardless of length.
14+
//
15+
// InputRichMessage implements Sendable, so the idiomatic way to send one is
16+
// through Send/Reply:
17+
//
18+
// b.Send(to, &telebot.InputRichMessage{Markdown: "# Title\n\nBody"})
19+
//
20+
// and to edit a message into rich content through Edit:
21+
//
22+
// b.Edit(msg, &telebot.InputRichMessage{HTML: "<b>done</b>"})
23+
type InputRichMessage struct {
24+
// (Optional) Content described using Telegram's extended HTML formatting.
25+
HTML string `json:"html,omitempty"`
26+
27+
// (Optional) Content described using Telegram's extended Markdown formatting.
28+
Markdown string `json:"markdown,omitempty"`
29+
30+
// (Optional) Pass true to render the message right-to-left.
31+
IsRTL bool `json:"is_rtl,omitempty"`
32+
33+
// (Optional) Pass true to skip automatic detection of URLs, email addresses,
34+
// mentions, hashtags, cashtags, bot commands and phone numbers in the text.
35+
SkipEntityDetection bool `json:"skip_entity_detection,omitempty"`
36+
}
37+
38+
// Send delivers the rich message through bot b to recipient via the
39+
// sendRichMessage method, satisfying the Sendable interface.
40+
func (i *InputRichMessage) Send(b *Bot, to Recipient, opt *SendOptions) (*Message, error) {
41+
data, err := json.Marshal(i)
42+
if err != nil {
43+
return nil, err
44+
}
45+
46+
params := map[string]string{
47+
"chat_id": to.Recipient(),
48+
"rich_message": string(data),
49+
}
50+
b.embedSendOptions(params, opt)
51+
52+
// Parse mode and entities apply to plain text, never to rich content; the
53+
// markup is carried inside the InputRichMessage itself.
54+
delete(params, "parse_mode")
55+
delete(params, "entities")
56+
57+
raw, err := b.Raw("sendRichMessage", params)
58+
if err != nil {
59+
return nil, err
60+
}
61+
62+
return extractMessage(raw)
63+
}
64+
65+
// SendRichDraft streams a partial rich message to a private chat while it is
66+
// being generated (the sendRichMessageDraft method). The streamed draft is
67+
// ephemeral and acts as a temporary 30-second preview; once the output is
68+
// finalized you must Send the complete InputRichMessage to persist it. draftID
69+
// must be non-zero — updates sharing the same identifier are animated
70+
// client-side. Of the send options only ThreadID is honored.
71+
func (b *Bot) SendRichDraft(to Recipient, draftID int, rich *InputRichMessage, opts ...interface{}) error {
72+
if to == nil {
73+
return ErrBadRecipient
74+
}
75+
if rich == nil {
76+
return ErrUnsupportedWhat
77+
}
78+
79+
data, err := json.Marshal(rich)
80+
if err != nil {
81+
return err
82+
}
83+
84+
params := map[string]string{
85+
"chat_id": to.Recipient(),
86+
"draft_id": strconv.Itoa(draftID),
87+
"rich_message": string(data),
88+
}
89+
90+
if sendOpts := b.extractOptions(opts); sendOpts != nil && sendOpts.ThreadID != 0 {
91+
params["message_thread_id"] = strconv.Itoa(sendOpts.ThreadID)
92+
}
93+
94+
_, err = b.Raw("sendRichMessageDraft", params)
95+
return err
96+
}
97+
98+
// InputRichMessageContent represents the content of a rich message to be sent
99+
// as the result of an inline, guest or Web App query (Bot API 10.1).
100+
type InputRichMessageContent struct {
101+
// The rich message to be sent.
102+
RichMessage *InputRichMessage `json:"rich_message"`
103+
}
104+
105+
// IsInputMessageContent marks InputRichMessageContent as InputMessageContent.
106+
func (i *InputRichMessageContent) IsInputMessageContent() bool {
107+
return true
108+
}
109+
110+
// RichMessage represents a received rich formatted message (Bot API 10.1),
111+
// available on Message.RichMessage. Its content is a tree of RichBlock values.
112+
type RichMessage struct {
113+
// Content of the message, as an ordered list of blocks.
114+
Blocks []RichBlock `json:"blocks"`
115+
116+
// (Optional) True if the message must be shown right-to-left.
117+
IsRTL bool `json:"is_rtl,omitempty"`
118+
}

‎rich_block.go‎

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
package telebot
2+
3+
import "encoding/json"
4+
5+
// RichBlock type discriminators, as carried in the "type" field of a received
6+
// rich block (Bot API 10.1).
7+
const (
8+
RichBlockParagraph = "paragraph"
9+
RichBlockHeading = "heading"
10+
RichBlockPre = "pre"
11+
RichBlockFooter = "footer"
12+
RichBlockDivider = "divider"
13+
RichBlockMath = "mathematical_expression"
14+
RichBlockAnchor = "anchor"
15+
RichBlockList = "list"
16+
RichBlockBlockquote = "blockquote"
17+
RichBlockPullquote = "pullquote"
18+
RichBlockCollage = "collage"
19+
RichBlockSlideshow = "slideshow"
20+
RichBlockTable = "table"
21+
RichBlockDetails = "details"
22+
RichBlockMap = "map"
23+
RichBlockAnimationType = "animation"
24+
RichBlockAudioType = "audio"
25+
RichBlockPhotoType = "photo"
26+
RichBlockVideoType = "video"
27+
RichBlockVoiceNote = "voice_note"
28+
RichBlockThinking = "thinking"
29+
)
30+
31+
// RichBlock is a single block of a received RichMessage (Bot API 10.1). It is a
32+
// polymorphic type discriminated by Type; only the fields relevant to a given
33+
// Type are populated. RichBlock is a received-only type.
34+
type RichBlock struct {
35+
// Type is the block discriminator (see the RichBlock* constants).
36+
Type string `json:"type"`
37+
38+
// Text is the block's formatted text (paragraph, heading, pre, footer,
39+
// pullquote, thinking).
40+
Text *RichText `json:"text,omitempty"`
41+
42+
// Size is the heading level, 1 (largest) to 6 (smallest).
43+
Size int `json:"size,omitempty"`
44+
45+
// Language is the optional source language of a "pre" block.
46+
Language string `json:"language,omitempty"`
47+
48+
// Expression is the LaTeX of a "mathematical_expression" block.
49+
Expression string `json:"expression,omitempty"`
50+
51+
// Name is the identifier of an "anchor" block.
52+
Name string `json:"name,omitempty"`
53+
54+
// Items are the entries of a "list" block.
55+
Items []RichBlockListItem `json:"items,omitempty"`
56+
57+
// Blocks are the nested blocks of a blockquote, collage, slideshow or
58+
// details block.
59+
Blocks []RichBlock `json:"blocks,omitempty"`
60+
61+
// Summary is the disclosure summary of a "details" block.
62+
Summary *RichText `json:"summary,omitempty"`
63+
64+
// IsOpen reports whether a "details" block is expanded by default.
65+
IsOpen bool `json:"is_open,omitempty"`
66+
67+
// Credit is the attribution of a blockquote or pullquote block.
68+
Credit *RichText `json:"credit,omitempty"`
69+
70+
// Cells are the rows-by-columns of a "table" block.
71+
Cells [][]RichBlockTableCell `json:"cells,omitempty"`
72+
73+
// IsBordered and IsStriped style a "table" block.
74+
IsBordered bool `json:"is_bordered,omitempty"`
75+
IsStriped bool `json:"is_striped,omitempty"`
76+
77+
// Location, Zoom, Width and Height describe a "map" block.
78+
Location *Location `json:"location,omitempty"`
79+
Zoom int `json:"zoom,omitempty"`
80+
Width int `json:"width,omitempty"`
81+
Height int `json:"height,omitempty"`
82+
83+
// Media of the corresponding block type.
84+
Animation *Animation `json:"animation,omitempty"`
85+
Audio *Audio `json:"audio,omitempty"`
86+
Photo []Photo `json:"photo,omitempty"`
87+
Video *Video `json:"video,omitempty"`
88+
VoiceNote *Voice `json:"voice_note,omitempty"`
89+
90+
// HasSpoiler marks an animation, photo or video block as a spoiler.
91+
HasSpoiler bool `json:"has_spoiler,omitempty"`
92+
93+
// Caption is the caption of a collage, slideshow, map, animation, audio,
94+
// photo, video or voice-note block.
95+
Caption *RichBlockCaption `json:"-"`
96+
97+
// TableCaption is the caption of a "table" block (a bare RichText rather
98+
// than a RichBlockCaption).
99+
TableCaption *RichText `json:"-"`
100+
}
101+
102+
// UnmarshalJSON decodes a RichBlock, resolving the polymorphic "caption" field:
103+
// a table carries a bare RichText caption, every other block a RichBlockCaption.
104+
func (rb *RichBlock) UnmarshalJSON(data []byte) error {
105+
type alias RichBlock
106+
aux := struct {
107+
*alias
108+
Caption json.RawMessage `json:"caption,omitempty"`
109+
}{alias: (*alias)(rb)}
110+
111+
if err := json.Unmarshal(data, &aux); err != nil {
112+
return err
113+
}
114+
115+
if len(aux.Caption) == 0 || string(aux.Caption) == "null" {
116+
return nil
117+
}
118+
119+
if rb.Type == RichBlockTable {
120+
rb.TableCaption = new(RichText)
121+
return json.Unmarshal(aux.Caption, rb.TableCaption)
122+
}
123+
124+
rb.Caption = new(RichBlockCaption)
125+
return json.Unmarshal(aux.Caption, rb.Caption)
126+
}
127+
128+
// RichBlockCaption is the caption of a media or container rich block.
129+
type RichBlockCaption struct {
130+
// Text of the caption.
131+
Text RichText `json:"text"`
132+
133+
// (Optional) Attribution shown alongside the caption.
134+
Credit *RichText `json:"credit,omitempty"`
135+
}
136+
137+
// RichBlockTableCell is a single cell of a "table" rich block. An omitted Text
138+
// denotes an invisible cell spanned into by a neighbour.
139+
type RichBlockTableCell struct {
140+
Text *RichText `json:"text,omitempty"`
141+
IsHeader bool `json:"is_header,omitempty"`
142+
Colspan int `json:"colspan,omitempty"`
143+
Rowspan int `json:"rowspan,omitempty"`
144+
Align string `json:"align,omitempty"` // "left", "center", "right"
145+
VAlign string `json:"valign,omitempty"` // "top", "middle", "bottom"
146+
}
147+
148+
// RichBlockListItem is a single entry of a "list" rich block.
149+
type RichBlockListItem struct {
150+
// Label is the rendered bullet or number of the item.
151+
Label string `json:"label,omitempty"`
152+
153+
// Blocks is the content of the item.
154+
Blocks []RichBlock `json:"blocks,omitempty"`
155+
156+
// HasCheckbox and IsChecked render a task-list item.
157+
HasCheckbox bool `json:"has_checkbox,omitempty"`
158+
IsChecked bool `json:"is_checked,omitempty"`
159+
160+
// Value is the ordinal of an ordered-list item.
161+
Value int `json:"value,omitempty"`
162+
163+
// Type is the label style of an ordered item ("a", "A", "i", "I", "1").
164+
Type string `json:"type,omitempty"`
165+
}

0 commit comments

Comments
 (0)
⌂ 换网址