From 6fdf666a11ebaa1da477bbf6885dc57be0baa3d4 Mon Sep 17 00:00:00 2001 From: Nash-Well <107937600+Nash-Well@users.noreply.github.com> Date: Sun, 10 Mar 2024 10:40:16 +0200 Subject: [PATCH] api: features 7.0, 7.1 (#658) * api: feature 7.0 init * message: remove blockquote * api: partrly implement replies 2.0 * api: update comments * message: add giveaway fields * message,chat: other changes init * boost: implement boost updates * reactions: refactor * api: implement multiple users request section * api: provide preview customization * api: implement 7.1 features * message: fix story issue * admin,context: refactor * bot_test: type refactor * api: partly fix review comments * api: small tidy up * react: reactions init * telebot: added boost_updated,boost_removed constants * message: fix InaccessibleMessage message struct parse * message,reac: naming refactor * context: add boost to the context of current events --- api.go | 31 +++++ boost.go | 111 ++++++++++++++++ bot.go | 36 ++++++ bot_test.go | 4 +- callback.go | 2 +- chat.go | 45 ++++--- context.go | 18 ++- giveaway.go | 103 +++++++++++++++ input_types.go | 6 +- markup.go | 7 +- message.go | 313 +++++++++++++++++++++++++++++++++++++++++++++- options.go | 48 ++++++- react/reaction.go | 112 +++++++++++++++++ telebot.go | 3 + update.go | 42 ++++--- 15 files changed, 835 insertions(+), 46 deletions(-) create mode 100644 boost.go create mode 100644 giveaway.go create mode 100644 react/reaction.go diff --git a/api.go b/api.go index c7242d0..408cbf2 100644 --- a/api.go +++ b/api.go @@ -327,6 +327,37 @@ func extractMessage(data []byte) (*Message, error) { return resp.Result, nil } +func (b *Bot) forwardCopyMessages(to Recipient, msgs []Editable, key string, opts ...*SendOptions) ([]Message, error) { + params := map[string]string{ + "chat_id": to.Recipient(), + } + + embedMessages(params, msgs) + + if len(opts) > 0 { + b.embedSendOptions(params, opts[0]) + } + + data, err := b.Raw(key, params) + if err != nil { + return nil, err + } + + var resp struct { + Result []Message + } + if err := json.Unmarshal(data, &resp); err != nil { + var resp struct { + Result bool + } + if err := json.Unmarshal(data, &resp); err != nil { + return nil, wrapError(err) + } + return nil, wrapError(err) + } + return resp.Result, nil +} + func verbose(method string, payload interface{}, data []byte) { body, _ := json.Marshal(payload) body = bytes.ReplaceAll(body, []byte(`\"`), []byte(`"`)) diff --git a/boost.go b/boost.go new file mode 100644 index 0000000..4cc7dbe --- /dev/null +++ b/boost.go @@ -0,0 +1,111 @@ +package telebot + +import ( + "encoding/json" + "time" +) + +// Boost contains information about a chat boost. +type Boost struct { + // Unique identifier of the boost. + ID string `json:"boost_id"` + + // Point in time (Unix timestamp) when the chat was boosted. + AddUnixtime int64 `json:"add_date"` + + // Point in time (Unix timestamp) when the boost will automatically expire, + // unless the booster's Telegram Premium subscription is prolonged. + ExpirationUnixtime int64 `json:"expiration_date"` + + // Source of the added boost. + Source *BoostSource `json:"source"` +} + +// AddDate returns the moment of time when the chat has been boosted in local time. +func (c *Boost) AddDate() time.Time { + return time.Unix(c.AddUnixtime, 0) +} + +// ExpirationDate returns the moment of time when the boost of the channel +// will expire in local time. +func (c *Boost) ExpirationDate() time.Time { + return time.Unix(c.ExpirationUnixtime, 0) +} + +// BoostSourceType describes a type of boost. +type BoostSourceType = string + +const ( + BoostPremium = "premium" + BoostGiftCode = "gift_code" + BoostGiveaway = "giveaway" +) + +// BoostSource describes the source of a chat boost. +type BoostSource struct { + // Source of the boost, always (“premium”, “gift_code”, “giveaway”). + Source BoostSourceType `json:"source"` + + // User that boosted the chat. + Booster *User `json:"user"` + + // Identifier of a message in the chat with the giveaway; the message + // could have been deleted already. May be 0 if the message isn't sent yet. + GiveawayMessageID int `json:"giveaway_message_id,omitempty"` + + // (Optional) True, if the giveaway was completed, but there was + // no user to win the prize. + Unclaimed bool `json:"is_unclaimed,omitempty"` +} + +// BoostAdded represents a service message about a user boosting a chat. +type BoostAdded struct { + // Number of boosts added by the user. + Count int `json:"boost_count"` +} + +// BoostUpdated represents a boost added to a chat or changed. +type BoostUpdated struct { + // Chat which was boosted. + Chat *Chat `json:"chat"` + + // Information about the chat boost. + Boost *Boost `json:"boost"` +} + +// BoostRemoved represents a boost removed from a chat. +type BoostRemoved struct { + // Chat which was boosted. + Chat *Chat `json:"chat"` + + // Unique identifier of the boost. + BoostID string `json:"boost_id"` + + // Point in time (Unix timestamp) when the boost was removed. + RemoveUnixtime int64 `json:"remove_date"` + + // Source of the removed boost. + Source *BoostSource `json:"source"` +} + +// UserBoosts gets the list of boosts added to a chat by a user. +// Requires administrator rights in the chat. +func (b *Bot) UserBoosts(chat, user Recipient) ([]Boost, error) { + params := map[string]string{ + "chat_id": chat.Recipient(), + "user_id": user.Recipient(), + } + + data, err := b.Raw("getUserChatBoosts", params) + if err != nil { + return nil, err + } + + var resp struct { + Result []Boost `json:"boosts"` + } + if err := json.Unmarshal(data, &resp); err != nil { + return nil, wrapError(err) + } + return resp.Result, nil +} diff --git a/bot.go b/bot.go index 29f37af..b9618e2 100644 --- a/bot.go +++ b/bot.go @@ -400,6 +400,17 @@ func (b *Bot) Forward(to Recipient, msg Editable, opts ...interface{}) (*Message return extractMessage(data) } +// ForwardMessages method 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. +func (b *Bot) ForwardMessages(to Recipient, msgs []Editable, opts ...*SendOptions) ([]Message, error) { + if to == nil { + return nil, ErrBadRecipient + } + return b.forwardCopyMessages(to, msgs, "forwardMessages", opts...) +} + // Copy behaves just like Forward() but the copied message doesn't have a link to the original message (see Bots API). // // This function will panic upon nil Editable. @@ -426,6 +437,20 @@ func (b *Bot) Copy(to Recipient, msg Editable, options ...interface{}) (*Message return extractMessage(data) } +// CopyMessages this method makes a copy of 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. A quiz poll can be copied only if the value of the field +// correct_option_id is known to the bot. The method is analogous +// to the method forwardMessages, but the copied messages don't have a link to the original message. +// Album grouping is kept for copied messages. +func (b *Bot) CopyMessages(to Recipient, msgs []Editable, opts ...*SendOptions) ([]Message, error) { + if to == nil { + return nil, ErrBadRecipient + } + return b.forwardCopyMessages(to, msgs, "copyMessages", opts...) +} + // Edit is magic, it lets you change already sent message. // This function will panic upon nil Editable. // @@ -668,6 +693,17 @@ func (b *Bot) Delete(msg Editable) error { return err } +// DeleteMessages deletes multiple messages simultaneously. +// If some of the specified messages can't be found, they are skipped. +func (b *Bot) DeleteMessages(msgs []Editable) error { + params := make(map[string]string) + + embedMessages(params, msgs) + + _, err := b.Raw("deleteMessages", params) + return err +} + // Notify updates the chat action for recipient. // // Chat action is a status message that recipient would see where diff --git a/bot_test.go b/bot_test.go index cf490c0..a4f3359 100644 --- a/bot_test.go +++ b/bot_test.go @@ -317,7 +317,7 @@ func TestBotProcessUpdate(t *testing.T) { b.ProcessUpdate(Update{Message: &Message{Text: "/start@other_bot"}}) b.ProcessUpdate(Update{Message: &Message{Text: "hello"}}) b.ProcessUpdate(Update{Message: &Message{Text: "text"}}) - b.ProcessUpdate(Update{Message: &Message{PinnedMessage: &Message{}}}) + b.ProcessUpdate(Update{Message: &Message{PinnedMessage: &InaccessibleMessage{}}}) b.ProcessUpdate(Update{Message: &Message{Photo: &Photo{}}}) b.ProcessUpdate(Update{Message: &Message{Voice: &Voice{}}}) b.ProcessUpdate(Update{Message: &Message{Audio: &Audio{}}}) @@ -342,7 +342,7 @@ func TestBotProcessUpdate(t *testing.T) { b.ProcessUpdate(Update{Message: &Message{Chat: &Chat{ID: 1}, MigrateTo: 2}}) b.ProcessUpdate(Update{EditedMessage: &Message{Text: "edited"}}) b.ProcessUpdate(Update{ChannelPost: &Message{Text: "post"}}) - b.ProcessUpdate(Update{ChannelPost: &Message{PinnedMessage: &Message{}}}) + b.ProcessUpdate(Update{ChannelPost: &Message{PinnedMessage: &InaccessibleMessage{}}}) b.ProcessUpdate(Update{EditedChannelPost: &Message{Text: "edited post"}}) b.ProcessUpdate(Update{Callback: &Callback{MessageID: "inline", Data: "callback"}}) b.ProcessUpdate(Update{Callback: &Callback{Data: "callback"}}) diff --git a/callback.go b/callback.go index bfd8a66..e8619f0 100644 --- a/callback.go +++ b/callback.go @@ -16,7 +16,7 @@ type Callback struct { // Message will be set if the button that originated the query // was attached to a message sent by a bot. - Message *Message `json:"message"` + Message *InaccessibleMessage `json:"message"` // MessageID will be set if the button was attached to a message // sent via the bot in inline mode. diff --git a/chat.go b/chat.go index 37ac05d..98dfd14 100644 --- a/chat.go +++ b/chat.go @@ -4,6 +4,8 @@ import ( "encoding/json" "strconv" "time" + + "gopkg.in/telebot.v3/react" ) // User object represents a Telegram user, bot. @@ -47,23 +49,32 @@ type Chat struct { Username string `json:"username"` // Returns only in getChat - Bio string `json:"bio,omitempty"` - Photo *ChatPhoto `json:"photo,omitempty"` - Description string `json:"description,omitempty"` - InviteLink string `json:"invite_link,omitempty"` - PinnedMessage *Message `json:"pinned_message,omitempty"` - Permissions *Rights `json:"permissions,omitempty"` - SlowMode int `json:"slow_mode_delay,omitempty"` - StickerSet string `json:"sticker_set_name,omitempty"` - CanSetStickerSet bool `json:"can_set_sticker_set,omitempty"` - LinkedChatID int64 `json:"linked_chat_id,omitempty"` - ChatLocation *ChatLocation `json:"location,omitempty"` - Private bool `json:"has_private_forwards,omitempty"` - Protected bool `json:"has_protected_content,omitempty"` - NoVoiceAndVideo bool `json:"has_restricted_voice_and_video_messages"` - HiddenMembers bool `json:"has_hidden_members,omitempty"` - AggressiveAntiSpam bool `json:"has_aggressive_anti_spam_enabled,omitempty"` - EmojiExpirationUnixtime int64 `json:"emoji_status_expiration_date"` + Bio string `json:"bio,omitempty"` + Photo *ChatPhoto `json:"photo,omitempty"` + Description string `json:"description,omitempty"` + InviteLink string `json:"invite_link,omitempty"` + PinnedMessage *Message `json:"pinned_message,omitempty"` + Permissions *Rights `json:"permissions,omitempty"` + Reactions []react.Reaction `json:"available_reactions"` + SlowMode int `json:"slow_mode_delay,omitempty"` + StickerSet string `json:"sticker_set_name,omitempty"` + CanSetStickerSet bool `json:"can_set_sticker_set,omitempty"` + CustomEmojiSetName string `json:"custom_emoji_sticker_set_name"` + LinkedChatID int64 `json:"linked_chat_id,omitempty"` + ChatLocation *ChatLocation `json:"location,omitempty"` + Private bool `json:"has_private_forwards,omitempty"` + Protected bool `json:"has_protected_content,omitempty"` + NoVoiceAndVideo bool `json:"has_restricted_voice_and_video_messages"` + HasHiddenMembers bool `json:"has_hidden_members,omitempty"` + AggressiveAntiSpam bool `json:"has_aggressive_anti_spam_enabled,omitempty"` + CustomEmojiID string `json:"emoji_status_custom_emoji_id"` + EmojiExpirationUnixtime int64 `json:"emoji_status_expiration_date"` + BackgroundEmojiID string `json:"background_custom_emoji_id"` + AccentColorID int `json:"accent_color_id"` + ProfileAccentColorID int `json:"profile_accent_color_id"` + ProfileBackgroundEmojiID string `json:"profile_background_custom_emoji_id"` + HasVisibleHistory bool `json:"has_visible_history"` + UnrestrictBoosts int `json:"unrestrict_boost_count"` } // Recipient returns chat ID (see Recipient interface). diff --git a/context.go b/context.go index fe799c3..df11373 100644 --- a/context.go +++ b/context.go @@ -16,6 +16,12 @@ type Context interface { // Bot returns the bot instance. Bot() *Bot + // Boost returns the boost instance. + Boost() *BoostUpdated + + // BoostRemoved returns the boost removed from a chat instance. + BoostRemoved() *BoostRemoved + // Update returns the original update. Update() Update @@ -178,6 +184,14 @@ func (c *nativeContext) Bot() *Bot { return c.b } +func (c *nativeContext) Boost() *BoostUpdated { + return c.u.Boost +} + +func (c *nativeContext) BoostRemoved() *BoostRemoved { + return c.u.BoostRemoved +} + func (c *nativeContext) Update() Update { return c.u } @@ -187,12 +201,12 @@ func (c *nativeContext) Message() *Message { case c.u.Message != nil: return c.u.Message case c.u.Callback != nil: - return c.u.Callback.Message + return c.u.Callback.Message.Message case c.u.EditedMessage != nil: return c.u.EditedMessage case c.u.ChannelPost != nil: if c.u.ChannelPost.PinnedMessage != nil { - return c.u.ChannelPost.PinnedMessage + return c.u.ChannelPost.PinnedMessage.Message } return c.u.ChannelPost case c.u.EditedChannelPost != nil: diff --git a/giveaway.go b/giveaway.go new file mode 100644 index 0000000..a0b54e2 --- /dev/null +++ b/giveaway.go @@ -0,0 +1,103 @@ +package telebot + +import "time" + +// Giveaway represents a message about a scheduled giveaway. +type Giveaway struct { + // The list of chats which the user must join to participate in the giveaway. + Chats []Chat `json:"chats"` + + // Point in time (Unix timestamp) when winners of the giveaway will be selected. + SelectionUnixtime int64 `json:"winners_selection_date"` + + // The number of users which are supposed to be selected as winners of the giveaway. + WinnerCount int `json:"winner_count"` + + // (Optional) True, if only users who join the chats after the giveaway + // started should be eligible to win. + OnlyNewMembers bool `json:"only_new_members"` + + // (Optional) True, if the list of giveaway winners will be visible to everyone. + HasPublicWinners bool `json:"has_public_winners"` + + // (Optional) Description of additional giveaway prize. + PrizeDescription string `json:"prize_description"` + + // (Optional) A list of two-letter ISO 3166-1 alpha-2 country codes indicating + // the countries from which eligible users for the giveaway must come. + // If empty, then all users can participate in the giveaway. Users with a phone number + // that was bought on Fragment can always participate in giveaways. + CountryCodes []string `json:"country_codes"` + + // (Optional) The number of months the Telegram Premium subscription won from + // the giveaway will be active for. + PremiumMonthCount int `json:"premium_subscription_month_count"` +} + +// SelectionDate returns the moment of when winners of the giveaway were selected in local time. +func (g *Giveaway) SelectionDate() time.Time { + return time.Unix(g.SelectionUnixtime, 0) +} + +// GiveawayWinners object represents a message about the completion of a +// giveaway with public winners. +type GiveawayWinners struct { + // The chat that created the giveaway. + Chat *Chat `json:"chat"` + + // Identifier of the message with the giveaway in the chat. + MessageID int `json:"message_id"` + + // Point in time (Unix timestamp) when winners of the giveaway were selected. + SelectionUnixtime int64 `json:"winners_selection_date"` + + // The number of users which are supposed to be selected as winners of the giveaway. + WinnerCount int `json:"winner_count"` + + // List of up to 100 winners of the giveaway. + Winners []User `json:"winners"` + + // (Optional) The number of other chats the user had to join in order + // to be eligible for the giveaway. + AdditionalChats int `json:"additional_chat_count"` + + // (Optional) The number of months the Telegram Premium subscription won from + // the giveaway will be active for. + PremiumMonthCount int `json:"premium_subscription_month_count"` + + // (Optional) Number of undistributed prizes. + UnclaimedPrizes int `json:"unclaimed_prize_count"` + + // (Optional) True, if only users who had joined the chats after the giveaway started + // were eligible to win. + OnlyNewMembers bool `json:"only_new_members"` + + // (Optional) True, if the giveaway was canceled because the payment for it was refunded. + Refunded bool `json:"was_refunded"` + + // (Optional) Description of additional giveaway prize. + PrizeDescription string `json:"prize_description"` +} + +// SelectionDate returns the moment of when winners of the giveaway +// were selected in local time. +func (g *GiveawayWinners) SelectionDate() time.Time { + return time.Unix(g.SelectionUnixtime, 0) +} + +// GiveawayCreated represents a service message about the creation of a scheduled giveaway. +// Currently holds no information. +type GiveawayCreated struct{} + +// GiveawayCompleted represents a service message about the completion of a +// giveaway without public winners. +type GiveawayCompleted struct { + // Number of winners in the giveaway. + WinnerCount int `json:"winner_count"` + + // (Optional) Number of undistributed prizes. + UnclaimedPrizes int `json:"unclaimed_prize_count"` + + // (Optional) Message with the giveaway that was completed, if it wasn't deleted. + Message *Message `json:"giveaway_message"` +} diff --git a/input_types.go b/input_types.go index 8186c07..8c33096 100644 --- a/input_types.go +++ b/input_types.go @@ -12,12 +12,12 @@ type InputTextMessageContent struct { // Text of the message to be sent, 1-4096 characters. Text string `json:"message_text"` - // Optional. Send Markdown or HTML, if you want Telegram apps to show + // (Optional) Send Markdown or HTML, if you want Telegram apps to show // bold, italic, fixed-width text or inline URLs in your bot's message. ParseMode string `json:"parse_mode,omitempty"` - // Optional. Disables link previews for links in the sent message. - DisablePreview bool `json:"disable_web_page_preview"` + // (Optional) Link preview generation options for the message. + PreviewOptions *PreviewOptions `json:"link_preview_options"` } func (input *InputTextMessageContent) IsInputMessageContent() bool { diff --git a/markup.go b/markup.go index 82d43e0..b45990c 100644 --- a/markup.go +++ b/markup.go @@ -220,7 +220,7 @@ type ReplyButton struct { Contact bool `json:"request_contact,omitempty"` Location bool `json:"request_location,omitempty"` Poll PollType `json:"request_poll,omitempty"` - User *ReplyRecipient `json:"request_user,omitempty"` + User *ReplyRecipient `json:"request_users,omitempty"` Chat *ReplyRecipient `json:"request_chat,omitempty"` WebApp *WebApp `json:"web_app,omitempty"` } @@ -244,8 +244,9 @@ func (pt PollType) MarshalJSON() ([]byte, error) { type ReplyRecipient struct { ID int32 `json:"request_id"` - Bot *bool `json:"user_is_bot,omitempty"` // user only, optional - Premium *bool `json:"user_is_premium,omitempty"` // user only, optional + Bot *bool `json:"user_is_bot,omitempty"` // user only, optional + Premium *bool `json:"user_is_premium,omitempty"` // user only, optional + Quantity int `json:"max_quantity,omitempty"` // user only, optional Channel bool `json:"chat_is_channel,omitempty"` // chat only, required Forum *bool `json:"chat_is_forum,omitempty"` // chat only, optional diff --git a/message.go b/message.go index d3fee36..1dd07c3 100644 --- a/message.go +++ b/message.go @@ -1,9 +1,12 @@ package telebot import ( + "encoding/json" "strconv" "time" "unicode/utf16" + + "gopkg.in/telebot.v3/react" ) // Message object represents a message. @@ -46,6 +49,9 @@ type Message struct { // For forwarded messages, unixtime of the original message. OriginalUnixtime int `json:"forward_date"` + // For information about the original message for forwarded messages. + Origin *MessageOrigin `json:"forward_origin"` + // Message is a channel post that was automatically forwarded to the connected discussion group. AutomaticForward bool `json:"is_automatic_forward"` @@ -57,11 +63,22 @@ type Message struct { ReplyTo *Message `json:"reply_to_message"` // (Optional) For replies to a story, the original story - Story *Story `json:"reply_to_story"` + Story *Story `json:"story"` + + // (Optional) Information about the message that is being replied to, + // which may come from another chat or forum topic. + ExternalReplyInfo *ExternalReplyInfo `json:"external_reply"` + + // (Optional) For replies that quote part of the original message, + // the quoted part of the message. + Quote *TextQuote `json:"quote"` // Shows through which bot the message was sent. Via *User `json:"via_bot"` + // For replies to a story, the original story. + ReplyToStory *Story `json:"reply_to_story"` + // (Optional) Time of last edit in Unix. LastEdit int64 `json:"edit_date"` @@ -90,6 +107,10 @@ type Message struct { // etc. that appear in the text. Entities Entities `json:"entities,omitempty"` + // (Optional) Options used for link preview generation for the message, + // if it is a text message and link preview options were changed + PreviewOptions PreviewOptions `json:"link_preview_options,omitempty"` + // Some messages containing media, may as well have a caption. Caption string `json:"caption,omitempty"` @@ -139,6 +160,18 @@ type Message struct { // For a dice, information about it. Dice *Dice `json:"dice"` + // (Optional) The message is a scheduled giveaway message. + Giveaway *Giveaway `json:"giveaway"` + + // (Optional) A giveaway with public winners was completed. + GiveawayWinners *GiveawayWinners `json:"giveaway_winners"` + + // (Optional) Service message: a scheduled giveaway was created. + GiveawayCreated *GiveawayCreated `json:"giveaway_created"` + + // (Optional) Service message: a giveaway without public winners was completed. + GiveawayCompleted *GiveawayCompleted `json:"giveaway_completed"` + // For a service message, represents a user, // that just got added to chat, this message came from. // @@ -224,7 +257,7 @@ type Message struct { // Specified message was pinned. Note that the Message object // in this field will not contain further ReplyTo fields even // if it is itself a reply. - PinnedMessage *Message `json:"pinned_message"` + PinnedMessage *InaccessibleMessage `json:"pinned_message"` // Message is an invoice for a payment. Invoice *Invoice `json:"invoice"` @@ -233,7 +266,7 @@ type Message struct { Payment *Payment `json:"successful_payment"` // For a service message, a user was shared with the bot. - UserShared *RecipientShared `json:"user_shared,omitempty"` + UserShared *RecipientShared `json:"users_shared,omitempty"` // For a service message, a chat was shared with the bot. ChatShared *RecipientShared `json:"chat_shared,omitempty"` @@ -266,6 +299,13 @@ type Message struct { // Inline keyboard attached to the message. ReplyMarkup *ReplyMarkup `json:"reply_markup,omitempty"` + // Service message: user boosted the chat. + BoostAdded *BoostAdded `json:"boost_added"` + + // If the sender of the message boosted the chat, the number of boosts + // added by the user. + SenderBoostCount int `json:"sender_boost_count"` + // Service message: forum topic created TopicCreated *Topic `json:"forum_topic_created,omitempty"` @@ -465,3 +505,270 @@ func (m *Message) Media() Media { return nil } } + +// InaccessibleMessage describes a message that was deleted or is otherwise +// inaccessible to the bot. An instance of MaybeInaccessibleMessage object. +type InaccessibleMessage struct { + // A message that can be inaccessible to the bot. + *Message + + // Chat the message belonged to. + Chat *Chat `json:"chat"` + + // Unique message identifier inside the chat. + MessageID int `json:"message_id"` + + // Always 0. The field can be used to differentiate regular and + // inaccessible messages. + DateUnixtime int64 `json:"date"` +} + +func (im *InaccessibleMessage) MessageSig() (string, int64) { + return strconv.Itoa(im.MessageID), im.Chat.ID +} + +func (im *InaccessibleMessage) Time() time.Time { + return time.Unix(im.DateUnixtime, 0) +} + +// MessageReaction object represents a change of a reaction on a message performed by a user. +type MessageReaction struct { + // The chat containing the message the user reacted to. + Chat *Chat `json:"chat"` + + // Unique identifier of the message inside the chat. + MessageID int `json:"message_id"` + + // (Optional) The user that changed the reaction, + // if the user isn't anonymous + User *User `json:"user"` + + // (Optional) The chat on behalf of which the reaction was changed, + // if the user is anonymous. + ActorChat *Chat `json:"actor_chat"` + + // Date of the change in Unix time. + DateUnixtime int64 `json:"date"` + + // Previous list of reaction types that were set by the user. + OldReaction []react.Reaction `json:"old_reaction"` + + // New list of reaction types that have been set by the user. + NewReaction []react.Reaction `json:"new_reaction"` +} + +func (mu *MessageReaction) Time() time.Time { + return time.Unix(mu.DateUnixtime, 0) +} + +// MessageReactionCount represents reaction changes on a message with +// anonymous reactions. +type MessageReactionCount struct { + // The chat containing the message. + Chat *Chat `json:"chat"` + + // Unique message identifier inside the chat. + MessageID int `json:"message_id"` + + // Date of the change in Unix time. + DateUnixtime int64 `json:"date"` + + // List of reactions that are present on the message. + Reactions *react.Count `json:"reactions"` +} + +// Time returns the moment of change in local time. +func (mc *MessageReactionCount) Time() time.Time { + return time.Unix(mc.DateUnixtime, 0) +} + +// 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 a message that is replied to by the given message. + Text string `json:"text"` + + // (Optional) Special entities that appear in the quote. + // Currently, only bold, italic, underline, strikethrough, spoiler, + // and custom_emoji entities are kept in quotes. + Entities []MessageEntity `json:"entities"` + + // Approximate quote position in the original message in UTF-16 code units + // as specified by the sender. + Position int `json:"position"` + + // (Optional) True, if the quote was chosen manually by the message sender. + // Otherwise, the quote was added automatically by the server. + Manual bool `json:"is_manual"` +} + +// MessageOrigin a message reference that has been sent originally by a known user. +type MessageOrigin struct { + // Type of the message origin, always “channel”. + Type string `json:"type"` + + // Date the message was sent originally in Unix time. + DateUnixtime int64 `json:"date"` + + // User that sent the message originally. + Sender *User `json:"sender_user,omitempty"` + + // Name of the user that sent the message originally. + SenderUsername string `json:"sender_user_name,omitempty"` + + // Chat that sent the message originally. + SenderChat *Chat `json:"sender_chat,omitempty"` + + // Channel chat to which the message was originally sent. + Chat *Chat `json:"chat,omitempty"` + + // Unique message identifier inside the chat. + MessageID int `json:"message_id,omitempty"` + + // (Optional) For messages originally sent by an anonymous chat administrator, + // original message author signature. + Signature string `json:"author_signature,omitempty"` +} + +// Time returns the moment of message that was sent originally in local time. +func (mo *MessageOrigin) Time() time.Time { + return time.Unix(mo.DateUnixtime, 0) +} + +// ExternalReplyInfo contains information about a message that is being replied to, +// which may come from another chat or forum topic. +type ExternalReplyInfo struct { + // Origin of the message replied to by the given message. + Origin *MessageOrigin `json:"origin"` + + // (Optional) Chat the original message belongs to. + // Available only if the chat is a supergroup or a channel. + Chat *Chat `json:"chat"` + + // (Optional) Unique message identifier inside the original chat. + // Available only if the original chat is a supergroup or a channel. + MessageID int `json:"message_id"` + + // (Optional) Options used for link preview generation for the original message, + // if it is a text message. + PreviewOptions *PreviewOptions `json:"link_preview_options"` + + // (Optional) Message is an animation, information about the animation. + Animation *Animation `json:"animation"` + + // (Optional) Message is an audio file, information about the file. + Audio *Audio `json:"audio"` + + // (Optional) Message is a general file, information about the file. + Document *Document `json:"document"` + + // (Optional) Message is a photo, available sizes of the photo. + Photo []Photo `json:"photo"` + + // (Optional) Message is a sticker, information about the sticker. + Sticker *Sticker `json:"sticker"` + + // (Optional) Message is a forwarded story. + Story *Story `json:"story"` + + // (Optional) Message is a video, information about the video. + Video *Video `json:"video"` + + // (Optional) Message is a video note, information about the video message. + Note *VideoNote `json:"video_note"` + + // (Optional) Message is a voice message, information about the file. + Voice *Voice `json:"voice"` + + // (Optional) True, if the message media is covered by a spoiler animation. + HasMediaSpoiler bool `json:"has_media_spoiler"` + + // (Optional) Message is a shared contact, information about the contact. + Contact *Contact `json:"contact"` + + // (Optional) Message is a dice with random value. + Dice *Dice `json:"dice"` + + //( Optional) Message is a game, information about the game. + Game *Game `json:"game"` + + // (Optional) Message is a venue, information about the venue. + Venue *Venue `json:"venue"` + + // (Optional) Message is a native poll, information about the poll. + Poll *Poll `json:"poll"` + + // (Optional) Message is a shared location, information about the location. + Location *Location `json:"location"` + + // (Optional) Message is an invoice for a payment, information about the invoice. + Invoice *Invoice `json:"invoice"` + + // (Optional) Message is a scheduled giveaway, information about the giveaway. + Giveaway *Giveaway `json:"giveaway"` + + // (Optional) A giveaway with public winners was completed. + GiveawayWinners *GiveawayWinners `json:"giveaway_winners"` +} + +// ReplyParams describes reply parameters for the message that is being sent. +type ReplyParams struct { + // Identifier of the message that will be replied to in the current chat, + // or in the chat chat_id if it is specified. + MessageID int `json:"message_id"` + + // (Optional) If the message to be replied to is from a different chat, + // unique identifier for the chat or username of the channel. + ChatID int64 `json:"chat_id"` + + // Optional. Pass True if the message should be sent even if the specified message + // to be replied to is not found; can be used only for replies in the + // same chat and forum topic. + AllowWithoutReply bool `json:"allow_sending_without_reply"` + + // (Optional) Quoted part of the message to be replied to; 0-1024 characters after + // entities parsing. The quote must be an exact substring of the message to be replied to, + // including bold, italic, underline, strikethrough, spoiler, and custom_emoji entities. + // The message will fail to send if the quote isn't found in the original message. + Quote string `json:"quote"` + + // (Optional) Mode for parsing entities in the quote. + QuoteParseMode ParseMode `json:"quote_parse_mode"` + + // (Optional) A JSON-serialized list of special entities that appear in the quote. + // It can be specified instead of quote_parse_mode. + QuoteEntities []MessageEntity `json:"quote_entities"` + + // (Optional) Position of the quote in the original message in UTF-16 code units. + QuotePosition int `json:"quote_position"` +} + +// React changes the chosen reactions on a message. Service messages can't be +// reacted to. Automatically forwarded messages from a channel to its discussion group have +// the same available reactions as messages in the channel. +func (b *Bot) React(to Recipient, msg Editable, opts ...react.Options) error { + if to == nil { + return ErrBadRecipient + } + msgID, _ := msg.MessageSig() + + params := map[string]string{ + "chat_id": to.Recipient(), + "message_id": msgID, + } + + if len(opts) > 0 { + opt := opts[0] + + if len(opt.Reactions) > 0 { + data, _ := json.Marshal(opt.Reactions) + params["reaction"] = string(data) + } + if opt.Big { + params["is_big"] = "true" + } + } + + _, err := b.Raw("setMessageReaction", params) + return err +} diff --git a/options.go b/options.go index 56e0d9c..5f38265 100644 --- a/options.go +++ b/options.go @@ -84,6 +84,8 @@ type SendOptions struct { // HasSpoiler marks the message as containing a spoiler. HasSpoiler bool + // ReplyParams Describes the message to reply to + ReplyParams *ReplyParams } func (og *SendOptions) copy() *SendOptions { @@ -105,6 +107,8 @@ func extractOptions(how []interface{}) *SendOptions { if opt != nil { opts.ReplyMarkup = opt.copy() } + case *ReplyParams: + opts.ReplyParams = opt case Option: switch opt { case NoPreview: @@ -198,7 +202,7 @@ func (b *Bot) embedSendOptions(params map[string]string, opt *SendOptions) { if opt.ThreadID != 0 { params["message_thread_id"] = strconv.Itoa(opt.ThreadID) } - + if opt.HasSpoiler { params["spoiler"] = "true" } @@ -224,3 +228,45 @@ func processButtons(keys [][]InlineButton) { } } } + +// PreviewOptions describes the options used for link preview generation. +type PreviewOptions struct { + // (Optional) True, if the link preview is disabled. + Disabled bool `json:"is_disabled"` + + // (Optional) URL to use for the link preview. If empty, then the first URL + // found in the message text will be used. + URL string `json:"url"` + + // (Optional) 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. + SmallMedia bool `json:"prefer_small_media"` + + // (Optional) 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. + LargeMedia bool `json:"prefer_large_media"` + + // (Optional) True, if the link preview must be shown above the message text; + // otherwise, the link preview will be shown below the message text. + AboveText bool `json:"show_above_text"` +} + +func embedMessages(params map[string]string, msgs []Editable) { + ids := make([]string, 0, len(msgs)) + + _, chatID := msgs[0].MessageSig() + for _, msg := range msgs { + msgID, _ := msg.MessageSig() + ids = append(ids, msgID) + } + + data, err := json.Marshal(ids) + if err != nil { + return + } + + params["message_ids"] = string(data) + params["chat_id"] = strconv.FormatInt(chatID, 10) +} diff --git a/react/reaction.go b/react/reaction.go new file mode 100644 index 0000000..32d1fd1 --- /dev/null +++ b/react/reaction.go @@ -0,0 +1,112 @@ +package react + +// EmojiType defines emoji types. +type EmojiType = string + +// Currently available emojis. +var ( + ThumbUp = Reaction{Emoji: "👍"} + ThumbDown = Reaction{Emoji: "👎"} + Heart = Reaction{Emoji: "❤"} + Fire = Reaction{Emoji: "🔥"} + HeartEyes = Reaction{Emoji: "😍"} + ClappingHands = Reaction{Emoji: "👏"} + GrinningFace = Reaction{Emoji: "😁"} + ThinkingFace = Reaction{Emoji: "🤔"} + ExplodingHead = Reaction{Emoji: "🤯"} + ScreamingFace = Reaction{Emoji: "😱"} + SwearingFace = Reaction{Emoji: "🤬"} + CryingFace = Reaction{Emoji: "😢"} + PartyPopper = Reaction{Emoji: "🎉"} + StarStruck = Reaction{Emoji: "🤩"} + VomitingFace = Reaction{Emoji: "🤮"} + PileOfPoo = Reaction{Emoji: "💩"} + PrayingHands = Reaction{Emoji: "🙏"} + OkHand = Reaction{Emoji: "👌"} + DoveOfPeace = Reaction{Emoji: "🕊"} + ClownFace = Reaction{Emoji: "🤡"} + YawningFace = Reaction{Emoji: "🥱"} + WoozyFace = Reaction{Emoji: "🥴"} + Whale = Reaction{Emoji: "🐳"} + HeartOnFire = Reaction{Emoji: "❤‍🔥"} + MoonFace = Reaction{Emoji: "🌚"} + HotDog = Reaction{Emoji: "🌭"} + HundredPoints = Reaction{Emoji: "💯"} + RollingOnTheFloorLaughing = Reaction{Emoji: "🤣"} + Lightning = Reaction{Emoji: "⚡"} + Banana = Reaction{Emoji: "🍌"} + Trophy = Reaction{Emoji: "🏆"} + BrokenHeart = Reaction{Emoji: "💔"} + FaceWithRaisedEyebrow = Reaction{Emoji: "🤨"} + NeutralFace = Reaction{Emoji: "😐"} + Strawberry = Reaction{Emoji: "🍓"} + Champagne = Reaction{Emoji: "🍾"} + KissMark = Reaction{Emoji: "💋"} + MiddleFinger = Reaction{Emoji: "🖕"} + EvilFace = Reaction{Emoji: "😈"} + SleepingFace = Reaction{Emoji: "😴"} + LoudlyCryingFace = Reaction{Emoji: "😭"} + NerdFace = Reaction{Emoji: "🤓"} + Ghost = Reaction{Emoji: "👻"} + Engineer = Reaction{Emoji: "👨‍💻"} + Eyes = Reaction{Emoji: "👀"} + JackOLantern = Reaction{Emoji: "🎃"} + NoMonkey = Reaction{Emoji: "🙈"} + SmilingFaceWithHalo = Reaction{Emoji: "😇"} + FearfulFace = Reaction{Emoji: "😨"} + Handshake = Reaction{Emoji: "🤝"} + WritingHand = Reaction{Emoji: "✍"} + HuggingFace = Reaction{Emoji: "🤗"} + Brain = Reaction{Emoji: "🫡"} + SantaClaus = Reaction{Emoji: "🎅"} + ChristmasTree = Reaction{Emoji: "🎄"} + Snowman = Reaction{Emoji: "☃"} + NailPolish = Reaction{Emoji: "💅"} + ZanyFace = Reaction{Emoji: "🤪"} + Moai = Reaction{Emoji: "🗿"} + Cool = Reaction{Emoji: "🆒"} + HeartWithArrow = Reaction{Emoji: "💘"} + HearMonkey = Reaction{Emoji: "🙉"} + Unicorn = Reaction{Emoji: "🦄"} + FaceBlowingKiss = Reaction{Emoji: "😘"} + Pill = Reaction{Emoji: "💊"} + SpeaklessMonkey = Reaction{Emoji: "🙊"} + Sunglasses = Reaction{Emoji: "😎"} + AlienMonster = Reaction{Emoji: "👾"} + ManShrugging = Reaction{Emoji: "🤷‍♂️"} + PersonShrugging = Reaction{Emoji: "🤷"} + WomanShrugging = Reaction{Emoji: "🤷‍♀️"} + PoutingFace = Reaction{Emoji: "😡"} +) + +// Reaction describes the type of reaction. +// Describes an instance of ReactionTypeCustomEmoji and ReactionTypeEmoji. +type Reaction struct { + // Type of the reaction, always “emoji” + Type string `json:"type"` + + // Reaction emoji. + Emoji EmojiType `json:"emoji,omitempty"` + + // Custom emoji identifier. + CustomEmoji string `json:"custom_emoji_id,omitempty"` +} + +// Count represents a reaction added to a message along +// with the number of times it was added. +type Count struct { + // Type of the reaction. + Type Reaction `json:"type"` + + // Number of times the reaction was added. + Count int `json:"total_count"` +} + +// Options represents an object of reaction options. +type Options struct { + // List of reaction types to set on the message. + Reactions []Reaction `json:"reaction"` + + // Pass True to set the reaction with a big animation. + Big bool `json:"is_big"` +} diff --git a/telebot.go b/telebot.go index b8271e9..0e35062 100644 --- a/telebot.go +++ b/telebot.go @@ -109,6 +109,9 @@ const ( OnVideoChatEnded = "\avideo_chat_ended" OnVideoChatParticipants = "\avideo_chat_participants_invited" OnVideoChatScheduled = "\avideo_chat_scheduled" + + OnBoost = "\aboost_updated" + onBoostRemoved = "\aboost_removed" ) // ChatAction is a client-side status indicating bot activity. diff --git a/update.go b/update.go index 12a065a..edd49b6 100644 --- a/update.go +++ b/update.go @@ -6,20 +6,24 @@ import "strings" type Update struct { ID int `json:"update_id"` - Message *Message `json:"message,omitempty"` - EditedMessage *Message `json:"edited_message,omitempty"` - ChannelPost *Message `json:"channel_post,omitempty"` - EditedChannelPost *Message `json:"edited_channel_post,omitempty"` - Callback *Callback `json:"callback_query,omitempty"` - Query *Query `json:"inline_query,omitempty"` - InlineResult *InlineResult `json:"chosen_inline_result,omitempty"` - ShippingQuery *ShippingQuery `json:"shipping_query,omitempty"` - PreCheckoutQuery *PreCheckoutQuery `json:"pre_checkout_query,omitempty"` - Poll *Poll `json:"poll,omitempty"` - PollAnswer *PollAnswer `json:"poll_answer,omitempty"` - MyChatMember *ChatMemberUpdate `json:"my_chat_member,omitempty"` - ChatMember *ChatMemberUpdate `json:"chat_member,omitempty"` - ChatJoinRequest *ChatJoinRequest `json:"chat_join_request,omitempty"` + Message *Message `json:"message,omitempty"` + EditedMessage *Message `json:"edited_message,omitempty"` + ChannelPost *Message `json:"channel_post,omitempty"` + EditedChannelPost *Message `json:"edited_channel_post,omitempty"` + MessageReaction *MessageReaction `json:"message_reaction"` + MessageReactionCount *MessageReactionCount `json:"message_reaction_count"` + Callback *Callback `json:"callback_query,omitempty"` + Query *Query `json:"inline_query,omitempty"` + InlineResult *InlineResult `json:"chosen_inline_result,omitempty"` + ShippingQuery *ShippingQuery `json:"shipping_query,omitempty"` + PreCheckoutQuery *PreCheckoutQuery `json:"pre_checkout_query,omitempty"` + Poll *Poll `json:"poll,omitempty"` + PollAnswer *PollAnswer `json:"poll_answer,omitempty"` + MyChatMember *ChatMemberUpdate `json:"my_chat_member,omitempty"` + ChatMember *ChatMemberUpdate `json:"chat_member,omitempty"` + ChatJoinRequest *ChatJoinRequest `json:"chat_join_request,omitempty"` + Boost *BoostUpdated `json:"chat_boost"` + BoostRemoved *BoostRemoved `json:"removed_chat_boost"` } // ProcessUpdate processes a single incoming update. @@ -308,6 +312,16 @@ func (b *Bot) ProcessUpdate(u Update) { b.handle(OnChatJoinRequest, c) return } + + if u.Boost != nil { + b.handle(OnBoost, c) + return + } + + if u.BoostRemoved != nil { + b.handle(onBoostRemoved, c) + return + } } func (b *Bot) handle(end string, c Context) bool {