diff --git a/.gitignore b/.gitignore index 9585d150..1db48e85 100644 --- a/.gitignore +++ b/.gitignore @@ -3,5 +3,6 @@ coverage.out tmp/ book/ .vscode/ +.cursor/ CLAUDE.md api.json \ No newline at end of file diff --git a/bot_api_10_2_test.go b/bot_api_10_2_test.go index 8611c451..0b5ec7d1 100644 --- a/bot_api_10_2_test.go +++ b/bot_api_10_2_test.go @@ -29,14 +29,18 @@ func TestBotAPI102InputRichBlockJSONContract(t *testing.T) { {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: "expandable quotation", block: InputRichBlockExpandableBlockQuotation{Type: "expandable_blockquote", Text: "quote", Credit: "author"}, typeName: "expandable_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: "compact table", block: InputRichBlockTable{Type: "table", Cells: [][]RichBlockTableCell{{{Text: "cell", Align: "left", Valign: "middle"}}}, IsBordered: true, IsCompact: true}, typeName: "table", fieldMatch: `"is_compact":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: "buttons", block: InputRichBlockButtons{Type: "buttons", Buttons: []RichMessageButton{{Text: "Go", CallbackData: "go"}}, Align: "center"}, typeName: "buttons", fieldMatch: `"align":"center"`}, {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: "document", block: InputRichBlockDocument{Type: "document", Document: NewInputMediaDocument(FileID("document"))}, typeName: "document", fieldMatch: `"document":{"type":"document","media":"document"}`}, {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"}`}, @@ -158,11 +162,18 @@ func TestBotAPI102NewEphemeralMessageDirectAdminParams(t *testing.T) { if err != nil { t.Fatalf("params: %v", err) } - if params["chat_id"] != "-1001" || params["receiver_user_id"] != "42" || params["text"] != "private" { + if params["chat_id"] != "-1001" || params["text"] != "private" { t.Fatalf("direct admin ephemeral params mismatch: %#v", params) } - if _, ok := params["callback_query_id"]; ok { - t.Fatalf("direct admin request unexpectedly contains callback query: %#v", params) + raw, ok := params["ephemeral_message_parameters"] + if !ok { + t.Fatalf("missing ephemeral_message_parameters: %#v", params) + } + if !strings.Contains(raw, `"receiver_user_id":42`) { + t.Fatalf("unexpected ephemeral_message_parameters: %s", raw) + } + if strings.Contains(raw, "callback_query_id") { + t.Fatalf("direct admin request unexpectedly contains callback query: %s", raw) } if _, ok := params["reply_parameters"]; ok { t.Fatalf("direct admin request unexpectedly contains reply parameters: %#v", params) @@ -170,32 +181,33 @@ func TestBotAPI102NewEphemeralMessageDirectAdminParams(t *testing.T) { } func TestBotAPI102EphemeralSendParams(t *testing.T) { + ephemeral := EphemeralMessageParameters{ReceiverUserID: 42, CallbackQueryID: "callback"} message := NewMessage(1, "text") - message.ReceiverUserID, message.CallbackQueryID = 42, "callback" + message.EphemeralMessageParameters = ephemeral animation := NewAnimation(1, FileID("animation")) - animation.ReceiverUserID, animation.CallbackQueryID = 42, "callback" + animation.EphemeralMessageParameters = ephemeral audio := NewAudio(1, FileID("audio")) - audio.ReceiverUserID, audio.CallbackQueryID = 42, "callback" + audio.EphemeralMessageParameters = ephemeral document := NewDocument(1, FileID("document")) - document.ReceiverUserID, document.CallbackQueryID = 42, "callback" + document.EphemeralMessageParameters = ephemeral photo := NewPhoto(1, FileID("photo")) - photo.ReceiverUserID, photo.CallbackQueryID = 42, "callback" + photo.EphemeralMessageParameters = ephemeral livePhoto := NewLivePhoto(1, FileID("live-photo"), FileID("photo")) - livePhoto.ReceiverUserID, livePhoto.CallbackQueryID = 42, "callback" + livePhoto.EphemeralMessageParameters = ephemeral sticker := NewSticker(1, FileID("sticker")) - sticker.ReceiverUserID, sticker.CallbackQueryID = 42, "callback" + sticker.EphemeralMessageParameters = ephemeral video := NewVideo(1, FileID("video")) - video.ReceiverUserID, video.CallbackQueryID = 42, "callback" + video.EphemeralMessageParameters = ephemeral videoNote := NewVideoNote(1, 10, FileID("video-note")) - videoNote.ReceiverUserID, videoNote.CallbackQueryID = 42, "callback" + videoNote.EphemeralMessageParameters = ephemeral voice := NewVoice(1, FileID("voice")) - voice.ReceiverUserID, voice.CallbackQueryID = 42, "callback" + voice.EphemeralMessageParameters = ephemeral contact := NewContact(1, "+12025550123", "Ada") - contact.ReceiverUserID, contact.CallbackQueryID = 42, "callback" + contact.EphemeralMessageParameters = ephemeral location := NewLocation(1, 10.5, 20.25) - location.ReceiverUserID, location.CallbackQueryID = 42, "callback" + location.EphemeralMessageParameters = ephemeral venue := NewVenue(1, "Office", "Main Street", 10.5, 20.25) - venue.ReceiverUserID, venue.CallbackQueryID = 42, "callback" + venue.EphemeralMessageParameters = ephemeral configs := []struct { method string @@ -224,8 +236,12 @@ func TestBotAPI102EphemeralSendParams(t *testing.T) { 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) + raw, ok := params["ephemeral_message_parameters"] + if !ok { + t.Fatalf("missing ephemeral_message_parameters: %#v", params) + } + if !strings.Contains(raw, `"receiver_user_id":42`) || !strings.Contains(raw, `"callback_query_id":"callback"`) { + t.Fatalf("ephemeral send params mismatch: %s", raw) } }) } @@ -235,11 +251,8 @@ func TestBotAPI102EphemeralSendParams(t *testing.T) { 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) + if _, ok := params["ephemeral_message_parameters"]; ok { + t.Fatalf("ephemeral_message_parameters leaked into unsupported method: %#v", params) } } @@ -247,7 +260,7 @@ 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")}) + mediaValue := NewInputMediaPhoto(FileID("photo")) media := NewEditEphemeralMessageMedia(1, 2, 3, &mediaValue) caption := NewEditEphemeralMessageCaption(1, 2, 3, "") replyMarkup := NewEditEphemeralMessageReplyMarkup(1, 2, 3, markup) @@ -260,7 +273,7 @@ func TestBotAPI102EphemeralLifecycleParams(t *testing.T) { 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: media, method: "editEphemeralMessageMedia", key: "media", value: `{"type":"photo","media":"photo"}`}, {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"}, @@ -281,14 +294,21 @@ func TestBotAPI102EphemeralLifecycleParams(t *testing.T) { if test.key != "" { value, ok := params[test.key] if !ok || value != test.value { - t.Fatalf("%s mismatch: %#v", test.key, params) + t.Fatalf("%s mismatch: got %q want %q in %#v", test.key, value, test.value, params) } } }) } - if _, ok := any(media).(Fileable); ok { - t.Fatal("ephemeral media edit must not support direct file uploads") + uploadMedia := NewInputMediaPhoto(FileBytes{Name: "photo.jpg", Bytes: []byte("photo")}) + uploadConfig := NewEditEphemeralMessageMedia(1, 2, 3, &uploadMedia) + fileable, ok := any(uploadConfig).(Fileable) + if !ok { + t.Fatal("ephemeral media edit must support direct file uploads") + } + files := fileable.files() + if len(files) != 1 || files[0].Name != "file-0" { + t.Fatalf("unexpected ephemeral media upload files: %#v", files) } } diff --git a/bot_api_10_3_test.go b/bot_api_10_3_test.go new file mode 100644 index 00000000..08d88fe8 --- /dev/null +++ b/bot_api_10_3_test.go @@ -0,0 +1,258 @@ +package tgbotapi + +import ( + "encoding/json" + "strings" + "testing" +) + +func TestBotAPI103EphemeralMessageParameters(t *testing.T) { + config := NewSendRichMessage(1, NewInputRichMessageHTML("
hi
")) + config.EphemeralMessageParameters = EphemeralMessageParameters{ + ReceiverUserID: 42, + CallbackQueryID: "cbq", + ReplaceCallbackQueryMessage: true, + } + + params, err := config.params() + if err != nil { + t.Fatalf("params: %v", err) + } + raw := params["ephemeral_message_parameters"] + if !strings.Contains(raw, `"receiver_user_id":42`) || + !strings.Contains(raw, `"callback_query_id":"cbq"`) || + !strings.Contains(raw, `"replace_callback_query_message":true`) { + t.Fatalf("unexpected ephemeral_message_parameters: %s", raw) + } +} + +func TestBotAPI103DraftStopParams(t *testing.T) { + draft := SendMessageDraftConfig{ + ChatConfig: ChatConfig{ChatID: 1}, + DraftID: 7, + Text: "partial", + CanStop: true, + KeepOnStop: true, + } + params, err := draft.params() + if err != nil { + t.Fatalf("params: %v", err) + } + if params["can_stop"] != "true" || params["keep_on_stop"] != "true" { + t.Fatalf("draft stop params mismatch: %#v", params) + } + + richDraft := NewSendRichMessageDraft(1, 7, NewInputRichMessageMarkdown("**hi**")) + richDraft.CanStop = true + richDraft.KeepOnStop = true + params, err = richDraft.params() + if err != nil { + t.Fatalf("rich draft params: %v", err) + } + if params["can_stop"] != "true" || params["keep_on_stop"] != "true" { + t.Fatalf("rich draft stop params mismatch: %#v", params) + } +} + +func TestBotAPI103EditEphemeralExtensions(t *testing.T) { + rich := NewInputRichMessageHTML("hi
") + text := NewEditEphemeralMessageText(1, 2, 3, "") + text.RichMessage = rich + params, err := text.params() + if err != nil { + t.Fatalf("params: %v", err) + } + if _, ok := params["text"]; ok { + t.Fatalf("empty text should be omitted when rich_message is set: %#v", params) + } + if !strings.Contains(params["rich_message"], `"html"`) || !strings.Contains(params["rich_message"], "hi") { + t.Fatalf("unexpected rich_message: %s", params["rich_message"]) + } + + caption := NewEditEphemeralMessageCaption(1, 2, 3, "cap") + caption.ShowCaptionAboveMedia = true + params, err = caption.params() + if err != nil { + t.Fatalf("caption params: %v", err) + } + if params["show_caption_above_media"] != "true" { + t.Fatalf("show_caption_above_media missing: %#v", params) + } +} + +func TestBotAPI103PromoteWelcomeMessages(t *testing.T) { + config := PromoteChatMemberConfig{ + ChatMemberConfig: ChatMemberConfig{ + ChatConfig: ChatConfig{ChatID: 1}, + UserID: 2, + }, + CanSendWelcomeMessages: true, + } + params, err := config.params() + if err != nil { + t.Fatalf("params: %v", err) + } + if params["can_send_welcome_messages"] != "true" { + t.Fatalf("can_send_welcome_messages missing: %#v", params) + } +} + +func TestBotAPI103NewTypesJSON(t *testing.T) { + tests := []struct { + name string + value any + want string + }{ + { + name: "rich message button", + value: RichMessageButton{ + Text: "Go", + Style: "primary", + CallbackData: "go", + Disabled: &DisabledButton{}, + }, + want: `"callback_data":"go"`, + }, + { + name: "rich text button", + value: RichTextButton{Type: "button", Button: RichMessageButton{Text: "Go", URL: "https://t.me"}}, + want: `"type":"button"`, + }, + { + name: "buttons block", + value: RichBlockButtons{Type: "buttons", Buttons: []RichMessageButton{{Text: "A", CallbackData: "a"}}, Align: RichBlockButtonsAlignCenter}, + want: `"align":"center"`, + }, + { + name: "expandable quotation", + value: RichBlockExpandableBlockQuotation{Type: "expandable_blockquote", Text: "quote"}, + want: `"type":"expandable_blockquote"`, + }, + { + name: "document block", + value: RichBlockDocument{Type: "document", Document: Document{FileID: "doc"}}, + want: `"type":"document"`, + }, + { + name: "compact table", + value: RichBlockTable{Type: "table", Cells: [][]RichBlockTableCell{{{Align: "left", Valign: "top"}}}, IsCompact: true}, + want: `"is_compact":true`, + }, + { + name: "input document block", + value: InputRichBlockDocument{ + Type: "document", + Document: NewInputMediaDocument(FileID("doc")), + }, + want: `"document":{"type":"document","media":"doc"}`, + }, + { + name: "unique gift info", + value: UniqueGiftInfo{ + Gift: UniqueGift{BaseName: "Gift"}, + Origin: "transfer", + Text: "hello", + IsPrivate: true, + }, + want: `"is_private":true`, + }, + { + name: "message generation stopped", + value: MessageGenerationStopped{ + Chat: Chat{ID: 1}, + DraftID: 9, + }, + want: `"draft_id":9`, + }, + { + name: "community chat joined", + value: CommunityChatJoined{Community: Community{ID: 5, Name: "Ops"}}, + want: `"name":"Ops"`, + }, + { + name: "keyboard force reply", + value: InlineKeyboardMarkup{ + InlineKeyboard: [][]InlineKeyboardButton{{{Text: "A", CallbackData: stringPtr("a")}}}, + ForceReply: true, + }, + want: `"force_reply":true`, + }, + { + name: "disabled inline button", + value: InlineKeyboardButton{ + Text: "Nope", + Disabled: &DisabledButton{}, + }, + want: `"disabled":{}`, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + raw, err := json.Marshal(test.value) + if err != nil { + t.Fatalf("marshal: %v", err) + } + if !strings.Contains(string(raw), test.want) { + t.Fatalf("got %s, want substring %q", raw, test.want) + } + }) + } +} + +func TestBotAPI103RichBlockButtonsAlign(t *testing.T) { + block := RichBlockButtons{Align: RichBlockButtonsAlignCenter} + if !block.Center() || block.Left() || block.Right() { + t.Fatalf("unexpected align helpers: %#v", block) + } + + input := InputRichBlockButtons{Align: RichBlockButtonsAlignRight} + if !input.Right() || input.Left() || input.Center() { + t.Fatalf("unexpected input align helpers: %#v", input) + } +} + +func TestBotAPI103RichMessageButtonStyle(t *testing.T) { + button := RichMessageButton{Style: RichMessageButtonStylePrimary} + if !button.Primary() || button.Danger() || button.LinkStyle() { + t.Fatalf("unexpected style helpers: %#v", button) + } +} + +func TestBotAPI103UpdateStoppedMessageGeneration(t *testing.T) { + var update Update + if err := json.Unmarshal([]byte(`{ + "update_id":1, + "stopped_message_generation":{"chat":{"id":10,"type":"private"},"draft_id":3} + }`), &update); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if update.StoppedMessageGeneration == nil || update.StoppedMessageGeneration.DraftID != 3 { + t.Fatalf("unexpected update: %#v", update.StoppedMessageGeneration) + } + if chat := update.FromChat(); chat == nil || chat.ID != 10 { + t.Fatalf("FromChat mismatch: %#v", chat) + } +} + +func TestBotAPI103RichDocumentUpload(t *testing.T) { + document := NewInputMediaDocument(FileBytes{Name: "notes.pdf", Bytes: []byte("%PDF")}) + block := InputRichBlockDocument{ + Type: "document", + Document: document, + } + config := NewSendRichMessage(1, NewInputRichMessageBlocks(block)) + assertRichMessageUpload(t, config, []string{"rich-message-block-0"}) + + data, err := json.Marshal(block) + if err != nil { + t.Fatalf("marshal: %v", err) + } + if strings.Contains(string(data), "attach://") { + t.Fatalf("user block was mutated: %s", data) + } +} + +func stringPtr(v string) *string { + return &v +} diff --git a/configs.go b/configs.go index 4c56f49b..6eb8d08d 100644 --- a/configs.go +++ b/configs.go @@ -136,6 +136,9 @@ const ( // UpdateTypeSubscription is emitted when a user payment subscription changes. UpdateTypeSubscription = "subscription" + + // UpdateTypeStoppedMessageGeneration is emitted when a user stops message generation. + UpdateTypeStoppedMessageGeneration = "stopped_message_generation" ) // Library errors @@ -309,12 +312,11 @@ func (CloseConfig) params() (Params, error) { // MessageConfig contains information about a SendMessage request. type MessageConfig struct { BaseChat - Text string - ParseMode string - Entities []MessageEntity - LinkPreviewOptions LinkPreviewOptions - ReceiverUserID int64 - CallbackQueryID string + Text string + ParseMode string + Entities []MessageEntity + LinkPreviewOptions LinkPreviewOptions + EphemeralMessageParameters EphemeralMessageParameters } func (config MessageConfig) params() (Params, error) { @@ -325,8 +327,9 @@ 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) + if err = params.AddInterfaceNonZero("ephemeral_message_parameters", config.EphemeralMessageParameters); err != nil { + return params, err + } err = params.AddInterface("entities", config.Entities) if err != nil { return params, err @@ -370,6 +373,8 @@ type SendMessageDraftConfig struct { ThinkingPlaceholder bool ParseMode string Entities []MessageEntity + CanStop bool + KeepOnStop bool } func (config SendMessageDraftConfig) method() string { @@ -391,6 +396,11 @@ func (config SendMessageDraftConfig) params() (Params, error) { } params.AddNonEmpty("parse_mode", config.ParseMode) err = params.AddInterface("entities", config.Entities) + if err != nil { + return params, err + } + params.AddBool("can_stop", config.CanStop) + params.AddBool("keep_on_stop", config.KeepOnStop) return params, err } @@ -398,7 +408,8 @@ func (config SendMessageDraftConfig) params() (Params, error) { // SendRichMessageConfig allows you to send a rich message. type SendRichMessageConfig struct { BaseChat - RichMessage InputRichMessage + RichMessage InputRichMessage + EphemeralMessageParameters EphemeralMessageParameters } func (config SendRichMessageConfig) method() string { @@ -413,6 +424,10 @@ func (config SendRichMessageConfig) params() (Params, error) { preparedRichMessage := prepareInputRichMessageForParams(config.RichMessage) err = params.AddInterface("rich_message", preparedRichMessage) + if err != nil { + return params, err + } + err = params.AddInterfaceNonZero("ephemeral_message_parameters", config.EphemeralMessageParameters) return params, err } @@ -427,6 +442,8 @@ type SendRichMessageDraftConfig struct { MessageThreadID int DraftID int RichMessage InputRichMessage + CanStop bool + KeepOnStop bool } func (config SendRichMessageDraftConfig) method() string { @@ -442,6 +459,11 @@ func (config SendRichMessageDraftConfig) params() (Params, error) { params.AddNonZero("message_thread_id", config.MessageThreadID) params.AddNonZero("draft_id", config.DraftID) err = params.AddInterface("rich_message", config.RichMessage) + if err != nil { + return params, err + } + params.AddBool("can_stop", config.CanStop) + params.AddBool("keep_on_stop", config.KeepOnStop) return params, err } @@ -573,13 +595,12 @@ func (config CopyMessagesConfig) method() string { type PhotoConfig struct { BaseFile BaseSpoiler - Thumb RequestFileData - Caption string - ParseMode string - CaptionEntities []MessageEntity - ShowCaptionAboveMedia bool - ReceiverUserID int64 - CallbackQueryID string + Thumb RequestFileData + Caption string + ParseMode string + CaptionEntities []MessageEntity + ShowCaptionAboveMedia bool + EphemeralMessageParameters EphemeralMessageParameters } func (config PhotoConfig) params() (Params, error) { @@ -591,8 +612,9 @@ 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) + if err = params.AddInterfaceNonZero("ephemeral_message_parameters", config.EphemeralMessageParameters); err != nil { + return params, err + } err = params.AddInterface("caption_entities", config.CaptionEntities) if err != nil { return params, err @@ -622,14 +644,13 @@ func (config PhotoConfig) files() []RequestFile { type SendLivePhotoConfig struct { BaseChat BaseSpoiler - LivePhoto RequestFileData - Photo RequestFileData - Caption string - ParseMode string - CaptionEntities []MessageEntity - ShowCaptionAboveMedia bool - ReceiverUserID int64 - CallbackQueryID string + LivePhoto RequestFileData + Photo RequestFileData + Caption string + ParseMode string + CaptionEntities []MessageEntity + ShowCaptionAboveMedia bool + EphemeralMessageParameters EphemeralMessageParameters } func (config SendLivePhotoConfig) params() (Params, error) { @@ -641,8 +662,9 @@ func (config SendLivePhotoConfig) 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) + if err = params.AddInterfaceNonZero("ephemeral_message_parameters", config.EphemeralMessageParameters); err != nil { + return params, err + } if err = params.AddInterface("caption_entities", config.CaptionEntities); err != nil { return params, err } @@ -670,15 +692,14 @@ func (config SendLivePhotoConfig) files() []RequestFile { // AudioConfig contains information about a SendAudio request. type AudioConfig struct { BaseFile - Thumb RequestFileData - Caption string - ParseMode string - CaptionEntities []MessageEntity - Duration int - Performer string - Title string - ReceiverUserID int64 - CallbackQueryID string + Thumb RequestFileData + Caption string + ParseMode string + CaptionEntities []MessageEntity + Duration int + Performer string + Title string + EphemeralMessageParameters EphemeralMessageParameters } func (config AudioConfig) params() (Params, error) { @@ -692,8 +713,9 @@ 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) + if err = params.AddInterfaceNonZero("ephemeral_message_parameters", config.EphemeralMessageParameters); err != nil { + return params, err + } err = params.AddInterface("caption_entities", config.CaptionEntities) return params, err @@ -718,8 +740,7 @@ type DocumentConfig struct { ParseMode string CaptionEntities []MessageEntity DisableContentTypeDetection bool - ReceiverUserID int64 - CallbackQueryID string + EphemeralMessageParameters EphemeralMessageParameters } func (config DocumentConfig) params() (Params, error) { @@ -731,8 +752,9 @@ 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) + if err = params.AddInterfaceNonZero("ephemeral_message_parameters", config.EphemeralMessageParameters); err != nil { + return params, err + } err = params.AddInterface("caption_entities", config.CaptionEntities) if err != nil { return params, err @@ -757,8 +779,7 @@ type StickerConfig struct { // Emoji associated with the sticker; only for just uploaded stickers Emoji string BaseFile - ReceiverUserID int64 - CallbackQueryID string + EphemeralMessageParameters EphemeralMessageParameters } func (config StickerConfig) params() (Params, error) { @@ -767,8 +788,9 @@ 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) + if err = params.AddInterfaceNonZero("ephemeral_message_parameters", config.EphemeralMessageParameters); err != nil { + return params, err + } return params, err } @@ -784,19 +806,18 @@ func (config StickerConfig) files() []RequestFile { type VideoConfig struct { BaseFile BaseSpoiler - Thumb RequestFileData - Duration int - Width int - Height int - Cover RequestFileData - StartTimestamp int64 - Caption string - ParseMode string - CaptionEntities []MessageEntity - ShowCaptionAboveMedia bool - SupportsStreaming bool - ReceiverUserID int64 - CallbackQueryID string + Thumb RequestFileData + Duration int + Width int + Height int + Cover RequestFileData + StartTimestamp int64 + Caption string + ParseMode string + CaptionEntities []MessageEntity + ShowCaptionAboveMedia bool + SupportsStreaming bool + EphemeralMessageParameters EphemeralMessageParameters } func (config VideoConfig) params() (Params, error) { @@ -813,8 +834,9 @@ 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) + if err = params.AddInterfaceNonZero("ephemeral_message_parameters", config.EphemeralMessageParameters); err != nil { + return params, err + } err = params.AddInterface("caption_entities", config.CaptionEntities) if err != nil { return params, err @@ -845,16 +867,15 @@ func (config VideoConfig) files() []RequestFile { type AnimationConfig struct { BaseFile BaseSpoiler - Duration int - Width int - Height int - Thumb RequestFileData - Caption string - ParseMode string - CaptionEntities []MessageEntity - ShowCaptionAboveMedia bool - ReceiverUserID int64 - CallbackQueryID string + Duration int + Width int + Height int + Thumb RequestFileData + Caption string + ParseMode string + CaptionEntities []MessageEntity + ShowCaptionAboveMedia bool + EphemeralMessageParameters EphemeralMessageParameters } func (config AnimationConfig) params() (Params, error) { @@ -869,8 +890,9 @@ 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) + if err = params.AddInterfaceNonZero("ephemeral_message_parameters", config.EphemeralMessageParameters); err != nil { + return params, err + } err = params.AddInterface("caption_entities", config.CaptionEntities) if err != nil { return params, err @@ -899,11 +921,10 @@ func (config AnimationConfig) files() []RequestFile { // VideoNoteConfig contains information about a SendVideoNote request. type VideoNoteConfig struct { BaseFile - Thumb RequestFileData - Duration int - Length int - ReceiverUserID int64 - CallbackQueryID string + Thumb RequestFileData + Duration int + Length int + EphemeralMessageParameters EphemeralMessageParameters } func (config VideoNoteConfig) params() (Params, error) { @@ -911,8 +932,12 @@ 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) + if err != nil { + return params, err + } + if err = params.AddInterfaceNonZero("ephemeral_message_parameters", config.EphemeralMessageParameters); err != nil { + return params, err + } return params, err } @@ -993,13 +1018,12 @@ func (config PaidMediaConfig) inputPaidMedia() []InputMedia { // VoiceConfig contains information about a SendVoice request. type VoiceConfig struct { BaseFile - Thumb RequestFileData - Caption string - ParseMode string - CaptionEntities []MessageEntity - Duration int - ReceiverUserID int64 - CallbackQueryID string + Thumb RequestFileData + Caption string + ParseMode string + CaptionEntities []MessageEntity + Duration int + EphemeralMessageParameters EphemeralMessageParameters } func (config VoiceConfig) params() (Params, error) { @@ -1011,8 +1035,9 @@ 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) + if err = params.AddInterfaceNonZero("ephemeral_message_parameters", config.EphemeralMessageParameters); err != nil { + return params, err + } err = params.AddInterface("caption_entities", config.CaptionEntities) return params, err @@ -1032,14 +1057,13 @@ func (config VoiceConfig) files() []RequestFile { // LocationConfig contains information about a SendLocation request. type LocationConfig struct { BaseChat - Latitude float64 // required - Longitude float64 // required - HorizontalAccuracy float64 // optional - LivePeriod int // optional - Heading int // optional - ProximityAlertRadius int // optional - ReceiverUserID int64 - CallbackQueryID string + Latitude float64 // required + Longitude float64 // required + HorizontalAccuracy float64 // optional + LivePeriod int // optional + Heading int // optional + ProximityAlertRadius int // optional + EphemeralMessageParameters EphemeralMessageParameters } func (config LocationConfig) params() (Params, error) { @@ -1051,8 +1075,12 @@ 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) + if err != nil { + return params, err + } + if err = params.AddInterfaceNonZero("ephemeral_message_parameters", config.EphemeralMessageParameters); err != nil { + return params, err + } return params, err } @@ -1105,16 +1133,15 @@ func (config StopMessageLiveLocationConfig) method() string { // VenueConfig contains information about a SendVenue request. type VenueConfig struct { BaseChat - Latitude float64 // required - Longitude float64 // required - Title string // required - Address string // required - FoursquareID string - FoursquareType string - GooglePlaceID string - GooglePlaceType string - ReceiverUserID int64 - CallbackQueryID string + Latitude float64 // required + Longitude float64 // required + Title string // required + Address string // required + FoursquareID string + FoursquareType string + GooglePlaceID string + GooglePlaceType string + EphemeralMessageParameters EphemeralMessageParameters } func (config VenueConfig) params() (Params, error) { @@ -1128,8 +1155,12 @@ 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) + if err != nil { + return params, err + } + if err = params.AddInterfaceNonZero("ephemeral_message_parameters", config.EphemeralMessageParameters); err != nil { + return params, err + } return params, err } @@ -1141,12 +1172,11 @@ func (config VenueConfig) method() string { // ContactConfig allows you to send a contact. type ContactConfig struct { BaseChat - PhoneNumber string - FirstName string - LastName string - VCard string - ReceiverUserID int64 - CallbackQueryID string + PhoneNumber string + FirstName string + LastName string + VCard string + EphemeralMessageParameters EphemeralMessageParameters } func (config ContactConfig) params() (Params, error) { @@ -1157,8 +1187,12 @@ 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) + if err != nil { + return params, err + } + if err = params.AddInterfaceNonZero("ephemeral_message_parameters", config.EphemeralMessageParameters); err != nil { + return params, err + } return params, err } @@ -1498,12 +1532,13 @@ func (config EditMessageReplyMarkupConfig) method() string { return "editMessageReplyMarkup" } -// EditEphemeralMessageTextConfig edits an ephemeral text message. +// EditEphemeralMessageTextConfig edits an ephemeral text or rich message. type EditEphemeralMessageTextConfig struct { BaseEphemeralMessage Text string ParseMode string Entities []MessageEntity + RichMessage InputRichMessage LinkPreviewOptions LinkPreviewOptions ReplyMarkup *InlineKeyboardMarkup } @@ -1514,11 +1549,14 @@ func (config EditEphemeralMessageTextConfig) params() (Params, error) { return params, err } - params["text"] = config.Text + params.AddNonEmpty("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("rich_message", prepareInputRichMessageForParams(config.RichMessage)); err != nil { + return params, err + } if err = params.AddInterfaceNonZero("link_preview_options", config.LinkPreviewOptions); err != nil { return params, err } @@ -1531,6 +1569,10 @@ func (EditEphemeralMessageTextConfig) method() string { return "editEphemeralMessageText" } +func (config EditEphemeralMessageTextConfig) files() []RequestFile { + return prepareInputRichMessageForFiles(config.RichMessage) +} + // EditEphemeralMessageMediaConfig edits the media of an ephemeral message. type EditEphemeralMessageMediaConfig struct { BaseEphemeralMessage @@ -1544,7 +1586,11 @@ func (config EditEphemeralMessageMediaConfig) params() (Params, error) { return params, err } - if err = params.AddInterface("media", config.Media); err != nil { + if isNilParamValue(config.Media) { + config.Media = nil + } + preparedMedia := prepareInputMediaForParams([]InputMedia{config.Media}) + if err = params.AddInterface("media", preparedMedia[0]); err != nil { return params, err } err = params.AddInterface("reply_markup", config.ReplyMarkup) @@ -1556,13 +1602,21 @@ func (EditEphemeralMessageMediaConfig) method() string { return "editEphemeralMessageMedia" } +func (config EditEphemeralMessageMediaConfig) files() []RequestFile { + if isNilParamValue(config.Media) { + return nil + } + return prepareInputMediaForFiles([]InputMedia{config.Media}) +} + // EditEphemeralMessageCaptionConfig edits the caption of an ephemeral message. type EditEphemeralMessageCaptionConfig struct { BaseEphemeralMessage - Caption string - ParseMode string - CaptionEntities []MessageEntity - ReplyMarkup *InlineKeyboardMarkup + Caption string + ParseMode string + CaptionEntities []MessageEntity + ShowCaptionAboveMedia bool + ReplyMarkup *InlineKeyboardMarkup } func (config EditEphemeralMessageCaptionConfig) params() (Params, error) { @@ -1576,6 +1630,7 @@ func (config EditEphemeralMessageCaptionConfig) params() (Params, error) { if err = params.AddInterface("caption_entities", config.CaptionEntities); err != nil { return params, err } + params.AddBool("show_caption_above_media", config.ShowCaptionAboveMedia) err = params.AddInterface("reply_markup", config.ReplyMarkup) return params, err @@ -2262,6 +2317,7 @@ type PromoteChatMemberConfig struct { CanManageTopics bool CanManageDirectMessages bool CanManageTags bool + CanSendWelcomeMessages bool } func (config PromoteChatMemberConfig) method() string { @@ -2291,6 +2347,7 @@ func (config PromoteChatMemberConfig) params() (Params, error) { params.AddBool("can_manage_topics", config.CanManageTopics) params.AddBool("can_manage_direct_messages", config.CanManageDirectMessages) params.AddBool("can_manage_tags", config.CanManageTags) + params.AddBool("can_send_welcome_messages", config.CanSendWelcomeMessages) return params, nil } diff --git a/docs/examples/bot-api-10.md b/docs/examples/bot-api-10.md index dec99830..87168fa6 100644 --- a/docs/examples/bot-api-10.md +++ b/docs/examples/bot-api-10.md @@ -39,7 +39,7 @@ richMessage.Media = []tgbotapi.InputRichMessageMedia{ _, 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. +Exactly one of `HTML`, `Markdown`, or `Blocks` must be used. Direct upload of new files is not available for `SendRichMessageDraftConfig` or inline `EditMessageTextConfig`; use a Telegram `file_id` there. ## Ephemeral Commands and Messages @@ -58,7 +58,9 @@ An incoming ephemeral command has `Message.MessageID == 0` and a separate `Messa ```go incoming := update.Message reply := tgbotapi.NewMessage(incoming.Chat.ID, "Checking...") -reply.ReceiverUserID = incoming.From.ID +reply.EphemeralMessageParameters = tgbotapi.EphemeralMessageParameters{ + ReceiverUserID: incoming.From.ID, +} reply.ReplyParameters.EphemeralMessageID = incoming.EphemeralMessageID sent, err := bot.Send(reply) @@ -74,9 +76,9 @@ if err != nil { } ``` -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. +For a callback-query-triggered response, set `EphemeralMessageParameters.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`. +The edit and delete methods require all three identifiers: chat, receiver user, and ephemeral message. `EditEphemeralMessageCaption` accepts an empty caption to remove it. `EditEphemeralMessageMedia` supports uploading new files. ## Subscription and Community Updates diff --git a/examples/bot_api_10.go b/examples/bot_api_10.go index a89f8b25..c6885301 100644 --- a/examples/bot_api_10.go +++ b/examples/bot_api_10.go @@ -138,7 +138,9 @@ func handle_bot_api_10_2_update(bot *api.BotAPI, update api.Update) { } reply := api.NewMessage(update.Message.Chat.ID, "Checking...") - reply.ReceiverUserID = update.Message.From.ID + reply.EphemeralMessageParameters = api.EphemeralMessageParameters{ + ReceiverUserID: update.Message.From.ID, + } reply.ReplyParameters.EphemeralMessageID = update.Message.EphemeralMessageID sent, err := bot.Send(reply) if err != nil { diff --git a/helper_methods.go b/helper_methods.go index a7db8d55..09f31f18 100644 --- a/helper_methods.go +++ b/helper_methods.go @@ -924,11 +924,13 @@ func newBaseEphemeralMessage(chatID, receiverUserID int64, ephemeralMessageID in // NewEphemeralMessage creates a text message visible only to receiverUserID. // // Bot administrators can send the returned config directly. Other bots must -// additionally set CallbackQueryID or ReplyParameters.EphemeralMessageID -// within 15 seconds of an eligible action. +// additionally set EphemeralMessageParameters.CallbackQueryID or +// ReplyParameters.EphemeralMessageID within 15 seconds of an eligible action. func NewEphemeralMessage(chatID, receiverUserID int64, text string) MessageConfig { config := NewMessage(chatID, text) - config.ReceiverUserID = receiverUserID + config.EphemeralMessageParameters = EphemeralMessageParameters{ + ReceiverUserID: receiverUserID, + } return config } diff --git a/parity_interfaces_test.go b/parity_interfaces_test.go index 4c4541e4..260a4204 100644 --- a/parity_interfaces_test.go +++ b/parity_interfaces_test.go @@ -57,6 +57,8 @@ var ( _ Fileable = SendLivePhotoConfig{} _ Fileable = SendRichMessageConfig{} _ Fileable = EditMessageTextConfig{} + _ Fileable = EditEphemeralMessageTextConfig{} + _ Fileable = EditEphemeralMessageMediaConfig{} _ Fileable = SendPollConfig{} _ Fileable = SetBusinessAccountProfilePhotoConfig{} _ Fileable = PostStoryConfig{} diff --git a/types.go b/types.go index 7cb064a1..f83ee47e 100644 --- a/types.go +++ b/types.go @@ -167,6 +167,10 @@ type Update struct { // // optional Subscription *BotSubscriptionUpdated `json:"subscription,omitempty"` + // StoppedMessageGeneration is emitted when a user stops message generation. + // + // optional + StoppedMessageGeneration *MessageGenerationStopped `json:"stopped_message_generation,omitempty"` } // SentFrom returns the user who sent an update. Can be nil, if Telegram did not provide information @@ -267,6 +271,8 @@ func (u *Update) FromChat() *Chat { return &u.ChatBoost.Chat case u.ChatBoostRemoved != nil: return &u.ChatBoostRemoved.Chat + case u.StoppedMessageGeneration != nil: + return &u.StoppedMessageGeneration.Chat default: return nil } @@ -1089,6 +1095,10 @@ type Message struct { // // optional CommunityChatAdded *CommunityChatAdded `json:"community_chat_added,omitempty"` + // CommunityChatJoined is a service message about a chat being joined from a community. + // + // optional + CommunityChatJoined *CommunityChatJoined `json:"community_chat_joined,omitempty"` // CommunityChatRemoved is a service message about a chat being removed from a community. // // optional @@ -2784,6 +2794,14 @@ type RichTextBotCommand struct { BotCommand string `json:"bot_command"` } +// RichTextButton describes a rich text button. +type RichTextButton struct { + // Type type of the rich text, always "button" + Type string `json:"type"` + // Button the button + Button RichMessageButton `json:"button"` +} + // RichTextAnchor describes an anchor. type RichTextAnchor struct { Type string `json:"type"` @@ -2896,6 +2914,18 @@ type RichBlockBlockQuotation struct { Credit RichText `json:"credit,omitempty"` } +// RichBlockExpandableBlockQuotation describes an expandable block quotation. +type RichBlockExpandableBlockQuotation struct { + // Type type of the block, always "expandable_blockquote" + Type string `json:"type"` + // Text content of the block + Text RichText `json:"text"` + // Credit credit of the block + // + // optional + Credit RichText `json:"credit,omitempty"` +} + // RichBlockPullQuotation describes a pull quotation. type RichBlockPullQuotation struct { Type string `json:"type"` @@ -2922,8 +2952,15 @@ type RichBlockTable 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"` + // IsStriped True, if the table is striped + // + // optional + IsStriped bool `json:"is_striped,omitempty"` + // IsCompact True, if table cells have smaller indents + // + // optional + IsCompact bool `json:"is_compact,omitempty"` + Caption RichText `json:"caption,omitempty"` } // RichBlockDetails describes an expandable details block. @@ -2944,6 +2981,36 @@ type RichBlockMap struct { Caption *RichBlockCaption `json:"caption,omitempty"` } +// RichBlockButtonsAlignLeft is the left horizontal alignment for RichBlockButtons. +const RichBlockButtonsAlignLeft = "left" + +// RichBlockButtonsAlignCenter is the center horizontal alignment for RichBlockButtons. +const RichBlockButtonsAlignCenter = "center" + +// RichBlockButtonsAlignRight is the right horizontal alignment for RichBlockButtons. +const RichBlockButtonsAlignRight = "right" + +// RichBlockButtons describes a row of rich message buttons. +type RichBlockButtons struct { + // Type type of the block, always "buttons" + Type string `json:"type"` + // Buttons the buttons + Buttons []RichMessageButton `json:"buttons"` + // Align horizontal alignment of the buttons; must be one of "left", "center", or "right" + // + // optional + Align string `json:"align,omitempty"` +} + +// Left returns true if the buttons are aligned to the left. +func (b RichBlockButtons) Left() bool { return b.Align == RichBlockButtonsAlignLeft } + +// Center returns true if the buttons are centered. +func (b RichBlockButtons) Center() bool { return b.Align == RichBlockButtonsAlignCenter } + +// Right returns true if the buttons are aligned to the right. +func (b RichBlockButtons) Right() bool { return b.Align == RichBlockButtonsAlignRight } + // RichBlockAnimation describes an animation block. type RichBlockAnimation struct { Type string `json:"type"` @@ -2959,6 +3026,18 @@ type RichBlockAudio struct { Caption *RichBlockCaption `json:"caption,omitempty"` } +// RichBlockDocument describes a document block. +type RichBlockDocument struct { + // Type type of the block, always "document" + Type string `json:"type"` + // Document the document + Document Document `json:"document"` + // Caption caption of the block + // + // optional + Caption *RichBlockCaption `json:"caption,omitempty"` +} + // RichBlockPhoto describes a photo block. type RichBlockPhoto struct { Type string `json:"type"` @@ -3056,6 +3135,18 @@ type InputRichBlockBlockQuotation struct { Credit RichText `json:"credit,omitempty"` } +// InputRichBlockExpandableBlockQuotation describes an outgoing expandable block quotation. +type InputRichBlockExpandableBlockQuotation struct { + // Type type of the block, always "expandable_blockquote" + Type string `json:"type"` + // Text content of the block + Text RichText `json:"text"` + // Credit credit of the block + // + // optional + Credit RichText `json:"credit,omitempty"` +} + // InputRichBlockPullQuotation describes an outgoing pull quotation. type InputRichBlockPullQuotation struct { Type string `json:"type"` @@ -3082,8 +3173,15 @@ 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"` + // IsStriped True, if the table is striped + // + // optional + IsStriped bool `json:"is_striped,omitempty"` + // IsCompact True, if table cells have smaller indents + // + // optional + IsCompact bool `json:"is_compact,omitempty"` + Caption RichText `json:"caption,omitempty"` } // InputRichBlockDetails describes an outgoing details block. @@ -3104,6 +3202,27 @@ type InputRichBlockMap struct { Caption *RichBlockCaption `json:"caption,omitempty"` } +// InputRichBlockButtons describes an outgoing row of rich message buttons. +type InputRichBlockButtons struct { + // Type type of the block, always "buttons" + Type string `json:"type"` + // Buttons list of 1-8 buttons to send + Buttons []RichMessageButton `json:"buttons"` + // Align horizontal alignment of the buttons; must be one of "left", "center", or "right" + // + // optional + Align string `json:"align,omitempty"` +} + +// Left returns true if the buttons are aligned to the left. +func (b InputRichBlockButtons) Left() bool { return b.Align == RichBlockButtonsAlignLeft } + +// Center returns true if the buttons are centered. +func (b InputRichBlockButtons) Center() bool { return b.Align == RichBlockButtonsAlignCenter } + +// Right returns true if the buttons are aligned to the right. +func (b InputRichBlockButtons) Right() bool { return b.Align == RichBlockButtonsAlignRight } + // InputRichBlockAnimation describes an outgoing animation block. type InputRichBlockAnimation struct { Type string `json:"type"` @@ -3118,6 +3237,18 @@ type InputRichBlockAudio struct { Caption *RichBlockCaption `json:"caption,omitempty"` } +// InputRichBlockDocument describes an outgoing document block. +type InputRichBlockDocument struct { + // Type type of the block, always "document" + Type string `json:"type"` + // Document the document; caption on the document is ignored + Document InputMediaDocument `json:"document"` + // Caption caption of the block + // + // optional + Caption *RichBlockCaption `json:"caption,omitempty"` +} + // InputRichBlockPhoto describes an outgoing photo block. type InputRichBlockPhoto struct { Type string `json:"type"` @@ -3228,6 +3359,11 @@ type ReplyKeyboardMarkup struct { // // optional Selective bool `json:"selective,omitempty"` + // ForceReply shows reply interface to the user, as if they manually + // selected the bot's message and tapped 'Reply'. + // + // optional + ForceReply bool `json:"force_reply,omitempty"` } // KeyboardButton represents one button of the reply keyboard. For simple text @@ -3437,6 +3573,12 @@ type InlineKeyboardMarkup struct { // InlineKeyboard array of button rows, each represented by an Array of // InlineKeyboardButton objects InlineKeyboard [][]InlineKeyboardButton `json:"inline_keyboard"` + // ForceReply shows reply interface to the user, as if they manually + // selected the bot's message and tapped 'Reply'. The value of the field + // can't be changed when the inline keyboard is edited. + // + // optional + ForceReply bool `json:"force_reply,omitempty"` } // InlineKeyboardButton represents one button of an inline keyboard. You must @@ -3524,6 +3666,10 @@ type InlineKeyboardButton struct { // // optional Pay bool `json:"pay,omitempty"` + // Disabled marks the button as disabled so it does nothing. + // + // optional + Disabled *DisabledButton `json:"disabled,omitempty"` } // LoginURL represents a parameter of the inline keyboard button used to @@ -3725,6 +3871,11 @@ type ChatAdministratorRights struct { CanManageTopics bool `json:"can_manage_topics"` CanManageDirectMessages bool `json:"can_manage_direct_messages,omitempty"` CanManageTags bool `json:"can_manage_tags,omitempty"` + // CanSendWelcomeMessages True, if the administrator can manage chat welcome + // messages or directly send them in the case of bots. + // + // optional + CanSendWelcomeMessages bool `json:"can_send_welcome_messages,omitempty"` } // ChatMember contains information about one member of a chat. @@ -3916,6 +4067,11 @@ type ChatMember struct { // // optional CanManageTags bool `json:"can_manage_tags,omitempty"` + // CanSendWelcomeMessages True, if the administrator can manage chat welcome + // messages or directly send them in the case of bots. + // + // optional + CanSendWelcomeMessages bool `json:"can_send_welcome_messages,omitempty"` // CanEditTag True, if the user is allowed to edit their own tag. // // optional @@ -7082,9 +7238,120 @@ type CommunityChatAdded struct { Community Community `json:"community"` } +// CommunityChatJoined describes a chat being joined by a user from a community. +type CommunityChatJoined struct { + // Community the community from which the chat was joined + Community Community `json:"community"` +} + // CommunityChatRemoved describes a chat being removed from a community. type CommunityChatRemoved struct{} +// MessageGenerationStopped describes an update about a user stopping message generation. +type MessageGenerationStopped struct { + // Chat chat in which the message is generated + Chat Chat `json:"chat"` + // MessageThreadID unique identifier of the message thread in which the message is generated + // + // optional + MessageThreadID int `json:"message_thread_id,omitempty"` + // DraftID unique identifier of the message draft which was stopped + DraftID int `json:"draft_id"` +} + +// EphemeralMessageParameters describes parameters of an ephemeral message to send. +type EphemeralMessageParameters struct { + // ReceiverUserID identifier of the user who will receive the message + ReceiverUserID int64 `json:"receiver_user_id"` + // CallbackQueryID identifier of the callback query which triggered the message, if any + // + // optional + CallbackQueryID string `json:"callback_query_id,omitempty"` + // ReplaceCallbackQueryMessage pass True to show the ephemeral message in place of + // the original message; must be False for callback queries from ephemeral messages + // + // optional + ReplaceCallbackQueryMessage bool `json:"replace_callback_query_message,omitempty"` +} + +// DisabledButton represents a disabled button which does nothing. +type DisabledButton struct{} + +// RichMessageButtonStyleDanger is the red button style. +const RichMessageButtonStyleDanger = "danger" + +// RichMessageButtonStyleSuccess is the green button style. +const RichMessageButtonStyleSuccess = "success" + +// RichMessageButtonStylePrimary is the blue button style. +const RichMessageButtonStylePrimary = "primary" + +// RichMessageButtonStyleLink shows the button as a regular link without borders. +const RichMessageButtonStyleLink = "link" + +// RichMessageButton represents a button in a RichMessage. +// +// Exactly one of the optional action fields must be used to specify the type of the button. +type RichMessageButton struct { + // Text text of the button; may contain only plain text, RichTextCustomEmoji and RichTextDateTime entities + Text RichText `json:"text"` + // Style style of the button; must be one of "danger", "success", "primary", or "link" + // + // optional + Style string `json:"style,omitempty"` + // URL HTTP or tg:// URL to be opened when the button is pressed + // + // optional + URL string `json:"url,omitempty"` + // CallbackData data to be sent in a callback query to the bot when the button is pressed, 1-64 bytes + // + // optional + CallbackData string `json:"callback_data,omitempty"` + // WebApp description of the Web App that will be launched when the user presses the button + // + // optional + WebApp *WebAppInfo `json:"web_app,omitempty"` + // LoginURL an HTTPS URL used to automatically authorize the user; not supported for ephemeral messages + // + // optional + LoginURL *LoginURL `json:"login_url,omitempty"` + // SwitchInlineQuery if set, pressing the button will prompt the user to select one of their chats, + // open that chat and insert the bot's username and the specified inline query in the input field + // + // optional + SwitchInlineQuery *string `json:"switch_inline_query,omitempty"` + // SwitchInlineQueryCurrentChat if set, pressing the button will insert the bot's username and the + // specified inline query in the current chat's input field + // + // optional + SwitchInlineQueryCurrentChat *string `json:"switch_inline_query_current_chat,omitempty"` + // SwitchInlineQueryChosenChat if set, pressing the button will prompt the user to select one of their + // chats of the specified type and insert the bot's username and the specified inline query + // + // optional + SwitchInlineQueryChosenChat *SwitchInlineQueryChosenChat `json:"switch_inline_query_chosen_chat,omitempty"` + // CopyText a button that copies the specified text to the clipboard + // + // optional + CopyText *CopyTextButton `json:"copy_text,omitempty"` + // Disabled if set, then the button is disabled and does nothing + // + // optional + Disabled *DisabledButton `json:"disabled,omitempty"` +} + +// Danger returns true if the button uses the danger style. +func (b RichMessageButton) Danger() bool { return b.Style == RichMessageButtonStyleDanger } + +// Success returns true if the button uses the success style. +func (b RichMessageButton) Success() bool { return b.Style == RichMessageButtonStyleSuccess } + +// Primary returns true if the button uses the primary style. +func (b RichMessageButton) Primary() bool { return b.Style == RichMessageButtonStylePrimary } + +// LinkStyle returns true if the button uses the link style. +func (b RichMessageButton) LinkStyle() bool { return b.Style == RichMessageButtonStyleLink } + // PollOptionAdded describes a service message about an option added to a poll. type PollOptionAdded struct { PollMessage *MaybeInaccessibleMessage `json:"poll_message,omitempty"` @@ -7389,13 +7656,25 @@ type UniqueGift struct { // UniqueGiftInfo describes information about a unique gift in a message. type UniqueGiftInfo struct { - Gift UniqueGift `json:"gift"` - Origin string `json:"origin"` - LastResaleCurrency string `json:"last_resale_currency,omitempty"` - LastResaleAmount int `json:"last_resale_amount,omitempty"` - OwnedGiftID string `json:"owned_gift_id,omitempty"` - TransferStarCount int `json:"transfer_star_count,omitempty"` - NextTransferDate int64 `json:"next_transfer_date,omitempty"` + Gift UniqueGift `json:"gift"` + Origin string `json:"origin"` + // Text text of the message that was added to the gift + // + // optional + Text string `json:"text,omitempty"` + // Entities special entities that appear in the text + // + // optional + Entities []MessageEntity `json:"entities,omitempty"` + // IsPrivate True, if the sender and gift text are shown only to the gift receiver + // + // optional + IsPrivate bool `json:"is_private,omitempty"` + LastResaleCurrency string `json:"last_resale_currency,omitempty"` + LastResaleAmount int `json:"last_resale_amount,omitempty"` + OwnedGiftID string `json:"owned_gift_id,omitempty"` + TransferStarCount int `json:"transfer_star_count,omitempty"` + NextTransferDate int64 `json:"next_transfer_date,omitempty"` } // UserProfileAudios contains audios displayed on user profile. diff --git a/upload.go b/upload.go index e1578977..42fc2cd3 100644 --- a/upload.go +++ b/upload.go @@ -328,6 +328,16 @@ func prepareInputRichBlock(block InputRichBlock, name string, plan *uploadPlan) prepared := *current prepareInputMediaItem(&prepared.Audio, name, plan) return &prepared + case InputRichBlockDocument: + prepareInputMediaItem(¤t.Document, name, plan) + return current + case *InputRichBlockDocument: + if current == nil { + return current + } + prepared := *current + prepareInputMediaItem(&prepared.Document, name, plan) + return &prepared case InputRichBlockPhoto: prepareInputMediaItem(¤t.Photo, name, plan) return current