From 82e84ed718636a27a9615891c77a3b5361e36d47 Mon Sep 17 00:00:00 2001 From: Maxim Lebedev Date: Thu, 9 Jan 2020 20:27:00 +0500 Subject: [PATCH] :recycle: Refactoring code --- add.go | 56 - answer.go | 227 --- const.go | 20 +- create.go | 76 - delete.go | 129 -- errors.go | 29 + export.go | 26 - forward.go | 45 - games.go | 165 ++ get.go | 314 ---- go.mod | 9 +- go.sum | 26 +- inline.go | 1143 ++++++++++++++ inline_test.go | 51 + kick.go | 39 - leave.go | 24 - methods.go | 1728 +++++++++++++++++++++ passport.go | 1070 +++++++++++++ payments.go | 347 +++++ pin.go | 34 - restrict.go | 35 - send.go | 796 ---------- set.go | 398 ----- stickers.go | 376 +++++ stickers_test.go | 55 + stop.go | 28 - telegram.go | 118 +- types.go | 2519 ++++++++---------------------- types_test.go | 2121 +++++++++++++++++++++++++ unban.go | 33 - unpin.go | 26 - updates.go | 299 ++++ updates_test.go | 208 +++ edit.go => updating_messages.go | 210 +-- upload.go | 186 --- utils.go | 2558 +------------------------------ utils_test.go | 195 --- 37 files changed, 8503 insertions(+), 7216 deletions(-) delete mode 100644 add.go delete mode 100644 answer.go delete mode 100644 create.go delete mode 100644 delete.go create mode 100644 errors.go delete mode 100644 export.go delete mode 100644 forward.go create mode 100644 games.go delete mode 100644 get.go create mode 100644 inline.go create mode 100644 inline_test.go delete mode 100644 kick.go delete mode 100644 leave.go create mode 100644 methods.go create mode 100644 passport.go create mode 100644 payments.go delete mode 100644 pin.go delete mode 100644 restrict.go delete mode 100644 send.go delete mode 100644 set.go create mode 100644 stickers.go create mode 100644 stickers_test.go delete mode 100644 stop.go create mode 100644 types_test.go delete mode 100644 unban.go delete mode 100644 unpin.go create mode 100644 updates.go create mode 100644 updates_test.go rename edit.go => updating_messages.go (60%) delete mode 100644 upload.go delete mode 100644 utils_test.go diff --git a/add.go b/add.go deleted file mode 100644 index 83c5afa..0000000 --- a/add.go +++ /dev/null @@ -1,56 +0,0 @@ -package telegram - -import ( - "strings" - - http "github.com/valyala/fasthttp" -) - -type AddStickerToSetParameters struct { - // User identifier of sticker set owner - UserID int `json:"user_id"` - - // Sticker set name - Name string `json:"name"` - - // Png image with the sticker, must be up to 512 kilobytes in size, dimensions must not exceed 512px, and either width or height must be exactly 512px. Pass a file_id as a String to send a file that already exists on the Telegram servers, pass an HTTP URL as a String for Telegram to get a file from the Internet, or upload a new one using multipart/form-data. More info on Sending Files » - PNGSticker interface{} `json:"png_sticker"` - - // One or more emoji corresponding to the sticker - Emojis string `json:"emojis"` - - // A JSON-serialized object for position where the mask should be placed on faces - MaskPosition *MaskPosition `json:"mask_position,omitempty"` -} - -// AddStickerToSet add a new sticker to a set created by the bot. Returns True -// on success. -func (b *Bot) AddStickerToSet(params *AddStickerToSetParameters) (bool, error) { - args := http.AcquireArgs() - defer http.ReleaseArgs(args) - args.SetUint("user_id", params.UserID) - - if !strings.HasSuffix(strings.ToLower(params.Name), strings.ToLower("_by_"+b.Username)) { - params.Name = params.Name + "_by_" + b.Username - } - - args.Set("emojis", params.Emojis) - - if params.MaskPosition != nil { - mp, err := parser.Marshal(params.MaskPosition) - if err != nil { - return false, err - } - - args.SetBytesV("mask_position", mp) - } - - resp, err := b.Upload(MethodAddStickerToSet, TypeSticker, "sticker", params.PNGSticker, args) - if err != nil { - return false, err - } - - var ok bool - err = parser.Unmarshal(resp.Result, &ok) - return ok, err -} diff --git a/answer.go b/answer.go deleted file mode 100644 index 99ed496..0000000 --- a/answer.go +++ /dev/null @@ -1,227 +0,0 @@ -package telegram - -type ( - // AnswerCallbackQueryParameters represents data for AnswerCallbackQuery method. - AnswerCallbackQueryParameters struct { - // Unique identifier for the query to be answered - CallbackQueryID string `json:"callback_query_id"` - - // Text of the notification. If not specified, nothing will be shown to the - // user, 0-200 characters - Text string `json:"text,omitempty"` - - // URL that will be opened by the user's client. If you have created a Game - // and accepted the conditions via @Botfather, specify the URL that opens - // your game – note that this will only work if the query comes from a - // callback_game button. - // - // Otherwise, you may use links like t.me/your_bot?start=XXXX that open your - // bot with a parameter. - URL string `json:"url,omitempty"` - - // If true, an alert will be shown by the client instead of a notification at - // the top of the chat screen. Defaults to false. - ShowAlert bool `json:"show_alert,omitempty"` - - // The maximum amount of time in seconds that the result of the callback - // query may be cached client-side. Telegram apps will support caching - // starting in version 3.14. Defaults to 0. - CacheTime int `json:"cache_time,omitempty"` - } - - // AnswerPreCheckoutQueryParameters represents data for AnswerPreCheckoutQuery - // method. - AnswerPreCheckoutQueryParameters struct { - // Unique identifier for the query to be answered - PreCheckoutQueryID string `json:"pre_checkout_query_id"` - - // Required if ok is False. Error message in human readable form that - // explains the reason for failure to proceed with the checkout (e.g. "Sorry, - // somebody just bought the last of our amazing black T-shirts while you were - // busy filling out your payment details. Please choose a different color or - // garment!"). Telegram will display this message to the user. - ErrorMessage string `json:"error_message,omitempty"` - - // Specify True if everything is alright (goods are available, etc.) and the - // bot is ready to proceed with the order. Use False if there are any - // problems. - Ok bool `json:"ok"` - } - - // AnswerShippingQueryParameters represents data for AnswerShippingQuery method. - AnswerShippingQueryParameters struct { - // Unique identifier for the query to be answered - ShippingQueryID string `json:"shipping_query_id"` - - // Required if ok is False. Error message in human readable form that - // explains why it is impossible to complete the order (e.g. "Sorry, delivery - // to your desired address is unavailable'). Telegram will display this - // message to the user. - ErrorMessage string `json:"error_message,omitempty"` - - // Specify True if delivery to the specified address is possible and False - // if there are any problems (for example, if delivery to the specified - // address is not possible) - Ok bool `json:"ok"` - - // Required if ok is True. A JSON-serialized array of available shipping - // options. - ShippingOptions []ShippingOption `json:"shipping_options,omitempty"` - } - - // AnswerInlineQueryParameters represents data for AnswerInlineQuery method. - AnswerInlineQueryParameters struct { - // Unique identifier for the answered query - InlineQueryID string `json:"inline_query_id"` - - // Pass the offset that a client should send in the next query with the same - // text to receive more results. Pass an empty string if there are no more - // results or if you don‘t support pagination. Offset length can’t exceed 64 - // bytes. - NextOffset string `json:"next_offset,omitempty"` - - // If passed, clients will display a button with specified text that switches - // the user to a private chat with the bot and sends the bot a start message - // with the parameter switch_pm_parameter - SwitchPrivateMessageText string `json:"switch_pm_text,omitempty"` - - // Deep-linking parameter for the /start message sent to the bot when user - // presses the switch button. 1-64 characters, only A-Z, a-z, 0-9, _ and - - // are allowed. - SwitchPrivateMessageParameter string `json:"switch_pm_parameter,omitempty"` - - // A JSON-serialized array of results for the inline query - Results []interface{} `json:"results"` - - // The maximum amount of time in seconds that the result of the inline query - // may be cached on the server. Defaults to 300. - CacheTime int `json:"cache_time,omitempty"` - - // Pass True, if results may be cached on the server side only for the user - // that sent the query. By default, results may be returned to any user who - // sends the same query - IsPersonal bool `json:"is_personal,omitempty"` - } -) - -// NewAnswerCallbackQuery creates AnswerCallbackQueryParameters only with -// required parameters. -func NewAnswerCallbackQuery(callbackQueryID string) *AnswerCallbackQueryParameters { - return &AnswerCallbackQueryParameters{CallbackQueryID: callbackQueryID} -} - -// NewAnswerPreCheckoutQuery creates AnswerPreCheckoutQueryParameters only with -// required parameters. -func NewAnswerPreCheckoutQuery(preCheckoutQueryID string, ok bool) *AnswerPreCheckoutQueryParameters { - return &AnswerPreCheckoutQueryParameters{ - PreCheckoutQueryID: preCheckoutQueryID, - Ok: ok, - } -} - -// NewAnswerShippingQuery creates AnswerShippingQueryParameters only with -// required parameters. -func NewAnswerShippingQuery(shippingQueryID string, ok bool) *AnswerShippingQueryParameters { - return &AnswerShippingQueryParameters{ - ShippingQueryID: shippingQueryID, - Ok: ok, - } -} - -// NewAnswerInlineQuery creates AnswerInlineQueryParameters only with required -// parameters. -func NewAnswerInlineQuery(inlineQueryID string, results ...interface{}) *AnswerInlineQueryParameters { - return &AnswerInlineQueryParameters{ - InlineQueryID: inlineQueryID, - Results: results, - } -} - -// AnswerCallbackQuery send answers to callback queries sent from inline -// keyboards. The answer will be displayed to the user as a notification at the -// top of the chat screen or as an alert. On success, True is returned. -// -// Alternatively, the user can be redirected to the specified Game URL. For this -// option to work, you must first create a game for your bot via @Botfather and -// accept the terms. Otherwise, you may use links like t.me/your_bot?start=XXXX -// that open your bot with a parameter. -func (bot *Bot) AnswerCallbackQuery(params *AnswerCallbackQueryParameters) (bool, error) { - dst, err := parser.Marshal(params) - if err != nil { - return false, err - } - - resp, err := bot.request(dst, MethodAnswerCallbackQuery) - if err != nil { - return false, err - } - - var ok bool - err = parser.Unmarshal(resp.Result, &ok) - return ok, err -} - -// AnswerPreCheckoutQuery respond to such pre-checkout queries. -// -// Once the user has confirmed their payment and shipping details, the Bot API -// sends the final confirmation in the form of an Update with the field -// pre_checkout_query. Use this method to respond to such pre-checkout queries. -// On success, True is returned. -// -// Note: The Bot API must receive an answer within 10 seconds after the -// pre-checkout query was sent. -func (bot *Bot) AnswerPreCheckoutQuery(params *AnswerShippingQueryParameters) (bool, error) { - dst, err := parser.Marshal(params) - if err != nil { - return false, err - } - - resp, err := bot.request(dst, MethodAnswerPreCheckoutQuery) - if err != nil { - return false, err - } - - var ok bool - err = parser.Unmarshal(resp.Result, &ok) - return ok, err -} - -// AnswerShippingQuery reply to shipping queries. -// -// If you sent an invoice requesting a shipping address and the parameter -// is_flexible was specified, the Bot API will send an Update with a -// shipping_query field to the bot. On success, True is returned. -func (bot *Bot) AnswerShippingQuery(params *AnswerShippingQueryParameters) (bool, error) { - dst, err := parser.Marshal(params) - if err != nil { - return false, err - } - - resp, err := bot.request(dst, MethodAnswerShippingQuery) - if err != nil { - return false, err - } - - var ok bool - err = parser.Unmarshal(resp.Result, &ok) - return ok, err -} - -// AnswerInlineQuery send answers to an inline query. On success, True is returned. -// -// No more than 50 results per query are allowed. -func (bot *Bot) AnswerInlineQuery(params *AnswerInlineQueryParameters) (bool, error) { - dst, err := parser.Marshal(params) - if err != nil { - return false, err - } - - resp, err := bot.request(dst, MethodAnswerInlineQuery) - if err != nil { - return false, err - } - - var ok bool - err = parser.Unmarshal(resp.Result, &ok) - return ok, err -} diff --git a/const.go b/const.go index 579966b..048ff1e 100644 --- a/const.go +++ b/const.go @@ -1,7 +1,9 @@ package telegram +import "github.com/Masterminds/semver" + // Version represents current version of Telegram API supported by this package -const Version string = "4.4" +var Version = semver.MustParse("4.5.0") // Action represents available and supported status actions of bot const ( @@ -25,14 +27,14 @@ const ( ChatSuperGroup string = "supergroup" ) -// Command represents global commands which should be supported by any bot. +// Command represents global commands which should be supported by any b. // You can user IsCommandEqual method of Message for checking. // // See: https://core.telegram.org/bots#global-commands const ( - CommandStart string = "start" CommandHelp string = "help" CommandSettings string = "settings" + CommandStart string = "start" ) // Entity represents available and supported entity types @@ -131,6 +133,13 @@ const ( ParseModeMarkdownV2 string = "MarkdownV2" ) +const ( + PointForehead string = "forehead" + PointEyes string = "eyes" + PointMouth string = "mouth" + PointChin string = "chin" +) + // Mime represents available and supported MIME types of data const ( MimeHTML string = "text/html" @@ -198,3 +207,8 @@ const ( UpdatePreCheckoutQuery string = "pre_checkout_query" UpdateShippingQuery string = "shipping_query" ) + +const ( + DefaultAudioSeparator = " – " + DefaultAudioTitle = "[untitled]" +) diff --git a/create.go b/create.go deleted file mode 100644 index aef2b77..0000000 --- a/create.go +++ /dev/null @@ -1,76 +0,0 @@ -package telegram - -import ( - "strconv" - "strings" - - http "github.com/valyala/fasthttp" -) - -type CreateNewStickerSetParameters struct { - // User identifier of created sticker set owner - UserID int `json:"user_id"` - - // Short name of sticker set, to be used in t.me/addstickers/ URLs - // (e.g., animals). Can contain only english letters, digits and - // underscores. Must begin with a letter, can't contain consecutive - // underscores and must end in “_by_”. - // is case insensitive. 1-64 characters. - Name string `json:"name"` - - // Sticker set title, 1-64 characters - Title string `json:"title"` - - // Png image with the sticker, must be up to 512 kilobytes in size, - // dimensions must not exceed 512px, and either width or height must - // be exactly 512px. Pass a file_id as a String to send a file that - // already exists on the Telegram servers, pass an HTTP URL as a - // String for Telegram to get a file from the Internet, or upload - // a new one using multipart/form-data. - PNGSticker interface{} `json:"png_sticker"` - - // One or more emoji corresponding to the sticker - Emojis string `json:"emojis"` - - // Pass True, if a set of mask stickers should be created - ContainsMasks bool `json:"contains_masks,omitempty"` - - // A JSON-serialized object for position where the mask should be - // placed on faces - MaskPosition *MaskPosition `json:"mask_position,omitempty"` -} - -// CreateNewStickerSet create new sticker set owned by a user. The bot will be -// able to edit the created sticker set. Returns True on success. -func (b *Bot) CreateNewStickerSet(params *CreateNewStickerSetParameters) (bool, error) { - args := http.AcquireArgs() - defer http.ReleaseArgs(args) - args.SetUint("user_id", params.UserID) - - if !strings.HasSuffix(strings.ToLower(params.Name), strings.ToLower("_by_"+b.Username)) { - params.Name = params.Name + "_by_" + b.Username - } - - args.Set("name", params.Name) - args.Set("title", params.Title) - args.Set("emojis", params.Emojis) - args.Set("contains_masks", strconv.FormatBool(params.ContainsMasks)) - - if params.MaskPosition != nil { - mp, err := parser.Marshal(params.MaskPosition) - if err != nil { - return false, err - } - - args.SetBytesV("mask_position", mp) - } - - resp, err := b.Upload(MethodCreateNewStickerSet, TypeSticker, "sticker", params.PNGSticker, args) - if err != nil { - return false, err - } - - var ok bool - err = parser.Unmarshal(resp.Result, &ok) - return ok, err -} diff --git a/delete.go b/delete.go deleted file mode 100644 index 23fb526..0000000 --- a/delete.go +++ /dev/null @@ -1,129 +0,0 @@ -package telegram - -type ( - // DeleteChatPhotoParameters represents data for DeleteChatPhoto method. - DeleteChatPhotoParameters struct { - // Unique identifier for the target chat - ChatID int64 `json:"chat_id"` - } - - // DeleteChatStickerSetParameters represents data for DeleteChatStickerSet method. - DeleteChatStickerSetParameters struct { - // Unique identifier for the target chat - ChatID int64 `json:"chat_id"` - } - - // DeleteMessageParameters represents data for DeleteMessage method. - DeleteMessageParameters struct { - // Unique identifier for the target chat - ChatID int64 `json:"chat_id"` - - // Identifier of the message to delete - MessageID int `json:"message_id"` - } - - // DeleteStickerFromSetParameters represents data for DeleteStickerFromSet method. - DeleteStickerFromSetParameters struct { - // File identifier of the sticker - Sticker string `json:"sticker"` - } -) - -// DeleteChatPhoto delete a chat photo. Photos can't be changed for private -// chats. The bot must be an administrator in the chat for this to work and must -// have the appropriate admin rights. Returns True on success. -// -// Note: In regular groups (non-supergroups), this method will only work if the -// 'All Members Are Admins' setting is off in the target group. -func (bot *Bot) DeleteChatPhoto(chatID int64) (bool, error) { - dst, err := parser.Marshal(&DeleteChatPhotoParameters{ChatID: chatID}) - if err != nil { - return false, err - } - - resp, err := bot.request(dst, MethodDeleteChatPhoto) - if err != nil { - return false, err - } - - var ok bool - err = parser.Unmarshal(resp.Result, &ok) - return ok, err -} - -// DeleteChatStickerSet delete a group sticker set from a supergroup. The bot must be an administrator -// in the chat for this to work and must have the appropriate admin rights. Use the field -// can_set_sticker_set optionally returned in getChat requests to check if the bot can use this -// method. Returns True on success. -func (bot *Bot) DeleteChatStickerSet(chatID int64) (bool, error) { - dst, err := parser.Marshal(&DeleteChatStickerSetParameters{ChatID: chatID}) - if err != nil { - return false, err - } - - resp, err := bot.request(dst, MethodDeleteChatStickerSet) - if err != nil { - return false, err - } - - var ok bool - err = parser.Unmarshal(resp.Result, &ok) - return ok, err -} - -// DeleteWebhook remove webhook integration if you decide to switch back to -// getUpdates. Returns True on success. Requires no parameters. -func (bot *Bot) DeleteWebhook() (bool, error) { - resp, err := bot.request(nil, MethodDeleteWebhook) - if err != nil { - return false, err - } - - var ok bool - err = parser.Unmarshal(resp.Result, &ok) - return ok, err -} - -// DeleteMessage delete a message, including service messages, with the following -// limitations: A message can only be deleted if it was sent less than 48 hours -// ago; Bots can delete outgoing messages in groups and supergroups; Bots granted -// can_post_messages permissions can delete outgoing messages in channels; If the -// bot is an administrator of a group, it can delete any message there; If the -// bot has can_delete_messages permission in a supergroup or a channel, it can -// delete any message there. Returns True on success. -func (bot *Bot) DeleteMessage(chatID int64, messageID int) (bool, error) { - dst, err := parser.Marshal(&DeleteMessageParameters{ - ChatID: chatID, - MessageID: messageID, - }) - if err != nil { - return false, err - } - - resp, err := bot.request(dst, MethodDeleteMessage) - if err != nil { - return false, err - } - - var ok bool - err = parser.Unmarshal(resp.Result, &ok) - return ok, err -} - -// DeleteStickerFromSet delete a sticker from a set created by the bot. Returns -// True on success. -func (bot *Bot) DeleteStickerFromSet(sticker string) (bool, error) { - dst, err := parser.Marshal(&DeleteStickerFromSetParameters{Sticker: sticker}) - if err != nil { - return false, err - } - - resp, err := bot.request(dst, MethodDeleteStickerFromSet) - if err != nil { - return false, err - } - - var ok bool - err = parser.Unmarshal(resp.Result, &ok) - return ok, err -} diff --git a/errors.go b/errors.go new file mode 100644 index 0000000..924dcc2 --- /dev/null +++ b/errors.go @@ -0,0 +1,29 @@ +package telegram + +import ( + "fmt" + + "golang.org/x/xerrors" +) + +type Error struct { + Code int `json:"error_code"` + Description string `json:"description"` + Parameters []*ResponseParameters `json:"parameters,omitempty"` + frame xerrors.Frame +} + +func (e Error) FormatError(p xerrors.Printer) error { + p.Printf("%d %s", e.Code, e.Description) + e.frame.Format(p) + + return nil +} + +func (e Error) Format(s fmt.State, r rune) { + xerrors.FormatError(e, s, r) +} + +func (e Error) Error() string { + return fmt.Sprint(e) +} diff --git a/export.go b/export.go deleted file mode 100644 index 534ab18..0000000 --- a/export.go +++ /dev/null @@ -1,26 +0,0 @@ -package telegram - -// ExportChatInviteLinkParameters represents data for ExportChatInviteLink method. -type ExportChatInviteLinkParameters struct { - // Unique identifier for the target chat - ChatID int64 `json:"chat_id"` -} - -// ExportChatInviteLink export an invite link to a supergroup or a channel. The -// bot must be an administrator in the chat for this to work and must have the -// appropriate admin rights. Returns exported invite link as String on success. -func (bot *Bot) ExportChatInviteLink(chatID int64) (string, error) { - dst, err := parser.Marshal(&ExportChatInviteLinkParameters{ChatID: chatID}) - if err != nil { - return "", err - } - - resp, err := bot.request(dst, MethodExportChatInviteLink) - if err != nil { - return "", err - } - - var inviteLink string - err = parser.Unmarshal(resp.Result, &inviteLink) - return inviteLink, err -} diff --git a/forward.go b/forward.go deleted file mode 100644 index 8bf9250..0000000 --- a/forward.go +++ /dev/null @@ -1,45 +0,0 @@ -package telegram - -// ForwardMessageParameters represents data for ForwardMessage method. -type ForwardMessageParameters struct { - // Unique identifier for the target chat or username of the target - // channel (in the format @channelusername) - ChatID int64 `json:"chat_id"` - - // Unique identifier for the chat where the original message was sent - // (or channel username in the format @channelusername) - FromChatID int64 `json:"from_chat_id"` - - // Sends the message silently. Users will receive a notification with no - // sound. - DisableNotification bool `json:"disable_notification,omitempty"` - - // Message identifier in the chat specified in from_chat_id - MessageID int `json:"message_id"` -} - -// NewForwardMessage creates ForwardMessageParameters only with reqired parameters. -func NewForwardMessage(from, to int64, messageID int) *ForwardMessageParameters { - return &ForwardMessageParameters{ - FromChatID: from, - ChatID: to, - MessageID: messageID, - } -} - -// ForwardMessage forward messages of any kind. On success, the sent Message is returned. -func (bot *Bot) ForwardMessage(params *ForwardMessageParameters) (*Message, error) { - dst, err := parser.Marshal(params) - if err != nil { - return nil, err - } - - resp, err := bot.request(dst, MethodForwardMessage) - if err != nil { - return nil, err - } - - var msg Message - err = parser.Unmarshal(resp.Result, &msg) - return &msg, err -} diff --git a/games.go b/games.go new file mode 100644 index 0000000..1760259 --- /dev/null +++ b/games.go @@ -0,0 +1,165 @@ +package telegram + +type ( + // Game represents a game. Use BotFather to create and edit games, their short names will act as unique + // identifiers. + Game struct { + // Title of the game + Title string `json:"title"` + + // Description of the game + Description string `json:"description"` + + // Brief description of the game or high scores included in the game message. Can be automatically + // edited to include current high scores for the game when the bot calls setGameScore, or manually + // edited using editMessageText. 0-4096 characters. + Text string `json:"text,omitempty"` + + // Photo that will be displayed in the game message in chats. + Photo []*PhotoSize `json:"photo"` + + // Special entities that appear in text, such as usernames, URLs, bot commands, etc. + TextEntities []*MessageEntity `json:"text_entities,omitempty"` + + // Animation that will be displayed in the game message in chats. Upload via BotFather + Animation *Animation `json:"animation,omitempty"` + } + + // CallbackGame a placeholder, currently holds no information. Use BotFather to set up your game. + CallbackGame struct{} + + // GameHighScore represents one row of the high scores table for a game. + GameHighScore struct { + // Position in high score table for the game + Position int `json:"position"` + + // Score + Score int `json:"score"` + + // User + User *User `json:"user"` + } + + // SendGameParameters represents data for SendGame method. + SendGame struct { + // Unique identifier for the target chat + ChatID int64 `json:"chat_id"` + + // Short name of the game, serves as the unique identifier for the game. Set up your games via + // Botfather. + GameShortName string `json:"game_short_name"` + + // Sends the message silently. Users will receive a notification with no sound. + DisableNotification bool `json:"disable_notification,omitempty"` + + // If the message is a reply, ID of the original message. + ReplyToMessageID int `json:"reply_to_message_id,omitempty"` + + // A JSON-serialized object for an inline keyboard. If empty, one ‘Play game_title’ button will be + // shown. If not empty, the first button must launch the game. + ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` + } + + // SetGameScoreParameters represents data for SetGameScore method. + SetGameScore struct { + // User identifier + UserID int `json:"user_id"` + + // New score, must be non-negative + Score int `json:"score"` + + // Required if inline_message_id is not specified. Identifier of the sent message + MessageID int `json:"message_id,omitempty"` + + // Pass True, if the high score is allowed to decrease. This can be useful when fixing mistakes or + // banning cheaters + Force bool `json:"force,omitempty"` + + // Pass True, if the game message should not be automatically edited to include the current scoreboard + DisableEditMessage bool `json:"disable_edit_message,omitempty"` + + // Required if inline_message_id is not specified. Unique identifier for the target chat + ChatID int64 `json:"chat_id,omitempty"` + + // Required if chat_id and message_id are not specified. Identifier of the inline message + InlineMessageID string `json:"inline_message_id,omitempty"` + } + + // GetGameHighScoresParameters represents data for GetGameHighScores method. + GetGameHighScores struct { + // Target user id + UserID int `json:"user_id"` + + // Required if inline_message_id is not specified. Identifier of the sent message + MessageID int `json:"message_id,omitempty"` + + // Required if inline_message_id is not specified. Unique identifier for the target chat + ChatID int64 `json:"chat_id,omitempty"` + + // Required if chat_id and message_id are not specified. Identifier of the inline message + InlineMessageID string `json:"inline_message_id,omitempty"` + } +) + +// SendGame send a game. On success, the sent Message is returned. +func (b *Bot) SendGame(p SendGame) (*Message, error) { + src, err := b.Do(MethodSendGame, p) + if err != nil { + return nil, err + } + + resp := new(Response) + if err = b.marshler.Unmarshal(src, resp); err != nil { + return nil, err + } + + result := new(Message) + if err = b.marshler.Unmarshal(resp.Result, result); err != nil { + return nil, err + } + + return result, nil +} + +// SetGameScore set the score of the specified user in a game. On success, if the message was sent by the bot, returns +// the edited Message, otherwise returns True. Returns an error, if the new score is not greater than the user's +// current score in the chat and force is False. +func (b *Bot) SetGameScore(p SetGameScore) (*Message, error) { + src, err := b.Do(MethodSetGameScore, p) + if err != nil { + return nil, err + } + + resp := new(Response) + if err = b.marshler.Unmarshal(src, resp); err != nil { + return nil, err + } + + result := new(Message) + if err = b.marshler.Unmarshal(resp.Result, result); err != nil { + return nil, err + } + + return result, nil +} + +// GetGameHighScores get data for high score tables. Will return the score of the specified user and several of his +// neighbors in a game. On success, returns an Array of GameHighScore objects. +func (b *Bot) GetGameHighScores(p GetGameHighScores) ([]*GameHighScore, error) { + src, err := b.Do(MethodGetGameHighScores, p) + if err != nil { + return nil, err + } + + resp := new(Response) + if err = b.marshler.Unmarshal(src, resp); err != nil { + return nil, err + } + + result := make([]*GameHighScore, 0) + if err = b.marshler.Unmarshal(resp.Result, &result); err != nil { + return nil, err + } + + return result, nil +} diff --git a/get.go b/get.go deleted file mode 100644 index d6f32ba..0000000 --- a/get.go +++ /dev/null @@ -1,314 +0,0 @@ -package telegram - -type ( - // GetChatParameters represents data for GetChat method. - GetChatParameters struct { - // Unique identifier for the target chat - ChatID int64 `json:"chat_id"` - } - - // GetChatAdministratorsParameters represents data for GetChatAdministrators - // method. - GetChatAdministratorsParameters struct { - // Unique identifier for the target chat - ChatID int64 `json:"chat_id"` - } - - // GetChatMemberParameters represents data for GetChatMember method. - GetChatMemberParameters struct { - // Unique identifier for the target chat - ChatID int64 `json:"chat_id"` - - // Unique identifier of the target user - UserID int `json:"user_id"` - } - - // GetChatMembersCountParameters represents data for GetChatMembersCount method. - GetChatMembersCountParameters struct { - // Unique identifier for the target chat - ChatID int64 `json:"chat_id"` - } - - // GetFileParameters represents data for GetFile method. - GetFileParameters struct { - // File identifier to get info about - FileID string `json:"file_id"` - } - - // GetUpdatesParameters represents data for GetUpdates method. - GetUpdatesParameters struct { - // Identifier of the first update to be returned. Must be greater by one than the highest among the - // identifiers of previously received updates. By default, updates starting with the earliest unconfirmed - // update are returned. An update is considered confirmed as soon as getUpdates is called with an offset - // higher than its update_id. The negative offset can be specified to retrieve updates starting from -offset - // update from the end of the updates queue. All previous updates will forgotten. - Offset int `json:"offset,omitempty"` - - // Limits the number of updates to be retrieved. Values between 1—100 are accepted. Defaults to 100. - Limit int `json:"limit,omitempty"` - - // Timeout in seconds for long polling. Defaults to 0, i.e. usual short polling. Should be positive, short - // polling should be used for testing purposes only. - Timeout int `json:"timeout,omitempty"` - - // List the types of updates you want your bot to receive. For example, specify ["message", - // "edited_channel_post", "callback_query"] to only receive updates of these types. See Update for a complete - // list of available update types. Specify an empty list to receive all updates regardless of type (default). - // If not specified, the previous setting will be used. - // - // Please note that this parameter doesn't affect updates created before the call to the getUpdates, so - // unwanted updates may be received for a short period of time. - AllowedUpdates []string `json:"allowed_updates,omitempty"` - } - - // GetUserProfilePhotosParameters represents data for GetUserProfilePhotos method. - GetUserProfilePhotosParameters struct { - // Unique identifier of the target user - UserID int `json:"user_id"` - - // Sequential number of the first photo to be returned. By default, all - // photos are returned. - Offset int `json:"offset,omitempty"` - - // Limits the number of photos to be retrieved. Values between 1—100 are - // accepted. Defaults to 100. - Limit int `json:"limit,omitempty"` - } - - // GetGameHighScoresParameters represents data for GetGameHighScores method. - GetGameHighScoresParameters struct { - // Target user id - UserID int `json:"user_id"` - - // Required if inline_message_id is not specified. Identifier of the sent - // message - MessageID int `json:"message_id,omitempty"` - - // Required if inline_message_id is not specified. Unique identifier for the - // target chat - ChatID int64 `json:"chat_id,omitempty"` - - // Required if chat_id and message_id are not specified. Identifier of the - // inline message - InlineMessageID string `json:"inline_message_id,omitempty"` - } - - // GetStickerSetParameters represents data for GetStickerSet method. - GetStickerSetParameters struct { - // Name of the sticker set - Name string `json:"name"` - } -) - -// NewGameHighScores creates GetGameHighScoresParameters only with required parameters. -func NewGameHighScores(userID int) *GetGameHighScoresParameters { - return &GetGameHighScoresParameters{ - UserID: userID, - } -} - -// GetChat get up to date information about the chat (current name of the user -// for one-on-one conversations, current username of a user, group or channel, -// etc.). Returns a Chat object on success. -func (bot *Bot) GetChat(chatID int64) (*Chat, error) { - dst, err := parser.Marshal(&GetChatParameters{ChatID: chatID}) - if err != nil { - return nil, err - } - - resp, err := bot.request(dst, MethodGetChat) - if err != nil { - return nil, err - } - - var chat Chat - err = parser.Unmarshal(resp.Result, &chat) - return &chat, err -} - -// GetChatAdministrators get a list of administrators in a chat. On success, -// returns an Array of ChatMember objects that contains information about all -// chat administrators except other bots. If the chat is a group or a supergroup -// and no administrators were appointed, only the creator will be returned. -func (bot *Bot) GetChatAdministrators(chatID int64) ([]ChatMember, error) { - dst, err := parser.Marshal(&GetChatAdministratorsParameters{ChatID: chatID}) - if err != nil { - return nil, err - } - - resp, err := bot.request(dst, MethodGetChatAdministrators) - if err != nil { - return nil, err - } - - var chatMembers []ChatMember - err = parser.Unmarshal(resp.Result, &chatMembers) - return chatMembers, err -} - -// GetChatMember get information about a member of a chat. Returns a ChatMember -// object on success. -func (bot *Bot) GetChatMember(chatID int64, userID int) (*ChatMember, error) { - dst, err := parser.Marshal(&GetChatMemberParameters{ - ChatID: chatID, - UserID: userID, - }) - if err != nil { - return nil, err - } - - resp, err := bot.request(dst, MethodGetChatMember) - if err != nil { - return nil, err - } - - var chatMember ChatMember - err = parser.Unmarshal(resp.Result, &chatMember) - return &chatMember, err -} - -// GetChatMembersCount get the number of members in a chat. Returns Int on -// success. -func (bot *Bot) GetChatMembersCount(chatID int64) (int, error) { - dst, err := parser.Marshal(&GetChatMembersCountParameters{ChatID: chatID}) - if err != nil { - return 0, err - } - - resp, err := bot.request(dst, MethodGetChatMembersCount) - if err != nil { - return 0, err - } - - var count int - err = parser.Unmarshal(resp.Result, &count) - return count, err -} - -// GetFile get basic info about a file and prepare it for downloading. For the -// moment, bots can download files of up to 20MB in size. On success, a File -// object is returned. The file can then be downloaded via the link -// https://api.telegram.org/file/bot/, where is -// taken from the response. It is guaranteed that the link will be valid for at -// least 1 hour. When the link expires, a new one can be requested by calling -// getFile again. -// -// Note: This function may not preserve the original file name and MIME type. You -// should save the file's MIME type and name (if available) when the File object -// is received. -func (bot *Bot) GetFile(fileID string) (*File, error) { - dst, err := parser.Marshal(&GetFileParameters{FileID: fileID}) - if err != nil { - return nil, err - } - - resp, err := bot.request(dst, MethodGetFile) - if err != nil { - return nil, err - } - - var file File - err = parser.Unmarshal(resp.Result, &file) - return &file, err -} - -// GetMe testing your bot's auth token. Requires no parameters. Returns basic -// information about the bot in form of a User object. -func (bot *Bot) GetMe() (*User, error) { - resp, err := bot.request(nil, MethodGetMe) - if err != nil { - return nil, err - } - - var me User - err = parser.Unmarshal(resp.Result, &me) - return &me, err -} - -// GetUpdates receive incoming updates using long polling. An Array of Update objects is returned. -func (bot *Bot) GetUpdates(params *GetUpdatesParameters) ([]Update, error) { - if params == nil { - params = &GetUpdatesParameters{Limit: 100} - } - - src, err := parser.Marshal(params) - if err != nil { - return nil, err - } - - resp, err := bot.request(src, MethodGetUpdates) - if err != nil { - return nil, err - } - - updates := make([]Update, params.Limit) - err = parser.Unmarshal(resp.Result, &updates) - return updates, err -} - -// GetUserProfilePhotos get a list of profile pictures for a user. Returns a UserProfilePhotos object. -func (bot *Bot) GetUserProfilePhotos(params *GetUserProfilePhotosParameters) (*UserProfilePhotos, error) { - dst, err := parser.Marshal(params) - if err != nil { - return nil, err - } - - resp, err := bot.request(dst, MethodGetUserProfilePhotos) - if err != nil { - return nil, err - } - - var photos UserProfilePhotos - err = parser.Unmarshal(resp.Result, &photos) - return &photos, err -} - -// GetWebhookInfo get current webhook status. Requires no parameters. On success, -// returns a WebhookInfo object. If the bot is using getUpdates, will return an -// object with the url field empty. -func (bot *Bot) GetWebhookInfo() (*WebhookInfo, error) { - resp, err := bot.request(nil, MethodGetWebhookInfo) - if err != nil { - return nil, err - } - - var info WebhookInfo - err = parser.Unmarshal(resp.Result, &info) - return &info, err -} - -// GetGameHighScores get data for high score tables. Will return the score of the -// specified user and several of his neighbors in a game. On success, returns an -// Array of GameHighScore objects. -func (bot *Bot) GetGameHighScores(params *GetGameHighScoresParameters) ([]GameHighScore, error) { - dst, err := parser.Marshal(params) - if err != nil { - return nil, err - } - - resp, err := bot.request(dst, MethodGetGameHighScores) - if err != nil { - return nil, err - } - - var scores []GameHighScore - err = parser.Unmarshal(resp.Result, &scores) - return scores, err -} - -// GetStickerSet get a sticker set. On success, a StickerSet object is returned. -func (bot *Bot) GetStickerSet(name string) (*StickerSet, error) { - dst, err := parser.Marshal(&GetStickerSetParameters{Name: name}) - if err != nil { - return nil, err - } - - resp, err := bot.request(dst, MethodGetStickerSet) - if err != nil { - return nil, err - } - - var set StickerSet - err = parser.Unmarshal(resp.Result, &set) - return &set, err -} diff --git a/go.mod b/go.mod index 1ff4075..bff2f46 100644 --- a/go.mod +++ b/go.mod @@ -3,14 +3,15 @@ module gitlab.com/toby3d/telegram go 1.12 require ( - github.com/json-iterator/go v1.1.7 + github.com/Masterminds/semver v1.5.0 + github.com/json-iterator/go v1.1.9 github.com/kirillDanshin/dlog v0.0.0-20170728000807-97d876b12bf9 github.com/kirillDanshin/myutils v0.0.0-20160713214838-182269b1fbcc // indirect - github.com/klauspost/compress v1.7.4 // indirect - github.com/klauspost/cpuid v1.2.1 // indirect + github.com/klauspost/compress v1.9.7 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.1 // indirect github.com/stretchr/testify v1.3.0 - github.com/valyala/fasthttp v1.4.0 + github.com/valyala/fasthttp v1.7.1 golang.org/x/text v0.3.2 + golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 ) diff --git a/go.sum b/go.sum index 15f25eb..fe71088 100644 --- a/go.sum +++ b/go.sum @@ -1,20 +1,19 @@ +github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= +github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/json-iterator/go v1.1.7 h1:KfgG9LzI+pYjr4xvmz/5H4FXjokeP+rlHLhv3iH62Fo= -github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/kirillDanshin/dlog v0.0.0-20170728000807-97d876b12bf9 h1:mA7k8E2Vrmyj5CW/D1XZBFmohVNi7jf757vibGwzRbo= github.com/kirillDanshin/dlog v0.0.0-20170728000807-97d876b12bf9/go.mod h1:l8CN7iyX1k2xlsTYVTpCtwBPcxThf/jLWDGVcF6T/bM= github.com/kirillDanshin/myutils v0.0.0-20160713214838-182269b1fbcc h1:OkOhOn3WBUmfATC1NsA3rBlgHGkjk0KGnR5akl/8uXc= github.com/kirillDanshin/myutils v0.0.0-20160713214838-182269b1fbcc/go.mod h1:Bt95qRxLvpdmASW9s2tTxGdQ5ma4o4n8QFhCvzCew/M= -github.com/klauspost/compress v1.4.0 h1:8nsMz3tWa9SWWPL60G1V6CUsf4lLjWLTNEtibhe8gh8= -github.com/klauspost/compress v1.4.0/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= -github.com/klauspost/compress v1.7.4 h1:4UqAIzZ1Ns2epCTyJ1d2xMWvxtX+FNSCYWeOFogK9nc= -github.com/klauspost/compress v1.7.4/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= -github.com/klauspost/cpuid v0.0.0-20180405133222-e7e905edc00e h1:+lIPJOWl+jSiJOc70QXJ07+2eg2Jy2EC7Mi11BWujeM= -github.com/klauspost/cpuid v0.0.0-20180405133222-e7e905edc00e/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= +github.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= +github.com/klauspost/compress v1.9.7 h1:hYW1gP94JUmAhBtJ+LNz5My+gBobDxPR1iVuKug26aA= +github.com/klauspost/compress v1.9.7/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/klauspost/cpuid v1.2.1 h1:vJi+O/nMdFt0vqm8NZBI6wzALWdA2X+egi0ogNyrC/w= github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -30,10 +29,15 @@ github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasthttp v1.4.0 h1:PuaTGZIw3mjYhhhbVbCQp8aciRZN9YdoB7MGX9Ko76A= -github.com/valyala/fasthttp v1.4.0/go.mod h1:4vX61m6KN+xDduDNwXrhIAVZaZaZiQ1luJk8LWSxF3s= +github.com/valyala/fasthttp v1.7.1 h1:UHtt5/7O70RSUZTR/hSu0PNWMAfWx5AtsPp9Jk+g17M= +github.com/valyala/fasthttp v1.7.1/go.mod h1:FstJa9V+Pj9vQ7OJie2qMHdwemEDaDiSdBnvPM1Su9w= github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio= -golang.org/x/net v0.0.0-20180911220305-26e67e76b6c3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/inline.go b/inline.go new file mode 100644 index 0000000..961729e --- /dev/null +++ b/inline.go @@ -0,0 +1,1143 @@ +package telegram + +type ( + // InlineQuery represents an incoming inline query. When the user sends an empty query, your bot could return some default or trending results. + InlineQuery struct { + // Unique identifier for this query + ID string `json:"id"` + + // Text of the query (up to 512 characters) + Query string `json:"query"` + + // Offset of the results to be returned, can be controlled by the bot + Offset string `json:"offset"` + + // Sender + From *User `json:"from"` + + // Sender location, only for bots that request user location + Location *Location `json:"location,omitempty"` + } + + // InlineQueryResult represents one result of an inline query. + InlineQueryResult interface { + IsCached() bool + } + + // InlineQueryResultArticle represents a link to an article or web page. + InlineQueryResultArticle struct { + // Type of the result, must be article + Type string `json:"type"` + + // Unique identifier for this result, 1-64 Bytes + ID string `json:"id"` + + // Title of the result + Title string `json:"title"` + + // URL of the result + URL string `json:"url,omitempty"` + + // Short description of the result + Description string `json:"description,omitempty"` + + // Url of the thumbnail for the result + ThumbURL string `json:"thumb_url,omitempty"` + + // Content of the message to be sent + InputMessageContent InputMessageContent `json:"input_message_content"` + + // Inline keyboard attached to the message + ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` + + // Pass True, if you don't want the URL to be shown in the message + HideURL bool `json:"hide_url,omitempty"` + + // Thumbnail width + ThumbWidth int `json:"thumb_width,omitempty"` + + // Thumbnail height + ThumbHeight int `json:"thumb_height,omitempty"` + } + + // InlineQueryResultPhoto represents a link to a photo. By default, this + // photo will be sent by the user with optional caption. Alternatively, you + // can use input_message_content to send a message with the specified content + // instead of the photo. + InlineQueryResultPhoto struct { + // Type of the result, must be photo + Type string `json:"type"` + + // Unique identifier for this result, 1-64 bytes + ID string `json:"id"` + + // A valid URL of the photo. Photo must be in jpeg format. Photo size + // must not exceed 5MB + PhotoURL string `json:"photo_url"` + + // URL of the thumbnail for the photo + ThumbURL string `json:"thumb_url"` + + // Title for the result + Title string `json:"title,omitempty"` + + // Short description of the result + Description string `json:"description,omitempty"` + + // Caption of the photo to be sent, 0-200 characters + Caption string `json:"caption,omitempty"` + + // Send Markdown or HTML, if you want Telegram apps to show bold, italic, + // fixed-width text or inline URLs in the media caption. + ParseMode string `json:"parse_mode,omitempty"` + + // Width of the photo + PhotoWidth int `json:"photo_width,omitempty"` + + // Height of the photo + PhotoHeight int `json:"photo_height,omitempty"` + + // Inline keyboard attached to the message + ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` + + // Content of the message to be sent instead of the photo + InputMessageContent InputMessageContent `json:"input_message_content,omitempty"` + } + + // InlineQueryResultGif represents a link to an animated GIF file. By + // default, this animated GIF file will be sent by the user with optional + // caption. Alternatively, you can use input_message_content to send a + // message with the specified content instead of the animation. + InlineQueryResultGif struct { + // Type of the result, must be gif + Type string `json:"type"` + + // Unique identifier for this result, 1-64 bytes + ID string `json:"id"` + + // A valid URL for the GIF file. File size must not exceed 1MB + GifURL string `json:"gif_url"` + + // URL of the static thumbnail for the result (jpeg or gif) + ThumbURL string `json:"thumb_url"` + + // Title for the result + Title string `json:"title,omitempty"` + + // Caption of the GIF file to be sent, 0-200 characters + Caption string `json:"caption,omitempty"` + + // Send Markdown or HTML, if you want Telegram apps to show bold, italic, + // fixed-width text or inline URLs in the media caption. + ParseMode string `json:"parse_mode,omitempty"` + + // Width of the GIF + GifWidth int `json:"gif_width,omitempty"` + + // Height of the GIF + GifHeight int `json:"gif_height,omitempty"` + + // Duration of the GIF + GifDuration int `json:"gif_duration,omitempty"` + + // Inline keyboard attached to the message + ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` + + // Content of the message to be sent instead of the GIF animation + InputMessageContent InputMessageContent `json:"input_message_content,omitempty"` + } + + // InlineQueryResultMpeg4Gif represents a link to a video animation + // (H.264/MPEG-4 AVC video without sound). By default, this animated MPEG-4 + // file will be sent by the user with optional caption. Alternatively, you + // can use input_message_content to send a message with the specified content + // instead of the animation. + InlineQueryResultMpeg4Gif struct { + // Type of the result, must be mpeg4_gif + Type string `json:"type"` + + // Unique identifier for this result, 1-64 bytes + ID string `json:"id"` + + // A valid URL for the MP4 file. File size must not exceed 1MB + Mpeg4URL string `json:"mpeg4_url"` + + // URL of the static thumbnail (jpeg or gif) for the result + ThumbURL string `json:"thumb_url"` + + // Title for the result + Title string `json:"title,omitempty"` + + // Caption of the MPEG-4 file to be sent, 0-200 characters + Caption string `json:"caption,omitempty"` + + // Video width + Mpeg4Width int `json:"mpeg4_width,omitempty"` + + // Video height + Mpeg4Height int `json:"mpeg4_height,omitempty"` + + // Video duration + Mpeg4Duration int `json:"mpeg4_duration,omitempty"` + + // Inline keyboard attached to the message + ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` + + // Content of the message to be sent instead of the video animation + InputMessageContent InputMessageContent `json:"input_message_content,omitempty"` + } + + // InlineQueryResultVideo represents a link to a page containing an embedded + // video player or a video file. By default, this video file will be sent by + // the user with an optional caption. Alternatively, you can use + // input_message_content to send a message with the specified content + // instead of the video. + // + // If an InlineQueryResultVideo message contains an embedded video (e.g., + // YouTube), you must replace its content using input_message_content. + InlineQueryResultVideo struct { + // Type of the result, must be video + Type string `json:"type"` + + // Unique identifier for this result, 1-64 bytes + ID string `json:"id"` + + // A valid URL for the embedded video player or video file + VideoURL string `json:"video_url"` + + // Mime type of the content of video url, "text/html" or "video/mp4" + MimeType string `json:"mime_type"` + + // URL of the thumbnail (jpeg only) for the video + ThumbURL string `json:"thumb_url"` + + // Title for the result + Title string `json:"title"` + + // Caption of the video to be sent, 0-200 characters + Caption string `json:"caption,omitempty"` + + // Send Markdown or HTML, if you want Telegram apps to show bold, italic, + // fixed-width text or inline URLs in the media caption. + ParseMode string `json:"parse_mode,omitempty"` + + // Short description of the result + Description string `json:"description,omitempty"` + + // Video width + VideoWidth int `json:"video_width,omitempty"` + + // Video height + VideoHeight int `json:"video_height,omitempty"` + + // Video duration in seconds + VideoDuration int `json:"video_duration,omitempty"` + + // Inline keyboard attached to the message + ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` + + // Content of the message to be sent instead of the video. This field is + // required if InlineQueryResultVideo is used to send an HTML-page as a + // result (e.g., a YouTube video). + InputMessageContent InputMessageContent `json:"input_message_content,omitempty"` + } + + // InlineQueryResultAudio represents a link to an mp3 audio file. By default, + // this audio file will be sent by the user. Alternatively, you can use + // input_message_content to send a message with the specified content + // instead of the audio. + InlineQueryResultAudio struct { + // Type of the result, must be audio + Type string `json:"type"` + + // Unique identifier for this result, 1-64 bytes + ID string `json:"id"` + + // A valid URL for the audio file + AudioURL string `json:"audio_url"` + + // Title + Title string `json:"title"` + + // Caption, 0-200 characters + Caption string `json:"caption,omitempty"` + + // Send Markdown or HTML, if you want Telegram apps to show bold, italic, + // fixed-width text or inline URLs in the media caption. + ParseMode string `json:"parse_mode,omitempty"` + + // Performer + Performer string `json:"performer,omitempty"` + + // Audio duration in seconds + AudioDuration int `json:"audio_duration,omitempty"` + + // Inline keyboard attached to the message + ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` + + // Content of the message to be sent instead of the audio + InputMessageContent InputMessageContent `json:"input_message_content,omitempty"` + } + + // InlineQueryResultVoice represents a link to a voice recording in an .ogg + // container encoded with OPUS. By default, this voice recording will be + // sent by the user. Alternatively, you can use input_message_content to + // send a message with the specified content instead of the the voice message. + InlineQueryResultVoice struct { + // Type of the result, must be voice + Type string `json:"type"` + + // Unique identifier for this result, 1-64 bytes + ID string `json:"id"` + + // A valid URL for the voice recording + VoiceURL string `json:"voice_url"` + + // Recording title + Title string `json:"title"` + + // Caption, 0-200 characters + Caption string `json:"caption,omitempty"` + + // Send Markdown or HTML, if you want Telegram apps to show bold, italic, + // fixed-width text or inline URLs in the media caption. + ParseMode string `json:"parse_mode,omitempty"` + + // Recording duration in seconds + VoiceDuration int `json:"voice_duration,omitempty"` + + // Inline keyboard attached to the message + ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` + + // Content of the message to be sent instead of the voice recording + InputMessageContent InputMessageContent `json:"input_message_content,omitempty"` + } + + // InlineQueryResultDocument represents a link to a file. By default, this + // file will be sent by the user with an optional caption. Alternatively, + // you can use input_message_content to send a message with the specified + // content instead of the file. Currently, only .PDF and .ZIP files can be + // sent using this method. + InlineQueryResultDocument struct { + // Type of the result, must be document + Type string `json:"type"` + + // Unique identifier for this result, 1-64 bytes + ID string `json:"id"` + + // Title for the result + Title string `json:"title"` + + // Caption of the document to be sent, 0-200 characters + Caption string `json:"caption,omitempty"` + + // Send Markdown or HTML, if you want Telegram apps to show bold, italic, + // fixed-width text or inline URLs in the media caption. + ParseMode string `json:"parse_mode,omitempty"` + + // A valid URL for the file + DocumentURL string `json:"document_url"` + + // Mime type of the content of the file, either "application/pdf" or + // "application/zip" + MimeType string `json:"mime_type"` + + // Short description of the result + Description string `json:"description,omitempty"` + + // URL of the thumbnail (jpeg only) for the file + ThumbURL string `json:"thumb_url,omitempty"` + + // Inline keyboard attached to the message + ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` + + // Content of the message to be sent instead of the file + InputMessageContent InputMessageContent `json:"input_message_content,omitempty"` + + // Thumbnail width + ThumbWidth int `json:"thumb_width,omitempty"` + + // Thumbnail height + ThumbHeight int `json:"thumb_height,omitempty"` + } + + // InlineQueryResultLocation represents a location on a map. By default, the + // location will be sent by the user. Alternatively, you can use + // input_message_content to send a message with the specified content + // instead of the location. + InlineQueryResultLocation struct { + // Type of the result, must be location + Type string `json:"type"` + + // Unique identifier for this result, 1-64 Bytes + ID string `json:"id"` + + // Location title + Title string `json:"title"` + + // Url of the thumbnail for the result + ThumbURL string `json:"thumb_url,omitempty"` + + // Location latitude in degrees + Latitude float32 `json:"latitude"` + + // Location longitude in degrees + Longitude float32 `json:"longitude"` + + // Inline keyboard attached to the message + ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` + + // Content of the message to be sent instead of the location + InputMessageContent InputMessageContent `json:"input_message_content,omitempty"` + + // Thumbnail width + ThumbWidth int `json:"thumb_width,omitempty"` + + // Thumbnail height + ThumbHeight int `json:"thumb_height,omitempty"` + } + + // InlineQueryResultVenue represents a venue. By default, the venue will be + // sent by the user. Alternatively, you can use input_message_content to + // send a message with the specified content instead of the venue. + InlineQueryResultVenue struct { + // Type of the result, must be venue + Type string `json:"type"` + + // Unique identifier for this result, 1-64 Bytes + ID string `json:"id"` + + // Title of the venue + Title string `json:"title"` + + // Address of the venue + Address string `json:"address"` + + // Foursquare identifier of the venue if known + FoursquareID string `json:"foursquare_id,omitempty"` + + // Foursquare type of the venue, if known. (For example, + // "arts_entertainment/default", "arts_entertainment/aquarium" or + // "food/icecream".) + FoursquareType string `json:"foursquare_type,omitempty"` + + // Url of the thumbnail for the result + ThumbURL string `json:"thumb_url,omitempty"` + + // Latitude of the venue location in degrees + Latitude float32 `json:"latitude"` + + // Longitude of the venue location in degrees + Longitude float32 `json:"longitude"` + + // Inline keyboard attached to the message + ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` + + // Content of the message to be sent instead of the venue + InputMessageContent InputMessageContent `json:"input_message_content,omitempty"` + + // Thumbnail width + ThumbWidth int `json:"thumb_width,omitempty"` + + // Thumbnail height + ThumbHeight int `json:"thumb_height,omitempty"` + } + + // InlineQueryResultContact represents a contact with a phone number. By + // default, this contact will be sent by the user. Alternatively, you can + // use input_message_content to send a message with the specified content + // instead of the contact. + InlineQueryResultContact struct { + // Type of the result, must be contact + Type string `json:"type"` + + // Unique identifier for this result, 1-64 Bytes + ID string `json:"id"` + + // Contact's phone number + PhoneNumber string `json:"phone_number"` + + // Contact's first name + FirstName string `json:"first_name"` + + // Contact's last name + LastName string `json:"last_name,omitempty"` + + // Additional data about the contact in the form of a vCard, 0-2048 bytes + VCard string `json:"vcard,omitempty"` + + // Url of the thumbnail for the result + ThumbURL string `json:"thumb_url,omitempty"` + + // Inline keyboard attached to the message + ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` + + // Content of the message to be sent instead of the contact + InputMessageContent InputMessageContent `json:"input_message_content,omitempty"` + + // Thumbnail width + ThumbWidth int `json:"thumb_width,omitempty"` + + // Thumbnail height + ThumbHeight int `json:"thumb_height,omitempty"` + } + + // InlineQueryResultGame represents a Game. + InlineQueryResultGame struct { + // Type of the result, must be game + Type string `json:"type"` + + // Unique identifier for this result, 1-64 bytes + ID string `json:"id"` + + // Short name of the game + GameShortName string `json:"game_short_name"` + + // Inline keyboard attached to the message + ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` + } + + // InlineQueryResultCachedPhoto represents a link to a photo stored on the + // Telegram servers. By default, this photo will be sent by the user with an + // optional caption. Alternatively, you can use input_message_content to + // send a message with the specified content instead of the photo. + InlineQueryResultCachedPhoto struct { + // Type of the result, must be photo + Type string `json:"type"` + + // Unique identifier for this result, 1-64 bytes + ID string `json:"id"` + + // A valid file identifier of the photo + PhotoFileID string `json:"photo_file_id"` + + // Title for the result + Title string `json:"title,omitempty"` + + // Short description of the result + Description string `json:"description,omitempty"` + + // Caption of the photo to be sent, 0-200 characters + Caption string `json:"caption,omitempty"` + + // Send Markdown or HTML, if you want Telegram apps to show bold, italic, + // fixed-width text or inline URLs in the media caption. + ParseMode string `json:"parse_mode,omitempty"` + + // Inline keyboard attached to the message + ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` + + // Content of the message to be sent instead of the photo + InputMessageContent InputMessageContent `json:"input_message_content,omitempty"` + } + + // InlineQueryResultCachedGif represents a link to an animated GIF file + // stored on the Telegram servers. By default, this animated GIF file will + // be sent by the user with an optional caption. Alternatively, you can use + // input_message_content to send a message with specified content instead of + // the animation. + InlineQueryResultCachedGif struct { + // Type of the result, must be gif + Type string `json:"type"` + + // Unique identifier for this result, 1-64 bytes + ID string `json:"id"` + + // A valid file identifier for the GIF file + GifFileID string `json:"gif_file_id"` + + // Title for the result + Title string `json:"title,omitempty"` + + // Caption of the GIF file to be sent, 0-200 characters + Caption string `json:"caption,omitempty"` + + // Send Markdown or HTML, if you want Telegram apps to show bold, italic, + // fixed-width text or inline URLs in the media caption. + ParseMode string `json:"parse_mode,omitempty"` + + // Inline keyboard attached to the message + ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` + + // Content of the message to be sent instead of the GIF animation + InputMessageContent InputMessageContent `json:"input_message_content,omitempty"` + } + + // InlineQueryResultCachedMpeg4Gif represents a link to a video animation + // (H.264/MPEG-4 AVC video without sound) stored on the Telegram servers. By + // default, this animated MPEG-4 file will be sent by the user with an + // optional caption. Alternatively, you can use input_message_content to + // send a message with the specified content instead of the animation. + InlineQueryResultCachedMpeg4Gif struct { + // Type of the result, must be mpeg4_gif + Type string `json:"type"` + + // Unique identifier for this result, 1-64 bytes + ID string `json:"id"` + + // A valid file identifier for the MP4 file + Mpeg4FileID string `json:"mpeg4_file_id"` + + // Title for the result + Title string `json:"title,omitempty"` + + // Caption of the MPEG-4 file to be sent, 0-200 characters + Caption string `json:"caption,omitempty"` + + // Send Markdown or HTML, if you want Telegram apps to show bold, italic, + // fixed-width text or inline URLs in the media caption. + ParseMode string `json:"parse_mode,omitempty"` + + // Inline keyboard attached to the message + ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` + + // Content of the message to be sent instead of the video animation + InputMessageContent InputMessageContent `json:"input_message_content,omitempty"` + } + + // InlineQueryResultCachedSticker represents a link to a sticker stored on + // the Telegram servers. By default, this sticker will be sent by the user. + // Alternatively, you can use input_message_content to send a message with + // the specified content instead of the sticker. + InlineQueryResultCachedSticker struct { + // Type of the result, must be sticker + Type string `json:"type"` + + // Unique identifier for this result, 1-64 bytes + ID string `json:"id"` + + // A valid file identifier of the sticker + StickerFileID string `json:"sticker_file_id"` + + // Inline keyboard attached to the message + ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` + + // Content of the message to be sent instead of the sticker + InputMessageContent InputMessageContent `json:"input_message_content,omitempty"` + } + + // InlineQueryResultCachedDocument represents a link to a file stored on the + // Telegram servers. By default, this file will be sent by the user with an + // optional caption. Alternatively, you can use input_message_content to + // send a message with the specified content instead of the file. + InlineQueryResultCachedDocument struct { + // Type of the result, must be document + Type string `json:"type"` + + // Unique identifier for this result, 1-64 bytes + ID string `json:"id"` + + // Title for the result + Title string `json:"title"` + + // A valid file identifier for the file + DocumentFileID string `json:"document_file_id"` + + // Short description of the result + Description string `json:"description,omitempty"` + + // Caption of the document to be sent, 0-200 characters + Caption string `json:"caption,omitempty"` + + // Send Markdown or HTML, if you want Telegram apps to show bold, italic, + // fixed-width text or inline URLs in the media caption. + ParseMode string `json:"parse_mode,omitempty"` + + // Inline keyboard attached to the message + ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` + + // Content of the message to be sent instead of the file + InputMessageContent InputMessageContent `json:"input_message_content,omitempty"` + } + + // InlineQueryResultCachedVideo represents a link to a video file stored on + // the Telegram servers. By default, this video file will be sent by the + // user with an optional caption. Alternatively, you can use + // input_message_content to send a message with the specified content + // instead of the video. + InlineQueryResultCachedVideo struct { + // Type of the result, must be video + Type string `json:"type"` + + // Unique identifier for this result, 1-64 bytes + ID string `json:"id"` + + // A valid file identifier for the video file + VideoFileID string `json:"video_file_id"` + + // Title for the result + Title string `json:"title"` + + // Short description of the result + Description string `json:"description,omitempty"` + + // Caption of the video to be sent, 0-200 characters + Caption string `json:"caption,omitempty"` + + // Send Markdown or HTML, if you want Telegram apps to show bold, italic, + // fixed-width text or inline URLs in the media caption. + ParseMode string `json:"parse_mode,omitempty"` + + // Inline keyboard attached to the message + ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` + + // Content of the message to be sent instead of the video + InputMessageContent InputMessageContent `json:"input_message_content,omitempty"` + } + + // InlineQueryResultCachedVoice represents a link to a voice message stored + // on the Telegram servers. By default, this voice message will be sent by + // the user. Alternatively, you can use input_message_content to send a + // message with the specified content instead of the voice message. + InlineQueryResultCachedVoice struct { + // Type of the result, must be voice + Type string `json:"type"` + + // Unique identifier for this result, 1-64 bytes + ID string `json:"id"` + + // A valid file identifier for the voice message + VoiceFileID string `json:"voice_file_id"` + + // Voice message title + Title string `json:"title"` + + // Caption, 0-200 characters + Caption string `json:"caption,omitempty"` + + // Send Markdown or HTML, if you want Telegram apps to show bold, italic, + // fixed-width text or inline URLs in the media caption. + ParseMode string `json:"parse_mode,omitempty"` + + // Inline keyboard attached to the message + ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` + + // Content of the message to be sent instead of the voice message + InputMessageContent InputMessageContent `json:"input_message_content,omitempty"` + } + + // InlineQueryResultCachedAudio represents a link to an mp3 audio file + // stored on the Telegram servers. By default, this audio file will be sent + // by the user. Alternatively, you can use input_message_content to send a + // message with the specified content instead of the audio. + InlineQueryResultCachedAudio struct { + // Type of the result, must be audio + Type string `json:"type"` + + // Unique identifier for this result, 1-64 bytes + ID string `json:"id"` + + // A valid file identifier for the audio file + AudioFileID string `json:"audio_file_id"` + + // Caption, 0-200 characters + Caption string `json:"caption,omitempty"` + + // Send Markdown or HTML, if you want Telegram apps to show bold, italic, + // fixed-width text or inline URLs in the media caption. + ParseMode string `json:"parse_mode,omitempty"` + + // Inline keyboard attached to the message + ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` + + // Content of the message to be sent instead of the audio + InputMessageContent InputMessageContent `json:"input_message_content,omitempty"` + } + + // InputMessageContent represents the content of a message to be sent as a result of an inline query. + InputMessageContent interface { + isInputMessageContent() + } + + // InputTextMessageContent represents the content of a text message to be + // sent as the result of an inline query. + InputTextMessageContent struct { + // Text of the message to be sent, 1-4096 characters + MessageText string `json:"message_text"` + + // 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"` + + // Disables link previews for links in the sent message + DisableWebPagePreview bool `json:"disable_web_page_preview,omitempty"` + } + + // InputLocationMessageContent represents the content of a location message + // to be sent as the result of an inline query. + InputLocationMessageContent struct { + // Latitude of the location in degrees + Latitude float32 `json:"latitude"` + + // Longitude of the location in degrees + Longitude float32 `json:"longitude"` + + // Period in seconds for which the location can be updated, should be between 60 and 86400. + LivePeriod int `json:"live_period,omitempty"` + } + + // InputVenueMessageContent represents the content of a venue message to be + // sent as the result of an inline query. + InputVenueMessageContent struct { + // Latitude of the location in degrees + Latitude float32 `json:"latitude"` + + // Longitude of the location in degrees + Longitude float32 `json:"longitude"` + + // Name of the venue + Title string `json:"title"` + + // Address of the venue + Address string `json:"address"` + + // Foursquare identifier of the venue, if known + FoursquareID string `json:"foursquare_id,omitempty"` + + // Foursquare type of the venue, if known. (For example, + // "arts_entertainment/default", "arts_entertainment/aquarium" or + // "food/icecream".) + FoursquareType string `json:"foursquare_type,omitempty"` + } + + // InputContactMessageContent represents the content of a contact message to + // be sent as the result of an inline query. + InputContactMessageContent struct { + // Contact's phone number + PhoneNumber string `json:"phone_number"` + + // Contact's first name + FirstName string `json:"first_name"` + + // Contact's last name + LastName string `json:"last_name,omitempty"` + + // Additional data about the contact in the form of a vCard, 0-2048 bytes + VCard string `json:"vcard,omitempty"` + } + + // ChosenInlineResult represents a result of an inline query that was chosen + // by the user and sent to their chat partner. + ChosenInlineResult struct { + // The unique identifier for the result that was chosen + ResultID string `json:"result_id"` + + // Identifier of the sent inline message. Available only if there is an + // inline keyboard attached to the message. Will be also received in + // callback queries and can be used to edit the message. + InlineMessageID string `json:"inline_message_id,omitempty"` + + // The query that was used to obtain the result + Query string `json:"query"` + + // The user that chose the result + From *User `json:"from"` + + // Sender location, only for bots that require user location + Location *Location `json:"location,omitempty"` + } + + // AnswerInlineQueryParameters represents data for AnswerInlineQuery method. + AnswerInlineQuery struct { + // Unique identifier for the answered query + InlineQueryID string `json:"inline_query_id"` + + // Pass the offset that a client should send in the next query with the same + // text to receive more results. Pass an empty string if there are no more + // results or if you don‘t support pagination. Offset length can’t exceed 64 + // bytes. + NextOffset string `json:"next_offset,omitempty"` + + // If passed, clients will display a button with specified text that switches + // the user to a private chat with the bot and sends the bot a start message + // with the parameter switch_pm_parameter + SwitchPrivateMessageText string `json:"switch_pm_text,omitempty"` + + // Deep-linking parameter for the /start message sent to the bot when user + // presses the switch button. 1-64 characters, only A-Z, a-z, 0-9, _ and - + // are allowed. + SwitchPrivateMessageParameter string `json:"switch_pm_parameter,omitempty"` + + // A JSON-serialized array of results for the inline query + Results []InlineQueryResult `json:"results"` + + // The maximum amount of time in seconds that the result of the inline query + // may be cached on the server. Defaults to 300. + CacheTime int `json:"cache_time,omitempty"` + + // Pass True, if results may be cached on the server side only for the user + // that sent the query. By default, results may be returned to any user who + // sends the same query + IsPersonal bool `json:"is_personal,omitempty"` + } + + ReplyMarkup interface { + isReplyMarkup() + } +) + +// AnswerInlineQuery send answers to an inline query. On success, True is returned. +// +// No more than 50 results per query are allowed. +func (b *Bot) AnswerInlineQuery(p AnswerInlineQuery) (bool, error) { + src, err := b.Do(MethodAnswerInlineQuery, p) + if err != nil { + return false, err + } + + resp := new(Response) + if err = b.marshler.Unmarshal(src, resp); err != nil { + return false, err + } + + var ok bool + if err = b.marshler.Unmarshal(resp.Result, &ok); err != nil { + return false, err + } + + return ok, nil +} + +func (iq InlineQuery) HasQuery() bool { return iq.Query != "" } + +func (iq InlineQuery) HasOffset() bool { return iq.Offset != "" } + +func (iq InlineQuery) HasLocation() bool { return iq.Location != nil } + +func (cir ChosenInlineResult) HasLocation() bool { return cir.Location != nil } + +func (InputTextMessageContent) isInputMessageContent() {} + +func (InputLocationMessageContent) isInputMessageContent() {} + +func (InputVenueMessageContent) isInputMessageContent() {} + +func (InputContactMessageContent) isInputMessageContent() {} + +func NewInlineQueryResultCachedAudio(id, file string) InlineQueryResultCachedAudio { + return InlineQueryResultCachedAudio{ + Type: TypeAudio, + ID: id, + AudioFileID: file, + } +} + +func (InlineQueryResultCachedAudio) IsCached() bool { return true } + +func NewInlineQueryResultCachedDocument(id, title, file string) InlineQueryResultCachedDocument { + return InlineQueryResultCachedDocument{ + Type: TypeDocument, + ID: id, + Title: title, + DocumentFileID: file, + } +} + +func (InlineQueryResultCachedDocument) IsCached() bool { return true } + +func NewInlineQueryResultCachedGif(id, file string) InlineQueryResultCachedGif { + return InlineQueryResultCachedGif{ + Type: TypeGIF, + ID: id, + GifFileID: file, + } +} + +func (InlineQueryResultCachedGif) IsCached() bool { return true } + +func NewInlineQueryResultCachedMpeg4Gif(id, file string) InlineQueryResultCachedMpeg4Gif { + return InlineQueryResultCachedMpeg4Gif{ + Type: TypeMpeg4Gif, + ID: id, + Mpeg4FileID: file, + } +} + +func (InlineQueryResultCachedMpeg4Gif) IsCached() bool { return true } + +func NewInlineQueryResultCachedPhoto(id, file string) InlineQueryResultCachedPhoto { + return InlineQueryResultCachedPhoto{ + Type: TypePhoto, + ID: id, + PhotoFileID: file, + } +} + +func (InlineQueryResultCachedPhoto) IsCached() bool { return true } + +func NewInlineQueryResultCachedSticker(id, file string) InlineQueryResultCachedSticker { + return InlineQueryResultCachedSticker{ + Type: TypeSticker, + ID: id, + StickerFileID: file, + } +} + +func (InlineQueryResultCachedSticker) IsCached() bool { return true } + +func NewInlineQueryResultCachedVideo(id, title, file string) InlineQueryResultCachedVideo { + return InlineQueryResultCachedVideo{ + Type: TypeVideo, + ID: id, + Title: title, + VideoFileID: file, + } +} + +func (InlineQueryResultCachedVideo) IsCached() bool { return true } + +func NewInlineQueryResultCachedVoice(id, title, file string) InlineQueryResultCachedVoice { + return InlineQueryResultCachedVoice{ + Type: TypeVoice, + ID: id, + Title: title, + VoiceFileID: file, + } +} + +func (InlineQueryResultCachedVoice) IsCached() bool { return true } + +func NewInlineQueryResultArticle(id, title string, content InputMessageContent) InlineQueryResultArticle { + return InlineQueryResultArticle{ + Type: TypeArticle, + ID: id, + Title: title, + InputMessageContent: content, + } +} + +func (InlineQueryResultArticle) IsCached() bool { return false } + +func NewInlineQueryResultAudio(id, title, audio string) InlineQueryResultAudio { + return InlineQueryResultAudio{ + Type: TypeAudio, + ID: id, + Title: title, + AudioURL: audio, + } +} + +func (InlineQueryResultAudio) IsCached() bool { return false } + +func NewInlineQueryResultContact(id, phone, name string) InlineQueryResultContact { + return InlineQueryResultContact{ + Type: TypeContact, + ID: id, + PhoneNumber: phone, + FirstName: name, + } +} + +func (InlineQueryResultContact) IsCached() bool { return false } + +func NewInlineQueryResultGame(id, shortName string) InlineQueryResultGame { + return InlineQueryResultGame{ + Type: TypeGame, + ID: id, + GameShortName: shortName, + } +} + +func (InlineQueryResultGame) IsCached() bool { return false } + +func NewInlineQueryResultDocument(id, title, mime, document string) InlineQueryResultDocument { + return InlineQueryResultDocument{ + Type: TypeDocument, + ID: id, + Title: title, + MimeType: mime, + DocumentURL: document, + } +} + +func (InlineQueryResultDocument) IsCached() bool { return false } + +func NewInlineQueryResultGif(id, gif, thumb string) InlineQueryResultGif { + return InlineQueryResultGif{ + Type: TypeGIF, + ID: id, + GifURL: gif, + ThumbURL: thumb, + } +} + +func (InlineQueryResultGif) IsCached() bool { return false } + +func NewInlineQueryResultLocation(id, title string, lat, long float32) InlineQueryResultLocation { + return InlineQueryResultLocation{ + Type: TypeLocation, + ID: id, + Title: title, + Latitude: lat, + Longitude: long, + } +} + +func (InlineQueryResultLocation) IsCached() bool { return false } + +func NewInlineQueryResultMpeg4Gif(id, mpeg4, thumb string) InlineQueryResultMpeg4Gif { + return InlineQueryResultMpeg4Gif{ + Type: TypeMpeg4Gif, + ID: id, + Mpeg4URL: mpeg4, + ThumbURL: thumb, + } +} + +func (InlineQueryResultMpeg4Gif) IsCached() bool { return false } + +func NewInlineQueryResultPhoto(id, photo, thumb string) InlineQueryResultPhoto { + return InlineQueryResultPhoto{ + Type: TypePhoto, + ID: id, + PhotoURL: photo, + ThumbURL: thumb, + } +} + +func (InlineQueryResultPhoto) IsCached() bool { return false } + +func NewInlineQueryResultVenue(id, title, addr string, lat, long float32) InlineQueryResultVenue { + return InlineQueryResultVenue{ + Type: TypeVenue, + ID: id, + Title: title, + Address: addr, + Latitude: lat, + Longitude: long, + } +} + +func (InlineQueryResultVenue) IsCached() bool { return false } + +func NewInlineQueryResultVideo(id, title, mime, video, thumb string) InlineQueryResultVideo { + return InlineQueryResultVideo{ + Type: TypeVideo, + ID: id, + VideoURL: video, + MimeType: mime, + Title: title, + ThumbURL: thumb, + } +} + +func (InlineQueryResultVideo) IsCached() bool { return false } + +func NewInlineQueryResultVoice(id, title, voice string) InlineQueryResultVoice { + return InlineQueryResultVoice{ + Type: TypeVoice, + ID: id, + Title: title, + VoiceURL: voice, + } +} + +func (InlineQueryResultVoice) IsCached() bool { return false } + +func (InlineKeyboardMarkup) isReplyMarkup() {} + +func (ReplyKeyboardMarkup) isReplyMarkup() {} + +func (ReplyKeyboardRemove) isReplyMarkup() {} + +func (ForceReply) isReplyMarkup() {} diff --git a/inline_test.go b/inline_test.go new file mode 100644 index 0000000..77e385c --- /dev/null +++ b/inline_test.go @@ -0,0 +1,51 @@ +package telegram + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestInlineQueryHasQuery(t *testing.T) { + t.Run("true", func(t *testing.T) { + iq := InlineQuery{Query: "sample text"} + assert.True(t, iq.HasQuery()) + }) + t.Run("false", func(t *testing.T) { + iq := InlineQuery{} + assert.False(t, iq.HasQuery()) + }) +} + +func TestInlineQueryHasOffset(t *testing.T) { + t.Run("true", func(t *testing.T) { + iq := InlineQuery{Offset: "42"} + assert.True(t, iq.HasOffset()) + }) + t.Run("false", func(t *testing.T) { + iq := InlineQuery{} + assert.False(t, iq.HasOffset()) + }) +} + +func TestInlineQueryHasLocation(t *testing.T) { + t.Run("true", func(t *testing.T) { + iq := InlineQuery{Location: &Location{Latitude: 56.085180, Longitude: 60.735150}} + assert.True(t, iq.HasLocation()) + }) + t.Run("false", func(t *testing.T) { + iq := InlineQuery{} + assert.False(t, iq.HasLocation()) + }) +} + +func TestChosenInlineResultInlineQueryHasLocation(t *testing.T) { + t.Run("true", func(t *testing.T) { + cir := ChosenInlineResult{Location: &Location{Latitude: 56.085180, Longitude: 60.735150}} + assert.True(t, cir.HasLocation()) + }) + t.Run("false", func(t *testing.T) { + cir := ChosenInlineResult{} + assert.False(t, cir.HasLocation()) + }) +} diff --git a/kick.go b/kick.go deleted file mode 100644 index c8197d3..0000000 --- a/kick.go +++ /dev/null @@ -1,39 +0,0 @@ -package telegram - -// KickChatMemberParameters represents data for KickChatMember method. -type KickChatMemberParameters struct { - // Unique identifier for the target chat - ChatID int64 `json:"chat_id"` - - // Unique identifier of the target user - UserID int `json:"user_id"` - - // Date when the user will be unbanned, unix time. If user is banned for - // more than 366 days or less than 30 seconds from the current time they - // are considered to be banned forever - UntilDate int64 `json:"until_date"` -} - -// KickChatMember kick a user from a group, a supergroup or a channel. In the case of supergroups and -// channels, the user will not be able to return to the group on their own using invite links, etc., -// unless unbanned first. The bot must be an administrator in the chat for this to work and must have -// the appropriate admin rights. Returns True on success. -// -// Note: In regular groups (non-supergroups), this method will only work if the 'All Members Are -// Admins' setting is off in the target group. Otherwise members may only be removed by the group's -// creator or by the member that added them. -func (bot *Bot) KickChatMember(params *KickChatMemberParameters) (bool, error) { - dst, err := parser.Marshal(params) - if err != nil { - return false, err - } - - resp, err := bot.request(dst, MethodKickChatMember) - if err != nil { - return false, err - } - - var ok bool - err = parser.Unmarshal(resp.Result, &ok) - return ok, err -} diff --git a/leave.go b/leave.go deleted file mode 100644 index eec9879..0000000 --- a/leave.go +++ /dev/null @@ -1,24 +0,0 @@ -package telegram - -// LeaveChatParameters represents data for LeaveChat method. -type LeaveChatParameters struct { - // Unique identifier for the target chat - ChatID int64 `json:"chat_id"` -} - -// LeaveChat leave a group, supergroup or channel. Returns True on success. -func (bot *Bot) LeaveChat(chatID int64) (bool, error) { - dst, err := parser.Marshal(&LeaveChatParameters{ChatID: chatID}) - if err != nil { - return false, err - } - - resp, err := bot.request(dst, MethodLeaveChat) - if err != nil { - return false, err - } - - var ok bool - err = parser.Unmarshal(resp.Result, &ok) - return ok, err -} diff --git a/methods.go b/methods.go new file mode 100644 index 0000000..c48ee4b --- /dev/null +++ b/methods.go @@ -0,0 +1,1728 @@ +package telegram + +import ( + "strconv" + "strings" +) + +type ( + // SendMessage represents data for SendMessage method. + SendMessage struct { + // Unique identifier for the target chat or username of the target channel (in the format @channelusername) + ChatID int64 `json:"chat_id"` + + // Text of the message to be sent + Text string `json:"text"` + + // 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"` + + // Disables link previews for links in this message + DisableWebPagePreview bool `json:"disable_web_page_preview,omitempty"` + + // Sends the message silently. Users will receive a notification with no sound. + DisableNotification bool `json:"disable_notification,omitempty"` + + // If the message is a reply, ID of the original message + ReplyToMessageID int `json:"reply_to_message_id,omitempty"` + + // Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user. + ReplyMarkup ReplyMarkup `json:"reply_markup,omitempty"` + } + + // ForwardMessage represents data for ForwardMessage method. + ForwardMessage struct { + // Unique identifier for the target chat or username of the target channel (in the format @channelusername) + ChatID int64 `json:"chat_id"` + + // Unique identifier for the chat where the original message was sent (or channel username in the format @channelusername) + FromChatID int64 `json:"from_chat_id"` + + // Sends the message silently. Users will receive a notification with no sound. + DisableNotification bool `json:"disable_notification,omitempty"` + + // Message identifier in the chat specified in from_chat_id + MessageID int `json:"message_id"` + } + + // SendPhoto represents data for SendPhoto method. + SendPhoto struct { + // Unique identifier for the target chat + ChatID int64 `json:"chat_id"` + + // Photo to send. Pass a file_id as String to send a photo that exists on the Telegram servers (recommended), pass an HTTP URL as a String for Telegram to get a photo from the Internet, or upload a new photo using multipart/form-data. + Photo *InputFile `json:"photo"` + + // Photo caption (may also be used when resending photos by file_id), 0-200 characters + Caption string `json:"caption,omitempty"` + + // Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. + ParseMode string `json:"parse_mode,omitempty"` + + // Disables link previews for links in this message + DisableWebPagePreview bool `json:"disable_web_page_preview,omitempty"` + + // Sends the message silently. Users will receive a notification with no sound. + DisableNotification bool `json:"disable_notification,omitempty"` + + // If the message is a reply, ID of the original message + ReplyToMessageID int `json:"reply_to_message_id,omitempty"` + + // Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user. + ReplyMarkup ReplyMarkup `json:"reply_markup,omitempty"` + } + + // SendAudio represents data for SendVenue method. + SendAudio struct { + // Unique identifier for the target chat or username of the target channel (in the format @channelusername) + ChatID int64 `json:"chat_id"` + + // Audio file to send. Pass a file_id as String to send an audio file that exists on the Telegram servers (recommended), pass an HTTP URL as a String for Telegram to get an audio file from the Internet, or upload a new one using multipart/form-data. + Audio *InputFile `json:"audio"` + + // Audio caption, 0-1024 characters + Caption string `json:"caption,omitempty"` + + // Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. + ParseMode string `json:"parse_mode,omitempty"` + + // Duration of the audio in seconds + Duration int `json:"duration,omitempty"` + + // Performer + Performer string `json:"performer,omitempty"` + + // Track name + Title string `json:"title,omitempty"` + + // Thumbnail of the file sent; can be ignored if thumbnail generation for the file is supported server-side. The thumbnail should be in JPEG format and less than 200 kB in size. A thumbnail‘s width and height should not exceed 320. Ignored if the file is not uploaded using multipart/form-data. Thumbnails can’t be reused and can be only uploaded as a new file, so you can pass “attach://” if the thumbnail was uploaded using multipart/form-data under . + Thumb *InputFile `json:"thumb,omitempty"` + + // Sends the message silently. Users will receive a notification with no sound. + DisableNotification bool `json:"disable_notification,omitempty"` + + // If the message is a reply, ID of the original message + ReplyToMessageID int `json:"reply_to_message_id,omitempty"` + + // Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user. + ReplyMarkup ReplyMarkup `json:"reply_markup,omitempty"` + } + + // SendDocument represents data for SendDocument method. + SendDocument struct { + // Unique identifier for the target chat + ChatID int64 `json:"chat_id"` + + // File to send. Pass a file_id as String to send a file that exists on the Telegram servers (recommended), pass an HTTP URL as a String for Telegram to get a file from the Internet, or upload a new one using multipart/form-data. + Document *InputFile `json:"document"` + + // Document caption (may also be used when resending documents by file_id), 0-200 characters + Caption string `json:"caption,omitempty"` + + // Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. + ParseMode string `json:"parse_mode,omitempty"` + + // Sends the message silently. Users will receive a notification with no sound. + DisableNotification bool `json:"disable_notification,omitempty"` + + // If the message is a reply, ID of the original message + ReplyToMessageID int `json:"reply_to_message_id,omitempty"` + + // Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user. + ReplyMarkup ReplyMarkup `json:"reply_markup,omitempty"` + } + + // SendDocument represents data for SendVideo method. + SendVideo struct { + // Unique identifier for the target chat or username of the target channel (in the format @channelusername) + ChatID int64 `json:"chat_id"` + + // Video to send. Pass a file_id as String to send a video that exists on the Telegram servers (recommended), pass an HTTP URL as a String for Telegram to get a video from the Internet, or upload a new video using multipart/form-data. + Video *InputFile `json:"video"` + + // Duration of sent video in seconds + Duration int `json:"duration,omitempty"` + + // Video width + Width int `json:"width,omitempty"` + + // Video height + Height int `json:"height,omitempty"` + + // Thumbnail of the file sent; can be ignored if thumbnail generation for the file is supported server-side. The thumbnail should be in JPEG format and less than 200 kB in size. A thumbnail‘s width and height should not exceed 320. Ignored if the file is not uploaded using multipart/form-data. Thumbnails can’t be reused and can be only uploaded as a new file, so you can pass “attach://” if the thumbnail was uploaded using multipart/form-data under . + Thumb *InputFile `json:"thumb,omitempty"` + + // Video caption (may also be used when resending videos by file_id), 0-1024 characters + Caption string `json:"caption,omitempty"` + + // Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. + ParseMode string `json:"parse_mode,omitempty"` + + // Pass True, if the uploaded video is suitable for streaming + SupportsStreaming bool `json:"supports_streaming,omitempty"` + + // Sends the message silently. Users will receive a notification with no sound. + DisableNotification bool `json:"disable_notification,omitempty"` + + // If the message is a reply, ID of the original message + ReplyToMessageID int `json:"reply_to_message_id,omitempty"` + + // Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user. + ReplyMarkup ReplyMarkup `json:"reply_markup,omitempty"` + } + + // SendAnimation represents data for SendAnimation method. + SendAnimation struct { + // Unique identifier for the target chat or username of the target channel (in the format @channelusername) + ChatID int64 `json:"chat_id"` + + // Animation to send. Pass a file_id as String to send an animation that exists on the Telegram servers (recommended), pass an HTTP URL as a String for Telegram to get an animation from the Internet, or upload a new animation using multipart/form-data. + Animation *InputFile `json:"animation"` + + // Duration of sent animation in seconds + Duration int `json:"duration,omitempty"` + + // Animation width + Width int `json:"width,omitempty"` + + // Animation height + Height int `json:"height,omitempty"` + + // Thumbnail of the file sent. The thumbnail should be in JPEG format and less than 200 kB in size. A thumbnail‘s width and height should not exceed 90. Ignored if the file is not uploaded using multipart/form-data. Thumbnails can’t be reused and can be only uploaded as a new file, so you can pass “attach://” if the thumbnail was uploaded using multipart/form-data under . + Thumb *InputFile `json:"thumb,omitempty"` + + // Animation caption (may also be used when resending animation by file_id), 0-200 characters + Caption string `json:"caption,omitempty"` + + // Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. + ParseMode string `json:"parse_mode,omitempty"` + + // Sends the message silently. Users will receive a notification with no sound. + DisableNotification bool `json:"disable_notification,omitempty"` + + // If the message is a reply, ID of the original message + ReplyToMessageID int `json:"reply_to_message_id,omitempty"` + + // Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user. + ReplyMarkup ReplyMarkup `json:"reply_markup,omitempty"` + } + + // SendVoice represents data for SendVoice method. + SendVoice struct { + // Unique identifier for the target chat or username of the target channel (in the format @channelusername) + ChatID int64 `json:"chat_id"` + + // Audio file to send. Pass a file_id as String to send a file that exists on the Telegram servers (recommended), pass an HTTP URL as a String for Telegram to get a file from the Internet, or upload a new one using multipart/form-data. + Voice *InputFile `json:"voice"` + + // Voice message caption, 0-1024 characters + Caption string `json:"caption,omitempty"` + + // Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. + ParseMode string `json:"parse_mode,omitempty"` + + // Duration of the voice message in seconds + Duration int `json:"duration,omitempty"` + + // Sends the message silently. Users will receive a notification with no sound. + DisableNotification bool `json:"disable_notification,omitempty"` + + // If the message is a reply, ID of the original message + ReplyToMessageID int `json:"reply_to_message_id,omitempty"` + + // Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user. + ReplyMarkup ReplyMarkup `json:"reply_markup,omitempty"` + } + + // SendVideoNote represents data for SendVideoNote method. + SendVideoNote struct { + // Unique identifier for the target chat or username of the target channel (in the format @channelusername) + ChatID int64 `json:"chat_id"` + + // Video note to send. Pass a file_id as String to send a video note that exists on the Telegram servers (recommended) or upload a new video using multipart/form-data.. Sending video notes by a URL is currently unsupported + VideoNote *InputFile `json:"video_note"` + + // Duration of sent video in seconds + Duration int `json:"duration,omitempty"` + + // Video width and height, i.e. diameter of the video message + Length int `json:"length,omitempty"` + + // Thumbnail of the file sent; can be ignored if thumbnail generation for the file is supported server-side. The thumbnail should be in JPEG format and less than 200 kB in size. A thumbnail‘s width and height should not exceed 320. Ignored if the file is not uploaded using multipart/form-data. Thumbnails can’t be reused and can be only uploaded as a new file, so you can pass “attach://” if the thumbnail was uploaded using multipart/form-data under . + Thumb *InputFile `json:"thumb,omitempty"` + + // Sends the message silently. Users will receive a notification with no sound. + DisableNotification bool `json:"disable_notification,omitempty"` + + // If the message is a reply, ID of the original message + ReplyToMessageID int `json:"reply_to_message_id,omitempty"` + + // Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user. + ReplyMarkup ReplyMarkup `json:"reply_markup,omitempty"` + } + + // SendMediaGroup represents data for SendMediaGroup method. + SendMediaGroup struct { + // Unique identifier for the target chat. + ChatID int64 `json:"chat_id" form:"chat_id"` + + // A JSON-serialized array describing photos and videos to be sent, must include 2–10 items + Media []AlbumMedia `json:"media" form:"media"` + + // Sends the messages silently. Users will receive a notification with no sound. + DisableNotification bool `json:"disable_notification,omitempty" form:"disable_notification"` + + // If the messages are a reply, ID of the original message + ReplyToMessageID int `json:"reply_to_message_id,omitempty" form:"reply_to_message_id"` + } + + // SendLocation represents data for SendLocation method. + SendLocation struct { + // Unique identifier for the target private chat + ChatID int64 `json:"chat_id"` + + // Latitude of the location + Latitude float32 `json:"latitude"` + + // Longitude of the location + Longitude float32 `json:"longitude"` + + // Period in seconds for which the location will be updated (see Live Locations), should be between 60 and 86400. + LivePeriod int `json:"live_period,omitempty"` + + // If the message is a reply, ID of the original message + ReplyToMessageID int `json:"reply_to_message_id,omitempty"` + + // Sends the message silently. Users will receive a notification with no sound. + DisableNotification bool `json:"disable_notification,omitempty"` + + // A JSON-serialized object for an inline keyboard. If empty, one 'Pay total price' button will be shown. If not empty, the first button must be a Pay button. + ReplyMarkup ReplyMarkup `json:"reply_markup,omitempty"` + } + + // EditMessageLiveLocation represents data for EditMessageLiveLocation method. + EditMessageLiveLocation struct { + // Required if inline_message_id is not specified. Unique identifier for the target chat or username of the target channel (in the format @channelusername) + ChatID int64 `json:"chat_id,omitempty"` + + // Required if inline_message_id is not specified. Identifier of the sent message + MessageID int `json:"message_id,omitempty"` + + // Required if chat_id and message_id are not specified. Identifier of the inline message + InlineMessageID string `json:"inline_message_id,omitempty"` + + // Latitude of new location + Latitude float32 `json:"latitude"` + + // Longitude of new location + Longitude float32 `json:"longitude"` + + // A JSON-serialized object for a new inline keyboard. + ReplyMarkup ReplyMarkup `json:"reply_markup,omitempty"` + } + + // StopMessageLiveLocation represents data for StopMessageLiveLocation method. + StopMessageLiveLocation struct { + // Required if inline_message_id is not specified. Unique identifier for the target chat or username of the target channel (in the format @channelusername) + ChatID int64 `json:"chat_id,omitempty"` + + // Required if inline_message_id is not specified. Identifier of the message with live location to stop + MessageID int `json:"message_id,omitempty"` + + // Required if chat_id and message_id are not specified. Identifier of the inline message + InlineMessageID string `json:"inline_message_id,omitempty"` + + // A JSON-serialized object for a new inline keyboard. + ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` + } + + // SendVenue represents data for SendVenue method. + SendVenue struct { + // Unique identifier for the target private chat + ChatID int64 `json:"chat_id"` + + // Latitude of the venue + Latitude float32 `json:"latitude"` + + // Longitude of the venue + Longitude float32 `json:"longitude"` + + // Name of the venue + Title string `json:"title"` + + // Address of the venue + Address string `json:"address"` + + // Foursquare identifier of the venue + FoursquareID string `json:"foursquare_id,omitempty"` + + // Foursquare type of the venue, if known. (For example, "arts_entertainment/default", "arts_entertainment/aquarium" or "food/icecream".) + FoursquareType string `json:"foursquare_type,omitempty"` + + // Sends the message silently. Users will receive a notification with no sound. + DisableNotification bool `json:"disable_notification,omitempty"` + + // If the message is a reply, ID of the original message + ReplyToMessageID int `json:"reply_to_message_id,omitempty"` + + // A JSON-serialized object for an inline keyboard. If empty, one 'Pay total price' button will be shown. If not empty, the first button must be a Pay button. + ReplyMarkup ReplyMarkup `json:"reply_markup,omitempty"` + } + + // SendContact represents data for SendContact method. + SendContact struct { + // Unique identifier for the target private chat + ChatID int64 `json:"chat_id"` + + // Contact's phone number + PhoneNumber string `json:"phone_number"` + + // Contact's first name + FirstName string `json:"first_name"` + + // Contact's last name + LastName string `json:"last_name"` + + // Additional data about the contact in the form of a vCard, 0-2048 bytes + VCard string `json:"vcard,omitempty"` + + // Sends the message silently. Users will receive a notification with no sound. + DisableNotification bool `json:"disable_notification,omitempty"` + + // If the message is a reply, ID of the original message + ReplyToMessageID int `json:"reply_to_message_id,omitempty"` + + // A JSON-serialized object for an inline keyboard. If empty, one 'Pay total price' button will be shown. If not empty, the first button must be a Pay button. + ReplyMarkup ReplyMarkup `json:"reply_markup,omitempty"` + } + + // SendPoll represents data for SendPoll method. + SendPoll struct { + // Unique identifier for the target chat. A native poll can't be sent to a private chat. + ChatID int64 `json:"chat_id"` + + // Poll question, 1-255 characters + Question string `json:"question"` + + // List of answer options, 2-10 strings 1-100 characters each + Options []string `json:"options"` + + // Sends the message silently. Users will receive a notification with no sound. + DisableNotification bool `json:"disable_notification,omitempty"` + + // If the message is a reply, ID of the original message + ReplyToMessageID int `json:"reply_to_message_id,omitempty"` + + // Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user. + ReplyMarkup ReplyMarkup `json:"reply_markup,omitempty"` + } + + // SendChatAction represents data for SendChatAction method. + SendChatAction struct { + // Unique identifier for the target chat + ChatID int64 `json:"chat_id"` + + // Type of action to broadcast + Action string `json:"action"` + } + + // GetUserProfilePhotos represents data for GetUserProfilePhotos method. + GetUserProfilePhotos struct { + // Unique identifier of the target user + UserID int `json:"user_id"` + + // Sequential number of the first photo to be returned. By default, all photos are returned. + Offset int `json:"offset,omitempty"` + + // Limits the number of photos to be retrieved. Values between 1—100 are accepted. Defaults to 100. + Limit int `json:"limit,omitempty"` + } + + // GetFile represents data for GetFile method. + GetFile struct { + // File identifier to get info about + FileID string `json:"file_id"` + } + + // KickChatMember represents data for KickChatMember method. + KickChatMember struct { + // Unique identifier for the target chat + ChatID int64 `json:"chat_id"` + + // Unique identifier of the target user + UserID int `json:"user_id"` + + // Date when the user will be unbanned, unix time. If user is banned for more than 366 days or less than 30 seconds from the current time they are considered to be banned forever + UntilDate int64 `json:"until_date"` + } + + // UnbanChatMember represents data for UnbanChatMember method. + UnbanChatMember struct { + // Unique identifier for the target chat + ChatID int64 `json:"chat_id"` + + UserID int `json:"user_id"` + } + + // RestrictChatMember represents data for RestrictChatMember method. + RestrictChatMember struct { + // Unique identifier for the target chat + ChatID int64 `json:"chat_id"` + + // Unique identifier of the target user + UserID int `json:"user_id"` + + // New user permissions + Permissions *ChatPermissions `json:"permissions"` + + // Date when restrictions will be lifted for the user, unix time. If user is restricted for more than 366 days or less than 30 seconds from the current time, they are considered to be restricted forever + UntilDate int64 `json:"until_date,omitempty"` + } + + // PromoteChatMember represents data for PromoteChatMember method. + PromoteChatMember struct { + // Unique identifier for the target chat or username of the target channel (in the format @channelusername) + ChatID int64 `json:"chat_id"` + + // Unique identifier of the target user + UserID int `json:"user_id"` + + // Pass True, if the administrator can change chat title, photo and other settings + CanChangeInfo bool `json:"can_change_info,omitempty"` + + // Pass True, if the administrator can create channel posts, channels only + CanPostMessages bool `json:"can_post_messages,omitempty"` + + // Pass True, if the administrator can edit messages of other users and can pin messages, channels only + CanEditMessages bool `json:"can_edit_messages,omitempty"` + + // Pass True, if the administrator can delete messages of other users + CanDeleteMessages bool `json:"can_delete_messages,omitempty"` + + // Pass True, if the administrator can invite new users to the chat + CanInviteUsers bool `json:"can_invite_users,omitempty"` + + // Pass True, if the administrator can restrict, ban or unban chat members + CanRestrictMembers bool `json:"can_restrict_members,omitempty"` + + // Pass True, if the administrator can pin messages, supergroups only + CanPinMessages bool `json:"can_pin_messages,omitempty"` + + // Pass True, if the administrator can add new administrators with a subset of his own privileges or demote administrators that he has promoted, directly or indirectly (promoted by administrators that were appointed by him) + CanPromoteMembers bool `json:"can_promote_members,omitempty"` + } + + // SetChatAdministratorCustomTitle represents data for SetChatAdministratorCustomTitle method. + SetChatAdministratorCustomTitle struct { + // Unique identifier for the target chat + ChatID int64 `json:"chat_id"` + + // Unique identifier of the target user + UserID int `json:"user_id"` + + // New custom title for the administrator; 0-16 characters, emoji are not allowed + CustomTitle string `json:"custom_title"` + } + + // SetChatPermissions represents data for SetChatPermissions method. + SetChatPermissions struct { + // Unique identifier for the target chat + ChatID int64 `json:"chat_id"` + + // New default chat permissions + Permissions ChatPermissions `json:"permissions"` + } + + // ExportChatInviteLink represents data for ExportChatInviteLink method. + ExportChatInviteLink struct { + // Unique identifier for the target chat + ChatID int64 `json:"chat_id"` + } + + // SetChatPhoto represents data for SetChatPhoto method. + SetChatPhoto struct { + // Unique identifier for the target chat + ChatID int64 `json:"chat_id"` + + // New chat photo, uploaded using multipart/form-data + ChatPhoto InputFile `json:"chat_photo"` + } + + // DeleteChatPhoto represents data for DeleteChatPhoto method. + DeleteChatPhoto struct { + // Unique identifier for the target chat + ChatID int64 `json:"chat_id"` + } + + // SetChatTitle represents data for SetChatTitle method. + SetChatTitle struct { + // Unique identifier for the target chat + ChatID int64 `json:"chat_id"` + + // New chat title, 1-255 characters + Title string `json:"title"` + } + + // SetChatDescription represents data for SetChatDescription method. + SetChatDescription struct { + // Unique identifier for the target chat + ChatID int64 `json:"chat_id"` + + // New chat description, 0-255 characters + Description string `json:"description"` + } + + // PinChatMessage represents data for PinChatMessage method. + PinChatMessage struct { + // Unique identifier for the target chat + ChatID int64 `json:"chat_id"` + + // Identifier of a message to pin + MessageID int `json:"message_id"` + + // Pass true, if it is not necessary to send a notification to all chat members about the new pinned message. Notifications are always disabled in channels. + DisableNotification bool `json:"disable_notification"` + } + + // UnpinChatMessage represents data for UnpinChatMessage method. + UnpinChatMessage struct { + // Unique identifier for the target chat + ChatID int64 `json:"chat_id"` + } + + // LeaveChat represents data for LeaveChat method. + LeaveChat struct { + // Unique identifier for the target chat + ChatID int64 `json:"chat_id"` + } + + // GetChat represents data for GetChat method. + GetChat struct { + // Unique identifier for the target chat + ChatID int64 `json:"chat_id"` + } + + // GetChatAdministrators represents data for GetChatAdministrators method. + GetChatAdministrators struct { + // Unique identifier for the target chat + ChatID int64 `json:"chat_id"` + } + + // GetChatMembersCount represents data for GetChatMembersCount method. + GetChatMembersCount struct { + // Unique identifier for the target chat + ChatID int64 `json:"chat_id"` + } + + // GetChatMember represents data for GetChatMember method. + GetChatMember struct { + // Unique identifier for the target chat + ChatID int64 `json:"chat_id"` + + // Unique identifier of the target user + UserID int `json:"user_id"` + } + + // SetChatStickerSet represents data for SetChatStickerSet method. + SetChatStickerSet struct { + // Unique identifier for the target chat + ChatID int64 `json:"chat_id"` + + // Name of the sticker set to be set as the group sticker set + StickerSetName string `json:"sticker_set_name"` + } + + // DeleteChatStickerSet represents data for DeleteChatStickerSet method. + DeleteChatStickerSet struct { + // Unique identifier for the target chat + ChatID int64 `json:"chat_id"` + } + + // AnswerCallbackQuery represents data for AnswerCallbackQuery method. + AnswerCallbackQuery struct { + // Unique identifier for the query to be answered + CallbackQueryID string `json:"callback_query_id"` + + // Text of the notification. If not specified, nothing will be shown to the user, 0-200 characters + Text string `json:"text,omitempty"` + + // URL that will be opened by the user's client. If you have created a Game and accepted the conditions via @Botfather, specify the URL that opens your game – note that this will only work if the query comes from a callback_game button. + // + // Otherwise, you may use links like t.me/your_bot?start=XXXX that open your bot with a parameter. + URL string `json:"url,omitempty"` + + // If true, an alert will be shown by the client instead of a notification at the top of the chat screen. Defaults to false. + ShowAlert bool `json:"show_alert,omitempty"` + + // The maximum amount of time in seconds that the result of the callback query may be cached client-side. Telegram apps will support caching starting in version 3.14. Defaults to 0. + CacheTime int `json:"cache_time,omitempty"` + } +) + +// GetMe testing your bot's auth token. Returns basic information about the bot in form of a User object. +func (b *Bot) GetMe() (*User, error) { + src, err := b.Do(MethodGetMe, nil) + if err != nil { + return nil, err + } + + resp := new(Response) + if err = b.marshler.Unmarshal(src, resp); err != nil { + return nil, err + } + + result := new(User) + if err = b.marshler.Unmarshal(resp.Result, &result); err != nil { + return nil, err + } + + return result, nil +} + +// SendMessage send text messages. On success, the sent Message is returned. +func (b *Bot) SendMessage(p SendMessage) (*Message, error) { + src, err := b.Do(MethodSendMessage, p) + if err != nil { + return nil, err + } + + resp := new(Response) + if err = b.marshler.Unmarshal(src, resp); err != nil { + return nil, err + } + + result := new(Message) + if err = b.marshler.Unmarshal(resp.Result, &result); err != nil { + return nil, err + } + + return result, nil +} + +// ForwardMessage forward messages of any kind. On success, the sent Message is returned. +func (b *Bot) ForwardMessage(p ForwardMessage) (*Message, error) { + src, err := b.Do(MethodForwardMessage, p) + if err != nil { + return nil, err + } + + resp := new(Response) + if err = b.marshler.Unmarshal(src, resp); err != nil { + return nil, err + } + + result := new(Message) + if err = b.marshler.Unmarshal(resp.Result, &result); err != nil { + return nil, err + } + + return result, err +} + +// SendPhoto send photos. On success, the sent Message is returned. +func (b *Bot) SendPhoto(p SendPhoto) (*Message, error) { + params := make(map[string]string) + params["chat_id"] = strconv.FormatInt(p.ChatID, 10) + params["caption"] = p.Caption + params["parse_mode"] = p.ParseMode + params["disable_web_page_preview"] = strconv.FormatBool(p.DisableWebPagePreview) + params["disable_notification"] = strconv.FormatBool(p.DisableNotification) + params["reply_to_message_id"] = strconv.Itoa(p.ReplyToMessageID) + + var err error + if params["photo"], err = b.marshler.MarshalToString(p.Photo); err != nil { + return nil, err + } + + if params["reply_markup"], err = b.marshler.MarshalToString(p.ReplyMarkup); err != nil { + return nil, err + } + + files := make([]*InputFile, 0) + if p.Photo.IsAttachment() { + files = append(files, p.Photo) + } + + src, err := b.Upload(MethodSendPhoto, params, files...) + if err != nil { + return nil, err + } + + resp := new(Response) + if err = b.marshler.Unmarshal(src, resp); err != nil { + return nil, err + } + + result := new(Message) + if err = b.marshler.Unmarshal(resp.Result, result); err != nil { + return nil, err + } + + return result, nil +} + +// SendAudio send audio files, if you want Telegram clients to display them in the music player. Your audio must be in the .MP3 or .M4A format. On success, the sent Message is returned. Bots can currently send audio files of up to 50 MB in size, this limit may be changed in the future. +// +// For sending voice messages, use the sendVoice method instead. +func (b *Bot) SendAudio(p SendAudio) (*Message, error) { + params := make(map[string]string) + params["chat_id"] = strconv.FormatInt(p.ChatID, 10) + params["caption"] = p.Caption + params["parse_mode"] = p.ParseMode + params["duration"] = strconv.Itoa(p.Duration) + params["performer"] = p.Performer + params["title"] = p.Title + params["disable_notification"] = strconv.FormatBool(p.DisableNotification) + params["reply_to_message_id"] = strconv.Itoa(p.ReplyToMessageID) + + var err error + if params["audio"], err = b.marshler.MarshalToString(p.Audio); err != nil { + return nil, err + } + + if params["thumb"], err = b.marshler.MarshalToString(p.Thumb); err != nil { + return nil, err + } + + if params["reply_markup"], err = b.marshler.MarshalToString(p.ReplyMarkup); err != nil { + return nil, err + } + + files := make([]*InputFile, 0) + if p.Audio.IsAttachment() { + files = append(files, p.Audio) + } + + if p.Thumb.IsAttachment() { + files = append(files, p.Thumb) + } + + src, err := b.Upload(MethodSendAudio, params, files...) + if err != nil { + return nil, err + } + + resp := new(Response) + if err = b.marshler.Unmarshal(src, resp); err != nil { + return nil, err + } + + result := new(Message) + if err = b.marshler.Unmarshal(resp.Result, &result); err != nil { + return nil, err + } + + return result, nil +} + +// SendDocument send general files. On success, the sent Message is returned. Bots can currently send files of any type of up to 50 MB in size, this limit may be changed in the future. +func (b *Bot) SendDocument(p SendDocument) (*Message, error) { + params := make(map[string]string) + params["chat_id"] = strconv.FormatInt(p.ChatID, 10) + params["caption"] = p.Caption + params["parse_mode"] = p.ParseMode + params["disable_notification"] = strconv.FormatBool(p.DisableNotification) + params["reply_to_message_id"] = strconv.Itoa(p.ReplyToMessageID) + + var err error + if params["document"], err = b.marshler.MarshalToString(p.Document); err != nil { + return nil, err + } + + if params["reply_markup"], err = b.marshler.MarshalToString(p.ReplyMarkup); err != nil { + return nil, err + } + + files := make([]*InputFile, 0) + if p.Document.IsAttachment() { + files = append(files, p.Document) + } + + src, err := b.Upload(MethodSendDocument, params, files...) + if err != nil { + return nil, err + } + + resp := new(Response) + if err = b.marshler.Unmarshal(src, resp); err != nil { + return nil, err + } + + result := new(Message) + if err = b.marshler.Unmarshal(resp.Result, result); err != nil { + return nil, err + } + + return result, nil +} + +// SendVideo send video files, Telegram clients support mp4 videos (other formats may be sent as Document). On success, the sent Message is returned. Bots can currently send video files of up to 50 MB in size, this limit may be changed in the future. +func (b *Bot) SendVideo(p SendVideo) (*Message, error) { + params := make(map[string]string) + params["chat_id"] = strconv.FormatInt(p.ChatID, 10) + params["duration"] = strconv.Itoa(p.Duration) + params["width"] = strconv.Itoa(p.Width) + params["height"] = strconv.Itoa(p.Height) + params["caption"] = p.Caption + params["parse_mode"] = p.ParseMode + params["supports_streaming"] = strconv.FormatBool(p.SupportsStreaming) + params["disable_notification"] = strconv.FormatBool(p.DisableNotification) + params["reply_to_message_id"] = strconv.Itoa(p.ReplyToMessageID) + + var err error + if params["video"], err = b.marshler.MarshalToString(p.Video); err != nil { + return nil, err + } + + if params["thumb"], err = b.marshler.MarshalToString(p.Thumb); err != nil { + return nil, err + } + + if params["reply_markup"], err = b.marshler.MarshalToString(p.ReplyMarkup); err != nil { + return nil, err + } + + files := make([]*InputFile, 0) + if p.Video.IsAttachment() { + files = append(files, p.Video) + } + + if p.Thumb.IsAttachment() { + files = append(files, p.Thumb) + } + + src, err := b.Upload(MethodSendVideo, params, files...) + if err != nil { + return nil, err + } + + resp := new(Response) + if err = b.marshler.Unmarshal(src, resp); err != nil { + return nil, err + } + + result := new(Message) + if err = b.marshler.Unmarshal(resp.Result, result); err != nil { + return nil, err + } + + return result, nil +} + +// SendAnimation send animation files (GIF or H.264/MPEG-4 AVC video without sound). On success, the sent Message is returned. Bots can currently send animation files of up to 50 MB in size, this limit may be changed in the future. +func (b *Bot) SendAnimation(p SendAnimation) (*Message, error) { + params := make(map[string]string) + params["chat_id"] = strconv.FormatInt(p.ChatID, 10) + params["duration"] = strconv.Itoa(p.Duration) + params["width"] = strconv.Itoa(p.Width) + params["height"] = strconv.Itoa(p.Height) + params["caption"] = p.Caption + params["parse_mode"] = p.ParseMode + params["disable_notification"] = strconv.FormatBool(p.DisableNotification) + params["reply_to_message_id"] = strconv.Itoa(p.ReplyToMessageID) + + var err error + if params["animation"], err = b.marshler.MarshalToString(p.Animation); err != nil { + return nil, err + } + + if params["thumb"], err = b.marshler.MarshalToString(p.Thumb); err != nil { + return nil, err + } + + if params["reply_markup"], err = b.marshler.MarshalToString(p.ReplyMarkup); err != nil { + return nil, err + } + + files := make([]*InputFile, 0) + if p.Animation.IsAttachment() { + files = append(files, p.Animation) + } + + if p.Thumb.IsAttachment() { + files = append(files, p.Thumb) + } + + src, err := b.Upload(MethodSendAnimation, params, files...) + if err != nil { + return nil, err + } + + resp := new(Response) + if err = b.marshler.Unmarshal(src, resp); err != nil { + return nil, err + } + + result := new(Message) + if err = b.marshler.Unmarshal(resp.Result, result); err != nil { + return nil, err + } + + return result, nil +} + +// SendVoice send audio files, if you want Telegram clients to display the file as a playable voice message. For this to work, your audio must be in an .ogg file encoded with OPUS (other formats may be sent as Audio or Document). On success, the sent Message is returned. Bots can currently send voice messages of up to 50 MB in size, this limit may be changed in the future. +func (b *Bot) SendVoice(p SendVoice) (*Message, error) { + params := make(map[string]string) + params["chat_id"] = strconv.FormatInt(p.ChatID, 10) + params["duration"] = strconv.Itoa(p.Duration) + params["caption"] = p.Caption + params["parse_mode"] = p.ParseMode + params["disable_notification"] = strconv.FormatBool(p.DisableNotification) + params["reply_to_message_id"] = strconv.Itoa(p.ReplyToMessageID) + + var err error + if params["voice"], err = b.marshler.MarshalToString(p.Voice); err != nil { + return nil, err + } + + if params["reply_markup"], err = b.marshler.MarshalToString(p.ReplyMarkup); err != nil { + return nil, err + } + + files := make([]*InputFile, 0) + if p.Voice.IsAttachment() { + files = append(files, p.Voice) + } + + src, err := b.Upload(MethodSendVoice, params, files...) + if err != nil { + return nil, err + } + + resp := new(Response) + if err = b.marshler.Unmarshal(src, resp); err != nil { + return nil, err + } + + result := new(Message) + if err = b.marshler.Unmarshal(resp.Result, result); err != nil { + return nil, err + } + + return result, nil +} + +// SendVideoNote send video messages. On success, the sent Message is returned. +func (b *Bot) SendVideoNote(p SendVideoNote) (*Message, error) { + params := make(map[string]string) + params["chat_id"] = strconv.FormatInt(p.ChatID, 10) + params["duration"] = strconv.Itoa(p.Duration) + params["length"] = strconv.Itoa(p.Length) + params["disable_notification"] = strconv.FormatBool(p.DisableNotification) + params["reply_to_message_id"] = strconv.Itoa(p.ReplyToMessageID) + + var err error + if params["video_note"], err = b.marshler.MarshalToString(p.VideoNote); err != nil { + return nil, err + } + + if params["thumb"], err = b.marshler.MarshalToString(p.Thumb); err != nil { + return nil, err + } + + if params["reply_markup"], err = b.marshler.MarshalToString(p.ReplyMarkup); err != nil { + return nil, err + } + + files := make([]*InputFile, 0) + if p.VideoNote.IsAttachment() { + files = append(files, p.VideoNote) + } + + if p.Thumb.IsAttachment() { + files = append(files, p.Thumb) + } + + src, err := b.Upload(MethodSendVideoNote, params, files...) + if err != nil { + return nil, err + } + + resp := new(Response) + if err = b.marshler.Unmarshal(src, resp); err != nil { + return nil, err + } + + result := new(Message) + if err = b.marshler.Unmarshal(resp.Result, result); err != nil { + return nil, err + } + + return result, nil +} + +// SendMediaGroup send a group of photos or videos as an album. On success, an array of the sent Messages is returned. +func (b *Bot) SendMediaGroup(p SendMediaGroup) ([]*Message, error) { + media := make([]string, len(p.Media), 10) + files := make([]*InputFile, 0) + + for i := range p.Media { + m := p.Media[i].GetMedia() + + if m.IsAttachment() { + files = append(files, m) + } + + src, err := b.marshler.MarshalToString(p.Media[i]) + if err != nil { + return nil, err + } + + media = append(media, src) + } + + params := make(map[string]string) + params["chat_id"] = strconv.FormatInt(p.ChatID, 10) + params["disable_notification"] = strconv.FormatBool(p.DisableNotification) + params["reply_to_message_id"] = strconv.Itoa(p.ReplyToMessageID) + params["media"] = "[" + strings.Join(media, ",") + "]" + + src, err := b.Upload(MethodSendMediaGroup, params, files...) + if err != nil { + return nil, err + } + + resp := new(Response) + if err = b.marshler.Unmarshal(src, resp); err != nil { + return nil, err + } + + result := make([]*Message, 0) + if err = b.marshler.Unmarshal(resp.Result, &result); err != nil { + return nil, err + } + + return result, nil +} + +// SendLocation send point on the map. On success, the sent Message is returned. +func (b *Bot) SendLocation(p SendLocation) (*Message, error) { + src, err := b.Do(MethodSendLocation, p) + if err != nil { + return nil, err + } + + resp := new(Response) + if err = b.marshler.Unmarshal(src, resp); err != nil { + return nil, err + } + + result := new(Message) + if err = b.marshler.Unmarshal(resp.Result, &result); err != nil { + return nil, err + } + + return result, nil +} + +// EditMessageLiveLocation edit live location messages. A location can be edited until its live_period expires or editing is explicitly disabled by a call to stopMessageLiveLocation. On success, if the edited message was sent by the bot, the edited Message is returned, otherwise True is returned. +func (b *Bot) EditMessageLiveLocation(p *EditMessageLiveLocation) (*Message, bool, error) { + src, err := b.Do(MethodEditMessageLiveLocation, p) + if err != nil { + return nil, false, err + } + + resp := new(Response) + if err = b.marshler.Unmarshal(src, resp); err != nil { + return nil, false, err + } + + result := new(Message) + if err = b.marshler.Unmarshal(resp.Result, &result); err != nil { + return nil, resp.Ok, err + } + + return result, resp.Ok, nil +} + +// StopMessageLiveLocation stop updating a live location message before live_period expires. On success, if the message was sent by the bot, the sent Message is returned, otherwise True is returned. +func (b *Bot) StopMessageLiveLocation(p StopMessageLiveLocation) (*Message, bool, error) { + src, err := b.Do(MethodStopMessageLiveLocation, p) + if err != nil { + return nil, false, err + } + + resp := new(Response) + if err = b.marshler.Unmarshal(src, resp); err != nil { + return nil, false, err + } + + result := new(Message) + if err = b.marshler.Unmarshal(resp.Result, result); err != nil { + return nil, resp.Ok, err + } + + return result, resp.Ok, nil +} + +// SendVenue send information about a venue. On success, the sent Message is returned. +func (b *Bot) SendVenue(p SendVenue) (*Message, error) { + src, err := b.Do(MethodSendVenue, p) + if err != nil { + return nil, err + } + + resp := new(Response) + if err = b.marshler.Unmarshal(src, resp); err != nil { + return nil, err + } + + result := new(Message) + if err = b.marshler.Unmarshal(resp.Result, &result); err != nil { + return nil, err + } + + return result, nil +} + +// SendContact send phone contacts. On success, the sent Message is returned. +func (b *Bot) SendContact(p SendContact) (*Message, error) { + src, err := b.Do(MethodSendContact, p) + if err != nil { + return nil, err + } + + resp := new(Response) + if err = b.marshler.Unmarshal(src, resp); err != nil { + return nil, err + } + + result := new(Message) + if err = b.marshler.Unmarshal(resp.Result, &result); err != nil { + return nil, err + } + + return result, nil +} + +// SendPoll send a native poll. A native poll can't be sent to a private chat. On success, the sent Message is returned. +func (b *Bot) SendPoll(p SendPoll) (*Message, error) { + src, err := b.Do(MethodSendPoll, p) + if err != nil { + return nil, err + } + + resp := new(Response) + if err = b.marshler.Unmarshal(src, resp); err != nil { + return nil, err + } + + result := new(Message) + if err = b.marshler.Unmarshal(resp.Result, &result); err != nil { + return nil, err + } + + return result, nil +} + +// SendChatAction tell the user that something is happening on the bot's side. The status is set for 5 seconds or less (when a message arrives from your bot, Telegram clients clear its typing status). Returns True on success. +// +// We only recommend using this method when a response from the bot will take a noticeable amount of time to arrive. +func (b *Bot) SendChatAction(chatID int64, action string) (bool, error) { + src, err := b.Do(MethodSendChatAction, SendChatAction{ + ChatID: chatID, Action: action, + }) + if err != nil { + return false, err + } + + resp := new(Response) + if err = b.marshler.Unmarshal(src, resp); err != nil { + return false, err + } + + var result bool + if err = b.marshler.Unmarshal(resp.Result, &result); err != nil { + return false, err + } + + return result, nil +} + +// GetUserProfilePhotos get a list of profile pictures for a user. Returns a UserProfilePhotos object. +func (b *Bot) GetUserProfilePhotos(p GetUserProfilePhotos) (*UserProfilePhotos, error) { + src, err := b.Do(MethodGetUserProfilePhotos, p) + if err != nil { + return nil, err + } + + resp := new(Response) + if err = b.marshler.Unmarshal(src, resp); err != nil { + return nil, err + } + + result := new(UserProfilePhotos) + if err = b.marshler.Unmarshal(resp.Result, &result); err != nil { + return nil, err + } + + return result, nil +} + +// GetFile get basic info about a file and prepare it for downloading. For the moment, bots can download files of up to 20MB in size. On success, a File object is returned. The file can then be downloaded via the link https://api.telegram.org/file/bot/, where is taken from the response. It is guaranteed that the link will be valid for at least 1 hour. When the link expires, a new one can be requested by calling getFile again. +// +// Note: This function may not preserve the original file name and MIME type. You should save the file's MIME type and name (if available) when the File object is received. +func (b *Bot) GetFile(fileID string) (*File, error) { + src, err := b.Do(MethodGetFile, GetFile{FileID: fileID}) + if err != nil { + return nil, err + } + + resp := new(Response) + if err = b.marshler.Unmarshal(src, resp); err != nil { + return nil, err + } + + result := new(File) + if err = b.marshler.Unmarshal(resp.Result, &result); err != nil { + return nil, err + } + + return result, nil +} + +// KickChatMember kick a user from a group, a supergroup or a channel. In the case of supergroups and channels, the user will not be able to return to the group on their own using invite links, etc., unless unbanned first. The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. Returns True on success. +// +// Note: In regular groups (non-supergroups), this method will only work if the 'All Members Are Admins' setting is off in the target group. Otherwise members may only be removed by the group's creator or by the member that added them. +func (b *Bot) KickChatMember(p KickChatMember) (bool, error) { + src, err := b.Do(MethodKickChatMember, p) + if err != nil { + return false, err + } + + resp := new(Response) + if err = b.marshler.Unmarshal(src, resp); err != nil { + return false, err + } + + var result bool + if err = b.marshler.Unmarshal(resp.Result, &result); err != nil { + return false, err + } + + return result, nil +} + +// UnbanChatMember unban a previously kicked user in a supergroup or channel. The user will not return to the group or channel automatically, but will be able to join via link, etc. The bot must be an administrator for this to work. Returns True on success. +func (b *Bot) UnbanChatMember(chatID int64, userID int) (bool, error) { + src, err := b.Do(MethodUnbanChatMember, UnbanChatMember{ + ChatID: chatID, UserID: userID, + }) + if err != nil { + return false, err + } + + resp := new(Response) + if err = b.marshler.Unmarshal(src, resp); err != nil { + return false, err + } + + var result bool + if err = b.marshler.Unmarshal(resp.Result, &result); err != nil { + return false, err + } + + return result, nil +} + +// restrict a user in a supergroup. The bot must be an administrator in the supergroup for this to work and must have the appropriate admin rights. Pass True for all permissions to lift restrictions from a user. Returns True on success. +func (b *Bot) RestrictChatMember(p RestrictChatMember) (bool, error) { + src, err := b.Do(MethodRestrictChatMember, p) + if err != nil { + return false, err + } + + resp := new(Response) + if err = b.marshler.Unmarshal(src, resp); err != nil { + return false, err + } + + var result bool + if err = b.marshler.Unmarshal(resp.Result, &result); err != nil { + return false, err + } + + return result, nil +} + +// PromoteChatMember promote or demote a user in a supergroup or a channel. The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. Pass False for all boolean to demote a user. Returns True on success. +func (b *Bot) PromoteChatMember(p PromoteChatMember) (bool, error) { + src, err := b.Do(MethodPromoteChatMember, p) + if err != nil { + return false, err + } + + resp := new(Response) + if err = b.marshler.Unmarshal(src, resp); err != nil { + return false, err + } + + var result bool + if err = b.marshler.Unmarshal(resp.Result, &result); err != nil { + return false, err + } + + return result, nil +} + +// SetChatAdministratorCustomTitle method to set a custom title for an administrator in a supergroup promoted by the b. Returns True on success. +func (b *Bot) SetChatAdministratorCustomTitle(p SetChatAdministratorCustomTitle) (bool, error) { + src, err := b.Do(MethodSetChatAdministratorCustomTitle, p) + if err != nil { + return false, err + } + + resp := new(Response) + if err = b.marshler.Unmarshal(src, resp); err != nil { + return false, err + } + + var result bool + if err = b.marshler.Unmarshal(resp.Result, &result); err != nil { + return false, err + } + + return result, nil +} + +// SetChatPermissions set default chat permissions for all members. The bot must be an administrator in the group or a supergroup for this to work and must have the can_restrict_members admin rights. Returns True on success. +func (b *Bot) SetChatPermissions(p SetChatPermissions) (bool, error) { + src, err := b.Do(MethodSetChatPermissions, p) + if err != nil { + return false, err + } + + resp := new(Response) + if err = b.marshler.Unmarshal(src, resp); err != nil { + return false, err + } + + var result bool + if err = b.marshler.Unmarshal(resp.Result, &result); err != nil { + return false, err + } + + return result, nil +} + +// ExportChatInviteLink export an invite link to a supergroup or a channel. The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. Returns exported invite link as String on success. +func (b *Bot) ExportChatInviteLink(chatID int64) (string, error) { + src, err := b.Do(MethodExportChatInviteLink, ExportChatInviteLink{ChatID: chatID}) + if err != nil { + return "", err + } + + resp := new(Response) + if err = b.marshler.Unmarshal(src, resp); err != nil { + return "", err + } + + var result string + if err = b.marshler.Unmarshal(resp.Result, &result); err != nil { + return "", err + } + + return result, nil +} + +// SetChatPhoto set a new profile photo for the chat. Photos can't be changed for private chats. The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. Returns True on success. +func (b *Bot) SetChatPhoto(chatID int64, chatPhoto *InputFile) (bool, error) { + params := make(map[string]string) + params["chat_id"] = strconv.FormatInt(chatID, 10) + + var err error + if params["photo"], err = b.marshler.MarshalToString(chatPhoto); err != nil { + return false, err + } + + files := make([]*InputFile, 0) + if chatPhoto.IsAttachment() { + files = append(files, chatPhoto) + } + + src, err := b.Upload(MethodSetChatPhoto, params, files...) + if err != nil { + return false, err + } + + resp := new(Response) + if err = b.marshler.Unmarshal(src, resp); err != nil { + return false, err + } + + var result bool + if err = b.marshler.Unmarshal(resp.Result, &result); err != nil { + return false, err + } + + return result, nil +} + +// DeleteChatPhoto delete a chat photo. Photos can't be changed for private chats. The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. Returns True on success. +func (b *Bot) DeleteChatPhoto(chatID int64) (bool, error) { + src, err := b.Do(MethodDeleteChatPhoto, DeleteChatPhoto{ChatID: chatID}) + if err != nil { + return false, err + } + + resp := new(Response) + if err = b.marshler.Unmarshal(src, resp); err != nil { + return false, err + } + + var result bool + if err = b.marshler.Unmarshal(resp.Result, &result); err != nil { + return false, err + } + + return result, nil +} + +// SetChatTitle change the title of a chat. Titles can't be changed for private chats. The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. Returns True on success. +func (b *Bot) SetChatTitle(chatID int64, title string) (bool, error) { + src, err := b.Do(MethodSetChatTitle, SetChatTitle{ + ChatID: chatID, Title: title, + }) + if err != nil { + return false, err + } + + resp := new(Response) + if err = b.marshler.Unmarshal(src, resp); err != nil { + return false, err + } + + var result bool + if err = b.marshler.Unmarshal(resp.Result, &result); err != nil { + return false, err + } + + return result, nil +} + +// SetChatDescription change the description of a group, a supergroup or a channel. The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. Returns True on success. +func (b *Bot) SetChatDescription(chatID int64, description string) (bool, error) { + src, err := b.Do(MethodSetChatDescription, SetChatDescription{ + ChatID: chatID, Description: description, + }) + if err != nil { + return false, err + } + + resp := new(Response) + if err = b.marshler.Unmarshal(src, resp); err != nil { + return false, err + } + + var result bool + if err = b.marshler.Unmarshal(resp.Result, &result); err != nil { + return false, err + } + + return result, nil +} + +// PinChatMessage pin a message in a group, a supergroup, or a channel. The bot must be an administrator in the chat for this to work and must have the ‘can_pin_messages’ admin right in the supergroup or ‘can_edit_messages’ admin right in the channel. Returns True on success. +func (b *Bot) PinChatMessage(p *PinChatMessage) (bool, error) { + src, err := b.Do(MethodPinChatMessage, p) + if err != nil { + return false, err + } + + resp := new(Response) + if err = b.marshler.Unmarshal(src, resp); err != nil { + return false, err + } + + var result bool + if err = b.marshler.Unmarshal(resp.Result, &result); err != nil { + return false, err + } + + return result, nil +} + +// UnpinChatMessage unpin a message in a group, a supergroup, or a channel. The bot must be an administrator in the chat for this to work and must have the ‘can_pin_messages’ admin right in the supergroup or ‘can_edit_messages’ admin right in the channel. Returns True on success. +func (b *Bot) UnpinChatMessage(chatID int64) (bool, error) { + src, err := b.Do(MethodUnpinChatMessage, UnpinChatMessage{ChatID: chatID}) + if err != nil { + return false, err + } + + resp := new(Response) + if err = b.marshler.Unmarshal(src, resp); err != nil { + return false, err + } + + var result bool + if err = b.marshler.Unmarshal(resp.Result, &result); err != nil { + return false, err + } + + return result, nil +} + +// LeaveChat leave a group, supergroup or channel. Returns True on success. +func (b *Bot) LeaveChat(chatID int64) (bool, error) { + src, err := b.Do(MethodLeaveChat, LeaveChat{ChatID: chatID}) + if err != nil { + return false, err + } + + resp := new(Response) + if err = b.marshler.Unmarshal(src, resp); err != nil { + return false, err + } + + var result bool + if err = b.marshler.Unmarshal(resp.Result, &result); err != nil { + return false, err + } + + return result, nil +} + +// GetChat get up to date information about the chat (current name of the user for one-on-one conversations, current username of a user, group or channel, etc.). Returns a Chat object on success. +func (b *Bot) GetChat(chatID int64) (*Chat, error) { + src, err := b.Do(MethodGetChat, GetChat{ChatID: chatID}) + if err != nil { + return nil, err + } + + resp := new(Response) + if err = b.marshler.Unmarshal(src, resp); err != nil { + return nil, err + } + + result := new(Chat) + if err = b.marshler.Unmarshal(resp.Result, result); err != nil { + return nil, err + } + + return result, nil +} + +// GetChatAdministrators get a list of administrators in a chat. On success, returns an Array of ChatMember objects that contains information about all chat administrators except other bots. If the chat is a group or a supergroup and no administrators were appointed, only the creator will be returned. +func (b *Bot) GetChatAdministrators(chatID int64) ([]*ChatMember, error) { + src, err := b.Do(MethodGetChatAdministrators, GetChatAdministrators{ChatID: chatID}) + if err != nil { + return nil, err + } + + resp := new(Response) + if err = b.marshler.Unmarshal(src, resp); err != nil { + return nil, err + } + + result := make([]*ChatMember, 0) + if err = b.marshler.Unmarshal(resp.Result, &result); err != nil { + return nil, err + } + + return result, nil +} + +// GetChatMembersCount get the number of members in a chat. Returns Int on success. +func (b *Bot) GetChatMembersCount(chatID int64) (int, error) { + src, err := b.Do(MethodGetChatMembersCount, GetChatMembersCount{ChatID: chatID}) + if err != nil { + return 0, err + } + + resp := new(Response) + if err = b.marshler.Unmarshal(src, resp); err != nil { + return 0, err + } + + var result int + if err = b.marshler.Unmarshal(resp.Result, &result); err != nil { + return 0, err + } + + return result, nil +} + +// GetChatMember get information about a member of a chat. Returns a ChatMember object on success. +func (b *Bot) GetChatMember(chatID int64, userID int) (*ChatMember, error) { + src, err := b.Do(MethodGetChatMember, GetChatMember{ + ChatID: chatID, UserID: userID, + }) + if err != nil { + return nil, err + } + + resp := new(Response) + if err = b.marshler.Unmarshal(src, resp); err != nil { + return nil, err + } + + result := new(ChatMember) + if err = b.marshler.Unmarshal(resp.Result, result); err != nil { + return nil, err + } + + return result, nil +} + +// SetChatStickerSet set a new group sticker set for a supergroup. The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. Use the field can_set_sticker_set optionally returned in getChat requests to check if the bot can use this method. Returns True on success. +func (b *Bot) SetChatStickerSet(chatID int64, stickerSetName string) (bool, error) { + src, err := b.Do(MethodSetChatStickerSet, SetChatStickerSet{ + ChatID: chatID, StickerSetName: stickerSetName, + }) + if err != nil { + return false, err + } + + resp := new(Response) + if err = b.marshler.Unmarshal(src, resp); err != nil { + return false, err + } + + var result bool + if err = b.marshler.Unmarshal(resp.Result, &result); err != nil { + return false, err + } + + return result, nil +} + +// DeleteChatStickerSet delete a group sticker set from a supergroup. The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. Use the field can_set_sticker_set optionally returned in getChat requests to check if the bot can use this method. Returns True on success. +func (b *Bot) DeleteChatStickerSet(chatID int64) (bool, error) { + src, err := b.Do(MethodDeleteChatStickerSet, DeleteChatStickerSet{ChatID: chatID}) + if err != nil { + return false, err + } + + resp := new(Response) + if err = b.marshler.Unmarshal(src, resp); err != nil { + return false, err + } + + var result bool + if err = b.marshler.Unmarshal(resp.Result, &result); err != nil { + return false, err + } + + return result, nil +} + +// AnswerCallbackQuery send answers to callback queries sent from inline keyboards. The answer will be displayed to the user as a notification at the top of the chat screen or as an alert. On success, True is returned. +func (b *Bot) AnswerCallbackQuery(p AnswerCallbackQuery) (bool, error) { + src, err := b.Do(MethodAnswerCallbackQuery, p) + if err != nil { + return false, err + } + + resp := new(Response) + if err = b.marshler.Unmarshal(src, resp); err != nil { + return false, err + } + + var result bool + if err = b.marshler.Unmarshal(resp.Result, &result); err != nil { + return false, err + } + + return result, nil +} diff --git a/passport.go b/passport.go new file mode 100644 index 0000000..a87097a --- /dev/null +++ b/passport.go @@ -0,0 +1,1070 @@ +package telegram + +import "errors" + +/* + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "crypto/rsa" + "crypto/sha1" //nolint: gosec + "crypto/sha256" + "crypto/sha512" + "encoding/base64" +*/ + +type ( + // PassportData contains information about Telegram Passport data shared with + // the bot by the user. + PassportData struct { + // Array with information about documents and other Telegram Passport + // elements that was shared with the bot + Data []*EncryptedPassportElement `json:"data"` + + // Encrypted credentials required to decrypt the data + Credentials *EncryptedCredentials `json:"credentials"` + } + + // PassportFile represents a file uploaded to Telegram Passport. Currently all + // Telegram Passport files are in JPEG format when decrypted and don't exceed + // 10MB. + PassportFile struct { + // Identifier for this file, which can be used to download or reuse the file + FileID string `json:"file_id"` + + // 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"` + + // File size + FileSize int `json:"file_size"` + + // Unix time when the file was uploaded + FileDate int `json:"file_date"` + } + + // EncryptedPassportElement contains information about documents or other + // Telegram Passport elements shared with the bot by the user. + EncryptedPassportElement struct { + // Element type. + Type string `json:"type"` + + // Base64-encoded encrypted Telegram Passport element data provided by + // the user, available for "personal_details", "passport", + // "driver_license", "identity_card", "identity_passport" and "address" + // types. Can be decrypted and verified using the accompanying + // EncryptedCredentials. + Data string `json:"data,omitempty"` + + // User's verified phone number, available only for "phone_number" type + PhoneNumber string `json:"phone_number,omitempty"` + + // User's verified email address, available only for "email" type + Email string `json:"email,omitempty"` + + // Array of encrypted files with documents provided by the user, + // available for "utility_bill", "bank_statement", "rental_agreement", + // "passport_registration" and "temporary_registration" types. Files can + // be decrypted and verified using the accompanying EncryptedCredentials. + Files []*PassportFile `json:"files,omitempty"` + + // Encrypted file with the front side of the document, provided by the + // user. Available for "passport", "driver_license", "identity_card" and + // "internal_passport". The file can be decrypted and verified using the + // accompanying EncryptedCredentials. + FrontSide *PassportFile `json:"front_side,omitempty"` + + // Encrypted file with the reverse side of the document, provided by the + // user. Available for "driver_license" and "identity_card". The file can + // be decrypted and verified using the accompanying EncryptedCredentials. + ReverseSide *PassportFile `json:"reverse_side,omitempty"` + + // Encrypted file with the selfie of the user holding a document, + // provided by the user; available for "passport", "driver_license", + // "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 + // authenticating EncryptedPassportElement. See the Telegram Passport + // Documentation for a complete description of the data decryption and + // authentication processes. + EncryptedCredentials struct { + // Base64-encoded encrypted JSON-serialized data with unique user's + // payload, data hashes and secrets required for EncryptedPassportElement + // decryption and authentication + Data string `json:"data"` + + // Base64-encoded data hash for data authentication + Hash string `json:"hash"` + + // Base64-encoded secret, encrypted with the bot's public RSA key, + // required for data decryption + Secret string `json:"secret"` + } + + // PassportElementError represents an error in the Telegram Passport element + // which was submitted that should be resolved by the user. + PassportElementError interface { + PassportElementErrorMessage() string + PassportElementErrorSource() string + PassportElementErrorType() string + } + + // PassportElementErrorDataField represents an issue in one of the data + // fields that was provided by the user. The error is considered resolved + // when the field's value changes. + PassportElementErrorDataField struct { + // Error source, must be data + Source string `json:"source"` + + // The section of the user's Telegram Passport which has the error, one + // of "personal_details", "passport", "driver_license", "identity_card", + // "internal_passport", "address" + Type string `json:"type"` + + // Name of the data field which has the error + FieldName string `json:"field_name"` + + // Base64-encoded data hash + DataHash string `json:"data_hash"` + + // Error message + Message string `json:"message"` + } + + // PassportElementErrorFrontSide represents an issue with the front side of + // a document. The error is considered resolved when the file with the front + // side of the document changes. + PassportElementErrorFrontSide struct { + // Error source, must be front_side + Source string `json:"source"` + + // The section of the user's Telegram Passport which has the issue, one + // of "passport", "driver_license", "identity_card", "internal_passport" + Type string `json:"type"` + + // Base64-encoded hash of the file with the front side of the document + FileHash string `json:"file_hash"` + + // Error message + Message string `json:"message"` + } + + // PassportElementErrorReverseSide represents an issue with the reverse side + // of a document. The error is considered resolved when the file with reverse + // side of the document changes. + PassportElementErrorReverseSide struct { + // Error source, must be reverse_side + Source string `json:"source"` + + // The section of the user's Telegram Passport which has the issue, one + // of "driver_license", "identity_card" + Type string `json:"type"` + + // Base64-encoded hash of the file with the reverse side of the document + FileHash string `json:"file_hash"` + + // Error message + Message string `json:"message"` + } + + // PassportElementErrorSelfie represents an issue with the selfie with a + // document. The error is considered resolved when the file with the selfie + // changes. + PassportElementErrorSelfie struct { + // Error source, must be selfie + Source string `json:"source"` + + // The section of the user's Telegram Passport which has the issue, one + // of "passport", "driver_license", "identity_card", "internal_passport" + Type string `json:"type"` + + // Base64-encoded hash of the file with the selfie + FileHash string `json:"file_hash"` + + // Error message + Message string `json:"message"` + } + + // PassportElementErrorFile represents an issue with a document scan. The + // error is considered resolved when the file with the document scan changes. + PassportElementErrorFile struct { + // Error source, must be file + Source string `json:"source"` + + // The section of the user's Telegram Passport which has the issue, one + // of "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"` + } + + // PassportElementErrorFiles represents an issue with a list of scans. The + // error is considered resolved when the list of files containing the scans + // changes. + PassportElementErrorFiles struct { + // Error source, must be files + Source string `json:"source"` + + // The section of the user's Telegram Passport which has the issue, one + // of "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"` + } + + // 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 change. + 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"` + } + + SetPassportDataErrors struct { + // User identifier + UserID int `json:"user_id"` + + // A JSON-serialized array describing the errors + Errors []PassportElementError `json:"errors"` + } + + // AuthParameters represent a Telegram Passport auth parameters for SDK. + Auth struct { + // Unique identifier for the b. You can get it from bot token. + // For example, for the bot token + // 1234567:4TT8bAc8GHUspu3ERYn-KGcvsvGB9u_n4ddy, the bot id is + // 1234567. + BotID int `json:"bot_id"` + + // A JSON-serialized object describing the data you want to + // request + Scope *PassportScope `json:"scope"` + + // Public key of the bot + PublicKey string `json:"public_key"` + + // Bot-specified nonce. + // + // Important: For security purposes it should be a + // cryptographically secure unique identifier of the request. In + // particular, it should be long enough and it should be + // generated using a cryptographically secure pseudorandom number + // generator. You should never accept credentials with the same + // nonce twice. + Nonce string `json:"nonce"` + } + + // PassportScope represents the data to be requested. + PassportScope struct { + // List of requested elements, each type may be used only once + // in the entire array of PassportScopeElement objects + Data []*PassportScopeElement `json:"data"` + + // Scope version, must be 1 + V int `json:"v"` + } + + // PassportScopeElement represents a requested element. + PassportScopeElement interface { + PassportScopeElementTranslation() bool + PassportScopeElementSelfie() bool + } + + // PassportScopeElementOneOfSeveral represents several elements one of which must be provided. + PassportScopeElementOneOfSeveral struct { + // List of elements one of which must be provided; + OneOf []*PassportScopeElementOne `json:"one_of"` + + // Use this parameter if you want to request a selfie with the + // document from this list that the user chooses to upload. + Selfie bool `json:"selfie,omitempty"` + + // Use this parameter if you want to request a translation of + // the document from this list that the user chooses to upload. + // Note: We suggest to only request translations after you have + // received a valid document that requires one. + Translation bool `json:"translation,omitempty"` + } + + // PassportScopeElementOne represents one particular element that must + // be provided. If no options are needed, String can be used instead of + // this object to specify the type of the element. + PassportScopeElementOne struct { + // Element type. + Type string `json:"type"` + + // Use this parameter if you want to request a selfie with the + // document as well. + Selfie bool `json:"selfie,omitempty"` + + // Use this parameter if you want to request a translation of + // the document as well. + Translation bool `json:"translation,omitempty"` + + // Use this parameter to request the first, last and middle name + // of the user in the language of the user's country of residence. + NativeNames bool `json:"native_names,omitempty"` + } + + Passport struct { + // Personal Details + PersonalDetails struct { + Data *PersonalDetails `json:"data"` + } `json:"personal_details"` + + // Passport + Passport struct { + Data *IDDocumentData `json:"data"` + FrontSide *PassportFile `json:"front_side"` + Selfie *PassportFile `json:"selfie,omitempty"` + Translation []*PassportFile `json:"translation,omitempty"` + } `json:"passport"` + + // Internal Passport + InternalPassport struct { + Data *IDDocumentData `json:"data"` + FrontSide *PassportFile `json:"front_side"` + Selfie *PassportFile `json:"selfie,omitempty"` + Translation []*PassportFile `json:"translation,omitempty"` + } `json:"internal_passport"` + + // Driver License + DriverLicense struct { + Data *IDDocumentData `json:"data"` + FrontSide *PassportFile `json:"front_side"` + ReverseSide *PassportFile `json:"reverse_side"` + Selfie *PassportFile `json:"selfie,omitempty"` + Translation []*PassportFile `json:"translation,omitempty"` + } `json:"driver_license"` + + // Identity Card + IdentityCard struct { + Data *IDDocumentData `json:"data"` + FrontSide *PassportFile `json:"front_side"` + ReverseSide *PassportFile `json:"reverse_side"` + Selfie *PassportFile `json:"selfie,omitempty"` + Translation []*PassportFile `json:"translation,omitempty"` + } `json:"identity_card"` + + // Address + Address struct { + Data *ResidentialAddress `json:"data"` + } `json:"address"` + + // Utility Bill + UtilityBill struct { + Files []*PassportFile `json:"files"` + Translation []*PassportFile `json:"translation,omitempty"` + } `json:"utility_bill"` + + // Bank Statement + BankStatement struct { + Files []*PassportFile `json:"files"` + Translation []*PassportFile `json:"translation,omitempty"` + } `json:"bank_statement"` + + // Rental Agreement + RentalAgreement struct { + Files []*PassportFile `json:"files"` + Translation []*PassportFile `json:"translation,omitempty"` + } `json:"rental_agreement"` + + // Registration Page in the Internal Passport + PassportRegistration struct { + Files []*PassportFile `json:"files"` + Translation []*PassportFile `json:"translation,omitempty"` + } `json:"passport_registration"` + + // Temporary Registration + TemporaryRegistration struct { + Files []*PassportFile `json:"files"` + Translation []*PassportFile `json:"translation,omitempty"` + } `json:"temporary_registration"` + + // Phone number + PhoneNumber string `json:"phone_number"` + + // Email + Email string `json:"email"` + } + + // PersonalDetails represents personal details. + PersonalDetails struct { + // First Name + FirstName string `json:"first_name"` + + // Last Name + LastName string `json:"last_name"` + + // Middle Name + MiddleName string `json:"middle_name,omitempty"` + + // Date of birth in DD.MM.YYYY format + BirthDate string `json:"birth_date"` + + // Gender, male or female + Gender string `json:"gender"` + + // Citizenship (ISO 3166-1 alpha-2 country code) + CountryCode string `json:"country_code"` + + // Country of residence (ISO 3166-1 alpha-2 country code) + ResidenceCountryCode string `json:"residence_country_code"` + + // First Name in the language of the user's country of residence + FirstNameNative string `json:"first_name_native"` + + // Last Name in the language of the user's country of residence + LastNameNative string `json:"last_name_native"` + + // Middle Name in the language of the user's country of residence + MiddleNameNative string `json:"middle_name_native,omitempty"` + } + + // ResidentialAddress represents a residential address. + ResidentialAddress struct { + // First line for the address + StreetLine1 string `json:"street_line1"` + + // Second line for the address + StreetLine2 string `json:"street_line2,omitempty"` + + // City + City string `json:"city"` + + // State + State string `json:"state,omitempty"` + + // ISO 3166-1 alpha-2 country code + CountryCode string `json:"country_code"` + + // Address post code + PostCode string `json:"post_code"` + } + + // IDDocumentData represents the data of an identity document. + IDDocumentData struct { + // Document number + DocumentNo string `json:"document_no"` + + // Date of expiry, in DD.MM.YYYY format + ExpiryDate string `json:"expiry_date,omitempty"` + } + + // Credentials is a JSON-serialized object. + Credentials struct { + // Credentials for encrypted data + SecureData *SecureData `json:"secure_data"` + + // Bot-specified nonce + Nonce string `json:"nonce"` + } + + // SecureData represents the credentials required to decrypt encrypted + // data. All fields are optional and depend on fields that were requested. + SecureData struct { + // Credentials for encrypted personal details + PersonalDetails *SecureValue `json:"personal_details,omitempty"` + + // Credentials for encrypted passport + Passport *SecureValue `json:"passport,omitempty"` + + // Credentials for encrypted internal passport + InternalPassport *SecureValue `json:"internal_passport,omitempty"` + + // Credentials for encrypted driver license + DriverLicense *SecureValue `json:"driver_license,omitempty"` + + // Credentials for encrypted ID card + IdentityCard *SecureValue `json:"identity_card,omitempty"` + + // Credentials for encrypted residential address + Address *SecureValue `json:"address,omitempty"` + + // Credentials for encrypted utility bill + UtilityBill *SecureValue `json:"utility_bill,omitempty"` + + // Credentials for encrypted bank statement + BankStatement *SecureValue `json:"bank_statement,omitempty"` + + // Credentials for encrypted rental agreement + RentalAgreement *SecureValue `json:"rental_agreement,omitempty"` + + // Credentials for encrypted registration from internal passport + PassportRegistration *SecureValue `json:"passport_registration,omitempty"` + + // Credentials for encrypted temporary registration + TemporaryRegistration *SecureValue `json:"temporary_registration,omitempty"` + } + + // SecureValue represents the credentials required to decrypt encrypted + // values. All fields are optional and depend on the type of fields that + // were requested. + SecureValue struct { + // Credentials for encrypted Telegram Passport data. + Data *DataCredentials `json:"data,omitempty"` + + // Credentials for an encrypted document's front side. + FrontSide *FileCredentials `json:"front_side,omitempty"` + + // Credentials for an encrypted document's reverse side. + ReverseSide *FileCredentials `json:"reverse_side,omitempty"` + + // Credentials for an encrypted selfie of the user with a document. + Selfie *FileCredentials `json:"selfie,omitempty"` + + // Credentials for an encrypted translation of the document. + Translation []*FileCredentials `json:"translation,omitempty"` + + // Credentials for encrypted files. + Files []*FileCredentials `json:"files,omitempty"` + } + + // DataCredentials can be used to decrypt encrypted data from the data + // field in EncryptedPassportElement. + DataCredentials struct { + // Checksum of encrypted data + DataHash string `json:"data_hash"` + + // Secret of encrypted data + Secret string `json:"secret"` + } + + // FileCredentials can be used to decrypt encrypted files from the + // front_side, reverse_side, selfie, files and translation fields in + // EncryptedPassportElement. + FileCredentials struct { + // Checksum of encrypted file + FileHash string `json:"file_hash"` + + // Secret of encrypted file + Secret string `json:"secret"` + } +) + +var ErrNotEqual = errors.New("credentials hash and credentials data hash is not equal") + +// SetPassportDataErrors 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). Returns True on success. +// +// Use this if the data submitted by the user doesn't satisfy the standards your +// service requires for any reason. For example, if a birthday date seems +// invalid, a submitted document is blurry, a scan shows evidence of tampering, +// etc. Supply some details in the error message to make sure the user knows how +// to correct the issues. +func (b *Bot) SetPassportDataErrors(uid int, errors ...PassportElementError) (bool, error) { + src, err := b.Do(MethodSetPassportDataErrors, SetPassportDataErrors{ + UserID: uid, Errors: errors, + }) + if err != nil { + return false, err + } + + resp := new(Response) + if err = b.marshler.Unmarshal(src, resp); err != nil { + return false, err + } + + var result bool + if err = b.marshler.Unmarshal(resp.Result, &result); err != nil { + return false, err + } + + return result, nil +} + +/* TODO(toby3d) +func (b *Bot) DecryptFile(pf *PassportFile, fc *FileCredentials) (data []byte, err error) { + secret, err := decodeField(fc.Secret) + if err != nil { + return nil, err + } + + hash, err := decodeField(fc.FileHash) + if err != nil { + return nil, err + } + + key, iv := decryptSecretHash(secret, hash) + + file, err := b.GetFile(pf.FileID) + if err != nil { + return nil, err + } + + if _, data, err = b.Client.Get(nil, b.NewFileURL(file.FilePath).String()); err != nil { + return nil, err + } + + if data, err = decryptData(key, iv, data); err != nil { + return nil, err + } + + if !match(hash, data) { + err = ErrNotEqual + return nil, err + } + + offset := int(data[0]) + data = data[offset:] + + return data, nil +} + +func (dc *DataCredentials) decrypt(d string) (data []byte, err error) { + secret, err := decodeField(dc.Secret) + if err != nil { + return + } + + hash, err := decodeField(dc.DataHash) + if err != nil { + return + } + + key, iv := decryptSecretHash(secret, hash) + + if data, err = decodeField(d); err != nil { + return + } + + if data, err = decryptData(key, iv, data); err != nil { + return + } + + if !match(hash, data) { + err = ErrNotEqual + } + + offset := int(data[0]) + data = data[offset:] + + return +} + +func (ec *EncryptedCredentials) Decrypt(pk *rsa.PrivateKey) (*Credentials, error) { + if ec == nil || pk == nil { + return nil, nil + } + + data, err := decrypt(pk, ec.Secret, ec.Hash, ec.Data) + if err != nil { + return nil, err + } + + var c Credentials + if err = json.ConfigFastest.Unmarshal(data, &c); err != nil { + return nil, Error{Description: err.Error()} + } + + return &c, err +} + +func (epe *EncryptedPassportElement) DecryptPersonalDetails(sv *SecureValue) (*PersonalDetails, error) { + if !epe.IsPersonalDetails() || !sv.HasData() { + return nil, nil + } + + body, err := sv.Data.decrypt(epe.Data) + if err != nil { + return nil, err + } + + var pd PersonalDetails + if err = json.ConfigFastest.Unmarshal(body, &pd); err != nil { + return nil, Error{Description: err.Error()} + } + + return &pd, err +} + +func (epe *EncryptedPassportElement) DecryptPassport(sv *SecureValue, b *Bot) (*IDDocumentData, []byte, []byte, [][]byte, error) { + if !epe.IsPassport() || !sv.HasData() || !sv.HasFrontSide() { + return nil, nil, nil, nil, nil + } + + body, err := sv.Data.decrypt(epe.Data) + if err != nil { + return nil, nil, nil, nil, err + } + + var idd IDDocumentData + if err = b.marshler.Unmarshal(body, &idd); err != nil { + return nil, nil, nil, nil, err + } + + fs, err := b.DecryptFile(epe.FrontSide, sv.FrontSide) + if err != nil { + return &idd, nil, nil, nil, err + } + + var s []byte + + if sv.HasSelfie() { + if s, err = b.DecryptFile(epe.Selfie, sv.Selfie); err != nil { + return &idd, fs, nil, nil, err + } + } + + t := make([][]byte, len(sv.Translation)) + + if sv.HasTranslation() { + for i := range t { + if t[i], err = b.DecryptFile(epe.Translation[i], sv.Translation[i]); err != nil { + return &idd, fs, s, nil, err + } + } + } + + return &idd, fs, s, t, nil +} + +func (epe *EncryptedPassportElement) DecryptInternalPassport(sv *SecureValue, b *Bot) (*IDDocumentData, []byte, []byte, [][]byte, error) { + if !epe.IsInternalPassport() || !sv.HasData() || !sv.HasFrontSide() { + return nil, nil, nil, nil, nil + } + + body, err := sv.Data.decrypt(epe.Data) + if err != nil { + return nil, nil, nil, nil, err + } + + var idd IDDocumentData + if err = b.marshler.Unmarshal(body, &idd); err != nil { + return nil, nil, nil, nil, err + } + + fs, err := b.DecryptFile(epe.FrontSide, sv.FrontSide) + if err != nil { + return &idd, nil, nil, nil, err + } + + var s []byte + + if sv.HasSelfie() { + if s, err = b.DecryptFile(epe.Selfie, sv.Selfie); err != nil { + return &idd, fs, nil, nil, err + } + } + + t := make([][]byte, len(sv.Translation)) + + if sv.HasTranslation() { + for i := range t { + if t[i], err = b.DecryptFile(epe.Translation[i], sv.Translation[i]); err != nil { + return &idd, fs, s, nil, err + } + } + } + + return &idd, fs, s, t, nil +} + +func (epe *EncryptedPassportElement) DecryptDriverLicense(sv *SecureValue, b *Bot) (*IDDocumentData, []byte, []byte, []byte, [][]byte, error) { + if !epe.IsDriverLicense() || !sv.HasData() || !sv.HasFrontSide() || !sv.HasReverseSide() { + return nil, nil, nil, nil, nil, nil + } + + body, err := sv.Data.decrypt(epe.Data) + if err != nil { + return nil, nil, nil, nil, nil, err + } + + var idd IDDocumentData + if err = b.marshler.Unmarshal(body, &idd); err != nil { + return nil, nil, nil, nil, nil, err + } + + fs, err := b.DecryptFile(epe.FrontSide, sv.FrontSide) + if err != nil { + return &idd, nil, nil, nil, nil, err + } + + rs, err := b.DecryptFile(epe.ReverseSide, sv.ReverseSide) + if err != nil { + return &idd, nil, nil, nil, nil, err + } + + var s []byte + + if sv.HasSelfie() { + if s, err = b.DecryptFile(epe.Selfie, sv.Selfie); err != nil { + return &idd, fs, rs, nil, nil, err + } + } + + t := make([][]byte, len(sv.Translation)) + + if sv.HasTranslation() { + for i := range t { + if t[i], err = b.DecryptFile(epe.Translation[i], sv.Translation[i]); err != nil { + return &idd, fs, rs, s, nil, err + } + } + } + + return &idd, fs, rs, s, t, nil +} + +func (epe *EncryptedPassportElement) IsAddress() bool { + return epe != nil && strings.EqualFold(epe.Type, TypeAddress) +} + +func (epe *EncryptedPassportElement) IsBankStatement() bool { + return epe != nil && strings.EqualFold(epe.Type, TypeBankStatement) +} + +func (epe *EncryptedPassportElement) IsDriverLicense() bool { + return epe != nil && strings.EqualFold(epe.Type, TypeDriverLicense) +} + +func (epe *EncryptedPassportElement) IsEmail() bool { + return epe != nil && strings.EqualFold(epe.Type, TypeEmail) +} + +func (epe *EncryptedPassportElement) IsIdentityCard() bool { + return epe != nil && strings.EqualFold(epe.Type, TypeIdentityCard) +} + +func (epe *EncryptedPassportElement) IsInternalPassport() bool { + return epe != nil && strings.EqualFold(epe.Type, TypeInternalPassport) +} + +func (epe *EncryptedPassportElement) IsPassport() bool { + return epe != nil && strings.EqualFold(epe.Type, TypePassport) +} + +func (epe *EncryptedPassportElement) IsPassportRegistration() bool { + return epe != nil && strings.EqualFold(epe.Type, TypePassportRegistration) +} + +func (epe *EncryptedPassportElement) IsPersonalDetails() bool { + return epe != nil && strings.EqualFold(epe.Type, TypePersonalDetails) +} + +func (epe *EncryptedPassportElement) IsPhoneNumber() bool { + return epe != nil && strings.EqualFold(epe.Type, TypePhoneNumber) +} + +func (epe *EncryptedPassportElement) IsRentalAgreement() bool { + return epe != nil && strings.EqualFold(epe.Type, TypeRentalAgreement) +} + +func (epe *EncryptedPassportElement) IsTemporaryRegistration() bool { + return epe != nil && strings.EqualFold(epe.Type, TypeTemporaryRegistration) +} + +func (epe *EncryptedPassportElement) IsUtilityBill() bool { + return epe != nil && strings.EqualFold(epe.Type, TypeUtilityBill) +} + +func (idd *IDDocumentData) ExpiryTime() *time.Time { + if idd == nil || idd.ExpiryDate == "" { + return nil + } + + et, err := time.Parse("02.01.2006", idd.ExpiryDate) + if err != nil { + return nil + } + + return &et +} + +func decrypt(pk *rsa.PrivateKey, s, h, d string) (obj []byte, err error) { + // Note that all base64-encoded fields should be decoded before use. + secret, err := decodeField(s) + if err != nil { + return nil, err + } + + hash, err := decodeField(h) + if err != nil { + return nil, err + } + + data, err := decodeField(d) + if err != nil { + return nil, err + } + + if pk != nil { + // Decrypt the credentials secret (secret field in EncryptedCredentials) + // using your private key + if secret, err = decryptSecret(pk, secret); err != nil { + return nil, err + } + } + + // Use this secret and the credentials hash (hash field in + // EncryptedCredentials) to calculate credentials_key and credentials_iv + key, iv := decryptSecretHash(secret, hash) + + // Decrypt the credentials data (data field in EncryptedCredentials) by + // AES256-CBC using these credentials_key and credentials_iv. + if data, err = decryptData(key, iv, data); err != nil { + return nil, err + } + + // IMPORTANT: At this step, make sure that the credentials hash is equal + // to SHA256(credentials_data) + if !match(hash, data) { + return nil, ErrNotEqual + } + + // Credentials data is padded with 32 to 255 random padding bytes to make + // its length divisible by 16 bytes. The first byte contains the length + // of this padding (including this byte). Remove the padding to get the + // data. + return data[int(data[0]):], nil +} + +func decodeField(rawField string) (field []byte, err error) { + return base64.StdEncoding.DecodeString(rawField) +} + +func decryptSecret(pk *rsa.PrivateKey, s []byte) (secret []byte, err error) { + return rsa.DecryptOAEP(sha1.New(), rand.Reader, pk, s, nil) //nolint: gosec +} + +func decryptSecretHash(s, h []byte) (key, iv []byte) { + var err error + + hash := sha512.New() + if _, err = hash.Write(s); err != nil { + return + } + + if _, err = hash.Write(h); err != nil { + return + } + + sh := hash.Sum(nil) + + return sh[0:32], sh[32 : 32+16] +} + +func match(h, d []byte) bool { + dh := sha256.New() + if _, err := dh.Write(d); err != nil { + return false + } + + return bytes.EqualFold(h, dh.Sum(nil)) +} + +func decryptData(key, iv, data []byte) (buf []byte, err error) { + block, err := aes.NewCipher(key) + if err != nil { + return + } + + buf = make([]byte, len(data)) + cipher.NewCBCDecrypter(block, iv).CryptBlocks(buf, data) + + return +} + +func (pd *PersonalDetails) BirthTime() *time.Time { + if pd == nil || pd.BirthDate == "" { + return nil + } + + bt, err := time.Parse("02.01.2006", pd.BirthDate) + if err != nil { + return nil + } + + return &bt +} + +func (pd PersonalDetails) FullName() string { return pd.FirstName + " " + pd.LastName } + +func (pd PersonalDetails) FullNameNative() string { return pd.FirstNameNative + " " + pd.LastNameNative } + +func (sv *SecureValue) HasData() bool { return sv != nil && sv.Data != nil } + +func (sv *SecureValue) HasFiles() bool { return sv != nil && len(sv.Files) > 0 } + +func (sv *SecureValue) HasFrontSide() bool { return sv != nil && sv.FrontSide != nil } + +func (sv *SecureValue) HasReverseSide() bool { return sv != nil && sv.ReverseSide != nil } + +func (sv *SecureValue) HasSelfie() bool { return sv != nil && sv.Selfie != nil } + +func (sv *SecureValue) HasTranslation() bool { return sv != nil && len(sv.Translation) > 0 } +*/ diff --git a/payments.go b/payments.go new file mode 100644 index 0000000..1a9b641 --- /dev/null +++ b/payments.go @@ -0,0 +1,347 @@ +package telegram + +type ( + // LabeledPrice represents a portion of the price for goods or services. + LabeledPrice struct { + // Portion label + Label string `json:"label"` + + // Price of the product in the smallest units of the currency (integer, + // not float/double). For example, for a price of US$ 1.45 pass amount = + // 145. See the exp parameter in currencies.json, it shows the number of + // digits past the decimal point for each currency (2 for the majority + // of currencies). + Amount int `json:"amount"` + } + + // Invoice contains basic information about an invoice. + Invoice struct { + // Product name + Title string `json:"title"` + + // Product description + Description string `json:"description"` + + // Unique bot deep-linking parameter that can be used to generate this + // invoice + StartParameter string `json:"start_parameter"` + + // Three-letter ISO 4217 currency code + Currency string `json:"currency"` + + // Total price in the smallest units of the currency (integer, not + // float/double). For example, for a price of US$ 1.45 pass amount = 145. + // See the exp parameter in currencies.json, it shows the number of + // digits past the decimal point for each currency (2 for the majority + // of currencies). + TotalAmount int `json:"total_amount"` + } + + // ShippingAddress represents a shipping address. + ShippingAddress struct { + // ISO 3166-1 alpha-2 country code + CountryCode string `json:"country_code"` + + // State, if applicable + State string `json:"state"` + + // City + City string `json:"city"` + + // First line for the address + StreetLine1 string `json:"street_line1"` + + // Second line for the address + StreetLine2 string `json:"street_line2"` + + // Address post code + PostCode string `json:"post_code"` + } + + // OrderInfo represents information about an order. + OrderInfo struct { + // User name + Name string `json:"name,omitempty"` + + // User's phone number + PhoneNumber string `json:"phone_number,omitempty"` + + // User email + Email string `json:"email,omitempty"` + + // User shipping address + ShippingAddress *ShippingAddress `json:"shipping_address,omitempty"` + } + + // ShippingOption represents one shipping option. + ShippingOption struct { + // Shipping option identifier + ID string `json:"id"` + + // Option title + Title string `json:"title"` + + // List of price portions + Prices []*LabeledPrice `json:"prices"` + } + + // SuccessfulPayment contains basic information about a successful payment. + SuccessfulPayment struct { + // Three-letter ISO 4217 currency code + Currency string `json:"currency"` + + // Bot specified invoice payload + InvoicePayload string `json:"invoice_payload"` + + // Identifier of the shipping option chosen by the user + ShippingOptionID string `json:"shipping_option_id,omitempty"` + + // Telegram payment identifier + TelegramPaymentChargeID string `json:"telegram_payment_charge_id"` + + // Provider payment identifier + ProviderPaymentChargeID string `json:"provider_payment_charge_id"` + + // Total price in the smallest units of the currency (integer, not + // float/double). For example, for a price of US$ 1.45 pass amount = 145. + // See the exp parameter in currencies.json, it shows the number of + // digits past the decimal point for each currency (2 for the majority + // of currencies). + TotalAmount int `json:"total_amount"` + + // Order info provided by the user + OrderInfo *OrderInfo `json:"order_info,omitempty"` + } + + // ShippingQuery contains information about an incoming shipping query. + ShippingQuery struct { + // Unique query identifier + ID string `json:"id"` + + // Bot specified invoice payload + InvoicePayload string `json:"invoice_payload"` + + // User who sent the query + From *User `json:"from"` + + // User specified shipping address + ShippingAddress *ShippingAddress `json:"shipping_address"` + } + + // PreCheckoutQuery contains information about an incoming pre-checkout query. + PreCheckoutQuery struct { + // Unique query identifier + ID string `json:"id"` + + // Three-letter ISO 4217 currency code + Currency string `json:"currency"` + + // Bot specified invoice payload + InvoicePayload string `json:"invoice_payload"` + + // Identifier of the shipping option chosen by the user + ShippingOptionID string `json:"shipping_option_id,omitempty"` + + // User who sent the query + From *User `json:"from"` + + // Total price in the smallest units of the currency (integer, not + // float/double). For example, for a price of US$ 1.45 pass amount = 145. + // See the exp parameter in currencies.json, it shows the number of + // digits past the decimal point for each currency (2 for the majority of + // currencies). + TotalAmount int `json:"total_amount"` + + // Order info provided by the user + OrderInfo *OrderInfo `json:"order_info,omitempty"` + } + + // SendInvoiceParameters represents data for SendInvoice method. + SendInvoice struct { + // Unique identifier for the target private chat + ChatID int64 `json:"chat_id"` + + // Product name, 1-32 characters + Title string `json:"title"` + + // Product description, 1-255 characters + Description string `json:"description"` + + // Bot-defined invoice payload, 1-128 bytes. This will not be displayed to + // the user, use for your internal processes. + Payload string `json:"payload"` + + // Payments provider token, obtained via Botfather + ProviderToken string `json:"provider_token"` + + // Unique deep-linking parameter that can be used to generate this invoice + // when used as a start parameter + StartParameter string `json:"start_parameter"` + + // Three-letter ISO 4217 currency code, see more on currencies + Currency string `json:"currency"` + + // JSON-encoded data about the invoice, which will be shared with the payment + // provider. A detailed description of required fields should be provided by + // the payment provider. + ProviderData string `json:"provider_data,omitempty"` + + // URL of the product photo for the invoice. Can be a photo of the goods or a + // marketing image for a service. People like it better when they see what + // they are paying for. + PhotoURL string `json:"photo_url,omitempty"` + + // Price breakdown, a list of components (e.g. product price, tax, discount, + // delivery cost, delivery tax, bonus, etc.) + Prices []*LabeledPrice `json:"prices"` + + // Photo size + PhotoSize int `json:"photo_size,omitempty"` + + // Photo width + PhotoWidth int `json:"photo_width,omitempty"` + + // Photo height + PhotoHeight int `json:"photo_height,omitempty"` + + // If the message is a reply, ID of the original message + ReplyToMessageID int `json:"reply_to_message_id,omitempty"` + + // Pass True, if you require the user's full name to complete the order + NeedName bool `json:"need_name,omitempty"` + + // Pass True, if you require the user's phone number to complete the order + NeedPhoneNumber bool `json:"need_phone_number,omitempty"` + + // Pass True, if you require the user's email to complete the order + NeedEmail bool `json:"need_email,omitempty"` + + // Pass True, if you require the user's shipping address to complete the + // order + NeedShippingAddress bool `json:"need_shipping_address,omitempty"` + + // Pass True, if the final price depends on the shipping method + IsFlexible bool `json:"is_flexible,omitempty"` + + // Sends the message silently. Users will receive a notification with no + // sound. + DisableNotification bool `json:"disable_notification,omitempty"` + + // A JSON-serialized object for an inline keyboard. If empty, one 'Pay total + // price' button will be shown. If not empty, the first button must be a Pay + // button. + ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` + } + + // AnswerShippingQueryParameters represents data for AnswerShippingQuery method. + AnswerShippingQuery struct { + // Unique identifier for the query to be answered + ShippingQueryID string `json:"shipping_query_id"` + + // Required if ok is False. Error message in human readable form that + // explains why it is impossible to complete the order (e.g. "Sorry, delivery + // to your desired address is unavailable'). Telegram will display this + // message to the user. + ErrorMessage string `json:"error_message,omitempty"` + + // Specify True if delivery to the specified address is possible and False + // if there are any problems (for example, if delivery to the specified + // address is not possible) + Ok bool `json:"ok"` + + // Required if ok is True. A JSON-serialized array of available shipping + // options. + ShippingOptions []*ShippingOption `json:"shipping_options,omitempty"` + } + + // AnswerPreCheckoutQueryParameters represents data for AnswerPreCheckoutQuery + // method. + AnswerPreCheckoutQuery struct { + // Unique identifier for the query to be answered + PreCheckoutQueryID string `json:"pre_checkout_query_id"` + + // Required if ok is False. Error message in human readable form that + // explains the reason for failure to proceed with the checkout (e.g. "Sorry, + // somebody just bought the last of our amazing black T-shirts while you were + // busy filling out your payment details. Please choose a different color or + // garment!"). Telegram will display this message to the user. + ErrorMessage string `json:"error_message,omitempty"` + + // Specify True if everything is alright (goods are available, etc.) and the + // bot is ready to proceed with the order. Use False if there are any + // problems. + Ok bool `json:"ok"` + } +) + +// SendInvoice send invoices. On success, the sent Message is returned. +func (b *Bot) SendInvoice(p SendInvoice) (*Message, error) { + src, err := b.Do(MethodSendInvoice, p) + if err != nil { + return nil, err + } + + resp := new(Response) + if err = b.marshler.Unmarshal(src, resp); err != nil { + return nil, err + } + + result := new(Message) + if err = b.marshler.Unmarshal(resp.Result, result); err != nil { + return nil, err + } + + return result, nil +} + +// AnswerShippingQuery reply to shipping queries. +// +// If you sent an invoice requesting a shipping address and the parameter +// is_flexible was specified, the Bot API will send an Update with a +// shipping_query field to the b. On success, True is returned. +func (b *Bot) AnswerShippingQuery(p AnswerShippingQuery) (bool, error) { + src, err := b.Do(MethodAnswerShippingQuery, p) + if err != nil { + return false, err + } + + resp := new(Response) + if err = b.marshler.Unmarshal(src, resp); err != nil { + return false, err + } + + var result bool + if err = b.marshler.Unmarshal(resp.Result, &result); err != nil { + return false, err + } + + return result, nil +} + +// AnswerPreCheckoutQuery respond to such pre-checkout queries. +// +// Once the user has confirmed their payment and shipping details, the Bot API +// sends the final confirmation in the form of an Update with the field +// pre_checkout_query. Use this method to respond to such pre-checkout queries. +// On success, True is returned. +// +// Note: The Bot API must receive an answer within 10 seconds after the +// pre-checkout query was sent. +func (b *Bot) AnswerPreCheckoutQuery(p AnswerShippingQuery) (bool, error) { + src, err := b.Do(MethodAnswerPreCheckoutQuery, p) + if err != nil { + return false, err + } + + resp := new(Response) + if err = b.marshler.Unmarshal(src, resp); err != nil { + return false, err + } + + var result bool + if err = b.marshler.Unmarshal(resp.Result, &result); err != nil { + return false, err + } + + return result, nil +} diff --git a/pin.go b/pin.go deleted file mode 100644 index 4c93e7b..0000000 --- a/pin.go +++ /dev/null @@ -1,34 +0,0 @@ -package telegram - -// PinChatMessageParameters represents data for PinChatMessage method. -type PinChatMessageParameters struct { - // Unique identifier for the target chat - ChatID int64 `json:"chat_id"` - - // Identifier of a message to pin - MessageID int `json:"message_id"` - - // Pass true, if it is not necessary to send a notification to all chat - // members about the new pinned message. Notifications are always - // disabled in channels. - DisableNotification bool `json:"disable_notification"` -} - -// PinChatMessage pin a message in a supergroup or a channel. The bot must be an administrator in the -// chat for this to work and must have the 'can_pin_messages' admin right in the supergroup or -// 'can_edit_messages' admin right in the channel. Returns True on success. -func (bot *Bot) PinChatMessage(params *PinChatMessageParameters) (bool, error) { - dst, err := parser.Marshal(params) - if err != nil { - return false, err - } - - resp, err := bot.request(dst, MethodPinChatMessage) - if err != nil { - return false, err - } - - var ok bool - err = parser.Unmarshal(resp.Result, &ok) - return ok, err -} diff --git a/restrict.go b/restrict.go deleted file mode 100644 index 8981cd3..0000000 --- a/restrict.go +++ /dev/null @@ -1,35 +0,0 @@ -package telegram - -type RestrictChatMemberParameters struct { - // Unique identifier for the target chat - ChatID int64 `json:"chat_id"` - - // Unique identifier of the target user - UserID int `json:"user_id"` - - // New user permissions - Permissions ChatPermissions `json:"permissions"` - - // Date when restrictions will be lifted for the user, unix time. If user is restricted for more than 366 days - // or less than 30 seconds from the current time, they are considered to be restricted forever - UntilDate int64 `json:"until_date,omitempty"` -} - -// restrict a user in a supergroup. The bot must be an administrator in the supergroup for this to work and must have -// the appropriate admin rights. Pass True for all permissions to lift restrictions from a user. Returns True on -// success. -func (b *Bot) RestrictChatMember(params RestrictChatMemberParameters) (bool, error) { - dst, err := parser.Marshal(¶ms) - if err != nil { - return false, err - } - - resp, err := b.request(dst, MethodRestrictChatMember) - if err != nil { - return false, err - } - - var ok bool - err = parser.Unmarshal(resp.Result, &ok) - return ok, err -} diff --git a/send.go b/send.go deleted file mode 100644 index 2b9e88a..0000000 --- a/send.go +++ /dev/null @@ -1,796 +0,0 @@ -package telegram - -import ( - "strconv" - - http "github.com/valyala/fasthttp" -) - -type ( - // SendAnimationParameters represents data for SendAnimation method. - SendAnimationParameters struct { - // Unique identifier for the target chat or username of the target channel (in the format @channelusername) - ChatID int64 `json:"chat_id"` - - // Animation to send. Pass a file_id as String to send an animation that exists on the Telegram servers (recommended), pass an HTTP URL as a String for Telegram to get an animation from the Internet, or upload a new animation using multipart/form-data. More info on Sending Files » - Animation InputFile `json:"animation"` - - // Duration of sent animation in seconds - Duration int `json:"duration,omitempty"` - - // Animation width - Width int `json:"width,omitempty"` - - // Animation height - Height int `json:"height,omitempty"` - - // Thumbnail of the file sent. The thumbnail should be in JPEG format and less than 200 kB in size. A thumbnail‘s width and height should not exceed 90. Ignored if the file is not uploaded using multipart/form-data. Thumbnails can’t be reused and can be only uploaded as a new file, so you can pass “attach://” if the thumbnail was uploaded using multipart/form-data under . More info on Sending Files » - Thumb InputFile `json:"thumb,omitempty"` - - // Animation caption (may also be used when resending animation by file_id), 0-200 characters - Caption string `json:"caption,omitempty"` - - // Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. - ParseMode string `json:"parse_mode,omitempty"` - - // Sends the message silently. Users will receive a notification with no sound. - DisableNotification bool `json:"disable_notification,omitempty"` - - // If the message is a reply, ID of the original message - ReplyToMessageID int `json:"reply_to_message_id,omitempty"` - - // Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user. - ReplyMarkup interface{} `json:"reply_markup,omitempty"` - } - - // SendChatActionParameters represents data for SendChatAction method. - SendChatActionParameters struct { - // Unique identifier for the target chat - ChatID int64 `json:"chat_id"` - - // Type of action to broadcast - Action string `json:"action"` - } - - // SendContactParameters represents data for SendContact method. - SendContactParameters struct { - // Unique identifier for the target private chat - ChatID int64 `json:"chat_id"` - - // Contact's phone number - PhoneNumber string `json:"phone_number"` - - // Contact's first name - FirstName string `json:"first_name"` - - // Contact's last name - LastName string `json:"last_name"` - - // Additional data about the contact in the form of a vCard, 0-2048 bytes - VCard string `json:"vcard,omitempty"` - - // Sends the message silently. Users will receive a notification with no - // sound. - DisableNotification bool `json:"disable_notification,omitempty"` - - // If the message is a reply, ID of the original message - ReplyToMessageID int `json:"reply_to_message_id,omitempty"` - - // A JSON-serialized object for an inline keyboard. If empty, one 'Pay total - // price' button will be shown. If not empty, the first button must be a Pay - // button. - ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` - } - - // SendDocumentParameters represents data for SendDocument method. - SendDocumentParameters struct { - // Unique identifier for the target chat - ChatID int64 `json:"chat_id"` - - // File to send. Pass a file_id as String to send a file that exists on the Telegram servers - // (recommended), pass an HTTP URL as a String for Telegram to get a file from the Internet, or - // upload a new one using multipart/form-data. - Document InputFile `json:"document"` - - // Document caption (may also be used when resending documents by file_id), 0-200 characters - Caption string `json:"caption,omitempty"` - - // Send Markdown or HTML, if you want Telegram apps to show bold, italic, - // fixed-width text or inline URLs in the media caption. - ParseMode string `json:"parse_mode,omitempty"` - - // Sends the message silently. Users will receive a notification with no sound. - DisableNotification bool `json:"disable_notification,omitempty"` - - // If the message is a reply, ID of the original message - ReplyToMessageID int `json:"reply_to_message_id,omitempty"` - - // Additional interface options. A JSON-serialized object for an inline keyboard, custom reply - // keyboard, instructions to remove reply keyboard or to force a reply from the user. - ReplyMarkup interface{} `json:"reply_markup,omitempty"` - } - - // SendInvoiceParameters represents data for SendInvoice method. - SendInvoiceParameters struct { - // Unique identifier for the target private chat - ChatID int64 `json:"chat_id"` - - // Product name, 1-32 characters - Title string `json:"title"` - - // Product description, 1-255 characters - Description string `json:"description"` - - // Bot-defined invoice payload, 1-128 bytes. This will not be displayed to - // the user, use for your internal processes. - Payload string `json:"payload"` - - // Payments provider token, obtained via Botfather - ProviderToken string `json:"provider_token"` - - // Unique deep-linking parameter that can be used to generate this invoice - // when used as a start parameter - StartParameter string `json:"start_parameter"` - - // Three-letter ISO 4217 currency code, see more on currencies - Currency string `json:"currency"` - - // JSON-encoded data about the invoice, which will be shared with the payment - // provider. A detailed description of required fields should be provided by - // the payment provider. - ProviderData string `json:"provider_data,omitempty"` - - // URL of the product photo for the invoice. Can be a photo of the goods or a - // marketing image for a service. People like it better when they see what - // they are paying for. - PhotoURL string `json:"photo_url,omitempty"` - - // Price breakdown, a list of components (e.g. product price, tax, discount, - // delivery cost, delivery tax, bonus, etc.) - Prices []LabeledPrice `json:"prices"` - - // Photo size - PhotoSize int `json:"photo_size,omitempty"` - - // Photo width - PhotoWidth int `json:"photo_width,omitempty"` - - // Photo height - PhotoHeight int `json:"photo_height,omitempty"` - - // If the message is a reply, ID of the original message - ReplyToMessageID int `json:"reply_to_message_id,omitempty"` - - // Pass True, if you require the user's full name to complete the order - NeedName bool `json:"need_name,omitempty"` - - // Pass True, if you require the user's phone number to complete the order - NeedPhoneNumber bool `json:"need_phone_number,omitempty"` - - // Pass True, if you require the user's email to complete the order - NeedEmail bool `json:"need_email,omitempty"` - - // Pass True, if you require the user's shipping address to complete the - // order - NeedShippingAddress bool `json:"need_shipping_address,omitempty"` - - // Pass True, if the final price depends on the shipping method - IsFlexible bool `json:"is_flexible,omitempty"` - - // Sends the message silently. Users will receive a notification with no - // sound. - DisableNotification bool `json:"disable_notification,omitempty"` - - // A JSON-serialized object for an inline keyboard. If empty, one 'Pay total - // price' button will be shown. If not empty, the first button must be a Pay - // button. - ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` - } - - // SendLocationParameters represents data for SendLocation method. - SendLocationParameters struct { - // Unique identifier for the target private chat - ChatID int64 `json:"chat_id"` - - // Latitude of the location - Latitude float32 `json:"latitude"` - - // Longitude of the location - Longitude float32 `json:"longitude"` - - // Period in seconds for which the location will be updated (see Live - // Locations), should be between 60 and 86400. - LivePeriod int `json:"live_period,omitempty"` - - // If the message is a reply, ID of the original message - ReplyToMessageID int `json:"reply_to_message_id,omitempty"` - - // Sends the message silently. Users will receive a notification with no - // sound. - DisableNotification bool `json:"disable_notification,omitempty"` - - // A JSON-serialized object for an inline keyboard. If empty, one 'Pay total - // price' button will be shown. If not empty, the first button must be a Pay - // button. - ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` - } - - // SendMediaGroupParameters represents data for SendMediaGroup method. - SendMediaGroupParameters struct { - // Unique identifier for the target chat. - ChatID int64 `json:"chat_id"` - - // A JSON-serialized array describing photos and videos to be sent, must - // include 2–10 items - Media []interface{} `json:"media"` - - // Sends the messages silently. Users will receive a notification with no - // sound. - DisableNotification bool `json:"disable_notification,omitempty"` - - // If the messages are a reply, ID of the original message - ReplyToMessageID int `json:"reply_to_message_id,omitempty"` - } - - // SendMessageParameters represents data for SendMessage method. - SendMessageParameters struct { - // Unique identifier for the target chat or username of the target channel - // (in the format @channelusername) - ChatID int64 `json:"chat_id"` - - // Text of the message to be sent - Text string `json:"text"` - - // 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"` - - // Disables link previews for links in this message - DisableWebPagePreview bool `json:"disable_web_page_preview,omitempty"` - - // Sends the message silently. Users will receive a notification with no - // sound. - DisableNotification bool `json:"disable_notification,omitempty"` - - // If the message is a reply, ID of the original message - ReplyToMessageID int `json:"reply_to_message_id,omitempty"` - - // Additional interface options. A JSON-serialized object for an inline - // keyboard, custom reply keyboard, instructions to remove reply keyboard or - // to force a reply from the user. - ReplyMarkup interface{} `json:"reply_markup,omitempty"` - } - - // SendPhotoParameters represents data for SendPhoto method. - SendPhotoParameters struct { - // Unique identifier for the target chat - ChatID int64 `json:"chat_id"` - - // Photo to send. Pass a file_id as String to send a photo that exists on the - // Telegram servers (recommended), pass an HTTP URL as a String for Telegram - // to get a photo from the Internet, or upload a new photo using - // multipart/form-data. - Photo InputFile `json:"photo"` - - // Photo caption (may also be used when resending photos by file_id), 0-200 - // characters - Caption string `json:"caption,omitempty"` - - // Send Markdown or HTML, if you want Telegram apps to show bold, italic, - // fixed-width text or inline URLs in the media caption. - ParseMode string `json:"parse_mode,omitempty"` - - // Disables link previews for links in this message - DisableWebPagePreview bool `json:"disable_web_page_preview,omitempty"` - - // Sends the message silently. Users will receive a notification with no - // sound. - DisableNotification bool `json:"disable_notification,omitempty"` - - // If the message is a reply, ID of the original message - ReplyToMessageID int `json:"reply_to_message_id,omitempty"` - - // Additional interface options. A JSON-serialized object for an inline - // keyboard, custom reply keyboard, instructions to remove reply keyboard or - // to force a reply from the user. - ReplyMarkup interface{} `json:"reply_markup,omitempty"` - } - - SendPollConfig struct { - // Unique identifier for the target chat. A native poll can't be sent to a private chat. - ChatID int64 `json:"chat_id"` - - // Poll question, 1-255 characters - Question string `json:"question"` - - // List of answer options, 2-10 strings 1-100 characters each - Options []string `json:"options"` - - // Sends the message silently. Users will receive a notification with no sound. - DisableNotification bool `json:"disable_notification,omitempty"` - - // If the message is a reply, ID of the original message - ReplyToMessageID int `json:"reply_to_message_id,omitempty"` - - // Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, - // instructions to remove reply keyboard or to force a reply from the user. - ReplyMarkup interface{} `json:"reply_markup,omitempty"` - } - - // SendVenueParameters represents data for SendVenue method. - SendVenueParameters struct { - // Unique identifier for the target private chat - ChatID int64 `json:"chat_id"` - - // Latitude of the venue - Latitude float32 `json:"latitude"` - - // Longitude of the venue - Longitude float32 `json:"longitude"` - - // Name of the venue - Title string `json:"title"` - - // Address of the venue - Address string `json:"address"` - - // Foursquare identifier of the venue - FoursquareID string `json:"foursquare_id,omitempty"` - - // Foursquare type of the venue, if known. (For example, - // "arts_entertainment/default", "arts_entertainment/aquarium" or - // "food/icecream".) - FoursquareType string `json:"foursquare_type,omitempty"` - - // Sends the message silently. Users will receive a notification with no - // sound. - DisableNotification bool `json:"disable_notification,omitempty"` - - // If the message is a reply, ID of the original message - ReplyToMessageID int `json:"reply_to_message_id,omitempty"` - - // A JSON-serialized object for an inline keyboard. If empty, one 'Pay total - // price' button will be shown. If not empty, the first button must be a Pay - // button. - ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` - } - - // SendGameParameters represents data for SendGame method. - SendGameParameters struct { - // Unique identifier for the target chat - ChatID int64 `json:"chat_id"` - - // Short name of the game, serves as the unique identifier for the game. Set - // up your games via Botfather. - GameShortName string `json:"game_short_name"` - - // Sends the message silently. Users will receive a notification with no - // sound. - DisableNotification bool `json:"disable_notification,omitempty"` - - // If the message is a reply, ID of the original message - ReplyToMessageID int `json:"reply_to_message_id,omitempty"` - - // A JSON-serialized object for an inline keyboard. If empty, one ‘Play - // game_title’ button will be shown. If not empty, the first button must - // launch the game. - ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` - } - - // SendStickerParameters represents data for SetSticker method. - SendStickerParameters struct { - // Unique identifier for the target chat - ChatID int64 `json:"chat_id"` - - // Sticker to send - Sticker interface{} `json:"sticker"` - - // Sends the message silently. Users will receive a notification - // with no sound - DisableNotification bool `json:"disable_notification,omitempty"` - - // If the message is a reply, ID of the original message - ReplyToMessageID int `json:"reply_to_message_id,omitempty"` - - // Additional interface options. A JSON-serialized object for an - // inline keyboard, custom reply keyboard, instructions to remove - // reply keyboard or to force a reply from the user. - ReplyMarkup interface{} `json:"reply_markup,omitempty"` - } -) - -// NewAnimation creates SendAnimationParameters only with required parameters. -func NewAnimation(chatID int64, animation interface{}) *SendAnimationParameters { - return &SendAnimationParameters{ - ChatID: chatID, - Animation: animation, - } -} - -// NewContact creates SendContactParameters only with required parameters. -func NewContact(chatID int64, phoneNumber, firstName string) *SendContactParameters { - return &SendContactParameters{ - ChatID: chatID, - PhoneNumber: phoneNumber, - FirstName: firstName, - } -} - -// NewDocument creates SendDocumentParameters only with required parameters. -func NewDocument(chatID int64, document interface{}) *SendDocumentParameters { - return &SendDocumentParameters{ - ChatID: chatID, - Document: document, - } -} - -// NewInvoice creates SendInvoiceParameters only with required parameters. -func NewInvoice(chatID int64, title, description, payload, providerToken, startParameter, currency string, prices ...LabeledPrice) *SendInvoiceParameters { - return &SendInvoiceParameters{ - ChatID: chatID, - Title: title, - Description: description, - Payload: payload, - ProviderToken: providerToken, - StartParameter: startParameter, - Currency: currency, - Prices: prices, - } -} - -// NewLocation creates SendLocationParameters only with required parameters. -func NewLocation(chatID int64, latitude, longitude float32) *SendLocationParameters { - return &SendLocationParameters{ - ChatID: chatID, - Latitude: latitude, - Longitude: longitude, - } -} - -// NewMediaGroup creates SendMediaGroupParameters only with required parameters. -func NewMediaGroup(chatID int64, media ...interface{}) *SendMediaGroupParameters { - return &SendMediaGroupParameters{ - ChatID: chatID, - Media: media, - } -} - -// NewMessage creates SendMessageParameters only with required parameters. -func NewMessage(chatID int64, text string) *SendMessageParameters { - return &SendMessageParameters{ - ChatID: chatID, - Text: text, - } -} - -// NewPhoto creates SendPhotoParameters only with required parameters. -func NewPhoto(chatID int64, photo interface{}) *SendPhotoParameters { - return &SendPhotoParameters{ - ChatID: chatID, - Photo: photo, - } -} - -func NewPoll(chatID int64, question string, options ...string) SendPollConfig { - return SendPollConfig{ - ChatID: chatID, - Question: question, - Options: options, - } -} - -// NewVenue creates SendVenueParameters only with required parameters. -func NewVenue(chatID int64, latitude, longitude float32, title, address string) *SendVenueParameters { - return &SendVenueParameters{ - ChatID: chatID, - Latitude: latitude, - Longitude: longitude, - Title: title, - Address: address, - } -} - -// NewGame creates SendGameParameters only with required parameters. -func NewGame(chatID int64, gameShortName string) *SendGameParameters { - return &SendGameParameters{ - ChatID: chatID, - GameShortName: gameShortName, - } -} - -// SendAnimation send animation files (GIF or H.264/MPEG-4 AVC video without -// sound). On success, the sent Message is returned. Bots can currently send -// animation files of up to 50 MB in size, this limit may be changed in the -// future. -func (bot *Bot) SendAnimation(params *SendAnimationParameters) (*Message, error) { - args := http.AcquireArgs() - defer http.ReleaseArgs(args) - args.Add("chat_id", strconv.FormatInt(params.ChatID, 10)) - - if params.Caption != "" { - args.Add("caption", params.Caption) - } - - if params.ReplyMarkup != nil { - dst, err := parser.Marshal(params.ReplyMarkup) - if err != nil { - return nil, err - } - args.Add("reply_markup", string(dst)) - } - - if params.ReplyToMessageID != 0 { - args.Add("reply_to_message_id", strconv.Itoa(params.ReplyToMessageID)) - } - - args.Add("disable_notification", strconv.FormatBool(params.DisableNotification)) - - resp, err := bot.Upload(MethodSendAnimation, "animation", "", params.Animation, args) - if err != nil { - return nil, err - } - - var result Message - err = parser.Unmarshal(resp.Result, &result) - return &result, err -} - -// SendChatAction tell the user that something is happening on the bot's side. -// The status is set for 5 seconds or less (when a message arrives from your bot, -// Telegram clients clear its typing status). Returns True on success. -// -// We only recommend using this method when a response from the bot will take a -// noticeable amount of time to arrive. -func (bot *Bot) SendChatAction(chatID int64, action string) (bool, error) { - dst, err := parser.Marshal(&SendChatActionParameters{ - ChatID: chatID, - Action: action, - }) - if err != nil { - return false, err - } - - resp, err := bot.request(dst, MethodSendChatAction) - if err != nil { - return false, err - } - - var ok bool - err = parser.Unmarshal(resp.Result, &ok) - return ok, err -} - -// SendContact send phone contacts. On success, the sent Message is returned. -func (bot *Bot) SendContact(params *SendContactParameters) (*Message, error) { - dst, err := parser.Marshal(*params) - if err != nil { - return nil, err - } - - resp, err := bot.request(dst, MethodSendContact) - if err != nil { - return nil, err - } - - var msg Message - err = parser.Unmarshal(resp.Result, &msg) - return &msg, err -} - -// SendDocument send general files. On success, the sent Message is returned. Bots can currently send -// files of any type of up to 50 MB in size, this limit may be changed in the future. -func (bot *Bot) SendDocument(params *SendDocumentParameters) (*Message, error) { - args := http.AcquireArgs() - defer http.ReleaseArgs(args) - args.Add("chat_id", strconv.FormatInt(params.ChatID, 10)) - - if params.Caption != "" { - args.Add("caption", params.Caption) - } - - if params.ReplyMarkup != nil { - dst, err := parser.Marshal(params.ReplyMarkup) - if err != nil { - return nil, err - } - args.Add("reply_markup", string(dst)) - } - - if params.ReplyToMessageID != 0 { - args.Add("reply_to_message_id", strconv.Itoa(params.ReplyToMessageID)) - } - - args.Add("disable_notification", strconv.FormatBool(params.DisableNotification)) - - resp, err := bot.Upload(MethodSendDocument, "document", "", params.Document, args) - if err != nil { - return nil, err - } - - var result Message - err = parser.Unmarshal(resp.Result, &result) - return &result, err -} - -// SendInvoice send invoices. On success, the sent Message is returned. -func (bot *Bot) SendInvoice(params *SendInvoiceParameters) (*Message, error) { - dst, err := parser.Marshal(params) - if err != nil { - return nil, err - } - - resp, err := bot.request(dst, MethodSendInvoice) - if err != nil { - return nil, err - } - - var msg Message - err = parser.Unmarshal(resp.Result, &msg) - return &msg, err -} - -// SendLocation send point on the map. On success, the sent Message is returned. -func (bot *Bot) SendLocation(params *SendLocationParameters) (*Message, error) { - dst, err := parser.Marshal(params) - if err != nil { - return nil, err - } - - resp, err := bot.request(dst, MethodSendLocation) - if err != nil { - return nil, err - } - - var msg Message - err = parser.Unmarshal(resp.Result, &msg) - return &msg, err -} - -// SendMediaGroup send a group of photos or videos as an album. On success, an array of the sent -// Messages is returned. -func (bot *Bot) SendMediaGroup(params *SendMediaGroupParameters) ([]Message, error) { - dst, err := parser.Marshal(params) - if err != nil { - return nil, err - } - - resp, err := bot.request(dst, MethodSendMediaGroup) - if err != nil { - return nil, err - } - - var group []Message - err = parser.Unmarshal(resp.Result, &group) - return group, err -} - -// SendMessage send text messages. On success, the sent Message is returned. -func (bot *Bot) SendMessage(params *SendMessageParameters) (*Message, error) { - dst, err := parser.Marshal(params) - if err != nil { - return nil, err - } - - resp, err := bot.request(dst, MethodSendMessage) - if err != nil { - return nil, err - } - - var msg Message - err = parser.Unmarshal(resp.Result, &msg) - return &msg, err -} - -// SendPhoto send photos. On success, the sent Message is returned. -func (bot *Bot) SendPhoto(params *SendPhotoParameters) (*Message, error) { - args := http.AcquireArgs() - defer http.ReleaseArgs(args) - args.Add("chat_id", strconv.FormatInt(params.ChatID, 10)) - - if params.Caption != "" { - args.Add("caption", params.Caption) - } - - if params.ReplyMarkup != nil { - dst, err := parser.Marshal(params.ReplyMarkup) - if err != nil { - return nil, err - } - args.Add("reply_markup", string(dst)) - } - - if params.ReplyToMessageID != 0 { - args.Add("reply_to_message_id", strconv.Itoa(params.ReplyToMessageID)) - } - - args.Add("disable_notification", strconv.FormatBool(params.DisableNotification)) - - resp, err := bot.Upload(MethodSendPhoto, "photo", "", params.Photo, args) - if err != nil { - return nil, err - } - - var result Message - err = parser.Unmarshal(resp.Result, &result) - return &result, err -} - -// SendPoll send a native poll. A native poll can't be sent to a private chat. On success, the sent Message is -// returned. -func (b *Bot) SendPoll(params SendPollConfig) (*Message, error) { - dst, err := parser.Marshal(params) - if err != nil { - return nil, err - } - - resp, err := b.request(dst, MethodSendPoll) - if err != nil { - return nil, err - } - - var msg Message - err = parser.Unmarshal(resp.Result, &msg) - return &msg, err -} - -// SendVenue send information about a venue. On success, the sent Message is returned. -func (bot *Bot) SendVenue(params *SendVenueParameters) (*Message, error) { - dst, err := parser.Marshal(params) - if err != nil { - return nil, err - } - - resp, err := bot.request(dst, MethodSendVenue) - if err != nil { - return nil, err - } - - var msg Message - err = parser.Unmarshal(resp.Result, &msg) - return &msg, err -} - -// SendGame send a game. On success, the sent Message is returned. -func (bot *Bot) SendGame(params *SendGameParameters) (*Message, error) { - dst, err := parser.Marshal(params) - if err != nil { - return nil, err - } - - resp, err := bot.request(dst, MethodSendGame) - if err != nil { - return nil, err - } - - var msg Message - err = parser.Unmarshal(resp.Result, &msg) - return &msg, err -} - -// SendSticker send .webp stickers. On success, the sent Message is returned. -func (b *Bot) SendSticker(params *SendStickerParameters) (*Message, error) { - args := http.AcquireArgs() - defer http.ReleaseArgs(args) - args.Set("chat_id", strconv.FormatInt(params.ChatID, 10)) - args.Set("disable_notification", strconv.FormatBool(params.DisableNotification)) - if params.ReplyToMessageID > 0 { - args.SetUint("reply_to_message_id", params.ReplyToMessageID) - } - if params.ReplyMarkup != nil { - rm, err := parser.Marshal(params.ReplyMarkup) - if err != nil { - return nil, err - } - - args.SetBytesV("reply_markup", rm) - } - - resp, err := b.Upload(MethodSendSticker, TypeSticker, "sticker", params.Sticker, args) - if err != nil { - return nil, err - } - - var result Message - err = parser.Unmarshal(resp.Result, &result) - return &result, err -} diff --git a/set.go b/set.go deleted file mode 100644 index 0ec8d0a..0000000 --- a/set.go +++ /dev/null @@ -1,398 +0,0 @@ -package telegram - -import ( - "strconv" - "strings" - - http "github.com/valyala/fasthttp" -) - -type ( - // SetChatDescriptionParameters represents data for SetChatDescription method. - SetChatDescriptionParameters struct { - // Unique identifier for the target chat - ChatID int64 `json:"chat_id"` - - // New chat description, 0-255 characters - Description string `json:"description"` - } - - // SetChatPhotoParameters represents data for SetChatPhoto method. - SetChatPhotoParameters struct { - // Unique identifier for the target chat - ChatID int64 `json:"chat_id"` - - // New chat photo, uploaded using multipart/form-data - ChatPhoto interface{} `json:"chat_photo"` - } - - // SetChatStickerSetParameters represents data for SetChatStickerSet method. - SetChatStickerSetParameters struct { - // Unique identifier for the target chat - ChatID int64 `json:"chat_id"` - - // Name of the sticker set to be set as the group sticker set - StickerSetName string `json:"sticker_set_name"` - } - - // SetChatTitleParameters represents data for SetChatTitle method. - SetChatTitleParameters struct { - // Unique identifier for the target chat - ChatID int64 `json:"chat_id"` - - // New chat title, 1-255 characters - Title string `json:"title"` - } - - SetPassportDataErrorsParameters struct { - // User identifier - UserID int `json:"user_id"` - - // A JSON-serialized array describing the errors - Errors []PassportElementError `json:"errors"` - } - - // SetWebhookParameters represents data for SetWebhook method. - SetWebhookParameters struct { - // HTTPS url to send updates to. Use an empty string to remove webhook - // integration - URL string `json:"url"` - - // Upload your public key certificate so that the root certificate in use can - // be checked. See our self-signed guide for details. - Certificate InputFile `json:"certificate,omitempty"` - - // Maximum allowed number of simultaneous HTTPS connections to the webhook - // for update delivery, 1-100. Defaults to 40. Use lower values to limit the - // load on your bot‘s server, and higher values to increase your bot’s - // throughput. - MaxConnections int `json:"max_connections,omitempty"` - - // List the types of updates you want your bot to receive. For example, - // specify [“message”, “edited_channel_post”, “callback_query”] to only - // receive updates of these types. See Update for a complete list of - // available update types. Specify an empty list to receive all updates - // regardless of type (default). If not specified, the previous setting will - // be used. - // - // Please note that this parameter doesn't affect updates created before the - // call to the setWebhook, so unwanted updates may be received for a short - // period of time. - AllowedUpdates []string `json:"allowed_updates,omitempty"` - } - - // SetGameScoreParameters represents data for SetGameScore method. - SetGameScoreParameters struct { - // User identifier - UserID int `json:"user_id"` - - // New score, must be non-negative - Score int `json:"score"` - - // Required if inline_message_id is not specified. Identifier of the sent - // message - MessageID int `json:"message_id,omitempty"` - - // Pass True, if the high score is allowed to decrease. This can be useful - // when fixing mistakes or banning cheaters - Force bool `json:"force,omitempty"` - - // Pass True, if the game message should not be automatically edited to - // include the current scoreboard - DisableEditMessage bool `json:"disable_edit_message,omitempty"` - - // Required if inline_message_id is not specified. Unique identifier for the - // target chat - ChatID int64 `json:"chat_id,omitempty"` - - // Required if chat_id and message_id are not specified. Identifier of the - // inline message - InlineMessageID string `json:"inline_message_id,omitempty"` - } - - // SetStickerPositionInSetParameters represents data for SetStickerPositionInSet - // method. - SetStickerPositionInSetParameters struct { - // File identifier of the sticker - Sticker string `json:"sticker"` - - // New sticker position in the set, zero-based - Position int `json:"position"` - } - - // SetChatPermissionsParameters represents data for SetChatPermissions method. - SetChatPermissionsParameters struct { - // Unique identifier for the target chat - ChatID int64 `json:"chat_id"` - - // New default chat permissions - Permissions ChatPermissions `json:"permissions"` - } - - SetChatAdministratorCustomTitle struct { - // Unique identifier for the target chat - ChatID int64 `json:"chat_id"` - - // Unique identifier of the target user - UserID int `json:"user_id"` - - // New custom title for the administrator; 0-16 characters, emoji are not allowed - CustomTitle string `json:"custom_title"` - } -) - -// NewWebhook creates new SetWebhookParameters only with required parameters. -func NewWebhook(url string, file interface{}) *SetWebhookParameters { - return &SetWebhookParameters{ - URL: url, - Certificate: file, - } -} - -// NewGameScore creates SetGameScoreParameters only with required parameters. -func NewGameScore(userID, score int) *SetGameScoreParameters { - return &SetGameScoreParameters{ - UserID: userID, - Score: score, - } -} - -// SetChatDescription change the description of a supergroup or a channel. The -// bot must be an administrator in the chat for this to work and must have the -// appropriate admin rights. Returns True on success. -func (bot *Bot) SetChatDescription(chatID int64, description string) (bool, error) { - dst, err := parser.Marshal(&SetChatDescriptionParameters{ - ChatID: chatID, - Description: description, - }) - if err != nil { - return false, err - } - - resp, err := bot.request(dst, MethodSetChatDescription) - if err != nil { - return false, err - } - - var ok bool - err = parser.Unmarshal(resp.Result, &ok) - return ok, err -} - -// SetChatPhoto set a new profile photo for the chat. Photos can't be changed for private chats. The -// bot must be an administrator in the chat for this to work and must have the appropriate admin -// rights. Returns True on success. -// -// Note: In regular groups (non-supergroups), this method will only work if the 'All Members Are -// Admins' setting is off in the target group. -func (bot *Bot) SetChatPhoto(chatID int64, chatPhoto interface{}) (bool, error) { - args := http.AcquireArgs() - defer http.ReleaseArgs(args) - args.Add("chat_id", strconv.FormatInt(chatID, 10)) - - resp, err := bot.Upload(MethodSetChatPhoto, TypePhoto, "chat_photo", chatPhoto, args) - if err != nil { - return false, err - } - - var ok bool - err = parser.Unmarshal(resp.Result, &ok) - return ok, err -} - -// SetChatStickerSet set a new group sticker set for a supergroup. The bot must be an administrator -// in the chat for this to work and must have the appropriate admin rights. Use the field -// can_set_sticker_set optionally returned in getChat requests to check if the bot can use this -// method. Returns True on success. -func (bot *Bot) SetChatStickerSet(chatID int64, stickerSetName string) (bool, error) { - dst, err := parser.Marshal(&SetChatStickerSetParameters{ - ChatID: chatID, - StickerSetName: stickerSetName, - }) - if err != nil { - return false, err - } - - resp, err := bot.request(dst, MethodSetChatStickerSet) - if err != nil { - return false, err - } - - var ok bool - err = parser.Unmarshal(resp.Result, &ok) - return ok, err -} - -// SetChatTitle change the title of a chat. Titles can't be changed for private -// chats. The bot must be an administrator in the chat for this to work and must -// have the appropriate admin rights. Returns True on success. -// -// Note: In regular groups (non-supergroups), this method will only work if the -// 'All Members Are Admins' setting is off in the target group. -func (bot *Bot) SetChatTitle(chatID int64, title string) (bool, error) { - dst, err := parser.Marshal(&SetChatTitleParameters{ - ChatID: chatID, - Title: title, - }) - if err != nil { - return false, err - } - - resp, err := bot.request(dst, MethodSetChatTitle) - if err != nil { - return false, err - } - - var ok bool - err = parser.Unmarshal(resp.Result, &ok) - return ok, err -} - -// SetPassportDataErrors 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). Returns True on success. -// -// Use this if the data submitted by the user doesn't satisfy the standards your -// service requires for any reason. For example, if a birthday date seems -// invalid, a submitted document is blurry, a scan shows evidence of tampering, -// etc. Supply some details in the error message to make sure the user knows how -// to correct the issues. -func (b *Bot) SetPassportDataErrors(userId int, errors []PassportElementError) (bool, error) { - dst, err := parser.Marshal(&SetPassportDataErrorsParameters{ - UserID: userId, - Errors: errors, - }) - if err != nil { - return false, err - } - - resp, err := b.request(dst, MethodSetPassportDataErrors) - if err != nil { - return false, err - } - - var ok bool - err = parser.Unmarshal(resp.Result, &ok) - return ok, err -} - -// SetWebhook specify a url and receive incoming updates via an outgoing webhook. -// Whenever there is an update for the bot, we will send an HTTPS POST request to -// the specified url, containing a JSON-serialized Update. In case of an -// unsuccessful request, we will give up after a reasonable amount of attempts. -// Returns true. -// -// If you'd like to make sure that the Webhook request comes from Telegram, we -// recommend using a secret path in the URL, e.g. https://www.example.com/. -// Since nobody else knows your bot‘s token, you can be pretty sure it’s us. -func (bot *Bot) SetWebhook(params *SetWebhookParameters) (bool, error) { - args := http.AcquireArgs() - defer http.ReleaseArgs(args) - args.Add("url", params.URL) - - if len(params.AllowedUpdates) > 0 { - args.Add("allowed_updates", strings.Join(params.AllowedUpdates, ",")) - } - if params.MaxConnections > 0 && - params.MaxConnections <= 100 { - args.Add("max_connections", strconv.Itoa(params.MaxConnections)) - } - - var resp *Response - var err error - if params.Certificate != nil { - resp, err = bot.Upload(MethodSetWebhook, "certificate", "cert.pem", params.Certificate, args) - } else { - var dst []byte - dst, err = parser.Marshal(params) - if err != nil { - return false, err - } - - resp, err = bot.request(dst, MethodSetWebhook) - } - if err != nil { - return false, err - } - - var ok bool - err = parser.Unmarshal(resp.Result, &ok) - return ok, err -} - -// SetGameScore set the score of the specified user in a game. On success, if the -// message was sent by the bot, returns the edited Message, otherwise returns -// True. Returns an error, if the new score is not greater than the user's -// current score in the chat and force is False. -func (bot *Bot) SetGameScore(params *SetGameScoreParameters) (*Message, error) { - dst, err := parser.Marshal(params) - if err != nil { - return nil, err - } - - resp, err := bot.request(dst, MethodSetGameScore) - if err != nil { - return nil, err - } - - var msg Message - err = parser.Unmarshal(resp.Result, &msg) - return &msg, err -} - -// SetStickerPositionInSet move a sticker in a set created by the bot to a -// specific position. Returns True on success. -func (b *Bot) SetStickerPositionInSet(sticker string, position int) (bool, error) { - dst, err := parser.Marshal(&SetStickerPositionInSetParameters{ - Sticker: sticker, - Position: position, - }) - if err != nil { - return false, err - } - - resp, err := b.request(dst, MethodSetStickerPositionInSet) - if err != nil { - return false, err - } - - var ok bool - err = parser.Unmarshal(resp.Result, &ok) - return ok, err -} - -// SetChatPermissions set default chat permissions for all members. The bot must be an administrator in the group or a -// supergroup for this to work and must have the can_restrict_members admin rights. Returns True on success. -func (b *Bot) SetChatPermissions(params SetChatPermissionsParameters) (bool, error) { - dst, err := parser.Marshal(¶ms) - if err != nil { - return false, err - } - - resp, err := b.request(dst, MethodSetChatPermissions) - if err != nil { - return false, err - } - - var ok bool - err = parser.Unmarshal(resp.Result, &ok) - return ok, err -} - -// SetChatAdministratorCustomTitle method to set a custom title for an administrator in a supergroup promoted by the bot. Returns True on success. -func (b *Bot) SetChatAdministratorCustomTitle(params SetChatAdministratorCustomTitle) (bool, error) { - dst, err := parser.Marshal(¶ms) - if err != nil { - return false, err - } - - resp, err := b.request(dst, MethodSetChatAdministratorCustomTitle) - if err != nil { - return false, err - } - - var ok bool - err = parser.Unmarshal(resp.Result, &ok) - return ok, err -} diff --git a/stickers.go b/stickers.go new file mode 100644 index 0000000..033205c --- /dev/null +++ b/stickers.go @@ -0,0 +1,376 @@ +package telegram + +import "strconv" + +type ( + // Sticker represents a sticker. + Sticker struct { + // Identifier for this file, which can be used to download or reuse the file + FileID string `json:"file_id"` + + // 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"` + + // Sticker width + Width int `json:"width"` + + // Sticker height + Height int `json:"height"` + + // true, if the sticker is animated + IsAnimated bool `json:"is_animated"` + + // Sticker thumbnail in the .webp or .jpg format + Thumb *PhotoSize `json:"thumb,omitempty"` + + // Emoji associated with the sticker + Emoji string `json:"emoji,omitempty"` + + // Name of the sticker set to which the sticker belongs + SetName string `json:"set_name,omitempty"` + + // For mask stickers, the position where the mask should be placed + MaskPosition *MaskPosition `json:"mask_position,omitempty"` + + // File size + FileSize int `json:"file_size,omitempty"` + } + + // StickerSet represents a sticker set. + StickerSet struct { + // Sticker set name + Name string `json:"name"` + + // Sticker set title + Title string `json:"title"` + + // List of all set stickers + Stickers []Sticker `json:"stickers"` + + // True, if the sticker set contains masks + ContainsMasks bool `json:"contains_masks"` + + // true, if the sticker set contains animated stickers + IsAnimated bool `json:"is_animated"` + } + + // MaskPosition describes the position on faces where a mask should be placed by default. + MaskPosition struct { + // The part of the face relative to which the mask should be placed. One of "forehead", "eyes", "mouth", or "chin". + Point string `json:"point"` + + // Shift by X-axis measured in widths of the mask scaled to the face size, from left to right. For example, choosing -1.0 will place mask just to the left of the default mask position. + XShift float32 `json:"x_shift"` + + // Shift by Y-axis measured in heights of the mask scaled to the face size, from top to bottom. For example, 1.0 will place the mask just below the default mask position. + YShift float32 `json:"y_shift"` + + // Mask scaling coefficient. For example, 2.0 means double size. + Scale float32 `json:"scale"` + } + + // SendStickerParameters represents data for SetSticker method. + SendSticker struct { + // Unique identifier for the target chat + ChatID int64 `json:"chat_id"` + + // Sticker to send + Sticker InputFile `json:"sticker"` + + // Sends the message silently. Users will receive a notification with no sound + DisableNotification bool `json:"disable_notification,omitempty"` + + // If the message is a reply, ID of the original message + ReplyToMessageID int `json:"reply_to_message_id,omitempty"` + + // Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user. + ReplyMarkup ReplyMarkup `json:"reply_markup,omitempty"` + } + + // GetStickerSetParameters represents data for GetStickerSet method. + GetStickerSet struct { + // Name of the sticker set + Name string `json:"name"` + } + + UploadStickerFile struct { + // User identifier of sticker file owner + UserID int `json:"user_id"` + + // Png image with the sticker, must be up to 512 kilobytes in size, + // dimensions must not exceed 512px, and either width or height + // must be exactly 512px. + PNGSticker *InputFile `json:"png_sticker"` + } + + CreateNewStickerSet struct { + // User identifier of created sticker set owner + UserID int `json:"user_id"` + + // Short name of sticker set, to be used in t.me/addstickers/ URLs + // (e.g., animals). Can contain only english letters, digits and + // underscores. Must begin with a letter, can't contain consecutive + // underscores and must end in “_by_”. + // is case insensitive. 1-64 characters. + Name string `json:"name"` + + // Sticker set title, 1-64 characters + Title string `json:"title"` + + // Png image with the sticker, must be up to 512 kilobytes in size, + // dimensions must not exceed 512px, and either width or height must + // be exactly 512px. Pass a file_id as a String to send a file that + // already exists on the Telegram servers, pass an HTTP URL as a + // String for Telegram to get a file from the Internet, or upload + // a new one using multipart/form-data. + PNGSticker *InputFile `json:"png_sticker"` + + // One or more emoji corresponding to the sticker + Emojis string `json:"emojis"` + + // Pass True, if a set of mask stickers should be created + ContainsMasks bool `json:"contains_masks,omitempty"` + + // A JSON-serialized object for position where the mask should be + // placed on faces + MaskPosition *MaskPosition `json:"mask_position,omitempty"` + } + + AddStickerToSet struct { + // User identifier of sticker set owner + UserID int `json:"user_id"` + + // Sticker set name + Name string `json:"name"` + + // Png image with the sticker, must be up to 512 kilobytes in size, dimensions must not exceed 512px, and either width or height must be exactly 512px. Pass a file_id as a String to send a file that already exists on the Telegram servers, pass an HTTP URL as a String for Telegram to get a file from the Internet, or upload a new one using multipart/form-data. More info on Sending Files » + PNGSticker *InputFile `json:"png_sticker"` + + // One or more emoji corresponding to the sticker + Emojis string `json:"emojis"` + + // A JSON-serialized object for position where the mask should be placed on faces + MaskPosition *MaskPosition `json:"mask_position,omitempty"` + } + + // SetStickerPositionInSetParameters represents data for SetStickerPositionInSet method. + SetStickerPositionInSet struct { + // File identifier of the sticker + Sticker string `json:"sticker"` + + // New sticker position in the set, zero-based + Position int `json:"position"` + } + + // DeleteStickerFromSetParameters represents data for DeleteStickerFromSet method. + DeleteStickerFromSet struct { + // File identifier of the sticker + Sticker string `json:"sticker"` + } +) + +// SendSticker send .webp stickers. On success, the sent Message is returned. +func (b *Bot) SendSticker(p SendSticker) (*Message, error) { + src, err := b.Do(MethodSendSticker, p) + if err != nil { + return nil, err + } + + resp := new(Response) + if err = b.marshler.Unmarshal(src, resp); err != nil { + return nil, err + } + + result := new(Message) + if err = b.marshler.Unmarshal(resp.Result, result); err != nil { + return nil, err + } + + return result, nil +} + +// GetStickerSet get a sticker set. On success, a StickerSet object is returned. +func (b *Bot) GetStickerSet(name string) (*StickerSet, error) { + src, err := b.Do(MethodGetStickerSet, GetStickerSet{Name: name}) + if err != nil { + return nil, err + } + + resp := new(Response) + if err = b.marshler.Unmarshal(src, resp); err != nil { + return nil, err + } + + result := new(StickerSet) + if err = b.marshler.Unmarshal(resp.Result, result); err != nil { + return nil, err + } + + return result, nil +} + +// UploadStickerFile upload a .png file with a sticker for later use in createNewStickerSet and addStickerToSet methods (can be used multiple times). Returns the uploaded File on success. +func (b *Bot) UploadStickerFile(userID int, pngSticker *InputFile) (*File, error) { + params := make(map[string]string) + params["user_id"] = strconv.Itoa(userID) + + var err error + if params["png_sticker"], err = b.marshler.MarshalToString(pngSticker); err != nil { + return nil, err + } + + src, err := b.Upload(MethodUploadStickerFile, params, pngSticker) + if err != nil { + return nil, err + } + + resp := new(Response) + if err = b.marshler.Unmarshal(src, resp); err != nil { + return nil, err + } + + result := new(File) + if err = b.marshler.Unmarshal(resp.Result, result); err != nil { + return nil, err + } + + return result, nil +} + +// CreateNewStickerSet create new sticker set owned by a user. The bot will be able to edit the created sticker set. Returns True on success. +func (b *Bot) CreateNewStickerSet(p CreateNewStickerSet) (bool, error) { + params := make(map[string]string) + params["user_id"] = strconv.Itoa(p.UserID) + params["name"] = p.Name + params["title"] = p.Title + params["emojis"] = p.Emojis + params["contains_masks"] = strconv.FormatBool(p.ContainsMasks) + + var err error + if params["png_sticker"], err = b.marshler.MarshalToString(p.PNGSticker); err != nil { + return false, err + } + + if params["mask_position"], err = b.marshler.MarshalToString(p.MaskPosition); err != nil { + return false, err + } + + files := make([]*InputFile, 0) + if p.PNGSticker.IsAttachment() { + files = append(files, p.PNGSticker) + } + + src, err := b.Upload(MethodCreateNewStickerSet, params, files...) + if err != nil { + return false, err + } + + resp := new(Response) + if err = b.marshler.Unmarshal(src, resp); err != nil { + return false, err + } + + var result bool + if err = b.marshler.Unmarshal(resp.Result, &result); err != nil { + return false, err + } + + return result, nil +} + +// AddStickerToSet add a new sticker to a set created by the b. Returns True on success. +func (b *Bot) AddStickerToSet(p AddStickerToSet) (bool, error) { + params := make(map[string]string) + params["user_id"] = strconv.Itoa(p.UserID) + params["name"] = p.Name + params["emojis"] = p.Emojis + + var err error + if params["png_sticker"], err = b.marshler.MarshalToString(p.PNGSticker); err != nil { + return false, err + } + + if params["mask_position"], err = b.marshler.MarshalToString(p.MaskPosition); err != nil { + return false, err + } + + files := make([]*InputFile, 0) + if p.PNGSticker.IsAttachment() { + files = append(files, p.PNGSticker) + } + + src, err := b.Upload(MethodAddStickerToSet, params, files...) + if err != nil { + return false, err + } + + resp := new(Response) + if err = b.marshler.Unmarshal(src, resp); err != nil { + return false, err + } + + var result bool + if err = b.marshler.Unmarshal(resp.Result, &result); err != nil { + return false, err + } + + return result, nil +} + +// SetStickerPositionInSet move a sticker in a set created by the bot to a specific position. Returns True on success. +func (b *Bot) SetStickerPositionInSet(sticker string, position int) (bool, error) { + src, err := b.marshler.Marshal(&SetStickerPositionInSet{ + Sticker: sticker, + Position: position, + }) + if err != nil { + return false, err + } + + resp := new(Response) + if err = b.marshler.Unmarshal(src, resp); err != nil { + return false, err + } + + var result bool + if err = b.marshler.Unmarshal(resp.Result, &result); err != nil { + return false, err + } + + return result, nil +} + +// DeleteStickerFromSet delete a sticker from a set created by the b. Returns True on success. +func (b *Bot) DeleteStickerFromSet(sticker string) (bool, error) { + src, err := b.Do(MethodDeleteStickerFromSet, DeleteStickerFromSet{Sticker: sticker}) + if err != nil { + return false, err + } + + resp := new(Response) + if err = b.marshler.Unmarshal(src, resp); err != nil { + return false, err + } + + var result bool + if err = b.marshler.Unmarshal(resp.Result, &result); err != nil { + return false, err + } + + return result, nil +} + +// InSet checks that the current sticker in the stickers set. +func (s Sticker) InSet() bool { return s.SetName != "" } + +func (s Sticker) HasThumb() bool { return s.Thumb != nil } + +func (s Sticker) IsMask() bool { return s.MaskPosition != nil } + +func (s Sticker) File() File { + return File{ + FileID: s.FileID, + FileUniqueID: s.FileUniqueID, + FileSize: s.FileSize, + } +} diff --git a/stickers_test.go b/stickers_test.go new file mode 100644 index 0000000..31f6e01 --- /dev/null +++ b/stickers_test.go @@ -0,0 +1,55 @@ +package telegram + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestStickerInSet(t *testing.T) { + t.Run("true", func(t *testing.T) { + s := Sticker{SetName: "HotCherry"} + assert.True(t, s.InSet()) + }) + t.Run("false", func(t *testing.T) { + s := Sticker{} + assert.False(t, s.InSet()) + }) +} + +func TestStickerHasThumb(t *testing.T) { + t.Run("true", func(t *testing.T) { + s := Sticker{Thumb: &PhotoSize{FileID: "abc"}} + assert.True(t, s.HasThumb()) + }) + t.Run("false", func(t *testing.T) { + s := Sticker{} + assert.False(t, s.HasThumb()) + }) +} + +func TestStickerIsMask(t *testing.T) { + t.Run("true", func(t *testing.T) { + s := Sticker{MaskPosition: &MaskPosition{Point: PointEyes}} + assert.True(t, s.IsMask()) + }) + t.Run("false", func(t *testing.T) { + s := Sticker{} + assert.False(t, s.IsMask()) + }) +} + +func TestStickerFile(t *testing.T) { + t.Run("valid", func(t *testing.T) { + s := Sticker{FileID: "abc", FileUniqueID: "really_abc", FileSize: 42} + assert.Equal(t, s.File(), File{ + FileID: "abc", + FileUniqueID: "really_abc", + FileSize: 42, + }) + }) + t.Run("empty", func(t *testing.T) { + var s Sticker + assert.Equal(t, s.File(), File{}) + }) +} diff --git a/stop.go b/stop.go deleted file mode 100644 index fad8e4a..0000000 --- a/stop.go +++ /dev/null @@ -1,28 +0,0 @@ -package telegram - -type StopPollConfig struct { - // Unique identifier for the target chat. A native poll can't be sent to a private chat. - ChatID int64 `json:"chat_id"` - - // Identifier of the original message with the poll - MessageID int `json:"message_id"` - - // A JSON-serialized object for a new message inline keyboard. - ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` -} - -func (b *Bot) StopPoll(params StopPollConfig) (*Poll, error) { - dst, err := parser.Marshal(params) - if err != nil { - return nil, err - } - - resp, err := b.request(dst, MethodStopPoll) - if err != nil { - return nil, err - } - - var poll Poll - err = parser.Unmarshal(resp.Result, &poll) - return &poll, err -} diff --git a/telegram.go b/telegram.go index 83422ed..931e513 100644 --- a/telegram.go +++ b/telegram.go @@ -1,11 +1,13 @@ package telegram import ( - gojson "encoding/json" - "errors" + "bytes" + "encoding/json" + "io" + "mime/multipart" "path" + "path/filepath" - json "github.com/json-iterator/go" http "github.com/valyala/fasthttp" ) @@ -14,55 +16,93 @@ import ( // of the query can be found in the result field. In case of an unsuccessful // request, ok equals false, and the error is explained in the error field. type Response struct { - Ok bool `json:"ok"` - ErrorCode int `json:"error_code,omitempty"` - Description string `json:"description,omitempty"` - Result gojson.RawMessage `json:"result,omitempty"` - Parameters *ResponseParameters `json:"parameters,omitempty"` + Description string `json:"description,omitempty"` + ErrorCode int `json:"error_code,omitempty"` + Ok bool `json:"ok"` + Parameters []*ResponseParameters `json:"parameters,omitempty"` + Result json.RawMessage `json:"result,omitempty"` } -var ( - defaultClient = http.Client{} - parser = json.ConfigFastest -) +func (b *Bot) Do(method string, payload interface{}) ([]byte, error) { + u := http.AcquireURI() + defer http.ReleaseURI(u) + u.SetScheme("https") + u.SetHost("api.telegram.org") + u.SetPath(path.Join("bot"+b.AccessToken, method)) -func (b *Bot) request(dst []byte, method string) (*Response, error) { - if b.Client == nil { - b.SetClient(&defaultClient) + var buf bytes.Buffer + if err := b.marshler.NewEncoder(&buf).Encode(payload); err != nil { + return nil, err } - requestURI := http.AcquireURI() - requestURI.SetScheme("https") - requestURI.SetHost("api.telegram.org") - requestURI.SetPath(path.Join("bot"+b.AccessToken, method)) - req := http.AcquireRequest() defer http.ReleaseRequest(req) - req.Header.SetContentType("application/json; charset=utf-8") req.Header.SetMethod(http.MethodPost) - if dst == nil { - req.Header.SetMethod(http.MethodGet) - } - req.Header.SetRequestURI(requestURI.String()) - req.Header.SetUserAgent(path.Join("telegram", Version)) - req.Header.SetHostBytes(requestURI.Host()) - req.SetBody(dst) + req.SetRequestURIBytes(u.RequestURI()) + req.Header.SetContentType("application/json") + req.SetBody(buf.Bytes()) resp := http.AcquireResponse() defer http.ReleaseResponse(resp) - if err := b.Client.Do(req, resp); err != nil { + if err := b.client.Do(req, resp); err != nil { return nil, err } - var data Response - if err := parser.Unmarshal(resp.Body(), &data); err != nil { - return nil, err - } - - if !data.Ok { - return nil, errors.New(data.Description) - } - - return &data, nil + return resp.Body(), nil +} + +func (b *Bot) Upload(method string, payload map[string]string, files ...*InputFile) ([]byte, error) { + if len(files) == 0 { + return b.Do(method, payload) + } + + body := new(bytes.Buffer) + w := multipart.NewWriter(body) + + for i := range files { + _, fileName := filepath.Split(files[i].Attachment.Name()) + + part, err := w.CreateFormFile(fileName, fileName) + if err != nil { + return nil, err + } + + if _, err = io.Copy(part, files[i].Attachment); err != nil { + return nil, err + } + } + + for key, val := range payload { + if err := w.WriteField(key, val); err != nil { + return nil, err + } + } + + if err := w.Close(); err != nil { + return nil, err + } + + u := http.AcquireURI() + defer http.ReleaseURI(u) + u.SetScheme("https") + u.SetHost("api.telegram.org") + u.SetPath(path.Join("bot"+b.AccessToken, method)) + + req := http.AcquireRequest() + defer http.ReleaseRequest(req) + req.Header.SetMethod(http.MethodPost) + req.SetRequestURIBytes(u.RequestURI()) + req.Header.SetContentType(w.FormDataContentType()) + req.Header.SetMultipartFormBoundary(w.Boundary()) + body.WriteTo(req.BodyWriter()) + + resp := http.AcquireResponse() + defer http.ReleaseResponse(resp) + + if err := b.client.Do(req, resp); err != nil { + return nil, err + } + + return resp.Body(), nil } diff --git a/types.go b/types.go index f29c593..9cf6c94 100644 --- a/types.go +++ b/types.go @@ -1,79 +1,16 @@ package telegram +import ( + "os" + "path/filepath" + "strings" + "time" + + http "github.com/valyala/fasthttp" + "golang.org/x/text/language" +) + type ( - // Update represents an incoming update. - // - // At most one of the optional parameters can be present in any given update. - Update struct { - // The update‘s unique identifier. Update identifiers start from a - // certain positive number and increase sequentially. This ID becomes - // especially handy if you’re using Webhooks, since it allows you to - // ignore repeated updates or to restore the correct update sequence, - // should they get out of order. - UpdateID int `json:"update_id"` - - // New incoming message of any kind — text, photo, sticker, etc. - Message *Message `json:"message,omitempty"` - - // New version of a message that is known to the bot and was edited - EditedMessage *Message `json:"edited_message,omitempty"` - - // New incoming channel post of any kind — text, photo, sticker, etc. - ChannelPost *Message `json:"channel_post,omitempty"` - - // New version of a channel post that is known to the bot and was edited - EditedChannelPost *Message `json:"adited_channel_post,omitempty"` - - // New incoming inline query - InlineQuery *InlineQuery `json:"inline_query,omitempty"` - - // The result of an inline query that was chosen by a user and sent to - // their chat partner. - ChosenInlineResult *ChosenInlineResult `json:"chosen_inline_result,omitempty"` - - // New incoming callback query - CallbackQuery *CallbackQuery `json:"callback_query,omitempty"` - - // New incoming shipping query. Only for invoices with flexible price - ShippingQuery *ShippingQuery `json:"shipping_query,omitempty"` - - // New incoming pre-checkout query. Contains full information about - // checkout - PreCheckoutQuery *PreCheckoutQuery `json:"pre_checkout_query,omitempty"` - - // New poll state. Bots receive only updates about polls, which are sent or stopped by the bot - Poll *Poll `json:"poll,omitempty"` - } - - // WebhookInfo contains information about the current status of a webhook. - WebhookInfo struct { - // Webhook URL, may be empty if webhook is not set up - URL string `json:"url"` - - // Error message in human-readable format for the most recent error that - // happened when trying to deliver an update via webhook - LastErrorMessage string `json:"last_error_message,omitempty"` - - // True, if a custom certificate was provided for webhook certificate - // checks - HasCustomCertificate bool `json:"has_custom_certificate"` - - // Number of updates awaiting delivery - PendingUpdateCount int `json:"pending_update_count"` - - // Maximum allowed number of simultaneous HTTPS connections to the - // webhook for update delivery - MaxConnections int `json:"max_connections,omitempty"` - - // Unix time for the most recent error that happened when trying to - // deliver an update via webhook - LastErrorDate int64 `json:"last_error_date,omitempty"` - - // A list of update types the bot is subscribed to. Defaults to all - // update types - AllowedUpdates []string `json:"allowed_updates,omitempty"` - } - // User represents a Telegram user or bot. User struct { // Unique identifier for this user or bot @@ -200,11 +137,11 @@ type ( // For text messages, special entities like usernames, URLs, bot // commands, etc. that appear in the text - Entities []MessageEntity `json:"entities,omitempty"` + Entities []*MessageEntity `json:"entities,omitempty"` // For messages with a caption, special entities like usernames, URLs, // bot commands, etc. that appear in the caption - CaptionEntities []MessageEntity `json:"caption_entities,omitempty"` + CaptionEntities []*MessageEntity `json:"caption_entities,omitempty"` // Message is an audio file, information about the file Audio *Audio `json:"audio,omitempty"` @@ -221,7 +158,7 @@ type ( Game *Game `json:"game,omitempty"` // Message is a photo, available sizes of the photo - Photo []PhotoSize `json:"photo,omitempty"` + Photo []*PhotoSize `json:"photo,omitempty"` // Message is a sticker, information about the sticker Sticker *Sticker `json:"sticker,omitempty"` @@ -248,11 +185,11 @@ type ( Venue *Venue `json:"venue,omitempty"` // Message is a native poll, information about the poll - Poll *Poll `json:"poll,omitempry"` + Poll *Poll `json:"poll,omitempty"` // New members that were added to the group or supergroup and information // about them (the bot itself may be one of these members) - NewChatMembers []User `json:"new_chat_members,omitempty"` + NewChatMembers []*User `json:"new_chat_members,omitempty"` // A member was removed from the group, information about them (this // member may be the bot itself) @@ -262,7 +199,7 @@ type ( NewChatTitle string `json:"new_chat_title,omitempty"` // A chat photo was change to this value - NewChatPhoto []PhotoSize `json:"new_chat_photo,omitempty"` + NewChatPhoto []*PhotoSize `json:"new_chat_photo,omitempty"` // Service message: the chat photo was deleted DeleteChatPhoto bool `json:"delete_chat_photo,omitempty"` @@ -342,7 +279,8 @@ type ( // Identifier for this file, which can be used to download or reuse the file FileID string `json:"file_id"` - // 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. + // 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"` // Photo width @@ -432,6 +370,38 @@ type ( FileSize int `json:"file_size,omitempty"` } + // Animation provide an animation for your game so that it looks stylish in + // chats (check out Lumberjack for an example). This object represents an + // animation file to be displayed in the message containing a game. + Animation struct { + // Unique file identifier + FileID string `json:"file_id"` + + // 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"` + + // Video width as defined by sender + Width int `json:"width"` + + // Video height as defined by sender + Height int `json:"height"` + + // Duration of the video in seconds as defined by sender + Duration int `json:"duration"` + + // Animation thumbnail as defined by sender + Thumb *PhotoSize `json:"thumb,omitempty"` + + // Original animation filename as defined by sender + FileName string `json:"file_name,omitempty"` + + // MIME type of the file as defined by sender + MimeType string `json:"mime_type,omitempty"` + + // File size + FileSize int `json:"file_size,omitempty"` + } + // Voice represents a voice note. Voice struct { // Identifier for this file, which can be used to download or reuse the file @@ -513,9 +483,7 @@ type ( // Foursquare identifier of the venue FoursquareID string `json:"foursquare_id,omitempty"` - // Foursquare type of the venue. (For example, - // "arts_entertainment/default", "arts_entertainment/aquarium" or - // "food/icecream".) + // Foursquare type of the venue. (For example, "arts_entertainment/default", "arts_entertainment/aquarium" or "food/icecream".) FoursquareType string `json:"foursquare_type,omitempty"` } @@ -537,7 +505,7 @@ type ( Question string `json:"question"` // List of poll options - Options []PollOption `json:"options"` + Options []*PollOption `json:"options"` // True, if the poll is closed IsClosed bool `json:"is_closed"` @@ -549,7 +517,7 @@ type ( TotalCount int `json:"total_count"` // Requested profile pictures (in up to 4 sizes each) - Photos [][]PhotoSize `json:"photos"` + Photos [][]*PhotoSize `json:"photos"` } // File represents a file ready to be downloaded. The file can be downloaded @@ -578,7 +546,7 @@ type ( ReplyKeyboardMarkup struct { // Array of button rows, each represented by an Array of KeyboardButton // objects - Keyboard [][]KeyboardButton `json:"keyboard"` + Keyboard [][]*KeyboardButton `json:"keyboard"` // Requests clients to resize the keyboard vertically for optimal fit // (e.g., make the keyboard smaller if there are just two rows of @@ -623,7 +591,7 @@ type ( // ReplyKeyboardRemove will remove the current custom keyboard and display // the default letter-keyboard. By default, custom keyboards are displayed - // until a new keyboard is sent by a bot. An exception is made for one-time + // until a new keyboard is sent by a b. An exception is made for one-time // keyboards that are hidden immediately after the user presses a button // (see ReplyKeyboardMarkup). ReplyKeyboardRemove struct { @@ -648,7 +616,7 @@ type ( InlineKeyboardMarkup struct { // Array of button rows, each represented by an Array of // InlineKeyboardButton objects - InlineKeyboard [][]InlineKeyboardButton `json:"inline_keyboard"` + InlineKeyboard [][]*InlineKeyboardButton `json:"inline_keyboard"` } // InlineKeyboardButton represents one button of an inline keyboard. You @@ -718,7 +686,7 @@ type ( // Username of a bot, which will be used for user authorization. See Setting up a bot for more // details. If not specified, the current bot's username will be assumed. The url's domain must be the - // same as the domain linked with the bot. See Linking your domain to the bot for more details. + // same as the domain linked with the b. See Linking your domain to the bot for more details. BotUsername string `json:"bot_username,omitempty"` // Pass true to request the permission for your bot to send messages to the user. @@ -736,7 +704,7 @@ type ( // display a progress bar until you call answerCallbackQuery. It is, // therefore, necessary to react by calling answerCallbackQuery even if no // notification to the user is needed (e.g., without specifying any of the - // optional parameters). + // optional ). CallbackQuery struct { // Unique identifier for this query ID string `json:"id"` @@ -835,38 +803,10 @@ type ( // by administrators that were appointed by the user) CanPromoteMembers bool `json:"can_promote_members,omitempty"` - // Administrators and restricted only. True, if the user is allowed to change the chat title, photo - // and other settings - CanChangeInfo bool `json:"can_change_info,omitempty"` - - // Administrators and restricted only. True, if the user is allowed to invite new users to the chat - CanInviteUsers bool `json:"can_invite_users,omitempty"` - - // Administrators and restricted only. True, if the user is allowed to pin messages; groups and - // supergroups only - CanPinMessages bool `json:"can_pin_messages,omitempty"` - // Restricted only. True, if the user is a member of the chat at the moment of the request IsMember bool `json:"is_member,omitempty"` - // Restricted only. True, if the user can send text messages, contacts, - // locations and venues - CanSendMessages bool `json:"can_send_messages,omitempty"` - - // Restricted only. True, if the user can send audios, documents, photos, - // videos, video notes and voice notes, implies can_send_messages - CanSendMediaMessages bool `json:"can_send_media_messages,omitempty"` - - // Restricted only. True, if the user is allowed to send polls - CanSendPolls bool `json:"can_send_polls,omitempty"` - - // Restricted only. True, if the user can send animations, games, - // stickers and use inline bots, implies can_send_media_messages - CanSendOtherMessages bool `json:"can_send_other_messages,omitempty"` - - // Restricted only. True, if user may add web page previews to his - // messages, implies can_send_media_messages - CanAddWebPagePreviews bool `json:"can_add_web_page_previews,omitempty"` + ChatPermissions } // ChatPermissions describes actions that a non-administrator user is allowed to take in a chat. @@ -913,10 +853,12 @@ type ( // InputMedia represents the content of a media message to be sent. InputMedia interface { - File() string - InputMediaCaption() string - InputMediaParseMode() string - InputMediaType() string + GetMedia() *InputFile + } + + AlbumMedia interface { + GetMedia() *InputFile + isAlbumMedia() } // InputMediaPhoto represents a photo to be sent. @@ -929,7 +871,7 @@ type ( // a file from the Internet, or pass "attach://" to // upload a new one using multipart/form-data under // name. - Media string `json:"media"` + Media *InputFile `json:"media"` // Caption of the photo to be sent, 0-200 characters Caption string `json:"caption,omitempty"` @@ -949,7 +891,7 @@ type ( // a file from the Internet, or pass "attach://" to // upload a new one using multipart/form-data under // name. - Media string `json:"media"` + Media *InputFile `json:"media"` // Caption of the video to be sent, 0-200 characters Caption string `json:"caption,omitempty"` @@ -982,7 +924,7 @@ type ( // a file from the Internet, or pass "attach://" to // upload a new one using multipart/form-data under " // if the thumbnail was uploaded using multipart/form-data under // . - Thumb InputFile `json:"thumb,omitempty"` + Thumb *InputFile `json:"thumb,omitempty"` // Caption of the animation to be sent, 0-200 characters Caption string `json:"caption,omitempty"` - // Send Markdown or HTML, if you want Telegram apps to show bold, italic, - // fixed-width text or inline URLs in the media caption. + // Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. ParseMode string `json:"parse_mode,omitempty"` // Animation width @@ -1015,27 +956,16 @@ type ( // Type of the result, must be audio Type string `json:"type"` - // File to send. Pass a file_id to send a file that exists on the - // Telegram servers (recommended), pass an HTTP URL for Telegram to get - // a file from the Internet, or pass "attach://" to - // upload a new one using multipart/form-data under - // name. - Media string `json:"media"` + // File to send. Pass a file_id to send a file that exists on the Telegram servers (recommended), pass an HTTP URL for Telegram to get a file from the Internet, or pass "attach://" to upload a new one using multipart/form-data under name. + Media *InputFile `json:"media"` - // Thumbnail of the file sent. The thumbnail should be in JPEG format and - // less than 200 kB in size. A thumbnail‘s width and height should not - // exceed 90. Ignored if the file is not uploaded using - // multipart/form-data. Thumbnails can’t be reused and can be only - // uploaded as a new file, so you can pass "attach://" - // if the thumbnail was uploaded using multipart/form-data under - // . - Thumb InputFile `json:"thumb,omitempty"` + // Thumbnail of the file sent. The thumbnail should be in JPEG format and less than 200 kB in size. A thumbnail‘s width and height should not exceed 90. Ignored if the file is not uploaded using multipart/form-data. Thumbnails can’t be reused and can be only uploaded as a new file, so you can pass "attach://" if the thumbnail was uploaded using multipart/form-data under . + Thumb *InputFile `json:"thumb,omitempty"` // Caption of the audio to be sent, 0-200 characters Caption string `json:"caption,omitempty"` - // Send Markdown or HTML, if you want Telegram apps to show bold, italic, - // fixed-width text or inline URLs in the media caption. + // Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. ParseMode string `json:"parse_mode,omitempty"` // Duration of the audio in seconds @@ -1053,1762 +983,563 @@ type ( // Type of the result, must be document Type string `json:"type"` - // File to send. Pass a file_id to send a file that exists on the - // Telegram servers (recommended), pass an HTTP URL for Telegram to get - // a file from the Internet, or pass "attach://" to - // upload a new one using multipart/form-data under - // name. - Media string `json:"media"` + // File to send. Pass a file_id to send a file that exists on the Telegram servers (recommended), pass an HTTP URL for Telegram to get a file from the Internet, or pass "attach://" to upload a new one using multipart/form-data under name. + Media *InputFile `json:"media"` - // Thumbnail of the file sent. The thumbnail should be in JPEG format and - // less than 200 kB in size. A thumbnail‘s width and height should not - // exceed 90. Ignored if the file is not uploaded using - // multipart/form-data. Thumbnails can’t be reused and can be only - // uploaded as a new file, so you can pass "attach://" - // if the thumbnail was uploaded using multipart/form-data under - // . - Thumb InputFile `json:"thumb,omitempty"` + // Thumbnail of the file sent. The thumbnail should be in JPEG format and less than 200 kB in size. A thumbnail‘s width and height should not exceed 90. Ignored if the file is not uploaded using multipart/form-data. Thumbnails can’t be reused and can be only uploaded as a new file, so you can pass "attach://" if the thumbnail was uploaded using multipart/form-data under . + Thumb *InputFile `json:"thumb,omitempty"` // Caption of the document to be sent, 0-200 characters Caption string `json:"caption,omitempty"` - // Send Markdown or HTML, if you want Telegram apps to show bold, italic, - // fixed-width text or inline URLs in the media caption. + // Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. ParseMode string `json:"parse_mode,omitempty"` } - // InputFile represents the contents of a file to be uploaded. Must be posted - // using multipart/form-data in the usual way that files are uploaded via the - // browser. - InputFile interface{} - - // Animation provide an animation for your game so that it looks stylish in - // chats (check out Lumberjack for an example). This object represents an - // animation file to be displayed in the message containing a game. - Animation struct { - // Unique file identifier - FileID string `json:"file_id"` - - // 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"` - - // Video width as defined by sender - Width int `json:"width"` - - // Video height as defined by sender - Height int `json:"height"` - - // Duration of the video in seconds as defined by sender - Duration int `json:"duration"` - - // Animation thumbnail as defined by sender - Thumb *PhotoSize `json:"thumb,omitempty"` - - // Original animation filename as defined by sender - FileName string `json:"file_name,omitempty"` - - // MIME type of the file as defined by sender - MimeType string `json:"mime_type,omitempty"` - - // File size - FileSize int `json:"file_size,omitempty"` - } - - // Game represents a game. Use BotFather to create and edit games, their - // short names will act as unique identifiers. - Game struct { - // Title of the game - Title string `json:"title"` - - // Description of the game - Description string `json:"description"` - - // Brief description of the game or high scores included in the game - // message. Can be automatically edited to include current high scores - // for the game when the bot calls setGameScore, or manually edited - // using editMessageText. 0-4096 characters. - Text string `json:"text,omitempty"` - - // Photo that will be displayed in the game message in chats. - Photo []PhotoSize `json:"photo"` - - // Special entities that appear in text, such as usernames, URLs, bot - // commands, etc. - TextEntities []MessageEntity `json:"text_entities,omitempty"` - - // Animation that will be displayed in the game message in chats. Upload - // via BotFather - Animation *Animation `json:"animation,omitempty"` - } - - // CallbackGame a placeholder, currently holds no information. Use BotFather - // to set up your game. - CallbackGame struct{} - - // GameHighScore represents one row of the high scores table for a game. - GameHighScore struct { - // Position in high score table for the game - Position int `json:"position"` - - // Score - Score int `json:"score"` - - // User - User *User `json:"user"` - } - - // InlineQuery represents an incoming inline query. When the user sends an - // empty query, your bot could return some default or trending results. - InlineQuery struct { - // Unique identifier for this query - ID string `json:"id"` - - // Text of the query (up to 512 characters) - Query string `json:"query"` - - // Offset of the results to be returned, can be controlled by the bot - Offset string `json:"offset"` - - // Sender - From *User `json:"from"` - - // Sender location, only for bots that request user location - Location *Location `json:"location,omitempty"` - } - - // InlineQueryResult represents one result of an inline query. - InlineQueryResult interface { - ResultID() string - ResultType() string - ResultReplyMarkup() *InlineKeyboardMarkup - } - - // InlineQueryResultArticle represents a link to an article or web page. - InlineQueryResultArticle struct { - // Type of the result, must be article - Type string `json:"type"` - - // Unique identifier for this result, 1-64 Bytes - ID string `json:"id"` - - // Title of the result - Title string `json:"title"` - - // URL of the result - URL string `json:"url,omitempty"` - - // Short description of the result - Description string `json:"description,omitempty"` - - // Url of the thumbnail for the result - ThumbURL string `json:"thumb_url,omitempty"` - - // Content of the message to be sent - InputMessageContent interface{} `json:"input_message_content"` - - // Inline keyboard attached to the message - ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` - - // Pass True, if you don't want the URL to be shown in the message - HideURL bool `json:"hide_url,omitempty"` - - // Thumbnail width - ThumbWidth int `json:"thumb_width,omitempty"` - - // Thumbnail height - ThumbHeight int `json:"thumb_height,omitempty"` - } - - // InlineQueryResultPhoto represents a link to a photo. By default, this - // photo will be sent by the user with optional caption. Alternatively, you - // can use input_message_content to send a message with the specified content - // instead of the photo. - InlineQueryResultPhoto struct { - // Type of the result, must be photo - Type string `json:"type"` - - // Unique identifier for this result, 1-64 bytes - ID string `json:"id"` - - // A valid URL of the photo. Photo must be in jpeg format. Photo size - // must not exceed 5MB - PhotoURL string `json:"photo_url"` - - // URL of the thumbnail for the photo - ThumbURL string `json:"thumb_url"` - - // Title for the result - Title string `json:"title,omitempty"` - - // Short description of the result - Description string `json:"description,omitempty"` - - // Caption of the photo to be sent, 0-200 characters - Caption string `json:"caption,omitempty"` - - // Send Markdown or HTML, if you want Telegram apps to show bold, italic, - // fixed-width text or inline URLs in the media caption. - ParseMode string `json:"parse_mode,omitempty"` - - // Width of the photo - PhotoWidth int `json:"photo_width,omitempty"` - - // Height of the photo - PhotoHeight int `json:"photo_height,omitempty"` - - // Inline keyboard attached to the message - ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` - - // Content of the message to be sent instead of the photo - InputMessageContent interface{} `json:"input_message_content,omitempty"` - } - - // InlineQueryResultGif represents a link to an animated GIF file. By - // default, this animated GIF file will be sent by the user with optional - // caption. Alternatively, you can use input_message_content to send a - // message with the specified content instead of the animation. - InlineQueryResultGif struct { - // Type of the result, must be gif - Type string `json:"type"` - - // Unique identifier for this result, 1-64 bytes - ID string `json:"id"` - - // A valid URL for the GIF file. File size must not exceed 1MB - GifURL string `json:"gif_url"` - - // URL of the static thumbnail for the result (jpeg or gif) - ThumbURL string `json:"thumb_url"` - - // Title for the result - Title string `json:"title,omitempty"` - - // Caption of the GIF file to be sent, 0-200 characters - Caption string `json:"caption,omitempty"` - - // Send Markdown or HTML, if you want Telegram apps to show bold, italic, - // fixed-width text or inline URLs in the media caption. - ParseMode string `json:"parse_mode,omitempty"` - - // Width of the GIF - GifWidth int `json:"gif_width,omitempty"` - - // Height of the GIF - GifHeight int `json:"gif_height,omitempty"` - - // Duration of the GIF - GifDuration int `json:"gif_duration,omitempty"` - - // Inline keyboard attached to the message - ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` - - // Content of the message to be sent instead of the GIF animation - InputMessageContent interface{} `json:"input_message_content,omitempty"` - } - - // InlineQueryResultMpeg4Gif represents a link to a video animation - // (H.264/MPEG-4 AVC video without sound). By default, this animated MPEG-4 - // file will be sent by the user with optional caption. Alternatively, you - // can use input_message_content to send a message with the specified content - // instead of the animation. - InlineQueryResultMpeg4Gif struct { - // Type of the result, must be mpeg4_gif - Type string `json:"type"` - - // Unique identifier for this result, 1-64 bytes - ID string `json:"id"` - - // A valid URL for the MP4 file. File size must not exceed 1MB - Mpeg4URL string `json:"mpeg4_url"` - - // URL of the static thumbnail (jpeg or gif) for the result - ThumbURL string `json:"thumb_url"` - - // Title for the result - Title string `json:"title,omitempty"` - - // Caption of the MPEG-4 file to be sent, 0-200 characters - Caption string `json:"caption,omitempty"` - - // Video width - Mpeg4Width int `json:"mpeg4_width,omitempty"` - - // Video height - Mpeg4Height int `json:"mpeg4_height,omitempty"` - - // Video duration - Mpeg4Duration int `json:"mpeg4_duration,omitempty"` - - // Inline keyboard attached to the message - ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` - - // Content of the message to be sent instead of the video animation - InputMessageContent interface{} `json:"input_message_content,omitempty"` - } - - // InlineQueryResultVideo represents a link to a page containing an embedded - // video player or a video file. By default, this video file will be sent by - // the user with an optional caption. Alternatively, you can use - // input_message_content to send a message with the specified content - // instead of the video. - // - // If an InlineQueryResultVideo message contains an embedded video (e.g., - // YouTube), you must replace its content using input_message_content. - InlineQueryResultVideo struct { - // Type of the result, must be video - Type string `json:"type"` - - // Unique identifier for this result, 1-64 bytes - ID string `json:"id"` - - // A valid URL for the embedded video player or video file - VideoURL string `json:"video_url"` - - // Mime type of the content of video url, "text/html" or "video/mp4" - MimeType string `json:"mime_type"` - - // URL of the thumbnail (jpeg only) for the video - ThumbURL string `json:"thumb_url"` - - // Title for the result - Title string `json:"title"` - - // Caption of the video to be sent, 0-200 characters - Caption string `json:"caption,omitempty"` - - // Send Markdown or HTML, if you want Telegram apps to show bold, italic, - // fixed-width text or inline URLs in the media caption. - ParseMode string `json:"parse_mode,omitempty"` - - // Short description of the result - Description string `json:"description,omitempty"` - - // Video width - VideoWidth int `json:"video_width,omitempty"` - - // Video height - VideoHeight int `json:"video_height,omitempty"` - - // Video duration in seconds - VideoDuration int `json:"video_duration,omitempty"` - - // Inline keyboard attached to the message - ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` - - // Content of the message to be sent instead of the video. This field is - // required if InlineQueryResultVideo is used to send an HTML-page as a - // result (e.g., a YouTube video). - InputMessageContent interface{} `json:"input_message_content,omitempty"` - } - - // InlineQueryResultAudio represents a link to an mp3 audio file. By default, - // this audio file will be sent by the user. Alternatively, you can use - // input_message_content to send a message with the specified content - // instead of the audio. - InlineQueryResultAudio struct { - // Type of the result, must be audio - Type string `json:"type"` - - // Unique identifier for this result, 1-64 bytes - ID string `json:"id"` - - // A valid URL for the audio file - AudioURL string `json:"audio_url"` - - // Title - Title string `json:"title"` - - // Caption, 0-200 characters - Caption string `json:"caption,omitempty"` - - // Send Markdown or HTML, if you want Telegram apps to show bold, italic, - // fixed-width text or inline URLs in the media caption. - ParseMode string `json:"parse_mode,omitempty"` - - // Performer - Performer string `json:"performer,omitempty"` - - // Audio duration in seconds - AudioDuration int `json:"audio_duration,omitempty"` - - // Inline keyboard attached to the message - ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` - - // Content of the message to be sent instead of the audio - InputMessageContent interface{} `json:"input_message_content,omitempty"` - } - - // InlineQueryResultVoice represents a link to a voice recording in an .ogg - // container encoded with OPUS. By default, this voice recording will be - // sent by the user. Alternatively, you can use input_message_content to - // send a message with the specified content instead of the the voice message. - InlineQueryResultVoice struct { - // Type of the result, must be voice - Type string `json:"type"` - - // Unique identifier for this result, 1-64 bytes - ID string `json:"id"` - - // A valid URL for the voice recording - VoiceURL string `json:"voice_url"` - - // Recording title - Title string `json:"title"` - - // Caption, 0-200 characters - Caption string `json:"caption,omitempty"` - - // Send Markdown or HTML, if you want Telegram apps to show bold, italic, - // fixed-width text or inline URLs in the media caption. - ParseMode string `json:"parse_mode,omitempty"` - - // Recording duration in seconds - VoiceDuration int `json:"voice_duration,omitempty"` - - // Inline keyboard attached to the message - ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` - - // Content of the message to be sent instead of the voice recording - InputMessageContent interface{} `json:"input_message_content,omitempty"` - } - - // InlineQueryResultDocument represents a link to a file. By default, this - // file will be sent by the user with an optional caption. Alternatively, - // you can use input_message_content to send a message with the specified - // content instead of the file. Currently, only .PDF and .ZIP files can be - // sent using this method. - InlineQueryResultDocument struct { - // Type of the result, must be document - Type string `json:"type"` - - // Unique identifier for this result, 1-64 bytes - ID string `json:"id"` - - // Title for the result - Title string `json:"title"` - - // Caption of the document to be sent, 0-200 characters - Caption string `json:"caption,omitempty"` - - // Send Markdown or HTML, if you want Telegram apps to show bold, italic, - // fixed-width text or inline URLs in the media caption. - ParseMode string `json:"parse_mode,omitempty"` - - // A valid URL for the file - DocumentURL string `json:"document_url"` - - // Mime type of the content of the file, either "application/pdf" or - // "application/zip" - MimeType string `json:"mime_type"` - - // Short description of the result - Description string `json:"description,omitempty"` - - // URL of the thumbnail (jpeg only) for the file - ThumbURL string `json:"thumb_url,omitempty"` - - // Inline keyboard attached to the message - ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` - - // Content of the message to be sent instead of the file - InputMessageContent interface{} `json:"input_message_content,omitempty"` - - // Thumbnail width - ThumbWidth int `json:"thumb_width,omitempty"` - - // Thumbnail height - ThumbHeight int `json:"thumb_height,omitempty"` - } - - // InlineQueryResultLocation represents a location on a map. By default, the - // location will be sent by the user. Alternatively, you can use - // input_message_content to send a message with the specified content - // instead of the location. - InlineQueryResultLocation struct { - // Type of the result, must be location - Type string `json:"type"` - - // Unique identifier for this result, 1-64 Bytes - ID string `json:"id"` - - // Location title - Title string `json:"title"` - - // Url of the thumbnail for the result - ThumbURL string `json:"thumb_url,omitempty"` - - // Location latitude in degrees - Latitude float32 `json:"latitude"` - - // Location longitude in degrees - Longitude float32 `json:"longitude"` - - // Inline keyboard attached to the message - ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` - - // Content of the message to be sent instead of the location - InputMessageContent interface{} `json:"input_message_content,omitempty"` - - // Thumbnail width - ThumbWidth int `json:"thumb_width,omitempty"` - - // Thumbnail height - ThumbHeight int `json:"thumb_height,omitempty"` - } - - // InlineQueryResultVenue represents a venue. By default, the venue will be - // sent by the user. Alternatively, you can use input_message_content to - // send a message with the specified content instead of the venue. - InlineQueryResultVenue struct { - // Type of the result, must be venue - Type string `json:"type"` - - // Unique identifier for this result, 1-64 Bytes - ID string `json:"id"` - - // Title of the venue - Title string `json:"title"` - - // Address of the venue - Address string `json:"address"` - - // Foursquare identifier of the venue if known - FoursquareID string `json:"foursquare_id,omitempty"` - - // Foursquare type of the venue, if known. (For example, - // "arts_entertainment/default", "arts_entertainment/aquarium" or - // "food/icecream".) - FoursquareType string `json:"foursquare_type,omitempty"` - - // Url of the thumbnail for the result - ThumbURL string `json:"thumb_url,omitempty"` - - // Latitude of the venue location in degrees - Latitude float32 `json:"latitude"` - - // Longitude of the venue location in degrees - Longitude float32 `json:"longitude"` - - // Inline keyboard attached to the message - ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` - - // Content of the message to be sent instead of the venue - InputMessageContent interface{} `json:"input_message_content,omitempty"` - - // Thumbnail width - ThumbWidth int `json:"thumb_width,omitempty"` - - // Thumbnail height - ThumbHeight int `json:"thumb_height,omitempty"` - } - - // InlineQueryResultContact represents a contact with a phone number. By - // default, this contact will be sent by the user. Alternatively, you can - // use input_message_content to send a message with the specified content - // instead of the contact. - InlineQueryResultContact struct { - // Type of the result, must be contact - Type string `json:"type"` - - // Unique identifier for this result, 1-64 Bytes - ID string `json:"id"` - - // Contact's phone number - PhoneNumber string `json:"phone_number"` - - // Contact's first name - FirstName string `json:"first_name"` - - // Contact's last name - LastName string `json:"last_name,omitempty"` - - // Additional data about the contact in the form of a vCard, 0-2048 bytes - VCard string `json:"vcard,omitempty"` - - // Url of the thumbnail for the result - ThumbURL string `json:"thumb_url,omitempty"` - - // Inline keyboard attached to the message - ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` - - // Content of the message to be sent instead of the contact - InputMessageContent interface{} `json:"input_message_content,omitempty"` - - // Thumbnail width - ThumbWidth int `json:"thumb_width,omitempty"` - - // Thumbnail height - ThumbHeight int `json:"thumb_height,omitempty"` - } - - // InlineQueryResultGame represents a Game. - InlineQueryResultGame struct { - // Type of the result, must be game - Type string `json:"type"` - - // Unique identifier for this result, 1-64 bytes - ID string `json:"id"` - - // Short name of the game - GameShortName string `json:"game_short_name"` - - // Inline keyboard attached to the message - ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` - } - - // InlineQueryResultCachedPhoto represents a link to a photo stored on the - // Telegram servers. By default, this photo will be sent by the user with an - // optional caption. Alternatively, you can use input_message_content to - // send a message with the specified content instead of the photo. - InlineQueryResultCachedPhoto struct { - // Type of the result, must be photo - Type string `json:"type"` - - // Unique identifier for this result, 1-64 bytes - ID string `json:"id"` - - // A valid file identifier of the photo - PhotoFileID string `json:"photo_file_id"` - - // Title for the result - Title string `json:"title,omitempty"` - - // Short description of the result - Description string `json:"description,omitempty"` - - // Caption of the photo to be sent, 0-200 characters - Caption string `json:"caption,omitempty"` - - // Send Markdown or HTML, if you want Telegram apps to show bold, italic, - // fixed-width text or inline URLs in the media caption. - ParseMode string `json:"parse_mode,omitempty"` - - // Inline keyboard attached to the message - ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` - - // Content of the message to be sent instead of the photo - InputMessageContent interface{} `json:"input_message_content,omitempty"` - } - - // InlineQueryResultCachedGif represents a link to an animated GIF file - // stored on the Telegram servers. By default, this animated GIF file will - // be sent by the user with an optional caption. Alternatively, you can use - // input_message_content to send a message with specified content instead of - // the animation. - InlineQueryResultCachedGif struct { - // Type of the result, must be gif - Type string `json:"type"` - - // Unique identifier for this result, 1-64 bytes - ID string `json:"id"` - - // A valid file identifier for the GIF file - GifFileID string `json:"gif_file_id"` - - // Title for the result - Title string `json:"title,omitempty"` - - // Caption of the GIF file to be sent, 0-200 characters - Caption string `json:"caption,omitempty"` - - // Send Markdown or HTML, if you want Telegram apps to show bold, italic, - // fixed-width text or inline URLs in the media caption. - ParseMode string `json:"parse_mode,omitempty"` - - // Inline keyboard attached to the message - ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` - - // Content of the message to be sent instead of the GIF animation - InputMessageContent interface{} `json:"input_message_content,omitempty"` - } - - // InlineQueryResultCachedMpeg4Gif represents a link to a video animation - // (H.264/MPEG-4 AVC video without sound) stored on the Telegram servers. By - // default, this animated MPEG-4 file will be sent by the user with an - // optional caption. Alternatively, you can use input_message_content to - // send a message with the specified content instead of the animation. - InlineQueryResultCachedMpeg4Gif struct { - // Type of the result, must be mpeg4_gif - Type string `json:"type"` - - // Unique identifier for this result, 1-64 bytes - ID string `json:"id"` - - // A valid file identifier for the MP4 file - Mpeg4FileID string `json:"mpeg4_file_id"` - - // Title for the result - Title string `json:"title,omitempty"` - - // Caption of the MPEG-4 file to be sent, 0-200 characters - Caption string `json:"caption,omitempty"` - - // Send Markdown or HTML, if you want Telegram apps to show bold, italic, - // fixed-width text or inline URLs in the media caption. - ParseMode string `json:"parse_mode,omitempty"` - - // Inline keyboard attached to the message - ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` - - // Content of the message to be sent instead of the video animation - InputMessageContent interface{} `json:"input_message_content,omitempty"` - } - - // InlineQueryResultCachedSticker represents a link to a sticker stored on - // the Telegram servers. By default, this sticker will be sent by the user. - // Alternatively, you can use input_message_content to send a message with - // the specified content instead of the sticker. - InlineQueryResultCachedSticker struct { - // Type of the result, must be sticker - Type string `json:"type"` - - // Unique identifier for this result, 1-64 bytes - ID string `json:"id"` - - // A valid file identifier of the sticker - StickerFileID string `json:"sticker_file_id"` - - // Inline keyboard attached to the message - ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` - - // Content of the message to be sent instead of the sticker - InputMessageContent interface{} `json:"input_message_content,omitempty"` - } - - // InlineQueryResultCachedDocument represents a link to a file stored on the - // Telegram servers. By default, this file will be sent by the user with an - // optional caption. Alternatively, you can use input_message_content to - // send a message with the specified content instead of the file. - InlineQueryResultCachedDocument struct { - // Type of the result, must be document - Type string `json:"type"` - - // Unique identifier for this result, 1-64 bytes - ID string `json:"id"` - - // Title for the result - Title string `json:"title"` - - // A valid file identifier for the file - DocumentFileID string `json:"document_file_id"` - - // Short description of the result - Description string `json:"description,omitempty"` - - // Caption of the document to be sent, 0-200 characters - Caption string `json:"caption,omitempty"` - - // Send Markdown or HTML, if you want Telegram apps to show bold, italic, - // fixed-width text or inline URLs in the media caption. - ParseMode string `json:"parse_mode,omitempty"` - - // Inline keyboard attached to the message - ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` - - // Content of the message to be sent instead of the file - InputMessageContent interface{} `json:"input_message_content,omitempty"` - } - - // InlineQueryResultCachedVideo represents a link to a video file stored on - // the Telegram servers. By default, this video file will be sent by the - // user with an optional caption. Alternatively, you can use - // input_message_content to send a message with the specified content - // instead of the video. - InlineQueryResultCachedVideo struct { - // Type of the result, must be video - Type string `json:"type"` - - // Unique identifier for this result, 1-64 bytes - ID string `json:"id"` - - // A valid file identifier for the video file - VideoFileID string `json:"video_file_id"` - - // Title for the result - Title string `json:"title"` - - // Short description of the result - Description string `json:"description,omitempty"` - - // Caption of the video to be sent, 0-200 characters - Caption string `json:"caption,omitempty"` - - // Send Markdown or HTML, if you want Telegram apps to show bold, italic, - // fixed-width text or inline URLs in the media caption. - ParseMode string `json:"parse_mode,omitempty"` - - // Inline keyboard attached to the message - ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` - - // Content of the message to be sent instead of the video - InputMessageContent interface{} `json:"input_message_content,omitempty"` - } - - // InlineQueryResultCachedVoice represents a link to a voice message stored - // on the Telegram servers. By default, this voice message will be sent by - // the user. Alternatively, you can use input_message_content to send a - // message with the specified content instead of the voice message. - InlineQueryResultCachedVoice struct { - // Type of the result, must be voice - Type string `json:"type"` - - // Unique identifier for this result, 1-64 bytes - ID string `json:"id"` - - // A valid file identifier for the voice message - VoiceFileID string `json:"voice_file_id"` - - // Voice message title - Title string `json:"title"` - - // Caption, 0-200 characters - Caption string `json:"caption,omitempty"` - - // Send Markdown or HTML, if you want Telegram apps to show bold, italic, - // fixed-width text or inline URLs in the media caption. - ParseMode string `json:"parse_mode,omitempty"` - - // Inline keyboard attached to the message - ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` - - // Content of the message to be sent instead of the voice message - InputMessageContent interface{} `json:"input_message_content,omitempty"` - } - - // InlineQueryResultCachedAudio represents a link to an mp3 audio file - // stored on the Telegram servers. By default, this audio file will be sent - // by the user. Alternatively, you can use input_message_content to send a - // message with the specified content instead of the audio. - InlineQueryResultCachedAudio struct { - // Type of the result, must be audio - Type string `json:"type"` - - // Unique identifier for this result, 1-64 bytes - ID string `json:"id"` - - // A valid file identifier for the audio file - AudioFileID string `json:"audio_file_id"` - - // Caption, 0-200 characters - Caption string `json:"caption,omitempty"` - - // Send Markdown or HTML, if you want Telegram apps to show bold, italic, - // fixed-width text or inline URLs in the media caption. - ParseMode string `json:"parse_mode,omitempty"` - - // Inline keyboard attached to the message - ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` - - // Content of the message to be sent instead of the audio - InputMessageContent interface{} `json:"input_message_content,omitempty"` - } - - // InputMessageContent represents the content of a message to be sent as a result - // of an inline query. - InputMessageContent interface { - IsInputMessageContent() bool - } - - // InputTextMessageContent represents the content of a text message to be - // sent as the result of an inline query. - InputTextMessageContent struct { - // Text of the message to be sent, 1-4096 characters - MessageText string `json:"message_text"` - - // 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"` - - // Disables link previews for links in the sent message - DisableWebPagePreview bool `json:"disable_web_page_preview,omitempty"` - } - - // InputLocationMessageContent represents the content of a location message - // to be sent as the result of an inline query. - InputLocationMessageContent struct { - // Latitude of the location in degrees - Latitude float32 `json:"latitude"` - - // Longitude of the location in degrees - Longitude float32 `json:"longitude"` - } - - // InputVenueMessageContent represents the content of a venue message to be - // sent as the result of an inline query. - InputVenueMessageContent struct { - // Latitude of the venue in degrees - Latitude float32 `json:"latitude"` - - // Longitude of the venue in degrees - Longitude float32 `json:"longitude"` - - // Name of the venue - Title string `json:"title"` - - // Address of the venue - Address string `json:"address"` - - // Foursquare identifier of the venue, if known - FoursquareID string `json:"foursquare_id,omitempty"` - - // Foursquare type of the venue, if known. (For example, - // "arts_entertainment/default", "arts_entertainment/aquarium" or - // "food/icecream".) - FoursquareType string `json:"foursquare_type,omitempty"` - } - - // InputContactMessageContent represents the content of a contact message to - // be sent as the result of an inline query. - InputContactMessageContent struct { - // Contact's phone number - PhoneNumber string `json:"phone_number"` - - // Contact's first name - FirstName string `json:"first_name"` - - // Contact's last name - LastName string `json:"last_name,omitempty"` - - // Additional data about the contact in the form of a vCard, 0-2048 bytes - VCard string `json:"vcard,omitempty"` - } - - // ChosenInlineResult represents a result of an inline query that was chosen - // by the user and sent to their chat partner. - ChosenInlineResult struct { - // The unique identifier for the result that was chosen - ResultID string `json:"result_id"` - - // Identifier of the sent inline message. Available only if there is an - // inline keyboard attached to the message. Will be also received in - // callback queries and can be used to edit the message. - InlineMessageID string `json:"inline_message_id,omitempty"` - - // The query that was used to obtain the result - Query string `json:"query"` - - // The user that chose the result - From *User `json:"from"` - - // Sender location, only for bots that require user location - Location *Location `json:"location,omitempty"` - } - - // AuthParameters represent a Telegram Passport auth parameters for SDK. - AuthParameters struct { - // Unique identifier for the bot. You can get it from bot token. - // For example, for the bot token - // 1234567:4TT8bAc8GHUspu3ERYn-KGcvsvGB9u_n4ddy, the bot id is - // 1234567. - BotID int `json:"bot_id"` - - // A JSON-serialized object describing the data you want to - // request - Scope PassportScope `json:"scope"` - - // Public key of the bot - PublicKey string `json:"public_key"` - - // Bot-specified nonce. - // - // Important: For security purposes it should be a - // cryptographically secure unique identifier of the request. In - // particular, it should be long enough and it should be - // generated using a cryptographically secure pseudorandom number - // generator. You should never accept credentials with the same - // nonce twice. - Nonce string `json:"nonce"` - } - - // PassportScope represents the data to be requested. - PassportScope struct { - // List of requested elements, each type may be used only once - // in the entire array of PassportScopeElement objects - Data []PassportScopeElement `json:"data"` - - // Scope version, must be 1 - V int `json:"v"` - } - - // PassportScopeElement represents a requested element. - PassportScopeElement interface { - PassportScopeElementTranslation() bool - PassportScopeElementSelfie() bool - } - - //PassportScopeElementOneOfSeveral represents several elements one of which must be provided. - PassportScopeElementOneOfSeveral struct { - // List of elements one of which must be provided; - OneOf []PassportScopeElementOne `json:"one_of"` - - // Use this parameter if you want to request a selfie with the - // document from this list that the user chooses to upload. - Selfie bool `json:"selfie,omitempty"` - - // Use this parameter if you want to request a translation of - // the document from this list that the user chooses to upload. - // Note: We suggest to only request translations after you have - // received a valid document that requires one. - Translation bool `json:"translation,omitempty"` - } - - // PassportScopeElementOne represents one particular element that must - // be provided. If no options are needed, String can be used instead of - // this object to specify the type of the element. - PassportScopeElementOne struct { - // Element type. - Type string `json:"type"` - - // Use this parameter if you want to request a selfie with the - // document as well. - Selfie bool `json:"selfie,omitempty"` - - // Use this parameter if you want to request a translation of - // the document as well. - Translation bool `json:"translation,omitempty"` - - // Use this parameter to request the first, last and middle name - // of the user in the language of the user's country of residence. - NativeNames bool `json:"native_names,omitempty"` - } - - Passport struct { - // Personal Details - PersonalDetails struct { - Data *PersonalDetails `json:"data"` - } `json:"personal_details"` - - // Passport - Passport struct { - Data *IDDocumentData `json:"data"` - FrontSide *PassportFile `json:"front_side"` - Selfie *PassportFile `json:"selfie,omitempty"` - Translation []PassportFile `json:"translation,omitempty"` - } `json:"passport"` - - // Internal Passport - InternalPassport struct { - Data *IDDocumentData `json:"data"` - FrontSide *PassportFile `json:"front_side"` - Selfie *PassportFile `json:"selfie,omitempty"` - Translation []PassportFile `json:"translation,omitempty"` - } `json:"internal_passport"` - - // Driver License - DriverLicense struct { - Data *IDDocumentData `json:"data"` - FrontSide *PassportFile `json:"front_side"` - ReverseSide *PassportFile `json:"reverse_side"` - Selfie *PassportFile `json:"selfie,omitempty"` - Translation []PassportFile `json:"translation,omitempty"` - } `json:"driver_license"` - - // Identity Card - IdentityCard struct { - Data *IDDocumentData `json:"data"` - FrontSide *PassportFile `json:"front_side"` - ReverseSide *PassportFile `json:"reverse_side"` - Selfie *PassportFile `json:"selfie,omitempty"` - Translation []PassportFile `json:"translation,omitempty"` - } `json:"identity_card"` - - // Address - Address struct { - Data *ResidentialAddress `json:"data"` - } `json:"address"` - - // Utility Bill - UtilityBill struct { - Files []PassportFile `json:"files"` - Translation []PassportFile `json:"translation,omitempty"` - } `json:"utility_bill"` - - // Bank Statement - BankStatement struct { - Files []PassportFile `json:"files"` - Translation []PassportFile `json:"translation,omitempty"` - } `json:"bank_statement"` - - // Rental Agreement - RentalAgreement struct { - Files []PassportFile `json:"files"` - Translation []PassportFile `json:"translation,omitempty"` - } `json:"rental_agreement"` - - // Registration Page in the Internal Passport - PassportRegistration struct { - Files []PassportFile `json:"files"` - Translation []PassportFile `json:"translation,omitempty"` - } `json:"passport_registration"` - - // Temporary Registration - TemporaryRegistration struct { - Files []PassportFile `json:"files"` - Translation []PassportFile `json:"translation,omitempty"` - } `json:"temporary_registration"` - - // Phone number - PhoneNumber string `json:"phone_number"` - - // Email - Email string `json:"email"` - } - - // PersonalDetails represents personal details. - PersonalDetails struct { - // First Name - FirstName string `json:"first_name"` - - // Last Name - LastName string `json:"last_name"` - - // Middle Name - MiddleName string `json:"middle_name,omitempty"` - - // Date of birth in DD.MM.YYYY format - BirthDate string `json:"birth_date"` - - // Gender, male or female - Gender string `json:"gender"` - - // Citizenship (ISO 3166-1 alpha-2 country code) - CountryCode string `json:"country_code"` - - // Country of residence (ISO 3166-1 alpha-2 country code) - ResidenceCountryCode string `json:"residence_country_code"` - - // First Name in the language of the user's country of residence - FirstNameNative string `json:"first_name_native"` - - // Last Name in the language of the user's country of residence - LastNameNative string `json:"last_name_native"` - - // Middle Name in the language of the user's country of residence - MiddleNameNative string `json:"middle_name_native,omitempty"` - } - - // ResidentialAddress represents a residential address. - ResidentialAddress struct { - // First line for the address - StreetLine1 string `json:"street_line1"` - - // Second line for the address - StreetLine2 string `json:"street_line2,omitempty"` - - // City - City string `json:"city"` - - // State - State string `json:"state,omitempty"` - - // ISO 3166-1 alpha-2 country code - CountryCode string `json:"country_code"` - - // Address post code - PostCode string `json:"post_code"` - } - - // IDDocumentData represents the data of an identity document. - IDDocumentData struct { - // Document number - DocumentNo string `json:"document_no"` - - // Date of expiry, in DD.MM.YYYY format - ExpiryDate string `json:"expiry_date,omitempty"` - } - - // PassportData contains information about Telegram Passport data shared with - // the bot by the user. - PassportData struct { - // Array with information about documents and other Telegram Passport - // elements that was shared with the bot - Data []EncryptedPassportElement `json:"data"` - - // Encrypted credentials required to decrypt the data - Credentials *EncryptedCredentials `json:"credentials"` - } - - // PassportFile represents a file uploaded to Telegram Passport. Currently all - // Telegram Passport files are in JPEG format when decrypted and don't exceed - // 10MB. - PassportFile struct { - // Identifier for this file, which can be used to download or reuse the file - FileID string `json:"file_id"` - - // 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"` - - // File size - FileSize int `json:"file_size"` - - // Unix time when the file was uploaded - FileDate int `json:"file_date"` - } - - // Credentials is a JSON-serialized object. - Credentials struct { - // Credentials for encrypted data - SecureData *SecureData `json:"secure_data"` - - // Bot-specified nonce - Nonce string `json:"nonce"` - } - - // SecureData represents the credentials required to decrypt encrypted - // data. All fields are optional and depend on fields that were requested. - SecureData struct { - // Credentials for encrypted personal details - PersonalDetails *SecureValue `json:"personal_details,omitempty"` - - // Credentials for encrypted passport - Passport *SecureValue `json:"passport,omitempty"` - - // Credentials for encrypted internal passport - InternalPassport *SecureValue `json:"internal_passport,omitempty"` - - // Credentials for encrypted driver license - DriverLicense *SecureValue `json:"driver_license,omitempty"` - - // Credentials for encrypted ID card - IdentityCard *SecureValue `json:"identity_card,omitempty"` - - // Credentials for encrypted residential address - Address *SecureValue `json:"address,omitempty"` - - // Credentials for encrypted utility bill - UtilityBill *SecureValue `json:"utility_bill,omitempty"` - - // Credentials for encrypted bank statement - BankStatement *SecureValue `json:"bank_statement,omitempty"` - - // Credentials for encrypted rental agreement - RentalAgreement *SecureValue `json:"rental_agreement,omitempty"` - - // Credentials for encrypted registration from internal passport - PassportRegistration *SecureValue `json:"passport_registration,omitempty"` - - // Credentials for encrypted temporary registration - TemporaryRegistration *SecureValue `json:"temporary_registration,omitempty"` - } - - // SecureValue represents the credentials required to decrypt encrypted - // values. All fields are optional and depend on the type of fields that - // were requested. - SecureValue struct { - // Credentials for encrypted Telegram Passport data. - Data *DataCredentials `json:"data,omitempty"` - - // Credentials for an encrypted document's front side. - FrontSide *FileCredentials `json:"front_side,omitempty"` - - // Credentials for an encrypted document's reverse side. - ReverseSide *FileCredentials `json:"reverse_side,omitempty"` - - // Credentials for an encrypted selfie of the user with a document. - Selfie *FileCredentials `json:"selfie,omitempty"` - - // Credentials for an encrypted translation of the document. - Translation []FileCredentials `json:"translation,omitempty"` - - // Credentials for encrypted files. - Files []FileCredentials `json:"files,omitempty"` - } - - // DataCredentials can be used to decrypt encrypted data from the data - // field in EncryptedPassportElement. - DataCredentials struct { - // Checksum of encrypted data - DataHash string `json:"data_hash"` - - // Secret of encrypted data - Secret string `json:"secret"` - } - - // FileCredentials can be used to decrypt encrypted files from the - // front_side, reverse_side, selfie, files and translation fields in - // EncryptedPassportElement. - FileCredentials struct { - // Checksum of encrypted file - FileHash string `json:"file_hash"` - - // Secret of encrypted file - Secret string `json:"secret"` - } - - // EncryptedPassportElement contains information about documents or other - // Telegram Passport elements shared with the bot by the user. - EncryptedPassportElement struct { - // Element type. - Type string `json:"type"` - - // Base64-encoded encrypted Telegram Passport element data provided by - // the user, available for "personal_details", "passport", - // "driver_license", "identity_card", "identity_passport" and "address" - // types. Can be decrypted and verified using the accompanying - // EncryptedCredentials. - Data string `json:"data,omitempty"` - - // User's verified phone number, available only for "phone_number" type - PhoneNumber string `json:"phone_number,omitempty"` - - // User's verified email address, available only for "email" type - Email string `json:"email,omitempty"` - - // Array of encrypted files with documents provided by the user, - // available for "utility_bill", "bank_statement", "rental_agreement", - // "passport_registration" and "temporary_registration" types. Files can - // be decrypted and verified using the accompanying EncryptedCredentials. - Files []PassportFile `json:"files,omitempty"` - - // Encrypted file with the front side of the document, provided by the - // user. Available for "passport", "driver_license", "identity_card" and - // "internal_passport". The file can be decrypted and verified using the - // accompanying EncryptedCredentials. - FrontSide *PassportFile `json:"front_side,omitempty"` - - // Encrypted file with the reverse side of the document, provided by the - // user. Available for "driver_license" and "identity_card". The file can - // be decrypted and verified using the accompanying EncryptedCredentials. - ReverseSide *PassportFile `json:"reverse_side,omitempty"` - - // Encrypted file with the selfie of the user holding a document, - // provided by the user; available for "passport", "driver_license", - // "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 - // authenticating EncryptedPassportElement. See the Telegram Passport - // Documentation for a complete description of the data decryption and - // authentication processes. - EncryptedCredentials struct { - // Base64-encoded encrypted JSON-serialized data with unique user's - // payload, data hashes and secrets required for EncryptedPassportElement - // decryption and authentication - Data string `json:"data"` - - // Base64-encoded data hash for data authentication - Hash string `json:"hash"` - - // Base64-encoded secret, encrypted with the bot's public RSA key, - // required for data decryption - Secret string `json:"secret"` - } - - // PassportElementError represents an error in the Telegram Passport element - // which was submitted that should be resolved by the user. - PassportElementError interface { - PassportElementErrorMessage() string - PassportElementErrorSource() string - PassportElementErrorType() string - } - - // PassportElementErrorDataField represents an issue in one of the data - // fields that was provided by the user. The error is considered resolved - // when the field's value changes. - PassportElementErrorDataField struct { - // Error source, must be data - Source string `json:"source"` - - // The section of the user's Telegram Passport which has the error, one - // of "personal_details", "passport", "driver_license", "identity_card", - // "internal_passport", "address" - Type string `json:"type"` - - // Name of the data field which has the error - FieldName string `json:"field_name"` - - // Base64-encoded data hash - DataHash string `json:"data_hash"` - - // Error message - Message string `json:"message"` - } - - // PassportElementErrorFrontSide represents an issue with the front side of - // a document. The error is considered resolved when the file with the front - // side of the document changes. - PassportElementErrorFrontSide struct { - // Error source, must be front_side - Source string `json:"source"` - - // The section of the user's Telegram Passport which has the issue, one - // of "passport", "driver_license", "identity_card", "internal_passport" - Type string `json:"type"` - - // Base64-encoded hash of the file with the front side of the document - FileHash string `json:"file_hash"` - - // Error message - Message string `json:"message"` - } - - // PassportElementErrorReverseSide represents an issue with the reverse side - // of a document. The error is considered resolved when the file with reverse - // side of the document changes. - PassportElementErrorReverseSide struct { - // Error source, must be reverse_side - Source string `json:"source"` - - // The section of the user's Telegram Passport which has the issue, one - // of "driver_license", "identity_card" - Type string `json:"type"` - - // Base64-encoded hash of the file with the reverse side of the document - FileHash string `json:"file_hash"` - - // Error message - Message string `json:"message"` - } - - // PassportElementErrorSelfie represents an issue with the selfie with a - // document. The error is considered resolved when the file with the selfie - // changes. - PassportElementErrorSelfie struct { - // Error source, must be selfie - Source string `json:"source"` - - // The section of the user's Telegram Passport which has the issue, one - // of "passport", "driver_license", "identity_card", "internal_passport" - Type string `json:"type"` - - // Base64-encoded hash of the file with the selfie - FileHash string `json:"file_hash"` - - // Error message - Message string `json:"message"` - } - - // PassportElementErrorFile represents an issue with a document scan. The - // error is considered resolved when the file with the document scan changes. - PassportElementErrorFile struct { - // Error source, must be file - Source string `json:"source"` - - // The section of the user's Telegram Passport which has the issue, one - // of "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"` - } - - // PassportElementErrorFiles represents an issue with a list of scans. The - // error is considered resolved when the list of files containing the scans - // changes. - PassportElementErrorFiles struct { - // Error source, must be files - Source string `json:"source"` - - // The section of the user's Telegram Passport which has the issue, one - // of "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"` - } - - // 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 change. - 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"` - } - - // LabeledPrice represents a portion of the price for goods or services. - LabeledPrice struct { - // Portion label - Label string `json:"label"` - - // Price of the product in the smallest units of the currency (integer, - // not float/double). For example, for a price of US$ 1.45 pass amount = - // 145. See the exp parameter in currencies.json, it shows the number of - // digits past the decimal point for each currency (2 for the majority - // of currencies). - Amount int `json:"amount"` - } - - // Invoice contains basic information about an invoice. - Invoice struct { - // Product name - Title string `json:"title"` - - // Product description - Description string `json:"description"` - - // Unique bot deep-linking parameter that can be used to generate this - // invoice - StartParameter string `json:"start_parameter"` - - // Three-letter ISO 4217 currency code - Currency string `json:"currency"` - - // Total price in the smallest units of the currency (integer, not - // float/double). For example, for a price of US$ 1.45 pass amount = 145. - // See the exp parameter in currencies.json, it shows the number of - // digits past the decimal point for each currency (2 for the majority - // of currencies). - TotalAmount int `json:"total_amount"` - } - - // ShippingAddress represents a shipping address. - ShippingAddress struct { - // ISO 3166-1 alpha-2 country code - CountryCode string `json:"country_code"` - - // State, if applicable - State string `json:"state"` - - // City - City string `json:"city"` - - // First line for the address - StreetLine1 string `json:"street_line1"` - - // Second line for the address - StreetLine2 string `json:"street_line2"` - - // Address post code - PostCode string `json:"post_code"` - } - - // OrderInfo represents information about an order. - OrderInfo struct { - // User name - Name string `json:"name,omitempty"` - - // User's phone number - PhoneNumber string `json:"phone_number,omitempty"` - - // User email - Email string `json:"email,omitempty"` - - // User shipping address - ShippingAddress *ShippingAddress `json:"shipping_address,omitempty"` - } - - // ShippingOption represents one shipping option. - ShippingOption struct { - // Shipping option identifier - ID string `json:"id"` - - // Option title - Title string `json:"title"` - - // List of price portions - Prices []LabeledPrice `json:"prices"` - } - - // SuccessfulPayment contains basic information about a successful payment. - SuccessfulPayment struct { - // Three-letter ISO 4217 currency code - Currency string `json:"currency"` - - // Bot specified invoice payload - InvoicePayload string `json:"invoice_payload"` - - // Identifier of the shipping option chosen by the user - ShippingOptionID string `json:"shipping_option_id,omitempty"` - - // Telegram payment identifier - TelegramPaymentChargeID string `json:"telegram_payment_charge_id"` - - // Provider payment identifier - ProviderPaymentChargeID string `json:"provider_payment_charge_id"` - - // Total price in the smallest units of the currency (integer, not - // float/double). For example, for a price of US$ 1.45 pass amount = 145. - // See the exp parameter in currencies.json, it shows the number of - // digits past the decimal point for each currency (2 for the majority - // of currencies). - TotalAmount int `json:"total_amount"` - - // Order info provided by the user - OrderInfo *OrderInfo `json:"order_info,omitempty"` - } - - // ShippingQuery contains information about an incoming shipping query. - ShippingQuery struct { - // Unique query identifier - ID string `json:"id"` - - // Bot specified invoice payload - InvoicePayload string `json:"invoice_payload"` - - // User who sent the query - From *User `json:"from"` - - // User specified shipping address - ShippingAddress *ShippingAddress `json:"shipping_address"` - } - - // PreCheckoutQuery contains information about an incoming pre-checkout query. - PreCheckoutQuery struct { - // Unique query identifier - ID string `json:"id"` - - // Three-letter ISO 4217 currency code - Currency string `json:"currency"` - - // Bot specified invoice payload - InvoicePayload string `json:"invoice_payload"` - - // Identifier of the shipping option chosen by the user - ShippingOptionID string `json:"shipping_option_id,omitempty"` - - // User who sent the query - From *User `json:"from"` - - // Total price in the smallest units of the currency (integer, not - // float/double). For example, for a price of US$ 1.45 pass amount = 145. - // See the exp parameter in currencies.json, it shows the number of - // digits past the decimal point for each currency (2 for the majority of - // currencies). - TotalAmount int `json:"total_amount"` - - // Order info provided by the user - OrderInfo *OrderInfo `json:"order_info,omitempty"` - } - - // Sticker represents a sticker. - Sticker struct { - // Identifier for this file, which can be used to download or reuse the file - FileID string `json:"file_id"` - - // 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"` - - // Sticker width - Width int `json:"width"` - - // Sticker height - Height int `json:"height"` - - // true, if the sticker is animated - IsAnimated bool `json:"is_animated"` - - // Sticker thumbnail in the .webp or .jpg format - Thumb *PhotoSize `json:"thumb,omitempty"` - - // Emoji associated with the sticker - Emoji string `json:"emoji,omitempty"` - - // Name of the sticker set to which the sticker belongs - SetName string `json:"set_name,omitempty"` - - // For mask stickers, the position where the mask should be placed - MaskPosition *MaskPosition `json:"mask_position,omitempty"` - - // File size - FileSize int `json:"file_size,omitempty"` - } - - // StickerSet represents a sticker set. - StickerSet struct { - // Sticker set name - Name string `json:"name"` - - // Sticker set title - Title string `json:"title"` - - // True, if the sticker set contains masks - ContainsMasks bool `json:"contains_masks"` - - // List of all set stickers - Stickers []Sticker `json:"stickers"` - - // true, if the sticker set contains animated stickers - IsAnimated bool `json:"is_animated"` - } - - // MaskPosition describes the position on faces where a mask should be placed - // by default. - MaskPosition struct { - // The part of the face relative to which the mask should be placed. One - // of "forehead", "eyes", "mouth", or "chin". - Point string `json:"point"` - - // Shift by X-axis measured in widths of the mask scaled to the face - // size, from left to right. For example, choosing -1.0 will place mask - // just to the left of the default mask position. - XShift float32 `json:"x_shift"` - - // Shift by Y-axis measured in heights of the mask scaled to the face - // size, from top to bottom. For example, 1.0 will place the mask just - // below the default mask position. - YShift float32 `json:"y_shift"` - - // Mask scaling coefficient. For example, 2.0 means double size. - Scale float32 `json:"scale"` + // InputFile represents the contents of a file to be uploaded. Must be poste using multipart/form-data in the usual way that files are uploaded via the browser. + InputFile struct { + ID string `json:"-"` + URI *http.URI `json:"-"` + Attachment *os.File `json:"-"` } ) + +// Language parse LanguageCode of current user and returns language.Tag. +func (u User) Language() language.Tag { + tag, err := language.Parse(u.LanguageCode) + if err != nil { + tag = language.Und + } + + return tag +} + +// FullName returns the full name of user or FirstName if LastName is not available. +func (u User) FullName() string { + if u.FirstName == "" { + return "" + } + + if u.HasLastName() { + return u.FirstName + " " + u.LastName + } + + return u.FirstName +} + +// HaveLastName checks what the current user has a LastName. +func (u User) HasLastName() bool { return u.LastName != "" } + +// HaveUsername checks what the current user has a username. +func (u User) HasUsername() bool { return u.Username != "" } + +// IsPrivate checks that the current chat is a private chat with single user. +func (c Chat) IsPrivate() bool { return strings.EqualFold(c.Type, ChatPrivate) } + +// IsGroup checks that the current chat is a group. +func (c Chat) IsGroup() bool { return strings.EqualFold(c.Type, ChatGroup) } + +// IsSuperGroup checks that the current chat is a supergroup. +func (c Chat) IsSuperGroup() bool { return strings.EqualFold(c.Type, ChatSuperGroup) } + +// IsChannel checks that the current chat is a channel. +func (c Chat) IsChannel() bool { return strings.EqualFold(c.Type, ChatChannel) } + +// HasPinnedMessage checks that the current chat has a pinned message. +func (c Chat) HasPinnedMessage() bool { return c.PinnedMessage != nil } + +// HasStickerSet checks that the current chat has a sticker set. +func (c Chat) HasStickerSet() bool { return c.StickerSetName != "" } + +// FullName returns the full name of chat or FirstName if LastName is not available. +func (c Chat) FullName() string { + if c.FirstName == "" { + return "" + } + + if c.HasLastName() { + return c.FirstName + " " + c.LastName + } + + return c.FirstName +} + +// HaveLastName checks what the current user has a LastName. +func (c Chat) HasLastName() bool { return c.LastName != "" } + +// HaveUsername checks what the current user has a username. +func (c Chat) HasUsername() bool { return c.Username != "" } + +func (c Chat) HasDescription() bool { return c.Description != "" } + +func (c Chat) HasInviteLink() bool { return c.InviteLink != "" } + +// IsCommand checks that the current message is a bot command. +func (m Message) IsCommand() bool { + return m.HasEntities() && m.Entities[0].IsBotCommand() && m.Entities[0].Offset == 0 +} + +// IsCommandEqual checks that the current message is a specific bot command. +func (m Message) IsCommandEqual(command string) bool { + return m.IsCommand() && strings.EqualFold(m.Command(), command) +} + +// Command returns identifier of the bot command without bot username, if it was available +func (m Message) Command() string { + if !m.IsCommand() { + return "" + } + + return strings.Split(m.RawCommand(), "@")[0] +} + +// RawCommand returns identifier of the bot command with bot username, if it was available +func (m Message) RawCommand() string { + if !m.IsCommand() { + return "" + } + + return string([]rune(m.Text)[1:m.Entities[0].Length]) +} + +// HasCommandArgument checks that the current command message contains argument. +func (m Message) HasCommandArgument() bool { + return m.IsCommand() && m.Entities[0].IsBotCommand() && len([]rune(m.Text)) != m.Entities[0].Length +} + +// CommandArgument returns raw command argument. +func (m Message) CommandArgument() string { + if !m.HasCommandArgument() { + return "" + } + + return string([]rune(m.Text)[m.Entities[0].Length+1:]) +} + +// IsReply checks that the current message is a reply on other message. +func (m Message) IsReply() bool { return m.ReplyToMessage != nil } + +// IsForward checks that the current message is a forward of other message. +func (m Message) IsForward() bool { return m.ForwardDate > 0 } + +// Time parse current message Date and returns time.Time. +func (m Message) Time() time.Time { + if m.Date <= 0 { + return time.Time{} + } + + return time.Unix(m.Date, 0) +} + +// ForwardTime parse current message ForwardDate and returns time.Time. +func (m Message) ForwardTime() time.Time { + if !m.IsForward() { + return time.Time{} + } + + return time.Unix(m.ForwardDate, 0) +} + +// EditTime parse current message EditDate and returns time.Time. +func (m Message) EditTime() time.Time { + if !m.HasBeenEdited() { + return time.Time{} + } + + return time.Unix(m.EditDate, 0) +} + +// HasBeenEdited checks that the current message has been edited. +func (m Message) HasBeenEdited() bool { return m.EditDate > 0 } + +// IsText checks that the current message is just a text message. +func (m Message) IsText() bool { return m.Text != "" } + +// IsAudio checks that the current message is a audio. +func (m Message) IsAudio() bool { return m.Audio != nil } + +// IsDocument checks that the current message is a document. +func (m Message) IsDocument() bool { return m.Document != nil } + +// IsGame checks that the current message is a game. +func (m Message) IsGame() bool { return m.Game != nil } + +// IsPhoto checks that the current message is a photo. +func (m Message) IsPhoto() bool { return len(m.Photo) > 0 } + +// IsSticker checks that the current message is a sticker. +func (m Message) IsSticker() bool { return m.Sticker != nil } + +// IsVideo checks that the current message is a video. +func (m Message) IsVideo() bool { return m.Video != nil } + +// IsVoice checks that the current message is a voice. +func (m Message) IsVoice() bool { return m.Voice != nil } + +// IsVideoNote checks that the current message is a video note. +func (m Message) IsVideoNote() bool { return m.VideoNote != nil } + +// IsContact checks that the current message is a contact. +func (m Message) IsContact() bool { return m.Contact != nil } + +// IsLocation checks that the current message is a location. +func (m Message) IsLocation() bool { return m.Location != nil } + +// IsVenue checks that the current message is a venue. +func (m Message) IsVenue() bool { return m.Venue != nil } + +// IsAnimation checks that the current message is a animation. +func (m Message) IsAnimation() bool { return m.Animation != nil } + +// IsNewChatMembersEvent checks that the current message is a event of entry of new members. +func (m Message) IsNewChatMembersEvent() bool { return len(m.NewChatMembers) > 0 } + +// IsLeftChatMemberEvent checks that the current message is a event of members exit. +func (m Message) IsLeftChatMemberEvent() bool { return m.LeftChatMember != nil } + +// IsNewChatTitleEvent checks that the current message is a event of setting a new chat title. +func (m Message) IsNewChatTitleEvent() bool { return m.NewChatTitle != "" } + +// IsNewChatPhotoEvent checks that the current message is a event of setting a new chat avatar. +func (m Message) IsNewChatPhotoEvent() bool { return len(m.NewChatPhoto) > 0 } + +// IsDeleteChatPhotoEvent checks that the current message is a event of deleting a chat avatar. +func (m Message) IsDeleteChatPhotoEvent() bool { return m.DeleteChatPhoto } + +// IsGroupChatCreatedEvent checks that the current message is a event of creating a new group. +func (m Message) IsGroupChatCreatedEvent() bool { return m.GroupChatCreated } + +// IsSupergroupChatCreatedEvent checks that the current message is a event of creating a new supergroup. +func (m Message) IsSupergroupChatCreatedEvent() bool { return m.SupergroupChatCreated } + +// IsChannelChatCreatedEvent checks that the current message is a event of creating a new channel. +func (m Message) IsChannelChatCreatedEvent() bool { return m.ChannelChatCreated } + +// IsPinnedMessage checks that the current message is a event of pinning another message. +func (m Message) IsPinnedMessage() bool { return m.PinnedMessage != nil } + +// IsInvoice checks that the current message is a invoice. +func (m Message) IsInvoice() bool { return m.Invoice != nil } + +// IsSuccessfulPayment checks that the current message is a event of successful payment. +func (m Message) IsSuccessfulPayment() bool { return m.SuccessfulPayment != nil } + +// IsPoll checks that the current message is a poll. +func (m Message) IsPoll() bool { return m.Poll != nil } + +// HasEntities checks that the current message contains entities. +func (m Message) HasEntities() bool { return len(m.Entities) > 0 } + +// HasCaptionEntities checks that the current media contains entities in caption. +func (m Message) HasCaptionEntities() bool { return len(m.CaptionEntities) > 0 } + +// HasMentions checks that the current message contains mentions. +func (m Message) HasMentions() bool { + if !m.HasEntities() { + return false + } + + for _, entity := range m.Entities { + if !entity.IsMention() && !entity.IsTextMention() { + continue + } + + return true + } + + return false +} + +// HasCaptionMentions checks that the current media contains mentions in caption. +func (m Message) HasCaptionMentions() bool { + if !m.HasCaptionEntities() { + return false + } + + for _, entity := range m.CaptionEntities { + if !entity.IsMention() && !entity.IsTextMention() { + continue + } + + return true + } + + return false +} + +// HasCaption checks that the current media has caption. +func (m Message) HasCaption() bool { return m.Caption != "" } + +// HasAuthorSignature checks that the current channel post has author signature. +func (m Message) HasAuthorSignature() bool { return m.AuthorSignature != "" } + +// IsEvent checks what current message is a any chat event. +func (m Message) IsEvent() bool { + return m.IsChannelChatCreatedEvent() || m.IsDeleteChatPhotoEvent() || m.IsGroupChatCreatedEvent() || + m.IsLeftChatMemberEvent() || m.IsNewChatMembersEvent() || m.IsNewChatTitleEvent() || + m.IsSupergroupChatCreatedEvent() || m.IsNewChatPhotoEvent() +} + +// IsBold checks that the current entity is a bold tag. +func (e MessageEntity) IsBold() bool { return strings.EqualFold(e.Type, EntityBold) } + +// IsBotCommand checks that the current entity is a bot command. +func (e MessageEntity) IsBotCommand() bool { return strings.EqualFold(e.Type, EntityBotCommand) } + +// IsCashtag checks that the current entity is a cashtag. +func (e MessageEntity) IsCashtag() bool { return strings.EqualFold(e.Type, EntityCashtag) } + +// IsCode checks that the current entity is a code tag. +func (e MessageEntity) IsCode() bool { return strings.EqualFold(e.Type, EntityCode) } + +// IsEmail checks that the current entity is a email. +func (e MessageEntity) IsEmail() bool { return strings.EqualFold(e.Type, EntityEmail) } + +// IsHashtag checks that the current entity is a hashtag. +func (e MessageEntity) IsHashtag() bool { return strings.EqualFold(e.Type, EntityHashtag) } + +// IsItalic checks that the current entity is a italic tag. +func (e MessageEntity) IsItalic() bool { return strings.EqualFold(e.Type, EntityItalic) } + +// IsMention checks that the current entity is a username mention. +func (e MessageEntity) IsMention() bool { return strings.EqualFold(e.Type, EntityMention) } + +// IsMPhoneNumberchecks that the current entity is a phone number. +func (e MessageEntity) IsPhoneNumber() bool { return strings.EqualFold(e.Type, EntityPhoneNumber) } + +// IsPre checks that the current entity is a pre tag. +func (e MessageEntity) IsPre() bool { return strings.EqualFold(e.Type, EntityPre) } + +// IsPre checks that the current entity is a pre tag. +func (e MessageEntity) IsStrikethrough() bool { return strings.EqualFold(e.Type, EntityStrikethrough) } + +// IsTextLink checks that the current entity is a text link. +func (e MessageEntity) IsTextLink() bool { return strings.EqualFold(e.Type, EntityTextLink) } + +// IsTextMention checks that the current entity is a mention without username. +func (e MessageEntity) IsTextMention() bool { return strings.EqualFold(e.Type, EntityTextMention) } + +// IsUnderline checks that the current entity is a underline. +func (e MessageEntity) IsUnderline() bool { return strings.EqualFold(e.Type, EntityUnderline) } + +// IsURL checks that the current entity is a URL. +func (e MessageEntity) IsURL() bool { return strings.EqualFold(e.Type, EntityURL) } + +// ParseURL selects URL from message text/caption and parse it as fasthttp.URI. +func (e MessageEntity) ParseURL(text string) *http.URI { + if !e.IsURL() || text == "" { + return nil + } + + link := http.AcquireURI() + to := e.Offset + e.Length + src := []rune(text) + + if len(src) < to { + return nil + } + + link.Update(string(src[e.Offset:to])) + + return link +} + +// TextLink parse current text link entity as fasthttp.URI. +func (e MessageEntity) TextLink() *http.URI { + if !e.IsTextLink() || e.URL == "" { + return nil + } + + link := http.AcquireURI() + + link.Update(e.URL) + + return link +} + +func (a Audio) FullName(separator string) (name string) { + if a.HasPerformer() { + if separator == "" { + separator = DefaultAudioSeparator + } + + name += a.Performer + separator + } + + title := DefaultAudioTitle + if a.HasTitle() { + title = a.Title + } + + name += title + + return +} + +func (a Audio) HasPerformer() bool { return a.Performer != "" } + +func (a Audio) HasTitle() bool { return a.Title != "" } + +func (a Audio) HasThumb() bool { return a.Thumb != nil } + +// File returns File structure without FilePath parameter. +func (a Audio) File() File { + return File{ + FileID: a.FileID, + FileSize: a.FileSize, + FileUniqueID: a.FileUniqueID, + } +} + +func (d Document) HasThumb() bool { return d.Thumb != nil } + +func (d Document) File() File { + return File{ + FileID: d.FileID, + FileUniqueID: d.FileUniqueID, + FileSize: d.FileSize, + } +} + +func (v Video) HasThumb() bool { return v.Thumb != nil } + +func (v Video) File() File { + return File{ + FileID: v.FileID, + FileUniqueID: v.FileUniqueID, + FileSize: v.FileSize, + } +} + +func (a Animation) HasThumb() bool { return a.Thumb != nil } + +func (a Animation) File() File { + return File{ + FileID: a.FileID, + FileUniqueID: a.FileUniqueID, + FileSize: a.FileSize, + } +} + +func (v Voice) File() File { + return File{ + FileID: v.FileID, + FileUniqueID: v.FileUniqueID, + FileSize: v.FileSize, + } +} + +func (vn VideoNote) HasThumb() bool { return vn.Thumb != nil } + +func (vn VideoNote) File() File { + return File{ + FileID: vn.FileID, + FileUniqueID: vn.FileUniqueID, + FileSize: vn.FileSize, + } +} + +// FullName returns the full name of contact or FirstName if LastName is not available. +func (c Contact) FullName() string { + if c.FirstName == "" { + return "" + } + + if c.HasLastName() { + return c.FirstName + " " + c.LastName + } + + return c.FirstName +} + +// HaveLastName checks what the current contact has a LastName. +func (c Contact) HasLastName() bool { return c.LastName != "" } + +func (c Contact) InTelegram() bool { return c.UserID != 0 } + +func (c Contact) HasVCard() bool { return c.VCard != "" } + +// VotesCount returns the total number of votes. +func (p Poll) VotesCount() int { + var v int + for i := range p.Options { + v += p.Options[i].VoterCount + } + + return v +} + +func (p ChatPhoto) SmallFile() File { + return File{ + FileID: p.SmallFileID, + FileUniqueID: p.SmallFileUniqueID, + } +} + +func (p ChatPhoto) BigFile() File { + return File{ + FileID: p.BigFileID, + FileUniqueID: p.BigFileUniqueID, + } +} + +// IsAdministrator checks that current member is administrator. +func (m ChatMember) IsAdministrator() bool { return strings.EqualFold(m.Status, StatusAdministrator) } + +// IsCreator checks that current member is creator. +func (m ChatMember) IsCreator() bool { return strings.EqualFold(m.Status, StatusCreator) } + +// IsKicked checks that current member has been kicked. +func (m ChatMember) IsKicked() bool { return strings.EqualFold(m.Status, StatusKicked) } + +// IsLeft checks that current member has left the chat. +func (m ChatMember) IsLeft() bool { return strings.EqualFold(m.Status, StatusLeft) } + +// IsRestricted checks that current member has been restricted. +func (m ChatMember) IsRestricted() bool { return strings.EqualFold(m.Status, StatusRestricted) } + +// UntilTime parse UntilDate of restrictions and returns time.Time. +func (m ChatMember) UntilTime() time.Time { return time.Unix(m.UntilDate, 0) } + +func (m *InputMediaAnimation) GetMedia() *InputFile { return m.Media } + +func (m *InputMediaAudio) GetMedia() *InputFile { return m.Media } + +func (m *InputMediaDocument) GetMedia() *InputFile { return m.Media } + +func (m *InputMediaPhoto) GetMedia() *InputFile { return m.Media } + +func (InputMediaPhoto) isAlbumMedia() {} + +func (m *InputMediaVideo) GetMedia() *InputFile { return m.Media } + +func (InputMediaVideo) isAlbumMedia() {} + +func (f *InputFile) IsFileID() bool { return f.ID != "" } + +func (f *InputFile) IsURI() bool { return f.URI != nil } + +func (f *InputFile) IsAttachment() bool { return f.Attachment != nil } + +func (f *InputFile) MarshalJSON() ([]byte, error) { + switch { + case f.IsFileID(): + return []byte(f.ID), nil + case f.IsURI(): + return f.URI.FullURI(), nil + case f.IsAttachment(): + _, fileName := filepath.Split(f.Attachment.Name()) + + u := http.AcquireURI() + defer http.ReleaseURI(u) + u.SetScheme(SchemeAttach) + u.SetHost(fileName) + u.SetPathBytes(nil) + + // NOTE(toby3d): remove slash on the end + uri := u.FullURI() + return uri[:len(uri)-1], nil + default: + return nil, nil + } +} diff --git a/types_test.go b/types_test.go new file mode 100644 index 0000000..1965484 --- /dev/null +++ b/types_test.go @@ -0,0 +1,2121 @@ +package telegram + +import ( + "io/ioutil" + "os" + "path/filepath" + "testing" + "time" + + "github.com/stretchr/testify/assert" + http "github.com/valyala/fasthttp" + "golang.org/x/text/language" +) + +func TestUserLanguage(t *testing.T) { + for _, tc := range []struct { + name string + user User + expResult language.Tag + }{{ + name: "russian", + user: User{LanguageCode: "ru"}, + expResult: language.Russian, + }, { + name: "english", + user: User{LanguageCode: "en"}, + expResult: language.English, + }, { + name: "other", + user: User{LanguageCode: "w-t-f"}, + expResult: language.Und, + }, { + name: "empty", + expResult: language.Und, + }} { + tc := tc + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.user.Language(), tc.expResult) + }) + } +} + +func TestUserFullName(t *testing.T) { + for _, tc := range []struct { + name string + user User + expResult string + }{{ + name: "full", + user: User{FirstName: "Maxim", LastName: "Lebedev"}, + expResult: "Maxim Lebedev", + }, { + name: "first", + user: User{FirstName: "Maxim"}, + expResult: "Maxim", + }, { + name: "last", + user: User{LastName: "Lebedev"}, + expResult: "", + }, { + name: "empty", + expResult: "", + }} { + tc := tc + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.user.FullName(), tc.expResult) + }) + } +} + +func TestUserHasLastName(t *testing.T) { + t.Run("true", func(t *testing.T) { + u := User{LastName: "Lebedev"} + assert.True(t, u.HasLastName()) + }) + t.Run("false", func(t *testing.T) { + u := User{FirstName: "Maxim"} + assert.False(t, u.HasLastName()) + }) +} + +func TestUserHasUsername(t *testing.T) { + t.Run("true", func(t *testing.T) { + u := User{Username: "toby3d", FirstName: "Maxim", LastName: "Lebedev"} + assert.True(t, u.HasUsername()) + }) + t.Run("false", func(t *testing.T) { + u := User{FirstName: "Maxim", LastName: "Lebedev"} + assert.False(t, u.HasUsername()) + }) +} + +func TestChatIsPrivate(t *testing.T) { + t.Run("true", func(t *testing.T) { + c := Chat{Type: ChatPrivate, FirstName: "Maxim", LastName: "Lebedev"} + assert.True(t, c.IsPrivate()) + }) + t.Run("false", func(t *testing.T) { + c := Chat{Type: ChatSuperGroup, FirstName: "Меня заставили создать эту группу"} + assert.False(t, c.IsPrivate()) + }) +} + +func TestChatIsGroup(t *testing.T) { + t.Run("true", func(t *testing.T) { + c := Chat{Type: ChatGroup, FirstName: "Меня заставили создать эту группу"} + assert.True(t, c.IsGroup()) + }) + t.Run("false", func(t *testing.T) { + c := Chat{Type: ChatPrivate, FirstName: "Maxim", LastName: "Lebedev"} + assert.False(t, c.IsGroup()) + }) +} + +func TestChatIsSuperGroup(t *testing.T) { + t.Run("true", func(t *testing.T) { + c := Chat{Type: ChatSuperGroup, FirstName: "Меня заставили создать эту группу"} + assert.True(t, c.IsSuperGroup()) + }) + t.Run("false", func(t *testing.T) { + c := Chat{Type: ChatPrivate, FirstName: "Maxim", LastName: "Lebedev"} + assert.False(t, c.IsSuperGroup()) + }) +} + +func TestChatIsChannel(t *testing.T) { + t.Run("true", func(t *testing.T) { + c := Chat{Type: ChatChannel, FirstName: "toby3d"} + assert.True(t, c.IsChannel()) + }) + t.Run("false", func(t *testing.T) { + c := Chat{Type: ChatPrivate, FirstName: "Меня заставили создать эту группу"} + assert.False(t, c.IsChannel()) + }) +} + +func TestChatHasPinnedMessage(t *testing.T) { + t.Run("true", func(t *testing.T) { + c := Chat{PinnedMessage: &Message{Text: "hello, world!"}} + assert.True(t, c.HasPinnedMessage()) + }) + t.Run("false", func(t *testing.T) { + c := Chat{} + assert.False(t, c.HasPinnedMessage()) + }) +} + +func TestChatHasStickerSet(t *testing.T) { + t.Run("true", func(t *testing.T) { + c := Chat{StickerSetName: "HotCherry"} + assert.True(t, c.HasStickerSet()) + }) + t.Run("false", func(t *testing.T) { + c := Chat{} + assert.False(t, c.HasStickerSet()) + }) +} + +func TestChatFullName(t *testing.T) { + for _, tc := range []struct { + name string + chat Chat + expResult string + }{{ + name: "full", + chat: Chat{FirstName: "Maxim", LastName: "Lebedev"}, + expResult: "Maxim Lebedev", + }, { + name: "first", + chat: Chat{FirstName: "Меня заставили создать эту группу"}, + expResult: "Меня заставили создать эту группу", + }, { + name: "last", + chat: Chat{LastName: "WTF"}, + expResult: "", + }, { + name: "empty", + expResult: "", + }} { + tc := tc + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.chat.FullName(), tc.expResult) + }) + } +} + +func TestChatHasLastName(t *testing.T) { + t.Run("true", func(t *testing.T) { + c := Chat{FirstName: "Maxim", LastName: "Lebedev"} + assert.True(t, c.HasLastName()) + }) + t.Run("false", func(t *testing.T) { + c := Chat{FirstName: "Меня заставили создать эту группу", LastName: ""} + assert.False(t, c.HasLastName()) + }) +} + +func TestChatHasUsername(t *testing.T) { + t.Run("true", func(t *testing.T) { + c := Chat{Username: "toby3dRu"} + assert.True(t, c.HasUsername()) + }) + t.Run("false", func(t *testing.T) { + c := Chat{} + assert.False(t, c.HasUsername()) + }) +} + +func TestChatHasDescription(t *testing.T) { + t.Run("true", func(t *testing.T) { + c := Chat{Description: "hello, world!"} + assert.True(t, c.HasDescription()) + }) + t.Run("false", func(t *testing.T) { + c := Chat{} + assert.False(t, c.HasDescription()) + }) +} + +func TestChatHasInviteLink(t *testing.T) { + t.Run("true", func(t *testing.T) { + c := Chat{InviteLink: "https://t.me/joinchat/AAAAAD84EWpWTRTMh4aahQ"} + assert.True(t, c.HasInviteLink()) + }) + t.Run("false", func(t *testing.T) { + c := Chat{} + assert.False(t, c.HasInviteLink()) + }) +} + +func TestMessageIsCommand(t *testing.T) { + t.Run("true", func(t *testing.T) { + m := Message{Text: "/" + CommandStart, Entities: []*MessageEntity{{Type: EntityBotCommand}}} + assert.True(t, m.IsCommand()) + }) + t.Run("false", func(t *testing.T) { + m := Message{Text: "hello, @toby3d", Entities: []*MessageEntity{{Type: EntityMention}}} + assert.False(t, m.IsCommand()) + }) +} + +func TestMessageIsCommandEqual(t *testing.T) { + for _, tc := range []struct { + name string + message Message + command string + expResult bool + }{{ + name: "exact", + message: Message{ + Text: "/" + CommandStart, + Entities: []*MessageEntity{{Type: EntityBotCommand, Length: len(CommandStart) + 1}}, + }, + command: CommandStart, + expResult: true, + }, { + name: "other", + message: Message{ + Text: "/" + CommandStart, + Entities: []*MessageEntity{{Type: EntityBotCommand, Length: len(CommandStart) + 1}}, + }, + command: CommandHelp, + expResult: false, + }, { + name: "empty", + command: CommandHelp, + expResult: false, + }} { + tc := tc + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.message.IsCommandEqual(tc.command), tc.expResult) + }) + } +} + +func TestMessageCommand(t *testing.T) { + for _, tc := range []struct { + name string + message Message + expResult string + }{{ + name: "command", + message: Message{ + Text: "/" + CommandStart, + Entities: []*MessageEntity{{Type: EntityBotCommand, Length: len(CommandStart) + 1}}, + }, + expResult: CommandStart, + }, { + name: "other", + message: Message{ + Text: "hello world", + Entities: []*MessageEntity{{Type: EntityBold, Offset: 6, Length: 5}}, + }, + expResult: "", + }, { + name: "empty", + expResult: "", + }} { + tc := tc + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.message.Command(), tc.expResult) + }) + } +} + +func TestMessageRawCommand(t *testing.T) { + for _, tc := range []struct { + name string + message Message + expResult string + }{{ + name: "command", + message: Message{ + Text: "/" + CommandStart, + Entities: []*MessageEntity{{Type: EntityBotCommand, Length: len(CommandStart) + 1}}, + }, + expResult: CommandStart, + }, { + name: "other", + message: Message{ + Text: "hello world", + Entities: []*MessageEntity{{Type: EntityBold, Offset: 6, Length: 5}}, + }, + expResult: "", + }, { + name: "empty", + expResult: "", + }} { + tc := tc + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.message.RawCommand(), tc.expResult) + }) + } +} + +func TestMessageHasCommandArgument(t *testing.T) { + for _, tc := range []struct { + name string + message Message + expResult bool + }{{ + name: "true", + message: Message{ + Text: "/start example", + Entities: []*MessageEntity{{Type: EntityBotCommand, Length: len(CommandStart) + 1}}, + }, + expResult: true, + }, { + name: "other", + message: Message{ + Text: "/start", + Entities: []*MessageEntity{{Type: EntityBold, Offset: 1, Length: len(CommandStart)}}, + }, + expResult: false, + }, { + name: "false", + message: Message{ + Text: "/start", + Entities: []*MessageEntity{{Type: EntityBotCommand, Length: len(CommandStart) + 1}}, + }, + expResult: false, + }, { + name: "empty", + expResult: false, + }} { + tc := tc + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.message.HasCommandArgument(), tc.expResult) + }) + } +} + +func TestMessageCommandArgument(t *testing.T) { + for _, tc := range []struct { + name string + message Message + expResult string + }{{ + name: "true", + message: Message{ + Text: "/start example", + Entities: []*MessageEntity{{Type: EntityBotCommand, Length: len(CommandStart) + 1}}, + }, + expResult: "example", + }, { + name: "false", + message: Message{ + Text: "/start", + Entities: []*MessageEntity{{Type: EntityBotCommand, Length: len(CommandStart) + 1}}, + }, + expResult: "", + }, { + name: "empty", + expResult: "", + }} { + tc := tc + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.message.CommandArgument(), tc.expResult) + }) + } +} + +func TestMessageIsReply(t *testing.T) { + for _, tc := range []struct { + name string + message Message + expResult bool + }{{ + name: "true", + message: Message{Text: "hello!", ReplyToMessage: &Message{Text: "hello, world!"}}, + expResult: true, + }, { + name: "false", + message: Message{Text: "hello!"}, + expResult: false, + }, { + name: "empty", + expResult: false, + }} { + tc := tc + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.message.IsReply(), tc.expResult) + }) + } +} + +func TestMessageIsForward(t *testing.T) { + t.Run("true", func(t *testing.T) { + m := Message{Text: "hello, world!", ForwardDate: time.Now().Unix()} + assert.True(t, m.IsForward()) + }) + t.Run("false", func(t *testing.T) { + m := Message{Text: "hello, world!"} + assert.False(t, m.IsForward()) + }) +} + +func TestMessageTime(t *testing.T) { + now := time.Now().Round(time.Second) + zero := time.Time{} + + for _, tc := range []struct { + name string + message Message + expResult time.Time + }{{ + name: "specific date", + message: Message{Date: now.Unix()}, + expResult: now, + }, { + name: "zero", + message: Message{Date: 0}, + expResult: zero, + }, { + name: "empty", + expResult: zero, + }} { + tc := tc + t.Run(tc.name, func(t *testing.T) { + assert.True(t, tc.message.Time().Equal(tc.expResult)) + }) + } +} + +func TestMessageForwardTime(t *testing.T) { + now := time.Now().Round(time.Second) + zero := time.Time{} + + for _, tc := range []struct { + name string + message Message + expResult time.Time + }{{ + name: "specific date", + message: Message{ForwardDate: now.Unix()}, + expResult: now, + }, { + name: "false", + message: Message{ForwardDate: 0}, + expResult: zero, + }, { + name: "empty", + expResult: zero, + }} { + tc := tc + t.Run(tc.name, func(t *testing.T) { + assert.True(t, tc.message.ForwardTime().Equal(tc.expResult)) + }) + } +} + +func TestMessageEditTime(t *testing.T) { + now := time.Now().Round(time.Second) + zero := time.Time{} + + for _, tc := range []struct { + name string + message Message + expResult time.Time + }{{ + name: "true", + message: Message{EditDate: now.Unix()}, + expResult: now, + }, { + name: "false", + message: Message{EditDate: 0}, + expResult: zero, + }, { + name: "empty", + expResult: zero, + }} { + tc := tc + t.Run(tc.name, func(t *testing.T) { + assert.True(t, tc.message.EditTime().Equal(tc.expResult)) + }) + } +} + +func TestMessageHasBeenEdited(t *testing.T) { + t.Run("true", func(t *testing.T) { + m := Message{Text: "sorry, fixed", EditDate: time.Now().UTC().Unix()} + assert.True(t, m.HasBeenEdited()) + }) + t.Run("false", func(t *testing.T) { + m := Message{Text: "sorry, fixed", EditDate: 0} + assert.False(t, m.HasBeenEdited()) + }) +} + +func TestMessageIsText(t *testing.T) { + t.Run("true", func(t *testing.T) { + m := Message{Text: "hello, world!"} + assert.True(t, m.IsText()) + }) + t.Run("false", func(t *testing.T) { + m := Message{} + assert.False(t, m.IsText()) + }) +} + +func TestMessageIsAudio(t *testing.T) { + t.Run("true", func(t *testing.T) { + m := Message{Audio: &Audio{Performer: "One-Aloner", Title: "Monitor"}} + assert.True(t, m.IsAudio()) + }) + t.Run("false", func(t *testing.T) { + m := Message{} + assert.False(t, m.IsAudio()) + }) +} + +func TestMessageIsDocument(t *testing.T) { + t.Run("true", func(t *testing.T) { + m := Message{Document: &Document{FileName: "readme.txt"}} + assert.True(t, m.IsDocument()) + }) + t.Run("false", func(t *testing.T) { + m := Message{Text: "hello, world!"} + assert.False(t, m.IsDocument()) + }) +} + +func TestMessageIsGame(t *testing.T) { + t.Run("true", func(t *testing.T) { + m := Message{Game: &Game{Title: "MuSquare"}} + assert.True(t, m.IsGame()) + }) + t.Run("false", func(t *testing.T) { + m := Message{Text: "hello, world!"} + assert.False(t, m.IsGame()) + }) +} + +func TestMessageIsPhoto(t *testing.T) { + t.Run("true", func(t *testing.T) { + m := Message{Photo: []*PhotoSize{{FileID: "abc"}}} + assert.True(t, m.IsPhoto()) + }) + t.Run("false", func(t *testing.T) { + m := Message{Text: "hello, world!"} + assert.False(t, m.IsPhoto()) + }) +} + +func TestMessageIsSticker(t *testing.T) { + t.Run("true", func(t *testing.T) { + m := Message{Sticker: &Sticker{FileID: "abc"}} + assert.True(t, m.IsSticker()) + }) + t.Run("false", func(t *testing.T) { + m := Message{Text: "hello, world!"} + assert.False(t, m.IsSticker()) + }) +} + +func TestMessageIsVideo(t *testing.T) { + t.Run("true", func(t *testing.T) { + m := Message{Video: &Video{FileID: "abc"}} + assert.True(t, m.IsVideo()) + }) + t.Run("false", func(t *testing.T) { + m := Message{Text: "hello, world!"} + assert.False(t, m.IsVideo()) + }) +} + +func TestMessageIsVoice(t *testing.T) { + t.Run("true", func(t *testing.T) { + m := Message{Voice: &Voice{FileID: "abc"}} + assert.True(t, m.IsVoice()) + }) + t.Run("false", func(t *testing.T) { + m := Message{Text: "hello, world!"} + assert.False(t, m.IsVoice()) + }) +} + +func TestMessageIsVideoNote(t *testing.T) { + t.Run("true", func(t *testing.T) { + m := Message{VideoNote: &VideoNote{FileID: "abc"}} + assert.True(t, m.IsVideoNote()) + }) + t.Run("false", func(t *testing.T) { + m := Message{Text: "hello, world!"} + assert.False(t, m.IsVideoNote()) + }) +} + +func TestMessageIsContact(t *testing.T) { + t.Run("true", func(t *testing.T) { + m := Message{Contact: &Contact{FirstName: "Maxim", PhoneNumber: "1234567890"}} + assert.True(t, m.IsContact()) + }) + t.Run("false", func(t *testing.T) { + m := Message{Text: "hello, world!"} + assert.False(t, m.IsContact()) + }) +} + +func TestMessageIsLocation(t *testing.T) { + t.Run("true", func(t *testing.T) { + m := Message{Location: &Location{Latitude: 56.085180, Longitude: 60.735150}} + assert.True(t, m.IsLocation()) + }) + t.Run("false", func(t *testing.T) { + m := Message{Text: "hello, world!"} + assert.False(t, m.IsLocation()) + }) +} + +func TestMessageIsVenue(t *testing.T) { + t.Run("true", func(t *testing.T) { + m := Message{Venue: &Venue{ + Title: "Памятник В. И. Ленину", + Address: "Россия, Челябинская область, Снежинск, улица Свердлова", + Location: &Location{Latitude: 56.085180, Longitude: 60.735150}, + }} + assert.True(t, m.IsVenue()) + }) + t.Run("false", func(t *testing.T) { + m := Message{Text: "hello, world!"} + assert.False(t, m.IsVenue()) + }) +} + +func TestMessageIsAnimation(t *testing.T) { + t.Run("true", func(t *testing.T) { + m := Message{Animation: &Animation{FileID: "abc"}} + assert.True(t, m.IsAnimation()) + }) + t.Run("false", func(t *testing.T) { + m := Message{Text: "hello, world!"} + assert.False(t, m.IsAnimation()) + }) +} + +func TestMessageIsNewChatMembersEvent(t *testing.T) { + t.Run("true", func(t *testing.T) { + m := Message{NewChatMembers: []*User{{ID: 42, FirstName: "Maxim", LastName: "Lebedev"}}} + assert.True(t, m.IsNewChatMembersEvent()) + }) + t.Run("false", func(t *testing.T) { + m := Message{Text: "hello, world!"} + assert.False(t, m.IsNewChatMembersEvent()) + }) +} + +func TestMessageIsLeftChatMemberEvent(t *testing.T) { + t.Run("true", func(t *testing.T) { + m := Message{LeftChatMember: &User{ID: 42, FirstName: "Maxim", LastName: "Lebedev"}} + assert.True(t, m.IsLeftChatMemberEvent()) + }) + t.Run("false", func(t *testing.T) { + m := Message{Text: "hello, world!"} + assert.False(t, m.IsLeftChatMemberEvent()) + }) +} + +func TestMessageIsNewChatTitleEvent(t *testing.T) { + t.Run("true", func(t *testing.T) { + m := Message{NewChatTitle: "Меня заставили создать эту группу"} + assert.True(t, m.IsNewChatTitleEvent()) + }) + t.Run("false", func(t *testing.T) { + m := Message{Text: "hello, world!"} + assert.False(t, m.IsNewChatTitleEvent()) + }) +} + +func TestMessageIsNewChatPhotoEvent(t *testing.T) { + t.Run("true", func(t *testing.T) { + m := Message{NewChatPhoto: []*PhotoSize{{FileID: "abc"}}} + assert.True(t, m.IsNewChatPhotoEvent()) + }) + t.Run("false", func(t *testing.T) { + m := Message{Text: "hello, world!"} + assert.False(t, m.IsNewChatPhotoEvent()) + }) +} + +func TestMessageIsDeleteChatPhotoEvent(t *testing.T) { + t.Run("true", func(t *testing.T) { + m := Message{DeleteChatPhoto: true} + assert.True(t, m.IsDeleteChatPhotoEvent()) + }) + t.Run("false", func(t *testing.T) { + m := Message{Text: "hello, world!"} + assert.False(t, m.IsDeleteChatPhotoEvent()) + }) +} + +func TestMessageIsGroupChatCreatedEvent(t *testing.T) { + t.Run("true", func(t *testing.T) { + m := Message{GroupChatCreated: true} + assert.True(t, m.IsGroupChatCreatedEvent()) + }) + t.Run("false", func(t *testing.T) { + m := Message{Text: "hello, world!"} + assert.False(t, m.IsGroupChatCreatedEvent()) + }) +} + +func TestMessageIsSupergroupChatCreatedEvent(t *testing.T) { + t.Run("true", func(t *testing.T) { + m := Message{SupergroupChatCreated: true} + assert.True(t, m.IsSupergroupChatCreatedEvent()) + }) + t.Run("false", func(t *testing.T) { + m := Message{Text: "hello, world!"} + assert.False(t, m.IsSupergroupChatCreatedEvent()) + }) +} + +func TestMessageIsChannelChatCreatedEvent(t *testing.T) { + t.Run("true", func(t *testing.T) { + m := Message{ChannelChatCreated: true} + assert.True(t, m.IsChannelChatCreatedEvent()) + }) + t.Run("false", func(t *testing.T) { + m := Message{Text: "hello, world!"} + assert.False(t, m.IsChannelChatCreatedEvent()) + }) +} + +func TestMessageIsPinnedMessage(t *testing.T) { + t.Run("true", func(t *testing.T) { + m := Message{PinnedMessage: &Message{Text: "hello, world!"}} + assert.True(t, m.IsPinnedMessage()) + }) + t.Run("false", func(t *testing.T) { + m := Message{Text: "hello, world!"} + assert.False(t, m.IsPinnedMessage()) + }) +} + +func TestMessageIsInvoice(t *testing.T) { + t.Run("true", func(t *testing.T) { + m := Message{Invoice: &Invoice{Title: "Time Machine", TotalAmount: 1}} + assert.True(t, m.IsInvoice()) + }) + t.Run("false", func(t *testing.T) { + m := Message{Text: "hello, world!"} + assert.False(t, m.IsInvoice()) + }) +} + +func TestMessageIsSuccessfulPayment(t *testing.T) { + t.Run("true", func(t *testing.T) { + m := Message{SuccessfulPayment: &SuccessfulPayment{ + OrderInfo: &OrderInfo{Name: "Maxim Lebedev"}, TotalAmount: 1, + }} + assert.True(t, m.IsSuccessfulPayment()) + }) + t.Run("false", func(t *testing.T) { + m := Message{Text: "hello, world!"} + assert.False(t, m.IsSuccessfulPayment()) + }) +} + +func TestMessageIsPoll(t *testing.T) { + t.Run("true", func(t *testing.T) { + m := Message{Poll: &Poll{ + Question: "What is the answer to life, the universe, and everything?", + Options: []*PollOption{ + {Text: "42", VoterCount: 420}, + {Text: "24", VoterCount: 140}, + }, + }} + assert.True(t, m.IsPoll()) + }) + t.Run("false", func(t *testing.T) { + m := Message{Text: "hello, world!"} + assert.False(t, m.IsPoll()) + }) +} + +func TestMessageHasEntities(t *testing.T) { + t.Run("true", func(t *testing.T) { + m := Message{Text: "hello, world!", Entities: []*MessageEntity{ + {Type: EntityBold, Offset: 7, Length: 5}, + }} + assert.True(t, m.HasEntities()) + }) + t.Run("false", func(t *testing.T) { + m := Message{Text: "hello, world!"} + assert.False(t, m.HasEntities()) + }) +} + +func TestMessageHasCaptionEntities(t *testing.T) { + for _, tc := range []struct { + name string + message Message + expResult bool + }{{ + name: "true", + message: Message{ + Caption: "hello, world!", CaptionEntities: []*MessageEntity{ + {Type: EntityBold, Offset: 7, Length: 5}, + }, + }, + expResult: true, + }, { + name: "false", + message: Message{Text: "hello, world!"}, + expResult: false, + }, { + name: "empty", + expResult: false, + }} { + tc := tc + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.message.HasCaptionEntities(), tc.expResult) + }) + } +} + +func TestMessageHasMentions(t *testing.T) { + for _, tc := range []struct { + name string + message Message + expResult bool + }{{ + name: "true", + message: Message{Text: "hello, @toby3d!", Entities: []*MessageEntity{ + {Type: EntityMention, Offset: 7, Length: 7}, + }}, + expResult: true, + }, { + name: "other", + message: Message{Text: "hello, world!", Entities: []*MessageEntity{ + {Type: EntityBold, Offset: 7, Length: 5}, + }}, + expResult: false, + }, { + name: "false", + message: Message{Text: "hello, world!"}, + expResult: false, + }, { + name: "empty", + expResult: false, + }} { + tc := tc + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.message.HasMentions(), tc.expResult) + }) + } +} + +func TestMessageHasCaptionMentions(t *testing.T) { + for _, tc := range []struct { + name string + message Message + expResult bool + }{{ + name: "true", + message: Message{Caption: "hello, @toby3d!", CaptionEntities: []*MessageEntity{ + {Type: EntityMention, Offset: 7, Length: 7}, + }}, + expResult: true, + }, { + name: "other", + message: Message{Text: "hello, world!", CaptionEntities: []*MessageEntity{ + {Type: EntityBold, Offset: 7, Length: 5}, + }}, + expResult: false, + }, { + name: "false", + message: Message{Text: "hello, world!"}, + expResult: false, + }, { + name: "empty", + expResult: false, + }} { + tc := tc + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.message.HasCaptionMentions(), tc.expResult) + }) + } +} + +func TestMessageHasCaption(t *testing.T) { + for _, tc := range []struct { + name string + message Message + expResult bool + }{{ + name: "true", + message: Message{Caption: "hello, world!"}, + expResult: true, + }, { + name: "false", + message: Message{Text: "hello, world!"}, + expResult: false, + }, { + name: "empty", + expResult: false, + }} { + tc := tc + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.message.HasCaption(), tc.expResult) + }) + } +} + +func TestMessageHasAuthorSignature(t *testing.T) { + for _, tc := range []struct { + name string + message Message + expResult bool + }{{ + name: "true", + message: Message{AuthorSignature: "Editor"}, + expResult: true, + }, { + name: "false", + message: Message{Text: "hello, world!"}, + expResult: false, + }, { + name: "empty", + expResult: false, + }} { + tc := tc + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.message.HasAuthorSignature(), tc.expResult) + }) + } +} + +func TestMessageIsEvent(t *testing.T) { + for _, tc := range []struct { + name string + message Message + expResult bool + }{{ + name: "ChannelChatCreated", + message: Message{ChannelChatCreated: true}, + expResult: true, + }, { + name: "DeleteChatPhoto", + message: Message{DeleteChatPhoto: true}, + expResult: true, + }, { + name: "GroupChatCreated", + message: Message{GroupChatCreated: true}, + expResult: true, + }, { + name: "LeftChatMember", + message: Message{LeftChatMember: &User{ID: 42}}, + expResult: true, + }, { + name: "NewChatMembers", + message: Message{NewChatMembers: []*User{{ID: 42}}}, + expResult: true, + }, { + name: "NewChatTitle", + message: Message{NewChatTitle: "Меня заставили создать эту группу"}, + expResult: true, + }, { + name: "SupergroupChatCreated", + message: Message{SupergroupChatCreated: true}, + expResult: true, + }, { + name: "NewChatPhoto", + message: Message{NewChatPhoto: []*PhotoSize{{FileID: "abc"}}}, + expResult: true, + }, { + name: "false", + message: Message{Text: "hello, world!"}, + expResult: false, + }, { + name: "empty", + expResult: false, + }} { + tc := tc + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.message.IsEvent(), tc.expResult) + }) + } +} + +func TestMessageEntityIsBold(t *testing.T) { + for _, tc := range []struct { + name string + entity MessageEntity + expResult bool + }{{ + name: "true", + entity: MessageEntity{Type: EntityBold}, + expResult: true, + }, { + name: "false", + entity: MessageEntity{Type: EntityMention}, + expResult: false, + }, { + name: "empty", + expResult: false, + }} { + tc := tc + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.entity.IsBold(), tc.expResult) + }) + } +} + +func TestMessageEntityIsBotCommand(t *testing.T) { + for _, tc := range []struct { + name string + entity MessageEntity + expResult bool + }{{ + name: "true", + entity: MessageEntity{Type: EntityBotCommand}, + expResult: true, + }, { + name: "false", + entity: MessageEntity{Type: EntityMention}, + expResult: false, + }, { + name: "empty", + expResult: false, + }} { + tc := tc + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.entity.IsBotCommand(), tc.expResult) + }) + } +} + +func TestMessageEntityIsCashtag(t *testing.T) { + for _, tc := range []struct { + name string + entity MessageEntity + expResult bool + }{{ + name: "true", + entity: MessageEntity{Type: EntityCashtag}, + expResult: true, + }, { + name: "false", + entity: MessageEntity{Type: EntityMention}, + expResult: false, + }, { + name: "empty", + expResult: false, + }} { + tc := tc + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.entity.IsCashtag(), tc.expResult) + }) + } +} + +func TestMessageEntityIsCode(t *testing.T) { + for _, tc := range []struct { + name string + entity MessageEntity + expResult bool + }{{ + name: "true", + entity: MessageEntity{Type: EntityCode}, + expResult: true, + }, { + name: "false", + entity: MessageEntity{Type: EntityMention}, + expResult: false, + }, { + name: "empty", + expResult: false, + }} { + tc := tc + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.entity.IsCode(), tc.expResult) + }) + } +} + +func TestMessageEntityIsEmail(t *testing.T) { + for _, tc := range []struct { + name string + entity MessageEntity + expResult bool + }{{ + name: "true", + entity: MessageEntity{Type: EntityEmail}, + expResult: true, + }, { + name: "false", + entity: MessageEntity{Type: EntityMention}, + expResult: false, + }, { + name: "empty", + expResult: false, + }} { + tc := tc + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.entity.IsEmail(), tc.expResult) + }) + } +} + +func TestMessageEntityIsHashtag(t *testing.T) { + for _, tc := range []struct { + name string + entity MessageEntity + expResult bool + }{{ + name: "true", + entity: MessageEntity{Type: EntityHashtag}, + expResult: true, + }, { + name: "false", + entity: MessageEntity{Type: EntityMention}, + expResult: false, + }, { + name: "empty", + expResult: false, + }} { + tc := tc + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.entity.IsHashtag(), tc.expResult) + }) + } +} + +func TestMessageEntityIsItalic(t *testing.T) { + for _, tc := range []struct { + name string + entity MessageEntity + expResult bool + }{{ + name: "true", + entity: MessageEntity{Type: EntityItalic}, + expResult: true, + }, { + name: "false", + entity: MessageEntity{Type: EntityMention}, + expResult: false, + }, { + name: "empty", + expResult: false, + }} { + tc := tc + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.entity.IsItalic(), tc.expResult) + }) + } +} + +func TestMessageEntityIsMention(t *testing.T) { + for _, tc := range []struct { + name string + entity MessageEntity + expResult bool + }{{ + name: "true", + entity: MessageEntity{Type: EntityMention}, + expResult: true, + }, { + name: "false", + entity: MessageEntity{Type: EntityURL}, + expResult: false, + }, { + name: "empty", + expResult: false, + }} { + tc := tc + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.entity.IsMention(), tc.expResult) + }) + } +} + +func TestMessageEntityIsPhoneNumber(t *testing.T) { + for _, tc := range []struct { + name string + entity MessageEntity + expResult bool + }{{ + name: "true", + entity: MessageEntity{Type: EntityPhoneNumber}, + expResult: true, + }, { + name: "false", + entity: MessageEntity{Type: EntityMention}, + expResult: false, + }, { + name: "empty", + expResult: false, + }} { + tc := tc + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.entity.IsPhoneNumber(), tc.expResult) + }) + } +} + +func TestMessageEntityIsPre(t *testing.T) { + for _, tc := range []struct { + name string + entity MessageEntity + expResult bool + }{{ + name: "true", + entity: MessageEntity{Type: EntityPre}, + expResult: true, + }, { + name: "false", + entity: MessageEntity{Type: EntityMention}, + expResult: false, + }, { + name: "empty", + expResult: false, + }} { + tc := tc + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.entity.IsPre(), tc.expResult) + }) + } +} + +func TestMessageEntityIsStrikethrough(t *testing.T) { + for _, tc := range []struct { + name string + entity MessageEntity + expResult bool + }{{ + name: "true", + entity: MessageEntity{Type: EntityStrikethrough}, + expResult: true, + }, { + name: "false", + entity: MessageEntity{Type: EntityMention}, + expResult: false, + }, { + name: "empty", + expResult: false, + }} { + tc := tc + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.entity.IsStrikethrough(), tc.expResult) + }) + } +} + +func TestMessageEntityIsTextLink(t *testing.T) { + for _, tc := range []struct { + name string + entity MessageEntity + expResult bool + }{{ + name: "true", + entity: MessageEntity{Type: EntityTextLink}, + expResult: true, + }, { + name: "false", + entity: MessageEntity{Type: EntityMention}, + expResult: false, + }, { + name: "empty", + expResult: false, + }} { + tc := tc + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.entity.IsTextLink(), tc.expResult) + }) + } +} + +func TestMessageEntityIsTextMention(t *testing.T) { + for _, tc := range []struct { + name string + entity MessageEntity + expResult bool + }{{ + name: "true", + entity: MessageEntity{Type: EntityTextMention}, + expResult: true, + }, { + name: "false", + entity: MessageEntity{Type: EntityMention}, + expResult: false, + }, { + name: "empty", + expResult: false, + }} { + tc := tc + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.entity.IsTextMention(), tc.expResult) + }) + } +} + +func TestMessageEntityIsUnderline(t *testing.T) { + for _, tc := range []struct { + name string + entity MessageEntity + expResult bool + }{{ + name: "true", + entity: MessageEntity{Type: EntityUnderline}, + expResult: true, + }, { + name: "false", + entity: MessageEntity{Type: EntityMention}, + expResult: false, + }, { + name: "empty", + expResult: false, + }} { + tc := tc + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.entity.IsUnderline(), tc.expResult) + }) + } +} + +func TestMessageEntityIsURL(t *testing.T) { + for _, tc := range []struct { + name string + entity MessageEntity + expResult bool + }{{ + name: "true", + entity: MessageEntity{Type: EntityURL}, + expResult: true, + }, { + name: "false", + entity: MessageEntity{Type: EntityMention}, + expResult: false, + }, { + name: "empty", + expResult: false, + }} { + tc := tc + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.entity.IsURL(), tc.expResult) + }) + } +} + +func TestMessageEntityParseURL(t *testing.T) { + link := http.AcquireURI() + defer http.ReleaseURI(link) + + link.Update("https://toby3d.me") + + for _, tc := range []struct { + name string + entity MessageEntity + text string + expResult *http.URI + }{{ + name: "valid", + entity: MessageEntity{Type: EntityURL, Length: len(link.String())}, + text: link.String(), + expResult: link, + }, { + name: "other", + entity: MessageEntity{Type: EntityTextLink}, + text: link.String(), + expResult: nil, + }, { + name: "wrong text", + entity: MessageEntity{Type: EntityURL, Length: len(link.String())}, + text: "wtf", + expResult: nil, + }, { + name: "empty", + text: "", + expResult: nil, + }} { + tc := tc + t.Run(tc.name, func(t *testing.T) { + if tc.expResult == nil { + assert.Nil(t, tc.entity.ParseURL(tc.text)) + return + } + + assert.Equal(t, tc.entity.ParseURL(tc.text).String(), tc.expResult.String()) + }) + } +} + +func TestMessageEntityTextLink(t *testing.T) { + link := http.AcquireURI() + defer http.ReleaseURI(link) + + link.Update("https://toby3d.me") + + for _, tc := range []struct { + name string + entity MessageEntity + expResult *http.URI + }{{ + name: "valid", + entity: MessageEntity{Type: EntityTextLink, URL: link.String()}, + expResult: link, + }, { + name: "other", + entity: MessageEntity{Type: EntityURL}, + expResult: nil, + }, { + name: "empty", + expResult: nil, + }} { + tc := tc + t.Run(tc.name, func(t *testing.T) { + if tc.expResult == nil { + assert.Nil(t, tc.entity.TextLink()) + return + } + + assert.Equal(t, tc.entity.TextLink().String(), tc.expResult.String()) + }) + } +} + +func TestAudioFullName(t *testing.T) { + for _, tc := range []struct { + name string + audio Audio + separator string + expResult string + }{{ + name: "full", + audio: Audio{Performer: "One-Aloner", Title: "Monitor"}, + separator: " | ", + expResult: "One-Aloner | Monitor", + }, { + name: "performer", + audio: Audio{Performer: "One-Aloner"}, + separator: " | ", + expResult: "One-Aloner | " + DefaultAudioTitle, + }, { + name: "title", + audio: Audio{Title: "Monitor"}, + separator: " | ", + expResult: "Monitor", + }, { + name: "without separator", + audio: Audio{Performer: "One-Aloner", Title: "Monitor"}, + expResult: "One-Aloner" + DefaultAudioSeparator + "Monitor", + }, { + name: "empty", + expResult: DefaultAudioTitle, + }} { + tc := tc + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.audio.FullName(tc.separator), tc.expResult) + }) + } +} + +func TestAudioHasPerformer(t *testing.T) { + for _, tc := range []struct { + name string + audio Audio + expResult bool + }{{ + name: "true", + audio: Audio{Performer: "One-Aloner", Title: "Monitor"}, + expResult: true, + }, { + name: "false", + audio: Audio{Title: "Monitor"}, + expResult: false, + }} { + tc := tc + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.audio.HasPerformer(), tc.expResult) + }) + } +} + +func TestAudioHasTitle(t *testing.T) { + for _, tc := range []struct { + name string + audio Audio + expResult bool + }{{ + name: "true", + audio: Audio{Performer: "One-Aloner", Title: "Monitor"}, + expResult: true, + }, { + name: "false", + audio: Audio{Performer: "One-Aloner"}, + expResult: false, + }} { + tc := tc + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.audio.HasTitle(), tc.expResult) + }) + } +} + +func TestAudioHasThumb(t *testing.T) { + for _, tc := range []struct { + name string + audio Audio + expResult bool + }{{ + name: "true", + audio: Audio{Thumb: &PhotoSize{FileID: "abc"}}, + expResult: true, + }, { + name: "false", + expResult: false, + }} { + tc := tc + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.audio.HasThumb(), tc.expResult) + }) + } +} + +func TestAudioFile(t *testing.T) { + for _, tc := range []struct { + name string + audio Audio + expResult File + }{{ + name: "valid", + audio: Audio{FileID: "abc", FileUniqueID: "really_abc", FileSize: 42}, + expResult: File{FileID: "abc", FileUniqueID: "really_abc", FileSize: 42}, + }, { + name: "empty", + expResult: File{}, + }} { + tc := tc + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.audio.File(), tc.expResult) + }) + } +} + +func TestDocumentHasThumb(t *testing.T) { + for _, tc := range []struct { + name string + document Document + expResult bool + }{{ + name: "true", + document: Document{Thumb: &PhotoSize{FileID: "abc"}}, + expResult: true, + }, { + name: "false", + expResult: false, + }} { + tc := tc + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.document.HasThumb(), tc.expResult) + }) + } +} + +func TestDocumentFile(t *testing.T) { + for _, tc := range []struct { + name string + document Document + expResult File + }{{ + name: "valid", + document: Document{FileID: "abc", FileUniqueID: "really_abc", FileSize: 42}, + expResult: File{FileID: "abc", FileUniqueID: "really_abc", FileSize: 42}, + }, { + name: "empty", + expResult: File{}, + }} { + tc := tc + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.document.File(), tc.expResult) + }) + } +} + +func TestVideoHasThumb(t *testing.T) { + for _, tc := range []struct { + name string + video Video + expResult bool + }{{ + name: "true", + video: Video{Thumb: &PhotoSize{FileID: "abc"}}, + expResult: true, + }, { + name: "false", + expResult: false, + }} { + tc := tc + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.video.HasThumb(), tc.expResult) + }) + } +} + +func TestVideoFile(t *testing.T) { + for _, tc := range []struct { + name string + video Video + expResult File + }{{ + name: "valid", + video: Video{FileID: "abc", FileUniqueID: "really_abc", FileSize: 42}, + expResult: File{FileID: "abc", FileUniqueID: "really_abc", FileSize: 42}, + }, { + name: "empty", + expResult: File{}, + }} { + tc := tc + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.video.File(), tc.expResult) + }) + } +} + +func TestAnimationHasThumb(t *testing.T) { + for _, tc := range []struct { + name string + animation Animation + expResult bool + }{{ + name: "true", + animation: Animation{Thumb: &PhotoSize{FileID: "abc"}}, + expResult: true, + }, { + name: "false", + expResult: false, + }} { + tc := tc + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.animation.HasThumb(), tc.expResult) + }) + } +} + +func TestAnimationFile(t *testing.T) { + for _, tc := range []struct { + name string + animation Animation + expResult File + }{{ + name: "valid", + animation: Animation{FileID: "abc", FileUniqueID: "really_abc", FileSize: 42}, + expResult: File{FileID: "abc", FileUniqueID: "really_abc", FileSize: 42}, + }, { + name: "empty", + expResult: File{}, + }} { + tc := tc + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.animation.File(), tc.expResult) + }) + } +} + +func TestVoiceFile(t *testing.T) { + for _, tc := range []struct { + name string + voice Voice + expResult File + }{{ + name: "valid", + voice: Voice{FileID: "abc", FileUniqueID: "really_abc", FileSize: 42}, + expResult: File{FileID: "abc", FileUniqueID: "really_abc", FileSize: 42}, + }, { + name: "empty", + expResult: File{}, + }} { + tc := tc + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.voice.File(), tc.expResult) + }) + } +} + +func TestVideoNoteHasThumb(t *testing.T) { + for _, tc := range []struct { + name string + videoNote VideoNote + expResult bool + }{{ + name: "true", + videoNote: VideoNote{Thumb: &PhotoSize{FileID: "abc"}}, + expResult: true, + }, { + name: "false", + expResult: false, + }} { + tc := tc + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.videoNote.HasThumb(), tc.expResult) + }) + } +} + +func TestVideoNoteFile(t *testing.T) { + for _, tc := range []struct { + name string + videoNote VideoNote + expResult File + }{{ + name: "valid", + videoNote: VideoNote{FileID: "abc", FileUniqueID: "really_abc", FileSize: 42}, + expResult: File{FileID: "abc", FileUniqueID: "really_abc", FileSize: 42}, + }, { + name: "empty", + expResult: File{}, + }} { + tc := tc + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.videoNote.File(), tc.expResult) + }) + } +} + +func TestContactFullName(t *testing.T) { + for _, tc := range []struct { + name string + contact Contact + expResult string + }{{ + name: "full", + contact: Contact{FirstName: "Maxim", LastName: "Lebedev"}, + expResult: "Maxim Lebedev", + }, { + name: "first", + contact: Contact{FirstName: "Maxim"}, + expResult: "Maxim", + }, { + name: "last", + contact: Contact{LastName: "Lebedev"}, + expResult: "", + }, { + name: "false", + expResult: "", + }} { + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.contact.FullName(), tc.expResult) + }) + } +} + +func TestContactHasLastName(t *testing.T) { + for _, tc := range []struct { + name string + contact Contact + expResult bool + }{{ + name: "true", + contact: Contact{FirstName: "Maxim", LastName: "Lebedev"}, + expResult: true, + }, { + name: "false", + contact: Contact{FirstName: "Maxim"}, + expResult: false, + }} { + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.contact.HasLastName(), tc.expResult) + }) + } +} + +func TestContactInTelegram(t *testing.T) { + for _, tc := range []struct { + name string + contact Contact + expResult bool + }{{ + name: "true", + contact: Contact{UserID: 42}, + expResult: true, + }, { + name: "false", + expResult: false, + }} { + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.contact.InTelegram(), tc.expResult) + }) + } +} + +func TestContactHasVCard(t *testing.T) { + for _, tc := range []struct { + name string + contact Contact + expResult bool + }{{ + name: "true", + contact: Contact{ + VCard: "BEGIN:VCARD\nVERSION:3.0\nFN;CHARSET=UTF-8:Maxim Maksimovich Lebedev\n" + + "N;CHARSET=UTF-8:Lebedev;Maxim;Maksimovich;;\nNICKNAME;CHARSET=UTF-8:toby3d\n" + + "GENDER:M\nURL;CHARSET=UTF-8:https://toby3d.me\nREV:2020-01-08T01:31:36.277Z\n" + + "END:VCARD", + }, + expResult: true, + }, { + name: "false", + expResult: false, + }} { + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.contact.HasVCard(), tc.expResult) + }) + } +} + +func TestPollVotesCount(t *testing.T) { + for _, tc := range []struct { + name string + poll Poll + expResult int + }{{ + name: "true", + poll: Poll{Options: []*PollOption{ + &PollOption{Text: "a", VoterCount: 24}, + &PollOption{Text: "b", VoterCount: 42}, + }}, + expResult: 66, + }, { + name: "true", + poll: Poll{Options: []*PollOption{ + &PollOption{Text: "a", VoterCount: 10}, + &PollOption{Text: "b", VoterCount: 0}, + &PollOption{Text: "c", VoterCount: 120}, + }}, + expResult: 130, + }} { + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.poll.VotesCount(), tc.expResult) + }) + } +} + +func TestChatPhotoSmallFile(t *testing.T) { + for _, tc := range []struct { + name string + chatPhoto ChatPhoto + expResult File + }{{ + name: "valid", + chatPhoto: ChatPhoto{SmallFileID: "abc", SmallFileUniqueID: "really_abc"}, + expResult: File{FileID: "abc", FileUniqueID: "really_abc"}, + }, { + name: "empty", + expResult: File{}, + }} { + tc := tc + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.chatPhoto.SmallFile(), tc.expResult) + }) + } +} + +func TestChatPhotoBigFile(t *testing.T) { + for _, tc := range []struct { + name string + chatPhoto ChatPhoto + expResult File + }{{ + name: "valid", + chatPhoto: ChatPhoto{BigFileID: "abc", BigFileUniqueID: "really_abc"}, + expResult: File{FileID: "abc", FileUniqueID: "really_abc"}, + }, { + name: "empty", + expResult: File{}, + }} { + tc := tc + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.chatPhoto.BigFile(), tc.expResult) + }) + } +} + +func TestChatMemberIsAdministrator(t *testing.T) { + for _, tc := range []struct { + name string + chatMember ChatMember + expResult bool + }{{ + name: "true", + chatMember: ChatMember{Status: StatusAdministrator}, + expResult: true, + }, { + name: "false", + chatMember: ChatMember{Status: StatusCreator}, + expResult: false, + }} { + tc := tc + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.chatMember.IsAdministrator(), tc.expResult) + }) + } +} + +func TestChatMemberIsCreator(t *testing.T) { + for _, tc := range []struct { + name string + chatMember ChatMember + expResult bool + }{{ + name: "true", + chatMember: ChatMember{Status: StatusCreator}, + expResult: true, + }, { + name: "false", + chatMember: ChatMember{Status: StatusAdministrator}, + expResult: false, + }} { + tc := tc + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.chatMember.IsCreator(), tc.expResult) + }) + } +} + +func TestChatMemberIsKicked(t *testing.T) { + for _, tc := range []struct { + name string + chatMember ChatMember + expResult bool + }{{ + name: "true", + chatMember: ChatMember{Status: StatusKicked}, + expResult: true, + }, { + name: "false", + chatMember: ChatMember{Status: StatusMember}, + expResult: false, + }} { + tc := tc + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.chatMember.IsKicked(), tc.expResult) + }) + } +} + +func TestChatMemberIsLeft(t *testing.T) { + for _, tc := range []struct { + name string + chatMember ChatMember + expResult bool + }{{ + name: "true", + chatMember: ChatMember{Status: StatusLeft}, + expResult: true, + }, { + name: "false", + chatMember: ChatMember{Status: StatusMember}, + expResult: false, + }} { + tc := tc + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.chatMember.IsLeft(), tc.expResult) + }) + } +} + +func TestChatMemberIsRestricted(t *testing.T) { + for _, tc := range []struct { + name string + chatMember ChatMember + expResult bool + }{{ + name: "true", + chatMember: ChatMember{Status: StatusRestricted}, + expResult: true, + }, { + name: "false", + chatMember: ChatMember{Status: StatusMember}, + expResult: false, + }} { + tc := tc + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.chatMember.IsRestricted(), tc.expResult) + }) + } +} + +func TestChatMemberUntilTime(t *testing.T) { + now := time.Now().AddDate(0, 1, 0).Round(time.Second) + + for _, tc := range []struct { + name string + chatMember ChatMember + expResult time.Time + }{{ + name: "valid", + chatMember: ChatMember{Status: StatusMember, UntilDate: now.Unix()}, + expResult: now, + }, { + name: "empty", + chatMember: ChatMember{Status: StatusMember}, + expResult: time.Unix(0, 0), + }} { + tc := tc + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.chatMember.UntilTime(), tc.expResult) + }) + } +} + +func TestInputFileIsFileID(t *testing.T) { + t.Run("true", func(t *testing.T) { + f := InputFile{ID: "abc"} + assert.True(t, f.IsFileID()) + }) + t.Run("false", func(t *testing.T) { + f := InputFile{} + assert.False(t, f.IsFileID()) + }) +} + +func TestInputFileIsURI(t *testing.T) { + t.Run("true", func(t *testing.T) { + u := http.AcquireURI() + defer http.ReleaseURI(u) + u.Update("https://toby3d.me/image.jpeg") + f := InputFile{URI: u} + assert.True(t, f.IsURI()) + }) + t.Run("false", func(t *testing.T) { + f := InputFile{} + assert.False(t, f.IsURI()) + }) +} + +func TestInputFileIsAttachment(t *testing.T) { + file, err := ioutil.TempFile(os.TempDir(), "photo_*.jpeg") + assert.NoError(t, err) + defer os.RemoveAll(file.Name()) + + t.Run("true", func(t *testing.T) { + f := InputFile{Attachment: file} + assert.True(t, f.IsAttachment()) + }) + t.Run("false", func(t *testing.T) { + f := InputFile{} + assert.False(t, f.IsAttachment()) + }) +} + +func TestInputFileMarshalJSON(t *testing.T) { + u := http.AcquireURI() + defer http.ReleaseURI(u) + u.Update("https://toby3d.me/image.jpeg") + + file, err := ioutil.TempFile(os.TempDir(), "photo_*.jpeg") + assert.NoError(t, err) + defer os.RemoveAll(file.Name()) + + _, fileName := filepath.Split(file.Name()) + + for _, tc := range []struct { + name string + inputFile InputFile + expResult string + }{{ + name: "id", + inputFile: InputFile{ID: "abc"}, + expResult: "abc", + }, { + name: "uri", + inputFile: InputFile{URI: u}, + expResult: u.String(), + }, { + name: "attach", + inputFile: InputFile{Attachment: file}, + expResult: SchemeAttach + "://" + fileName, + }, { + name: "empty", + inputFile: InputFile{}, + expResult: "", + }} { + tc := tc + t.Run(tc.name, func(t *testing.T) { + src, err := tc.inputFile.MarshalJSON() + assert.NoError(t, err) + assert.Equal(t, tc.expResult, string(src)) + }) + } +} diff --git a/unban.go b/unban.go deleted file mode 100644 index fc50158..0000000 --- a/unban.go +++ /dev/null @@ -1,33 +0,0 @@ -package telegram - -// UnbanChatMemberParameters represents data for UnbanChatMember method. -type UnbanChatMemberParameters struct { - // Unique identifier for the target chat - ChatID int64 `json:"chat_id"` - - UserID int `json:"user_id"` -} - -// UnbanChatMember unban a previously kicked user in a supergroup or channel. The -// user will not return to the group or channel automatically, but will be able -// to join via link, etc. The bot must be an administrator for this to work. -// Returns True on success. -func (bot *Bot) UnbanChatMember(chatID int64, userID int) (bool, error) { - params := UnbanChatMemberParameters{ - ChatID: chatID, - UserID: userID, - } - dst, err := parser.Marshal(¶ms) - if err != nil { - return false, err - } - - resp, err := bot.request(dst, MethodUnbanChatMember) - if err != nil { - return false, err - } - - var ok bool - err = parser.Unmarshal(resp.Result, &ok) - return ok, err -} diff --git a/unpin.go b/unpin.go deleted file mode 100644 index e623168..0000000 --- a/unpin.go +++ /dev/null @@ -1,26 +0,0 @@ -package telegram - -// UnpinChatMessageParameters represents data for UnpinChatMessage method. -type UnpinChatMessageParameters struct { - // Unique identifier for the target chat - ChatID int64 `json:"chat_id"` -} - -// UnpinChatMessage unpin a message in a supergroup chat. The bot must be an -// administrator in the chat for this to work and must have the appropriate admin -// rights. Returns True on success. -func (bot *Bot) UnpinChatMessage(chatID int64) (bool, error) { - dst, err := parser.Marshal(&UnpinChatMessageParameters{ChatID: chatID}) - if err != nil { - return false, err - } - - resp, err := bot.request(dst, MethodUnpinChatMessage) - if err != nil { - return false, err - } - - var ok bool - err = parser.Unmarshal(resp.Result, &ok) - return ok, err -} diff --git a/updates.go b/updates.go new file mode 100644 index 0000000..b405e61 --- /dev/null +++ b/updates.go @@ -0,0 +1,299 @@ +package telegram + +import ( + "time" + + http "github.com/valyala/fasthttp" +) + +type ( + // Update represents an incoming update. + // + // At most one of the optional parameters can be present in any given update. + Update struct { + // The update‘s unique identifier. Update identifiers start from a + // certain positive number and increase sequentially. This ID becomes + // especially handy if you’re using Webhooks, since it allows you to + // ignore repeated updates or to restore the correct update sequence, + // should they get out of order. + UpdateID int `json:"update_id"` + + // New incoming message of any kind — text, photo, sticker, etc. + Message *Message `json:"message,omitempty"` + + // New version of a message that is known to the bot and was edited + EditedMessage *Message `json:"edited_message,omitempty"` + + // New incoming channel post of any kind — text, photo, sticker, etc. + ChannelPost *Message `json:"channel_post,omitempty"` + + // New version of a channel post that is known to the bot and was edited + EditedChannelPost *Message `json:"adited_channel_post,omitempty"` + + // New incoming inline query + InlineQuery *InlineQuery `json:"inline_query,omitempty"` + + // The result of an inline query that was chosen by a user and sent to + // their chat partner. + ChosenInlineResult *ChosenInlineResult `json:"chosen_inline_result,omitempty"` + + // New incoming callback query + CallbackQuery *CallbackQuery `json:"callback_query,omitempty"` + + // New incoming shipping query. Only for invoices with flexible price + ShippingQuery *ShippingQuery `json:"shipping_query,omitempty"` + + // New incoming pre-checkout query. Contains full information about + // checkout + PreCheckoutQuery *PreCheckoutQuery `json:"pre_checkout_query,omitempty"` + + // New poll state. Bots receive only updates about polls, which are sent or stopped by the bot + Poll *Poll `json:"poll,omitempty"` + } + + // WebhookInfo contains information about the current status of a webhook. + WebhookInfo struct { + // Webhook URL, may be empty if webhook is not set up + URL string `json:"url"` + + // Error message in human-readable format for the most recent error that + // happened when trying to deliver an update via webhook + LastErrorMessage string `json:"last_error_message,omitempty"` + + // True, if a custom certificate was provided for webhook certificate + // checks + HasCustomCertificate bool `json:"has_custom_certificate"` + + // Number of updates awaiting delivery + PendingUpdateCount int `json:"pending_update_count"` + + // Maximum allowed number of simultaneous HTTPS connections to the + // webhook for update delivery + MaxConnections int `json:"max_connections,omitempty"` + + // Unix time for the most recent error that happened when trying to + // deliver an update via webhook + LastErrorDate int64 `json:"last_error_date,omitempty"` + + // A list of update types the bot is subscribed to. Defaults to all + // update types + AllowedUpdates []string `json:"allowed_updates,omitempty"` + } + + // GetUpdatesParameters represents data for GetUpdates method. + GetUpdates struct { + // Identifier of the first update to be returned. Must be greater by one than the highest among the + // identifiers of previously received updates. By default, updates starting with the earliest unconfirmed + // update are returned. An update is considered confirmed as soon as getUpdates is called with an offset + // higher than its update_id. The negative offset can be specified to retrieve updates starting from -offset + // update from the end of the updates queue. All previous updates will forgotten. + Offset int `json:"offset,omitempty"` + + // Limits the number of updates to be retrieved. Values between 1—100 are accepted. Defaults to 100. + Limit int `json:"limit,omitempty"` + + // Timeout in seconds for long polling. Defaults to 0, i.e. usual short polling. Should be positive, short + // polling should be used for testing purposes only. + Timeout int `json:"timeout,omitempty"` + + // List the types of updates you want your bot to receive. For example, specify ["message", + // "edited_channel_post", "callback_query"] to only receive updates of these types. See Update for a complete + // list of available update types. Specify an empty list to receive all updates regardless of type (default). + // If not specified, the previous setting will be used. + // + // Please note that this parameter doesn't affect updates created before the call to the getUpdates, so + // unwanted updates may be received for a short period of time. + AllowedUpdates []string `json:"allowed_updates,omitempty"` + } + + // SetWebhookParameters represents data for SetWebhook method. + SetWebhook struct { + // HTTPS url to send updates to. Use an empty string to remove webhook + // integration + URL string `json:"url"` + + // Upload your public key certificate so that the root certificate in use can + // be checked. See our self-signed guide for details. + Certificate InputFile `json:"certificate,omitempty"` + + // Maximum allowed number of simultaneous HTTPS connections to the webhook + // for update delivery, 1-100. Defaults to 40. Use lower values to limit the + // load on your bot‘s server, and higher values to increase your bot’s + // throughput. + MaxConnections int `json:"max_connections,omitempty"` + + // List the types of updates you want your bot to receive. For example, + // specify [“message”, “edited_channel_post”, “callback_query”] to only + // receive updates of these types. See Update for a complete list of + // available update types. Specify an empty list to receive all updates + // regardless of type (default). If not specified, the previous setting will + // be used. + // + // Please note that this parameter doesn't affect updates created before the + // call to the setWebhook, so unwanted updates may be received for a short + // period of time. + AllowedUpdates []string `json:"allowed_updates,omitempty"` + } +) + +// GetUpdates receive incoming updates using long polling. An Array of Update objects is returned. +func (b *Bot) GetUpdates(p *GetUpdates) ([]*Update, error) { + src, err := b.Do(MethodGetUpdates, p) + if err != nil { + return nil, err + } + + resp := new(Response) + if err = b.marshler.Unmarshal(src, resp); err != nil { + return nil, err + } + + result := make([]*Update, 0) + if err = b.marshler.Unmarshal(resp.Result, &result); err != nil { + return nil, err + } + + return result, nil +} + +// SetWebhook specify a url and receive incoming updates via an outgoing webhook. Whenever there is an update for the bot, we will send an HTTPS POST request to the specified url, containing a JSON-serialized Update. In case of an unsuccessful request, we will give up after a reasonable amount of attempts. Returns true. +// +// If you'd like to make sure that the Webhook request comes from Telegram, we recommend using a secret path in the URL, e.g. https://www.example.com/. Since nobody else knows your bot‘s token, you can be pretty sure it’s us. +func (b *Bot) SetWebhook(p SetWebhook) (bool, error) { + var src []byte + var err error + + // if p.Certificate != nil { + // src, err = b.Upload(MethodSetWebhook, "certificate", "cert.pem", params.Certificate, args) + // } else { + src, err = b.Do(MethodSetWebhook, p) + // } + + if err != nil { + return false, err + } + + resp := new(Response) + if err = b.marshler.Unmarshal(src, resp); err != nil { + return false, err + } + + var result bool + if err = b.marshler.Unmarshal(resp.Result, &result); err != nil { + return false, err + } + + return result, nil +} + +// DeleteWebhook remove webhook integration if you decide to switch back to getUpdates. Returns True on success. Requires no parameters. +func (b *Bot) DeleteWebhook() (bool, error) { + src, err := b.Do(MethodDeleteWebhook, nil) + if err != nil { + return false, err + } + + resp := new(Response) + if err = b.marshler.Unmarshal(src, resp); err != nil { + return false, err + } + + var result bool + if err = b.marshler.Unmarshal(resp.Result, &result); err != nil { + return false, err + } + + return result, nil +} + +// GetWebhookInfo get current webhook status. Requires no parameters. On success, returns a WebhookInfo object. If the bot is using getUpdates, will return an object with the url field empty. +func (b *Bot) GetWebhookInfo() (*WebhookInfo, error) { + src, err := b.Do(MethodGetWebhookInfo, nil) + if err != nil { + return nil, err + } + + resp := new(Response) + if err = b.marshler.Unmarshal(src, resp); err != nil { + return nil, err + } + + result := new(WebhookInfo) + if err = b.marshler.Unmarshal(resp.Result, &result); err != nil { + return nil, err + } + + return result, nil +} + +// IsMessage checks that the current update is a message creation event. +func (u Update) IsMessage() bool { return u.Message != nil } + +// IsEditedMessage checks that the current update is a editing message event. +func (u Update) IsEditedMessage() bool { return u.EditedMessage != nil } + +// IsChannelPost checks that the current update is a post channel creation event. +func (u Update) IsChannelPost() bool { return u.ChannelPost != nil } + +// IsEditedChannelPost checks that the current update is a editing post channel event. +func (u Update) IsEditedChannelPost() bool { return u.EditedChannelPost != nil } + +// IsInlineQuery checks that the current update is a inline query update. +func (u Update) IsInlineQuery() bool { return u.InlineQuery != nil } + +// IsChosenInlineResult checks that the current update is a chosen inline result update. +func (u Update) IsChosenInlineResult() bool { return u.ChosenInlineResult != nil } + +// IsCallbackQuery checks that the current update is a callback query update. +func (u Update) IsCallbackQuery() bool { return u.CallbackQuery != nil } + +// IsShippingQuery checks that the current update is a shipping query update. +func (u Update) IsShippingQuery() bool { return u.ShippingQuery != nil } + +// IsPreCheckoutQuery checks that the current update is a pre checkout query update. +func (u Update) IsPreCheckoutQuery() bool { return u.PreCheckoutQuery != nil } + +// IsPoll checks that the current update is a poll update. +func (u Update) IsPoll() bool { return u.Poll != nil } + +// Type return update type for current update. +func (u Update) Type() string { + switch { + case u.IsCallbackQuery(): + return UpdateCallbackQuery + case u.IsChannelPost(): + return UpdateChannelPost + case u.IsChosenInlineResult(): + return UpdateChosenInlineResult + case u.IsEditedChannelPost(): + return UpdateEditedChannelPost + case u.IsEditedMessage(): + return UpdateEditedMessage + case u.IsInlineQuery(): + return UpdateInlineQuery + case u.IsMessage(): + return UpdateMessage + case u.IsPreCheckoutQuery(): + return UpdatePreCheckoutQuery + case u.IsShippingQuery(): + return UpdateShippingQuery + case u.IsPoll(): + return UpdatePoll + default: + return "" + } +} + +func (wi WebhookInfo) LastErrorTime() time.Time { return time.Unix(wi.LastErrorDate, 0) } + +func (wi WebhookInfo) HasURL() bool { return wi.URL != "" } + +func (wi WebhookInfo) URI() *http.URI { + if !wi.HasURL() { + return nil + } + + u := http.AcquireURI() + u.Update(wi.URL) + return u +} diff --git a/updates_test.go b/updates_test.go new file mode 100644 index 0000000..ef5f58b --- /dev/null +++ b/updates_test.go @@ -0,0 +1,208 @@ +package telegram + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + http "github.com/valyala/fasthttp" +) + +func TestUpdateIsMessage(t *testing.T) { + t.Run("true", func(t *testing.T) { + u := Update{Message: &Message{ID: 42}} + assert.True(t, u.IsMessage()) + }) + t.Run("false", func(t *testing.T) { + u := Update{} + assert.False(t, u.IsMessage()) + }) +} + +func TestUpdateIsEditedMessage(t *testing.T) { + t.Run("true", func(t *testing.T) { + u := Update{EditedMessage: &Message{ID: 42}} + assert.True(t, u.IsEditedMessage()) + }) + t.Run("false", func(t *testing.T) { + u := Update{} + assert.False(t, u.IsEditedMessage()) + }) +} + +func TestUpdateIsChannelPost(t *testing.T) { + t.Run("true", func(t *testing.T) { + u := Update{ChannelPost: &Message{ID: 42}} + assert.True(t, u.IsChannelPost()) + }) + t.Run("false", func(t *testing.T) { + u := Update{} + assert.False(t, u.IsChannelPost()) + }) +} + +func TestUpdateIsEditedChannelPost(t *testing.T) { + t.Run("true", func(t *testing.T) { + u := Update{EditedChannelPost: &Message{ID: 42}} + assert.True(t, u.IsEditedChannelPost()) + }) + t.Run("false", func(t *testing.T) { + u := Update{} + assert.False(t, u.IsEditedChannelPost()) + }) +} + +func TestUpdateIsInlineQuery(t *testing.T) { + t.Run("true", func(t *testing.T) { + u := Update{InlineQuery: &InlineQuery{Query: "abc"}} + assert.True(t, u.IsInlineQuery()) + }) + t.Run("false", func(t *testing.T) { + u := Update{} + assert.False(t, u.IsInlineQuery()) + }) +} + +func TestUpdateIsChosenInlineResult(t *testing.T) { + t.Run("true", func(t *testing.T) { + u := Update{ChosenInlineResult: &ChosenInlineResult{ResultID: "abc"}} + assert.True(t, u.IsChosenInlineResult()) + }) + t.Run("false", func(t *testing.T) { + u := Update{} + assert.False(t, u.IsChosenInlineResult()) + }) +} + +func TestUpdateIsCallbackQuery(t *testing.T) { + t.Run("true", func(t *testing.T) { + u := Update{CallbackQuery: &CallbackQuery{ID: "abc"}} + assert.True(t, u.IsCallbackQuery()) + }) + t.Run("false", func(t *testing.T) { + u := Update{} + assert.False(t, u.IsCallbackQuery()) + }) +} + +func TestUpdateIsShippingQuery(t *testing.T) { + t.Run("true", func(t *testing.T) { + u := Update{ShippingQuery: &ShippingQuery{ID: "abc"}} + assert.True(t, u.IsShippingQuery()) + }) + t.Run("false", func(t *testing.T) { + u := Update{} + assert.False(t, u.IsShippingQuery()) + }) +} + +func TestUpdateIsPreCheckoutQuery(t *testing.T) { + t.Run("true", func(t *testing.T) { + u := Update{PreCheckoutQuery: &PreCheckoutQuery{ID: "abc"}} + assert.True(t, u.IsPreCheckoutQuery()) + }) + t.Run("false", func(t *testing.T) { + u := Update{} + assert.False(t, u.IsPreCheckoutQuery()) + }) +} + +func TestUpdateIsPoll(t *testing.T) { + t.Run("true", func(t *testing.T) { + u := Update{Poll: &Poll{ID: "abc"}} + assert.True(t, u.IsPoll()) + }) + t.Run("false", func(t *testing.T) { + u := Update{} + assert.False(t, u.IsPoll()) + }) +} + +func TestUpdateType(t *testing.T) { + for _, tc := range []struct { + name string + update Update + expResult string + }{{ + name: UpdateCallbackQuery, + update: Update{CallbackQuery: &CallbackQuery{ID: "abc"}}, + expResult: UpdateCallbackQuery, + }, { + name: UpdateChannelPost, + update: Update{ChannelPost: &Message{ID: 42}}, + expResult: UpdateChannelPost, + }, { + name: UpdateChosenInlineResult, + update: Update{ChosenInlineResult: &ChosenInlineResult{Query: "query"}}, + expResult: UpdateChosenInlineResult, + }, { + name: UpdateEditedChannelPost, + update: Update{EditedChannelPost: &Message{ID: 42}}, + expResult: UpdateEditedChannelPost, + }, { + name: UpdateEditedMessage, + update: Update{EditedMessage: &Message{ID: 42}}, + expResult: UpdateEditedMessage, + }, { + name: UpdateInlineQuery, + update: Update{InlineQuery: &InlineQuery{ID: "abc"}}, + expResult: UpdateInlineQuery, + }, { + name: UpdateMessage, + update: Update{Message: &Message{ID: 42}}, + expResult: UpdateMessage, + }, { + name: UpdatePoll, + update: Update{Poll: &Poll{ID: "abc"}}, + expResult: UpdatePoll, + }, { + name: UpdatePreCheckoutQuery, + update: Update{PreCheckoutQuery: &PreCheckoutQuery{ID: "abc"}}, + expResult: UpdatePreCheckoutQuery, + }, { + name: UpdateShippingQuery, + update: Update{ShippingQuery: &ShippingQuery{ID: "abc"}}, + expResult: UpdateShippingQuery, + }, { + name: "other", + update: Update{}, + expResult: "", + }} { + tc := tc + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.update.Type(), tc.expResult) + }) + } +} + +func TestWebhookInfoLastErrorTime(t *testing.T) { + now := time.Now().Round(time.Second) + wi := WebhookInfo{LastErrorDate: now.Unix()} + assert.Equal(t, wi.LastErrorTime(), now) +} + +func TestWebhookInfoHasURL(t *testing.T) { + t.Run("true", func(t *testing.T) { + wi := WebhookInfo{URL: "https://bot.toby3d.me"} + assert.True(t, wi.HasURL()) + }) + t.Run("false", func(t *testing.T) { + wi := WebhookInfo{} + assert.False(t, wi.HasURL()) + }) +} + +func TestWebhookInfoURI(t *testing.T) { + u := http.AcquireURI() + defer http.ReleaseURI(u) + + u.Update("https://bot.toby3d.me") + t.Run("true", func(t *testing.T) { + wi := WebhookInfo{URL: u.String()} + assert.Equal(t, wi.URI().String(), u.String()) + }) + t.Run("false", func(t *testing.T) { + wi := WebhookInfo{} + assert.Nil(t, wi.URI()) + }) +} diff --git a/edit.go b/updating_messages.go similarity index 60% rename from edit.go rename to updating_messages.go index c92a683..95f8c0f 100644 --- a/edit.go +++ b/updating_messages.go @@ -1,34 +1,8 @@ package telegram type ( - // EditMessageLiveLocationParameters represents data for EditMessageLiveLocation - // method. - EditMessageLiveLocationParameters struct { - // Required if inline_message_id is not specified. Unique identifier for the - // target chat or username of the target channel (in the format - // @channelusername) - ChatID int64 `json:"chat_id,omitempty"` - - // Required if inline_message_id is not specified. Identifier of the sent - // message - MessageID int `json:"message_id,omitempty"` - - // Required if chat_id and message_id are not specified. Identifier of the - // inline message - InlineMessageID string `json:"inline_message_id,omitempty"` - - // Latitude of new location - Latitude float32 `json:"latitude"` - - // Longitude of new location - Longitude float32 `json:"longitude"` - - // A JSON-serialized object for a new inline keyboard. - ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` - } - // EditMessageTextParameters represents data for EditMessageText method. - EditMessageTextParameters struct { + EditMessageText struct { // Required if inline_message_id is not specified. Unique identifier for the // target chat or username of the target channel (in the format // @channelusername) @@ -57,7 +31,7 @@ type ( } // EditMessageCaptionParameters represents data for EditMessageCaption method. - EditMessageCaptionParameters struct { + EditMessageCaption struct { // Required if inline_message_id is not specified. Unique identifier for the // target chat or username of the target channel (in the format // @channelusername) @@ -83,7 +57,7 @@ type ( } // EditMessageMediaParameters represents data for EditMessageMedia method. - EditMessageMediaParameters struct { + EditMessageMedia struct { // Required if inline_message_id is not specified. Unique identifier for the target chat or username of the target channel (in the format @channelusername) ChatID int64 `json:"chat_id,omitempty"` @@ -101,7 +75,7 @@ type ( } // EditMessageReplyMarkupParameters represents data for EditMessageReplyMarkup method. - EditMessageReplyMarkupParameters struct { + EditMessageReplyMarkup struct { // Required if inline_message_id is not specified. Unique identifier for the // target chat or username of the target channel (in the format // @channelusername) @@ -118,81 +92,70 @@ type ( // A JSON-serialized object for an inline keyboard. ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` } + + StopPoll struct { + // Unique identifier for the target chat. A native poll can't be sent to a private chat. + ChatID int64 `json:"chat_id"` + + // Identifier of the original message with the poll + MessageID int `json:"message_id"` + + // A JSON-serialized object for a new message inline keyboard. + ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` + } + + // DeleteMessageParameters represents data for DeleteMessage method. + DeleteMessage struct { + // Unique identifier for the target chat + ChatID int64 `json:"chat_id"` + + // Identifier of the message to delete + MessageID int `json:"message_id"` + } ) -// NewLiveLocation creates EditMessageLiveLocationParameters only with required -// parameters. -func NewLiveLocation(latitude, longitude float32) *EditMessageLiveLocationParameters { - return &EditMessageLiveLocationParameters{ - Latitude: latitude, - Longitude: longitude, - } -} - -// NewMessageText creates EditMessageTextParameters only with required parameters. -func NewMessageText(text string) *EditMessageTextParameters { - return &EditMessageTextParameters{ - Text: text, - } -} - -// EditMessageLiveLocation edit live location messages sent by the bot or via the -// bot (for inline bots). A location can be edited until its live_period expires -// or editing is explicitly disabled by a call to stopMessageLiveLocation. On -// success, if the edited message was sent by the bot, the edited Message is -// returned, otherwise True is returned. -func (bot *Bot) EditMessageLiveLocation(params *EditMessageLiveLocationParameters) (*Message, error) { - dst, err := parser.Marshal(params) - if err != nil { - return nil, err - } - - resp, err := bot.request(dst, MethodEditMessageLiveLocation) - if err != nil { - return nil, err - } - - var msg Message - err = parser.Unmarshal(resp.Result, &msg) - return &msg, err -} - // EditMessageText edit text and game messages sent by the bot or via the bot // (for inline bots). On success, if edited message is sent by the bot, the // edited Message is returned, otherwise True is returned. -func (bot *Bot) EditMessageText(params *EditMessageTextParameters) (*Message, error) { - dst, err := parser.Marshal(params) +func (b *Bot) EditMessageText(p *EditMessageText) (*Message, error) { + src, err := b.Do(MethodEditMessageText, p) if err != nil { return nil, err } - resp, err := bot.request(dst, MethodEditMessageText) - if err != nil { + resp := new(Response) + if err = b.marshler.Unmarshal(src, resp); err != nil { return nil, err } - var msg Message - err = parser.Unmarshal(resp.Result, &msg) - return &msg, err + result := new(Message) + if err = b.marshler.Unmarshal(resp.Result, result); err != nil { + return nil, err + } + + return result, nil } // EditMessageCaption edit captions of messages sent by the bot or via the bot // (for inline bots). On success, if edited message is sent by the bot, the // edited Message is returned, otherwise True is returned. -func (bot *Bot) EditMessageCaption(params *EditMessageCaptionParameters) (*Message, error) { - dst, err := parser.Marshal(params) +func (b *Bot) EditMessageCaption(p *EditMessageCaption) (*Message, error) { + src, err := b.Do(MethodEditMessageCaption, p) if err != nil { return nil, err } - resp, err := bot.request(dst, MethodEditMessageCaption) - if err != nil { + resp := new(Response) + if err = b.marshler.Unmarshal(src, resp); err != nil { return nil, err } - var msg Message - err = parser.Unmarshal(resp.Result, &msg) - return &msg, err + result := new(Message) + if err = b.marshler.Unmarshal(resp.Result, result); err != nil { + return nil, err + } + + return result, nil } // EditMessageMedia edit audio, document, photo, or video messages. If a message @@ -201,37 +164,94 @@ func (bot *Bot) EditMessageCaption(params *EditMessageCaptionParameters) (*Messa // edited, new file can't be uploaded. Use previously uploaded file via its // file_id or specify a URL. On success, if the edited message was sent by the // bot, the edited Message is returned, otherwise True is returned. -func (b *Bot) EditMessageMedia(emmp *EditMessageMediaParameters) (*Message, error) { - src, err := parser.Marshal(emmp) +func (b *Bot) EditMessageMedia(p EditMessageMedia) (*Message, error) { + src, err := b.Do(MethodEditMessageMedia, p) if err != nil { return nil, err } - resp, err := b.request(src, MethodEditMessageMedia) - if err != nil { + resp := new(Response) + if err = b.marshler.Unmarshal(src, resp); err != nil { return nil, err } - var msg Message - err = parser.Unmarshal(resp.Result, &msg) - return &msg, err + result := new(Message) + if err = b.marshler.Unmarshal(resp.Result, result); err != nil { + return nil, err + } + + return result, nil } // EditMessageReplyMarkup edit only the reply markup of messages sent by the bot // or via the bot (for inline bots). On success, if edited message is sent by the // bot, the edited Message is returned, otherwise True is returned. -func (bot *Bot) EditMessageReplyMarkup(params *EditMessageReplyMarkupParameters) (*Message, error) { - dst, err := parser.Marshal(params) +func (b *Bot) EditMessageReplyMarkup(p EditMessageReplyMarkup) (*Message, error) { + src, err := b.Do(MethodEditMessageReplyMarkup, p) if err != nil { return nil, err } - resp, err := bot.request(dst, MethodEditMessageReplyMarkup) - if err != nil { + resp := new(Response) + if err = b.marshler.Unmarshal(src, resp); err != nil { return nil, err } - var msg Message - err = parser.Unmarshal(resp.Result, &msg) - return &msg, err + result := new(Message) + if err = b.marshler.Unmarshal(resp.Result, result); err != nil { + return nil, err + } + + return result, nil +} + +func (b *Bot) StopPoll(p StopPoll) (*Poll, error) { + src, err := b.Do(MethodStopPoll, p) + if err != nil { + return nil, err + } + + resp := new(Response) + if err = b.marshler.Unmarshal(src, resp); err != nil { + return nil, err + } + + result := new(Poll) + if err = b.marshler.Unmarshal(resp.Result, result); err != nil { + return nil, err + } + + return result, nil +} + +// DeleteMessage delete a message, including service messages, with the following limitations: +// +// - A message can only be deleted if it was sent less than 48 hours ago. +// - Bots can delete outgoing messages in private chats, groups, and supergroups. +// - Bots can delete incoming messages in private chats. +// - Bots granted can_post_messages permissions can delete outgoing messages in channels. +// - If the bot is an administrator of a group, it can delete any message there. +// - If the bot has can_delete_messages permission in a supergroup or a channel, it can delete any message there. +// +// Returns True on success. +func (b *Bot) DeleteMessage(chatID int64, messageID int) (bool, error) { + src, err := b.Do(MethodDeleteMessage, DeleteMessage{ + ChatID: chatID, + MessageID: messageID, + }) + if err != nil { + return false, err + } + + resp := new(Response) + if err = b.marshler.Unmarshal(src, resp); err != nil { + return false, err + } + + var result bool + if err = b.marshler.Unmarshal(resp.Result, &result); err != nil { + return false, err + } + + return result, nil } diff --git a/upload.go b/upload.go deleted file mode 100644 index 963c883..0000000 --- a/upload.go +++ /dev/null @@ -1,186 +0,0 @@ -package telegram - -import ( - "bytes" - "errors" - "io" - "mime/multipart" - "os" - "path" - - http "github.com/valyala/fasthttp" -) - -type UploadStickerFileParameters struct { - // User identifier of sticker file owner - UserID int `json:"user_id"` - - // Png image with the sticker, must be up to 512 kilobytes in size, - // dimensions must not exceed 512px, and either width or height - // must be exactly 512px. - PNGSticker interface{} `json:"png_sticker"` -} - -// ErrBadFileType describes error of the unsupported file data type for uploading -var ErrBadFileType = errors.New("bad file type") - -/* -Upload is a helper method which provide are three ways to send files (photos, stickers, audio, -media, etc.): - -1. If the file is already stored somewhere on the Telegram servers, you don't need to reupload it: -each file object has a file_id field, simply pass this file_id as a parameter instead of uploading. -There are no limits for files sent this way. - -2. Provide Telegram with an *fasthttp.URI for the file to be sent. Telegram will download and send the -file. 5 MB max size for photos and 20 MB max for other types of content. - -3. Post the file using multipart/form-data in the usual way that files are uploaded via the -browser. Use []byte or io.Reader for this. 10 MB max size for photos, 50 MB for other files. - -Sending by FileID - -* It is not possible to change the file type when resending by file_id. I.e. a video can't be sent -as a photo, a photo can't be sent as a document, etc. - -* It is not possible to resend thumbnails. - -* Resending a photo by file_id will send all of its sizes. - -* file_id is unique for each individual bot and can't be transferred from one bot to another. - -Sending by URL - -* When sending by *fasthttp.URI the target file must have the correct MIME type (e.g., audio/mpeg for -sendAudio, etc.). - -* In sendDocument, sending by URL will currently only work for gif, pdf and zip files. - -* To use SendVoice, the file must have the type audio/ogg and be no more than 1MB in size. 1–20MB -voice notes will be sent as files. - -* Other configurations may work but we can't guarantee that they will. -*/ -func (b *Bot) Upload(method, key, name string, file InputFile, args *http.Args) (*Response, error) { - buffer := bytes.NewBuffer(nil) - multi := multipart.NewWriter(buffer) - - requestURI := http.AcquireURI() - requestURI.SetScheme("https") - requestURI.SetHost("api.telegram.org") - requestURI.SetPath(path.Join("bot"+b.AccessToken, method)) - - args.VisitAll(func(key, value []byte) { - _ = multi.WriteField(string(key), string(value)) - }) - - if err := createFileField(multi, file, key, name); err != nil { - return nil, err - } - - if err := multi.Close(); err != nil { - return nil, err - } - - req := http.AcquireRequest() - defer http.ReleaseRequest(req) - req.SetBody(buffer.Bytes()) - req.Header.SetContentType(multi.FormDataContentType()) - req.Header.SetMethod("POST") - req.Header.SetRequestURI(requestURI.String()) - req.Header.SetUserAgent(path.Join("telegram", Version)) - req.Header.SetHostBytes(requestURI.Host()) - - resp := http.AcquireResponse() - defer http.ReleaseResponse(resp) - - if err := http.Do(req, resp); err != nil { - return nil, err - } - - var response Response - if err := parser.Unmarshal(resp.Body(), &response); err != nil { - return nil, err - } - - if !response.Ok { - return nil, errors.New(response.Description) - } - - return &response, nil -} - -func createFileField(w *multipart.Writer, file interface{}, key, val string) (err error) { - switch src := file.(type) { - case string: // Send FileID of file on disk path - return createFileFieldString(w, key, src) - case *http.URI: // Send by URL - return w.WriteField(key, src.String()) - case []byte: // Upload new - return createFileFieldRaw(w, key, val, bytes.NewReader(src)) - case io.Reader: // Upload new - return createFileFieldRaw(w, key, val, src) - default: - return ErrBadFileType - } -} - -func createFileFieldString(w *multipart.Writer, key, src string) (err error) { - _, err = os.Stat(src) - - switch { - case os.IsNotExist(err): - err = w.WriteField(key, src) - case os.IsExist(err): - err = uploadFromDisk(w, key, src) - } - - return -} - -func uploadFromDisk(w *multipart.Writer, key, src string) error { - file, err := os.Open(src) - if err != nil { - return err - } - defer func() { - _ = file.Close() - }() - - var formFile io.Writer - formFile, err = w.CreateFormFile(key, file.Name()) - if err != nil { - return err - } - - _, err = io.Copy(formFile, file) - return err -} - -func createFileFieldRaw(w *multipart.Writer, key, value string, src io.Reader) error { - field, err := w.CreateFormFile(key, value) - if err != nil { - return err - } - - _, err = io.Copy(field, src) - return err -} - -// UploadStickerFile upload a .png file with a sticker for later use in -// createNewStickerSet and addStickerToSet methods (can be used multiple times). -// Returns the uploaded File on success. -func (b *Bot) UploadStickerFile(userID int, pngSticker interface{}) (*File, error) { - args := http.AcquireArgs() - defer http.ReleaseArgs(args) - args.SetUint("user_id", userID) - - resp, err := b.Upload(MethodUploadStickerFile, TypeSticker, "sticker", pngSticker, args) - if err != nil { - return nil, err - } - - var f File - err = parser.Unmarshal(resp.Result, &f) - return &f, err -} diff --git a/utils.go b/utils.go index b00b9ca..49ee409 100644 --- a/utils.go +++ b/utils.go @@ -2,58 +2,33 @@ package telegram import ( "bytes" - "crypto/aes" - "crypto/cipher" - "crypto/rand" - "crypto/rsa" - "crypto/sha1" - "crypto/sha256" - "crypto/sha512" - "encoding/base64" - "errors" - "fmt" "log" "net" "path" - "sort" - "strconv" "strings" "time" + json "github.com/json-iterator/go" "github.com/kirillDanshin/dlog" http "github.com/valyala/fasthttp" - "golang.org/x/text/language" - "golang.org/x/text/message" ) -type ( - // Bot represents a bot user with access token getted from @BotFather and - // fasthttp.Client for requests. - Bot struct { - *User - AccessToken string - Client *http.Client - } +// Bot represents a bot user with access token getted from @BotFather. +type Bot struct { + *User + AccessToken string + Updates chan *Update - // UpdatesChannel is a channel for reading updates of bot. - UpdatesChannel <-chan Update - ShutdownFunc func() error -) - -const ( - DefaultAudioSeparator = " – " - DefaultAudioTitle = "[untitled]" -) - -var ErrNotEqual = errors.New("credentials hash and credentials data hash is not equal") + client *http.Client + marshler json.API +} // New creates a new default Bot structure based on the input access token. -func New(accessToken string) (*Bot, error) { - var err error - b := new(Bot) - b.SetClient(&defaultClient) +func New(accessToken string) (b *Bot, err error) { + b = new(Bot) + b.marshler = json.ConfigFastest + b.SetClient(&http.Client{}) b.AccessToken = accessToken - b.User, err = b.GetMe() return b, err } @@ -64,144 +39,22 @@ func (b *Bot) SetClient(newClient *http.Client) { b = new(Bot) } - b.Client = newClient + b.client = newClient } -// NewForceReply calls the response interface to the message. -func NewForceReply() *ForceReply { - return &ForceReply{ - ForceReply: true, - } -} - -// NewInlineMentionURL creates a url.URL for the mention user without username. -func NewInlineMentionURL(userID int) *http.URI { - link := http.AcquireURI() - link.Update("tg://user?id=" + strconv.Itoa(userID)) - return link -} - -func NewMarkdownBold(text string) string { - return "*" + text + "*" -} - -func NewMarkdownItalic(text string) string { - return "_" + text + "_" -} - -func NewMarkdownURL(text string, link fmt.Stringer) string { - return "[" + text + "](" + link.String() + ")" -} - -func NewMarkdownMention(text string, id int) string { - return NewMarkdownURL(text, NewInlineMentionURL(id)) -} - -func NewMarkdownCode(text string) string { - return "`" + text + "`" -} - -func NewMarkdownCodeBlock(text string) string { - return "```" + text + "```" -} - -func NewHTMLBold(text string) string { - return "" + text + "" -} - -func NewHTMLItalic(text string) string { - return "" + text + "" -} - -func NewHTMLURL(text string, link fmt.Stringer) string { - return `` + text + `` -} - -func NewHTMLMention(text string, id int) string { - return NewHTMLURL(text, NewInlineMentionURL(id)) -} - -func NewHTMLCode(text string) string { - return "" + text + "" -} - -func NewHTMLCodeBlock(text string) string { - return "
" + text + "
" -} - -func (a *Animation) HasThumb() bool { - return a != nil && a.Thumb != nil -} - -func (a *Animation) File() *File { - if a == nil { - return nil - } - - return &File{ - FileID: a.FileID, - FileSize: a.FileSize, - } -} - -func (a *Audio) FullName(separator string) (name string) { - if a.HasPerformer() { - if separator == "" { - separator = DefaultAudioSeparator - } - name += a.Performer + separator - } - - title := DefaultAudioTitle - if a.HasTitle() { - title = a.Title - } - - name += title - return -} - -func (a *Audio) HasPerformer() bool { - return a != nil && a.Performer != "" -} - -func (a *Audio) HasTitle() bool { - return a != nil && a.Title != "" -} - -func (a *Audio) HasThumb() bool { - return a != nil && a.Thumb != nil -} - -func (a *Audio) File() *File { - if a == nil { - return nil - } - - return &File{ - FileID: a.FileID, - FileSize: a.FileSize, - } -} - -// IsMessageFromMe checks that the input message is a message from the current -// bot. +// IsMessageFromMe checks that the input message is a message from the current bot. func (b *Bot) IsMessageFromMe(m *Message) bool { - return b != nil && b.User != nil && - m != nil && m.From != nil && m.From.ID == b.ID + return b != nil && b.User != nil && m != nil && m.From != nil && m.From.ID == b.ID } -// IsForwardFromMe checks that the input message is a forwarded message from the -// current bot. +// IsForwardFromMe checks that the input message is a forwarded message from the current bot. func (b *Bot) IsForwardFromMe(m *Message) bool { - return b != nil && b.User != nil && - m.IsForward() && m.ForwardFrom.ID == b.ID + return b != nil && b.User != nil && m.IsForward() && m.ForwardFrom.ID == b.ID } // IsReplyToMe checks that the input message is a reply to the current bot. func (b *Bot) IsReplyToMe(m *Message) bool { - return m.Chat.IsPrivate() || - (m.IsReply() && b.IsMessageFromMe(m.ReplyToMessage)) + return m.Chat.IsPrivate() || (m.IsReply() && b.IsMessageFromMe(m.ReplyToMessage)) } // IsCommandToMe checks that the input message is a command for the current bot. @@ -232,7 +85,8 @@ func (b *Bot) IsMessageMentionsMe(m *Message) bool { return true } - var entities []MessageEntity + var entities []*MessageEntity + switch { case m.HasMentions(): entities = m.Entities @@ -249,18 +103,15 @@ func (b *Bot) IsMessageMentionsMe(m *Message) bool { return false } -// IsForwardMentionsMe checks that the input forwarded message mentions the -// current bot. -func (b *Bot) IsForwardMentionsMe(m *Message) bool { - return m.IsForward() && b.IsMessageMentionsMe(m) -} +// IsForwardMentionsMe checks that the input forwarded message mentions the current bot. +func (b *Bot) IsForwardMentionsMe(m *Message) bool { return m.IsForward() && b.IsMessageMentionsMe(m) } -// IsReplyMentionsMe checks that the input message mentions the current bot. +// IsReplyMentionsMe checks that the input message mentions the current b. func (b *Bot) IsReplyMentionsMe(m *Message) bool { return m.IsReply() && b.IsMessageMentionsMe(m.ReplyToMessage) } -// IsMessageToMe checks that the input message is addressed to the current bot. +// IsMessageToMe checks that the input message is addressed to the current b. func (b *Bot) IsMessageToMe(m *Message) bool { if m == nil || m.Chat == nil { return false @@ -301,2158 +152,29 @@ func (b *Bot) NewRedirectURL(param string, group bool) *http.URI { q := link.QueryArgs() key := "start" + if group { key += "group" } - q.Set(key, param) + q.Set(key, param) link.SetQueryStringBytes(q.QueryString()) return link } -func (b *Bot) DecryptFile(pf *PassportFile, fc *FileCredentials) (data []byte, err error) { - secret, err := decodeField(fc.Secret) - if err != nil { - return nil, err - } - - hash, err := decodeField(fc.FileHash) - if err != nil { - return nil, err - } - - key, iv := decryptSecretHash(secret, hash) - file, err := b.GetFile(pf.FileID) - if err != nil { - return nil, err - } - - if _, data, err = b.Client.Get(nil, b.NewFileURL(file.FilePath).String()); err != nil { - return nil, err - } - - if data, err = decryptData(key, iv, data); err != nil { - return nil, err - } - - if !match(hash, data) { - err = ErrNotEqual - return nil, err - } - - offset := int(data[0]) - data = data[offset:] - return data, nil -} - -// IsPrivate checks that the current chat is a private chat with single user. -func (c *Chat) IsPrivate() bool { - return c != nil && strings.EqualFold(c.Type, ChatPrivate) -} - -// IsGroup checks that the current chat is a group. -func (c *Chat) IsGroup() bool { - return c != nil && strings.EqualFold(c.Type, ChatGroup) -} - -// IsSuperGroup checks that the current chat is a supergroup. -func (c *Chat) IsSuperGroup() bool { - return c != nil && strings.EqualFold(c.Type, ChatSuperGroup) -} - -// IsChannel checks that the current chat is a channel. -func (c *Chat) IsChannel() bool { - return c != nil && strings.EqualFold(c.Type, ChatChannel) -} - -// HasPinnedMessage checks that the current chat has a pinned message. -func (c *Chat) HasPinnedMessage() bool { - return c != nil && c.PinnedMessage != nil -} - -// HasStickerSet checks that the current chat has a sticker set. -func (c *Chat) HasStickerSet() bool { - return c != nil && c.StickerSetName != "" -} - -// StickerSet return StickerSet structure if StickerSetName is available. -func (c *Chat) StickerSet(bot *Bot) *StickerSet { - if !c.HasStickerSet() || bot == nil { - return nil - } - - set, err := bot.GetStickerSet(c.StickerSetName) - if err != nil { - return nil - } - - return set -} - -// FullName returns the full name of chat or FirstName if LastName is not available. -func (c *Chat) FullName() string { - if c == nil { - return "" - } - - if c.HasLastName() { - return c.FirstName + " " + c.LastName - } - - return c.FirstName -} - -// HaveLastName checks what the current user has a LastName. -func (c *Chat) HasLastName() bool { - return c != nil && c.LastName != "" -} - -// HaveUsername checks what the current user has a username. -func (c *Chat) HasUsername() bool { - return c != nil && c.Username != "" -} - -func (c *Chat) HasDescription() bool { - return c != nil && c.Description != "" -} - -func (c *Chat) HasInviteLink() bool { - return c != nil && c.InviteLink != "" -} - -func (cir *ChosenInlineResult) HasLocation() bool { - return cir != nil && cir.Location != nil -} - -// FullName returns the full name of contact or FirstName if LastName is not -// available. -func (c *Contact) FullName() string { - if c == nil { - return "" - } - - if c.HasLastName() { - return c.FirstName + " " + c.LastName - } - - return c.FirstName -} - -// HaveLastName checks what the current contact has a LastName. -func (c *Contact) HasLastName() bool { - return c != nil && c.LastName != "" -} - -func (c *Contact) HasTelegram() bool { - return c != nil && c.UserID != 0 -} - -func (c *Contact) HasVCard() bool { - return c != nil && c.VCard != "" -} - -func (dc *DataCredentials) decrypt(d string) (data []byte, err error) { - secret, err := decodeField(dc.Secret) - if err != nil { - return - } - - hash, err := decodeField(dc.DataHash) - if err != nil { - return - } - - key, iv := decryptSecretHash(secret, hash) - if data, err = decodeField(d); err != nil { - return - } - - if data, err = decryptData(key, iv, data); err != nil { - return - } - - if !match(hash, data) { - err = ErrNotEqual - } - - offset := int(data[0]) - data = data[offset:] - - return -} - -func (d *Document) HasThumb() bool { - return d != nil && d.Thumb != nil -} - -func (d *Document) File() *File { - return &File{ - FileID: d.FileID, - FileSize: d.FileSize, - } -} - -func (ec *EncryptedCredentials) Decrypt(pk *rsa.PrivateKey) (*Credentials, error) { - if ec == nil || pk == nil { - return nil, nil - } - - data, err := decrypt(pk, ec.Secret, ec.Hash, ec.Data) - if err != nil { - return nil, err - } - - var c Credentials - err = parser.Unmarshal(data, &c) - return &c, err -} - -func (epe *EncryptedPassportElement) DecryptPersonalDetails(sv *SecureValue) (*PersonalDetails, error) { - if !epe.IsPersonalDetails() || !sv.HasData() { - return nil, nil - } - - body, err := sv.Data.decrypt(epe.Data) - if err != nil { - return nil, err - } - - var pd PersonalDetails - err = parser.Unmarshal(body, &pd) - return &pd, err -} - -func (epe *EncryptedPassportElement) DecryptPassport(sv *SecureValue, b *Bot) (*IDDocumentData, []byte, []byte, [][]byte, error) { - if !epe.IsPassport() || !sv.HasData() || !sv.HasFrontSide() { - return nil, nil, nil, nil, nil - } - - body, err := sv.Data.decrypt(epe.Data) - if err != nil { - return nil, nil, nil, nil, err - } - - var idd IDDocumentData - if err = parser.Unmarshal(body, &idd); err != nil { - return nil, nil, nil, nil, err - } - - fs, err := b.DecryptFile(epe.FrontSide, sv.FrontSide) - if err != nil { - return &idd, nil, nil, nil, err - } - - var s []byte - if sv.HasSelfie() { - if s, err = b.DecryptFile(epe.Selfie, sv.Selfie); err != nil { - return &idd, fs, nil, nil, err - } - } - - t := make([][]byte, len(sv.Translation)) - if sv.HasTranslation() { - for i := range t { - if t[i], err = b.DecryptFile(&epe.Translation[i], &sv.Translation[i]); err != nil { - return &idd, fs, s, nil, err - } - } - } - - return &idd, fs, s, t, nil -} - -func (epe *EncryptedPassportElement) DecryptInternalPassport(sv *SecureValue, b *Bot) (*IDDocumentData, []byte, []byte, [][]byte, error) { - if !epe.IsInternalPassport() || !sv.HasData() || !sv.HasFrontSide() { - return nil, nil, nil, nil, nil - } - - body, err := sv.Data.decrypt(epe.Data) - if err != nil { - return nil, nil, nil, nil, err - } - - var idd IDDocumentData - if err = parser.Unmarshal(body, &idd); err != nil { - return nil, nil, nil, nil, err - } - - fs, err := b.DecryptFile(epe.FrontSide, sv.FrontSide) - if err != nil { - return &idd, nil, nil, nil, err - } - - var s []byte - if sv.HasSelfie() { - if s, err = b.DecryptFile(epe.Selfie, sv.Selfie); err != nil { - return &idd, fs, nil, nil, err - } - } - - t := make([][]byte, len(sv.Translation)) - if sv.HasTranslation() { - for i := range t { - if t[i], err = b.DecryptFile(&epe.Translation[i], &sv.Translation[i]); err != nil { - return &idd, fs, s, nil, err - } - } - } - - return &idd, fs, s, t, nil -} - -func (epe *EncryptedPassportElement) DecryptDriverLicense(sv *SecureValue, b *Bot) (*IDDocumentData, []byte, []byte, []byte, [][]byte, error) { - if !epe.IsDriverLicense() || !sv.HasData() || !sv.HasFrontSide() || !sv.HasReverseSide() { - return nil, nil, nil, nil, nil, nil - } - - body, err := sv.Data.decrypt(epe.Data) - if err != nil { - return nil, nil, nil, nil, nil, err - } - - var idd IDDocumentData - if err = parser.Unmarshal(body, &idd); err != nil { - return nil, nil, nil, nil, nil, err - } - - fs, err := b.DecryptFile(epe.FrontSide, sv.FrontSide) - if err != nil { - return &idd, nil, nil, nil, nil, err - } - - rs, err := b.DecryptFile(epe.ReverseSide, sv.ReverseSide) - if err != nil { - return &idd, nil, nil, nil, nil, err - } - - var s []byte - if sv.HasSelfie() { - if s, err = b.DecryptFile(epe.Selfie, sv.Selfie); err != nil { - return &idd, fs, rs, nil, nil, err - } - } - - t := make([][]byte, len(sv.Translation)) - if sv.HasTranslation() { - for i := range t { - if t[i], err = b.DecryptFile(&epe.Translation[i], &sv.Translation[i]); err != nil { - return &idd, fs, rs, s, nil, err - } - } - } - - return &idd, fs, rs, s, t, nil -} - -func (epe *EncryptedPassportElement) IsAddress() bool { - return epe != nil && strings.EqualFold(epe.Type, TypeAddress) -} - -func (epe *EncryptedPassportElement) IsBankStatement() bool { - return epe != nil && strings.EqualFold(epe.Type, TypeBankStatement) -} - -func (epe *EncryptedPassportElement) IsDriverLicense() bool { - return epe != nil && strings.EqualFold(epe.Type, TypeDriverLicense) -} - -func (epe *EncryptedPassportElement) IsEmail() bool { - return epe != nil && strings.EqualFold(epe.Type, TypeEmail) -} - -func (epe *EncryptedPassportElement) IsIdentityCard() bool { - return epe != nil && strings.EqualFold(epe.Type, TypeIdentityCard) -} - -func (epe *EncryptedPassportElement) IsInternalPassport() bool { - return epe != nil && strings.EqualFold(epe.Type, TypeInternalPassport) -} - -func (epe *EncryptedPassportElement) IsPassport() bool { - return epe != nil && strings.EqualFold(epe.Type, TypePassport) -} - -func (epe *EncryptedPassportElement) IsPassportRegistration() bool { - return epe != nil && strings.EqualFold(epe.Type, TypePassportRegistration) -} - -func (epe *EncryptedPassportElement) IsPersonalDetails() bool { - return epe != nil && strings.EqualFold(epe.Type, TypePersonalDetails) -} - -func (epe *EncryptedPassportElement) IsPhoneNumber() bool { - return epe != nil && strings.EqualFold(epe.Type, TypePhoneNumber) -} - -func (epe *EncryptedPassportElement) IsRentalAgreement() bool { - return epe != nil && strings.EqualFold(epe.Type, TypeRentalAgreement) -} - -func (epe *EncryptedPassportElement) IsTemporaryRegistration() bool { - return epe != nil && strings.EqualFold(epe.Type, TypeTemporaryRegistration) -} - -func (epe *EncryptedPassportElement) IsUtilityBill() bool { - return epe != nil && strings.EqualFold(epe.Type, TypeUtilityBill) -} - -// ParseURL selects URL from entered text of message and parse it as fasthttp.URI. -func (e *MessageEntity) ParseURL(messageText string) *http.URI { - if e == nil || !e.IsURL() || messageText == "" { - return nil - } - - from := e.Offset - to := from + e.Length - text := []rune(messageText) - if len(text) < to { - return nil - } - - link := http.AcquireURI() - link.Update(string(text[from:to])) - - return link -} - -// IsBold checks that the current entity is a bold tag. -func (e *MessageEntity) IsBold() bool { - return e != nil && strings.EqualFold(e.Type, EntityBold) -} - -// IsBotCommand checks that the current entity is a bot command. -func (e *MessageEntity) IsBotCommand() bool { - return e != nil && strings.EqualFold(e.Type, EntityBotCommand) -} - -// IsCode checks that the current entity is a code tag. -func (e *MessageEntity) IsCode() bool { - return e != nil && strings.EqualFold(e.Type, EntityCode) -} - -// IsEmail checks that the current entity is a email. -func (e *MessageEntity) IsEmail() bool { - return e != nil && strings.EqualFold(e.Type, EntityEmail) -} - -// IsHashtag checks that the current entity is a hashtag. -func (e *MessageEntity) IsHashtag() bool { - return e != nil && strings.EqualFold(e.Type, EntityHashtag) -} - -// IsItalic checks that the current entity is a italic tag. -func (e *MessageEntity) IsItalic() bool { - return e != nil && strings.EqualFold(e.Type, EntityItalic) -} - -// IsMention checks that the current entity is a username mention. -func (e *MessageEntity) IsMention() bool { - return e != nil && strings.EqualFold(e.Type, EntityMention) -} - -// IsPre checks that the current entity is a pre tag. -func (e *MessageEntity) IsPre() bool { - return e != nil && strings.EqualFold(e.Type, EntityPre) -} - -// IsTextLink checks that the current entity is a text link. -func (e *MessageEntity) IsTextLink() bool { - return e != nil && strings.EqualFold(e.Type, EntityTextLink) -} - -// IsTextMention checks that the current entity is a mention without username. -func (e *MessageEntity) IsTextMention() bool { - return e != nil && strings.EqualFold(e.Type, EntityTextMention) -} - -// IsURL checks that the current entity is a URL -func (e *MessageEntity) IsURL() bool { - return e != nil && strings.EqualFold(e.Type, EntityURL) -} - -// TextLink parse current text link entity as fasthttp.URI. -func (e *MessageEntity) TextLink() *http.URI { - if e == nil { - return nil - } - - link := http.AcquireURI() - link.Update(e.URL) - - return link -} - -func (idd *IDDocumentData) ExpiryTime() *time.Time { - if idd == nil || idd.ExpiryDate == "" { - return nil - } - - et, err := time.Parse("02.01.2006", idd.ExpiryDate) - if err != nil { - return nil - } - - return &et -} - -// NewInlineKeyboardMarkup creates a new inline keyboard markup for message. -func NewInlineKeyboardMarkup(rows ...[]InlineKeyboardButton) *InlineKeyboardMarkup { - var keyboard [][]InlineKeyboardButton - keyboard = append(keyboard, rows...) - return &InlineKeyboardMarkup{ - InlineKeyboard: keyboard, - } -} - -// NewInlineKeyboardRow creates a new inline keyboard row for buttons. -func NewInlineKeyboardRow(buttons ...InlineKeyboardButton) []InlineKeyboardButton { - var row []InlineKeyboardButton - row = append(row, buttons...) - return row -} - -// NewInlineKeyboardButton creates a new inline keyboard callback button. -func NewInlineKeyboardButton(text, data string) InlineKeyboardButton { - return InlineKeyboardButton{ - Text: text, - CallbackData: data, - } -} - -// NewInlineKeyboardButtonURL creates a new inline keyboard button with URL. -func NewInlineKeyboardButtonURL(text, url string) InlineKeyboardButton { - return InlineKeyboardButton{ - Text: text, - URL: url, - } -} - -// NewInlineKeyboardButtonSwitch creates a new inline keyboard button to make -// specific inline query in other chat. -func NewInlineKeyboardButtonSwitch(text, query string) InlineKeyboardButton { - return InlineKeyboardButton{ - Text: text, - SwitchInlineQuery: query, - } -} - -// NewInlineKeyboardButtonSwitchSelf creates a new inline keyboard button to make -// specific inline query in same chat. -func NewInlineKeyboardButtonSwitchSelf(text, query string) InlineKeyboardButton { - return InlineKeyboardButton{ - Text: text, - SwitchInlineQueryCurrentChat: query, - } -} - -// NewInlineKeyboardButtonGame creates a new inline keyboard button with game -// callback. -func NewInlineKeyboardButtonGame(text string) InlineKeyboardButton { - var game CallbackGame - return InlineKeyboardButton{ - Text: text, - CallbackGame: &game, - } -} - -// NewInlineKeyboardButtonPay creates a new inline keyboard button with pay -// callback. -func NewInlineKeyboardButtonPay(text string) InlineKeyboardButton { - return InlineKeyboardButton{ - Text: text, - Pay: true, - } -} - -// HasLocation checks what current InlineQuery has Location info. -func (iq *InlineQuery) HasLocation() bool { - return iq != nil && iq.Location != nil -} - -// HasOffset checks what current InlineQuery has Offset. -func (iq *InlineQuery) HasOffset() bool { - return iq != nil && iq.Offset != "" -} - -// HasQuery checks what current InlineQuery has Query string. -func (iq *InlineQuery) HasQuery() bool { - return iq != nil && iq.Query != "" -} - -// NewInlineQueryResultCachedAudio creates a new inline query result with cached -// audio. -func NewInlineQueryResultCachedAudio(resultID, fileID string) *InlineQueryResultCachedAudio { - return &InlineQueryResultCachedAudio{ - Type: TypeAudio, - ID: resultID, - AudioFileID: fileID, - } -} - -// NewInlineQueryResultCachedDocument creates a new inline query result with -// cached document. -func NewInlineQueryResultCachedDocument(resultID, fileID, title string) *InlineQueryResultCachedDocument { - return &InlineQueryResultCachedDocument{ - Type: TypeDocument, - ID: resultID, - Title: title, - DocumentFileID: fileID, - } -} - -// NewInlineQueryResultCachedGif creates a new inline query result with cached -// GIF. -func NewInlineQueryResultCachedGif(resultID, fileID string) *InlineQueryResultCachedGif { - return &InlineQueryResultCachedGif{ - Type: TypeGIF, - ID: resultID, - GifFileID: fileID, - } -} - -// NewInlineQueryResultCachedMpeg4Gif creates a new inline query result with -// cached MPEG GIF. -func NewInlineQueryResultCachedMpeg4Gif(resultID, fileID string) *InlineQueryResultCachedMpeg4Gif { - return &InlineQueryResultCachedMpeg4Gif{ - Type: TypeMpeg4Gif, - ID: resultID, - Mpeg4FileID: fileID, - } -} - -// NewInlineQueryResultCachedPhoto creates a new inline query result with cached -// photo. -func NewInlineQueryResultCachedPhoto(resultID, fileID string) *InlineQueryResultCachedPhoto { - return &InlineQueryResultCachedPhoto{ - Type: TypePhoto, - ID: resultID, - PhotoFileID: fileID, - } -} - -// NewInlineQueryResultCachedSticker creates a new inline query result with -// cached sticker. -func NewInlineQueryResultCachedSticker(resultID, fileID string) *InlineQueryResultCachedSticker { - return &InlineQueryResultCachedSticker{ - Type: TypeSticker, - ID: resultID, - StickerFileID: fileID, - } -} - -// NewInlineQueryResultCachedVideo creates a new inline query result with cached -// video. -func NewInlineQueryResultCachedVideo(resultID, fileID, title string) *InlineQueryResultCachedVideo { - return &InlineQueryResultCachedVideo{ - Type: TypeVideo, - ID: resultID, - VideoFileID: fileID, - Title: title, - } -} - -// NewInlineQueryResultCachedVoice creates a new inline query result with cached -// voice. -func NewInlineQueryResultCachedVoice(resultID, fileID, title string) *InlineQueryResultCachedVoice { - return &InlineQueryResultCachedVoice{ - Type: TypeVoice, - ID: resultID, - VoiceFileID: fileID, - Title: title, - } -} - -// NewInlineQueryResultArticle creates a new inline query result with article. -func NewInlineQueryResultArticle(resultID, title string, content interface{}) *InlineQueryResultArticle { - return &InlineQueryResultArticle{ - Type: TypeArticle, - ID: resultID, - Title: title, - InputMessageContent: content, - } -} - -// NewInlineQueryResultAudio creates a new inline query result with audio. -func NewInlineQueryResultAudio(resultID, audioURL, title string) *InlineQueryResultAudio { - return &InlineQueryResultAudio{ - Type: TypeAudio, - ID: resultID, - AudioURL: audioURL, - Title: title, - } -} - -// NewInlineQueryResultContact creates a new inline query result with contact. -func NewInlineQueryResultContact(resultID, phoneNumber, firstName string) *InlineQueryResultContact { - return &InlineQueryResultContact{ - Type: TypeContact, - ID: resultID, - PhoneNumber: phoneNumber, - FirstName: firstName, - } -} - -// NewInlineQueryResultGame creates a new inline query result with game. -func NewInlineQueryResultGame(resultID, gameShortName string) *InlineQueryResultGame { - return &InlineQueryResultGame{ - Type: TypeGame, - ID: resultID, - GameShortName: gameShortName, - } -} - -// NewInlineQueryResultDocument creates a new inline query result with document. -func NewInlineQueryResultDocument(resultID, title, documentURL, mimeType string) *InlineQueryResultDocument { - return &InlineQueryResultDocument{ - Type: TypeDocument, - ID: resultID, - Title: title, - DocumentURL: documentURL, - MimeType: mimeType, - } -} - -// NewInlineQueryResultGif creates a new inline query result with GIF. -func NewInlineQueryResultGif(resultID, gifURL, thumbURL string) *InlineQueryResultGif { - return &InlineQueryResultGif{ - Type: TypeGIF, - ID: resultID, - GifURL: gifURL, - ThumbURL: thumbURL, - } -} - -// NewInlineQueryResultLocation creates a new inline query result with location. -func NewInlineQueryResultLocation(resultID, title string, latitude, longitude float32) *InlineQueryResultLocation { - return &InlineQueryResultLocation{ - Type: TypeLocation, - ID: resultID, - Latitude: latitude, - Longitude: longitude, - Title: title, - } -} - -// NewInlineQueryResultMpeg4Gif creates a new inline query result with MPEG GIF. -func NewInlineQueryResultMpeg4Gif(resultID, mpeg4URL, thumbURL string) *InlineQueryResultMpeg4Gif { - return &InlineQueryResultMpeg4Gif{ - Type: TypeMpeg4Gif, - ID: resultID, - Mpeg4URL: mpeg4URL, - ThumbURL: thumbURL, - } -} - -// NewInlineQueryResultPhoto creates a new inline query result with photo. -func NewInlineQueryResultPhoto(resultID, photoURL, thumbURL string) *InlineQueryResultPhoto { - return &InlineQueryResultPhoto{ - Type: TypePhoto, - ID: resultID, - PhotoURL: photoURL, - ThumbURL: thumbURL, - } -} - -// NewInlineQueryResultVenue creates a new inline query result with venue. -func NewInlineQueryResultVenue(resultID, title, address string, latitude, longitude float32) *InlineQueryResultVenue { - return &InlineQueryResultVenue{ - Type: TypeVenue, - ID: resultID, - Latitude: latitude, - Longitude: longitude, - Title: title, - Address: address, - } -} - -// NewInlineQueryResultVideo creates a new inline query result with video. -func NewInlineQueryResultVideo(resultID, videoURL, mimeType, thumbURL, title string) *InlineQueryResultVideo { - return &InlineQueryResultVideo{ - Type: TypeVideo, - ID: resultID, - VideoURL: videoURL, - MimeType: mimeType, - ThumbURL: thumbURL, - Title: title, - } -} - -// NewInlineQueryResultVoice creates a new inline query result with voice. -func NewInlineQueryResultVoice(resultID, voiceURL, title string) *InlineQueryResultVoice { - return &InlineQueryResultVoice{ - Type: TypeVoice, - ID: resultID, - VoiceURL: voiceURL, - Title: title, - } -} - -func (iqra *InlineQueryResultArticle) ResultID() string { - return iqra.ID -} - -func (iqra *InlineQueryResultArticle) ResultType() string { - return iqra.Type -} - -func (iqra *InlineQueryResultArticle) ResultReplyMarkup() *InlineKeyboardMarkup { - return iqra.ReplyMarkup -} - -func (iqrp *InlineQueryResultPhoto) ResultID() string { - return iqrp.ID -} - -func (iqrp *InlineQueryResultPhoto) ResultType() string { - return iqrp.Type -} - -func (iqrp *InlineQueryResultPhoto) ResultReplyMarkup() *InlineKeyboardMarkup { - return iqrp.ReplyMarkup -} - -func (iqrg *InlineQueryResultGif) ResultID() string { - return iqrg.ID -} - -func (iqrg *InlineQueryResultGif) ResultType() string { - return iqrg.Type -} - -func (iqrg *InlineQueryResultGif) ResultReplyMarkup() *InlineKeyboardMarkup { - return iqrg.ReplyMarkup -} - -func (iqrm4g *InlineQueryResultMpeg4Gif) ResultID() string { - return iqrm4g.ID -} - -func (iqrm4g *InlineQueryResultMpeg4Gif) ResultType() string { - return iqrm4g.Type -} - -func (iqrm4g *InlineQueryResultMpeg4Gif) ResultReplyMarkup() *InlineKeyboardMarkup { - return iqrm4g.ReplyMarkup -} - -func (iqrv *InlineQueryResultVideo) ResultID() string { - return iqrv.ID -} - -func (iqrv *InlineQueryResultVideo) ResultType() string { - return iqrv.Type -} - -func (iqrv *InlineQueryResultVideo) ResultReplyMarkup() *InlineKeyboardMarkup { - return iqrv.ReplyMarkup -} - -func (iqra *InlineQueryResultAudio) ResultID() string { - return iqra.ID -} - -func (iqra *InlineQueryResultAudio) ResultType() string { - return iqra.Type -} - -func (iqra *InlineQueryResultAudio) ResultReplyMarkup() *InlineKeyboardMarkup { - return iqra.ReplyMarkup -} - -func (iqrv *InlineQueryResultVoice) ResultID() string { - return iqrv.ID -} - -func (iqrv *InlineQueryResultVoice) ResultType() string { - return iqrv.Type -} - -func (iqrv *InlineQueryResultVoice) ResultReplyMarkup() *InlineKeyboardMarkup { - return iqrv.ReplyMarkup -} - -func (iqrd *InlineQueryResultDocument) ResultID() string { - return iqrd.ID -} - -func (iqrd *InlineQueryResultDocument) ResultType() string { - return iqrd.Type -} - -func (iqrd *InlineQueryResultDocument) ResultReplyMarkup() *InlineKeyboardMarkup { - return iqrd.ReplyMarkup -} - -func (iqrl *InlineQueryResultLocation) ResultID() string { - return iqrl.ID -} - -func (iqrl *InlineQueryResultLocation) ResultType() string { - return iqrl.Type -} - -func (iqrl *InlineQueryResultLocation) ResultReplyMarkup() *InlineKeyboardMarkup { - return iqrl.ReplyMarkup -} - -func (iqrv *InlineQueryResultVenue) ResultID() string { - return iqrv.ID -} - -func (iqrv *InlineQueryResultVenue) ResultType() string { - return iqrv.Type -} - -func (iqrv *InlineQueryResultVenue) ResultReplyMarkup() *InlineKeyboardMarkup { - return iqrv.ReplyMarkup -} - -func (iqrc *InlineQueryResultContact) ResultID() string { - return iqrc.ID -} - -func (iqrc *InlineQueryResultContact) ResultType() string { - return iqrc.Type -} - -func (iqrc *InlineQueryResultContact) ResultReplyMarkup() *InlineKeyboardMarkup { - return iqrc.ReplyMarkup -} - -func (iqrg *InlineQueryResultGame) ResultID() string { - return iqrg.ID -} - -func (iqrg *InlineQueryResultGame) ResultType() string { - return iqrg.Type -} - -func (iqrg *InlineQueryResultGame) ResultReplyMarkup() *InlineKeyboardMarkup { - return iqrg.ReplyMarkup -} - -func (iqrcp *InlineQueryResultCachedPhoto) ResultID() string { - return iqrcp.ID -} - -func (iqrcp *InlineQueryResultCachedPhoto) ResultType() string { - return iqrcp.Type -} - -func (iqrcp *InlineQueryResultCachedPhoto) ResultReplyMarkup() *InlineKeyboardMarkup { - return iqrcp.ReplyMarkup -} - -func (iqrcg *InlineQueryResultCachedGif) ResultID() string { - return iqrcg.ID -} - -func (iqrcg *InlineQueryResultCachedGif) ResultType() string { - return iqrcg.Type -} - -func (iqrcg *InlineQueryResultCachedGif) ResultReplyMarkup() *InlineKeyboardMarkup { - return iqrcg.ReplyMarkup -} - -func (iqrcm4g *InlineQueryResultCachedMpeg4Gif) ResultID() string { - return iqrcm4g.ID -} - -func (iqrcm4g *InlineQueryResultCachedMpeg4Gif) ResultType() string { - return iqrcm4g.Type -} - -func (iqrcm4g *InlineQueryResultCachedMpeg4Gif) ResultReplyMarkup() *InlineKeyboardMarkup { - return iqrcm4g.ReplyMarkup -} - -func (iqrcs *InlineQueryResultCachedSticker) ResultID() string { - return iqrcs.ID -} - -func (iqrcs *InlineQueryResultCachedSticker) ResultType() string { - return iqrcs.Type -} - -func (iqrcs *InlineQueryResultCachedSticker) ResultReplyMarkup() *InlineKeyboardMarkup { - return iqrcs.ReplyMarkup -} - -func (iqrcd *InlineQueryResultCachedDocument) ResultID() string { - return iqrcd.ID -} - -func (iqrcd *InlineQueryResultCachedDocument) ResultType() string { - return iqrcd.Type -} - -func (iqrcd *InlineQueryResultCachedDocument) ResultReplyMarkup() *InlineKeyboardMarkup { - return iqrcd.ReplyMarkup -} - -func (iqrcv *InlineQueryResultCachedVideo) ResultID() string { - return iqrcv.ID -} - -func (iqrcv *InlineQueryResultCachedVideo) ResultType() string { - return iqrcv.Type -} - -func (iqrcv *InlineQueryResultCachedVideo) ResultReplyMarkup() *InlineKeyboardMarkup { - return iqrcv.ReplyMarkup -} - -func (iqrcv *InlineQueryResultCachedVoice) ResultID() string { - return iqrcv.ID -} - -func (iqrcv *InlineQueryResultCachedVoice) ResultType() string { - return iqrcv.Type -} - -func (iqrcv *InlineQueryResultCachedVoice) ResultReplyMarkup() *InlineKeyboardMarkup { - return iqrcv.ReplyMarkup -} - -func (iqrca *InlineQueryResultCachedAudio) ResultID() string { - return iqrca.ID -} - -func (iqrca *InlineQueryResultCachedAudio) ResultType() string { - return iqrca.Type -} - -func (iqrca *InlineQueryResultCachedAudio) ResultReplyMarkup() *InlineKeyboardMarkup { - return iqrca.ReplyMarkup -} - -// NewInputTextMessageContent creates a new text of message. -func NewInputTextMessageContent(messageText string) *InputTextMessageContent { - return &InputTextMessageContent{ - MessageText: messageText, - } -} - -// NewInputLocationMessageContent creates a new location. -func NewInputLocationMessageContent(latitude, longitude float32) *InputLocationMessageContent { - return &InputLocationMessageContent{ - Latitude: latitude, - Longitude: longitude, - } -} - -// NewInputVenueMessageContent creates a new venue. -func NewInputVenueMessageContent(latitude, longitude float32, title, address string) *InputVenueMessageContent { - return &InputVenueMessageContent{ - Latitude: latitude, - Longitude: longitude, - Title: title, - Address: address, - } -} - -// NewInputContactMessageContent creates a new contact. -func NewInputContactMessageContent(phoneNumber, firstName string) *InputContactMessageContent { - return &InputContactMessageContent{ - PhoneNumber: phoneNumber, - FirstName: firstName, - } -} - -// NewInputMediaPhoto creates a new photo in media album. -func NewInputMediaPhoto(media string) *InputMediaPhoto { - return &InputMediaPhoto{ - Type: TypePhoto, - Media: media, - } -} - -// NewInputMediaVideo creates a new video in media album. -func NewInputMediaVideo(media string) *InputMediaVideo { - return &InputMediaVideo{ - Type: TypeVideo, - Media: media, - } -} - -func (ima *InputMediaAnimation) File() string { - if ima == nil { - return "" - } - - return ima.Media -} - -func (ima *InputMediaAnimation) InputMediaCaption() string { - if ima == nil { - return "" - } - - return ima.Caption -} - -func (ima *InputMediaAnimation) InputMediaParseMode() string { - if ima == nil { - return "" - } - - return ima.ParseMode -} - -func (ima *InputMediaAnimation) InputMediaType() string { - if ima == nil { - return "" - } - - return ima.Type -} - -func (imd *InputMediaDocument) File() string { - if imd == nil { - return "" - } - - return imd.Media -} - -func (imd *InputMediaDocument) InputMediaCaption() string { - if imd == nil { - return "" - } - - return imd.Caption -} - -func (imd *InputMediaDocument) InputMediaParseMode() string { - if imd == nil { - return "" - } - - return imd.ParseMode -} - -func (imd *InputMediaDocument) InputMediaType() string { - if imd == nil { - return "" - } - - return imd.Type -} - -func (ima *InputMediaAudio) File() string { - if ima == nil { - return "" - } - - return ima.Media -} - -func (ima *InputMediaAudio) InputMediaCaption() string { - if ima == nil { - return "" - } - - return ima.Caption -} - -func (ima *InputMediaAudio) InputMediaParseMode() string { - if ima == nil { - return "" - } - - return ima.ParseMode -} - -func (ima *InputMediaAudio) InputMediaType() string { - if ima == nil { - return "" - } - - return ima.Type -} - -func (imp *InputMediaPhoto) File() string { - if imp == nil { - return "" - } - - return imp.Media -} - -func (imp *InputMediaPhoto) InputMediaCaption() string { - if imp == nil { - return "" - } - - return imp.Caption -} - -func (imp *InputMediaPhoto) InputMediaParseMode() string { - if imp == nil { - return "" - } - - return imp.ParseMode -} - -func (imp *InputMediaPhoto) InputMediaType() string { - if imp == nil { - return "" - } - - return imp.Type -} - -func (imv *InputMediaVideo) File() string { - if imv == nil { - return "" - } - - return imv.Media -} - -func (imv *InputMediaVideo) InputMediaCaption() string { - if imv == nil { - return "" - } - - return imv.Caption -} - -func (imv *InputMediaVideo) InputMediaParseMode() string { - if imv == nil { - return "" - } - - return imv.ParseMode -} - -func (imv *InputMediaVideo) InputMediaType() string { - if imv == nil { - return "" - } - - return imv.Type -} - -func (itmc *InputTextMessageContent) IsInputMessageContent() bool { return true } - -func (ilmc *InputLocationMessageContent) IsInputMessageContent() bool { return true } - -func (ivmc *InputVenueMessageContent) IsInputMessageContent() bool { return true } - -func (icmc *InputContactMessageContent) IsInputMessageContent() bool { return true } - -// NewReplyKeyboardRemove just hides keyboard. -func NewReplyKeyboardRemove(selective bool) *ReplyKeyboardRemove { - return &ReplyKeyboardRemove{ - RemoveKeyboard: true, - Selective: selective, - } -} - -// NewReplyKeyboardMarkup creates new keyboard markup of simple buttons. -func NewReplyKeyboardMarkup(rows ...[]KeyboardButton) *ReplyKeyboardMarkup { - var keyboard [][]KeyboardButton - keyboard = append(keyboard, rows...) - return &ReplyKeyboardMarkup{Keyboard: keyboard} -} - -// NewReplyKeyboardRow creates new keyboard row for buttons. -func NewReplyKeyboardRow(buttons ...KeyboardButton) []KeyboardButton { - var row []KeyboardButton - row = append(row, buttons...) - return row -} - -// NewReplyKeyboardButton creates new button with custom text for sending it. -func NewReplyKeyboardButton(text string) KeyboardButton { - return KeyboardButton{ - Text: text, - } -} - -// NewReplyKeyboardButtonContact creates new button with custom text for sending -// user contact. -func NewReplyKeyboardButtonContact(text string) KeyboardButton { - return KeyboardButton{ - Text: text, - RequestContact: true, - } -} - -// NewReplyKeyboardButtonLocation creates new button with custom text for sending -// user location. -func NewReplyKeyboardButtonLocation(text string) KeyboardButton { - return KeyboardButton{ - Text: text, - RequestLocation: true, - } -} - -// IsCreator checks that current member is creator. -func (m *ChatMember) IsCreator() bool { - return m != nil && strings.EqualFold(m.Status, StatusCreator) -} - -// IsAdministrator checks that current member is administrator. -func (m *ChatMember) IsAdministrator() bool { - return m != nil && strings.EqualFold(m.Status, StatusAdministrator) -} - -// IsRestricted checks that current member has been restricted. -func (m *ChatMember) IsRestricted() bool { - return m != nil && strings.EqualFold(m.Status, StatusRestricted) -} - -// IsLeft checks that current member has left the chat. -func (m *ChatMember) IsLeft() bool { - return m != nil && strings.EqualFold(m.Status, StatusLeft) -} - -// IsKicked checks that current member has been kicked. -func (m *ChatMember) IsKicked() bool { - return m != nil && strings.EqualFold(m.Status, StatusKicked) -} - -// UntilTime parse UntilDate of restrictions and returns time.Time. -func (m *ChatMember) UntilTime() *time.Time { - if m == nil { - return nil - } - - ut := time.Unix(m.UntilDate, 0) - return &ut -} - -// IsCommand checks that the current message is a bot command. -func (m *Message) IsCommand() bool { - if !m.IsText() || !m.HasEntities() { - return false - } - - entity := m.Entities[0] - return entity.IsBotCommand() && entity.Offset == 0 -} - -// IsCommandEqual checks that the current message is a specific bot command. -func (m *Message) IsCommandEqual(command string) bool { - return m.IsCommand() && strings.EqualFold(m.Command(), command) -} - -// Command returns identifier of the bot command without bot username, if it was -// available -func (m *Message) Command() string { - if !m.IsCommand() { - return "" - } - - return strings.Split(m.RawCommand(), "@")[0] -} - -// RawCommand returns identifier of the bot command with bot username, if it was -// available -func (m *Message) RawCommand() string { - if !m.IsCommand() { - return "" - } - - return string([]rune(m.Text)[1:m.Entities[0].Length]) -} - -// HasCommandArgument checks that the current command message contains argument. -func (m *Message) HasCommandArgument() bool { - if !m.IsCommand() { - return false - } - - entity := m.Entities[0] - if !entity.IsBotCommand() { - return false - } - - return len([]rune(m.Text)) != entity.Length -} - -// CommandArgument returns raw command argument. -func (m *Message) CommandArgument() string { - if !m.HasCommandArgument() { - return "" - } - - return string([]rune(m.Text)[m.Entities[0].Length+1:]) -} - -// IsReply checks that the current message is a reply on other message. -func (m *Message) IsReply() bool { - return m != nil && m.ReplyToMessage != nil -} - -// IsForward checks that the current message is a forward of other message. -func (m *Message) IsForward() bool { - return m != nil && m.ForwardFrom != nil -} - -// Time parse current message Date and returns time.Time. -func (m *Message) Time() *time.Time { - if m == nil { - return nil - } - - t := time.Unix(m.Date, 0) - return &t -} - -// ForwardTime parse current message ForwardDate and returns time.Time. -func (m *Message) ForwardTime() *time.Time { - if m == nil { - return nil - } - - ft := time.Unix(m.ForwardDate, 0) - return &ft -} - -// EditTime parse current message EditDate and returns time.Time. -func (m *Message) EditTime() *time.Time { - if m == nil || !m.HasBeenEdited() { - return nil - } - - et := time.Unix(m.EditDate, 0) - return &et -} - -// HasBeenEdited checks that the current message has been edited. -func (m *Message) HasBeenEdited() bool { - return m != nil && m.EditDate > 0 -} - -// IsText checks that the current message is just a text message. -func (m *Message) IsText() bool { - return m != nil && m.Text != "" -} - -// IsAudio checks that the current message is a audio. -func (m *Message) IsAudio() bool { - return m != nil && m.Audio != nil -} - -// IsDocument checks that the current message is a document. -func (m *Message) IsDocument() bool { - return m != nil && m.Document != nil -} - -// IsGame checks that the current message is a game. -func (m *Message) IsGame() bool { - return m != nil && m.Game != nil -} - -// IsPhoto checks that the current message is a photo. -func (m *Message) IsPhoto() bool { - return m != nil && len(m.Photo) > 0 -} - -// IsSticker checks that the current message is a sticker. -func (m *Message) IsSticker() bool { - return m != nil && m.Sticker != nil -} - -// IsVideo checks that the current message is a video. -func (m *Message) IsVideo() bool { - return m != nil && m.Video != nil -} - -// IsVoice checks that the current message is a voice. -func (m *Message) IsVoice() bool { - return m != nil && m.Voice != nil -} - -// IsVideoNote checks that the current message is a video note. -func (m *Message) IsVideoNote() bool { - return m != nil && m.VideoNote != nil -} - -// IsContact checks that the current message is a contact. -func (m *Message) IsContact() bool { - return m != nil && m.Contact != nil -} - -// IsLocation checks that the current message is a location. -func (m *Message) IsLocation() bool { - return m != nil && m.Location != nil -} - -// IsVenue checks that the current message is a venue. -func (m *Message) IsVenue() bool { - return m != nil && m.Venue != nil -} - -// IsAnimation checks that the current message is a animation. -func (m *Message) IsAnimation() bool { - return m != nil && m.Animation != nil -} - -// IsNewChatMembersEvent checks that the current message is a event of entry of -// new members. -func (m *Message) IsNewChatMembersEvent() bool { - return m != nil && len(m.NewChatMembers) > 0 -} - -// IsLeftChatMemberEvent checks that the current message is a event of members -// exit. -func (m *Message) IsLeftChatMemberEvent() bool { - return m != nil && m.LeftChatMember != nil -} - -// IsNewChatTitleEvent checks that the current message is a event of setting a -// new chat title. -func (m *Message) IsNewChatTitleEvent() bool { - return m != nil && !strings.EqualFold(m.NewChatTitle, "") -} - -// IsNewChatPhotoEvent checks that the current message is a event of setting a -// new chat avatar. -func (m *Message) IsNewChatPhotoEvent() bool { - return m != nil && len(m.NewChatPhoto) > 0 -} - -// IsDeleteChatPhotoEvent checks that the current message is a event of deleting -// a chat avatar. -func (m *Message) IsDeleteChatPhotoEvent() bool { - return m != nil && m.DeleteChatPhoto -} - -// IsGroupChatCreatedEvent checks that the current message is a event of creating -// a new group. -func (m *Message) IsGroupChatCreatedEvent() bool { - return m != nil && m.GroupChatCreated -} - -// IsSupergroupChatCreatedEvent checks that the current message is a event of -// creating a new supergroup. -func (m *Message) IsSupergroupChatCreatedEvent() bool { - return m != nil && m.SupergroupChatCreated -} - -// IsChannelChatCreatedEvent checks that the current message is a event of -// creating a new channel. -func (m *Message) IsChannelChatCreatedEvent() bool { - return m != nil && m.ChannelChatCreated -} - -// IsPinnedMessage checks that the current message is a event of pinning another -// message. -func (m *Message) IsPinnedMessage() bool { - return m != nil && m.PinnedMessage != nil -} - -// IsInvoice checks that the current message is a invoice. -func (m *Message) IsInvoice() bool { - return m != nil && m.Invoice != nil -} - -// IsSuccessfulPayment checks that the current message is a event of successful -// payment. -func (m *Message) IsSuccessfulPayment() bool { - return m != nil && m.SuccessfulPayment != nil -} - -// IsPoll checks that the current message is a poll. -func (m *Message) IsPoll() bool { - return m != nil && m.Poll != nil -} - -// HasEntities checks that the current message contains entities. -func (m *Message) HasEntities() bool { - return m != nil && len(m.Entities) > 0 -} - -// HasCaptionEntities checks that the current media contains entities in caption. -func (m *Message) HasCaptionEntities() bool { - return m != nil && len(m.CaptionEntities) > 0 -} - -// HasMentions checks that the current message contains mentions. -func (m *Message) HasMentions() bool { - if !m.HasEntities() { - return false - } - - for _, entity := range m.Entities { - if entity.IsMention() || entity.IsTextMention() { - return true - } - } - - return false -} - -// HasCaptionMentions checks that the current media contains mentions in caption. -func (m *Message) HasCaptionMentions() bool { - if !m.HasCaptionEntities() { - return false - } - - for _, entity := range m.CaptionEntities { - if entity.IsMention() || entity.IsTextMention() { - return true - } - } - - return false -} - -// HasCaption checks that the current media has caption. -func (m *Message) HasCaption() bool { - return m != nil && m.Caption != "" -} - -// HasAuthorSignature checks that the current channel post has author signature. -func (m *Message) HasAuthorSignature() bool { - return m != nil && m.AuthorSignature != "" -} - -// IsEvent checks what current message is a any chat event. -func (m *Message) IsEvent() bool { - return m.IsChannelChatCreatedEvent() || - m.IsDeleteChatPhotoEvent() || - m.IsGroupChatCreatedEvent() || - m.IsLeftChatMemberEvent() || - m.IsNewChatMembersEvent() || - m.IsNewChatTitleEvent() || - m.IsSupergroupChatCreatedEvent() || - m.IsNewChatPhotoEvent() -} - -func sortPhotos(ps []PhotoSize, reverse bool) []PhotoSize { - buf := make([]PhotoSize, len(ps)) - copy(buf, ps) - - sort.Slice(buf, func(i, j int) bool { - if reverse { - return buf[i].Width > buf[j].Width && - buf[i].Height > buf[j].Height - } - - return buf[i].Width < buf[j].Width && - buf[i].Height < buf[j].Height - }) - - return buf -} - -func (m *Message) BigPhoto() *PhotoSize { - if m == nil || !m.IsPhoto() { - return nil - } - - if len(m.Photo) == 1 { - return &m.Photo[0] - } - - sp := sortPhotos(m.Photo, true) - return &sp[0] -} - -func (m *Message) SmallPhoto() *PhotoSize { - if m == nil || !m.IsPhoto() { - return nil - } - - if len(m.Photo) == 1 { - return &m.Photo[0] - } - - sp := sortPhotos(m.Photo, false) - return &sp[0] -} - -func (m *Message) BigChatPhoto() *PhotoSize { - if m == nil || !m.IsNewChatPhotoEvent() { - return nil - } - - if len(m.NewChatPhoto) == 1 { - return &m.NewChatPhoto[0] - } - - sp := sortPhotos(m.NewChatPhoto, true) - return &sp[0] -} - -func (m *Message) SmallChatPhoto() *PhotoSize { - if m == nil || !m.IsNewChatPhotoEvent() { - return nil - } - - if len(m.NewChatPhoto) == 1 { - return &m.NewChatPhoto[0] - } - - sp := sortPhotos(m.NewChatPhoto, false) - return &sp[0] -} - -func decrypt(pk *rsa.PrivateKey, s, h, d string) (obj []byte, err error) { - // Note that all base64-encoded fields should be decoded before use. - secret, err := decodeField(s) - if err != nil { - return nil, err - } - - hash, err := decodeField(h) - if err != nil { - return nil, err - } - - data, err := decodeField(d) - if err != nil { - return nil, err - } - - if pk != nil { - // Decrypt the credentials secret (secret field in EncryptedCredentials) - // using your private key - secret, err = decryptSecret(pk, secret) - if err != nil { - return nil, err - } - } - - // Use this secret and the credentials hash (hash field in - // EncryptedCredentials) to calculate credentials_key and credentials_iv - key, iv := decryptSecretHash(secret, hash) - if err != nil { - return nil, err - } - - // Decrypt the credentials data (data field in EncryptedCredentials) by - // AES256-CBC using these credentials_key and credentials_iv. - data, err = decryptData(key, iv, data) - if err != nil { - return nil, err - } - - // IMPORTANT: At this step, make sure that the credentials hash is equal - // to SHA256(credentials_data) - if !match(hash, data) { - return nil, ErrNotEqual - } - - // Credentials data is padded with 32 to 255 random padding bytes to make - // its length divisible by 16 bytes. The first byte contains the length - // of this padding (including this byte). Remove the padding to get the - // data. - offset := int(data[0]) - return data[offset:], nil -} - -func decodeField(rawField string) (field []byte, err error) { - return base64.StdEncoding.DecodeString(rawField) -} - -func decryptSecret(pk *rsa.PrivateKey, s []byte) (secret []byte, err error) { - return rsa.DecryptOAEP(sha1.New(), rand.Reader, pk, s, nil) -} - -func decryptSecretHash(s, h []byte) (key, iv []byte) { - hash := sha512.New() - var err error - if _, err = hash.Write(s); err != nil { - return - } - if _, err = hash.Write(h); err != nil { - return - } - sh := hash.Sum(nil) - - return sh[0:32], sh[32 : 32+16] -} - -func match(h, d []byte) bool { - dh := sha256.New() - if _, err := dh.Write(d); err != nil { - return false - } - - return bytes.EqualFold(h, dh.Sum(nil)) -} - -func decryptData(key, iv, data []byte) (buf []byte, err error) { - block, err := aes.NewCipher(key) - if err != nil { - return - } - - buf = make([]byte, len(data)) - cipher.NewCBCDecrypter(block, iv).CryptBlocks(buf, data) - - return -} - -func (peedf *PassportElementErrorDataField) PassportElementErrorMessage() string { - if peedf == nil { - return "" - } - - return peedf.Message -} - -func (peedf *PassportElementErrorDataField) PassportElementErrorSource() string { - if peedf == nil { - return "" - } - - return peedf.Source -} - -func (peedf *PassportElementErrorDataField) PassportElementErrorType() string { - if peedf == nil { - return "" - } - - return peedf.Type -} - -func (peeff *PassportElementErrorFrontSide) PassportElementErrorMessage() string { - if peeff == nil { - return "" - } - - return peeff.Message -} - -func (peeff *PassportElementErrorFrontSide) PassportElementErrorSource() string { - if peeff == nil { - return "" - } - - return peeff.Source -} - -func (peeff *PassportElementErrorFrontSide) PassportElementErrorType() string { - if peeff == nil { - return "" - } - - return peeff.Type -} - -func (peerf *PassportElementErrorReverseSide) PassportElementErrorMessage() string { - if peerf == nil { - return "" - } - - return peerf.Message -} - -func (peerf *PassportElementErrorReverseSide) PassportElementErrorSource() string { - if peerf == nil { - return "" - } - - return peerf.Source -} - -func (peerf *PassportElementErrorReverseSide) PassportElementErrorType() string { - if peerf == nil { - return "" - } - - return peerf.Type -} - -func (pees *PassportElementErrorSelfie) PassportElementErrorMessage() string { - if pees == nil { - return "" - } - - return pees.Message -} - -func (pees *PassportElementErrorSelfie) PassportElementErrorSource() string { - if pees == nil { - return "" - } - - return pees.Source -} - -func (pees *PassportElementErrorSelfie) PassportElementErrorType() string { - if pees == nil { - return "" - } - - return pees.Type -} - -func (peef *PassportElementErrorFile) PassportElementErrorMessage() string { - if peef == nil { - return "" - } - - return peef.Message -} - -func (peef *PassportElementErrorFile) PassportElementErrorSource() string { - if peef == nil { - return "" - } - - return peef.Source -} - -func (peef *PassportElementErrorFile) PassportElementErrorType() string { - if peef == nil { - return "" - } - - return peef.Type -} - -func (peef *PassportElementErrorFiles) PassportElementErrorMessage() string { - if peef == nil { - return "" - } - - return peef.Message -} - -func (peef *PassportElementErrorFiles) PassportElementErrorSource() string { - if peef == nil { - return "" - } - - return peef.Source -} - -func (peef *PassportElementErrorFiles) PassportElementErrorType() string { - if peef == nil { - return "" - } - - return peef.Type -} - -func (peetf *PassportElementErrorTranslationFile) PassportElementErrorMessage() string { - if peetf == nil { - return "" - } - - return peetf.Message -} - -func (peetf *PassportElementErrorTranslationFile) PassportElementErrorSource() string { - if peetf == nil { - return "" - } - - return peetf.Source -} - -func (peetf *PassportElementErrorTranslationFile) PassportElementErrorType() string { - if peetf == nil { - return "" - } - - return peetf.Type -} - -func (peetf *PassportElementErrorTranslationFiles) PassportElementErrorMessage() string { - if peetf == nil { - return "" - } - - return peetf.Message -} - -func (peetf *PassportElementErrorTranslationFiles) PassportElementErrorSource() string { - if peetf == nil { - return "" - } - - return peetf.Source -} - -func (peetf *PassportElementErrorTranslationFiles) PassportElementErrorType() string { - if peetf == nil { - return "" - } - - return peetf.Type -} - -func (peeu *PassportElementErrorUnspecified) PassportElementErrorMessage() string { - if peeu == nil { - return "" - } - - return peeu.Message -} - -func (peeu *PassportElementErrorUnspecified) PassportElementErrorSource() string { - if peeu == nil { - return "" - } - - return peeu.Source -} - -func (peeu *PassportElementErrorUnspecified) PassportElementErrorType() string { - if peeu == nil { - return "" - } - - return peeu.Type -} - -func (pseoos *PassportScopeElementOneOfSeveral) PassportScopeElementTranslation() bool { - if pseoos == nil { - return false - } - - return pseoos.Translation -} - -func (pseoos *PassportScopeElementOneOfSeveral) PassportScopeElementSelfie() bool { - if pseoos == nil { - return false - } - - return pseoos.Selfie -} - -func (pseo *PassportScopeElementOne) PassportScopeElementTranslation() bool { - if pseo == nil { - return false - } - - return pseo.Translation -} - -func (pseo *PassportScopeElementOne) PassportScopeElementSelfie() bool { - if pseo == nil { - return false - } - - return pseo.Selfie -} - -func (pd *PersonalDetails) BirthTime() *time.Time { - if pd == nil || pd.BirthDate == "" { - return nil - } - - bt, err := time.Parse("02.01.2006", pd.BirthDate) - if err != nil { - return nil - } - - return &bt -} - -func (pd *PersonalDetails) FullName() string { - if pd == nil { - return "" - } - - return pd.FirstName + " " + pd.LastName -} - -func (pd *PersonalDetails) FullNameNative() string { - if pd == nil { - return "" - } - - return pd.FirstNameNative + " " + pd.LastNameNative -} - -func (sv *SecureValue) HasData() bool { - return sv != nil && sv.Data != nil -} - -func (sv *SecureValue) HasFiles() bool { - return sv != nil && len(sv.Files) > 0 -} - -func (sv *SecureValue) HasFrontSide() bool { - return sv != nil && sv.FrontSide != nil -} - -func (sv *SecureValue) HasReverseSide() bool { - return sv != nil && sv.ReverseSide != nil -} - -func (sv *SecureValue) HasSelfie() bool { - return sv != nil && sv.Selfie != nil -} - -func (sv *SecureValue) HasTranslation() bool { - return sv != nil && len(sv.Translation) > 0 -} - -// InSet checks that the current sticker in the stickers set. -// -// For uploaded WebP files this return false. -func (s *Sticker) InSet() bool { - return s != nil && s.SetName != "" -} - -// IsWebP check that the current sticker is a WebP file uploaded by user. -func (s *Sticker) IsWebP() bool { - return s != nil && s.SetName == "" -} - -// Set use bot for getting parent StickerSet if SetName is present. -// -// Return nil if current sticker has been uploaded by user as WebP file. -func (s *Sticker) Set(bot *Bot) *StickerSet { - if s.IsWebP() || bot == nil { - return nil - } - - set, err := bot.GetStickerSet(s.SetName) - if err != nil { - return nil - } - - return set -} - -func (s *Sticker) HasThumb() bool { - return s != nil && s.Thumb != nil -} - -func (s *Sticker) IsMask() bool { - return s != nil && s.MaskPosition != nil -} - -func (s *Sticker) File() *File { - if s == nil { - return nil - } - - return &File{ - FileID: s.FileID, - FileSize: s.FileSize, - } -} - -// NewLongPollingChannel creates channel for receive incoming updates using long -// polling. -func (b *Bot) NewLongPollingChannel(params *GetUpdatesParameters) UpdatesChannel { +// NewLongPollingChannel creates channel for receive incoming updates using long polling. +func (b *Bot) NewLongPollingChannel(params *GetUpdates) chan *Update { if params == nil { - params = &GetUpdatesParameters{ + params = &GetUpdates{ Offset: 0, Limit: 100, Timeout: 60, } } - channel := make(chan Update, params.Limit) + b.Updates = make(chan *Update, params.Limit) + go func() { for { updates, err := b.GetUpdates(params) @@ -2464,239 +186,69 @@ func (b *Bot) NewLongPollingChannel(params *GetUpdatesParameters) UpdatesChannel } for _, update := range updates { - if update.ID >= params.Offset { - params.Offset = update.ID + 1 - channel <- update + if update.UpdateID < params.Offset { + continue } + + params.Offset = update.UpdateID + 1 + b.Updates <- update } } }() - return channel + return b.Updates } // NewWebhookChannel creates channel for receive incoming updates via an outgoing webhook. // -// If cert argument is provided by two strings (["path/to/cert.file", "path/to/cert.key"]), then TLS server will -// be created by this filepaths. -func (b *Bot) NewWebhookChannel(setURL *http.URI, params *SetWebhookParameters, ln net.Listener, cert ...string) (updates UpdatesChannel, shutdown ShutdownFunc) { - defer http.ReleaseURI(setURL) - if params == nil { - params = &SetWebhookParameters{ - URL: setURL.String(), - MaxConnections: 40, - } - } - - var err error - channel := make(chan Update, 100) +// If cert argument is provided by two strings (["path/to/cert.file", "path/to/cert.key"]), then TLS server will be created by this filepaths. +func (b *Bot) NewWebhookChannel(u *http.URI, p SetWebhook, ln net.Listener, crt ...string) (chan *Update, func() error) { + b.Updates = make(chan *Update, 100) handleFunc := func(ctx *http.RequestCtx) { dlog.Ln("Request path:", string(ctx.Path())) - if !bytes.HasPrefix(ctx.Path(), setURL.Path()) { + + if !bytes.HasPrefix(ctx.Path(), u.Path()) { dlog.Ln("Unsupported request path:", string(ctx.Path())) return } + dlog.Ln("Catched supported request path:", string(ctx.Path())) - var update Update - if err = parser.Unmarshal(ctx.Request.Body(), &update); err != nil { + upd := new(Update) + if err := b.marshler.Unmarshal(ctx.Request.Body(), upd); err != nil { return } - channel <- update + b.Updates <- upd } srv := http.Server{ Name: b.Username, - Concurrency: params.MaxConnections, + Concurrency: p.MaxConnections, Handler: handleFunc, ReduceMemoryUsage: true, } + var err error + go func() { switch { - case len(cert) == 2: + case len(crt) == 2: dlog.Ln("Creating TLS router...") - err = srv.ServeTLS(ln, cert[0], cert[1]) + err = srv.ServeTLS(ln, crt[0], crt[1]) default: dlog.Ln("Creating simple router...") err = srv.Serve(ln) } + if err != nil { log.Fatalln(err.Error()) } }() - if _, err = b.SetWebhook(params); err != nil { + if _, err = b.SetWebhook(p); err != nil { log.Fatalln(err.Error()) } - return channel, srv.Shutdown -} - -// IsMessage checks that the current update is a message creation event. -func (u *Update) IsMessage() bool { - return u != nil && u.Message != nil -} - -// IsEditedMessage checks that the current update is a editing message event. -func (u *Update) IsEditedMessage() bool { - return u != nil && u.EditedMessage != nil -} - -// IsChannelPost checks that the current update is a post channel creation event. -func (u *Update) IsChannelPost() bool { - return u != nil && u.ChannelPost != nil -} - -// IsEditedChannelPost checks that the current update is a editing post channel -// event. -func (u *Update) IsEditedChannelPost() bool { - return u != nil && u.EditedChannelPost != nil -} - -// IsInlineQuery checks that the current update is a inline query update. -func (u *Update) IsInlineQuery() bool { - return u != nil && u.InlineQuery != nil -} - -// IsChosenInlineResult checks that the current update is a chosen inline result -// update. -func (u *Update) IsChosenInlineResult() bool { - return u != nil && u.ChosenInlineResult != nil -} - -// IsCallbackQuery checks that the current update is a callback query update. -func (u *Update) IsCallbackQuery() bool { - return u != nil && u.CallbackQuery != nil -} - -// IsShippingQuery checks that the current update is a shipping query update. -func (u *Update) IsShippingQuery() bool { - return u != nil && u.ShippingQuery != nil -} - -// IsPreCheckoutQuery checks that the current update is a pre checkout query -// update. -func (u *Update) IsPreCheckoutQuery() bool { - return u != nil && u.PreCheckoutQuery != nil -} - -// IsPoll checks that the current update is a poll update. -func (u *Update) IsPoll() bool { - return u != nil && u.Poll != nil -} - -// Type return update type for current update. -func (u *Update) Type() string { - switch { - case u.IsCallbackQuery(): - return UpdateCallbackQuery - case u.IsChannelPost(): - return UpdateChannelPost - case u.IsChosenInlineResult(): - return UpdateChosenInlineResult - case u.IsEditedChannelPost(): - return UpdateEditedChannelPost - case u.IsEditedMessage(): - return UpdateEditedMessage - case u.IsInlineQuery(): - return UpdateInlineQuery - case u.IsMessage(): - return UpdateMessage - case u.IsPreCheckoutQuery(): - return UpdatePreCheckoutQuery - case u.IsShippingQuery(): - return UpdateShippingQuery - case u.IsPoll(): - return UpdatePoll - default: - return "" - } -} - -// Language parse LanguageCode of current user and returns language.Tag. -func (u *User) Language() language.Tag { - if u == nil { - return language.Und - } - - tag, err := language.Parse(u.LanguageCode) - if err != nil { - return language.Und - } - - return tag -} - -// NewPrinter create simple message.Printer with User.Language() by default. -func (u *User) NewPrinter() *message.Printer { - return message.NewPrinter(u.Language()) -} - -// FullName returns the full name of user or FirstName if LastName is not -// available. -func (u *User) FullName() string { - if u == nil { - return "" - } - - if u.HasLastName() { - return u.FirstName + " " + u.LastName - } - - return u.FirstName -} - -// HaveLastName checks what the current user has a LastName. -func (u *User) HasLastName() bool { - return u != nil && u.LastName != "" -} - -// HaveUsername checks what the current user has a username. -func (u *User) HasUsername() bool { - return u != nil && u.Username != "" -} - -func (v *Video) HasThumb() bool { - return v != nil && v.Thumb != nil -} - -func (v *Video) File() *File { - if v == nil { - return nil - } - - return &File{ - FileID: v.FileID, - FileSize: v.FileSize, - } -} - -func (vn *VideoNote) HasThumb() bool { - return vn != nil && vn.Thumb != nil -} - -func (vn *VideoNote) File() *File { - if vn == nil { - return nil - } - - return &File{ - FileID: vn.FileID, - FileSize: vn.FileSize, - } -} - -func (wi *WebhookInfo) HasURL() bool { - return wi != nil && wi.URL != "" -} - -func (wi *WebhookInfo) LastErrorTime() *time.Time { - if wi == nil { - return nil - } - - led := time.Unix(wi.LastErrorDate, 0) - return &led + return b.Updates, srv.Shutdown } diff --git a/utils_test.go b/utils_test.go deleted file mode 100644 index d9ed2cb..0000000 --- a/utils_test.go +++ /dev/null @@ -1,195 +0,0 @@ -package telegram - -import ( - "strconv" - "testing" - - "github.com/stretchr/testify/assert" - http "github.com/valyala/fasthttp" -) - -const ( - testPhrase = "Hello, world!" - testLink = "https://toby3d.me/" - testID = 123456789 - // testUsername = "toby3d" - // testChannel = "toby3dRu" - testCode = "hide me" -) - -var ( - testPhotoSize = PhotoSize{ - FileID: "cba", - FileSize: 123, - Width: 128, - Height: 128, - } - testAnimation = Animation{ - FileID: "abc", - FileName: "animation", - FileSize: 321, - Thumb: &testPhotoSize, - } - testAudio = Audio{ - Performer: "One-Aloner", - Title: "(Not) your reality (Doki Doki Literature Club cover)", - Thumb: &testPhotoSize, - } -) - -func TestMarkdown(t *testing.T) { - t.Run("bold", func(t *testing.T) { - assert.Equal(t, "*"+testPhrase+"*", NewMarkdownBold(testPhrase)) - }) - t.Run("italic", func(t *testing.T) { - assert.Equal(t, "_"+testPhrase+"_", NewMarkdownItalic(testPhrase)) - }) - t.Run("link", func(t *testing.T) { - link := http.AcquireURI() - defer http.ReleaseURI(link) - link.Update(testLink) - assert.Equal(t, "["+testPhrase+"]("+testLink+")", NewMarkdownURL(testPhrase, link)) - }) - t.Run("mention", func(t *testing.T) { - assert.Equal(t, "["+testPhrase+"](tg://user/?id="+strconv.Itoa(testID)+")", NewMarkdownMention(testPhrase, testID)) - }) - t.Run("code", func(t *testing.T) { - assert.Equal(t, "`"+testCode+"`", NewMarkdownCode(testCode)) - }) - t.Run("code block", func(t *testing.T) { - assert.Equal(t, "```"+testCode+"```", NewMarkdownCodeBlock(testCode)) - }) -} - -func TestHTML(t *testing.T) { - t.Run("bold", func(t *testing.T) { - assert.Equal(t, ""+testPhrase+"", NewHTMLBold(testPhrase)) - }) - t.Run("italic", func(t *testing.T) { - assert.Equal(t, ""+testPhrase+"", NewHTMLItalic(testPhrase)) - }) - t.Run("link", func(t *testing.T) { - link := http.AcquireURI() - defer http.ReleaseURI(link) - link.Update(testLink) - assert.Equal(t, ``+testPhrase+``, NewHTMLURL(testPhrase, link)) - }) - t.Run("mention", func(t *testing.T) { - assert.Equal(t, ``+testPhrase+``, NewHTMLMention(testPhrase, testID)) - }) - t.Run("code", func(t *testing.T) { - assert.Equal(t, ""+testCode+"", NewHTMLCode(testCode)) - }) - t.Run("code block", func(t *testing.T) { - assert.Equal(t, "
"+testCode+"
", NewHTMLCodeBlock(testCode)) - }) -} - -func TestAnimation(t *testing.T) { - t.Run("has thumb", func(t *testing.T) { - t.Run("false", func(t *testing.T) { - a := new(Animation) - assert.False(t, a.HasThumb()) - }) - t.Run("true", func(t *testing.T) { - assert.True(t, testAnimation.HasThumb()) - }) - }) - t.Run("file", func(t *testing.T) { - assert.NotNil(t, testAnimation.File()) - }) -} - -func TestAudio(t *testing.T) { - t.Run("has performer", func(t *testing.T) { - t.Run("false", func(t *testing.T) { - a := new(Audio) - assert.False(t, a.HasPerformer()) - }) - t.Run("true", func(t *testing.T) { - assert.True(t, testAudio.HasPerformer()) - }) - }) - t.Run("has title", func(t *testing.T) { - t.Run("false", func(t *testing.T) { - a := new(Audio) - assert.False(t, a.HasTitle()) - }) - t.Run("true", func(t *testing.T) { - assert.True(t, testAudio.HasTitle()) - }) - }) - t.Run("has thumb", func(t *testing.T) { - t.Run("false", func(t *testing.T) { - a := new(Audio) - assert.False(t, a.HasThumb()) - }) - t.Run("true", func(t *testing.T) { - assert.True(t, testAudio.HasThumb()) - }) - }) - t.Run("file", func(t *testing.T) { - assert.NotNil(t, testAudio.File()) - }) - t.Run("full name", func(t *testing.T) { - for _, tc := range []struct { - message string - audio *Audio - separator string - expResult string - }{{ - message: "empty", - expResult: DefaultAudioTitle, - }, { - message: "separator only", - separator: DefaultAudioSeparator, - expResult: DefaultAudioTitle, - }, { - message: "title only", - audio: &Audio{ - Title: testAudio.Title, - }, - expResult: testAudio.Title, - }, { - message: "performer only", - audio: &Audio{ - Performer: testAudio.Performer, - }, - expResult: testAudio.Performer + DefaultAudioSeparator + DefaultAudioTitle, - }, { - message: "title & performer", - audio: &Audio{ - Performer: testAudio.Performer, - Title: testAudio.Title, - }, - expResult: testAudio.Performer + DefaultAudioSeparator + testAudio.Title, - }, { - message: "title & separator", - separator: " | ", - audio: &Audio{ - Title: testAudio.Title, - }, - expResult: testAudio.Title, - }, { - message: "performer & separator", - separator: " | ", - audio: &Audio{ - Performer: testAudio.Performer, - }, - expResult: testAudio.Performer + " | " + DefaultAudioTitle, - }, { - message: "performer, title & separator", - separator: " | ", - audio: &Audio{ - Performer: testAudio.Performer, - Title: testAudio.Title, - }, - expResult: testAudio.Performer + " | " + testAudio.Title, - }} { - tc := tc - t.Run(tc.message, func(t *testing.T) { - assert.Equal(t, tc.expResult, tc.audio.FullName(tc.separator)) - }) - } - }) -}