From 1c2fc943662b4eb0fca5c923206cc3455a87dbf5 Mon Sep 17 00:00:00 2001 From: Kirill Date: Mon, 13 Oct 2025 01:38:05 +0400 Subject: [PATCH 01/65] Change module path from github.com/go-telegram-bot-api to github.com/kirugan --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 167e5e45..f5a3705a 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,3 @@ -module github.com/go-telegram-bot-api/telegram-bot-api/v5 +module github.com/kirugan/telegram-bot-api/v5 go 1.16 From cc8014b2b58bc365af7ade2f7917d941cd8d7055 Mon Sep 17 00:00:00 2001 From: Kirill Date: Mon, 13 Oct 2025 01:53:42 +0400 Subject: [PATCH 02/65] Update Go version to 1.24 --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index f5a3705a..f793bc6f 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,3 @@ module github.com/kirugan/telegram-bot-api/v5 -go 1.16 +go 1.24 From f913f7481dcde191a896384421b5aa84ba7c3abd Mon Sep 17 00:00:00 2001 From: Kirill Date: Mon, 13 Oct 2025 02:16:09 +0400 Subject: [PATCH 03/65] Update .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index eb7a23b2..6f4da007 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ coverage.out tmp/ book/ +.claude/ From d039d86310ea6dbde18b7c466af5128676844f21 Mon Sep 17 00:00:00 2001 From: Kirill Date: Mon, 13 Oct 2025 23:56:19 +0400 Subject: [PATCH 04/65] Add effect_id to messages --- configs.go | 2 ++ types.go | 13 +++++++++++++ 2 files changed, 15 insertions(+) diff --git a/configs.go b/configs.go index 1831337b..2aef8f58 100644 --- a/configs.go +++ b/configs.go @@ -266,6 +266,7 @@ func (CloseConfig) params() (Params, error) { type BaseChat struct { ChatID int64 // required ChannelUsername string + MessageEffectID string ProtectContent bool ReplyToMessageID int ReplyMarkup interface{} @@ -277,6 +278,7 @@ func (chat *BaseChat) params() (Params, error) { params := make(Params) params.AddFirstValid("chat_id", chat.ChatID, chat.ChannelUsername) + params.AddNonEmpty("message_effect_id", chat.MessageEffectID) params.AddNonZero("reply_to_message_id", chat.ReplyToMessageID) params.AddBool("disable_notification", chat.DisableNotification) params.AddBool("allow_sending_without_reply", chat.AllowSendingWithoutReply) diff --git a/types.go b/types.go index 36c174b8..cafb0f71 100644 --- a/types.go +++ b/types.go @@ -9,6 +9,15 @@ import ( "time" ) +const ( + EffectFire = "5104841245755180586" // 🔥 + EffectThumbsUp = "5107584321108051014" // 👍 + EffectThumbsDown = "5104858069142078462" // 👎 + EffectHeart = "5159385139981059251" // ❤️ + EffectParty = "5046509860389126442" // 🎉 + EffectPoop = "5046589136895476101" // 💩 +) + // APIResponse is a response from the Telegram API with the result // stored raw. type APIResponse struct { @@ -431,6 +440,10 @@ type Message struct { // // optional MediaGroupID string `json:"media_group_id,omitempty"` + // EffectID is the unique identifier of the message effect added to the message; + // + // optional + EffectID string `json:"effect_id,omitempty"` // AuthorSignature is the signature of the post author for messages in channels; // // optional From d54991670539bc66903f50b96b805d4580ba2b19 Mon Sep 17 00:00:00 2001 From: Kirill Date: Tue, 7 Apr 2026 23:29:23 +0400 Subject: [PATCH 05/65] Support "Managed Bots" fuctionality from API 9.6 --- bot.go | 41 ++++++++++++++++++++++++++++++++++ configs.go | 62 +++++++++++++++++++++++++++++++++++++++++++++++++++ helpers.go | 9 ++++++++ types.go | 65 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 177 insertions(+) diff --git a/bot.go b/bot.go index 39037b8d..2275c198 100644 --- a/bot.go +++ b/bot.go @@ -719,6 +719,47 @@ func (bot *BotAPI) GetMyDefaultAdministratorRights(config GetMyDefaultAdministra return rights, err } +// GetManagedBotToken returns the token of a managed bot. +func (bot *BotAPI) GetManagedBotToken(config GetManagedBotTokenConfig) (string, error) { + resp, err := bot.Request(config) + if err != nil { + return "", err + } + + var token string + err = json.Unmarshal(resp.Result, &token) + + return token, err +} + +// ReplaceManagedBotToken revokes the current token of a managed bot +// and generates a new one. Returns the new token. +func (bot *BotAPI) ReplaceManagedBotToken(config ReplaceManagedBotTokenConfig) (string, error) { + resp, err := bot.Request(config) + if err != nil { + return "", err + } + + var token string + err = json.Unmarshal(resp.Result, &token) + + return token, err +} + +// SavePreparedKeyboardButton stores a keyboard button that can be used +// by a user within a Mini App. Returns a PreparedKeyboardButton. +func (bot *BotAPI) SavePreparedKeyboardButton(config SavePreparedKeyboardButtonConfig) (PreparedKeyboardButton, error) { + resp, err := bot.Request(config) + if err != nil { + return PreparedKeyboardButton{}, err + } + + var button PreparedKeyboardButton + err = json.Unmarshal(resp.Result, &button) + + return button, err +} + // EscapeText takes an input text and escape Telegram markup symbols. // In this way we can send a text without being afraid of having to escape the characters manually. // Note that you don't have to include the formatting style in the input text, or it will be escaped too. diff --git a/configs.go b/configs.go index 2aef8f58..f9ef2c82 100644 --- a/configs.go +++ b/configs.go @@ -92,6 +92,10 @@ const ( // UpdateTypeChatMember is when the bot must be an administrator in the chat and must explicitly specify // this update in the list of allowed_updates to receive these updates. UpdateTypeChatMember = "chat_member" + + // UpdateTypeManagedBot is when a new bot was created to be managed by the bot, + // or token or owner of a managed bot was changed. + UpdateTypeManagedBot = "managed_bot" ) // Library errors @@ -2425,6 +2429,64 @@ func (config GetMyDefaultAdministratorRightsConfig) params() (Params, error) { // media and "attach://file-%d-thumb" for thumbnails. // // It is expected to be used in conjunction with prepareInputMediaFile. +// GetManagedBotTokenConfig contains the parameters for the getManagedBotToken method. +type GetManagedBotTokenConfig struct { + // UserID is the user identifier of the managed bot whose token will be returned. + UserID int64 +} + +func (GetManagedBotTokenConfig) method() string { + return "getManagedBotToken" +} + +func (config GetManagedBotTokenConfig) params() (Params, error) { + params := make(Params) + + params.AddNonZero64("user_id", config.UserID) + + return params, nil +} + +// ReplaceManagedBotTokenConfig contains the parameters for the replaceManagedBotToken method. +type ReplaceManagedBotTokenConfig struct { + // UserID is the user identifier of the managed bot whose token will be replaced. + UserID int64 +} + +func (ReplaceManagedBotTokenConfig) method() string { + return "replaceManagedBotToken" +} + +func (config ReplaceManagedBotTokenConfig) params() (Params, error) { + params := make(Params) + + params.AddNonZero64("user_id", config.UserID) + + return params, nil +} + +// SavePreparedKeyboardButtonConfig contains the parameters for the savePreparedKeyboardButton method. +type SavePreparedKeyboardButtonConfig struct { + // UserID is the unique identifier of the target user that can use the button. + UserID int64 + // Button is a KeyboardButton describing the button to be saved. + // The button must be of the type request_users, request_chat, or request_managed_bot. + Button KeyboardButton +} + +func (SavePreparedKeyboardButtonConfig) method() string { + return "savePreparedKeyboardButton" +} + +func (config SavePreparedKeyboardButtonConfig) params() (Params, error) { + params := make(Params) + + params.AddNonZero64("user_id", config.UserID) + err := params.AddInterface("button", config.Button) + + return params, err +} + func prepareInputMediaParam(inputMedia interface{}, idx int) interface{} { switch m := inputMedia.(type) { case InputMediaPhoto: diff --git a/helpers.go b/helpers.go index 3a0e8187..fc902fd4 100644 --- a/helpers.go +++ b/helpers.go @@ -985,3 +985,12 @@ func ValidateWebAppData(token, telegramInitData string) (bool, error) { return true, nil } + +// todo add comment +func NewManagedBotLink(managerUsername, suggestedUsername, suggestedName string) string { + link := fmt.Sprintf("https://t.me/newbot/%s/%s", managerUsername, suggestedUsername) + if suggestedName != "" { + link += "?name=" + url.QueryEscape(suggestedName) + } + return link +} diff --git a/types.go b/types.go index cafb0f71..149bf78a 100644 --- a/types.go +++ b/types.go @@ -123,6 +123,11 @@ type Update struct { // // optional ChatJoinRequest *ChatJoinRequest `json:"chat_join_request,omitempty"` + // ManagedBot is a new bot was created to be managed by the bot, + // or token or owner of a managed bot was changed. + // + // optional + ManagedBot *ManagedBotUpdated `json:"managed_bot,omitempty"` } // SentFrom returns the user who sent an update. Can be nil, if Telegram did not provide information @@ -143,6 +148,8 @@ func (u *Update) SentFrom() *User { return u.ShippingQuery.From case u.PreCheckoutQuery != nil: return u.PreCheckoutQuery.From + case u.ManagedBot != nil: + return &u.ManagedBot.User default: return nil } @@ -226,6 +233,11 @@ type User struct { // // optional SupportsInlineQueries bool `json:"supports_inline_queries,omitempty"` + // CanManageBots is true, if other bots can be created to be controlled by the bot. + // Returned only in getMe. + // + // optional + CanManageBots bool `json:"can_manage_bots,omitempty"` } // String displays a simple text version of a user. @@ -642,6 +654,11 @@ type Message struct { // // optional WebAppData *WebAppData `json:"web_app_data,omitempty"` + // ManagedBotCreated is a service message: user created a bot + // that will be managed by the current bot. + // + // optional + ManagedBotCreated *ManagedBotCreated `json:"managed_bot_created,omitempty"` // ReplyMarkup is the Inline keyboard attached to the message. // login_url buttons are represented as ordinary url buttons. // @@ -1227,6 +1244,31 @@ type VideoChatParticipantsInvited struct { Users []User `json:"users,omitempty"` } +// ManagedBotCreated contains information about the bot that was created +// to be managed by the current bot. +type ManagedBotCreated struct { + // Bot is information about the bot. The bot's token can be fetched + // using the method getManagedBotToken. + Bot User `json:"bot"` +} + +// ManagedBotUpdated contains information about the creation, token update, +// or owner update of a bot that is managed by the current bot. +type ManagedBotUpdated struct { + // User that created the bot. + User User `json:"user"` + // Bot is information about the bot. Token of the bot can be fetched + // using the method getManagedBotToken. + Bot User `json:"bot"` +} + +// PreparedKeyboardButton describes a keyboard button to be used +// by a user of a Mini App. +type PreparedKeyboardButton struct { + // ID is the unique identifier of the keyboard button. + ID string `json:"id"` +} + // UserProfilePhotos contains a set of user profile photos. type UserProfilePhotos struct { // TotalCount total number of profile pictures the target user has @@ -1336,6 +1378,29 @@ type KeyboardButton struct { // // optional WebApp *WebAppInfo `json:"web_app,omitempty"` + // RequestManagedBot if specified, pressing the button will ask the user + // to create and share a bot that will be managed by the current bot. + // Available in private chats only. + // + // optional + RequestManagedBot *KeyboardButtonRequestManagedBot `json:”request_managed_bot,omitempty”` +} + +// KeyboardButtonRequestManagedBot defines the parameters for the creation +// of a managed bot. Information about the created bot will be shared with the +// bot using the update managed_bot and a Message with the field managed_bot_created. +type KeyboardButtonRequestManagedBot struct { + // RequestID is a signed 32-bit identifier of the request. + // Must be unique within the message. + RequestID int `json:"request_id"` + // SuggestedName is the suggested name for the bot. + // + // optional + SuggestedName string `json:"suggested_name,omitempty"` + // SuggestedUsername is the suggested username for the bot. + // + // optional + SuggestedUsername string `json:"suggested_username,omitempty"` } // KeyboardButtonPollType represents type of poll, which is allowed to From 8b02790e7e825b8666bcc65ac6a59f8c1bccd89d Mon Sep 17 00:00:00 2001 From: Kirill Date: Sat, 11 Apr 2026 01:31:22 +0400 Subject: [PATCH 06/65] Full support of API 6.1 --- configs.go | 71 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- types.go | 28 +++++++++++++++------ 2 files changed, 90 insertions(+), 9 deletions(-) diff --git a/configs.go b/configs.go index f9ef2c82..28bfc8c7 100644 --- a/configs.go +++ b/configs.go @@ -2,6 +2,7 @@ package tgbotapi import ( "bytes" + "encoding/json" "fmt" "io" "net/url" @@ -1170,6 +1171,7 @@ type WebhookConfig struct { MaxConnections int AllowedUpdates []string DropPendingUpdates bool + SecretToken string } func (config WebhookConfig) method() string { @@ -1187,6 +1189,7 @@ func (config WebhookConfig) params() (Params, error) { params.AddNonZero("max_connections", config.MaxConnections) err := params.AddInterface("allowed_updates", config.AllowedUpdates) params.AddBool("drop_pending_updates", config.DropPendingUpdates) + params.AddNonEmpty("secret_token", config.SecretToken) return params, err } @@ -1738,7 +1741,7 @@ type InvoiceConfig struct { MaxTipAmount int SuggestedTipAmounts []int StartParameter string - ProviderData string + ProviderData json.RawMessage PhotoURL string PhotoSize int PhotoWidth int @@ -1770,7 +1773,9 @@ func (config InvoiceConfig) params() (Params, error) { params.AddNonZero("max_tip_amount", config.MaxTipAmount) err = params.AddInterface("suggested_tip_amounts", config.SuggestedTipAmounts) params.AddNonEmpty("start_parameter", config.StartParameter) - params.AddNonEmpty("provider_data", config.ProviderData) + if len(config.ProviderData) > 0 { + params["provider_data"] = string(config.ProviderData) + } params.AddNonEmpty("photo_url", config.PhotoURL) params.AddNonZero("photo_size", config.PhotoSize) params.AddNonZero("photo_width", config.PhotoWidth) @@ -1790,6 +1795,68 @@ func (config InvoiceConfig) method() string { return "sendInvoice" } +// InvoiceLinkConfig contains information for createInvoiceLink request. +type InvoiceLinkConfig struct { + Title string // required + Description string // required + Payload string // required + ProviderToken string // required + Currency string // required + Prices []LabeledPrice // required + MaxTipAmount int + SuggestedTipAmounts []int + ProviderData json.RawMessage + PhotoURL string + PhotoSize int + PhotoWidth int + PhotoHeight int + NeedName bool + NeedPhoneNumber bool + NeedEmail bool + NeedShippingAddress bool + SendPhoneNumberToProvider bool + SendEmailToProvider bool + IsFlexible bool +} + +func (config InvoiceLinkConfig) params() (Params, error) { + params := make(Params) + + params["title"] = config.Title + params["description"] = config.Description + params["payload"] = config.Payload + params["provider_token"] = config.ProviderToken + params["currency"] = config.Currency + if err := params.AddInterface("prices", config.Prices); err != nil { + return params, err + } + + params.AddNonZero("max_tip_amount", config.MaxTipAmount) + if err := params.AddInterface("suggested_tip_amounts", config.SuggestedTipAmounts); err != nil { + return params, err + } + if len(config.ProviderData) > 0 { + params["provider_data"] = string(config.ProviderData) + } + params.AddNonEmpty("photo_url", config.PhotoURL) + params.AddNonZero("photo_size", config.PhotoSize) + params.AddNonZero("photo_width", config.PhotoWidth) + params.AddNonZero("photo_height", config.PhotoHeight) + params.AddBool("need_name", config.NeedName) + params.AddBool("need_phone_number", config.NeedPhoneNumber) + params.AddBool("need_email", config.NeedEmail) + params.AddBool("need_shipping_address", config.NeedShippingAddress) + params.AddBool("send_phone_number_to_provider", config.SendPhoneNumberToProvider) + params.AddBool("send_email_to_provider", config.SendEmailToProvider) + params.AddBool("is_flexible", config.IsFlexible) + + return params, nil +} + +func (config InvoiceLinkConfig) method() string { + return "createInvoiceLink" +} + // ShippingConfig contains information for answerShippingQuery request. type ShippingConfig struct { ShippingQueryID string // required diff --git a/types.go b/types.go index 149bf78a..03364ff9 100644 --- a/types.go +++ b/types.go @@ -203,6 +203,10 @@ type User struct { // // optional IsPremium bool `json:"is_premium,omitempty"` + // AddedToAttachmentMenu true, if this user added the bot to the attachment menu + // + // optional + AddedToAttachmentMenu bool `json:"added_to_attachment_menu,omitempty"` // FirstName user's or bot's first name FirstName string `json:"first_name"` // LastName user's or bot's last name @@ -299,6 +303,16 @@ type Chat struct { // // optional Description string `json:"description,omitempty"` + // JoinToSendMessages is true, if users need to join the supergroup before + // they can send messages. Returned only in getChat. + // + // optional + JoinToSendMessages bool `json:"join_to_send_messages,omitempty"` + // JoinByRequest is true, if all users directly joining the supergroup need + // to be approved by supergroup administrators. Returned only in getChat. + // + // optional + JoinByRequest bool `json:"join_by_request,omitempty"` // InviteLink is a chat invite link, for groups, supergroups and channel chats. // Each administrator in a chat generates their own invite links, // so the bot must first generate the link using exportChatInviteLink @@ -893,7 +907,7 @@ type Animation struct { // FileSize file size // // optional - FileSize int `json:"file_size,omitempty"` + FileSize int64 `json:"file_size,omitempty"` } // Audio represents an audio file to be treated as music by the Telegram clients. @@ -926,7 +940,7 @@ type Audio struct { // FileSize file size // // optional - FileSize int `json:"file_size,omitempty"` + FileSize int64 `json:"file_size,omitempty"` // Thumbnail is the album cover to which the music file belongs // // optional @@ -957,7 +971,7 @@ type Document struct { // FileSize file size // // optional - FileSize int `json:"file_size,omitempty"` + FileSize int64 `json:"file_size,omitempty"` } // Video represents a video file. @@ -990,7 +1004,7 @@ type Video struct { // FileSize file size // // optional - FileSize int `json:"file_size,omitempty"` + FileSize int64 `json:"file_size,omitempty"` } // VideoNote object represents a video message. @@ -1032,7 +1046,7 @@ type Voice struct { // FileSize file size // // optional - FileSize int `json:"file_size,omitempty"` + FileSize int64 `json:"file_size,omitempty"` } // Contact represents a phone contact. @@ -1289,7 +1303,7 @@ type File struct { // FileSize file size, if known // // optional - FileSize int `json:"file_size,omitempty"` + FileSize int64 `json:"file_size,omitempty"` // FilePath file path // // optional @@ -1383,7 +1397,7 @@ type KeyboardButton struct { // Available in private chats only. // // optional - RequestManagedBot *KeyboardButtonRequestManagedBot `json:”request_managed_bot,omitempty”` + RequestManagedBot *KeyboardButtonRequestManagedBot `json:"request_managed_bot,omitempty"` } // KeyboardButtonRequestManagedBot defines the parameters for the creation From b8ddd888f3195048692b3be068cb35607bdbb76e Mon Sep 17 00:00:00 2001 From: Kirill Date: Sat, 11 Apr 2026 01:46:02 +0400 Subject: [PATCH 07/65] Full support of API 6.2 --- bot.go | 13 +++++++++++++ configs.go | 36 +++++++++++++++++++++++++++--------- types.go | 27 ++++++++++++++++++++++++--- 3 files changed, 64 insertions(+), 12 deletions(-) diff --git a/bot.go b/bot.go index 2275c198..8337f4cc 100644 --- a/bot.go +++ b/bot.go @@ -646,6 +646,19 @@ func (bot *BotAPI) GetStickerSet(config GetStickerSetConfig) (StickerSet, error) return stickers, err } +// GetCustomEmojiStickers returns information about custom emoji stickers by their identifiers. +func (bot *BotAPI) GetCustomEmojiStickers(config GetCustomEmojiStickersConfig) ([]Sticker, error) { + resp, err := bot.Request(config) + if err != nil { + return nil, err + } + + var stickers []Sticker + err = json.Unmarshal(resp.Result, &stickers) + + return stickers, err +} + // StopPoll stops a poll and returns the result. func (bot *BotAPI) StopPoll(config StopPollConfig) (Poll, error) { resp, err := bot.Request(config) diff --git a/configs.go b/configs.go index 28bfc8c7..2326a5ff 100644 --- a/configs.go +++ b/configs.go @@ -2077,6 +2077,24 @@ func (config GetStickerSetConfig) params() (Params, error) { return params, nil } +// GetCustomEmojiStickersConfig get information about custom emoji stickers +// by their identifiers. +type GetCustomEmojiStickersConfig struct { + CustomEmojiIDs []string +} + +func (config GetCustomEmojiStickersConfig) method() string { + return "getCustomEmojiStickers" +} + +func (config GetCustomEmojiStickersConfig) params() (Params, error) { + params := make(Params) + + err := params.AddInterface("custom_emoji_ids", config.CustomEmojiIDs) + + return params, err +} + // UploadStickerConfig allows you to upload a sticker for use in a set later. type UploadStickerConfig struct { UserID int64 @@ -2106,14 +2124,14 @@ func (config UploadStickerConfig) files() []RequestFile { // // You must set either PNGSticker or TGSSticker. type NewStickerSetConfig struct { - UserID int64 - Name string - Title string - PNGSticker RequestFileData - TGSSticker RequestFileData - Emojis string - ContainsMasks bool - MaskPosition *MaskPosition + UserID int64 + Name string + Title string + PNGSticker RequestFileData + TGSSticker RequestFileData + Emojis string + StickerType string // one of StickerTypeRegular, StickerTypeMask, StickerTypeCustomEmoji + MaskPosition *MaskPosition } func (config NewStickerSetConfig) method() string { @@ -2129,7 +2147,7 @@ func (config NewStickerSetConfig) params() (Params, error) { params["emojis"] = config.Emojis - params.AddBool("contains_masks", config.ContainsMasks) + params.AddNonEmpty("sticker_type", config.StickerType) err := params.AddInterface("mask_position", config.MaskPosition) diff --git a/types.go b/types.go index 03364ff9..50fcf482 100644 --- a/types.go +++ b/types.go @@ -299,6 +299,12 @@ type Chat struct { // // optional HasPrivateForwards bool `json:"has_private_forwards,omitempty"` + // HasRestrictedVoiceAndVideoMessages is true, if the privacy settings of the + // other party restrict sending voice and video note messages in the private + // chat. Returned only in getChat. + // + // optional + HasRestrictedVoiceAndVideoMessages bool `json:"has_restricted_voice_and_video_messages,omitempty"` // Description for groups, supergroups and channel chats // // optional @@ -773,7 +779,8 @@ type MessageEntity struct { // “code” (monowidth string), // “pre” (monowidth block), // “text_link” (for clickable text URLs), - // “text_mention” (for users without usernames) + // “text_mention” (for users without usernames), + // “custom_emoji” (for inline custom emoji stickers) Type string `json:"type"` // Offset in UTF-16 code units to the start of the entity Offset int `json:"offset"` @@ -791,6 +798,11 @@ type MessageEntity struct { // // optional Language string `json:"language,omitempty"` + // CustomEmojiID for "custom_emoji" only, unique identifier of the custom emoji. + // Use getCustomEmojiStickers to get full information about the sticker. + // + // optional + CustomEmojiID string `json:"custom_emoji_id,omitempty"` } // ParseURL attempts to parse a URL contained within a MessageEntity. @@ -2076,6 +2088,13 @@ type InputMediaDocument struct { DisableContentTypeDetection bool `json:"disable_content_type_detection,omitempty"` } +// Sticker type constants. +const ( + StickerTypeRegular = "regular" + StickerTypeMask = "mask" + StickerTypeCustomEmoji = "custom_emoji" +) + // Sticker represents a sticker. type Sticker struct { // FileID is an identifier for this file, which can be used to download or @@ -2085,6 +2104,10 @@ type Sticker struct { // which is supposed to be the same over time and for different bots. // Can't be used to download or reuse the file. FileUniqueID string `json:"file_unique_id"` + // Type of the sticker, currently one of "regular", "mask", "custom_emoji". + // The type of the sticker is independent from its format, + // which is determined by the fields IsAnimated and IsVideo. + Type string `json:"type"` // Width sticker width Width int `json:"width"` // Height sticker height @@ -2140,8 +2163,6 @@ type StickerSet struct { IsAnimated bool `json:"is_animated"` // IsVideo true, if the sticker set contains video stickers IsVideo bool `json:"is_video"` - // ContainsMasks true, if the sticker set contains masks - ContainsMasks bool `json:"contains_masks"` // Stickers list of all set stickers Stickers []Sticker `json:"stickers"` // Thumb is the sticker set thumbnail in the .WEBP or .TGS format From 760cfb41a4dc9f57f1e53e5bdd362fb696fb87f9 Mon Sep 17 00:00:00 2001 From: Kirill Date: Sat, 11 Apr 2026 02:05:44 +0400 Subject: [PATCH 08/65] Full support of API 6.3 --- bot.go | 28 ++++++++++ configs.go | 159 +++++++++++++++++++++++++++++++++++++++++++++++++++++ types.go | 84 ++++++++++++++++++++++++++++ 3 files changed, 271 insertions(+) diff --git a/bot.go b/bot.go index 8337f4cc..aa55f349 100644 --- a/bot.go +++ b/bot.go @@ -659,6 +659,34 @@ func (bot *BotAPI) GetCustomEmojiStickers(config GetCustomEmojiStickersConfig) ( return stickers, err } +// CreateForumTopic creates a topic in a forum supergroup chat and returns +// information about the created topic. +func (bot *BotAPI) CreateForumTopic(config CreateForumTopicConfig) (ForumTopic, error) { + resp, err := bot.Request(config) + if err != nil { + return ForumTopic{}, err + } + + var topic ForumTopic + err = json.Unmarshal(resp.Result, &topic) + + return topic, err +} + +// GetForumTopicIconStickers returns custom emoji stickers, which can be used +// as a forum topic icon by any user. +func (bot *BotAPI) GetForumTopicIconStickers() ([]Sticker, error) { + resp, err := bot.Request(GetForumTopicIconStickersConfig{}) + if err != nil { + return nil, err + } + + var stickers []Sticker + err = json.Unmarshal(resp.Result, &stickers) + + return stickers, err +} + // StopPoll stops a poll and returns the result. func (bot *BotAPI) StopPoll(config StopPollConfig) (Poll, error) { resp, err := bot.Request(config) diff --git a/configs.go b/configs.go index 2326a5ff..9d1a5f39 100644 --- a/configs.go +++ b/configs.go @@ -271,6 +271,7 @@ func (CloseConfig) params() (Params, error) { type BaseChat struct { ChatID int64 // required ChannelUsername string + MessageThreadID int MessageEffectID string ProtectContent bool ReplyToMessageID int @@ -283,6 +284,7 @@ func (chat *BaseChat) params() (Params, error) { params := make(Params) params.AddFirstValid("chat_id", chat.ChatID, chat.ChannelUsername) + params.AddNonZero("message_thread_id", chat.MessageThreadID) params.AddNonEmpty("message_effect_id", chat.MessageEffectID) params.AddNonZero("reply_to_message_id", chat.ReplyToMessageID) params.AddBool("disable_notification", chat.DisableNotification) @@ -1392,6 +1394,7 @@ type PromoteChatMemberConfig struct { CanRestrictMembers bool CanPinMessages bool CanPromoteMembers bool + CanManageTopics bool } func (config PromoteChatMemberConfig) method() string { @@ -1415,6 +1418,7 @@ func (config PromoteChatMemberConfig) params() (Params, error) { params.AddBool("can_restrict_members", config.CanRestrictMembers) params.AddBool("can_pin_messages", config.CanPinMessages) params.AddBool("can_promote_members", config.CanPromoteMembers) + params.AddBool("can_manage_topics", config.CanManageTopics) return params, nil } @@ -1984,6 +1988,159 @@ func (config UnpinAllChatMessagesConfig) params() (Params, error) { return params, nil } +// CreateForumTopicConfig creates a topic in a forum supergroup chat. +// The bot must have the can_manage_topics administrator rights. +// Returns information about the created topic as a ForumTopic object. +type CreateForumTopicConfig struct { + ChatID int64 + SuperGroupUsername string + Name string // required, 1-128 characters + IconColor int + IconCustomEmojiID string +} + +func (config CreateForumTopicConfig) method() string { + return "createForumTopic" +} + +func (config CreateForumTopicConfig) params() (Params, error) { + params := make(Params) + + params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername) + params["name"] = config.Name + params.AddNonZero("icon_color", config.IconColor) + params.AddNonEmpty("icon_custom_emoji_id", config.IconCustomEmojiID) + + return params, nil +} + +// EditForumTopicConfig edits the name and icon of a topic in a forum +// supergroup chat. The bot must have the can_manage_topics administrator +// rights, unless it is the creator of the topic. +type EditForumTopicConfig struct { + ChatID int64 + SuperGroupUsername string + MessageThreadID int // required + Name string + IconCustomEmojiID string +} + +func (config EditForumTopicConfig) method() string { + return "editForumTopic" +} + +func (config EditForumTopicConfig) params() (Params, error) { + params := make(Params) + + params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername) + params.AddNonZero("message_thread_id", config.MessageThreadID) + params.AddNonEmpty("name", config.Name) + params.AddNonEmpty("icon_custom_emoji_id", config.IconCustomEmojiID) + + return params, nil +} + +// CloseForumTopicConfig closes an open topic in a forum supergroup chat. +// The bot must have the can_manage_topics administrator rights, unless it +// is the creator of the topic. +type CloseForumTopicConfig struct { + ChatID int64 + SuperGroupUsername string + MessageThreadID int // required +} + +func (config CloseForumTopicConfig) method() string { + return "closeForumTopic" +} + +func (config CloseForumTopicConfig) params() (Params, error) { + params := make(Params) + + params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername) + params.AddNonZero("message_thread_id", config.MessageThreadID) + + return params, nil +} + +// ReopenForumTopicConfig reopens a closed topic in a forum supergroup chat. +// The bot must have the can_manage_topics administrator rights, unless it +// is the creator of the topic. +type ReopenForumTopicConfig struct { + ChatID int64 + SuperGroupUsername string + MessageThreadID int // required +} + +func (config ReopenForumTopicConfig) method() string { + return "reopenForumTopic" +} + +func (config ReopenForumTopicConfig) params() (Params, error) { + params := make(Params) + + params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername) + params.AddNonZero("message_thread_id", config.MessageThreadID) + + return params, nil +} + +// DeleteForumTopicConfig deletes a forum topic along with all its messages +// in a forum supergroup chat. The bot must have the can_delete_messages +// administrator rights. +type DeleteForumTopicConfig struct { + ChatID int64 + SuperGroupUsername string + MessageThreadID int // required +} + +func (config DeleteForumTopicConfig) method() string { + return "deleteForumTopic" +} + +func (config DeleteForumTopicConfig) params() (Params, error) { + params := make(Params) + + params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername) + params.AddNonZero("message_thread_id", config.MessageThreadID) + + return params, nil +} + +// UnpinAllForumTopicMessagesConfig clears the list of pinned messages in a +// forum topic. The bot must have the can_pin_messages administrator right +// in the supergroup. +type UnpinAllForumTopicMessagesConfig struct { + ChatID int64 + SuperGroupUsername string + MessageThreadID int // required +} + +func (config UnpinAllForumTopicMessagesConfig) method() string { + return "unpinAllForumTopicMessages" +} + +func (config UnpinAllForumTopicMessagesConfig) params() (Params, error) { + params := make(Params) + + params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername) + params.AddNonZero("message_thread_id", config.MessageThreadID) + + return params, nil +} + +// GetForumTopicIconStickersConfig gets custom emoji stickers, which can be +// used as a forum topic icon by any user. Requires no parameters. +// Returns an Array of Sticker objects. +type GetForumTopicIconStickersConfig struct{} + +func (config GetForumTopicIconStickersConfig) method() string { + return "getForumTopicIconStickers" +} + +func (config GetForumTopicIconStickersConfig) params() (Params, error) { + return make(Params), nil +} + // SetChatPhotoConfig allows you to set a group, supergroup, or channel's photo. type SetChatPhotoConfig struct { BaseFile @@ -2317,6 +2474,7 @@ func (config DeleteChatStickerSetConfig) params() (Params, error) { type MediaGroupConfig struct { ChatID int64 ChannelUsername string + MessageThreadID int Media []interface{} DisableNotification bool @@ -2331,6 +2489,7 @@ func (config MediaGroupConfig) params() (Params, error) { params := make(Params) params.AddFirstValid("chat_id", config.ChatID, config.ChannelUsername) + params.AddNonZero("message_thread_id", config.MessageThreadID) params.AddBool("disable_notification", config.DisableNotification) params.AddNonZero("reply_to_message_id", config.ReplyToMessageID) diff --git a/types.go b/types.go index 50fcf482..c243639c 100644 --- a/types.go +++ b/types.go @@ -286,6 +286,20 @@ type Chat struct { // // optional LastName string `json:"last_name,omitempty"` + // IsForum is true, if the supergroup chat is a forum (has topics enabled) + // + // optional + IsForum bool `json:"is_forum,omitempty"` + // ActiveUsernames is a list of all active chat usernames; for private chats, + // supergroups and channels. Returned only in getChat. + // + // optional + ActiveUsernames []string `json:"active_usernames,omitempty"` + // EmojiStatusCustomEmojiID is the custom emoji identifier of emoji status of + // the other party in a private chat. Returned only in getChat. + // + // optional + EmojiStatusCustomEmojiID string `json:"emoji_status_custom_emoji_id,omitempty"` // Photo is a chat photo Photo *ChatPhoto `json:"photo"` // Bio is the bio of the other party in a private chat. Returned only in @@ -402,6 +416,15 @@ func (c Chat) ChatConfig() ChatConfig { type Message struct { // MessageID is a unique message identifier inside this chat MessageID int `json:"message_id"` + // MessageThreadID is the unique identifier of a message thread to which + // the message belongs; for supergroups only. + // + // optional + MessageThreadID int `json:"message_thread_id,omitempty"` + // IsTopicMessage is true, if the message is sent to a forum topic. + // + // optional + IsTopicMessage bool `json:"is_topic_message,omitempty"` // From is a sender, empty for messages sent to channels; // // optional @@ -674,6 +697,18 @@ type Message struct { // // optional WebAppData *WebAppData `json:"web_app_data,omitempty"` + // ForumTopicCreated is a service message: forum topic created + // + // optional + ForumTopicCreated *ForumTopicCreated `json:"forum_topic_created,omitempty"` + // ForumTopicClosed is a service message: forum topic closed + // + // optional + ForumTopicClosed *ForumTopicClosed `json:"forum_topic_closed,omitempty"` + // ForumTopicReopened is a service message: forum topic reopened + // + // optional + ForumTopicReopened *ForumTopicReopened `json:"forum_topic_reopened,omitempty"` // ManagedBotCreated is a service message: user created a bot // that will be managed by the current bot. // @@ -1695,6 +1730,7 @@ type ChatAdministratorRights struct { CanPostMessages bool `json:"can_post_messages"` CanEditMessages bool `json:"can_edit_messages"` CanPinMessages bool `json:"can_pin_messages"` + CanManageTopics bool `json:"can_manage_topics"` } // ChatMember contains information about one member of a chat. @@ -1787,6 +1823,12 @@ type ChatMember struct { // // optional CanPinMessages bool `json:"can_pin_messages,omitempty"` + // CanManageTopics administrators and restricted only. + // True, if the user is allowed to create, rename, close, and reopen + // forum topics; supergroups only. + // + // optional + CanManageTopics bool `json:"can_manage_topics,omitempty"` // IsMember is true, if the user is a member of the chat at the moment of // the request IsMember bool `json:"is_member"` @@ -1910,6 +1952,11 @@ type ChatPermissions struct { // // optional CanPinMessages bool `json:"can_pin_messages,omitempty"` + // CanManageTopics is true, if the user is allowed to create forum topics. + // If omitted defaults to the value of can_pin_messages + // + // optional + CanManageTopics bool `json:"can_manage_topics,omitempty"` } // ChatLocation represents a location to which a chat is connected. @@ -1922,6 +1969,43 @@ type ChatLocation struct { Address string `json:"address"` } +// ForumTopic represents a forum topic. +type ForumTopic struct { + // MessageThreadID is the unique identifier of the forum topic + MessageThreadID int `json:"message_thread_id"` + // Name of the topic + Name string `json:"name"` + // IconColor is the color of the topic icon in RGB format + IconColor int `json:"icon_color"` + // IconCustomEmojiID is the unique identifier of the custom emoji shown + // as the topic icon. + // + // optional + IconCustomEmojiID string `json:"icon_custom_emoji_id,omitempty"` +} + +// ForumTopicCreated represents a service message about a new forum topic +// created in the chat. +type ForumTopicCreated struct { + // Name of the topic + Name string `json:"name"` + // IconColor is the color of the topic icon in RGB format + IconColor int `json:"icon_color"` + // IconCustomEmojiID is the unique identifier of the custom emoji shown + // as the topic icon. + // + // optional + IconCustomEmojiID string `json:"icon_custom_emoji_id,omitempty"` +} + +// ForumTopicClosed represents a service message about a forum topic closed +// in the chat. Currently holds no information. +type ForumTopicClosed struct{} + +// ForumTopicReopened represents a service message about a forum topic +// reopened in the chat. Currently holds no information. +type ForumTopicReopened struct{} + // BotCommand represents a bot command. type BotCommand struct { // Command text of the command, 1-32 characters. From 710b2d591e0d65acaa6c1de8108825901377ff23 Mon Sep 17 00:00:00 2001 From: Kirill Date: Sat, 11 Apr 2026 02:14:54 +0400 Subject: [PATCH 09/65] Full support of API 6.4 --- configs.go | 106 +++++++++++++++++++++++++++++++++++++++++++++++++++++ helpers.go | 2 +- types.go | 75 +++++++++++++++++++++++++++++++++++++ 3 files changed, 182 insertions(+), 1 deletion(-) diff --git a/configs.go b/configs.go index 9d1a5f39..5f9ff538 100644 --- a/configs.go +++ b/configs.go @@ -418,6 +418,7 @@ type PhotoConfig struct { Caption string ParseMode string CaptionEntities []MessageEntity + HasSpoiler bool } func (config PhotoConfig) params() (Params, error) { @@ -428,6 +429,7 @@ func (config PhotoConfig) params() (Params, error) { params.AddNonEmpty("caption", config.Caption) params.AddNonEmpty("parse_mode", config.ParseMode) + params.AddBool("has_spoiler", config.HasSpoiler) err = params.AddInterface("caption_entities", config.CaptionEntities) return params, err @@ -570,6 +572,7 @@ type VideoConfig struct { ParseMode string CaptionEntities []MessageEntity SupportsStreaming bool + HasSpoiler bool } func (config VideoConfig) params() (Params, error) { @@ -582,6 +585,7 @@ func (config VideoConfig) params() (Params, error) { params.AddNonEmpty("caption", config.Caption) params.AddNonEmpty("parse_mode", config.ParseMode) params.AddBool("supports_streaming", config.SupportsStreaming) + params.AddBool("has_spoiler", config.HasSpoiler) err = params.AddInterface("caption_entities", config.CaptionEntities) return params, err @@ -615,6 +619,7 @@ type AnimationConfig struct { Caption string ParseMode string CaptionEntities []MessageEntity + HasSpoiler bool } func (config AnimationConfig) params() (Params, error) { @@ -626,6 +631,7 @@ func (config AnimationConfig) params() (Params, error) { params.AddNonZero("duration", config.Duration) params.AddNonEmpty("caption", config.Caption) params.AddNonEmpty("parse_mode", config.ParseMode) + params.AddBool("has_spoiler", config.HasSpoiler) err = params.AddInterface("caption_entities", config.CaptionEntities) return params, err @@ -2141,6 +2147,106 @@ func (config GetForumTopicIconStickersConfig) params() (Params, error) { return make(Params), nil } +// EditGeneralForumTopicConfig edits the name of the 'General' topic in a +// forum supergroup chat. The bot must have the can_manage_topics +// administrator rights. +type EditGeneralForumTopicConfig struct { + ChatID int64 + SuperGroupUsername string + Name string // required, 1-128 characters +} + +func (config EditGeneralForumTopicConfig) method() string { + return "editGeneralForumTopic" +} + +func (config EditGeneralForumTopicConfig) params() (Params, error) { + params := make(Params) + + params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername) + params["name"] = config.Name + + return params, nil +} + +// CloseGeneralForumTopicConfig closes an open 'General' topic in a forum +// supergroup chat. The bot must have the can_manage_topics administrator rights. +type CloseGeneralForumTopicConfig struct { + ChatID int64 + SuperGroupUsername string +} + +func (config CloseGeneralForumTopicConfig) method() string { + return "closeGeneralForumTopic" +} + +func (config CloseGeneralForumTopicConfig) params() (Params, error) { + params := make(Params) + + params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername) + + return params, nil +} + +// ReopenGeneralForumTopicConfig reopens a closed 'General' topic in a forum +// supergroup chat. The bot must have the can_manage_topics administrator +// rights. The topic will be automatically unhidden if it was hidden. +type ReopenGeneralForumTopicConfig struct { + ChatID int64 + SuperGroupUsername string +} + +func (config ReopenGeneralForumTopicConfig) method() string { + return "reopenGeneralForumTopic" +} + +func (config ReopenGeneralForumTopicConfig) params() (Params, error) { + params := make(Params) + + params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername) + + return params, nil +} + +// HideGeneralForumTopicConfig hides the 'General' topic in a forum supergroup +// chat. The bot must have the can_manage_topics administrator rights. The +// topic will be automatically closed if it was open. +type HideGeneralForumTopicConfig struct { + ChatID int64 + SuperGroupUsername string +} + +func (config HideGeneralForumTopicConfig) method() string { + return "hideGeneralForumTopic" +} + +func (config HideGeneralForumTopicConfig) params() (Params, error) { + params := make(Params) + + params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername) + + return params, nil +} + +// UnhideGeneralForumTopicConfig unhides the 'General' topic in a forum +// supergroup chat. The bot must have the can_manage_topics administrator rights. +type UnhideGeneralForumTopicConfig struct { + ChatID int64 + SuperGroupUsername string +} + +func (config UnhideGeneralForumTopicConfig) method() string { + return "unhideGeneralForumTopic" +} + +func (config UnhideGeneralForumTopicConfig) params() (Params, error) { + params := make(Params) + + params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername) + + return params, nil +} + // SetChatPhotoConfig allows you to set a group, supergroup, or channel's photo. type SetChatPhotoConfig struct { BaseFile diff --git a/helpers.go b/helpers.go index fc902fd4..b85b7725 100644 --- a/helpers.go +++ b/helpers.go @@ -186,7 +186,7 @@ func NewMediaGroup(chatID int64, files []interface{}) MediaGroupConfig { // NewInputMediaPhoto creates a new InputMediaPhoto. func NewInputMediaPhoto(media RequestFileData) InputMediaPhoto { return InputMediaPhoto{ - BaseInputMedia{ + BaseInputMedia: BaseInputMedia{ Type: "photo", Media: media, }, diff --git a/types.go b/types.go index c243639c..168eb4e7 100644 --- a/types.go +++ b/types.go @@ -300,6 +300,17 @@ type Chat struct { // // optional EmojiStatusCustomEmojiID string `json:"emoji_status_custom_emoji_id,omitempty"` + // HasHiddenMembers is true, if non-administrators can only see bots and + // administrators in the chat. Returned only in getChat. + // + // optional + HasHiddenMembers bool `json:"has_hidden_members,omitempty"` + // HasAggressiveAntiSpamEnabled is true, if aggressive anti-spam checks are + // enabled in the supergroup. Visible only to chat administrators. Returned + // only in getChat. + // + // optional + HasAggressiveAntiSpamEnabled bool `json:"has_aggressive_anti_spam_enabled,omitempty"` // Photo is a chat photo Photo *ChatPhoto `json:"photo"` // Bio is the bio of the other party in a private chat. Returned only in @@ -701,6 +712,10 @@ type Message struct { // // optional ForumTopicCreated *ForumTopicCreated `json:"forum_topic_created,omitempty"` + // ForumTopicEdited is a service message: forum topic edited + // + // optional + ForumTopicEdited *ForumTopicEdited `json:"forum_topic_edited,omitempty"` // ForumTopicClosed is a service message: forum topic closed // // optional @@ -709,6 +724,23 @@ type Message struct { // // optional ForumTopicReopened *ForumTopicReopened `json:"forum_topic_reopened,omitempty"` + // GeneralForumTopicHidden is a service message: the General forum topic hidden + // + // optional + GeneralForumTopicHidden *GeneralForumTopicHidden `json:"general_forum_topic_hidden,omitempty"` + // GeneralForumTopicUnhidden is a service message: the General forum topic unhidden + // + // optional + GeneralForumTopicUnhidden *GeneralForumTopicUnhidden `json:"general_forum_topic_unhidden,omitempty"` + // WriteAccessAllowed is a service message: the user allowed the bot added to + // the attachment menu to write messages + // + // optional + WriteAccessAllowed *WriteAccessAllowed `json:"write_access_allowed,omitempty"` + // HasMediaSpoiler is true, if the message media is covered by a spoiler animation + // + // optional + HasMediaSpoiler bool `json:"has_media_spoiler,omitempty"` // ManagedBotCreated is a service message: user created a bot // that will be managed by the current bot. // @@ -1375,6 +1407,12 @@ type WebAppInfo struct { type ReplyKeyboardMarkup struct { // Keyboard is an array of button rows, each represented by an Array of KeyboardButton objects Keyboard [][]KeyboardButton `json:"keyboard"` + // IsPersistent requests clients to always show the keyboard when the regular + // keyboard is hidden. Defaults to false, in which case the custom keyboard + // can be hidden and opened with a keyboard icon. + // + // optional + IsPersistent bool `json:"is_persistent,omitempty"` // ResizeKeyboard requests clients to resize the keyboard vertically for optimal fit // (e.g., make the keyboard smaller if there are just two rows of buttons). // Defaults to false, in which case the custom keyboard @@ -1998,6 +2036,19 @@ type ForumTopicCreated struct { IconCustomEmojiID string `json:"icon_custom_emoji_id,omitempty"` } +// ForumTopicEdited represents a service message about an edited forum topic. +type ForumTopicEdited struct { + // Name is the new name of the topic, if it was edited. + // + // optional + Name string `json:"name,omitempty"` + // IconCustomEmojiID is the new identifier of the custom emoji shown as the + // topic icon, if it was edited; an empty string if the icon was removed. + // + // optional + IconCustomEmojiID string `json:"icon_custom_emoji_id,omitempty"` +} + // ForumTopicClosed represents a service message about a forum topic closed // in the chat. Currently holds no information. type ForumTopicClosed struct{} @@ -2006,6 +2057,18 @@ type ForumTopicClosed struct{} // reopened in the chat. Currently holds no information. type ForumTopicReopened struct{} +// GeneralForumTopicHidden represents a service message about General forum +// topic hidden in the chat. Currently holds no information. +type GeneralForumTopicHidden struct{} + +// GeneralForumTopicUnhidden represents a service message about General forum +// topic unhidden in the chat. Currently holds no information. +type GeneralForumTopicUnhidden struct{} + +// WriteAccessAllowed represents a service message about a user allowing a bot +// added to the attachment menu to write messages. Currently holds no information. +type WriteAccessAllowed struct{} + // BotCommand represents a bot command. type BotCommand struct { // Command text of the command, 1-32 characters. @@ -2084,6 +2147,10 @@ type BaseInputMedia struct { // InputMediaPhoto is a photo to send as part of a media group. type InputMediaPhoto struct { BaseInputMedia + // HasSpoiler pass True if the photo needs to be covered with a spoiler animation. + // + // optional + HasSpoiler bool `json:"has_spoiler,omitempty"` } // InputMediaVideo is a video to send as part of a media group. @@ -2110,6 +2177,10 @@ type InputMediaVideo struct { // // optional SupportsStreaming bool `json:"supports_streaming,omitempty"` + // HasSpoiler pass True if the video needs to be covered with a spoiler animation. + // + // optional + HasSpoiler bool `json:"has_spoiler,omitempty"` } // InputMediaAnimation is an animation to send as part of a media group. @@ -2132,6 +2203,10 @@ type InputMediaAnimation struct { // // optional Duration int `json:"duration,omitempty"` + // HasSpoiler pass True if the animation needs to be covered with a spoiler animation. + // + // optional + HasSpoiler bool `json:"has_spoiler,omitempty"` } // InputMediaAudio is an audio to send as part of a media group. From 97d3bc5057fe7e922afab1493e09564fcc47810c Mon Sep 17 00:00:00 2001 From: Kirill Date: Sat, 11 Apr 2026 02:21:48 +0400 Subject: [PATCH 10/65] Full support of API 6.5 --- configs.go | 10 ++- types.go | 174 ++++++++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 166 insertions(+), 18 deletions(-) diff --git a/configs.go b/configs.go index 5f9ff538..6821822d 100644 --- a/configs.go +++ b/configs.go @@ -1366,8 +1366,9 @@ type KickChatMemberConfig = BanChatMemberConfig // RestrictChatMemberConfig contains fields to restrict members of chat type RestrictChatMemberConfig struct { ChatMemberConfig - UntilDate int64 - Permissions *ChatPermissions + UntilDate int64 + Permissions *ChatPermissions + UseIndependentChatPermissions bool } func (config RestrictChatMemberConfig) method() string { @@ -1381,6 +1382,7 @@ func (config RestrictChatMemberConfig) params() (Params, error) { params.AddNonZero64("user_id", config.UserID) err := params.AddInterface("permissions", config.Permissions) + params.AddBool("use_independent_chat_permissions", config.UseIndependentChatPermissions) params.AddNonZero64("until_date", config.UntilDate) return params, err @@ -1544,7 +1546,8 @@ func (ChatAdministratorsConfig) method() string { // restrict members. type SetChatPermissionsConfig struct { ChatConfig - Permissions *ChatPermissions + Permissions *ChatPermissions + UseIndependentChatPermissions bool } func (SetChatPermissionsConfig) method() string { @@ -1556,6 +1559,7 @@ func (config SetChatPermissionsConfig) params() (Params, error) { params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername) err := params.AddInterface("permissions", config.Permissions) + params.AddBool("use_independent_chat_permissions", config.UseIndependentChatPermissions) return params, err } diff --git a/types.go b/types.go index 168eb4e7..0c389123 100644 --- a/types.go +++ b/types.go @@ -708,6 +708,14 @@ type Message struct { // // optional WebAppData *WebAppData `json:"web_app_data,omitempty"` + // UserShared is a service message: a user was shared with the bot. + // + // optional + UserShared *UserShared `json:"user_shared,omitempty"` + // ChatShared is a service message: a chat was shared with the bot. + // + // optional + ChatShared *ChatShared `json:"chat_shared,omitempty"` // ForumTopicCreated is a service message: forum topic created // // optional @@ -1454,6 +1462,18 @@ type KeyboardButton struct { // Text of the button. If none of the optional fields are used, // it will be sent as a message when the button is pressed. Text string `json:"text"` + // RequestUser if specified, pressing the button will open a list of + // suitable users. Identifier of the selected user will be shared with the + // bot in a "user_shared" service message. Available in private chats only. + // + // optional + RequestUser *KeyboardButtonRequestUser `json:"request_user,omitempty"` + // RequestChat if specified, pressing the button will open a list of + // suitable chats. Tapping on a chat will send its identifier to the bot in + // a "chat_shared" service message. Available in private chats only. + // + // optional + RequestChat *KeyboardButtonRequestChat `json:"request_chat,omitempty"` // RequestContact if True, the user's phone number will be sent // as a contact when the button is pressed. // Available in private chats only. @@ -1485,6 +1505,88 @@ type KeyboardButton struct { RequestManagedBot *KeyboardButtonRequestManagedBot `json:"request_managed_bot,omitempty"` } +// KeyboardButtonRequestUser defines the criteria used to request a suitable +// user. The identifier of the selected user will be shared with the bot when +// the corresponding button is pressed. +type KeyboardButtonRequestUser struct { + // RequestID is a signed 32-bit identifier of the request, which will be + // received back in the UserShared object. Must be unique within the message. + RequestID int `json:"request_id"` + // UserIsBot pass True to request a bot, pass False to request a regular + // user. If not specified, no additional restrictions are applied. + // + // optional + UserIsBot *bool `json:"user_is_bot,omitempty"` + // UserIsPremium pass True to request a premium user, pass False to request + // a non-premium user. If not specified, no additional restrictions are applied. + // + // optional + UserIsPremium *bool `json:"user_is_premium,omitempty"` +} + +// KeyboardButtonRequestChat defines the criteria used to request a suitable +// chat. The identifier of the selected chat will be shared with the bot when +// the corresponding button is pressed. +type KeyboardButtonRequestChat struct { + // RequestID is a signed 32-bit identifier of the request, which will be + // received back in the ChatShared object. Must be unique within the message. + RequestID int `json:"request_id"` + // ChatIsChannel pass True to request a channel chat, pass False to request + // a group or a supergroup chat. + ChatIsChannel bool `json:"chat_is_channel"` + // ChatIsForum pass True to request a forum supergroup, pass False to + // request a non-forum chat. If not specified, no additional restrictions + // are applied. + // + // optional + ChatIsForum *bool `json:"chat_is_forum,omitempty"` + // ChatHasUsername pass True to request a supergroup or a channel with a + // username, pass False to request a chat without a username. If not + // specified, no additional restrictions are applied. + // + // optional + ChatHasUsername *bool `json:"chat_has_username,omitempty"` + // ChatIsCreated pass True to request a chat owned by the user. Otherwise, + // no additional restrictions are applied. + // + // optional + ChatIsCreated bool `json:"chat_is_created,omitempty"` + // UserAdministratorRights is the required administrator rights of the + // user in the chat. If not specified, no additional restrictions are applied. + // + // optional + UserAdministratorRights *ChatAdministratorRights `json:"user_administrator_rights,omitempty"` + // BotAdministratorRights is the required administrator rights of the bot + // in the chat. The rights must be a subset of UserAdministratorRights. + // If not specified, no additional restrictions are applied. + // + // optional + BotAdministratorRights *ChatAdministratorRights `json:"bot_administrator_rights,omitempty"` + // BotIsMember pass True to request a chat with the bot as a member. + // Otherwise, no additional restrictions are applied. + // + // optional + BotIsMember bool `json:"bot_is_member,omitempty"` +} + +// UserShared contains information about the user whose identifier was shared +// with the bot using a KeyboardButtonRequestUser button. +type UserShared struct { + // RequestID is the identifier of the request. + RequestID int `json:"request_id"` + // UserID is the identifier of the shared user. + UserID int64 `json:"user_id"` +} + +// ChatShared contains information about the chat whose identifier was shared +// with the bot using a KeyboardButtonRequestChat button. +type ChatShared struct { + // RequestID is the identifier of the request. + RequestID int `json:"request_id"` + // ChatID is the identifier of the shared chat. + ChatID int64 `json:"chat_id"` +} + // KeyboardButtonRequestManagedBot defines the parameters for the creation // of a managed bot. Information about the created bot will be shared with the // bot using the update managed_bot and a Message with the field managed_bot_created. @@ -1870,23 +1972,43 @@ type ChatMember struct { // IsMember is true, if the user is a member of the chat at the moment of // the request IsMember bool `json:"is_member"` - // CanSendMessages + // CanSendMessages restricted only. + // True, if the user is allowed to send text messages, contacts, + // invoices, locations and venues. // // optional CanSendMessages bool `json:"can_send_messages,omitempty"` - // CanSendMediaMessages restricted only. - // True, if the user is allowed to send text messages, contacts, locations and venues + // CanSendAudios restricted only. True, if the user is allowed to send audios. + // + // optional + CanSendAudios bool `json:"can_send_audios,omitempty"` + // CanSendDocuments restricted only. True, if the user is allowed to send documents. + // + // optional + CanSendDocuments bool `json:"can_send_documents,omitempty"` + // CanSendPhotos restricted only. True, if the user is allowed to send photos. + // + // optional + CanSendPhotos bool `json:"can_send_photos,omitempty"` + // CanSendVideos restricted only. True, if the user is allowed to send videos. // // optional - CanSendMediaMessages bool `json:"can_send_media_messages,omitempty"` + CanSendVideos bool `json:"can_send_videos,omitempty"` + // CanSendVideoNotes restricted only. True, if the user is allowed to send video notes. + // + // optional + CanSendVideoNotes bool `json:"can_send_video_notes,omitempty"` + // CanSendVoiceNotes restricted only. True, if the user is allowed to send voice notes. + // + // optional + CanSendVoiceNotes bool `json:"can_send_voice_notes,omitempty"` // CanSendPolls restricted only. // True, if the user is allowed to send polls // // optional CanSendPolls bool `json:"can_send_polls,omitempty"` // CanSendOtherMessages restricted only. - // True, if the user is allowed to send audios, documents, - // photos, videos, video notes and voice notes. + // True, if the user is allowed to send animations, games, stickers and use inline bots. // // optional CanSendOtherMessages bool `json:"can_send_other_messages,omitempty"` @@ -1934,6 +2056,11 @@ type ChatJoinRequest struct { Chat Chat `json:"chat"` // User that sent the join request. From User `json:"from"` + // UserChatID is the identifier of a private chat with the user who sent the + // join request. The bot can use this identifier for 5 minutes to send messages + // until the join request is processed, assuming no other administrator + // contacted the user. + UserChatID int64 `json:"user_chat_id"` // Date the request was sent in Unix time. Date int `json:"date"` // Bio of the user. @@ -1950,28 +2077,45 @@ type ChatJoinRequest struct { // allowed to take in a chat. All fields are optional. type ChatPermissions struct { // CanSendMessages is true, if the user is allowed to send text messages, - // contacts, locations and venues + // contacts, invoices, locations and venues // // optional CanSendMessages bool `json:"can_send_messages,omitempty"` - // CanSendMediaMessages is true, if the user is allowed to send audios, - // documents, photos, videos, video notes and voice notes, implies - // can_send_messages + // CanSendAudios is true, if the user is allowed to send audios. + // + // optional + CanSendAudios bool `json:"can_send_audios,omitempty"` + // CanSendDocuments is true, if the user is allowed to send documents. + // + // optional + CanSendDocuments bool `json:"can_send_documents,omitempty"` + // CanSendPhotos is true, if the user is allowed to send photos. + // + // optional + CanSendPhotos bool `json:"can_send_photos,omitempty"` + // CanSendVideos is true, if the user is allowed to send videos. + // + // optional + CanSendVideos bool `json:"can_send_videos,omitempty"` + // CanSendVideoNotes is true, if the user is allowed to send video notes. + // + // optional + CanSendVideoNotes bool `json:"can_send_video_notes,omitempty"` + // CanSendVoiceNotes is true, if the user is allowed to send voice notes. // // optional - CanSendMediaMessages bool `json:"can_send_media_messages,omitempty"` - // CanSendPolls is true, if the user is allowed to send polls, implies - // can_send_messages + CanSendVoiceNotes bool `json:"can_send_voice_notes,omitempty"` + // CanSendPolls is true, if the user is allowed to send polls. // // optional CanSendPolls bool `json:"can_send_polls,omitempty"` // CanSendOtherMessages is true, if the user is allowed to send animations, - // games, stickers and use inline bots, implies can_send_media_messages + // games, stickers and use inline bots. // // optional CanSendOtherMessages bool `json:"can_send_other_messages,omitempty"` // CanAddWebPagePreviews is true, if the user is allowed to add web page - // previews to their messages, implies can_send_media_messages + // previews to their messages. // // optional CanAddWebPagePreviews bool `json:"can_add_web_page_previews,omitempty"` From 53bc57e815e4c5a13c2dd11605ab81b8b65f8897 Mon Sep 17 00:00:00 2001 From: Kirill Date: Sat, 11 Apr 2026 02:54:37 +0400 Subject: [PATCH 11/65] Add new method AddAny() to Params struct --- params.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/params.go b/params.go index 134f85e4..df93967e 100644 --- a/params.go +++ b/params.go @@ -60,6 +60,11 @@ func (p Params) AddInterface(key string, value interface{}) error { return nil } +// AddAny adds a value of any type by JSON-marshalling it. +func (p Params) AddAny(key string, value any) error { + return p.AddInterface(key, value) +} + // AddFirstValid attempts to add the first item that is not a default value. // // For example, AddFirstValid(0, "", "test") would add "test". From d8147c306a545e60af7534e0b3b9766e320e7f37 Mon Sep 17 00:00:00 2001 From: Kirill Date: Sat, 11 Apr 2026 03:14:56 +0400 Subject: [PATCH 12/65] Full support of API 6.6 --- bot.go | 27 ++++ bot_test.go | 4 +- configs.go | 417 ++++++++++++++++++++++++++++++++++++------------ helpers.go | 12 +- helpers_test.go | 6 +- types.go | 120 ++++++++++---- types_test.go | 2 +- 7 files changed, 443 insertions(+), 145 deletions(-) diff --git a/bot.go b/bot.go index aa55f349..b202c9eb 100644 --- a/bot.go +++ b/bot.go @@ -687,6 +687,33 @@ func (bot *BotAPI) GetForumTopicIconStickers() ([]Sticker, error) { return stickers, err } +// GetMyDescription returns the current bot description for the given user language. +func (bot *BotAPI) GetMyDescription(config GetMyDescriptionConfig) (BotDescription, error) { + resp, err := bot.Request(config) + if err != nil { + return BotDescription{}, err + } + + var desc BotDescription + err = json.Unmarshal(resp.Result, &desc) + + return desc, err +} + +// GetMyShortDescription returns the current bot short description for the +// given user language. +func (bot *BotAPI) GetMyShortDescription(config GetMyShortDescriptionConfig) (BotShortDescription, error) { + resp, err := bot.Request(config) + if err != nil { + return BotShortDescription{}, err + } + + var desc BotShortDescription + err = json.Unmarshal(resp.Result, &desc) + + return desc, err +} + // StopPoll stops a poll and returns the result. func (bot *BotAPI) StopPoll(config StopPollConfig) (Poll, error) { resp, err := bot.Request(config) diff --git a/bot_test.go b/bot_test.go index 7abd790a..687b06ce 100644 --- a/bot_test.go +++ b/bot_test.go @@ -246,11 +246,11 @@ func TestSendWithNewDocument(t *testing.T) { } } -func TestSendWithNewDocumentAndThumb(t *testing.T) { +func TestSendWithNewDocumentAndThumbnail(t *testing.T) { bot, _ := getBot(t) msg := NewDocument(ChatID, FilePath("tests/voice.ogg")) - msg.Thumb = FilePath("tests/image.jpg") + msg.Thumbnail = FilePath("tests/image.jpg") _, err := bot.Send(msg) if err != nil { diff --git a/configs.go b/configs.go index 6821822d..993787ab 100644 --- a/configs.go +++ b/configs.go @@ -414,7 +414,7 @@ func (config CopyMessageConfig) method() string { // PhotoConfig contains information about a SendPhoto request. type PhotoConfig struct { BaseFile - Thumb RequestFileData + Thumbnail RequestFileData Caption string ParseMode string CaptionEntities []MessageEntity @@ -445,10 +445,10 @@ func (config PhotoConfig) files() []RequestFile { Data: config.File, }} - if config.Thumb != nil { + if config.Thumbnail != nil { files = append(files, RequestFile{ - Name: "thumb", - Data: config.Thumb, + Name: "thumbnail", + Data: config.Thumbnail, }) } @@ -458,7 +458,7 @@ func (config PhotoConfig) files() []RequestFile { // AudioConfig contains information about a SendAudio request. type AudioConfig struct { BaseFile - Thumb RequestFileData + Thumbnail RequestFileData Caption string ParseMode string CaptionEntities []MessageEntity @@ -493,10 +493,10 @@ func (config AudioConfig) files() []RequestFile { Data: config.File, }} - if config.Thumb != nil { + if config.Thumbnail != nil { files = append(files, RequestFile{ - Name: "thumb", - Data: config.Thumb, + Name: "thumbnail", + Data: config.Thumbnail, }) } @@ -506,7 +506,7 @@ func (config AudioConfig) files() []RequestFile { // DocumentConfig contains information about a SendDocument request. type DocumentConfig struct { BaseFile - Thumb RequestFileData + Thumbnail RequestFileData Caption string ParseMode string CaptionEntities []MessageEntity @@ -533,10 +533,10 @@ func (config DocumentConfig) files() []RequestFile { Data: config.File, }} - if config.Thumb != nil { + if config.Thumbnail != nil { files = append(files, RequestFile{ - Name: "thumb", - Data: config.Thumb, + Name: "thumbnail", + Data: config.Thumbnail, }) } @@ -546,10 +546,19 @@ func (config DocumentConfig) files() []RequestFile { // StickerConfig contains information about a SendSticker request. type StickerConfig struct { BaseFile + // Emoji associated with the sticker; only for just uploaded stickers. + Emoji string } func (config StickerConfig) params() (Params, error) { - return config.BaseChat.params() + params, err := config.BaseChat.params() + if err != nil { + return params, err + } + + params.AddNonEmpty("emoji", config.Emoji) + + return params, nil } func (config StickerConfig) method() string { @@ -566,7 +575,7 @@ func (config StickerConfig) files() []RequestFile { // VideoConfig contains information about a SendVideo request. type VideoConfig struct { BaseFile - Thumb RequestFileData + Thumbnail RequestFileData Duration int Caption string ParseMode string @@ -601,10 +610,10 @@ func (config VideoConfig) files() []RequestFile { Data: config.File, }} - if config.Thumb != nil { + if config.Thumbnail != nil { files = append(files, RequestFile{ - Name: "thumb", - Data: config.Thumb, + Name: "thumbnail", + Data: config.Thumbnail, }) } @@ -615,7 +624,7 @@ func (config VideoConfig) files() []RequestFile { type AnimationConfig struct { BaseFile Duration int - Thumb RequestFileData + Thumbnail RequestFileData Caption string ParseMode string CaptionEntities []MessageEntity @@ -647,10 +656,10 @@ func (config AnimationConfig) files() []RequestFile { Data: config.File, }} - if config.Thumb != nil { + if config.Thumbnail != nil { files = append(files, RequestFile{ - Name: "thumb", - Data: config.Thumb, + Name: "thumbnail", + Data: config.Thumbnail, }) } @@ -660,9 +669,9 @@ func (config AnimationConfig) files() []RequestFile { // VideoNoteConfig contains information about a SendVideoNote request. type VideoNoteConfig struct { BaseFile - Thumb RequestFileData - Duration int - Length int + Thumbnail RequestFileData + Duration int + Length int } func (config VideoNoteConfig) params() (Params, error) { @@ -684,10 +693,10 @@ func (config VideoNoteConfig) files() []RequestFile { Data: config.File, }} - if config.Thumb != nil { + if config.Thumbnail != nil { files = append(files, RequestFile{ - Name: "thumb", - Data: config.Thumb, + Name: "thumbnail", + Data: config.Thumbnail, }) } @@ -697,7 +706,7 @@ func (config VideoNoteConfig) files() []RequestFile { // VoiceConfig contains information about a SendVoice request. type VoiceConfig struct { BaseFile - Thumb RequestFileData + Thumbnail RequestFileData Caption string ParseMode string CaptionEntities []MessageEntity @@ -728,10 +737,10 @@ func (config VoiceConfig) files() []RequestFile { Data: config.File, }} - if config.Thumb != nil { + if config.Thumbnail != nil { files = append(files, RequestFile{ - Name: "thumb", - Data: config.Thumb, + Name: "thumbnail", + Data: config.Thumbnail, }) } @@ -2362,10 +2371,11 @@ func (config GetCustomEmojiStickersConfig) params() (Params, error) { return params, err } -// UploadStickerConfig allows you to upload a sticker for use in a set later. +// UploadStickerConfig uploads a sticker file for later use in a sticker set. type UploadStickerConfig struct { - UserID int64 - PNGSticker RequestFileData + UserID int64 + Sticker RequestFileData // required + StickerFormat string // required, one of StickerFormatStatic, StickerFormatAnimated, StickerFormatVideo } func (config UploadStickerConfig) method() string { @@ -2376,29 +2386,27 @@ func (config UploadStickerConfig) params() (Params, error) { params := make(Params) params.AddNonZero64("user_id", config.UserID) + params.AddNonEmpty("sticker_format", config.StickerFormat) return params, nil } func (config UploadStickerConfig) files() []RequestFile { return []RequestFile{{ - Name: "png_sticker", - Data: config.PNGSticker, + Name: "sticker", + Data: config.Sticker, }} } -// NewStickerSetConfig allows creating a new sticker set. -// -// You must set either PNGSticker or TGSSticker. +// NewStickerSetConfig creates a new sticker set owned by a user. type NewStickerSetConfig struct { - UserID int64 - Name string - Title string - PNGSticker RequestFileData - TGSSticker RequestFileData - Emojis string - StickerType string // one of StickerTypeRegular, StickerTypeMask, StickerTypeCustomEmoji - MaskPosition *MaskPosition + UserID int64 + Name string + Title string + Stickers []InputSticker + StickerFormat string // one of StickerFormatStatic, StickerFormatAnimated, StickerFormatVideo + StickerType string // one of StickerTypeRegular, StickerTypeMask, StickerTypeCustomEmoji + NeedsRepainting bool } func (config NewStickerSetConfig) method() string { @@ -2411,38 +2419,24 @@ func (config NewStickerSetConfig) params() (Params, error) { params.AddNonZero64("user_id", config.UserID) params["name"] = config.Name params["title"] = config.Title - - params["emojis"] = config.Emojis - + params.AddNonEmpty("sticker_format", config.StickerFormat) params.AddNonEmpty("sticker_type", config.StickerType) + params.AddBool("needs_repainting", config.NeedsRepainting) - err := params.AddInterface("mask_position", config.MaskPosition) + err := params.AddAny("stickers", prepareInputStickersForParams(config.Stickers)) return params, err } func (config NewStickerSetConfig) files() []RequestFile { - if config.PNGSticker != nil { - return []RequestFile{{ - Name: "png_sticker", - Data: config.PNGSticker, - }} - } - - return []RequestFile{{ - Name: "tgs_sticker", - Data: config.TGSSticker, - }} + return prepareInputStickersForFiles(config.Stickers) } -// AddStickerConfig allows you to add a sticker to a set. +// AddStickerConfig adds a new sticker to an existing sticker set. type AddStickerConfig struct { - UserID int64 - Name string - PNGSticker RequestFileData - TGSSticker RequestFileData - Emojis string - MaskPosition *MaskPosition + UserID int64 + Name string + Sticker InputSticker } func (config AddStickerConfig) method() string { @@ -2454,26 +2448,52 @@ func (config AddStickerConfig) params() (Params, error) { params.AddNonZero64("user_id", config.UserID) params["name"] = config.Name - params["emojis"] = config.Emojis - err := params.AddInterface("mask_position", config.MaskPosition) + err := params.AddAny("sticker", prepareInputStickerForParams(config.Sticker, 0)) return params, err } func (config AddStickerConfig) files() []RequestFile { - if config.PNGSticker != nil { + return prepareInputStickerForFiles(config.Sticker, 0) +} + +// prepareInputStickerForParams returns a copy of the sticker with the +// Sticker field replaced by an attach:// reference if it needs uploading. +func prepareInputStickerForParams(s InputSticker, idx int) InputSticker { + if s.Sticker != nil && s.Sticker.NeedsUpload() { + s.Sticker = fileAttach(fmt.Sprintf("attach://sticker-%d", idx)) + } + return s +} + +// prepareInputStickerForFiles returns the upload entries for a single sticker. +func prepareInputStickerForFiles(s InputSticker, idx int) []RequestFile { + if s.Sticker != nil && s.Sticker.NeedsUpload() { return []RequestFile{{ - Name: "png_sticker", - Data: config.PNGSticker, + Name: fmt.Sprintf("sticker-%d", idx), + Data: s.Sticker, }} } + return nil +} - return []RequestFile{{ - Name: "tgs_sticker", - Data: config.TGSSticker, - }} +// prepareInputStickersForParams applies prepareInputStickerForParams to a slice. +func prepareInputStickersForParams(stickers []InputSticker) []InputSticker { + out := make([]InputSticker, len(stickers)) + for i, s := range stickers { + out[i] = prepareInputStickerForParams(s, i) + } + return out +} +// prepareInputStickersForFiles flattens the upload entries for a slice of stickers. +func prepareInputStickersForFiles(stickers []InputSticker) []RequestFile { + var files []RequestFile + for i, s := range stickers { + files = append(files, prepareInputStickerForFiles(s, i)...) + } + return files } // SetStickerPositionConfig allows you to change the position of a sticker in a set. @@ -2512,18 +2532,18 @@ func (config DeleteStickerConfig) params() (Params, error) { return params, nil } -// SetStickerSetThumbConfig allows you to set the thumbnail for a sticker set. -type SetStickerSetThumbConfig struct { - Name string - UserID int64 - Thumb RequestFileData +// SetStickerSetThumbnailConfig sets the thumbnail of a sticker set. +type SetStickerSetThumbnailConfig struct { + Name string + UserID int64 + Thumbnail RequestFileData } -func (config SetStickerSetThumbConfig) method() string { - return "setStickerSetThumb" +func (config SetStickerSetThumbnailConfig) method() string { + return "setStickerSetThumbnail" } -func (config SetStickerSetThumbConfig) params() (Params, error) { +func (config SetStickerSetThumbnailConfig) params() (Params, error) { params := make(Params) params["name"] = config.Name @@ -2532,13 +2552,131 @@ func (config SetStickerSetThumbConfig) params() (Params, error) { return params, nil } -func (config SetStickerSetThumbConfig) files() []RequestFile { +func (config SetStickerSetThumbnailConfig) files() []RequestFile { return []RequestFile{{ - Name: "thumb", - Data: config.Thumb, + Name: "thumbnail", + Data: config.Thumbnail, }} } +// SetCustomEmojiStickerSetThumbnailConfig sets the thumbnail of a custom +// emoji sticker set. The bot must own the sticker set. +type SetCustomEmojiStickerSetThumbnailConfig struct { + Name string + CustomEmojiID string // pass an empty string to drop the thumbnail +} + +func (config SetCustomEmojiStickerSetThumbnailConfig) method() string { + return "setCustomEmojiStickerSetThumbnail" +} + +func (config SetCustomEmojiStickerSetThumbnailConfig) params() (Params, error) { + params := make(Params) + + params["name"] = config.Name + params.AddNonEmpty("custom_emoji_id", config.CustomEmojiID) + + return params, nil +} + +// SetStickerSetTitleConfig sets the title of a sticker set created by the bot. +type SetStickerSetTitleConfig struct { + Name string + Title string +} + +func (config SetStickerSetTitleConfig) method() string { + return "setStickerSetTitle" +} + +func (config SetStickerSetTitleConfig) params() (Params, error) { + params := make(Params) + + params["name"] = config.Name + params["title"] = config.Title + + return params, nil +} + +// DeleteStickerSetConfig deletes a sticker set created by the bot. +type DeleteStickerSetConfig struct { + Name string +} + +func (config DeleteStickerSetConfig) method() string { + return "deleteStickerSet" +} + +func (config DeleteStickerSetConfig) params() (Params, error) { + params := make(Params) + + params["name"] = config.Name + + return params, nil +} + +// SetStickerEmojiListConfig changes 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 // file identifier of the sticker + EmojiList []string +} + +func (config SetStickerEmojiListConfig) method() string { + return "setStickerEmojiList" +} + +func (config SetStickerEmojiListConfig) params() (Params, error) { + params := make(Params) + + params["sticker"] = config.Sticker + err := params.AddAny("emoji_list", config.EmojiList) + + return params, err +} + +// SetStickerKeywordsConfig changes the search keywords assigned to a regular +// or custom emoji sticker. The sticker must belong to a sticker set created +// by the bot. +type SetStickerKeywordsConfig struct { + Sticker string // file identifier of the sticker + Keywords []string +} + +func (config SetStickerKeywordsConfig) method() string { + return "setStickerKeywords" +} + +func (config SetStickerKeywordsConfig) params() (Params, error) { + params := make(Params) + + params["sticker"] = config.Sticker + err := params.AddAny("keywords", config.Keywords) + + return params, err +} + +// SetStickerMaskPositionConfig changes the mask position of a mask sticker. +// The sticker must belong to a sticker set created by the bot. +type SetStickerMaskPositionConfig struct { + Sticker string // file identifier of the sticker + MaskPosition *MaskPosition +} + +func (config SetStickerMaskPositionConfig) method() string { + return "setStickerMaskPosition" +} + +func (config SetStickerMaskPositionConfig) params() (Params, error) { + params := make(Params) + + params["sticker"] = config.Sticker + err := params.AddAny("mask_position", config.MaskPosition) + + return params, err +} + // SetChatStickerSetConfig allows you to set the sticker set for a supergroup. type SetChatStickerSetConfig struct { ChatID int64 @@ -2698,6 +2836,83 @@ func (config DeleteMyCommandsConfig) params() (Params, error) { return params, err } +// SetMyDescriptionConfig changes the bot's description, which is shown in the +// chat with the bot if the chat is empty. +type SetMyDescriptionConfig struct { + Description string + LanguageCode string +} + +func (config SetMyDescriptionConfig) method() string { + return "setMyDescription" +} + +func (config SetMyDescriptionConfig) params() (Params, error) { + params := make(Params) + + params.AddNonEmpty("description", config.Description) + params.AddNonEmpty("language_code", config.LanguageCode) + + return params, nil +} + +// GetMyDescriptionConfig returns the current bot description for the given +// user language. +type GetMyDescriptionConfig struct { + LanguageCode string +} + +func (config GetMyDescriptionConfig) method() string { + return "getMyDescription" +} + +func (config GetMyDescriptionConfig) params() (Params, error) { + params := make(Params) + + params.AddNonEmpty("language_code", config.LanguageCode) + + return params, nil +} + +// SetMyShortDescriptionConfig changes the bot's short description, which is +// shown on the bot's profile page and is sent together with the link when +// users share the bot. +type SetMyShortDescriptionConfig struct { + ShortDescription string + LanguageCode string +} + +func (config SetMyShortDescriptionConfig) method() string { + return "setMyShortDescription" +} + +func (config SetMyShortDescriptionConfig) params() (Params, error) { + params := make(Params) + + params.AddNonEmpty("short_description", config.ShortDescription) + params.AddNonEmpty("language_code", config.LanguageCode) + + return params, nil +} + +// GetMyShortDescriptionConfig returns the current bot short description for +// the given user language. +type GetMyShortDescriptionConfig struct { + LanguageCode string +} + +func (config GetMyShortDescriptionConfig) method() string { + return "getMyShortDescription" +} + +func (config GetMyShortDescriptionConfig) params() (Params, error) { + params := make(Params) + + params.AddNonEmpty("language_code", config.LanguageCode) + + return params, nil +} + // SetChatMenuButtonConfig changes the bot's menu button in a private chat, // or the default menu button. type SetChatMenuButtonConfig struct { @@ -2854,8 +3069,8 @@ func prepareInputMediaParam(inputMedia interface{}, idx int) interface{} { m.Media = fileAttach(fmt.Sprintf("attach://file-%d", idx)) } - if m.Thumb != nil && m.Thumb.NeedsUpload() { - m.Thumb = fileAttach(fmt.Sprintf("attach://file-%d-thumb", idx)) + if m.Thumbnail != nil && m.Thumbnail.NeedsUpload() { + m.Thumbnail = fileAttach(fmt.Sprintf("attach://file-%d-thumbnail", idx)) } return m @@ -2864,8 +3079,8 @@ func prepareInputMediaParam(inputMedia interface{}, idx int) interface{} { m.Media = fileAttach(fmt.Sprintf("attach://file-%d", idx)) } - if m.Thumb != nil && m.Thumb.NeedsUpload() { - m.Thumb = fileAttach(fmt.Sprintf("attach://file-%d-thumb", idx)) + if m.Thumbnail != nil && m.Thumbnail.NeedsUpload() { + m.Thumbnail = fileAttach(fmt.Sprintf("attach://file-%d-thumbnail", idx)) } return m @@ -2874,8 +3089,8 @@ func prepareInputMediaParam(inputMedia interface{}, idx int) interface{} { m.Media = fileAttach(fmt.Sprintf("attach://file-%d", idx)) } - if m.Thumb != nil && m.Thumb.NeedsUpload() { - m.Thumb = fileAttach(fmt.Sprintf("attach://file-%d-thumb", idx)) + if m.Thumbnail != nil && m.Thumbnail.NeedsUpload() { + m.Thumbnail = fileAttach(fmt.Sprintf("attach://file-%d-thumbnail", idx)) } return m @@ -2911,10 +3126,10 @@ func prepareInputMediaFile(inputMedia interface{}, idx int) []RequestFile { }) } - if m.Thumb != nil && m.Thumb.NeedsUpload() { + if m.Thumbnail != nil && m.Thumbnail.NeedsUpload() { files = append(files, RequestFile{ Name: fmt.Sprintf("file-%d", idx), - Data: m.Thumb, + Data: m.Thumbnail, }) } case InputMediaDocument: @@ -2925,10 +3140,10 @@ func prepareInputMediaFile(inputMedia interface{}, idx int) []RequestFile { }) } - if m.Thumb != nil && m.Thumb.NeedsUpload() { + if m.Thumbnail != nil && m.Thumbnail.NeedsUpload() { files = append(files, RequestFile{ Name: fmt.Sprintf("file-%d", idx), - Data: m.Thumb, + Data: m.Thumbnail, }) } case InputMediaAudio: @@ -2939,10 +3154,10 @@ func prepareInputMediaFile(inputMedia interface{}, idx int) []RequestFile { }) } - if m.Thumb != nil && m.Thumb.NeedsUpload() { + if m.Thumbnail != nil && m.Thumbnail.NeedsUpload() { files = append(files, RequestFile{ Name: fmt.Sprintf("file-%d", idx), - Data: m.Thumb, + Data: m.Thumbnail, }) } } diff --git a/helpers.go b/helpers.go index b85b7725..0047efef 100644 --- a/helpers.go +++ b/helpers.go @@ -432,13 +432,13 @@ func NewInlineQueryResultPhoto(id, url string) InlineQueryResultPhoto { } } -// NewInlineQueryResultPhotoWithThumb creates a new inline query photo. -func NewInlineQueryResultPhotoWithThumb(id, url, thumb string) InlineQueryResultPhoto { +// NewInlineQueryResultPhotoWithThumbnail creates a new inline query photo. +func NewInlineQueryResultPhotoWithThumbnail(id, url, thumbnail string) InlineQueryResultPhoto { return InlineQueryResultPhoto{ - Type: "photo", - ID: id, - URL: url, - ThumbURL: thumb, + Type: "photo", + ID: id, + URL: url, + ThumbnailURL: thumbnail, } } diff --git a/helpers_test.go b/helpers_test.go index 9119543d..8f42ec07 100644 --- a/helpers_test.go +++ b/helpers_test.go @@ -94,13 +94,13 @@ func TestNewInlineQueryResultPhoto(t *testing.T) { } } -func TestNewInlineQueryResultPhotoWithThumb(t *testing.T) { - result := NewInlineQueryResultPhotoWithThumb("id", "google.com", "thumb.com") +func TestNewInlineQueryResultPhotoWithThumbnail(t *testing.T) { + result := NewInlineQueryResultPhotoWithThumbnail("id", "google.com", "thumbnail.com") if result.Type != "photo" || result.ID != "id" || result.URL != "google.com" || - result.ThumbURL != "thumb.com" { + result.ThumbnailURL != "thumbnail.com" { t.Fail() } } diff --git a/types.go b/types.go index 0c389123..1ec5dece 100644 --- a/types.go +++ b/types.go @@ -982,7 +982,7 @@ type Animation struct { // Thumbnail animation thumbnail as defined by sender // // optional - Thumbnail *PhotoSize `json:"thumb,omitempty"` + Thumbnail *PhotoSize `json:"thumbnail,omitempty"` // FileName original animation filename as defined by sender // // optional @@ -1031,7 +1031,7 @@ type Audio struct { // Thumbnail is the album cover to which the music file belongs // // optional - Thumbnail *PhotoSize `json:"thumb,omitempty"` + Thumbnail *PhotoSize `json:"thumbnail,omitempty"` } // Document represents a general file. @@ -1046,7 +1046,7 @@ type Document struct { // Thumbnail document thumbnail as defined by sender // // optional - Thumbnail *PhotoSize `json:"thumb,omitempty"` + Thumbnail *PhotoSize `json:"thumbnail,omitempty"` // FileName original filename as defined by sender // // optional @@ -1079,7 +1079,7 @@ type Video struct { // Thumbnail video thumbnail // // optional - Thumbnail *PhotoSize `json:"thumb,omitempty"` + Thumbnail *PhotoSize `json:"thumbnail,omitempty"` // FileName is the original filename as defined by sender // // optional @@ -1109,7 +1109,7 @@ type VideoNote struct { // Thumbnail video thumbnail // // optional - Thumbnail *PhotoSize `json:"thumb,omitempty"` + Thumbnail *PhotoSize `json:"thumbnail,omitempty"` // FileSize file size // // optional @@ -2304,7 +2304,7 @@ type InputMediaVideo struct { // the file is supported server-side. // // optional - Thumb RequestFileData `json:"thumb,omitempty"` + Thumbnail RequestFileData `json:"thumbnail,omitempty"` // Width video width // // optional @@ -2334,7 +2334,7 @@ type InputMediaAnimation struct { // the file is supported server-side. // // optional - Thumb RequestFileData `json:"thumb,omitempty"` + Thumbnail RequestFileData `json:"thumbnail,omitempty"` // Width video width // // optional @@ -2360,7 +2360,7 @@ type InputMediaAudio struct { // the file is supported server-side. // // optional - Thumb RequestFileData `json:"thumb,omitempty"` + Thumbnail RequestFileData `json:"thumbnail,omitempty"` // Duration of the audio in seconds // // optional @@ -2382,7 +2382,7 @@ type InputMediaDocument struct { // the file is supported server-side. // // optional - Thumb RequestFileData `json:"thumb,omitempty"` + Thumbnail RequestFileData `json:"thumbnail,omitempty"` // DisableContentTypeDetection disables automatic server-side content type // detection for files uploaded using multipart/form-data. Always true, if // the document is sent as part of an album @@ -2426,7 +2426,7 @@ type Sticker struct { // Thumbnail sticker thumbnail in the .WEBP or .JPG format // // optional - Thumbnail *PhotoSize `json:"thumb,omitempty"` + Thumbnail *PhotoSize `json:"thumbnail,omitempty"` // Emoji associated with the sticker // // optional @@ -2448,12 +2448,56 @@ type Sticker struct { // // optional CustomEmojiID string `json:"custom_emoji_id,omitempty"` + // NeedsRepainting is true if the sticker must be repainted to a text color + // in messages, the color of the Telegram Premium badge in emoji status, + // white color on chat photos, or another appropriate color in other places. + // + // optional + NeedsRepainting bool `json:"needs_repainting,omitempty"` // FileSize // // optional FileSize int `json:"file_size,omitempty"` } +// BotDescription represents the bot's description. +type BotDescription struct { + Description string `json:"description"` +} + +// Sticker format constants for the createNewStickerSet method +// and the uploadStickerFile method. +const ( + StickerFormatStatic = "static" + StickerFormatAnimated = "animated" + StickerFormatVideo = "video" +) + +// InputSticker describes a sticker to be added to a sticker set. +type InputSticker struct { + // Sticker is the file to upload. May be a file ID, an HTTP URL, or + // new file data via FilePath/FileBytes/FileReader. Animated and video + // stickers can't be uploaded via HTTP URL. + Sticker RequestFileData `json:"sticker"` + // EmojiList is the list of 1-20 emoji associated with the sticker. + EmojiList []string `json:"emoji_list"` + // MaskPosition for "mask" stickers, the position where the mask should + // be placed on faces. + // + // optional + MaskPosition *MaskPosition `json:"mask_position,omitempty"` + // Keywords is the list of 0-20 search keywords for the sticker. + // + // optional + Keywords []string `json:"keywords,omitempty"` +} + +// BotShortDescription represents the bot's short description, shown on the +// bot's profile page and sent together with the link when users share the bot. +type BotShortDescription struct { + ShortDescription string `json:"short_description"` +} + // StickerSet represents a sticker set. type StickerSet struct { // Name sticker set name @@ -2469,7 +2513,7 @@ type StickerSet struct { // Stickers list of all set stickers Stickers []Sticker `json:"stickers"` // Thumb is the sticker set thumbnail in the .WEBP or .TGS format - Thumbnail *PhotoSize `json:"thumb"` + Thumbnail *PhotoSize `json:"thumbnail"` } // MaskPosition describes the position on faces where a mask should be placed @@ -2909,15 +2953,15 @@ type InlineQueryResultArticle struct { // ThumbURL url of the thumbnail for the result // // optional - ThumbURL string `json:"thumb_url,omitempty"` + ThumbnailURL string `json:"thumbnail_url,omitempty"` // ThumbWidth thumbnail width // // optional - ThumbWidth int `json:"thumb_width,omitempty"` + ThumbnailWidth int `json:"thumbnail_width,omitempty"` // ThumbHeight thumbnail height // // optional - ThumbHeight int `json:"thumb_height,omitempty"` + ThumbnailHeight int `json:"thumbnail_height,omitempty"` } // InlineQueryResultAudio is an inline query response audio. @@ -2973,9 +3017,9 @@ type InlineQueryResultContact struct { VCard string `json:"vcard"` ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` InputMessageContent interface{} `json:"input_message_content,omitempty"` - ThumbURL string `json:"thumb_url"` - ThumbWidth int `json:"thumb_width"` - ThumbHeight int `json:"thumb_height"` + ThumbnailURL string `json:"thumbnail_url"` + ThumbnailWidth int `json:"thumbnail_width"` + ThumbnailHeight int `json:"thumbnail_height"` } // InlineQueryResultGame is an inline query response game. @@ -3023,15 +3067,15 @@ type InlineQueryResultDocument struct { // ThumbURL url of the thumbnail (jpeg only) for the file // // optional - ThumbURL string `json:"thumb_url,omitempty"` + ThumbnailURL string `json:"thumbnail_url,omitempty"` // ThumbWidth thumbnail width // // optional - ThumbWidth int `json:"thumb_width,omitempty"` + ThumbnailWidth int `json:"thumbnail_width,omitempty"` // ThumbHeight thumbnail height // // optional - ThumbHeight int `json:"thumb_height,omitempty"` + ThumbnailHeight int `json:"thumbnail_height,omitempty"` } // InlineQueryResultGIF is an inline query response GIF. @@ -3042,8 +3086,14 @@ type InlineQueryResultGIF struct { ID string `json:"id"` // URL a valid URL for the GIF file. File size must not exceed 1MB. URL string `json:"gif_url"` - // ThumbURL url of the static (JPEG or GIF) or animated (MPEG4) thumbnail for the result. - ThumbURL string `json:"thumb_url"` + // ThumbnailURL is the URL of the static (JPEG or GIF) or animated (MPEG4) + // thumbnail for the result. + ThumbnailURL string `json:"thumbnail_url"` + // ThumbnailMimeType is the MIME type of the thumbnail. Must be one of + // "image/jpeg", "image/gif", or "video/mp4". Defaults to "image/jpeg". + // + // optional + ThumbnailMimeType string `json:"thumbnail_mime_type,omitempty"` // Width of the GIF // // optional @@ -3129,15 +3179,15 @@ type InlineQueryResultLocation struct { // ThumbURL url of the thumbnail for the result // // optional - ThumbURL string `json:"thumb_url,omitempty"` + ThumbnailURL string `json:"thumbnail_url,omitempty"` // ThumbWidth thumbnail width // // optional - ThumbWidth int `json:"thumb_width,omitempty"` + ThumbnailWidth int `json:"thumbnail_width,omitempty"` // ThumbHeight thumbnail height // // optional - ThumbHeight int `json:"thumb_height,omitempty"` + ThumbnailHeight int `json:"thumbnail_height,omitempty"` } // InlineQueryResultMPEG4GIF is an inline query response MPEG4 GIF. @@ -3160,8 +3210,14 @@ type InlineQueryResultMPEG4GIF struct { // // optional Duration int `json:"mpeg4_duration,omitempty"` - // ThumbURL url of the static (JPEG or GIF) or animated (MPEG4) thumbnail for the result. - ThumbURL string `json:"thumb_url"` + // ThumbnailURL is the URL of the static (JPEG or GIF) or animated (MPEG4) + // thumbnail for the result. + ThumbnailURL string `json:"thumbnail_url"` + // ThumbnailMimeType is the MIME type of the thumbnail. Must be one of + // "image/jpeg", "image/gif", or "video/mp4". Defaults to "image/jpeg". + // + // optional + ThumbnailMimeType string `json:"thumbnail_mime_type,omitempty"` // Title for the result // // optional @@ -3213,7 +3269,7 @@ type InlineQueryResultPhoto struct { // ThumbURL url of the thumbnail for the photo. // // optional - ThumbURL string `json:"thumb_url,omitempty"` + ThumbnailURL string `json:"thumbnail_url,omitempty"` // Title for the result // // optional @@ -3289,15 +3345,15 @@ type InlineQueryResultVenue struct { // ThumbURL url of the thumbnail for the result // // optional - ThumbURL string `json:"thumb_url,omitempty"` + ThumbnailURL string `json:"thumbnail_url,omitempty"` // ThumbWidth thumbnail width // // optional - ThumbWidth int `json:"thumb_width,omitempty"` + ThumbnailWidth int `json:"thumbnail_width,omitempty"` // ThumbHeight thumbnail height // // optional - ThumbHeight int `json:"thumb_height,omitempty"` + ThumbnailHeight int `json:"thumbnail_height,omitempty"` } // InlineQueryResultVideo is an inline query response video. @@ -3313,7 +3369,7 @@ type InlineQueryResultVideo struct { // // ThumbURL url of the thumbnail (jpeg only) for the video // optional - ThumbURL string `json:"thumb_url,omitempty"` + ThumbnailURL string `json:"thumbnail_url,omitempty"` // Title for the result Title string `json:"title"` // Caption of the video to be sent, 0-1024 characters after entities parsing diff --git a/types_test.go b/types_test.go index 0c6ba4ab..70f2f6b7 100644 --- a/types_test.go +++ b/types_test.go @@ -367,7 +367,7 @@ var ( _ Fileable = (*AddStickerConfig)(nil) _ Fileable = (*MediaGroupConfig)(nil) _ Fileable = (*WebhookConfig)(nil) - _ Fileable = (*SetStickerSetThumbConfig)(nil) + _ Fileable = (*SetStickerSetThumbnailConfig)(nil) ) // Ensure all RequestFileData types are correct. From 7c9d4afdb3c302fd31c1fc3f7358b502a33c7eae Mon Sep 17 00:00:00 2001 From: Kirill Date: Sat, 11 Apr 2026 03:20:13 +0400 Subject: [PATCH 13/65] Full support of API 6.7 --- bot.go | 13 ++++++++++ configs.go | 57 +++++++++++++++++++++++++++++++++++-------- types.go | 71 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 129 insertions(+), 12 deletions(-) diff --git a/bot.go b/bot.go index b202c9eb..df96cf22 100644 --- a/bot.go +++ b/bot.go @@ -687,6 +687,19 @@ func (bot *BotAPI) GetForumTopicIconStickers() ([]Sticker, error) { return stickers, err } +// GetMyName returns the current bot name for the given user language. +func (bot *BotAPI) GetMyName(config GetMyNameConfig) (BotName, error) { + resp, err := bot.Request(config) + if err != nil { + return BotName{}, err + } + + var name BotName + err = json.Unmarshal(resp.Result, &name) + + return name, err +} + // GetMyDescription returns the current bot description for the given user language. func (bot *BotAPI) GetMyDescription(config GetMyDescriptionConfig) (BotDescription, error) { resp, err := bot.Request(config) diff --git a/configs.go b/configs.go index 993787ab..44870533 100644 --- a/configs.go +++ b/configs.go @@ -1241,13 +1241,12 @@ func (config DeleteWebhookConfig) params() (Params, error) { // InlineConfig contains information on making an InlineQuery response. type InlineConfig struct { - InlineQueryID string `json:"inline_query_id"` - Results []interface{} `json:"results"` - CacheTime int `json:"cache_time"` - IsPersonal bool `json:"is_personal"` - NextOffset string `json:"next_offset"` - SwitchPMText string `json:"switch_pm_text"` - SwitchPMParameter string `json:"switch_pm_parameter"` + InlineQueryID string `json:"inline_query_id"` + Results []interface{} `json:"results"` + CacheTime int `json:"cache_time"` + IsPersonal bool `json:"is_personal"` + NextOffset string `json:"next_offset"` + Button *InlineQueryResultsButton `json:"button,omitempty"` } func (config InlineConfig) method() string { @@ -1261,9 +1260,10 @@ func (config InlineConfig) params() (Params, error) { params.AddNonZero("cache_time", config.CacheTime) params.AddBool("is_personal", config.IsPersonal) params.AddNonEmpty("next_offset", config.NextOffset) - params.AddNonEmpty("switch_pm_text", config.SwitchPMText) - params.AddNonEmpty("switch_pm_parameter", config.SwitchPMParameter) - err := params.AddInterface("results", config.Results) + if err := params.AddAny("button", config.Button); err != nil { + return params, err + } + err := params.AddAny("results", config.Results) return params, err } @@ -2836,6 +2836,43 @@ func (config DeleteMyCommandsConfig) params() (Params, error) { return params, err } +// SetMyNameConfig changes the bot's name. Different names can be set for +// different user languages. +type SetMyNameConfig struct { + Name string + LanguageCode string +} + +func (config SetMyNameConfig) method() string { + return "setMyName" +} + +func (config SetMyNameConfig) params() (Params, error) { + params := make(Params) + + params.AddNonEmpty("name", config.Name) + params.AddNonEmpty("language_code", config.LanguageCode) + + return params, nil +} + +// GetMyNameConfig returns the current bot name for the given user language. +type GetMyNameConfig struct { + LanguageCode string +} + +func (config GetMyNameConfig) method() string { + return "getMyName" +} + +func (config GetMyNameConfig) params() (Params, error) { + params := make(Params) + + params.AddNonEmpty("language_code", config.LanguageCode) + + return params, nil +} + // SetMyDescriptionConfig changes the bot's description, which is shown in the // chat with the bot if the chat is empty. type SetMyDescriptionConfig struct { diff --git a/types.go b/types.go index 1ec5dece..ad47c4f7 100644 --- a/types.go +++ b/types.go @@ -1695,6 +1695,13 @@ type InlineKeyboardButton struct { // // 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, open that chat + // and insert the bot's username and the specified inline query in the + // input field. + // + // optional + SwitchInlineQueryChosenChat *SwitchInlineQueryChosenChat `json:"switch_inline_query_chosen_chat,omitempty"` // CallbackGame description of the game that will be launched when the user presses the button. // // optional @@ -1707,6 +1714,50 @@ type InlineKeyboardButton struct { Pay bool `json:"pay,omitempty"` } +// SwitchInlineQueryChosenChat represents an inline button that switches the +// current user to inline mode in a chosen chat, with an optional default +// inline query. +type SwitchInlineQueryChosenChat struct { + // Query is the default inline query to be inserted in the input field. + // If left empty, only the bot's username will be inserted. + // + // optional + Query string `json:"query,omitempty"` + // AllowUserChats is true if private chats with users can be chosen. + // + // optional + AllowUserChats bool `json:"allow_user_chats,omitempty"` + // AllowBotChats is true if private chats with bots can be chosen. + // + // optional + AllowBotChats bool `json:"allow_bot_chats,omitempty"` + // AllowGroupChats is true if group and supergroup chats can be chosen. + // + // optional + AllowGroupChats bool `json:"allow_group_chats,omitempty"` + // AllowChannelChats is true if channel chats can be chosen. + // + // optional + AllowChannelChats bool `json:"allow_channel_chats,omitempty"` +} + +// InlineQueryResultsButton represents a button to be shown above inline query +// results. Exactly one of WebApp or StartParameter must be set. +type InlineQueryResultsButton struct { + // Text label of the button. + Text string `json:"text"` + // WebApp is the description of the Web App that will be launched when the + // user presses the button. + // + // optional + WebApp *WebAppInfo `json:"web_app,omitempty"` + // StartParameter is the deep-linking parameter for the /start message + // sent to the bot when the user presses the button. 1-64 characters. + // + // optional + StartParameter string `json:"start_parameter,omitempty"` +} + // LoginURL represents a parameter of the inline keyboard button used to // automatically authorize a user. Serves as a great replacement for the // Telegram Login Widget when the user is coming from Telegram. All the user @@ -2048,6 +2099,11 @@ type ChatMemberUpdated struct { // // optional InviteLink *ChatInviteLink `json:"invite_link,omitempty"` + // ViaChatFolderInviteLink is true, if the user joined the chat via a chat + // folder invite link. + // + // optional + ViaChatFolderInviteLink bool `json:"via_chat_folder_invite_link,omitempty"` } // ChatJoinRequest represents a join request sent to a chat. @@ -2210,8 +2266,14 @@ type GeneralForumTopicHidden struct{} type GeneralForumTopicUnhidden struct{} // WriteAccessAllowed represents a service message about a user allowing a bot -// added to the attachment menu to write messages. Currently holds no information. -type WriteAccessAllowed struct{} +// to write messages after adding it to the attachment menu, launching a Web +// App from a link, or accepting an explicit request. +type WriteAccessAllowed struct { + // WebAppName is the name of the Web App which was launched from a link. + // + // optional + WebAppName string `json:"web_app_name,omitempty"` +} // BotCommand represents a bot command. type BotCommand struct { @@ -2460,6 +2522,11 @@ type Sticker struct { FileSize int `json:"file_size,omitempty"` } +// BotName represents the bot's name. +type BotName struct { + Name string `json:"name"` +} + // BotDescription represents the bot's description. type BotDescription struct { Description string `json:"description"` From a45a6dbe1294f57051792d2d1a096c56ecb5cf77 Mon Sep 17 00:00:00 2001 From: Kirill Date: Sat, 11 Apr 2026 03:31:08 +0400 Subject: [PATCH 14/65] Full support of API 6.8 --- configs.go | 20 ++++++++++++++++++++ types.go | 22 +++++++++++++++++++++- 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/configs.go b/configs.go index 44870533..bc79f79f 100644 --- a/configs.go +++ b/configs.go @@ -2260,6 +2260,26 @@ func (config UnhideGeneralForumTopicConfig) params() (Params, error) { return params, nil } +// UnpinAllGeneralForumTopicMessagesConfig clears the list of pinned messages +// in the General forum topic. The bot must be an administrator in the chat +// with the can_pin_messages administrator right in the supergroup. +type UnpinAllGeneralForumTopicMessagesConfig struct { + ChatID int64 + SuperGroupUsername string +} + +func (config UnpinAllGeneralForumTopicMessagesConfig) method() string { + return "unpinAllGeneralForumTopicMessages" +} + +func (config UnpinAllGeneralForumTopicMessagesConfig) params() (Params, error) { + params := make(Params) + + params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername) + + return params, nil +} + // SetChatPhotoConfig allows you to set a group, supergroup, or channel's photo. type SetChatPhotoConfig struct { BaseFile diff --git a/types.go b/types.go index ad47c4f7..c0258505 100644 --- a/types.go +++ b/types.go @@ -300,6 +300,12 @@ type Chat struct { // // optional EmojiStatusCustomEmojiID string `json:"emoji_status_custom_emoji_id,omitempty"` + // EmojiStatusExpirationDate is the expiration date of the emoji status of + // the other party in a private chat in Unix time, if any. Returned only in + // getChat. + // + // optional + EmojiStatusExpirationDate int64 `json:"emoji_status_expiration_date,omitempty"` // HasHiddenMembers is true, if non-administrators can only see bots and // administrators in the chat. Returned only in getChat. // @@ -557,6 +563,10 @@ type Message struct { // // optional VideoNote *VideoNote `json:"video_note,omitempty"` + // Story is a forwarded story. + // + // optional + Story *Story `json:"story,omitempty"` // Voice message is a voice message, information about the file; // // optional @@ -1178,7 +1188,14 @@ type PollOption struct { type PollAnswer struct { // PollID is the unique poll identifier PollID string `json:"poll_id"` - // User who changed the answer to the poll + // VoterChat is the chat that changed the answer to the poll, if the voter + // is anonymous. + // + // optional + VoterChat *Chat `json:"voter_chat,omitempty"` + // User who changed the answer to the poll, if the voter isn't anonymous. + // For backward compatibility, the field user in such objects will contain + // the user 136817688 (@Channel_Bot). User User `json:"user"` // OptionIDs is the 0-based identifiers of poll options chosen by the user. // May be empty if user retracted vote. @@ -2197,6 +2214,9 @@ type ChatPermissions struct { CanManageTopics bool `json:"can_manage_topics,omitempty"` } +// Story represents a forwarded story. Currently holds no information. +type Story struct{} + // ChatLocation represents a location to which a chat is connected. type ChatLocation struct { // Location is the location to which the supergroup is connected. Can't be a From 7c3a0b867dd13c0f8d2a97c144c4fe28318a2efd Mon Sep 17 00:00:00 2001 From: Kirill Date: Sat, 11 Apr 2026 03:36:26 +0400 Subject: [PATCH 15/65] Full support of API 6.9 --- configs.go | 6 ++++++ types.go | 28 ++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/configs.go b/configs.go index bc79f79f..3f53576e 100644 --- a/configs.go +++ b/configs.go @@ -1411,6 +1411,9 @@ type PromoteChatMemberConfig struct { CanRestrictMembers bool CanPinMessages bool CanPromoteMembers bool + CanPostStories bool + CanEditStories bool + CanDeleteStories bool CanManageTopics bool } @@ -1435,6 +1438,9 @@ func (config PromoteChatMemberConfig) params() (Params, error) { params.AddBool("can_restrict_members", config.CanRestrictMembers) params.AddBool("can_pin_messages", config.CanPinMessages) params.AddBool("can_promote_members", config.CanPromoteMembers) + params.AddBool("can_post_stories", config.CanPostStories) + params.AddBool("can_edit_stories", config.CanEditStories) + params.AddBool("can_delete_stories", config.CanDeleteStories) params.AddBool("can_manage_topics", config.CanManageTopics) return params, nil diff --git a/types.go b/types.go index c0258505..3cca6a0e 100644 --- a/types.go +++ b/types.go @@ -1938,6 +1938,9 @@ type ChatAdministratorRights struct { CanPostMessages bool `json:"can_post_messages"` CanEditMessages bool `json:"can_edit_messages"` CanPinMessages bool `json:"can_pin_messages"` + CanPostStories bool `json:"can_post_stories"` + CanEditStories bool `json:"can_edit_stories"` + CanDeleteStories bool `json:"can_delete_stories"` CanManageTopics bool `json:"can_manage_topics"` } @@ -2031,6 +2034,21 @@ type ChatMember struct { // // optional CanPinMessages bool `json:"can_pin_messages,omitempty"` + // CanPostStories administrators only. + // True, if the administrator can post stories to the chat. + // + // optional + CanPostStories bool `json:"can_post_stories,omitempty"` + // CanEditStories administrators only. + // True, if the administrator can edit stories posted by other users. + // + // optional + CanEditStories bool `json:"can_edit_stories,omitempty"` + // CanDeleteStories administrators only. + // True, if the administrator can delete stories posted by other users. + // + // optional + CanDeleteStories bool `json:"can_delete_stories,omitempty"` // CanManageTopics administrators and restricted only. // True, if the user is allowed to create, rename, close, and reopen // forum topics; supergroups only. @@ -2289,10 +2307,20 @@ type GeneralForumTopicUnhidden struct{} // to write messages after adding it to the attachment menu, launching a Web // App from a link, or accepting an explicit request. type WriteAccessAllowed struct { + // FromRequest is true if the access was granted after the user accepted + // an explicit request from a Web App sent by the method requestWriteAccess. + // + // optional + FromRequest bool `json:"from_request,omitempty"` // WebAppName is the name of the Web App which was launched from a link. // // optional WebAppName string `json:"web_app_name,omitempty"` + // FromAttachmentMenu is true if the access was granted when the bot was + // added to the attachment or side menu. + // + // optional + FromAttachmentMenu bool `json:"from_attachment_menu,omitempty"` } // BotCommand represents a bot command. From 56720b3853f973c28b33d69a2c8c0928ac118772 Mon Sep 17 00:00:00 2001 From: Kirill Date: Sat, 11 Apr 2026 03:45:01 +0400 Subject: [PATCH 16/65] Support of reactions in API 7.0 --- configs.go | 38 ++++++++++++++++++++++- types.go | 90 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 127 insertions(+), 1 deletion(-) diff --git a/configs.go b/configs.go index 3f53576e..6ad35b8f 100644 --- a/configs.go +++ b/configs.go @@ -86,6 +86,12 @@ const ( // only in polls that were sent by the bot itself. UpdateTypePollAnswer = "poll_answer" + // UpdateTypeMessageReaction is when a reaction to a message was changed by a user. + UpdateTypeMessageReaction = "message_reaction" + + // UpdateTypeMessageReactionCount is when reactions to a message with anonymous reactions were changed. + UpdateTypeMessageReactionCount = "message_reaction_count" + // UpdateTypeMyChatMember is when the bot's chat member status was updated in a chat. For private chats, this // update is received only when the bot is blocked or unblocked by the user. UpdateTypeMyChatMember = "my_chat_member" @@ -1950,6 +1956,34 @@ func (config DeleteMessageConfig) params() (Params, error) { return params, nil } +// SetMessageReactionConfig sets the bot's reaction to a message. Pass an +// empty Reaction to remove all reactions from the message. +type SetMessageReactionConfig struct { + ChatID int64 + ChannelUsername string + MessageID int + Reaction []ReactionType + IsBig bool +} + +func (config SetMessageReactionConfig) method() string { + return "setMessageReaction" +} + +func (config SetMessageReactionConfig) params() (Params, error) { + params := make(Params) + + if err := params.AddFirstValid("chat_id", config.ChatID, config.ChannelUsername); err != nil { + return params, err + } + params.AddNonZero("message_id", config.MessageID) + params.AddBool("is_big", config.IsBig) + + err := params.AddAny("reaction", config.Reaction) + + return params, err +} + // PinChatMessageConfig contains information of a message in a chat to pin. type PinChatMessageConfig struct { ChatID int64 @@ -2281,7 +2315,9 @@ func (config UnpinAllGeneralForumTopicMessagesConfig) method() string { func (config UnpinAllGeneralForumTopicMessagesConfig) params() (Params, error) { params := make(Params) - params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername) + if err := params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername); err != nil { + return params, err + } return params, nil } diff --git a/types.go b/types.go index 3cca6a0e..d5155b90 100644 --- a/types.go +++ b/types.go @@ -105,6 +105,19 @@ type Update struct { // // optional PollAnswer *PollAnswer `json:"poll_answer,omitempty"` + // MessageReaction is a reaction to a message that was changed by a user. + // The bot must be an administrator in the chat and must explicitly specify + // "message_reaction" in the list of allowed_updates to receive these updates. + // The update isn't received for reactions set by bots. + // + // optional + MessageReaction *MessageReactionUpdated `json:"message_reaction,omitempty"` + // MessageReactionCount is reactions to a message with anonymous reactions + // were changed. The bot must be an administrator in the chat and must + // explicitly specify "message_reaction_count" in the list of allowed_updates. + // + // optional + MessageReactionCount *MessageReactionCountUpdated `json:"message_reaction_count,omitempty"` // MyChatMember is the bot's chat member status was updated in a chat. For // private chats, this update is received only when the bot is blocked or // unblocked by the user. @@ -360,6 +373,12 @@ type Chat struct { // // optional PinnedMessage *Message `json:"pinned_message,omitempty"` + // AvailableReactions is the list of available reactions allowed in the + // chat. If omitted, then all emoji reactions are allowed. Returned only + // in getChat. + // + // optional + AvailableReactions []ReactionType `json:"available_reactions,omitempty"` // Permissions are default chat member permissions, for groups and // supergroups. Returned only in getChat. // @@ -2235,6 +2254,77 @@ type ChatPermissions struct { // Story represents a forwarded story. Currently holds no information. type Story struct{} +// Reaction type constants. +const ( + ReactionTypeEmoji = "emoji" + ReactionTypeCustomEmoji = "custom_emoji" + ReactionTypePaid = "paid" +) + +// ReactionType describes the type of a reaction. The Type field discriminates +// between concrete variants: +// - "emoji" → Emoji is set +// - "custom_emoji" → CustomEmojiID is set +// - "paid" → no extra fields +type ReactionType struct { + // Type of the reaction. One of "emoji", "custom_emoji", "paid". + Type string `json:"type"` + // Emoji is set when Type == "emoji". One of the supported emoji. + // + // optional + Emoji string `json:"emoji,omitempty"` + // CustomEmojiID is set when Type == "custom_emoji". + // + // optional + CustomEmojiID string `json:"custom_emoji_id,omitempty"` +} + +// ReactionCount represents a reaction added to a message along with the +// number of times it was added. +type ReactionCount struct { + // Type of the reaction. + Type ReactionType `json:"type"` + // TotalCount is the number of times the reaction was added. + TotalCount int `json:"total_count"` +} + +// MessageReactionUpdated represents a change of a reaction on a message +// performed by a user. +type MessageReactionUpdated struct { + // Chat containing the message the user reacted to. + Chat Chat `json:"chat"` + // MessageID is the unique identifier of the message inside the chat. + MessageID int `json:"message_id"` + // User that changed the reaction, if the user isn't anonymous. + // + // optional + User *User `json:"user,omitempty"` + // ActorChat is the chat on behalf of which the reaction was changed, + // if the user is anonymous. + // + // optional + ActorChat *Chat `json:"actor_chat,omitempty"` + // Date of the change in Unix time. + Date int `json:"date"` + // OldReaction is the previous list of reaction types that were set by the user. + OldReaction []ReactionType `json:"old_reaction"` + // NewReaction is the new list of reaction types that have been set by the user. + NewReaction []ReactionType `json:"new_reaction"` +} + +// MessageReactionCountUpdated represents reaction changes on a message with +// anonymous reactions. +type MessageReactionCountUpdated struct { + // Chat containing the message. + Chat Chat `json:"chat"` + // MessageID is the unique identifier of the message inside the chat. + MessageID int `json:"message_id"` + // Date of the change in Unix time. + Date int `json:"date"` + // Reactions is the list of reactions that are present on the message. + Reactions []ReactionCount `json:"reactions"` +} + // ChatLocation represents a location to which a chat is connected. type ChatLocation struct { // Location is the location to which the supergroup is connected. Can't be a From 587514aeb90f34821b1df4e3391f0d25f96a23c6 Mon Sep 17 00:00:00 2001 From: Kirill Date: Sat, 11 Apr 2026 03:57:17 +0400 Subject: [PATCH 17/65] Support of replies 2.0 in API 7.0 --- bot_test.go | 6 +- configs.go | 40 ++++++++------ helpers.go | 3 +- types.go | 156 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 183 insertions(+), 22 deletions(-) diff --git a/bot_test.go b/bot_test.go index 687b06ce..7c723dc1 100644 --- a/bot_test.go +++ b/bot_test.go @@ -84,7 +84,7 @@ func TestSendWithMessageReply(t *testing.T) { bot, _ := getBot(t) msg := NewMessage(ChatID, "A test message from the test library in telegram-bot-api") - msg.ReplyToMessageID = ReplyToMessageID + msg.ReplyParameters = &ReplyParameters{MessageID: ReplyToMessageID} _, err := bot.Send(msg) if err != nil { @@ -169,7 +169,7 @@ func TestSendWithNewPhotoReply(t *testing.T) { bot, _ := getBot(t) msg := NewPhoto(ChatID, FilePath("tests/image.jpg")) - msg.ReplyToMessageID = ReplyToMessageID + msg.ReplyParameters = &ReplyParameters{MessageID: ReplyToMessageID} _, err := bot.Send(msg) @@ -699,7 +699,7 @@ func ExampleNewBotAPI() { log.Printf("[%s] %s", update.Message.From.UserName, update.Message.Text) msg := NewMessage(update.Message.Chat.ID, update.Message.Text) - msg.ReplyToMessageID = update.Message.MessageID + msg.ReplyParameters = &ReplyParameters{MessageID: update.Message.MessageID} bot.Send(msg) } diff --git a/configs.go b/configs.go index 6ad35b8f..c7e1ce11 100644 --- a/configs.go +++ b/configs.go @@ -275,29 +275,31 @@ func (CloseConfig) params() (Params, error) { // BaseChat is base type for all chat config types. type BaseChat struct { - ChatID int64 // required - ChannelUsername string - MessageThreadID int - MessageEffectID string - ProtectContent bool - ReplyToMessageID int - ReplyMarkup interface{} - DisableNotification bool - AllowSendingWithoutReply bool + ChatID int64 // required + ChannelUsername string + MessageThreadID int + MessageEffectID string + ProtectContent bool + ReplyParameters *ReplyParameters + ReplyMarkup interface{} + DisableNotification bool } func (chat *BaseChat) params() (Params, error) { params := make(Params) - params.AddFirstValid("chat_id", chat.ChatID, chat.ChannelUsername) + if err := params.AddFirstValid("chat_id", chat.ChatID, chat.ChannelUsername); err != nil { + return params, err + } params.AddNonZero("message_thread_id", chat.MessageThreadID) params.AddNonEmpty("message_effect_id", chat.MessageEffectID) - params.AddNonZero("reply_to_message_id", chat.ReplyToMessageID) params.AddBool("disable_notification", chat.DisableNotification) - params.AddBool("allow_sending_without_reply", chat.AllowSendingWithoutReply) params.AddBool("protect_content", chat.ProtectContent) - err := params.AddInterface("reply_markup", chat.ReplyMarkup) + if err := params.AddAny("reply_parameters", chat.ReplyParameters); err != nil { + return params, err + } + err := params.AddAny("reply_markup", chat.ReplyMarkup) return params, err } @@ -2788,7 +2790,7 @@ type MediaGroupConfig struct { Media []interface{} DisableNotification bool - ReplyToMessageID int + ReplyParameters *ReplyParameters } func (config MediaGroupConfig) method() string { @@ -2798,12 +2800,16 @@ func (config MediaGroupConfig) method() string { func (config MediaGroupConfig) params() (Params, error) { params := make(Params) - params.AddFirstValid("chat_id", config.ChatID, config.ChannelUsername) + if err := params.AddFirstValid("chat_id", config.ChatID, config.ChannelUsername); err != nil { + return params, err + } params.AddNonZero("message_thread_id", config.MessageThreadID) params.AddBool("disable_notification", config.DisableNotification) - params.AddNonZero("reply_to_message_id", config.ReplyToMessageID) + if err := params.AddAny("reply_parameters", config.ReplyParameters); err != nil { + return params, err + } - err := params.AddInterface("media", prepareInputMediaForParams(config.Media)) + err := params.AddAny("media", prepareInputMediaForParams(config.Media)) return params, err } diff --git a/helpers.go b/helpers.go index 0047efef..683ea131 100644 --- a/helpers.go +++ b/helpers.go @@ -17,8 +17,7 @@ import ( func NewMessage(chatID int64, text string) MessageConfig { return MessageConfig{ BaseChat: BaseChat{ - ChatID: chatID, - ReplyToMessageID: 0, + ChatID: chatID, }, Text: text, DisableWebPagePreview: false, diff --git a/types.go b/types.go index d5155b90..e1de2329 100644 --- a/types.go +++ b/types.go @@ -515,6 +515,15 @@ type Message struct { // // optional ReplyToMessage *Message `json:"reply_to_message,omitempty"` + // ExternalReply is information about the message that is being replied to, + // which may come from another chat or forum topic. + // + // optional + ExternalReply *ExternalReplyInfo `json:"external_reply,omitempty"` + // Quote is the part of the message that is actually quoted in the reply. + // + // optional + Quote *TextQuote `json:"quote,omitempty"` // ViaBot through which the message was sent; // // optional @@ -2325,6 +2334,153 @@ type MessageReactionCountUpdated struct { Reactions []ReactionCount `json:"reactions"` } +// TextQuote contains information about the quoted part of a message that is +// replied to by the given message. +type TextQuote struct { + // Text of the quoted part of the message that is replied to by the given + // message. + Text string `json:"text"` + // Entities are special entities that appear in the quote. Currently, only + // bold, italic, underline, strikethrough, spoiler, and custom_emoji + // entities are kept in quotes. + // + // optional + Entities []MessageEntity `json:"entities,omitempty"` + // Position is the approximate quote position in the original message in + // UTF-16 code units as specified by the sender. + Position int `json:"position"` + // IsManual is true, if the quote was chosen manually by the message + // sender. Otherwise, the quote was added automatically by the server. + // + // optional + IsManual bool `json:"is_manual,omitempty"` +} + +// ExternalReplyInfo contains information about a message that is being replied +// to, which may come from another chat or forum topic. +// +// Note: this struct grows across Bot API 7.0 sections — fields like Origin +// (MessageOrigin), LinkPreviewOptions, Giveaway, GiveawayWinners are added +// alongside their respective sections. +type ExternalReplyInfo struct { + // Chat is the conversation the original message belongs to. Available + // only if the chat is a supergroup or a channel. + // + // optional + Chat *Chat `json:"chat,omitempty"` + // MessageID is the unique message identifier inside the original chat. + // Available only if the original chat is a supergroup or a channel. + // + // optional + MessageID int `json:"message_id,omitempty"` + // Animation is set if the message is an animation. + // + // optional + Animation *Animation `json:"animation,omitempty"` + // Audio is set if the message is an audio file. + // + // optional + Audio *Audio `json:"audio,omitempty"` + // Document is set if the message is a general file. + // + // optional + Document *Document `json:"document,omitempty"` + // Photo is set if the message is a photo. + // + // optional + Photo []PhotoSize `json:"photo,omitempty"` + // Sticker is set if the message is a sticker. + // + // optional + Sticker *Sticker `json:"sticker,omitempty"` + // Story is set if the message is a forwarded story. + // + // optional + Story *Story `json:"story,omitempty"` + // Video is set if the message is a video. + // + // optional + Video *Video `json:"video,omitempty"` + // VideoNote is set if the message is a video note. + // + // optional + VideoNote *VideoNote `json:"video_note,omitempty"` + // Voice is set if the message is a voice message. + // + // optional + Voice *Voice `json:"voice,omitempty"` + // HasMediaSpoiler is true, if the message media is covered by a spoiler animation. + // + // optional + HasMediaSpoiler bool `json:"has_media_spoiler,omitempty"` + // Contact is set if the message is a shared contact. + // + // optional + Contact *Contact `json:"contact,omitempty"` + // Dice is set if the message is a dice with random value. + // + // optional + Dice *Dice `json:"dice,omitempty"` + // Game is set if the message is a game. + // + // optional + Game *Game `json:"game,omitempty"` + // Invoice is set if the message is an invoice for a payment. + // + // optional + Invoice *Invoice `json:"invoice,omitempty"` + // Location is set if the message is a shared location. + // + // optional + Location *Location `json:"location,omitempty"` + // Poll is set if the message is a native poll. + // + // optional + Poll *Poll `json:"poll,omitempty"` + // Venue is set if the message is a venue. + // + // optional + Venue *Venue `json:"venue,omitempty"` +} + +// ReplyParameters describes reply parameters for the message that is being sent. +type ReplyParameters struct { + // MessageID is the identifier of the message that will be replied to in + // the current chat, or in the chat ChatID if it is specified. + MessageID int `json:"message_id"` + // ChatID, if the message to be replied to is from a different chat, is + // the unique identifier for the chat or username of the channel + // (@channelusername). Pass int64 or string. + // + // optional + ChatID any `json:"chat_id,omitempty"` + // AllowSendingWithoutReply is true if the message should be sent even if + // the specified message to be replied to is not found. + // + // optional + AllowSendingWithoutReply bool `json:"allow_sending_without_reply,omitempty"` + // Quote is the quoted part of the message to be replied to. Should be a + // substring of the original message, including bold, italic, underline, + // strikethrough, spoiler, and custom_emoji entities. + // + // optional + Quote string `json:"quote,omitempty"` + // QuoteParseMode is the mode for parsing entities in the quote. + // + // optional + QuoteParseMode string `json:"quote_parse_mode,omitempty"` + // QuoteEntities is a list of special entities that appear in the quote. + // It can be specified instead of QuoteParseMode. + // + // optional + QuoteEntities []MessageEntity `json:"quote_entities,omitempty"` + // QuotePosition is the position of the quote in the original message in + // UTF-16 code units. + // + // optional + QuotePosition int `json:"quote_position,omitempty"` +} + // ChatLocation represents a location to which a chat is connected. type ChatLocation struct { // Location is the location to which the supergroup is connected. Can't be a From 7120fd5a89ae41ccd35f3ef88f56e07452fccaa0 Mon Sep 17 00:00:00 2001 From: Kirill Date: Sat, 11 Apr 2026 04:01:23 +0400 Subject: [PATCH 18/65] Support of link preview customization in API 7.0 --- configs.go | 28 ++++++++++++++++------------ helpers.go | 3 +-- types.go | 46 ++++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 61 insertions(+), 16 deletions(-) diff --git a/configs.go b/configs.go index c7e1ce11..9a40b766 100644 --- a/configs.go +++ b/configs.go @@ -341,10 +341,10 @@ func (edit BaseEdit) params() (Params, error) { // MessageConfig contains information about a SendMessage request. type MessageConfig struct { BaseChat - Text string - ParseMode string - Entities []MessageEntity - DisableWebPagePreview bool + Text string + ParseMode string + Entities []MessageEntity + LinkPreviewOptions *LinkPreviewOptions } func (config MessageConfig) params() (Params, error) { @@ -354,9 +354,11 @@ func (config MessageConfig) params() (Params, error) { } params.AddNonEmpty("text", config.Text) - params.AddBool("disable_web_page_preview", config.DisableWebPagePreview) params.AddNonEmpty("parse_mode", config.ParseMode) - err = params.AddInterface("entities", config.Entities) + if err = params.AddAny("link_preview_options", config.LinkPreviewOptions); err != nil { + return params, err + } + err = params.AddAny("entities", config.Entities) return params, err } @@ -1024,10 +1026,10 @@ func (config ChatActionConfig) method() string { // EditMessageTextConfig allows you to modify the text in a message. type EditMessageTextConfig struct { BaseEdit - Text string - ParseMode string - Entities []MessageEntity - DisableWebPagePreview bool + Text string + ParseMode string + Entities []MessageEntity + LinkPreviewOptions *LinkPreviewOptions } func (config EditMessageTextConfig) params() (Params, error) { @@ -1038,8 +1040,10 @@ func (config EditMessageTextConfig) params() (Params, error) { params["text"] = config.Text params.AddNonEmpty("parse_mode", config.ParseMode) - params.AddBool("disable_web_page_preview", config.DisableWebPagePreview) - err = params.AddInterface("entities", config.Entities) + if err = params.AddAny("link_preview_options", config.LinkPreviewOptions); err != nil { + return params, err + } + err = params.AddAny("entities", config.Entities) return params, err } diff --git a/helpers.go b/helpers.go index 683ea131..3e09be44 100644 --- a/helpers.go +++ b/helpers.go @@ -19,8 +19,7 @@ func NewMessage(chatID int64, text string) MessageConfig { BaseChat: BaseChat{ ChatID: chatID, }, - Text: text, - DisableWebPagePreview: false, + Text: text, } } diff --git a/types.go b/types.go index e1de2329..1c2b0a83 100644 --- a/types.go +++ b/types.go @@ -557,6 +557,11 @@ type Message struct { // // optional Entities []MessageEntity `json:"entities,omitempty"` + // LinkPreviewOptions are options used for link preview generation for the + // message, if it is a text message and link preview options were changed. + // + // optional + LinkPreviewOptions *LinkPreviewOptions `json:"link_preview_options,omitempty"` // Animation message is an animation, information about the animation. // For backward compatibility, when this field is set, the document field will also be set; // @@ -2373,6 +2378,11 @@ type ExternalReplyInfo struct { // // optional MessageID int `json:"message_id,omitempty"` + // LinkPreviewOptions are options used for link preview generation for the + // original message, if it is a text message. + // + // optional + LinkPreviewOptions *LinkPreviewOptions `json:"link_preview_options,omitempty"` // Animation is set if the message is an animation. // // optional @@ -2443,6 +2453,37 @@ type ExternalReplyInfo struct { Venue *Venue `json:"venue,omitempty"` } +// LinkPreviewOptions describes the options used for link preview generation. +type LinkPreviewOptions struct { + // IsDisabled is true, if the link preview is disabled. + // + // optional + IsDisabled bool `json:"is_disabled,omitempty"` + // URL to use for the link preview. If empty, then the first URL found in + // the message text will be used. + // + // optional + URL string `json:"url,omitempty"` + // PreferSmallMedia is true, if the media in the link preview is supposed + // to be shrunk; ignored if the URL isn't explicitly specified or media + // size change isn't supported for the preview. + // + // optional + PreferSmallMedia bool `json:"prefer_small_media,omitempty"` + // PreferLargeMedia is true, if the media in the link preview is supposed + // to be enlarged; ignored if the URL isn't explicitly specified or media + // size change isn't supported for the preview. + // + // optional + PreferLargeMedia bool `json:"prefer_large_media,omitempty"` + // ShowAboveText is true, if the link preview must be shown above the + // message text; otherwise, the link preview will be shown below the + // message text. + // + // optional + ShowAboveText bool `json:"show_above_text,omitempty"` +} + // ReplyParameters describes reply parameters for the message that is being sent. type ReplyParameters struct { // MessageID is the identifier of the message that will be replied to in @@ -3850,10 +3891,11 @@ type InputTextMessageContent struct { // // optional Entities []MessageEntity `json:"entities,omitempty"` - // DisableWebPagePreview disables link previews for links in the sent message + // LinkPreviewOptions are options used for link preview generation for the + // message. // // optional - DisableWebPagePreview bool `json:"disable_web_page_preview,omitempty"` + LinkPreviewOptions *LinkPreviewOptions `json:"link_preview_options,omitempty"` } // InputLocationMessageContent contains a location for displaying From 07b04294785ac19c07df3c1fca1616c46be467c6 Mon Sep 17 00:00:00 2001 From: Kirill Date: Sat, 11 Apr 2026 04:02:37 +0400 Subject: [PATCH 19/65] Update MessageEntity docs for block quotation support in API 7.0 --- types.go | 1 + 1 file changed, 1 insertion(+) diff --git a/types.go b/types.go index 1c2b0a83..c40da419 100644 --- a/types.go +++ b/types.go @@ -894,6 +894,7 @@ type MessageEntity struct { // “underline” (underlined text), // “strikethrough” (strikethrough text), // "spoiler" (spoiler message), + // “blockquote” (block quotation), // “code” (monowidth string), // “pre” (monowidth block), // “text_link” (for clickable text URLs), From 4683ae2810ceb3c44b48eda520e7e13f17f3738e Mon Sep 17 00:00:00 2001 From: Kirill Date: Sat, 11 Apr 2026 04:06:03 +0400 Subject: [PATCH 20/65] Support of multiple message actions in API 7.0 --- bot.go | 28 ++++++++++++++++ configs.go | 97 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 125 insertions(+) diff --git a/bot.go b/bot.go index df96cf22..446356e4 100644 --- a/bot.go +++ b/bot.go @@ -687,6 +687,34 @@ func (bot *BotAPI) GetForumTopicIconStickers() ([]Sticker, error) { return stickers, err } +// ForwardMessages forwards multiple messages of any kind and returns the +// identifiers of the sent messages. +func (bot *BotAPI) ForwardMessages(config ForwardMessagesConfig) ([]MessageID, error) { + resp, err := bot.Request(config) + if err != nil { + return nil, err + } + + var ids []MessageID + err = json.Unmarshal(resp.Result, &ids) + + return ids, err +} + +// CopyMessages copies messages of any kind and returns the identifiers of +// the sent messages. +func (bot *BotAPI) CopyMessages(config CopyMessagesConfig) ([]MessageID, error) { + resp, err := bot.Request(config) + if err != nil { + return nil, err + } + + var ids []MessageID + err = json.Unmarshal(resp.Result, &ids) + + return ids, err +} + // GetMyName returns the current bot name for the given user language. func (bot *BotAPI) GetMyName(config GetMyNameConfig) (BotName, error) { resp, err := bot.Request(config) diff --git a/configs.go b/configs.go index 9a40b766..ca8749ee 100644 --- a/configs.go +++ b/configs.go @@ -1962,6 +1962,103 @@ func (config DeleteMessageConfig) params() (Params, error) { return params, nil } +// DeleteMessagesConfig deletes multiple messages simultaneously. If some of +// the specified messages can't be found, they are skipped. +type DeleteMessagesConfig struct { + ChatID int64 + ChannelUsername string + MessageIDs []int // 1-100 message identifiers +} + +func (config DeleteMessagesConfig) method() string { + return "deleteMessages" +} + +func (config DeleteMessagesConfig) params() (Params, error) { + params := make(Params) + + if err := params.AddFirstValid("chat_id", config.ChatID, config.ChannelUsername); err != nil { + return params, err + } + err := params.AddAny("message_ids", config.MessageIDs) + + return params, err +} + +// ForwardMessagesConfig forwards multiple messages of any kind. If some of the +// specified messages can't be found or forwarded, they are skipped. Service +// messages and messages with protected content can't be forwarded. Album +// grouping is kept for forwarded messages. +type ForwardMessagesConfig struct { + ChatID int64 + ChannelUsername string + MessageThreadID int + FromChatID int64 + FromChannelUsername string + MessageIDs []int // 1-100 message identifiers, must be in strictly increasing order + DisableNotification bool + ProtectContent bool +} + +func (config ForwardMessagesConfig) method() string { + return "forwardMessages" +} + +func (config ForwardMessagesConfig) params() (Params, error) { + params := make(Params) + + if err := params.AddFirstValid("chat_id", config.ChatID, config.ChannelUsername); err != nil { + return params, err + } + if err := params.AddFirstValid("from_chat_id", config.FromChatID, config.FromChannelUsername); err != nil { + return params, err + } + params.AddNonZero("message_thread_id", config.MessageThreadID) + params.AddBool("disable_notification", config.DisableNotification) + params.AddBool("protect_content", config.ProtectContent) + err := params.AddAny("message_ids", config.MessageIDs) + + return params, err +} + +// CopyMessagesConfig copies messages of any kind. If some of the specified +// messages can't be found or copied, they are skipped. Service messages, +// giveaway messages, giveaway winners messages, and invoice messages can't +// be copied. Album grouping is kept for copied messages. +type CopyMessagesConfig struct { + ChatID int64 + ChannelUsername string + MessageThreadID int + FromChatID int64 + FromChannelUsername string + MessageIDs []int // 1-100 message identifiers, must be in strictly increasing order + DisableNotification bool + ProtectContent bool + RemoveCaption bool +} + +func (config CopyMessagesConfig) method() string { + return "copyMessages" +} + +func (config CopyMessagesConfig) params() (Params, error) { + params := make(Params) + + if err := params.AddFirstValid("chat_id", config.ChatID, config.ChannelUsername); err != nil { + return params, err + } + if err := params.AddFirstValid("from_chat_id", config.FromChatID, config.FromChannelUsername); err != nil { + return params, err + } + params.AddNonZero("message_thread_id", config.MessageThreadID) + params.AddBool("disable_notification", config.DisableNotification) + params.AddBool("protect_content", config.ProtectContent) + params.AddBool("remove_caption", config.RemoveCaption) + err := params.AddAny("message_ids", config.MessageIDs) + + return params, err +} + // SetMessageReactionConfig sets the bot's reaction to a message. Pass an // empty Reaction to remove all reactions from the message. type SetMessageReactionConfig struct { From 5ef148173323dd82c5e400d39f3397f3fb09b339 Mon Sep 17 00:00:00 2001 From: Kirill Date: Sat, 11 Apr 2026 04:11:44 +0400 Subject: [PATCH 21/65] Support of request for multiple users in API 7.0 --- types.go | 48 +++++++++++++++++++++++++----------------------- 1 file changed, 25 insertions(+), 23 deletions(-) diff --git a/types.go b/types.go index c40da419..edf9ed46 100644 --- a/types.go +++ b/types.go @@ -208,9 +208,7 @@ func (ch UpdatesChannel) Clear() { type User struct { // ID is a unique identifier for this user or bot ID int64 `json:"id"` - // IsBot true, if this user is a bot - // - // optional + // IsBot is true if this user is a bot. IsBot bool `json:"is_bot,omitempty"` // IsPremium true, if user has Telegram Premium // @@ -751,10 +749,10 @@ type Message struct { // // optional WebAppData *WebAppData `json:"web_app_data,omitempty"` - // UserShared is a service message: a user was shared with the bot. + // UsersShared is a service message: users were shared with the bot. // // optional - UserShared *UserShared `json:"user_shared,omitempty"` + UsersShared *UsersShared `json:"users_shared,omitempty"` // ChatShared is a service message: a chat was shared with the bot. // // optional @@ -1513,12 +1511,12 @@ type KeyboardButton struct { // Text of the button. If none of the optional fields are used, // it will be sent as a message when the button is pressed. Text string `json:"text"` - // RequestUser if specified, pressing the button will open a list of - // suitable users. Identifier of the selected user will be shared with the - // bot in a "user_shared" service message. Available in private chats only. + // RequestUsers if specified, pressing the button will open a list of + // suitable users. Identifiers of the selected users will be shared with the + // bot in a "users_shared" service message. Available in private chats only. // // optional - RequestUser *KeyboardButtonRequestUser `json:"request_user,omitempty"` + RequestUsers *KeyboardButtonRequestUsers `json:"request_users,omitempty"` // RequestChat if specified, pressing the button will open a list of // suitable chats. Tapping on a chat will send its identifier to the bot in // a "chat_shared" service message. Available in private chats only. @@ -1556,23 +1554,27 @@ type KeyboardButton struct { RequestManagedBot *KeyboardButtonRequestManagedBot `json:"request_managed_bot,omitempty"` } -// KeyboardButtonRequestUser defines the criteria used to request a suitable -// user. The identifier of the selected user will be shared with the bot when -// the corresponding button is pressed. -type KeyboardButtonRequestUser struct { +// KeyboardButtonRequestUsers defines the criteria used to request suitable +// users. The identifiers of the selected users will be shared with the bot +// when the corresponding button is pressed. +type KeyboardButtonRequestUsers struct { // RequestID is a signed 32-bit identifier of the request, which will be - // received back in the UserShared object. Must be unique within the message. + // received back in the UsersShared object. Must be unique within the message. RequestID int `json:"request_id"` - // UserIsBot pass True to request a bot, pass False to request a regular - // user. If not specified, no additional restrictions are applied. + // UserIsBot pass True to request bots, pass False to request regular + // users. If not specified, no additional restrictions are applied. // // optional UserIsBot *bool `json:"user_is_bot,omitempty"` - // UserIsPremium pass True to request a premium user, pass False to request - // a non-premium user. If not specified, no additional restrictions are applied. + // UserIsPremium pass True to request premium users, pass False to request + // non-premium users. If not specified, no additional restrictions are applied. // // optional UserIsPremium *bool `json:"user_is_premium,omitempty"` + // MaxQuantity is the maximum number of users to be selected; 1-10. Defaults to 1. + // + // optional + MaxQuantity int `json:"max_quantity,omitempty"` } // KeyboardButtonRequestChat defines the criteria used to request a suitable @@ -1620,13 +1622,13 @@ type KeyboardButtonRequestChat struct { BotIsMember bool `json:"bot_is_member,omitempty"` } -// UserShared contains information about the user whose identifier was shared -// with the bot using a KeyboardButtonRequestUser button. -type UserShared struct { +// UsersShared contains information about the users whose identifiers were +// shared with the bot using a KeyboardButtonRequestUsers button. +type UsersShared struct { // RequestID is the identifier of the request. RequestID int `json:"request_id"` - // UserID is the identifier of the shared user. - UserID int64 `json:"user_id"` + // UserIDs are the identifiers of the shared users. + UserIDs []int64 `json:"user_ids"` } // ChatShared contains information about the chat whose identifier was shared From 7256960278583045a5815453572e1b7a0afa5c23 Mon Sep 17 00:00:00 2001 From: Kirill Date: Sat, 11 Apr 2026 04:15:39 +0400 Subject: [PATCH 22/65] Support of chat boost in API 7.0 --- bot.go | 13 +++++++++ configs.go | 33 ++++++++++++++++++++++ types.go | 82 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 128 insertions(+) diff --git a/bot.go b/bot.go index 446356e4..fb2011e5 100644 --- a/bot.go +++ b/bot.go @@ -687,6 +687,19 @@ func (bot *BotAPI) GetForumTopicIconStickers() ([]Sticker, error) { return stickers, err } +// GetUserChatBoosts returns the list of boosts added to a chat by a user. +func (bot *BotAPI) GetUserChatBoosts(config GetUserChatBoostsConfig) (UserChatBoosts, error) { + resp, err := bot.Request(config) + if err != nil { + return UserChatBoosts{}, err + } + + var boosts UserChatBoosts + err = json.Unmarshal(resp.Result, &boosts) + + return boosts, err +} + // ForwardMessages forwards multiple messages of any kind and returns the // identifiers of the sent messages. func (bot *BotAPI) ForwardMessages(config ForwardMessagesConfig) ([]MessageID, error) { diff --git a/configs.go b/configs.go index ca8749ee..f4f242a0 100644 --- a/configs.go +++ b/configs.go @@ -92,6 +92,12 @@ const ( // UpdateTypeMessageReactionCount is when reactions to a message with anonymous reactions were changed. UpdateTypeMessageReactionCount = "message_reaction_count" + // UpdateTypeChatBoost is when a boost was added to a chat or changed. + UpdateTypeChatBoost = "chat_boost" + + // UpdateTypeRemovedChatBoost is when a boost was removed from a chat. + UpdateTypeRemovedChatBoost = "removed_chat_boost" + // UpdateTypeMyChatMember is when the bot's chat member status was updated in a chat. For private chats, this // update is received only when the bot is blocked or unblocked by the user. UpdateTypeMyChatMember = "my_chat_member" @@ -2087,6 +2093,33 @@ func (config SetMessageReactionConfig) params() (Params, error) { return params, err } +// GetUserChatBoostsConfig returns the list of boosts added to a chat by a +// user. The bot must be an administrator in the chat. +// +// Provide the target chat via either ChatID (numeric identifier) or +// ChannelUsername ("@channelusername"); the first non-zero / non-empty +// value is used. +type GetUserChatBoostsConfig struct { + ChatID int64 + ChannelUsername string + UserID int64 +} + +func (config GetUserChatBoostsConfig) method() string { + return "getUserChatBoosts" +} + +func (config GetUserChatBoostsConfig) params() (Params, error) { + params := make(Params) + + if err := params.AddFirstValid("chat_id", config.ChatID, config.ChannelUsername); err != nil { + return params, err + } + params.AddNonZero64("user_id", config.UserID) + + return params, nil +} + // PinChatMessageConfig contains information of a message in a chat to pin. type PinChatMessageConfig struct { ChatID int64 diff --git a/types.go b/types.go index edf9ed46..4b574a88 100644 --- a/types.go +++ b/types.go @@ -118,6 +118,16 @@ type Update struct { // // optional MessageReactionCount *MessageReactionCountUpdated `json:"message_reaction_count,omitempty"` + // ChatBoost is a boost added to a chat or changed. The bot must be an + // administrator in the chat to receive these updates. + // + // optional + ChatBoost *ChatBoostUpdated `json:"chat_boost,omitempty"` + // RemovedChatBoost is a boost removed from a chat. The bot must be an + // administrator in the chat to receive these updates. + // + // optional + RemovedChatBoost *ChatBoostRemoved `json:"removed_chat_boost,omitempty"` // MyChatMember is the bot's chat member status was updated in a chat. For // private chats, this update is received only when the bot is blocked or // unblocked by the user. @@ -2456,6 +2466,78 @@ type ExternalReplyInfo struct { Venue *Venue `json:"venue,omitempty"` } +// Chat boost source constants. +const ( + ChatBoostSourcePremium = "premium" + ChatBoostSourceGiftCode = "gift_code" + ChatBoostSourceGiveaway = "giveaway" +) + +// ChatBoostSource describes the source of a chat boost. The Source field +// discriminates between concrete variants: +// - "premium" → User is set (the user that boosted the chat) +// - "gift_code" → User is set (the user for which the gift code was created) +// - "giveaway" → GiveawayMessageID is set; User is set for the prize winner; +// IsUnclaimed is true if the giveaway prize wasn't claimed +type ChatBoostSource struct { + // Source of the boost. One of "premium", "gift_code", "giveaway". + Source string `json:"source"` + // User that boosted the chat. Set for "premium" and "gift_code"; for + // "giveaway" set to the user that won the prize, if any. + // + // optional + User *User `json:"user,omitempty"` + // GiveawayMessageID is the identifier of the message with the giveaway in + // the chat. Set for "giveaway". + // + // optional + GiveawayMessageID int `json:"giveaway_message_id,omitempty"` + // IsUnclaimed is true if the giveaway was completed but no user won the + // prize. Set for "giveaway". + // + // optional + IsUnclaimed bool `json:"is_unclaimed,omitempty"` +} + +// ChatBoost contains information about a chat boost. +type ChatBoost struct { + // BoostID is the unique identifier of the boost. + BoostID string `json:"boost_id"` + // AddDate is the point in time (Unix timestamp) when the chat was boosted. + AddDate int `json:"add_date"` + // ExpirationDate is the point in time (Unix timestamp) when the boost + // will automatically expire, unless it's renewed by the premium subscriber. + ExpirationDate int `json:"expiration_date"` + // Source of the added boost. + Source ChatBoostSource `json:"source"` +} + +// ChatBoostUpdated represents a boost added to a chat or changed. +type ChatBoostUpdated struct { + // Chat which was boosted. + Chat Chat `json:"chat"` + // Boost is information about the chat boost. + Boost ChatBoost `json:"boost"` +} + +// ChatBoostRemoved represents a boost removed from a chat. +type ChatBoostRemoved struct { + // Chat which was boosted. + Chat Chat `json:"chat"` + // BoostID is the unique identifier of the boost. + BoostID string `json:"boost_id"` + // RemoveDate is the point in time (Unix timestamp) when the boost was removed. + RemoveDate int `json:"remove_date"` + // Source of the removed boost. + Source ChatBoostSource `json:"source"` +} + +// UserChatBoosts represents a list of boosts added to a chat by a user. +type UserChatBoosts struct { + // Boosts is the list of boosts added to the chat by the user. + Boosts []ChatBoost `json:"boosts"` +} + // LinkPreviewOptions describes the options used for link preview generation. type LinkPreviewOptions struct { // IsDisabled is true, if the link preview is disabled. From e3ecd75ab4d7ecb4b6eab0e55ea8afaf51a7b3f4 Mon Sep 17 00:00:00 2001 From: Kirill Date: Sat, 11 Apr 2026 04:19:27 +0400 Subject: [PATCH 23/65] Support of giveaway in API 7.0 --- types.go | 128 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 128 insertions(+) diff --git a/types.go b/types.go index 4b574a88..a0830380 100644 --- a/types.go +++ b/types.go @@ -796,6 +796,23 @@ type Message struct { // // optional WriteAccessAllowed *WriteAccessAllowed `json:"write_access_allowed,omitempty"` + // GiveawayCreated is a service message: a scheduled giveaway was created. + // + // optional + GiveawayCreated *GiveawayCreated `json:"giveaway_created,omitempty"` + // Giveaway is a scheduled giveaway message. + // + // optional + Giveaway *Giveaway `json:"giveaway,omitempty"` + // GiveawayWinners is a giveaway with public winners that was completed. + // + // optional + GiveawayWinners *GiveawayWinners `json:"giveaway_winners,omitempty"` + // GiveawayCompleted is a service message about the completion of a + // giveaway without public winners. + // + // optional + GiveawayCompleted *GiveawayCompleted `json:"giveaway_completed,omitempty"` // HasMediaSpoiler is true, if the message media is covered by a spoiler animation // // optional @@ -2448,6 +2465,14 @@ type ExternalReplyInfo struct { // // optional Game *Game `json:"game,omitempty"` + // Giveaway is set if the message is a scheduled giveaway. + // + // optional + Giveaway *Giveaway `json:"giveaway,omitempty"` + // GiveawayWinners is set if the message is a giveaway with public winners. + // + // optional + GiveawayWinners *GiveawayWinners `json:"giveaway_winners,omitempty"` // Invoice is set if the message is an invoice for a payment. // // optional @@ -2538,6 +2563,109 @@ type UserChatBoosts struct { Boosts []ChatBoost `json:"boosts"` } +// Giveaway represents a message about a scheduled giveaway. +type Giveaway struct { + // Chats is the list of chats which the user must join to participate in + // the giveaway. + Chats []Chat `json:"chats"` + // WinnersSelectionDate is the point in time (Unix timestamp) when winners + // of the giveaway will be selected. + WinnersSelectionDate int `json:"winners_selection_date"` + // WinnerCount is the number of users which are supposed to be selected + // as winners of the giveaway. + WinnerCount int `json:"winner_count"` + // OnlyNewMembers is true, if only users who join the chats after the + // giveaway started should be eligible to win. + // + // optional + OnlyNewMembers bool `json:"only_new_members,omitempty"` + // HasPublicWinners is true, if the list of giveaway winners will be + // visible to everyone. + // + // optional + HasPublicWinners bool `json:"has_public_winners,omitempty"` + // PrizeDescription is the description of additional giveaway prize. + // + // optional + PrizeDescription string `json:"prize_description,omitempty"` + // CountryCodes is a list of two-letter ISO 3166-1 alpha-2 country codes + // indicating the countries from which eligible users for the giveaway + // must come. + // + // optional + CountryCodes []string `json:"country_codes,omitempty"` + // PremiumSubscriptionMonthCount is the number of months the Telegram + // Premium subscription won from the giveaway will be active for. + // + // optional + PremiumSubscriptionMonthCount int `json:"premium_subscription_month_count,omitempty"` +} + +// GiveawayCreated represents a service message about the creation of a +// scheduled giveaway. Currently holds no information. +type GiveawayCreated struct{} + +// GiveawayWinners represents a message about the completion of a giveaway +// with public winners. +type GiveawayWinners struct { + // Chat that created the giveaway. + Chat Chat `json:"chat"` + // GiveawayMessageID is the identifier of the message with the giveaway + // in the chat. + GiveawayMessageID int `json:"giveaway_message_id"` + // WinnersSelectionDate is the point in time (Unix timestamp) when winners + // of the giveaway were selected. + WinnersSelectionDate int `json:"winners_selection_date"` + // WinnerCount is the total number of winners in the giveaway. + WinnerCount int `json:"winner_count"` + // Winners is the list of up to 100 winners of the giveaway. + Winners []User `json:"winners"` + // AdditionalChatCount is the number of other chats the user had to join + // in order to be eligible for the giveaway. + // + // optional + AdditionalChatCount int `json:"additional_chat_count,omitempty"` + // PremiumSubscriptionMonthCount is the number of months the Telegram + // Premium subscription won from the giveaway will be active for. + // + // optional + PremiumSubscriptionMonthCount int `json:"premium_subscription_month_count,omitempty"` + // UnclaimedPrizeCount is the number of undistributed prizes. + // + // optional + UnclaimedPrizeCount int `json:"unclaimed_prize_count,omitempty"` + // OnlyNewMembers is true, if only users who joined the chats after the + // giveaway started were eligible to win. + // + // optional + OnlyNewMembers bool `json:"only_new_members,omitempty"` + // WasRefunded is true, if the giveaway was canceled because the payment + // for it was refunded. + // + // optional + WasRefunded bool `json:"was_refunded,omitempty"` + // PrizeDescription is the description of additional giveaway prize. + // + // optional + PrizeDescription string `json:"prize_description,omitempty"` +} + +// GiveawayCompleted represents a service message about the completion of a +// giveaway without public winners. +type GiveawayCompleted struct { + // WinnerCount is the number of winners in the giveaway. + WinnerCount int `json:"winner_count"` + // UnclaimedPrizeCount is the number of undistributed prizes. + // + // optional + UnclaimedPrizeCount int `json:"unclaimed_prize_count,omitempty"` + // GiveawayMessage is the message with the giveaway that was completed, + // if it wasn't deleted. + // + // optional + GiveawayMessage *Message `json:"giveaway_message,omitempty"` +} + // LinkPreviewOptions describes the options used for link preview generation. type LinkPreviewOptions struct { // IsDisabled is true, if the link preview is disabled. From be6a729b05cccc070ad0421503d1007471d1810b Mon Sep 17 00:00:00 2001 From: Kirill Date: Sat, 11 Apr 2026 04:29:11 +0400 Subject: [PATCH 24/65] Full support of API 7.0 --- types.go | 135 +++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 102 insertions(+), 33 deletions(-) diff --git a/types.go b/types.go index a0830380..58e48240 100644 --- a/types.go +++ b/types.go @@ -327,6 +327,34 @@ type Chat struct { // // optional EmojiStatusExpirationDate int64 `json:"emoji_status_expiration_date,omitempty"` + // AccentColorID is the identifier of the accent color for the chat name + // and backgrounds of the chat photo, reply header, and link preview. + // Returned only in getChat. + // + // optional + AccentColorID int `json:"accent_color_id,omitempty"` + // BackgroundCustomEmojiID is the custom emoji identifier of the emoji + // chosen by the chat for the reply header and link preview background. + // Returned only in getChat. + // + // optional + BackgroundCustomEmojiID string `json:"background_custom_emoji_id,omitempty"` + // ProfileAccentColorID is the identifier of the accent color for the + // chat's profile background. Returned only in getChat. + // + // optional + ProfileAccentColorID int `json:"profile_accent_color_id,omitempty"` + // ProfileBackgroundCustomEmojiID is the custom emoji identifier of the + // emoji chosen by the chat for its profile background. Returned only in + // getChat. + // + // optional + ProfileBackgroundCustomEmojiID string `json:"profile_background_custom_emoji_id,omitempty"` + // HasVisibleHistory is true, if new chat members will have access to old + // messages; available only to chat administrators. Returned only in getChat. + // + // optional + HasVisibleHistory bool `json:"has_visible_history,omitempty"` // HasHiddenMembers is true, if non-administrators can only see bots and // administrators in the chat. Returned only in getChat. // @@ -484,34 +512,11 @@ type Message struct { Date int `json:"date"` // Chat is the conversation the message belongs to Chat *Chat `json:"chat"` - // ForwardFrom for forwarded messages, sender of the original message; + // ForwardOrigin is information about the original message for forwarded + // messages. // // optional - ForwardFrom *User `json:"forward_from,omitempty"` - // ForwardFromChat for messages forwarded from channels, - // information about the original channel; - // - // optional - ForwardFromChat *Chat `json:"forward_from_chat,omitempty"` - // ForwardFromMessageID for messages forwarded from channels, - // identifier of the original message in the channel; - // - // optional - ForwardFromMessageID int `json:"forward_from_message_id,omitempty"` - // ForwardSignature for messages forwarded from channels, signature of the - // post author if present - // - // optional - ForwardSignature string `json:"forward_signature,omitempty"` - // ForwardSenderName is the sender's name for messages forwarded from users - // who disallow adding a link to their account in forwarded messages - // - // optional - ForwardSenderName string `json:"forward_sender_name,omitempty"` - // ForwardDate for forwarded messages, date the original message was sent in Unix time; - // - // optional - ForwardDate int `json:"forward_date,omitempty"` + ForwardOrigin *MessageOrigin `json:"forward_origin,omitempty"` // IsAutomaticForward is true if the message is a channel post that was // automatically forwarded to the connected discussion group. // @@ -1883,9 +1888,10 @@ type CallbackQuery struct { ID string `json:"id"` // From sender From *User `json:"from"` - // Message with the callback button that originated the query. - // Note that message content and message date will not be available if the - // message is too old. + // Message with the callback button that originated the query. The message + // can be inaccessible (deleted or otherwise unreachable) — in that case + // only Message.MessageID and Message.Chat are populated and Message.Date + // is 0. See InaccessibleMessage for the equivalent dedicated type. // // optional Message *Message `json:"message,omitempty"` @@ -2393,11 +2399,9 @@ type TextQuote struct { // ExternalReplyInfo contains information about a message that is being replied // to, which may come from another chat or forum topic. -// -// Note: this struct grows across Bot API 7.0 sections — fields like Origin -// (MessageOrigin), LinkPreviewOptions, Giveaway, GiveawayWinners are added -// alongside their respective sections. type ExternalReplyInfo struct { + // Origin is the origin of the message replied to by the given message. + Origin MessageOrigin `json:"origin"` // Chat is the conversation the original message belongs to. Available // only if the chat is a supergroup or a channel. // @@ -2666,6 +2670,71 @@ type GiveawayCompleted struct { GiveawayMessage *Message `json:"giveaway_message,omitempty"` } +// Message origin type constants. +const ( + MessageOriginTypeUser = "user" + MessageOriginTypeHiddenUser = "hidden_user" + MessageOriginTypeChat = "chat" + MessageOriginTypeChannel = "channel" +) + +// MessageOrigin describes the origin of a message. The Type field discriminates +// between concrete variants: +// - "user" → SenderUser is set +// - "hidden_user" → SenderUserName is set +// - "chat" → SenderChat is set; AuthorSignature optional +// - "channel" → Chat and MessageID are set; AuthorSignature optional +type MessageOrigin struct { + // Type of the message origin. One of "user", "hidden_user", "chat", "channel". + Type string `json:"type"` + // Date the message was sent originally in Unix time. + Date int `json:"date"` + // SenderUser is the user that sent the message originally. Set when Type + // is "user". + // + // optional + SenderUser *User `json:"sender_user,omitempty"` + // SenderUserName is the name of the user that sent the message originally. + // Set when Type is "hidden_user". + // + // optional + SenderUserName string `json:"sender_user_name,omitempty"` + // SenderChat is the chat that sent the message originally. Set when Type + // is "chat". + // + // optional + SenderChat *Chat `json:"sender_chat,omitempty"` + // AuthorSignature is the signature of the original message author or + // the post author for messages from a channel. Optional for "chat" and + // "channel". + // + // optional + AuthorSignature string `json:"author_signature,omitempty"` + // Chat is the channel chat to which the message was originally sent. + // Set when Type is "channel". + // + // optional + Chat *Chat `json:"chat,omitempty"` + // MessageID is the unique message identifier inside the chat. Set when + // Type is "channel". + // + // optional + MessageID int `json:"message_id,omitempty"` +} + +// InaccessibleMessage describes a message that was deleted or otherwise +// inaccessible to the bot. +type InaccessibleMessage struct { + // Chat the message belonged to. + Chat Chat `json:"chat"` + // MessageID is the unique message identifier inside the chat. + MessageID int `json:"message_id"` + // Date is always 0. The field is present in order to mimic the Message + // type so that callers can distinguish accessible from inaccessible + // messages by checking Date == 0. + Date int `json:"date"` +} + // LinkPreviewOptions describes the options used for link preview generation. type LinkPreviewOptions struct { // IsDisabled is true, if the link preview is disabled. From 9b64df8c170298bcdebf7e967e7ffed67acba2f8 Mon Sep 17 00:00:00 2001 From: Kirill Date: Sat, 11 Apr 2026 04:30:36 +0400 Subject: [PATCH 25/65] Simplify Message.IsCommand() method --- types.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/types.go b/types.go index 58e48240..50e8ef8c 100644 --- a/types.go +++ b/types.go @@ -841,7 +841,7 @@ func (m *Message) Time() time.Time { // IsCommand returns true if message starts with a "bot_command" entity. func (m *Message) IsCommand() bool { - if m.Entities == nil || len(m.Entities) == 0 { + if len(m.Entities) == 0 { return false } From abfa73f25c3941d2ed04ab4de5eef376d48b8f20 Mon Sep 17 00:00:00 2001 From: Kirill Date: Sat, 11 Apr 2026 04:35:00 +0400 Subject: [PATCH 26/65] Fix tests --- bot_test.go | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/bot_test.go b/bot_test.go index 7c723dc1..5a6ace87 100644 --- a/bot_test.go +++ b/bot_test.go @@ -35,17 +35,21 @@ func (t testLogger) Printf(format string, v ...interface{}) { } func getBot(t *testing.T) (*BotAPI, error) { - bot, err := NewBotAPI(TestToken) - bot.Debug = true - - logger := testLogger{t} - SetLogger(logger) + token := os.Getenv("TELEGRAM_BOT_TOKEN") + if token == "" { + token = TestToken + } + bot, err := NewBotAPI(token) if err != nil { - t.Error(err) + t.Skipf("skipping: NewBotAPI failed (set TELEGRAM_BOT_TOKEN to a real token to run): %v", err) + return nil, err } - return bot, err + bot.Debug = true + SetLogger(testLogger{t}) + + return bot, nil } func TestNewBotAPI_notoken(t *testing.T) { @@ -745,7 +749,7 @@ func ExampleNewWebhook() { } } -func ExampleWebhookHandler() { +func ExampleNewWebhookWithCert() { bot, err := NewBotAPI("MyAwesomeBotToken") if err != nil { panic(err) From fc2cf22ae5e34aa954723d03f254092fd3fc2bca Mon Sep 17 00:00:00 2001 From: Kirill Date: Sat, 11 Apr 2026 04:43:33 +0400 Subject: [PATCH 27/65] Forward parameter error in params() method if any --- configs.go | 164 +++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 123 insertions(+), 41 deletions(-) diff --git a/configs.go b/configs.go index f4f242a0..dcaa3b6e 100644 --- a/configs.go +++ b/configs.go @@ -335,7 +335,9 @@ func (edit BaseEdit) params() (Params, error) { if edit.InlineMessageID != "" { params["inline_message_id"] = edit.InlineMessageID } else { - params.AddFirstValid("chat_id", edit.ChatID, edit.ChannelUsername) + if err := params.AddFirstValid("chat_id", edit.ChatID, edit.ChannelUsername); err != nil { + return params, err + } params.AddNonZero("message_id", edit.MessageID) } @@ -414,7 +416,9 @@ func (config CopyMessageConfig) params() (Params, error) { return params, err } - params.AddFirstValid("from_chat_id", config.FromChatID, config.FromChannelUsername) + if err = params.AddFirstValid("from_chat_id", config.FromChatID, config.FromChannelUsername); err != nil { + return params, err + } params.AddNonZero("message_id", config.MessageID) params.AddNonEmpty("caption", config.Caption) params.AddNonEmpty("parse_mode", config.ParseMode) @@ -972,7 +976,9 @@ func (config SetGameScoreConfig) params() (Params, error) { if config.InlineMessageID != "" { params["inline_message_id"] = config.InlineMessageID } else { - params.AddFirstValid("chat_id", config.ChatID, config.ChannelUsername) + if err := params.AddFirstValid("chat_id", config.ChatID, config.ChannelUsername); err != nil { + return params, err + } params.AddNonZero("message_id", config.MessageID) } @@ -1000,7 +1006,9 @@ func (config GetGameHighScoresConfig) params() (Params, error) { if config.InlineMessageID != "" { params["inline_message_id"] = config.InlineMessageID } else { - params.AddFirstValid("chat_id", config.ChatID, config.ChannelUsername) + if err := params.AddFirstValid("chat_id", config.ChatID, config.ChannelUsername); err != nil { + return params, err + } params.AddNonZero("message_id", config.MessageID) } @@ -1356,7 +1364,9 @@ func (config UnbanChatMemberConfig) method() string { func (config UnbanChatMemberConfig) params() (Params, error) { params := make(Params) - params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername, config.ChannelUsername) + if err := params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername, config.ChannelUsername); err != nil { + return params, err + } params.AddNonZero64("user_id", config.UserID) params.AddBool("only_if_banned", config.OnlyIfBanned) @@ -1377,7 +1387,9 @@ func (config BanChatMemberConfig) method() string { func (config BanChatMemberConfig) params() (Params, error) { params := make(Params) - params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername) + if err := params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername); err != nil { + return params, err + } params.AddNonZero64("user_id", config.UserID) params.AddNonZero64("until_date", config.UntilDate) params.AddBool("revoke_messages", config.RevokeMessages) @@ -1405,7 +1417,9 @@ func (config RestrictChatMemberConfig) method() string { func (config RestrictChatMemberConfig) params() (Params, error) { params := make(Params) - params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername, config.ChannelUsername) + if err := params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername, config.ChannelUsername); err != nil { + return params, err + } params.AddNonZero64("user_id", config.UserID) err := params.AddInterface("permissions", config.Permissions) @@ -1442,7 +1456,9 @@ func (config PromoteChatMemberConfig) method() string { func (config PromoteChatMemberConfig) params() (Params, error) { params := make(Params) - params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername, config.ChannelUsername) + if err := params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername, config.ChannelUsername); err != nil { + return params, err + } params.AddNonZero64("user_id", config.UserID) params.AddBool("is_anonymous", config.IsAnonymous) @@ -1478,7 +1494,9 @@ func (SetChatAdministratorCustomTitle) method() string { func (config SetChatAdministratorCustomTitle) params() (Params, error) { params := make(Params) - params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername, config.ChannelUsername) + if err := params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername, config.ChannelUsername); err != nil { + return params, err + } params.AddNonZero64("user_id", config.UserID) params.AddNonEmpty("custom_title", config.CustomTitle) @@ -1504,7 +1522,9 @@ func (config BanChatSenderChatConfig) method() string { func (config BanChatSenderChatConfig) params() (Params, error) { params := make(Params) - _ = params.AddFirstValid("chat_id", config.ChatID, config.ChannelUsername) + if err := params.AddFirstValid("chat_id", config.ChatID, config.ChannelUsername); err != nil { + return params, err + } params.AddNonZero64("sender_chat_id", config.SenderChatID) params.AddNonZero("until_date", config.UntilDate) @@ -1527,7 +1547,9 @@ func (config UnbanChatSenderChatConfig) method() string { func (config UnbanChatSenderChatConfig) params() (Params, error) { params := make(Params) - _ = params.AddFirstValid("chat_id", config.ChatID, config.ChannelUsername) + if err := params.AddFirstValid("chat_id", config.ChatID, config.ChannelUsername); err != nil { + return params, err + } params.AddNonZero64("sender_chat_id", config.SenderChatID) return params, nil @@ -1542,7 +1564,9 @@ type ChatConfig struct { func (config ChatConfig) params() (Params, error) { params := make(Params) - params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername) + if err := params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername); err != nil { + return params, err + } return params, nil } @@ -1590,7 +1614,9 @@ func (SetChatPermissionsConfig) method() string { func (config SetChatPermissionsConfig) params() (Params, error) { params := make(Params) - params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername) + if err := params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername); err != nil { + return params, err + } err := params.AddInterface("permissions", config.Permissions) params.AddBool("use_independent_chat_permissions", config.UseIndependentChatPermissions) @@ -1611,7 +1637,9 @@ func (ChatInviteLinkConfig) method() string { func (config ChatInviteLinkConfig) params() (Params, error) { params := make(Params) - params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername) + if err := params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername); err != nil { + return params, err + } return params, nil } @@ -1636,7 +1664,9 @@ func (config CreateChatInviteLinkConfig) params() (Params, error) { params := make(Params) params.AddNonEmpty("name", config.Name) - params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername) + if err := params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername); err != nil { + return params, err + } params.AddNonZero("expire_date", config.ExpireDate) params.AddNonZero("member_limit", config.MemberLimit) params.AddBool("creates_join_request", config.CreatesJoinRequest) @@ -1663,7 +1693,9 @@ func (EditChatInviteLinkConfig) method() string { func (config EditChatInviteLinkConfig) params() (Params, error) { params := make(Params) - params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername) + if err := params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername); err != nil { + return params, err + } params.AddNonEmpty("name", config.Name) params["invite_link"] = config.InviteLink params.AddNonZero("expire_date", config.ExpireDate) @@ -1689,7 +1721,9 @@ func (RevokeChatInviteLinkConfig) method() string { func (config RevokeChatInviteLinkConfig) params() (Params, error) { params := make(Params) - params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername) + if err := params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername); err != nil { + return params, err + } params["invite_link"] = config.InviteLink return params, nil @@ -1708,7 +1742,9 @@ func (ApproveChatJoinRequestConfig) method() string { func (config ApproveChatJoinRequestConfig) params() (Params, error) { params := make(Params) - params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername) + if err := params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername); err != nil { + return params, err + } params.AddNonZero("user_id", int(config.UserID)) return params, nil @@ -1727,7 +1763,9 @@ func (DeclineChatJoinRequest) method() string { func (config DeclineChatJoinRequest) params() (Params, error) { params := make(Params) - params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername) + if err := params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername); err != nil { + return params, err + } params.AddNonZero("user_id", int(config.UserID)) return params, nil @@ -1746,7 +1784,9 @@ func (config LeaveChatConfig) method() string { func (config LeaveChatConfig) params() (Params, error) { params := make(Params) - params.AddFirstValid("chat_id", config.ChatID, config.ChannelUsername) + if err := params.AddFirstValid("chat_id", config.ChatID, config.ChannelUsername); err != nil { + return params, err + } return params, nil } @@ -1761,7 +1801,9 @@ type ChatConfigWithUser struct { func (config ChatConfigWithUser) params() (Params, error) { params := make(Params) - params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername) + if err := params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername); err != nil { + return params, err + } params.AddNonZero64("user_id", config.UserID) return params, nil @@ -1962,7 +2004,9 @@ func (config DeleteMessageConfig) method() string { func (config DeleteMessageConfig) params() (Params, error) { params := make(Params) - params.AddFirstValid("chat_id", config.ChatID, config.ChannelUsername) + if err := params.AddFirstValid("chat_id", config.ChatID, config.ChannelUsername); err != nil { + return params, err + } params.AddNonZero("message_id", config.MessageID) return params, nil @@ -2135,7 +2179,9 @@ func (config PinChatMessageConfig) method() string { func (config PinChatMessageConfig) params() (Params, error) { params := make(Params) - params.AddFirstValid("chat_id", config.ChatID, config.ChannelUsername) + if err := params.AddFirstValid("chat_id", config.ChatID, config.ChannelUsername); err != nil { + return params, err + } params.AddNonZero("message_id", config.MessageID) params.AddBool("disable_notification", config.DisableNotification) @@ -2158,7 +2204,9 @@ func (config UnpinChatMessageConfig) method() string { func (config UnpinChatMessageConfig) params() (Params, error) { params := make(Params) - params.AddFirstValid("chat_id", config.ChatID, config.ChannelUsername) + if err := params.AddFirstValid("chat_id", config.ChatID, config.ChannelUsername); err != nil { + return params, err + } params.AddNonZero("message_id", config.MessageID) return params, nil @@ -2178,7 +2226,9 @@ func (config UnpinAllChatMessagesConfig) method() string { func (config UnpinAllChatMessagesConfig) params() (Params, error) { params := make(Params) - params.AddFirstValid("chat_id", config.ChatID, config.ChannelUsername) + if err := params.AddFirstValid("chat_id", config.ChatID, config.ChannelUsername); err != nil { + return params, err + } return params, nil } @@ -2201,7 +2251,9 @@ func (config CreateForumTopicConfig) method() string { func (config CreateForumTopicConfig) params() (Params, error) { params := make(Params) - params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername) + if err := params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername); err != nil { + return params, err + } params["name"] = config.Name params.AddNonZero("icon_color", config.IconColor) params.AddNonEmpty("icon_custom_emoji_id", config.IconCustomEmojiID) @@ -2227,7 +2279,9 @@ func (config EditForumTopicConfig) method() string { func (config EditForumTopicConfig) params() (Params, error) { params := make(Params) - params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername) + if err := params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername); err != nil { + return params, err + } params.AddNonZero("message_thread_id", config.MessageThreadID) params.AddNonEmpty("name", config.Name) params.AddNonEmpty("icon_custom_emoji_id", config.IconCustomEmojiID) @@ -2251,7 +2305,9 @@ func (config CloseForumTopicConfig) method() string { func (config CloseForumTopicConfig) params() (Params, error) { params := make(Params) - params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername) + if err := params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername); err != nil { + return params, err + } params.AddNonZero("message_thread_id", config.MessageThreadID) return params, nil @@ -2273,7 +2329,9 @@ func (config ReopenForumTopicConfig) method() string { func (config ReopenForumTopicConfig) params() (Params, error) { params := make(Params) - params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername) + if err := params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername); err != nil { + return params, err + } params.AddNonZero("message_thread_id", config.MessageThreadID) return params, nil @@ -2295,7 +2353,9 @@ func (config DeleteForumTopicConfig) method() string { func (config DeleteForumTopicConfig) params() (Params, error) { params := make(Params) - params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername) + if err := params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername); err != nil { + return params, err + } params.AddNonZero("message_thread_id", config.MessageThreadID) return params, nil @@ -2317,7 +2377,9 @@ func (config UnpinAllForumTopicMessagesConfig) method() string { func (config UnpinAllForumTopicMessagesConfig) params() (Params, error) { params := make(Params) - params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername) + if err := params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername); err != nil { + return params, err + } params.AddNonZero("message_thread_id", config.MessageThreadID) return params, nil @@ -2352,7 +2414,9 @@ func (config EditGeneralForumTopicConfig) method() string { func (config EditGeneralForumTopicConfig) params() (Params, error) { params := make(Params) - params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername) + if err := params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername); err != nil { + return params, err + } params["name"] = config.Name return params, nil @@ -2372,7 +2436,9 @@ func (config CloseGeneralForumTopicConfig) method() string { func (config CloseGeneralForumTopicConfig) params() (Params, error) { params := make(Params) - params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername) + if err := params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername); err != nil { + return params, err + } return params, nil } @@ -2392,7 +2458,9 @@ func (config ReopenGeneralForumTopicConfig) method() string { func (config ReopenGeneralForumTopicConfig) params() (Params, error) { params := make(Params) - params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername) + if err := params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername); err != nil { + return params, err + } return params, nil } @@ -2412,7 +2480,9 @@ func (config HideGeneralForumTopicConfig) method() string { func (config HideGeneralForumTopicConfig) params() (Params, error) { params := make(Params) - params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername) + if err := params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername); err != nil { + return params, err + } return params, nil } @@ -2431,7 +2501,9 @@ func (config UnhideGeneralForumTopicConfig) method() string { func (config UnhideGeneralForumTopicConfig) params() (Params, error) { params := make(Params) - params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername) + if err := params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername); err != nil { + return params, err + } return params, nil } @@ -2487,7 +2559,9 @@ func (config DeleteChatPhotoConfig) method() string { func (config DeleteChatPhotoConfig) params() (Params, error) { params := make(Params) - params.AddFirstValid("chat_id", config.ChatID, config.ChannelUsername) + if err := params.AddFirstValid("chat_id", config.ChatID, config.ChannelUsername); err != nil { + return params, err + } return params, nil } @@ -2507,7 +2581,9 @@ func (config SetChatTitleConfig) method() string { func (config SetChatTitleConfig) params() (Params, error) { params := make(Params) - params.AddFirstValid("chat_id", config.ChatID, config.ChannelUsername) + if err := params.AddFirstValid("chat_id", config.ChatID, config.ChannelUsername); err != nil { + return params, err + } params["title"] = config.Title return params, nil @@ -2528,7 +2604,9 @@ func (config SetChatDescriptionConfig) method() string { func (config SetChatDescriptionConfig) params() (Params, error) { params := make(Params) - params.AddFirstValid("chat_id", config.ChatID, config.ChannelUsername) + if err := params.AddFirstValid("chat_id", config.ChatID, config.ChannelUsername); err != nil { + return params, err + } params["description"] = config.Description return params, nil @@ -2890,7 +2968,9 @@ func (config SetChatStickerSetConfig) method() string { func (config SetChatStickerSetConfig) params() (Params, error) { params := make(Params) - params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername) + if err := params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername); err != nil { + return params, err + } params["sticker_set_name"] = config.StickerSetName return params, nil @@ -2909,7 +2989,9 @@ func (config DeleteChatStickerSetConfig) method() string { func (config DeleteChatStickerSetConfig) params() (Params, error) { params := make(Params) - params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername) + if err := params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername); err != nil { + return params, err + } return params, nil } From 4854fb0df0cf1fe7273e4e68131856c612d53752 Mon Sep 17 00:00:00 2001 From: Kirill Date: Sat, 11 Apr 2026 04:54:25 +0400 Subject: [PATCH 28/65] Small improvements --- bot.go | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/bot.go b/bot.go index fb2011e5..faa4492e 100644 --- a/bot.go +++ b/bot.go @@ -77,19 +77,22 @@ func (bot *BotAPI) SetAPIEndpoint(apiEndpoint string) { } func buildParams(in Params) url.Values { - if in == nil { - return url.Values{} - } - out := url.Values{} - for key, value := range in { out.Set(key, value) } - return out } +// closeBody drains any unread bytes before closing the response body so that +// net/http can return the underlying connection to the keep-alive pool. +// json.Decoder may stop short of EOF (trailing whitespace, partial errors), +// and an undrained body forces the transport to discard the connection. +func closeBody(body io.ReadCloser) { + _, _ = io.Copy(io.Discard, body) + body.Close() +} + // MakeRequest makes a request to a specific endpoint with our token. func (bot *BotAPI) MakeRequest(endpoint string, params Params) (*APIResponse, error) { if bot.Debug { @@ -100,7 +103,7 @@ func (bot *BotAPI) MakeRequest(endpoint string, params Params) (*APIResponse, er values := buildParams(params) - req, err := http.NewRequest("POST", method, strings.NewReader(values.Encode())) + req, err := http.NewRequest(http.MethodPost, method, strings.NewReader(values.Encode())) if err != nil { return &APIResponse{}, err } @@ -110,7 +113,7 @@ func (bot *BotAPI) MakeRequest(endpoint string, params Params) (*APIResponse, er if err != nil { return nil, err } - defer resp.Body.Close() + defer closeBody(resp.Body) var apiResp APIResponse bytes, err := bot.decodeAPIResponse(resp.Body, &apiResp) @@ -223,7 +226,7 @@ func (bot *BotAPI) UploadFiles(endpoint string, params Params, files []RequestFi method := fmt.Sprintf(bot.apiEndpoint, bot.Token, endpoint) - req, err := http.NewRequest("POST", method, r) + req, err := http.NewRequest(http.MethodPost, method, r) if err != nil { return nil, err } @@ -234,7 +237,7 @@ func (bot *BotAPI) UploadFiles(endpoint string, params Params, files []RequestFi if err != nil { return nil, err } - defer resp.Body.Close() + defer closeBody(resp.Body) var apiResp APIResponse bytes, err := bot.decodeAPIResponse(resp.Body, &apiResp) @@ -254,6 +257,7 @@ func (bot *BotAPI) UploadFiles(endpoint string, params Params, files []RequestFi } return &apiResp, &Error{ + Code: apiResp.ErrorCode, Message: apiResp.Description, ResponseParameters: parameters, } From 7f36336a08bb099f57658480774dba7cbe4302cc Mon Sep 17 00:00:00 2001 From: Kirill Date: Sat, 11 Apr 2026 04:59:31 +0400 Subject: [PATCH 29/65] Full support of API 7.1 --- types.go | 39 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/types.go b/types.go index 50e8ef8c..2dc82acf 100644 --- a/types.go +++ b/types.go @@ -366,6 +366,17 @@ type Chat struct { // // optional HasAggressiveAntiSpamEnabled bool `json:"has_aggressive_anti_spam_enabled,omitempty"` + // UnrestrictBoostCount is the minimum number of boosts that a non-administrator + // user needs to add to the chat in order to ignore slow mode and chat + // permissions. Returned only in getChat. + // + // optional + UnrestrictBoostCount int `json:"unrestrict_boost_count,omitempty"` + // CustomEmojiStickerSetName is the name of the chat's custom emoji sticker + // set. Returned only in getChat. + // + // optional + CustomEmojiStickerSetName string `json:"custom_emoji_sticker_set_name,omitempty"` // Photo is a chat photo Photo *ChatPhoto `json:"photo"` // Bio is the bio of the other party in a private chat. Returned only in @@ -522,6 +533,11 @@ type Message struct { // // optional IsAutomaticForward bool `json:"is_automatic_forward,omitempty"` + // SenderBoostCount is the number of boosts added by the user, if the + // sender of the message boosted the chat. + // + // optional + SenderBoostCount int `json:"sender_boost_count,omitempty"` // ReplyToMessage for replies, the original message. // Note that the Message object in this field will not contain further ReplyToMessage fields // even if it itself is a reply; @@ -537,6 +553,10 @@ type Message struct { // // optional Quote *TextQuote `json:"quote,omitempty"` + // ReplyToStory is the story that this message is a reply to. + // + // optional + ReplyToStory *Story `json:"reply_to_story,omitempty"` // ViaBot through which the message was sent; // // optional @@ -801,6 +821,10 @@ type Message struct { // // optional WriteAccessAllowed *WriteAccessAllowed `json:"write_access_allowed,omitempty"` + // BoostAdded is a service message: a user boosted the chat. + // + // optional + BoostAdded *ChatBoostAdded `json:"boost_added,omitempty"` // GiveawayCreated is a service message: a scheduled giveaway was created. // // optional @@ -2301,8 +2325,19 @@ type ChatPermissions struct { CanManageTopics bool `json:"can_manage_topics,omitempty"` } -// Story represents a forwarded story. Currently holds no information. -type Story struct{} +// Story represents a story. +type Story struct { + // Chat that posted the story. + Chat Chat `json:"chat"` + // ID is the unique identifier of the story in the chat. + ID int `json:"id"` +} + +// ChatBoostAdded represents a service message about a user boosting a chat. +type ChatBoostAdded struct { + // BoostCount is the number of boosts added by the user. + BoostCount int `json:"boost_count"` +} // Reaction type constants. const ( From a400735e0d4b8065791ad7198db29669d1b6c9b5 Mon Sep 17 00:00:00 2001 From: Kirill Date: Sat, 11 Apr 2026 07:17:45 +0400 Subject: [PATCH 30/65] Full support of API 7.2 --- bot.go | 14 ++++ configs.go | 87 +++++++++++++++++--- types.go | 230 +++++++++++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 315 insertions(+), 16 deletions(-) diff --git a/bot.go b/bot.go index faa4492e..e4c10b84 100644 --- a/bot.go +++ b/bot.go @@ -732,6 +732,20 @@ func (bot *BotAPI) CopyMessages(config CopyMessagesConfig) ([]MessageID, error) return ids, err } +// GetBusinessConnection returns information about the connection of the bot +// with a business account. +func (bot *BotAPI) GetBusinessConnection(config GetBusinessConnectionConfig) (BusinessConnection, error) { + resp, err := bot.Request(config) + if err != nil { + return BusinessConnection{}, err + } + + var bc BusinessConnection + err = json.Unmarshal(resp.Result, &bc) + + return bc, err +} + // GetMyName returns the current bot name for the given user language. func (bot *BotAPI) GetMyName(config GetMyNameConfig) (BotName, error) { resp, err := bot.Request(config) diff --git a/configs.go b/configs.go index dcaa3b6e..5aea6419 100644 --- a/configs.go +++ b/configs.go @@ -98,6 +98,23 @@ const ( // UpdateTypeRemovedChatBoost is when a boost was removed from a chat. UpdateTypeRemovedChatBoost = "removed_chat_boost" + // UpdateTypeBusinessConnection is when the bot was connected to or + // disconnected from a business account, or a user edited an existing + // connection with the bot. + UpdateTypeBusinessConnection = "business_connection" + + // UpdateTypeBusinessMessage is a new non-service message from a connected + // business account. + UpdateTypeBusinessMessage = "business_message" + + // UpdateTypeEditedBusinessMessage is a new version of a message from a + // connected business account. + UpdateTypeEditedBusinessMessage = "edited_business_message" + + // UpdateTypeDeletedBusinessMessages is when messages were deleted from a + // connected business account. + UpdateTypeDeletedBusinessMessages = "deleted_business_messages" + // UpdateTypeMyChatMember is when the bot's chat member status was updated in a chat. For private chats, this // update is received only when the bot is blocked or unblocked by the user. UpdateTypeMyChatMember = "my_chat_member" @@ -281,14 +298,15 @@ func (CloseConfig) params() (Params, error) { // BaseChat is base type for all chat config types. type BaseChat struct { - ChatID int64 // required - ChannelUsername string - MessageThreadID int - MessageEffectID string - ProtectContent bool - ReplyParameters *ReplyParameters - ReplyMarkup interface{} - DisableNotification bool + ChatID int64 // required + ChannelUsername string + BusinessConnectionID string + MessageThreadID int + MessageEffectID string + ProtectContent bool + ReplyParameters *ReplyParameters + ReplyMarkup interface{} + DisableNotification bool } func (chat *BaseChat) params() (Params, error) { @@ -297,6 +315,7 @@ func (chat *BaseChat) params() (Params, error) { if err := params.AddFirstValid("chat_id", chat.ChatID, chat.ChannelUsername); err != nil { return params, err } + params.AddNonEmpty("business_connection_id", chat.BusinessConnectionID) params.AddNonZero("message_thread_id", chat.MessageThreadID) params.AddNonEmpty("message_effect_id", chat.MessageEffectID) params.AddBool("disable_notification", chat.DisableNotification) @@ -2675,12 +2694,14 @@ func (config UploadStickerConfig) files() []RequestFile { } // NewStickerSetConfig creates a new sticker set owned by a user. +// +// Each sticker's format is specified on the InputSticker itself via its +// Format field, allowing mixed-format sticker packs. type NewStickerSetConfig struct { UserID int64 Name string Title string Stickers []InputSticker - StickerFormat string // one of StickerFormatStatic, StickerFormatAnimated, StickerFormatVideo StickerType string // one of StickerTypeRegular, StickerTypeMask, StickerTypeCustomEmoji NeedsRepainting bool } @@ -2695,7 +2716,6 @@ func (config NewStickerSetConfig) params() (Params, error) { params.AddNonZero64("user_id", config.UserID) params["name"] = config.Name params["title"] = config.Title - params.AddNonEmpty("sticker_format", config.StickerFormat) params.AddNonEmpty("sticker_type", config.StickerType) params.AddBool("needs_repainting", config.NeedsRepainting) @@ -2953,6 +2973,35 @@ func (config SetStickerMaskPositionConfig) params() (Params, error) { return params, err } +// ReplaceStickerInSetConfig replaces an existing sticker in a sticker set +// with a new one. The sticker set must have been created by the bot. +type ReplaceStickerInSetConfig struct { + UserID int64 + Name string + OldSticker string // file identifier of the replaced sticker + Sticker InputSticker +} + +func (config ReplaceStickerInSetConfig) method() string { + return "replaceStickerInSet" +} + +func (config ReplaceStickerInSetConfig) params() (Params, error) { + params := make(Params) + + params.AddNonZero64("user_id", config.UserID) + params["name"] = config.Name + params["old_sticker"] = config.OldSticker + + err := params.AddAny("sticker", prepareInputStickerForParams(config.Sticker, 0)) + + return params, err +} + +func (config ReplaceStickerInSetConfig) files() []RequestFile { + return prepareInputStickerForFiles(config.Sticker, 0) +} + // SetChatStickerSetConfig allows you to set the sticker set for a supergroup. type SetChatStickerSetConfig struct { ChatID int64 @@ -3216,6 +3265,24 @@ func (config SetMyShortDescriptionConfig) params() (Params, error) { return params, nil } +// GetBusinessConnectionConfig returns information about the connection of +// the bot with a business account. +type GetBusinessConnectionConfig struct { + BusinessConnectionID string +} + +func (config GetBusinessConnectionConfig) method() string { + return "getBusinessConnection" +} + +func (config GetBusinessConnectionConfig) params() (Params, error) { + params := make(Params) + + params["business_connection_id"] = config.BusinessConnectionID + + return params, nil +} + // GetMyShortDescriptionConfig returns the current bot short description for // the given user language. type GetMyShortDescriptionConfig struct { diff --git a/types.go b/types.go index 2dc82acf..ce543883 100644 --- a/types.go +++ b/types.go @@ -118,6 +118,25 @@ type Update struct { // // optional MessageReactionCount *MessageReactionCountUpdated `json:"message_reaction_count,omitempty"` + // BusinessConnection is the bot was connected to or disconnected from a + // business account, or a user edited an existing connection with the bot. + // + // optional + BusinessConnection *BusinessConnection `json:"business_connection,omitempty"` + // BusinessMessage is a new message from a connected business account. + // + // optional + BusinessMessage *Message `json:"business_message,omitempty"` + // EditedBusinessMessage is a new version of a message from a connected + // business account that is known to the bot and was edited. + // + // optional + EditedBusinessMessage *Message `json:"edited_business_message,omitempty"` + // DeletedBusinessMessages are messages that were deleted from a connected + // business account. + // + // optional + DeletedBusinessMessages *BusinessMessagesDeleted `json:"deleted_business_messages,omitempty"` // ChatBoost is a boost added to a chat or changed. The bot must be an // administrator in the chat to receive these updates. // @@ -228,6 +247,11 @@ type User struct { // // optional AddedToAttachmentMenu bool `json:"added_to_attachment_menu,omitempty"` + // CanConnectToBusiness true, if the bot can be connected to a Telegram + // Business account to receive its messages. Returned only in getMe. + // + // optional + CanConnectToBusiness bool `json:"can_connect_to_business,omitempty"` // FirstName user's or bot's first name FirstName string `json:"first_name"` // LastName user's or bot's last name @@ -377,6 +401,30 @@ type Chat struct { // // optional CustomEmojiStickerSetName string `json:"custom_emoji_sticker_set_name,omitempty"` + // Birthdate of the other party in a private chat. Returned only in getChat. + // + // optional + Birthdate *Birthdate `json:"birthdate,omitempty"` + // BusinessIntro is the intro of the business account. Returned only in + // getChat for business accounts. + // + // optional + BusinessIntro *BusinessIntro `json:"business_intro,omitempty"` + // BusinessLocation is the location of the business account. Returned only + // in getChat for business accounts. + // + // optional + BusinessLocation *BusinessLocation `json:"business_location,omitempty"` + // BusinessOpeningHours is the opening hours of the business account. + // Returned only in getChat for business accounts. + // + // optional + BusinessOpeningHours *BusinessOpeningHours `json:"business_opening_hours,omitempty"` + // PersonalChat is the personal channel of the private chat's user. + // Returned only in getChat for private chats. + // + // optional + PersonalChat *Chat `json:"personal_chat,omitempty"` // Photo is a chat photo Photo *ChatPhoto `json:"photo"` // Bio is the bio of the other party in a private chat. Returned only in @@ -499,6 +547,14 @@ func (c Chat) ChatConfig() ChatConfig { type Message struct { // MessageID is a unique message identifier inside this chat MessageID int `json:"message_id"` + // BusinessConnectionID is the unique identifier of the business + // connection from which the message was received. If non-empty, the + // message belongs to a chat of the corresponding business account that + // is independent from any potential bot chat which might share the same + // identifier. + // + // optional + BusinessConnectionID string `json:"business_connection_id,omitempty"` // MessageThreadID is the unique identifier of a message thread to which // the message belongs; for supergroups only. // @@ -538,6 +594,18 @@ type Message struct { // // optional SenderBoostCount int `json:"sender_boost_count,omitempty"` + // SenderBusinessBot is the bot that actually sent the message on behalf + // of the business account. Available only for outgoing messages sent on + // behalf of the connected business account. + // + // optional + SenderBusinessBot *User `json:"sender_business_bot,omitempty"` + // IsFromOffline is true, if the message was sent by an implicit action, + // for example, as an away or a greeting business message, or as a + // scheduled message. + // + // optional + IsFromOffline bool `json:"is_from_offline,omitempty"` // ReplyToMessage for replies, the original message. // Note that the Message object in this field will not contain further ReplyToMessage fields // even if it itself is a reply; @@ -1631,6 +1699,18 @@ type KeyboardButtonRequestUsers struct { // // optional MaxQuantity int `json:"max_quantity,omitempty"` + // RequestName pass true to request the users' first and last names. + // + // optional + RequestName bool `json:"request_name,omitempty"` + // RequestUsername pass true to request the users' usernames. + // + // optional + RequestUsername bool `json:"request_username,omitempty"` + // RequestPhoto pass true to request the users' photos. + // + // optional + RequestPhoto bool `json:"request_photo,omitempty"` } // KeyboardButtonRequestChat defines the criteria used to request a suitable @@ -1676,6 +1756,18 @@ type KeyboardButtonRequestChat struct { // // optional BotIsMember bool `json:"bot_is_member,omitempty"` + // RequestTitle pass true to request the chat's title. + // + // optional + RequestTitle bool `json:"request_title,omitempty"` + // RequestUsername pass true to request the chat's username. + // + // optional + RequestUsername bool `json:"request_username,omitempty"` + // RequestPhoto pass true to request the chat's photo. + // + // optional + RequestPhoto bool `json:"request_photo,omitempty"` } // UsersShared contains information about the users whose identifiers were @@ -1683,8 +1775,8 @@ type KeyboardButtonRequestChat struct { type UsersShared struct { // RequestID is the identifier of the request. RequestID int `json:"request_id"` - // UserIDs are the identifiers of the shared users. - UserIDs []int64 `json:"user_ids"` + // Users is the list of shared users. + Users []SharedUser `json:"users"` } // ChatShared contains information about the chat whose identifier was shared @@ -1694,6 +1786,19 @@ type ChatShared struct { RequestID int `json:"request_id"` // ChatID is the identifier of the shared chat. ChatID int64 `json:"chat_id"` + // Title of the chat, if the title was requested by the bot. + // + // optional + Title string `json:"title,omitempty"` + // Username of the chat, if the username was requested by the bot and + // available. + // + // optional + Username string `json:"username,omitempty"` + // Photo of the chat, if the photo was requested by the bot. + // + // optional + Photo []PhotoSize `json:"photo,omitempty"` } // KeyboardButtonRequestManagedBot defines the parameters for the creation @@ -2339,6 +2444,120 @@ type ChatBoostAdded struct { BoostCount int `json:"boost_count"` } +// BusinessConnection describes the connection of the bot with a business account. +type BusinessConnection struct { + // ID is the unique identifier of the business connection. + ID string `json:"id"` + // User is the business account user that created the business connection. + User User `json:"user"` + // UserChatID is the identifier of a private chat with the user who + // created the business connection. + UserChatID int64 `json:"user_chat_id"` + // Date the connection was established in Unix time. + Date int `json:"date"` + // CanReply is true, if the bot can act on behalf of the business account + // in chats that were active in the last 24 hours. + CanReply bool `json:"can_reply"` + // IsEnabled is true, if the connection is active. + IsEnabled bool `json:"is_enabled"` +} + +// BusinessMessagesDeleted is received when messages are deleted from a +// connected business account. +type BusinessMessagesDeleted struct { + // BusinessConnectionID is the unique identifier of the business connection. + BusinessConnectionID string `json:"business_connection_id"` + // Chat in which the messages were deleted. The bot may not have access to + // the chat or the corresponding user. + Chat Chat `json:"chat"` + // MessageIDs is the list of identifiers of deleted messages in the chat. + MessageIDs []int `json:"message_ids"` +} + +// BusinessIntro contains information about the intro of a business. +type BusinessIntro struct { + // Title text of the business intro. + // + // optional + Title string `json:"title,omitempty"` + // Message text of the business intro. + // + // optional + Message string `json:"message,omitempty"` + // Sticker of the business intro. + // + // optional + Sticker *Sticker `json:"sticker,omitempty"` +} + +// BusinessLocation contains information about the location of a business. +type BusinessLocation struct { + // Address of the business. + Address string `json:"address"` + // Location of the business. + // + // optional + Location *Location `json:"location,omitempty"` +} + +// BusinessOpeningHoursInterval describes an interval of time during which a +// business is open. +type BusinessOpeningHoursInterval struct { + // OpeningMinute is the minute's sequence number in a week, starting on + // Monday, marking the start of the time interval during which the + // business is open; 0 - 7 * 24 * 60. + OpeningMinute int `json:"opening_minute"` + // ClosingMinute is the minute's sequence number in a week, starting on + // Monday, marking the end of the time interval during which the business + // is open; 0 - 8 * 24 * 60. + ClosingMinute int `json:"closing_minute"` +} + +// BusinessOpeningHours describes the opening hours of a business. +type BusinessOpeningHours struct { + // TimeZoneName is the unique name of the time zone for which the opening + // hours are defined. + TimeZoneName string `json:"time_zone_name"` + // OpeningHours is the list of time intervals describing business opening + // hours. + OpeningHours []BusinessOpeningHoursInterval `json:"opening_hours"` +} + +// Birthdate contains information about a user's birthdate. +type Birthdate struct { + // Day of the user's birth; 1-31. + Day int `json:"day"` + // Month of the user's birth; 1-12. + Month int `json:"month"` + // Year of the user's birth. + // + // optional + Year int `json:"year,omitempty"` +} + +// SharedUser contains information about a user that was shared with the bot +// using a KeyboardButtonRequestUsers button. +type SharedUser struct { + // UserID is the identifier of the shared user. + UserID int64 `json:"user_id"` + // FirstName of the user, if the name was requested by the bot. + // + // optional + FirstName string `json:"first_name,omitempty"` + // LastName of the user, if the name was requested by the bot. + // + // optional + LastName string `json:"last_name,omitempty"` + // Username of the user, if the username was requested by the bot. + // + // optional + Username string `json:"username,omitempty"` + // Photo of the user, if the photo was requested by the bot. + // + // optional + Photo []PhotoSize `json:"photo,omitempty"` +} + // Reaction type constants. const ( ReactionTypeEmoji = "emoji" @@ -3198,6 +3417,9 @@ type InputSticker struct { // new file data via FilePath/FileBytes/FileReader. Animated and video // stickers can't be uploaded via HTTP URL. Sticker RequestFileData `json:"sticker"` + // Format of the added sticker. One of StickerFormatStatic, + // StickerFormatAnimated, StickerFormatVideo. + Format string `json:"format"` // EmojiList is the list of 1-20 emoji associated with the sticker. EmojiList []string `json:"emoji_list"` // MaskPosition for "mask" stickers, the position where the mask should @@ -3225,10 +3447,6 @@ type StickerSet struct { Title string `json:"title"` // StickerType of stickers in the set, currently one of “regular”, “mask”, “custom_emoji” StickerType string `json:"sticker_type"` - // IsAnimated true, if the sticker set contains animated stickers - IsAnimated bool `json:"is_animated"` - // IsVideo true, if the sticker set contains video stickers - IsVideo bool `json:"is_video"` // Stickers list of all set stickers Stickers []Sticker `json:"stickers"` // Thumb is the sticker set thumbnail in the .WEBP or .TGS format From 9703bfe04575b653b5216e0fa54d80ae50529ed0 Mon Sep 17 00:00:00 2001 From: Kirill Date: Sat, 11 Apr 2026 07:24:22 +0400 Subject: [PATCH 31/65] Full support of API 7.3 --- bot.go | 15 +++--- configs.go | 14 +++-- helpers.go | 9 +++- types.go | 156 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 183 insertions(+), 11 deletions(-) diff --git a/bot.go b/bot.go index e4c10b84..86036310 100644 --- a/bot.go +++ b/bot.go @@ -556,17 +556,20 @@ func WriteToHTTPResponse(w http.ResponseWriter, c Chattable) error { return err } -// GetChat gets information about a chat. -func (bot *BotAPI) GetChat(config ChatInfoConfig) (Chat, error) { +// GetChat gets full information about a chat. +// +// As of Bot API 7.3 the return type is ChatFullInfo, which embeds Chat and +// adds the fields that are only populated by getChat. +func (bot *BotAPI) GetChat(config ChatInfoConfig) (ChatFullInfo, error) { resp, err := bot.Request(config) if err != nil { - return Chat{}, err + return ChatFullInfo{}, err } - var chat Chat - err = json.Unmarshal(resp.Result, &chat) + var info ChatFullInfo + err = json.Unmarshal(resp.Result, &info) - return chat, err + return info, err } // GetChatAdministrators gets a list of administrators in the chat. diff --git a/configs.go b/configs.go index 5aea6419..b3ee7ea4 100644 --- a/configs.go +++ b/configs.go @@ -819,6 +819,7 @@ type EditMessageLiveLocationConfig struct { BaseEdit Latitude float64 // required Longitude float64 // required + LivePeriod int // optional; pass 0x7FFFFFFF to keep live indefinitely HorizontalAccuracy float64 // optional Heading int // optional ProximityAlertRadius int // optional @@ -829,6 +830,7 @@ func (config EditMessageLiveLocationConfig) params() (Params, error) { params.AddNonZeroFloat("latitude", config.Latitude) params.AddNonZeroFloat("longitude", config.Longitude) + params.AddNonZero("live_period", config.LivePeriod) params.AddNonZeroFloat("horizontal_accuracy", config.HorizontalAccuracy) params.AddNonZero("heading", config.Heading) params.AddNonZero("proximity_alert_radius", config.ProximityAlertRadius) @@ -914,7 +916,9 @@ func (config ContactConfig) method() string { type SendPollConfig struct { BaseChat Question string - Options []string + QuestionParseMode string + QuestionEntities []MessageEntity + Options []InputPollOption IsAnonymous bool Type string AllowsMultipleAnswers bool @@ -934,7 +938,11 @@ func (config SendPollConfig) params() (Params, error) { } params["question"] = config.Question - if err = params.AddInterface("options", config.Options); err != nil { + params.AddNonEmpty("question_parse_mode", config.QuestionParseMode) + if err = params.AddAny("question_entities", config.QuestionEntities); err != nil { + return params, err + } + if err = params.AddAny("options", config.Options); err != nil { return params, err } params["is_anonymous"] = strconv.FormatBool(config.IsAnonymous) @@ -946,7 +954,7 @@ func (config SendPollConfig) params() (Params, error) { params.AddNonEmpty("explanation_parse_mode", config.ExplanationParseMode) params.AddNonZero("open_period", config.OpenPeriod) params.AddNonZero("close_date", config.CloseDate) - err = params.AddInterface("explanation_entities", config.ExplanationEntities) + err = params.AddAny("explanation_entities", config.ExplanationEntities) return params, err } diff --git a/helpers.go b/helpers.go index 3e09be44..4f135634 100644 --- a/helpers.go +++ b/helpers.go @@ -811,13 +811,20 @@ func NewDeleteChatPhoto(chatID int64) DeleteChatPhotoConfig { } // NewPoll allows you to create a new poll. +// +// Option texts are wrapped into InputPollOption values. To set custom_emoji +// entities on options, build the SendPollConfig directly. func NewPoll(chatID int64, question string, options ...string) SendPollConfig { + opts := make([]InputPollOption, 0, len(options)) + for _, text := range options { + opts = append(opts, InputPollOption{Text: text}) + } return SendPollConfig{ BaseChat: BaseChat{ ChatID: chatID, }, Question: question, - Options: options, + Options: opts, IsAnonymous: true, // This is Telegram's default. } } diff --git a/types.go b/types.go index ce543883..377aaaec 100644 --- a/types.go +++ b/types.go @@ -543,6 +543,16 @@ func (c Chat) ChatConfig() ChatConfig { return ChatConfig{ChatID: c.ID} } +// ChatFullInfo contains full information about a chat. This is the return +// type of getChat as of Bot API 7.3. It embeds Chat so all Chat fields are +// accessible via field promotion. +type ChatFullInfo struct { + Chat + // MaxReactionCount is the maximum number of reactions that can be set + // on a message in the chat. + MaxReactionCount int `json:"max_reaction_count"` +} + // Message represents a message. type Message struct { // MessageID is a unique message identifier inside this chat @@ -893,6 +903,10 @@ type Message struct { // // optional BoostAdded *ChatBoostAdded `json:"boost_added,omitempty"` + // ChatBackgroundSet is a service message: chat background set. + // + // optional + ChatBackgroundSet *ChatBackground `json:"chat_background_set,omitempty"` // GiveawayCreated is a service message: a scheduled giveaway was created. // // optional @@ -1336,6 +1350,11 @@ type Dice struct { type PollOption struct { // Text is the option text, 1-100 characters Text string `json:"text"` + // TextEntities are the special entities that appear in the option text. + // Currently, only custom_emoji entities are allowed in poll option texts. + // + // optional + TextEntities []MessageEntity `json:"text_entities,omitempty"` // VoterCount is the number of users that voted for this option VoterCount int `json:"voter_count"` } @@ -1362,8 +1381,13 @@ type PollAnswer struct { type Poll struct { // ID is the unique poll identifier ID string `json:"id"` - // Question is the poll question, 1-255 characters + // Question is the poll question, 1-300 characters Question string `json:"question"` + // QuestionEntities are the special entities that appear in the question. + // Currently, only custom_emoji entities are allowed in poll questions. + // + // optional + QuestionEntities []MessageEntity `json:"question_entities,omitempty"` // Options is the list of poll options Options []PollOption `json:"options"` // TotalVoterCount is the total numbers of users who voted in the poll @@ -2332,6 +2356,12 @@ type ChatMemberUpdated struct { // // optional InviteLink *ChatInviteLink `json:"invite_link,omitempty"` + // ViaJoinRequest is true, if the user joined the chat after sending a + // direct join request without using an invite link and being approved + // by an administrator. + // + // optional + ViaJoinRequest bool `json:"via_join_request,omitempty"` // ViaChatFolderInviteLink is true, if the user joined the chat via a chat // folder invite link. // @@ -2523,6 +2553,130 @@ type BusinessOpeningHours struct { OpeningHours []BusinessOpeningHoursInterval `json:"opening_hours"` } +// Background fill type constants. +const ( + BackgroundFillTypeSolid = "solid" + BackgroundFillTypeGradient = "gradient" + BackgroundFillTypeFreeformGradient = "freeform_gradient" +) + +// Background type constants. +const ( + BackgroundTypeFill = "fill" + BackgroundTypeWallpaper = "wallpaper" + BackgroundTypePattern = "pattern" + BackgroundTypeChatTheme = "chat_theme" +) + +// BackgroundFill describes the way a background is filled based on the +// selected colors. Flat polymorphic by Type: +// - "solid" → Color +// - "gradient" → TopColor, BottomColor, RotationAngle +// - "freeform_gradient" → Colors (3 or 4 RGB colors) +type BackgroundFill struct { + // Type of the fill. One of "solid", "gradient", "freeform_gradient". + Type string `json:"type"` + // Color is the fill color in RGB format. Set when Type is "solid". + // + // optional + Color int `json:"color,omitempty"` + // TopColor is the top color of the gradient in RGB format. Set when Type + // is "gradient". + // + // optional + TopColor int `json:"top_color,omitempty"` + // BottomColor is the bottom color of the gradient in RGB format. Set + // when Type is "gradient". + // + // optional + BottomColor int `json:"bottom_color,omitempty"` + // RotationAngle is the clockwise rotation angle of the background fill + // in degrees; 0-359. Set when Type is "gradient". + // + // optional + RotationAngle int `json:"rotation_angle,omitempty"` + // Colors is a list of 3 or 4 RGB colors. Set when Type is + // "freeform_gradient". + // + // optional + Colors []int `json:"colors,omitempty"` +} + +// BackgroundType describes the type of a chat background. Flat polymorphic +// by Type: +// - "fill" → Fill, DarkThemeDimming +// - "wallpaper" → Document, DarkThemeDimming, IsBlurred, IsMoving +// - "pattern" → Document, Fill, Intensity, IsInverted, IsMoving +// - "chat_theme" → ThemeName +type BackgroundType struct { + // Type of the background. One of "fill", "wallpaper", "pattern", + // "chat_theme". + Type string `json:"type"` + // Fill is the background fill. Set when Type is "fill" or "pattern". + // + // optional + Fill *BackgroundFill `json:"fill,omitempty"` + // DarkThemeDimming is the dimming of the background in dark themes, as + // a percentage; 0-100. Set when Type is "fill" or "wallpaper". + // + // optional + DarkThemeDimming int `json:"dark_theme_dimming,omitempty"` + // Document is the document with the wallpaper/pattern. Set when Type is + // "wallpaper" or "pattern". + // + // optional + Document *Document `json:"document,omitempty"` + // IsBlurred is true, if the wallpaper is downscaled to fit in a 450x450 + // square and then box-blurred with radius 12. Set when Type is "wallpaper". + // + // optional + IsBlurred bool `json:"is_blurred,omitempty"` + // IsMoving is true, if the background moves slightly when the device is + // tilted. Set when Type is "wallpaper" or "pattern". + // + // optional + IsMoving bool `json:"is_moving,omitempty"` + // Intensity is the intensity of the pattern when it is shown above the + // filled background; 0-100. Set when Type is "pattern". + // + // optional + Intensity int `json:"intensity,omitempty"` + // IsInverted is true, if the background fill must be applied only to + // the pattern itself. All other pixels are black. Set when Type is + // "pattern". + // + // optional + IsInverted bool `json:"is_inverted,omitempty"` + // ThemeName is the name of the chat theme, which is usually an emoji. + // Set when Type is "chat_theme". + // + // optional + ThemeName string `json:"theme_name,omitempty"` +} + +// ChatBackground represents a chat background. +type ChatBackground struct { + // Type of the background. + Type BackgroundType `json:"type"` +} + +// InputPollOption contains information about one answer option in a poll +// to be sent. +type InputPollOption struct { + // Text is the option text, 1-100 characters. + Text string `json:"text"` + // TextParseMode is the mode for parsing entities in the text. Currently, + // only custom_emoji entities are allowed. + // + // optional + TextParseMode string `json:"text_parse_mode,omitempty"` + // TextEntities is a list of special entities that appear in the poll + // option text. It can be specified instead of TextParseMode. + // + // optional + TextEntities []MessageEntity `json:"text_entities,omitempty"` +} + // Birthdate contains information about a user's birthdate. type Birthdate struct { // Day of the user's birth; 1-31. From 559ab7583a641ed7d1b2e64f1c560cc1bf5a36f4 Mon Sep 17 00:00:00 2001 From: Kirill Date: Sat, 11 Apr 2026 07:34:28 +0400 Subject: [PATCH 32/65] Full support of API 7.4 --- configs.go | 125 ++++++++++++++++++++++++++++++++++------------------- types.go | 77 +++++++++++++++++++++++++++------ 2 files changed, 143 insertions(+), 59 deletions(-) diff --git a/configs.go b/configs.go index b3ee7ea4..236897e0 100644 --- a/configs.go +++ b/configs.go @@ -421,12 +421,13 @@ func (config ForwardConfig) method() string { // CopyMessageConfig contains information about a copyMessage request. type CopyMessageConfig struct { BaseChat - FromChatID int64 - FromChannelUsername string - MessageID int - Caption string - ParseMode string - CaptionEntities []MessageEntity + FromChatID int64 + FromChannelUsername string + MessageID int + Caption string + ParseMode string + CaptionEntities []MessageEntity + ShowCaptionAboveMedia bool } func (config CopyMessageConfig) params() (Params, error) { @@ -441,7 +442,8 @@ func (config CopyMessageConfig) params() (Params, error) { params.AddNonZero("message_id", config.MessageID) params.AddNonEmpty("caption", config.Caption) params.AddNonEmpty("parse_mode", config.ParseMode) - err = params.AddInterface("caption_entities", config.CaptionEntities) + params.AddBool("show_caption_above_media", config.ShowCaptionAboveMedia) + err = params.AddAny("caption_entities", config.CaptionEntities) return params, err } @@ -453,11 +455,12 @@ func (config CopyMessageConfig) method() string { // PhotoConfig contains information about a SendPhoto request. type PhotoConfig struct { BaseFile - Thumbnail RequestFileData - Caption string - ParseMode string - CaptionEntities []MessageEntity - HasSpoiler bool + Thumbnail RequestFileData + Caption string + ParseMode string + CaptionEntities []MessageEntity + ShowCaptionAboveMedia bool + HasSpoiler bool } func (config PhotoConfig) params() (Params, error) { @@ -468,8 +471,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.AddBool("has_spoiler", config.HasSpoiler) - err = params.AddInterface("caption_entities", config.CaptionEntities) + err = params.AddAny("caption_entities", config.CaptionEntities) return params, err } @@ -614,13 +618,14 @@ func (config StickerConfig) files() []RequestFile { // VideoConfig contains information about a SendVideo request. type VideoConfig struct { BaseFile - Thumbnail RequestFileData - Duration int - Caption string - ParseMode string - CaptionEntities []MessageEntity - SupportsStreaming bool - HasSpoiler bool + Thumbnail RequestFileData + Duration int + Caption string + ParseMode string + CaptionEntities []MessageEntity + ShowCaptionAboveMedia bool + SupportsStreaming bool + HasSpoiler bool } func (config VideoConfig) params() (Params, error) { @@ -632,9 +637,10 @@ func (config VideoConfig) params() (Params, error) { params.AddNonZero("duration", config.Duration) params.AddNonEmpty("caption", config.Caption) params.AddNonEmpty("parse_mode", config.ParseMode) + params.AddBool("show_caption_above_media", config.ShowCaptionAboveMedia) params.AddBool("supports_streaming", config.SupportsStreaming) params.AddBool("has_spoiler", config.HasSpoiler) - err = params.AddInterface("caption_entities", config.CaptionEntities) + err = params.AddAny("caption_entities", config.CaptionEntities) return params, err } @@ -662,12 +668,13 @@ func (config VideoConfig) files() []RequestFile { // AnimationConfig contains information about a SendAnimation request. type AnimationConfig struct { BaseFile - Duration int - Thumbnail RequestFileData - Caption string - ParseMode string - CaptionEntities []MessageEntity - HasSpoiler bool + Duration int + Thumbnail RequestFileData + Caption string + ParseMode string + CaptionEntities []MessageEntity + ShowCaptionAboveMedia bool + HasSpoiler bool } func (config AnimationConfig) params() (Params, error) { @@ -679,8 +686,9 @@ func (config AnimationConfig) params() (Params, error) { params.AddNonZero("duration", config.Duration) params.AddNonEmpty("caption", config.Caption) params.AddNonEmpty("parse_mode", config.ParseMode) + params.AddBool("show_caption_above_media", config.ShowCaptionAboveMedia) params.AddBool("has_spoiler", config.HasSpoiler) - err = params.AddInterface("caption_entities", config.CaptionEntities) + err = params.AddAny("caption_entities", config.CaptionEntities) return params, err } @@ -1096,9 +1104,10 @@ func (config EditMessageTextConfig) method() string { // EditMessageCaptionConfig allows you to modify the caption of a message. type EditMessageCaptionConfig struct { BaseEdit - Caption string - ParseMode string - CaptionEntities []MessageEntity + Caption string + ParseMode string + CaptionEntities []MessageEntity + ShowCaptionAboveMedia bool } func (config EditMessageCaptionConfig) params() (Params, error) { @@ -1109,7 +1118,8 @@ func (config EditMessageCaptionConfig) params() (Params, error) { params["caption"] = config.Caption params.AddNonEmpty("parse_mode", config.ParseMode) - err = params.AddInterface("caption_entities", config.CaptionEntities) + params.AddBool("show_caption_above_media", config.ShowCaptionAboveMedia) + err = params.AddAny("caption_entities", config.CaptionEntities) return params, err } @@ -1851,8 +1861,8 @@ type InvoiceConfig struct { Title string // required Description string // required Payload string // required - ProviderToken string // required - Currency string // required + ProviderToken string // omit for payments in Telegram Stars + Currency string // required ("XTR" for Telegram Stars) Prices []LabeledPrice // required MaxTipAmount int SuggestedTipAmounts []int @@ -1880,14 +1890,14 @@ func (config InvoiceConfig) params() (Params, error) { params["title"] = config.Title params["description"] = config.Description params["payload"] = config.Payload - params["provider_token"] = config.ProviderToken + params.AddNonEmpty("provider_token", config.ProviderToken) params["currency"] = config.Currency - if err = params.AddInterface("prices", config.Prices); err != nil { + if err = params.AddAny("prices", config.Prices); err != nil { return params, err } params.AddNonZero("max_tip_amount", config.MaxTipAmount) - err = params.AddInterface("suggested_tip_amounts", config.SuggestedTipAmounts) + err = params.AddAny("suggested_tip_amounts", config.SuggestedTipAmounts) params.AddNonEmpty("start_parameter", config.StartParameter) if len(config.ProviderData) > 0 { params["provider_data"] = string(config.ProviderData) @@ -1916,8 +1926,8 @@ type InvoiceLinkConfig struct { Title string // required Description string // required Payload string // required - ProviderToken string // required - Currency string // required + ProviderToken string // omit for payments in Telegram Stars + Currency string // required ("XTR" for Telegram Stars) Prices []LabeledPrice // required MaxTipAmount int SuggestedTipAmounts []int @@ -1941,14 +1951,14 @@ func (config InvoiceLinkConfig) params() (Params, error) { params["title"] = config.Title params["description"] = config.Description params["payload"] = config.Payload - params["provider_token"] = config.ProviderToken + params.AddNonEmpty("provider_token", config.ProviderToken) params["currency"] = config.Currency - if err := params.AddInterface("prices", config.Prices); err != nil { + if err := params.AddAny("prices", config.Prices); err != nil { return params, err } params.AddNonZero("max_tip_amount", config.MaxTipAmount) - if err := params.AddInterface("suggested_tip_amounts", config.SuggestedTipAmounts); err != nil { + if err := params.AddAny("suggested_tip_amounts", config.SuggestedTipAmounts); err != nil { return params, err } if len(config.ProviderData) > 0 { @@ -1973,6 +1983,25 @@ func (config InvoiceLinkConfig) method() string { return "createInvoiceLink" } +// RefundStarPaymentConfig refunds a successful payment in Telegram Stars. +type RefundStarPaymentConfig struct { + UserID int64 + TelegramPaymentChargeID string +} + +func (config RefundStarPaymentConfig) method() string { + return "refundStarPayment" +} + +func (config RefundStarPaymentConfig) params() (Params, error) { + params := make(Params) + + params.AddNonZero64("user_id", config.UserID) + params["telegram_payment_charge_id"] = config.TelegramPaymentChargeID + + return params, nil +} + // ShippingConfig contains information for answerShippingQuery request. type ShippingConfig struct { ShippingQueryID string // required @@ -3057,12 +3086,15 @@ func (config DeleteChatStickerSetConfig) params() (Params, error) { // // Media consist of InputMedia items (InputMediaPhoto, InputMediaVideo). type MediaGroupConfig struct { - ChatID int64 - ChannelUsername string - MessageThreadID int + ChatID int64 + ChannelUsername string + BusinessConnectionID string + MessageThreadID int + MessageEffectID string Media []interface{} DisableNotification bool + ProtectContent bool ReplyParameters *ReplyParameters } @@ -3076,8 +3108,11 @@ func (config MediaGroupConfig) params() (Params, error) { if err := params.AddFirstValid("chat_id", config.ChatID, config.ChannelUsername); err != nil { return params, err } + params.AddNonEmpty("business_connection_id", config.BusinessConnectionID) params.AddNonZero("message_thread_id", config.MessageThreadID) + params.AddNonEmpty("message_effect_id", config.MessageEffectID) params.AddBool("disable_notification", config.DisableNotification) + params.AddBool("protect_content", config.ProtectContent) if err := params.AddAny("reply_parameters", config.ReplyParameters); err != nil { return params, err } diff --git a/types.go b/types.go index 377aaaec..3d173115 100644 --- a/types.go +++ b/types.go @@ -719,6 +719,11 @@ type Message struct { // // optional Caption string `json:"caption,omitempty"` + // ShowCaptionAboveMedia is true, if the caption must be shown above the + // message media. + // + // optional + ShowCaptionAboveMedia bool `json:"show_caption_above_media,omitempty"` // CaptionEntities; // // optional @@ -1031,6 +1036,7 @@ type MessageEntity struct { // “strikethrough” (strikethrough text), // "spoiler" (spoiler message), // “blockquote” (block quotation), + // “expandable_blockquote” (collapsed-by-default block quotation), // “code” (monowidth string), // “pre” (monowidth block), // “text_link” (for clickable text URLs), @@ -3373,15 +3379,23 @@ type BaseInputMedia struct { // // optional CaptionEntities []MessageEntity `json:"caption_entities,omitempty"` + // ShowCaptionAboveMedia pass True if the caption must be shown above the + // message media. Valid for photo, video, and animation variants only; the + // field is ignored for audio and document. + // + // optional + ShowCaptionAboveMedia bool `json:"show_caption_above_media,omitempty"` + // HasSpoiler pass True if the media needs to be covered with a spoiler + // animation. Valid for photo, video, and animation variants only; the + // field is ignored for audio and document. + // + // optional + HasSpoiler bool `json:"has_spoiler,omitempty"` } // InputMediaPhoto is a photo to send as part of a media group. type InputMediaPhoto struct { BaseInputMedia - // HasSpoiler pass True if the photo needs to be covered with a spoiler animation. - // - // optional - HasSpoiler bool `json:"has_spoiler,omitempty"` } // InputMediaVideo is a video to send as part of a media group. @@ -3408,10 +3422,6 @@ type InputMediaVideo struct { // // optional SupportsStreaming bool `json:"supports_streaming,omitempty"` - // HasSpoiler pass True if the video needs to be covered with a spoiler animation. - // - // optional - HasSpoiler bool `json:"has_spoiler,omitempty"` } // InputMediaAnimation is an animation to send as part of a media group. @@ -3434,10 +3444,6 @@ type InputMediaAnimation struct { // // optional Duration int `json:"duration,omitempty"` - // HasSpoiler pass True if the animation needs to be covered with a spoiler animation. - // - // optional - HasSpoiler bool `json:"has_spoiler,omitempty"` } // InputMediaAudio is an audio to send as part of a media group. @@ -3831,6 +3837,11 @@ type InlineQueryResultCachedGIF struct { // // optional CaptionEntities []MessageEntity `json:"caption_entities,omitempty"` + // ShowCaptionAboveMedia pass True if the caption must be shown above the + // message media. + // + // optional + ShowCaptionAboveMedia bool `json:"show_caption_above_media,omitempty"` // ReplyMarkup inline keyboard attached to the message. // // optional @@ -3870,6 +3881,11 @@ type InlineQueryResultCachedMPEG4GIF struct { // // optional CaptionEntities []MessageEntity `json:"caption_entities,omitempty"` + // ShowCaptionAboveMedia pass True if the caption must be shown above the + // message media. + // + // optional + ShowCaptionAboveMedia bool `json:"show_caption_above_media,omitempty"` // ReplyMarkup inline keyboard attached to the message. // // optional @@ -3911,6 +3927,11 @@ type InlineQueryResultCachedPhoto struct { // // optional CaptionEntities []MessageEntity `json:"caption_entities,omitempty"` + // ShowCaptionAboveMedia pass True if the caption must be shown above the + // message media. + // + // optional + ShowCaptionAboveMedia bool `json:"show_caption_above_media,omitempty"` // ReplyMarkup inline keyboard attached to the message. // // optional @@ -3970,6 +3991,11 @@ type InlineQueryResultCachedVideo struct { // // optional CaptionEntities []MessageEntity `json:"caption_entities,omitempty"` + // ShowCaptionAboveMedia pass True if the caption must be shown above the + // message media. + // + // optional + ShowCaptionAboveMedia bool `json:"show_caption_above_media,omitempty"` // ReplyMarkup inline keyboard attached to the message // // optional @@ -4216,6 +4242,11 @@ type InlineQueryResultGIF struct { // // optional CaptionEntities []MessageEntity `json:"caption_entities,omitempty"` + // ShowCaptionAboveMedia pass True if the caption must be shown above the + // message media. + // + // optional + ShowCaptionAboveMedia bool `json:"show_caption_above_media,omitempty"` // ReplyMarkup inline keyboard attached to the message // // optional @@ -4328,6 +4359,11 @@ type InlineQueryResultMPEG4GIF struct { // // optional CaptionEntities []MessageEntity `json:"caption_entities,omitempty"` + // ShowCaptionAboveMedia pass True if the caption must be shown above the + // message media. + // + // optional + ShowCaptionAboveMedia bool `json:"show_caption_above_media,omitempty"` // ReplyMarkup inline keyboard attached to the message // // optional @@ -4388,6 +4424,11 @@ type InlineQueryResultPhoto struct { // // optional CaptionEntities []MessageEntity `json:"caption_entities,omitempty"` + // ShowCaptionAboveMedia pass True if the caption must be shown above the + // message media. + // + // optional + ShowCaptionAboveMedia bool `json:"show_caption_above_media,omitempty"` // InputMessageContent content of the message to be sent instead of the photo. // // optional @@ -4467,6 +4508,11 @@ type InlineQueryResultVideo struct { // // optional Caption string `json:"caption,omitempty"` + // ShowCaptionAboveMedia pass True if the caption must be shown above the + // message media. + // + // optional + ShowCaptionAboveMedia bool `json:"show_caption_above_media,omitempty"` // Width video width // // optional @@ -4673,8 +4719,11 @@ type InputInvoiceMessageContent struct { // Bot-defined invoice payload, 1-128 bytes. This will not be displayed to // the user, use for your internal processes. Payload string `json:"payload"` - // Payment provider token, obtained via Botfather - ProviderToken string `json:"provider_token"` + // Payment provider token, obtained via Botfather. Omit for payments in + // Telegram Stars. + // + // optional + ProviderToken string `json:"provider_token,omitempty"` // Three-letter ISO 4217 currency code Currency string `json:"currency"` // Price breakdown, a JSON-serialized list of components (e.g. product From 649e56c600630fa73b495a81f79fb7646e03da31 Mon Sep 17 00:00:00 2001 From: Kirill Date: Sat, 11 Apr 2026 07:37:25 +0400 Subject: [PATCH 33/65] Full support of API 7.5 --- bot.go | 14 ++++++++++ configs.go | 38 +++++++++++++++++++++---- types.go | 81 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 127 insertions(+), 6 deletions(-) diff --git a/bot.go b/bot.go index 86036310..559e6a49 100644 --- a/bot.go +++ b/bot.go @@ -735,6 +735,20 @@ func (bot *BotAPI) CopyMessages(config CopyMessagesConfig) ([]MessageID, error) return ids, err } +// GetStarTransactions returns the bot's Telegram Star transactions in +// chronological order. +func (bot *BotAPI) GetStarTransactions(config GetStarTransactionsConfig) (StarTransactions, error) { + resp, err := bot.Request(config) + if err != nil { + return StarTransactions{}, err + } + + var st StarTransactions + err = json.Unmarshal(resp.Result, &st) + + return st, err +} + // GetBusinessConnection returns information about the connection of the bot // with a business account. func (bot *BotAPI) GetBusinessConnection(config GetBusinessConnectionConfig) (BusinessConnection, error) { diff --git a/configs.go b/configs.go index 236897e0..5b1a6669 100644 --- a/configs.go +++ b/configs.go @@ -341,16 +341,19 @@ func (file BaseFile) params() (Params, error) { // BaseEdit is base type of all chat edits. type BaseEdit struct { - ChatID int64 - ChannelUsername string - MessageID int - InlineMessageID string - ReplyMarkup *InlineKeyboardMarkup + BusinessConnectionID string + ChatID int64 + ChannelUsername string + MessageID int + InlineMessageID string + ReplyMarkup *InlineKeyboardMarkup } func (edit BaseEdit) params() (Params, error) { params := make(Params) + params.AddNonEmpty("business_connection_id", edit.BusinessConnectionID) + if edit.InlineMessageID != "" { params["inline_message_id"] = edit.InlineMessageID } else { @@ -360,7 +363,7 @@ func (edit BaseEdit) params() (Params, error) { params.AddNonZero("message_id", edit.MessageID) } - err := params.AddInterface("reply_markup", edit.ReplyMarkup) + err := params.AddAny("reply_markup", edit.ReplyMarkup) return params, err } @@ -1983,6 +1986,29 @@ func (config InvoiceLinkConfig) method() string { return "createInvoiceLink" } +// GetStarTransactionsConfig returns the bot's Telegram Star transactions in +// chronological order. +type GetStarTransactionsConfig struct { + // Offset is the number of transactions to skip in the response. + Offset int + // Limit is the maximum number of transactions to be retrieved; 1-100. + // Defaults to 100. + Limit int +} + +func (config GetStarTransactionsConfig) method() string { + return "getStarTransactions" +} + +func (config GetStarTransactionsConfig) params() (Params, error) { + params := make(Params) + + params.AddNonZero("offset", config.Offset) + params.AddNonZero("limit", config.Limit) + + return params, nil +} + // RefundStarPaymentConfig refunds a successful payment in Telegram Stars. type RefundStarPaymentConfig struct { UserID int64 diff --git a/types.go b/types.go index 3d173115..260749a8 100644 --- a/types.go +++ b/types.go @@ -2695,6 +2695,87 @@ type Birthdate struct { Year int `json:"year,omitempty"` } +// Revenue withdrawal state type constants. +const ( + RevenueWithdrawalStateTypePending = "pending" + RevenueWithdrawalStateTypeSucceeded = "succeeded" + RevenueWithdrawalStateTypeFailed = "failed" +) + +// RevenueWithdrawalState describes the state of a revenue withdrawal. Flat +// polymorphic by Type: +// - "pending" → (no extra fields) +// - "succeeded" → Date, URL +// - "failed" → (no extra fields) +type RevenueWithdrawalState struct { + // Type of the state. One of "pending", "succeeded", "failed". + Type string `json:"type"` + // Date of the withdrawal in Unix time. Set when Type is "succeeded". + // + // optional + Date int `json:"date,omitempty"` + // URL is an HTTPS URL where the withdrawal transaction can be seen. + // Set when Type is "succeeded". + // + // optional + URL string `json:"url,omitempty"` +} + +// Transaction partner type constants. +const ( + TransactionPartnerTypeUser = "user" + TransactionPartnerTypeFragment = "fragment" + TransactionPartnerTypeOther = "other" +) + +// TransactionPartner describes the source or recipient of a StarTransaction. +// Flat polymorphic by Type: +// - "user" → User is set +// - "fragment" → WithdrawalState is set +// - "other" → (no extra fields) +type TransactionPartner struct { + // Type of the transaction partner. One of "user", "fragment", "other". + Type string `json:"type"` + // User that the transaction involves. Set when Type is "user". + // + // optional + User *User `json:"user,omitempty"` + // WithdrawalState is the state of the transaction if the transaction is + // outgoing. Set when Type is "fragment". + // + // optional + WithdrawalState *RevenueWithdrawalState `json:"withdrawal_state,omitempty"` +} + +// StarTransaction describes a Telegram Star transaction. +type StarTransaction struct { + // ID is the unique identifier of the transaction. Coincides with the + // identifier of the original transaction for refund transactions. Can + // be used to match transactions to refunds. + ID string `json:"id"` + // Amount of Telegram Stars transferred by the transaction. + Amount int `json:"amount"` + // Date the transaction was created in Unix time. + Date int `json:"date"` + // Source of an incoming transaction (e.g. a user purchasing goods or + // services, Fragment refunding a failed withdrawal). Only for incoming + // transactions. + // + // optional + Source *TransactionPartner `json:"source,omitempty"` + // Receiver of an outgoing transaction (e.g. a user for a purchase + // refund, Fragment for a withdrawal). Only for outgoing transactions. + // + // optional + Receiver *TransactionPartner `json:"receiver,omitempty"` +} + +// StarTransactions contains a list of Telegram Star transactions. +type StarTransactions struct { + // Transactions is the list of transactions. + Transactions []StarTransaction `json:"transactions"` +} + // SharedUser contains information about a user that was shared with the bot // using a KeyboardButtonRequestUsers button. type SharedUser struct { From 0434aace44ac1ac534c041525a3b14a71eb81637 Mon Sep 17 00:00:00 2001 From: Kirill Date: Sat, 11 Apr 2026 07:40:24 +0400 Subject: [PATCH 34/65] Full support of API 7.6 --- configs.go | 82 +++++++++++++++++++++++++++++++++++++ types.go | 117 +++++++++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 192 insertions(+), 7 deletions(-) diff --git a/configs.go b/configs.go index 5b1a6669..595c5045 100644 --- a/configs.go +++ b/configs.go @@ -3108,6 +3108,88 @@ func (config DeleteChatStickerSetConfig) params() (Params, error) { return params, nil } +// SendPaidMediaConfig sends paid media to a channel or private chat. +type SendPaidMediaConfig struct { + BaseChat + // StarCount is the number of Telegram Stars that must be paid to buy + // access to the media; 1-2500. + StarCount int + // Media is the list of media to be sent; 1-10 items. + Media []InputPaidMedia + // Caption of the media to be sent, 0-1024 characters after entities parsing. + Caption string + // ParseMode mode for parsing entities in the caption. + ParseMode string + // CaptionEntities is a list of special entities that appear in the caption. + CaptionEntities []MessageEntity + // ShowCaptionAboveMedia pass True if the caption must be shown above the message media. + ShowCaptionAboveMedia bool +} + +func (config SendPaidMediaConfig) method() string { + return "sendPaidMedia" +} + +func (config SendPaidMediaConfig) params() (Params, error) { + params, err := config.BaseChat.params() + if err != nil { + return params, err + } + + params.AddNonZero("star_count", config.StarCount) + params.AddNonEmpty("caption", config.Caption) + params.AddNonEmpty("parse_mode", config.ParseMode) + params.AddBool("show_caption_above_media", config.ShowCaptionAboveMedia) + if err = params.AddAny("caption_entities", config.CaptionEntities); err != nil { + return params, err + } + err = params.AddAny("media", prepareInputPaidMediaForParams(config.Media)) + + return params, err +} + +func (config SendPaidMediaConfig) files() []RequestFile { + return prepareInputPaidMediaForFiles(config.Media) +} + +// prepareInputPaidMediaForParams rewrites InputPaidMedia entries whose Media +// or Thumbnail need uploading to attach:// references, mirroring +// prepareInputMediaForParams for regular media groups. +func prepareInputPaidMediaForParams(items []InputPaidMedia) []InputPaidMedia { + out := make([]InputPaidMedia, len(items)) + for i, m := range items { + if m.Media != nil && m.Media.NeedsUpload() { + m.Media = fileAttach(fmt.Sprintf("attach://paid-media-%d", i)) + } + if m.Thumbnail != nil && m.Thumbnail.NeedsUpload() { + m.Thumbnail = fileAttach(fmt.Sprintf("attach://paid-media-%d-thumbnail", i)) + } + out[i] = m + } + return out +} + +// prepareInputPaidMediaForFiles returns the upload entries for items in the +// slice whose Media or Thumbnail need uploading. +func prepareInputPaidMediaForFiles(items []InputPaidMedia) []RequestFile { + var files []RequestFile + for i, m := range items { + if m.Media != nil && m.Media.NeedsUpload() { + files = append(files, RequestFile{ + Name: fmt.Sprintf("paid-media-%d", i), + Data: m.Media, + }) + } + if m.Thumbnail != nil && m.Thumbnail.NeedsUpload() { + files = append(files, RequestFile{ + Name: fmt.Sprintf("paid-media-%d-thumbnail", i), + Data: m.Thumbnail, + }) + } + } + return files +} + // MediaGroupConfig allows you to send a group of media. // // Media consist of InputMedia items (InputMediaPhoto, InputMediaVideo). diff --git a/types.go b/types.go index 260749a8..c8f82615 100644 --- a/types.go +++ b/types.go @@ -551,6 +551,11 @@ type ChatFullInfo struct { // MaxReactionCount is the maximum number of reactions that can be set // on a message in the chat. MaxReactionCount int `json:"max_reaction_count"` + // CanSendPaidMedia is true, if paid media messages can be sent or + // forwarded to the channel chat. Channel chats only. + // + // optional + CanSendPaidMedia bool `json:"can_send_paid_media,omitempty"` } // Message represents a message. @@ -695,6 +700,10 @@ type Message struct { // // optional Photo []PhotoSize `json:"photo,omitempty"` + // PaidMedia is the paid media attached to the message. + // + // optional + PaidMedia *PaidMediaInfo `json:"paid_media,omitempty"` // Sticker message is a sticker, information about the sticker; // // optional @@ -2723,23 +2732,31 @@ type RevenueWithdrawalState struct { // Transaction partner type constants. const ( - TransactionPartnerTypeUser = "user" - TransactionPartnerTypeFragment = "fragment" - TransactionPartnerTypeOther = "other" + TransactionPartnerTypeUser = "user" + TransactionPartnerTypeFragment = "fragment" + TransactionPartnerTypeTelegramAds = "telegram_ads" + TransactionPartnerTypeOther = "other" ) // TransactionPartner describes the source or recipient of a StarTransaction. // Flat polymorphic by Type: -// - "user" → User is set -// - "fragment" → WithdrawalState is set -// - "other" → (no extra fields) +// - "user" → User is set; InvoicePayload optionally set +// - "fragment" → WithdrawalState is set +// - "telegram_ads" → (no extra fields) +// - "other" → (no extra fields) type TransactionPartner struct { - // Type of the transaction partner. One of "user", "fragment", "other". + // Type of the transaction partner. One of "user", "fragment", + // "telegram_ads", "other". Type string `json:"type"` // User that the transaction involves. Set when Type is "user". // // optional User *User `json:"user,omitempty"` + // InvoicePayload is the bot-specified invoice payload. Set when Type is + // "user". + // + // optional + InvoicePayload string `json:"invoice_payload,omitempty"` // WithdrawalState is the state of the transaction if the transaction is // outgoing. Set when Type is "fragment". // @@ -2776,6 +2793,88 @@ type StarTransactions struct { Transactions []StarTransaction `json:"transactions"` } +// Paid media type constants. +const ( + PaidMediaTypePreview = "preview" + PaidMediaTypePhoto = "photo" + PaidMediaTypeVideo = "video" +) + +// PaidMedia describes a media received in a paid message. Flat polymorphic +// by Type: +// - "preview" → Width, Height, Duration (optional) +// - "photo" → Photo is set +// - "video" → Video is set +type PaidMedia struct { + // Type of the paid media. One of "preview", "photo", "video". + Type string `json:"type"` + // Width is the media width as defined by the sender. Set when Type is + // "preview". + // + // optional + Width int `json:"width,omitempty"` + // Height is the media height as defined by the sender. Set when Type is + // "preview". + // + // optional + Height int `json:"height,omitempty"` + // Duration is the duration of the media in seconds as defined by the + // sender. Set when Type is "preview". + // + // optional + Duration int `json:"duration,omitempty"` + // Photo is the photo media. Set when Type is "photo". + // + // optional + Photo []PhotoSize `json:"photo,omitempty"` + // Video is the video media. Set when Type is "video". + // + // optional + Video *Video `json:"video,omitempty"` +} + +// PaidMediaInfo describes the paid media added to a message. +type PaidMediaInfo struct { + // StarCount is the number of Telegram Stars that must be paid to buy + // access to the media. + StarCount int `json:"star_count"` + // PaidMedia is the information about the paid media. + PaidMedia []PaidMedia `json:"paid_media"` +} + +// InputPaidMedia describes the paid media to be sent. Flat polymorphic by +// Type: +// - "photo" → Media is set +// - "video" → Media, Thumbnail, Width, Height, Duration, SupportsStreaming +type InputPaidMedia struct { + // Type of the media. One of "photo", "video". + Type string `json:"type"` + // Media is the file to send. Pass a file_id or URL, or upload a new one + // via FilePath/FileBytes/FileReader. + Media RequestFileData `json:"media"` + // Thumbnail of the file (video only). + // + // optional + Thumbnail RequestFileData `json:"thumbnail,omitempty"` + // Width of the video. + // + // optional + Width int `json:"width,omitempty"` + // Height of the video. + // + // optional + Height int `json:"height,omitempty"` + // Duration of the video in seconds. + // + // optional + Duration int `json:"duration,omitempty"` + // SupportsStreaming is true if the uploaded video is suitable for + // streaming. Video only. + // + // optional + SupportsStreaming bool `json:"supports_streaming,omitempty"` +} + // SharedUser contains information about a user that was shared with the bot // using a KeyboardButtonRequestUsers button. type SharedUser struct { @@ -2928,6 +3027,10 @@ type ExternalReplyInfo struct { // // optional Photo []PhotoSize `json:"photo,omitempty"` + // PaidMedia is set if the message contains paid media. + // + // optional + PaidMedia *PaidMediaInfo `json:"paid_media,omitempty"` // Sticker is set if the message is a sticker. // // optional From 11ffa8058297b39e850525abe39a8767326cc2da Mon Sep 17 00:00:00 2001 From: Kirill Date: Sat, 11 Apr 2026 07:41:41 +0400 Subject: [PATCH 35/65] Full support of API 7.7 --- types.go | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/types.go b/types.go index c8f82615..79b1fcbe 100644 --- a/types.go +++ b/types.go @@ -841,6 +841,11 @@ type Message struct { // // optional SuccessfulPayment *SuccessfulPayment `json:"successful_payment,omitempty"` + // RefundedPayment is a service message about a refunded payment, + // information about the payment. + // + // optional + RefundedPayment *RefundedPayment `json:"refunded_payment,omitempty"` // ConnectedWebsite is the domain name of the website on which the user has // logged in; // @@ -5086,6 +5091,25 @@ type SuccessfulPayment struct { ProviderPaymentChargeID string `json:"provider_payment_charge_id"` } +// RefundedPayment contains information about a refunded payment. +type RefundedPayment struct { + // Currency is the three-letter ISO 4217 currency code, or "XTR" for + // payments in Telegram Stars. Currently, always "XTR". + Currency string `json:"currency"` + // TotalAmount is the total refunded price in the smallest units of the + // currency (integer, not float/double). For example, for a price of + // US$ 1.45, total_amount == 145. + TotalAmount int `json:"total_amount"` + // InvoicePayload is the bot-specified invoice payload. + InvoicePayload string `json:"invoice_payload"` + // TelegramPaymentChargeID is the Telegram payment identifier. + TelegramPaymentChargeID string `json:"telegram_payment_charge_id"` + // ProviderPaymentChargeID is the provider payment identifier. + // + // optional + ProviderPaymentChargeID string `json:"provider_payment_charge_id,omitempty"` +} + // ShippingQuery contains information about an incoming shipping query. type ShippingQuery struct { // ID unique query identifier From 6e2bfc8965665ddce2d1adfe5c6d2a20844a51c9 Mon Sep 17 00:00:00 2001 From: Kirill Date: Sat, 11 Apr 2026 07:42:51 +0400 Subject: [PATCH 36/65] Full support of API 7.8 --- configs.go | 26 +++++++++++++++++++------- types.go | 4 ++++ 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/configs.go b/configs.go index 595c5045..0f4377ae 100644 --- a/configs.go +++ b/configs.go @@ -2247,11 +2247,16 @@ func (config GetUserChatBoostsConfig) params() (Params, error) { } // PinChatMessageConfig contains information of a message in a chat to pin. +// +// Provide the target chat via either ChatID (numeric identifier) or +// ChannelUsername ("@channelusername"); the first non-zero / non-empty +// value is used. type PinChatMessageConfig struct { - ChatID int64 - ChannelUsername string - MessageID int - DisableNotification bool + BusinessConnectionID string + ChatID int64 + ChannelUsername string + MessageID int + DisableNotification bool } func (config PinChatMessageConfig) method() string { @@ -2261,6 +2266,7 @@ func (config PinChatMessageConfig) method() string { func (config PinChatMessageConfig) params() (Params, error) { params := make(Params) + params.AddNonEmpty("business_connection_id", config.BusinessConnectionID) if err := params.AddFirstValid("chat_id", config.ChatID, config.ChannelUsername); err != nil { return params, err } @@ -2273,10 +2279,15 @@ func (config PinChatMessageConfig) params() (Params, error) { // UnpinChatMessageConfig contains information of a chat message to unpin. // // If MessageID is not specified, it will unpin the most recent pin. +// +// Provide the target chat via either ChatID (numeric identifier) or +// ChannelUsername ("@channelusername"); the first non-zero / non-empty +// value is used. type UnpinChatMessageConfig struct { - ChatID int64 - ChannelUsername string - MessageID int + BusinessConnectionID string + ChatID int64 + ChannelUsername string + MessageID int } func (config UnpinChatMessageConfig) method() string { @@ -2286,6 +2297,7 @@ func (config UnpinChatMessageConfig) method() string { func (config UnpinChatMessageConfig) params() (Params, error) { params := make(Params) + params.AddNonEmpty("business_connection_id", config.BusinessConnectionID) if err := params.AddFirstValid("chat_id", config.ChatID, config.ChannelUsername); err != nil { return params, err } diff --git a/types.go b/types.go index 79b1fcbe..bac41828 100644 --- a/types.go +++ b/types.go @@ -252,6 +252,10 @@ type User struct { // // optional CanConnectToBusiness bool `json:"can_connect_to_business,omitempty"` + // HasMainWebApp true, if the bot has a main Web App. Returned only in getMe. + // + // optional + HasMainWebApp bool `json:"has_main_web_app,omitempty"` // FirstName user's or bot's first name FirstName string `json:"first_name"` // LastName user's or bot's last name From 92d92943823413ed4ed8d3bbbfc0b7f4fcaaa9f3 Mon Sep 17 00:00:00 2001 From: Kirill Date: Sat, 11 Apr 2026 07:44:42 +0400 Subject: [PATCH 37/65] Full support of API 7.9 --- configs.go | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++++ types.go | 16 ++++++++++++++++ 2 files changed, 68 insertions(+) diff --git a/configs.go b/configs.go index 0f4377ae..a5ad40ef 100644 --- a/configs.go +++ b/configs.go @@ -1745,6 +1745,58 @@ func (config EditChatInviteLinkConfig) params() (Params, error) { return params, nil } +// CreateChatSubscriptionInviteLinkConfig creates a subscription invite link +// for a channel chat. The bot must have the can_invite_users administrator +// rights. The link can be edited using EditChatSubscriptionInviteLinkConfig +// or revoked using RevokeChatInviteLinkConfig. +type CreateChatSubscriptionInviteLinkConfig struct { + ChatConfig + Name string + SubscriptionPeriod int // required, in seconds; currently must be 2592000 (30 days) + SubscriptionPrice int // required, 1-10000 Telegram Stars +} + +func (CreateChatSubscriptionInviteLinkConfig) method() string { + return "createChatSubscriptionInviteLink" +} + +func (config CreateChatSubscriptionInviteLinkConfig) params() (Params, error) { + params := make(Params) + + if err := params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername); err != nil { + return params, err + } + params.AddNonEmpty("name", config.Name) + params.AddNonZero("subscription_period", config.SubscriptionPeriod) + params.AddNonZero("subscription_price", config.SubscriptionPrice) + + return params, nil +} + +// EditChatSubscriptionInviteLinkConfig edits a subscription invite link +// created by the bot. Only the Name field can be edited. +type EditChatSubscriptionInviteLinkConfig struct { + ChatConfig + InviteLink string + Name string +} + +func (EditChatSubscriptionInviteLinkConfig) method() string { + return "editChatSubscriptionInviteLink" +} + +func (config EditChatSubscriptionInviteLinkConfig) params() (Params, error) { + params := make(Params) + + if err := params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername); err != nil { + return params, err + } + params["invite_link"] = config.InviteLink + params.AddNonEmpty("name", config.Name) + + return params, nil +} + // RevokeChatInviteLinkConfig allows you to revoke an invite link created by the // bot. If the primary link is revoked, a new link is automatically generated. // The bot must be an administrator in the chat for this to work and must have diff --git a/types.go b/types.go index bac41828..8e99a966 100644 --- a/types.go +++ b/types.go @@ -2170,6 +2170,17 @@ type ChatInviteLink struct { // // optional PendingJoinRequestCount int `json:"pending_join_request_count,omitempty"` + // SubscriptionPeriod is the number of seconds the subscription will be + // active for before the next payment. + // + // optional + SubscriptionPeriod int `json:"subscription_period,omitempty"` + // SubscriptionPrice is the amount of Telegram Stars a user must pay + // initially and after each subsequent subscription period to be a member + // of the chat using the link. + // + // optional + SubscriptionPrice int `json:"subscription_price,omitempty"` } type ChatAdministratorRights struct { @@ -2766,6 +2777,11 @@ type TransactionPartner struct { // // optional InvoicePayload string `json:"invoice_payload,omitempty"` + // PaidMedia is the information about the paid media bought by the user. + // Set when Type is "user" and the transaction involves paid media. + // + // optional + PaidMedia []PaidMedia `json:"paid_media,omitempty"` // WithdrawalState is the state of the transaction if the transaction is // outgoing. Set when Type is "fragment". // From d37049b8b2f9f1a6235522504d01c46f6bf5f6e5 Mon Sep 17 00:00:00 2001 From: Kirill Date: Sat, 11 Apr 2026 07:47:09 +0400 Subject: [PATCH 38/65] Full support of API 7.10 --- configs.go | 8 ++++++++ types.go | 48 ++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 54 insertions(+), 2 deletions(-) diff --git a/configs.go b/configs.go index a5ad40ef..1705fc07 100644 --- a/configs.go +++ b/configs.go @@ -115,6 +115,10 @@ const ( // connected business account. UpdateTypeDeletedBusinessMessages = "deleted_business_messages" + // UpdateTypePurchasedPaidMedia is when a user purchased paid media with + // a non-empty payload sent by the bot in a non-channel chat. + UpdateTypePurchasedPaidMedia = "purchased_paid_media" + // UpdateTypeMyChatMember is when the bot's chat member status was updated in a chat. For private chats, this // update is received only when the bot is blocked or unblocked by the user. UpdateTypeMyChatMember = "my_chat_member" @@ -3180,6 +3184,9 @@ type SendPaidMediaConfig struct { StarCount int // Media is the list of media to be sent; 1-10 items. Media []InputPaidMedia + // Payload is the bot-defined paid media payload, 0-128 bytes. Received + // back in a PurchasedPaidMedia update and in TransactionPartner. + Payload string // Caption of the media to be sent, 0-1024 characters after entities parsing. Caption string // ParseMode mode for parsing entities in the caption. @@ -3201,6 +3208,7 @@ func (config SendPaidMediaConfig) params() (Params, error) { } params.AddNonZero("star_count", config.StarCount) + params.AddNonEmpty("payload", config.Payload) params.AddNonEmpty("caption", config.Caption) params.AddNonEmpty("parse_mode", config.ParseMode) params.AddBool("show_caption_above_media", config.ShowCaptionAboveMedia) diff --git a/types.go b/types.go index 8e99a966..8913be8d 100644 --- a/types.go +++ b/types.go @@ -137,6 +137,11 @@ type Update struct { // // optional DeletedBusinessMessages *BusinessMessagesDeleted `json:"deleted_business_messages,omitempty"` + // PurchasedPaidMedia is a user purchased paid media with a non-empty + // payload sent by the bot in a non-channel chat. + // + // optional + PurchasedPaidMedia *PaidMediaPurchased `json:"purchased_paid_media,omitempty"` // ChatBoost is a boost added to a chat or changed. The bot must be an // administrator in the chat to receive these updates. // @@ -2867,6 +2872,15 @@ type PaidMediaInfo struct { PaidMedia []PaidMedia `json:"paid_media"` } +// PaidMediaPurchased is received when a user purchases paid media with a +// non-empty payload sent by the bot in a non-channel chat. +type PaidMediaPurchased struct { + // From is the user who purchased the media. + From User `json:"from"` + // PaidMediaPayload is the bot-specified paid media payload. + PaidMediaPayload string `json:"paid_media_payload"` +} + // InputPaidMedia describes the paid media to be sent. Flat polymorphic by // Type: // - "photo" → Media is set @@ -3149,6 +3163,12 @@ type ChatBoostSource struct { // // optional IsUnclaimed bool `json:"is_unclaimed,omitempty"` + // PrizeStarCount is the number of Telegram Stars to be split between + // giveaway winners. Set for "giveaway" and only for Telegram Star + // giveaways. + // + // optional + PrizeStarCount int `json:"prize_star_count,omitempty"` } // ChatBoost contains information about a chat boost. @@ -3226,14 +3246,28 @@ type Giveaway struct { // // optional PremiumSubscriptionMonthCount int `json:"premium_subscription_month_count,omitempty"` + // PrizeStarCount is the number of Telegram Stars to be split between + // giveaway winners. For Telegram Star giveaways only. + // + // optional + PrizeStarCount int `json:"prize_star_count,omitempty"` } // GiveawayCreated represents a service message about the creation of a -// scheduled giveaway. Currently holds no information. -type GiveawayCreated struct{} +// scheduled giveaway. +type GiveawayCreated struct { + // PrizeStarCount is the number of Telegram Stars to be split between + // giveaway winners. For Telegram Star giveaways only. + // + // optional + PrizeStarCount int `json:"prize_star_count,omitempty"` +} // GiveawayWinners represents a message about the completion of a giveaway // with public winners. +// +// The PrizeStarCount field holds the number of Telegram Stars split between +// winners, if this was a Telegram Star giveaway. type GiveawayWinners struct { // Chat that created the giveaway. Chat Chat `json:"chat"` @@ -3257,6 +3291,11 @@ type GiveawayWinners struct { // // optional PremiumSubscriptionMonthCount int `json:"premium_subscription_month_count,omitempty"` + // PrizeStarCount is the number of Telegram Stars that were split between + // giveaway winners. For Telegram Star giveaways only. + // + // optional + PrizeStarCount int `json:"prize_star_count,omitempty"` // UnclaimedPrizeCount is the number of undistributed prizes. // // optional @@ -3291,6 +3330,11 @@ type GiveawayCompleted struct { // // optional GiveawayMessage *Message `json:"giveaway_message,omitempty"` + // IsStarGiveaway is true, if the giveaway is a Telegram Star giveaway. + // Otherwise, currently, the giveaway is a Telegram Premium giveaway. + // + // optional + IsStarGiveaway bool `json:"is_star_giveaway,omitempty"` } // Message origin type constants. From e8c298eb0e87679b658c0fc7c9a39909e819696b Mon Sep 17 00:00:00 2001 From: Kirill Date: Sat, 11 Apr 2026 07:49:20 +0400 Subject: [PATCH 39/65] Full support of API 7.11 --- configs.go | 4 ++++ types.go | 31 +++++++++++++++++++++++++------ 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/configs.go b/configs.go index 1705fc07..44ac57a6 100644 --- a/configs.go +++ b/configs.go @@ -308,6 +308,7 @@ type BaseChat struct { MessageThreadID int MessageEffectID string ProtectContent bool + AllowPaidBroadcast bool ReplyParameters *ReplyParameters ReplyMarkup interface{} DisableNotification bool @@ -324,6 +325,7 @@ func (chat *BaseChat) params() (Params, error) { params.AddNonEmpty("message_effect_id", chat.MessageEffectID) params.AddBool("disable_notification", chat.DisableNotification) params.AddBool("protect_content", chat.ProtectContent) + params.AddBool("allow_paid_broadcast", chat.AllowPaidBroadcast) if err := params.AddAny("reply_parameters", chat.ReplyParameters); err != nil { return params, err @@ -3275,6 +3277,7 @@ type MediaGroupConfig struct { Media []interface{} DisableNotification bool ProtectContent bool + AllowPaidBroadcast bool ReplyParameters *ReplyParameters } @@ -3293,6 +3296,7 @@ func (config MediaGroupConfig) params() (Params, error) { params.AddNonEmpty("message_effect_id", config.MessageEffectID) params.AddBool("disable_notification", config.DisableNotification) params.AddBool("protect_content", config.ProtectContent) + params.AddBool("allow_paid_broadcast", config.AllowPaidBroadcast) if err := params.AddAny("reply_parameters", config.ReplyParameters); err != nil { return params, err } diff --git a/types.go b/types.go index 8913be8d..bbc08b1b 100644 --- a/types.go +++ b/types.go @@ -1969,6 +1969,11 @@ type InlineKeyboardButton struct { // // optional SwitchInlineQueryChosenChat *SwitchInlineQueryChosenChat `json:"switch_inline_query_chosen_chat,omitempty"` + // CopyText if set, pressing the button will copy the specified text to + // the clipboard. + // + // optional + CopyText *CopyTextButton `json:"copy_text,omitempty"` // CallbackGame description of the game that will be launched when the user presses the button. // // optional @@ -1981,6 +1986,13 @@ type InlineKeyboardButton struct { Pay bool `json:"pay,omitempty"` } +// CopyTextButton represents an inline keyboard button that copies specified +// text to the clipboard when pressed. +type CopyTextButton struct { + // Text is the text to be copied to the clipboard; 1-256 characters. + Text string `json:"text"` +} + // SwitchInlineQueryChosenChat represents an inline button that switches the // current user to inline mode in a chosen chat, with an optional default // inline query. @@ -2757,22 +2769,29 @@ type RevenueWithdrawalState struct { // Transaction partner type constants. const ( - TransactionPartnerTypeUser = "user" - TransactionPartnerTypeFragment = "fragment" - TransactionPartnerTypeTelegramAds = "telegram_ads" - TransactionPartnerTypeOther = "other" + TransactionPartnerTypeUser = "user" + TransactionPartnerTypeFragment = "fragment" + TransactionPartnerTypeTelegramAds = "telegram_ads" + TransactionPartnerTypeTelegramApi = "telegram_api" + TransactionPartnerTypeOther = "other" ) // TransactionPartner describes the source or recipient of a StarTransaction. // Flat polymorphic by Type: -// - "user" → User is set; InvoicePayload optionally set +// - "user" → User is set; InvoicePayload / PaidMedia optionally set // - "fragment" → WithdrawalState is set // - "telegram_ads" → (no extra fields) +// - "telegram_api" → RequestCount is set // - "other" → (no extra fields) type TransactionPartner struct { // Type of the transaction partner. One of "user", "fragment", - // "telegram_ads", "other". + // "telegram_ads", "telegram_api", "other". Type string `json:"type"` + // RequestCount is the number of successful requests that caused the + // transaction. Set when Type is "telegram_api". + // + // optional + RequestCount int `json:"request_count,omitempty"` // User that the transaction involves. Set when Type is "user". // // optional From 5d6de3fb57a4c7af19e714c1e619e4364ef38de7 Mon Sep 17 00:00:00 2001 From: Kirill Date: Sat, 11 Apr 2026 16:29:19 +0400 Subject: [PATCH 40/65] Full support of API 8.0 --- bot.go | 28 +++++++++++++ configs.go | 115 +++++++++++++++++++++++++++++++++++++++++++++++++++++ types.go | 79 +++++++++++++++++++++++++++++++++++- 3 files changed, 220 insertions(+), 2 deletions(-) diff --git a/bot.go b/bot.go index 559e6a49..7d494825 100644 --- a/bot.go +++ b/bot.go @@ -735,6 +735,34 @@ func (bot *BotAPI) CopyMessages(config CopyMessagesConfig) ([]MessageID, error) return ids, err } +// GetAvailableGifts returns the list of gifts that can be sent by the bot +// to users. +func (bot *BotAPI) GetAvailableGifts() (Gifts, error) { + resp, err := bot.Request(GetAvailableGiftsConfig{}) + if err != nil { + return Gifts{}, err + } + + var gifts Gifts + err = json.Unmarshal(resp.Result, &gifts) + + return gifts, err +} + +// SavePreparedInlineMessage stores a message that can be sent by a user of +// a Mini App and returns a PreparedInlineMessage. +func (bot *BotAPI) SavePreparedInlineMessage(config SavePreparedInlineMessageConfig) (PreparedInlineMessage, error) { + resp, err := bot.Request(config) + if err != nil { + return PreparedInlineMessage{}, err + } + + var msg PreparedInlineMessage + err = json.Unmarshal(resp.Result, &msg) + + return msg, err +} + // GetStarTransactions returns the bot's Telegram Star transactions in // chronological order. func (bot *BotAPI) GetStarTransactions(config GetStarTransactionsConfig) (StarTransactions, error) { diff --git a/configs.go b/configs.go index 44ac57a6..718339e3 100644 --- a/configs.go +++ b/configs.go @@ -1984,12 +1984,14 @@ func (config InvoiceConfig) method() string { // InvoiceLinkConfig contains information for createInvoiceLink request. type InvoiceLinkConfig struct { + BusinessConnectionID string Title string // required Description string // required Payload string // required ProviderToken string // omit for payments in Telegram Stars Currency string // required ("XTR" for Telegram Stars) Prices []LabeledPrice // required + SubscriptionPeriod int // in seconds; currently must be 2592000 (30 days) if set MaxTipAmount int SuggestedTipAmounts []int ProviderData json.RawMessage @@ -2009,11 +2011,13 @@ type InvoiceLinkConfig struct { func (config InvoiceLinkConfig) params() (Params, error) { params := make(Params) + params.AddNonEmpty("business_connection_id", config.BusinessConnectionID) params["title"] = config.Title params["description"] = config.Description params["payload"] = config.Payload params.AddNonEmpty("provider_token", config.ProviderToken) params["currency"] = config.Currency + params.AddNonZero("subscription_period", config.SubscriptionPeriod) if err := params.AddAny("prices", config.Prices); err != nil { return params, err } @@ -2044,6 +2048,117 @@ func (config InvoiceLinkConfig) method() string { return "createInvoiceLink" } +// GetAvailableGiftsConfig returns the list of gifts that can be sent by the +// bot to users. +type GetAvailableGiftsConfig struct{} + +func (GetAvailableGiftsConfig) method() string { + return "getAvailableGifts" +} + +func (GetAvailableGiftsConfig) params() (Params, error) { + return make(Params), nil +} + +// SendGiftConfig sends a gift to a user. +type SendGiftConfig struct { + UserID int64 + GiftID string + Text string + TextParseMode string + TextEntities []MessageEntity +} + +func (SendGiftConfig) method() string { + return "sendGift" +} + +func (config SendGiftConfig) params() (Params, error) { + params := make(Params) + + params.AddNonZero64("user_id", config.UserID) + params["gift_id"] = config.GiftID + params.AddNonEmpty("text", config.Text) + params.AddNonEmpty("text_parse_mode", config.TextParseMode) + err := params.AddAny("text_entities", config.TextEntities) + + return params, err +} + +// SetUserEmojiStatusConfig changes the emoji status for a given user that +// previously allowed the bot to manage their emoji status. +type SetUserEmojiStatusConfig struct { + UserID int64 + EmojiStatusCustomEmojiID string + EmojiStatusExpirationDate int +} + +func (SetUserEmojiStatusConfig) method() string { + return "setUserEmojiStatus" +} + +func (config SetUserEmojiStatusConfig) params() (Params, error) { + params := make(Params) + + params.AddNonZero64("user_id", config.UserID) + params.AddNonEmpty("emoji_status_custom_emoji_id", config.EmojiStatusCustomEmojiID) + params.AddNonZero("emoji_status_expiration_date", config.EmojiStatusExpirationDate) + + return params, nil +} + +// EditUserStarSubscriptionConfig cancels or reenables an active Telegram +// Star subscription. +type EditUserStarSubscriptionConfig struct { + UserID int64 + TelegramPaymentChargeID string + IsCanceled bool +} + +func (EditUserStarSubscriptionConfig) method() string { + return "editUserStarSubscription" +} + +func (config EditUserStarSubscriptionConfig) params() (Params, error) { + params := make(Params) + + params.AddNonZero64("user_id", config.UserID) + params["telegram_payment_charge_id"] = config.TelegramPaymentChargeID + params.AddBool("is_canceled", config.IsCanceled) + + return params, nil +} + +// SavePreparedInlineMessageConfig stores a message that can be sent by a +// user of a Mini App. +type SavePreparedInlineMessageConfig struct { + UserID int64 + Result interface{} // InlineQueryResult + AllowUserChats bool + AllowBotChats bool + AllowGroupChats bool + AllowChannelChats bool +} + +func (SavePreparedInlineMessageConfig) method() string { + return "savePreparedInlineMessage" +} + +func (config SavePreparedInlineMessageConfig) params() (Params, error) { + params := make(Params) + + params.AddNonZero64("user_id", config.UserID) + if err := params.AddAny("result", config.Result); err != nil { + return params, err + } + params.AddBool("allow_user_chats", config.AllowUserChats) + params.AddBool("allow_bot_chats", config.AllowBotChats) + params.AddBool("allow_group_chats", config.AllowGroupChats) + params.AddBool("allow_channel_chats", config.AllowChannelChats) + + return params, nil +} + // GetStarTransactionsConfig returns the bot's Telegram Star transactions in // chronological order. type GetStarTransactionsConfig struct { diff --git a/types.go b/types.go index bbc08b1b..29bfe1c0 100644 --- a/types.go +++ b/types.go @@ -569,7 +569,13 @@ type ChatFullInfo 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. + // + // Note: starting December 1, 2024, video messages sent, copied or + // forwarded to groups and channels with a sufficiently large audience + // may be scheduled by the server until the video is reencoded. Such + // messages come back with MessageID == 0 and cannot be referenced + // (replied to, edited, forwarded) until Telegram finishes processing. MessageID int `json:"message_id"` // BusinessConnectionID is the unique identifier of the business // connection from which the message was received. If non-empty, the @@ -1037,7 +1043,13 @@ func (m *Message) CommandArguments() string { return m.Text[entity.Length+1:] } -// MessageID represents a unique message identifier. +// MessageID represents a unique message identifier. Returned by +// forwardMessage(s), copyMessage(s), and sendMediaGroup. +// +// Note: for video forwards/copies to large groups and channels, Telegram +// may schedule the message until the video finishes reencoding (since +// December 1, 2024). In that case MessageID == 0 and the message cannot +// be referenced until it is actually sent. type MessageID struct { MessageID int `json:"message_id"` } @@ -2801,11 +2813,22 @@ type TransactionPartner struct { // // optional InvoicePayload string `json:"invoice_payload,omitempty"` + // SubscriptionPeriod is the number of seconds the subscription will be + // active for. Set when Type is "user" and the transaction is the + // payment for a subscription. + // + // optional + SubscriptionPeriod int `json:"subscription_period,omitempty"` // PaidMedia is the information about the paid media bought by the user. // Set when Type is "user" and the transaction involves paid media. // // optional PaidMedia []PaidMedia `json:"paid_media,omitempty"` + // Gift is the gift sent to the user by the bot. Set when Type is + // "user" and the transaction is a gift purchase. + // + // optional + Gift *Gift `json:"gift,omitempty"` // WithdrawalState is the state of the transaction if the transaction is // outgoing. Set when Type is "fragment". // @@ -2900,6 +2923,43 @@ type PaidMediaPurchased struct { PaidMediaPayload string `json:"paid_media_payload"` } +// Gift represents a gift that can be sent by the bot. +type Gift struct { + // ID is the unique identifier of the gift. + ID string `json:"id"` + // Sticker representing the gift. + Sticker Sticker `json:"sticker"` + // StarCount is the number of Telegram Stars that must be paid to send + // the sticker. + StarCount int `json:"star_count"` + // TotalCount is the total number of the gifts of this type that can + // be sent; for limited gifts only. + // + // optional + TotalCount int `json:"total_count,omitempty"` + // RemainingCount is the number of remaining gifts of this type that + // can be sent; for limited gifts only. + // + // optional + RemainingCount int `json:"remaining_count,omitempty"` +} + +// Gifts represents a list of gifts. +type Gifts struct { + // Gifts is the list of gifts. + Gifts []Gift `json:"gifts"` +} + +// PreparedInlineMessage describes an inline message to be sent by a user of +// a Mini App. +type PreparedInlineMessage struct { + // ID is the unique identifier of the prepared message. + ID string `json:"id"` + // ExpirationDate is the Unix timestamp at which the message can no + // longer be used. + ExpirationDate int `json:"expiration_date"` +} + // InputPaidMedia describes the paid media to be sent. Flat polymorphic by // Type: // - "photo" → Media is set @@ -5160,6 +5220,21 @@ type SuccessfulPayment struct { TotalAmount int `json:"total_amount"` // InvoicePayload bot specified invoice payload InvoicePayload string `json:"invoice_payload"` + // SubscriptionExpirationDate is the expiration date of the subscription, + // in Unix time. For recurring payments only. + // + // optional + SubscriptionExpirationDate int `json:"subscription_expiration_date,omitempty"` + // IsRecurring is true, if the payment is a recurring payment for a + // subscription. + // + // optional + IsRecurring bool `json:"is_recurring,omitempty"` + // IsFirstRecurring is true, if the payment is the first payment for a + // subscription. + // + // optional + IsFirstRecurring bool `json:"is_first_recurring,omitempty"` // ShippingOptionID identifier of the shipping option chosen by the user // // optional From e9977844b778147b373faf7c3ca56812760025c6 Mon Sep 17 00:00:00 2001 From: Kirill Date: Sat, 11 Apr 2026 16:30:39 +0400 Subject: [PATCH 41/65] Full support of API 8.1 --- types.go | 72 ++++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 60 insertions(+), 12 deletions(-) diff --git a/types.go b/types.go index 29bfe1c0..5a5c86fe 100644 --- a/types.go +++ b/types.go @@ -2781,23 +2781,50 @@ type RevenueWithdrawalState struct { // Transaction partner type constants. const ( - TransactionPartnerTypeUser = "user" - TransactionPartnerTypeFragment = "fragment" - TransactionPartnerTypeTelegramAds = "telegram_ads" - TransactionPartnerTypeTelegramApi = "telegram_api" - TransactionPartnerTypeOther = "other" + TransactionPartnerTypeUser = "user" + TransactionPartnerTypeAffiliateProgram = "affiliate_program" + TransactionPartnerTypeFragment = "fragment" + TransactionPartnerTypeTelegramAds = "telegram_ads" + TransactionPartnerTypeTelegramApi = "telegram_api" + TransactionPartnerTypeOther = "other" ) +// AffiliateInfo contains information about the affiliate that received a +// commission via an affiliate program. +type AffiliateInfo struct { + // AffiliateUser is the bot or user that received the commission. + // + // optional + AffiliateUser *User `json:"affiliate_user,omitempty"` + // AffiliateChat is the chat that received the commission. + // + // optional + AffiliateChat *Chat `json:"affiliate_chat,omitempty"` + // CommissionPerMille is the number of Telegram Stars received by the + // affiliate for each 1000 Telegram Stars received by the bot from + // referred users. + CommissionPerMille int `json:"commission_per_mille"` + // Amount is the integer amount of Telegram Stars received by the + // affiliate from the transaction, rounded to 0; can be negative. + Amount int `json:"amount"` + // NanostarAmount is the number of 1/1000000000 shares of Telegram Stars + // received by the affiliate. Can be negative. + // + // optional + NanostarAmount int `json:"nanostar_amount,omitempty"` +} + // TransactionPartner describes the source or recipient of a StarTransaction. // Flat polymorphic by Type: -// - "user" → User is set; InvoicePayload / PaidMedia optionally set -// - "fragment" → WithdrawalState is set -// - "telegram_ads" → (no extra fields) -// - "telegram_api" → RequestCount is set -// - "other" → (no extra fields) +// - "user" → User is set; Affiliate / InvoicePayload / PaidMedia / Gift optionally set +// - "affiliate_program" → SponsorUser, CommissionPerMille +// - "fragment" → WithdrawalState is set +// - "telegram_ads" → (no extra fields) +// - "telegram_api" → RequestCount is set +// - "other" → (no extra fields) type TransactionPartner struct { - // Type of the transaction partner. One of "user", "fragment", - // "telegram_ads", "telegram_api", "other". + // Type of the transaction partner. One of "user", "affiliate_program", + // "fragment", "telegram_ads", "telegram_api", "other". Type string `json:"type"` // RequestCount is the number of successful requests that caused the // transaction. Set when Type is "telegram_api". @@ -2808,6 +2835,11 @@ type TransactionPartner struct { // // optional User *User `json:"user,omitempty"` + // Affiliate information if the transaction involves an affiliate + // commission. Set when Type is "user". + // + // optional + Affiliate *AffiliateInfo `json:"affiliate,omitempty"` // InvoicePayload is the bot-specified invoice payload. Set when Type is // "user". // @@ -2829,6 +2861,17 @@ type TransactionPartner struct { // // optional Gift *Gift `json:"gift,omitempty"` + // SponsorUser is the bot owning the affiliate program. Set when Type + // is "affiliate_program". + // + // optional + SponsorUser *User `json:"sponsor_user,omitempty"` + // CommissionPerMille is the number of Telegram Stars received by the + // bot for each 1000 Telegram Stars received by the affiliate program + // sponsor from referred users. Set when Type is "affiliate_program". + // + // optional + CommissionPerMille int `json:"commission_per_mille,omitempty"` // WithdrawalState is the state of the transaction if the transaction is // outgoing. Set when Type is "fragment". // @@ -2844,6 +2887,11 @@ type StarTransaction struct { ID string `json:"id"` // Amount of Telegram Stars transferred by the transaction. Amount int `json:"amount"` + // NanostarAmount is the number of 1/1000000000 shares of Telegram Stars + // transferred by the transaction; from 0 to 999999999. + // + // optional + NanostarAmount int `json:"nanostar_amount,omitempty"` // Date the transaction was created in Unix time. Date int `json:"date"` // Source of an incoming transaction (e.g. a user purchasing goods or From e98ae487951c88047c3c8e9cab5ef803709a9175 Mon Sep 17 00:00:00 2001 From: Kirill Date: Sat, 11 Apr 2026 16:31:56 +0400 Subject: [PATCH 42/65] Full support of API 8.2 --- configs.go | 92 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ types.go | 9 +++--- 2 files changed, 97 insertions(+), 4 deletions(-) diff --git a/configs.go b/configs.go index 718339e3..2e9afc6f 100644 --- a/configs.go +++ b/configs.go @@ -2064,6 +2064,7 @@ func (GetAvailableGiftsConfig) params() (Params, error) { type SendGiftConfig struct { UserID int64 GiftID string + PayForUpgrade bool Text string TextParseMode string TextEntities []MessageEntity @@ -2078,6 +2079,7 @@ func (config SendGiftConfig) params() (Params, error) { params.AddNonZero64("user_id", config.UserID) params["gift_id"] = config.GiftID + params.AddBool("pay_for_upgrade", config.PayForUpgrade) params.AddNonEmpty("text", config.Text) params.AddNonEmpty("text_parse_mode", config.TextParseMode) err := params.AddAny("text_entities", config.TextEntities) @@ -2085,6 +2087,96 @@ func (config SendGiftConfig) params() (Params, error) { return params, err } +// VerifyUserConfig verifies a user on behalf of the organization which is +// represented by the bot. +type VerifyUserConfig struct { + UserID int64 + CustomDescription string +} + +func (VerifyUserConfig) method() string { + return "verifyUser" +} + +func (config VerifyUserConfig) params() (Params, error) { + params := make(Params) + + params.AddNonZero64("user_id", config.UserID) + params.AddNonEmpty("custom_description", config.CustomDescription) + + return params, nil +} + +// VerifyChatConfig verifies a chat on behalf of the organization which is +// represented by the bot. +// +// Provide the target chat via either ChatID (numeric identifier) or +// ChannelUsername ("@channelusername"); the first non-zero / non-empty +// value is used. +type VerifyChatConfig struct { + ChatID int64 + ChannelUsername string + CustomDescription string +} + +func (VerifyChatConfig) method() string { + return "verifyChat" +} + +func (config VerifyChatConfig) params() (Params, error) { + params := make(Params) + + if err := params.AddFirstValid("chat_id", config.ChatID, config.ChannelUsername); err != nil { + return params, err + } + params.AddNonEmpty("custom_description", config.CustomDescription) + + return params, nil +} + +// RemoveUserVerificationConfig removes verification from a user who is +// currently verified on behalf of the organization represented by the bot. +type RemoveUserVerificationConfig struct { + UserID int64 +} + +func (RemoveUserVerificationConfig) method() string { + return "removeUserVerification" +} + +func (config RemoveUserVerificationConfig) params() (Params, error) { + params := make(Params) + + params.AddNonZero64("user_id", config.UserID) + + return params, nil +} + +// RemoveChatVerificationConfig removes verification from a chat that is +// currently verified on behalf of the organization represented by the bot. +// +// Provide the target chat via either ChatID (numeric identifier) or +// ChannelUsername ("@channelusername"); the first non-zero / non-empty +// value is used. +type RemoveChatVerificationConfig struct { + ChatID int64 + ChannelUsername string +} + +func (RemoveChatVerificationConfig) method() string { + return "removeChatVerification" +} + +func (config RemoveChatVerificationConfig) params() (Params, error) { + params := make(Params) + + if err := params.AddFirstValid("chat_id", config.ChatID, config.ChannelUsername); err != nil { + return params, err + } + + return params, nil +} + // SetUserEmojiStatusConfig changes the emoji status for a given user that // previously allowed the bot to manage their emoji status. type SetUserEmojiStatusConfig struct { diff --git a/types.go b/types.go index 5a5c86fe..0dfaa465 100644 --- a/types.go +++ b/types.go @@ -2980,6 +2980,11 @@ type Gift struct { // StarCount is the number of Telegram Stars that must be paid to send // the sticker. StarCount int `json:"star_count"` + // UpgradeStarCount is the number of Telegram Stars that must be paid + // to upgrade the gift to a unique one. + // + // optional + UpgradeStarCount int `json:"upgrade_star_count,omitempty"` // TotalCount is the total number of the gifts of this type that can // be sent; for limited gifts only. // @@ -4439,10 +4444,6 @@ type InlineQueryResultArticle struct { // // optional URL string `json:"url,omitempty"` - // HideURL pass True, if you don't want the URL to be shown in the message. - // - // optional - HideURL bool `json:"hide_url,omitempty"` // Description short description of the result. // // optional From 5a5b3623090fdc8f4e65b3687ea9c8dfb99a366d Mon Sep 17 00:00:00 2001 From: Kirill Date: Sat, 11 Apr 2026 16:36:04 +0400 Subject: [PATCH 43/65] Full support of API 8.3 --- configs.go | 56 ++++++++++++++++++++++++++++++++++++++++++++---------- types.go | 45 ++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 88 insertions(+), 13 deletions(-) diff --git a/configs.go b/configs.go index 2e9afc6f..cba9ab37 100644 --- a/configs.go +++ b/configs.go @@ -409,6 +409,7 @@ type ForwardConfig struct { FromChatID int64 // required FromChannelUsername string MessageID int // required + VideoStartTimestamp int } func (config ForwardConfig) params() (Params, error) { @@ -419,6 +420,7 @@ func (config ForwardConfig) params() (Params, error) { params.AddNonZero64("from_chat_id", config.FromChatID) params.AddNonZero("message_id", config.MessageID) + params.AddNonZero("video_start_timestamp", config.VideoStartTimestamp) return params, nil } @@ -433,6 +435,7 @@ type CopyMessageConfig struct { FromChatID int64 FromChannelUsername string MessageID int + VideoStartTimestamp int Caption string ParseMode string CaptionEntities []MessageEntity @@ -449,6 +452,7 @@ func (config CopyMessageConfig) params() (Params, error) { return params, err } params.AddNonZero("message_id", config.MessageID) + params.AddNonZero("video_start_timestamp", config.VideoStartTimestamp) params.AddNonEmpty("caption", config.Caption) params.AddNonEmpty("parse_mode", config.ParseMode) params.AddBool("show_caption_above_media", config.ShowCaptionAboveMedia) @@ -628,6 +632,8 @@ func (config StickerConfig) files() []RequestFile { type VideoConfig struct { BaseFile Thumbnail RequestFileData + Cover RequestFileData + StartTimestamp int Duration int Caption string ParseMode string @@ -644,6 +650,7 @@ func (config VideoConfig) params() (Params, error) { } params.AddNonZero("duration", config.Duration) + params.AddNonZero("start_timestamp", config.StartTimestamp) params.AddNonEmpty("caption", config.Caption) params.AddNonEmpty("parse_mode", config.ParseMode) params.AddBool("show_caption_above_media", config.ShowCaptionAboveMedia) @@ -671,6 +678,13 @@ func (config VideoConfig) files() []RequestFile { }) } + if config.Cover != nil { + files = append(files, RequestFile{ + Name: "cover", + Data: config.Cover, + }) + } + return files } @@ -2060,14 +2074,20 @@ func (GetAvailableGiftsConfig) params() (Params, error) { return make(Params), nil } -// SendGiftConfig sends a gift to a user. +// SendGiftConfig sends a gift to a user or channel chat. +// +// Provide the recipient via either UserID (for a user) or ChatID / +// ChannelUsername (for a channel chat); exactly one of UserID or the chat +// identifier should be set. type SendGiftConfig struct { - UserID int64 - GiftID string - PayForUpgrade bool - Text string - TextParseMode string - TextEntities []MessageEntity + UserID int64 + ChatID int64 + ChannelUsername string + GiftID string + PayForUpgrade bool + Text string + TextParseMode string + TextEntities []MessageEntity } func (SendGiftConfig) method() string { @@ -2078,6 +2098,11 @@ func (config SendGiftConfig) params() (Params, error) { params := make(Params) params.AddNonZero64("user_id", config.UserID) + if config.ChatID != 0 || config.ChannelUsername != "" { + if err := params.AddFirstValid("chat_id", config.ChatID, config.ChannelUsername); err != nil { + return params, err + } + } params["gift_id"] = config.GiftID params.AddBool("pay_for_upgrade", config.PayForUpgrade) params.AddNonEmpty("text", config.Text) @@ -3895,6 +3920,10 @@ func prepareInputMediaParam(inputMedia interface{}, idx int) interface{} { m.Thumbnail = fileAttach(fmt.Sprintf("attach://file-%d-thumbnail", idx)) } + if m.Cover != nil && m.Cover.NeedsUpload() { + m.Cover = fileAttach(fmt.Sprintf("attach://file-%d-cover", idx)) + } + return m case InputMediaAudio: if m.Media.NeedsUpload() { @@ -3950,10 +3979,17 @@ func prepareInputMediaFile(inputMedia interface{}, idx int) []RequestFile { if m.Thumbnail != nil && m.Thumbnail.NeedsUpload() { files = append(files, RequestFile{ - Name: fmt.Sprintf("file-%d", idx), + Name: fmt.Sprintf("file-%d-thumbnail", idx), Data: m.Thumbnail, }) } + + if m.Cover != nil && m.Cover.NeedsUpload() { + files = append(files, RequestFile{ + Name: fmt.Sprintf("file-%d-cover", idx), + Data: m.Cover, + }) + } case InputMediaDocument: if m.Media.NeedsUpload() { files = append(files, RequestFile{ @@ -3964,7 +4000,7 @@ func prepareInputMediaFile(inputMedia interface{}, idx int) []RequestFile { if m.Thumbnail != nil && m.Thumbnail.NeedsUpload() { files = append(files, RequestFile{ - Name: fmt.Sprintf("file-%d", idx), + Name: fmt.Sprintf("file-%d-thumbnail", idx), Data: m.Thumbnail, }) } @@ -3978,7 +4014,7 @@ func prepareInputMediaFile(inputMedia interface{}, idx int) []RequestFile { if m.Thumbnail != nil && m.Thumbnail.NeedsUpload() { files = append(files, RequestFile{ - Name: fmt.Sprintf("file-%d", idx), + Name: fmt.Sprintf("file-%d-thumbnail", idx), Data: m.Thumbnail, }) } diff --git a/types.go b/types.go index 0dfaa465..d3cb47ca 100644 --- a/types.go +++ b/types.go @@ -565,6 +565,10 @@ type ChatFullInfo struct { // // optional CanSendPaidMedia bool `json:"can_send_paid_media,omitempty"` + // CanSendGift is true, if gifts can be sent to the chat. Channel chats only. + // + // optional + CanSendGift bool `json:"can_send_gift,omitempty"` } // Message represents a message. @@ -1301,6 +1305,15 @@ type Video struct { // // optional Thumbnail *PhotoSize `json:"thumbnail,omitempty"` + // Cover is the available sizes of the cover of the video in the message. + // + // optional + Cover []PhotoSize `json:"cover,omitempty"` + // StartTimestamp is the timestamp in seconds from which the video will + // play in the message. + // + // optional + StartTimestamp int `json:"start_timestamp,omitempty"` // FileName is the original filename as defined by sender // // optional @@ -2782,6 +2795,7 @@ type RevenueWithdrawalState struct { // Transaction partner type constants. const ( TransactionPartnerTypeUser = "user" + TransactionPartnerTypeChat = "chat" TransactionPartnerTypeAffiliateProgram = "affiliate_program" TransactionPartnerTypeFragment = "fragment" TransactionPartnerTypeTelegramAds = "telegram_ads" @@ -2817,15 +2831,21 @@ type AffiliateInfo struct { // TransactionPartner describes the source or recipient of a StarTransaction. // Flat polymorphic by Type: // - "user" → User is set; Affiliate / InvoicePayload / PaidMedia / Gift optionally set +// - "chat" → Chat is set; Gift optionally set // - "affiliate_program" → SponsorUser, CommissionPerMille // - "fragment" → WithdrawalState is set // - "telegram_ads" → (no extra fields) // - "telegram_api" → RequestCount is set // - "other" → (no extra fields) type TransactionPartner struct { - // Type of the transaction partner. One of "user", "affiliate_program", - // "fragment", "telegram_ads", "telegram_api", "other". + // Type of the transaction partner. One of "user", "chat", + // "affiliate_program", "fragment", "telegram_ads", "telegram_api", + // "other". Type string `json:"type"` + // Chat that the transaction involves. Set when Type is "chat". + // + // optional + Chat *Chat `json:"chat,omitempty"` // RequestCount is the number of successful requests that caused the // transaction. Set when Type is "telegram_api". // @@ -3016,7 +3036,8 @@ type PreparedInlineMessage struct { // InputPaidMedia describes the paid media to be sent. Flat polymorphic by // Type: // - "photo" → Media is set -// - "video" → Media, Thumbnail, Width, Height, Duration, SupportsStreaming +// - "video" → Media, Thumbnail, Cover, StartTimestamp, Width, Height, +// Duration, SupportsStreaming type InputPaidMedia struct { // Type of the media. One of "photo", "video". Type string `json:"type"` @@ -3027,6 +3048,15 @@ type InputPaidMedia struct { // // optional Thumbnail RequestFileData `json:"thumbnail,omitempty"` + // Cover for the video in the message (video only). + // + // optional + Cover RequestFileData `json:"cover,omitempty"` + // StartTimestamp is the timestamp in seconds from which the video will + // play in the message (video only). + // + // optional + StartTimestamp int `json:"start_timestamp,omitempty"` // Width of the video. // // optional @@ -3791,6 +3821,15 @@ type InputMediaVideo struct { // // optional Thumbnail RequestFileData `json:"thumbnail,omitempty"` + // Cover for the video in the message. + // + // optional + Cover RequestFileData `json:"cover,omitempty"` + // StartTimestamp is the timestamp in seconds from which the video will + // play in the message. + // + // optional + StartTimestamp int `json:"start_timestamp,omitempty"` // Width video width // // optional From 9d713943abcf4c2a01a474244962979856bb3caa Mon Sep 17 00:00:00 2001 From: Kirill Date: Sat, 11 Apr 2026 16:40:48 +0400 Subject: [PATCH 44/65] Full support of API 9.0 --- bot.go | 56 ++++++ configs.go | 484 ++++++++++++++++++++++++++++++++++++++++++++++ types.go | 558 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 1091 insertions(+), 7 deletions(-) diff --git a/bot.go b/bot.go index 7d494825..501eec9f 100644 --- a/bot.go +++ b/bot.go @@ -777,6 +777,62 @@ func (bot *BotAPI) GetStarTransactions(config GetStarTransactionsConfig) (StarTr return st, err } +// GetBusinessAccountStarBalance returns the amount of Telegram Stars owned +// by a managed business account. +func (bot *BotAPI) GetBusinessAccountStarBalance(config GetBusinessAccountStarBalanceConfig) (StarAmount, error) { + resp, err := bot.Request(config) + if err != nil { + return StarAmount{}, err + } + + var amount StarAmount + err = json.Unmarshal(resp.Result, &amount) + + return amount, err +} + +// GetBusinessAccountGifts returns the gifts received and owned by a managed +// business account. +func (bot *BotAPI) GetBusinessAccountGifts(config GetBusinessAccountGiftsConfig) (OwnedGifts, error) { + resp, err := bot.Request(config) + if err != nil { + return OwnedGifts{}, err + } + + var gifts OwnedGifts + err = json.Unmarshal(resp.Result, &gifts) + + return gifts, err +} + +// PostStory posts a story on behalf of a managed business account and +// returns the posted Story. +func (bot *BotAPI) PostStory(config PostStoryConfig) (Story, error) { + resp, err := bot.Request(config) + if err != nil { + return Story{}, err + } + + var story Story + err = json.Unmarshal(resp.Result, &story) + + return story, err +} + +// EditStory edits a story previously posted by the bot and returns the +// edited Story. +func (bot *BotAPI) EditStory(config EditStoryConfig) (Story, error) { + resp, err := bot.Request(config) + if err != nil { + return Story{}, err + } + + var story Story + err = json.Unmarshal(resp.Result, &story) + + return story, err +} + // GetBusinessConnection returns information about the connection of the bot // with a business account. func (bot *BotAPI) GetBusinessConnection(config GetBusinessConnectionConfig) (BusinessConnection, error) { diff --git a/configs.go b/configs.go index cba9ab37..e10946b8 100644 --- a/configs.go +++ b/configs.go @@ -3742,6 +3742,490 @@ func (config GetBusinessConnectionConfig) params() (Params, error) { return params, nil } +// ReadBusinessMessageConfig marks an incoming message as read on behalf of +// a business account. +type ReadBusinessMessageConfig struct { + BusinessConnectionID string + ChatID int64 + MessageID int +} + +func (ReadBusinessMessageConfig) method() string { + return "readBusinessMessage" +} + +func (config ReadBusinessMessageConfig) params() (Params, error) { + params := make(Params) + + params["business_connection_id"] = config.BusinessConnectionID + params.AddNonZero64("chat_id", config.ChatID) + params.AddNonZero("message_id", config.MessageID) + + return params, nil +} + +// DeleteBusinessMessagesConfig deletes messages on behalf of a business account. +type DeleteBusinessMessagesConfig struct { + BusinessConnectionID string + MessageIDs []int +} + +func (DeleteBusinessMessagesConfig) method() string { + return "deleteBusinessMessages" +} + +func (config DeleteBusinessMessagesConfig) params() (Params, error) { + params := make(Params) + + params["business_connection_id"] = config.BusinessConnectionID + err := params.AddAny("message_ids", config.MessageIDs) + + return params, err +} + +// SetBusinessAccountNameConfig changes the first and last name of a managed +// business account. +type SetBusinessAccountNameConfig struct { + BusinessConnectionID string + FirstName string + LastName string +} + +func (SetBusinessAccountNameConfig) method() string { + return "setBusinessAccountName" +} + +func (config SetBusinessAccountNameConfig) params() (Params, error) { + params := make(Params) + + params["business_connection_id"] = config.BusinessConnectionID + params["first_name"] = config.FirstName + params.AddNonEmpty("last_name", config.LastName) + + return params, nil +} + +// SetBusinessAccountUsernameConfig changes the username of a managed +// business account. +type SetBusinessAccountUsernameConfig struct { + BusinessConnectionID string + Username string +} + +func (SetBusinessAccountUsernameConfig) method() string { + return "setBusinessAccountUsername" +} + +func (config SetBusinessAccountUsernameConfig) params() (Params, error) { + params := make(Params) + + params["business_connection_id"] = config.BusinessConnectionID + params.AddNonEmpty("username", config.Username) + + return params, nil +} + +// SetBusinessAccountBioConfig changes the bio of a managed business account. +type SetBusinessAccountBioConfig struct { + BusinessConnectionID string + Bio string +} + +func (SetBusinessAccountBioConfig) method() string { + return "setBusinessAccountBio" +} + +func (config SetBusinessAccountBioConfig) params() (Params, error) { + params := make(Params) + + params["business_connection_id"] = config.BusinessConnectionID + params.AddNonEmpty("bio", config.Bio) + + return params, nil +} + +// SetBusinessAccountProfilePhotoConfig changes the profile photo of a +// managed business account. +type SetBusinessAccountProfilePhotoConfig struct { + BusinessConnectionID string + Photo InputProfilePhoto + IsPublic bool +} + +func (SetBusinessAccountProfilePhotoConfig) method() string { + return "setBusinessAccountProfilePhoto" +} + +func (config SetBusinessAccountProfilePhotoConfig) params() (Params, error) { + params := make(Params) + + params["business_connection_id"] = config.BusinessConnectionID + params.AddBool("is_public", config.IsPublic) + err := params.AddAny("photo", prepareInputProfilePhotoForParams(config.Photo)) + + return params, err +} + +func (config SetBusinessAccountProfilePhotoConfig) files() []RequestFile { + return prepareInputProfilePhotoForFiles(config.Photo) +} + +// RemoveBusinessAccountProfilePhotoConfig removes the current profile photo +// of a managed business account. +type RemoveBusinessAccountProfilePhotoConfig struct { + BusinessConnectionID string + IsPublic bool +} + +func (RemoveBusinessAccountProfilePhotoConfig) method() string { + return "removeBusinessAccountProfilePhoto" +} + +func (config RemoveBusinessAccountProfilePhotoConfig) params() (Params, error) { + params := make(Params) + + params["business_connection_id"] = config.BusinessConnectionID + params.AddBool("is_public", config.IsPublic) + + return params, nil +} + +// SetBusinessAccountGiftSettingsConfig changes the privacy settings pertaining +// to incoming gifts in a managed business account. +type SetBusinessAccountGiftSettingsConfig struct { + BusinessConnectionID string + ShowGiftButton bool + AcceptedGiftTypes AcceptedGiftTypes +} + +func (SetBusinessAccountGiftSettingsConfig) method() string { + return "setBusinessAccountGiftSettings" +} + +func (config SetBusinessAccountGiftSettingsConfig) params() (Params, error) { + params := make(Params) + + params["business_connection_id"] = config.BusinessConnectionID + params.AddBool("show_gift_button", config.ShowGiftButton) + err := params.AddAny("accepted_gift_types", config.AcceptedGiftTypes) + + return params, err +} + +// GetBusinessAccountStarBalanceConfig returns the amount of Telegram Stars +// owned by a managed business account. +type GetBusinessAccountStarBalanceConfig struct { + BusinessConnectionID string +} + +func (GetBusinessAccountStarBalanceConfig) method() string { + return "getBusinessAccountStarBalance" +} + +func (config GetBusinessAccountStarBalanceConfig) params() (Params, error) { + params := make(Params) + + params["business_connection_id"] = config.BusinessConnectionID + + return params, nil +} + +// TransferBusinessAccountStarsConfig transfers Telegram Stars from the +// business account balance to the bot's balance. +type TransferBusinessAccountStarsConfig struct { + BusinessConnectionID string + StarCount int +} + +func (TransferBusinessAccountStarsConfig) method() string { + return "transferBusinessAccountStars" +} + +func (config TransferBusinessAccountStarsConfig) params() (Params, error) { + params := make(Params) + + params["business_connection_id"] = config.BusinessConnectionID + params.AddNonZero("star_count", config.StarCount) + + return params, nil +} + +// GetBusinessAccountGiftsConfig returns the gifts received and owned by a +// managed business account. +type GetBusinessAccountGiftsConfig struct { + BusinessConnectionID string + ExcludeUnsaved bool + ExcludeSaved bool + ExcludeUnlimited bool + ExcludeLimited bool + ExcludeUnique bool + SortByPrice bool + Offset string + Limit int +} + +func (GetBusinessAccountGiftsConfig) method() string { + return "getBusinessAccountGifts" +} + +func (config GetBusinessAccountGiftsConfig) params() (Params, error) { + params := make(Params) + + params["business_connection_id"] = config.BusinessConnectionID + params.AddBool("exclude_unsaved", config.ExcludeUnsaved) + params.AddBool("exclude_saved", config.ExcludeSaved) + params.AddBool("exclude_unlimited", config.ExcludeUnlimited) + params.AddBool("exclude_limited", config.ExcludeLimited) + params.AddBool("exclude_unique", config.ExcludeUnique) + params.AddBool("sort_by_price", config.SortByPrice) + params.AddNonEmpty("offset", config.Offset) + params.AddNonZero("limit", config.Limit) + + return params, nil +} + +// ConvertGiftToStarsConfig converts a given regular gift to Telegram Stars. +type ConvertGiftToStarsConfig struct { + BusinessConnectionID string + OwnedGiftID string +} + +func (ConvertGiftToStarsConfig) method() string { + return "convertGiftToStars" +} + +func (config ConvertGiftToStarsConfig) params() (Params, error) { + params := make(Params) + + params["business_connection_id"] = config.BusinessConnectionID + params["owned_gift_id"] = config.OwnedGiftID + + return params, nil +} + +// UpgradeGiftConfig upgrades a regular gift to a unique one. +type UpgradeGiftConfig struct { + BusinessConnectionID string + OwnedGiftID string + KeepOriginalDetails bool + StarCount int +} + +func (UpgradeGiftConfig) method() string { + return "upgradeGift" +} + +func (config UpgradeGiftConfig) params() (Params, error) { + params := make(Params) + + params["business_connection_id"] = config.BusinessConnectionID + params["owned_gift_id"] = config.OwnedGiftID + params.AddBool("keep_original_details", config.KeepOriginalDetails) + params.AddNonZero("star_count", config.StarCount) + + return params, nil +} + +// TransferGiftConfig transfers an owned unique gift to another user. +type TransferGiftConfig struct { + BusinessConnectionID string + OwnedGiftID string + NewOwnerChatID int64 + StarCount int +} + +func (TransferGiftConfig) method() string { + return "transferGift" +} + +func (config TransferGiftConfig) params() (Params, error) { + params := make(Params) + + params["business_connection_id"] = config.BusinessConnectionID + params["owned_gift_id"] = config.OwnedGiftID + params.AddNonZero64("new_owner_chat_id", config.NewOwnerChatID) + params.AddNonZero("star_count", config.StarCount) + + return params, nil +} + +// GiftPremiumSubscriptionConfig gifts a Telegram Premium subscription to +// the given user. +type GiftPremiumSubscriptionConfig struct { + UserID int64 + MonthCount int + StarCount int + Text string + TextParseMode string + TextEntities []MessageEntity +} + +func (GiftPremiumSubscriptionConfig) method() string { + return "giftPremiumSubscription" +} + +func (config GiftPremiumSubscriptionConfig) params() (Params, error) { + params := make(Params) + + params.AddNonZero64("user_id", config.UserID) + params.AddNonZero("month_count", config.MonthCount) + params.AddNonZero("star_count", config.StarCount) + params.AddNonEmpty("text", config.Text) + params.AddNonEmpty("text_parse_mode", config.TextParseMode) + err := params.AddAny("text_entities", config.TextEntities) + + return params, err +} + +// prepareInputProfilePhotoForParams rewrites the photo reference to attach:// +// if the data needs uploading. +func prepareInputProfilePhotoForParams(p InputProfilePhoto) InputProfilePhoto { + if p.Photo != nil && p.Photo.NeedsUpload() { + p.Photo = fileAttach("attach://profile-photo") + } + if p.Animation != nil && p.Animation.NeedsUpload() { + p.Animation = fileAttach("attach://profile-photo") + } + return p +} + +// prepareInputProfilePhotoForFiles returns the upload entry for a profile +// photo whose data needs uploading. +func prepareInputProfilePhotoForFiles(p InputProfilePhoto) []RequestFile { + var files []RequestFile + if p.Photo != nil && p.Photo.NeedsUpload() { + files = append(files, RequestFile{Name: "profile-photo", Data: p.Photo}) + } + if p.Animation != nil && p.Animation.NeedsUpload() { + files = append(files, RequestFile{Name: "profile-photo", Data: p.Animation}) + } + return files +} + +// PostStoryConfig posts a story on behalf of a managed business account. +type PostStoryConfig struct { + BusinessConnectionID string + Content InputStoryContent + ActivePeriod int + Caption string + ParseMode string + CaptionEntities []MessageEntity + Areas []StoryArea + PostToChatPage bool + ProtectContent bool +} + +func (PostStoryConfig) method() string { + return "postStory" +} + +func (config PostStoryConfig) params() (Params, error) { + params := make(Params) + + params["business_connection_id"] = config.BusinessConnectionID + params.AddNonZero("active_period", config.ActivePeriod) + params.AddNonEmpty("caption", config.Caption) + params.AddNonEmpty("parse_mode", config.ParseMode) + params.AddBool("post_to_chat_page", config.PostToChatPage) + params.AddBool("protect_content", config.ProtectContent) + if err := params.AddAny("content", prepareInputStoryContentForParams(config.Content)); err != nil { + return params, err + } + if err := params.AddAny("caption_entities", config.CaptionEntities); err != nil { + return params, err + } + err := params.AddAny("areas", config.Areas) + + return params, err +} + +func (config PostStoryConfig) files() []RequestFile { + return prepareInputStoryContentForFiles(config.Content) +} + +// EditStoryConfig edits a story previously posted by the bot on behalf of +// a managed business account. +type EditStoryConfig struct { + BusinessConnectionID string + StoryID int + Content InputStoryContent + Caption string + ParseMode string + CaptionEntities []MessageEntity + Areas []StoryArea +} + +func (EditStoryConfig) method() string { + return "editStory" +} + +func (config EditStoryConfig) params() (Params, error) { + params := make(Params) + + params["business_connection_id"] = config.BusinessConnectionID + params.AddNonZero("story_id", config.StoryID) + params.AddNonEmpty("caption", config.Caption) + params.AddNonEmpty("parse_mode", config.ParseMode) + if err := params.AddAny("content", prepareInputStoryContentForParams(config.Content)); err != nil { + return params, err + } + if err := params.AddAny("caption_entities", config.CaptionEntities); err != nil { + return params, err + } + err := params.AddAny("areas", config.Areas) + + return params, err +} + +func (config EditStoryConfig) files() []RequestFile { + return prepareInputStoryContentForFiles(config.Content) +} + +// DeleteStoryConfig deletes a story previously posted by the bot on behalf +// of a managed business account. +type DeleteStoryConfig struct { + BusinessConnectionID string + StoryID int +} + +func (DeleteStoryConfig) method() string { + return "deleteStory" +} + +func (config DeleteStoryConfig) params() (Params, error) { + params := make(Params) + + params["business_connection_id"] = config.BusinessConnectionID + params.AddNonZero("story_id", config.StoryID) + + return params, nil +} + +// prepareInputStoryContentForParams rewrites the story content's photo/video +// reference to attach:// if the data needs uploading. +func prepareInputStoryContentForParams(c InputStoryContent) InputStoryContent { + if c.Photo != nil && c.Photo.NeedsUpload() { + c.Photo = fileAttach("attach://story-content") + } + if c.Video != nil && c.Video.NeedsUpload() { + c.Video = fileAttach("attach://story-content") + } + return c +} + +// prepareInputStoryContentForFiles returns the upload entries for a story +// content. +func prepareInputStoryContentForFiles(c InputStoryContent) []RequestFile { + var files []RequestFile + if c.Photo != nil && c.Photo.NeedsUpload() { + files = append(files, RequestFile{Name: "story-content", Data: c.Photo}) + } + if c.Video != nil && c.Video.NeedsUpload() { + files = append(files, RequestFile{Name: "story-content", Data: c.Video}) + } + return files +} + // GetMyShortDescriptionConfig returns the current bot short description for // the given user language. type GetMyShortDescriptionConfig struct { diff --git a/types.go b/types.go index d3cb47ca..2395d6fd 100644 --- a/types.go +++ b/types.go @@ -565,10 +565,8 @@ type ChatFullInfo struct { // // optional CanSendPaidMedia bool `json:"can_send_paid_media,omitempty"` - // CanSendGift is true, if gifts can be sent to the chat. Channel chats only. - // - // optional - CanSendGift bool `json:"can_send_gift,omitempty"` + // AcceptedGiftTypes are the types of gifts accepted by the chat. + AcceptedGiftTypes AcceptedGiftTypes `json:"accepted_gift_types"` } // Message represents a message. @@ -945,6 +943,24 @@ type Message struct { // // optional ChatBackgroundSet *ChatBackground `json:"chat_background_set,omitempty"` + // Gift is a service message: a gift was sent or received. + // + // optional + Gift *GiftInfo `json:"gift,omitempty"` + // UniqueGift is a service message: a unique gift was sent or received. + // + // optional + UniqueGift *UniqueGiftInfo `json:"unique_gift,omitempty"` + // PaidMessagePriceChanged is a service message about a change in the + // price of paid messages within the chat. + // + // optional + PaidMessagePriceChanged *PaidMessagePriceChanged `json:"paid_message_price_changed,omitempty"` + // PaidStarCount is the number of Telegram Stars that were paid by the + // sender of the message to send it. + // + // optional + PaidStarCount int `json:"paid_star_count,omitempty"` // GiveawayCreated is a service message: a scheduled giveaway was created. // // optional @@ -2551,6 +2567,73 @@ type ChatBoostAdded struct { BoostCount int `json:"boost_count"` } +// BusinessBotRights represents the rights of a business bot. +type BusinessBotRights struct { + // CanReply is true, if the bot can send and edit messages in the chats + // that were active in the last 24 hours. + // + // optional + CanReply bool `json:"can_reply,omitempty"` + // CanReadMessages is true, if the bot can mark incoming private messages as read. + // + // optional + CanReadMessages bool `json:"can_read_messages,omitempty"` + // CanDeleteSentMessages is true, if the bot can delete sent messages. + // + // optional + CanDeleteSentMessages bool `json:"can_delete_sent_messages,omitempty"` + // CanDeleteAllMessages is true, if the bot can delete any message. + // + // optional + CanDeleteAllMessages bool `json:"can_delete_all_messages,omitempty"` + // CanEditName is true, if the bot can edit the first and last name of the account. + // + // optional + CanEditName bool `json:"can_edit_name,omitempty"` + // CanEditBio is true, if the bot can edit the bio of the account. + // + // optional + CanEditBio bool `json:"can_edit_bio,omitempty"` + // CanEditProfilePhoto is true, if the bot can edit the profile photo of the account. + // + // optional + CanEditProfilePhoto bool `json:"can_edit_profile_photo,omitempty"` + // CanEditUsername is true, if the bot can edit the username of the account. + // + // optional + CanEditUsername bool `json:"can_edit_username,omitempty"` + // CanChangeGiftSettings is true, if the bot can change the privacy settings + // pertaining to gifts for the account. + // + // optional + CanChangeGiftSettings bool `json:"can_change_gift_settings,omitempty"` + // CanViewGiftsAndStars is true, if the bot can view gifts and the amount + // of Telegram Stars owned by the account. + // + // optional + CanViewGiftsAndStars bool `json:"can_view_gifts_and_stars,omitempty"` + // CanConvertGiftsToStars is true, if the bot can convert regular gifts owned + // by the account to Telegram Stars. + // + // optional + CanConvertGiftsToStars bool `json:"can_convert_gifts_to_stars,omitempty"` + // CanTransferAndUpgradeGifts is true, if the bot can transfer and upgrade + // gifts owned by the account. + // + // optional + CanTransferAndUpgradeGifts bool `json:"can_transfer_and_upgrade_gifts,omitempty"` + // CanTransferStars is true, if the bot can transfer Telegram Stars received + // by the account. + // + // optional + CanTransferStars bool `json:"can_transfer_stars,omitempty"` + // CanManageStories is true, if the bot can post, edit and delete stories + // on behalf of the account. + // + // optional + CanManageStories bool `json:"can_manage_stories,omitempty"` +} + // BusinessConnection describes the connection of the bot with a business account. type BusinessConnection struct { // ID is the unique identifier of the business connection. @@ -2562,13 +2645,39 @@ type BusinessConnection struct { UserChatID int64 `json:"user_chat_id"` // Date the connection was established in Unix time. Date int `json:"date"` - // CanReply is true, if the bot can act on behalf of the business account - // in chats that were active in the last 24 hours. - CanReply bool `json:"can_reply"` + // Rights is the rights of the business bot. + // + // optional + Rights *BusinessBotRights `json:"rights,omitempty"` // IsEnabled is true, if the connection is active. IsEnabled bool `json:"is_enabled"` } +// StarAmount describes an amount of Telegram Stars. +type StarAmount struct { + // Amount is the integer amount of Telegram Stars, rounded to 0; can be negative. + Amount int `json:"amount"` + // NanostarAmount is the number of 1/1000000000 shares of Telegram Stars; + // from -999999999 to 999999999. + // + // optional + NanostarAmount int `json:"nanostar_amount,omitempty"` +} + +// AcceptedGiftTypes describes the types of gifts accepted by a user or chat. +type AcceptedGiftTypes struct { + // UnlimitedGifts is true, if unlimited regular gifts are accepted. + UnlimitedGifts bool `json:"unlimited_gifts"` + // LimitedGifts is true, if limited regular gifts are accepted. + LimitedGifts bool `json:"limited_gifts"` + // UniqueGifts is true, if unique gifts or gifts that can be upgraded to + // unique for free are accepted. + UniqueGifts bool `json:"unique_gifts"` + // PremiumSubscription is true, if a Telegram Premium subscription is + // accepted. + PremiumSubscription bool `json:"premium_subscription"` +} + // BusinessMessagesDeleted is received when messages are deleted from a // connected business account. type BusinessMessagesDeleted struct { @@ -2871,6 +2980,18 @@ type TransactionPartner struct { // // optional SubscriptionPeriod int `json:"subscription_period,omitempty"` + // PremiumSubscriptionDuration is the duration of a paid Telegram + // Premium subscription, in months. Set when Type is "user" and the + // transaction is a premium subscription gift. + // + // optional + PremiumSubscriptionDuration int `json:"premium_subscription_duration,omitempty"` + // TransactionType describes the type of the transaction. Set when + // Type is "user". Known values: "invoice_payment", "paid_media_payment", + // "gift_purchase", "premium_purchase", "business_account_transfer". + // + // optional + TransactionType string `json:"transaction_type,omitempty"` // PaidMedia is the information about the paid media bought by the user. // Set when Type is "user" and the transaction involves paid media. // @@ -3023,6 +3144,232 @@ type Gifts struct { Gifts []Gift `json:"gifts"` } +// UniqueGiftModel describes the model of a unique gift. +type UniqueGiftModel struct { + // Name of the model. + Name string `json:"name"` + // Sticker representing the model. + Sticker Sticker `json:"sticker"` + // RarityPerMille is the number of unique gifts that receive this model + // for every 1000 gifts upgraded. + RarityPerMille int `json:"rarity_per_mille"` +} + +// UniqueGiftSymbol describes the symbol shown on the pattern of a unique gift. +type UniqueGiftSymbol struct { + // Name of the symbol. + Name string `json:"name"` + // Sticker representing the symbol. + Sticker Sticker `json:"sticker"` + // RarityPerMille is the number of unique gifts that receive this symbol + // for every 1000 gifts upgraded. + RarityPerMille int `json:"rarity_per_mille"` +} + +// UniqueGiftBackdropColors describes the colors of a unique gift backdrop. +type UniqueGiftBackdropColors struct { + // CenterColor is the color in the center of the backdrop in RGB format. + CenterColor int `json:"center_color"` + // EdgeColor is the color on the edges of the backdrop in RGB format. + EdgeColor int `json:"edge_color"` + // SymbolColor is the color used to paint the symbol in RGB format. + SymbolColor int `json:"symbol_color"` + // TextColor is the color for the text on the backdrop in RGB format. + TextColor int `json:"text_color"` +} + +// UniqueGiftBackdrop describes the backdrop of a unique gift. +type UniqueGiftBackdrop struct { + // Name of the backdrop. + Name string `json:"name"` + // Colors of the backdrop. + Colors UniqueGiftBackdropColors `json:"colors"` + // RarityPerMille is the number of unique gifts that receive this backdrop + // for every 1000 gifts upgraded. + RarityPerMille int `json:"rarity_per_mille"` +} + +// UniqueGift describes a unique gift that was upgraded from a regular gift. +type UniqueGift struct { + // BaseName is the human-readable name of the regular gift from which + // this unique gift was upgraded. + BaseName string `json:"base_name"` + // Name is the unique name of the gift. + Name string `json:"name"` + // Number is the unique number of the upgraded gift among gifts upgraded + // from the same regular gift. + Number int `json:"number"` + // Model of the unique gift. + Model UniqueGiftModel `json:"model"` + // Symbol of the unique gift. + Symbol UniqueGiftSymbol `json:"symbol"` + // Backdrop of the unique gift. + Backdrop UniqueGiftBackdrop `json:"backdrop"` +} + +// GiftInfo describes a service message about a regular gift that was sent +// or received. +type GiftInfo struct { + // Gift is the information about the gift. + Gift Gift `json:"gift"` + // OwnedGiftID is the unique identifier of the received gift for the + // bot; only present for gifts received on behalf of business accounts. + // + // optional + OwnedGiftID string `json:"owned_gift_id,omitempty"` + // ConvertStarCount is the number of Telegram Stars that can be claimed + // by the receiver instead of the gift. + // + // optional + ConvertStarCount int `json:"convert_star_count,omitempty"` + // PrepaidUpgradeStarCount is the number of Telegram Stars that were + // prepaid by the sender for the ability to upgrade the gift. + // + // optional + PrepaidUpgradeStarCount int `json:"prepaid_upgrade_star_count,omitempty"` + // CanBeUpgraded is true, if the gift can be upgraded to a unique gift. + // + // optional + CanBeUpgraded bool `json:"can_be_upgraded,omitempty"` + // Text is the text of the message that was added to the gift. + // + // optional + Text string `json:"text,omitempty"` + // Entities are the special entities that appear in the text. + // + // optional + Entities []MessageEntity `json:"entities,omitempty"` + // IsPrivate is true, if the sender and gift text are shown only to the + // gift receiver; otherwise, everyone able to access the chat with the + // receiver will be able to see them. + // + // optional + IsPrivate bool `json:"is_private,omitempty"` +} + +// Unique gift origin constants. +const ( + UniqueGiftOriginUpgrade = "upgrade" + UniqueGiftOriginTransfer = "transfer" +) + +// UniqueGiftInfo describes a service message about a unique gift that was +// sent or received. +type UniqueGiftInfo struct { + // Gift is the information about the unique gift. + Gift UniqueGift `json:"gift"` + // Origin of the gift. Currently, either "upgrade" or "transfer". + Origin string `json:"origin"` + // OwnedGiftID is the unique identifier of the received gift for the + // bot; only present for gifts received on behalf of business accounts. + // + // optional + OwnedGiftID string `json:"owned_gift_id,omitempty"` + // TransferStarCount is the number of Telegram Stars that must be paid + // to transfer the gift; omitted if the bot cannot transfer the gift. + // + // optional + TransferStarCount int `json:"transfer_star_count,omitempty"` +} + +// Owned gift type constants. +const ( + OwnedGiftTypeRegular = "regular" + OwnedGiftTypeUnique = "unique" +) + +// OwnedGift describes a gift received and owned by a user or chat. Flat +// polymorphic by Type: +// - "regular" → Gift (Gift), fields: ConvertStarCount, CanBeUpgraded, etc. +// - "unique" → Gift (UniqueGift), fields: CanBeTransferred, TransferStarCount +type OwnedGift struct { + // Type of the gift. One of "regular", "unique". + Type string `json:"type"` + // Gift is the regular gift, when Type is "regular". + // + // optional + Gift *Gift `json:"gift,omitempty"` + // UniqueGift is the unique gift, when Type is "unique". + // + // optional + UniqueGift *UniqueGift `json:"unique_gift,omitempty"` + // OwnedGiftID is the unique identifier of the gift for the bot; for + // gifts received on behalf of business accounts only. + // + // optional + OwnedGiftID string `json:"owned_gift_id,omitempty"` + // SenderUser is the sender of the gift, if it was sent by a user. + // + // optional + SenderUser *User `json:"sender_user,omitempty"` + // SendDate is the date the gift was sent in Unix time. + SendDate int `json:"send_date"` + // Text is the text of the message that was added to the gift. + // + // optional + Text string `json:"text,omitempty"` + // Entities are the special entities that appear in the text. + // + // optional + Entities []MessageEntity `json:"entities,omitempty"` + // IsPrivate is true, if the sender and gift text are shown only to the + // gift receiver. + // + // optional + IsPrivate bool `json:"is_private,omitempty"` + // IsSaved is true, if the gift is displayed on the account's profile + // page; for gifts received on behalf of business accounts only. + // + // optional + IsSaved bool `json:"is_saved,omitempty"` + // CanBeUpgraded is true, if the regular gift can be upgraded to a + // unique gift; for gifts received on behalf of business accounts only. + // Regular only. + // + // optional + CanBeUpgraded bool `json:"can_be_upgraded,omitempty"` + // WasRefunded is true, if the gift was refunded and isn't available + // anymore. Regular only. + // + // optional + WasRefunded bool `json:"was_refunded,omitempty"` + // ConvertStarCount is the number of Telegram Stars that can be claimed + // by the receiver instead of the gift. Regular only. + // + // optional + ConvertStarCount int `json:"convert_star_count,omitempty"` + // PrepaidUpgradeStarCount is the number of Telegram Stars that were + // paid by the sender for the ability to upgrade the gift. Regular only. + // + // optional + PrepaidUpgradeStarCount int `json:"prepaid_upgrade_star_count,omitempty"` + // CanBeTransferred is true, if the gift can be transferred to another + // owner; for gifts received on behalf of business accounts only. + // Unique only. + // + // optional + CanBeTransferred bool `json:"can_be_transferred,omitempty"` + // TransferStarCount is the number of Telegram Stars that must be paid + // to transfer the gift; omitted if the bot cannot transfer the gift. + // Unique only. + // + // optional + TransferStarCount int `json:"transfer_star_count,omitempty"` +} + +// OwnedGifts contains the list of gifts received and owned by a user or chat. +type OwnedGifts struct { + // TotalCount is the total number of gifts owned by the user or chat. + TotalCount int `json:"total_count"` + // Gifts is the list of gifts. + Gifts []OwnedGift `json:"gifts"` + // NextOffset is the offset for the next request. Empty if there are no + // more results. + // + // optional + NextOffset string `json:"next_offset,omitempty"` +} + // PreparedInlineMessage describes an inline message to be sent by a user of // a Mini App. type PreparedInlineMessage struct { @@ -3033,6 +3380,203 @@ type PreparedInlineMessage struct { ExpirationDate int `json:"expiration_date"` } +// InputProfilePhoto type constants. +const ( + InputProfilePhotoTypeStatic = "static" + InputProfilePhotoTypeAnimated = "animated" +) + +// InputProfilePhoto describes a profile photo to be set. Flat polymorphic +// by Type: +// - "static" → Photo is set +// - "animated" → Animation is set, MainFrameTimestamp optional +type InputProfilePhoto struct { + // Type of the photo. One of "static", "animated". + Type string `json:"type"` + // Photo is the static profile photo. Profile photos can't be reused and + // can only be uploaded as a new file. Set when Type is "static". + // + // optional + Photo RequestFileData `json:"photo,omitempty"` + // Animation is the animated profile photo. Set when Type is "animated". + // + // optional + Animation RequestFileData `json:"animation,omitempty"` + // MainFrameTimestamp is the timestamp in seconds of the frame that will + // be used as the static profile photo. Set when Type is "animated". + // Defaults to 0.0. + // + // optional + MainFrameTimestamp float64 `json:"main_frame_timestamp,omitempty"` +} + +// Input story content type constants. +const ( + InputStoryContentTypePhoto = "photo" + InputStoryContentTypeVideo = "video" +) + +// InputStoryContent describes the content of a story to be posted. Flat +// polymorphic by Type: +// - "photo" → Photo is set +// - "video" → Video is set; Duration, CoverFrameTimestamp, IsAnimation optional +type InputStoryContent struct { + // Type of the content. One of "photo", "video". + Type string `json:"type"` + // Photo is the photo content. Set when Type is "photo". + // + // optional + Photo RequestFileData `json:"photo,omitempty"` + // Video is the video content. Set when Type is "video". + // + // optional + Video RequestFileData `json:"video,omitempty"` + // Duration is the precise duration of the video in seconds. 0-60. + // Video only. + // + // optional + Duration float64 `json:"duration,omitempty"` + // CoverFrameTimestamp is the timestamp in seconds of the frame that + // will be used as the static cover for the story. Defaults to 0.0. + // Video only. + // + // optional + CoverFrameTimestamp float64 `json:"cover_frame_timestamp,omitempty"` + // IsAnimation is true if the video has no sound. Video only. + // + // optional + IsAnimation bool `json:"is_animation,omitempty"` +} + +// StoryAreaPosition describes the position of a clickable area within a +// story. +type StoryAreaPosition struct { + // XPercentage is the abscissa of the area's center, as a percentage of + // the media width. + XPercentage float64 `json:"x_percentage"` + // YPercentage is the ordinate of the area's center, as a percentage of + // the media height. + YPercentage float64 `json:"y_percentage"` + // WidthPercentage is the width of the area's rectangle, as a percentage + // of the media width. + WidthPercentage float64 `json:"width_percentage"` + // HeightPercentage is the height of the area's rectangle, as a percentage + // of the media height. + HeightPercentage float64 `json:"height_percentage"` + // RotationAngle is the clockwise rotation angle of the rectangle, in + // degrees; 0-360. + RotationAngle float64 `json:"rotation_angle"` + // CornerRadiusPercentage is the radius of the rectangle corner rounding, + // as a percentage of the media width. + CornerRadiusPercentage float64 `json:"corner_radius_percentage"` +} + +// LocationAddress describes the physical address of a location. +type LocationAddress struct { + // CountryCode is the two-letter ISO 3166-1 alpha-2 country code of the + // country where the location is located. + CountryCode string `json:"country_code"` + // State is the state of the location. Optional. + // + // optional + State string `json:"state,omitempty"` + // City is the city of the location. Optional. + // + // optional + City string `json:"city,omitempty"` + // Street is the street address of the location. Optional. + // + // optional + Street string `json:"street,omitempty"` +} + +// Story area type constants. +const ( + StoryAreaTypeLocation = "location" + StoryAreaTypeSuggestedReaction = "suggested_reaction" + StoryAreaTypeLink = "link" + StoryAreaTypeWeather = "weather" + StoryAreaTypeUniqueGift = "unique_gift" +) + +// StoryAreaType describes the type of a clickable area on a story. Flat +// polymorphic by Type: +// - "location" → Latitude, Longitude, Address +// - "suggested_reaction" → ReactionType, IsDark, IsFlipped +// - "link" → URL +// - "weather" → Temperature, Emoji, BackgroundColor +// - "unique_gift" → Name +type StoryAreaType struct { + // Type of the area. One of "location", "suggested_reaction", "link", + // "weather", "unique_gift". + Type string `json:"type"` + // Latitude in degrees. Set when Type is "location". + // + // optional + Latitude float64 `json:"latitude,omitempty"` + // Longitude in degrees. Set when Type is "location". + // + // optional + Longitude float64 `json:"longitude,omitempty"` + // Address of the location. Set when Type is "location". + // + // optional + Address *LocationAddress `json:"address,omitempty"` + // ReactionType is the type of the reaction. Set when Type is + // "suggested_reaction". + // + // optional + ReactionType *ReactionType `json:"reaction_type,omitempty"` + // IsDark is true, if the reaction area has a dark background. Set + // when Type is "suggested_reaction". + // + // optional + IsDark bool `json:"is_dark,omitempty"` + // IsFlipped is true, if reaction area corner is flipped. Set when + // Type is "suggested_reaction". + // + // optional + IsFlipped bool `json:"is_flipped,omitempty"` + // URL to be opened. Set when Type is "link". + // + // optional + URL string `json:"url,omitempty"` + // Temperature in degree Celsius. Set when Type is "weather". + // + // optional + Temperature float64 `json:"temperature,omitempty"` + // Emoji representing the weather. Set when Type is "weather". + // + // optional + Emoji string `json:"emoji,omitempty"` + // BackgroundColor is the color of the area background in the ARGB + // format. Set when Type is "weather". + // + // optional + BackgroundColor int `json:"background_color,omitempty"` + // Name of the unique gift. Set when Type is "unique_gift". + // + // optional + Name string `json:"name,omitempty"` +} + +// StoryArea describes a clickable area on a story. +type StoryArea struct { + // Position of the area. + Position StoryAreaPosition `json:"position"` + // Type of the area. + Type StoryAreaType `json:"type"` +} + +// PaidMessagePriceChanged describes a service message about a change in the +// price of paid messages within a chat. +type PaidMessagePriceChanged struct { + // PaidMessageStarCount is the new number of Telegram Stars that must + // be paid by non-administrator users of the supergroup chat for each + // sent message. + PaidMessageStarCount int `json:"paid_message_star_count"` +} + // InputPaidMedia describes the paid media to be sent. Flat polymorphic by // Type: // - "photo" → Media is set From a81c2edcda5e5bd9695be4e484753b6211587193 Mon Sep 17 00:00:00 2001 From: Kirill Date: Sat, 11 Apr 2026 16:43:41 +0400 Subject: [PATCH 45/65] Full support of API 9.1 --- bot.go | 13 ++++ configs.go | 75 ++++++++++++++++++++++ types.go | 185 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 272 insertions(+), 1 deletion(-) diff --git a/bot.go b/bot.go index 501eec9f..2aea4f97 100644 --- a/bot.go +++ b/bot.go @@ -777,6 +777,19 @@ func (bot *BotAPI) GetStarTransactions(config GetStarTransactionsConfig) (StarTr return st, err } +// GetMyStarBalance returns the current Telegram Stars balance of the bot. +func (bot *BotAPI) GetMyStarBalance() (StarAmount, error) { + resp, err := bot.Request(GetMyStarBalanceConfig{}) + if err != nil { + return StarAmount{}, err + } + + var amount StarAmount + err = json.Unmarshal(resp.Result, &amount) + + return amount, err +} + // GetBusinessAccountStarBalance returns the amount of Telegram Stars owned // by a managed business account. func (bot *BotAPI) GetBusinessAccountStarBalance(config GetBusinessAccountStarBalanceConfig) (StarAmount, error) { diff --git a/configs.go b/configs.go index e10946b8..dca2843e 100644 --- a/configs.go +++ b/configs.go @@ -4181,6 +4181,81 @@ func (config EditStoryConfig) files() []RequestFile { return prepareInputStoryContentForFiles(config.Content) } +// SendChecklistConfig sends a checklist message on behalf of a connected +// business account. +type SendChecklistConfig struct { + BusinessConnectionID string + ChatID int64 + Checklist InputChecklist + DisableNotification bool + ProtectContent bool + MessageEffectID string + ReplyParameters *ReplyParameters + ReplyMarkup interface{} +} + +func (SendChecklistConfig) method() string { + return "sendChecklist" +} + +func (config SendChecklistConfig) params() (Params, error) { + params := make(Params) + + params["business_connection_id"] = config.BusinessConnectionID + params.AddNonZero64("chat_id", config.ChatID) + params.AddBool("disable_notification", config.DisableNotification) + params.AddBool("protect_content", config.ProtectContent) + params.AddNonEmpty("message_effect_id", config.MessageEffectID) + if err := params.AddAny("checklist", config.Checklist); err != nil { + return params, err + } + if err := params.AddAny("reply_parameters", config.ReplyParameters); err != nil { + return params, err + } + err := params.AddAny("reply_markup", config.ReplyMarkup) + + return params, err +} + +// EditMessageChecklistConfig edits a checklist previously sent by the bot. +type EditMessageChecklistConfig struct { + BusinessConnectionID string + ChatID int64 + MessageID int + Checklist InputChecklist + ReplyMarkup *InlineKeyboardMarkup +} + +func (EditMessageChecklistConfig) method() string { + return "editMessageChecklist" +} + +func (config EditMessageChecklistConfig) params() (Params, error) { + params := make(Params) + + params["business_connection_id"] = config.BusinessConnectionID + params.AddNonZero64("chat_id", config.ChatID) + params.AddNonZero("message_id", config.MessageID) + if err := params.AddAny("checklist", config.Checklist); err != nil { + return params, err + } + err := params.AddAny("reply_markup", config.ReplyMarkup) + + return params, err +} + +// GetMyStarBalanceConfig returns the current Telegram Stars balance of the +// bot. +type GetMyStarBalanceConfig struct{} + +func (GetMyStarBalanceConfig) method() string { + return "getMyStarBalance" +} + +func (GetMyStarBalanceConfig) params() (Params, error) { + return make(Params), nil +} + // DeleteStoryConfig deletes a story previously posted by the bot on behalf // of a managed business account. type DeleteStoryConfig struct { diff --git a/types.go b/types.go index 2395d6fd..51ef3f6d 100644 --- a/types.go +++ b/types.go @@ -657,6 +657,11 @@ type Message struct { // // optional ReplyToStory *Story `json:"reply_to_story,omitempty"` + // ReplyToChecklistTaskID is the identifier of the specific checklist + // task that is being replied to. + // + // optional + ReplyToChecklistTaskID int `json:"reply_to_checklist_task_id,omitempty"` // ViaBot through which the message was sent; // // optional @@ -956,6 +961,26 @@ type Message struct { // // optional PaidMessagePriceChanged *PaidMessagePriceChanged `json:"paid_message_price_changed,omitempty"` + // DirectMessagePriceChanged is a service message about a change in the + // pricing of direct messages sent to a channel chat. + // + // optional + DirectMessagePriceChanged *DirectMessagePriceChanged `json:"direct_message_price_changed,omitempty"` + // Checklist is the checklist that was sent, if the message contains a + // checklist. + // + // optional + Checklist *Checklist `json:"checklist,omitempty"` + // ChecklistTasksDone is a service message about tasks in a checklist + // being marked as done or not done. + // + // optional + ChecklistTasksDone *ChecklistTasksDone `json:"checklist_tasks_done,omitempty"` + // ChecklistTasksAdded is a service message about tasks added to a + // checklist. + // + // optional + ChecklistTasksAdded *ChecklistTasksAdded `json:"checklist_tasks_added,omitempty"` // PaidStarCount is the number of Telegram Stars that were paid by the // sender of the message to send it. // @@ -3136,6 +3161,10 @@ type Gift struct { // // optional RemainingCount int `json:"remaining_count,omitempty"` + // PublisherChat is the chat that published the gift. + // + // optional + PublisherChat *Chat `json:"publisher_chat,omitempty"` } // Gifts represents a list of gifts. @@ -3205,6 +3234,10 @@ type UniqueGift struct { Symbol UniqueGiftSymbol `json:"symbol"` // Backdrop of the unique gift. Backdrop UniqueGiftBackdrop `json:"backdrop"` + // PublisherChat is the chat that published the underlying regular gift. + // + // optional + PublisherChat *Chat `json:"publisher_chat,omitempty"` } // GiftInfo describes a service message about a regular gift that was sent @@ -3251,6 +3284,7 @@ type GiftInfo struct { const ( UniqueGiftOriginUpgrade = "upgrade" UniqueGiftOriginTransfer = "transfer" + UniqueGiftOriginResale = "resale" ) // UniqueGiftInfo describes a service message about a unique gift that was @@ -3258,8 +3292,13 @@ const ( type UniqueGiftInfo struct { // Gift is the information about the unique gift. Gift UniqueGift `json:"gift"` - // Origin of the gift. Currently, either "upgrade" or "transfer". + // Origin of the gift. One of "upgrade", "transfer", "resale". Origin string `json:"origin"` + // LastResaleStarCount is the number of Telegram Stars the gift was + // resold for; for gifts bought on a resale market only. + // + // optional + LastResaleStarCount int `json:"last_resale_star_count,omitempty"` // OwnedGiftID is the unique identifier of the received gift for the // bot; only present for gifts received on behalf of business accounts. // @@ -3355,6 +3394,12 @@ type OwnedGift struct { // // optional TransferStarCount int `json:"transfer_star_count,omitempty"` + // NextTransferDate is the point in time (Unix timestamp) when the gift + // can be transferred. If it is in the past, then the gift can be + // transferred now. Unique only. + // + // optional + NextTransferDate int `json:"next_transfer_date,omitempty"` } // OwnedGifts contains the list of gifts received and owned by a user or chat. @@ -3577,6 +3622,139 @@ type PaidMessagePriceChanged struct { PaidMessageStarCount int `json:"paid_message_star_count"` } +// DirectMessagePriceChanged describes a service message about a change in +// the pricing of direct messages sent to a channel chat. +type DirectMessagePriceChanged struct { + // AreDirectMessagesEnabled is true, if direct messages are enabled + // for the channel chat. + AreDirectMessagesEnabled bool `json:"are_direct_messages_enabled"` + // DirectMessageStarCount is the new number of Telegram Stars that + // must be paid to send each direct message. + // + // optional + DirectMessageStarCount int `json:"direct_message_star_count,omitempty"` +} + +// ChecklistTask describes a task in a checklist. +type ChecklistTask struct { + // ID is the unique identifier of the task. + ID int `json:"id"` + // Text is the text of the task. + Text string `json:"text"` + // TextEntities are the special entities that appear in the task text. + // + // optional + TextEntities []MessageEntity `json:"text_entities,omitempty"` + // CompletedByUser is the user that completed the task; for tasks that + // were completed. + // + // optional + CompletedByUser *User `json:"completed_by_user,omitempty"` + // CompletionDate is the point in time (Unix timestamp) when the task + // was completed; 0 for tasks that weren't completed. + // + // optional + CompletionDate int `json:"completion_date,omitempty"` +} + +// Checklist describes a checklist on a message. +type Checklist struct { + // Title is the title of the checklist. + Title string `json:"title"` + // TitleEntities are the special entities that appear in the title. + // + // optional + TitleEntities []MessageEntity `json:"title_entities,omitempty"` + // Tasks is the list of tasks in the checklist. + Tasks []ChecklistTask `json:"tasks"` + // OthersCanAddTasks is true, if users other than the creator can add + // tasks to the checklist. + // + // optional + OthersCanAddTasks bool `json:"others_can_add_tasks,omitempty"` + // OthersCanMarkTasksAsDone is true, if users other than the creator + // can mark tasks as done or not done. + // + // optional + OthersCanMarkTasksAsDone bool `json:"others_can_mark_tasks_as_done,omitempty"` +} + +// InputChecklistTask describes a task to be added to a checklist. +type InputChecklistTask struct { + // ID is the unique identifier of the task; must be positive and unique + // among all task identifiers in the checklist. + ID int `json:"id"` + // Text is the text of the task. + Text string `json:"text"` + // ParseMode is the mode for parsing entities in the text. + // + // optional + ParseMode string `json:"parse_mode,omitempty"` + // TextEntities are the special entities that appear in the text. + // + // optional + TextEntities []MessageEntity `json:"text_entities,omitempty"` +} + +// InputChecklist describes a checklist to create. +type InputChecklist struct { + // Title is the title of the checklist. + Title string `json:"title"` + // ParseMode is the mode for parsing entities in the title. + // + // optional + ParseMode string `json:"parse_mode,omitempty"` + // TitleEntities are the special entities that appear in the title. + // + // optional + TitleEntities []MessageEntity `json:"title_entities,omitempty"` + // Tasks is the list of 1-30 tasks in the checklist. + Tasks []InputChecklistTask `json:"tasks"` + // OthersCanAddTasks pass true if users other than the creator can add + // tasks to the checklist. + // + // optional + OthersCanAddTasks bool `json:"others_can_add_tasks,omitempty"` + // OthersCanMarkTasksAsDone pass true if users other than the creator + // can mark tasks as done or not done. + // + // optional + OthersCanMarkTasksAsDone bool `json:"others_can_mark_tasks_as_done,omitempty"` +} + +// ChecklistTasksDone describes a service message about tasks in a checklist +// being marked as done or not done. +type ChecklistTasksDone struct { + // ChecklistMessage is the message containing the checklist whose tasks + // were marked as done or not done. Note that the Message object in this + // field will not contain the ReplyToMessage field even if it itself is + // a reply. + // + // optional + ChecklistMessage *Message `json:"checklist_message,omitempty"` + // MarkedAsDoneTaskIDs are the IDs of tasks that were marked as done. + // + // optional + MarkedAsDoneTaskIDs []int `json:"marked_as_done_task_ids,omitempty"` + // MarkedAsNotDoneTaskIDs are the IDs of tasks that were marked as not + // done. + // + // optional + MarkedAsNotDoneTaskIDs []int `json:"marked_as_not_done_task_ids,omitempty"` +} + +// ChecklistTasksAdded describes a service message about tasks added to a +// checklist. +type ChecklistTasksAdded struct { + // ChecklistMessage is the message containing the checklist to which + // the tasks were added. + // + // optional + ChecklistMessage *Message `json:"checklist_message,omitempty"` + // Tasks is the list of tasks added to the checklist. + Tasks []ChecklistTask `json:"tasks"` +} + // InputPaidMedia describes the paid media to be sent. Flat polymorphic by // Type: // - "photo" → Media is set @@ -4175,6 +4353,11 @@ type ReplyParameters struct { // // optional QuotePosition int `json:"quote_position,omitempty"` + // ChecklistTaskID is the identifier of the specific checklist task to + // reply to. Required if replying to a task in a checklist. + // + // optional + ChecklistTaskID int `json:"checklist_task_id,omitempty"` } // ChatLocation represents a location to which a chat is connected. From e38a1b48ae877633a42d5e566c691c30fc8f40b2 Mon Sep 17 00:00:00 2001 From: Kirill Date: Sat, 11 Apr 2026 16:47:42 +0400 Subject: [PATCH 46/65] Full support of API 9.2 --- configs.go | 148 ++++++++++++++++++++++++------------ types.go | 218 +++++++++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 304 insertions(+), 62 deletions(-) diff --git a/configs.go b/configs.go index dca2843e..11127fb5 100644 --- a/configs.go +++ b/configs.go @@ -302,16 +302,17 @@ func (CloseConfig) params() (Params, error) { // BaseChat is base type for all chat config types. type BaseChat struct { - ChatID int64 // required - ChannelUsername string - BusinessConnectionID string - MessageThreadID int - MessageEffectID string - ProtectContent bool - AllowPaidBroadcast bool - ReplyParameters *ReplyParameters - ReplyMarkup interface{} - DisableNotification bool + ChatID int64 // required + ChannelUsername string + BusinessConnectionID string + MessageThreadID int + DirectMessagesTopicID int + MessageEffectID string + ProtectContent bool + AllowPaidBroadcast bool + ReplyParameters *ReplyParameters + ReplyMarkup interface{} + DisableNotification bool } func (chat *BaseChat) params() (Params, error) { @@ -322,6 +323,7 @@ func (chat *BaseChat) params() (Params, error) { } params.AddNonEmpty("business_connection_id", chat.BusinessConnectionID) params.AddNonZero("message_thread_id", chat.MessageThreadID) + params.AddNonZero("direct_messages_topic_id", chat.DirectMessagesTopicID) params.AddNonEmpty("message_effect_id", chat.MessageEffectID) params.AddBool("disable_notification", chat.DisableNotification) params.AddBool("protect_content", chat.ProtectContent) @@ -1492,21 +1494,22 @@ func (config RestrictChatMemberConfig) params() (Params, error) { // PromoteChatMemberConfig contains fields to promote members of chat type PromoteChatMemberConfig struct { ChatMemberConfig - IsAnonymous bool - CanManageChat bool - CanChangeInfo bool - CanPostMessages bool - CanEditMessages bool - CanDeleteMessages bool - CanManageVideoChats bool - CanInviteUsers bool - CanRestrictMembers bool - CanPinMessages bool - CanPromoteMembers bool - CanPostStories bool - CanEditStories bool - CanDeleteStories bool - CanManageTopics bool + IsAnonymous bool + CanManageChat bool + CanChangeInfo bool + CanPostMessages bool + CanEditMessages bool + CanDeleteMessages bool + CanManageVideoChats bool + CanInviteUsers bool + CanRestrictMembers bool + CanPinMessages bool + CanPromoteMembers bool + CanPostStories bool + CanEditStories bool + CanDeleteStories bool + CanManageTopics bool + CanManageDirectMessages bool } func (config PromoteChatMemberConfig) method() string { @@ -1536,6 +1539,7 @@ func (config PromoteChatMemberConfig) params() (Params, error) { params.AddBool("can_edit_stories", config.CanEditStories) params.AddBool("can_delete_stories", config.CanDeleteStories) params.AddBool("can_manage_topics", config.CanManageTopics) + params.AddBool("can_manage_direct_messages", config.CanManageDirectMessages) return params, nil } @@ -2412,14 +2416,15 @@ func (config DeleteMessagesConfig) params() (Params, error) { // messages and messages with protected content can't be forwarded. Album // grouping is kept for forwarded messages. type ForwardMessagesConfig struct { - ChatID int64 - ChannelUsername string - MessageThreadID int - FromChatID int64 - FromChannelUsername string - MessageIDs []int // 1-100 message identifiers, must be in strictly increasing order - DisableNotification bool - ProtectContent bool + ChatID int64 + ChannelUsername string + MessageThreadID int + DirectMessagesTopicID int + FromChatID int64 + FromChannelUsername string + MessageIDs []int // 1-100 message identifiers, must be in strictly increasing order + DisableNotification bool + ProtectContent bool } func (config ForwardMessagesConfig) method() string { @@ -2436,6 +2441,7 @@ func (config ForwardMessagesConfig) params() (Params, error) { return params, err } params.AddNonZero("message_thread_id", config.MessageThreadID) + params.AddNonZero("direct_messages_topic_id", config.DirectMessagesTopicID) params.AddBool("disable_notification", config.DisableNotification) params.AddBool("protect_content", config.ProtectContent) err := params.AddAny("message_ids", config.MessageIDs) @@ -2448,15 +2454,16 @@ func (config ForwardMessagesConfig) params() (Params, error) { // giveaway messages, giveaway winners messages, and invoice messages can't // be copied. Album grouping is kept for copied messages. type CopyMessagesConfig struct { - ChatID int64 - ChannelUsername string - MessageThreadID int - FromChatID int64 - FromChannelUsername string - MessageIDs []int // 1-100 message identifiers, must be in strictly increasing order - DisableNotification bool - ProtectContent bool - RemoveCaption bool + ChatID int64 + ChannelUsername string + MessageThreadID int + DirectMessagesTopicID int + FromChatID int64 + FromChannelUsername string + MessageIDs []int // 1-100 message identifiers, must be in strictly increasing order + DisableNotification bool + ProtectContent bool + RemoveCaption bool } func (config CopyMessagesConfig) method() string { @@ -2473,6 +2480,7 @@ func (config CopyMessagesConfig) params() (Params, error) { return params, err } params.AddNonZero("message_thread_id", config.MessageThreadID) + params.AddNonZero("direct_messages_topic_id", config.DirectMessagesTopicID) params.AddBool("disable_notification", config.DisableNotification) params.AddBool("protect_content", config.ProtectContent) params.AddBool("remove_caption", config.RemoveCaption) @@ -2481,6 +2489,50 @@ func (config CopyMessagesConfig) params() (Params, error) { return params, err } +// ApproveSuggestedPostConfig approves a suggested post in a direct messages +// chat. +type ApproveSuggestedPostConfig struct { + ChatID int64 + MessageID int + SendDate int +} + +func (ApproveSuggestedPostConfig) method() string { + return "approveSuggestedPost" +} + +func (config ApproveSuggestedPostConfig) params() (Params, error) { + params := make(Params) + + params.AddNonZero64("chat_id", config.ChatID) + params.AddNonZero("message_id", config.MessageID) + params.AddNonZero("send_date", config.SendDate) + + return params, nil +} + +// DeclineSuggestedPostConfig declines a suggested post in a direct messages +// chat. +type DeclineSuggestedPostConfig struct { + ChatID int64 + MessageID int + Comment string +} + +func (DeclineSuggestedPostConfig) method() string { + return "declineSuggestedPost" +} + +func (config DeclineSuggestedPostConfig) params() (Params, error) { + params := make(Params) + + params.AddNonZero64("chat_id", config.ChatID) + params.AddNonZero("message_id", config.MessageID) + params.AddNonEmpty("comment", config.Comment) + + return params, nil +} + // SetMessageReactionConfig sets the bot's reaction to a message. Pass an // empty Reaction to remove all reactions from the message. type SetMessageReactionConfig struct { @@ -3500,11 +3552,12 @@ func prepareInputPaidMediaForFiles(items []InputPaidMedia) []RequestFile { // // Media consist of InputMedia items (InputMediaPhoto, InputMediaVideo). type MediaGroupConfig struct { - ChatID int64 - ChannelUsername string - BusinessConnectionID string - MessageThreadID int - MessageEffectID string + ChatID int64 + ChannelUsername string + BusinessConnectionID string + MessageThreadID int + DirectMessagesTopicID int + MessageEffectID string Media []interface{} DisableNotification bool @@ -3525,6 +3578,7 @@ func (config MediaGroupConfig) params() (Params, error) { } params.AddNonEmpty("business_connection_id", config.BusinessConnectionID) params.AddNonZero("message_thread_id", config.MessageThreadID) + params.AddNonZero("direct_messages_topic_id", config.DirectMessagesTopicID) params.AddNonEmpty("message_effect_id", config.MessageEffectID) params.AddBool("disable_notification", config.DisableNotification) params.AddBool("protect_content", config.ProtectContent) diff --git a/types.go b/types.go index 51ef3f6d..ed32e81a 100644 --- a/types.go +++ b/types.go @@ -344,6 +344,11 @@ type Chat struct { // // optional IsForum bool `json:"is_forum,omitempty"` + // IsDirectMessages is true, if the chat is a supergroup that serves as + // a direct messages chat for a channel. + // + // optional + IsDirectMessages bool `json:"is_direct_messages,omitempty"` // ActiveUsernames is a list of all active chat usernames; for private chats, // supergroups and channels. Returned only in getChat. // @@ -567,6 +572,10 @@ type ChatFullInfo struct { CanSendPaidMedia bool `json:"can_send_paid_media,omitempty"` // AcceptedGiftTypes are the types of gifts accepted by the chat. AcceptedGiftTypes AcceptedGiftTypes `json:"accepted_gift_types"` + // ParentChat is the parent channel chat for a channel direct messages chat. + // + // optional + ParentChat *Chat `json:"parent_chat,omitempty"` } // Message represents a message. @@ -981,6 +990,46 @@ type Message struct { // // optional ChecklistTasksAdded *ChecklistTasksAdded `json:"checklist_tasks_added,omitempty"` + // DirectMessagesTopic is the topic of a direct messages chat to which + // the message belongs. + // + // optional + DirectMessagesTopic *DirectMessagesTopic `json:"direct_messages_topic,omitempty"` + // IsPaidPost is true, if the message is a paid post. Note that such + // posts must not be deleted for 24 hours after the payment. + // + // optional + IsPaidPost bool `json:"is_paid_post,omitempty"` + // SuggestedPostInfo describes the suggested post if this message is a + // suggested post. + // + // optional + SuggestedPostInfo *SuggestedPostInfo `json:"suggested_post_info,omitempty"` + // SuggestedPostApproved is a service message about the approval of a + // suggested post. + // + // optional + SuggestedPostApproved *SuggestedPostApproved `json:"suggested_post_approved,omitempty"` + // SuggestedPostApprovalFailed is a service message about a failure to + // approve a suggested post. + // + // optional + SuggestedPostApprovalFailed *SuggestedPostApprovalFailed `json:"suggested_post_approval_failed,omitempty"` + // SuggestedPostDeclined is a service message about the rejection of a + // suggested post. + // + // optional + SuggestedPostDeclined *SuggestedPostDeclined `json:"suggested_post_declined,omitempty"` + // SuggestedPostPaid is a service message about a successful payment + // for a suggested post. + // + // optional + SuggestedPostPaid *SuggestedPostPaid `json:"suggested_post_paid,omitempty"` + // SuggestedPostRefunded is a service message about a payment refund + // for a suggested post. + // + // optional + SuggestedPostRefunded *SuggestedPostRefunded `json:"suggested_post_refunded,omitempty"` // PaidStarCount is the number of Telegram Stars that were paid by the // sender of the message to send it. // @@ -2267,21 +2316,22 @@ type ChatInviteLink struct { } type ChatAdministratorRights struct { - IsAnonymous bool `json:"is_anonymous"` - CanManageChat bool `json:"can_manage_chat"` - CanDeleteMessages bool `json:"can_delete_messages"` - CanManageVideoChats bool `json:"can_manage_video_chats"` - CanRestrictMembers bool `json:"can_restrict_members"` - CanPromoteMembers bool `json:"can_promote_members"` - CanChangeInfo bool `json:"can_change_info"` - CanInviteUsers bool `json:"can_invite_users"` - CanPostMessages bool `json:"can_post_messages"` - CanEditMessages bool `json:"can_edit_messages"` - CanPinMessages bool `json:"can_pin_messages"` - CanPostStories bool `json:"can_post_stories"` - CanEditStories bool `json:"can_edit_stories"` - CanDeleteStories bool `json:"can_delete_stories"` - CanManageTopics bool `json:"can_manage_topics"` + IsAnonymous bool `json:"is_anonymous"` + CanManageChat bool `json:"can_manage_chat"` + CanDeleteMessages bool `json:"can_delete_messages"` + CanManageVideoChats bool `json:"can_manage_video_chats"` + CanRestrictMembers bool `json:"can_restrict_members"` + CanPromoteMembers bool `json:"can_promote_members"` + CanChangeInfo bool `json:"can_change_info"` + CanInviteUsers bool `json:"can_invite_users"` + CanPostMessages bool `json:"can_post_messages"` + CanEditMessages bool `json:"can_edit_messages"` + CanPinMessages bool `json:"can_pin_messages"` + CanPostStories bool `json:"can_post_stories"` + CanEditStories bool `json:"can_edit_stories"` + CanDeleteStories bool `json:"can_delete_stories"` + CanManageTopics bool `json:"can_manage_topics"` + CanManageDirectMessages bool `json:"can_manage_direct_messages"` } // ChatMember contains information about one member of a chat. @@ -2395,6 +2445,12 @@ type ChatMember struct { // // optional CanManageTopics bool `json:"can_manage_topics,omitempty"` + // CanManageDirectMessages administrators only. + // True, if the administrator can manage direct messages within the + // channel and decline suggested posts; for channels only. + // + // optional + CanManageDirectMessages bool `json:"can_manage_direct_messages,omitempty"` // IsMember is true, if the user is a member of the chat at the moment of // the request IsMember bool `json:"is_member"` @@ -3635,6 +3691,138 @@ type DirectMessagePriceChanged struct { DirectMessageStarCount int `json:"direct_message_star_count,omitempty"` } +// DirectMessagesTopic describes a topic of a direct messages chat. +type DirectMessagesTopic struct { + // TopicID is the unique identifier of the topic. + TopicID int `json:"topic_id"` + // User is the information about the user that created the topic. + // Currently, the user is always present for topics in direct messages + // chats of channel direct messages chats. + // + // optional + User *User `json:"user,omitempty"` +} + +// SuggestedPostPrice describes the price of a suggested post. +type SuggestedPostPrice struct { + // Currency is the currency in which the price is expressed. Currently, + // always "XTR" (Telegram Stars) or "TON" (Toncoin). + Currency string `json:"currency"` + // Amount is the amount of the currency to be paid for the post. + Amount int `json:"amount"` +} + +// SuggestedPostParameters contains parameters of a post that is suggested +// by the bot. +type SuggestedPostParameters struct { + // Price of the suggested post. If the field is omitted, then the post + // is unpaid. + // + // optional + Price *SuggestedPostPrice `json:"price,omitempty"` + // SendDate is the Unix timestamp when the post is suggested to be + // published. If specified, then the date must be between 300 and + // 2678400 seconds (30 days) in the future. If omitted, then the post + // can be published at any time within 30 days at the sole discretion + // of the administrator. + // + // optional + SendDate int `json:"send_date,omitempty"` +} + +// SuggestedPostInfo contains information about a suggested post. +type SuggestedPostInfo struct { + // State of the suggested post. Currently, one of "pending", "approved", + // "declined". + State string `json:"state"` + // Price of the suggested post. + // + // optional + Price *SuggestedPostPrice `json:"price,omitempty"` + // SendDate is the Unix timestamp when the post is suggested to be + // published. + // + // optional + SendDate int `json:"send_date,omitempty"` +} + +// SuggestedPostApproved describes a service message about the approval of +// a suggested post. +type SuggestedPostApproved struct { + // SuggestedPostMessage is the message containing the suggested post + // that was approved. + // + // optional + SuggestedPostMessage *Message `json:"suggested_post_message,omitempty"` + // Price at which the post was approved. + // + // optional + Price *SuggestedPostPrice `json:"price,omitempty"` + // SendDate is the Unix timestamp when the post will be published. + SendDate int `json:"send_date"` +} + +// SuggestedPostApprovalFailed describes a service message about the failure +// to approve a suggested post due to insufficient funds at the time of +// payment. +type SuggestedPostApprovalFailed struct { + // SuggestedPostMessage is the message containing the suggested post + // whose approval has failed. + // + // optional + SuggestedPostMessage *Message `json:"suggested_post_message,omitempty"` + // Price at which the post would have been approved. + Price SuggestedPostPrice `json:"price"` +} + +// SuggestedPostDeclined describes a service message about the rejection of +// a suggested post. +type SuggestedPostDeclined struct { + // SuggestedPostMessage is the message containing the suggested post + // that was declined. + // + // optional + SuggestedPostMessage *Message `json:"suggested_post_message,omitempty"` + // Comment with which the post was declined. + // + // optional + Comment string `json:"comment,omitempty"` +} + +// SuggestedPostPaid describes a service message about a successful payment +// for a suggested post. +type SuggestedPostPaid struct { + // SuggestedPostMessage is the message containing the suggested post. + // + // optional + SuggestedPostMessage *Message `json:"suggested_post_message,omitempty"` + // Currency in which the payment was made. Currently, one of "XTR" or "TON". + Currency string `json:"currency"` + // Amount in the smallest units of the currency that was received by + // the channel in nanotoncoins; for payments in toncoins only. + // + // optional + Amount int `json:"amount,omitempty"` + // StarAmount is the amount of Telegram Stars that was received by the + // channel; for payments in Telegram Stars only. + // + // optional + StarAmount *StarAmount `json:"star_amount,omitempty"` +} + +// SuggestedPostRefunded describes a service message about a payment refund +// for a suggested post. +type SuggestedPostRefunded struct { + // SuggestedPostMessage is the message containing the suggested post + // that was refunded. + // + // optional + SuggestedPostMessage *Message `json:"suggested_post_message,omitempty"` + // Reason for the refund. Currently, one of "post_deleted", + // "payment_refunded". + Reason string `json:"reason"` +} + // ChecklistTask describes a task in a checklist. type ChecklistTask struct { // ID is the unique identifier of the task. From 9569766144bd47dc56b216316562043d8f648fdc Mon Sep 17 00:00:00 2001 From: Kirill Date: Sat, 11 Apr 2026 16:52:49 +0400 Subject: [PATCH 47/65] Full support of API 9.3 --- bot.go | 40 +++++++++++ configs.go | 140 ++++++++++++++++++++++++++++++++++--- types.go | 201 +++++++++++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 364 insertions(+), 17 deletions(-) diff --git a/bot.go b/bot.go index 2aea4f97..7191fb4f 100644 --- a/bot.go +++ b/bot.go @@ -804,6 +804,46 @@ func (bot *BotAPI) GetBusinessAccountStarBalance(config GetBusinessAccountStarBa return amount, err } +// GetUserGifts returns the list of gifts received and owned by a user. +func (bot *BotAPI) GetUserGifts(config GetUserGiftsConfig) (OwnedGifts, error) { + resp, err := bot.Request(config) + if err != nil { + return OwnedGifts{}, err + } + + var gifts OwnedGifts + err = json.Unmarshal(resp.Result, &gifts) + + return gifts, err +} + +// GetChatGifts returns the list of gifts received and owned by a chat. +func (bot *BotAPI) GetChatGifts(config GetChatGiftsConfig) (OwnedGifts, error) { + resp, err := bot.Request(config) + if err != nil { + return OwnedGifts{}, err + } + + var gifts OwnedGifts + err = json.Unmarshal(resp.Result, &gifts) + + return gifts, err +} + +// RepostStory reposts a story across different business accounts managed +// by the bot and returns the newly posted Story. +func (bot *BotAPI) RepostStory(config RepostStoryConfig) (Story, error) { + resp, err := bot.Request(config) + if err != nil { + return Story{}, err + } + + var story Story + err = json.Unmarshal(resp.Result, &story) + + return story, err +} + // GetBusinessAccountGifts returns the gifts received and owned by a managed // business account. func (bot *BotAPI) GetBusinessAccountGifts(config GetBusinessAccountGiftsConfig) (OwnedGifts, error) { diff --git a/configs.go b/configs.go index 11127fb5..77c6ca15 100644 --- a/configs.go +++ b/configs.go @@ -4007,15 +4007,17 @@ func (config TransferBusinessAccountStarsConfig) params() (Params, error) { // GetBusinessAccountGiftsConfig returns the gifts received and owned by a // managed business account. type GetBusinessAccountGiftsConfig struct { - BusinessConnectionID string - ExcludeUnsaved bool - ExcludeSaved bool - ExcludeUnlimited bool - ExcludeLimited bool - ExcludeUnique bool - SortByPrice bool - Offset string - Limit int + BusinessConnectionID string + ExcludeUnsaved bool + ExcludeSaved bool + ExcludeUnlimited bool + ExcludeLimitedUpgradable bool + ExcludeLimitedNonUpgradable bool + ExcludeUnique bool + ExcludeFromBlockchain bool + SortByPrice bool + Offset string + Limit int } func (GetBusinessAccountGiftsConfig) method() string { @@ -4029,8 +4031,10 @@ func (config GetBusinessAccountGiftsConfig) params() (Params, error) { params.AddBool("exclude_unsaved", config.ExcludeUnsaved) params.AddBool("exclude_saved", config.ExcludeSaved) params.AddBool("exclude_unlimited", config.ExcludeUnlimited) - params.AddBool("exclude_limited", config.ExcludeLimited) + params.AddBool("exclude_limited_upgradable", config.ExcludeLimitedUpgradable) + params.AddBool("exclude_limited_non_upgradable", config.ExcludeLimitedNonUpgradable) params.AddBool("exclude_unique", config.ExcludeUnique) + params.AddBool("exclude_from_blockchain", config.ExcludeFromBlockchain) params.AddBool("sort_by_price", config.SortByPrice) params.AddNonEmpty("offset", config.Offset) params.AddNonZero("limit", config.Limit) @@ -4310,6 +4314,122 @@ func (GetMyStarBalanceConfig) params() (Params, error) { return make(Params), nil } +// SendMessageDraftConfig streams a partial text message to a user while the +// content is still being generated. Stream of partial messages is ended by +// calling sendMessage. +type SendMessageDraftConfig struct { + ChatID int64 + MessageThreadID int + Text string + ParseMode string + Entities []MessageEntity + ReplyParameters *ReplyParameters +} + +func (SendMessageDraftConfig) method() string { + return "sendMessageDraft" +} + +func (config SendMessageDraftConfig) params() (Params, error) { + params := make(Params) + + params.AddNonZero64("chat_id", config.ChatID) + params.AddNonZero("message_thread_id", config.MessageThreadID) + params.AddNonEmpty("text", config.Text) + params.AddNonEmpty("parse_mode", config.ParseMode) + if err := params.AddAny("entities", config.Entities); err != nil { + return params, err + } + err := params.AddAny("reply_parameters", config.ReplyParameters) + + return params, err +} + +// GetUserGiftsConfig returns the list of gifts received and owned by a user. +type GetUserGiftsConfig struct { + UserID int64 + Offset string + Limit int +} + +func (GetUserGiftsConfig) method() string { + return "getUserGifts" +} + +func (config GetUserGiftsConfig) params() (Params, error) { + params := make(Params) + + params.AddNonZero64("user_id", config.UserID) + params.AddNonEmpty("offset", config.Offset) + params.AddNonZero("limit", config.Limit) + + return params, nil +} + +// GetChatGiftsConfig returns the list of gifts received and owned by a chat. +// +// Provide the target chat via either ChatID (numeric identifier) or +// ChannelUsername ("@channelusername"); the first non-zero / non-empty +// value is used. +type GetChatGiftsConfig struct { + ChatID int64 + ChannelUsername string + Offset string + Limit int +} + +func (GetChatGiftsConfig) method() string { + return "getChatGifts" +} + +func (config GetChatGiftsConfig) params() (Params, error) { + params := make(Params) + + if err := params.AddFirstValid("chat_id", config.ChatID, config.ChannelUsername); err != nil { + return params, err + } + params.AddNonEmpty("offset", config.Offset) + params.AddNonZero("limit", config.Limit) + + return params, nil +} + +// RepostStoryConfig reposts a story across different business accounts +// managed by the bot. +type RepostStoryConfig struct { + BusinessConnectionID string + FromChatID int64 + StoryID int + Caption string + ParseMode string + CaptionEntities []MessageEntity + Areas []StoryArea + PostToChatPage bool + ProtectContent bool +} + +func (RepostStoryConfig) method() string { + return "repostStory" +} + +func (config RepostStoryConfig) params() (Params, error) { + params := make(Params) + + params["business_connection_id"] = config.BusinessConnectionID + params.AddNonZero64("from_chat_id", config.FromChatID) + params.AddNonZero("story_id", config.StoryID) + params.AddNonEmpty("caption", config.Caption) + params.AddNonEmpty("parse_mode", config.ParseMode) + params.AddBool("post_to_chat_page", config.PostToChatPage) + params.AddBool("protect_content", config.ProtectContent) + if err := params.AddAny("caption_entities", config.CaptionEntities); err != nil { + return params, err + } + err := params.AddAny("areas", config.Areas) + + return params, err +} + // DeleteStoryConfig deletes a story previously posted by the bot on behalf // of a managed business account. type DeleteStoryConfig struct { diff --git a/types.go b/types.go index ed32e81a..f671437d 100644 --- a/types.go +++ b/types.go @@ -261,6 +261,11 @@ type User struct { // // optional HasMainWebApp bool `json:"has_main_web_app,omitempty"` + // HasTopicsEnabled is true, if forum topic mode is enabled for the bot + // in private chats. + // + // optional + HasTopicsEnabled bool `json:"has_topics_enabled,omitempty"` // FirstName user's or bot's first name FirstName string `json:"first_name"` // LastName user's or bot's last name @@ -576,6 +581,21 @@ type ChatFullInfo struct { // // optional ParentChat *Chat `json:"parent_chat,omitempty"` + // Rating is the rating of the user in a private chat. + // + // optional + Rating *UserRating `json:"rating,omitempty"` + // PaidMessageStarCount is the number of Telegram Stars that must be + // paid by non-administrator users of the supergroup chat for each sent + // message. + // + // optional + PaidMessageStarCount int `json:"paid_message_star_count,omitempty"` + // UniqueGiftColors defines the color scheme for the chat's name, replies + // to messages, and link previews based on a unique gift. + // + // optional + UniqueGiftColors *UniqueGiftColors `json:"unique_gift_colors,omitempty"` } // Message represents a message. @@ -965,6 +985,11 @@ type Message struct { // // optional UniqueGift *UniqueGiftInfo `json:"unique_gift,omitempty"` + // GiftUpgradeSent is a service message about a gift upgrade sent to + // another user. + // + // optional + GiftUpgradeSent *GiftInfo `json:"gift_upgrade_sent,omitempty"` // PaidMessagePriceChanged is a service message about a change in the // price of paid messages within the chat. // @@ -2757,6 +2782,10 @@ type AcceptedGiftTypes struct { // PremiumSubscription is true, if a Telegram Premium subscription is // accepted. PremiumSubscription bool `json:"premium_subscription"` + // GiftsFromChannels is true, if gifts published by channels are accepted. + // + // optional + GiftsFromChannels bool `json:"gifts_from_channels,omitempty"` } // BusinessMessagesDeleted is received when messages are deleted from a @@ -3193,6 +3222,73 @@ type PaidMediaPurchased struct { PaidMediaPayload string `json:"paid_media_payload"` } +// UniqueGiftColors describes the color scheme for a user's name, replies +// to messages, and link previews based on a unique gift. +// +// Note: the exact field layout of this type is not fully verified. When +// serializing or deserializing you may need to use the Raw payload. +type UniqueGiftColors struct { + Raw json.RawMessage `json:"-"` +} + +// UnmarshalJSON preserves the raw JSON payload for UniqueGiftColors. +func (u *UniqueGiftColors) UnmarshalJSON(b []byte) error { + u.Raw = append(u.Raw[:0], b...) + return nil +} + +// MarshalJSON emits the raw JSON payload, or `null` if unset. +func (u UniqueGiftColors) MarshalJSON() ([]byte, error) { + if len(u.Raw) == 0 { + return []byte("null"), nil + } + return u.Raw, nil +} + +// GiftBackground describes the background associated with a gift. +// +// Note: the exact field layout of this type is not fully verified. When +// serializing or deserializing you may need to use the Raw payload. +type GiftBackground struct { + Raw json.RawMessage `json:"-"` +} + +// UnmarshalJSON preserves the raw JSON payload for GiftBackground. +func (g *GiftBackground) UnmarshalJSON(b []byte) error { + g.Raw = append(g.Raw[:0], b...) + return nil +} + +// MarshalJSON emits the raw JSON payload, or `null` if unset. +func (g GiftBackground) MarshalJSON() ([]byte, error) { + if len(g.Raw) == 0 { + return []byte("null"), nil + } + return g.Raw, nil +} + +// UserRating describes the rating of a user in a private chat. +// +// Note: the exact field layout of this type is not fully verified. When +// serializing or deserializing you may need to use the Raw payload. +type UserRating struct { + Raw json.RawMessage `json:"-"` +} + +// UnmarshalJSON preserves the raw JSON payload for UserRating. +func (u *UserRating) UnmarshalJSON(b []byte) error { + u.Raw = append(u.Raw[:0], b...) + return nil +} + +// MarshalJSON emits the raw JSON payload, or `null` if unset. +func (u UserRating) MarshalJSON() ([]byte, error) { + if len(u.Raw) == 0 { + return []byte("null"), nil + } + return u.Raw, nil +} + // Gift represents a gift that can be sent by the bot. type Gift struct { // ID is the unique identifier of the gift. @@ -3217,6 +3313,34 @@ type Gift struct { // // optional RemainingCount int `json:"remaining_count,omitempty"` + // PersonalTotalCount is the total number of gifts of this type that + // can be sent by the bot to each user. + // + // optional + PersonalTotalCount int `json:"personal_total_count,omitempty"` + // PersonalRemainingCount is the number of remaining gifts of this type + // that can be sent by the bot to each user. + // + // optional + PersonalRemainingCount int `json:"personal_remaining_count,omitempty"` + // IsPremium is true, if the gift can only be sent by bots owned by + // users with an active Telegram Premium subscription. + // + // optional + IsPremium bool `json:"is_premium,omitempty"` + // HasColors is true, if the gift defines a color scheme. + // + // optional + HasColors bool `json:"has_colors,omitempty"` + // UniqueGiftVariantCount is the total number of variants of unique + // gifts that can be upgraded from the gift. + // + // optional + UniqueGiftVariantCount int `json:"unique_gift_variant_count,omitempty"` + // Background describing the gift background. + // + // optional + Background *GiftBackground `json:"background,omitempty"` // PublisherChat is the chat that published the gift. // // optional @@ -3284,12 +3408,31 @@ type UniqueGift struct { // Number is the unique number of the upgraded gift among gifts upgraded // from the same regular gift. Number int `json:"number"` + // GiftID is the identifier of the regular gift that was upgraded to + // this unique gift. + // + // optional + GiftID string `json:"gift_id,omitempty"` + // IsFromBlockchain is true, if the gift was assigned from the TON + // blockchain. + // + // optional + IsFromBlockchain bool `json:"is_from_blockchain,omitempty"` + // IsPremium is true, if the gift can only be owned by users with an + // active Telegram Premium subscription. + // + // optional + IsPremium bool `json:"is_premium,omitempty"` // Model of the unique gift. Model UniqueGiftModel `json:"model"` // Symbol of the unique gift. Symbol UniqueGiftSymbol `json:"symbol"` // Backdrop of the unique gift. Backdrop UniqueGiftBackdrop `json:"backdrop"` + // Colors defines the color scheme based on this unique gift. + // + // optional + Colors *UniqueGiftColors `json:"colors,omitempty"` // PublisherChat is the chat that published the underlying regular gift. // // optional @@ -3334,13 +3477,25 @@ type GiftInfo struct { // // optional IsPrivate bool `json:"is_private,omitempty"` + // IsUpgradeSeparate is true, if the gift upgrade to a unique gift can + // be requested separately by the receiver. + // + // optional + IsUpgradeSeparate bool `json:"is_upgrade_separate,omitempty"` + // UniqueGiftNumber is the sequential number of the unique gift among + // gifts upgraded from the same regular gift. + // + // optional + UniqueGiftNumber int `json:"unique_gift_number,omitempty"` } // Unique gift origin constants. const ( - UniqueGiftOriginUpgrade = "upgrade" - UniqueGiftOriginTransfer = "transfer" - UniqueGiftOriginResale = "resale" + UniqueGiftOriginUpgrade = "upgrade" + UniqueGiftOriginTransfer = "transfer" + UniqueGiftOriginResale = "resale" + UniqueGiftOriginGiftedUpgrade = "gifted_upgrade" + UniqueGiftOriginOffer = "offer" ) // UniqueGiftInfo describes a service message about a unique gift that was @@ -3348,13 +3503,19 @@ const ( type UniqueGiftInfo struct { // Gift is the information about the unique gift. Gift UniqueGift `json:"gift"` - // Origin of the gift. One of "upgrade", "transfer", "resale". + // Origin of the gift. One of "upgrade", "transfer", "resale", + // "gifted_upgrade", "offer". Origin string `json:"origin"` - // LastResaleStarCount is the number of Telegram Stars the gift was - // resold for; for gifts bought on a resale market only. + // LastResaleCurrency is the currency in which the gift was last resold + // on a resale market; for gifts with origin "resale" only. + // + // optional + LastResaleCurrency string `json:"last_resale_currency,omitempty"` + // LastResaleAmount is the amount in the smallest units of the currency + // for which the gift was last resold; for gifts with origin "resale" only. // // optional - LastResaleStarCount int `json:"last_resale_star_count,omitempty"` + LastResaleAmount int `json:"last_resale_amount,omitempty"` // OwnedGiftID is the unique identifier of the received gift for the // bot; only present for gifts received on behalf of business accounts. // @@ -3438,6 +3599,16 @@ type OwnedGift struct { // // optional PrepaidUpgradeStarCount int `json:"prepaid_upgrade_star_count,omitempty"` + // IsUpgradeSeparate is true, if the gift upgrade to a unique gift can + // be requested separately by the receiver. Regular only. + // + // optional + IsUpgradeSeparate bool `json:"is_upgrade_separate,omitempty"` + // UniqueGiftNumber is the sequential number of the unique gift among + // gifts upgraded from the same regular gift. Regular only. + // + // optional + UniqueGiftNumber int `json:"unique_gift_number,omitempty"` // CanBeTransferred is true, if the gift can be transferred to another // owner; for gifts received on behalf of business accounts only. // Unique only. @@ -3838,6 +4009,11 @@ type ChecklistTask struct { // // optional CompletedByUser *User `json:"completed_by_user,omitempty"` + // CompletedByChat is the chat that completed the task on behalf of + // an anonymous user. + // + // optional + CompletedByChat *Chat `json:"completed_by_chat,omitempty"` // CompletionDate is the point in time (Unix timestamp) when the task // was completed; 0 for tasks that weren't completed. // @@ -4564,6 +4740,12 @@ type ForumTopic struct { MessageThreadID int `json:"message_thread_id"` // Name of the topic Name string `json:"name"` + // IsNameImplicit is true, if the name of the topic was implicitly + // derived from the content of the first message, e.g. for topics + // created automatically in private chats. + // + // optional + IsNameImplicit bool `json:"is_name_implicit,omitempty"` // IconColor is the color of the topic icon in RGB format IconColor int `json:"icon_color"` // IconCustomEmojiID is the unique identifier of the custom emoji shown @@ -4578,6 +4760,11 @@ type ForumTopic struct { type ForumTopicCreated struct { // Name of the topic Name string `json:"name"` + // IsNameImplicit is true, if the name of the topic was implicitly + // derived from the content of the first message. + // + // optional + IsNameImplicit bool `json:"is_name_implicit,omitempty"` // IconColor is the color of the topic icon in RGB format IconColor int `json:"icon_color"` // IconCustomEmojiID is the unique identifier of the custom emoji shown From 58bd3c101b174a5395ada023a78e2ef1db6fe052 Mon Sep 17 00:00:00 2001 From: Kirill Date: Sat, 11 Apr 2026 16:55:15 +0400 Subject: [PATCH 48/65] Full support of API 9.4 --- bot.go | 14 +++++++ configs.go | 54 +++++++++++++++++++++++++++ types.go | 107 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 175 insertions(+) diff --git a/bot.go b/bot.go index 7191fb4f..2f3f4f5c 100644 --- a/bot.go +++ b/bot.go @@ -777,6 +777,20 @@ func (bot *BotAPI) GetStarTransactions(config GetStarTransactionsConfig) (StarTr return st, err } +// GetUserProfileAudios fetches a list of audios added to the profile of +// a user. +func (bot *BotAPI) GetUserProfileAudios(config GetUserProfileAudiosConfig) (UserProfileAudios, error) { + resp, err := bot.Request(config) + if err != nil { + return UserProfileAudios{}, err + } + + var audios UserProfileAudios + err = json.Unmarshal(resp.Result, &audios) + + return audios, err +} + // GetMyStarBalance returns the current Telegram Stars balance of the bot. func (bot *BotAPI) GetMyStarBalance() (StarAmount, error) { resp, err := bot.Request(GetMyStarBalanceConfig{}) diff --git a/configs.go b/configs.go index 77c6ca15..eff4b610 100644 --- a/configs.go +++ b/configs.go @@ -3719,6 +3719,60 @@ func (config GetMyNameConfig) params() (Params, error) { return params, nil } +// SetMyProfilePhotoConfig changes the profile photo of the bot. +type SetMyProfilePhotoConfig struct { + Photo InputProfilePhoto +} + +func (SetMyProfilePhotoConfig) method() string { + return "setMyProfilePhoto" +} + +func (config SetMyProfilePhotoConfig) params() (Params, error) { + params := make(Params) + + err := params.AddAny("photo", prepareInputProfilePhotoForParams(config.Photo)) + + return params, err +} + +func (config SetMyProfilePhotoConfig) files() []RequestFile { + return prepareInputProfilePhotoForFiles(config.Photo) +} + +// RemoveMyProfilePhotoConfig removes the current profile photo of the bot. +type RemoveMyProfilePhotoConfig struct{} + +func (RemoveMyProfilePhotoConfig) method() string { + return "removeMyProfilePhoto" +} + +func (RemoveMyProfilePhotoConfig) params() (Params, error) { + return make(Params), nil +} + +// GetUserProfileAudiosConfig fetches a list of audios added to the profile +// of a user. +type GetUserProfileAudiosConfig struct { + UserID int64 + Offset int + Limit int +} + +func (GetUserProfileAudiosConfig) method() string { + return "getUserProfileAudios" +} + +func (config GetUserProfileAudiosConfig) params() (Params, error) { + params := make(Params) + + params.AddNonZero64("user_id", config.UserID) + params.AddNonZero("offset", config.Offset) + params.AddNonZero("limit", config.Limit) + + return params, nil +} + // SetMyDescriptionConfig changes the bot's description, which is shown in the // chat with the bot if the chat is empty. type SetMyDescriptionConfig struct { diff --git a/types.go b/types.go index f671437d..7a68529f 100644 --- a/types.go +++ b/types.go @@ -266,6 +266,11 @@ type User struct { // // optional HasTopicsEnabled bool `json:"has_topics_enabled,omitempty"` + // AllowsUsersToCreateTopics is true, if users can create forum topics + // in the private chat with the bot. + // + // optional + AllowsUsersToCreateTopics bool `json:"allows_users_to_create_topics,omitempty"` // FirstName user's or bot's first name FirstName string `json:"first_name"` // LastName user's or bot's last name @@ -596,6 +601,10 @@ type ChatFullInfo struct { // // optional UniqueGiftColors *UniqueGiftColors `json:"unique_gift_colors,omitempty"` + // FirstProfileAudio is the first audio on the user profile. + // + // optional + FirstProfileAudio *Audio `json:"first_profile_audio,omitempty"` } // Message represents a message. @@ -990,6 +999,15 @@ type Message struct { // // optional GiftUpgradeSent *GiftInfo `json:"gift_upgrade_sent,omitempty"` + // ChatOwnerLeft is a service message about the owner leaving the chat. + // + // optional + ChatOwnerLeft *ChatOwnerLeft `json:"chat_owner_left,omitempty"` + // ChatOwnerChanged is a service message about a change of the chat + // owner. + // + // optional + ChatOwnerChanged *ChatOwnerChanged `json:"chat_owner_changed,omitempty"` // PaidMessagePriceChanged is a service message about a change in the // price of paid messages within the chat. // @@ -1429,6 +1447,10 @@ type Video struct { // // optional StartTimestamp int `json:"start_timestamp,omitempty"` + // Qualities lists the other available qualities of this video. + // + // optional + Qualities []VideoQuality `json:"qualities,omitempty"` // FileName is the original filename as defined by sender // // optional @@ -1869,6 +1891,17 @@ type KeyboardButton struct { // // optional RequestManagedBot *KeyboardButtonRequestManagedBot `json:"request_managed_bot,omitempty"` + // IconCustomEmojiID is the unique identifier of the custom emoji to be + // displayed on the button. Available only if the bot can use custom + // emoji in the message. + // + // optional + IconCustomEmojiID string `json:"icon_custom_emoji_id,omitempty"` + // Style of the button. Currently, one of "default", "primary", + // "destructive". Defaults to "default". + // + // optional + Style string `json:"style,omitempty"` } // KeyboardButtonRequestUsers defines the criteria used to request suitable @@ -2114,6 +2147,17 @@ type InlineKeyboardButton struct { // // optional CopyText *CopyTextButton `json:"copy_text,omitempty"` + // IconCustomEmojiID is the unique identifier of the custom emoji to be + // displayed on the button. Available only if the bot can use custom + // emoji in the message. + // + // optional + IconCustomEmojiID string `json:"icon_custom_emoji_id,omitempty"` + // Style of the button. Currently, one of "default", "primary", + // "destructive". Defaults to "default". + // + // optional + Style string `json:"style,omitempty"` // CallbackGame description of the game that will be launched when the user presses the button. // // optional @@ -2667,6 +2711,61 @@ type Story struct { ID int `json:"id"` } +// ChatOwnerLeft represents a service message about the owner leaving the chat. +// Currently holds no information. +type ChatOwnerLeft struct{} + +// ChatOwnerChanged represents a service message about a change of the chat +// owner. +type ChatOwnerChanged struct { + // NewOwner is the new owner of the chat. + NewOwner User `json:"new_owner"` +} + +// VideoQuality describes an available quality variant for a video. +// +// Note: the exact field layout of this type is not fully verified. When +// serializing or deserializing you may need to use the Raw payload. +type VideoQuality struct { + Raw json.RawMessage `json:"-"` +} + +// UnmarshalJSON preserves the raw JSON payload for VideoQuality. +func (v *VideoQuality) UnmarshalJSON(b []byte) error { + v.Raw = append(v.Raw[:0], b...) + return nil +} + +// MarshalJSON emits the raw JSON payload, or `null` if unset. +func (v VideoQuality) MarshalJSON() ([]byte, error) { + if len(v.Raw) == 0 { + return []byte("null"), nil + } + return v.Raw, nil +} + +// UserProfileAudios describes the audios posted to a user's profile. +// +// Note: the exact field layout of this type is not fully verified. When +// serializing or deserializing you may need to use the Raw payload. +type UserProfileAudios struct { + Raw json.RawMessage `json:"-"` +} + +// UnmarshalJSON preserves the raw JSON payload for UserProfileAudios. +func (u *UserProfileAudios) UnmarshalJSON(b []byte) error { + u.Raw = append(u.Raw[:0], b...) + return nil +} + +// MarshalJSON emits the raw JSON payload, or `null` if unset. +func (u UserProfileAudios) MarshalJSON() ([]byte, error) { + if len(u.Raw) == 0 { + return []byte("null"), nil + } + return u.Raw, nil +} + // ChatBoostAdded represents a service message about a user boosting a chat. type ChatBoostAdded struct { // BoostCount is the number of boosts added by the user. @@ -3362,6 +3461,10 @@ type UniqueGiftModel struct { // RarityPerMille is the number of unique gifts that receive this model // for every 1000 gifts upgraded. RarityPerMille int `json:"rarity_per_mille"` + // Rarity is a human-readable name for the rarity of the model. + // + // optional + Rarity string `json:"rarity,omitempty"` } // UniqueGiftSymbol describes the symbol shown on the pattern of a unique gift. @@ -3418,6 +3521,10 @@ type UniqueGift struct { // // optional IsFromBlockchain bool `json:"is_from_blockchain,omitempty"` + // IsBurned is true, if the gift was burned by the owner. + // + // optional + IsBurned bool `json:"is_burned,omitempty"` // IsPremium is true, if the gift can only be owned by users with an // active Telegram Premium subscription. // From a4a42b76ff5118ff21639128913a4da64d62730d Mon Sep 17 00:00:00 2001 From: Kirill Date: Sat, 11 Apr 2026 16:57:05 +0400 Subject: [PATCH 49/65] Full support of API 9.5 --- configs.go | 24 ++++++++++++++++++++++++ types.go | 25 ++++++++++++++++++++++++- 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/configs.go b/configs.go index eff4b610..a03e83b2 100644 --- a/configs.go +++ b/configs.go @@ -1510,6 +1510,7 @@ type PromoteChatMemberConfig struct { CanDeleteStories bool CanManageTopics bool CanManageDirectMessages bool + CanManageTags bool } func (config PromoteChatMemberConfig) method() string { @@ -1540,6 +1541,29 @@ func (config PromoteChatMemberConfig) params() (Params, error) { params.AddBool("can_delete_stories", config.CanDeleteStories) params.AddBool("can_manage_topics", config.CanManageTopics) params.AddBool("can_manage_direct_messages", config.CanManageDirectMessages) + params.AddBool("can_manage_tags", config.CanManageTags) + + return params, nil +} + +// SetChatMemberTagConfig sets the custom tag for a chat member. +type SetChatMemberTagConfig struct { + ChatMemberConfig + Tag string +} + +func (SetChatMemberTagConfig) method() string { + return "setChatMemberTag" +} + +func (config SetChatMemberTagConfig) params() (Params, error) { + params := make(Params) + + if err := params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername, config.ChannelUsername); err != nil { + return params, err + } + params.AddNonZero64("user_id", config.UserID) + params.AddNonEmpty("tag", config.Tag) return params, nil } diff --git a/types.go b/types.go index 7a68529f..2baece54 100644 --- a/types.go +++ b/types.go @@ -664,6 +664,10 @@ type Message struct { // // optional SenderBoostCount int `json:"sender_boost_count,omitempty"` + // SenderTag is the custom tag assigned to the sender in the chat. + // + // optional + SenderTag string `json:"sender_tag,omitempty"` // SenderBusinessBot is the bot that actually sent the message on behalf // of the business account. Available only for outgoing messages sent on // behalf of the connected business account. @@ -1213,7 +1217,8 @@ type MessageEntity struct { // “pre” (monowidth block), // “text_link” (for clickable text URLs), // “text_mention” (for users without usernames), - // “custom_emoji” (for inline custom emoji stickers) + // “custom_emoji” (for inline custom emoji stickers), + // “date_time” (for formatted date and time) Type string `json:"type"` // Offset in UTF-16 code units to the start of the entity Offset int `json:"offset"` @@ -2401,6 +2406,7 @@ type ChatAdministratorRights struct { CanDeleteStories bool `json:"can_delete_stories"` CanManageTopics bool `json:"can_manage_topics"` CanManageDirectMessages bool `json:"can_manage_direct_messages"` + CanManageTags bool `json:"can_manage_tags"` } // ChatMember contains information about one member of a chat. @@ -2520,6 +2526,19 @@ type ChatMember struct { // // optional CanManageDirectMessages bool `json:"can_manage_direct_messages,omitempty"` + // CanManageTags administrators only. + // True, if the administrator can manage tags of chat members. + // + // optional + CanManageTags bool `json:"can_manage_tags,omitempty"` + // Tag is the member's custom tag in the chat, if any. + // + // optional + Tag string `json:"tag,omitempty"` + // CanEditTag is true, if the user is allowed to edit their own tag. + // + // optional + CanEditTag bool `json:"can_edit_tag,omitempty"` // IsMember is true, if the user is a member of the chat at the moment of // the request IsMember bool `json:"is_member"` @@ -2701,6 +2720,10 @@ type ChatPermissions struct { // // optional CanManageTopics bool `json:"can_manage_topics,omitempty"` + // CanEditTag is true, if users are allowed to edit their own tag. + // + // optional + CanEditTag bool `json:"can_edit_tag,omitempty"` } // Story represents a story. From 799ea163b6dd898d8e1c156b219a86f8db44d9c9 Mon Sep 17 00:00:00 2001 From: Kirill Date: Sat, 11 Apr 2026 16:59:38 +0400 Subject: [PATCH 50/65] Full support of API 9.6 --- configs.go | 24 +++++++++++++-- types.go | 87 +++++++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 105 insertions(+), 6 deletions(-) diff --git a/configs.go b/configs.go index a03e83b2..9474eeff 100644 --- a/configs.go +++ b/configs.go @@ -951,11 +951,18 @@ type SendPollConfig struct { Question string QuestionParseMode string QuestionEntities []MessageEntity + Description string + DescriptionParseMode string + DescriptionEntities []MessageEntity Options []InputPollOption IsAnonymous bool Type string AllowsMultipleAnswers bool - CorrectOptionID int64 + AllowsRevoting bool + ShuffleOptions bool + AllowAddingOptions bool + HideResultsUntilCloses bool + CorrectOptionIDs []int Explanation string ExplanationParseMode string ExplanationEntities []MessageEntity @@ -975,13 +982,26 @@ func (config SendPollConfig) params() (Params, error) { if err = params.AddAny("question_entities", config.QuestionEntities); err != nil { return params, err } + params.AddNonEmpty("description", config.Description) + params.AddNonEmpty("description_parse_mode", config.DescriptionParseMode) + if err = params.AddAny("description_entities", config.DescriptionEntities); err != nil { + return params, err + } if err = params.AddAny("options", config.Options); err != nil { return params, err } params["is_anonymous"] = strconv.FormatBool(config.IsAnonymous) params.AddNonEmpty("type", config.Type) params["allows_multiple_answers"] = strconv.FormatBool(config.AllowsMultipleAnswers) - params["correct_option_id"] = strconv.FormatInt(config.CorrectOptionID, 10) + params.AddBool("allows_revoting", config.AllowsRevoting) + params.AddBool("shuffle_options", config.ShuffleOptions) + params.AddBool("allow_adding_options", config.AllowAddingOptions) + params.AddBool("hide_results_until_closes", config.HideResultsUntilCloses) + if len(config.CorrectOptionIDs) > 0 { + if err = params.AddAny("correct_option_ids", config.CorrectOptionIDs); err != nil { + return params, err + } + } params.AddBool("is_closed", config.IsClosed) params.AddNonEmpty("explanation", config.Explanation) params.AddNonEmpty("explanation_parse_mode", config.ExplanationParseMode) diff --git a/types.go b/types.go index 2baece54..63118e93 100644 --- a/types.go +++ b/types.go @@ -1012,6 +1012,21 @@ type Message struct { // // optional ChatOwnerChanged *ChatOwnerChanged `json:"chat_owner_changed,omitempty"` + // PollOptionAdded is a service message about a poll option being added + // to a poll. + // + // optional + PollOptionAdded *PollOptionAdded `json:"poll_option_added,omitempty"` + // PollOptionDeleted is a service message about a poll option being + // deleted from a poll. + // + // optional + PollOptionDeleted *PollOptionDeleted `json:"poll_option_deleted,omitempty"` + // ReplyToPollOptionID is the persistent identifier of the poll option + // that this message is a reply to. + // + // optional + ReplyToPollOptionID string `json:"reply_to_poll_option_id,omitempty"` // PaidMessagePriceChanged is a service message about a change in the // price of paid messages within the chat. // @@ -1553,6 +1568,46 @@ type PollOption struct { TextEntities []MessageEntity `json:"text_entities,omitempty"` // VoterCount is the number of users that voted for this option VoterCount int `json:"voter_count"` + // PersistentID is the persistent identifier of the poll option; stays + // the same even if the option is moved, renamed, added, or deleted. + // + // optional + PersistentID string `json:"persistent_id,omitempty"` + // AddedByUser is the user that added the option to the poll. + // + // optional + AddedByUser *User `json:"added_by_user,omitempty"` + // AddedByChat is the chat that added the option to the poll anonymously. + // + // optional + AddedByChat *Chat `json:"added_by_chat,omitempty"` + // AdditionDate is the point in time (Unix timestamp) when the option + // was added to the poll. + // + // optional + AdditionDate int `json:"addition_date,omitempty"` +} + +// PollOptionAdded describes a service message about a poll option being +// added to a poll. +type PollOptionAdded struct { + // PollMessage is the message containing the poll. + // + // optional + PollMessage *Message `json:"poll_message,omitempty"` + // Option is the added poll option. + Option PollOption `json:"option"` +} + +// PollOptionDeleted describes a service message about a poll option being +// deleted from a poll. +type PollOptionDeleted struct { + // PollMessage is the message containing the poll. + // + // optional + PollMessage *Message `json:"poll_message,omitempty"` + // OptionPersistentID is the persistent identifier of the deleted option. + OptionPersistentID string `json:"option_persistent_id"` } // PollAnswer represents an answer of a user in a non-anonymous poll. @@ -1571,6 +1626,11 @@ type PollAnswer struct { // OptionIDs is the 0-based identifiers of poll options chosen by the user. // May be empty if user retracted vote. OptionIDs []int `json:"option_ids"` + // OptionPersistentIDs is the persistent identifiers of the poll options + // chosen by the user, matching PollOption.PersistentID. + // + // optional + OptionPersistentIDs []string `json:"option_persistent_ids,omitempty"` } // Poll contains information about a poll. @@ -1596,12 +1656,26 @@ type Poll struct { Type string `json:"type"` // AllowsMultipleAnswers is true, if the poll allows multiple answers AllowsMultipleAnswers bool `json:"allows_multiple_answers"` - // CorrectOptionID is the 0-based identifier of the correct answer option. - // Available only for polls in quiz mode, which are closed, or was sent (not - // forwarded) by the bot or to the private chat with the bot. + // CorrectOptionIDs lists the 0-based identifiers of the correct answer + // options. Available only for polls in quiz mode, which are closed, or + // was sent (not forwarded) by the bot or to the private chat with the + // bot. Multi-answer quizzes may have more than one correct option. + // + // optional + CorrectOptionIDs []int `json:"correct_option_ids,omitempty"` + // AllowsRevoting is true, if the poll allows users to change their vote. + // + // optional + AllowsRevoting bool `json:"allows_revoting,omitempty"` + // Description is the text of the poll description. + // + // optional + Description string `json:"description,omitempty"` + // DescriptionEntities are the special entities that appear in the + // description. // // optional - CorrectOptionID int `json:"correct_option_id,omitempty"` + DescriptionEntities []MessageEntity `json:"description_entities,omitempty"` // Explanation is text that is shown when a user chooses an incorrect answer // or taps on the lamp icon in a quiz-style poll, 0-200 characters // @@ -4852,6 +4926,11 @@ type ReplyParameters struct { // // optional ChecklistTaskID int `json:"checklist_task_id,omitempty"` + // PollOptionID is the persistent identifier of the specific poll + // option to reply to. Required if replying to a poll option. + // + // optional + PollOptionID string `json:"poll_option_id,omitempty"` } // ChatLocation represents a location to which a chat is connected. From b2cbcaaf8edb40939a62f9404050981515dcd48d Mon Sep 17 00:00:00 2001 From: Kirill Date: Sat, 11 Apr 2026 18:16:53 +0400 Subject: [PATCH 51/65] Add BREAKING.md that describes breaking changes from original library --- BREAKING.md | 460 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 460 insertions(+) create mode 100644 BREAKING.md diff --git a/BREAKING.md b/BREAKING.md new file mode 100644 index 00000000..b28501a2 --- /dev/null +++ b/BREAKING.md @@ -0,0 +1,460 @@ +# Breaking Changes + +This file lists every source-incompatible change introduced while bringing +this fork from Bot API 6.0 up to 9.6. Upgrades are grouped by topic so you +can jump straight to the area your code touches. + +Each section lists **what to rename** or **what to replace** — the fastest +way to migrate is `grep` for the old identifier in your code and apply the +rewrite. Nothing here changes runtime semantics beyond what the Telegram +Bot API itself changed. + +--- + +## `thumb` → `thumbnail` (Bot API 6.6) + +The single largest mechanical rename. Applies everywhere media has a +thumbnail. + +### Config fields +```go +PhotoConfig.Thumb → PhotoConfig.Thumbnail +AudioConfig.Thumb → AudioConfig.Thumbnail +DocumentConfig.Thumb → DocumentConfig.Thumbnail +VideoConfig.Thumb → VideoConfig.Thumbnail +AnimationConfig.Thumb → AnimationConfig.Thumbnail +VideoNoteConfig.Thumb → VideoNoteConfig.Thumbnail +VoiceConfig.Thumb → VoiceConfig.Thumbnail +``` + +### `InputMedia*` types +```go +InputMediaVideo.Thumb → InputMediaVideo.Thumbnail +InputMediaAnimation.Thumb → InputMediaAnimation.Thumbnail +InputMediaAudio.Thumb → InputMediaAudio.Thumbnail +InputMediaDocument.Thumb → InputMediaDocument.Thumbnail +``` + +### Inline query result types +```go +InlineQueryResult*.ThumbURL → .ThumbnailURL +InlineQueryResult*.ThumbWidth → .ThumbnailWidth +InlineQueryResult*.ThumbHeight → .ThumbnailHeight +``` +(Applies to Article, Contact, Document, Location, Venue, Photo, Video, +GIF, MPEG4GIF.) + +### Helper functions +```go +NewInlineQueryResultPhotoWithThumb → NewInlineQueryResultPhotoWithThumbnail +``` + +### Config types +```go +SetStickerSetThumbConfig → SetStickerSetThumbnailConfig +SetStickerSetThumbConfig.Thumb → SetStickerSetThumbnailConfig.Thumbnail +``` + +--- + +## `reply_to_message_id` → `ReplyParameters` (Bot API 7.0) + +`ReplyToMessageID` and `AllowSendingWithoutReply` were removed from +`BaseChat` and `MediaGroupConfig`. Replies now go through a structured +`ReplyParameters` value that also supports cross-chat replies and quoting. + +### Before +```go +msg := tgbotapi.NewMessage(chatID, "hi") +msg.ReplyToMessageID = origMsgID +msg.AllowSendingWithoutReply = true +``` + +### After +```go +msg := tgbotapi.NewMessage(chatID, "hi") +msg.ReplyParameters = &tgbotapi.ReplyParameters{ + MessageID: origMsgID, + AllowSendingWithoutReply: true, +} +``` + +--- + +## `DisableWebPagePreview` → `LinkPreviewOptions` (Bot API 7.0) + +### Send / edit configs +```go +MessageConfig.DisableWebPagePreview → LinkPreviewOptions +EditMessageTextConfig.DisableWebPagePreview → LinkPreviewOptions +``` + +### Inline content +```go +InputTextMessageContent.DisableWebPagePreview → LinkPreviewOptions +``` + +### Before +```go +msg := tgbotapi.NewMessage(chatID, "see https://example.com") +msg.DisableWebPagePreview = true +``` + +### After +```go +msg := tgbotapi.NewMessage(chatID, "see https://example.com") +msg.LinkPreviewOptions = &tgbotapi.LinkPreviewOptions{IsDisabled: true} +``` + +--- + +## `forward_from` fields → `ForwardOrigin` (Bot API 7.0) + +Removed from `Message`: +- `ForwardFrom *User` +- `ForwardFromChat *Chat` +- `ForwardFromMessageID int` +- `ForwardSignature string` +- `ForwardSenderName string` +- `ForwardDate int` + +Added: `ForwardOrigin *MessageOrigin` — a flat polymorphic struct with a +`Type` discriminator ("user", "hidden_user", "chat", "channel") and all +variant fields as optional. + +### Before +```go +if msg.ForwardFrom != nil { + fmt.Println("forwarded from user", msg.ForwardFrom.UserName) +} +``` + +### After +```go +if msg.ForwardOrigin != nil && msg.ForwardOrigin.Type == tgbotapi.MessageOriginTypeUser { + fmt.Println("forwarded from user", msg.ForwardOrigin.SenderUser.UserName) +} +``` + +--- + +## Sticker API rewrite (Bot API 6.6) + +`createNewStickerSet`, `uploadStickerFile`, and `addStickerToSet` all +changed shape. The old per-format parameters (`png_sticker`, `tgs_sticker`, +`webm_sticker`) were replaced with the new `InputSticker` type. + +### `UploadStickerConfig` +```go +// Before +cfg := tgbotapi.UploadStickerConfig{ + UserID: userID, + PNGSticker: tgbotapi.FilePath("sticker.png"), +} + +// After +cfg := tgbotapi.UploadStickerConfig{ + UserID: userID, + Sticker: tgbotapi.FilePath("sticker.png"), + StickerFormat: tgbotapi.StickerFormatStatic, +} +``` + +### `NewStickerSetConfig` +```go +// Before +cfg := tgbotapi.NewStickerSetConfig{ + UserID: userID, + Name: "my_pack", + Title: "My Pack", + PNGSticker: tgbotapi.FilePath("s.png"), + Emojis: "😀", +} + +// After — Stickers is a slice of InputSticker, each with its own Format (7.2) +cfg := tgbotapi.NewStickerSetConfig{ + UserID: userID, + Name: "my_pack", + Title: "My Pack", + Stickers: []tgbotapi.InputSticker{{ + Sticker: tgbotapi.FilePath("s.png"), + Format: tgbotapi.StickerFormatStatic, + EmojiList: []string{"😀"}, + }}, +} +``` +Dropped fields: `PNGSticker`, `TGSSticker`, `Emojis`, `MaskPosition`, +`StickerFormat` (moved per-sticker in 7.2), `ContainsMasks` (deprecated 6.2). + +### `AddStickerConfig` +```go +// Before +cfg := tgbotapi.AddStickerConfig{ + UserID: userID, + Name: "my_pack", + PNGSticker: tgbotapi.FilePath("new.png"), + Emojis: "🎉", +} + +// After +cfg := tgbotapi.AddStickerConfig{ + UserID: userID, + Name: "my_pack", + Sticker: tgbotapi.InputSticker{ + Sticker: tgbotapi.FilePath("new.png"), + Format: tgbotapi.StickerFormatStatic, + EmojiList: []string{"🎉"}, + }, +} +``` + +### Removed fields +```go +StickerSet.IsAnimated // (removed 7.2; mixed-format packs) +StickerSet.IsVideo // (removed 7.2) +StickerSet.ContainsMasks // (removed 6.2; use StickerType instead) +``` + +--- + +## Chat permissions: granular media (Bot API 6.5) + +`CanSendMediaMessages` was replaced with six per-type fields on both +`ChatPermissions` and `ChatMember`: + +```go +CanSendMediaMessages → + CanSendAudios + + CanSendDocuments + + CanSendPhotos + + CanSendVideos + + CanSendVideoNotes + + CanSendVoiceNotes +``` + +--- + +## `UserShared` → `UsersShared` (Bot API 7.0, 7.2) + +### 7.0 — rename + slice of IDs +```go +KeyboardButtonRequestUser → KeyboardButtonRequestUsers +UserShared (type) → UsersShared +KeyboardButton.RequestUser → KeyboardButton.RequestUsers +Message.UserShared → Message.UsersShared +UsersShared.UserID (int64) → UsersShared.UserIDs ([]int64) +``` + +### 7.2 — slice of user IDs becomes slice of `SharedUser` +```go +UsersShared.UserIDs ([]int64) → UsersShared.Users ([]SharedUser) +``` + +`SharedUser` has the basic profile info (first/last name, username, photo) +when the bot requested it via `KeyboardButtonRequestUsers`. + +--- + +## `BusinessConnection.CanReply` → `Rights` (Bot API 9.0) + +`BusinessConnection.CanReply bool` was replaced with +`BusinessConnection.Rights *BusinessBotRights`, which carries ~14 +granular permissions (including a new `CanReply` field inside the +sub-struct). + +### Before +```go +if bc.CanReply { + ... +} +``` + +### After +```go +if bc.Rights != nil && bc.Rights.CanReply { + ... +} +``` + +--- + +## `ChatFullInfo.CanSendGift` → `AcceptedGiftTypes` (Bot API 9.0) + +The single-bool `can_send_gift` was replaced with a struct describing which +specific kinds of gifts a chat accepts: + +```go +ChatFullInfo.CanSendGift (bool) → ChatFullInfo.AcceptedGiftTypes (AcceptedGiftTypes) +``` + +`AcceptedGiftTypes` has four bools: `UnlimitedGifts`, `LimitedGifts`, +`UniqueGifts`, `PremiumSubscription` (plus `GiftsFromChannels` added in 9.3). + +--- + +## `Poll.CorrectOptionID` → `CorrectOptionIDs` (Bot API 9.6) + +Multi-answer quizzes meant the singular field had to become a slice: + +```go +Poll.CorrectOptionID (int) → Poll.CorrectOptionIDs ([]int) +SendPollConfig.CorrectOptionID → SendPollConfig.CorrectOptionIDs +``` + +`SendPollConfig.CorrectOptionID` also changed from `int64` to `int` as part +of the rewrite. + +--- + +## `SendPollConfig.Options` type change (Bot API 7.3) + +```go +SendPollConfig.Options ([]string) → SendPollConfig.Options ([]InputPollOption) +``` + +The `NewPoll` helper still takes `options ...string` and wraps them +internally, so callers that use the helper don't need to change. +Direct struct literal callers do: + +### Before +```go +SendPollConfig{Options: []string{"yes", "no"}} +``` + +### After +```go +SendPollConfig{Options: []tgbotapi.InputPollOption{ + {Text: "yes"}, + {Text: "no"}, +}} +``` + +--- + +## `UniqueGiftInfo.LastResaleStarCount` → currency+amount (Bot API 9.3) + +The Stars-only resale field was generalized when TON payments arrived: + +```go +UniqueGiftInfo.LastResaleStarCount (int) → + UniqueGiftInfo.LastResaleCurrency (string) + + UniqueGiftInfo.LastResaleAmount (int) +``` + +--- + +## `GetBusinessAccountGiftsConfig.ExcludeLimited` split (Bot API 9.3) + +```go +ExcludeLimited (bool) → + ExcludeLimitedUpgradable (bool) + + ExcludeLimitedNonUpgradable (bool) +``` + +Plus `ExcludeFromBlockchain (bool)` was added in the same version. + +--- + +## `switch_pm_*` → `InlineQueryResultsButton` (Bot API 6.7) + +```go +InlineConfig.SwitchPMText → InlineConfig.Button.Text +InlineConfig.SwitchPMParameter → InlineConfig.Button.StartParameter +``` + +`InlineConfig.Button` is a `*InlineQueryResultsButton` which additionally +supports launching a Web App: + +```go +cfg := tgbotapi.InlineConfig{ + // ... + Button: &tgbotapi.InlineQueryResultsButton{ + Text: "Show help", + StartParameter: "help", + }, +} +``` + +--- + +## Removed fields (no direct replacement) + +- **`InlineQueryResultArticle.HideURL`** (Bot API 8.2) — pass an empty + string as the `URL` instead. +- **`Gift.ContainsMasks`** / **`StickerSet.ContainsMasks`** — + use the `StickerType` / `Type` field (`"regular"`, `"mask"`, + `"custom_emoji"`) instead. + +--- + +## `InvoiceConfig.ProviderData` type change (Bot API 6.1 era) + +```go +InvoiceConfig.ProviderData (string) → json.RawMessage +InvoiceLinkConfig.ProviderData (string) → json.RawMessage +``` + +If you had a pre-marshalled JSON string, wrap it with `json.RawMessage(s)`. + +--- + +## Method return type change: `GetChat` (Bot API 7.3) + +```go +bot.GetChat(cfg) (Chat, error) → (ChatFullInfo, error) +``` + +`ChatFullInfo` embeds `Chat`, so field access via `.ID`, `.Title`, `.Bio`, +etc. still works through Go's field promotion. The only thing that changes +is the variable's declared type: + +```go +// Before +var c tgbotapi.Chat +c, err = bot.GetChat(cfg) + +// After +var c tgbotapi.ChatFullInfo +c, err = bot.GetChat(cfg) +``` + +--- + +## Types that gained fields (soft-breaking) + +A few types changed from empty structs to carrying fields. Anyone who used +them via pointer (`*Story`) is unaffected; callers who constructed them by +value may need to initialize new fields. + +- **`Story`** — 6.8 empty struct → 7.1 `{Chat, ID}` +- **`GiveawayCreated`** — 7.0 empty struct → 7.10 `{PrizeStarCount}` +- **`WriteAccessAllowed`** — 6.4 empty struct → grew `WebAppName` (6.7), + `FromRequest`, `FromAttachmentMenu` (6.9) + +--- + +## Internal helper `AddFirstValid` error propagation + +Not API-breaking for callers of the public surface, but if you wrote +custom `Chattable` implementations and copied the old pattern of +ignoring the `Params.AddFirstValid` return value, know that the fork +now consistently captures and returns that error from `params()`. Your +custom configs should do the same. + +--- + +## Checklist for upgrading + +1. Global find-replace for the `Thumb` → `Thumbnail` renames (Section 1). +2. Search for `ReplyToMessageID` and migrate each to `ReplyParameters`. +3. Search for `DisableWebPagePreview` and migrate to `LinkPreviewOptions`. +4. Search for `ForwardFrom` / `ForwardDate` and switch to `ForwardOrigin`. +5. Anything touching sticker set creation needs the `InputSticker` rewrite. +6. If you read `ChatMember.CanSendMediaMessages` or + `ChatPermissions.CanSendMediaMessages`, switch to the six granular + `CanSend*` fields. +7. If you handle business connections, check `Rights` instead of `CanReply`. +8. If you read quiz correctness, use `CorrectOptionIDs[0]` (or loop for + multi-answer). + +Everything else is additive and should compile unchanged. From 4a9a33e12d7dbc0b65bfec493d0e41fe53c7aa78 Mon Sep 17 00:00:00 2001 From: Kirill Date: Sat, 11 Apr 2026 18:55:42 +0400 Subject: [PATCH 52/65] Fix some issues from original repo --- README.md | 6 ++++-- bot.go | 41 ++++++++++++++++++++++++++++++++++++++--- configs.go | 7 +++++-- 3 files changed, 47 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index b18d15dd..77acdd67 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,9 @@ func main() { log.Printf("[%s] %s", update.Message.From.UserName, update.Message.Text) msg := tgbotapi.NewMessage(update.Message.Chat.ID, update.Message.Text) - msg.ReplyToMessageID = update.Message.MessageID + msg.ReplyParameters = &tgbotapi.ReplyParameters{ + MessageID: update.Message.MessageID, + } bot.Send(msg) } @@ -85,7 +87,7 @@ func main() { log.Printf("Authorized on account %s", bot.Self.UserName) - wh, _ := tgbotapi.NewWebhookWithCert("https://www.example.com:8443/"+bot.Token, "cert.pem") + wh, _ := tgbotapi.NewWebhookWithCert("https://www.example.com:8443/"+bot.Token, tgbotapi.FilePath("cert.pem")) _, err = bot.Request(wh) if err != nil { diff --git a/bot.go b/bot.go index 2f3f4f5c..67cd68df 100644 --- a/bot.go +++ b/bot.go @@ -3,6 +3,7 @@ package tgbotapi import ( + "bytes" "encoding/json" "errors" "fmt" @@ -29,7 +30,8 @@ type BotAPI struct { Client HTTPClient `json:"-"` shutdownChannel chan interface{} - apiEndpoint string + apiEndpoint string + fileEndpoint string } // NewBotAPI creates a new BotAPI instance. @@ -58,7 +60,8 @@ func NewBotAPIWithClient(token, apiEndpoint string, client HTTPClient) (*BotAPI, Buffer: 100, shutdownChannel: make(chan interface{}), - apiEndpoint: apiEndpoint, + apiEndpoint: apiEndpoint, + fileEndpoint: FileEndpoint, } self, err := bot.GetMe() @@ -76,6 +79,23 @@ func (bot *BotAPI) SetAPIEndpoint(apiEndpoint string) { bot.apiEndpoint = apiEndpoint } +// SetFileEndpoint changes the file download endpoint used by the instance. +// The value must be a format string with two %s placeholders for token and +// file path, matching the default FileEndpoint constant. +func (bot *BotAPI) SetFileEndpoint(fileEndpoint string) { + bot.fileEndpoint = fileEndpoint +} + +// FileLink returns the full download URL for the given File, using the +// file endpoint configured on this BotAPI instance. +func (bot *BotAPI) FileLink(f File) string { + endpoint := bot.fileEndpoint + if endpoint == "" { + endpoint = FileEndpoint + } + return fmt.Sprintf(endpoint, bot.Token, f.FilePath) +} + func buildParams(in Params) url.Values { out := url.Values{} for key, value := range in { @@ -341,12 +361,23 @@ func (bot *BotAPI) Request(c Chattable) (*APIResponse, error) { // Send will send a Chattable item to Telegram and provides the // returned Message. +// +// Some Bot API methods (banChatMember, setChatTitle, sendChatAction, +// etc.) return a bare `true` on success rather than a Message. Send +// tolerates that shape and returns a zero Message with nil error, so +// callers that reach for Send by reflex don't get a confusing JSON +// unmarshal error. Prefer Request for methods whose documented return +// type is not a Message. func (bot *BotAPI) Send(c Chattable) (Message, error) { resp, err := bot.Request(c) if err != nil { return Message{}, err } + if len(resp.Result) == 0 || bytes.Equal(resp.Result, []byte("true")) { + return Message{}, nil + } + var message Message err = json.Unmarshal(resp.Result, &message) @@ -445,7 +476,11 @@ func (bot *BotAPI) GetUpdatesChan(config UpdateConfig) UpdatesChannel { updates, err := bot.GetUpdates(config) if err != nil { - log.Println(err) + // Network errors from http.Post embed the full request URL, + // which in our case contains bot.Token. Strip it before + // logging so the token can't leak to logs. + redacted := strings.ReplaceAll(err.Error(), bot.Token, "") + log.Println(redacted) log.Println("Failed to get updates, retrying in 3 seconds...") time.Sleep(time.Second * 3) diff --git a/configs.go b/configs.go index 9474eeff..b9b5a740 100644 --- a/configs.go +++ b/configs.go @@ -1050,7 +1050,7 @@ func (config SetGameScoreConfig) params() (Params, error) { params := make(Params) params.AddNonZero64("user_id", config.UserID) - params.AddNonZero("scrore", config.Score) + params.AddNonZero("score", config.Score) params.AddBool("disable_edit_message", config.DisableEditMessage) if config.InlineMessageID != "" { @@ -1365,7 +1365,10 @@ func (config InlineConfig) params() (Params, error) { params := make(Params) params["inline_query_id"] = config.InlineQueryID - params.AddNonZero("cache_time", config.CacheTime) + // answerInlineQuery defaults cache_time to 300 seconds server-side, so + // we must serialize the zero value explicitly when the caller wants + // to disable caching. + params["cache_time"] = strconv.Itoa(config.CacheTime) params.AddBool("is_personal", config.IsPersonal) params.AddNonEmpty("next_offset", config.NextOffset) if err := params.AddAny("button", config.Button); err != nil { From 55982d6c49869d27724a0e1f3d0f70b1195e4227 Mon Sep 17 00:00:00 2001 From: Kirill Date: Sat, 11 Apr 2026 19:14:07 +0400 Subject: [PATCH 53/65] Update README.md --- README.md | 161 +++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 135 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 77acdd67..9d7930c0 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,68 @@ # Golang bindings for the Telegram Bot API -[![Go Reference](https://pkg.go.dev/badge/github.com/go-telegram-bot-api/telegram-bot-api/v5.svg)](https://pkg.go.dev/github.com/go-telegram-bot-api/telegram-bot-api/v5) -[![Test](https://github.com/go-telegram-bot-api/telegram-bot-api/actions/workflows/test.yml/badge.svg)](https://github.com/go-telegram-bot-api/telegram-bot-api/actions/workflows/test.yml) +[![Go Reference](https://pkg.go.dev/badge/github.com/kirugan/telegram-bot-api/v5.svg)](https://pkg.go.dev/github.com/kirugan/telegram-bot-api/v5) -All methods are fairly self-explanatory, and reading the [godoc](https://pkg.go.dev/github.com/go-telegram-bot-api/telegram-bot-api/v5) page should -explain everything. If something isn't clear, open an issue or submit -a pull request. +> **This is a maintained fork of +> [`go-telegram-bot-api/telegram-bot-api`](https://github.com/go-telegram-bot-api/telegram-bot-api).** +> The upstream project stopped updating at Bot API 6.0 (April 2022). This fork +> is brought up to **Bot API 9.6** (the latest release as of this writing) and +> tracks the [official changelog](https://core.telegram.org/bots/api-changelog) +> going forward. -There are more tutorials and high-level information on the website, [go-telegram-bot-api.dev](https://go-telegram-bot-api.dev). +All methods are fairly self-explanatory, and reading the +[godoc](https://pkg.go.dev/github.com/kirugan/telegram-bot-api/v5) page should +explain everything. -The scope of this project is just to provide a wrapper around the API -without any additional features. There are other projects for creating -something with plugins and command handlers without having to design -all that yourself. +The scope of this project is just to provide a wrapper around the API without +any additional features. There are other projects for creating something with +plugins and command handlers without having to design all that yourself. -Join [the development group](https://telegram.me/go_telegram_bot_api) if -you want to ask questions or discuss development. +--- -## Example +## Installing + +```sh +go get github.com/kirugan/telegram-bot-api/v5 +``` + +```go +import tgbotapi "github.com/kirugan/telegram-bot-api/v5" +``` + +Note: because the module path changed from `go-telegram-bot-api` to `kirugan`, +a Go `replace` directive in `go.mod` **will not** silently redirect existing +code — Go requires the replacement's module path to match the original. Either +update your imports, or keep using upstream on 6.0. + +## Migrating from upstream -First, ensure the library is installed and up to date by running -`go get -u github.com/go-telegram-bot-api/telegram-bot-api/v5`. +If you're coming from `go-telegram-bot-api/telegram-bot-api` v5.5.x and want to +jump to the latest Bot API, read [**BREAKING.md**](./BREAKING.md). It lists +every source-incompatible change introduced while bringing the fork from 6.0 +to 9.6, grouped by topic, with before/after snippets for each one. The fastest +way to migrate is `grep` your codebase for the old identifier and apply the +rewrite. + +### Migrating with Claude (or any LLM) + +`BREAKING.md` was written so it can be fed directly to a coding assistant. +A prompt like: + +> Here is `BREAKING.md` from a Go library I'm upgrading. Here are the files in +> my project that use `tgbotapi`. Please rewrite them to compile against the +> new API, preserving behavior. + +handles most of the mechanical rewrites (`Thumb` → `Thumbnail`, +`ReplyToMessageID` → `ReplyParameters`, `DisableWebPagePreview` → +`LinkPreviewOptions`, sticker creation, etc.) in one pass. Review the diff, +run `go build ./...`, done. + +--- + +## Example -This is a very simple bot that just displays any gotten updates, -then replies it to that chat. +This is a very simple bot that just displays any gotten updates, then replies +it to that chat. ```go package main @@ -31,7 +70,7 @@ package main import ( "log" - tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" + tgbotapi "github.com/kirugan/telegram-bot-api/v5" ) func main() { @@ -64,8 +103,7 @@ func main() { } ``` -If you need to use webhooks (if you wish to run on Google App Engine), -you may use a slightly different method. +If you need to use webhooks, you may use a slightly different method. ```go package main @@ -74,7 +112,7 @@ import ( "log" "net/http" - "github.com/go-telegram-bot-api/telegram-bot-api/v5" + tgbotapi "github.com/kirugan/telegram-bot-api/v5" ) func main() { @@ -113,11 +151,82 @@ func main() { ``` If you need, you may generate a self-signed certificate, as this requires -HTTPS / TLS. The above example tells Telegram that this is your -certificate and that it should be trusted, even though it is not -properly signed. +HTTPS / TLS. The above example tells Telegram that this is your certificate +and that it should be trusted, even though it is not properly signed. openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 3560 -subj "//O=Org\CN=Test" -nodes -Now that [Let's Encrypt](https://letsencrypt.org) is available, -you may wish to generate your free TLS certificate there. +Now that [Let's Encrypt](https://letsencrypt.org) is available, you may wish +to generate your free TLS certificate there. + +--- + +## What this fork adds + +### Bot API versions + +Full support for every Bot API version from **6.1 through 9.6**. See git log +for the per-version commits; each `Full support of API X` commit message is +the authoritative trail for what that version added. + +### Upstream issues fixed + +Real bugs and gaps reported against the original repository that are now +closed in this fork: + +- [#781](https://github.com/go-telegram-bot-api/telegram-bot-api/issues/781) + / [#745](https://github.com/go-telegram-bot-api/telegram-bot-api/issues/745) + — `SetGameScoreConfig` serialized the score under the key `scrore`; game + scores never reached Telegram. +- [#628](https://github.com/go-telegram-bot-api/telegram-bot-api/issues/628) + — `GetUpdatesChan` logged raw `http.Post` errors, which embed the full + request URL including the bot token. The token is now redacted before + logging. +- [#683](https://github.com/go-telegram-bot-api/telegram-bot-api/issues/683) + — `FileEndpoint` was a hard-coded package constant, so + `GetFileDirectURL` still pointed at `api.telegram.org` when you were + running a local Bot API server. There is now a `SetFileEndpoint` / + `FileLink` pair and the field is per-bot. +- [#740](https://github.com/go-telegram-bot-api/telegram-bot-api/issues/740) + — `InlineConfig.CacheTime = 0` was silently dropped by `AddNonZero`, so + Telegram applied its 300s default instead of disabling the cache. + `cache_time` is now serialized unconditionally. +- [#639](https://github.com/go-telegram-bot-api/telegram-bot-api/issues/639) + / [#705](https://github.com/go-telegram-bot-api/telegram-bot-api/issues/705) + — `Send()` tried to unmarshal the bare `true` returned by methods like + `banChatMember` / `setChatTitle` / `sendChatAction`, producing + `json: cannot unmarshal bool into Message`. It now returns a zero + `Message, nil` for those shapes. (Prefer `Request` for methods whose + documented return type is not a `Message`.) +- [#624](https://github.com/go-telegram-bot-api/telegram-bot-api/issues/624) + — README webhook example passed `"cert.pem"` as a `string` to + `NewWebhookWithCert`, which takes a `RequestFileData`. Example now uses + `tgbotapi.FilePath("cert.pem")`. + +### Drive-by improvements + +Small enhancements that didn't correspond to a filed issue but were worth +doing while the code was already open: + +- **Multipart upload name collision fix.** `prepareInputMediaFile` was using + `file-%d` for *both* the main media and the thumbnail on `InputMediaAudio` + / `InputMediaDocument`. The two multipart fields collided in the same form + body; any audio or document upload with a thumbnail probably didn't work on + the wire. Thumbnails now use `file-%d-thumbnail`, matching the existing + `InputMediaVideo` pattern. +- **`closeBody` drains response body before close.** `json.Decoder` can stop + short of EOF, and `net/http` will discard the underlying TCP connection if + the body isn't fully consumed — no keep-alive reuse. The fork drains the + remainder before `Close()`. +- **`Params.AddAny(key, value any) error`.** Replaces the older + `AddInterface` — same behavior, but uses `any` and has a clearer name. + `AddInterface` is kept as an alias for existing callers. +- **`Params.AddFirstValid` errors are propagated.** The upstream pattern + ignored its return value inside `params()` methods, so JSON marshaling + errors in complex fields were silently swallowed. The fork consistently + captures and returns it. +- **`json.RawMessage` for JSON-serialized string fields.** Fields the + Telegram docs describe as "JSON-serialized object" (e.g. `provider_data` + on `InvoiceConfig` / `InvoiceLinkConfig`) are now `json.RawMessage` + instead of `string`, so you can hand them an already-marshaled payload + without double-encoding. From 06ee8c995d6cb2ca4c61825b01ffc165b8debb46 Mon Sep 17 00:00:00 2001 From: Kirill Date: Sat, 11 Apr 2026 20:54:25 +0400 Subject: [PATCH 54/65] Add golangci-lint config+build, fix formatting --- .github/workflows/lint.yml | 22 ++++++++++++++ .golangci.yml | 8 +++++ configs.go | 62 +++++++++++++++++++------------------- types.go | 10 +++--- 4 files changed, 66 insertions(+), 36 deletions(-) create mode 100644 .github/workflows/lint.yml create mode 100644 .golangci.yml diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 00000000..2b2a7aff --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,22 @@ +name: Lint + +on: + push: + branches: + - master + pull_request: + +jobs: + golangci: + name: golangci-lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-go@v5 + with: + go-version: '1.24' + + - uses: golangci/golangci-lint-action@v6 + with: + version: v2.5.0 diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 00000000..a69f1fa6 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,8 @@ +version: "2" + +linters: + default: none + +formatters: + enable: + - gofmt diff --git a/configs.go b/configs.go index b9b5a740..aeb4cd8c 100644 --- a/configs.go +++ b/configs.go @@ -948,27 +948,27 @@ func (config ContactConfig) method() string { // SendPollConfig allows you to send a poll. type SendPollConfig struct { BaseChat - Question string - QuestionParseMode string - QuestionEntities []MessageEntity - Description string - DescriptionParseMode string - DescriptionEntities []MessageEntity - Options []InputPollOption - IsAnonymous bool - Type string - AllowsMultipleAnswers bool - AllowsRevoting bool - ShuffleOptions bool - AllowAddingOptions bool + Question string + QuestionParseMode string + QuestionEntities []MessageEntity + Description string + DescriptionParseMode string + DescriptionEntities []MessageEntity + Options []InputPollOption + IsAnonymous bool + Type string + AllowsMultipleAnswers bool + AllowsRevoting bool + ShuffleOptions bool + AllowAddingOptions bool HideResultsUntilCloses bool - CorrectOptionIDs []int - Explanation string - ExplanationParseMode string - ExplanationEntities []MessageEntity - OpenPeriod int - CloseDate int - IsClosed bool + CorrectOptionIDs []int + Explanation string + ExplanationParseMode string + ExplanationEntities []MessageEntity + OpenPeriod int + CloseDate int + IsClosed bool } func (config SendPollConfig) params() (Params, error) { @@ -4108,17 +4108,17 @@ func (config TransferBusinessAccountStarsConfig) params() (Params, error) { // GetBusinessAccountGiftsConfig returns the gifts received and owned by a // managed business account. type GetBusinessAccountGiftsConfig struct { - BusinessConnectionID string - ExcludeUnsaved bool - ExcludeSaved bool - ExcludeUnlimited bool - ExcludeLimitedUpgradable bool - ExcludeLimitedNonUpgradable bool - ExcludeUnique bool - ExcludeFromBlockchain bool - SortByPrice bool - Offset string - Limit int + BusinessConnectionID string + ExcludeUnsaved bool + ExcludeSaved bool + ExcludeUnlimited bool + ExcludeLimitedUpgradable bool + ExcludeLimitedNonUpgradable bool + ExcludeUnique bool + ExcludeFromBlockchain bool + SortByPrice bool + Offset string + Limit int } func (GetBusinessAccountGiftsConfig) method() string { diff --git a/types.go b/types.go index 63118e93..2836cd60 100644 --- a/types.go +++ b/types.go @@ -3695,11 +3695,11 @@ type GiftInfo struct { // Unique gift origin constants. const ( - UniqueGiftOriginUpgrade = "upgrade" - UniqueGiftOriginTransfer = "transfer" - UniqueGiftOriginResale = "resale" - UniqueGiftOriginGiftedUpgrade = "gifted_upgrade" - UniqueGiftOriginOffer = "offer" + UniqueGiftOriginUpgrade = "upgrade" + UniqueGiftOriginTransfer = "transfer" + UniqueGiftOriginResale = "resale" + UniqueGiftOriginGiftedUpgrade = "gifted_upgrade" + UniqueGiftOriginOffer = "offer" ) // UniqueGiftInfo describes a service message about a unique gift that was From c6b66aa7d42c96eeb9fcbffbd8442e6113495818 Mon Sep 17 00:00:00 2001 From: Kirill Date: Sat, 11 Apr 2026 20:55:21 +0400 Subject: [PATCH 55/65] Fix lint CI build --- .github/workflows/lint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 2b2a7aff..cb8d8867 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -17,6 +17,6 @@ jobs: with: go-version: '1.24' - - uses: golangci/golangci-lint-action@v6 + - uses: golangci/golangci-lint-action@v7 with: version: v2.5.0 From 59bfb60729f3a652fb896541eb7fcdafa5a60d59 Mon Sep 17 00:00:00 2001 From: Kirill Date: Sat, 9 May 2026 00:39:17 +0400 Subject: [PATCH 56/65] Full support of API 10.0 --- README.md | 18 +++- bot.go | 41 ++++++++ configs.go | 286 ++++++++++++++++++++++++++++++++++++++++++++++++--- helpers.go | 14 +++ types.go | 293 ++++++++++++++++++++++++++++++++++++++++++++++++++--- 5 files changed, 625 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index 9d7930c0..f181fef3 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ > **This is a maintained fork of > [`go-telegram-bot-api/telegram-bot-api`](https://github.com/go-telegram-bot-api/telegram-bot-api).** > The upstream project stopped updating at Bot API 6.0 (April 2022). This fork -> is brought up to **Bot API 9.6** (the latest release as of this writing) and +> is brought up to **Bot API 10.0** (the latest release as of this writing) and > tracks the [official changelog](https://core.telegram.org/bots/api-changelog) > going forward. @@ -39,7 +39,7 @@ update your imports, or keep using upstream on 6.0. If you're coming from `go-telegram-bot-api/telegram-bot-api` v5.5.x and want to jump to the latest Bot API, read [**BREAKING.md**](./BREAKING.md). It lists every source-incompatible change introduced while bringing the fork from 6.0 -to 9.6, grouped by topic, with before/after snippets for each one. The fastest +to 10.0, grouped by topic, with before/after snippets for each one. The fastest way to migrate is `grep` your codebase for the old identifier and apply the rewrite. @@ -165,10 +165,22 @@ to generate your free TLS certificate there. ### Bot API versions -Full support for every Bot API version from **6.1 through 9.6**. See git log +Full support for every Bot API version from **6.1 through 10.0**. See git log for the per-version commits; each `Full support of API X` commit message is the authoritative trail for what that version added. +The 10.0 surface includes guest mode (`AnswerGuestQuery`, +`Update.GuestMessage`, `User.SupportsGuestQueries`), live photos +(`SendLivePhoto` via `LivePhotoConfig` + `NewLivePhoto`, `LivePhoto` / +`InputMediaLivePhoto` / `PaidMediaLivePhoto`), expanded poll media +(`PollMedia`, plus `InputMediaSticker` / `InputMediaLocation` / +`InputMediaVenue` for poll questions, options, and quiz explanations), +poll restrictions (`MembersOnly`, `CountryCodes`), reaction administration +(`DeleteMessageReactionConfig`, `DeleteAllMessageReactionsConfig`, +`CanReactToMessages` on `ChatPermissions` / `ChatMember`), managed-bot +access settings (`GetManagedBotAccessSettings` / `SetManagedBotAccessSettings`, +`BotAccessSettings`), and `GetUserPersonalChatMessages`. + ### Upstream issues fixed Real bugs and gaps reported against the original repository that are now diff --git a/bot.go b/bot.go index 67cd68df..e9ea2aa2 100644 --- a/bot.go +++ b/bot.go @@ -1103,6 +1103,47 @@ func (bot *BotAPI) SavePreparedKeyboardButton(config SavePreparedKeyboardButtonC return button, err } +// AnswerGuestQuery replies to a received guest message and returns the +// resulting SentGuestMessage. +func (bot *BotAPI) AnswerGuestQuery(config AnswerGuestQueryConfig) (SentGuestMessage, error) { + var sent SentGuestMessage + + resp, err := bot.Request(config) + if err != nil { + return sent, err + } + + err = json.Unmarshal(resp.Result, &sent) + return sent, err +} + +// GetManagedBotAccessSettings returns the access settings of a managed bot. +func (bot *BotAPI) GetManagedBotAccessSettings(config GetManagedBotAccessSettingsConfig) (BotAccessSettings, error) { + var settings BotAccessSettings + + resp, err := bot.Request(config) + if err != nil { + return settings, err + } + + err = json.Unmarshal(resp.Result, &settings) + return settings, err +} + +// GetUserPersonalChatMessages returns the last messages from the personal +// chat of a user. +func (bot *BotAPI) GetUserPersonalChatMessages(config GetUserPersonalChatMessagesConfig) ([]Message, error) { + resp, err := bot.Request(config) + if err != nil { + return nil, err + } + + var messages []Message + err = json.Unmarshal(resp.Result, &messages) + + return messages, err +} + // EscapeText takes an input text and escape Telegram markup symbols. // In this way we can send a text without being afraid of having to escape the characters manually. // Note that you don't have to include the formatting style in the input text, or it will be escaped too. diff --git a/configs.go b/configs.go index aeb4cd8c..5f01306f 100644 --- a/configs.go +++ b/configs.go @@ -690,6 +690,56 @@ func (config VideoConfig) files() []RequestFile { return files } +// LivePhotoConfig contains information about a SendLivePhoto request. +// +// File holds the live photo video (must be 10 seconds or less and 10 MB or +// less). Photo holds the static image. Sending live photos by URL is not +// currently supported. +type LivePhotoConfig struct { + BaseFile + Photo RequestFileData + Caption string + ParseMode string + CaptionEntities []MessageEntity + ShowCaptionAboveMedia bool + HasSpoiler bool +} + +func (config LivePhotoConfig) params() (Params, error) { + params, err := config.BaseChat.params() + if err != nil { + return params, err + } + + params.AddNonEmpty("caption", config.Caption) + params.AddNonEmpty("parse_mode", config.ParseMode) + params.AddBool("show_caption_above_media", config.ShowCaptionAboveMedia) + params.AddBool("has_spoiler", config.HasSpoiler) + err = params.AddAny("caption_entities", config.CaptionEntities) + + return params, err +} + +func (config LivePhotoConfig) method() string { + return "sendLivePhoto" +} + +func (config LivePhotoConfig) files() []RequestFile { + files := []RequestFile{{ + Name: "live_photo", + Data: config.File, + }} + + if config.Photo != nil { + files = append(files, RequestFile{ + Name: "photo", + Data: config.Photo, + }) + } + + return files +} + // AnimationConfig contains information about a SendAnimation request. type AnimationConfig struct { BaseFile @@ -946,6 +996,11 @@ func (config ContactConfig) method() string { } // SendPollConfig allows you to send a poll. +// +// Media and ExplanationMedia accept any of the InputMedia* variants +// allowed by InputPollMedia (InputMediaAnimation, InputMediaAudio, +// InputMediaDocument, InputMediaLivePhoto, InputMediaLocation, +// InputMediaPhoto, InputMediaVenue, InputMediaVideo). type SendPollConfig struct { BaseChat Question string @@ -954,6 +1009,7 @@ type SendPollConfig struct { Description string DescriptionParseMode string DescriptionEntities []MessageEntity + Media any Options []InputPollOption IsAnonymous bool Type string @@ -962,10 +1018,13 @@ type SendPollConfig struct { ShuffleOptions bool AllowAddingOptions bool HideResultsUntilCloses bool + MembersOnly bool + CountryCodes []string CorrectOptionIDs []int Explanation string ExplanationParseMode string ExplanationEntities []MessageEntity + ExplanationMedia any OpenPeriod int CloseDate int IsClosed bool @@ -987,6 +1046,9 @@ func (config SendPollConfig) params() (Params, error) { if err = params.AddAny("description_entities", config.DescriptionEntities); err != nil { return params, err } + if err = params.AddAny("media", config.Media); err != nil { + return params, err + } if err = params.AddAny("options", config.Options); err != nil { return params, err } @@ -997,6 +1059,12 @@ func (config SendPollConfig) params() (Params, error) { params.AddBool("shuffle_options", config.ShuffleOptions) params.AddBool("allow_adding_options", config.AllowAddingOptions) params.AddBool("hide_results_until_closes", config.HideResultsUntilCloses) + params.AddBool("members_only", config.MembersOnly) + if len(config.CountryCodes) > 0 { + if err = params.AddAny("country_codes", config.CountryCodes); err != nil { + return params, err + } + } if len(config.CorrectOptionIDs) > 0 { if err = params.AddAny("correct_option_ids", config.CorrectOptionIDs); err != nil { return params, err @@ -1007,7 +1075,10 @@ func (config SendPollConfig) params() (Params, error) { params.AddNonEmpty("explanation_parse_mode", config.ExplanationParseMode) params.AddNonZero("open_period", config.OpenPeriod) params.AddNonZero("close_date", config.CloseDate) - err = params.AddAny("explanation_entities", config.ExplanationEntities) + if err = params.AddAny("explanation_entities", config.ExplanationEntities); err != nil { + return params, err + } + err = params.AddAny("explanation_media", config.ExplanationMedia) return params, err } @@ -1703,12 +1774,24 @@ func (ChatMemberCountConfig) method() string { // ChatAdministratorsConfig contains information about getting chat administrators. type ChatAdministratorsConfig struct { ChatConfig + // ReturnBots, if true, additionally returns bots that are administrators + // of the chat. By default, bots other than the current bot are omitted. + ReturnBots bool } func (ChatAdministratorsConfig) method() string { return "getChatAdministrators" } +func (config ChatAdministratorsConfig) params() (Params, error) { + params, err := config.ChatConfig.params() + if err != nil { + return params, err + } + params.AddBool("return_bots", config.ReturnBots) + return params, nil +} + // SetChatPermissionsConfig allows you to set default permissions for the // members in a group. The bot must be an administrator and have rights to // restrict members. @@ -2608,6 +2691,61 @@ func (config SetMessageReactionConfig) params() (Params, error) { return params, err } +// DeleteMessageReactionConfig removes a reaction from a message in a group +// or supergroup. The bot must have the can_delete_messages administrator +// right. Either UserID or ActorChatID identifies whose reaction is removed. +type DeleteMessageReactionConfig struct { + ChatID int64 + ChannelUsername string + MessageID int + UserID int64 + ActorChatID int64 +} + +func (config DeleteMessageReactionConfig) method() string { + return "deleteMessageReaction" +} + +func (config DeleteMessageReactionConfig) params() (Params, error) { + params := make(Params) + + if err := params.AddFirstValid("chat_id", config.ChatID, config.ChannelUsername); err != nil { + return params, err + } + params.AddNonZero("message_id", config.MessageID) + params.AddNonZero64("user_id", config.UserID) + params.AddNonZero64("actor_chat_id", config.ActorChatID) + + return params, nil +} + +// DeleteAllMessageReactionsConfig removes up to 10000 recent reactions in a +// group or supergroup added by a given user or chat. The bot must have the +// can_delete_messages administrator right. Either UserID or ActorChatID +// identifies whose reactions are removed. +type DeleteAllMessageReactionsConfig struct { + ChatID int64 + ChannelUsername string + UserID int64 + ActorChatID int64 +} + +func (config DeleteAllMessageReactionsConfig) method() string { + return "deleteAllMessageReactions" +} + +func (config DeleteAllMessageReactionsConfig) params() (Params, error) { + params := make(Params) + + if err := params.AddFirstValid("chat_id", config.ChatID, config.ChannelUsername); err != nil { + return params, err + } + params.AddNonZero64("user_id", config.UserID) + params.AddNonZero64("actor_chat_id", config.ActorChatID) + + return params, nil +} + // GetUserChatBoostsConfig returns the list of boosts added to a chat by a // user. The bot must be an administrator in the chat. // @@ -3557,8 +3695,8 @@ func (config SendPaidMediaConfig) files() []RequestFile { return prepareInputPaidMediaForFiles(config.Media) } -// prepareInputPaidMediaForParams rewrites InputPaidMedia entries whose Media -// or Thumbnail need uploading to attach:// references, mirroring +// prepareInputPaidMediaForParams rewrites InputPaidMedia entries whose Media, +// Photo, or Thumbnail need uploading to attach:// references, mirroring // prepareInputMediaForParams for regular media groups. func prepareInputPaidMediaForParams(items []InputPaidMedia) []InputPaidMedia { out := make([]InputPaidMedia, len(items)) @@ -3566,6 +3704,9 @@ func prepareInputPaidMediaForParams(items []InputPaidMedia) []InputPaidMedia { if m.Media != nil && m.Media.NeedsUpload() { m.Media = fileAttach(fmt.Sprintf("attach://paid-media-%d", i)) } + if m.Photo != nil && m.Photo.NeedsUpload() { + m.Photo = fileAttach(fmt.Sprintf("attach://paid-media-%d-photo", i)) + } if m.Thumbnail != nil && m.Thumbnail.NeedsUpload() { m.Thumbnail = fileAttach(fmt.Sprintf("attach://paid-media-%d-thumbnail", i)) } @@ -3575,7 +3716,7 @@ func prepareInputPaidMediaForParams(items []InputPaidMedia) []InputPaidMedia { } // prepareInputPaidMediaForFiles returns the upload entries for items in the -// slice whose Media or Thumbnail need uploading. +// slice whose Media, Photo, or Thumbnail need uploading. func prepareInputPaidMediaForFiles(items []InputPaidMedia) []RequestFile { var files []RequestFile for i, m := range items { @@ -3585,6 +3726,12 @@ func prepareInputPaidMediaForFiles(items []InputPaidMedia) []RequestFile { Data: m.Media, }) } + if m.Photo != nil && m.Photo.NeedsUpload() { + files = append(files, RequestFile{ + Name: fmt.Sprintf("paid-media-%d-photo", i), + Data: m.Photo, + }) + } if m.Thumbnail != nil && m.Thumbnail.NeedsUpload() { files = append(files, RequestFile{ Name: fmt.Sprintf("paid-media-%d-thumbnail", i), @@ -4416,15 +4563,18 @@ func (GetMyStarBalanceConfig) params() (Params, error) { } // SendMessageDraftConfig streams a partial text message to a user while the -// content is still being generated. Stream of partial messages is ended by -// calling sendMessage. +// content is still being generated. The streamed draft is ephemeral and acts +// as a 30-second preview; to persist the message, call sendMessage with the +// finalized text. Pass an empty Text to show a "Thinking..." placeholder. +// +// DraftID must be non-zero; updates with the same DraftID animate together. type SendMessageDraftConfig struct { ChatID int64 MessageThreadID int + DraftID int64 Text string ParseMode string Entities []MessageEntity - ReplyParameters *ReplyParameters } func (SendMessageDraftConfig) method() string { @@ -4436,12 +4586,11 @@ func (config SendMessageDraftConfig) params() (Params, error) { params.AddNonZero64("chat_id", config.ChatID) params.AddNonZero("message_thread_id", config.MessageThreadID) - params.AddNonEmpty("text", config.Text) + params.AddNonZero64("draft_id", config.DraftID) + // text is optional; an empty string is meaningful (placeholder). + params["text"] = config.Text params.AddNonEmpty("parse_mode", config.ParseMode) - if err := params.AddAny("entities", config.Entities); err != nil { - return params, err - } - err := params.AddAny("reply_parameters", config.ReplyParameters) + err := params.AddAny("entities", config.Entities) return params, err } @@ -4715,6 +4864,96 @@ func (config ReplaceManagedBotTokenConfig) params() (Params, error) { return params, nil } +// GetManagedBotAccessSettingsConfig returns the access settings of a managed +// bot. +type GetManagedBotAccessSettingsConfig struct { + // UserID is the user identifier of the managed bot whose access + // settings will be returned. + UserID int64 +} + +func (GetManagedBotAccessSettingsConfig) method() string { + return "getManagedBotAccessSettings" +} + +func (config GetManagedBotAccessSettingsConfig) params() (Params, error) { + params := make(Params) + + params.AddNonZero64("user_id", config.UserID) + + return params, nil +} + +// SetManagedBotAccessSettingsConfig updates the access settings of a managed +// bot. AddedUserIDs is a list of up to 10 users that will gain access in +// addition to the bot's owner; it is ignored when IsAccessRestricted is +// false. +type SetManagedBotAccessSettingsConfig struct { + UserID int64 + IsAccessRestricted bool + AddedUserIDs []int64 +} + +func (SetManagedBotAccessSettingsConfig) method() string { + return "setManagedBotAccessSettings" +} + +func (config SetManagedBotAccessSettingsConfig) params() (Params, error) { + params := make(Params) + + params.AddNonZero64("user_id", config.UserID) + params["is_access_restricted"] = strconv.FormatBool(config.IsAccessRestricted) + if len(config.AddedUserIDs) > 0 { + if err := params.AddAny("added_user_ids", config.AddedUserIDs); err != nil { + return params, err + } + } + + return params, nil +} + +// GetUserPersonalChatMessagesConfig returns the last messages from a user's +// personal chat. +type GetUserPersonalChatMessagesConfig struct { + // UserID is the unique identifier of the target user. + UserID int64 + // Limit is the maximum number of messages to return; 1-20. + Limit int +} + +func (GetUserPersonalChatMessagesConfig) method() string { + return "getUserPersonalChatMessages" +} + +func (config GetUserPersonalChatMessagesConfig) params() (Params, error) { + params := make(Params) + + params.AddNonZero64("user_id", config.UserID) + params.AddNonZero("limit", config.Limit) + + return params, nil +} + +// AnswerGuestQueryConfig replies to a received guest message. The Result is +// any of the InlineQueryResult* variants describing the message to be sent. +type AnswerGuestQueryConfig struct { + GuestQueryID string + Result any +} + +func (AnswerGuestQueryConfig) method() string { + return "answerGuestQuery" +} + +func (config AnswerGuestQueryConfig) params() (Params, error) { + params := make(Params) + + params["guest_query_id"] = config.GuestQueryID + err := params.AddAny("result", config.Result) + + return params, err +} + // SavePreparedKeyboardButtonConfig contains the parameters for the savePreparedKeyboardButton method. type SavePreparedKeyboardButtonConfig struct { // UserID is the unique identifier of the target user that can use the button. @@ -4778,6 +5017,16 @@ func prepareInputMediaParam(inputMedia interface{}, idx int) interface{} { m.Thumbnail = fileAttach(fmt.Sprintf("attach://file-%d-thumbnail", idx)) } + return m + case InputMediaLivePhoto: + if m.Media != nil && m.Media.NeedsUpload() { + m.Media = fileAttach(fmt.Sprintf("attach://file-%d", idx)) + } + + if m.Photo != nil && m.Photo.NeedsUpload() { + m.Photo = fileAttach(fmt.Sprintf("attach://file-%d-photo", idx)) + } + return m } @@ -4838,6 +5087,19 @@ func prepareInputMediaFile(inputMedia interface{}, idx int) []RequestFile { Data: m.Thumbnail, }) } + case InputMediaLivePhoto: + if m.Media != nil && m.Media.NeedsUpload() { + files = append(files, RequestFile{ + Name: fmt.Sprintf("file-%d", idx), + Data: m.Media, + }) + } + if m.Photo != nil && m.Photo.NeedsUpload() { + files = append(files, RequestFile{ + Name: fmt.Sprintf("file-%d-photo", idx), + Data: m.Photo, + }) + } case InputMediaAudio: if m.Media.NeedsUpload() { files = append(files, RequestFile{ diff --git a/helpers.go b/helpers.go index 4f135634..fd73cc7b 100644 --- a/helpers.go +++ b/helpers.go @@ -138,6 +138,20 @@ func NewVideo(chatID int64, file RequestFileData) VideoConfig { } } +// NewLivePhoto creates a new sendLivePhoto request. +// +// video is the live photo's video portion (≤10 s, ≤10 MB) and photo is the +// static image. Sending live photos by URL is not currently supported. +func NewLivePhoto(chatID int64, video, photo RequestFileData) LivePhotoConfig { + return LivePhotoConfig{ + BaseFile: BaseFile{ + BaseChat: BaseChat{ChatID: chatID}, + File: video, + }, + Photo: photo, + } +} + // NewAnimation creates a new sendAnimation request. func NewAnimation(chatID int64, file RequestFileData) AnimationConfig { return AnimationConfig{ diff --git a/types.go b/types.go index 2836cd60..b1a84f97 100644 --- a/types.go +++ b/types.go @@ -175,6 +175,12 @@ type Update struct { // // optional ManagedBot *ManagedBotUpdated `json:"managed_bot,omitempty"` + // GuestMessage is a message received from a chat where the bot is not + // a member, delivered via guest mode. Reply with answerGuestQuery + // using Message.GuestQueryID. + // + // optional + GuestMessage *Message `json:"guest_message,omitempty"` } // SentFrom returns the user who sent an update. Can be nil, if Telegram did not provide information @@ -301,6 +307,11 @@ type User struct { // // optional SupportsInlineQueries bool `json:"supports_inline_queries,omitempty"` + // SupportsGuestQueries is true, if the bot supports guest queries from + // chats it is not a member of. Returned only in getMe. + // + // optional + SupportsGuestQueries bool `json:"supports_guest_queries,omitempty"` // CanManageBots is true, if other bots can be created to be controlled by the bot. // Returned only in getMe. // @@ -708,6 +719,24 @@ type Message struct { // // optional ViaBot *User `json:"via_bot,omitempty"` + // GuestQueryID is the unique identifier for the guest query. Use this + // identifier with the method answerGuestQuery to send a response + // message. If non-empty, the message belongs to the chat where the + // guest bot was summoned, which may not coincide with other existing + // bot chats sharing the same identifier. + // + // optional + GuestQueryID string `json:"guest_query_id,omitempty"` + // GuestBotCallerUser is, for a message sent by a guest bot, the user + // whose original message triggered the bot's response. + // + // optional + GuestBotCallerUser *User `json:"guest_bot_caller_user,omitempty"` + // GuestBotCallerChat is, for a message sent by a guest bot, the chat + // whose original message triggered the bot's response. + // + // optional + GuestBotCallerChat *Chat `json:"guest_bot_caller_chat,omitempty"` // EditDate of the message was last edited in Unix time; // // optional @@ -764,6 +793,12 @@ type Message struct { // // optional Photo []PhotoSize `json:"photo,omitempty"` + // LivePhoto message is a live photo, information about the live photo. + // For backward compatibility, when this field is set, the photo field + // will also be set. + // + // optional + LivePhoto *LivePhoto `json:"live_photo,omitempty"` // PaidMedia is the paid media attached to the message. // // optional @@ -1439,6 +1474,35 @@ type Document struct { FileSize int64 `json:"file_size,omitempty"` } +// LivePhoto represents a live photo (a photo with a short video). +type LivePhoto struct { + // FileID is an identifier for the video file which can be used to + // download or reuse the file. + FileID string `json:"file_id"` + // FileUniqueID is the unique identifier for the video file which is + // supposed to be the same over time and for different bots. Can't be + // used to download or reuse the file. + FileUniqueID string `json:"file_unique_id"` + // Width is the video width as defined by the sender. + Width int `json:"width"` + // Height is the video height as defined by the sender. + Height int `json:"height"` + // Duration of the video in seconds as defined by the sender. + Duration int `json:"duration"` + // Photo are the available sizes of the corresponding static photo. + // + // optional + Photo []PhotoSize `json:"photo,omitempty"` + // MimeType is the MIME type of the file as defined by the sender. + // + // optional + MimeType string `json:"mime_type,omitempty"` + // FileSize is the file size in bytes. + // + // optional + FileSize int64 `json:"file_size,omitempty"` +} + // Video represents a video file. type Video struct { // FileID identifier for this file, which can be used to download or reuse @@ -1557,6 +1621,51 @@ type Dice struct { Value int `json:"value"` } +// PollMedia represents media attached to a poll, poll option, or quiz +// explanation. At most one of the optional fields can be present in a given +// PollMedia value. +type PollMedia struct { + // Animation is set if the media is an animation. + // + // optional + Animation *Animation `json:"animation,omitempty"` + // Audio is set if the media is an audio file. Currently, can't appear + // in a poll option. + // + // optional + Audio *Audio `json:"audio,omitempty"` + // Document is set if the media is a general file. Currently, can't + // appear in a poll option. + // + // optional + Document *Document `json:"document,omitempty"` + // LivePhoto is set if the media is a live photo. + // + // optional + LivePhoto *LivePhoto `json:"live_photo,omitempty"` + // Location is set if the media is a shared location. + // + // optional + Location *Location `json:"location,omitempty"` + // Photo is set if the media is a photo. + // + // optional + Photo []PhotoSize `json:"photo,omitempty"` + // Sticker is set if the media is a sticker. Currently, only valid for + // poll options. + // + // optional + Sticker *Sticker `json:"sticker,omitempty"` + // Venue is set if the media is a venue. + // + // optional + Venue *Venue `json:"venue,omitempty"` + // Video is set if the media is a video. + // + // optional + Video *Video `json:"video,omitempty"` +} + // PollOption contains information about one answer option in a poll. type PollOption struct { // Text is the option text, 1-100 characters @@ -1566,6 +1675,10 @@ type PollOption struct { // // optional TextEntities []MessageEntity `json:"text_entities,omitempty"` + // Media is the optional media attached to the poll option. + // + // optional + Media *PollMedia `json:"media,omitempty"` // VoterCount is the number of users that voted for this option VoterCount int `json:"voter_count"` // PersistentID is the persistent identifier of the poll option; stays @@ -1667,7 +1780,20 @@ type Poll struct { // // optional AllowsRevoting bool `json:"allows_revoting,omitempty"` - // Description is the text of the poll description. + // MembersOnly is true if voting is limited to users who have been + // members of the chat where the poll was originally sent for more than + // 24 hours. + // + // optional + MembersOnly bool `json:"members_only,omitempty"` + // CountryCodes is a list of two-letter ISO 3166-1 alpha-2 country codes + // indicating the countries from which users can vote in the poll. If + // empty, then users from any country can participate in the poll. + // + // optional + CountryCodes []string `json:"country_codes,omitempty"` + // Description is the text of the poll description; for polls inside + // the Message object only. // // optional Description string `json:"description,omitempty"` @@ -1676,6 +1802,11 @@ type Poll struct { // // optional DescriptionEntities []MessageEntity `json:"description_entities,omitempty"` + // Media is the media attached to the poll description; for polls inside + // the Message object only. + // + // optional + Media *PollMedia `json:"media,omitempty"` // Explanation is text that is shown when a user chooses an incorrect answer // or taps on the lamp icon in a quiz-style poll, 0-200 characters // @@ -1686,6 +1817,10 @@ type Poll struct { // // optional ExplanationEntities []MessageEntity `json:"explanation_entities,omitempty"` + // ExplanationMedia is the media attached to the quiz explanation. + // + // optional + ExplanationMedia *PollMedia `json:"explanation_media,omitempty"` // OpenPeriod is the amount of time in seconds the poll will be active // after creation // @@ -2661,6 +2796,11 @@ type ChatMember struct { // // optional CanAddWebPagePreviews bool `json:"can_add_web_page_previews,omitempty"` + // CanReactToMessages restricted only. + // True, if the user is allowed to react to messages. + // + // optional + CanReactToMessages bool `json:"can_react_to_messages,omitempty"` } // IsCreator returns if the ChatMember was the creator of the chat. @@ -2774,6 +2914,11 @@ type ChatPermissions struct { // // optional CanAddWebPagePreviews bool `json:"can_add_web_page_previews,omitempty"` + // CanReactToMessages is true, if the user is allowed to react to + // messages. If omitted, defaults to the value of CanSendMessages. + // + // optional + CanReactToMessages bool `json:"can_react_to_messages,omitempty"` // CanChangeInfo is true, if the user is allowed to change the chat title, // photo and other settings. Ignored in public supergroups // @@ -3167,6 +3312,13 @@ type InputPollOption struct { // // optional TextEntities []MessageEntity `json:"text_entities,omitempty"` + // Media is the optional media attached to the poll option. The value + // must be one of the InputMedia* variants accepted by InputPollOption + // (InputMediaAnimation, InputMediaLivePhoto, InputMediaLocation, + // InputMediaPhoto, InputMediaSticker, InputMediaVenue, InputMediaVideo). + // + // optional + Media any `json:"media,omitempty"` } // Birthdate contains information about a user's birthdate. @@ -3362,18 +3514,20 @@ type StarTransactions struct { // Paid media type constants. const ( - PaidMediaTypePreview = "preview" - PaidMediaTypePhoto = "photo" - PaidMediaTypeVideo = "video" + PaidMediaTypePreview = "preview" + PaidMediaTypePhoto = "photo" + PaidMediaTypeVideo = "video" + PaidMediaTypeLivePhoto = "live_photo" ) // PaidMedia describes a media received in a paid message. Flat polymorphic // by Type: -// - "preview" → Width, Height, Duration (optional) -// - "photo" → Photo is set -// - "video" → Video is set +// - "preview" → Width, Height, Duration (optional) +// - "photo" → Photo is set +// - "video" → Video is set +// - "live_photo" → LivePhoto is set type PaidMedia struct { - // Type of the paid media. One of "preview", "photo", "video". + // Type of the paid media. One of "preview", "photo", "video", "live_photo". Type string `json:"type"` // Width is the media width as defined by the sender. Set when Type is // "preview". @@ -3398,6 +3552,10 @@ type PaidMedia struct { // // optional Video *Video `json:"video,omitempty"` + // LivePhoto is the live photo media. Set when Type is "live_photo". + // + // optional + LivePhoto *LivePhoto `json:"live_photo,omitempty"` } // PaidMediaInfo describes the paid media added to a message. @@ -4323,17 +4481,32 @@ type ChecklistTasksAdded struct { Tasks []ChecklistTask `json:"tasks"` } +// Input paid media type constants. +const ( + InputPaidMediaTypePhoto = "photo" + InputPaidMediaTypeVideo = "video" + InputPaidMediaTypeLivePhoto = "live_photo" +) + // InputPaidMedia describes the paid media to be sent. Flat polymorphic by // Type: -// - "photo" → Media is set -// - "video" → Media, Thumbnail, Cover, StartTimestamp, Width, Height, +// - "photo" → Media is set +// - "video" → Media, Thumbnail, Cover, StartTimestamp, Width, Height, // Duration, SupportsStreaming +// - "live_photo" → Media (the video portion of the live photo) and Photo +// (the static photo) are set; sending live photos by URL is not +// supported. type InputPaidMedia struct { - // Type of the media. One of "photo", "video". + // Type of the media. One of "photo", "video", "live_photo". Type string `json:"type"` // Media is the file to send. Pass a file_id or URL, or upload a new one - // via FilePath/FileBytes/FileReader. + // via FilePath/FileBytes/FileReader. For live_photo, this is the video + // portion of the live photo. Media RequestFileData `json:"media"` + // Photo is the static photo to send when Type is "live_photo". + // + // optional + Photo RequestFileData `json:"photo,omitempty"` // Thumbnail of the file (video only). // // optional @@ -4518,6 +4691,10 @@ type ExternalReplyInfo struct { // // optional Photo []PhotoSize `json:"photo,omitempty"` + // LivePhoto is set if the message is a live photo. + // + // optional + LivePhoto *LivePhoto `json:"live_photo,omitempty"` // PaidMedia is set if the message contains paid media. // // optional @@ -5219,6 +5396,79 @@ type InputMediaDocument struct { DisableContentTypeDetection bool `json:"disable_content_type_detection,omitempty"` } +// InputMediaLivePhoto represents a live photo to be sent as part of a media +// group, sendLivePhoto, or as media in a poll/option/explanation. +// +// Sending live photos by URL is not currently supported; both Media and +// Photo must be either a file_id or a multipart upload. +type InputMediaLivePhoto struct { + BaseInputMedia + // Photo is the static photo of the live photo. Pass a file_id to send + // a file that exists on the Telegram servers (recommended) or pass an + // upload via "attach://". + Photo RequestFileData `json:"photo"` +} + +// InputMediaSticker represents a sticker file to be sent. Currently used as +// poll-option media. +type InputMediaSticker struct { + // Type must be "sticker". + Type string `json:"type"` + // Media is the file to send. + Media RequestFileData `json:"media"` + // Emoji associated with the sticker; only for just-uploaded stickers. + // + // optional + Emoji string `json:"emoji,omitempty"` +} + +// InputMediaLocation represents a shared location to be sent as media in a +// poll, poll option, or quiz explanation. +type InputMediaLocation struct { + // Type must be "location". + Type string `json:"type"` + // Latitude of the location. + Latitude float64 `json:"latitude"` + // Longitude of the location. + Longitude float64 `json:"longitude"` + // HorizontalAccuracy is the radius of uncertainty for the location, + // measured in meters; 0-1500. + // + // optional + HorizontalAccuracy float64 `json:"horizontal_accuracy,omitempty"` +} + +// InputMediaVenue represents a venue to be sent as media in a poll, poll +// option, or quiz explanation. +type InputMediaVenue struct { + // Type must be "venue". + Type string `json:"type"` + // Latitude of the venue. + Latitude float64 `json:"latitude"` + // Longitude of the venue. + Longitude float64 `json:"longitude"` + // Title is the name of the venue. + Title string `json:"title"` + // Address of the venue. + Address string `json:"address"` + // FoursquareID is the Foursquare identifier of the venue. + // + // optional + FoursquareID string `json:"foursquare_id,omitempty"` + // FoursquareType is the Foursquare type of the venue. + // + // optional + FoursquareType string `json:"foursquare_type,omitempty"` + // GooglePlaceID is the Google Places identifier of the venue. + // + // optional + GooglePlaceID string `json:"google_place_id,omitempty"` + // GooglePlaceType is the Google Places type of the venue. + // + // optional + GooglePlaceType string `json:"google_place_type,omitempty"` +} + // Sticker type constants. const ( StickerTypeRegular = "regular" @@ -6341,6 +6591,25 @@ type SentWebAppMessage struct { InlineMessageID string `json:"inline_message_id,omitempty"` } +// SentGuestMessage describes an inline message sent by a guest bot in +// response to a guest query. +type SentGuestMessage struct { + // InlineMessageID is the identifier of the sent inline message. + InlineMessageID string `json:"inline_message_id"` +} + +// BotAccessSettings describes the access settings of a managed bot. +type BotAccessSettings struct { + // IsAccessRestricted is true, if only selected users can access the + // bot. The bot's owner can always access it. + IsAccessRestricted bool `json:"is_access_restricted"` + // AddedUsers is the list of other users who have access to the bot if + // the access is restricted. + // + // optional + AddedUsers []User `json:"added_users,omitempty"` +} + // InputTextMessageContent contains text for displaying // as an inline query result. type InputTextMessageContent struct { From 5bd2165bb8b88f773c99271928d8891b07df61a2 Mon Sep 17 00:00:00 2001 From: Kirill Date: Sat, 20 Jun 2026 02:56:40 +0400 Subject: [PATCH 57/65] Full support of API 10.1 --- README.md | 16 +- configs.go | 132 ++++++++++- helpers.go | 9 + richmessage_test.go | 153 +++++++++++++ types.go | 543 +++++++++++++++++++++++++++++++++++++++++++- 5 files changed, 847 insertions(+), 6 deletions(-) create mode 100644 richmessage_test.go diff --git a/README.md b/README.md index f181fef3..abb57e14 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ > **This is a maintained fork of > [`go-telegram-bot-api/telegram-bot-api`](https://github.com/go-telegram-bot-api/telegram-bot-api).** > The upstream project stopped updating at Bot API 6.0 (April 2022). This fork -> is brought up to **Bot API 10.0** (the latest release as of this writing) and +> is brought up to **Bot API 10.1** (the latest release as of this writing) and > tracks the [official changelog](https://core.telegram.org/bots/api-changelog) > going forward. @@ -39,7 +39,8 @@ update your imports, or keep using upstream on 6.0. If you're coming from `go-telegram-bot-api/telegram-bot-api` v5.5.x and want to jump to the latest Bot API, read [**BREAKING.md**](./BREAKING.md). It lists every source-incompatible change introduced while bringing the fork from 6.0 -to 10.0, grouped by topic, with before/after snippets for each one. The fastest +to 10.0, grouped by topic, with before/after snippets for each one (10.1 is +purely additive and introduced no breaking changes). The fastest way to migrate is `grep` your codebase for the old identifier and apply the rewrite. @@ -165,10 +166,19 @@ to generate your free TLS certificate there. ### Bot API versions -Full support for every Bot API version from **6.1 through 10.0**. See git log +Full support for every Bot API version from **6.1 through 10.1**. See git log for the per-version commits; each `Full support of API X` commit message is the authoritative trail for what that version added. +The 10.1 surface includes rich messages (`SendRichMessage` via +`SendRichMessageConfig` + `NewRichMessage`, streaming drafts via +`SendRichMessageDraftConfig`, the `InputRichMessage` send type, the received +`RichMessage` / `RichText` / `RichBlock` tree, and `EditMessageTextConfig.RichMessage`), +join request queries (`AnswerChatJoinRequestQueryConfig`, +`SendChatJoinRequestWebAppConfig`, `ChatJoinRequest.QueryID`, +`ChatFullInfo.GuardBot`, `User.SupportsJoinRequestQueries`), and poll links +(`Link`, `PollMedia.Link`, `InputMediaLink`). + The 10.0 surface includes guest mode (`AnswerGuestQuery`, `Update.GuestMessage`, `User.SupportsGuestQueries`), live photos (`SendLivePhoto` via `LivePhotoConfig` + `NewLivePhoto`, `LivePhoto` / diff --git a/configs.go b/configs.go index 5f01306f..b1efafa9 100644 --- a/configs.go +++ b/configs.go @@ -1195,6 +1195,9 @@ type EditMessageTextConfig struct { ParseMode string Entities []MessageEntity LinkPreviewOptions *LinkPreviewOptions + // RichMessage is the new rich content of the message. Required if Text + // is not specified. + RichMessage *InputRichMessage } func (config EditMessageTextConfig) params() (Params, error) { @@ -1208,7 +1211,10 @@ func (config EditMessageTextConfig) params() (Params, error) { if err = params.AddAny("link_preview_options", config.LinkPreviewOptions); err != nil { return params, err } - err = params.AddAny("entities", config.Entities) + if err = params.AddAny("entities", config.Entities); err != nil { + return params, err + } + err = params.AddAny("rich_message", config.RichMessage) return params, err } @@ -5153,3 +5159,127 @@ func prepareInputMediaForFiles(inputMedia []interface{}) []RequestFile { return files } + +// SendRichMessageConfig contains information about a sendRichMessage request. +// If the message contains a block with a media element, the bot must have the +// right to send that media to the chat. On success the sent Message is +// returned, so it can be passed to BotAPI.Send. +type SendRichMessageConfig struct { + BaseChat + // RichMessage is the message to be sent. + RichMessage *InputRichMessage + // SuggestedPostParameters contains the parameters of the suggested post + // to send; for direct messages chats only. + SuggestedPostParameters *SuggestedPostParameters +} + +func (config SendRichMessageConfig) params() (Params, error) { + params, err := config.BaseChat.params() + if err != nil { + return params, err + } + + if err = params.AddAny("rich_message", config.RichMessage); err != nil { + return params, err + } + err = params.AddAny("suggested_post_parameters", config.SuggestedPostParameters) + + return params, err +} + +func (config SendRichMessageConfig) method() string { + return "sendRichMessage" +} + +// SendRichMessageDraftConfig streams a partial rich message to a user while +// the message is being generated. The streamed draft is ephemeral and acts as +// a temporary 30-second preview; once the output is finalized, sendRichMessage +// must be called with the complete message to persist it. Returns True on +// success, so use BotAPI.Request. +type SendRichMessageDraftConfig struct { + // ChatID is the unique identifier for the target private chat. Required. + ChatID int64 + // MessageThreadID is the unique identifier for the target message thread. + MessageThreadID int + // DraftID is the unique identifier of the message draft; must be + // non-zero. Changes to drafts with the same identifier are animated. + // Required. + DraftID int + // RichMessage is the partial message to be streamed. Required. + RichMessage *InputRichMessage +} + +func (config SendRichMessageDraftConfig) params() (Params, error) { + params := make(Params) + + params.AddNonZero64("chat_id", config.ChatID) + params.AddNonZero("message_thread_id", config.MessageThreadID) + params.AddNonZero("draft_id", config.DraftID) + err := params.AddAny("rich_message", config.RichMessage) + + return params, err +} + +func (config SendRichMessageDraftConfig) method() string { + return "sendRichMessageDraft" +} + +// Result values for AnswerChatJoinRequestQueryConfig. +const ( + // ChatJoinRequestApprove allows the user to join the chat. + ChatJoinRequestApprove = "approve" + // ChatJoinRequestDecline disallows the user from joining the chat. + ChatJoinRequestDecline = "decline" + // ChatJoinRequestQueue leaves the decision to other administrators. + ChatJoinRequestQueue = "queue" +) + +// AnswerChatJoinRequestQueryConfig processes a received chat join request +// query. Returns True on success, so use BotAPI.Request. +type AnswerChatJoinRequestQueryConfig struct { + // ChatJoinRequestQueryID is the unique identifier of the join request + // query. Required. + ChatJoinRequestQueryID string + // Result of the query. Must be one of ChatJoinRequestApprove, + // ChatJoinRequestDecline, or ChatJoinRequestQueue. Required. + Result string +} + +func (config AnswerChatJoinRequestQueryConfig) params() (Params, error) { + params := make(Params) + + params["chat_join_request_query_id"] = config.ChatJoinRequestQueryID + params["result"] = config.Result + + return params, nil +} + +func (config AnswerChatJoinRequestQueryConfig) method() string { + return "answerChatJoinRequestQuery" +} + +// SendChatJoinRequestWebAppConfig processes a received chat join request query +// by showing a Mini App to the user before deciding the outcome. Call +// AnswerChatJoinRequestQueryConfig afterwards to resolve the join request based +// on the user's interaction with the Mini App. Returns True on success, so use +// BotAPI.Request. +type SendChatJoinRequestWebAppConfig struct { + // ChatJoinRequestQueryID is the unique identifier of the join request + // query. Required. + ChatJoinRequestQueryID string + // WebAppURL is the URL of the Mini App to be opened. Required. + WebAppURL string +} + +func (config SendChatJoinRequestWebAppConfig) params() (Params, error) { + params := make(Params) + + params["chat_join_request_query_id"] = config.ChatJoinRequestQueryID + params["web_app_url"] = config.WebAppURL + + return params, nil +} + +func (config SendChatJoinRequestWebAppConfig) method() string { + return "sendChatJoinRequestWebApp" +} diff --git a/helpers.go b/helpers.go index fd73cc7b..b142ae50 100644 --- a/helpers.go +++ b/helpers.go @@ -152,6 +152,15 @@ func NewLivePhoto(chatID int64, video, photo RequestFileData) LivePhotoConfig { } } +// NewRichMessage creates a new sendRichMessage request for the given chat. +// Exactly one of message.HTML or message.Markdown must be set. +func NewRichMessage(chatID int64, message InputRichMessage) SendRichMessageConfig { + return SendRichMessageConfig{ + BaseChat: BaseChat{ChatID: chatID}, + RichMessage: &message, + } +} + // NewAnimation creates a new sendAnimation request. func NewAnimation(chatID int64, file RequestFileData) AnimationConfig { return AnimationConfig{ diff --git a/richmessage_test.go b/richmessage_test.go new file mode 100644 index 00000000..dddd4b44 --- /dev/null +++ b/richmessage_test.go @@ -0,0 +1,153 @@ +package tgbotapi + +import ( + "encoding/json" + "reflect" + "testing" +) + +// TestRichTextUnmarshalForms verifies that the polymorphic RichText type +// decodes the three wire forms: plain string, array, and styled span. +func TestRichTextUnmarshalForms(t *testing.T) { + var plain RichText + if err := json.Unmarshal([]byte(`"hello"`), &plain); err != nil { + t.Fatalf("plain unmarshal: %v", err) + } + if !plain.IsPlain || plain.PlainText != "hello" { + t.Fatalf("plain form not decoded: %+v", plain) + } + + var arr RichText + if err := json.Unmarshal([]byte(`["a","b"]`), &arr); err != nil { + t.Fatalf("array unmarshal: %v", err) + } + if len(arr.Parts) != 2 || arr.Parts[0].PlainText != "a" { + t.Fatalf("array form not decoded: %+v", arr) + } + + var span RichText + if err := json.Unmarshal([]byte(`{"type":"url","text":"site","url":"https://example.com"}`), &span); err != nil { + t.Fatalf("span unmarshal: %v", err) + } + if span.Type != RichTextTypeURL || span.URL != "https://example.com" { + t.Fatalf("span form not decoded: %+v", span) + } + if span.Text == nil || !span.Text.IsPlain || span.Text.PlainText != "site" { + t.Fatalf("nested span text not decoded: %+v", span.Text) + } +} + +// TestRichTextMarshalRoundTrip verifies that each RichText form survives a +// decode/encode/decode cycle unchanged. +func TestRichTextMarshalRoundTrip(t *testing.T) { + cases := []string{ + `"plain"`, + `["a","b"]`, + `{"type":"bold","text":"x"}`, + `{"type":"date_time","text":"now","unix_time":1700000000,"date_time_format":"d MMM"}`, + `{"type":"text_mention","text":"u","user":{"id":1,"is_bot":true,"first_name":"A"}}`, + } + for _, in := range cases { + var first RichText + if err := json.Unmarshal([]byte(in), &first); err != nil { + t.Fatalf("unmarshal %s: %v", in, err) + } + out, err := json.Marshal(first) + if err != nil { + t.Fatalf("marshal %s: %v", in, err) + } + var second RichText + if err := json.Unmarshal(out, &second); err != nil { + t.Fatalf("re-unmarshal %s: %v", out, err) + } + // Raw differs by construction (only set on decode), so clear it. + first.Raw, second.Raw = nil, nil + if !reflect.DeepEqual(first, second) { + t.Errorf("round trip mismatch:\n in: %s\n out: %s", in, out) + } + } +} + +// TestRichBlockCaptionRouting verifies that the shared "caption" wire field is +// routed to TableCaption for a table block and to Caption for a media block. +func TestRichBlockCaptionRouting(t *testing.T) { + var table RichBlock + if err := json.Unmarshal([]byte(`{"type":"table","cells":[],"caption":"a table"}`), &table); err != nil { + t.Fatalf("table unmarshal: %v", err) + } + if table.TableCaption == nil || table.TableCaption.PlainText != "a table" { + t.Fatalf("table caption not routed to TableCaption: %+v", table) + } + if table.Caption != nil { + t.Fatalf("table caption wrongly set Caption: %+v", table.Caption) + } + + var photo RichBlock + if err := json.Unmarshal([]byte(`{"type":"photo","photo":[],"caption":{"text":"pic"}}`), &photo); err != nil { + t.Fatalf("photo unmarshal: %v", err) + } + if photo.Caption == nil || !photo.Caption.Text.IsPlain || photo.Caption.Text.PlainText != "pic" { + t.Fatalf("photo caption not routed to Caption: %+v", photo) + } + if photo.TableCaption != nil { + t.Fatalf("photo caption wrongly set TableCaption: %+v", photo.TableCaption) + } + + // The table caption must marshal back out under the shared "caption" key. + out, err := json.Marshal(table) + if err != nil { + t.Fatalf("table marshal: %v", err) + } + var check map[string]json.RawMessage + if err := json.Unmarshal(out, &check); err != nil { + t.Fatalf("remarshal check: %v", err) + } + if string(check["caption"]) != `"a table"` { + t.Errorf("table caption not emitted under caption key: %s", out) + } +} + +// TestRichMessageDecode verifies a full RichMessage with nested blocks decodes. +func TestRichMessageDecode(t *testing.T) { + raw := `{"blocks":[{"type":"heading","text":"Title","size":1},{"type":"paragraph","text":["plain ",{"type":"bold","text":"bold"}]}],"is_rtl":false}` + var rm RichMessage + if err := json.Unmarshal([]byte(raw), &rm); err != nil { + t.Fatalf("rich message unmarshal: %v", err) + } + if len(rm.Blocks) != 2 { + t.Fatalf("expected 2 blocks, got %d", len(rm.Blocks)) + } + if rm.Blocks[0].Type != RichBlockTypeHeading || rm.Blocks[0].Size != 1 { + t.Errorf("heading block not decoded: %+v", rm.Blocks[0]) + } + para := rm.Blocks[1] + if para.Type != RichBlockTypeParagraph || para.Text == nil || len(para.Text.Parts) != 2 { + t.Fatalf("paragraph block not decoded: %+v", para) + } + if para.Text.Parts[1].Type != RichTextTypeBold { + t.Errorf("nested bold span not decoded: %+v", para.Text.Parts[1]) + } +} + +// TestInputRichMessageParams verifies the send-side config serializes the +// rich_message parameter as JSON. +func TestInputRichMessageParams(t *testing.T) { + cfg := NewRichMessage(123, InputRichMessage{HTML: "hi"}) + params, err := cfg.params() + if err != nil { + t.Fatalf("params: %v", err) + } + if params["chat_id"] != "123" { + t.Errorf("chat_id = %q", params["chat_id"]) + } + if cfg.method() != "sendRichMessage" { + t.Errorf("method = %q", cfg.method()) + } + var got InputRichMessage + if err := json.Unmarshal([]byte(params["rich_message"]), &got); err != nil { + t.Fatalf("rich_message param not valid JSON: %v", err) + } + if got.HTML != "hi" { + t.Errorf("rich_message html = %q", got.HTML) + } +} diff --git a/types.go b/types.go index b1a84f97..05deac95 100644 --- a/types.go +++ b/types.go @@ -317,6 +317,11 @@ type User struct { // // optional CanManageBots bool `json:"can_manage_bots,omitempty"` + // SupportsJoinRequestQueries is true, if the bot supports join request + // queries and can be assigned to process them. Returned only in getMe. + // + // optional + SupportsJoinRequestQueries bool `json:"supports_join_request_queries,omitempty"` } // String displays a simple text version of a user. @@ -616,6 +621,11 @@ type ChatFullInfo struct { // // optional FirstProfileAudio *Audio `json:"first_profile_audio,omitempty"` + // GuardBot is the bot that processes join request queries in the chat. + // The field is only available to chat administrators. + // + // optional + GuardBot *User `json:"guard_bot,omitempty"` } // Message represents a message. @@ -719,6 +729,10 @@ type Message struct { // // optional ViaBot *User `json:"via_bot,omitempty"` + // RichMessage is set if the message is a rich formatted message. + // + // optional + RichMessage *RichMessage `json:"rich_message,omitempty"` // GuestQueryID is the unique identifier for the guest query. Use this // identifier with the method answerGuestQuery to send a response // message. If non-empty, the message belongs to the chat where the @@ -1639,6 +1653,11 @@ type PollMedia struct { // // optional Document *Document `json:"document,omitempty"` + // Link is set if the media is an HTTP link. Currently, only valid for + // poll options. + // + // optional + Link *Link `json:"link,omitempty"` // LivePhoto is set if the media is a live photo. // // optional @@ -2866,6 +2885,13 @@ type ChatJoinRequest struct { // // optional InviteLink *ChatInviteLink `json:"invite_link,omitempty"` + // QueryID is the identifier of the join request query; for bots assigned + // to process join requests only. If present, then the bot must call + // sendChatJoinRequestWebApp or directly call answerChatJoinRequestQuery + // within 10 seconds. + // + // optional + QueryID string `json:"query_id,omitempty"` } // ChatPermissions describes actions that a non-administrator user is @@ -3314,8 +3340,9 @@ type InputPollOption struct { TextEntities []MessageEntity `json:"text_entities,omitempty"` // Media is the optional media attached to the poll option. The value // must be one of the InputMedia* variants accepted by InputPollOption - // (InputMediaAnimation, InputMediaLivePhoto, InputMediaLocation, - // InputMediaPhoto, InputMediaSticker, InputMediaVenue, InputMediaVideo). + // (InputMediaAnimation, InputMediaLink, InputMediaLivePhoto, + // InputMediaLocation, InputMediaPhoto, InputMediaSticker, + // InputMediaVenue, InputMediaVideo). // // optional Media any `json:"media,omitempty"` @@ -6975,3 +7002,515 @@ type PreCheckoutQuery struct { // optional OrderInfo *OrderInfo `json:"order_info,omitempty"` } + +// =========================================================================== +// Bot API 10.1 — Links and Rich Messages +// =========================================================================== + +// Link represents an HTTP link. +type Link struct { + // URL of the link. + URL string `json:"url"` +} + +// InputMediaLinkType is the discriminator value for InputMediaLink. +const InputMediaLinkType = "link" + +// InputMediaLink represents an HTTP link to be sent. It is one of the +// InputMedia* variants accepted by InputPollOption.Media. +type InputMediaLink struct { + // Type of the media, must be "link". + Type string `json:"type"` + // URL is the HTTP URL of the link. + URL string `json:"url"` +} + +// InputRichMessage describes a rich message to be sent. Exactly one of HTML +// or Markdown must be set. See the Telegram "rich message formatting options" +// documentation for the supported markup. +type InputRichMessage struct { + // HTML is the content of the rich message described using HTML + // formatting. Exactly one of HTML or Markdown must be set. + // + // optional + HTML string `json:"html,omitempty"` + // Markdown is the content of the rich message described using Markdown + // formatting. Exactly one of HTML or Markdown must be set. + // + // optional + Markdown string `json:"markdown,omitempty"` + // IsRTL, if true, requests that the rich message be shown right-to-left. + // + // optional + IsRTL bool `json:"is_rtl,omitempty"` + // SkipEntityDetection, if true, skips automatic detection of entities + // (URLs, email addresses, username mentions, hashtags, cashtags, bot + // commands, or phone numbers) in the text. + // + // optional + SkipEntityDetection bool `json:"skip_entity_detection,omitempty"` +} + +// InputRichMessageContent represents the content of a rich message to be sent +// as the result of an inline query. +type InputRichMessageContent struct { + // RichMessage is the message to be sent. + RichMessage InputRichMessage `json:"rich_message"` +} + +// RichMessage represents a received rich formatted message, exposed via +// Message.RichMessage. +type RichMessage struct { + // Blocks is the content of the message. + Blocks []RichBlock `json:"blocks"` + // IsRTL is true if the rich message must be shown right-to-left. + // + // optional + IsRTL bool `json:"is_rtl,omitempty"` +} + +// RichText styled-span (object form) discriminators. +const ( + RichTextTypeBold = "bold" + RichTextTypeItalic = "italic" + RichTextTypeUnderline = "underline" + RichTextTypeStrikethrough = "strikethrough" + RichTextTypeSpoiler = "spoiler" + RichTextTypeDateTime = "date_time" + RichTextTypeTextMention = "text_mention" + RichTextTypeSubscript = "subscript" + RichTextTypeSuperscript = "superscript" + RichTextTypeMarked = "marked" + RichTextTypeCode = "code" + RichTextTypeCustomEmoji = "custom_emoji" + RichTextTypeMathematicalExpression = "mathematical_expression" + RichTextTypeURL = "url" + RichTextTypeEmailAddress = "email_address" + RichTextTypePhoneNumber = "phone_number" + RichTextTypeBankCardNumber = "bank_card_number" + RichTextTypeMention = "mention" + RichTextTypeHashtag = "hashtag" + RichTextTypeCashtag = "cashtag" + RichTextTypeBotCommand = "bot_command" + RichTextTypeAnchor = "anchor" + RichTextTypeAnchorLink = "anchor_link" + RichTextTypeReference = "reference" + RichTextTypeReferenceLink = "reference_link" +) + +// RichText represents a rich formatted text. It is polymorphic: on the wire it +// is one of +// - a plain string (PlainText, with IsPlain set true), +// - an array of RichText (Parts), or +// - a styled span identified by Type, with nested Text and type-specific +// fields. +// +// Use the field that matches the form of the value; RichText marshals back to +// whichever form is populated. +type RichText struct { + // PlainText holds the value when the rich text is a bare string. + // + // optional + PlainText string `json:"-"` + // IsPlain reports whether the value was a bare string, so that an empty + // PlainText can be distinguished from an unset value. + // + // optional + IsPlain bool `json:"-"` + // Parts holds the value when the rich text is an array of RichText. + // + // optional + Parts []RichText `json:"-"` + + // Type is the styled-span discriminator (one of the RichTextType* + // constants) when the value is an object; empty for the string and array + // forms. + // + // optional + Type string `json:"type,omitempty"` + // Text is the nested content of the styled span. Present for most span + // types; absent for "anchor". + // + // optional + Text *RichText `json:"text,omitempty"` + // UnixTime is the Unix time associated with a "date_time" span. + // + // optional + UnixTime int64 `json:"unix_time,omitempty"` + // DateTimeFormat defines the formatting of a "date_time" span. + // + // optional + DateTimeFormat string `json:"date_time_format,omitempty"` + // User is the mentioned user of a "text_mention" span. + // + // optional + User *User `json:"user,omitempty"` + // CustomEmojiID is the identifier of the custom emoji of a "custom_emoji" + // span. + // + // optional + CustomEmojiID string `json:"custom_emoji_id,omitempty"` + // AlternativeText is the fallback emoji of a "custom_emoji" span. + // + // optional + AlternativeText string `json:"alternative_text,omitempty"` + // Expression is the LaTeX expression of a "mathematical_expression" span. + // + // optional + Expression string `json:"expression,omitempty"` + // URL is the link target of a "url" span. + // + // optional + URL string `json:"url,omitempty"` + // EmailAddress is the email address of an "email_address" span. + // + // optional + EmailAddress string `json:"email_address,omitempty"` + // PhoneNumber is the phone number of a "phone_number" span. + // + // optional + PhoneNumber string `json:"phone_number,omitempty"` + // BankCardNumber is the bank card number of a "bank_card_number" span. + // + // optional + BankCardNumber string `json:"bank_card_number,omitempty"` + // Username is the username of a "mention" span. + // + // optional + Username string `json:"username,omitempty"` + // Hashtag is the hashtag of a "hashtag" span. + // + // optional + Hashtag string `json:"hashtag,omitempty"` + // Cashtag is the cashtag of a "cashtag" span. + // + // optional + Cashtag string `json:"cashtag,omitempty"` + // BotCommand is the bot command of a "bot_command" span. + // + // optional + BotCommand string `json:"bot_command,omitempty"` + // Name is the anchor name of an "anchor" span or the reference name of a + // "reference" span. + // + // optional + Name string `json:"name,omitempty"` + // AnchorName is the target anchor name of an "anchor_link" span. If empty, + // the link points back to the top of the message. + // + // optional + AnchorName string `json:"anchor_name,omitempty"` + // ReferenceName is the target reference name of a "reference_link" span. + // + // optional + ReferenceName string `json:"reference_name,omitempty"` + + // Raw preserves the original JSON of the object form for forward + // compatibility with span types not yet modeled. + // + // optional + Raw json.RawMessage `json:"-"` +} + +// UnmarshalJSON decodes a RichText from its string, array, or object form. +func (t *RichText) UnmarshalJSON(data []byte) error { + *t = RichText{} + + i := 0 + for i < len(data) && (data[i] == ' ' || data[i] == '\t' || data[i] == '\n' || data[i] == '\r') { + i++ + } + if i >= len(data) { + return nil + } + + switch data[i] { + case 'n': // null + return nil + case '"': + t.IsPlain = true + return json.Unmarshal(data, &t.PlainText) + case '[': + return json.Unmarshal(data, &t.Parts) + default: + type span RichText + if err := json.Unmarshal(data, (*span)(t)); err != nil { + return err + } + t.Raw = append(t.Raw[:0], data...) + return nil + } +} + +// MarshalJSON encodes a RichText in whichever form is populated. +func (t RichText) MarshalJSON() ([]byte, error) { + switch { + case t.IsPlain: + return json.Marshal(t.PlainText) + case t.Parts != nil: + return json.Marshal(t.Parts) + case t.Type != "": + type span RichText + return json.Marshal(span(t)) + case len(t.Raw) > 0: + return t.Raw, nil + default: + return []byte("null"), nil + } +} + +// RichBlock block-type discriminators. +const ( + RichBlockTypeParagraph = "paragraph" + RichBlockTypeHeading = "heading" + RichBlockTypePre = "pre" + RichBlockTypeFooter = "footer" + RichBlockTypeDivider = "divider" + RichBlockTypeMathematicalExpression = "mathematical_expression" + RichBlockTypeAnchor = "anchor" + RichBlockTypeList = "list" + RichBlockTypeBlockquote = "blockquote" + RichBlockTypePullquote = "pullquote" + RichBlockTypeCollage = "collage" + RichBlockTypeSlideshow = "slideshow" + RichBlockTypeTable = "table" + RichBlockTypeDetails = "details" + RichBlockTypeMap = "map" + RichBlockTypeAnimation = "animation" + RichBlockTypeAudio = "audio" + RichBlockTypePhoto = "photo" + RichBlockTypeVideo = "video" + RichBlockTypeVoiceNote = "voice_note" + RichBlockTypeThinking = "thinking" +) + +// RichBlock represents a block in a rich formatted message. It is a flat +// polymorphic type keyed by Type; only the fields relevant to a given Type are +// set. The "caption" wire field is split into Caption (for media blocks) and +// TableCaption (for the "table" block), which have different shapes; at most +// one is ever set. +type RichBlock struct { + // Type of the block, one of the RichBlockType* constants. + Type string `json:"type"` + // Text is the block text. Set for "paragraph", "heading", "pre", + // "footer", "pullquote", and "thinking" blocks. + // + // optional + Text *RichText `json:"text,omitempty"` + // Size is the relative font size of a "heading" block; 1-6, 1 is largest. + // + // optional + Size int `json:"size,omitempty"` + // Language is the programming language of a "pre" block. + // + // optional + Language string `json:"language,omitempty"` + // Expression is the LaTeX expression of a "mathematical_expression" + // block. + // + // optional + Expression string `json:"expression,omitempty"` + // Name is the anchor name of an "anchor" block. + // + // optional + Name string `json:"name,omitempty"` + // Items are the items of a "list" block. + // + // optional + Items []RichBlockListItem `json:"items,omitempty"` + // Blocks is the nested content of "blockquote", "collage", "slideshow", + // and "details" blocks. + // + // optional + Blocks []RichBlock `json:"blocks,omitempty"` + // Credit is the credit of "blockquote" and "pullquote" blocks. + // + // optional + Credit *RichText `json:"credit,omitempty"` + // Cells are the rows of cells of a "table" block. + // + // optional + Cells [][]RichBlockTableCell `json:"cells,omitempty"` + // IsBordered is true if a "table" block has borders. + // + // optional + IsBordered bool `json:"is_bordered,omitempty"` + // IsStriped is true if a "table" block is striped. + // + // optional + IsStriped bool `json:"is_striped,omitempty"` + // Summary is the always-shown summary of a "details" block. + // + // optional + Summary *RichText `json:"summary,omitempty"` + // IsOpen is true if a "details" block is visible by default. + // + // optional + IsOpen bool `json:"is_open,omitempty"` + // Location is the center of a "map" block. + // + // optional + Location *Location `json:"location,omitempty"` + // Zoom is the zoom level of a "map" block; 13-20. + // + // optional + Zoom int `json:"zoom,omitempty"` + // Width is the expected width of a "map" block. + // + // optional + Width int `json:"width,omitempty"` + // Height is the expected height of a "map" block. + // + // optional + Height int `json:"height,omitempty"` + // Animation is the animation of an "animation" block. + // + // optional + Animation *Animation `json:"animation,omitempty"` + // HasSpoiler is true if the preview of an "animation", "photo", or + // "video" block is covered by a spoiler. + // + // optional + HasSpoiler bool `json:"has_spoiler,omitempty"` + // Audio is the audio of an "audio" block. + // + // optional + Audio *Audio `json:"audio,omitempty"` + // Photo are the available sizes of a "photo" block. + // + // optional + Photo []PhotoSize `json:"photo,omitempty"` + // Video is the video of a "video" block. + // + // optional + Video *Video `json:"video,omitempty"` + // VoiceNote is the voice note of a "voice_note" block. + // + // optional + VoiceNote *Voice `json:"voice_note,omitempty"` + // Caption is the caption of a media block ("collage", "slideshow", "map", + // "animation", "audio", "photo", "video", "voice_note"). It shares the + // "caption" wire field with TableCaption. + // + // optional + Caption *RichBlockCaption `json:"-"` + // TableCaption is the caption of a "table" block. It shares the "caption" + // wire field with Caption. + // + // optional + TableCaption *RichText `json:"-"` +} + +// UnmarshalJSON decodes a RichBlock, routing the polymorphic "caption" field +// to Caption or TableCaption based on Type. +func (b *RichBlock) UnmarshalJSON(data []byte) error { + type alias RichBlock + aux := struct { + *alias + Caption json.RawMessage `json:"caption,omitempty"` + }{alias: (*alias)(b)} + + if err := json.Unmarshal(data, &aux); err != nil { + return err + } + + if len(aux.Caption) == 0 || string(aux.Caption) == "null" { + return nil + } + + if b.Type == RichBlockTypeTable { + b.TableCaption = new(RichText) + return json.Unmarshal(aux.Caption, b.TableCaption) + } + + b.Caption = new(RichBlockCaption) + return json.Unmarshal(aux.Caption, b.Caption) +} + +// MarshalJSON encodes a RichBlock, emitting Caption or TableCaption under the +// shared "caption" wire field. +func (b RichBlock) MarshalJSON() ([]byte, error) { + type alias RichBlock + aux := struct { + alias + Caption json.RawMessage `json:"caption,omitempty"` + }{alias: alias(b)} + + switch { + case b.TableCaption != nil: + raw, err := json.Marshal(b.TableCaption) + if err != nil { + return nil, err + } + aux.Caption = raw + case b.Caption != nil: + raw, err := json.Marshal(b.Caption) + if err != nil { + return nil, err + } + aux.Caption = raw + } + + return json.Marshal(aux) +} + +// RichBlockCaption is the caption of a rich formatted block. +type RichBlockCaption struct { + // Text is the block caption. + Text RichText `json:"text"` + // Credit is the block credit, corresponding to the HTML tag . + // + // optional + Credit *RichText `json:"credit,omitempty"` +} + +// RichBlockTableCell is a cell in a rich formatted table. +type RichBlockTableCell struct { + // Text in the cell. If omitted, the cell is invisible. + // + // optional + Text *RichText `json:"text,omitempty"` + // IsHeader is true if the cell is a header cell. + // + // optional + IsHeader bool `json:"is_header,omitempty"` + // Colspan is the number of columns the cell spans if it is bigger than 1. + // + // optional + Colspan int `json:"colspan,omitempty"` + // Rowspan is the number of rows the cell spans if it is bigger than 1. + // + // optional + Rowspan int `json:"rowspan,omitempty"` + // Align is the horizontal cell content alignment; one of "left", + // "center", or "right". + Align string `json:"align"` + // Valign is the vertical cell content alignment; one of "top", "middle", + // or "bottom". + Valign string `json:"valign"` +} + +// RichBlockListItem is an item of a rich formatted list. +type RichBlockListItem struct { + // Label of the item. + Label string `json:"label"` + // Blocks is the content of the item. + Blocks []RichBlock `json:"blocks"` + // HasCheckbox is true if the item has a checkbox. + // + // optional + HasCheckbox bool `json:"has_checkbox,omitempty"` + // IsChecked is true if the item has a checked checkbox. + // + // optional + IsChecked bool `json:"is_checked,omitempty"` + // Value is, for ordered lists, the numeric value of the item label. + // + // optional + Value int `json:"value,omitempty"` + // Type is, for ordered lists, the type of the item label; one of "a", "A", + // "i", "I", or "1". + // + // optional + Type string `json:"type,omitempty"` +} From fe09e7e2faa80f0ffc726173ba85a9372d3f637e Mon Sep 17 00:00:00 2001 From: Kirill Date: Mon, 20 Jul 2026 04:00:31 +0400 Subject: [PATCH 58/65] Full support of API 10.2 --- README.md | 18 ++- configs.go | 266 ++++++++++++++++++++++++++++++++++++ ephemeral_test.go | 293 +++++++++++++++++++++++++++++++++++++++ helpers.go | 78 ++++++++++- types.go | 339 ++++++++++++++++++++++++++++++++++++++++++++-- 5 files changed, 981 insertions(+), 13 deletions(-) create mode 100644 ephemeral_test.go diff --git a/README.md b/README.md index abb57e14..e2f298e4 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ > **This is a maintained fork of > [`go-telegram-bot-api/telegram-bot-api`](https://github.com/go-telegram-bot-api/telegram-bot-api).** > The upstream project stopped updating at Bot API 6.0 (April 2022). This fork -> is brought up to **Bot API 10.1** (the latest release as of this writing) and +> is brought up to **Bot API 10.2** (the latest release as of this writing) and > tracks the [official changelog](https://core.telegram.org/bots/api-changelog) > going forward. @@ -39,8 +39,8 @@ update your imports, or keep using upstream on 6.0. If you're coming from `go-telegram-bot-api/telegram-bot-api` v5.5.x and want to jump to the latest Bot API, read [**BREAKING.md**](./BREAKING.md). It lists every source-incompatible change introduced while bringing the fork from 6.0 -to 10.0, grouped by topic, with before/after snippets for each one (10.1 is -purely additive and introduced no breaking changes). The fastest +to 10.0, grouped by topic, with before/after snippets for each one (10.1 and +10.2 are purely additive and introduced no breaking changes). The fastest way to migrate is `grep` your codebase for the old identifier and apply the rewrite. @@ -166,7 +166,7 @@ to generate your free TLS certificate there. ### Bot API versions -Full support for every Bot API version from **6.1 through 10.1**. See git log +Full support for every Bot API version from **6.1 through 10.2**. See git log for the per-version commits; each `Full support of API X` commit message is the authoritative trail for what that version added. @@ -179,6 +179,16 @@ join request queries (`AnswerChatJoinRequestQueryConfig`, `ChatFullInfo.GuardBot`, `User.SupportsJoinRequestQueries`), and poll links (`Link`, `PollMedia.Link`, `InputMediaLink`). +The 10.2 surface adds block-structured rich messages (`InputRichBlock`, +`InputRichBlockListItem`, `InputRichMessage.Blocks`) and explicit media for +markdown/HTML rich messages (`InputRichMessageMedia`, `InputMediaVoiceNote`), +ephemeral messages (`EphemeralSendParams` on the send configs that support +them, `ReplyParameters.EphemeralMessageID`, the `EditEphemeralMessage*` and +`DeleteEphemeralMessage` configs, `Message.ReceiverUser`, +`BotCommand.IsEphemeral`), communities (`Community`, `CommunityChatAdded`, +`CommunityChatRemoved`, `ChatFullInfo.Community`), and payment subscription +updates (`BotSubscriptionUpdated`, `Update.Subscription`). + The 10.0 surface includes guest mode (`AnswerGuestQuery`, `Update.GuestMessage`, `User.SupportsGuestQueries`), live photos (`SendLivePhoto` via `LivePhotoConfig` + `NewLivePhoto`, `LivePhoto` / diff --git a/configs.go b/configs.go index b1efafa9..d2ee4006 100644 --- a/configs.go +++ b/configs.go @@ -130,6 +130,10 @@ const ( // UpdateTypeManagedBot is when a new bot was created to be managed by the bot, // or token or owner of a managed bot was changed. UpdateTypeManagedBot = "managed_bot" + + // UpdateTypeSubscription is when a user payment subscription toward the + // bot was changed. + UpdateTypeSubscription = "subscription" ) // Library errors @@ -337,6 +341,29 @@ func (chat *BaseChat) params() (Params, error) { return params, err } +// EphemeralSendParams holds the parameters that make an outgoing message +// ephemeral, i.e. visible only to a single recipient and the bot. It is +// embedded by the send configs whose methods support ephemeral messages. +// +// Leaving ReceiverUserID zero sends an ordinary, non-ephemeral message. +type EphemeralSendParams struct { + // ReceiverUserID is the unique identifier of the user who will receive + // the ephemeral message; for group and supergroup chats only. Delivery is + // not guaranteed, especially if the user is offline. + ReceiverUserID int64 + // CallbackQueryID is the identifier of the callback query which triggered + // the ephemeral message, if any. + CallbackQueryID string +} + +// addTo writes the ephemeral parameters into params. It is a named method +// rather than params() so that it does not collide with the params() promoted +// from BaseChat in configs that embed both. +func (e EphemeralSendParams) addTo(params Params) { + params.AddNonZero64("receiver_user_id", e.ReceiverUserID) + params.AddNonEmpty("callback_query_id", e.CallbackQueryID) +} + // BaseFile is a base type for all file config types. type BaseFile struct { BaseChat @@ -379,6 +406,7 @@ func (edit BaseEdit) params() (Params, error) { // MessageConfig contains information about a SendMessage request. type MessageConfig struct { BaseChat + EphemeralSendParams Text string ParseMode string Entities []MessageEntity @@ -398,6 +426,8 @@ func (config MessageConfig) params() (Params, error) { } err = params.AddAny("entities", config.Entities) + config.EphemeralSendParams.addTo(params) + return params, err } @@ -470,6 +500,7 @@ func (config CopyMessageConfig) method() string { // PhotoConfig contains information about a SendPhoto request. type PhotoConfig struct { BaseFile + EphemeralSendParams Thumbnail RequestFileData Caption string ParseMode string @@ -490,6 +521,8 @@ func (config PhotoConfig) params() (Params, error) { params.AddBool("has_spoiler", config.HasSpoiler) err = params.AddAny("caption_entities", config.CaptionEntities) + config.EphemeralSendParams.addTo(params) + return params, err } @@ -516,6 +549,7 @@ func (config PhotoConfig) files() []RequestFile { // AudioConfig contains information about a SendAudio request. type AudioConfig struct { BaseFile + EphemeralSendParams Thumbnail RequestFileData Caption string ParseMode string @@ -538,6 +572,8 @@ func (config AudioConfig) params() (Params, error) { params.AddNonEmpty("parse_mode", config.ParseMode) err = params.AddInterface("caption_entities", config.CaptionEntities) + config.EphemeralSendParams.addTo(params) + return params, err } @@ -564,6 +600,7 @@ func (config AudioConfig) files() []RequestFile { // DocumentConfig contains information about a SendDocument request. type DocumentConfig struct { BaseFile + EphemeralSendParams Thumbnail RequestFileData Caption string ParseMode string @@ -578,6 +615,8 @@ func (config DocumentConfig) params() (Params, error) { params.AddNonEmpty("parse_mode", config.ParseMode) params.AddBool("disable_content_type_detection", config.DisableContentTypeDetection) + config.EphemeralSendParams.addTo(params) + return params, err } @@ -604,6 +643,7 @@ func (config DocumentConfig) files() []RequestFile { // StickerConfig contains information about a SendSticker request. type StickerConfig struct { BaseFile + EphemeralSendParams // Emoji associated with the sticker; only for just uploaded stickers. Emoji string } @@ -616,6 +656,8 @@ func (config StickerConfig) params() (Params, error) { params.AddNonEmpty("emoji", config.Emoji) + config.EphemeralSendParams.addTo(params) + return params, nil } @@ -633,6 +675,7 @@ func (config StickerConfig) files() []RequestFile { // VideoConfig contains information about a SendVideo request. type VideoConfig struct { BaseFile + EphemeralSendParams Thumbnail RequestFileData Cover RequestFileData StartTimestamp int @@ -660,6 +703,8 @@ func (config VideoConfig) params() (Params, error) { params.AddBool("has_spoiler", config.HasSpoiler) err = params.AddAny("caption_entities", config.CaptionEntities) + config.EphemeralSendParams.addTo(params) + return params, err } @@ -697,6 +742,7 @@ func (config VideoConfig) files() []RequestFile { // currently supported. type LivePhotoConfig struct { BaseFile + EphemeralSendParams Photo RequestFileData Caption string ParseMode string @@ -717,6 +763,8 @@ func (config LivePhotoConfig) params() (Params, error) { params.AddBool("has_spoiler", config.HasSpoiler) err = params.AddAny("caption_entities", config.CaptionEntities) + config.EphemeralSendParams.addTo(params) + return params, err } @@ -743,6 +791,7 @@ func (config LivePhotoConfig) files() []RequestFile { // AnimationConfig contains information about a SendAnimation request. type AnimationConfig struct { BaseFile + EphemeralSendParams Duration int Thumbnail RequestFileData Caption string @@ -765,6 +814,8 @@ func (config AnimationConfig) params() (Params, error) { params.AddBool("has_spoiler", config.HasSpoiler) err = params.AddAny("caption_entities", config.CaptionEntities) + config.EphemeralSendParams.addTo(params) + return params, err } @@ -791,6 +842,7 @@ func (config AnimationConfig) files() []RequestFile { // VideoNoteConfig contains information about a SendVideoNote request. type VideoNoteConfig struct { BaseFile + EphemeralSendParams Thumbnail RequestFileData Duration int Length int @@ -802,6 +854,8 @@ func (config VideoNoteConfig) params() (Params, error) { params.AddNonZero("duration", config.Duration) params.AddNonZero("length", config.Length) + config.EphemeralSendParams.addTo(params) + return params, err } @@ -828,6 +882,7 @@ func (config VideoNoteConfig) files() []RequestFile { // VoiceConfig contains information about a SendVoice request. type VoiceConfig struct { BaseFile + EphemeralSendParams Thumbnail RequestFileData Caption string ParseMode string @@ -846,6 +901,8 @@ func (config VoiceConfig) params() (Params, error) { params.AddNonEmpty("parse_mode", config.ParseMode) err = params.AddInterface("caption_entities", config.CaptionEntities) + config.EphemeralSendParams.addTo(params) + return params, err } @@ -872,6 +929,7 @@ func (config VoiceConfig) files() []RequestFile { // LocationConfig contains information about a SendLocation request. type LocationConfig struct { BaseChat + EphemeralSendParams Latitude float64 // required Longitude float64 // required HorizontalAccuracy float64 // optional @@ -890,6 +948,8 @@ func (config LocationConfig) params() (Params, error) { params.AddNonZero("heading", config.Heading) params.AddNonZero("proximity_alert_radius", config.ProximityAlertRadius) + config.EphemeralSendParams.addTo(params) + return params, err } @@ -941,6 +1001,7 @@ func (config StopMessageLiveLocationConfig) method() string { // VenueConfig contains information about a SendVenue request. type VenueConfig struct { BaseChat + EphemeralSendParams Latitude float64 // required Longitude float64 // required Title string // required @@ -963,6 +1024,8 @@ func (config VenueConfig) params() (Params, error) { params.AddNonEmpty("google_place_id", config.GooglePlaceID) params.AddNonEmpty("google_place_type", config.GooglePlaceType) + config.EphemeralSendParams.addTo(params) + return params, err } @@ -973,6 +1036,7 @@ func (config VenueConfig) method() string { // ContactConfig allows you to send a contact. type ContactConfig struct { BaseChat + EphemeralSendParams PhoneNumber string FirstName string LastName string @@ -988,6 +1052,8 @@ func (config ContactConfig) params() (Params, error) { params.AddNonEmpty("last_name", config.LastName) params.AddNonEmpty("vcard", config.VCard) + config.EphemeralSendParams.addTo(params) + return params, err } @@ -1290,6 +1356,182 @@ func (config EditMessageReplyMarkupConfig) method() string { return "editMessageReplyMarkup" } +// BaseEphemeralEdit is the base type for edits and deletions of ephemeral +// messages. Ephemeral messages are addressed by the chat they were sent to, +// the user who received them, and their per-chat ephemeral identifier, so they +// do not use BaseEdit. +// +// Either ChatID or ChannelUsername must be set; ChannelUsername targets a +// supergroup in the @username format. +type BaseEphemeralEdit struct { + // ChatID is the unique identifier for the target chat. + ChatID int64 + // ChannelUsername is the username of the target supergroup, in the + // @username format. Used when ChatID is not set. + ChannelUsername string + // ReceiverUserID is the identifier of the user who received the message. + ReceiverUserID int64 + // EphemeralMessageID is the identifier of the ephemeral message. + EphemeralMessageID int +} + +func (edit BaseEphemeralEdit) params() (Params, error) { + params := make(Params) + + if err := params.AddFirstValid("chat_id", edit.ChatID, edit.ChannelUsername); err != nil { + return params, err + } + params.AddNonZero64("receiver_user_id", edit.ReceiverUserID) + params.AddNonZero("ephemeral_message_id", edit.EphemeralMessageID) + + return params, nil +} + +// EditEphemeralMessageTextConfig allows you to edit the text of an ephemeral +// message. Delivery of the edit is not guaranteed, especially if the user is +// offline. +type EditEphemeralMessageTextConfig struct { + BaseEphemeralEdit + // Text is the new text of the message, 1-4096 characters after entity + // parsing. + Text string + // ParseMode is the mode for parsing entities in the message text. + ParseMode string + // Entities is a list of special entities that appear in the message text, + // which can be specified instead of ParseMode. + Entities []MessageEntity + // LinkPreviewOptions are the link preview generation options. + LinkPreviewOptions *LinkPreviewOptions + // ReplyMarkup is the new inline keyboard for the message. + ReplyMarkup *InlineKeyboardMarkup +} + +func (config EditEphemeralMessageTextConfig) params() (Params, error) { + params, err := config.BaseEphemeralEdit.params() + if err != nil { + return params, err + } + + params["text"] = config.Text + params.AddNonEmpty("parse_mode", config.ParseMode) + if err = params.AddAny("entities", config.Entities); err != nil { + return params, err + } + if err = params.AddAny("link_preview_options", config.LinkPreviewOptions); err != nil { + return params, err + } + err = params.AddAny("reply_markup", config.ReplyMarkup) + + return params, err +} + +func (config EditEphemeralMessageTextConfig) method() string { + return "editEphemeralMessageText" +} + +// EditEphemeralMessageMediaConfig allows you to edit the media of an ephemeral +// message. A new file can't be uploaded; use a previously uploaded file via +// its file_id, or specify a URL. +type EditEphemeralMessageMediaConfig struct { + BaseEphemeralEdit + // Media is the new media content of the message. + Media interface{} + // ReplyMarkup is the new inline keyboard for the message. + ReplyMarkup *InlineKeyboardMarkup +} + +func (config EditEphemeralMessageMediaConfig) params() (Params, error) { + params, err := config.BaseEphemeralEdit.params() + if err != nil { + return params, err + } + + if err = params.AddAny("media", prepareInputMediaParam(config.Media, 0)); err != nil { + return params, err + } + err = params.AddAny("reply_markup", config.ReplyMarkup) + + return params, err +} + +func (config EditEphemeralMessageMediaConfig) method() string { + return "editEphemeralMessageMedia" +} + +// EditEphemeralMessageCaptionConfig allows you to edit the caption of an +// ephemeral message. +type EditEphemeralMessageCaptionConfig struct { + BaseEphemeralEdit + // Caption is the new caption of the message, 0-1024 characters after + // entities parsing. + Caption string + // ParseMode is the mode for parsing entities in the message caption. + ParseMode string + // CaptionEntities is a list of special entities that appear in the + // caption, which can be specified instead of ParseMode. + CaptionEntities []MessageEntity + // ReplyMarkup is the new inline keyboard for the message. + ReplyMarkup *InlineKeyboardMarkup +} + +func (config EditEphemeralMessageCaptionConfig) params() (Params, error) { + params, err := config.BaseEphemeralEdit.params() + if err != nil { + return params, err + } + + params.AddNonEmpty("caption", config.Caption) + params.AddNonEmpty("parse_mode", config.ParseMode) + if err = params.AddAny("caption_entities", config.CaptionEntities); err != nil { + return params, err + } + err = params.AddAny("reply_markup", config.ReplyMarkup) + + return params, err +} + +func (config EditEphemeralMessageCaptionConfig) method() string { + return "editEphemeralMessageCaption" +} + +// EditEphemeralMessageReplyMarkupConfig allows you to edit only the reply +// markup of an ephemeral message. +type EditEphemeralMessageReplyMarkupConfig struct { + BaseEphemeralEdit + // ReplyMarkup is the new inline keyboard for the message. + ReplyMarkup *InlineKeyboardMarkup +} + +func (config EditEphemeralMessageReplyMarkupConfig) params() (Params, error) { + params, err := config.BaseEphemeralEdit.params() + if err != nil { + return params, err + } + + err = params.AddAny("reply_markup", config.ReplyMarkup) + + return params, err +} + +func (config EditEphemeralMessageReplyMarkupConfig) method() string { + return "editEphemeralMessageReplyMarkup" +} + +// DeleteEphemeralMessageConfig allows you to delete an ephemeral message. +// Delivery of the deletion is not guaranteed, especially if the user is +// offline. +type DeleteEphemeralMessageConfig struct { + BaseEphemeralEdit +} + +func (config DeleteEphemeralMessageConfig) params() (Params, error) { + return config.BaseEphemeralEdit.params() +} + +func (config DeleteEphemeralMessageConfig) method() string { + return "deleteEphemeralMessage" +} + // StopPollConfig allows you to stop a poll sent by the bot. type StopPollConfig struct { BaseEdit @@ -5003,6 +5245,16 @@ func prepareInputMediaParam(inputMedia interface{}, idx int) interface{} { m.Cover = fileAttach(fmt.Sprintf("attach://file-%d-cover", idx)) } + return m + case InputMediaAnimation: + if m.Media.NeedsUpload() { + m.Media = fileAttach(fmt.Sprintf("attach://file-%d", idx)) + } + + if m.Thumbnail != nil && m.Thumbnail.NeedsUpload() { + m.Thumbnail = fileAttach(fmt.Sprintf("attach://file-%d-thumbnail", idx)) + } + return m case InputMediaAudio: if m.Media.NeedsUpload() { @@ -5106,6 +5358,20 @@ func prepareInputMediaFile(inputMedia interface{}, idx int) []RequestFile { Data: m.Photo, }) } + case InputMediaAnimation: + if m.Media.NeedsUpload() { + files = append(files, RequestFile{ + Name: fmt.Sprintf("file-%d", idx), + Data: m.Media, + }) + } + + if m.Thumbnail != nil && m.Thumbnail.NeedsUpload() { + files = append(files, RequestFile{ + Name: fmt.Sprintf("file-%d-thumbnail", idx), + Data: m.Thumbnail, + }) + } case InputMediaAudio: if m.Media.NeedsUpload() { files = append(files, RequestFile{ diff --git a/ephemeral_test.go b/ephemeral_test.go new file mode 100644 index 00000000..3bd12c42 --- /dev/null +++ b/ephemeral_test.go @@ -0,0 +1,293 @@ +package tgbotapi + +import ( + "encoding/json" + "reflect" + "testing" +) + +// TestInputRichBlockCaptionRouting verifies that the shared "caption" wire +// field is routed to TableCaption for a table block and to Caption for a media +// block, mirroring RichBlock. +func TestInputRichBlockCaptionRouting(t *testing.T) { + var table InputRichBlock + if err := json.Unmarshal([]byte(`{"type":"table","cells":[],"caption":"a table"}`), &table); err != nil { + t.Fatalf("table unmarshal: %v", err) + } + if table.TableCaption == nil || table.TableCaption.PlainText != "a table" { + t.Fatalf("table caption not routed to TableCaption: %+v", table) + } + if table.Caption != nil { + t.Fatalf("table caption also set Caption: %+v", table.Caption) + } + + var photo InputRichBlock + if err := json.Unmarshal([]byte(`{"type":"photo","caption":{"text":"a photo"}}`), &photo); err != nil { + t.Fatalf("photo unmarshal: %v", err) + } + if photo.Caption == nil || !photo.Caption.Text.IsPlain || photo.Caption.Text.PlainText != "a photo" { + t.Fatalf("photo caption not routed to Caption: %+v", photo) + } + if photo.TableCaption != nil { + t.Fatalf("photo caption also set TableCaption: %+v", photo.TableCaption) + } +} + +// TestInputRichBlockRoundTrip verifies that the block variants survive a +// decode/encode/decode cycle unchanged. +func TestInputRichBlockRoundTrip(t *testing.T) { + cases := []string{ + `{"type":"paragraph","text":"hello"}`, + `{"type":"heading","text":"title","size":2}`, + `{"type":"pre","text":"x := 1","language":"go"}`, + `{"type":"divider"}`, + `{"type":"mathematical_expression","expression":"e^{i\\pi}+1=0"}`, + `{"type":"anchor","name":"intro"}`, + `{"type":"list","items":[{"blocks":[{"type":"paragraph","text":"a"}],"value":1,"type":"1"}]}`, + `{"type":"blockquote","blocks":[{"type":"paragraph","text":"q"}],"credit":"someone"}`, + `{"type":"details","summary":"more","blocks":[{"type":"paragraph","text":"d"}],"is_open":true}`, + `{"type":"table","cells":[[{"text":"c","align":"left","valign":"top"}]],"is_bordered":true,"caption":"cap"}`, + `{"type":"map","location":{"latitude":1.5,"longitude":2.5},"zoom":10,"width":600,"height":400}`, + `{"type":"thinking","text":"working"}`, + } + + for _, in := range cases { + var first InputRichBlock + if err := json.Unmarshal([]byte(in), &first); err != nil { + t.Fatalf("unmarshal %s: %v", in, err) + } + out, err := json.Marshal(first) + if err != nil { + t.Fatalf("marshal %s: %v", in, err) + } + var second InputRichBlock + if err := json.Unmarshal(out, &second); err != nil { + t.Fatalf("re-unmarshal %s: %v", out, err) + } + if !reflect.DeepEqual(first, second) { + t.Errorf("round trip mismatch:\n in: %s\n out: %s", in, out) + } + } +} + +// TestInputRichBlockMediaMarshal verifies the encoding of the media-bearing +// block variants. These are marshal-only: the embedded InputMedia* types hold +// the file as a RequestFileData interface, which cannot be decoded, so they +// are not part of the round-trip test above. +func TestInputRichBlockMediaMarshal(t *testing.T) { + photo := NewInputMediaPhoto(FileID("file_id")) + block := InputRichBlock{ + Type: RichBlockTypePhoto, + Photo: &photo, + Caption: &RichBlockCaption{Text: RichText{IsPlain: true, PlainText: "pic"}}, + } + out, err := json.Marshal(block) + if err != nil { + t.Fatalf("marshal photo block: %v", err) + } + want := `{"type":"photo","photo":{"type":"photo","media":"file_id"},"caption":{"text":"pic"}}` + if string(out) != want { + t.Errorf("photo block encoding:\n got: %s\n want: %s", out, want) + } + + voice := NewInputMediaVoiceNote(FileID("file_id")) + voice.Duration = 5 + out, err = json.Marshal(InputRichBlock{Type: RichBlockTypeVoiceNote, VoiceNote: &voice}) + if err != nil { + t.Fatalf("marshal voice note block: %v", err) + } + want = `{"type":"voice_note","voice_note":{"type":"voice_note","media":"file_id","duration":5}}` + if string(out) != want { + t.Errorf("voice note block encoding:\n got: %s\n want: %s", out, want) + } +} + +// TestInputRichMessageBlocks verifies that a block-formatted rich message and +// a markdown rich message with explicit media both encode as expected. +func TestInputRichMessageBlocks(t *testing.T) { + msg := InputRichMessage{ + Blocks: []InputRichBlock{ + {Type: RichBlockTypeParagraph, Text: &RichText{IsPlain: true, PlainText: "hi"}}, + }, + } + out, err := json.Marshal(msg) + if err != nil { + t.Fatalf("marshal blocks: %v", err) + } + if got, want := string(out), `{"blocks":[{"type":"paragraph","text":"hi"}]}`; got != want { + t.Errorf("blocks encoding:\n got: %s\n want: %s", got, want) + } + + withMedia := InputRichMessage{ + Markdown: "![pic](tg://photo?id=p1)", + Media: []InputRichMessageMedia{ + {ID: "p1", Media: NewInputMediaPhoto(FileID("file_id"))}, + }, + } + out, err = json.Marshal(withMedia) + if err != nil { + t.Fatalf("marshal media: %v", err) + } + want := `{"markdown":"![pic](tg://photo?id=p1)","media":[{"id":"p1","media":{"type":"photo","media":"file_id"}}]}` + if got := string(out); got != want { + t.Errorf("media encoding:\n got: %s\n want: %s", got, want) + } +} + +// TestEphemeralSendParams verifies that the ephemeral parameters are only +// emitted when set, and that they reach the wire for a send config. +func TestEphemeralSendParams(t *testing.T) { + plain := NewMessage(12345, "hello") + params, err := plain.params() + if err != nil { + t.Fatalf("plain params: %v", err) + } + if _, ok := params["receiver_user_id"]; ok { + t.Error("receiver_user_id emitted for a non-ephemeral message") + } + if _, ok := params["callback_query_id"]; ok { + t.Error("callback_query_id emitted for a non-ephemeral message") + } + + ephemeral := NewMessage(12345, "hello") + ephemeral.ReceiverUserID = 777 + ephemeral.CallbackQueryID = "cbq" + params, err = ephemeral.params() + if err != nil { + t.Fatalf("ephemeral params: %v", err) + } + if params["receiver_user_id"] != "777" { + t.Errorf("receiver_user_id = %q, want 777", params["receiver_user_id"]) + } + if params["callback_query_id"] != "cbq" { + t.Errorf("callback_query_id = %q, want cbq", params["callback_query_id"]) + } +} + +// TestEphemeralEditConfigs verifies the params and methods of the ephemeral +// edit and delete configs. +func TestEphemeralEditConfigs(t *testing.T) { + edit := NewEditEphemeralMessageText(12345, 777, 42, "updated") + params, err := edit.params() + if err != nil { + t.Fatalf("edit params: %v", err) + } + for key, want := range map[string]string{ + "chat_id": "12345", + "receiver_user_id": "777", + "ephemeral_message_id": "42", + "text": "updated", + } { + if params[key] != want { + t.Errorf("%s = %q, want %q", key, params[key], want) + } + } + if edit.method() != "editEphemeralMessageText" { + t.Errorf("method = %q", edit.method()) + } + + del := NewDeleteEphemeralMessage(12345, 777, 42) + params, err = del.params() + if err != nil { + t.Fatalf("delete params: %v", err) + } + if _, ok := params["reply_markup"]; ok { + t.Error("deleteEphemeralMessage must not send reply_markup") + } + if del.method() != "deleteEphemeralMessage" { + t.Errorf("method = %q", del.method()) + } +} + +// TestReplyParametersEphemeral verifies that ReplyParameters can address an +// ephemeral message and that message_id is omitted when unset. +func TestReplyParametersEphemeral(t *testing.T) { + out, err := json.Marshal(ReplyParameters{EphemeralMessageID: 42}) + if err != nil { + t.Fatalf("marshal: %v", err) + } + if got, want := string(out), `{"ephemeral_message_id":42}`; got != want { + t.Errorf("encoding:\n got: %s\n want: %s", got, want) + } +} + +// TestPrepareInputMediaAnimation verifies that InputMediaAnimation is handled +// by the input media preparation helpers. It was previously missing from both +// switches, so animations were silently dropped from the request. +func TestPrepareInputMediaAnimation(t *testing.T) { + byID := NewInputMediaAnimation(FileID("file_id")) + if param := prepareInputMediaParam(byID, 0); param == nil { + t.Fatal("prepareInputMediaParam dropped InputMediaAnimation") + } + + upload := NewInputMediaAnimation(FilePath("/tmp/anim.gif")) + upload.Thumbnail = FilePath("/tmp/thumb.jpg") + + param := prepareInputMediaParam(upload, 3) + animation, ok := param.(InputMediaAnimation) + if !ok { + t.Fatalf("prepareInputMediaParam returned %T, want InputMediaAnimation", param) + } + if got := animation.Media.SendData(); got != "attach://file-3" { + t.Errorf("media = %q, want attach://file-3", got) + } + if got := animation.Thumbnail.SendData(); got != "attach://file-3-thumbnail" { + t.Errorf("thumbnail = %q, want attach://file-3-thumbnail", got) + } + + files := prepareInputMediaFile(upload, 3) + if len(files) != 2 { + t.Fatalf("prepareInputMediaFile returned %d files, want 2", len(files)) + } + if files[0].Name != "file-3" || files[1].Name != "file-3-thumbnail" { + t.Errorf("file names = %q, %q", files[0].Name, files[1].Name) + } +} + +// TestUpdateSentFromSubscription verifies that SentFrom resolves the user of a +// subscription update. +func TestUpdateSentFromSubscription(t *testing.T) { + update := Update{ + Subscription: &BotSubscriptionUpdated{ + User: User{ID: 5, FirstName: "A"}, + State: BotSubscriptionStateCanceled, + }, + } + from := update.SentFrom() + if from == nil { + t.Fatal("SentFrom returned nil for a subscription update") + } + if from.ID != 5 { + t.Errorf("SentFrom().ID = %d, want 5", from.ID) + } +} + +// TestCommunityServiceMessages verifies decoding of the community service +// messages and the subscription update. +func TestCommunityServiceMessages(t *testing.T) { + var msg Message + raw := `{"message_id":1,"date":1,"chat":{"id":1,"type":"supergroup"},` + + `"community_chat_added":{"community":{"id":99,"name":"Gophers"}}}` + if err := json.Unmarshal([]byte(raw), &msg); err != nil { + t.Fatalf("unmarshal message: %v", err) + } + if msg.CommunityChatAdded == nil { + t.Fatal("community_chat_added not decoded") + } + if msg.CommunityChatAdded.Community.ID != 99 || msg.CommunityChatAdded.Community.Name != "Gophers" { + t.Errorf("community not decoded: %+v", msg.CommunityChatAdded.Community) + } + + var update Update + rawUpdate := `{"update_id":1,"subscription":{"user":{"id":5,"is_bot":false,"first_name":"A"},` + + `"invoice_payload":"p","state":"active"}}` + if err := json.Unmarshal([]byte(rawUpdate), &update); err != nil { + t.Fatalf("unmarshal update: %v", err) + } + if update.Subscription == nil { + t.Fatal("subscription not decoded") + } + if update.Subscription.State != BotSubscriptionStateActive || update.Subscription.User.ID != 5 { + t.Errorf("subscription not decoded: %+v", update.Subscription) + } +} diff --git a/helpers.go b/helpers.go index b142ae50..a18a5e18 100644 --- a/helpers.go +++ b/helpers.go @@ -153,7 +153,8 @@ func NewLivePhoto(chatID int64, video, photo RequestFileData) LivePhotoConfig { } // NewRichMessage creates a new sendRichMessage request for the given chat. -// Exactly one of message.HTML or message.Markdown must be set. +// Exactly one of message.HTML, message.Markdown, or message.Blocks must be +// set. func NewRichMessage(chatID int64, message InputRichMessage) SendRichMessageConfig { return SendRichMessageConfig{ BaseChat: BaseChat{ChatID: chatID}, @@ -244,6 +245,16 @@ func NewInputMediaAudio(media RequestFileData) InputMediaAudio { } } +// NewInputMediaVoiceNote creates a new InputMediaVoiceNote. +func NewInputMediaVoiceNote(media RequestFileData) InputMediaVoiceNote { + return InputMediaVoiceNote{ + BaseInputMedia: BaseInputMedia{ + Type: "voice_note", + Media: media, + }, + } +} + // NewInputMediaDocument creates a new InputMediaDocument. func NewInputMediaDocument(media RequestFileData) InputMediaDocument { return InputMediaDocument{ @@ -630,6 +641,71 @@ func NewEditMessageReplyMarkup(chatID int64, messageID int, replyMarkup InlineKe } } +// NewEditEphemeralMessageText allows you to edit the text of an ephemeral +// message sent to receiverUserID in the given chat. +func NewEditEphemeralMessageText(chatID, receiverUserID int64, ephemeralMessageID int, text string) EditEphemeralMessageTextConfig { + return EditEphemeralMessageTextConfig{ + BaseEphemeralEdit: BaseEphemeralEdit{ + ChatID: chatID, + ReceiverUserID: receiverUserID, + EphemeralMessageID: ephemeralMessageID, + }, + Text: text, + } +} + +// NewEditEphemeralMessageCaption allows you to edit the caption of an +// ephemeral message sent to receiverUserID in the given chat. +func NewEditEphemeralMessageCaption(chatID, receiverUserID int64, ephemeralMessageID int, caption string) EditEphemeralMessageCaptionConfig { + return EditEphemeralMessageCaptionConfig{ + BaseEphemeralEdit: BaseEphemeralEdit{ + ChatID: chatID, + ReceiverUserID: receiverUserID, + EphemeralMessageID: ephemeralMessageID, + }, + Caption: caption, + } +} + +// NewEditEphemeralMessageMedia allows you to edit the media of an ephemeral +// message sent to receiverUserID in the given chat. A new file can't be +// uploaded; use a previously uploaded file via its file_id, or specify a URL. +func NewEditEphemeralMessageMedia(chatID, receiverUserID int64, ephemeralMessageID int, media interface{}) EditEphemeralMessageMediaConfig { + return EditEphemeralMessageMediaConfig{ + BaseEphemeralEdit: BaseEphemeralEdit{ + ChatID: chatID, + ReceiverUserID: receiverUserID, + EphemeralMessageID: ephemeralMessageID, + }, + Media: media, + } +} + +// NewEditEphemeralMessageReplyMarkup allows you to edit the reply markup of an +// ephemeral message sent to receiverUserID in the given chat. +func NewEditEphemeralMessageReplyMarkup(chatID, receiverUserID int64, ephemeralMessageID int, replyMarkup InlineKeyboardMarkup) EditEphemeralMessageReplyMarkupConfig { + return EditEphemeralMessageReplyMarkupConfig{ + BaseEphemeralEdit: BaseEphemeralEdit{ + ChatID: chatID, + ReceiverUserID: receiverUserID, + EphemeralMessageID: ephemeralMessageID, + }, + ReplyMarkup: &replyMarkup, + } +} + +// NewDeleteEphemeralMessage allows you to delete an ephemeral message sent to +// receiverUserID in the given chat. +func NewDeleteEphemeralMessage(chatID, receiverUserID int64, ephemeralMessageID int) DeleteEphemeralMessageConfig { + return DeleteEphemeralMessageConfig{ + BaseEphemeralEdit: BaseEphemeralEdit{ + ChatID: chatID, + ReceiverUserID: receiverUserID, + EphemeralMessageID: ephemeralMessageID, + }, + } +} + // NewRemoveKeyboard hides the keyboard, with the option for being selective // or hiding for everyone. func NewRemoveKeyboard(selective bool) ReplyKeyboardRemove { diff --git a/types.go b/types.go index 05deac95..43c5ad90 100644 --- a/types.go +++ b/types.go @@ -181,6 +181,11 @@ type Update struct { // // optional GuestMessage *Message `json:"guest_message,omitempty"` + // Subscription is a change to a user payment subscription toward the + // bot. + // + // optional + Subscription *BotSubscriptionUpdated `json:"subscription,omitempty"` } // SentFrom returns the user who sent an update. Can be nil, if Telegram did not provide information @@ -203,6 +208,8 @@ func (u *Update) SentFrom() *User { return u.PreCheckoutQuery.From case u.ManagedBot != nil: return &u.ManagedBot.User + case u.Subscription != nil: + return &u.Subscription.User default: return nil } @@ -602,6 +609,10 @@ type ChatFullInfo struct { // // optional ParentChat *Chat `json:"parent_chat,omitempty"` + // Community is the community the chat belongs to. + // + // optional + Community *Community `json:"community,omitempty"` // Rating is the rating of the user in a private chat. // // optional @@ -689,6 +700,17 @@ type Message struct { // // optional SenderTag string `json:"sender_tag,omitempty"` + // ReceiverUser is, for ephemeral messages, the user who received the + // message. + // + // optional + ReceiverUser *User `json:"receiver_user,omitempty"` + // EphemeralMessageID is, for ephemeral messages, the identifier of the + // ephemeral message inside this chat. The identifier may be reused for + // another ephemeral message after the message is deleted or expires. + // + // optional + EphemeralMessageID int `json:"ephemeral_message_id,omitempty"` // SenderBusinessBot is the bot that actually sent the message on behalf // of the business account. Available only for outgoing messages sent on // behalf of the connected business account. @@ -1071,6 +1093,16 @@ type Message struct { // // optional PollOptionDeleted *PollOptionDeleted `json:"poll_option_deleted,omitempty"` + // CommunityChatAdded is a service message about the chat being added to a + // community. + // + // optional + CommunityChatAdded *CommunityChatAdded `json:"community_chat_added,omitempty"` + // CommunityChatRemoved is a service message about the chat being removed + // from a community. + // + // optional + CommunityChatRemoved *CommunityChatRemoved `json:"community_chat_removed,omitempty"` // ReplyToPollOptionID is the persistent identifier of the poll option // that this message is a reply to. // @@ -5092,14 +5124,27 @@ type LinkPreviewOptions struct { // ReplyParameters describes reply parameters for the message that is being sent. type ReplyParameters struct { // MessageID is the identifier of the message that will be replied to in - // the current chat, or in the chat ChatID if it is specified. - MessageID int `json:"message_id"` + // the current chat, or in the chat ChatID if it is specified. Required if + // EphemeralMessageID is not specified. + // + // optional + MessageID int `json:"message_id,omitempty"` // ChatID, if the message to be replied to is from a different chat, is // the unique identifier for the chat or username of the channel - // (@channelusername). Pass int64 or string. + // (@channelusername). Pass int64 or string. Not supported for messages + // sent on behalf of a business account, messages from channel direct + // messages chats, and ephemeral messages. // // optional ChatID any `json:"chat_id,omitempty"` + // EphemeralMessageID is the identifier of the incoming ephemeral message + // that will be replied to in the current chat. A reply to an ephemeral + // message must itself be an ephemeral message, and may only be sent + // within 15 seconds of the original. Required if MessageID is not + // specified. + // + // optional + EphemeralMessageID int `json:"ephemeral_message_id,omitempty"` // AllowSendingWithoutReply is true if the message should be sent even if // the specified message to be replied to is not found. // @@ -5243,6 +5288,11 @@ type BotCommand struct { Command string `json:"command"` // Description of the command, 3-256 characters. Description string `json:"description"` + // IsEphemeral is true if the result of the command is an ephemeral + // message, visible only to the user who sent the command and the bot. + // + // optional + IsEphemeral bool `json:"is_ephemeral,omitempty"` } // BotCommandScope represents the scope to which bot commands are applied. @@ -5407,6 +5457,17 @@ type InputMediaAudio struct { Title string `json:"title,omitempty"` } +// InputMediaVoiceNote represents a voice message file to be sent. It is +// accepted as the media of an InputRichBlock "voice_note" block and by +// InputRichMessageMedia. +type InputMediaVoiceNote struct { + BaseInputMedia + // Duration of the voice message in seconds + // + // optional + Duration int `json:"duration,omitempty"` +} + // InputMediaDocument is a general file to send as part of a media group. type InputMediaDocument struct { BaseInputMedia @@ -7025,20 +7086,32 @@ type InputMediaLink struct { URL string `json:"url"` } -// InputRichMessage describes a rich message to be sent. Exactly one of HTML -// or Markdown must be set. See the Telegram "rich message formatting options" -// documentation for the supported markup. +// InputRichMessage describes a rich message to be sent. Exactly one of HTML, +// Markdown, or Blocks must be set. See the Telegram "rich message formatting +// options" documentation for the supported markup. type InputRichMessage struct { + // Blocks is the content of the rich message described as a list of + // blocks. Exactly one of HTML, Markdown, or Blocks must be set. + // + // optional + Blocks []InputRichBlock `json:"blocks,omitempty"` // HTML is the content of the rich message described using HTML - // formatting. Exactly one of HTML or Markdown must be set. + // formatting. Exactly one of HTML, Markdown, or Blocks must be set. Use + // Media to specify the media used in the message. // // optional HTML string `json:"html,omitempty"` // Markdown is the content of the rich message described using Markdown - // formatting. Exactly one of HTML or Markdown must be set. + // formatting. Exactly one of HTML, Markdown, or Blocks must be set. Use + // Media to specify the media used in the message. // // optional Markdown string `json:"markdown,omitempty"` + // Media is the list of media referenced from the Markdown or HTML fields + // using tg://photo?id=, tg://video?id=, and tg://audio?id= links. + // + // optional + Media []InputRichMessageMedia `json:"media,omitempty"` // IsRTL, if true, requests that the rich message be shown right-to-left. // // optional @@ -7514,3 +7587,253 @@ type RichBlockListItem struct { // optional Type string `json:"type,omitempty"` } + +// InputRichMessageMedia describes a media element embedded in an outgoing rich +// message, referenced from InputRichMessage.HTML or InputRichMessage.Markdown. +type InputRichMessageMedia struct { + // ID is the unique identifier of the media used in a tg://photo?id=, + // tg://video?id=, or tg://audio?id= link. 1-64 characters; only A-Z, a-z, + // 0-9, _ and - are allowed. + ID string `json:"id"` + // Media to be sent; one of InputMediaAnimation, InputMediaAudio, + // InputMediaPhoto, InputMediaVideo, or InputMediaVoiceNote. Everything + // except the media itself and its properties is ignored. + Media interface{} `json:"media"` +} + +// InputRichBlockListItem is an item of a rich formatted list to be sent. +type InputRichBlockListItem struct { + // Blocks is the content of the item. + Blocks []InputRichBlock `json:"blocks"` + // HasCheckbox, if true, gives the item a checkbox. + // + // optional + HasCheckbox bool `json:"has_checkbox,omitempty"` + // IsChecked, if true, gives the item a checked checkbox. + // + // optional + IsChecked bool `json:"is_checked,omitempty"` + // Value is, for ordered lists, the numeric value of the item label. + // + // optional + Value int `json:"value,omitempty"` + // Type is, for ordered lists, the type of the item label; one of "a", "A", + // "i", "I", or "1". + // + // optional + Type string `json:"type,omitempty"` +} + +// InputRichBlock represents a block in a rich formatted message to be sent. It +// is a flat polymorphic type keyed by Type; only the fields relevant to a given +// Type are set. Type takes the same values as RichBlock.Type, so the +// RichBlockType* constants apply here too. +// +// The "caption" wire field is split into Caption (for media blocks) and +// TableCaption (for the "table" block), which have different shapes; at most +// one is ever set. +type InputRichBlock struct { + // Type of the block, one of the RichBlockType* constants. + Type string `json:"type"` + // Text is the block text. Set for "paragraph", "heading", "pre", + // "footer", "pullquote", and "thinking" blocks. + // + // optional + Text *RichText `json:"text,omitempty"` + // Size is the relative font size of a "heading" block; 1-6, 1 is largest. + // + // optional + Size int `json:"size,omitempty"` + // Language is the programming language of a "pre" block. + // + // optional + Language string `json:"language,omitempty"` + // Expression is the LaTeX expression of a "mathematical_expression" + // block. + // + // optional + Expression string `json:"expression,omitempty"` + // Name is the anchor name of an "anchor" block. + // + // optional + Name string `json:"name,omitempty"` + // Items are the items of a "list" block. + // + // optional + Items []InputRichBlockListItem `json:"items,omitempty"` + // Blocks is the nested content of "blockquote", "collage", "slideshow", + // and "details" blocks. + // + // optional + Blocks []InputRichBlock `json:"blocks,omitempty"` + // Credit is the credit of "blockquote" and "pullquote" blocks. + // + // optional + Credit *RichText `json:"credit,omitempty"` + // Cells are the rows of cells of a "table" block. + // + // optional + Cells [][]RichBlockTableCell `json:"cells,omitempty"` + // IsBordered, if true, gives a "table" block borders. + // + // optional + IsBordered bool `json:"is_bordered,omitempty"` + // IsStriped, if true, makes a "table" block striped. + // + // optional + IsStriped bool `json:"is_striped,omitempty"` + // Summary is the always-shown summary of a "details" block. + // + // optional + Summary *RichText `json:"summary,omitempty"` + // IsOpen, if true, makes the content of a "details" block visible by + // default. + // + // optional + IsOpen bool `json:"is_open,omitempty"` + // Location is the center of a "map" block. + // + // optional + Location *Location `json:"location,omitempty"` + // Zoom is the zoom level of a "map" block; 0-24. + // + // optional + Zoom int `json:"zoom,omitempty"` + // Width is the width of a "map" block; 0-10000. + // + // optional + Width int `json:"width,omitempty"` + // Height is the height of a "map" block; 0-10000. + // + // optional + Height int `json:"height,omitempty"` + // Animation is the animation of an "animation" block. Its caption is + // ignored. + // + // optional + Animation *InputMediaAnimation `json:"animation,omitempty"` + // Audio is the audio of an "audio" block. Its caption is ignored. + // + // optional + Audio *InputMediaAudio `json:"audio,omitempty"` + // Photo is the photo of a "photo" block. Its caption is ignored. + // + // optional + Photo *InputMediaPhoto `json:"photo,omitempty"` + // Video is the video of a "video" block. Its caption is ignored. + // + // optional + Video *InputMediaVideo `json:"video,omitempty"` + // VoiceNote is the voice note of a "voice_note" block. Its caption is + // ignored. + // + // optional + VoiceNote *InputMediaVoiceNote `json:"voice_note,omitempty"` + // Caption is the caption of a media block ("collage", "slideshow", "map", + // "animation", "audio", "photo", "video", "voice_note"). It shares the + // "caption" wire field with TableCaption. + // + // optional + Caption *RichBlockCaption `json:"-"` + // TableCaption is the caption of a "table" block. It shares the "caption" + // wire field with Caption. + // + // optional + TableCaption *RichText `json:"-"` +} + +// UnmarshalJSON decodes an InputRichBlock, routing the polymorphic "caption" +// field to Caption or TableCaption based on Type. +func (b *InputRichBlock) UnmarshalJSON(data []byte) error { + type alias InputRichBlock + aux := struct { + *alias + Caption json.RawMessage `json:"caption,omitempty"` + }{alias: (*alias)(b)} + + if err := json.Unmarshal(data, &aux); err != nil { + return err + } + + if len(aux.Caption) == 0 || string(aux.Caption) == "null" { + return nil + } + + if b.Type == RichBlockTypeTable { + b.TableCaption = new(RichText) + return json.Unmarshal(aux.Caption, b.TableCaption) + } + + b.Caption = new(RichBlockCaption) + return json.Unmarshal(aux.Caption, b.Caption) +} + +// MarshalJSON encodes an InputRichBlock, emitting Caption or TableCaption +// under the shared "caption" wire field. +func (b InputRichBlock) MarshalJSON() ([]byte, error) { + type alias InputRichBlock + aux := struct { + alias + Caption json.RawMessage `json:"caption,omitempty"` + }{alias: alias(b)} + + switch { + case b.TableCaption != nil: + raw, err := json.Marshal(b.TableCaption) + if err != nil { + return nil, err + } + aux.Caption = raw + case b.Caption != nil: + raw, err := json.Marshal(b.Caption) + if err != nil { + return nil, err + } + aux.Caption = raw + } + + return json.Marshal(aux) +} + +// Community represents a community, a group of chats linked together around a +// shared topic or audience. +type Community struct { + // ID is the unique identifier for this community. + ID int64 `json:"id"` + // Name of the community. + Name string `json:"name"` +} + +// CommunityChatAdded describes a service message about a chat being added to a +// community. +type CommunityChatAdded struct { + // Community is the new community to which the chat belongs. + Community Community `json:"community"` +} + +// CommunityChatRemoved describes a service message about a chat being removed +// from a community. Currently holds no information. +type CommunityChatRemoved struct{} + +// BotSubscriptionUpdated contains information about changes to a user payment +// subscription toward the current bot. +type BotSubscriptionUpdated struct { + // User who subscribed for payments toward the bot. + User User `json:"user"` + // InvoicePayload is the bot-specified invoice payload. + InvoicePayload string `json:"invoice_payload"` + // State is the new state of the subscription, one of the + // BotSubscriptionState* constants. + State string `json:"state"` +} + +// BotSubscriptionUpdated.State values. +const ( + // BotSubscriptionStateCanceled means the user canceled the subscription. + BotSubscriptionStateCanceled = "canceled" + // BotSubscriptionStateActive means the user re-enabled a previously + // canceled subscription. + BotSubscriptionStateActive = "active" + // BotSubscriptionStateFailed means payment for the subscription failed. + BotSubscriptionStateFailed = "failed" +) From 7b5d84e3dc5211ff33bb26037b53d24eec7acd4d Mon Sep 17 00:00:00 2001 From: Kirill Date: Sat, 29 Aug 2026 02:48:04 +0400 Subject: [PATCH 59/65] Full support of API 10.3 --- README.md | 19 +++- configs.go | 177 ++++++++++++++++++++++++++--------- ephemeral_test.go | 16 ++-- richmessage_test.go | 69 ++++++++++++++ types.go | 221 +++++++++++++++++++++++++++++++++++++++++--- 5 files changed, 432 insertions(+), 70 deletions(-) diff --git a/README.md b/README.md index e2f298e4..adb9f4a3 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ > **This is a maintained fork of > [`go-telegram-bot-api/telegram-bot-api`](https://github.com/go-telegram-bot-api/telegram-bot-api).** > The upstream project stopped updating at Bot API 6.0 (April 2022). This fork -> is brought up to **Bot API 10.2** (the latest release as of this writing) and +> is brought up to **Bot API 10.3** (the latest release as of this writing) and > tracks the [official changelog](https://core.telegram.org/bots/api-changelog) > going forward. @@ -39,8 +39,8 @@ update your imports, or keep using upstream on 6.0. If you're coming from `go-telegram-bot-api/telegram-bot-api` v5.5.x and want to jump to the latest Bot API, read [**BREAKING.md**](./BREAKING.md). It lists every source-incompatible change introduced while bringing the fork from 6.0 -to 10.0, grouped by topic, with before/after snippets for each one (10.1 and -10.2 are purely additive and introduced no breaking changes). The fastest +to 10.0, grouped by topic, with before/after snippets for each one (10.1 +through 10.3 are purely additive and introduced no breaking changes). The fastest way to migrate is `grep` your codebase for the old identifier and apply the rewrite. @@ -166,7 +166,7 @@ to generate your free TLS certificate there. ### Bot API versions -Full support for every Bot API version from **6.1 through 10.2**. See git log +Full support for every Bot API version from **6.1 through 10.3**. See git log for the per-version commits; each `Full support of API X` commit message is the authoritative trail for what that version added. @@ -189,6 +189,17 @@ them, `ReplyParameters.EphemeralMessageID`, the `EditEphemeralMessage*` and `CommunityChatRemoved`, `ChatFullInfo.Community`), and payment subscription updates (`BotSubscriptionUpdated`, `Update.Subscription`). +The 10.3 surface adds rich message buttons (`RichMessageButton`, the "button" +`RichText` span, the "buttons", "document", and "expandable_blockquote" +blocks), structured ephemeral send parameters (`EphemeralMessageParameters` +with `ReplaceCallbackQueryMessage`, sent as one JSON object), disabled and +force-reply keyboards (`DisabledButton`, `InlineKeyboardButton.Disabled`, +`ForceReply` on both keyboard markups), stoppable drafts (`CanStop` / +`KeepOnStop`, `MessageGenerationStopped`, `Update.StoppedMessageGeneration`), +welcome message rights (`CanSendWelcomeMessages`), community join service +messages (`CommunityChatJoined`), and gift text fields +(`UniqueGiftInfo.Text` / `Entities` / `IsPrivate`). + The 10.0 surface includes guest mode (`AnswerGuestQuery`, `Update.GuestMessage`, `User.SupportsGuestQueries`), live photos (`SendLivePhoto` via `LivePhotoConfig` + `NewLivePhoto`, `LivePhoto` / diff --git a/configs.go b/configs.go index d2ee4006..fb4bd659 100644 --- a/configs.go +++ b/configs.go @@ -134,6 +134,10 @@ const ( // UpdateTypeSubscription is when a user payment subscription toward the // bot was changed. UpdateTypeSubscription = "subscription" + + // UpdateTypeStoppedMessageGeneration is when a user asked the bot to stop + // the generation of a message. + UpdateTypeStoppedMessageGeneration = "stopped_message_generation" ) // Library errors @@ -341,27 +345,44 @@ func (chat *BaseChat) params() (Params, error) { return params, err } -// EphemeralSendParams holds the parameters that make an outgoing message -// ephemeral, i.e. visible only to a single recipient and the bot. It is -// embedded by the send configs whose methods support ephemeral messages. +// EphemeralMessageParameters holds the parameters that make an outgoing +// message ephemeral, i.e. visible only to a single recipient and the bot. It +// is embedded (as EphemeralSendParams) by the send configs whose methods +// support ephemeral messages and is sent on the wire as the JSON-serialized +// ephemeral_message_parameters parameter. // // Leaving ReceiverUserID zero sends an ordinary, non-ephemeral message. -type EphemeralSendParams struct { +type EphemeralMessageParameters struct { // ReceiverUserID is the unique identifier of the user who will receive // the ephemeral message; for group and supergroup chats only. Delivery is // not guaranteed, especially if the user is offline. - ReceiverUserID int64 + ReceiverUserID int64 `json:"receiver_user_id"` // CallbackQueryID is the identifier of the callback query which triggered // the ephemeral message, if any. - CallbackQueryID string + // + // optional + CallbackQueryID string `json:"callback_query_id,omitempty"` + // ReplaceCallbackQueryMessage, if true, shows the ephemeral message in + // place of the original message. Must be false for callback queries from + // ephemeral messages, which must be edited with the editEphemeralMessage* + // methods. + // + // optional + ReplaceCallbackQueryMessage bool `json:"replace_callback_query_message,omitempty"` } -// addTo writes the ephemeral parameters into params. It is a named method -// rather than params() so that it does not collide with the params() promoted -// from BaseChat in configs that embed both. -func (e EphemeralSendParams) addTo(params Params) { - params.AddNonZero64("receiver_user_id", e.ReceiverUserID) - params.AddNonEmpty("callback_query_id", e.CallbackQueryID) +// EphemeralSendParams is the embedded form of EphemeralMessageParameters kept +// for source compatibility with Bot API 10.2 code. +type EphemeralSendParams = EphemeralMessageParameters + +// addTo writes the ephemeral_message_parameters parameter into params. It is +// a named method rather than params() so that it does not collide with the +// params() promoted from BaseChat in configs that embed both. +func (e EphemeralMessageParameters) addTo(params Params) error { + if e == (EphemeralMessageParameters{}) { + return nil + } + return params.AddAny("ephemeral_message_parameters", e) } // BaseFile is a base type for all file config types. @@ -424,9 +445,10 @@ func (config MessageConfig) params() (Params, error) { if err = params.AddAny("link_preview_options", config.LinkPreviewOptions); err != nil { return params, err } - err = params.AddAny("entities", config.Entities) - - config.EphemeralSendParams.addTo(params) + if err = params.AddAny("entities", config.Entities); err != nil { + return params, err + } + err = config.EphemeralSendParams.addTo(params) return params, err } @@ -519,9 +541,10 @@ func (config PhotoConfig) params() (Params, error) { params.AddNonEmpty("parse_mode", config.ParseMode) params.AddBool("show_caption_above_media", config.ShowCaptionAboveMedia) params.AddBool("has_spoiler", config.HasSpoiler) - err = params.AddAny("caption_entities", config.CaptionEntities) - - config.EphemeralSendParams.addTo(params) + if err = params.AddAny("caption_entities", config.CaptionEntities); err != nil { + return params, err + } + err = config.EphemeralSendParams.addTo(params) return params, err } @@ -570,9 +593,10 @@ func (config AudioConfig) params() (Params, error) { params.AddNonEmpty("title", config.Title) params.AddNonEmpty("caption", config.Caption) params.AddNonEmpty("parse_mode", config.ParseMode) - err = params.AddInterface("caption_entities", config.CaptionEntities) - - config.EphemeralSendParams.addTo(params) + if err = params.AddInterface("caption_entities", config.CaptionEntities); err != nil { + return params, err + } + err = config.EphemeralSendParams.addTo(params) return params, err } @@ -610,12 +634,15 @@ type DocumentConfig struct { func (config DocumentConfig) params() (Params, error) { params, err := config.BaseFile.params() + if err != nil { + return params, err + } params.AddNonEmpty("caption", config.Caption) params.AddNonEmpty("parse_mode", config.ParseMode) params.AddBool("disable_content_type_detection", config.DisableContentTypeDetection) - config.EphemeralSendParams.addTo(params) + err = config.EphemeralSendParams.addTo(params) return params, err } @@ -656,9 +683,9 @@ func (config StickerConfig) params() (Params, error) { params.AddNonEmpty("emoji", config.Emoji) - config.EphemeralSendParams.addTo(params) + err = config.EphemeralSendParams.addTo(params) - return params, nil + return params, err } func (config StickerConfig) method() string { @@ -701,9 +728,10 @@ func (config VideoConfig) params() (Params, error) { params.AddBool("show_caption_above_media", config.ShowCaptionAboveMedia) params.AddBool("supports_streaming", config.SupportsStreaming) params.AddBool("has_spoiler", config.HasSpoiler) - err = params.AddAny("caption_entities", config.CaptionEntities) - - config.EphemeralSendParams.addTo(params) + if err = params.AddAny("caption_entities", config.CaptionEntities); err != nil { + return params, err + } + err = config.EphemeralSendParams.addTo(params) return params, err } @@ -761,9 +789,10 @@ func (config LivePhotoConfig) params() (Params, error) { params.AddNonEmpty("parse_mode", config.ParseMode) params.AddBool("show_caption_above_media", config.ShowCaptionAboveMedia) params.AddBool("has_spoiler", config.HasSpoiler) - err = params.AddAny("caption_entities", config.CaptionEntities) - - config.EphemeralSendParams.addTo(params) + if err = params.AddAny("caption_entities", config.CaptionEntities); err != nil { + return params, err + } + err = config.EphemeralSendParams.addTo(params) return params, err } @@ -812,9 +841,10 @@ func (config AnimationConfig) params() (Params, error) { params.AddNonEmpty("parse_mode", config.ParseMode) params.AddBool("show_caption_above_media", config.ShowCaptionAboveMedia) params.AddBool("has_spoiler", config.HasSpoiler) - err = params.AddAny("caption_entities", config.CaptionEntities) - - config.EphemeralSendParams.addTo(params) + if err = params.AddAny("caption_entities", config.CaptionEntities); err != nil { + return params, err + } + err = config.EphemeralSendParams.addTo(params) return params, err } @@ -850,11 +880,14 @@ type VideoNoteConfig struct { func (config VideoNoteConfig) params() (Params, error) { params, err := config.BaseChat.params() + if err != nil { + return params, err + } params.AddNonZero("duration", config.Duration) params.AddNonZero("length", config.Length) - config.EphemeralSendParams.addTo(params) + err = config.EphemeralSendParams.addTo(params) return params, err } @@ -899,9 +932,10 @@ func (config VoiceConfig) params() (Params, error) { params.AddNonZero("duration", config.Duration) params.AddNonEmpty("caption", config.Caption) params.AddNonEmpty("parse_mode", config.ParseMode) - err = params.AddInterface("caption_entities", config.CaptionEntities) - - config.EphemeralSendParams.addTo(params) + if err = params.AddInterface("caption_entities", config.CaptionEntities); err != nil { + return params, err + } + err = config.EphemeralSendParams.addTo(params) return params, err } @@ -940,6 +974,9 @@ type LocationConfig struct { func (config LocationConfig) params() (Params, error) { params, err := config.BaseChat.params() + if err != nil { + return params, err + } params.AddNonZeroFloat("latitude", config.Latitude) params.AddNonZeroFloat("longitude", config.Longitude) @@ -948,7 +985,7 @@ func (config LocationConfig) params() (Params, error) { params.AddNonZero("heading", config.Heading) params.AddNonZero("proximity_alert_radius", config.ProximityAlertRadius) - config.EphemeralSendParams.addTo(params) + err = config.EphemeralSendParams.addTo(params) return params, err } @@ -1014,6 +1051,9 @@ type VenueConfig struct { func (config VenueConfig) params() (Params, error) { params, err := config.BaseChat.params() + if err != nil { + return params, err + } params.AddNonZeroFloat("latitude", config.Latitude) params.AddNonZeroFloat("longitude", config.Longitude) @@ -1024,7 +1064,7 @@ func (config VenueConfig) params() (Params, error) { params.AddNonEmpty("google_place_id", config.GooglePlaceID) params.AddNonEmpty("google_place_type", config.GooglePlaceType) - config.EphemeralSendParams.addTo(params) + err = config.EphemeralSendParams.addTo(params) return params, err } @@ -1045,6 +1085,9 @@ type ContactConfig struct { func (config ContactConfig) params() (Params, error) { params, err := config.BaseChat.params() + if err != nil { + return params, err + } params["phone_number"] = config.PhoneNumber params["first_name"] = config.FirstName @@ -1052,7 +1095,7 @@ func (config ContactConfig) params() (Params, error) { params.AddNonEmpty("last_name", config.LastName) params.AddNonEmpty("vcard", config.VCard) - config.EphemeralSendParams.addTo(params) + err = config.EphemeralSendParams.addTo(params) return params, err } @@ -1393,8 +1436,11 @@ func (edit BaseEphemeralEdit) params() (Params, error) { type EditEphemeralMessageTextConfig struct { BaseEphemeralEdit // Text is the new text of the message, 1-4096 characters after entity - // parsing. + // parsing. Required if RichMessage is not set. Text string + // RichMessage is the new rich content of the message. Required if Text is + // not set. + RichMessage *InputRichMessage // ParseMode is the mode for parsing entities in the message text. ParseMode string // Entities is a list of special entities that appear in the message text, @@ -1412,7 +1458,16 @@ func (config EditEphemeralMessageTextConfig) params() (Params, error) { return params, err } - params["text"] = config.Text + // text stays present when RichMessage is not set; an empty string is + // rejected by the API rather than silently dropped. + if config.RichMessage == nil { + params["text"] = config.Text + } else { + params.AddNonEmpty("text", config.Text) + if err = params.AddAny("rich_message", config.RichMessage); err != nil { + return params, err + } + } params.AddNonEmpty("parse_mode", config.ParseMode) if err = params.AddAny("entities", config.Entities); err != nil { return params, err @@ -1430,7 +1485,7 @@ func (config EditEphemeralMessageTextConfig) method() string { } // EditEphemeralMessageMediaConfig allows you to edit the media of an ephemeral -// message. A new file can't be uploaded; use a previously uploaded file via +// message. A new file can be uploaded, or use a previously uploaded file via // its file_id, or specify a URL. type EditEphemeralMessageMediaConfig struct { BaseEphemeralEdit @@ -1454,6 +1509,10 @@ func (config EditEphemeralMessageMediaConfig) params() (Params, error) { return params, err } +func (config EditEphemeralMessageMediaConfig) files() []RequestFile { + return prepareInputMediaFile(config.Media, 0) +} + func (config EditEphemeralMessageMediaConfig) method() string { return "editEphemeralMessageMedia" } @@ -1470,6 +1529,9 @@ type EditEphemeralMessageCaptionConfig struct { // CaptionEntities is a list of special entities that appear in the // caption, which can be specified instead of ParseMode. CaptionEntities []MessageEntity + // ShowCaptionAboveMedia shows the caption above the message media. + // Supported only for animation, photo, and video messages. + ShowCaptionAboveMedia bool // ReplyMarkup is the new inline keyboard for the message. ReplyMarkup *InlineKeyboardMarkup } @@ -1482,6 +1544,7 @@ func (config EditEphemeralMessageCaptionConfig) params() (Params, error) { params.AddNonEmpty("caption", config.Caption) params.AddNonEmpty("parse_mode", config.ParseMode) + params.AddBool("show_caption_above_media", config.ShowCaptionAboveMedia) if err = params.AddAny("caption_entities", config.CaptionEntities); err != nil { return params, err } @@ -1853,6 +1916,7 @@ type PromoteChatMemberConfig struct { CanManageTopics bool CanManageDirectMessages bool CanManageTags bool + CanSendWelcomeMessages bool } func (config PromoteChatMemberConfig) method() string { @@ -1884,6 +1948,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 } @@ -4823,6 +4888,14 @@ type SendMessageDraftConfig struct { Text string ParseMode string Entities []MessageEntity + // CanStop shows the user a button to stop further drafts. The bot + // receives an Update with StoppedMessageGeneration if the user presses + // the button. + CanStop bool + // KeepOnStop keeps the draft in the chat when the stop button is pressed. + // The draft still disappears after a short time or if the bot sends a + // message. To fully preserve the partial draft, send it as a new message. + KeepOnStop bool } func (SendMessageDraftConfig) method() string { @@ -4838,6 +4911,8 @@ func (config SendMessageDraftConfig) params() (Params, error) { // text is optional; an empty string is meaningful (placeholder). params["text"] = config.Text params.AddNonEmpty("parse_mode", config.ParseMode) + params.AddBool("can_stop", config.CanStop) + params.AddBool("keep_on_stop", config.KeepOnStop) err := params.AddAny("entities", config.Entities) return params, err @@ -5432,6 +5507,7 @@ func prepareInputMediaForFiles(inputMedia []interface{}) []RequestFile { // returned, so it can be passed to BotAPI.Send. type SendRichMessageConfig struct { BaseChat + EphemeralSendParams // RichMessage is the message to be sent. RichMessage *InputRichMessage // SuggestedPostParameters contains the parameters of the suggested post @@ -5448,7 +5524,10 @@ func (config SendRichMessageConfig) params() (Params, error) { if err = params.AddAny("rich_message", config.RichMessage); err != nil { return params, err } - err = params.AddAny("suggested_post_parameters", config.SuggestedPostParameters) + if err = params.AddAny("suggested_post_parameters", config.SuggestedPostParameters); err != nil { + return params, err + } + err = config.EphemeralSendParams.addTo(params) return params, err } @@ -5473,6 +5552,14 @@ type SendRichMessageDraftConfig struct { DraftID int // RichMessage is the partial message to be streamed. Required. RichMessage *InputRichMessage + // CanStop shows the user a button to stop further drafts. The bot + // receives an Update with StoppedMessageGeneration if the user presses + // the button. + CanStop bool + // KeepOnStop keeps the draft in the chat when the stop button is pressed. + // The draft still disappears after a short time or if the bot sends a + // message. To fully preserve the partial draft, send it as a new message. + KeepOnStop bool } func (config SendRichMessageDraftConfig) params() (Params, error) { @@ -5481,6 +5568,8 @@ func (config SendRichMessageDraftConfig) params() (Params, error) { params.AddNonZero64("chat_id", config.ChatID) params.AddNonZero("message_thread_id", config.MessageThreadID) params.AddNonZero("draft_id", config.DraftID) + params.AddBool("can_stop", config.CanStop) + params.AddBool("keep_on_stop", config.KeepOnStop) err := params.AddAny("rich_message", config.RichMessage) return params, err diff --git a/ephemeral_test.go b/ephemeral_test.go index 3bd12c42..2456ef1f 100644 --- a/ephemeral_test.go +++ b/ephemeral_test.go @@ -142,25 +142,21 @@ func TestEphemeralSendParams(t *testing.T) { if err != nil { t.Fatalf("plain params: %v", err) } - if _, ok := params["receiver_user_id"]; ok { - t.Error("receiver_user_id emitted for a non-ephemeral message") - } - if _, ok := params["callback_query_id"]; ok { - t.Error("callback_query_id emitted for a non-ephemeral message") + if _, ok := params["ephemeral_message_parameters"]; ok { + t.Error("ephemeral_message_parameters emitted for a non-ephemeral message") } ephemeral := NewMessage(12345, "hello") ephemeral.ReceiverUserID = 777 ephemeral.CallbackQueryID = "cbq" + ephemeral.ReplaceCallbackQueryMessage = true params, err = ephemeral.params() if err != nil { t.Fatalf("ephemeral params: %v", err) } - if params["receiver_user_id"] != "777" { - t.Errorf("receiver_user_id = %q, want 777", params["receiver_user_id"]) - } - if params["callback_query_id"] != "cbq" { - t.Errorf("callback_query_id = %q, want cbq", params["callback_query_id"]) + want := `{"receiver_user_id":777,"callback_query_id":"cbq","replace_callback_query_message":true}` + if params["ephemeral_message_parameters"] != want { + t.Errorf("ephemeral_message_parameters = %q, want %q", params["ephemeral_message_parameters"], want) } } diff --git a/richmessage_test.go b/richmessage_test.go index dddd4b44..78360013 100644 --- a/richmessage_test.go +++ b/richmessage_test.go @@ -151,3 +151,72 @@ func TestInputRichMessageParams(t *testing.T) { t.Errorf("rich_message html = %q", got.HTML) } } + +// TestRichBlockButtonsAndDocument verifies the Bot API 10.3 block types: +// "buttons", "document", and "expandable_blockquote", plus the "button" span +// and the is_compact table flag. +func TestRichBlockButtonsAndDocument(t *testing.T) { + buttons := InputRichBlock{ + Type: RichBlockTypeButtons, + Align: "center", + Buttons: []RichMessageButton{{ + Text: RichText{IsPlain: true, PlainText: "Press"}, + Style: "primary", + CallbackData: "cb", + }}, + } + out, err := json.Marshal(buttons) + if err != nil { + t.Fatalf("marshal buttons block: %v", err) + } + want := `{"type":"buttons","buttons":[{"text":"Press","style":"primary","callback_data":"cb"}],"align":"center"}` + if string(out) != want { + t.Errorf("buttons block encoding:\n got: %s\n want: %s", out, want) + } + + raw := `{"type":"document","document":{"file_id":"f1","file_unique_id":"u1"},` + + `"caption":{"text":"a file"}}` + var doc RichBlock + if err := json.Unmarshal([]byte(raw), &doc); err != nil { + t.Fatalf("unmarshal document block: %v", err) + } + if doc.Document == nil || doc.Document.FileID != "f1" { + t.Errorf("document not decoded: %+v", doc.Document) + } + if doc.Caption == nil || !doc.Caption.Text.IsPlain || doc.Caption.Text.PlainText != "a file" { + t.Errorf("document caption not routed: %+v", doc.Caption) + } + + quote := InputRichBlock{ + Type: RichBlockTypeExpandableBlockquote, + Text: &RichText{IsPlain: true, PlainText: "long quote"}, + } + out, err = json.Marshal(quote) + if err != nil { + t.Fatalf("marshal expandable blockquote: %v", err) + } + if got, want := string(out), `{"type":"expandable_blockquote","text":"long quote"}`; got != want { + t.Errorf("expandable blockquote encoding:\n got: %s\n want: %s", got, want) + } + + table := InputRichBlock{Type: RichBlockTypeTable, IsCompact: true} + out, err = json.Marshal(table) + if err != nil { + t.Fatalf("marshal table: %v", err) + } + if got, want := string(out), `{"type":"table","is_compact":true}`; got != want { + t.Errorf("compact table encoding:\n got: %s\n want: %s", got, want) + } + + span := RichText{Type: RichTextTypeButton, Button: &RichMessageButton{ + Text: RichText{IsPlain: true, PlainText: "Go"}, + URL: "https://example.com", + }} + out, err = json.Marshal(span) + if err != nil { + t.Fatalf("marshal button span: %v", err) + } + if got, want := string(out), `{"type":"button","button":{"text":"Go","url":"https://example.com"}}`; got != want { + t.Errorf("button span encoding:\n got: %s\n want: %s", got, want) + } +} diff --git a/types.go b/types.go index 43c5ad90..3ab3cebb 100644 --- a/types.go +++ b/types.go @@ -186,6 +186,11 @@ type Update struct { // // optional Subscription *BotSubscriptionUpdated `json:"subscription,omitempty"` + // StoppedMessageGeneration means a user asked the bot to stop the + // generation of a message. + // + // 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 @@ -1103,6 +1108,11 @@ type Message struct { // // optional CommunityChatRemoved *CommunityChatRemoved `json:"community_chat_removed,omitempty"` + // CommunityChatJoined is a service message about the chat being joined by + // a user from a community. + // + // optional + CommunityChatJoined *CommunityChatJoined `json:"community_chat_joined,omitempty"` // ReplyToPollOptionID is the persistent identifier of the poll option // that this message is a reply to. // @@ -2105,6 +2115,11 @@ type ReplyKeyboardMarkup struct { // // optional Selective bool `json:"selective,omitempty"` + // ForceReply requests clients to show the reply interface to the user, as + // if they had 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 @@ -2348,6 +2363,12 @@ type InlineKeyboardMarkup struct { // InlineKeyboard array of button rows, each represented by an Array of // InlineKeyboardButton objects InlineKeyboard [][]InlineKeyboardButton `json:"inline_keyboard"` + // ForceReply requests clients to show the reply interface to the user, as + // if they had 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 @@ -2433,6 +2454,10 @@ type InlineKeyboardButton struct { // // optional Pay bool `json:"pay,omitempty"` + // Disabled if set, then the button is disabled and does nothing. + // + // optional + Disabled *DisabledButton `json:"disabled,omitempty"` } // CopyTextButton represents an inline keyboard button that copies specified @@ -2442,6 +2467,10 @@ type CopyTextButton struct { Text string `json:"text"` } +// DisabledButton represents a disabled button which does nothing. Currently +// holds no information. +type DisabledButton struct{} + // SwitchInlineQueryChosenChat represents an inline button that switches the // current user to inline mode in a chosen chat, with an optional default // inline query. @@ -2667,6 +2696,7 @@ type ChatAdministratorRights struct { CanManageTopics bool `json:"can_manage_topics"` CanManageDirectMessages bool `json:"can_manage_direct_messages"` CanManageTags bool `json:"can_manage_tags"` + CanSendWelcomeMessages bool `json:"can_send_welcome_messages"` } // ChatMember contains information about one member of a chat. @@ -2791,6 +2821,12 @@ type ChatMember struct { // // optional CanManageTags bool `json:"can_manage_tags,omitempty"` + // CanSendWelcomeMessages administrators only. + // 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"` // Tag is the member's custom tag in the chat, if any. // // optional @@ -3927,6 +3963,19 @@ type UniqueGiftInfo struct { // Origin of the gift. One of "upgrade", "transfer", "resale", // "gifted_upgrade", "offer". Origin string `json:"origin"` + // Text of the message that was added to the gift. + // + // optional + Text string `json:"text,omitempty"` + // Entities are the special entities that appear in the text. + // + // optional + Entities []MessageEntity `json:"entities,omitempty"` + // IsPrivate is true, if the sender and gift text are shown only to the + // gift receiver; otherwise, everyone can see them. + // + // optional + IsPrivate bool `json:"is_private,omitempty"` // LastResaleCurrency is the currency in which the gift was last resold // on a resale market; for gifts with origin "resale" only. // @@ -3947,6 +3996,12 @@ type UniqueGiftInfo struct { // // optional TransferStarCount int `json:"transfer_star_count,omitempty"` + // NextTransferDate is the point in time (Unix timestamp) when the gift + // can be transferred. If it is in the past, the gift can be transferred + // now. + // + // optional + NextTransferDate int `json:"next_transfer_date,omitempty"` } // Owned gift type constants. @@ -7165,6 +7220,7 @@ const ( RichTextTypeHashtag = "hashtag" RichTextTypeCashtag = "cashtag" RichTextTypeBotCommand = "bot_command" + RichTextTypeButton = "button" RichTextTypeAnchor = "anchor" RichTextTypeAnchorLink = "anchor_link" RichTextTypeReference = "reference" @@ -7277,6 +7333,10 @@ type RichText struct { // // optional ReferenceName string `json:"reference_name,omitempty"` + // Button is the button of a "button" span. + // + // optional + Button *RichMessageButton `json:"button,omitempty"` // Raw preserves the original JSON of the object form for forward // compatibility with span types not yet modeled. @@ -7343,14 +7403,17 @@ const ( RichBlockTypeAnchor = "anchor" RichBlockTypeList = "list" RichBlockTypeBlockquote = "blockquote" + RichBlockTypeExpandableBlockquote = "expandable_blockquote" RichBlockTypePullquote = "pullquote" RichBlockTypeCollage = "collage" RichBlockTypeSlideshow = "slideshow" RichBlockTypeTable = "table" RichBlockTypeDetails = "details" RichBlockTypeMap = "map" + RichBlockTypeButtons = "buttons" RichBlockTypeAnimation = "animation" RichBlockTypeAudio = "audio" + RichBlockTypeDocument = "document" RichBlockTypePhoto = "photo" RichBlockTypeVideo = "video" RichBlockTypeVoiceNote = "voice_note" @@ -7366,7 +7429,7 @@ type RichBlock struct { // Type of the block, one of the RichBlockType* constants. Type string `json:"type"` // Text is the block text. Set for "paragraph", "heading", "pre", - // "footer", "pullquote", and "thinking" blocks. + // "footer", "expandable_blockquote", "pullquote", and "thinking" blocks. // // optional Text *RichText `json:"text,omitempty"` @@ -7396,7 +7459,8 @@ type RichBlock struct { // // optional Blocks []RichBlock `json:"blocks,omitempty"` - // Credit is the credit of "blockquote" and "pullquote" blocks. + // Credit is the credit of "blockquote", "expandable_blockquote", and + // "pullquote" blocks. // // optional Credit *RichText `json:"credit,omitempty"` @@ -7412,6 +7476,19 @@ type RichBlock struct { // // optional IsStriped bool `json:"is_striped,omitempty"` + // IsCompact is true if the cells of a "table" block have smaller indents. + // + // optional + IsCompact bool `json:"is_compact,omitempty"` + // Buttons are the buttons of a "buttons" block, shown in one row. + // + // optional + Buttons []RichMessageButton `json:"buttons,omitempty"` + // Align is the horizontal alignment of the buttons of a "buttons" block; + // one of "left", "center", or "right". + // + // optional + Align string `json:"align,omitempty"` // Summary is the always-shown summary of a "details" block. // // optional @@ -7449,6 +7526,10 @@ type RichBlock struct { // // optional Audio *Audio `json:"audio,omitempty"` + // Document is the general file of a "document" block. + // + // optional + Document *Document `json:"document,omitempty"` // Photo are the available sizes of a "photo" block. // // optional @@ -7462,8 +7543,8 @@ type RichBlock struct { // optional VoiceNote *Voice `json:"voice_note,omitempty"` // Caption is the caption of a media block ("collage", "slideshow", "map", - // "animation", "audio", "photo", "video", "voice_note"). It shares the - // "caption" wire field with TableCaption. + // "animation", "audio", "document", "photo", "video", "voice_note"). It + // shares the "caption" wire field with TableCaption. // // optional Caption *RichBlockCaption `json:"-"` @@ -7563,6 +7644,79 @@ type RichBlockTableCell struct { Valign string `json:"valign"` } +// RichMessageButton represents a button in a rich message. Use exactly one of +// the fields other than Text and Style to specify the type of the button. +type RichMessageButton struct { + // Text of the button. May contain only plain text, "custom_emoji" and + // "date_time" entities. + Text RichText `json:"text"` + // Style of the button. One of "danger", "success", "primary", or "link" + // (the button is shown as a regular link without borders). Apps may use + // theme-specific colors for the button background and text based on the + // style. The style "link" is allowed only for callback buttons. + // + // optional + Style string `json:"style,omitempty"` + // URL is an HTTP or tg:// URL to be opened when the button is pressed. + // Links tg://user?id= can be used to mention a user by their + // identifier without using a username, if this is allowed by their + // privacy settings. + // + // optional + URL string `json:"url,omitempty"` + // CallbackData is the 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 is the description of the Web App that will be launched when the + // user presses the button. The Web App will be able to send an arbitrary + // message on behalf of the user using the method answerWebAppQuery. + // Available only in private chats between a user and the bot. Not + // supported for messages sent on behalf of a business account. + // + // optional + WebApp *WebAppInfo `json:"web_app,omitempty"` + // LoginURL is an HTTPS URL used to automatically authorize the user. Can + // be used as a replacement for the Telegram Login Widget. Not supported + // for ephemeral messages. + // + // optional + LoginURL *LoginURL `json:"login_url,omitempty"` + // SwitchInlineQuery, if set, prompts 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. May be empty, in which case just the + // bot's username will be inserted. Not supported for messages sent in + // channel direct messages chats and on behalf of a business account. + // + // optional + SwitchInlineQuery *string `json:"switch_inline_query,omitempty"` + // SwitchInlineQueryCurrentChat, if set, inserts the bot's username and + // the specified inline query in the current chat's input field. May be + // empty, in which case only the bot's username will be inserted. Not + // supported in channels and for messages sent in channel direct messages + // chats and on behalf of a business account. + // + // optional + SwitchInlineQueryCurrentChat *string `json:"switch_inline_query_current_chat,omitempty"` + // SwitchInlineQueryChosenChat, if set, prompts the user to select one of + // their chats of the specified type, open that chat and insert the bot's + // username and the specified inline query in the input field. Not + // supported for messages sent in channel direct messages chats and on + // behalf of a business account. + // + // optional + SwitchInlineQueryChosenChat *SwitchInlineQueryChosenChat `json:"switch_inline_query_chosen_chat,omitempty"` + // CopyText is 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"` +} + // RichBlockListItem is an item of a rich formatted list. type RichBlockListItem struct { // Label of the item. @@ -7592,12 +7746,13 @@ type RichBlockListItem struct { // message, referenced from InputRichMessage.HTML or InputRichMessage.Markdown. type InputRichMessageMedia struct { // ID is the unique identifier of the media used in a tg://photo?id=, - // tg://video?id=, or tg://audio?id= link. 1-64 characters; only A-Z, a-z, - // 0-9, _ and - are allowed. + // tg://video?id=, tg://document?id=, or tg://audio?id= link. 1-64 + // characters; only A-Z, a-z, 0-9, _ and - are allowed. ID string `json:"id"` // Media to be sent; one of InputMediaAnimation, InputMediaAudio, - // InputMediaPhoto, InputMediaVideo, or InputMediaVoiceNote. Everything - // except the media itself and its properties is ignored. + // InputMediaDocument, InputMediaPhoto, InputMediaVideo, or + // InputMediaVoiceNote. Everything except the media itself and its + // properties is ignored. Media interface{} `json:"media"` } @@ -7636,7 +7791,7 @@ type InputRichBlock struct { // Type of the block, one of the RichBlockType* constants. Type string `json:"type"` // Text is the block text. Set for "paragraph", "heading", "pre", - // "footer", "pullquote", and "thinking" blocks. + // "footer", "expandable_blockquote", "pullquote", and "thinking" blocks. // // optional Text *RichText `json:"text,omitempty"` @@ -7666,7 +7821,8 @@ type InputRichBlock struct { // // optional Blocks []InputRichBlock `json:"blocks,omitempty"` - // Credit is the credit of "blockquote" and "pullquote" blocks. + // Credit is the credit of "blockquote", "expandable_blockquote", and + // "pullquote" blocks. // // optional Credit *RichText `json:"credit,omitempty"` @@ -7682,6 +7838,20 @@ type InputRichBlock struct { // // optional IsStriped bool `json:"is_striped,omitempty"` + // IsCompact, if true, gives the cells of a "table" block smaller indents. + // + // optional + IsCompact bool `json:"is_compact,omitempty"` + // Buttons is the list of 1-8 buttons of a "buttons" block, shown in one + // row. + // + // optional + Buttons []RichMessageButton `json:"buttons,omitempty"` + // Align is the horizontal alignment of the buttons of a "buttons" block; + // one of "left", "center", or "right". + // + // optional + Align string `json:"align,omitempty"` // Summary is the always-shown summary of a "details" block. // // optional @@ -7716,6 +7886,11 @@ type InputRichBlock struct { // // optional Audio *InputMediaAudio `json:"audio,omitempty"` + // Document is the general file of a "document" block. Its caption is + // ignored. + // + // optional + Document *InputMediaDocument `json:"document,omitempty"` // Photo is the photo of a "photo" block. Its caption is ignored. // // optional @@ -7730,8 +7905,8 @@ type InputRichBlock struct { // optional VoiceNote *InputMediaVoiceNote `json:"voice_note,omitempty"` // Caption is the caption of a media block ("collage", "slideshow", "map", - // "animation", "audio", "photo", "video", "voice_note"). It shares the - // "caption" wire field with TableCaption. + // "animation", "audio", "document", "photo", "video", "voice_note"). It + // shares the "caption" wire field with TableCaption. // // optional Caption *RichBlockCaption `json:"-"` @@ -7815,6 +7990,28 @@ type CommunityChatAdded struct { // from a community. Currently holds no information. type CommunityChatRemoved struct{} +// CommunityChatJoined describes a service message about a chat being joined by +// a user from a community. +type CommunityChatJoined struct { + // Community is the community from which the chat was joined. + Community Community `json:"community"` +} + +// MessageGenerationStopped describes an update about a user stopping message +// generation. +type MessageGenerationStopped struct { + // Chat in which the message is generated. + Chat Chat `json:"chat"` + // MessageThreadID is the unique identifier of the message thread in which + // the message is generated. + // + // optional + MessageThreadID int `json:"message_thread_id,omitempty"` + // DraftID is the unique identifier of the message draft which was + // stopped. + DraftID int64 `json:"draft_id"` +} + // BotSubscriptionUpdated contains information about changes to a user payment // subscription toward the current bot. type BotSubscriptionUpdated struct { From e3fbc43fe9992900618b32af5013455bf8de8c09 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Oct 2026 02:54:08 +0000 Subject: [PATCH 60/65] Switch module path to github.com/bssth/telegram-bot-api/v5 The upstream repository is abandoned, so the module now lives under this fork and can be installed with go get github.com/bssth/telegram-bot-api/v5. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EHdMw9fUyf4m1g524X8rhj --- book.toml | 2 +- docs/examples/command-handling.md | 2 +- docs/examples/inline-keyboard.md | 2 +- docs/examples/keyboard.md | 2 +- docs/getting-started/README.md | 6 +++--- go.mod | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/book.toml b/book.toml index 841d5ba6..1c0d4b4a 100644 --- a/book.toml +++ b/book.toml @@ -6,4 +6,4 @@ src = "docs" title = "Go Telegram Bot API" [output.html] -git-repository-url = "https://github.com/go-telegram-bot-api/telegram-bot-api" +git-repository-url = "https://github.com/bssth/telegram-bot-api" diff --git a/docs/examples/command-handling.md b/docs/examples/command-handling.md index d1d8b29d..3f01c9c2 100644 --- a/docs/examples/command-handling.md +++ b/docs/examples/command-handling.md @@ -9,7 +9,7 @@ import ( "log" "os" - tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" + tgbotapi "github.com/bssth/telegram-bot-api/v5" ) func main() { diff --git a/docs/examples/inline-keyboard.md b/docs/examples/inline-keyboard.md index e14ee631..1a0f204c 100644 --- a/docs/examples/inline-keyboard.md +++ b/docs/examples/inline-keyboard.md @@ -11,7 +11,7 @@ import ( "log" "os" - tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" + tgbotapi "github.com/bssth/telegram-bot-api/v5" ) var numericKeyboard = tgbotapi.NewInlineKeyboardMarkup( diff --git a/docs/examples/keyboard.md b/docs/examples/keyboard.md index d67a9154..44fd9b98 100644 --- a/docs/examples/keyboard.md +++ b/docs/examples/keyboard.md @@ -10,7 +10,7 @@ import ( "log" "os" - tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" + tgbotapi "github.com/bssth/telegram-bot-api/v5" ) var numericKeyboard = tgbotapi.NewReplyKeyboard( diff --git a/docs/getting-started/README.md b/docs/getting-started/README.md index 25b77f74..4803e9c0 100644 --- a/docs/getting-started/README.md +++ b/docs/getting-started/README.md @@ -10,7 +10,7 @@ approaches to solve common problems. ## Installing ```bash -go get -u github.com/go-telegram-bot-api/telegram-bot-api/v5 +go get -u github.com/bssth/telegram-bot-api/v5 ``` ## A Simple Bot @@ -22,7 +22,7 @@ messages repeating what you said. Make sure you get an API token from Let's start by constructing a new [BotAPI][bot-api-docs]. [botfather]: https://t.me/Botfather -[bot-api-docs]: https://pkg.go.dev/github.com/go-telegram-bot-api/telegram-bot-api/v5?tab=doc#BotAPI +[bot-api-docs]: https://pkg.go.dev/github.com/bssth/telegram-bot-api/v5?tab=doc#BotAPI ```go package main @@ -30,7 +30,7 @@ package main import ( "os" - tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" + tgbotapi "github.com/bssth/telegram-bot-api/v5" ) func main() { diff --git a/go.mod b/go.mod index f793bc6f..0cc06c61 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,3 @@ -module github.com/kirugan/telegram-bot-api/v5 +module github.com/bssth/telegram-bot-api/v5 go 1.24 From 04bcb9aed0c78cb2b8985cc9a7706b14f7afbb4c Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Oct 2026 02:54:08 +0000 Subject: [PATCH 61/65] Close remaining gaps against Bot API 10.3 Checked every type and method against the machine-readable Bot API 10.3 specification and fixed what was missing or wrong: Types - Replace the raw-JSON placeholders VideoQuality, UserProfileAudios, UserRating, GiftBackground and UniqueGiftColors with real structs. - OwnedGift: decode the polymorphic "gift" field into Gift or UniqueGift by type (it was read from a non-existent "unique_gift" field). - PollOptionAdded/PollOptionDeleted: add option_persistent_id, option_text and option_text_entities. - MessageEntity: add unix_time and date_time_format for date_time entities; ExternalReplyInfo: add checklist; ChatOwnerLeft: add new_owner; TransactionPartner: add paid_media_payload. - InlineQueryResultVideo/Document: add parse_mode and caption_entities. - Telegram Passport: add translation and hash to EncryptedPassportElement and the missing PassportElementErrorTranslationFile(s) and PassportElementErrorUnspecified types. - Move the getChat-only fields from Chat to ChatFullInfo, where Telegram actually returns them, and drop Message.PremiumAnimation, which is not part of the Bot API. Methods - Add SetPassportDataErrorsConfig (setPassportDataErrors). - Use getChatMemberCount instead of the deprecated getChatMembersCount. - Add suggested_post_parameters to all send methods via BaseChat. - getUserGifts/getChatGifts: add all filter and sorting parameters. - repostStory: use from_story_id and active_period (it sent postStory parameters). - setStickerSetThumbnail: add format; allow dropping the thumbnail. - setGameScore: send force, and send a zero score. - sendDocument: actually send caption_entities. - sendVideo/sendAnimation: add width and height. - editMessageText: do not send an empty text along with rich_message. Uploads - sendPoll (media, options and explanation media) and rich messages (sendRichMessage, sendRichMessageDraft, editMessageText, editEphemeralMessageText) can now upload new files via attach:// instead of serializing local paths or bytes into the JSON. Other - Add UpdateTypeChatJoinRequest and UpdateTypeGuestMessage. - Update.SentFrom/FromChat cover all update kinds and no longer panic on callback queries from inline messages. - Redact the bot token from transport errors returned by MakeRequest and UploadFiles. - Add NewEditMessageMedia and NewInputMedia{LivePhoto,Sticker,Location, Venue,Link} helpers. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EHdMw9fUyf4m1g524X8rhj --- bot.go | 19 +- configs.go | 243 ++++++++++++++++++----- helpers.go | 65 ++++++ nested_media.go | 209 ++++++++++++++++++++ passport.go | 71 +++++++ spec_fixes_test.go | 241 +++++++++++++++++++++++ types.go | 481 ++++++++++++++++++++++++++++----------------- upload_e2e_test.go | 149 ++++++++++++++ 8 files changed, 1256 insertions(+), 222 deletions(-) create mode 100644 nested_media.go create mode 100644 spec_fixes_test.go create mode 100644 upload_e2e_test.go diff --git a/bot.go b/bot.go index e9ea2aa2..2443d93b 100644 --- a/bot.go +++ b/bot.go @@ -104,6 +104,21 @@ func buildParams(in Params) url.Values { return out } +// redactToken hides the bot token in transport errors. A *url.Error embeds +// the full request URL in its message, and the URL contains the token, so +// returning (and then logging) it verbatim would leak the token. +func (bot *BotAPI) redactToken(err error) error { + urlErr, ok := err.(*url.Error) + if !ok || bot.Token == "" { + return err + } + + redacted := *urlErr + redacted.URL = strings.ReplaceAll(redacted.URL, bot.Token, "") + + return &redacted +} + // closeBody drains any unread bytes before closing the response body so that // net/http can return the underlying connection to the keep-alive pool. // json.Decoder may stop short of EOF (trailing whitespace, partial errors), @@ -131,7 +146,7 @@ func (bot *BotAPI) MakeRequest(endpoint string, params Params) (*APIResponse, er resp, err := bot.Client.Do(req) if err != nil { - return nil, err + return nil, bot.redactToken(err) } defer closeBody(resp.Body) @@ -255,7 +270,7 @@ func (bot *BotAPI) UploadFiles(endpoint string, params Params, files []RequestFi resp, err := bot.Client.Do(req) if err != nil { - return nil, err + return nil, bot.redactToken(err) } defer closeBody(resp.Body) diff --git a/configs.go b/configs.go index fb4bd659..dfa99b99 100644 --- a/configs.go +++ b/configs.go @@ -127,6 +127,15 @@ const ( // this update in the list of allowed_updates to receive these updates. UpdateTypeChatMember = "chat_member" + // UpdateTypeChatJoinRequest is when a request to join the chat has been sent. + // The bot must have the can_invite_users administrator right in the chat to + // receive these updates. + UpdateTypeChatJoinRequest = "chat_join_request" + + // UpdateTypeGuestMessage is a new guest message. The bot can use + // Message.GuestQueryID and the answerGuestQuery method to reply to it. + UpdateTypeGuestMessage = "guest_message" + // UpdateTypeManagedBot is when a new bot was created to be managed by the bot, // or token or owner of a managed bot was changed. UpdateTypeManagedBot = "managed_bot" @@ -321,6 +330,11 @@ type BaseChat struct { ReplyParameters *ReplyParameters ReplyMarkup interface{} DisableNotification bool + // SuggestedPostParameters contains the parameters of the suggested post + // to send; for direct messages chats only. If the message is sent as a + // reply to another suggested post, then that suggested post is + // automatically declined. + SuggestedPostParameters *SuggestedPostParameters } func (chat *BaseChat) params() (Params, error) { @@ -340,6 +354,9 @@ func (chat *BaseChat) params() (Params, error) { if err := params.AddAny("reply_parameters", chat.ReplyParameters); err != nil { return params, err } + if err := params.AddAny("suggested_post_parameters", chat.SuggestedPostParameters); err != nil { + return params, err + } err := params.AddAny("reply_markup", chat.ReplyMarkup) return params, err @@ -641,6 +658,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) + if err = params.AddAny("caption_entities", config.CaptionEntities); err != nil { + return params, err + } err = config.EphemeralSendParams.addTo(params) @@ -707,6 +727,8 @@ type VideoConfig struct { Cover RequestFileData StartTimestamp int Duration int + Width int + Height int Caption string ParseMode string CaptionEntities []MessageEntity @@ -722,6 +744,8 @@ func (config VideoConfig) params() (Params, error) { } params.AddNonZero("duration", config.Duration) + params.AddNonZero("width", config.Width) + params.AddNonZero("height", config.Height) params.AddNonZero("start_timestamp", config.StartTimestamp) params.AddNonEmpty("caption", config.Caption) params.AddNonEmpty("parse_mode", config.ParseMode) @@ -822,6 +846,8 @@ type AnimationConfig struct { BaseFile EphemeralSendParams Duration int + Width int + Height int Thumbnail RequestFileData Caption string ParseMode string @@ -837,6 +863,8 @@ func (config AnimationConfig) params() (Params, error) { } params.AddNonZero("duration", config.Duration) + params.AddNonZero("width", config.Width) + params.AddNonZero("height", config.Height) params.AddNonEmpty("caption", config.Caption) params.AddNonEmpty("parse_mode", config.ParseMode) params.AddBool("show_caption_above_media", config.ShowCaptionAboveMedia) @@ -1139,12 +1167,33 @@ type SendPollConfig struct { IsClosed bool } +// prepareMedia returns copies of the poll's media, explanation media and +// options in which files that need uploading are replaced by attach:// +// references, together with the files to upload. +func (config SendPollConfig) prepareMedia() (media, explanationMedia any, options []InputPollOption, files []RequestFile) { + u := nestedMediaUploader{prefix: "poll-media"} + + media = u.media(config.Media) + if config.Options != nil { + options = make([]InputPollOption, len(config.Options)) + for i, option := range config.Options { + option.Media = u.media(option.Media) + options[i] = option + } + } + explanationMedia = u.media(config.ExplanationMedia) + + return media, explanationMedia, options, u.files +} + func (config SendPollConfig) params() (Params, error) { params, err := config.BaseChat.params() if err != nil { return params, err } + media, explanationMedia, options, _ := config.prepareMedia() + params["question"] = config.Question params.AddNonEmpty("question_parse_mode", config.QuestionParseMode) if err = params.AddAny("question_entities", config.QuestionEntities); err != nil { @@ -1155,10 +1204,10 @@ func (config SendPollConfig) params() (Params, error) { if err = params.AddAny("description_entities", config.DescriptionEntities); err != nil { return params, err } - if err = params.AddAny("media", config.Media); err != nil { + if err = params.AddAny("media", media); err != nil { return params, err } - if err = params.AddAny("options", config.Options); err != nil { + if err = params.AddAny("options", options); err != nil { return params, err } params["is_anonymous"] = strconv.FormatBool(config.IsAnonymous) @@ -1187,7 +1236,7 @@ func (config SendPollConfig) params() (Params, error) { if err = params.AddAny("explanation_entities", config.ExplanationEntities); err != nil { return params, err } - err = params.AddAny("explanation_media", config.ExplanationMedia) + err = params.AddAny("explanation_media", explanationMedia) return params, err } @@ -1196,6 +1245,11 @@ func (SendPollConfig) method() string { return "sendPoll" } +func (config SendPollConfig) files() []RequestFile { + _, _, _, files := config.prepareMedia() + return files +} + // GameConfig allows you to send a game. type GameConfig struct { BaseChat @@ -1230,7 +1284,8 @@ func (config SetGameScoreConfig) params() (Params, error) { params := make(Params) params.AddNonZero64("user_id", config.UserID) - params.AddNonZero("score", config.Score) + params["score"] = strconv.Itoa(config.Score) + params.AddBool("force", config.Force) params.AddBool("disable_edit_message", config.DisableEditMessage) if config.InlineMessageID != "" { @@ -1315,15 +1370,22 @@ func (config EditMessageTextConfig) params() (Params, error) { return params, err } - params["text"] = config.Text + // text stays present when RichMessage is not set; an empty string is + // rejected by the API rather than silently dropped. + if config.RichMessage == nil { + params["text"] = config.Text + } else { + params.AddNonEmpty("text", config.Text) + richMessage, _ := prepareRichMessage(config.RichMessage) + if err = params.AddAny("rich_message", richMessage); err != nil { + return params, err + } + } params.AddNonEmpty("parse_mode", config.ParseMode) if err = params.AddAny("link_preview_options", config.LinkPreviewOptions); err != nil { return params, err } - if err = params.AddAny("entities", config.Entities); err != nil { - return params, err - } - err = params.AddAny("rich_message", config.RichMessage) + err = params.AddAny("entities", config.Entities) return params, err } @@ -1332,6 +1394,11 @@ func (config EditMessageTextConfig) method() string { return "editMessageText" } +func (config EditMessageTextConfig) files() []RequestFile { + _, files := prepareRichMessage(config.RichMessage) + return files +} + // EditMessageCaptionConfig allows you to modify the caption of a message. type EditMessageCaptionConfig struct { BaseEdit @@ -1464,7 +1531,8 @@ func (config EditEphemeralMessageTextConfig) params() (Params, error) { params["text"] = config.Text } else { params.AddNonEmpty("text", config.Text) - if err = params.AddAny("rich_message", config.RichMessage); err != nil { + richMessage, _ := prepareRichMessage(config.RichMessage) + if err = params.AddAny("rich_message", richMessage); err != nil { return params, err } } @@ -1484,6 +1552,11 @@ func (config EditEphemeralMessageTextConfig) method() string { return "editEphemeralMessageText" } +func (config EditEphemeralMessageTextConfig) files() []RequestFile { + _, files := prepareRichMessage(config.RichMessage) + return files +} + // EditEphemeralMessageMediaConfig allows you to edit the media of an ephemeral // message. A new file can be uploaded, or use a previously uploaded file via // its file_id, or specify a URL. @@ -2007,7 +2080,10 @@ type BanChatSenderChatConfig struct { ChatID int64 ChannelUsername string SenderChatID int64 - UntilDate int + // Deprecated: banChatSenderChat no longer accepts until_date; sender chats + // are banned until they are explicitly unbanned. The value is ignored by + // Telegram. + UntilDate int } func (config BanChatSenderChatConfig) method() string { @@ -2081,7 +2157,7 @@ type ChatMemberCountConfig struct { } func (ChatMemberCountConfig) method() string { - return "getChatMembersCount" + return "getChatMemberCount" } // ChatAdministratorsConfig contains information about getting chat administrators. @@ -3748,6 +3824,10 @@ type SetStickerSetThumbnailConfig struct { Name string UserID int64 Thumbnail RequestFileData + // Format of the thumbnail, must be one of StickerFormatStatic for a .WEBP + // or .PNG image, StickerFormatAnimated for a .TGS animation, or + // StickerFormatVideo for a .WEBM video. + Format string } func (config SetStickerSetThumbnailConfig) method() string { @@ -3759,11 +3839,16 @@ func (config SetStickerSetThumbnailConfig) params() (Params, error) { params["name"] = config.Name params.AddNonZero64("user_id", config.UserID) + params.AddNonEmpty("format", config.Format) return params, nil } func (config SetStickerSetThumbnailConfig) files() []RequestFile { + if config.Thumbnail == nil { + return nil + } + return []RequestFile{{ Name: "thumbnail", Data: config.Thumbnail, @@ -4920,9 +5005,15 @@ func (config SendMessageDraftConfig) params() (Params, error) { // GetUserGiftsConfig returns the list of gifts received and owned by a user. type GetUserGiftsConfig struct { - UserID int64 - Offset string - Limit int + UserID int64 + ExcludeUnlimited bool + ExcludeLimitedUpgradable bool + ExcludeLimitedNonUpgradable bool + ExcludeFromBlockchain bool + ExcludeUnique bool + SortByPrice bool + Offset string + Limit int } func (GetUserGiftsConfig) method() string { @@ -4933,6 +5024,12 @@ func (config GetUserGiftsConfig) params() (Params, error) { params := make(Params) params.AddNonZero64("user_id", config.UserID) + params.AddBool("exclude_unlimited", config.ExcludeUnlimited) + params.AddBool("exclude_limited_upgradable", config.ExcludeLimitedUpgradable) + params.AddBool("exclude_limited_non_upgradable", config.ExcludeLimitedNonUpgradable) + params.AddBool("exclude_from_blockchain", config.ExcludeFromBlockchain) + params.AddBool("exclude_unique", config.ExcludeUnique) + params.AddBool("sort_by_price", config.SortByPrice) params.AddNonEmpty("offset", config.Offset) params.AddNonZero("limit", config.Limit) @@ -4945,10 +5042,18 @@ func (config GetUserGiftsConfig) params() (Params, error) { // ChannelUsername ("@channelusername"); the first non-zero / non-empty // value is used. type GetChatGiftsConfig struct { - ChatID int64 - ChannelUsername string - Offset string - Limit int + ChatID int64 + ChannelUsername string + ExcludeUnsaved bool + ExcludeSaved bool + ExcludeUnlimited bool + ExcludeLimitedUpgradable bool + ExcludeLimitedNonUpgradable bool + ExcludeFromBlockchain bool + ExcludeUnique bool + SortByPrice bool + Offset string + Limit int } func (GetChatGiftsConfig) method() string { @@ -4961,24 +5066,39 @@ func (config GetChatGiftsConfig) params() (Params, error) { if err := params.AddFirstValid("chat_id", config.ChatID, config.ChannelUsername); err != nil { return params, err } + params.AddBool("exclude_unsaved", config.ExcludeUnsaved) + params.AddBool("exclude_saved", config.ExcludeSaved) + params.AddBool("exclude_unlimited", config.ExcludeUnlimited) + params.AddBool("exclude_limited_upgradable", config.ExcludeLimitedUpgradable) + params.AddBool("exclude_limited_non_upgradable", config.ExcludeLimitedNonUpgradable) + params.AddBool("exclude_from_blockchain", config.ExcludeFromBlockchain) + params.AddBool("exclude_unique", config.ExcludeUnique) + params.AddBool("sort_by_price", config.SortByPrice) params.AddNonEmpty("offset", config.Offset) params.AddNonZero("limit", config.Limit) return params, nil } -// RepostStoryConfig reposts a story across different business accounts -// managed by the bot. +// RepostStoryConfig reposts a story on behalf of a business account from +// another business account. Both business accounts must be managed by the +// same bot, and the story on the source account must have been posted (or +// reposted) by the bot. Requires the can_manage_stories business bot right +// for both business accounts. type RepostStoryConfig struct { BusinessConnectionID string - FromChatID int64 - StoryID int - Caption string - ParseMode string - CaptionEntities []MessageEntity - Areas []StoryArea - PostToChatPage bool - ProtectContent bool + // FromChatID is the unique identifier of the chat which posted the story + // that should be reposted. + FromChatID int64 + // FromStoryID is the unique identifier of the story that should be + // reposted. + FromStoryID int + // ActivePeriod is the period after which the story is moved to the + // archive, in seconds; must be one of 6 * 3600, 12 * 3600, 86400, or + // 2 * 86400. + ActivePeriod int + PostToChatPage bool + ProtectContent bool } func (RepostStoryConfig) method() string { @@ -4990,17 +5110,12 @@ func (config RepostStoryConfig) params() (Params, error) { params["business_connection_id"] = config.BusinessConnectionID params.AddNonZero64("from_chat_id", config.FromChatID) - params.AddNonZero("story_id", config.StoryID) - params.AddNonEmpty("caption", config.Caption) - params.AddNonEmpty("parse_mode", config.ParseMode) + params.AddNonZero("from_story_id", config.FromStoryID) + params.AddNonZero("active_period", config.ActivePeriod) params.AddBool("post_to_chat_page", config.PostToChatPage) params.AddBool("protect_content", config.ProtectContent) - if err := params.AddAny("caption_entities", config.CaptionEntities); err != nil { - return params, err - } - err := params.AddAny("areas", config.Areas) - return params, err + return params, nil } // DeleteStoryConfig deletes a story previously posted by the bot on behalf @@ -5510,9 +5625,6 @@ type SendRichMessageConfig struct { EphemeralSendParams // RichMessage is the message to be sent. RichMessage *InputRichMessage - // SuggestedPostParameters contains the parameters of the suggested post - // to send; for direct messages chats only. - SuggestedPostParameters *SuggestedPostParameters } func (config SendRichMessageConfig) params() (Params, error) { @@ -5521,10 +5633,8 @@ func (config SendRichMessageConfig) params() (Params, error) { return params, err } - if err = params.AddAny("rich_message", config.RichMessage); err != nil { - return params, err - } - if err = params.AddAny("suggested_post_parameters", config.SuggestedPostParameters); err != nil { + richMessage, _ := prepareRichMessage(config.RichMessage) + if err = params.AddAny("rich_message", richMessage); err != nil { return params, err } err = config.EphemeralSendParams.addTo(params) @@ -5536,6 +5646,11 @@ func (config SendRichMessageConfig) method() string { return "sendRichMessage" } +func (config SendRichMessageConfig) files() []RequestFile { + _, files := prepareRichMessage(config.RichMessage) + return files +} + // SendRichMessageDraftConfig streams a partial rich message to a user while // the message is being generated. The streamed draft is ephemeral and acts as // a temporary 30-second preview; once the output is finalized, sendRichMessage @@ -5570,7 +5685,8 @@ func (config SendRichMessageDraftConfig) params() (Params, error) { params.AddNonZero("draft_id", config.DraftID) params.AddBool("can_stop", config.CanStop) params.AddBool("keep_on_stop", config.KeepOnStop) - err := params.AddAny("rich_message", config.RichMessage) + richMessage, _ := prepareRichMessage(config.RichMessage) + err := params.AddAny("rich_message", richMessage) return params, err } @@ -5579,6 +5695,11 @@ func (config SendRichMessageDraftConfig) method() string { return "sendRichMessageDraft" } +func (config SendRichMessageDraftConfig) files() []RequestFile { + _, files := prepareRichMessage(config.RichMessage) + return files +} + // Result values for AnswerChatJoinRequestQueryConfig. const ( // ChatJoinRequestApprove allows the user to join the chat. @@ -5638,3 +5759,35 @@ func (config SendChatJoinRequestWebAppConfig) params() (Params, error) { func (config SendChatJoinRequestWebAppConfig) method() string { return "sendChatJoinRequestWebApp" } + +// SetPassportDataErrorsConfig informs a user that some of the Telegram +// Passport elements they provided contains errors. The user will not be able +// to re-submit their Passport to you until the errors are fixed (the contents +// of the field for which you returned the error must change). +// +// Errors must hold PassportElementError* values, for example +// PassportElementErrorDataField or PassportElementErrorUnspecified. +type SetPassportDataErrorsConfig struct { + // UserID is the user identifier. + UserID int64 + // Errors is the list of errors describing what is wrong with the + // submitted Passport data. + Errors []PassportElementError +} + +func (config SetPassportDataErrorsConfig) method() string { + return "setPassportDataErrors" +} + +func (config SetPassportDataErrorsConfig) params() (Params, error) { + params := make(Params) + + params.AddNonZero64("user_id", config.UserID) + if config.Errors == nil { + params["errors"] = "[]" + return params, nil + } + err := params.AddAny("errors", config.Errors) + + return params, err +} diff --git a/helpers.go b/helpers.go index a18a5e18..5f84151e 100644 --- a/helpers.go +++ b/helpers.go @@ -265,6 +265,58 @@ func NewInputMediaDocument(media RequestFileData) InputMediaDocument { } } +// NewInputMediaLivePhoto creates a new InputMediaLivePhoto. Media is the +// live photo video and photo is its static image. +func NewInputMediaLivePhoto(media, photo RequestFileData) InputMediaLivePhoto { + return InputMediaLivePhoto{ + BaseInputMedia: BaseInputMedia{ + Type: "live_photo", + Media: media, + }, + Photo: photo, + } +} + +// NewInputMediaSticker creates a new InputMediaSticker, which can be used as +// the media of a poll option. +func NewInputMediaSticker(media RequestFileData) InputMediaSticker { + return InputMediaSticker{ + Type: "sticker", + Media: media, + } +} + +// NewInputMediaLocation creates a new InputMediaLocation, which can be used +// as the media of a poll or a poll option. +func NewInputMediaLocation(latitude, longitude float64) InputMediaLocation { + return InputMediaLocation{ + Type: "location", + Latitude: latitude, + Longitude: longitude, + } +} + +// NewInputMediaVenue creates a new InputMediaVenue, which can be used as the +// media of a poll or a poll option. +func NewInputMediaVenue(latitude, longitude float64, title, address string) InputMediaVenue { + return InputMediaVenue{ + Type: "venue", + Latitude: latitude, + Longitude: longitude, + Title: title, + Address: address, + } +} + +// NewInputMediaLink creates a new InputMediaLink, which can be used as the +// media of a poll option. +func NewInputMediaLink(url string) InputMediaLink { + return InputMediaLink{ + Type: "link", + URL: url, + } +} + // NewContact allows you to send a shared contact. func NewContact(chatID int64, phoneNumber, firstName string) ContactConfig { return ContactConfig{ @@ -629,6 +681,19 @@ func NewEditMessageCaption(chatID int64, messageID int, caption string) EditMess } } +// NewEditMessageMedia allows you to replace the media of a message. Media +// must be one of InputMediaAnimation, InputMediaAudio, InputMediaDocument, +// InputMediaLivePhoto, InputMediaPhoto or InputMediaVideo. +func NewEditMessageMedia(chatID int64, messageID int, media interface{}) EditMessageMediaConfig { + return EditMessageMediaConfig{ + BaseEdit: BaseEdit{ + ChatID: chatID, + MessageID: messageID, + }, + Media: media, + } +} + // NewEditMessageReplyMarkup allows you to edit the inline // keyboard markup. func NewEditMessageReplyMarkup(chatID int64, messageID int, replyMarkup InlineKeyboardMarkup) EditMessageReplyMarkupConfig { diff --git a/nested_media.go b/nested_media.go new file mode 100644 index 00000000..29f30095 --- /dev/null +++ b/nested_media.go @@ -0,0 +1,209 @@ +package tgbotapi + +import "fmt" + +// nestedMediaUploader rewrites InputMedia* values nested inside JSON-serialized +// parameters (poll media, rich messages) so that every file which needs +// uploading is replaced by an "attach://" reference, and collects the +// matching RequestFile entries. +// +// Names are generated sequentially from prefix, so a config's params() and +// files() produce the same names as long as both walk the media in the same +// order (which they do by calling the same prepare function). +type nestedMediaUploader struct { + prefix string + files []RequestFile +} + +func (u *nestedMediaUploader) attach(data RequestFileData) RequestFileData { + if data == nil || !data.NeedsUpload() { + return data + } + + name := fmt.Sprintf("%s-%d", u.prefix, len(u.files)) + u.files = append(u.files, RequestFile{Name: name, Data: data}) + + return fileAttach("attach://" + name) +} + +func (u *nestedMediaUploader) base(b BaseInputMedia) BaseInputMedia { + b.Media = u.attach(b.Media) + return b +} + +func (u *nestedMediaUploader) photo(m *InputMediaPhoto) *InputMediaPhoto { + if m == nil { + return nil + } + c := *m + c.BaseInputMedia = u.base(c.BaseInputMedia) + return &c +} + +func (u *nestedMediaUploader) video(m *InputMediaVideo) *InputMediaVideo { + if m == nil { + return nil + } + c := *m + c.BaseInputMedia = u.base(c.BaseInputMedia) + c.Thumbnail = u.attach(c.Thumbnail) + c.Cover = u.attach(c.Cover) + return &c +} + +func (u *nestedMediaUploader) animation(m *InputMediaAnimation) *InputMediaAnimation { + if m == nil { + return nil + } + c := *m + c.BaseInputMedia = u.base(c.BaseInputMedia) + c.Thumbnail = u.attach(c.Thumbnail) + return &c +} + +func (u *nestedMediaUploader) audio(m *InputMediaAudio) *InputMediaAudio { + if m == nil { + return nil + } + c := *m + c.BaseInputMedia = u.base(c.BaseInputMedia) + c.Thumbnail = u.attach(c.Thumbnail) + return &c +} + +func (u *nestedMediaUploader) document(m *InputMediaDocument) *InputMediaDocument { + if m == nil { + return nil + } + c := *m + c.BaseInputMedia = u.base(c.BaseInputMedia) + c.Thumbnail = u.attach(c.Thumbnail) + return &c +} + +func (u *nestedMediaUploader) voiceNote(m *InputMediaVoiceNote) *InputMediaVoiceNote { + if m == nil { + return nil + } + c := *m + c.BaseInputMedia = u.base(c.BaseInputMedia) + return &c +} + +func (u *nestedMediaUploader) livePhoto(m *InputMediaLivePhoto) *InputMediaLivePhoto { + if m == nil { + return nil + } + c := *m + c.BaseInputMedia = u.base(c.BaseInputMedia) + c.Photo = u.attach(c.Photo) + return &c +} + +func (u *nestedMediaUploader) sticker(m *InputMediaSticker) *InputMediaSticker { + if m == nil { + return nil + } + c := *m + c.Media = u.attach(c.Media) + return &c +} + +// media prepares an InputMedia* value or pointer of any kind. Values keep +// their value type and pointers keep their pointer type; anything else (for +// example InputMediaLocation, which carries no file) is returned unchanged. +func (u *nestedMediaUploader) media(m any) any { + switch v := m.(type) { + case InputMediaPhoto: + return *u.photo(&v) + case *InputMediaPhoto: + return u.photo(v) + case InputMediaVideo: + return *u.video(&v) + case *InputMediaVideo: + return u.video(v) + case InputMediaAnimation: + return *u.animation(&v) + case *InputMediaAnimation: + return u.animation(v) + case InputMediaAudio: + return *u.audio(&v) + case *InputMediaAudio: + return u.audio(v) + case InputMediaDocument: + return *u.document(&v) + case *InputMediaDocument: + return u.document(v) + case InputMediaVoiceNote: + return *u.voiceNote(&v) + case *InputMediaVoiceNote: + return u.voiceNote(v) + case InputMediaLivePhoto: + return *u.livePhoto(&v) + case *InputMediaLivePhoto: + return u.livePhoto(v) + case InputMediaSticker: + return *u.sticker(&v) + case *InputMediaSticker: + return u.sticker(v) + } + + return m +} + +func (u *nestedMediaUploader) richBlocks(blocks []InputRichBlock) []InputRichBlock { + if blocks == nil { + return nil + } + + out := make([]InputRichBlock, len(blocks)) + for i, b := range blocks { + b.Blocks = u.richBlocks(b.Blocks) + if b.Items != nil { + items := make([]InputRichBlockListItem, len(b.Items)) + for j, item := range b.Items { + item.Blocks = u.richBlocks(item.Blocks) + items[j] = item + } + b.Items = items + } + b.Animation = u.animation(b.Animation) + b.Audio = u.audio(b.Audio) + b.Document = u.document(b.Document) + b.Photo = u.photo(b.Photo) + b.Video = u.video(b.Video) + b.VoiceNote = u.voiceNote(b.VoiceNote) + out[i] = b + } + + return out +} + +// richMessage returns a copy of m in which every file that needs uploading, +// in Media and anywhere in Blocks, is replaced by an attach:// reference. +func (u *nestedMediaUploader) richMessage(m *InputRichMessage) *InputRichMessage { + if m == nil { + return nil + } + + c := *m + c.Blocks = u.richBlocks(c.Blocks) + if c.Media != nil { + media := make([]InputRichMessageMedia, len(c.Media)) + for i, item := range c.Media { + item.Media = u.media(item.Media) + media[i] = item + } + c.Media = media + } + + return &c +} + +// prepareRichMessage returns the rich message to serialize and the files +// that must be uploaded alongside it. +func prepareRichMessage(m *InputRichMessage) (*InputRichMessage, []RequestFile) { + u := nestedMediaUploader{prefix: "rich-media"} + prepared := u.richMessage(m) + return prepared, u.files +} diff --git a/passport.go b/passport.go index 4fedb965..73a076b0 100644 --- a/passport.go +++ b/passport.go @@ -111,6 +111,19 @@ type ( // "identity_card" and "internal_passport". The file can be decrypted // and verified using the accompanying EncryptedCredentials. Selfie *PassportFile `json:"selfie,omitempty"` + + // Array of encrypted files with translated versions of documents + // provided by the user; available if requested for "passport", + // "driver_license", "identity_card", "internal_passport", + // "utility_bill", "bank_statement", "rental_agreement", + // "passport_registration" and "temporary_registration" types. Files + // can be decrypted and verified using the accompanying + // EncryptedCredentials. + Translation []PassportFile `json:"translation,omitempty"` + + // Base64-encoded element hash for using in + // PassportElementErrorUnspecified + Hash string `json:"hash"` } // EncryptedCredentials contains data required for decrypting and @@ -248,6 +261,64 @@ type ( Message string `json:"message"` } + // PassportElementErrorTranslationFile represents an issue with one of the + // files that constitute the translation of a document. The error is + // considered resolved when the file changes. + PassportElementErrorTranslationFile struct { + // Error source, must be translation_file + Source string `json:"source"` + + // Type of element of the user's Telegram Passport which has the + // issue, one of "passport", "driver_license", "identity_card", + // "internal_passport", "utility_bill", "bank_statement", + // "rental_agreement", "passport_registration", + // "temporary_registration" + Type string `json:"type"` + + // Base64-encoded file hash + FileHash string `json:"file_hash"` + + // Error message + Message string `json:"message"` + } + + // PassportElementErrorTranslationFiles represents an issue with the + // translated version of a document. The error is considered resolved + // when a file with the document translation changes. + PassportElementErrorTranslationFiles struct { + // Error source, must be translation_files + Source string `json:"source"` + + // Type of element of the user's Telegram Passport which has the + // issue, one of "passport", "driver_license", "identity_card", + // "internal_passport", "utility_bill", "bank_statement", + // "rental_agreement", "passport_registration", + // "temporary_registration" + Type string `json:"type"` + + // List of base64-encoded file hashes + FileHashes []string `json:"file_hashes"` + + // Error message + Message string `json:"message"` + } + + // PassportElementErrorUnspecified represents an issue in an unspecified + // place. The error is considered resolved when new data is added. + PassportElementErrorUnspecified struct { + // Error source, must be unspecified + Source string `json:"source"` + + // Type of element of the user's Telegram Passport which has the issue + Type string `json:"type"` + + // Base64-encoded element hash + ElementHash string `json:"element_hash"` + + // Error message + Message string `json:"message"` + } + // Credentials contains encrypted data. Credentials struct { Data SecureData `json:"secure_data"` diff --git a/spec_fixes_test.go b/spec_fixes_test.go new file mode 100644 index 00000000..ef7b66d6 --- /dev/null +++ b/spec_fixes_test.go @@ -0,0 +1,241 @@ +package tgbotapi + +import ( + "encoding/json" + "strings" + "testing" +) + +func TestOwnedGiftGiftRouting(t *testing.T) { + var regular OwnedGift + if err := json.Unmarshal([]byte(`{"type":"regular","gift":{"id":"g1","star_count":10},"send_date":1}`), ®ular); err != nil { + t.Fatalf("regular unmarshal: %v", err) + } + if regular.Gift == nil || regular.Gift.ID != "g1" || regular.UniqueGift != nil { + t.Fatalf("regular gift not routed to Gift: %+v", regular) + } + + var unique OwnedGift + if err := json.Unmarshal([]byte(`{"type":"unique","gift":{"base_name":"Cap","name":"Cap-1","number":1},"send_date":1}`), &unique); err != nil { + t.Fatalf("unique unmarshal: %v", err) + } + if unique.UniqueGift == nil || unique.UniqueGift.Name != "Cap-1" || unique.Gift != nil { + t.Fatalf("unique gift not routed to UniqueGift: %+v", unique) + } + + out, err := json.Marshal(unique) + if err != nil { + t.Fatalf("marshal: %v", err) + } + if !strings.Contains(string(out), `"gift":{`) || strings.Contains(string(out), "unique_gift") { + t.Fatalf("unique gift not emitted under \"gift\": %s", out) + } +} + +func TestSpecTypesDecode(t *testing.T) { + var video Video + if err := json.Unmarshal([]byte(`{"file_id":"v","file_unique_id":"u","width":1,"height":1,"duration":1, + "qualities":[{"file_id":"q","file_unique_id":"qu","width":1920,"height":1080,"codec":"av01","file_size":42}]}`), &video); err != nil { + t.Fatalf("video unmarshal: %v", err) + } + if len(video.Qualities) != 1 || video.Qualities[0].Codec != "av01" || video.Qualities[0].Height != 1080 || video.Qualities[0].FileSize != 42 { + t.Fatalf("video qualities not decoded: %+v", video.Qualities) + } + + var chat ChatFullInfo + if err := json.Unmarshal([]byte(`{"id":1,"type":"private","bio":"hi","accent_color_id":3,"max_reaction_count":11, + "accepted_gift_types":{},"rating":{"level":2,"rating":150,"current_level_rating":100,"next_level_rating":200}, + "unique_gift_colors":{"model_custom_emoji_id":"m","symbol_custom_emoji_id":"s","light_theme_main_color":1, + "light_theme_other_colors":[2,3],"dark_theme_main_color":4,"dark_theme_other_colors":[5]}}`), &chat); err != nil { + t.Fatalf("chat unmarshal: %v", err) + } + if chat.ID != 1 || chat.Bio != "hi" || chat.AccentColorID != 3 { + t.Fatalf("chat full info fields not decoded: %+v", chat) + } + if chat.Rating == nil || chat.Rating.Level != 2 || chat.Rating.NextLevelRating != 200 { + t.Fatalf("user rating not decoded: %+v", chat.Rating) + } + if chat.UniqueGiftColors == nil || len(chat.UniqueGiftColors.LightThemeOtherColors) != 2 { + t.Fatalf("unique gift colors not decoded: %+v", chat.UniqueGiftColors) + } + + var msg Message + if err := json.Unmarshal([]byte(`{"message_id":1,"date":1,"chat":{"id":1,"type":"group"}, + "poll_option_added":{"option_persistent_id":"p","option_text":"new","option_text_entities":[{"type":"bold","offset":0,"length":3}]}, + "chat_owner_left":{"new_owner":{"id":7,"is_bot":false,"first_name":"N"}}, + "entities":[{"type":"date_time","offset":0,"length":1,"unix_time":1700000000,"date_time_format":"wDT"}]}`), &msg); err != nil { + t.Fatalf("message unmarshal: %v", err) + } + if msg.PollOptionAdded == nil || msg.PollOptionAdded.OptionText != "new" || len(msg.PollOptionAdded.OptionTextEntities) != 1 { + t.Fatalf("poll option added not decoded: %+v", msg.PollOptionAdded) + } + if msg.ChatOwnerLeft == nil || msg.ChatOwnerLeft.NewOwner == nil || msg.ChatOwnerLeft.NewOwner.ID != 7 { + t.Fatalf("chat owner left not decoded: %+v", msg.ChatOwnerLeft) + } + if len(msg.Entities) != 1 || msg.Entities[0].UnixTime != 1700000000 || msg.Entities[0].DateTimeFormat != "wDT" { + t.Fatalf("date_time entity not decoded: %+v", msg.Entities) + } +} + +func TestUpdateFromChatInlineCallback(t *testing.T) { + // Callback queries from inline messages carry no message; FromChat must + // not panic. + update := Update{CallbackQuery: &CallbackQuery{ID: "1", InlineMessageID: "inline"}} + if chat := update.FromChat(); chat != nil { + t.Fatalf("FromChat() = %+v, want nil", chat) + } + + update = Update{ChatJoinRequest: &ChatJoinRequest{Chat: Chat{ID: 5}, From: User{ID: 6}}} + if chat := update.FromChat(); chat == nil || chat.ID != 5 { + t.Fatalf("FromChat() for chat join request = %+v, want chat 5", chat) + } + if from := update.SentFrom(); from == nil || from.ID != 6 { + t.Fatalf("SentFrom() for chat join request = %+v, want user 6", from) + } +} + +func paramsOf(t *testing.T, c Chattable) Params { + t.Helper() + params, err := c.params() + if err != nil { + t.Fatalf("%T.params(): %v", c, err) + } + return params +} + +func TestSpecMethodParams(t *testing.T) { + params := paramsOf(t, RepostStoryConfig{BusinessConnectionID: "bc", FromChatID: 1, FromStoryID: 2, ActivePeriod: 86400}) + for key, want := range map[string]string{"business_connection_id": "bc", "from_chat_id": "1", "from_story_id": "2", "active_period": "86400"} { + if params[key] != want { + t.Errorf("repostStory %s = %q, want %q", key, params[key], want) + } + } + + params = paramsOf(t, GetChatGiftsConfig{ChatID: 1, ExcludeUnsaved: true, SortByPrice: true, ExcludeFromBlockchain: true}) + for _, key := range []string{"exclude_unsaved", "sort_by_price", "exclude_from_blockchain"} { + if params[key] != "true" { + t.Errorf("getChatGifts %s = %q, want true", key, params[key]) + } + } + + params = paramsOf(t, SetGameScoreConfig{UserID: 1, Score: 0, Force: true, ChatID: 2, MessageID: 3}) + if params["score"] != "0" || params["force"] != "true" { + t.Errorf("setGameScore params = %v, want score=0 and force=true", params) + } + + params = paramsOf(t, SetStickerSetThumbnailConfig{Name: "set", UserID: 1, Format: StickerFormatStatic}) + if params["format"] != StickerFormatStatic { + t.Errorf("setStickerSetThumbnail format = %q", params["format"]) + } + if files := (SetStickerSetThumbnailConfig{Name: "set"}).files(); len(files) != 0 { + t.Errorf("setStickerSetThumbnail without thumbnail has files: %v", files) + } + + params = paramsOf(t, DocumentConfig{ + BaseFile: BaseFile{BaseChat: BaseChat{ChatID: 1}}, + CaptionEntities: []MessageEntity{{Type: "bold", Offset: 0, Length: 1}}, + }) + if !strings.Contains(params["caption_entities"], `"bold"`) { + t.Errorf("sendDocument caption_entities = %q", params["caption_entities"]) + } + + params = paramsOf(t, VideoConfig{BaseFile: BaseFile{BaseChat: BaseChat{ChatID: 1}}, Width: 640, Height: 480}) + if params["width"] != "640" || params["height"] != "480" { + t.Errorf("sendVideo width/height = %q/%q", params["width"], params["height"]) + } + + params = paramsOf(t, MessageConfig{ + BaseChat: BaseChat{ChatID: 1, SuggestedPostParameters: &SuggestedPostParameters{SendDate: 100}}, + Text: "hi", + }) + if !strings.Contains(params["suggested_post_parameters"], `"send_date":100`) { + t.Errorf("sendMessage suggested_post_parameters = %q", params["suggested_post_parameters"]) + } + + params = paramsOf(t, SetPassportDataErrorsConfig{UserID: 1, Errors: []PassportElementError{ + PassportElementErrorUnspecified{Source: "unspecified", Type: "passport", ElementHash: "h", Message: "m"}, + }}) + if !strings.Contains(params["errors"], `"element_hash":"h"`) { + t.Errorf("setPassportDataErrors errors = %q", params["errors"]) + } + if (SetPassportDataErrorsConfig{}).method() != "setPassportDataErrors" { + t.Error("SetPassportDataErrorsConfig has the wrong method") + } + if (ChatMemberCountConfig{}).method() != "getChatMemberCount" { + t.Error("ChatMemberCountConfig uses the deprecated method name") + } +} + +func TestSendPollMediaUpload(t *testing.T) { + config := SendPollConfig{ + BaseChat: BaseChat{ChatID: 1}, + Question: "q", + Media: NewInputMediaPhoto(FileBytes{Name: "q.jpg", Bytes: []byte("q")}), + Options: []InputPollOption{ + {Text: "a", Media: NewInputMediaPhoto(FileID("existing"))}, + {Text: "b", Media: &InputMediaSticker{Type: "sticker", Media: FilePath("tests/image.jpg")}}, + }, + ExplanationMedia: InputMediaLocation{Type: "location", Latitude: 1, Longitude: 2}, + } + + params := paramsOf(t, config) + if !strings.Contains(params["media"], `"media":"attach://poll-media-0"`) { + t.Errorf("media = %s", params["media"]) + } + if !strings.Contains(params["options"], `"media":"existing"`) || !strings.Contains(params["options"], `"media":"attach://poll-media-1"`) { + t.Errorf("options = %s", params["options"]) + } + if !strings.Contains(params["explanation_media"], `"latitude":1`) { + t.Errorf("explanation_media = %s", params["explanation_media"]) + } + + files := config.files() + if len(files) != 2 || files[0].Name != "poll-media-0" || files[1].Name != "poll-media-1" { + t.Fatalf("files = %+v", files) + } + + // The caller's config must not be modified. + if _, ok := config.Media.(InputMediaPhoto).Media.(FileBytes); !ok { + t.Errorf("SendPollConfig.Media was modified: %+v", config.Media) + } +} + +func TestRichMessageMediaUpload(t *testing.T) { + photo := NewInputMediaPhoto(FilePath("tests/image.jpg")) + config := SendRichMessageConfig{ + BaseChat: BaseChat{ChatID: 1}, + RichMessage: &InputRichMessage{ + Markdown: "![](tg://photo?id=p1)", + Media: []InputRichMessageMedia{{ID: "p1", Media: NewInputMediaPhoto(FileBytes{Name: "a.jpg", Bytes: []byte("a")})}}, + Blocks: []InputRichBlock{{ + Type: "list", + Items: []InputRichBlockListItem{{Blocks: []InputRichBlock{{ + Type: "photo", + Photo: &photo, + }}}}, + }}, + }, + } + + params := paramsOf(t, config) + if !strings.Contains(params["rich_message"], `"attach://rich-media-0"`) || !strings.Contains(params["rich_message"], `"attach://rich-media-1"`) { + t.Errorf("rich_message = %s", params["rich_message"]) + } + + files := config.files() + if len(files) != 2 { + t.Fatalf("files = %+v", files) + } + if _, ok := photo.Media.(FilePath); !ok { + t.Errorf("nested block photo was modified: %+v", photo) + } + + edit := EditMessageTextConfig{BaseEdit: BaseEdit{ChatID: 1, MessageID: 2}, RichMessage: config.RichMessage} + params = paramsOf(t, edit) + if _, ok := params["text"]; ok { + t.Errorf("editMessageText sends an empty text alongside rich_message: %v", params) + } + if len(edit.files()) != 2 { + t.Errorf("editMessageText files = %+v", edit.files()) + } +} diff --git a/types.go b/types.go index 3ab3cebb..e16be689 100644 --- a/types.go +++ b/types.go @@ -201,6 +201,20 @@ func (u *Update) SentFrom() *User { return u.Message.From case u.EditedMessage != nil: return u.EditedMessage.From + case u.ChannelPost != nil: + return u.ChannelPost.From + case u.EditedChannelPost != nil: + return u.EditedChannelPost.From + case u.BusinessConnection != nil: + return &u.BusinessConnection.User + case u.BusinessMessage != nil: + return u.BusinessMessage.From + case u.EditedBusinessMessage != nil: + return u.EditedBusinessMessage.From + case u.GuestMessage != nil: + return u.GuestMessage.From + case u.MessageReaction != nil: + return u.MessageReaction.User case u.InlineQuery != nil: return u.InlineQuery.From case u.ChosenInlineResult != nil: @@ -211,6 +225,19 @@ func (u *Update) SentFrom() *User { return u.ShippingQuery.From case u.PreCheckoutQuery != nil: return u.PreCheckoutQuery.From + case u.PurchasedPaidMedia != nil: + return &u.PurchasedPaidMedia.From + case u.PollAnswer != nil: + if u.PollAnswer.VoterChat != nil { + return nil + } + return &u.PollAnswer.User + case u.MyChatMember != nil: + return &u.MyChatMember.From + case u.ChatMember != nil: + return &u.ChatMember.From + case u.ChatJoinRequest != nil: + return &u.ChatJoinRequest.From case u.ManagedBot != nil: return &u.ManagedBot.User case u.Subscription != nil: @@ -239,8 +266,38 @@ func (u *Update) FromChat() *Chat { return u.ChannelPost.Chat case u.EditedChannelPost != nil: return u.EditedChannelPost.Chat + case u.BusinessMessage != nil: + return u.BusinessMessage.Chat + case u.EditedBusinessMessage != nil: + return u.EditedBusinessMessage.Chat + case u.DeletedBusinessMessages != nil: + return &u.DeletedBusinessMessages.Chat + case u.GuestMessage != nil: + return u.GuestMessage.Chat + case u.MessageReaction != nil: + return &u.MessageReaction.Chat + case u.MessageReactionCount != nil: + return &u.MessageReactionCount.Chat case u.CallbackQuery != nil: + if u.CallbackQuery.Message == nil { + // Callback queries from inline messages carry no message. + return nil + } return u.CallbackQuery.Message.Chat + case u.PollAnswer != nil: + return u.PollAnswer.VoterChat + case u.MyChatMember != nil: + return &u.MyChatMember.Chat + case u.ChatMember != nil: + return &u.ChatMember.Chat + case u.ChatJoinRequest != nil: + return &u.ChatJoinRequest.Chat + case u.ChatBoost != nil: + return &u.ChatBoost.Chat + case u.RemovedChatBoost != nil: + return &u.RemovedChatBoost.Chat + case u.StoppedMessageGeneration != nil: + return &u.StoppedMessageGeneration.Chat default: return nil } @@ -387,52 +444,80 @@ type Chat struct { // // optional IsDirectMessages bool `json:"is_direct_messages,omitempty"` +} + +// IsPrivate returns if the Chat is a private conversation. +func (c Chat) IsPrivate() bool { + return c.Type == "private" +} + +// IsGroup returns if the Chat is a group. +func (c Chat) IsGroup() bool { + return c.Type == "group" +} + +// IsSuperGroup returns if the Chat is a supergroup. +func (c Chat) IsSuperGroup() bool { + return c.Type == "supergroup" +} + +// IsChannel returns if the Chat is a channel. +func (c Chat) IsChannel() bool { + return c.Type == "channel" +} + +// ChatConfig returns a ChatConfig struct for chat related methods. +func (c Chat) ChatConfig() ChatConfig { + return ChatConfig{ChatID: c.ID} +} + +// ChatFullInfo contains full information about a chat. This is the return +// type of getChat as of Bot API 7.3. It embeds Chat so all Chat fields are +// accessible via field promotion. +type ChatFullInfo struct { + Chat // ActiveUsernames is a list of all active chat usernames; for private chats, - // supergroups and channels. Returned only in getChat. + // supergroups and channels. // // optional ActiveUsernames []string `json:"active_usernames,omitempty"` // EmojiStatusCustomEmojiID is the custom emoji identifier of emoji status of - // the other party in a private chat. Returned only in getChat. + // the other party in a private chat. // // optional EmojiStatusCustomEmojiID string `json:"emoji_status_custom_emoji_id,omitempty"` // EmojiStatusExpirationDate is the expiration date of the emoji status of - // the other party in a private chat in Unix time, if any. Returned only in - // getChat. + // the other party in a private chat in Unix time, if any. // // optional EmojiStatusExpirationDate int64 `json:"emoji_status_expiration_date,omitempty"` // AccentColorID is the identifier of the accent color for the chat name // and backgrounds of the chat photo, reply header, and link preview. - // Returned only in getChat. // // optional AccentColorID int `json:"accent_color_id,omitempty"` // BackgroundCustomEmojiID is the custom emoji identifier of the emoji // chosen by the chat for the reply header and link preview background. - // Returned only in getChat. // // optional BackgroundCustomEmojiID string `json:"background_custom_emoji_id,omitempty"` // ProfileAccentColorID is the identifier of the accent color for the - // chat's profile background. Returned only in getChat. + // chat's profile background. // // optional ProfileAccentColorID int `json:"profile_accent_color_id,omitempty"` // ProfileBackgroundCustomEmojiID is the custom emoji identifier of the - // emoji chosen by the chat for its profile background. Returned only in - // getChat. + // emoji chosen by the chat for its profile background. // // optional ProfileBackgroundCustomEmojiID string `json:"profile_background_custom_emoji_id,omitempty"` // HasVisibleHistory is true, if new chat members will have access to old - // messages; available only to chat administrators. Returned only in getChat. + // messages; available only to chat administrators. // // optional HasVisibleHistory bool `json:"has_visible_history,omitempty"` // HasHiddenMembers is true, if non-administrators can only see bots and - // administrators in the chat. Returned only in getChat. + // administrators in the chat. // // optional HasHiddenMembers bool `json:"has_hidden_members,omitempty"` @@ -444,16 +529,16 @@ type Chat struct { HasAggressiveAntiSpamEnabled bool `json:"has_aggressive_anti_spam_enabled,omitempty"` // UnrestrictBoostCount is the minimum number of boosts that a non-administrator // user needs to add to the chat in order to ignore slow mode and chat - // permissions. Returned only in getChat. + // permissions. // // optional UnrestrictBoostCount int `json:"unrestrict_boost_count,omitempty"` // CustomEmojiStickerSetName is the name of the chat's custom emoji sticker - // set. Returned only in getChat. + // set. // // optional CustomEmojiStickerSetName string `json:"custom_emoji_sticker_set_name,omitempty"` - // Birthdate of the other party in a private chat. Returned only in getChat. + // Birthdate of the other party in a private chat. // // optional Birthdate *Birthdate `json:"birthdate,omitempty"` @@ -478,7 +563,7 @@ type Chat struct { // optional PersonalChat *Chat `json:"personal_chat,omitempty"` // Photo is a chat photo - Photo *ChatPhoto `json:"photo"` + Photo *ChatPhoto `json:"photo,omitempty"` // Bio is the bio of the other party in a private chat. Returned only in // getChat // @@ -486,13 +571,13 @@ type Chat struct { Bio string `json:"bio,omitempty"` // HasPrivateForwards is true if privacy settings of the other party in the // private chat allows to use tg://user?id= links only in chats - // with the user. Returned only in getChat. + // with the user. // // optional HasPrivateForwards bool `json:"has_private_forwards,omitempty"` // HasRestrictedVoiceAndVideoMessages is true, if the privacy settings of the // other party restrict sending voice and video note messages in the private - // chat. Returned only in getChat. + // chat. // // optional HasRestrictedVoiceAndVideoMessages bool `json:"has_restricted_voice_and_video_messages,omitempty"` @@ -501,12 +586,12 @@ type Chat struct { // optional Description string `json:"description,omitempty"` // JoinToSendMessages is true, if users need to join the supergroup before - // they can send messages. Returned only in getChat. + // they can send messages. // // optional JoinToSendMessages bool `json:"join_to_send_messages,omitempty"` // JoinByRequest is true, if all users directly joining the supergroup need - // to be approved by supergroup administrators. Returned only in getChat. + // to be approved by supergroup administrators. // // optional JoinByRequest bool `json:"join_by_request,omitempty"` @@ -527,23 +612,22 @@ type Chat struct { // optional AvailableReactions []ReactionType `json:"available_reactions,omitempty"` // Permissions are default chat member permissions, for groups and - // supergroups. Returned only in getChat. + // supergroups. // // optional Permissions *ChatPermissions `json:"permissions,omitempty"` // SlowModeDelay is for supergroups, the minimum allowed delay between - // consecutive messages sent by each unprivileged user. Returned only in - // getChat. + // consecutive messages sent by each unprivileged user. // // optional SlowModeDelay int `json:"slow_mode_delay,omitempty"` // MessageAutoDeleteTime is the time after which all messages sent to the - // chat will be automatically deleted; in seconds. Returned only in getChat. + // chat will be automatically deleted; in seconds. // // optional MessageAutoDeleteTime int `json:"message_auto_delete_time,omitempty"` // HasProtectedContent is true if messages from the chat can't be forwarded - // to other chats. Returned only in getChat. + // to other chats. // // optional HasProtectedContent bool `json:"has_protected_content,omitempty"` @@ -553,7 +637,6 @@ type Chat struct { // optional StickerSetName string `json:"sticker_set_name,omitempty"` // CanSetStickerSet is true, if the bot can change the group sticker set. - // Returned only in getChat. // // optional CanSetStickerSet bool `json:"can_set_sticker_set,omitempty"` @@ -564,42 +647,10 @@ type Chat struct { // optional LinkedChatID int64 `json:"linked_chat_id,omitempty"` // Location is for supergroups, the location to which the supergroup is - // connected. Returned only in getChat. + // connected. // // optional Location *ChatLocation `json:"location,omitempty"` -} - -// IsPrivate returns if the Chat is a private conversation. -func (c Chat) IsPrivate() bool { - return c.Type == "private" -} - -// IsGroup returns if the Chat is a group. -func (c Chat) IsGroup() bool { - return c.Type == "group" -} - -// IsSuperGroup returns if the Chat is a supergroup. -func (c Chat) IsSuperGroup() bool { - return c.Type == "supergroup" -} - -// IsChannel returns if the Chat is a channel. -func (c Chat) IsChannel() bool { - return c.Type == "channel" -} - -// ChatConfig returns a ChatConfig struct for chat related methods. -func (c Chat) ChatConfig() ChatConfig { - return ChatConfig{ChatID: c.ID} -} - -// ChatFullInfo contains full information about a chat. This is the return -// type of getChat as of Bot API 7.3. It embeds Chat so all Chat fields are -// accessible via field promotion. -type ChatFullInfo struct { - Chat // MaxReactionCount is the maximum number of reactions that can be set // on a message in the chat. MaxReactionCount int `json:"max_reaction_count"` @@ -817,11 +868,6 @@ type Message struct { // // optional Animation *Animation `json:"animation,omitempty"` - // PremiumAnimation message is an animation, information about the animation. - // For backward compatibility, when this field is set, the document field will also be set; - // - // optional - PremiumAnimation *Animation `json:"premium_animation,omitempty"` // Audio message is an audio file, information about the file; // // optional @@ -1347,6 +1393,16 @@ type MessageEntity struct { // // optional CustomEmojiID string `json:"custom_emoji_id,omitempty"` + // UnixTime for "date_time" only, the Unix time associated with the entity. + // + // optional + UnixTime int64 `json:"unix_time,omitempty"` + // DateTimeFormat for "date_time" only, the string that defines the + // formatting of the date and time. See date-time entity formatting for + // more details. + // + // optional + DateTimeFormat string `json:"date_time_format,omitempty"` } // ParseURL attempts to parse a URL contained within a MessageEntity. @@ -1769,8 +1825,14 @@ type PollOptionAdded struct { // // optional PollMessage *Message `json:"poll_message,omitempty"` - // Option is the added poll option. - Option PollOption `json:"option"` + // OptionPersistentID is the unique identifier of the added option. + OptionPersistentID string `json:"option_persistent_id"` + // OptionText is the option text. + OptionText string `json:"option_text"` + // OptionTextEntities are special entities that appear in the OptionText. + // + // optional + OptionTextEntities []MessageEntity `json:"option_text_entities,omitempty"` } // PollOptionDeleted describes a service message about a poll option being @@ -1780,8 +1842,14 @@ type PollOptionDeleted struct { // // optional PollMessage *Message `json:"poll_message,omitempty"` - // OptionPersistentID is the persistent identifier of the deleted option. + // OptionPersistentID is the unique identifier of the deleted option. OptionPersistentID string `json:"option_persistent_id"` + // OptionText is the option text. + OptionText string `json:"option_text"` + // OptionTextEntities are special entities that appear in the OptionText. + // + // optional + OptionTextEntities []MessageEntity `json:"option_text_entities,omitempty"` } // PollAnswer represents an answer of a user in a non-anonymous poll. @@ -3047,9 +3115,15 @@ type Story struct { ID int `json:"id"` } -// ChatOwnerLeft represents a service message about the owner leaving the chat. -// Currently holds no information. -type ChatOwnerLeft struct{} +// ChatOwnerLeft represents a service message about the chat owner leaving the +// chat. +type ChatOwnerLeft struct { + // NewOwner is the user who will become the new owner of the chat if the + // previous owner does not return to the chat. + // + // optional + NewOwner *User `json:"new_owner,omitempty"` +} // ChatOwnerChanged represents a service message about a change of the chat // owner. @@ -3058,48 +3132,34 @@ type ChatOwnerChanged struct { NewOwner User `json:"new_owner"` } -// VideoQuality describes an available quality variant for a video. -// -// Note: the exact field layout of this type is not fully verified. When -// serializing or deserializing you may need to use the Raw payload. +// VideoQuality represents a video file of a specific quality. type VideoQuality struct { - Raw json.RawMessage `json:"-"` -} - -// UnmarshalJSON preserves the raw JSON payload for VideoQuality. -func (v *VideoQuality) UnmarshalJSON(b []byte) error { - v.Raw = append(v.Raw[:0], b...) - return nil -} - -// MarshalJSON emits the raw JSON payload, or `null` if unset. -func (v VideoQuality) MarshalJSON() ([]byte, error) { - if len(v.Raw) == 0 { - return []byte("null"), nil - } - return v.Raw, nil + // FileID is the identifier for this file, which can be used to download + // or reuse the file. + FileID string `json:"file_id"` + // FileUniqueID is the unique identifier for this file, which is supposed + // to be the same over time and for different bots. Can't be used to + // download or reuse the file. + FileUniqueID string `json:"file_unique_id"` + // Width is the video width. + Width int `json:"width"` + // Height is the video height. + Height int `json:"height"` + // Codec is the codec that was used to encode the video, for example, + // "h264", "h265", or "av01". + Codec string `json:"codec"` + // FileSize is the file size in bytes. + // + // optional + FileSize int64 `json:"file_size,omitempty"` } -// UserProfileAudios describes the audios posted to a user's profile. -// -// Note: the exact field layout of this type is not fully verified. When -// serializing or deserializing you may need to use the Raw payload. +// UserProfileAudios represents the audios displayed on a user's profile. type UserProfileAudios struct { - Raw json.RawMessage `json:"-"` -} - -// UnmarshalJSON preserves the raw JSON payload for UserProfileAudios. -func (u *UserProfileAudios) UnmarshalJSON(b []byte) error { - u.Raw = append(u.Raw[:0], b...) - return nil -} - -// MarshalJSON emits the raw JSON payload, or `null` if unset. -func (u UserProfileAudios) MarshalJSON() ([]byte, error) { - if len(u.Raw) == 0 { - return []byte("null"), nil - } - return u.Raw, nil + // TotalCount is the total number of profile audios for the target user. + TotalCount int `json:"total_count"` + // Audios are the requested profile audios. + Audios []Audio `json:"audios"` } // ChatBoostAdded represents a service message about a user boosting a chat. @@ -3550,6 +3610,11 @@ type TransactionPartner struct { // // optional PaidMedia []PaidMedia `json:"paid_media,omitempty"` + // PaidMediaPayload is the bot-specified paid media payload. Set when + // Type is "user" and TransactionType is "paid_media_payment". + // + // optional + PaidMediaPayload string `json:"paid_media_payload,omitempty"` // Gift is the gift sent to the user by the bot. Set when Type is // "user" and the transaction is a gift purchase. // @@ -3671,71 +3736,56 @@ type PaidMediaPurchased struct { PaidMediaPayload string `json:"paid_media_payload"` } -// UniqueGiftColors describes the color scheme for a user's name, replies -// to messages, and link previews based on a unique gift. -// -// Note: the exact field layout of this type is not fully verified. When -// serializing or deserializing you may need to use the Raw payload. +// UniqueGiftColors contains information about the color scheme for a user's +// name, message replies and link previews based on a unique gift. type UniqueGiftColors struct { - Raw json.RawMessage `json:"-"` -} - -// UnmarshalJSON preserves the raw JSON payload for UniqueGiftColors. -func (u *UniqueGiftColors) UnmarshalJSON(b []byte) error { - u.Raw = append(u.Raw[:0], b...) - return nil -} - -// MarshalJSON emits the raw JSON payload, or `null` if unset. -func (u UniqueGiftColors) MarshalJSON() ([]byte, error) { - if len(u.Raw) == 0 { - return []byte("null"), nil - } - return u.Raw, nil -} - -// GiftBackground describes the background associated with a gift. -// -// Note: the exact field layout of this type is not fully verified. When -// serializing or deserializing you may need to use the Raw payload. + // ModelCustomEmojiID is the custom emoji identifier of the unique gift's + // model. + ModelCustomEmojiID string `json:"model_custom_emoji_id"` + // SymbolCustomEmojiID is the custom emoji identifier of the unique + // gift's symbol. + SymbolCustomEmojiID string `json:"symbol_custom_emoji_id"` + // LightThemeMainColor is the main color used in light themes; RGB format. + LightThemeMainColor int `json:"light_theme_main_color"` + // LightThemeOtherColors is the list of 1-3 additional colors used in + // light themes; RGB format. + LightThemeOtherColors []int `json:"light_theme_other_colors"` + // DarkThemeMainColor is the main color used in dark themes; RGB format. + DarkThemeMainColor int `json:"dark_theme_main_color"` + // DarkThemeOtherColors is the list of 1-3 additional colors used in dark + // themes; RGB format. + DarkThemeOtherColors []int `json:"dark_theme_other_colors"` +} + +// GiftBackground describes the background of a gift. type GiftBackground struct { - Raw json.RawMessage `json:"-"` -} - -// UnmarshalJSON preserves the raw JSON payload for GiftBackground. -func (g *GiftBackground) UnmarshalJSON(b []byte) error { - g.Raw = append(g.Raw[:0], b...) - return nil -} - -// MarshalJSON emits the raw JSON payload, or `null` if unset. -func (g GiftBackground) MarshalJSON() ([]byte, error) { - if len(g.Raw) == 0 { - return []byte("null"), nil - } - return g.Raw, nil + // CenterColor is the center color of the background in RGB format. + CenterColor int `json:"center_color"` + // EdgeColor is the edge color of the background in RGB format. + EdgeColor int `json:"edge_color"` + // TextColor is the text color of the background in RGB format. + TextColor int `json:"text_color"` } -// UserRating describes the rating of a user in a private chat. -// -// Note: the exact field layout of this type is not fully verified. When -// serializing or deserializing you may need to use the Raw payload. +// UserRating describes the rating of a user based on their Telegram Star +// spendings. type UserRating struct { - Raw json.RawMessage `json:"-"` -} - -// UnmarshalJSON preserves the raw JSON payload for UserRating. -func (u *UserRating) UnmarshalJSON(b []byte) error { - u.Raw = append(u.Raw[:0], b...) - return nil -} - -// MarshalJSON emits the raw JSON payload, or `null` if unset. -func (u UserRating) MarshalJSON() ([]byte, error) { - if len(u.Raw) == 0 { - return []byte("null"), nil - } - return u.Raw, nil + // Level is the current level of the user, indicating their reliability + // when purchasing digital goods and services. A higher level suggests a + // more trustworthy customer; a negative level is likely reason for + // concern. + Level int `json:"level"` + // Rating is the numerical value of the user's rating; the higher the + // rating, the better. + Rating int `json:"rating"` + // CurrentLevelRating is the rating value required to get the current + // level. + CurrentLevelRating int `json:"current_level_rating"` + // NextLevelRating is the rating value required to get to the next level; + // omitted if the maximum level was reached. + // + // optional + NextLevelRating int `json:"next_level_rating,omitempty"` } // Gift represents a gift that can be sent by the bot. @@ -4017,14 +4067,16 @@ const ( type OwnedGift struct { // Type of the gift. One of "regular", "unique". Type string `json:"type"` - // Gift is the regular gift, when Type is "regular". + // Gift is the regular gift, when Type is "regular". It shares the "gift" + // wire field with UniqueGift. // // optional - Gift *Gift `json:"gift,omitempty"` - // UniqueGift is the unique gift, when Type is "unique". + Gift *Gift `json:"-"` + // UniqueGift is the unique gift, when Type is "unique". It shares the + // "gift" wire field with Gift. // // optional - UniqueGift *UniqueGift `json:"unique_gift,omitempty"` + UniqueGift *UniqueGift `json:"-"` // OwnedGiftID is the unique identifier of the gift for the bot; for // gifts received on behalf of business accounts only. // @@ -4105,6 +4157,51 @@ type OwnedGift struct { NextTransferDate int `json:"next_transfer_date,omitempty"` } +// UnmarshalJSON decodes an OwnedGift, routing the polymorphic "gift" field to +// Gift or UniqueGift based on Type. +func (g *OwnedGift) UnmarshalJSON(data []byte) error { + type alias OwnedGift + aux := struct { + *alias + Gift json.RawMessage `json:"gift,omitempty"` + }{alias: (*alias)(g)} + + if err := json.Unmarshal(data, &aux); err != nil { + return err + } + + if len(aux.Gift) == 0 || string(aux.Gift) == "null" { + return nil + } + + if g.Type == OwnedGiftTypeUnique { + g.UniqueGift = new(UniqueGift) + return json.Unmarshal(aux.Gift, g.UniqueGift) + } + + g.Gift = new(Gift) + return json.Unmarshal(aux.Gift, g.Gift) +} + +// MarshalJSON encodes an OwnedGift, emitting Gift or UniqueGift under the +// shared "gift" wire field. +func (g OwnedGift) MarshalJSON() ([]byte, error) { + type alias OwnedGift + aux := struct { + alias + Gift any `json:"gift,omitempty"` + }{alias: alias(g)} + + switch { + case g.UniqueGift != nil: + aux.Gift = g.UniqueGift + case g.Gift != nil: + aux.Gift = g.Gift + } + + return json.Marshal(aux) +} + // OwnedGifts contains the list of gifts received and owned by a user or chat. type OwnedGifts struct { // TotalCount is the total number of gifts owned by the user or chat. @@ -4837,6 +4934,10 @@ type ExternalReplyInfo struct { // // optional HasMediaSpoiler bool `json:"has_media_spoiler,omitempty"` + // Checklist is set if the message is a checklist. + // + // optional + Checklist *Checklist `json:"checklist,omitempty"` // Contact is set if the message is a shared contact. // // optional @@ -6078,8 +6179,12 @@ type InlineQueryResultCachedSticker struct { ID string `json:"id"` // StickerID a valid file identifier of the sticker StickerID string `json:"sticker_file_id"` - // Title is a title - Title string `json:"title"` + // Title is not a part of the Bot API for cached sticker results and is + // ignored by Telegram. + // + // Deprecated: Telegram does not support this field. It is kept only for + // source compatibility and will be removed in a future release. + Title string `json:"title,omitempty"` // ReplyMarkup inline keyboard attached to the message // // optional @@ -6289,6 +6394,17 @@ type InlineQueryResultDocument struct { // // optional Caption string `json:"caption,omitempty"` + // ParseMode mode for parsing entities in the document caption. + // See formatting options for more details + // (https://core.telegram.org/bots/api#formatting-options). + // + // optional + ParseMode string `json:"parse_mode,omitempty"` + // CaptionEntities is a list of special entities that appear in the caption, + // which can be specified instead of parse_mode + // + // optional + CaptionEntities []MessageEntity `json:"caption_entities,omitempty"` // URL a valid url for the file URL string `json:"document_url"` // MimeType of the content of the file, either “application/pdf” or “application/zip” @@ -6507,8 +6623,12 @@ type InlineQueryResultPhoto struct { // URL a valid URL of the photo. Photo must be in jpeg format. // Photo size must not exceed 5MB. URL string `json:"photo_url"` - // MimeType - MimeType string `json:"mime_type"` + // MimeType is not a part of the Bot API for photo results and is ignored + // by Telegram. + // + // Deprecated: Telegram does not support this field. It is kept only for + // source compatibility and will be removed in a future release. + MimeType string `json:"mime_type,omitempty"` // Width of the photo // // optional @@ -6632,6 +6752,17 @@ type InlineQueryResultVideo struct { // // optional Caption string `json:"caption,omitempty"` + // ParseMode mode for parsing entities in the video caption. + // See formatting options for more details + // (https://core.telegram.org/bots/api#formatting-options). + // + // optional + ParseMode string `json:"parse_mode,omitempty"` + // CaptionEntities is a list of special entities that appear in the caption, + // which can be specified instead of parse_mode + // + // optional + CaptionEntities []MessageEntity `json:"caption_entities,omitempty"` // ShowCaptionAboveMedia pass True if the caption must be shown above the // message media. // diff --git a/upload_e2e_test.go b/upload_e2e_test.go new file mode 100644 index 00000000..033cf451 --- /dev/null +++ b/upload_e2e_test.go @@ -0,0 +1,149 @@ +package tgbotapi + +import ( + "io" + "net/http" + "net/http/httptest" + "strings" + "sync" + "testing" +) + +// fakeBotAPI is a minimal stand-in for the Bot API server. It answers getMe, +// records every other request (including multipart uploads) and replies with +// a successful empty message. +type fakeBotAPI struct { + mu sync.Mutex + requests map[string]*http.Request + fields map[string]map[string]string + files map[string]map[string]string +} + +func newFakeBotAPI(t *testing.T) (*BotAPI, *fakeBotAPI) { + t.Helper() + + fake := &fakeBotAPI{ + requests: map[string]*http.Request{}, + fields: map[string]map[string]string{}, + files: map[string]map[string]string{}, + } + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + method := r.URL.Path[strings.LastIndex(r.URL.Path, "/")+1:] + if method == "getMe" { + io.WriteString(w, `{"ok":true,"result":{"id":1,"is_bot":true,"first_name":"Bot","username":"bot"}}`) + return + } + + fields, files := map[string]string{}, map[string]string{} + if strings.HasPrefix(r.Header.Get("Content-Type"), "multipart/form-data") { + if err := r.ParseMultipartForm(1 << 20); err != nil { + t.Errorf("parse multipart: %v", err) + } + for k, v := range r.MultipartForm.Value { + fields[k] = v[0] + } + for k, v := range r.MultipartForm.File { + f, _ := v[0].Open() + b, _ := io.ReadAll(f) + files[k] = string(b) + } + } else { + r.ParseForm() + for k, v := range r.PostForm { + fields[k] = v[0] + } + } + + fake.mu.Lock() + fake.requests[method] = r + fake.fields[method] = fields + fake.files[method] = files + fake.mu.Unlock() + + io.WriteString(w, `{"ok":true,"result":{"message_id":1,"date":1,"chat":{"id":1,"type":"private"}}}`) + })) + t.Cleanup(srv.Close) + + bot, err := NewBotAPIWithAPIEndpoint("TOKEN", srv.URL+"/bot%s/%s") + if err != nil { + t.Fatalf("NewBotAPIWithAPIEndpoint: %v", err) + } + + return bot, fake +} + +func TestUploadNestedPollMedia(t *testing.T) { + bot, fake := newFakeBotAPI(t) + + _, err := bot.Send(SendPollConfig{ + BaseChat: BaseChat{ChatID: 1}, + Question: "Which one?", + Options: []InputPollOption{ + {Text: "first", Media: NewInputMediaPhoto(FileBytes{Name: "a.jpg", Bytes: []byte("AAA")})}, + {Text: "second"}, + }, + }) + if err != nil { + t.Fatalf("Send: %v", err) + } + + if got := fake.files["sendPoll"]["poll-media-0"]; got != "AAA" { + t.Fatalf("uploaded file = %q, want AAA (files: %v)", got, fake.files["sendPoll"]) + } + if !strings.Contains(fake.fields["sendPoll"]["options"], "attach://poll-media-0") { + t.Fatalf("options field = %s", fake.fields["sendPoll"]["options"]) + } +} + +func TestUploadRichMessageMedia(t *testing.T) { + bot, fake := newFakeBotAPI(t) + + _, err := bot.Send(SendRichMessageConfig{ + BaseChat: BaseChat{ChatID: 1}, + RichMessage: &InputRichMessage{ + Markdown: "![](tg://photo?id=p1)", + Media: []InputRichMessageMedia{{ID: "p1", Media: NewInputMediaPhoto(FileBytes{Name: "p.jpg", Bytes: []byte("PPP")})}}, + }, + }) + if err != nil { + t.Fatalf("Send: %v", err) + } + + if got := fake.files["sendRichMessage"]["rich-media-0"]; got != "PPP" { + t.Fatalf("uploaded file = %q, want PPP (files: %v)", got, fake.files["sendRichMessage"]) + } +} + +func TestSendWithoutUploadUsesPlainForm(t *testing.T) { + bot, fake := newFakeBotAPI(t) + + _, err := bot.Send(SendPollConfig{ + BaseChat: BaseChat{ChatID: 1}, + Question: "Which one?", + Options: []InputPollOption{{Text: "first", Media: NewInputMediaPhoto(FileID("abc"))}, {Text: "second"}}, + }) + if err != nil { + t.Fatalf("Send: %v", err) + } + + if len(fake.files["sendPoll"]) != 0 { + t.Fatalf("unexpected uploads: %v", fake.files["sendPoll"]) + } + if !strings.Contains(fake.fields["sendPoll"]["options"], `"media":"abc"`) { + t.Fatalf("options field = %s", fake.fields["sendPoll"]["options"]) + } +} + +func TestTransportErrorsDoNotLeakToken(t *testing.T) { + bot, _ := newFakeBotAPI(t) + bot.Token = "123456:SECRET" + bot.SetAPIEndpoint("http://127.0.0.1:1/bot%s/%s") + + _, err := bot.Request(NewMessage(1, "hi")) + if err == nil { + t.Fatal("expected a transport error") + } + if strings.Contains(err.Error(), "SECRET") { + t.Fatalf("error leaks the token: %v", err) + } +} From 33adfe46012a935325948fee115c3e1003f98018 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Oct 2026 02:59:18 +0000 Subject: [PATCH 62/65] Add GetChatMemberCount and honour SetFileEndpoint in GetFileDirectURL GetChatMembersCount is kept as a deprecated wrapper. GetFileDirectURL used the package-level FileEndpoint even when a custom file endpoint was set for a local Bot API server. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EHdMw9fUyf4m1g524X8rhj --- bot.go | 9 ++++++++- upload_e2e_test.go | 25 +++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/bot.go b/bot.go index 2443d93b..43dab377 100644 --- a/bot.go +++ b/bot.go @@ -311,7 +311,7 @@ func (bot *BotAPI) GetFileDirectURL(fileID string) (string, error) { return "", err } - return file.Link(bot.Token), nil + return bot.FileLink(file), nil } // GetMe fetches the currently authenticated bot. @@ -639,7 +639,14 @@ func (bot *BotAPI) GetChatAdministrators(config ChatAdministratorsConfig) ([]Cha } // GetChatMembersCount gets the number of users in a chat. +// +// Deprecated: use GetChatMemberCount, which matches the Bot API method name. func (bot *BotAPI) GetChatMembersCount(config ChatMemberCountConfig) (int, error) { + return bot.GetChatMemberCount(config) +} + +// GetChatMemberCount gets the number of members in a chat. +func (bot *BotAPI) GetChatMemberCount(config ChatMemberCountConfig) (int, error) { resp, err := bot.Request(config) if err != nil { return -1, err diff --git a/upload_e2e_test.go b/upload_e2e_test.go index 033cf451..5be1dbd8 100644 --- a/upload_e2e_test.go +++ b/upload_e2e_test.go @@ -147,3 +147,28 @@ func TestTransportErrorsDoNotLeakToken(t *testing.T) { t.Fatalf("error leaks the token: %v", err) } } + +func TestGetFileDirectURLUsesFileEndpoint(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if strings.HasSuffix(r.URL.Path, "/getMe") { + io.WriteString(w, `{"ok":true,"result":{"id":1,"is_bot":true,"first_name":"Bot"}}`) + return + } + io.WriteString(w, `{"ok":true,"result":{"file_id":"f","file_unique_id":"u","file_path":"photos/1.jpg"}}`) + })) + defer srv.Close() + + bot, err := NewBotAPIWithAPIEndpoint("TOKEN", srv.URL+"/bot%s/%s") + if err != nil { + t.Fatal(err) + } + bot.SetFileEndpoint("http://local-bot-api/file/bot%s/%s") + + link, err := bot.GetFileDirectURL("f") + if err != nil { + t.Fatal(err) + } + if link != "http://local-bot-api/file/botTOKEN/photos/1.jpg" { + t.Fatalf("GetFileDirectURL() = %q", link) + } +} From 71c7bb16d427b4fba90ae89d5e0efbfbe88b678b Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Oct 2026 02:59:18 +0000 Subject: [PATCH 63/65] Add specdiff tool and weekly Bot API spec check; modernize CI internal/cmd/specdiff compares the package with the machine-readable Bot API specification and reports missing types, fields, methods and parameters. The new Bot API spec workflow runs it weekly so that new Bot API releases are noticed. The test workflow now uses current actions, tests on Go 1.24 and the latest stable Go, and runs vet and the race detector. The deprecated codecov v1 upload step is dropped. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EHdMw9fUyf4m1g524X8rhj --- .github/workflows/bot-api-spec.yml | 23 ++ .github/workflows/test.yml | 66 ++-- internal/cmd/specdiff/main.go | 609 +++++++++++++++++++++++++++++ 3 files changed, 665 insertions(+), 33 deletions(-) create mode 100644 .github/workflows/bot-api-spec.yml create mode 100644 internal/cmd/specdiff/main.go diff --git a/.github/workflows/bot-api-spec.yml b/.github/workflows/bot-api-spec.yml new file mode 100644 index 00000000..9fa058f7 --- /dev/null +++ b/.github/workflows/bot-api-spec.yml @@ -0,0 +1,23 @@ +name: Bot API spec + +# Compares the library with the latest published Telegram Bot API +# specification, so that new Bot API releases are noticed quickly. + +on: + schedule: + - cron: '17 6 * * 1' + workflow_dispatch: + +jobs: + specdiff: + name: Compare with the Bot API specification + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-go@v5 + with: + go-version: '1.24' + + - name: Report missing types, fields, methods and parameters + run: go run ./internal/cmd/specdiff -strict diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 48b2859b..d6b19e3f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,33 +1,33 @@ -name: Test - -on: - push: - branches: - - master - - develop - pull_request: - -jobs: - build: - name: Test - runs-on: ubuntu-latest - steps: - - name: Set up Go 1.x - uses: actions/setup-go@v2 - with: - go-version: ^1.15 - id: go - - - name: Check out code into the Go module directory - uses: actions/checkout@v2 - - - name: Build - run: go build -v . - - - name: Test - run: go test -coverprofile=coverage.out -covermode=atomic -v . - - - name: Upload coverage report - uses: codecov/codecov-action@v1 - with: - file: ./coverage.out +name: Test + +on: + push: + branches: + - master + - develop + pull_request: + +jobs: + build: + name: Test (Go ${{ matrix.go }}) + runs-on: ubuntu-latest + strategy: + matrix: + go: ['1.24', 'stable'] + steps: + - name: Check out code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: ${{ matrix.go }} + + - name: Build + run: go build -v ./... + + - name: Vet + run: go vet ./... + + - name: Test + run: go test -race -coverprofile=coverage.out -covermode=atomic ./... diff --git a/internal/cmd/specdiff/main.go b/internal/cmd/specdiff/main.go new file mode 100644 index 00000000..9e982d47 --- /dev/null +++ b/internal/cmd/specdiff/main.go @@ -0,0 +1,609 @@ +// Command specdiff compares this library with the machine-readable Telegram +// Bot API specification and reports types, fields, methods and parameters +// that are missing from the Go code. +// +// Usage (from the repository root): +// +// go run ./internal/cmd/specdiff # report against the latest spec +// go run ./internal/cmd/specdiff -strict # exit with status 1 on gaps +// go run ./internal/cmd/specdiff -v # also list extra fields/params +// go run ./internal/cmd/specdiff -spec api.json +// +// The specification is the api.json published by +// https://github.com/PaulSonOfLars/telegram-bot-api-spec, which is generated +// from https://core.telegram.org/bots/api. +package main + +import ( + "encoding/json" + "flag" + "fmt" + "go/ast" + "go/parser" + "go/token" + "io" + "net/http" + "os" + "reflect" + "regexp" + "sort" + "strconv" + "strings" +) + +const defaultSpec = "https://raw.githubusercontent.com/PaulSonOfLars/telegram-bot-api-spec/main/api.json" + +// Go names that differ from the Bot API names. +var typeAliases = map[string]string{ + "MessageId": "MessageID", + "LoginUrl": "LoginURL", + "InlineQueryResultGif": "InlineQueryResultGIF", + "InlineQueryResultMpeg4Gif": "InlineQueryResultMPEG4GIF", + "InlineQueryResultCachedGif": "InlineQueryResultCachedGIF", + "InlineQueryResultCachedMpeg4Gif": "InlineQueryResultCachedMPEG4GIF", +} + +// Types that are not represented by a struct. +var skipTypes = map[string]bool{ + "InputFile": true, // RequestFileData +} + +// Wire fields that are routed by a custom (Un)MarshalJSON instead of a tag. +var customFields = map[string][]string{ + "OwnedGift": {"gift"}, + "RichBlock": {"caption"}, + "InputRichBlock": {"caption"}, +} + +// Methods implemented directly on BotAPI rather than through a config. +var directMethods = map[string]bool{ + "getMe": true, + "getWebhookInfo": true, +} + +type specField struct { + Name string `json:"name"` + Types []string `json:"types"` + Required bool `json:"required"` +} + +type specEntry struct { + Name string `json:"name"` + Fields []specField `json:"fields"` + Subtypes []string `json:"subtypes"` +} + +type spec struct { + Version string `json:"version"` + ReleaseDate string `json:"release_date"` + Methods map[string]specEntry `json:"methods"` + Types map[string]specEntry `json:"types"` +} + +type goField struct { + name string + json string + typ string + embedded bool +} + +type pkg struct { + structs map[string][]goField + aliases map[string]string // type X = Y + methods map[string]string // config type -> Bot API method + keys map[string][]string // config type -> parameter keys + calls map[string][]string // config type -> fields whose params()/addTo() are called + params map[string]bool // config types that define their own params() + consts map[string]string +} + +func main() { + specSource := flag.String("spec", defaultSpec, "URL or path of the Bot API api.json") + dir := flag.String("dir", ".", "directory of the tgbotapi package") + strict := flag.Bool("strict", false, "exit with status 1 if anything is missing") + verbose := flag.Bool("v", false, "also report fields and parameters that are not in the spec") + flag.Parse() + + s, err := loadSpec(*specSource) + if err != nil { + fmt.Fprintln(os.Stderr, "loading spec:", err) + os.Exit(2) + } + + p, err := loadPackage(*dir) + if err != nil { + fmt.Fprintln(os.Stderr, "parsing package:", err) + os.Exit(2) + } + + missing, extra := compare(s, p) + + fmt.Printf("Telegram %s (%s)\n\n", s.Version, s.ReleaseDate) + if len(missing) == 0 { + fmt.Println("Nothing is missing.") + } else { + fmt.Printf("Missing (%d):\n", len(missing)) + for _, m := range missing { + fmt.Println(" -", m) + } + } + if *verbose && len(extra) > 0 { + fmt.Printf("\nNot in the spec (%d):\n", len(extra)) + for _, e := range extra { + fmt.Println(" -", e) + } + } + + if *strict && len(missing) > 0 { + os.Exit(1) + } +} + +func loadSpec(source string) (*spec, error) { + var r io.Reader + if strings.HasPrefix(source, "http://") || strings.HasPrefix(source, "https://") { + resp, err := http.Get(source) + if err != nil { + return nil, err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("GET %s: %s", source, resp.Status) + } + r = resp.Body + } else { + f, err := os.Open(source) + if err != nil { + return nil, err + } + defer f.Close() + r = f + } + + var s spec + if err := json.NewDecoder(r).Decode(&s); err != nil { + return nil, err + } + + return &s, nil +} + +func exprString(e ast.Expr) string { + switch t := e.(type) { + case *ast.Ident: + return t.Name + case *ast.StarExpr: + return "*" + exprString(t.X) + case *ast.ArrayType: + return "[]" + exprString(t.Elt) + case *ast.SelectorExpr: + return exprString(t.X) + "." + t.Sel.Name + case *ast.MapType: + return "map[" + exprString(t.Key) + "]" + exprString(t.Value) + case *ast.InterfaceType: + return "interface{}" + } + return "?" +} + +func loadPackage(dir string) (*pkg, error) { + fset := token.NewFileSet() + pkgs, err := parser.ParseDir(fset, dir, func(fi os.FileInfo) bool { + return !strings.HasSuffix(fi.Name(), "_test.go") + }, 0) + if err != nil { + return nil, err + } + + p := &pkg{ + structs: map[string][]goField{}, + aliases: map[string]string{}, + methods: map[string]string{}, + keys: map[string][]string{}, + calls: map[string][]string{}, + params: map[string]bool{}, + consts: map[string]string{}, + } + + for _, astPkg := range pkgs { + for _, f := range astPkg.Files { + for _, decl := range f.Decls { + switch d := decl.(type) { + case *ast.GenDecl: + p.addGenDecl(d) + case *ast.FuncDecl: + p.addFuncDecl(d) + } + } + } + } + + return p, nil +} + +func (p *pkg) addGenDecl(d *ast.GenDecl) { + for _, spec := range d.Specs { + switch s := spec.(type) { + case *ast.TypeSpec: + if s.Assign != 0 { + p.aliases[s.Name.Name] = exprString(s.Type) + continue + } + st, ok := s.Type.(*ast.StructType) + if !ok { + continue + } + var fields []goField + for _, fl := range st.Fields.List { + tag := "" + if fl.Tag != nil { + v, _ := strconv.Unquote(fl.Tag.Value) + tag = strings.Split(reflect.StructTag(v).Get("json"), ",")[0] + } + typ := exprString(fl.Type) + if len(fl.Names) == 0 { + fields = append(fields, goField{name: strings.TrimPrefix(typ, "*"), json: tag, typ: typ, embedded: true}) + } + for _, n := range fl.Names { + fields = append(fields, goField{name: n.Name, json: tag, typ: typ}) + } + } + p.structs[s.Name.Name] = fields + case *ast.ValueSpec: + for i, n := range s.Names { + if i >= len(s.Values) { + continue + } + if lit, ok := s.Values[i].(*ast.BasicLit); ok && lit.Kind == token.STRING { + p.consts[n.Name], _ = strconv.Unquote(lit.Value) + } + } + } + } +} + +func (p *pkg) addFuncDecl(d *ast.FuncDecl) { + if d.Recv == nil || len(d.Recv.List) == 0 || d.Body == nil { + return + } + recv := strings.TrimPrefix(exprString(d.Recv.List[0].Type), "*") + + switch d.Name.Name { + case "method": + ast.Inspect(d.Body, func(n ast.Node) bool { + ret, ok := n.(*ast.ReturnStmt) + if !ok || len(ret.Results) != 1 { + return true + } + switch v := ret.Results[0].(type) { + case *ast.BasicLit: + p.methods[recv], _ = strconv.Unquote(v.Value) + case *ast.Ident: + p.methods[recv] = "$" + v.Name + } + return true + }) + case "params", "addTo": + ast.Inspect(d.Body, func(n ast.Node) bool { + switch c := n.(type) { + case *ast.CallExpr: + sel, ok := c.Fun.(*ast.SelectorExpr) + if !ok { + return true + } + if sel.Sel.Name == "params" || sel.Sel.Name == "addTo" { + if inner, ok := sel.X.(*ast.SelectorExpr); ok { + p.calls[recv] = append(p.calls[recv], inner.Sel.Name) + } + return true + } + if len(c.Args) > 0 { + if lit, ok := c.Args[0].(*ast.BasicLit); ok && lit.Kind == token.STRING { + key, _ := strconv.Unquote(lit.Value) + p.keys[recv] = append(p.keys[recv], key) + } + } + case *ast.IndexExpr: + if lit, ok := c.Index.(*ast.BasicLit); ok && lit.Kind == token.STRING { + key, _ := strconv.Unquote(lit.Value) + p.keys[recv] = append(p.keys[recv], key) + } + } + return true + }) + if d.Name.Name == "params" { + p.params[recv] = true + } + case "files": + ast.Inspect(d.Body, func(n ast.Node) bool { + kv, ok := n.(*ast.KeyValueExpr) + if !ok { + return true + } + if id, ok := kv.Key.(*ast.Ident); ok && id.Name == "Name" { + if lit, ok := kv.Value.(*ast.BasicLit); ok && lit.Kind == token.STRING { + key, _ := strconv.Unquote(lit.Value) + p.keys[recv] = append(p.keys[recv], key) + } + } + return true + }) + } +} + +func (p *pkg) resolve(name string) string { + for { + target, ok := p.aliases[name] + if !ok { + return name + } + name = target + } +} + +// jsonFields returns the wire fields of a struct, including embedded ones. +func (p *pkg) jsonFields(name string, seen map[string]bool) map[string]goField { + name = p.resolve(name) + out := map[string]goField{} + if seen[name] { + return out + } + seen[name] = true + + for _, f := range p.structs[name] { + switch { + case f.embedded && f.json == "": + for k, v := range p.jsonFields(f.name, seen) { + out[k] = v + } + case f.json != "" && f.json != "-": + out[f.json] = f + } + } + for _, k := range customFields[name] { + out[k] = goField{name: k, json: k, typ: "custom"} + } + + return out +} + +// paramKeys returns the parameter keys sent by a config, following calls to +// the params() of embedded base configs. +func (p *pkg) paramKeys(cfg string, seen map[string]bool) map[string]bool { + cfg = p.resolve(cfg) + out := map[string]bool{} + if seen[cfg] { + return out + } + seen[cfg] = true + + for _, k := range p.keys[cfg] { + out[k] = true + } + + fieldTypes := map[string]string{} + var embedded []string + for _, f := range p.structs[cfg] { + fieldTypes[f.name] = strings.TrimPrefix(f.typ, "*") + if f.embedded { + embedded = append(embedded, f.name) + } + } + + calls := p.calls[cfg] + if !p.params[cfg] { + // params() is promoted from the embedded structs. + calls = append(calls, embedded...) + } + for _, c := range calls { + typ, ok := fieldTypes[c] + if !ok { + typ = c + } + for k := range p.paramKeys(typ, seen) { + out[k] = true + } + } + + return out +} + +var arrayOf = regexp.MustCompile(`^Array of (.+)$`) + +func (p *pkg) goTypeFor(specType string) string { + if m := arrayOf.FindStringSubmatch(specType); m != nil { + return "[]" + p.goTypeFor(m[1]) + } + switch specType { + case "Integer": + return "int" + case "String": + return "string" + case "Boolean", "True": + return "bool" + case "Float": + return "float64" + case "InputFile": + return "RequestFileData" + } + if alias, ok := typeAliases[specType]; ok { + return alias + } + return specType +} + +func normalize(t string) string { + t = strings.TrimPrefix(t, "*") + t = strings.ReplaceAll(t, "[]*", "[]") + t = strings.ReplaceAll(t, "int64", "int") + return t +} + +func (p *pkg) typeMatches(specTypes []string, goType string) bool { + g := normalize(goType) + switch g { + case "interface{}", "any", "json.RawMessage", "custom": + return true + } + for _, st := range specTypes { + if normalize(p.goTypeFor(st)) == g { + return true + } + // Files are always represented as RequestFileData, and a message that + // may be inaccessible is represented as a Message. + if st == "String" && g == "RequestFileData" { + return true + } + if st == "MaybeInaccessibleMessage" && g == "Message" { + return true + } + } + return false +} + +func compare(s *spec, p *pkg) (missing, extra []string) { + // Union types are represented either as a single flat struct with a type + // discriminator, or as one struct per variant. + covered := map[string]bool{} + var markCovered func(string) + markCovered = func(name string) { + covered[name] = true + for _, sub := range s.Types[name].Subtypes { + markCovered(sub) + } + } + + unionFields := func(name string) map[string][]specField { + out := map[string][]specField{} + var walk func(string) + walk = func(n string) { + t, ok := s.Types[n] + if !ok { + return + } + for _, sub := range t.Subtypes { + walk(sub) + } + for _, f := range t.Fields { + out[f.Name] = append(out[f.Name], f) + } + } + walk(name) + return out + } + + for _, name := range sortedKeys(s.Types) { + t := s.Types[name] + goName := p.goTypeFor(name) + if len(t.Subtypes) == 0 || len(p.structs[goName]) == 0 { + continue + } + markCovered(name) + have := p.jsonFields(goName, map[string]bool{}) + want := unionFields(name) + for _, k := range sortedKeys(want) { + f, ok := have[k] + if !ok { + missing = append(missing, fmt.Sprintf("field %s.%s", name, k)) + continue + } + var types []string + for _, sf := range want[k] { + types = append(types, sf.Types...) + } + if !p.typeMatches(types, f.typ) { + missing = append(missing, fmt.Sprintf("field %s.%s has type %s, want %s", name, k, f.typ, strings.Join(types, " or "))) + } + } + for _, k := range sortedKeys(have) { + if _, ok := want[k]; !ok { + extra = append(extra, fmt.Sprintf("field %s.%s", name, k)) + } + } + } + + for _, name := range sortedKeys(s.Types) { + t := s.Types[name] + if skipTypes[name] || len(t.Subtypes) > 0 { + continue + } + goName := p.goTypeFor(name) + if _, ok := p.structs[goName]; !ok { + if !covered[name] { + missing = append(missing, "type "+name) + } + continue + } + have := p.jsonFields(goName, map[string]bool{}) + want := map[string]bool{} + for _, f := range t.Fields { + want[f.Name] = true + gf, ok := have[f.Name] + if !ok { + missing = append(missing, fmt.Sprintf("field %s.%s", name, f.Name)) + continue + } + if !p.typeMatches(f.Types, gf.typ) { + missing = append(missing, fmt.Sprintf("field %s.%s has type %s, want %s", name, f.Name, gf.typ, strings.Join(f.Types, " or "))) + } + } + for _, k := range sortedKeys(have) { + if !want[k] { + extra = append(extra, fmt.Sprintf("field %s.%s", name, k)) + } + } + } + + configs := map[string][]string{} + for cfg, method := range p.methods { + if strings.HasPrefix(method, "$") { + method = p.consts[method[1:]] + } + configs[method] = append(configs[method], cfg) + } + + for _, name := range sortedKeys(s.Methods) { + cfgs := configs[name] + if len(cfgs) == 0 { + if !directMethods[name] { + missing = append(missing, "method "+name) + } + continue + } + sort.Strings(cfgs) + want := map[string]bool{} + for _, f := range s.Methods[name].Fields { + want[f.Name] = true + } + for _, cfg := range cfgs { + have := p.paramKeys(cfg, map[string]bool{}) + for _, k := range sortedKeys(want) { + if !have[k] { + missing = append(missing, fmt.Sprintf("parameter %s(%s) in %s", name, k, cfg)) + } + } + for _, k := range sortedKeys(have) { + if !want[k] { + extra = append(extra, fmt.Sprintf("parameter %s(%s) in %s", name, k, cfg)) + } + } + } + } + + for _, method := range sortedKeys(configs) { + if _, ok := s.Methods[method]; !ok { + missing = append(missing, fmt.Sprintf("method %s used by %s is not in the spec", method, strings.Join(configs[method], ", "))) + } + } + + return missing, extra +} + +func sortedKeys[V any](m map[string]V) []string { + keys := make([]string, 0, len(m)) + for k := range m { + keys = append(keys, k) + } + sort.Strings(keys) + return keys +} From b95c3c12a849bb8581b141420d81c33cb8914ff4 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Oct 2026 02:59:18 +0000 Subject: [PATCH 64/65] Update README, BREAKING.md and docs for the fork - Install from github.com/bssth/telegram-bot-api/v5 and explain how to migrate from the abandoned upstream module. - Credit go-telegram-bot-api/telegram-bot-api#794 and list the fixes made on top of it. - Document the moved Chat fields and the removed Message.PremiumAnimation in BREAKING.md. - Fix outdated identifiers in the docs and describe nested media uploads. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EHdMw9fUyf4m1g524X8rhj --- BREAKING.md | 69 +++++++++- README.md | 198 ++++++++++++++++------------- docs/changelog.md | 52 +++++--- docs/getting-started/README.md | 6 +- docs/internals/adding-endpoints.md | 19 +-- docs/internals/uploading-files.md | 33 ++++- 6 files changed, 255 insertions(+), 122 deletions(-) diff --git a/BREAKING.md b/BREAKING.md index b28501a2..cd14e6ff 100644 --- a/BREAKING.md +++ b/BREAKING.md @@ -1,8 +1,9 @@ # Breaking Changes -This file lists every source-incompatible change introduced while bringing -this fork from Bot API 6.0 up to 9.6. Upgrades are grouped by topic so you -can jump straight to the area your code touches. +This file lists every source-incompatible change between upstream +`github.com/go-telegram-bot-api/telegram-bot-api/v5` v5.5.1 (Bot API 6.0) +and this fork (Bot API 10.3). Upgrades are grouped by topic so you can jump +straight to the area your code touches. Each section lists **what to rename** or **what to replace** — the fastest way to migrate is `grep` for the old identifier in your code and apply the @@ -11,6 +12,21 @@ Bot API itself changed. --- +## Module path + +```go +// Before +import tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" + +// After +import tgbotapi "github.com/bssth/telegram-bot-api/v5" +``` + +The package name is still `tgbotapi`. See the README for a one-line `sed` +command that rewrites the imports. + +--- + ## `thumb` → `thumbnail` (Bot API 6.6) The single largest mechanical rename. Applies everywhere media has a @@ -420,6 +436,48 @@ c, err = bot.GetChat(cfg) --- +## `getChat`-only fields moved from `Chat` to `ChatFullInfo` (Bot API 7.3) + +Since Bot API 7.3 Telegram returns these fields only from `getChat`, as part +of `ChatFullInfo`; the `Chat` objects inside messages and other updates never +contain them. They were removed from `Chat` so that reading them there is a +compile error instead of a silent zero value: + +```go +ActiveUsernames, EmojiStatusCustomEmojiID, EmojiStatusExpirationDate, +AccentColorID, BackgroundCustomEmojiID, ProfileAccentColorID, +ProfileBackgroundCustomEmojiID, HasVisibleHistory, HasHiddenMembers, +HasAggressiveAntiSpamEnabled, UnrestrictBoostCount, +CustomEmojiStickerSetName, Birthdate, BusinessIntro, BusinessLocation, +BusinessOpeningHours, PersonalChat, Photo, Bio, HasPrivateForwards, +HasRestrictedVoiceAndVideoMessages, Description, JoinToSendMessages, +JoinByRequest, InviteLink, PinnedMessage, AvailableReactions, Permissions, +SlowModeDelay, MessageAutoDeleteTime, HasProtectedContent, StickerSetName, +CanSetStickerSet, LinkedChatID, Location +``` + +Code that reads them from the result of `bot.GetChat` keeps compiling, since +it already returns `ChatFullInfo`: + +```go +// Before (always empty) +update.Message.Chat.Description + +// After +info, err := bot.GetChat(tgbotapi.ChatInfoConfig{ChatConfig: update.Message.Chat.ChatConfig()}) +info.Description +``` + +--- + +## `Message.PremiumAnimation` removed + +`premium_animation` is a field of `Sticker`, not `Message`, so +`Message.PremiumAnimation` was never populated. Use +`Message.Sticker.PremiumAnimation` instead. + +--- + ## Types that gained fields (soft-breaking) A few types changed from empty structs to carrying fields. Anyone who used @@ -445,7 +503,8 @@ custom configs should do the same. ## Checklist for upgrading -1. Global find-replace for the `Thumb` → `Thumbnail` renames (Section 1). +1. Change the import path (see "Module path"), then do a global + find-replace for the `Thumb` → `Thumbnail` renames. 2. Search for `ReplyToMessageID` and migrate each to `ReplyParameters`. 3. Search for `DisableWebPagePreview` and migrate to `LinkPreviewOptions`. 4. Search for `ForwardFrom` / `ForwardDate` and switch to `ForwardOrigin`. @@ -456,5 +515,7 @@ custom configs should do the same. 7. If you handle business connections, check `Rights` instead of `CanReply`. 8. If you read quiz correctness, use `CorrectOptionIDs[0]` (or loop for multi-answer). +9. If you read `getChat`-only fields such as `Bio` or `Description` from a + `Chat`, read them from the `ChatFullInfo` returned by `bot.GetChat`. Everything else is additive and should compile unchanged. diff --git a/README.md b/README.md index adb9f4a3..6cf10e00 100644 --- a/README.md +++ b/README.md @@ -1,64 +1,66 @@ # Golang bindings for the Telegram Bot API -[![Go Reference](https://pkg.go.dev/badge/github.com/kirugan/telegram-bot-api/v5.svg)](https://pkg.go.dev/github.com/kirugan/telegram-bot-api/v5) +[![Go Reference](https://pkg.go.dev/badge/github.com/bssth/telegram-bot-api/v5.svg)](https://pkg.go.dev/github.com/bssth/telegram-bot-api/v5) +[![Test](https://github.com/bssth/telegram-bot-api/actions/workflows/test.yml/badge.svg)](https://github.com/bssth/telegram-bot-api/actions/workflows/test.yml) +[![Bot API](https://img.shields.io/badge/Bot%20API-10.3-blue.svg)](https://core.telegram.org/bots/api-changelog) -> **This is a maintained fork of +> **This is the maintained continuation of > [`go-telegram-bot-api/telegram-bot-api`](https://github.com/go-telegram-bot-api/telegram-bot-api).** -> The upstream project stopped updating at Bot API 6.0 (April 2022). This fork -> is brought up to **Bot API 10.3** (the latest release as of this writing) and -> tracks the [official changelog](https://core.telegram.org/bots/api-changelog) -> going forward. +> The original project stopped at Bot API 6.0 (April 2022) and no longer +> accepts changes. This fork supports **Bot API 10.3** (August 24, 2026) and +> follows the [official changelog](https://core.telegram.org/bots/api-changelog). All methods are fairly self-explanatory, and reading the -[godoc](https://pkg.go.dev/github.com/kirugan/telegram-bot-api/v5) page should -explain everything. +[godoc](https://pkg.go.dev/github.com/bssth/telegram-bot-api/v5) page should +explain everything. If something isn't clear, open an +[issue](https://github.com/bssth/telegram-bot-api/issues) or submit a pull +request. The scope of this project is just to provide a wrapper around the API without any additional features. There are other projects for creating something with plugins and command handlers without having to design all that yourself. ---- +More tutorials and high-level information live in the [`docs`](./docs) +directory. ## Installing ```sh -go get github.com/kirugan/telegram-bot-api/v5 +go get github.com/bssth/telegram-bot-api/v5@latest ``` ```go -import tgbotapi "github.com/kirugan/telegram-bot-api/v5" +import tgbotapi "github.com/bssth/telegram-bot-api/v5" ``` -Note: because the module path changed from `go-telegram-bot-api` to `kirugan`, -a Go `replace` directive in `go.mod` **will not** silently redirect existing -code — Go requires the replacement's module path to match the original. Either -update your imports, or keep using upstream on 6.0. - -## Migrating from upstream +Go 1.24 or newer is required. -If you're coming from `go-telegram-bot-api/telegram-bot-api` v5.5.x and want to -jump to the latest Bot API, read [**BREAKING.md**](./BREAKING.md). It lists -every source-incompatible change introduced while bringing the fork from 6.0 -to 10.0, grouped by topic, with before/after snippets for each one (10.1 -through 10.3 are purely additive and introduced no breaking changes). The fastest -way to migrate is `grep` your codebase for the old identifier and apply the -rewrite. +## Migrating from `go-telegram-bot-api/telegram-bot-api` -### Migrating with Claude (or any LLM) +1. Change the import path. The package name stays `tgbotapi`, so nothing else + in your code needs to be renamed: -`BREAKING.md` was written so it can be fed directly to a coding assistant. -A prompt like: + ```sh + grep -rl 'github.com/go-telegram-bot-api/telegram-bot-api/v5' --include='*.go' . \ + | xargs sed -i 's#github.com/go-telegram-bot-api/telegram-bot-api/v5#github.com/bssth/telegram-bot-api/v5#g' + go get github.com/bssth/telegram-bot-api/v5@latest + go mod tidy + ``` -> Here is `BREAKING.md` from a Go library I'm upgrading. Here are the files in -> my project that use `tgbotapi`. Please rewrite them to compile against the -> new API, preserving behavior. + (On macOS use `sed -i ''`.) A `replace` directive in `go.mod` is not + enough: Go requires a replacement module to declare the same module path + as the one it replaces. -handles most of the mechanical rewrites (`Thumb` → `Thumbnail`, -`ReplyToMessageID` → `ReplyParameters`, `DisableWebPagePreview` → -`LinkPreviewOptions`, sticker creation, etc.) in one pass. Review the diff, -run `go build ./...`, done. +2. Fix whatever no longer compiles using [**BREAKING.md**](./BREAKING.md). It + lists every source-incompatible change between upstream v5.5.1 (Bot API + 6.0) and this fork, grouped by topic, with before/after snippets. Most of + them come from Telegram itself: `Thumb` became `Thumbnail`, + `ReplyToMessageID` became `ReplyParameters`, `DisableWebPagePreview` became + `LinkPreviewOptions`, and so on. ---- +`BREAKING.md` is written so it can be handed to a coding assistant together +with the files that use `tgbotapi`; it handles most of the mechanical +rewrites in one pass. Review the diff, run `go build ./...`, done. ## Example @@ -71,7 +73,7 @@ package main import ( "log" - tgbotapi "github.com/kirugan/telegram-bot-api/v5" + tgbotapi "github.com/bssth/telegram-bot-api/v5" ) func main() { @@ -113,7 +115,7 @@ import ( "log" "net/http" - tgbotapi "github.com/kirugan/telegram-bot-api/v5" + tgbotapi "github.com/bssth/telegram-bot-api/v5" ) func main() { @@ -162,55 +164,79 @@ to generate your free TLS certificate there. --- -## What this fork adds - -### Bot API versions - -Full support for every Bot API version from **6.1 through 10.3**. See git log -for the per-version commits; each `Full support of API X` commit message is -the authoritative trail for what that version added. - -The 10.1 surface includes rich messages (`SendRichMessage` via -`SendRichMessageConfig` + `NewRichMessage`, streaming drafts via -`SendRichMessageDraftConfig`, the `InputRichMessage` send type, the received -`RichMessage` / `RichText` / `RichBlock` tree, and `EditMessageTextConfig.RichMessage`), -join request queries (`AnswerChatJoinRequestQueryConfig`, -`SendChatJoinRequestWebAppConfig`, `ChatJoinRequest.QueryID`, -`ChatFullInfo.GuardBot`, `User.SupportsJoinRequestQueries`), and poll links -(`Link`, `PollMedia.Link`, `InputMediaLink`). - -The 10.2 surface adds block-structured rich messages (`InputRichBlock`, -`InputRichBlockListItem`, `InputRichMessage.Blocks`) and explicit media for -markdown/HTML rich messages (`InputRichMessageMedia`, `InputMediaVoiceNote`), -ephemeral messages (`EphemeralSendParams` on the send configs that support -them, `ReplyParameters.EphemeralMessageID`, the `EditEphemeralMessage*` and -`DeleteEphemeralMessage` configs, `Message.ReceiverUser`, -`BotCommand.IsEphemeral`), communities (`Community`, `CommunityChatAdded`, -`CommunityChatRemoved`, `ChatFullInfo.Community`), and payment subscription -updates (`BotSubscriptionUpdated`, `Update.Subscription`). - -The 10.3 surface adds rich message buttons (`RichMessageButton`, the "button" -`RichText` span, the "buttons", "document", and "expandable_blockquote" -blocks), structured ephemeral send parameters (`EphemeralMessageParameters` -with `ReplaceCallbackQueryMessage`, sent as one JSON object), disabled and -force-reply keyboards (`DisabledButton`, `InlineKeyboardButton.Disabled`, -`ForceReply` on both keyboard markups), stoppable drafts (`CanStop` / -`KeepOnStop`, `MessageGenerationStopped`, `Update.StoppedMessageGeneration`), -welcome message rights (`CanSendWelcomeMessages`), community join service -messages (`CommunityChatJoined`), and gift text fields -(`UniqueGiftInfo.Text` / `Entities` / `IsPrivate`). - -The 10.0 surface includes guest mode (`AnswerGuestQuery`, -`Update.GuestMessage`, `User.SupportsGuestQueries`), live photos -(`SendLivePhoto` via `LivePhotoConfig` + `NewLivePhoto`, `LivePhoto` / -`InputMediaLivePhoto` / `PaidMediaLivePhoto`), expanded poll media -(`PollMedia`, plus `InputMediaSticker` / `InputMediaLocation` / -`InputMediaVenue` for poll questions, options, and quiz explanations), -poll restrictions (`MembersOnly`, `CountryCodes`), reaction administration -(`DeleteMessageReactionConfig`, `DeleteAllMessageReactionsConfig`, -`CanReactToMessages` on `ChatPermissions` / `ChatMember`), managed-bot -access settings (`GetManagedBotAccessSettings` / `SetManagedBotAccessSettings`, -`BotAccessSettings`), and `GetUserPersonalChatMessages`. +## What changed since upstream + +### Bot API coverage + +Every Bot API version from **6.1 through 10.3** is supported: all types, +fields, methods and parameters of the current specification are present. +Highlights of the last releases: + +- **10.3** — rich message buttons (`RichMessageButton`, the "buttons", + "document" and "expandable_blockquote" blocks), structured ephemeral send + parameters (`EphemeralMessageParameters`), disabled buttons and force-reply + keyboards (`DisabledButton`, `InlineKeyboardButton.Disabled`), stoppable + drafts (`CanStop` / `KeepOnStop`, `Update.StoppedMessageGeneration`), + welcome message rights, `CommunityChatJoined`, gift text fields. +- **10.2** — block-structured rich messages (`InputRichBlock`), explicit media + for rich messages (`InputRichMessageMedia`, `InputMediaVoiceNote`), + ephemeral messages (`ReplyParameters.EphemeralMessageID`, the + `EditEphemeralMessage*` and `DeleteEphemeralMessage` configs, + `Message.ReceiverUser`), communities, payment subscription updates. +- **10.1** — rich messages (`NewRichMessage`, `SendRichMessageDraftConfig`, + `RichMessage` / `RichText` / `RichBlock`), join request queries + (`AnswerChatJoinRequestQueryConfig`, `SendChatJoinRequestWebAppConfig`), + poll links (`InputMediaLink`). +- **10.0** — guest mode (`AnswerGuestQuery`, `Update.GuestMessage`), live + photos (`NewLivePhoto`, `InputMediaLivePhoto`), poll media for questions, + options and explanations, reaction administration, managed bot access + settings. +- **9.x** — managed bots, checklists, suggested posts, direct messages in + channels, gifts and Telegram Stars, business accounts, stories. +- **7.x – 8.x** — replies 2.0 (`ReplyParameters`), link preview options, + reactions, boosts, giveaways, business connections, paid media, Mini App + improvements, and much more. + +See the git history for the per-version commits; each `Full support of API X` +commit message lists what that version added. + +### Keeping up with the Bot API + +`internal/cmd/specdiff` compares the code with the machine-readable +[Bot API specification](https://github.com/PaulSonOfLars/telegram-bot-api-spec) +and lists every missing type, field, method and parameter: + +```sh +go run ./internal/cmd/specdiff # add -v to also list non-spec extras +``` + +The [Bot API spec](./.github/workflows/bot-api-spec.yml) workflow runs it +weekly, so a new Bot API release shows up as a failed run. + +### Credits + +The bulk of the Bot API 6.1 → 10.3 work comes from +[go-telegram-bot-api/telegram-bot-api#794](https://github.com/go-telegram-bot-api/telegram-bot-api/pull/794) +by [@kirugan](https://github.com/kirugan), which was never merged upstream. +This fork merges it with authorship preserved and builds on top of it with a +spec-driven audit (see below), plus ideas from other unmerged upstream pull +requests. + +### Fixes on top of the upstream pull request + +- Placeholder types that only kept raw JSON (`VideoQuality`, `UserRating`, + `UserProfileAudios`, `GiftBackground`, `UniqueGiftColors`) are real structs. +- `OwnedGift` decodes unique gifts correctly; `repostStory`, + `getUserGifts`, `getChatGifts`, `setStickerSetThumbnail`, `setGameScore` + (`force`) and `sendDocument` (`caption_entities`) send the parameters the + Bot API expects; `suggested_post_parameters` is supported by every send + method; `setPassportDataErrors` is available. +- Files nested in polls and rich messages are uploaded via `attach://` + instead of being serialized into the JSON. +- `Update.FromChat` / `SentFrom` cover all update kinds and no longer panic + on callback queries from inline messages. +- Transport errors no longer leak the bot token, and `GetFileDirectURL` + honours `SetFileEndpoint`. ### Upstream issues fixed diff --git a/docs/changelog.md b/docs/changelog.md index dd6399f9..9b4b45b9 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,17 +1,35 @@ -# Change Log - -## v5.4.0 - -- Remove all methods that return `(APIResponse, error)`. - - Use the `Request` method instead. - - For more information, see [Library Structure][library-structure]. -- Remove all `New*Upload` and `New*Share` methods, replace with `New*`. - - Use different [file types][files] to specify if upload or share. -- Rename `UploadFile` to `UploadFiles`, accept `[]RequestFile` instead of a - single fieldname and file. -- Fix methods returning `APIResponse` and errors to always use pointers. -- Update user IDs to `int64` because of Bot API changes. -- Add missing Bot API features. - -[library-structure]: ./getting-started/library-structure.md#methods -[files]: ./getting-started/files.md +# Change Log + +## Unreleased + +- Module path is now `github.com/bssth/telegram-bot-api/v5`; the upstream + repository is no longer maintained. +- Support every Bot API version from 6.1 to 10.3, based on the unmerged + upstream pull request + [#794](https://github.com/go-telegram-bot-api/telegram-bot-api/pull/794). +- Audit against the machine-readable Bot API 10.3 specification: all types, + fields, methods and parameters are present. +- Upload files nested in polls and rich messages via `attach://`. +- Redact the bot token from transport errors. +- Add `internal/cmd/specdiff` and a weekly workflow that report gaps against + the latest Bot API specification. +- Require Go 1.24. +- See [BREAKING.md][breaking] for source-incompatible changes. + +[breaking]: https://github.com/bssth/telegram-bot-api/blob/master/BREAKING.md + +## v5.4.0 + +- Remove all methods that return `(APIResponse, error)`. + - Use the `Request` method instead. + - For more information, see [Library Structure][library-structure]. +- Remove all `New*Upload` and `New*Share` methods, replace with `New*`. + - Use different [file types][files] to specify if upload or share. +- Rename `UploadFile` to `UploadFiles`, accept `[]RequestFile` instead of a + single fieldname and file. +- Fix methods returning `APIResponse` and errors to always use pointers. +- Update user IDs to `int64` because of Bot API changes. +- Add missing Bot API features. + +[library-structure]: ./getting-started/library-structure.md#methods +[files]: ./getting-started/files.md diff --git a/docs/getting-started/README.md b/docs/getting-started/README.md index 4803e9c0..304c7157 100644 --- a/docs/getting-started/README.md +++ b/docs/getting-started/README.md @@ -10,7 +10,7 @@ approaches to solve common problems. ## Installing ```bash -go get -u github.com/bssth/telegram-bot-api/v5 +go get github.com/bssth/telegram-bot-api/v5 ``` ## A Simple Bot @@ -90,7 +90,9 @@ things. We can add this code in right after the line enabling debug mode. // We'll also say that this message is a reply to the previous message. // For any other specifications than Chat ID or Text, you'll need to // set fields on the `MessageConfig`. - msg.ReplyToMessageID = update.Message.MessageID + msg.ReplyParameters = &tgbotapi.ReplyParameters{ + MessageID: update.Message.MessageID, + } // Okay, we're sending our message off! We don't care about the message // we just sent, so we'll discard it. diff --git a/docs/internals/adding-endpoints.md b/docs/internals/adding-endpoints.md index c4ff59a6..5e5fc6de 100644 --- a/docs/internals/adding-endpoints.md +++ b/docs/internals/adding-endpoints.md @@ -105,12 +105,12 @@ have similar fields for their files. ChatID int64 MessageID int + Delete RequestFileData -+ Thumb RequestFileData ++ Thumbnail RequestFileData } ``` Adding another method is pretty simple. We'll always add a file named `delete` -and add the `thumb` file if we have one. +and add the `thumbnail` file if we have one. ```go func (config DeleteMessageConfig) files() []RequestFile { @@ -119,10 +119,10 @@ func (config DeleteMessageConfig) files() []RequestFile { Data: config.Delete, }} - if config.Thumb != nil { + if config.Thumbnail != nil { files = append(files, RequestFile{ - Name: "thumb", - Data: config.Thumb, + Name: "thumbnail", + Data: config.Thumbnail, }) } @@ -136,7 +136,7 @@ is a `FilePath`, `FileURL`, `FileBytes`, `FileReader`, or `FileID`. ### Base Configs Certain Configs have repeated elements. For example, many of the items sent to a -chat have `ChatID` or `ChannelUsername` fields, along with `ReplyToMessageID`, +chat have `ChatID` or `ChannelUsername` fields, along with `ReplyParameters`, `ReplyMarkup`, and `DisableNotification`. Instead of implementing all of this code for each item, there's a `BaseChat` that handles it for your Config. Simply embed it in your struct to get all of those fields. @@ -147,9 +147,10 @@ embedding the `BaseChat` struct. ```go type MessageConfig struct { BaseChat - Text string - ParseMode string - DisableWebPagePreview bool + Text string + ParseMode string + Entities []MessageEntity + LinkPreviewOptions *LinkPreviewOptions } ``` diff --git a/docs/internals/uploading-files.md b/docs/internals/uploading-files.md index 1845269c..e7d7ad19 100644 --- a/docs/internals/uploading-files.md +++ b/docs/internals/uploading-files.md @@ -17,7 +17,7 @@ Config. Most endpoints use static file fields. For example, `sendPhoto` expects a single file named `photo`. All we have to do is set that single field with the correct value (either a string or multipart file). Methods like `sendDocument` take two -file uploads, a `document` and a `thumb`. These are pretty straightforward. +file uploads, a `document` and a `thumbnail`. These are pretty straightforward. Remembering that the `Fileable` interface only requires one method, let's implement it for `DocumentConfig`. @@ -32,10 +32,10 @@ func (config DocumentConfig) files() []RequestFile { }} // We'll only add a file if we have one. - if config.Thumb != nil { + if config.Thumbnail != nil { files = append(files, RequestFile{ - Name: "thumb", - Data: config.Thumb, + Name: "thumbnail", + Data: config.Thumbnail, }) } @@ -85,3 +85,28 @@ are all changed into `attach://file-%d`. When collecting a list of files to upload, it names them the same way. This creates a nearly transparent way of handling multiple files in the background without the user having to consider what's going on. + +### Nested Media + +Newer methods accept media nested inside JSON-serialized parameters: a poll can +carry media for its question, each option and the quiz explanation, and a rich +message can embed photos, videos, documents and so on in its `media` list and +in its blocks. Telegram resolves `attach://` references anywhere in the request, +so these work the same way as media groups. + +`SendPollConfig`, `SendRichMessageConfig`, `SendRichMessageDraftConfig`, +`EditMessageTextConfig` and `EditEphemeralMessageTextConfig` walk their nested +media with a `nestedMediaUploader` (see `nested_media.go`). Every file that +needs uploading is replaced by `attach://poll-media-%d` or +`attach://rich-media-%d` in a copy of the media, and the matching +`RequestFile` is collected for `files()`. Because `params()` and `files()` +walk the media in the same order, the names always line up, and the caller's +config is never modified. + +```go +poll := tgbotapi.NewPoll(chatID, "Which one?", "Left", "Right") +poll.Options[0].Media = tgbotapi.NewInputMediaPhoto(tgbotapi.FilePath("left.jpg")) +poll.Options[1].Media = tgbotapi.NewInputMediaPhoto(tgbotapi.FileID(rightFileID)) + +bot.Send(poll) +``` From f16374cad7526124d097ffd642c18dcc88ae4ae5 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Oct 2026 03:18:09 +0000 Subject: [PATCH 65/65] Publish the fork as github.com/bssth/telegram-bot-api/v6 The API is not source-compatible with upstream v5, so the fork starts a new major version instead of breaking code that pins /v5. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EHdMw9fUyf4m1g524X8rhj --- BREAKING.md | 8 +++++--- README.md | 20 ++++++++++++-------- docs/changelog.md | 7 ++++--- docs/examples/command-handling.md | 2 +- docs/examples/inline-keyboard.md | 2 +- docs/examples/keyboard.md | 2 +- docs/getting-started/README.md | 6 +++--- go.mod | 2 +- 8 files changed, 28 insertions(+), 21 deletions(-) diff --git a/BREAKING.md b/BREAKING.md index cd14e6ff..cb060a0a 100644 --- a/BREAKING.md +++ b/BREAKING.md @@ -19,11 +19,13 @@ Bot API itself changed. import tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" // After -import tgbotapi "github.com/bssth/telegram-bot-api/v5" +import tgbotapi "github.com/bssth/telegram-bot-api/v6" ``` -The package name is still `tgbotapi`. See the README for a one-line `sed` -command that rewrites the imports. +Both the owner and the major version change: the fork is published as `/v6` +because it is not source-compatible with upstream v5. The package name is +still `tgbotapi`. See the README for a one-line `sed` command that rewrites +the imports. --- diff --git a/README.md b/README.md index 6cf10e00..3945df4f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Golang bindings for the Telegram Bot API -[![Go Reference](https://pkg.go.dev/badge/github.com/bssth/telegram-bot-api/v5.svg)](https://pkg.go.dev/github.com/bssth/telegram-bot-api/v5) +[![Go Reference](https://pkg.go.dev/badge/github.com/bssth/telegram-bot-api/v6.svg)](https://pkg.go.dev/github.com/bssth/telegram-bot-api/v6) [![Test](https://github.com/bssth/telegram-bot-api/actions/workflows/test.yml/badge.svg)](https://github.com/bssth/telegram-bot-api/actions/workflows/test.yml) [![Bot API](https://img.shields.io/badge/Bot%20API-10.3-blue.svg)](https://core.telegram.org/bots/api-changelog) @@ -11,7 +11,7 @@ > follows the [official changelog](https://core.telegram.org/bots/api-changelog). All methods are fairly self-explanatory, and reading the -[godoc](https://pkg.go.dev/github.com/bssth/telegram-bot-api/v5) page should +[godoc](https://pkg.go.dev/github.com/bssth/telegram-bot-api/v6) page should explain everything. If something isn't clear, open an [issue](https://github.com/bssth/telegram-bot-api/issues) or submit a pull request. @@ -26,15 +26,19 @@ directory. ## Installing ```sh -go get github.com/bssth/telegram-bot-api/v5@latest +go get github.com/bssth/telegram-bot-api/v6@latest ``` ```go -import tgbotapi "github.com/bssth/telegram-bot-api/v5" +import tgbotapi "github.com/bssth/telegram-bot-api/v6" ``` Go 1.24 or newer is required. +The major version is **v6**: the API is not source-compatible with upstream +v5 (see below), so the fork starts a new major version instead of breaking +code that pins `/v5`. + ## Migrating from `go-telegram-bot-api/telegram-bot-api` 1. Change the import path. The package name stays `tgbotapi`, so nothing else @@ -42,8 +46,8 @@ Go 1.24 or newer is required. ```sh grep -rl 'github.com/go-telegram-bot-api/telegram-bot-api/v5' --include='*.go' . \ - | xargs sed -i 's#github.com/go-telegram-bot-api/telegram-bot-api/v5#github.com/bssth/telegram-bot-api/v5#g' - go get github.com/bssth/telegram-bot-api/v5@latest + | xargs sed -i 's#github.com/go-telegram-bot-api/telegram-bot-api/v5#github.com/bssth/telegram-bot-api/v6#g' + go get github.com/bssth/telegram-bot-api/v6@latest go mod tidy ``` @@ -73,7 +77,7 @@ package main import ( "log" - tgbotapi "github.com/bssth/telegram-bot-api/v5" + tgbotapi "github.com/bssth/telegram-bot-api/v6" ) func main() { @@ -115,7 +119,7 @@ import ( "log" "net/http" - tgbotapi "github.com/bssth/telegram-bot-api/v5" + tgbotapi "github.com/bssth/telegram-bot-api/v6" ) func main() { diff --git a/docs/changelog.md b/docs/changelog.md index 9b4b45b9..88b9e542 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,9 +1,10 @@ # Change Log -## Unreleased +## v6.0.0 (unreleased) -- Module path is now `github.com/bssth/telegram-bot-api/v5`; the upstream - repository is no longer maintained. +- Module path is now `github.com/bssth/telegram-bot-api/v6`: the upstream + repository is no longer maintained, and the new major version reflects + the source-incompatible changes listed in BREAKING.md. - Support every Bot API version from 6.1 to 10.3, based on the unmerged upstream pull request [#794](https://github.com/go-telegram-bot-api/telegram-bot-api/pull/794). diff --git a/docs/examples/command-handling.md b/docs/examples/command-handling.md index 3f01c9c2..17a37a9a 100644 --- a/docs/examples/command-handling.md +++ b/docs/examples/command-handling.md @@ -9,7 +9,7 @@ import ( "log" "os" - tgbotapi "github.com/bssth/telegram-bot-api/v5" + tgbotapi "github.com/bssth/telegram-bot-api/v6" ) func main() { diff --git a/docs/examples/inline-keyboard.md b/docs/examples/inline-keyboard.md index 1a0f204c..973d897f 100644 --- a/docs/examples/inline-keyboard.md +++ b/docs/examples/inline-keyboard.md @@ -11,7 +11,7 @@ import ( "log" "os" - tgbotapi "github.com/bssth/telegram-bot-api/v5" + tgbotapi "github.com/bssth/telegram-bot-api/v6" ) var numericKeyboard = tgbotapi.NewInlineKeyboardMarkup( diff --git a/docs/examples/keyboard.md b/docs/examples/keyboard.md index 44fd9b98..6880e360 100644 --- a/docs/examples/keyboard.md +++ b/docs/examples/keyboard.md @@ -10,7 +10,7 @@ import ( "log" "os" - tgbotapi "github.com/bssth/telegram-bot-api/v5" + tgbotapi "github.com/bssth/telegram-bot-api/v6" ) var numericKeyboard = tgbotapi.NewReplyKeyboard( diff --git a/docs/getting-started/README.md b/docs/getting-started/README.md index 304c7157..e5a506bd 100644 --- a/docs/getting-started/README.md +++ b/docs/getting-started/README.md @@ -10,7 +10,7 @@ approaches to solve common problems. ## Installing ```bash -go get github.com/bssth/telegram-bot-api/v5 +go get github.com/bssth/telegram-bot-api/v6 ``` ## A Simple Bot @@ -22,7 +22,7 @@ messages repeating what you said. Make sure you get an API token from Let's start by constructing a new [BotAPI][bot-api-docs]. [botfather]: https://t.me/Botfather -[bot-api-docs]: https://pkg.go.dev/github.com/bssth/telegram-bot-api/v5?tab=doc#BotAPI +[bot-api-docs]: https://pkg.go.dev/github.com/bssth/telegram-bot-api/v6?tab=doc#BotAPI ```go package main @@ -30,7 +30,7 @@ package main import ( "os" - tgbotapi "github.com/bssth/telegram-bot-api/v5" + tgbotapi "github.com/bssth/telegram-bot-api/v6" ) func main() { diff --git a/go.mod b/go.mod index 0cc06c61..adcf5d47 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,3 @@ -module github.com/bssth/telegram-bot-api/v5 +module github.com/bssth/telegram-bot-api/v6 go 1.24