diff --git a/README.md b/README.md index 266f4ed..d9a6873 100644 --- a/README.md +++ b/README.md @@ -91,7 +91,13 @@ func main() { if err != nil { log.Fatal(err) } - + info, err := bot.GetWebhookInfo() + if err != nil { + log.Fatal(err) + } + if info.LastErrorDate != 0 { + log.Printf("[Telegram callback failed]%s", info.LastErrorMessage) + } updates := bot.ListenForWebhook("/" + bot.Token) go http.ListenAndServeTLS("0.0.0.0:8443", "cert.pem", "key.pem", nil) diff --git a/bot.go b/bot.go index 9d2adab..8fb6200 100644 --- a/bot.go +++ b/bot.go @@ -7,6 +7,7 @@ import ( "encoding/json" "errors" "fmt" + "io" "io/ioutil" "log" "net/http" @@ -67,31 +68,49 @@ func (bot *BotAPI) MakeRequest(endpoint string, params url.Values) (APIResponse, } defer resp.Body.Close() - if resp.StatusCode == http.StatusForbidden { - return APIResponse{}, errors.New(ErrAPIForbidden) + var apiResp APIResponse + bytes, err := bot.decodeAPIResponse(resp.Body, &apiResp) + if err != nil { + return apiResp, err } - if resp.StatusCode != http.StatusOK { - return APIResponse{}, errors.New(http.StatusText(resp.StatusCode)) + if bot.Debug { + log.Printf("%s resp: %s", endpoint, bytes) } - bytes, err := ioutil.ReadAll(resp.Body) - if err != nil { - return APIResponse{}, err + if !apiResp.Ok { + parameters := ResponseParameters{} + if apiResp.Parameters != nil { + parameters = *apiResp.Parameters + } + return apiResp, Error{apiResp.Description, parameters} } - if bot.Debug { - log.Println(endpoint, string(bytes)) + return apiResp, nil +} + +// decodeAPIResponse decode response and return slice of bytes if debug enabled. +// If debug disabled, just decode http.Response.Body stream to APIResponse struct +// for efficient memory usage +func (bot *BotAPI) decodeAPIResponse(responseBody io.Reader, resp *APIResponse) (_ []byte, err error) { + if !bot.Debug { + dec := json.NewDecoder(responseBody) + err = dec.Decode(resp) + return } - var apiResp APIResponse - json.Unmarshal(bytes, &apiResp) + // if debug, read reponse body + data, err := ioutil.ReadAll(responseBody) + if err != nil { + return + } - if !apiResp.Ok { - return apiResp, errors.New(apiResp.Description) + err = json.Unmarshal(data, resp) + if err != nil { + return } - return apiResp, nil + return data, nil } // makeMessageRequest makes a request to a method that returns a Message. @@ -712,24 +731,28 @@ func (bot *BotAPI) RestrictChatMember(config RestrictChatMemberConfig) (APIRespo } v.Add("user_id", strconv.Itoa(config.UserID)) - if &config.CanSendMessages != nil { + if config.CanSendMessages != nil { v.Add("can_send_messages", strconv.FormatBool(*config.CanSendMessages)) } - if &config.CanSendMediaMessages != nil { + if config.CanSendMediaMessages != nil { v.Add("can_send_media_messages", strconv.FormatBool(*config.CanSendMediaMessages)) } - if &config.CanSendOtherMessages != nil { + if config.CanSendOtherMessages != nil { v.Add("can_send_other_messages", strconv.FormatBool(*config.CanSendOtherMessages)) } - if &config.CanAddWebPagePreviews != nil { + if config.CanAddWebPagePreviews != nil { v.Add("can_add_web_page_previews", strconv.FormatBool(*config.CanAddWebPagePreviews)) } + if config.UntilDate != 0 { + v.Add("until_date", strconv.FormatInt(config.UntilDate, 10)) + } bot.debugLog("restrictChatMember", v, nil) return bot.MakeRequest("restrictChatMember", v) } +// PromoteChatMember add admin rights to user func (bot *BotAPI) PromoteChatMember(config PromoteChatMemberConfig) (APIResponse, error) { v := url.Values{} @@ -742,28 +765,28 @@ func (bot *BotAPI) PromoteChatMember(config PromoteChatMemberConfig) (APIRespons } v.Add("user_id", strconv.Itoa(config.UserID)) - if &config.CanChangeInfo != nil { + if config.CanChangeInfo != nil { v.Add("can_change_info", strconv.FormatBool(*config.CanChangeInfo)) } - if &config.CanPostMessages != nil { + if config.CanPostMessages != nil { v.Add("can_post_messages", strconv.FormatBool(*config.CanPostMessages)) } - if &config.CanEditMessages != nil { + if config.CanEditMessages != nil { v.Add("can_edit_messages", strconv.FormatBool(*config.CanEditMessages)) } - if &config.CanDeleteMessages != nil { + if config.CanDeleteMessages != nil { v.Add("can_delete_messages", strconv.FormatBool(*config.CanDeleteMessages)) } - if &config.CanInviteUsers != nil { + if config.CanInviteUsers != nil { v.Add("can_invite_users", strconv.FormatBool(*config.CanInviteUsers)) } - if &config.CanRestrictMembers != nil { + if config.CanRestrictMembers != nil { v.Add("can_restrict_members", strconv.FormatBool(*config.CanRestrictMembers)) } - if &config.CanPinMessages != nil { + if config.CanPinMessages != nil { v.Add("can_pin_messages", strconv.FormatBool(*config.CanPinMessages)) } - if &config.CanPromoteMembers != nil { + if config.CanPromoteMembers != nil { v.Add("can_promote_members", strconv.FormatBool(*config.CanPromoteMembers)) } @@ -837,18 +860,93 @@ func (bot *BotAPI) DeleteMessage(config DeleteMessageConfig) (APIResponse, error // GetInviteLink get InviteLink for a chat func (bot *BotAPI) GetInviteLink(config ChatConfig) (string, error) { - v := url.Values{} + v := url.Values{} + + if config.SuperGroupUsername == "" { + v.Add("chat_id", strconv.FormatInt(config.ChatID, 10)) + } else { + v.Add("chat_id", config.SuperGroupUsername) + } + + resp, err := bot.MakeRequest("exportChatInviteLink", v) + if err != nil { + return "", err + } + + var inviteLink string + err = json.Unmarshal(resp.Result, &inviteLink) + + return inviteLink, err +} + +// PinChatMessage pin message in supergroup +func (bot *BotAPI) PinChatMessage(config PinChatMessageConfig) (APIResponse, error) { + v, err := config.values() + if err != nil { + return APIResponse{}, err + } + + bot.debugLog(config.method(), v, nil) + + return bot.MakeRequest(config.method(), v) +} - if config.SuperGroupUsername == "" { - v.Add("chat_id", strconv.FormatInt(config.ChatID, 10)) - } else { - v.Add("chat_id", config.SuperGroupUsername) - } +// UnpinChatMessage unpin message in supergroup +func (bot *BotAPI) UnpinChatMessage(config UnpinChatMessageConfig) (APIResponse, error) { + v, err := config.values() + if err != nil { + return APIResponse{}, err + } + + bot.debugLog(config.method(), v, nil) + + return bot.MakeRequest(config.method(), v) +} + +// SetChatTitle change title of chat. +func (bot *BotAPI) SetChatTitle(config SetChatTitleConfig) (APIResponse, error) { + v, err := config.values() + if err != nil { + return APIResponse{}, err + } + + bot.debugLog(config.method(), v, nil) + + return bot.MakeRequest(config.method(), v) +} + +// SetChatDescription change description of chat. +func (bot *BotAPI) SetChatDescription(config SetChatDescriptionConfig) (APIResponse, error) { + v, err := config.values() + if err != nil { + return APIResponse{}, err + } + + bot.debugLog(config.method(), v, nil) + + return bot.MakeRequest(config.method(), v) +} + +// SetChatPhoto change photo of chat. +func (bot *BotAPI) SetChatPhoto(config SetChatPhotoConfig) (APIResponse, error) { + params, err := config.params() + if err != nil { + return APIResponse{}, err + } - resp, err := bot.MakeRequest("exportChatInviteLink", v) + file := config.getFile() - var inviteLink string - err = json.Unmarshal(resp.Result, &inviteLink) + return bot.UploadFile(config.method(), params, config.name(), file) +} - return inviteLink, err +// DeleteChatPhoto delete photo of chat. +func (bot *BotAPI) DeleteChatPhoto(config DeleteChatPhotoConfig) (APIResponse, error) { + v, err := config.values() + if err != nil { + return APIResponse{}, err + } + + bot.debugLog(config.method(), v, nil) + + return bot.MakeRequest(config.method(), v) } diff --git a/bot_test.go b/bot_test.go index d7cec08..e7aa7ac 100644 --- a/bot_test.go +++ b/bot_test.go @@ -14,6 +14,7 @@ import ( const ( TestToken = "153667468:AAHlSHlMqSt1f_uFmVRJbm5gntu2HI4WW8I" ChatID = 76918703 + SupergroupChatID = -1001120141283 ReplyToMessageID = 35 ExistingPhotoFileID = "AgADAgADw6cxG4zHKAkr42N7RwEN3IFShCoABHQwXEtVks4EH2wBAAEC" ExistingDocumentFileID = "BQADAgADOQADjMcoCcioX1GrDvp3Ag" @@ -372,7 +373,10 @@ func TestSendWithNewStickerAndKeyboardHide(t *testing.T) { bot, _ := getBot(t) msg := tgbotapi.NewStickerUpload(ChatID, "tests/image.jpg") - msg.ReplyMarkup = tgbotapi.ReplyKeyboardRemove{true, false} + msg.ReplyMarkup = tgbotapi.ReplyKeyboardRemove{ + RemoveKeyboard: true, + Selective: false, + } _, err := bot.Send(msg) if err != nil { @@ -385,7 +389,10 @@ func TestSendWithExistingStickerAndKeyboardHide(t *testing.T) { bot, _ := getBot(t) msg := tgbotapi.NewStickerShare(ChatID, ExistingStickerFileID) - msg.ReplyMarkup = tgbotapi.ReplyKeyboardRemove{true, false} + msg.ReplyMarkup = tgbotapi.ReplyKeyboardRemove{ + RemoveKeyboard: true, + Selective: false, + } _, err := bot.Send(msg) @@ -398,7 +405,7 @@ func TestSendWithExistingStickerAndKeyboardHide(t *testing.T) { func TestGetFile(t *testing.T) { bot, _ := getBot(t) - file := tgbotapi.FileConfig{ExistingPhotoFileID} + file := tgbotapi.FileConfig{FileID: ExistingPhotoFileID} _, err := bot.GetFile(file) @@ -466,7 +473,13 @@ func TestSetWebhookWithCert(t *testing.T) { t.Error(err) t.Fail() } - + info, err := bot.GetWebhookInfo() + if err != nil { + t.Error(err) + } + if info.LastErrorDate != 0 { + t.Errorf("[Telegram callback failed]%s", info.LastErrorMessage) + } bot.RemoveWebhook() } @@ -483,7 +496,13 @@ func TestSetWebhookWithoutCert(t *testing.T) { t.Error(err) t.Fail() } - + info, err := bot.GetWebhookInfo() + if err != nil { + t.Error(err) + } + if info.LastErrorDate != 0 { + t.Errorf("[Telegram callback failed]%s", info.LastErrorMessage) + } bot.RemoveWebhook() } @@ -548,7 +567,13 @@ func ExampleNewWebhook() { if err != nil { log.Fatal(err) } - + info, err := bot.GetWebhookInfo() + if err != nil { + log.Fatal(err) + } + if info.LastErrorDate != 0 { + log.Printf("[Telegram callback failed]%s", info.LastErrorMessage) + } updates := bot.ListenForWebhook("/" + bot.Token) go http.ListenAndServeTLS("0.0.0.0:8443", "cert.pem", "key.pem", nil) @@ -609,3 +634,49 @@ func TestDeleteMessage(t *testing.T) { t.Fail() } } + +func TestPinChatMessage(t *testing.T) { + bot, _ := getBot(t) + + msg := tgbotapi.NewMessage(SupergroupChatID, "A test message from the test library in telegram-bot-api") + msg.ParseMode = "markdown" + message, _ := bot.Send(msg) + + pinChatMessageConfig := tgbotapi.PinChatMessageConfig{ + ChatID: message.Chat.ID, + MessageID: message.MessageID, + DisableNotification: false, + } + _, err := bot.PinChatMessage(pinChatMessageConfig) + + if err != nil { + t.Error(err) + t.Fail() + } +} + +func TestUnpinChatMessage(t *testing.T) { + bot, _ := getBot(t) + + msg := tgbotapi.NewMessage(SupergroupChatID, "A test message from the test library in telegram-bot-api") + msg.ParseMode = "markdown" + message, _ := bot.Send(msg) + + // We need pin message to unpin something + pinChatMessageConfig := tgbotapi.PinChatMessageConfig{ + ChatID: message.Chat.ID, + MessageID: message.MessageID, + DisableNotification: false, + } + _, err := bot.PinChatMessage(pinChatMessageConfig) + + unpinChatMessageConfig := tgbotapi.UnpinChatMessageConfig{ + ChatID: message.Chat.ID, + } + _, err = bot.UnpinChatMessage(unpinChatMessageConfig) + + if err != nil { + t.Error(err) + t.Fail() + } +} diff --git a/configs.go b/configs.go index d7358ee..574b3dd 100644 --- a/configs.go +++ b/configs.go @@ -676,7 +676,7 @@ type SetGameScoreConfig struct { Score int Force bool DisableEditMessage bool - ChatID int + ChatID int64 ChannelUsername string MessageID int InlineMessageID string @@ -689,7 +689,7 @@ func (config SetGameScoreConfig) values() (url.Values, error) { v.Add("score", strconv.Itoa(config.Score)) if config.InlineMessageID == "" { if config.ChannelUsername == "" { - v.Add("chat_id", strconv.Itoa(config.ChatID)) + v.Add("chat_id", strconv.FormatInt(config.ChatID, 10)) } else { v.Add("chat_id", config.ChannelUsername) } @@ -1035,3 +1035,111 @@ func (config DeleteMessageConfig) values() (url.Values, error) { return v, nil } + +// PinChatMessageConfig contains information of a message in a chat to pin. +type PinChatMessageConfig struct { + ChatID int64 + MessageID int + DisableNotification bool +} + +func (config PinChatMessageConfig) method() string { + return "pinChatMessage" +} + +func (config PinChatMessageConfig) values() (url.Values, error) { + v := url.Values{} + + v.Add("chat_id", strconv.FormatInt(config.ChatID, 10)) + v.Add("message_id", strconv.Itoa(config.MessageID)) + v.Add("disable_notification", strconv.FormatBool(config.DisableNotification)) + + return v, nil +} + +// UnpinChatMessageConfig contains information of chat to unpin. +type UnpinChatMessageConfig struct { + ChatID int64 +} + +func (config UnpinChatMessageConfig) method() string { + return "unpinChatMessage" +} + +func (config UnpinChatMessageConfig) values() (url.Values, error) { + v := url.Values{} + + v.Add("chat_id", strconv.FormatInt(config.ChatID, 10)) + + return v, nil +} + +// SetChatTitleConfig contains information for change chat title. +type SetChatTitleConfig struct { + ChatID int64 + Title string +} + +func (config SetChatTitleConfig) method() string { + return "setChatTitle" +} + +func (config SetChatTitleConfig) values() (url.Values, error) { + v := url.Values{} + + v.Add("chat_id", strconv.FormatInt(config.ChatID, 10)) + v.Add("title", config.Title) + + return v, nil +} + +// SetChatDescriptionConfig contains information for change chat description. +type SetChatDescriptionConfig struct { + ChatID int64 + Description string +} + +func (config SetChatDescriptionConfig) method() string { + return "setChatDescription" +} + +func (config SetChatDescriptionConfig) values() (url.Values, error) { + v := url.Values{} + + v.Add("chat_id", strconv.FormatInt(config.ChatID, 10)) + v.Add("description", config.Description) + + return v, nil +} + +// SetChatPhotoConfig contains information for change chat photo +type SetChatPhotoConfig struct { + BaseFile +} + +// name returns the field name for the Photo. +func (config SetChatPhotoConfig) name() string { + return "photo" +} + +// method returns Telegram API method name for sending Photo. +func (config SetChatPhotoConfig) method() string { + return "setChatPhoto" +} + +// DeleteChatPhotoConfig contains information for delete chat photo. +type DeleteChatPhotoConfig struct { + ChatID int64 +} + +func (config DeleteChatPhotoConfig) method() string { + return "deleteChatPhoto" +} + +func (config DeleteChatPhotoConfig) values() (url.Values, error) { + v := url.Values{} + + v.Add("chat_id", strconv.FormatInt(config.ChatID, 10)) + + return v, nil +} diff --git a/helpers.go b/helpers.go index 132d957..c23a3bf 100644 --- a/helpers.go +++ b/helpers.go @@ -641,7 +641,7 @@ func NewCallbackWithAlert(id, text string) CallbackConfig { } } -// NewInvoice created a new Invoice request to the user. +// NewInvoice creates a new Invoice request to the user. func NewInvoice(chatID int64, title, description, payload, providerToken, startParameter, currency string, prices *[]LabeledPrice) InvoiceConfig { return InvoiceConfig{ BaseChat: BaseChat{ChatID: chatID}, @@ -653,3 +653,34 @@ func NewInvoice(chatID int64, title, description, payload, providerToken, startP Currency: currency, Prices: prices} } + +// NewSetChatPhotoUpload creates a new chat photo uploader. +// +// chatID is where to send it, file is a string path to the file, +// FileReader, or FileBytes. +// +// Note that you must send animated GIFs as a document. +func NewSetChatPhotoUpload(chatID int64, file interface{}) SetChatPhotoConfig { + return SetChatPhotoConfig{ + BaseFile: BaseFile{ + BaseChat: BaseChat{ChatID: chatID}, + File: file, + UseExisting: false, + }, + } +} + +// NewSetChatPhotoShare shares an existing photo. +// You may use this to reshare an existing photo without reuploading it. +// +// chatID is where to send it, fileID is the ID of the file +// already uploaded. +func NewSetChatPhotoShare(chatID int64, fileID string) SetChatPhotoConfig { + return SetChatPhotoConfig{ + BaseFile: BaseFile{ + BaseChat: BaseChat{ChatID: chatID}, + FileID: fileID, + UseExisting: true, + }, + } +} diff --git a/types.go b/types.go index 95231e4..d3d30dc 100644 --- a/types.go +++ b/types.go @@ -56,6 +56,7 @@ type User struct { LastName string `json:"last_name"` // optional UserName string `json:"username"` // optional LanguageCode string `json:"language_code"` // optional + IsBot bool `json:"is_bot"` // optional } // String displays a simple text version of a user. @@ -173,21 +174,23 @@ func (m *Message) Time() time.Time { return time.Unix(int64(m.Date), 0) } -// IsCommand returns true if message starts with '/'. +// IsCommand returns true if message starts with a "bot_command" entity. func (m *Message) IsCommand() bool { - return m.Text != "" && m.Text[0] == '/' + if m.Entities == nil || len(*m.Entities) == 0 { + return false + } + + entity := (*m.Entities)[0] + return entity.Offset == 0 && entity.Type == "bot_command" } // Command checks if the message was a command and if it was, returns the // command. If the Message was not a command, it returns an empty string. // -// If the command contains the at bot syntax, it removes the bot name. +// If the command contains the at name syntax, it is removed. Use +// CommandWithAt() if you do not want that. func (m *Message) Command() string { - if !m.IsCommand() { - return "" - } - - command := strings.SplitN(m.Text, " ", 2)[0][1:] + command := m.CommandWithAt() if i := strings.Index(command, "@"); i != -1 { command = command[:i] @@ -196,20 +199,42 @@ func (m *Message) Command() string { return command } +// CommandWithAt checks if the message was a command and if it was, returns the +// command. If the Message was not a command, it returns an empty string. +// +// If the command contains the at name syntax, it is not removed. Use Command() +// if you want that. +func (m *Message) CommandWithAt() string { + if !m.IsCommand() { + return "" + } + + // IsCommand() checks that the message begins with a bot_command entity + entity := (*m.Entities)[0] + return m.Text[1:entity.Length] +} + // CommandArguments checks if the message was a command and if it was, // returns all text after the command name. If the Message was not a // command, it returns an empty string. +// +// Note: The first character after the command name is omitted: +// - "/foo bar baz" yields "bar baz", not " bar baz" +// - "/foo-bar baz" yields "bar baz", too +// Even though the latter is not a command conforming to the spec, the API +// marks "/foo" as command entity. func (m *Message) CommandArguments() string { if !m.IsCommand() { return "" } - split := strings.SplitN(m.Text, " ", 2) - if len(split) != 2 { - return "" + // IsCommand() checks that the message begins with a bot_command entity + entity := (*m.Entities)[0] + if len(m.Text) == entity.Length { + return "" // The command makes up the whole message } - return split[1] + return m.Text[entity.Length+1:] } // MessageEntity contains information about data in a Message. @@ -385,7 +410,7 @@ type InlineKeyboardButton struct { SwitchInlineQuery *string `json:"switch_inline_query,omitempty"` // optional SwitchInlineQueryCurrentChat *string `json:"switch_inline_query_current_chat,omitempty"` // optional CallbackGame *CallbackGame `json:"callback_game,omitempty"` // optional - Pay bool `json:"pay,omitempty"` // optional + Pay bool `json:"pay,omitempty"` // optional } // CallbackQuery is data sent when a keyboard button with callback data @@ -632,7 +657,7 @@ type InlineQueryResultGame struct { Type string `json:"type"` ID string `json:"id"` GameShortName string `json:"game_short_name"` - ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup"` + ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` } // ChosenInlineResult is an inline query result chosen by a User @@ -746,3 +771,13 @@ type PreCheckoutQuery struct { ShippingOptionID string `json:"shipping_option_id,omitempty"` OrderInfo *OrderInfo `json:"order_info,omitempty"` } + +// Error is an error containing extra information returned by the Telegram API. +type Error struct { + Message string + ResponseParameters +} + +func (e Error) Error() string { + return e.Message +} diff --git a/types_test.go b/types_test.go index d2c24d4..bb7bb64 100644 --- a/types_test.go +++ b/types_test.go @@ -8,7 +8,14 @@ import ( ) func TestUserStringWith(t *testing.T) { - user := tgbotapi.User{0, "Test", "Test", "", "en"} + user := tgbotapi.User{ + ID: 0, + FirstName: "Test", + LastName: "Test", + UserName: "", + LanguageCode: "en", + IsBot: false, + } if user.String() != "Test Test" { t.Fail() @@ -16,7 +23,13 @@ func TestUserStringWith(t *testing.T) { } func TestUserStringWithUserName(t *testing.T) { - user := tgbotapi.User{0, "Test", "Test", "@test", "en"} + user := tgbotapi.User{ + ID: 0, + FirstName: "Test", + LastName: "Test", + UserName: "@test", + LanguageCode: "en", + } if user.String() != "@test" { t.Fail() @@ -34,6 +47,7 @@ func TestMessageTime(t *testing.T) { func TestMessageIsCommandWithCommand(t *testing.T) { message := tgbotapi.Message{Text: "/command"} + message.Entities = &[]tgbotapi.MessageEntity{{Type: "bot_command", Offset: 0, Length: 8}} if message.IsCommand() != true { t.Fail() @@ -58,6 +72,7 @@ func TestIsCommandWithEmptyText(t *testing.T) { func TestCommandWithCommand(t *testing.T) { message := tgbotapi.Message{Text: "/command"} + message.Entities = &[]tgbotapi.MessageEntity{{Type: "bot_command", Offset: 0, Length: 8}} if message.Command() != "command" { t.Fail() @@ -82,19 +97,38 @@ func TestCommandWithNonCommand(t *testing.T) { func TestCommandWithBotName(t *testing.T) { message := tgbotapi.Message{Text: "/command@testbot"} + message.Entities = &[]tgbotapi.MessageEntity{{Type: "bot_command", Offset: 0, Length: 16}} if message.Command() != "command" { t.Fail() } } +func TestCommandWithAtWithBotName(t *testing.T) { + message := tgbotapi.Message{Text: "/command@testbot"} + message.Entities = &[]tgbotapi.MessageEntity{{Type: "bot_command", Offset: 0, Length: 16}} + + if message.CommandWithAt() != "command@testbot" { + t.Fail() + } +} + func TestMessageCommandArgumentsWithArguments(t *testing.T) { message := tgbotapi.Message{Text: "/command with arguments"} + message.Entities = &[]tgbotapi.MessageEntity{{Type: "bot_command", Offset: 0, Length: 8}} if message.CommandArguments() != "with arguments" { t.Fail() } } +func TestMessageCommandArgumentsWithMalformedArguments(t *testing.T) { + message := tgbotapi.Message{Text: "/command-without argument space"} + message.Entities = &[]tgbotapi.MessageEntity{{Type: "bot_command", Offset: 0, Length: 8}} + if message.CommandArguments() != "without argument space" { + t.Fail() + } +} + func TestMessageCommandArgumentsWithoutArguments(t *testing.T) { message := tgbotapi.Message{Text: "/command"} if message.CommandArguments() != "" {