diff --git a/README.md b/README.md index c77e767..2acf115 100644 --- a/README.md +++ b/README.md @@ -90,7 +90,6 @@ More examples can be seen here: - [Predicate as middleware](examples/middleware_with_predicates/main.go) - [Update processor](examples/update_processor/main.go) - [Message entities](examples/message_entity/main.go) -- [Empty values](examples/empty_values/main.go) - [Multi bot webhook](examples/multi_bot_webhook/main.go) diff --git a/bot.go b/bot.go index 485baeb..999d90a 100644 --- a/bot.go +++ b/bot.go @@ -1,7 +1,6 @@ package telego import ( - "bytes" "errors" "fmt" "reflect" @@ -15,8 +14,7 @@ import ( ) const ( - defaultBotAPIServer = "https://api.telegram.org" - defaultBotEmptyValue = "TELEGO_EMPTY_VALUE" + defaultBotAPIServer = "https://api.telegram.org" tokenRegexp = `^\d{9,10}:[\w-]{35}$` //nolint:gosec @@ -44,7 +42,6 @@ type Bot struct { healthCheckRequested bool warningAsErrors bool - replaceToEmpty string longPollingContext *longPollingContext webhookContext *webhookContext @@ -95,13 +92,6 @@ func (b *Bot) Logger() Logger { return b.log } -// EmptyValue returns value that will be erased from all requests useful for things like SwitchInlineQuery in -// telego.InlineKeyboardButton that have empty string as valid parameter value -// Warning: Only works if at least one of the bot options, WithEmptyValues or WithCustomEmptyValues are used -func (b *Bot) EmptyValue() string { - return b.replaceToEmpty -} - // performRequest executes and parses response of method func (b *Bot) performRequest(methodName string, parameters interface{}, vs ...interface{}) error { resp, err := b.constructAndCallRequest(methodName, parameters) @@ -170,10 +160,6 @@ func (b *Bot) constructAndCallRequest(methodName string, parameters interface{}) debugData := strings.TrimSuffix(debug.String(), "\n") b.log.Debugf("API call to: %q, with data: %s", url, debugData) - if b.replaceToEmpty != "" { - data.Buffer = bytes.NewBuffer(bytes.ReplaceAll(data.Buffer.Bytes(), []byte(b.replaceToEmpty), []byte{})) - } - resp, err := b.api.Call(url, data) if err != nil { return nil, fmt.Errorf("request call: %w", err) @@ -293,3 +279,8 @@ func logRequestWithFiles(debug strings.Builder, parameters map[string]string, fi func Bool(value bool) *bool { return &value } + +// String converts string value into a string pointer +func String(value string) *string { + return &value +} diff --git a/bot_options.go b/bot_options.go index 9d222de..68a6820 100644 --- a/bot_options.go +++ b/bot_options.go @@ -2,12 +2,10 @@ package telego import ( "errors" - "fmt" "net/http" "os" "strings" - "github.com/goccy/go-json" "github.com/valyala/fasthttp" "github.com/mymmrac/telego/telegoapi" @@ -62,7 +60,7 @@ func WithDefaultLogger(debugMode, printErrors bool) BotOption { } // WithExtendedDefaultLogger configures default logger, replacer can be nil. Redefines existing logger. -// Note: Keep in mind that debug logs will include your bot token, it's only safe to have them enabled in +// Note: Keep in mind that debug logs will include your bot token. It's only safe to have them enabled in // testing environment, or hide sensitive information (like bot token) yourself. func WithExtendedDefaultLogger(debugMode, printErrors bool, replacer *strings.Replacer) BotOption { return func(bot *Bot) error { @@ -89,7 +87,7 @@ func WithDiscardLogger() BotOption { } // WithLogger sets logger to use. Redefines existing logger. -// Note: Keep in mind that debug logs will include your bot token, it's only safe to have them enabled in +// Note: Keep in mind that debug logs will include your bot token. It's only safe to have them enabled in // testing environment, or hide sensitive information (like bot token) yourself. func WithLogger(log Logger) BotOption { return func(bot *Bot) error { @@ -118,44 +116,11 @@ func WithHealthCheck() BotOption { } } -// WithWarnings treat Telegram warnings as errors -// Note: Any request that has non-empty error will return both result and error +// WithWarnings treat Telegram warnings as an error +// Note: Any request that has a non-empty error will return both result and error func WithWarnings() BotOption { return func(bot *Bot) error { bot.warningAsErrors = true return nil } } - -// WithEmptyValues sets empty value to default one that will be erased from all requests -// Note: Used with Bot.EmptyValue() to get empty strings as parameters -func WithEmptyValues() BotOption { - return func(bot *Bot) error { - bot.replaceToEmpty = defaultBotEmptyValue - return nil - } -} - -// WithCustomEmptyValues sets empty value to custom value that will be erased from all requests -// Note: Used with Bot.EmptyValue() to get empty strings as parameters values -// Warning: Request data is encoded using JSON, so the value will be escaped in JSON and may not match intended value -func WithCustomEmptyValues(emptyValue string) BotOption { - return func(bot *Bot) error { - if emptyValue == "" { - return fmt.Errorf("empty value can't be zero length") - } - - data, err := json.Marshal(emptyValue) - if err != nil { - return fmt.Errorf("marshal empty value: %w", err) - } - - if fmt.Sprintf(`"%s"`, emptyValue) != string(data) { - return fmt.Errorf(`empty value does't match it's JSON encoded varian: "%s" not equal to %s`, - emptyValue, string(data)) - } - - bot.replaceToEmpty = emptyValue - return nil - } -} diff --git a/bot_options_test.go b/bot_options_test.go index ab327e9..9b52881 100644 --- a/bot_options_test.go +++ b/bot_options_test.go @@ -184,33 +184,3 @@ func TestWithWarnings(t *testing.T) { assert.True(t, bot.warningAsErrors) } - -func TestWithEmptyValues(t *testing.T) { - bot := &Bot{} - - err := WithEmptyValues()(bot) - assert.NoError(t, err) - - assert.Equal(t, defaultBotEmptyValue, bot.replaceToEmpty) -} - -func TestWithCustomEmptyValues(t *testing.T) { - bot := &Bot{} - - t.Run("success", func(t *testing.T) { - err := WithCustomEmptyValues(defaultBotEmptyValue)(bot) - assert.NoError(t, err) - - assert.Equal(t, defaultBotEmptyValue, bot.replaceToEmpty) - }) - - t.Run("error_empty", func(t *testing.T) { - err := WithCustomEmptyValues("")(bot) - assert.Error(t, err) - }) - - t.Run("error_marshal_does_not_match", func(t *testing.T) { - err := WithCustomEmptyValues(string([]byte{1}))(bot) - assert.Error(t, err) - }) -} diff --git a/bot_test.go b/bot_test.go index 718f53a..8faec7e 100644 --- a/bot_test.go +++ b/bot_test.go @@ -156,13 +156,6 @@ func TestBot_Logger(t *testing.T) { assert.Equal(t, bot.log, bot.Logger()) } -func TestBot_EmptyValue(t *testing.T) { - bot, err := NewBot(token, WithEmptyValues()) - assert.NoError(t, err) - - assert.Equal(t, defaultBotEmptyValue, bot.EmptyValue()) -} - type testErrorMarshal struct { Number int `json:"number"` } @@ -663,3 +656,8 @@ func TestBool(t *testing.T) { assert.True(t, *Bool(true)) assert.False(t, *Bool(false)) } + +func TestString(t *testing.T) { + assert.Equal(t, "", *String("")) + assert.Equal(t, "a", *String("a")) +} diff --git a/examples/configuration/main.go b/examples/configuration/main.go index d418ce2..fb52bc0 100644 --- a/examples/configuration/main.go +++ b/examples/configuration/main.go @@ -31,7 +31,7 @@ func main() { telego.WithHealthCheck(), // Make all warnings an errors for all requests (default: false) - // Note: Things like `deleteWebhook` may return result as true, but also error description with warning + // Note: Things like `deleteWebhook` may return a result as true, but also error description with warning telego.WithWarnings(), // Configuration of default logger, enable printing debug information and errors (default: false, true) @@ -54,15 +54,6 @@ func main() { // Note: Please keep in mind that logger may expose sensitive information, use in development only or configure // it not to leak unwanted content telego.WithLogger(myLogger), - - // Used in combination with telego.Bot.EmptyValue() to get empty values for string parameters in cases where - // empty parameter is a valid value (default: TELEGO_EMPTY_VALUE) - // Note: By default, no empty value is set, so using telego.Bot.EmptyValue() does nothing - telego.WithEmptyValues(), - - // Same as telego.WithEmptyValues(), but you can define your own empty value that will be used - // Note: Request data will be encoded as JSON, so an empty value should match it after encoding too - telego.WithCustomEmptyValues("the_empty_value"), ) if err != nil { fmt.Println(err) diff --git a/examples/empty_values/main.go b/examples/empty_values/main.go deleted file mode 100644 index 1274baa..0000000 --- a/examples/empty_values/main.go +++ /dev/null @@ -1,41 +0,0 @@ -package main - -import ( - "fmt" - "os" - - "github.com/mymmrac/telego" - tu "github.com/mymmrac/telego/telegoutil" -) - -func main() { - botToken := os.Getenv("TOKEN") - - // Note: Please keep in mind that default logger may expose sensitive information, use in development only - bot, err := telego.NewBot(botToken, telego.WithDefaultDebugLogger(), - telego.WithEmptyValues(), // Enable empty values - ) - if err != nil { - fmt.Println(err) - os.Exit(1) - } - - // In cases, when empty value is a valid parameter value, like here in telego.InlineKeyboardButton we have - // SwitchInlineQueryCurrentChat that has an empty value as expected argument - keyboard := tu.InlineKeyboard( - tu.InlineKeyboardRow( - telego.InlineKeyboardButton{ - Text: "Inline query in current chat", - - // Pass empty value that will be properly passed in request as "switch_inline_query_current_chat": "" - // Warning: If telego.WithEmptyValues() or telego.WithCustomEmptyValues() bot options are not used, - // this will do nothing - SwitchInlineQueryCurrentChat: bot.EmptyValue(), - }, - ), - ) - - _, _ = bot.SendMessage( - tu.Message(tu.ID(1234567), "Query").WithReplyMarkup(keyboard), - ) -} diff --git a/internal/generator/main.go b/internal/generator/main.go index 3ef430c..45a1a20 100644 --- a/internal/generator/main.go +++ b/internal/generator/main.go @@ -98,6 +98,8 @@ func main() { sr := sharedResources{} + // TODO: Add cases for *bool and *string + for _, arg := range args { logInfo("==== %s ====", arg) start := time.Now() diff --git a/internal/test/main.go b/internal/test/main.go index fd6a570..d8d06b6 100644 --- a/internal/test/main.go +++ b/internal/test/main.go @@ -27,7 +27,7 @@ func main() { testToken := os.Getenv("TOKEN") bot, err := telego.NewBot(testToken, - telego.WithDefaultDebugLogger(), telego.WithWarnings(), telego.WithEmptyValues()) + telego.WithDefaultDebugLogger(), telego.WithWarnings()) if err != nil { fmt.Println(err) return @@ -683,7 +683,7 @@ func main() { tu.InlineKeyboard( tu.InlineKeyboardRow( tu.InlineKeyboardButton("OK"). - WithSwitchInlineQueryCurrentChat(bot.EmptyValue()), + WithSwitchInlineQueryCurrentChat(""), ), ), ), diff --git a/methods.go b/methods.go index ed74f6a..1acda57 100644 --- a/methods.go +++ b/methods.go @@ -2298,7 +2298,7 @@ type EditForumTopicParams struct { // getForumTopicIconStickers (https://core.telegram.org/bots/api#getforumtopiciconstickers) to get all allowed // custom emoji identifiers. Pass an empty string to remove the icon. If not specified, the current icon will be // kept - IconCustomEmojiID string `json:"icon_custom_emoji_id,omitempty"` + IconCustomEmojiID *string `json:"icon_custom_emoji_id,omitempty"` } // EditForumTopic - Use this method to edit name and icon of a topic in a forum supergroup chat. The bot must diff --git a/methods_setters.go b/methods_setters.go index 7a20258..ab22904 100644 --- a/methods_setters.go +++ b/methods_setters.go @@ -1740,7 +1740,7 @@ func (p *EditForumTopicParams) WithName(name string) *EditForumTopicParams { // WithIconCustomEmojiID adds icon custom emoji ID parameter func (p *EditForumTopicParams) WithIconCustomEmojiID(iconCustomEmojiID string) *EditForumTopicParams { - p.IconCustomEmojiID = iconCustomEmojiID + p.IconCustomEmojiID = String(iconCustomEmojiID) return p } diff --git a/methods_setters_test.go b/methods_setters_test.go index 80231a3..8356d50 100644 --- a/methods_setters_test.go +++ b/methods_setters_test.go @@ -961,7 +961,7 @@ func TestEditForumTopicParams_Setters(t *testing.T) { ChatID: ChatID{ID: 2}, MessageThreadID: 1, Name: "Name", - IconCustomEmojiID: "IconCustomEmojiID", + IconCustomEmojiID: String("IconCustomEmojiID"), }, e) } diff --git a/telegoapi/api.go b/telegoapi/api.go index da10595..0197c8e 100644 --- a/telegoapi/api.go +++ b/telegoapi/api.go @@ -72,7 +72,7 @@ type Caller interface { // NamedReader represents a way to send files (or other data). // Implemented by os.File. -// Note: Name method should return unique names for all files sent in one request. +// Note: Name method may be called multiple times and should return unique names for all files sent in one request. // // Warning: Since, for sending data (files) reader data will be copied, using the same reader multiple times as is // will not work. diff --git a/types.go b/types.go index 48af04f..96102e7 100644 --- a/types.go +++ b/types.go @@ -3,6 +3,7 @@ package telego import ( "errors" "fmt" + "strconv" "github.com/goccy/go-json" @@ -1354,14 +1355,14 @@ type InlineKeyboardButton struct { // (https://core.telegram.org/bots/inline) when they are currently in a private chat with it. Especially useful // when combined with switch_pm… (https://core.telegram.org/bots/api#answerinlinequery) actions - in this case // the user will be automatically returned to the chat they switched from, skipping the chat selection screen. - SwitchInlineQuery string `json:"switch_inline_query,omitempty"` + SwitchInlineQuery *string `json:"switch_inline_query,omitempty"` // SwitchInlineQueryCurrentChat - Optional. If set, pressing the button will insert the bot's username and // the specified inline query in the current chat's input field. May be empty, in which case only the bot's // username will be inserted. // This offers a quick way for the user to open your bot in inline mode in the same chat - good for selecting // something from multiple options. - SwitchInlineQueryCurrentChat string `json:"switch_inline_query_current_chat,omitempty"` + SwitchInlineQueryCurrentChat *string `json:"switch_inline_query_current_chat,omitempty"` // CallbackGame - Optional. Description of the game that will be launched when the user presses the button. // NOTE: This type of button must always be the first button in the first row. @@ -2099,6 +2100,19 @@ type ChatID struct { Username string } +// String returns string representation of ChatID +func (c ChatID) String() string { + if c.ID != 0 { + return strconv.FormatInt(c.ID, 10) + } + + if c.Username != "" { + return c.Username + } + + return "" +} + // MarshalJSON returns JSON representation of ChatID func (c ChatID) MarshalJSON() ([]byte, error) { if c.ID != 0 { @@ -2574,6 +2588,23 @@ type InputFile struct { needAttach bool } +// String returns string representation of InputFile +func (i InputFile) String() string { + if i.FileID != "" { + return i.FileID + } + + if i.URL != "" { + return i.URL + } + + if i.File != nil { + return i.File.Name() + } + + return "" +} + // MarshalJSON return JSON representation of InputFile func (i InputFile) MarshalJSON() ([]byte, error) { if i.FileID != "" { diff --git a/types_setters.go b/types_setters.go index d0796e9..2c1d233 100644 --- a/types_setters.go +++ b/types_setters.go @@ -128,14 +128,14 @@ func (i InlineKeyboardButton) WithLoginURL(loginURL *LoginURL) InlineKeyboardBut // WithSwitchInlineQuery adds switch inline query parameter func (i InlineKeyboardButton) WithSwitchInlineQuery(switchInlineQuery string) InlineKeyboardButton { - i.SwitchInlineQuery = switchInlineQuery + i.SwitchInlineQuery = String(switchInlineQuery) return i } // WithSwitchInlineQueryCurrentChat adds switch inline query current chat parameter func (i InlineKeyboardButton) WithSwitchInlineQueryCurrentChat(switchInlineQueryCurrentChat string, ) InlineKeyboardButton { - i.SwitchInlineQueryCurrentChat = switchInlineQueryCurrentChat + i.SwitchInlineQueryCurrentChat = String(switchInlineQueryCurrentChat) return i } diff --git a/types_setters_test.go b/types_setters_test.go index 814255a..344de99 100644 --- a/types_setters_test.go +++ b/types_setters_test.go @@ -85,8 +85,8 @@ func TestInlineKeyboardButton_Setters(t *testing.T) { CallbackData: "CallbackData", WebApp: &WebAppInfo{}, LoginURL: &LoginURL{URL: "LoginURL"}, - SwitchInlineQuery: "SwitchInlineQuery", - SwitchInlineQueryCurrentChat: "SwitchInlineQueryCurrentChat", + SwitchInlineQuery: String("SwitchInlineQuery"), + SwitchInlineQueryCurrentChat: String("SwitchInlineQueryCurrentChat"), CallbackGame: &CallbackGame{}, Pay: true, }, i) diff --git a/types_test.go b/types_test.go index efb8f8a..1e55443 100644 --- a/types_test.go +++ b/types_test.go @@ -775,3 +775,76 @@ func TestUpdate_CloneSafe(t *testing.T) { assert.Zero(t, uc) }) } + +func TestChatID_String(t *testing.T) { + tests := []struct { + name string + chatID ChatID + stringValue string + }{ + { + name: "empty", + chatID: ChatID{}, + stringValue: "", + }, + { + name: "id", + chatID: ChatID{ + ID: 123, + }, + stringValue: "123", + }, + { + name: "username", + chatID: ChatID{ + Username: "test", + }, + stringValue: "test", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.stringValue, tt.chatID.String()) + }) + } +} + +func TestInputFile_String(t *testing.T) { + tests := []struct { + name string + inputFile InputFile + stringValue string + }{ + { + name: "empty", + inputFile: InputFile{}, + stringValue: "", + }, + { + name: "file", + inputFile: InputFile{ + File: &testNamedReade{}, + }, + stringValue: "test", + }, + { + name: "id", + inputFile: InputFile{ + FileID: "fileID", + }, + stringValue: "fileID", + }, + { + name: "url", + inputFile: InputFile{ + URL: "url", + }, + stringValue: "url", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.stringValue, tt.inputFile.String()) + }) + } +}