diff --git a/bot.go b/bot.go
index 33af3074..6642ceab 100644
--- a/bot.go
+++ b/bot.go
@@ -331,6 +331,31 @@ func (bot *BotAPI) SendRichMessageDraft(config SendRichMessageDraftConfig) (bool
return bot.requestBool(config)
}
+// EditEphemeralMessageText edits an ephemeral text message.
+func (bot *BotAPI) EditEphemeralMessageText(config EditEphemeralMessageTextConfig) (bool, error) {
+ return bot.requestBool(config)
+}
+
+// EditEphemeralMessageMedia edits the media of an ephemeral message.
+func (bot *BotAPI) EditEphemeralMessageMedia(config EditEphemeralMessageMediaConfig) (bool, error) {
+ return bot.requestBool(config)
+}
+
+// EditEphemeralMessageCaption edits the caption of an ephemeral message.
+func (bot *BotAPI) EditEphemeralMessageCaption(config EditEphemeralMessageCaptionConfig) (bool, error) {
+ return bot.requestBool(config)
+}
+
+// EditEphemeralMessageReplyMarkup edits the reply markup of an ephemeral message.
+func (bot *BotAPI) EditEphemeralMessageReplyMarkup(config EditEphemeralMessageReplyMarkupConfig) (bool, error) {
+ return bot.requestBool(config)
+}
+
+// DeleteEphemeralMessage deletes an ephemeral message.
+func (bot *BotAPI) DeleteEphemeralMessage(config DeleteEphemeralMessageConfig) (bool, error) {
+ return bot.requestBool(config)
+}
+
// SendMediaGroup sends a media group and returns the resulting messages.
func (bot *BotAPI) SendMediaGroup(config MediaGroupConfig) ([]Message, error) {
resp, err := bot.Request(config)
diff --git a/bot_api_10_2_test.go b/bot_api_10_2_test.go
new file mode 100644
index 00000000..16e8a89e
--- /dev/null
+++ b/bot_api_10_2_test.go
@@ -0,0 +1,443 @@
+package tgbotapi
+
+import (
+ "encoding/json"
+ "slices"
+ "strings"
+ "testing"
+)
+
+func TestBotAPI102InputRichBlockJSONContract(t *testing.T) {
+ animation := NewInputMediaAnimation(FileID("animation"))
+ audio := NewInputMediaAudio(FileID("audio"))
+ photo := NewInputMediaPhoto(FileID("photo"))
+ video := NewInputMediaVideo(FileID("video"))
+ voiceNote := NewInputMediaVoiceNote(FileID("voice"))
+
+ tests := []struct {
+ name string
+ block InputRichBlock
+ typeName string
+ fieldMatch string
+ }{
+ {name: "paragraph", block: InputRichBlockParagraph{Type: "paragraph", Text: "text"}, typeName: "paragraph", fieldMatch: `"text":"text"`},
+ {name: "section heading", block: InputRichBlockSectionHeading{Type: "heading", Text: "heading", Size: 2}, typeName: "heading", fieldMatch: `"size":2`},
+ {name: "preformatted", block: InputRichBlockPreformatted{Type: "pre", Text: "code", Language: "go"}, typeName: "pre", fieldMatch: `"language":"go"`},
+ {name: "footer", block: InputRichBlockFooter{Type: "footer", Text: "footer"}, typeName: "footer", fieldMatch: `"text":"footer"`},
+ {name: "divider", block: InputRichBlockDivider{Type: "divider"}, typeName: "divider"},
+ {name: "mathematical expression", block: InputRichBlockMathematicalExpression{Type: "mathematical_expression", Expression: "x^2"}, typeName: "mathematical_expression", fieldMatch: `"expression":"x^2"`},
+ {name: "anchor", block: InputRichBlockAnchor{Type: "anchor", Name: "intro"}, typeName: "anchor", fieldMatch: `"name":"intro"`},
+ {name: "list", block: InputRichBlockList{Type: "list", Items: []InputRichBlockListItem{{Blocks: []InputRichBlock{InputRichBlockParagraph{Type: "paragraph", Text: "item"}}, HasCheckbox: true}}}, typeName: "list", fieldMatch: `"has_checkbox":true`},
+ {name: "block quotation", block: InputRichBlockBlockQuotation{Type: "blockquote", Blocks: []InputRichBlock{InputRichBlockParagraph{Type: "paragraph", Text: "quote"}}, Credit: "author"}, typeName: "blockquote", fieldMatch: `"credit":"author"`},
+ {name: "pull quotation", block: InputRichBlockPullQuotation{Type: "pullquote", Text: "quote", Credit: "author"}, typeName: "pullquote", fieldMatch: `"credit":"author"`},
+ {name: "collage", block: InputRichBlockCollage{Type: "collage", Blocks: []InputRichBlock{InputRichBlockPhoto{Type: "photo", Photo: photo}}}, typeName: "collage", fieldMatch: `"blocks":[`},
+ {name: "slideshow", block: InputRichBlockSlideshow{Type: "slideshow", Blocks: []InputRichBlock{InputRichBlockPhoto{Type: "photo", Photo: photo}}}, typeName: "slideshow", fieldMatch: `"blocks":[`},
+ {name: "table", block: InputRichBlockTable{Type: "table", Cells: [][]RichBlockTableCell{{{Text: "cell", Align: "left", Valign: "middle"}}}, IsBordered: true}, typeName: "table", fieldMatch: `"is_bordered":true`},
+ {name: "details", block: InputRichBlockDetails{Type: "details", Summary: "summary", Blocks: []InputRichBlock{InputRichBlockParagraph{Type: "paragraph", Text: "body"}}, IsOpen: true}, typeName: "details", fieldMatch: `"is_open":true`},
+ {name: "map", block: InputRichBlockMap{Type: "map", Location: Location{Latitude: 10.5, Longitude: 20.25}, Zoom: 12, Width: 640, Height: 480}, typeName: "map", fieldMatch: `"zoom":12`},
+ {name: "animation", block: InputRichBlockAnimation{Type: "animation", Animation: animation}, typeName: "animation", fieldMatch: `"animation":{"type":"animation","media":"animation"}`},
+ {name: "audio", block: InputRichBlockAudio{Type: "audio", Audio: audio}, typeName: "audio", fieldMatch: `"audio":{"type":"audio","media":"audio"}`},
+ {name: "photo", block: InputRichBlockPhoto{Type: "photo", Photo: photo}, typeName: "photo", fieldMatch: `"photo":{"type":"photo","media":"photo"}`},
+ {name: "video", block: InputRichBlockVideo{Type: "video", Video: video}, typeName: "video", fieldMatch: `"video":{"type":"video","media":"video"}`},
+ {name: "voice note", block: InputRichBlockVoiceNote{Type: "voice_note", VoiceNote: voiceNote}, typeName: "voice_note", fieldMatch: `"voice_note":{"type":"voice_note","media":"voice"}`},
+ {name: "thinking", block: InputRichBlockThinking{Type: "thinking", Text: "thinking"}, typeName: "thinking", fieldMatch: `"text":"thinking"`},
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ data, err := json.Marshal(test.block)
+ if err != nil {
+ t.Fatalf("marshal block: %v", err)
+ }
+
+ payload := string(data)
+ if !strings.Contains(payload, `"type":"`+test.typeName+`"`) {
+ t.Fatalf("missing type discriminator in %s", payload)
+ }
+ if test.fieldMatch != "" && !strings.Contains(payload, test.fieldMatch) {
+ t.Fatalf("missing %s in %s", test.fieldMatch, payload)
+ }
+ })
+ }
+}
+
+func TestBotAPI102InputRichMessageForms(t *testing.T) {
+ photo := NewInputMediaPhoto(FileID("photo-id"))
+ tests := []struct {
+ name string
+ message InputRichMessage
+ match string
+ }{
+ {name: "html", message: NewInputRichMessageHTML("Hello"), match: `"html":"\u003cb\u003eHello\u003c/b\u003e"`},
+ {name: "markdown", message: NewInputRichMessageMarkdown("**Hello**"), match: `"markdown":"**Hello**"`},
+ {name: "blocks", message: NewInputRichMessageBlocks(InputRichBlockParagraph{Type: "paragraph", Text: "Hello"}), match: `"blocks":[{"type":"paragraph","text":"Hello"}]`},
+ {name: "media", message: InputRichMessage{HTML: `
`, Media: []InputRichMessageMedia{{ID: "hero", Media: &photo}}}, match: `"media":[{"id":"hero","media":{"type":"photo","media":"photo-id"}}]`},
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ data, err := json.Marshal(test.message)
+ if err != nil {
+ t.Fatalf("marshal rich message: %v", err)
+ }
+ if payload := string(data); !strings.Contains(payload, test.match) {
+ t.Fatalf("missing %s in %s", test.match, payload)
+ }
+ })
+ }
+}
+
+func TestBotAPI102IncomingTypes(t *testing.T) {
+ var update Update
+ if err := json.Unmarshal([]byte(`{"update_id":1,"subscription":{"user":{"id":9,"is_bot":false,"first_name":"Ada"},"invoice_payload":"plan","state":"active"}}`), &update); err != nil {
+ t.Fatalf("unmarshal subscription update: %v", err)
+ }
+ if update.Subscription == nil || update.Subscription.InvoicePayload != "plan" || update.Subscription.State != "active" {
+ t.Fatalf("subscription mismatch: %#v", update.Subscription)
+ }
+ if from := update.SentFrom(); from == nil || from.ID != 9 {
+ t.Fatalf("subscription sender mismatch: %#v", from)
+ }
+
+ var message Message
+ if err := json.Unmarshal([]byte(`{"message_id":0,"receiver_user":{"id":10,"is_bot":false,"first_name":"Grace"},"ephemeral_message_id":42,"community_chat_added":{"community":{"id":1000000000000,"name":"Go"}},"community_chat_removed":{}}`), &message); err != nil {
+ t.Fatalf("unmarshal ephemeral message: %v", err)
+ }
+ if message.MessageID != 0 || message.EphemeralMessageID != 42 || message.ReceiverUser == nil || message.ReceiverUser.ID != 10 {
+ t.Fatalf("ephemeral message mismatch: %#v", message)
+ }
+ if message.CommunityChatAdded == nil || message.CommunityChatAdded.Community.ID != 1000000000000 || message.CommunityChatRemoved == nil {
+ t.Fatalf("community service fields mismatch: %#v", message)
+ }
+
+ var chat ChatFullInfo
+ if err := json.Unmarshal([]byte(`{"id":-1001,"type":"supergroup","community":{"id":2000000000000,"name":"Backend"}}`), &chat); err != nil {
+ t.Fatalf("unmarshal chat community: %v", err)
+ }
+ if chat.Community == nil || chat.Community.ID != 2000000000000 || chat.Community.Name != "Backend" {
+ t.Fatalf("chat community mismatch: %#v", chat.Community)
+ }
+
+ data, err := json.Marshal(BotCommand{Command: "quick", Description: "Quick reply", IsEphemeral: true})
+ if err != nil {
+ t.Fatalf("marshal ephemeral command: %v", err)
+ }
+ if !strings.Contains(string(data), `"is_ephemeral":true`) {
+ t.Fatalf("missing ephemeral command flag: %s", data)
+ }
+}
+
+func TestBotAPI102ReplyParametersIdentifiers(t *testing.T) {
+ tests := []struct {
+ name string
+ parameters ReplyParameters
+ match string
+ notMatch string
+ }{
+ {name: "regular message", parameters: ReplyParameters{MessageID: 123}, match: `"message_id":123`, notMatch: `"ephemeral_message_id"`},
+ {name: "ephemeral message", parameters: ReplyParameters{EphemeralMessageID: 456}, match: `"ephemeral_message_id":456`, notMatch: `"message_id"`},
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ data, err := json.Marshal(test.parameters)
+ if err != nil {
+ t.Fatalf("marshal reply parameters: %v", err)
+ }
+ payload := string(data)
+ if !strings.Contains(payload, test.match) || strings.Contains(payload, test.notMatch) {
+ t.Fatalf("identifier contract mismatch: %s", payload)
+ }
+ })
+ }
+}
+
+func TestBotAPI102EphemeralSendParams(t *testing.T) {
+ message := NewMessage(1, "text")
+ message.ReceiverUserID, message.CallbackQueryID = 42, "callback"
+ animation := NewAnimation(1, FileID("animation"))
+ animation.ReceiverUserID, animation.CallbackQueryID = 42, "callback"
+ audio := NewAudio(1, FileID("audio"))
+ audio.ReceiverUserID, audio.CallbackQueryID = 42, "callback"
+ document := NewDocument(1, FileID("document"))
+ document.ReceiverUserID, document.CallbackQueryID = 42, "callback"
+ photo := NewPhoto(1, FileID("photo"))
+ photo.ReceiverUserID, photo.CallbackQueryID = 42, "callback"
+ sticker := NewSticker(1, FileID("sticker"))
+ sticker.ReceiverUserID, sticker.CallbackQueryID = 42, "callback"
+ video := NewVideo(1, FileID("video"))
+ video.ReceiverUserID, video.CallbackQueryID = 42, "callback"
+ videoNote := NewVideoNote(1, 10, FileID("video-note"))
+ videoNote.ReceiverUserID, videoNote.CallbackQueryID = 42, "callback"
+ voice := NewVoice(1, FileID("voice"))
+ voice.ReceiverUserID, voice.CallbackQueryID = 42, "callback"
+ contact := NewContact(1, "+12025550123", "Ada")
+ contact.ReceiverUserID, contact.CallbackQueryID = 42, "callback"
+ location := NewLocation(1, 10.5, 20.25)
+ location.ReceiverUserID, location.CallbackQueryID = 42, "callback"
+ venue := NewVenue(1, "Office", "Main Street", 10.5, 20.25)
+ venue.ReceiverUserID, venue.CallbackQueryID = 42, "callback"
+
+ configs := []struct {
+ method string
+ config Chattable
+ }{
+ {method: "sendMessage", config: message},
+ {method: "sendAnimation", config: animation},
+ {method: "sendAudio", config: audio},
+ {method: "sendDocument", config: document},
+ {method: "sendPhoto", config: photo},
+ {method: "sendSticker", config: sticker},
+ {method: "sendVideo", config: video},
+ {method: "sendVideoNote", config: videoNote},
+ {method: "sendVoice", config: voice},
+ {method: "sendContact", config: contact},
+ {method: "sendLocation", config: location},
+ {method: "sendVenue", config: venue},
+ }
+ for _, test := range configs {
+ t.Run(test.method, func(t *testing.T) {
+ if method := test.config.method(); method != test.method {
+ t.Fatalf("method mismatch: got %q, want %q", method, test.method)
+ }
+ params, err := test.config.params()
+ if err != nil {
+ t.Fatalf("params: %v", err)
+ }
+ if params["receiver_user_id"] != "42" || params["callback_query_id"] != "callback" {
+ t.Fatalf("ephemeral send params mismatch: %#v", params)
+ }
+ })
+ }
+
+ chatAction := NewChatAction(1, ChatTyping)
+ params, err := chatAction.params()
+ if err != nil {
+ t.Fatalf("chat action params: %v", err)
+ }
+ if _, ok := params["receiver_user_id"]; ok {
+ t.Fatalf("receiver_user_id leaked into unsupported method: %#v", params)
+ }
+ if _, ok := params["callback_query_id"]; ok {
+ t.Fatalf("callback_query_id leaked into unsupported method: %#v", params)
+ }
+}
+
+func TestBotAPI102EphemeralLifecycleParams(t *testing.T) {
+ markup := NewInlineKeyboardMarkup(NewInlineKeyboardRow(NewInlineKeyboardButtonData("Done", "done")))
+ text := NewEditEphemeralMessageText(1, 2, 3, "updated")
+ text.ParseMode = ModeMarkdownV2
+ mediaValue := NewInputMediaPhoto(FileBytes{Name: "photo.jpg", Bytes: []byte("photo")})
+ media := NewEditEphemeralMessageMedia(1, 2, 3, &mediaValue)
+ caption := NewEditEphemeralMessageCaption(1, 2, 3, "")
+ replyMarkup := NewEditEphemeralMessageReplyMarkup(1, 2, 3, markup)
+ deleteMessage := NewDeleteEphemeralMessage(1, 2, 3)
+
+ tests := []struct {
+ config Chattable
+ method string
+ key string
+ value string
+ }{
+ {config: text, method: "editEphemeralMessageText", key: "text", value: "updated"},
+ {config: media, method: "editEphemeralMessageMedia", key: "media", value: `{"type":"photo","media":{"Name":"photo.jpg","Bytes":"cGhvdG8="}}`},
+ {config: caption, method: "editEphemeralMessageCaption", key: "caption", value: ""},
+ {config: replyMarkup, method: "editEphemeralMessageReplyMarkup", key: "reply_markup", value: `{"inline_keyboard":[[{"text":"Done","callback_data":"done"}]]}`},
+ {config: deleteMessage, method: "deleteEphemeralMessage"},
+ }
+
+ for _, test := range tests {
+ t.Run(test.method, func(t *testing.T) {
+ if method := test.config.method(); method != test.method {
+ t.Fatalf("method mismatch: got %q, want %q", method, test.method)
+ }
+ params, err := test.config.params()
+ if err != nil {
+ t.Fatalf("params: %v", err)
+ }
+ if params["chat_id"] != "1" || params["receiver_user_id"] != "2" || params["ephemeral_message_id"] != "3" {
+ t.Fatalf("ephemeral identifiers mismatch: %#v", params)
+ }
+ if test.key != "" {
+ value, ok := params[test.key]
+ if !ok || value != test.value {
+ t.Fatalf("%s mismatch: %#v", test.key, params)
+ }
+ }
+ })
+ }
+
+ if _, ok := any(media).(Fileable); ok {
+ t.Fatal("ephemeral media edit must not support direct file uploads")
+ }
+}
+
+func TestBotAPI102RichMessageMultipartPreparation(t *testing.T) {
+ topPhoto := NewInputMediaPhoto(FileBytes{Name: "top.jpg", Bytes: []byte("top")})
+ topRichMessage := InputRichMessage{
+ HTML: `
`,
+ Media: []InputRichMessageMedia{{ID: "hero", Media: &topPhoto}},
+ }
+ topConfig := NewSendRichMessage(1, topRichMessage)
+ assertRichMessageUpload(t, topConfig, []string{"rich-message-media-0"})
+ if _, ok := topPhoto.Media.(FileBytes); !ok {
+ t.Fatalf("top-level user media was mutated: %#v", topPhoto.Media)
+ }
+
+ video := NewInputMediaVideo(FileBytes{Name: "video.mp4", Bytes: []byte("video")})
+ video.Thumb = FileBytes{Name: "thumb.jpg", Bytes: []byte("thumb")}
+ voiceNote := NewInputMediaVoiceNote(FileBytes{Name: "voice.ogg", Bytes: []byte("voice")})
+ photo := NewInputMediaPhoto(FileBytes{Name: "photo.jpg", Bytes: []byte("photo")})
+ videoBlock := &InputRichBlockVideo{Type: "video", Video: video}
+ voiceNoteBlock := &InputRichBlockVoiceNote{Type: "voice_note", VoiceNote: voiceNote}
+ photoBlock := &InputRichBlockPhoto{Type: "photo", Photo: photo}
+ details := &InputRichBlockDetails{
+ Type: "details",
+ Summary: "Media",
+ Blocks: []InputRichBlock{
+ videoBlock,
+ },
+ }
+ list := &InputRichBlockList{
+ Type: "list",
+ Items: []InputRichBlockListItem{{
+ Blocks: []InputRichBlock{details},
+ }},
+ }
+ collage := &InputRichBlockCollage{
+ Type: "collage",
+ Blocks: []InputRichBlock{
+ list,
+ voiceNoteBlock,
+ &InputRichBlockBlockQuotation{
+ Type: "blockquote",
+ Blocks: []InputRichBlock{
+ &InputRichBlockSlideshow{
+ Type: "slideshow",
+ Blocks: []InputRichBlock{
+ photoBlock,
+ },
+ },
+ },
+ },
+ },
+ }
+ blocksConfig := NewSendRichMessage(1, NewInputRichMessageBlocks(collage))
+ expectedFiles := []string{
+ "rich-message-block-0-block-0-item-0-block-0-block-0",
+ "rich-message-block-0-block-0-item-0-block-0-block-0-thumb",
+ "rich-message-block-0-block-1",
+ "rich-message-block-0-block-2-block-0-block-0",
+ }
+ assertRichMessageUpload(t, blocksConfig, expectedFiles)
+ if _, ok := videoBlock.Video.Media.(FileBytes); !ok {
+ t.Fatalf("nested user media was mutated: %#v", videoBlock.Video.Media)
+ }
+ if _, ok := videoBlock.Video.Thumb.(FileBytes); !ok {
+ t.Fatalf("nested user thumbnail was mutated: %#v", videoBlock.Video.Thumb)
+ }
+ if _, ok := voiceNoteBlock.VoiceNote.Media.(FileBytes); !ok {
+ t.Fatalf("nested user voice note was mutated: %#v", voiceNoteBlock.VoiceNote.Media)
+ }
+ if _, ok := photoBlock.Photo.Media.(FileBytes); !ok {
+ t.Fatalf("nested user photo was mutated: %#v", photoBlock.Photo.Media)
+ }
+
+ edit := NewEditMessageText(1, 2, "")
+ edit.RichMessage = topRichMessage
+ assertRichMessageUpload(t, edit, []string{"rich-message-media-0"})
+ if _, ok := topPhoto.Media.(FileBytes); !ok {
+ t.Fatalf("regular edit mutated user media: %#v", topPhoto.Media)
+ }
+
+ inlinePhoto := NewInputMediaPhoto(FileBytes{Name: "inline.jpg", Bytes: []byte("inline")})
+ inline := EditMessageTextConfig{
+ BaseEdit: BaseEdit{InlineMessageID: "inline"},
+ RichMessage: InputRichMessage{
+ HTML: `
`,
+ Media: []InputRichMessageMedia{{ID: "hero", Media: &inlinePhoto}},
+ },
+ }
+ params, err := inline.params()
+ if err != nil {
+ t.Fatalf("inline edit params: %v", err)
+ }
+ if files := inline.files(); len(files) != 0 {
+ t.Fatalf("inline edit unexpectedly supports uploads: %+v", files)
+ }
+ if strings.Contains(params["rich_message"], "attach://") {
+ t.Fatalf("inline rich message was rewritten: %s", params["rich_message"])
+ }
+ if _, ok := inlinePhoto.Media.(FileBytes); !ok {
+ t.Fatalf("inline user media was mutated: %#v", inlinePhoto.Media)
+ }
+
+ draft := NewSendRichMessageDraft(1, 2, topRichMessage)
+ if _, ok := any(draft).(Fileable); ok {
+ t.Fatal("rich message drafts must not support direct file uploads")
+ }
+}
+
+func TestBotAPI102RichMediaBlockUploads(t *testing.T) {
+ tests := []struct {
+ name string
+ block InputRichBlock
+ }{
+ {name: "animation", block: InputRichBlockAnimation{Type: "animation", Animation: NewInputMediaAnimation(FileBytes{Name: "animation.mp4", Bytes: []byte("animation")})}},
+ {name: "audio", block: InputRichBlockAudio{Type: "audio", Audio: NewInputMediaAudio(FileBytes{Name: "audio.mp3", Bytes: []byte("audio")})}},
+ {name: "photo", block: InputRichBlockPhoto{Type: "photo", Photo: NewInputMediaPhoto(FileBytes{Name: "photo.jpg", Bytes: []byte("photo")})}},
+ {name: "video", block: InputRichBlockVideo{Type: "video", Video: NewInputMediaVideo(FileBytes{Name: "video.mp4", Bytes: []byte("video")})}},
+ {name: "voice note", block: InputRichBlockVoiceNote{Type: "voice_note", VoiceNote: NewInputMediaVoiceNote(FileBytes{Name: "voice.ogg", Bytes: []byte("voice")})}},
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ config := NewSendRichMessage(1, NewInputRichMessageBlocks(test.block))
+ assertRichMessageUpload(t, config, []string{"rich-message-block-0"})
+
+ data, err := json.Marshal(test.block)
+ if err != nil {
+ t.Fatalf("marshal original block: %v", err)
+ }
+ if strings.Contains(string(data), "attach://") {
+ t.Fatalf("user block was mutated: %s", data)
+ }
+ })
+ }
+}
+
+func assertRichMessageUpload(t *testing.T, config Fileable, expectedFiles []string) {
+ t.Helper()
+
+ params, err := config.params()
+ if err != nil {
+ t.Fatalf("rich message params: %v", err)
+ }
+ for _, name := range expectedFiles {
+ if !strings.Contains(params["rich_message"], `"attach://`+name+`"`) {
+ t.Fatalf("missing attachment %q in %s", name, params["rich_message"])
+ }
+ }
+
+ files := config.files()
+ names := make([]string, len(files))
+ for idx, file := range files {
+ names[idx] = file.Name
+ }
+ for _, name := range expectedFiles {
+ if !slices.Contains(names, name) {
+ t.Fatalf("missing file %q in %v", name, names)
+ }
+ }
+ if len(files) != len(expectedFiles) {
+ t.Fatalf("unexpected files: %v", names)
+ }
+}
+
+var (
+ _ func(*BotAPI, EditEphemeralMessageTextConfig) (bool, error) = (*BotAPI).EditEphemeralMessageText
+ _ func(*BotAPI, EditEphemeralMessageMediaConfig) (bool, error) = (*BotAPI).EditEphemeralMessageMedia
+ _ func(*BotAPI, EditEphemeralMessageCaptionConfig) (bool, error) = (*BotAPI).EditEphemeralMessageCaption
+ _ func(*BotAPI, EditEphemeralMessageReplyMarkupConfig) (bool, error) = (*BotAPI).EditEphemeralMessageReplyMarkup
+ _ func(*BotAPI, DeleteEphemeralMessageConfig) (bool, error) = (*BotAPI).DeleteEphemeralMessage
+)
diff --git a/configs.go b/configs.go
index dae83b2f..99dc0b31 100644
--- a/configs.go
+++ b/configs.go
@@ -133,6 +133,9 @@ const (
// UpdateTypeManagedBot is emitted when a managed bot is created or its token changes.
UpdateTypeManagedBot = "managed_bot"
+
+ // UpdateTypeSubscription is emitted when a user payment subscription changes.
+ UpdateTypeSubscription = "subscription"
)
// Library errors
@@ -310,6 +313,8 @@ type MessageConfig struct {
ParseMode string
Entities []MessageEntity
LinkPreviewOptions LinkPreviewOptions
+ ReceiverUserID int64
+ CallbackQueryID string
}
func (config MessageConfig) params() (Params, error) {
@@ -320,6 +325,8 @@ func (config MessageConfig) params() (Params, error) {
params["text"] = config.Text
params.AddNonEmpty("parse_mode", config.ParseMode)
+ params.AddNonZero64("receiver_user_id", config.ReceiverUserID)
+ params.AddNonEmpty("callback_query_id", config.CallbackQueryID)
err = params.AddInterface("entities", config.Entities)
if err != nil {
return params, err
@@ -404,11 +411,16 @@ func (config SendRichMessageConfig) params() (Params, error) {
return params, err
}
- err = params.AddInterface("rich_message", config.RichMessage)
+ preparedRichMessage := prepareInputRichMessageForParams(config.RichMessage)
+ err = params.AddInterface("rich_message", preparedRichMessage)
return params, err
}
+func (config SendRichMessageConfig) files() []RequestFile {
+ return prepareInputRichMessageForFiles(config.RichMessage)
+}
+
// SendRichMessageDraftConfig allows you to stream a partial rich message.
type SendRichMessageDraftConfig struct {
ChatConfig
@@ -566,6 +578,8 @@ type PhotoConfig struct {
ParseMode string
CaptionEntities []MessageEntity
ShowCaptionAboveMedia bool
+ ReceiverUserID int64
+ CallbackQueryID string
}
func (config PhotoConfig) params() (Params, error) {
@@ -577,6 +591,8 @@ func (config PhotoConfig) params() (Params, error) {
params.AddNonEmpty("caption", config.Caption)
params.AddNonEmpty("parse_mode", config.ParseMode)
params.AddBool("show_caption_above_media", config.ShowCaptionAboveMedia)
+ params.AddNonZero64("receiver_user_id", config.ReceiverUserID)
+ params.AddNonEmpty("callback_query_id", config.CallbackQueryID)
err = params.AddInterface("caption_entities", config.CaptionEntities)
if err != nil {
return params, err
@@ -657,6 +673,8 @@ type AudioConfig struct {
Duration int
Performer string
Title string
+ ReceiverUserID int64
+ CallbackQueryID string
}
func (config AudioConfig) params() (Params, error) {
@@ -670,6 +688,8 @@ func (config AudioConfig) params() (Params, error) {
params.AddNonEmpty("title", config.Title)
params.AddNonEmpty("caption", config.Caption)
params.AddNonEmpty("parse_mode", config.ParseMode)
+ params.AddNonZero64("receiver_user_id", config.ReceiverUserID)
+ params.AddNonEmpty("callback_query_id", config.CallbackQueryID)
err = params.AddInterface("caption_entities", config.CaptionEntities)
return params, err
@@ -694,6 +714,8 @@ type DocumentConfig struct {
ParseMode string
CaptionEntities []MessageEntity
DisableContentTypeDetection bool
+ ReceiverUserID int64
+ CallbackQueryID string
}
func (config DocumentConfig) params() (Params, error) {
@@ -705,6 +727,8 @@ func (config DocumentConfig) params() (Params, error) {
params.AddNonEmpty("caption", config.Caption)
params.AddNonEmpty("parse_mode", config.ParseMode)
params.AddBool("disable_content_type_detection", config.DisableContentTypeDetection)
+ params.AddNonZero64("receiver_user_id", config.ReceiverUserID)
+ params.AddNonEmpty("callback_query_id", config.CallbackQueryID)
err = params.AddInterface("caption_entities", config.CaptionEntities)
if err != nil {
return params, err
@@ -729,6 +753,8 @@ type StickerConfig struct {
// Emoji associated with the sticker; only for just uploaded stickers
Emoji string
BaseFile
+ ReceiverUserID int64
+ CallbackQueryID string
}
func (config StickerConfig) params() (Params, error) {
@@ -737,6 +763,8 @@ func (config StickerConfig) params() (Params, error) {
return params, err
}
params.AddNonEmpty("emoji", config.Emoji)
+ params.AddNonZero64("receiver_user_id", config.ReceiverUserID)
+ params.AddNonEmpty("callback_query_id", config.CallbackQueryID)
return params, err
}
@@ -763,6 +791,8 @@ type VideoConfig struct {
CaptionEntities []MessageEntity
ShowCaptionAboveMedia bool
SupportsStreaming bool
+ ReceiverUserID int64
+ CallbackQueryID string
}
func (config VideoConfig) params() (Params, error) {
@@ -779,6 +809,8 @@ func (config VideoConfig) params() (Params, error) {
params.AddNonEmpty("parse_mode", config.ParseMode)
params.AddBool("supports_streaming", config.SupportsStreaming)
params.AddBool("show_caption_above_media", config.ShowCaptionAboveMedia)
+ params.AddNonZero64("receiver_user_id", config.ReceiverUserID)
+ params.AddNonEmpty("callback_query_id", config.CallbackQueryID)
err = params.AddInterface("caption_entities", config.CaptionEntities)
if err != nil {
return params, err
@@ -817,6 +849,8 @@ type AnimationConfig struct {
ParseMode string
CaptionEntities []MessageEntity
ShowCaptionAboveMedia bool
+ ReceiverUserID int64
+ CallbackQueryID string
}
func (config AnimationConfig) params() (Params, error) {
@@ -831,6 +865,8 @@ func (config AnimationConfig) params() (Params, error) {
params.AddNonEmpty("caption", config.Caption)
params.AddNonEmpty("parse_mode", config.ParseMode)
params.AddBool("show_caption_above_media", config.ShowCaptionAboveMedia)
+ params.AddNonZero64("receiver_user_id", config.ReceiverUserID)
+ params.AddNonEmpty("callback_query_id", config.CallbackQueryID)
err = params.AddInterface("caption_entities", config.CaptionEntities)
if err != nil {
return params, err
@@ -859,9 +895,11 @@ func (config AnimationConfig) files() []RequestFile {
// VideoNoteConfig contains information about a SendVideoNote request.
type VideoNoteConfig struct {
BaseFile
- Thumb RequestFileData
- Duration int
- Length int
+ Thumb RequestFileData
+ Duration int
+ Length int
+ ReceiverUserID int64
+ CallbackQueryID string
}
func (config VideoNoteConfig) params() (Params, error) {
@@ -869,6 +907,8 @@ func (config VideoNoteConfig) params() (Params, error) {
params.AddNonZero("duration", config.Duration)
params.AddNonZero("length", config.Length)
+ params.AddNonZero64("receiver_user_id", config.ReceiverUserID)
+ params.AddNonEmpty("callback_query_id", config.CallbackQueryID)
return params, err
}
@@ -954,6 +994,8 @@ type VoiceConfig struct {
ParseMode string
CaptionEntities []MessageEntity
Duration int
+ ReceiverUserID int64
+ CallbackQueryID string
}
func (config VoiceConfig) params() (Params, error) {
@@ -965,6 +1007,8 @@ func (config VoiceConfig) params() (Params, error) {
params.AddNonZero("duration", config.Duration)
params.AddNonEmpty("caption", config.Caption)
params.AddNonEmpty("parse_mode", config.ParseMode)
+ params.AddNonZero64("receiver_user_id", config.ReceiverUserID)
+ params.AddNonEmpty("callback_query_id", config.CallbackQueryID)
err = params.AddInterface("caption_entities", config.CaptionEntities)
return params, err
@@ -990,6 +1034,8 @@ type LocationConfig struct {
LivePeriod int // optional
Heading int // optional
ProximityAlertRadius int // optional
+ ReceiverUserID int64
+ CallbackQueryID string
}
func (config LocationConfig) params() (Params, error) {
@@ -1001,6 +1047,8 @@ func (config LocationConfig) params() (Params, error) {
params.AddNonZero("live_period", config.LivePeriod)
params.AddNonZero("heading", config.Heading)
params.AddNonZero("proximity_alert_radius", config.ProximityAlertRadius)
+ params.AddNonZero64("receiver_user_id", config.ReceiverUserID)
+ params.AddNonEmpty("callback_query_id", config.CallbackQueryID)
return params, err
}
@@ -1061,6 +1109,8 @@ type VenueConfig struct {
FoursquareType string
GooglePlaceID string
GooglePlaceType string
+ ReceiverUserID int64
+ CallbackQueryID string
}
func (config VenueConfig) params() (Params, error) {
@@ -1074,6 +1124,8 @@ func (config VenueConfig) params() (Params, error) {
params.AddNonEmpty("foursquare_type", config.FoursquareType)
params.AddNonEmpty("google_place_id", config.GooglePlaceID)
params.AddNonEmpty("google_place_type", config.GooglePlaceType)
+ params.AddNonZero64("receiver_user_id", config.ReceiverUserID)
+ params.AddNonEmpty("callback_query_id", config.CallbackQueryID)
return params, err
}
@@ -1085,10 +1137,12 @@ func (config VenueConfig) method() string {
// ContactConfig allows you to send a contact.
type ContactConfig struct {
BaseChat
- PhoneNumber string
- FirstName string
- LastName string
- VCard string
+ PhoneNumber string
+ FirstName string
+ LastName string
+ VCard string
+ ReceiverUserID int64
+ CallbackQueryID string
}
func (config ContactConfig) params() (Params, error) {
@@ -1099,6 +1153,8 @@ func (config ContactConfig) params() (Params, error) {
params.AddNonEmpty("last_name", config.LastName)
params.AddNonEmpty("vcard", config.VCard)
+ params.AddNonZero64("receiver_user_id", config.ReceiverUserID)
+ params.AddNonEmpty("callback_query_id", config.CallbackQueryID)
return params, err
}
@@ -1345,11 +1401,12 @@ func (config EditMessageTextConfig) params() (Params, error) {
if err != nil {
return params, err
}
- if config.RichMessage != (InputRichMessage{}) {
- err = params.AddInterface("rich_message", config.RichMessage)
- if err != nil {
- return params, err
- }
+ richMessage := config.RichMessage
+ if config.InlineMessageID == "" {
+ richMessage = prepareInputRichMessageForParams(richMessage)
+ }
+ if err = params.AddInterfaceNonZero("rich_message", richMessage); err != nil {
+ return params, err
}
err = params.AddInterfaceNonZero("link_preview_options", config.LinkPreviewOptions)
@@ -1360,6 +1417,14 @@ func (config EditMessageTextConfig) method() string {
return "editMessageText"
}
+func (config EditMessageTextConfig) files() []RequestFile {
+ if config.InlineMessageID != "" {
+ return nil
+ }
+
+ return prepareInputRichMessageForFiles(config.RichMessage)
+}
+
// EditMessageCaptionConfig allows you to modify the caption of a message.
type EditMessageCaptionConfig struct {
BaseEdit
@@ -1429,6 +1494,127 @@ func (config EditMessageReplyMarkupConfig) method() string {
return "editMessageReplyMarkup"
}
+// EditEphemeralMessageTextConfig edits an ephemeral text message.
+type EditEphemeralMessageTextConfig struct {
+ BaseEphemeralMessage
+ Text string
+ ParseMode string
+ Entities []MessageEntity
+ LinkPreviewOptions LinkPreviewOptions
+ ReplyMarkup *InlineKeyboardMarkup
+}
+
+func (config EditEphemeralMessageTextConfig) params() (Params, error) {
+ params, err := config.BaseEphemeralMessage.params()
+ if err != nil {
+ return params, err
+ }
+
+ params["text"] = config.Text
+ params.AddNonEmpty("parse_mode", config.ParseMode)
+ if err = params.AddInterface("entities", config.Entities); err != nil {
+ return params, err
+ }
+ if err = params.AddInterfaceNonZero("link_preview_options", config.LinkPreviewOptions); err != nil {
+ return params, err
+ }
+ err = params.AddInterface("reply_markup", config.ReplyMarkup)
+
+ return params, err
+}
+
+func (EditEphemeralMessageTextConfig) method() string {
+ return "editEphemeralMessageText"
+}
+
+// EditEphemeralMessageMediaConfig edits the media of an ephemeral message.
+type EditEphemeralMessageMediaConfig struct {
+ BaseEphemeralMessage
+ Media InputMedia
+ ReplyMarkup *InlineKeyboardMarkup
+}
+
+func (config EditEphemeralMessageMediaConfig) params() (Params, error) {
+ params, err := config.BaseEphemeralMessage.params()
+ if err != nil {
+ return params, err
+ }
+
+ if err = params.AddInterface("media", config.Media); err != nil {
+ return params, err
+ }
+ err = params.AddInterface("reply_markup", config.ReplyMarkup)
+
+ return params, err
+}
+
+func (EditEphemeralMessageMediaConfig) method() string {
+ return "editEphemeralMessageMedia"
+}
+
+// EditEphemeralMessageCaptionConfig edits the caption of an ephemeral message.
+type EditEphemeralMessageCaptionConfig struct {
+ BaseEphemeralMessage
+ Caption string
+ ParseMode string
+ CaptionEntities []MessageEntity
+ ReplyMarkup *InlineKeyboardMarkup
+}
+
+func (config EditEphemeralMessageCaptionConfig) params() (Params, error) {
+ params, err := config.BaseEphemeralMessage.params()
+ if err != nil {
+ return params, err
+ }
+
+ params["caption"] = config.Caption
+ params.AddNonEmpty("parse_mode", config.ParseMode)
+ if err = params.AddInterface("caption_entities", config.CaptionEntities); err != nil {
+ return params, err
+ }
+ err = params.AddInterface("reply_markup", config.ReplyMarkup)
+
+ return params, err
+}
+
+func (EditEphemeralMessageCaptionConfig) method() string {
+ return "editEphemeralMessageCaption"
+}
+
+// EditEphemeralMessageReplyMarkupConfig edits the reply markup of an ephemeral message.
+type EditEphemeralMessageReplyMarkupConfig struct {
+ BaseEphemeralMessage
+ ReplyMarkup *InlineKeyboardMarkup
+}
+
+func (config EditEphemeralMessageReplyMarkupConfig) params() (Params, error) {
+ params, err := config.BaseEphemeralMessage.params()
+ if err != nil {
+ return params, err
+ }
+
+ err = params.AddInterface("reply_markup", config.ReplyMarkup)
+
+ return params, err
+}
+
+func (EditEphemeralMessageReplyMarkupConfig) method() string {
+ return "editEphemeralMessageReplyMarkup"
+}
+
+// DeleteEphemeralMessageConfig deletes an ephemeral message.
+type DeleteEphemeralMessageConfig struct {
+ BaseEphemeralMessage
+}
+
+func (config DeleteEphemeralMessageConfig) params() (Params, error) {
+ return config.BaseEphemeralMessage.params()
+}
+
+func (DeleteEphemeralMessageConfig) method() string {
+ return "deleteEphemeralMessage"
+}
+
// EditMessageChecklistConfig allows you to edit checklist of a message.
type EditMessageChecklistConfig struct {
BaseChatMessage
@@ -4656,6 +4842,16 @@ func prepareInputStoryContentForFiles(content InputStoryContent) []RequestFile {
return plan.Files()
}
+func prepareInputRichMessageForParams(message InputRichMessage) InputRichMessage {
+ prepared, _ := prepareInputRichMessageUploadPlan(message)
+ return prepared
+}
+
+func prepareInputRichMessageForFiles(message InputRichMessage) []RequestFile {
+ _, plan := prepareInputRichMessageUploadPlan(message)
+ return plan.Files()
+}
+
func prepareInputStickersForParams(stickers []InputSticker) []InputSticker {
prepared := make([]InputSticker, len(stickers))
for idx := range stickers {
@@ -4745,6 +4941,8 @@ func cloneInputMedia(media InputMedia) InputMedia {
return ptr(*m)
case *InputMediaDocument:
return ptr(*m)
+ case *InputMediaVoiceNote:
+ return ptr(*m)
case *InputMediaLivePhoto:
return ptr(*m)
case *InputMediaLocation:
diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md
index 6268753a..da52f863 100644
--- a/docs/SUMMARY.md
+++ b/docs/SUMMARY.md
@@ -8,7 +8,7 @@
- [Command Handling](./examples/command-handling.md)
- [Keyboard](./examples/keyboard.md)
- [Inline Keyboard](./examples/inline-keyboard.md)
- - [Bot API 10.0](./examples/bot-api-10.md)
+ - [Bot API 10.x](./examples/bot-api-10.md)
- [Change Log](./changelog.md)
# Contributing
diff --git a/docs/examples/bot-api-10.md b/docs/examples/bot-api-10.md
index 5191b592..dec99830 100644
--- a/docs/examples/bot-api-10.md
+++ b/docs/examples/bot-api-10.md
@@ -1,4 +1,113 @@
-# Bot API 10.0
+# Bot API 10.x
+
+## Rich Messages
+
+Bot API 10.2 supports structured outgoing blocks and media uploads in rich messages.
+
+```go
+photo := tgbotapi.NewInputMediaPhoto(tgbotapi.FilePath("diagram.jpg"))
+richMessage := tgbotapi.NewInputRichMessageBlocks(
+ tgbotapi.InputRichBlockSectionHeading{
+ Type: "heading",
+ Text: "Release summary",
+ Size: 2,
+ },
+ tgbotapi.InputRichBlockParagraph{
+ Type: "paragraph",
+ Text: "The migration completed successfully.",
+ },
+ tgbotapi.InputRichBlockPhoto{
+ Type: "photo",
+ Photo: photo,
+ },
+)
+
+_, err := bot.SendRichMessage(tgbotapi.NewSendRichMessage(chatID, richMessage))
+```
+
+`SendRichMessageConfig` and regular, non-inline `EditMessageTextConfig` recursively upload media from top-level `media` entries and nested collage, slideshow, list, quotation, details, and media blocks. The library replaces uploaded values with stable `attach://...` references without changing the caller's values.
+
+For HTML or Markdown, associate an explicit media identifier with the corresponding `tg://photo?id=`, `tg://video?id=`, or `tg://audio?id=` link:
+
+```go
+photo := tgbotapi.NewInputMediaPhoto(tgbotapi.FilePath("diagram.jpg"))
+richMessage := tgbotapi.NewInputRichMessageHTML(`
`)
+richMessage.Media = []tgbotapi.InputRichMessageMedia{
+ {ID: "diagram", Media: &photo},
+}
+
+_, err := bot.SendRichMessage(tgbotapi.NewSendRichMessage(chatID, richMessage))
+```
+
+Exactly one of `HTML`, `Markdown`, or `Blocks` must be used. Direct upload of new files is not available for `SendRichMessageDraftConfig`, inline `EditMessageTextConfig`, or `EditEphemeralMessageMediaConfig`; use a Telegram `file_id` or an HTTP URL there.
+
+## Ephemeral Commands and Messages
+
+Register an ephemeral command by setting `BotCommand.IsEphemeral`:
+
+```go
+_, err := bot.Request(tgbotapi.NewSetMyCommands(tgbotapi.BotCommand{
+ Command: "private_status",
+ Description: "Show status privately",
+ IsEphemeral: true,
+}))
+```
+
+An incoming ephemeral command has `Message.MessageID == 0` and a separate `Message.EphemeralMessageID`. Use that identifier in `ReplyParameters` and in the ephemeral edit/delete methods:
+
+```go
+incoming := update.Message
+reply := tgbotapi.NewMessage(incoming.Chat.ID, "Checking...")
+reply.ReceiverUserID = incoming.From.ID
+reply.ReplyParameters.EphemeralMessageID = incoming.EphemeralMessageID
+
+sent, err := bot.Send(reply)
+if err != nil {
+ log.Println(err)
+} else {
+ _, err = bot.EditEphemeralMessageText(tgbotapi.NewEditEphemeralMessageText(
+ incoming.Chat.ID,
+ incoming.From.ID,
+ sent.EphemeralMessageID,
+ "Ready",
+ ))
+}
+```
+
+For a callback-query-triggered response, set `CallbackQueryID` on the selected send config instead of replying to an ephemeral message. Non-administrator bots must respond within 15 seconds of the eligible action. Telegram does not guarantee delivery, especially when the receiver is offline.
+
+The edit and delete methods require all three identifiers: chat, receiver user, and ephemeral message. `EditEphemeralMessageCaption` accepts an empty caption to remove it. New uploads are not supported by `EditEphemeralMessageMedia`.
+
+## Subscription and Community Updates
+
+Request `UpdateTypeSubscription` to receive payment subscription changes. Community membership changes arrive as service messages:
+
+```go
+updateConfig := tgbotapi.NewUpdate(0)
+updateConfig.AllowedUpdates = []string{
+ tgbotapi.UpdateTypeMessage,
+ tgbotapi.UpdateTypeSubscription,
+}
+
+for update := range bot.GetUpdatesChan(updateConfig) {
+ if update.Subscription != nil {
+ log.Printf("subscription %s for user %d", update.Subscription.State, update.Subscription.User.ID)
+ }
+ if update.Message != nil && update.Message.CommunityChatAdded != nil {
+ community := update.Message.CommunityChatAdded.Community
+ log.Printf("joined community %s (%d)", community.Name, community.ID)
+ }
+ if update.Message != nil && update.Message.CommunityChatRemoved != nil {
+ log.Print("removed from community")
+ }
+}
+```
+
+`Update.SentFrom()` returns the subscriber for subscription updates. `ChatFullInfo.Community` identifies the community associated with a chat when Telegram returns it.
+
+## Mini App Origin Protection
+
+Telegram enables origin protection for all Mini Apps on July 20, 2026. Mini App methods are then accepted only from the original Mini App domain. This is configured through BotFather and requires no library code; opting out makes the bot responsible for avoiding links to untrusted sites.
## Guest Bots
diff --git a/examples/bot_api_10.go b/examples/bot_api_10.go
index a975a5bd..a89f8b25 100644
--- a/examples/bot_api_10.go
+++ b/examples/bot_api_10.go
@@ -84,3 +84,75 @@ func remove_message_reactions() {
log.Println(err)
}
}
+
+func send_structured_rich_message(bot *api.BotAPI, chatID int64) {
+ photo := api.NewInputMediaPhoto(api.FilePath("diagram.jpg"))
+ richMessage := api.NewInputRichMessageBlocks(
+ api.InputRichBlockSectionHeading{
+ Type: "heading",
+ Text: "Release summary",
+ Size: 2,
+ },
+ api.InputRichBlockParagraph{
+ Type: "paragraph",
+ Text: "The migration completed successfully.",
+ },
+ api.InputRichBlockPhoto{
+ Type: "photo",
+ Photo: photo,
+ },
+ )
+
+ if _, err := bot.SendRichMessage(api.NewSendRichMessage(chatID, richMessage)); err != nil {
+ log.Println(err)
+ }
+}
+
+func configure_ephemeral_command(bot *api.BotAPI) {
+ command := api.BotCommand{
+ Command: "private_status",
+ Description: "Show status privately",
+ IsEphemeral: true,
+ }
+ if _, err := bot.Request(api.NewSetMyCommands(command)); err != nil {
+ log.Println(err)
+ }
+}
+
+func handle_bot_api_10_2_update(bot *api.BotAPI, update api.Update) {
+ if update.Subscription != nil {
+ log.Printf("subscription %s for user %d", update.Subscription.State, update.Subscription.User.ID)
+ }
+ if update.Message == nil {
+ return
+ }
+ if update.Message.CommunityChatAdded != nil {
+ community := update.Message.CommunityChatAdded.Community
+ log.Printf("joined community %s (%d)", community.Name, community.ID)
+ }
+ if update.Message.CommunityChatRemoved != nil {
+ log.Print("removed from community")
+ }
+ if update.Message.EphemeralMessageID == 0 || update.Message.From == nil {
+ return
+ }
+
+ reply := api.NewMessage(update.Message.Chat.ID, "Checking...")
+ reply.ReceiverUserID = update.Message.From.ID
+ reply.ReplyParameters.EphemeralMessageID = update.Message.EphemeralMessageID
+ sent, err := bot.Send(reply)
+ if err != nil {
+ log.Println(err)
+ return
+ }
+
+ edit := api.NewEditEphemeralMessageText(
+ update.Message.Chat.ID,
+ update.Message.From.ID,
+ sent.EphemeralMessageID,
+ "Ready",
+ )
+ if _, err := bot.EditEphemeralMessageText(edit); err != nil {
+ log.Println(err)
+ }
+}
diff --git a/helper_methods.go b/helper_methods.go
index 05191295..218e3695 100644
--- a/helper_methods.go
+++ b/helper_methods.go
@@ -42,6 +42,13 @@ func NewInputRichMessageMarkdown(markdown string) InputRichMessage {
}
}
+// NewInputRichMessageBlocks creates a rich message input from block entities.
+func NewInputRichMessageBlocks(blocks ...InputRichBlock) InputRichMessage {
+ return InputRichMessage{
+ Blocks: blocks,
+ }
+}
+
// NewInputRichMessageContent creates new rich message content for inline query results.
func NewInputRichMessageContent(richMessage InputRichMessage) InputRichMessageContent {
return InputRichMessageContent{
@@ -374,6 +381,16 @@ func NewInputMediaDocument(media RequestFileData) InputMediaDocument {
}
}
+// NewInputMediaVoiceNote creates a new InputMediaVoiceNote.
+func NewInputMediaVoiceNote(media RequestFileData) InputMediaVoiceNote {
+ return InputMediaVoiceNote{
+ BaseInputMedia: BaseInputMedia{
+ Type: "voice_note",
+ Media: media,
+ },
+ }
+}
+
// NewInputMediaLivePhoto creates a new InputMediaLivePhoto.
func NewInputMediaLivePhoto(livePhoto, photo RequestFileData) InputMediaLivePhoto {
return InputMediaLivePhoto{
@@ -894,6 +911,55 @@ func NewEditMessageReplyMarkup(chatID int64, messageID int, replyMarkup InlineKe
}
}
+func newBaseEphemeralMessage(chatID, receiverUserID int64, ephemeralMessageID int) BaseEphemeralMessage {
+ return BaseEphemeralMessage{
+ ChatConfig: ChatConfig{
+ ChatID: chatID,
+ },
+ ReceiverUserID: receiverUserID,
+ EphemeralMessageID: ephemeralMessageID,
+ }
+}
+
+// NewEditEphemeralMessageText creates a request to edit ephemeral message text.
+func NewEditEphemeralMessageText(chatID, receiverUserID int64, ephemeralMessageID int, text string) EditEphemeralMessageTextConfig {
+ return EditEphemeralMessageTextConfig{
+ BaseEphemeralMessage: newBaseEphemeralMessage(chatID, receiverUserID, ephemeralMessageID),
+ Text: text,
+ }
+}
+
+// NewEditEphemeralMessageMedia creates a request to edit ephemeral message media.
+func NewEditEphemeralMessageMedia(chatID, receiverUserID int64, ephemeralMessageID int, media InputMedia) EditEphemeralMessageMediaConfig {
+ return EditEphemeralMessageMediaConfig{
+ BaseEphemeralMessage: newBaseEphemeralMessage(chatID, receiverUserID, ephemeralMessageID),
+ Media: media,
+ }
+}
+
+// NewEditEphemeralMessageCaption creates a request to edit an ephemeral message caption.
+func NewEditEphemeralMessageCaption(chatID, receiverUserID int64, ephemeralMessageID int, caption string) EditEphemeralMessageCaptionConfig {
+ return EditEphemeralMessageCaptionConfig{
+ BaseEphemeralMessage: newBaseEphemeralMessage(chatID, receiverUserID, ephemeralMessageID),
+ Caption: caption,
+ }
+}
+
+// NewEditEphemeralMessageReplyMarkup creates a request to edit ephemeral message reply markup.
+func NewEditEphemeralMessageReplyMarkup(chatID, receiverUserID int64, ephemeralMessageID int, replyMarkup InlineKeyboardMarkup) EditEphemeralMessageReplyMarkupConfig {
+ return EditEphemeralMessageReplyMarkupConfig{
+ BaseEphemeralMessage: newBaseEphemeralMessage(chatID, receiverUserID, ephemeralMessageID),
+ ReplyMarkup: &replyMarkup,
+ }
+}
+
+// NewDeleteEphemeralMessage creates a request to delete an ephemeral message.
+func NewDeleteEphemeralMessage(chatID, receiverUserID int64, ephemeralMessageID int) DeleteEphemeralMessageConfig {
+ return DeleteEphemeralMessageConfig{
+ BaseEphemeralMessage: newBaseEphemeralMessage(chatID, receiverUserID, ephemeralMessageID),
+ }
+}
+
// NewRemoveKeyboard hides the keyboard, with the option for being selective
// or hiding for everyone.
func NewRemoveKeyboard(selective bool) ReplyKeyboardRemove {
diff --git a/helper_structs.go b/helper_structs.go
index 8c52635e..6e2128c8 100644
--- a/helper_structs.go
+++ b/helper_structs.go
@@ -96,6 +96,25 @@ func (edit BaseEdit) params() (Params, error) {
return params, err
}
+// BaseEphemeralMessage identifies an ephemeral message received by a user.
+type BaseEphemeralMessage struct {
+ ChatConfig
+ ReceiverUserID int64
+ EphemeralMessageID int
+}
+
+func (message BaseEphemeralMessage) params() (Params, error) {
+ params, err := message.ChatConfig.params()
+ if err != nil {
+ return params, err
+ }
+
+ params.AddNonZero64("receiver_user_id", message.ReceiverUserID)
+ params.AddNonZero("ephemeral_message_id", message.EphemeralMessageID)
+
+ return params, nil
+}
+
// BaseSpoiler is base type of structures with spoilers.
type BaseSpoiler struct {
HasSpoiler bool
diff --git a/parity_interfaces_test.go b/parity_interfaces_test.go
index 5550b8b7..4c4541e4 100644
--- a/parity_interfaces_test.go
+++ b/parity_interfaces_test.go
@@ -7,6 +7,11 @@ var (
_ Chattable = SendMessageDraftConfig{}
_ Chattable = SendRichMessageConfig{}
_ Chattable = SendRichMessageDraftConfig{}
+ _ Chattable = EditEphemeralMessageTextConfig{}
+ _ Chattable = EditEphemeralMessageMediaConfig{}
+ _ Chattable = EditEphemeralMessageCaptionConfig{}
+ _ Chattable = EditEphemeralMessageReplyMarkupConfig{}
+ _ Chattable = DeleteEphemeralMessageConfig{}
_ Chattable = ApproveSuggestedPostConfig{}
_ Chattable = DeclineSuggestedPostConfig{}
_ Chattable = UserProfileAudiosConfig{}
@@ -50,6 +55,8 @@ var (
var (
_ Fileable = SetMyProfilePhotoConfig{}
_ Fileable = SendLivePhotoConfig{}
+ _ Fileable = SendRichMessageConfig{}
+ _ Fileable = EditMessageTextConfig{}
_ Fileable = SendPollConfig{}
_ Fileable = SetBusinessAccountProfilePhotoConfig{}
_ Fileable = PostStoryConfig{}
@@ -65,4 +72,5 @@ var (
_ InlineQueryResultCachedMpeg4Gif
_ TransactionPartnerTelegramApi
_ InputMedia = (*InputMediaLink)(nil)
+ _ InputMedia = (*InputMediaVoiceNote)(nil)
)
diff --git a/types.go b/types.go
index b0f8868b..7cb064a1 100644
--- a/types.go
+++ b/types.go
@@ -163,6 +163,10 @@ type Update struct {
//
// optional
ManagedBot *ManagedBotUpdated `json:"managed_bot,omitempty"`
+ // Subscription is emitted when a user payment subscription changes.
+ //
+ // optional
+ Subscription *BotSubscriptionUpdated `json:"subscription,omitempty"`
}
// SentFrom returns the user who sent an update. Can be nil, if Telegram did not provide information
@@ -213,6 +217,8 @@ func (u *Update) SentFrom() *User {
return u.ChatBoostRemoved.Source.User
case u.ManagedBot != nil:
return &u.ManagedBot.Bot
+ case u.Subscription != nil:
+ return &u.Subscription.User
default:
return nil
}
@@ -627,6 +633,10 @@ type ChatFullInfo struct {
//
// optional
GuardBot *User `json:"guard_bot,omitempty"`
+ // Community is the community to which the chat belongs.
+ //
+ // optional
+ Community *Community `json:"community,omitempty"`
}
// IsPrivate returns if the Chat is a private conversation.
@@ -666,8 +676,17 @@ type InaccessibleMessage struct {
// Message represents a message.
type Message struct {
- // MessageID is a unique message identifier inside this chat
+ // MessageID is a unique message identifier inside this chat; it is 0 for ephemeral messages.
MessageID int `json:"message_id"`
+ // ReceiverUser is the user who received an ephemeral message.
+ //
+ // optional
+ ReceiverUser *User `json:"receiver_user,omitempty"`
+ // EphemeralMessageID is the identifier of an ephemeral message inside the chat.
+ // It may be reused after the message is deleted or expires.
+ //
+ // optional
+ EphemeralMessageID int `json:"ephemeral_message_id,omitempty"`
// Unique identifier of a message thread to which the message belongs;
// for supergroups only
//
@@ -1066,6 +1085,14 @@ type Message struct {
//
// optional
ChecklistTasksAdded *ChecklistTasksAdded `json:"checklist_tasks_added,omitempty"`
+ // CommunityChatAdded is a service message about a chat being added to a community.
+ //
+ // optional
+ CommunityChatAdded *CommunityChatAdded `json:"community_chat_added,omitempty"`
+ // CommunityChatRemoved is a service message about a chat being removed from a community.
+ //
+ // optional
+ CommunityChatRemoved *CommunityChatRemoved `json:"community_chat_removed,omitempty"`
// DirectMessagePriceChanged is a service message for direct message price changes.
//
// optional
@@ -1503,12 +1530,18 @@ type ExternalReplyInfo struct {
type ReplyParameters struct {
// MessageID identifier of the message that will be replied to in
// the current chat, or in the chat chat_id if it is specified
- MessageID int `json:"message_id"`
+ //
+ // optional if EphemeralMessageID is specified
+ MessageID int `json:"message_id,omitempty"`
// ChatID if the message to be replied to is from a different chat,
// unique identifier for the chat or username of the channel (in the format @channelusername)
//
// optional
ChatID any `json:"chat_id,omitempty"`
+ // EphemeralMessageID identifies the incoming ephemeral message to reply to.
+ //
+ // optional if MessageID is specified
+ EphemeralMessageID int `json:"ephemeral_message_id,omitempty"`
// AllowSendingWithoutReply true if the message should be sent even
// if the specified message to be replied to is not found;
// can be used only for replies in the same chat and forum topic.
@@ -2596,10 +2629,18 @@ type RichMessage struct {
// InputRichMessage describes a rich message to be sent.
type InputRichMessage struct {
- HTML string `json:"html,omitempty"`
- Markdown string `json:"markdown,omitempty"`
- IsRTL bool `json:"is_rtl,omitempty"`
- SkipEntityDetection bool `json:"skip_entity_detection,omitempty"`
+ Blocks []InputRichBlock `json:"blocks,omitempty"`
+ HTML string `json:"html,omitempty"`
+ Markdown string `json:"markdown,omitempty"`
+ Media []InputRichMessageMedia `json:"media,omitempty"`
+ IsRTL bool `json:"is_rtl,omitempty"`
+ SkipEntityDetection bool `json:"skip_entity_detection,omitempty"`
+}
+
+// InputRichMessageMedia describes media embedded in an outgoing rich message.
+type InputRichMessageMedia struct {
+ ID string `json:"id"`
+ Media InputMedia `json:"media"`
}
// RichText represents any rich formatted text value.
@@ -2947,6 +2988,163 @@ type RichBlockThinking struct {
Text RichText `json:"text"`
}
+// InputRichBlockListItem describes an item in an outgoing rich list.
+type InputRichBlockListItem struct {
+ Blocks []InputRichBlock `json:"blocks"`
+ HasCheckbox bool `json:"has_checkbox,omitempty"`
+ IsChecked bool `json:"is_checked,omitempty"`
+ Value int `json:"value,omitempty"`
+ Type string `json:"type,omitempty"`
+}
+
+// InputRichBlock represents any outgoing rich message block.
+type InputRichBlock any
+
+// InputRichBlockParagraph describes an outgoing paragraph block.
+type InputRichBlockParagraph struct {
+ Type string `json:"type"`
+ Text RichText `json:"text"`
+}
+
+// InputRichBlockSectionHeading describes an outgoing heading block.
+type InputRichBlockSectionHeading struct {
+ Type string `json:"type"`
+ Text RichText `json:"text"`
+ Size int `json:"size"`
+}
+
+// InputRichBlockPreformatted describes an outgoing preformatted block.
+type InputRichBlockPreformatted struct {
+ Type string `json:"type"`
+ Text RichText `json:"text"`
+ Language string `json:"language,omitempty"`
+}
+
+// InputRichBlockFooter describes an outgoing footer block.
+type InputRichBlockFooter struct {
+ Type string `json:"type"`
+ Text RichText `json:"text"`
+}
+
+// InputRichBlockDivider describes an outgoing divider block.
+type InputRichBlockDivider struct {
+ Type string `json:"type"`
+}
+
+// InputRichBlockMathematicalExpression describes an outgoing mathematical expression block.
+type InputRichBlockMathematicalExpression struct {
+ Type string `json:"type"`
+ Expression string `json:"expression"`
+}
+
+// InputRichBlockAnchor describes an outgoing anchor block.
+type InputRichBlockAnchor struct {
+ Type string `json:"type"`
+ Name string `json:"name"`
+}
+
+// InputRichBlockList describes an outgoing list block.
+type InputRichBlockList struct {
+ Type string `json:"type"`
+ Items []InputRichBlockListItem `json:"items"`
+}
+
+// InputRichBlockBlockQuotation describes an outgoing block quotation.
+type InputRichBlockBlockQuotation struct {
+ Type string `json:"type"`
+ Blocks []InputRichBlock `json:"blocks"`
+ Credit RichText `json:"credit,omitempty"`
+}
+
+// InputRichBlockPullQuotation describes an outgoing pull quotation.
+type InputRichBlockPullQuotation struct {
+ Type string `json:"type"`
+ Text RichText `json:"text"`
+ Credit RichText `json:"credit,omitempty"`
+}
+
+// InputRichBlockCollage describes an outgoing collage block.
+type InputRichBlockCollage struct {
+ Type string `json:"type"`
+ Blocks []InputRichBlock `json:"blocks"`
+ Caption *RichBlockCaption `json:"caption,omitempty"`
+}
+
+// InputRichBlockSlideshow describes an outgoing slideshow block.
+type InputRichBlockSlideshow struct {
+ Type string `json:"type"`
+ Blocks []InputRichBlock `json:"blocks"`
+ Caption *RichBlockCaption `json:"caption,omitempty"`
+}
+
+// InputRichBlockTable describes an outgoing table block.
+type InputRichBlockTable struct {
+ Type string `json:"type"`
+ Cells [][]RichBlockTableCell `json:"cells"`
+ IsBordered bool `json:"is_bordered,omitempty"`
+ IsStriped bool `json:"is_striped,omitempty"`
+ Caption RichText `json:"caption,omitempty"`
+}
+
+// InputRichBlockDetails describes an outgoing details block.
+type InputRichBlockDetails struct {
+ Type string `json:"type"`
+ Summary RichText `json:"summary"`
+ Blocks []InputRichBlock `json:"blocks"`
+ IsOpen bool `json:"is_open,omitempty"`
+}
+
+// InputRichBlockMap describes an outgoing map block.
+type InputRichBlockMap struct {
+ Type string `json:"type"`
+ Location Location `json:"location"`
+ Zoom int `json:"zoom"`
+ Width int `json:"width"`
+ Height int `json:"height"`
+ Caption *RichBlockCaption `json:"caption,omitempty"`
+}
+
+// InputRichBlockAnimation describes an outgoing animation block.
+type InputRichBlockAnimation struct {
+ Type string `json:"type"`
+ Animation InputMediaAnimation `json:"animation"`
+ Caption *RichBlockCaption `json:"caption,omitempty"`
+}
+
+// InputRichBlockAudio describes an outgoing audio block.
+type InputRichBlockAudio struct {
+ Type string `json:"type"`
+ Audio InputMediaAudio `json:"audio"`
+ Caption *RichBlockCaption `json:"caption,omitempty"`
+}
+
+// InputRichBlockPhoto describes an outgoing photo block.
+type InputRichBlockPhoto struct {
+ Type string `json:"type"`
+ Photo InputMediaPhoto `json:"photo"`
+ Caption *RichBlockCaption `json:"caption,omitempty"`
+}
+
+// InputRichBlockVideo describes an outgoing video block.
+type InputRichBlockVideo struct {
+ Type string `json:"type"`
+ Video InputMediaVideo `json:"video"`
+ Caption *RichBlockCaption `json:"caption,omitempty"`
+}
+
+// InputRichBlockVoiceNote describes an outgoing voice note block.
+type InputRichBlockVoiceNote struct {
+ Type string `json:"type"`
+ VoiceNote InputMediaVoiceNote `json:"voice_note"`
+ Caption *RichBlockCaption `json:"caption,omitempty"`
+}
+
+// InputRichBlockThinking describes an outgoing thinking placeholder block.
+type InputRichBlockThinking struct {
+ Type string `json:"type"`
+ Text RichText `json:"text"`
+}
+
// UserProfilePhotos contains a set of user profile photos.
type UserProfilePhotos struct {
// TotalCount total number of profile pictures the target user has
@@ -4135,6 +4333,10 @@ type BotCommand struct {
Command string `json:"command"`
// Description of the command, 3-256 characters.
Description string `json:"description"`
+ // IsEphemeral is true if the command sends an ephemeral message.
+ //
+ // optional
+ IsEphemeral bool `json:"is_ephemeral,omitempty"`
}
// BotCommandScope represents the scope to which bot commands are applied.
@@ -4608,6 +4810,15 @@ type InputMediaDocument struct {
DisableContentTypeDetection bool `json:"disable_content_type_detection,omitempty"`
}
+// InputMediaVoiceNote represents a voice message file to send.
+type InputMediaVoiceNote struct {
+ BaseInputMedia
+ // Duration of the voice message in seconds.
+ //
+ // optional
+ Duration int `json:"duration,omitempty"`
+}
+
// InputMediaLivePhoto represents a live photo to send.
type InputMediaLivePhoto struct {
BaseInputMedia
@@ -6853,6 +7064,27 @@ type ManagedBotUpdated struct {
Bot User `json:"bot"`
}
+// BotSubscriptionUpdated contains information about a user payment subscription change.
+type BotSubscriptionUpdated struct {
+ User User `json:"user"`
+ InvoicePayload string `json:"invoice_payload"`
+ State string `json:"state"`
+}
+
+// Community represents a group of related chats.
+type Community struct {
+ ID int64 `json:"id"`
+ Name string `json:"name"`
+}
+
+// CommunityChatAdded describes a chat being added to a community.
+type CommunityChatAdded struct {
+ Community Community `json:"community"`
+}
+
+// CommunityChatRemoved describes a chat being removed from a community.
+type CommunityChatRemoved struct{}
+
// PollOptionAdded describes a service message about an option added to a poll.
type PollOptionAdded struct {
PollMessage *MaybeInaccessibleMessage `json:"poll_message,omitempty"`
diff --git a/upload.go b/upload.go
index ad39a53b..e1578977 100644
--- a/upload.go
+++ b/upload.go
@@ -225,6 +225,154 @@ func prepareInputMediaUploadPlan(inputMedia []InputMedia, prefix string) ([]Inpu
return prepared, plan
}
+func prepareInputRichMessageUploadPlan(message InputRichMessage) (InputRichMessage, *uploadPlan) {
+ prepared := message
+ prepared.Media = append([]InputRichMessageMedia(nil), message.Media...)
+ plan := newUploadPlan()
+
+ for idx := range prepared.Media {
+ media := cloneInputMedia(prepared.Media[idx].Media)
+ if media == nil {
+ continue
+ }
+
+ prepareInputMediaItem(media, fmt.Sprintf("rich-message-media-%d", idx), plan)
+ prepared.Media[idx].Media = media
+ }
+
+ prepared.Blocks = prepareInputRichBlocks(message.Blocks, "rich-message-block", plan)
+
+ return prepared, plan
+}
+
+func prepareInputRichBlocks(blocks []InputRichBlock, prefix string, plan *uploadPlan) []InputRichBlock {
+ if blocks == nil {
+ return nil
+ }
+
+ prepared := make([]InputRichBlock, len(blocks))
+ for idx, block := range blocks {
+ prepared[idx] = prepareInputRichBlock(block, fmt.Sprintf("%s-%d", prefix, idx), plan)
+ }
+
+ return prepared
+}
+
+func prepareInputRichBlock(block InputRichBlock, name string, plan *uploadPlan) InputRichBlock {
+ switch current := block.(type) {
+ case InputRichBlockList:
+ return prepareInputRichBlockList(current, name, plan)
+ case *InputRichBlockList:
+ if current == nil {
+ return current
+ }
+ prepared := prepareInputRichBlockList(*current, name, plan)
+ return &prepared
+ case InputRichBlockBlockQuotation:
+ current.Blocks = prepareInputRichBlocks(current.Blocks, name+"-block", plan)
+ return current
+ case *InputRichBlockBlockQuotation:
+ if current == nil {
+ return current
+ }
+ prepared := *current
+ prepared.Blocks = prepareInputRichBlocks(current.Blocks, name+"-block", plan)
+ return &prepared
+ case InputRichBlockCollage:
+ current.Blocks = prepareInputRichBlocks(current.Blocks, name+"-block", plan)
+ return current
+ case *InputRichBlockCollage:
+ if current == nil {
+ return current
+ }
+ prepared := *current
+ prepared.Blocks = prepareInputRichBlocks(current.Blocks, name+"-block", plan)
+ return &prepared
+ case InputRichBlockSlideshow:
+ current.Blocks = prepareInputRichBlocks(current.Blocks, name+"-block", plan)
+ return current
+ case *InputRichBlockSlideshow:
+ if current == nil {
+ return current
+ }
+ prepared := *current
+ prepared.Blocks = prepareInputRichBlocks(current.Blocks, name+"-block", plan)
+ return &prepared
+ case InputRichBlockDetails:
+ current.Blocks = prepareInputRichBlocks(current.Blocks, name+"-block", plan)
+ return current
+ case *InputRichBlockDetails:
+ if current == nil {
+ return current
+ }
+ prepared := *current
+ prepared.Blocks = prepareInputRichBlocks(current.Blocks, name+"-block", plan)
+ return &prepared
+ case InputRichBlockAnimation:
+ prepareInputMediaItem(¤t.Animation, name, plan)
+ return current
+ case *InputRichBlockAnimation:
+ if current == nil {
+ return current
+ }
+ prepared := *current
+ prepareInputMediaItem(&prepared.Animation, name, plan)
+ return &prepared
+ case InputRichBlockAudio:
+ prepareInputMediaItem(¤t.Audio, name, plan)
+ return current
+ case *InputRichBlockAudio:
+ if current == nil {
+ return current
+ }
+ prepared := *current
+ prepareInputMediaItem(&prepared.Audio, name, plan)
+ return &prepared
+ case InputRichBlockPhoto:
+ prepareInputMediaItem(¤t.Photo, name, plan)
+ return current
+ case *InputRichBlockPhoto:
+ if current == nil {
+ return current
+ }
+ prepared := *current
+ prepareInputMediaItem(&prepared.Photo, name, plan)
+ return &prepared
+ case InputRichBlockVideo:
+ prepareInputMediaItem(¤t.Video, name, plan)
+ return current
+ case *InputRichBlockVideo:
+ if current == nil {
+ return current
+ }
+ prepared := *current
+ prepareInputMediaItem(&prepared.Video, name, plan)
+ return &prepared
+ case InputRichBlockVoiceNote:
+ prepareInputMediaItem(¤t.VoiceNote, name, plan)
+ return current
+ case *InputRichBlockVoiceNote:
+ if current == nil {
+ return current
+ }
+ prepared := *current
+ prepareInputMediaItem(&prepared.VoiceNote, name, plan)
+ return &prepared
+ default:
+ return block
+ }
+}
+
+func prepareInputRichBlockList(block InputRichBlockList, name string, plan *uploadPlan) InputRichBlockList {
+ block.Items = append([]InputRichBlockListItem(nil), block.Items...)
+ for itemIdx := range block.Items {
+ prefix := fmt.Sprintf("%s-item-%d-block", name, itemIdx)
+ block.Items[itemIdx].Blocks = prepareInputRichBlocks(block.Items[itemIdx].Blocks, prefix, plan)
+ }
+
+ return block
+}
+
func prepareInputMediaItem(media InputMedia, name string, plan *uploadPlan) {
if file := media.getMedia(); file != nil && file.NeedsUpload() {
media.setUploadMedia("attach://" + name)