diff --git a/api_parity_test.go b/api_parity_test.go index 3448619a..b4d9c4ed 100644 --- a/api_parity_test.go +++ b/api_parity_test.go @@ -621,7 +621,7 @@ func (index *packageIndex) collectCallParam(receiverType string, call *ast.CallE } switch selector.Sel.Name { - case "AddNonEmpty", "AddNonZero", "AddNonZero64", "AddNonZeroFloat", "AddBool", "AddBoolPtr", "AddInterface", "AddFirstValid", "paramsWithKey": + case "AddNonEmpty", "AddNonZero", "AddNonZero64", "AddNonZeroFloat", "AddBool", "AddBoolValue", "AddBoolPtr", "AddInterface", "AddInterfaceNonZero", "AddFirstValid", "paramsWithKey": if key, ok := firstStringArg(call); ok { params[key] = struct{}{} } diff --git a/configs.go b/configs.go index 605354ab..dae83b2f 100644 --- a/configs.go +++ b/configs.go @@ -324,7 +324,7 @@ func (config MessageConfig) params() (Params, error) { if err != nil { return params, err } - err = params.AddInterface("link_preview_options", config.LinkPreviewOptions) + err = params.AddInterfaceNonZero("link_preview_options", config.LinkPreviewOptions) return params, err } @@ -357,11 +357,12 @@ func (config SendChecklistConfig) params() (Params, error) { // SendMessageDraftConfig allows you to send a draft message. type SendMessageDraftConfig struct { ChatConfig - MessageThreadID int - DraftID int - Text string - ParseMode string - Entities []MessageEntity + MessageThreadID int + DraftID int + Text string + ThinkingPlaceholder bool + ParseMode string + Entities []MessageEntity } func (config SendMessageDraftConfig) method() string { @@ -376,7 +377,11 @@ func (config SendMessageDraftConfig) params() (Params, error) { params.AddNonZero("message_thread_id", config.MessageThreadID) params.AddNonZero("draft_id", config.DraftID) - params.AddNonEmpty("text", config.Text) + if config.ThinkingPlaceholder { + params["text"] = "" + } else { + params.AddNonEmpty("text", config.Text) + } params.AddNonEmpty("parse_mode", config.ParseMode) err = params.AddInterface("entities", config.Entities) @@ -884,6 +889,7 @@ type PaidMediaConfig struct { BaseChat StarCount int64 Media *InputPaidMedia + MediaItems []InputPaidMedia Payload string Caption string // optional ParseMode string // optional @@ -903,9 +909,9 @@ func (config PaidMediaConfig) params() (Params, error) { params.AddNonEmpty("parse_mode", config.ParseMode) params.AddBool("show_caption_above_media", config.ShowCaptionAboveMedia) - media := []InputMedia{config.Media} + media := config.inputPaidMedia() newMedia := prepareInputMediaForParams(media) - err = params.AddInterface("media", newMedia[0]) + err = params.AddInterface("media", newMedia) if err != nil { return params, err } @@ -914,17 +920,32 @@ func (config PaidMediaConfig) params() (Params, error) { } func (config PaidMediaConfig) files() []RequestFile { - if config.Media == nil { + media := config.inputPaidMedia() + if len(media) == 0 { return nil } - return prepareInputMediaForFiles([]InputMedia{config.Media}) + return prepareInputMediaForFiles(media) } func (config PaidMediaConfig) method() string { return "sendPaidMedia" } +func (config PaidMediaConfig) inputPaidMedia() []InputMedia { + if len(config.MediaItems) > 0 { + media := make([]InputMedia, 0, len(config.MediaItems)) + for idx := range config.MediaItems { + media = append(media, &config.MediaItems[idx]) + } + return media + } + if config.Media != nil { + return []InputMedia{config.Media} + } + return nil +} + // VoiceConfig contains information about a SendVoice request. type VoiceConfig struct { BaseFile @@ -1330,7 +1351,7 @@ func (config EditMessageTextConfig) params() (Params, error) { return params, err } } - err = params.AddInterface("link_preview_options", config.LinkPreviewOptions) + err = params.AddInterfaceNonZero("link_preview_options", config.LinkPreviewOptions) return params, err } @@ -1626,6 +1647,7 @@ func (config UserPersonalChatMessagesConfig) params() (Params, error) { type SetUserEmojiStatusConfig struct { UserID int64 // required EmojiStatusCustomEmojiID string + RemoveStatus bool EmojiStatusExpirationDate int64 } @@ -1637,7 +1659,11 @@ func (config SetUserEmojiStatusConfig) params() (Params, error) { params := make(Params) params.AddNonZero64("user_id", config.UserID) - params.AddNonEmpty("emoji_status_custom_emoji_id", config.EmojiStatusCustomEmojiID) + if config.RemoveStatus { + params["emoji_status_custom_emoji_id"] = "" + } else { + params.AddNonEmpty("emoji_status_custom_emoji_id", config.EmojiStatusCustomEmojiID) + } params.AddNonZero64("emoji_status_expiration_date", config.EmojiStatusExpirationDate) return params, nil @@ -2622,7 +2648,7 @@ func (config ShippingConfig) params() (Params, error) { params := make(Params) params["shipping_query_id"] = config.ShippingQueryID - params.AddBool("ok", config.OK) + params.AddBoolValue("ok", config.OK) err := params.AddInterface("shipping_options", config.ShippingOptions) params.AddNonEmpty("error_message", config.ErrorMessage) @@ -2644,7 +2670,7 @@ func (config PreCheckoutConfig) params() (Params, error) { params := make(Params) params["pre_checkout_query_id"] = config.PreCheckoutQueryID - params.AddBool("ok", config.OK) + params.AddBoolValue("ok", config.OK) params.AddNonEmpty("error_message", config.ErrorMessage) return params, nil @@ -2772,7 +2798,7 @@ func (config EditUserStarSubscriptionConfig) params() (Params, error) { params["telegram_payment_charge_id"] = config.TelegramPaymentChargeID params.AddNonZero64("user_id", config.UserID) - params.AddBool("is_canceled", config.IsCanceled) + params.AddBoolValue("is_canceled", config.IsCanceled) return params, nil } @@ -3338,17 +3364,13 @@ func (config NewStickerSetConfig) params() (Params, error) { params.AddBool("needs_repainting", config.NeedsRepainting) params.AddNonEmpty("sticker_type", string(config.StickerType)) - err := params.AddInterface("stickers", config.Stickers) + err := params.AddInterface("stickers", prepareInputStickersForParams(config.Stickers)) return params, err } func (config NewStickerSetConfig) files() []RequestFile { - requestFiles := []RequestFile{} - for _, v := range config.Stickers { - requestFiles = append(requestFiles, v.Sticker) - } - return requestFiles + return prepareInputStickersForFiles(config.Stickers) } // AddStickerConfig allows you to add a sticker to a set. @@ -3367,12 +3389,12 @@ func (config AddStickerConfig) params() (Params, error) { params.AddNonZero64("user_id", config.UserID) params["name"] = config.Name - err := params.AddInterface("sticker", config.Sticker) + err := params.AddInterface("sticker", prepareInputStickerForParams(config.Sticker, "sticker")) return params, err } func (config AddStickerConfig) files() []RequestFile { - return []RequestFile{config.Sticker.Sticker} + return prepareInputStickerForFiles(config.Sticker, "sticker") } // SetStickerPositionConfig allows you to change the position of a sticker in a set. @@ -3398,6 +3420,7 @@ func (config SetStickerPositionConfig) params() (Params, error) { type SetCustomEmojiStickerSetThumbnailConfig struct { Name string CustomEmojiID string + DropThumbnail bool } func (config SetCustomEmojiStickerSetThumbnailConfig) method() string { @@ -3408,7 +3431,11 @@ func (config SetCustomEmojiStickerSetThumbnailConfig) params() (Params, error) { params := make(Params) params["name"] = config.Name - params.AddNonEmpty("custom_emoji_id", config.CustomEmojiID) + if config.DropThumbnail { + params["custom_emoji_id"] = "" + } else { + params.AddNonEmpty("custom_emoji_id", config.CustomEmojiID) + } return params, nil } @@ -3488,11 +3515,15 @@ func (config ReplaceStickerInSetConfig) params() (Params, error) { params["name"] = config.Name params["old_sticker"] = config.OldSticker - err := params.AddInterface("sticker", config.Sticker) + err := params.AddInterface("sticker", prepareInputStickerForParams(config.Sticker, "sticker")) return params, err } +func (config ReplaceStickerInSetConfig) files() []RequestFile { + return prepareInputStickerForFiles(config.Sticker, "sticker") +} + // SetStickerEmojiListConfig allows you to change the list of emoji assigned to a regular or custom emoji sticker. The sticker must belong to a sticker set created by the bot type SetStickerEmojiListConfig struct { Sticker string @@ -3671,6 +3702,7 @@ type EditForumTopicConfig struct { BaseForum Name string IconCustomEmojiID string + RemoveIcon bool } func (config EditForumTopicConfig) method() string { @@ -3683,7 +3715,11 @@ func (config EditForumTopicConfig) params() (Params, error) { return params, err } params.AddNonEmpty("name", config.Name) - params.AddNonEmpty("icon_custom_emoji_id", config.IconCustomEmojiID) + if config.RemoveIcon { + params["icon_custom_emoji_id"] = "" + } else { + params.AddNonEmpty("icon_custom_emoji_id", config.IconCustomEmojiID) + } return params, nil } @@ -3947,7 +3983,7 @@ func (config GetBusinessConnectionConfig) params() (Params, error) { func (config BusinessConnectionID) params() (Params, error) { params := make(Params) - params["business_connection_id"] = string(config) + params.AddNonEmpty("business_connection_id", string(config)) return params, nil } @@ -4323,6 +4359,7 @@ func (config DeleteMyCommandsConfig) params() (Params, error) { // SetMyNameConfig change the bot's name type SetMyNameConfig struct { Name string + RemoveName bool LanguageCode string } @@ -4333,7 +4370,11 @@ func (config SetMyNameConfig) method() string { func (config SetMyNameConfig) params() (Params, error) { params := make(Params) - params.AddNonEmpty("name", config.Name) + if config.RemoveName { + params["name"] = "" + } else { + params.AddNonEmpty("name", config.Name) + } params.AddNonEmpty("language_code", config.LanguageCode) return params, nil @@ -4408,7 +4449,8 @@ func (config GetMyDescriptionConfig) params() (Params, error) { // SetMyDescroptionConfig sets the bot's description, which is shown in the chat with the bot if the chat is empty type SetMyDescriptionConfig struct { // Pass an empty string to remove the dedicated description for the given language. - Description string + Description string + RemoveDescription bool // If empty, the description will be applied to all users for whose language there is no dedicated description. LanguageCode string } @@ -4420,7 +4462,11 @@ func (config SetMyDescriptionConfig) method() string { func (config SetMyDescriptionConfig) params() (Params, error) { params := make(Params) - params.AddNonEmpty("description", config.Description) + if config.RemoveDescription { + params["description"] = "" + } else { + params.AddNonEmpty("description", config.Description) + } params.AddNonEmpty("language_code", config.LanguageCode) return params, nil @@ -4448,7 +4494,8 @@ type SetMyShortDescriptionConfig struct { // New short description for the bot; 0-120 characters. // //Pass an empty string to remove the dedicated short description for the given language. - ShortDescription string + ShortDescription string + RemoveShortDescription bool //A two-letter ISO 639-1 language code. // //If empty, the short description will be applied to all users for whose language there is no dedicated short description. @@ -4462,7 +4509,11 @@ func (config SetMyShortDescriptionConfig) method() string { func (config SetMyShortDescriptionConfig) params() (Params, error) { params := make(Params) - params.AddNonEmpty("short_description", config.ShortDescription) + if config.RemoveShortDescription { + params["short_description"] = "" + } else { + params.AddNonEmpty("short_description", config.ShortDescription) + } params.AddNonEmpty("language_code", config.LanguageCode) return params, nil @@ -4605,6 +4656,37 @@ func prepareInputStoryContentForFiles(content InputStoryContent) []RequestFile { return plan.Files() } +func prepareInputStickersForParams(stickers []InputSticker) []InputSticker { + prepared := make([]InputSticker, len(stickers)) + for idx := range stickers { + prepared[idx] = prepareInputStickerForParams(stickers[idx], fmt.Sprintf("sticker-%d", idx)) + } + return prepared +} + +func prepareInputStickerForParams(sticker InputSticker, name string) InputSticker { + if sticker.Sticker.Data != nil && sticker.Sticker.Data.NeedsUpload() && sticker.Sticker.Name == "" { + sticker.Sticker.Name = name + } + return sticker +} + +func prepareInputStickersForFiles(stickers []InputSticker) []RequestFile { + files := make([]RequestFile, 0, len(stickers)) + for idx := range stickers { + files = append(files, prepareInputStickerForFiles(stickers[idx], fmt.Sprintf("sticker-%d", idx))...) + } + return files +} + +func prepareInputStickerForFiles(sticker InputSticker, name string) []RequestFile { + prepared := prepareInputStickerForParams(sticker, name) + if prepared.Sticker.Data == nil || !prepared.Sticker.Data.NeedsUpload() { + return nil + } + return []RequestFile{prepared.Sticker} +} + func ptr[T any](v T) *T { return &v } @@ -4693,6 +4775,8 @@ func cloneInputMedia(media InputMedia) InputMedia { clone := &PaidMediaConfig{ BaseChat: m.BaseChat, StarCount: m.StarCount, + MediaItems: append([]InputPaidMedia(nil), m.MediaItems...), + Payload: m.Payload, Caption: m.Caption, ParseMode: m.ParseMode, CaptionEntities: m.CaptionEntities, diff --git a/configs_test.go b/configs_test.go index fea6f2eb..60f674c9 100644 --- a/configs_test.go +++ b/configs_test.go @@ -1,6 +1,7 @@ package tgbotapi import ( + "encoding/json" "fmt" "strings" "testing" @@ -269,6 +270,46 @@ func TestPaidMediaLivePhotoSerialization(t *testing.T) { } } +func TestPaidMediaConfigSerializesMediaArray(t *testing.T) { + photo := NewInputMediaPhoto(FileID("paid-photo-id")) + paid := NewInputPaidMediaPhoto(&photo) + config := NewPaidMedia(1, 10, &paid) + + params, err := config.params() + if err != nil { + t.Fatalf("params failed: %v", err) + } + + var media []map[string]any + if err := json.Unmarshal([]byte(params["media"]), &media); err != nil { + t.Fatalf("media must be a JSON array, got %q: %v", params["media"], err) + } + if len(media) != 1 || media[0]["type"] != "photo" || media[0]["media"] != "paid-photo-id" { + t.Fatalf("unexpected paid media payload: %#v", media) + } +} + +func TestPaidMediaGroupConfigSerializesMultipleMedia(t *testing.T) { + photo := NewInputMediaPhoto(FileID("paid-photo-id")) + paidPhoto := NewInputPaidMediaPhoto(&photo) + video := NewInputMediaVideo(FileID("paid-video-id")) + paidVideo := NewInputPaidMediaVideo(&video) + config := NewPaidMediaGroup(1, 10, paidPhoto, paidVideo) + + params, err := config.params() + if err != nil { + t.Fatalf("params failed: %v", err) + } + + var media []map[string]any + if err := json.Unmarshal([]byte(params["media"]), &media); err != nil { + t.Fatalf("media must be a JSON array, got %q: %v", params["media"], err) + } + if len(media) != 2 || media[0]["type"] != "photo" || media[1]["type"] != "video" { + t.Fatalf("unexpected paid media payload: %#v", media) + } +} + func TestPaidMediaVideoMetadataSurvivesUploadPreparation(t *testing.T) { video := NewInputMediaVideo(FilePath("tests/video.mp4")) paid := NewInputPaidMediaVideo(&video) @@ -1522,6 +1563,186 @@ func TestAPIParityRegressionFixes(t *testing.T) { t.Fatalf("unexpected keywords param in sticker mask position params") } + addSticker := AddStickerConfig{ + UserID: 1, + Name: "stickers", + Sticker: InputSticker{ + Sticker: RequestFile{Name: "sticker", Data: FileID("sticker-file-id")}, + Format: "static", + EmojiList: []string{"\u263a"}, + }, + } + params, err = addSticker.params() + if err != nil { + t.Fatalf("addStickerToSet params error: %v", err) + } + if !strings.Contains(params["sticker"], `"sticker":"sticker-file-id"`) || strings.Contains(params["sticker"], `"Name"`) { + t.Fatalf("expected InputSticker.sticker to serialize as a file string, got %#v", params["sticker"]) + } + if files := addSticker.files(); len(files) != 0 { + t.Fatalf("expected file_id sticker not to be treated as upload file, got %+v", files) + } + + newStickerSet := NewStickerSetConfig{ + UserID: 1, + Name: "stickers", + Title: "Stickers", + Stickers: []InputSticker{ + { + Sticker: RequestFile{Data: FilePath("tests/sticker.webp")}, + Format: "static", + EmojiList: []string{"\u263a"}, + }, + }, + } + params, err = newStickerSet.params() + if err != nil { + t.Fatalf("createNewStickerSet params error: %v", err) + } + if !strings.Contains(params["stickers"], `"sticker":"attach://sticker-0"`) { + t.Fatalf("expected uploaded InputSticker to use attach reference, got %#v", params["stickers"]) + } + stickerFiles := newStickerSet.files() + if len(stickerFiles) != 1 || stickerFiles[0].Name != "sticker-0" { + t.Fatalf("expected sticker upload file field, got %+v", stickerFiles) + } + + shipping := ShippingConfig{ + ShippingQueryID: "shipping-query", + OK: false, + ErrorMessage: "no shipping", + } + params, err = shipping.params() + if err != nil { + t.Fatalf("answerShippingQuery params error: %v", err) + } + if params["ok"] != "false" { + t.Fatalf("expected required ok=false param, got %#v", params) + } + + preCheckout := PreCheckoutConfig{ + PreCheckoutQueryID: "pre-checkout-query", + OK: false, + ErrorMessage: "no payment", + } + params, err = preCheckout.params() + if err != nil { + t.Fatalf("answerPreCheckoutQuery params error: %v", err) + } + if params["ok"] != "false" { + t.Fatalf("expected required ok=false param, got %#v", params) + } + + starSubscription := EditUserStarSubscriptionConfig{ + UserID: 1, + TelegramPaymentChargeID: "charge-id", + IsCanceled: false, + } + params, err = starSubscription.params() + if err != nil { + t.Fatalf("editUserStarSubscription params error: %v", err) + } + if params["is_canceled"] != "false" { + t.Fatalf("expected required is_canceled=false param, got %#v", params) + } + + message := NewMessage(1, "hello") + params, err = message.params() + if err != nil { + t.Fatalf("sendMessage params error: %v", err) + } + for _, key := range []string{"business_connection_id", "reply_parameters", "link_preview_options", "entities"} { + if _, ok := params[key]; ok { + t.Fatalf("unexpected empty %s param in sendMessage: %#v", key, params) + } + } + + message.BusinessConnectionID = "business-connection" + message.ReplyParameters = ReplyParameters{MessageID: 123} + message.LinkPreviewOptions = LinkPreviewOptions{IsDisabled: true} + params, err = message.params() + if err != nil { + t.Fatalf("sendMessage params error: %v", err) + } + if params["business_connection_id"] != "business-connection" || + !strings.Contains(params["reply_parameters"], `"message_id":123`) || + !strings.Contains(params["link_preview_options"], `"is_disabled":true`) { + t.Fatalf("expected explicit optional params in sendMessage: %#v", params) + } + + draft := SendMessageDraftConfig{ + ChatConfig: ChatConfig{ChatID: 1}, + DraftID: 2, + ThinkingPlaceholder: true, + } + params, err = draft.params() + if err != nil { + t.Fatalf("sendMessageDraft params error: %v", err) + } + if text, ok := params["text"]; !ok || text != "" { + t.Fatalf("expected explicit empty text for thinking placeholder, got %#v", params) + } + + removeEmojiStatus := SetUserEmojiStatusConfig{UserID: 1, RemoveStatus: true} + params, err = removeEmojiStatus.params() + if err != nil { + t.Fatalf("setUserEmojiStatus params error: %v", err) + } + if status, ok := params["emoji_status_custom_emoji_id"]; !ok || status != "" { + t.Fatalf("expected explicit empty emoji status, got %#v", params) + } + + dropThumbnail := SetCustomEmojiStickerSetThumbnailConfig{Name: "emoji_set", DropThumbnail: true} + params, err = dropThumbnail.params() + if err != nil { + t.Fatalf("setCustomEmojiStickerSetThumbnail params error: %v", err) + } + if thumbnail, ok := params["custom_emoji_id"]; !ok || thumbnail != "" { + t.Fatalf("expected explicit empty custom_emoji_id, got %#v", params) + } + + removeForumIcon := EditForumTopicConfig{ + BaseForum: BaseForum{ + ChatConfig: ChatConfig{ChatID: 1}, + MessageThreadID: 2, + }, + RemoveIcon: true, + } + params, err = removeForumIcon.params() + if err != nil { + t.Fatalf("editForumTopic params error: %v", err) + } + if icon, ok := params["icon_custom_emoji_id"]; !ok || icon != "" { + t.Fatalf("expected explicit empty icon_custom_emoji_id, got %#v", params) + } + + removeName := SetMyNameConfig{LanguageCode: "en", RemoveName: true} + params, err = removeName.params() + if err != nil { + t.Fatalf("setMyName params error: %v", err) + } + if name, ok := params["name"]; !ok || name != "" { + t.Fatalf("expected explicit empty name, got %#v", params) + } + + removeDescription := SetMyDescriptionConfig{LanguageCode: "en", RemoveDescription: true} + params, err = removeDescription.params() + if err != nil { + t.Fatalf("setMyDescription params error: %v", err) + } + if description, ok := params["description"]; !ok || description != "" { + t.Fatalf("expected explicit empty description, got %#v", params) + } + + removeShortDescription := SetMyShortDescriptionConfig{LanguageCode: "en", RemoveShortDescription: true} + params, err = removeShortDescription.params() + if err != nil { + t.Fatalf("setMyShortDescription params error: %v", err) + } + if description, ok := params["short_description"]; !ok || description != "" { + t.Fatalf("expected explicit empty short_description, got %#v", params) + } + uploadSticker := UploadStickerConfig{ UserID: 42, Sticker: RequestFile{Name: "custom-name", Data: FileBytes{Name: "sticker.webp", Bytes: []byte("sticker")}}, diff --git a/helper_methods.go b/helper_methods.go index a607407b..05191295 100644 --- a/helper_methods.go +++ b/helper_methods.go @@ -1562,6 +1562,17 @@ func NewPaidMedia(chatID, starCount int64, media *InputPaidMedia) PaidMediaConfi } } +// NewPaidMediaGroup creates a new PaidMediaConfig with one or more paid media items. +func NewPaidMediaGroup(chatID, starCount int64, media ...InputPaidMedia) PaidMediaConfig { + return PaidMediaConfig{ + BaseChat: BaseChat{ + ChatConfig: ChatConfig{ChatID: chatID}, + }, + StarCount: starCount, + MediaItems: media, + } +} + // NewInputPaidMediaPhoto creates a new InputPaidMedia for photos. func NewInputPaidMediaPhoto(media *InputMediaPhoto) InputPaidMedia { return InputPaidMedia{ diff --git a/helper_structs.go b/helper_structs.go index 79ec3017..8c52635e 100644 --- a/helper_structs.go +++ b/helper_structs.go @@ -53,7 +53,7 @@ func (chat *BaseChat) params() (Params, error) { if err != nil { return params, err } - err = params.AddInterface("reply_parameters", chat.ReplyParameters) + err = params.AddInterfaceNonZero("reply_parameters", chat.ReplyParameters) if err != nil { return params, err } diff --git a/params.go b/params.go index 565fc771..abb24d66 100644 --- a/params.go +++ b/params.go @@ -38,6 +38,11 @@ func (p Params) AddBool(key string, value bool) { } } +// AddBoolValue adds a boolean value even when it is false. +func (p Params) AddBoolValue(key string, value bool) { + p[key] = strconv.FormatBool(value) +} + // AddBoolPtr adds a value of a bool pointer if it is not nil. func (p Params) AddBoolPtr(key string, value *bool) { if value != nil { @@ -54,7 +59,7 @@ func (p Params) AddNonZeroFloat(key string, value float64) { // AddInterface adds an interface if it is not nil and can be JSON marshalled. func (p Params) AddInterface(key string, value any) error { - if value == nil || (reflect.ValueOf(value).Kind() == reflect.Pointer && reflect.ValueOf(value).IsNil()) { + if isNilParamValue(value) { return nil } @@ -68,6 +73,32 @@ func (p Params) AddInterface(key string, value any) error { return nil } +// AddInterfaceNonZero adds an interface if it is not nil, not zero, and can be JSON marshalled. +func (p Params) AddInterfaceNonZero(key string, value any) error { + if isNilParamValue(value) { + return nil + } + if reflect.ValueOf(value).IsZero() { + return nil + } + + return p.AddInterface(key, value) +} + +func isNilParamValue(value any) bool { + if value == nil { + return true + } + + v := reflect.ValueOf(value) + switch v.Kind() { + case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Pointer, reflect.Slice: + return v.IsNil() + default: + return false + } +} + // AddFirstValid attempts to add the first item that is not a default value. // // For example, AddFirstValid(0, "", "test") would add "test". diff --git a/params_test.go b/params_test.go index 2b34a8ac..71f8b7a3 100644 --- a/params_test.go +++ b/params_test.go @@ -100,6 +100,25 @@ func TestAddInterface(t *testing.T) { assertEq(t, params["test"], "") } +func TestAddInterfaceSkipsNilSlicesAndKeepsEmptySlices(t *testing.T) { + params := make(Params) + + var nilEntities []MessageEntity + err := params.AddInterface("entities", nilEntities) + if err != nil { + t.Fatalf("AddInterface returned error: %v", err) + } + assertLen(t, params, 0) + + emptyEntities := []MessageEntity{} + err = params.AddInterface("entities", emptyEntities) + if err != nil { + t.Fatalf("AddInterface returned error: %v", err) + } + assertLen(t, params, 1) + assertEq(t, params["entities"], "[]") +} + func TestAddFirstValid(t *testing.T) { params := make(Params) params.AddFirstValid("value", 0, "", "test") diff --git a/types.go b/types.go index 6b363b1e..b0f8868b 100644 --- a/types.go +++ b/types.go @@ -5014,6 +5014,32 @@ type InputSticker struct { Keywords []string `json:"keywords"` } +func (sticker InputSticker) MarshalJSON() ([]byte, error) { + out := map[string]any{ + "format": sticker.Format, + "emoji_list": sticker.EmojiList, + } + if sticker.Sticker.Data != nil { + if sticker.Sticker.Data.NeedsUpload() { + name := sticker.Sticker.Name + if name == "" { + name = "sticker" + } + out["sticker"] = "attach://" + name + } else { + out["sticker"] = sticker.Sticker.Data.SendData() + } + } + if sticker.MaskPosition != nil { + out["mask_position"] = sticker.MaskPosition + } + if sticker.Keywords != nil { + out["keywords"] = sticker.Keywords + } + + return json.Marshal(out) +} + // Game represents a game. Use BotFather to create and edit games, their short // names will act as unique identifiers. type Game struct {