From 96d43b5942d691f1d916f8215e51545afe8bd21f Mon Sep 17 00:00:00 2001 From: Aldino Kemal Date: Wed, 28 Dec 2022 12:48:57 +0700 Subject: [PATCH] feat: standardize error response (#48) * feat: update openapi * feat: standardize error --- docs/openapi.yaml | 187 ++++++++++++----------- src/go.mod | 1 + src/go.sum | 3 + src/internal/rest/app.go | 6 +- src/internal/rest/middleware/recovery.go | 3 +- src/internal/rest/send.go | 9 ++ src/internal/rest/user.go | 6 +- src/pkg/error/app_error.go | 108 +++++++++++++ src/pkg/error/auth_error.go | 19 --- src/pkg/error/whatsapp_error.go | 18 +++ src/pkg/utils/response.go | 4 +- src/services/app.go | 23 +-- 12 files changed, 262 insertions(+), 125 deletions(-) create mode 100644 src/pkg/error/app_error.go delete mode 100644 src/pkg/error/auth_error.go diff --git a/docs/openapi.yaml b/docs/openapi.yaml index 12a2c59..bcb6a40 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -1,7 +1,7 @@ openapi: 3.0.0 info: title: WhatsApp API MultiDevice - version: 3.3.0 + version: 3.3.1 description: This API is used for sending whatsapp via API servers: - url: http://localhost:3000 @@ -85,54 +85,10 @@ paths: responses: '200': description: OK - headers: - Date: - schema: - type: string - example: Sun, 13 Feb 2022 05:46:55 GMT - Content-Type: - schema: - type: string - example: application/json - Content-Length: - schema: - type: integer - example: '394' - Vary: - schema: - type: string - example: Origin - Access-Control-Allow-Origin: - schema: - type: string - example: '*' content: application/json: schema: - type: object - example: - code: 200 - message: Success - results: - verified_name: '' - status: you are blocked - picture_id: '1635239861' - devices: - - User: '6289685028129@s.whatsapp.net' - Agent: 0 - Device: UNKNOWN - Server: s.whatsapp.net - AD: true - - User: '6289685028129@s.whatsapp.net' - Agent: 0 - Device: SAFARI - Server: s.whatsapp.net - AD: true - - User: '6289685028129@s.whatsapp.net' - Agent: 0 - Device: IPAD - Server: s.whatsapp.net - AD: true + $ref: '#/components/schemas/UserInfoResponse' '400': description: Bad Request content: @@ -190,40 +146,10 @@ paths: responses: '200': description: OK - headers: - Date: - schema: - type: string - example: Sun, 13 Feb 2022 04:01:41 GMT - Content-Type: - schema: - type: string - example: application/json - Content-Length: - schema: - type: integer - example: '147' - Vary: - schema: - type: string - example: Origin - Access-Control-Allow-Origin: - schema: - type: string - example: '*' content: application/json: schema: - type: object - example: - code: 200 - message: Success get privacy - results: - group_add: all - last_seen: '' - status: all - profile: contacts - read_receipts: all + $ref: '#/components/schemas/UserPrivacyResponse' '500': description: Internal Server Error content: @@ -601,6 +527,9 @@ components: UserGroupResponse: type: object properties: + code: + type: string + example: SUCCESS message: type: string example: Success get list groups @@ -638,9 +567,53 @@ components: type: boolean Error: type: number + UserInfoResponse: + type: object + properties: + code: + type: string + example: SUCCESS + message: + type: string + example: + results: + type: object + properties: + verified_name: + type: string + example: Aldino Kemal + status: + type: string + example: Hello World + picture_id: + type: string + example: 1651459152 + devices: + type: array + items: + type: object + properties: + User: + type: string + example: 6289685021291 + Agent: + type: integer + example: 0 + Device: + type: string + example: UNKNOWN + Server: + type: string + example: s.whatsapp.net + AD: + type: boolean + example: true UserAvatarResponse: type: object properties: + code: + type: string + example: SUCCESS message: type: string example: Success @@ -656,39 +629,57 @@ components: type: type: string example: 'image' - SendResponse: + UserPrivacyResponse: type: object properties: + code: + type: string + example: SUCCESS message: type: string - example: Success + example: Success get privacy results: type: object properties: - message_id: + group_add: type: string - example: '3EB0B430B6F8F1D0E053AC120E0A9E5C' + example: all + last_seen: + type: string + example: null status: type: string - example: ' success ....' - GenericResponse: + example: all + profile: + type: string + example: all + read_receipts: + type: string + example: all + SendResponse: type: object properties: code: - type: integer - example: 200 + type: string + example: SUCCESS message: type: string example: Success results: - type: string - example: null + type: object + properties: + message_id: + type: string + example: '3EB0B430B6F8F1D0E053AC120E0A9E5C' + status: + type: string + example: ' success ....' LoginResponse: type: object properties: code: - type: integer - example: 200 + type: string + example: SUCCESS message: type: string example: Success @@ -701,13 +692,25 @@ components: qr_link: type: string example: 'http://localhost:3000/statics/images/qrcode/scan-qr-b0b7bb43-9a22-455a-814f-5a225c743310.png' + GenericResponse: + type: object + properties: + code: + type: string + example: SUCCESS + message: + type: string + example: Success + results: + type: string + example: null ErrorInternalServer: type: object properties: code: - type: integer - example: 500 - description: 'HTTP Status Code' + type: string + example: INTERNAL_SERVER_ERROR + description: 'SYSTEM_CODE_ERROR' message: type: string example: you are not loggin diff --git a/src/go.mod b/src/go.mod index 5b9dd98..20acdc0 100644 --- a/src/go.mod +++ b/src/go.mod @@ -35,6 +35,7 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rivo/uniseg v0.4.3 // indirect github.com/savsgio/gotils v0.0.0-20220530130905-52f3993e8d6d // indirect + github.com/sirupsen/logrus v1.9.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/tcplisten v1.0.0 // indirect diff --git a/src/go.sum b/src/go.sum index f6845d3..d95c52c 100644 --- a/src/go.sum +++ b/src/go.sum @@ -365,6 +365,8 @@ github.com/savsgio/gotils v0.0.0-20220530130905-52f3993e8d6d/go.mod h1:Gy+0tqhJv github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= +github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= @@ -617,6 +619,7 @@ golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211205182925-97ca703d548d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220111092808-5a964db01320/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ= diff --git a/src/internal/rest/app.go b/src/internal/rest/app.go index d400bbf..19775d4 100644 --- a/src/internal/rest/app.go +++ b/src/internal/rest/app.go @@ -27,7 +27,8 @@ func (handler *App) Login(c *fiber.Ctx) error { return c.JSON(utils.ResponseData{ Status: 200, - Message: "Success", + Code: "SUCCESS", + Message: "Login success", Results: map[string]any{ "qr_link": fmt.Sprintf("%s://%s/%s", c.Protocol(), c.Hostname(), response.ImagePath), "qr_duration": response.Duration, @@ -41,6 +42,7 @@ func (handler *App) Logout(c *fiber.Ctx) error { return c.JSON(utils.ResponseData{ Status: 200, + Code: "SUCCESS", Message: "Success logout", Results: nil, }) @@ -52,6 +54,7 @@ func (handler *App) Reconnect(c *fiber.Ctx) error { return c.JSON(utils.ResponseData{ Status: 200, + Code: "SUCCESS", Message: "Reconnect success", Results: nil, }) @@ -63,6 +66,7 @@ func (handler *App) Devices(c *fiber.Ctx) error { return c.JSON(utils.ResponseData{ Status: 200, + Code: "SUCCESS", Message: "Fetch device success", Results: devices, }) diff --git a/src/internal/rest/middleware/recovery.go b/src/internal/rest/middleware/recovery.go index 54cf4c9..73e1263 100644 --- a/src/internal/rest/middleware/recovery.go +++ b/src/internal/rest/middleware/recovery.go @@ -14,7 +14,8 @@ func Recovery() fiber.Handler { if err != nil { var res utils.ResponseData res.Status = 500 - res.Message = fmt.Sprintf("%s", err) + res.Code = "INTERNAL_SERVER_ERROR" + res.Message = fmt.Sprintf("%v", err) errValidation, isValidationError := err.(pkgError.GenericError) if isValidationError { diff --git a/src/internal/rest/send.go b/src/internal/rest/send.go index 7872427..0559e29 100644 --- a/src/internal/rest/send.go +++ b/src/internal/rest/send.go @@ -37,6 +37,7 @@ func (controller *Send) SendText(c *fiber.Ctx) error { return c.JSON(utils.ResponseData{ Status: 200, + Code: "SUCCESS", Message: response.Status, Results: response, }) @@ -60,6 +61,7 @@ func (controller *Send) SendImage(c *fiber.Ctx) error { return c.JSON(utils.ResponseData{ Status: 200, + Code: "SUCCESS", Message: response.Status, Results: response, }) @@ -81,6 +83,7 @@ func (controller *Send) SendFile(c *fiber.Ctx) error { return c.JSON(utils.ResponseData{ Status: 200, + Code: "SUCCESS", Message: response.Status, Results: response, }) @@ -102,6 +105,7 @@ func (controller *Send) SendVideo(c *fiber.Ctx) error { return c.JSON(utils.ResponseData{ Status: 200, + Code: "SUCCESS", Message: response.Status, Results: response, }) @@ -119,6 +123,7 @@ func (controller *Send) SendContact(c *fiber.Ctx) error { return c.JSON(utils.ResponseData{ Status: 200, + Code: "SUCCESS", Message: response.Status, Results: response, }) @@ -136,6 +141,7 @@ func (controller *Send) SendLink(c *fiber.Ctx) error { return c.JSON(utils.ResponseData{ Status: 200, + Code: "SUCCESS", Message: response.Status, Results: response, }) @@ -153,6 +159,7 @@ func (controller *Send) SendLocation(c *fiber.Ctx) error { return c.JSON(utils.ResponseData{ Status: 200, + Code: "SUCCESS", Message: response.Status, Results: response, }) @@ -171,6 +178,7 @@ func (controller *Send) RevokeMessage(c *fiber.Ctx) error { return c.JSON(utils.ResponseData{ Status: 200, + Code: "SUCCESS", Message: response.Status, Results: response, }) @@ -189,6 +197,7 @@ func (controller *Send) UpdateMessage(c *fiber.Ctx) error { return c.JSON(utils.ResponseData{ Status: 200, + Code: "SUCCESS", Message: response.Status, Results: response, }) diff --git a/src/internal/rest/user.go b/src/internal/rest/user.go index 80d0d2d..dd01f5a 100644 --- a/src/internal/rest/user.go +++ b/src/internal/rest/user.go @@ -33,7 +33,8 @@ func (controller *User) UserInfo(c *fiber.Ctx) error { return c.JSON(utils.ResponseData{ Status: 200, - Message: "Success", + Code: "SUCCESS", + Message: "Success get user info", Results: response.Data[0], }) } @@ -50,6 +51,7 @@ func (controller *User) UserAvatar(c *fiber.Ctx) error { return c.JSON(utils.ResponseData{ Status: 200, + Code: "SUCCESS", Message: "Success get avatar", Results: response, }) @@ -61,6 +63,7 @@ func (controller *User) UserMyPrivacySetting(c *fiber.Ctx) error { return c.JSON(utils.ResponseData{ Status: 200, + Code: "SUCCESS", Message: "Success get privacy", Results: response, }) @@ -72,6 +75,7 @@ func (controller *User) UserMyListGroups(c *fiber.Ctx) error { return c.JSON(utils.ResponseData{ Status: 200, + Code: "SUCCESS", Message: "Success get list groups", Results: response, }) diff --git a/src/pkg/error/app_error.go b/src/pkg/error/app_error.go new file mode 100644 index 0000000..7b865b6 --- /dev/null +++ b/src/pkg/error/app_error.go @@ -0,0 +1,108 @@ +package error + +import "net/http" + +type LoginError string + +// Error for complying the error interface +func (e LoginError) Error() string { + return string(e) +} + +// ErrCode will return the error code based on the error data type +func (e LoginError) ErrCode() string { + return "ALREADY_LOGGED_IN" +} + +// StatusCode will return the HTTP status code based on the error data type +func (e LoginError) StatusCode() int { + return http.StatusBadRequest +} + +type ReconnectError string + +func throwReconnectError(text string) GenericError { + return AuthError(text) +} + +// Error for complying the error interface +func (e ReconnectError) Error() string { + return string(e) +} + +// ErrCode will return the error code based on the error data type +func (e ReconnectError) ErrCode() string { + return "RECONNECT_ERROR" +} + +// StatusCode will return the HTTP status code based on the error data type +func (e ReconnectError) StatusCode() int { + return http.StatusBadRequest +} + +type AuthError string + +func throwAuthError(text string) GenericError { + return AuthError(text) +} + +func (err AuthError) Error() string { + return string(err) +} + +// ErrCode will return the error code based on the error data type +func (err AuthError) ErrCode() string { + return "AUTHENTICATION_ERROR" +} + +// StatusCode will return the HTTP status code based on the error data type +func (err AuthError) StatusCode() int { + return http.StatusUnauthorized +} + +type qrChannelError string + +func throwQrChannelError(text string) GenericError { + return qrChannelError(text) +} + +func (err qrChannelError) Error() string { + return string(err) +} + +// ErrCode will return the error code based on the error data type +func (err qrChannelError) ErrCode() string { + return "QR_CHANNEL_ERROR" +} + +// StatusCode will return the HTTP status code based on the error data type +func (err qrChannelError) StatusCode() int { + return http.StatusInternalServerError +} + +type sessionSavedError string + +func throwSessionSavedError(text string) GenericError { + return sessionSavedError(text) +} + +func (err sessionSavedError) Error() string { + return string(err) +} + +// ErrCode will return the error code based on the error data type +func (err sessionSavedError) ErrCode() string { + return "SESSION_SAVED_ERROR" +} + +// StatusCode will return the HTTP status code based on the error data type +func (err sessionSavedError) StatusCode() int { + return http.StatusInternalServerError +} + +var ( + ErrAlreadyLoggedIn = LoginError("You already logged in :)") + ErrReconnect = throwReconnectError("Reconnect error") + ErrQrChannel = throwQrChannelError("QR channel error") + ErrorSessionSaved = throwSessionSavedError("Your session have been saved, please wait to connect 2 second and refresh again") +) diff --git a/src/pkg/error/auth_error.go b/src/pkg/error/auth_error.go deleted file mode 100644 index 72ef01e..0000000 --- a/src/pkg/error/auth_error.go +++ /dev/null @@ -1,19 +0,0 @@ -package error - -import "net/http" - -type AuthError string - -func (err AuthError) Error() string { - return string(err) -} - -// ErrCode will return the error code based on the error data type -func (err AuthError) ErrCode() string { - return "AUTHENTICATION_ERROR" -} - -// StatusCode will return the HTTP status code based on the error data type -func (err AuthError) StatusCode() int { - return http.StatusUnauthorized -} diff --git a/src/pkg/error/whatsapp_error.go b/src/pkg/error/whatsapp_error.go index bc8fca3..2dfd099 100644 --- a/src/pkg/error/whatsapp_error.go +++ b/src/pkg/error/whatsapp_error.go @@ -36,6 +36,24 @@ func (e WebhookError) StatusCode() int { return http.StatusInternalServerError } +type WaCliError string + +// Error for complying the error interface +func (e WaCliError) Error() string { + return string(e) +} + +// ErrCode will return the error code based on the error data type +func (e WaCliError) ErrCode() string { + return "INVALID_WA_CLI" +} + +// StatusCode will return the HTTP status code based on the error data type +func (e WaCliError) StatusCode() int { + return http.StatusInternalServerError +} + const ( ErrInvalidJID = InvalidJID("your JID is invalid") + ErrWaCLI = WaCliError("your WhatsApp CLI is invalid or empty") ) diff --git a/src/pkg/utils/response.go b/src/pkg/utils/response.go index f76b3c2..a421aa8 100644 --- a/src/pkg/utils/response.go +++ b/src/pkg/utils/response.go @@ -2,7 +2,7 @@ package utils type ResponseData struct { Status int `json:"-"` - Code string `json:"code,omitempty"` + Code string `json:"code"` Message string `json:"message"` - Results any `json:"results"` + Results any `json:"results,omitempty"` } diff --git a/src/services/app.go b/src/services/app.go index 8ec0bd8..e9fe66f 100644 --- a/src/services/app.go +++ b/src/services/app.go @@ -6,8 +6,11 @@ import ( "fmt" "github.com/aldinokemal/go-whatsapp-web-multidevice/config" domainApp "github.com/aldinokemal/go-whatsapp-web-multidevice/domains/app" + pkgError "github.com/aldinokemal/go-whatsapp-web-multidevice/pkg/error" fiberUtils "github.com/gofiber/fiber/v2/utils" + "github.com/sirupsen/logrus" "github.com/skip2/go-qrcode" + "go.mau.fi/libsignal/logger" "go.mau.fi/whatsmeow" "go.mau.fi/whatsmeow/store/sqlstore" "os" @@ -30,7 +33,7 @@ func NewAppService(waCli *whatsmeow.Client, db *sqlstore.Container) domainApp.IA func (service serviceApp) Login(_ context.Context) (response domainApp.LoginResponse, err error) { if service.WaCli == nil { - return response, errors.New("wa cli nil cok") + return response, pkgError.ErrWaCLI } // Disconnect for reconnecting @@ -40,15 +43,16 @@ func (service serviceApp) Login(_ context.Context) (response domainApp.LoginResp ch, err := service.WaCli.GetQRChannel(context.Background()) if err != nil { + logrus.Error(err.Error()) // This error means that we're already logged in, so ignore it. if errors.Is(err, whatsmeow.ErrQRStoreContainsID) { _ = service.WaCli.Connect() // just connect to websocket if service.WaCli.IsLoggedIn() { - return response, errors.New("you already logged in :)") + return response, pkgError.ErrAlreadyLoggedIn } - return response, errors.New("your session have been saved, please wait to connect 2 second and refresh again") + return response, pkgError.ErrorSessionSaved } else { - return response, errors.New("Error when GetQRChannel:" + err.Error()) + return response, pkgError.ErrQrChannel } } else { go func() { @@ -59,18 +63,18 @@ func (service serviceApp) Login(_ context.Context) (response domainApp.LoginResp qrPath := fmt.Sprintf("%s/scan-qr-%s.png", config.PathQrCode, fiberUtils.UUIDv4()) err = qrcode.WriteFile(evt.Code, qrcode.Medium, 512, qrPath) if err != nil { - fmt.Println("error when write qrImage file", err.Error()) + logrus.Error("Error when write qr code to file: ", err) } go func() { time.Sleep(response.Duration * time.Second) err := os.Remove(qrPath) if err != nil { - fmt.Println("Failed to remove qrPath " + qrPath) + logrus.Error("error when remove qrImage file", err.Error()) } }() chImage <- qrPath } else { - fmt.Printf("QR channel result: %s", evt.Event) + logrus.Error("error when get qrCode", evt.Event) } } }() @@ -78,7 +82,8 @@ func (service serviceApp) Login(_ context.Context) (response domainApp.LoginResp err = service.WaCli.Connect() if err != nil { - return response, errors.New("Failed to connect bro " + err.Error()) + logger.Error("Error when connect to whatsapp", err) + return response, pkgError.ErrReconnect } response.ImagePath = <-chImage @@ -137,7 +142,7 @@ func (service serviceApp) Reconnect(_ context.Context) (err error) { func (service serviceApp) FetchDevices(_ context.Context) (response []domainApp.FetchDevicesResponse, err error) { if service.WaCli == nil { - return response, errors.New("wa cli nil cok") + return response, pkgError.ErrWaCLI } devices, err := service.db.GetAllDevices()