From 9805b1f622d8161ed5615d0738c8071066ed7dea Mon Sep 17 00:00:00 2001 From: Demian Date: Sun, 30 Jan 2022 13:22:58 +0200 Subject: [PATCH] errors: refactor --- api_test.go | 2 +- errors.go | 142 +++++++++++++++++++++++++++++---------------------- util.go | 55 ++++++++++---------- util_test.go | 29 ++++++++--- 4 files changed, 132 insertions(+), 96 deletions(-) diff --git a/api_test.go b/api_test.go index 3fbbe34..e9e0f1e 100644 --- a/api_test.go +++ b/api_test.go @@ -70,5 +70,5 @@ func TestRaw(t *testing.T) { assert.EqualError(t, err, "telebot: "+io.ErrUnexpectedEOF.Error()) _, err = b.Raw("testUnknownError", nil) - assert.EqualError(t, err, "telegram unknown: unknown error (400)") + assert.EqualError(t, err, "telegram: unknown error (400)") } diff --git a/errors.go b/errors.go index e6a31e2..2671ece 100644 --- a/errors.go +++ b/errors.go @@ -5,26 +5,32 @@ import ( "strings" ) -type APIError struct { - Code int - Description string - Message string - Parameters map[string]interface{} -} +type ( + Error struct { + Code int + Description string + Message string + } -type FloodError struct { - *APIError - RetryAfter int -} + FloodError struct { + err *Error + RetryAfter int + } + + GroupError struct { + err *Error + MigratedTo int64 + } +) // ʔ returns description of error. // A tiny shortcut to make code clearer. -func (err *APIError) ʔ() string { +func (err *Error) ʔ() string { return err.Description } // Error implements error interface. -func (err *APIError) Error() string { +func (err *Error) Error() string { msg := err.Message if msg == "" { split := strings.Split(err.Description, ": ") @@ -37,10 +43,20 @@ func (err *APIError) Error() string { return fmt.Sprintf("telegram: %s (%d)", msg, err.Code) } -// NewAPIError returns new APIError instance with given description. +// Error implements error interface. +func (err FloodError) Error() string { + return err.err.Error() +} + +// Error implements error interface. +func (err GroupError) Error() string { + return err.err.Error() +} + +// NewError returns new Error instance with given description. // First element of msgs is Description. The second is optional Message. -func NewAPIError(code int, msgs ...string) *APIError { - err := &APIError{Code: code} +func NewError(code int, msgs ...string) *Error { + err := &Error{Code: code} if len(msgs) >= 1 { err.Description = msgs[0] } @@ -50,57 +66,63 @@ func NewAPIError(code int, msgs ...string) *APIError { return err } +// General errors var ( - // General errors - ErrUnauthorized = NewAPIError(401, "Unauthorized") - ErrNotStartedByUser = NewAPIError(403, "Forbidden: bot can't initiate conversation with a user") - ErrBlockedByUser = NewAPIError(401, "Forbidden: bot was blocked by the user") - ErrUserIsDeactivated = NewAPIError(401, "Forbidden: user is deactivated") - ErrNotFound = NewAPIError(404, "Not Found") - ErrInternal = NewAPIError(500, "Internal Server Error") + ErrUnauthorized = NewError(401, "Unauthorized") + ErrNotStartedByUser = NewError(403, "Forbidden: bot can't initiate conversation with a user") + ErrBlockedByUser = NewError(401, "Forbidden: bot was blocked by the user") + ErrUserIsDeactivated = NewError(401, "Forbidden: user is deactivated") + ErrNotFound = NewError(404, "Not Found") + ErrInternal = NewError(500, "Internal Server Error") +) - // Bad request errors - ErrTooLarge = NewAPIError(400, "Request Entity Too Large") - ErrMessageTooLong = NewAPIError(400, "Bad Request: message is too long") - ErrToForwardNotFound = NewAPIError(400, "Bad Request: message to forward not found") - ErrToReplyNotFound = NewAPIError(400, "Bad Request: reply message not found") - ErrToDeleteNotFound = NewAPIError(400, "Bad Request: message to delete not found") - ErrEmptyMessage = NewAPIError(400, "Bad Request: message must be non-empty") - ErrEmptyText = NewAPIError(400, "Bad Request: text is empty") - ErrEmptyChatID = NewAPIError(400, "Bad Request: chat_id is empty") - ErrChatNotFound = NewAPIError(400, "Bad Request: chat not found") - ErrMessageNotModified = NewAPIError(400, "Bad Request: message is not modified") - ErrSameMessageContent = NewAPIError(400, "Bad Request: message is not modified: specified new message content and reply markup are exactly the same as a current content and reply markup of the message") - ErrCantEditMessage = NewAPIError(400, "Bad Request: message can't be edited") - ErrButtonDataInvalid = NewAPIError(400, "Bad Request: BUTTON_DATA_INVALID") - ErrWrongTypeOfContent = NewAPIError(400, "Bad Request: wrong type of the web page content") - ErrBadURLContent = NewAPIError(400, "Bad Request: failed to get HTTP URL content") - ErrWrongFileID = NewAPIError(400, "Bad Request: wrong file identifier/HTTP URL specified") - ErrWrongFileIDSymbol = NewAPIError(400, "Bad Request: wrong remote file id specified: can't unserialize it. Wrong last symbol") - ErrWrongFileIDLength = NewAPIError(400, "Bad Request: wrong remote file id specified: Wrong string length") - ErrWrongFileIDCharacter = NewAPIError(400, "Bad Request: wrong remote file id specified: Wrong character in the string") - ErrWrongFileIDPadding = NewAPIError(400, "Bad Request: wrong remote file id specified: Wrong padding in the string") - ErrFailedImageProcess = NewAPIError(400, "Bad Request: IMAGE_PROCESS_FAILED", "Image process failed") - ErrInvalidStickerSet = NewAPIError(400, "Bad Request: STICKERSET_INVALID", "Stickerset is invalid") - ErrBadPollOptions = NewAPIError(400, "Bad Request: expected an Array of String as options") - ErrGroupMigrated = NewAPIError(400, "Bad Request: group chat was upgraded to a supergroup chat") +// Bad request errors +var ( + ErrTooLarge = NewError(400, "Request Entity Too Large") + ErrMessageTooLong = NewError(400, "Bad Request: message is too long") + ErrToForwardNotFound = NewError(400, "Bad Request: message to forward not found") + ErrToReplyNotFound = NewError(400, "Bad Request: reply message not found") + ErrToDeleteNotFound = NewError(400, "Bad Request: message to delete not found") + ErrEmptyMessage = NewError(400, "Bad Request: message must be non-empty") + ErrEmptyText = NewError(400, "Bad Request: text is empty") + ErrEmptyChatID = NewError(400, "Bad Request: chat_id is empty") + ErrChatNotFound = NewError(400, "Bad Request: chat not found") + ErrMessageNotModified = NewError(400, "Bad Request: message is not modified") + ErrSameMessageContent = NewError(400, "Bad Request: message is not modified: specified new message content and reply markup are exactly the same as a current content and reply markup of the message") + ErrCantEditMessage = NewError(400, "Bad Request: message can't be edited") + ErrButtonDataInvalid = NewError(400, "Bad Request: BUTTON_DATA_INVALID") + ErrWrongTypeOfContent = NewError(400, "Bad Request: wrong type of the web page content") + ErrBadURLContent = NewError(400, "Bad Request: failed to get HTTP URL content") + ErrWrongFileID = NewError(400, "Bad Request: wrong file identifier/HTTP URL specified") + ErrWrongFileIDSymbol = NewError(400, "Bad Request: wrong remote file id specified: can't unserialize it. Wrong last symbol") + ErrWrongFileIDLength = NewError(400, "Bad Request: wrong remote file id specified: Wrong string length") + ErrWrongFileIDCharacter = NewError(400, "Bad Request: wrong remote file id specified: Wrong character in the string") + ErrWrongFileIDPadding = NewError(400, "Bad Request: wrong remote file id specified: Wrong padding in the string") + ErrFailedImageProcess = NewError(400, "Bad Request: IMAGE_PROCESS_FAILED", "Image process failed") + ErrInvalidStickerSet = NewError(400, "Bad Request: STICKERSET_INVALID", "Stickerset is invalid") + ErrBadPollOptions = NewError(400, "Bad Request: expected an Array of String as options") + ErrGroupMigrated = NewError(400, "Bad Request: group chat was upgraded to a supergroup chat") +) - // No rights errors - ErrNoRightsToRestrict = NewAPIError(400, "Bad Request: not enough rights to restrict/unrestrict chat member") - ErrNoRightsToSend = NewAPIError(400, "Bad Request: have no rights to send a message") - ErrNoRightsToSendPhoto = NewAPIError(400, "Bad Request: not enough rights to send photos to the chat") - ErrNoRightsToSendStickers = NewAPIError(400, "Bad Request: not enough rights to send stickers to the chat") - ErrNoRightsToSendGifs = NewAPIError(400, "Bad Request: CHAT_SEND_GIFS_FORBIDDEN", "sending GIFS is not allowed in this chat") - ErrNoRightsToDelete = NewAPIError(400, "Bad Request: message can't be deleted") - ErrKickingChatOwner = NewAPIError(400, "Bad Request: can't remove chat owner") +// No rights errors +var ( + ErrNoRightsToRestrict = NewError(400, "Bad Request: not enough rights to restrict/unrestrict chat member") + ErrNoRightsToSend = NewError(400, "Bad Request: have no rights to send a message") + ErrNoRightsToSendPhoto = NewError(400, "Bad Request: not enough rights to send photos to the chat") + ErrNoRightsToSendStickers = NewError(400, "Bad Request: not enough rights to send stickers to the chat") + ErrNoRightsToSendGifs = NewError(400, "Bad Request: CHAT_SEND_GIFS_FORBIDDEN", "sending GIFS is not allowed in this chat") + ErrNoRightsToDelete = NewError(400, "Bad Request: message can't be deleted") + ErrKickingChatOwner = NewError(400, "Bad Request: can't remove chat owner") +) - // Super/groups errors - ErrBotKickedFromGroup = NewAPIError(403, "Forbidden: bot was kicked from the group chat") - ErrBotKickedFromSuperGroup = NewAPIError(403, "Forbidden: bot was kicked from the supergroup chat") +// Super/groups errors +var ( + ErrBotKickedFromGroup = NewError(403, "Forbidden: bot was kicked from the group chat") + ErrBotKickedFromSuperGroup = NewError(403, "Forbidden: bot was kicked from the supergroup chat") ) -// ErrByDescription returns APIError instance by given description. -func ErrByDescription(s string) error { +// Err returns Error instance by given description. +func Err(s string) error { switch s { case ErrUnauthorized.ʔ(): return ErrUnauthorized diff --git a/util.go b/util.go index 49ad60d..d7c071d 100644 --- a/util.go +++ b/util.go @@ -57,52 +57,49 @@ func wrapError(err error) error { // In other cases it extracts API error. If error is not presented // in errors.go, it will be prefixed with `unknown` keyword. func extractOk(data []byte) error { - // Parse the error message as JSON - var tgramApiError struct { + var e struct { Ok bool `json:"ok"` - ErrorCode int `json:"error_code"` + Code int `json:"error_code"` Description string `json:"description"` Parameters map[string]interface{} `json:"parameters"` } - jdecoder := json.NewDecoder(bytes.NewReader(data)) - jdecoder.UseNumber() - - err := jdecoder.Decode(&tgramApiError) - if err != nil { - //return errors.Wrap(err, "can't parse JSON reply, the Telegram server is mibehaving") - // FIXME / TODO: in this case the error might be at HTTP level, or the content is not JSON (eg. image?) - return nil + if json.NewDecoder(bytes.NewReader(data)).Decode(&e) != nil { + return nil // FIXME } - - if tgramApiError.Ok { - // No error + if e.Ok { return nil } - err = ErrByDescription(tgramApiError.Description) - if err != nil { - apierr, _ := err.(*APIError) - // Formally this is wrong, as the error is not created on the fly - // However, given the current way of handling errors, this a working - // workaround which doesn't break the API - apierr.Parameters = tgramApiError.Parameters - return apierr + err := Err(e.Description) + switch err { + case nil: + case ErrGroupMigrated: + migratedTo, ok := e.Parameters["migrate_to_chat_id"] + if !ok { + return NewError(e.Code, e.Description) + } + + return GroupError{ + err: err.(*Error), + MigratedTo: int64(migratedTo.(float64)), + } + default: + return err } - switch tgramApiError.ErrorCode { + switch e.Code { case http.StatusTooManyRequests: - retryAfter, ok := tgramApiError.Parameters["retry_after"] + retryAfter, ok := e.Parameters["retry_after"] if !ok { - return NewAPIError(429, tgramApiError.Description) + return NewError(e.Code, e.Description) } - retryAfterInt, _ := strconv.Atoi(fmt.Sprint(retryAfter)) err = FloodError{ - APIError: NewAPIError(429, tgramApiError.Description), - RetryAfter: retryAfterInt, + err: NewError(e.Code, e.Description), + RetryAfter: int(retryAfter.(float64)), } default: - err = fmt.Errorf("telegram unknown: %s (%d)", tgramApiError.Description, tgramApiError.ErrorCode) + err = fmt.Errorf("telegram: %s (%d)", e.Description, e.Code) } return err diff --git a/util_test.go b/util_test.go index b62d429..9a83c3d 100644 --- a/util_test.go +++ b/util_test.go @@ -8,20 +8,37 @@ import ( ) func TestExtractOk(t *testing.T) { - data := []byte(`{"ok":true,"result":{"foo":"bar"}}`) + data := []byte(`{"ok": true, "result": {}}`) require.NoError(t, extractOk(data)) - data = []byte(`{"ok":false,"error_code":400,"description":"Bad Request: reply message not found"}`) + data = []byte(`{ + "ok": false, + "error_code": 400, + "description": "Bad Request: reply message not found" + }`) assert.EqualError(t, extractOk(data), ErrToReplyNotFound.Error()) - data = []byte(`{"ok":false,"error_code":429,"description":"Too Many Requests: retry after 8","parameters":{"retry_after":8}}`) + data = []byte(`{ + "ok": false, + "error_code": 429, + "description": "Too Many Requests: retry after 8", + "parameters": {"retry_after": 8} + }`) assert.Equal(t, FloodError{ - APIError: NewAPIError(429, "Too Many Requests: retry after 8"), + err: NewError(429, "Too Many Requests: retry after 8"), RetryAfter: 8, }, extractOk(data)) - data = []byte(`{"ok":false,"error_code":400,"description":"Bad Request: group chat was upgraded to a supergroup chat","parameters":{"migrate_to_chat_id": -1234}}`) - assert.EqualError(t, extractOk(data), ErrGroupMigrated.Error()) + data = []byte(`{ + "ok": false, + "error_code": 400, + "description": "Bad Request: group chat was upgraded to a supergroup chat", + "parameters": {"migrate_to_chat_id": -100123456789} + }`) + assert.Equal(t, GroupError{ + err: ErrGroupMigrated, + MigratedTo: -100123456789, + }, extractOk(data)) } func TestExtractMessage(t *testing.T) {