From 05ec7f827f419460b25813cb9a616864911359be Mon Sep 17 00:00:00 2001 From: Aldino Kemal Date: Sun, 20 Nov 2022 20:46:43 +0700 Subject: [PATCH] feat: add send link (#29) * feat: add send link feat: add more response when send api * feat: remove url * feat: update open api * chore: update docs --- docs/openapi.yaml | 111 ++++++++++++++++++++++++++- readme.md | 8 +- src/config/settings.go | 2 +- src/domains/send/contact.go | 3 +- src/domains/send/file.go | 3 +- src/domains/send/image.go | 3 +- src/domains/send/link.go | 13 ++++ src/domains/send/send.go | 1 + src/domains/send/text.go | 3 +- src/domains/send/video.go | 3 +- src/internal/rest/send_controller.go | 25 ++++++ src/services/send_service.go | 89 +++++++++++++++------ src/validations/send_validation.go | 16 ++++ 13 files changed, 246 insertions(+), 34 deletions(-) create mode 100644 src/domains/send/link.go diff --git a/docs/openapi.yaml b/docs/openapi.yaml index febf58e..f0e380f 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -1,7 +1,7 @@ openapi: 3.0.0 info: title: WhatsApp API MultiDevice - version: 2.3.0 + version: 2.4.0 description: This API is used for sending whatsapp via API servers: - url: http://localhost:3000 @@ -15,6 +15,7 @@ tags: paths: /app/login: get: + operationId: authLogin tags: - auth summary: Login to whatsapp server @@ -47,6 +48,7 @@ paths: http://localhost:3000/statics/images/qrcode/scan-qr-b0b7bb43-9a22-455a-814f-5a225c743310.png /app/logout: get: + operationId: authLogout tags: - auth summary: Remove database and logout @@ -96,6 +98,7 @@ paths: example: the store doesn't contain a device JID /app/reconnect: get: + operationId: authReconnect tags: - auth summary: Reconnecting to whatsapp server @@ -125,6 +128,7 @@ paths: results: null /user/info: get: + operationId: userInfo tags: - user summary: User Info @@ -241,6 +245,7 @@ paths: results: null /user/avatar: get: + operationId: userAvatar tags: - user summary: User Avatar @@ -303,6 +308,7 @@ paths: results: null /user/my/privacy: get: + operationId: userMyPrivacy tags: - user summary: User My Privacy Setting @@ -368,6 +374,7 @@ paths: results: null /user/my/groups: get: + operationId: userMyGroups tags: - user summary: User My List Groups @@ -397,6 +404,7 @@ paths: results: null /send/message: post: + operationId: sendMessage tags: - send summary: Send Message @@ -491,6 +499,7 @@ paths: results: null /send/image: post: + operationId: sendImage tags: - send summary: Send Image @@ -594,6 +603,7 @@ paths: results: null /send/file: post: + operationId: sendFile tags: - send summary: Send File @@ -691,6 +701,7 @@ paths: results: null /send/video: post: + operationId: sendVideo tags: - send summary: Send Video @@ -793,6 +804,7 @@ paths: results: null /send/contact: post: + operationId: sendContact tags: - send summary: Send Contact @@ -887,4 +899,101 @@ paths: code: 500 message: you are not loggin results: null + /send/link: + post: + operationId: sendLink + tags: + - send + summary: Send Link + requestBody: + content: + multipart/form-data: + schema: + type: object + properties: + phone: + type: integer + example: '6289685024051' + link: + type: string + example: "https://google.com" + caption: + type: string + example: 'Halo ini contoh caption' + type: + type: string + example: 'user' + description: 'user/group | default: user' + responses: + '200': + description: OK + headers: + Date: + schema: + type: string + example: Fri, 11 Feb 2022 03:42:57 GMT + Content-Type: + schema: + type: string + example: application/json + Content-Length: + schema: + type: integer + example: '123' + content: + application/json: + schema: + type: object + example: + code: 200 + message: Success + results: + status: >- + Link sent to 6289685024051@s.whatsapp.net (server timestamp: 2022-05-17 14:39:29 +0700 WIB) + '400': + description: Bad Request + headers: + Date: + schema: + type: string + example: Fri, 11 Feb 2022 03:02:17 GMT + Content-Type: + schema: + type: string + example: application/json + Content-Length: + schema: + type: integer + example: '70' + content: + application/json: + schema: + type: object + example: + code: 400 + message: 'phone: cannot be blank.' + results: null + '500': + description: Internal Server Error + headers: + Date: + schema: + type: string + example: Fri, 11 Feb 2022 03:02:48 GMT + Content-Type: + schema: + type: string + example: application/json + Content-Length: + schema: + type: integer + example: '58' + content: + application/json: + schema: + type: object + example: + code: 500 + message: you are not loggin + results: null diff --git a/readme.md b/readme.md index 7b59908..881e61f 100644 --- a/readme.md +++ b/readme.md @@ -1,4 +1,5 @@ ## Go Whatsapp API Multi Device Version + [![buddy pipeline](https://app.buddy.works/aldinokemal/go-whatsapp-web-multidevice/pipelines/pipeline/423077/badge.svg?token=a951a4546fe3f54079e678cc9d0eea12069fbdc21f8ed5ea22e1e95c4f63215f "buddy pipeline")](https://app.buddy.works/aldinokemal/go-whatsapp-web-multidevice/pipelines/pipeline/423077) [![release windows](https://github.com/aldinokemal/go-whatsapp-web-multidevice/actions/workflows/deploy-windows.yml/badge.svg "release windows")](https://github.com/aldinokemal/go-whatsapp-web-multidevice/actions/workflows/deploy-windows.yml) [![release linux](https://github.com/aldinokemal/go-whatsapp-web-multidevice/actions/workflows/deploy-linux.yml/badge.svg "release linux")](https://github.com/aldinokemal/go-whatsapp-web-multidevice/actions/workflows/deploy-linux.yml) @@ -10,7 +11,7 @@ - Compress image before send - Compress video before send - Change OS name become your app (it's the device name when connect via mobile) - - `--os=Chrome` or `--os=MyApplication` + - `--os=Chrome` or `--os=MyApplication` - Basic Auth - `--basic-auth=kemal:secret`, or you can simplify - `-b=kemal:secret` @@ -19,7 +20,7 @@ - `--debug true` - Auto reply message - `--autoreply="Don't reply this message"` -- Webhook for received message +- Webhook for received message - `--webhook="http://yourwebhook.site/handler"`, or you can simplify - `-w="http://yourwebhook.site/handler"` - For more command `./main --help` @@ -100,7 +101,8 @@ You can check [docs/openapi.yml](./docs/openapi.yaml) for detail API | ✅ | Send Image | POST | /send/image | | ✅ | Send File | POST | /send/file | | ✅ | Send Video | POST | /send/video | -| ✅ | Send Contact | POST | /send/contact | +| ✅ | Send Contact | POST | /send/contact | +| ✅ | Send Link | POST | /send/link | ``` ✅ = Available diff --git a/src/config/settings.go b/src/config/settings.go index c23500e..4589fed 100644 --- a/src/config/settings.go +++ b/src/config/settings.go @@ -6,7 +6,7 @@ import ( ) var ( - AppVersion = "v3.7.1" + AppVersion = "v3.10.0" AppPort = "3000" AppDebug = false AppOs = fmt.Sprintf("AldinoKemal") diff --git a/src/domains/send/contact.go b/src/domains/send/contact.go index d6aabe2..fdd275f 100644 --- a/src/domains/send/contact.go +++ b/src/domains/send/contact.go @@ -8,5 +8,6 @@ type ContactRequest struct { } type ContactResponse struct { - Status string `json:"status"` + MessageID string `json:"message_id"` + Status string `json:"status"` } diff --git a/src/domains/send/file.go b/src/domains/send/file.go index e05fa9b..9288522 100644 --- a/src/domains/send/file.go +++ b/src/domains/send/file.go @@ -9,5 +9,6 @@ type FileRequest struct { } type FileResponse struct { - Status string `json:"status"` + MessageID string `json:"message_id"` + Status string `json:"status"` } diff --git a/src/domains/send/image.go b/src/domains/send/image.go index 9c15672..3bffd7d 100644 --- a/src/domains/send/image.go +++ b/src/domains/send/image.go @@ -12,5 +12,6 @@ type ImageRequest struct { } type ImageResponse struct { - Status string `json:"status"` + MessageID string `json:"message_id"` + Status string `json:"status"` } diff --git a/src/domains/send/link.go b/src/domains/send/link.go new file mode 100644 index 0000000..83bb68e --- /dev/null +++ b/src/domains/send/link.go @@ -0,0 +1,13 @@ +package send + +type LinkRequest struct { + Phone string `json:"phone" form:"phone"` + Caption string `json:"caption"` + Link string `json:"link"` + Type Type `json:"type" form:"type"` +} + +type LinkResponse struct { + MessageID string `json:"message_id"` + Status string `json:"status"` +} diff --git a/src/domains/send/send.go b/src/domains/send/send.go index 8e6b8ef..06fca62 100644 --- a/src/domains/send/send.go +++ b/src/domains/send/send.go @@ -15,4 +15,5 @@ type ISendService interface { SendFile(ctx context.Context, request FileRequest) (response FileResponse, err error) SendVideo(ctx context.Context, request VideoRequest) (response VideoResponse, err error) SendContact(ctx context.Context, request ContactRequest) (response ContactResponse, err error) + SendLink(ctx context.Context, request LinkRequest) (response LinkResponse, err error) } diff --git a/src/domains/send/text.go b/src/domains/send/text.go index 6f49354..f115fa4 100644 --- a/src/domains/send/text.go +++ b/src/domains/send/text.go @@ -7,5 +7,6 @@ type MessageRequest struct { } type MessageResponse struct { - Status string `json:"status"` + MessageID string `json:"message_id"` + Status string `json:"status"` } diff --git a/src/domains/send/video.go b/src/domains/send/video.go index 90cb64e..66281db 100644 --- a/src/domains/send/video.go +++ b/src/domains/send/video.go @@ -12,5 +12,6 @@ type VideoRequest struct { } type VideoResponse struct { - Status string `json:"status"` + MessageID string `json:"message_id"` + Status string `json:"status"` } diff --git a/src/internal/rest/send_controller.go b/src/internal/rest/send_controller.go index 81a5c35..dee923a 100644 --- a/src/internal/rest/send_controller.go +++ b/src/internal/rest/send_controller.go @@ -18,6 +18,7 @@ func InitRestSend(app *fiber.App, service domainSend.ISendService) Send { app.Post("/send/file", rest.SendFile) app.Post("/send/video", rest.SendVideo) app.Post("/send/contact", rest.SendContact) + app.Post("/send/link", rest.SendLink) return rest } @@ -158,3 +159,27 @@ func (controller *Send) SendContact(c *fiber.Ctx) error { Results: response, }) } + +func (controller *Send) SendLink(c *fiber.Ctx) error { + var request domainSend.LinkRequest + err := c.BodyParser(&request) + utils.PanicIfNeeded(err) + + err = validations.ValidateSendLink(request) + utils.PanicIfNeeded(err) + + if request.Type == domainSend.TypeGroup { + request.Phone = request.Phone + "@g.us" + } else { + request.Phone = request.Phone + "@s.whatsapp.net" + } + + response, err := controller.Service.SendLink(c.UserContext(), request) + utils.PanicIfNeeded(err) + + return c.JSON(utils.ResponseData{ + Code: 200, + Message: response.Status, + Results: response, + }) +} diff --git a/src/services/send_service.go b/src/services/send_service.go index d08456f..652a5c1 100644 --- a/src/services/send_service.go +++ b/src/services/send_service.go @@ -35,13 +35,16 @@ func (service serviceSend) SendText(ctx context.Context, request domainSend.Mess if !ok { return response, errors.New("invalid JID " + request.Phone) } + + msgId := whatsmeow.GenerateMessageID() msg := &waProto.Message{Conversation: proto.String(request.Message)} - ts, err := service.WaCli.SendMessage(ctx, recipient, "", msg) + ts, err := service.WaCli.SendMessage(ctx, recipient, msgId, msg) if err != nil { return response, err - } else { - response.Status = fmt.Sprintf("Message sent to %s (server timestamp: %s)", request.Phone, ts) } + + response.MessageID = msgId + response.Status = fmt.Sprintf("Message sent to %s (server timestamp: %s)", request.Phone, ts) return response, nil } @@ -111,6 +114,7 @@ func (service serviceSend) SendImage(ctx context.Context, request domainSend.Ima } dataWaThumbnail, err := os.ReadFile(imageThumbnail) + msgId := whatsmeow.GenerateMessageID() msg := &waProto.Message{ImageMessage: &waProto.ImageMessage{ JpegThumbnail: dataWaThumbnail, Caption: proto.String(dataWaCaption), @@ -123,7 +127,7 @@ func (service serviceSend) SendImage(ctx context.Context, request domainSend.Ima FileLength: proto.Uint64(uint64(len(dataWaImage))), ViewOnce: proto.Bool(request.ViewOnce), }} - ts, err := service.WaCli.SendMessage(ctx, dataWaRecipient, "", msg) + ts, err := service.WaCli.SendMessage(ctx, dataWaRecipient, msgId, msg) go func() { errDelete := utils.RemoveFile(0, deletedItems...) if errDelete != nil { @@ -132,10 +136,11 @@ func (service serviceSend) SendImage(ctx context.Context, request domainSend.Ima }() if err != nil { return response, err - } else { - response.Status = fmt.Sprintf("Message sent to %s (server timestamp: %s)", request.Phone, ts) - return response, nil } + + response.MessageID = msgId + response.Status = fmt.Sprintf("Message sent to %s (server timestamp: %s)", request.Phone, ts) + return response, nil } func (service serviceSend) SendFile(ctx context.Context, request domainSend.FileRequest) (response domainSend.FileResponse, err error) { @@ -162,6 +167,7 @@ func (service serviceSend) SendFile(ctx context.Context, request domainSend.File return response, err } + msgId := whatsmeow.GenerateMessageID() msg := &waProto.Message{DocumentMessage: &waProto.DocumentMessage{ Url: proto.String(uploadedFile.URL), Mimetype: proto.String(http.DetectContentType(dataWaFile)), @@ -173,7 +179,7 @@ func (service serviceSend) SendFile(ctx context.Context, request domainSend.File FileEncSha256: uploadedFile.FileEncSHA256, DirectPath: proto.String(uploadedFile.DirectPath), }} - ts, err := service.WaCli.SendMessage(ctx, dataWaRecipient, "", msg) + ts, err := service.WaCli.SendMessage(ctx, dataWaRecipient, msgId, msg) go func() { errDelete := utils.RemoveFile(0, oriFilePath) if errDelete != nil { @@ -182,10 +188,11 @@ func (service serviceSend) SendFile(ctx context.Context, request domainSend.File }() if err != nil { return response, err - } else { - response.Status = fmt.Sprintf("Document sent to %s (server timestamp: %s)", request.Phone, ts) - return response, nil } + + response.MessageID = msgId + response.Status = fmt.Sprintf("Document sent to %s (server timestamp: %s)", request.Phone, ts) + return response, nil } func (service serviceSend) SendVideo(ctx context.Context, request domainSend.VideoRequest) (response domainSend.VideoResponse, err error) { @@ -250,7 +257,7 @@ func (service serviceSend) SendVideo(ctx context.Context, request domainSend.Vid if err != nil { return response, err } - uploadedFile, err := service.WaCli.Upload(context.Background(), dataWaVideo, whatsmeow.MediaVideo) + uploaded, err := service.WaCli.Upload(context.Background(), dataWaVideo, whatsmeow.MediaVideo) if err != nil { fmt.Printf("Failed to upload file: %v", err) return response, err @@ -260,19 +267,20 @@ func (service serviceSend) SendVideo(ctx context.Context, request domainSend.Vid return response, err } + msgId := whatsmeow.GenerateMessageID() msg := &waProto.Message{VideoMessage: &waProto.VideoMessage{ - Url: proto.String(uploadedFile.URL), + Url: proto.String(uploaded.URL), Mimetype: proto.String(http.DetectContentType(dataWaVideo)), Caption: proto.String(request.Caption), - FileLength: proto.Uint64(uploadedFile.FileLength), - FileSha256: uploadedFile.FileSHA256, - FileEncSha256: uploadedFile.FileEncSHA256, - MediaKey: uploadedFile.MediaKey, - DirectPath: proto.String(uploadedFile.DirectPath), + FileLength: proto.Uint64(uploaded.FileLength), + FileSha256: uploaded.FileSHA256, + FileEncSha256: uploaded.FileEncSHA256, + MediaKey: uploaded.MediaKey, + DirectPath: proto.String(uploaded.DirectPath), ViewOnce: proto.Bool(request.ViewOnce), JpegThumbnail: dataWaThumbnail, }} - ts, err := service.WaCli.SendMessage(ctx, dataWaRecipient, "", msg) + ts, err := service.WaCli.SendMessage(ctx, dataWaRecipient, msgId, msg) go func() { errDelete := utils.RemoveFile(0, deletedItems...) if errDelete != nil { @@ -281,10 +289,11 @@ func (service serviceSend) SendVideo(ctx context.Context, request domainSend.Vid }() if err != nil { return response, err - } else { - response.Status = fmt.Sprintf("Video sent to %s (server timestamp: %s)", request.Phone, ts) - return response, nil } + + response.MessageID = msgId + response.Status = fmt.Sprintf("Video sent to %s (server timestamp: %s)", request.Phone, ts) + return response, nil } func (service serviceSend) SendContact(ctx context.Context, request domainSend.ContactRequest) (response domainSend.ContactResponse, err error) { @@ -296,6 +305,7 @@ func (service serviceSend) SendContact(ctx context.Context, request domainSend.C } msgVCard := fmt.Sprintf("BEGIN:VCARD\nVERSION:3.0\nN:;%v;;;\nFN:%v\nTEL;type=CELL;waid=%v:+%v\nEND:VCARD", request.ContactName, request.ContactName, request.ContactPhone, request.ContactPhone) + msgId := whatsmeow.GenerateMessageID() msg := &waProto.Message{ContactMessage: &waProto.ContactMessage{ DisplayName: proto.String(request.ContactName), Vcard: proto.String(msgVCard), @@ -303,8 +313,39 @@ func (service serviceSend) SendContact(ctx context.Context, request domainSend.C ts, err := service.WaCli.SendMessage(ctx, recipient, "", msg) if err != nil { return response, err - } else { - response.Status = fmt.Sprintf("Contact sent to %s (server timestamp: %s)", request.Phone, ts) } + + response.MessageID = msgId + response.Status = fmt.Sprintf("Contact sent to %s (server timestamp: %s)", request.Phone, ts) + return response, nil +} + +func (service serviceSend) SendLink(ctx context.Context, request domainSend.LinkRequest) (response domainSend.LinkResponse, err error) { + utils.MustLogin(service.WaCli) + + recipient, ok := utils.ParseJID(request.Phone) + if !ok { + return response, errors.New("invalid JID " + request.Phone) + } + + msgId := whatsmeow.GenerateMessageID() + msg := &waProto.Message{ExtendedTextMessage: &waProto.ExtendedTextMessage{ + Text: proto.String(request.Caption), + MatchedText: proto.String(request.Caption), + CanonicalUrl: proto.String(request.Link), + ContextInfo: &waProto.ContextInfo{ + ActionLink: &waProto.ActionLink{ + Url: proto.String(request.Link), + ButtonTitle: proto.String(request.Caption), + }, + }, + }} + ts, err := service.WaCli.SendMessage(ctx, recipient, msgId, msg) + if err != nil { + return response, err + } + + response.MessageID = msgId + response.Status = fmt.Sprintf("Link sent to %s (server timestamp: %s)", request.Phone, ts) return response, nil } diff --git a/src/validations/send_validation.go b/src/validations/send_validation.go index 9474232..b4e30ae 100644 --- a/src/validations/send_validation.go +++ b/src/validations/send_validation.go @@ -113,3 +113,19 @@ func ValidateSendContact(request domainSend.ContactRequest) { }) } } + +func ValidateSendLink(request domainSend.LinkRequest) error { + err := validation.ValidateStruct(&request, + validation.Field(&request.Phone, validation.Required, validation.Length(10, 25)), + validation.Field(&request.Link, validation.Required), + validation.Field(&request.Caption, validation.Required), + ) + + if err != nil { + return utils.ValidationError{ + Message: err.Error(), + } + } + + return nil +}