Browse Source

feat: add send location (#43)

* feat: add send location

* feat: update frontend

* feat: update docs

* feat: update docs

* fix: logout remove file

* fix: remove gitignore

* fix: send video

* feat: add context in validation
pull/45/head v4.3.0
Aldino Kemal 3 years ago
committed by GitHub
parent
commit
38ef3ec468
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 45
      docs/openapi.yaml
  2. 28
      readme.md
  3. 2
      src/config/settings.go
  4. 12
      src/domains/send/location.go
  5. 1
      src/domains/send/send.go
  6. 18
      src/internal/rest/send.go
  7. 10
      src/pkg/utils/general.go
  8. 2
      src/pkg/whatsapp/whatsapp.go
  9. 20
      src/services/app.go
  10. 53
      src/services/send.go
  11. 10
      src/services/user.go
  12. 47
      src/validations/send_validation.go
  13. 90
      src/validations/send_validation_test.go
  14. 9
      src/validations/user_validation.go
  15. 5
      src/validations/user_validation_test.go
  16. 130
      src/views/index.html

45
docs/openapi.yaml

@ -1,7 +1,7 @@
openapi: 3.0.0 openapi: 3.0.0
info: info:
title: WhatsApp API MultiDevice title: WhatsApp API MultiDevice
version: 3.0.0
version: 3.1.0
description: This API is used for sending whatsapp via API description: This API is used for sending whatsapp via API
servers: servers:
- url: http://localhost:3000 - url: http://localhost:3000
@ -566,6 +566,49 @@ paths:
application/json: application/json:
schema: schema:
$ref: '#/components/schemas/ErrorInternalServer' $ref: '#/components/schemas/ErrorInternalServer'
/send/location:
post:
operationId: sendLocation
tags:
- message
summary: Send Location
requestBody:
content:
multipart/form-data:
schema:
type: object
properties:
phone:
type: integer
example: '6289685024051@s.whatsapp.net'
description: Phone number with country code
latitude:
type: string
example: "-7.797068"
description: Latitude coordinate
longitude:
type: string
example: '110.370529'
description: Longitude coordinate
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/SendResponse'
'400':
description: Bad Request
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorBadRequest'
'500':
description: Internal Server Error
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorInternalServer'
/message/:message_id/revoke: /message/:message_id/revoke:
post: post:
operationId: revokeMessage operationId: revokeMessage

28
readme.md

@ -107,7 +107,8 @@ API using [openapi-generator](https://openapi-generator.tech/#try)
| ✅ | Send Video | POST | /send/video | | ✅ | Send Video | POST | /send/video |
| ✅ | Send Contact | POST | /send/contact | | ✅ | Send Contact | POST | /send/contact |
| ✅ | Send Link | POST | /send/link | | ✅ | Send Link | POST | /send/link |
| ✅ | Revoke Messave | POST | /message/:message_id/revoke |
| ✅ | Send Location | POST | /send/location |
| ✅ | Revoke Message | POST | /message/:message_id/revoke |
``` ```
✅ = Available ✅ = Available
@ -116,20 +117,21 @@ API using [openapi-generator](https://openapi-generator.tech/#try)
### App User Interface ### App User Interface
1. Homepage ![Homepage](https://i.ibb.co/NNX2wWY/home.png)
1. Homepage ![Homepage](https://i.ibb.co/GMNWqRq/homepage.png)
2. Login ![Login](https://i.ibb.co/jkcB15R/login.png) 2. Login ![Login](https://i.ibb.co/jkcB15R/login.png)
3. Send Message ![Send Message](https://i.ibb.co/DrCVXS7/send-message.png)
4. Send Image ![Send Image](https://i.ibb.co/WykfQc8/send-image.png)
5. Send File ![Send File](https://i.ibb.co/wC4SfRp/send-file.png)
6. Send Video ![Send Video](https://i.ibb.co/VDCRH3G/send-video.png)
3. Send Message ![Send Message](https://i.ibb.co/rc3NXMX/send-message.png)
4. Send Image ![Send Image](https://i.ibb.co/BcFL3SD/send-image.png)
5. Send File ![Send File](https://i.ibb.co/f4yxjpp/send-file.png)
6. Send Video ![Send Video](https://i.ibb.co/PrD3P51/send-video.png)
7. Send Contact ![Send Contact](https://i.ibb.co/4810H7N/send-contact.png) 7. Send Contact ![Send Contact](https://i.ibb.co/4810H7N/send-contact.png)
8. Revoke Message ![Revoke Message](https://i.ibb.co/yswhvQY/revoke.png?)
9. User Info ![User Info](https://i.ibb.co/3zjX6Cz/user-info.png)
10. User Avatar ![User Avatar](https://i.ibb.co/cysjmjT/user-avatar.png?)
11. My Privacy ![My Privacy](https://i.ibb.co/Cw1sMQz/my-privacy.png)
12. My Group ![My Group](https://i.ibb.co/B6rW8Sh/list-group.png)
13. Auto Reply ![Auto Reply](https://i.ibb.co/D4rTytX/IMG-20220517-162500.jpg)
14. Basic Auth Prompt ![Basic Auth](https://i.ibb.co/PDjQ92W/Screenshot-2022-11-06-at-14-06-29.png)
8. Send Location ![Send Location](https://i.ibb.co/TWsy09G/send-location.png)
9. Revoke Message ![Revoke Message](https://i.ibb.co/yswhvQY/revoke.png?)
10. User Info ![User Info](https://i.ibb.co/3zjX6Cz/user-info.png)
11. User Avatar ![User Avatar](https://i.ibb.co/cysjmjT/user-avatar.png?)
12. My Privacy ![My Privacy](https://i.ibb.co/Cw1sMQz/my-privacy.png)
13. My Group ![My Group](https://i.ibb.co/B6rW8Sh/list-group.png)
14. Auto Reply ![Auto Reply](https://i.ibb.co/D4rTytX/IMG-20220517-162500.jpg)
15. Basic Auth Prompt ![Basic Auth](https://i.ibb.co/PDjQ92W/Screenshot-2022-11-06-at-14-06-29.png)
### Mac OS NOTE ### Mac OS NOTE

2
src/config/settings.go

@ -6,7 +6,7 @@ import (
) )
var ( var (
AppVersion = "v4.2.0"
AppVersion = "v4.3.0"
AppPort = "3000" AppPort = "3000"
AppDebug = false AppDebug = false
AppOs = fmt.Sprintf("AldinoKemal") AppOs = fmt.Sprintf("AldinoKemal")

12
src/domains/send/location.go

@ -0,0 +1,12 @@
package send
type LocationRequest struct {
Phone string `json:"phone" form:"phone"`
Latitude string `json:"latitude" form:"latitude"`
Longitude string `json:"longitude" form:"longitude"`
}
type LocationResponse struct {
MessageID string `json:"message_id"`
Status string `json:"status"`
}

1
src/domains/send/send.go

@ -11,6 +11,7 @@ type ISendService interface {
SendVideo(ctx context.Context, request VideoRequest) (response VideoResponse, err error) SendVideo(ctx context.Context, request VideoRequest) (response VideoResponse, err error)
SendContact(ctx context.Context, request ContactRequest) (response ContactResponse, err error) SendContact(ctx context.Context, request ContactRequest) (response ContactResponse, err error)
SendLink(ctx context.Context, request LinkRequest) (response LinkResponse, err error) SendLink(ctx context.Context, request LinkRequest) (response LinkResponse, err error)
SendLocation(ctx context.Context, request LocationRequest) (response LocationResponse, err error)
Revoke(ctx context.Context, request RevokeRequest) (response RevokeResponse, err error) Revoke(ctx context.Context, request RevokeRequest) (response RevokeResponse, err error)
UpdateMessage(ctx context.Context, request UpdateMessageRequest) (response UpdateMessageResponse, err error) UpdateMessage(ctx context.Context, request UpdateMessageRequest) (response UpdateMessageResponse, err error)
} }

18
src/internal/rest/send.go

@ -19,6 +19,7 @@ func InitRestSend(app *fiber.App, service domainSend.ISendService) Send {
app.Post("/send/video", rest.SendVideo) app.Post("/send/video", rest.SendVideo)
app.Post("/send/contact", rest.SendContact) app.Post("/send/contact", rest.SendContact)
app.Post("/send/link", rest.SendLink) app.Post("/send/link", rest.SendLink)
app.Post("/send/location", rest.SendLocation)
app.Post("/message/:message_id/revoke", rest.RevokeMessage) app.Post("/message/:message_id/revoke", rest.RevokeMessage)
app.Post("/message/:message_id/update", rest.UpdateMessage) app.Post("/message/:message_id/update", rest.UpdateMessage)
return rest return rest
@ -140,6 +141,23 @@ func (controller *Send) SendLink(c *fiber.Ctx) error {
}) })
} }
func (controller *Send) SendLocation(c *fiber.Ctx) error {
var request domainSend.LocationRequest
err := c.BodyParser(&request)
utils.PanicIfNeeded(err)
whatsapp.SanitizePhone(&request.Phone)
response, err := controller.Service.SendLocation(c.UserContext(), request)
utils.PanicIfNeeded(err)
return c.JSON(utils.ResponseData{
Status: 200,
Message: response.Status,
Results: response,
})
}
func (controller *Send) RevokeMessage(c *fiber.Ctx) error { func (controller *Send) RevokeMessage(c *fiber.Ctx) error {
var request domainSend.RevokeRequest var request domainSend.RevokeRequest
err := c.BodyParser(&request) err := c.BodyParser(&request)

10
src/pkg/utils/general.go

@ -4,6 +4,8 @@ import (
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"strconv"
"strings"
"time" "time"
) )
@ -46,3 +48,11 @@ func PanicIfNeeded(err any, message ...string) {
} }
} }
} }
func StrToFloat64(text string) float64 {
var result float64
if text != "" {
result, _ = strconv.ParseFloat(strings.TrimSpace(text), 64)
}
return result
}

2
src/pkg/whatsapp/whatsapp.go

@ -240,7 +240,7 @@ func handler(rawEvt interface{}) {
} }
case *events.HistorySync: case *events.HistorySync:
id := atomic.AddInt32(&historySyncID, 1) id := atomic.AddInt32(&historySyncID, 1)
fileName := fmt.Sprintf("%s/history-%d-%d.json", config.PathStorages, startupTime, id)
fileName := fmt.Sprintf("%s/history-%d-%s-%d-%s.json", config.PathStorages, startupTime, cli.Store.ID.String(), id, evt.Data.SyncType.String())
file, err := os.OpenFile(fileName, os.O_WRONLY|os.O_CREATE, 0600) file, err := os.OpenFile(fileName, os.O_WRONLY|os.O_CREATE, 0600)
if err != nil { if err != nil {
log.Errorf("Failed to open file to write history sync: %v", err) log.Errorf("Failed to open file to write history sync: %v", err)

20
src/services/app.go

@ -12,6 +12,7 @@ import (
"go.mau.fi/whatsmeow/store/sqlstore" "go.mau.fi/whatsmeow/store/sqlstore"
"os" "os"
"path/filepath" "path/filepath"
"strings"
"time" "time"
) )
@ -86,7 +87,7 @@ func (service serviceApp) Login(_ context.Context) (response domainApp.LoginResp
func (service serviceApp) Logout(_ context.Context) (err error) { func (service serviceApp) Logout(_ context.Context) (err error) {
// delete history // delete history
files, err := filepath.Glob("./history-*")
files, err := filepath.Glob(fmt.Sprintf("./%s/history-*", config.PathStorages))
if err != nil { if err != nil {
return err return err
} }
@ -98,7 +99,7 @@ func (service serviceApp) Logout(_ context.Context) (err error) {
} }
} }
// delete qr images // delete qr images
qrImages, err := filepath.Glob("./statics/images/qrcode/scan-*")
qrImages, err := filepath.Glob(fmt.Sprintf("./%s/scan-*", config.PathQrCode))
if err != nil { if err != nil {
return err return err
} }
@ -110,6 +111,21 @@ func (service serviceApp) Logout(_ context.Context) (err error) {
} }
} }
// delete senditems
qrItems, err := filepath.Glob(fmt.Sprintf("./%s/*", config.PathSendItems))
if err != nil {
return err
}
for _, f := range qrItems {
if !strings.Contains(f, ".gitignore") {
err = os.Remove(f)
if err != nil {
return err
}
}
}
err = service.WaCli.Logout() err = service.WaCli.Logout()
return return
} }

53
src/services/send.go

@ -32,7 +32,7 @@ func NewSendService(waCli *whatsmeow.Client) domainSend.ISendService {
} }
func (service serviceSend) SendText(ctx context.Context, request domainSend.MessageRequest) (response domainSend.MessageResponse, err error) { func (service serviceSend) SendText(ctx context.Context, request domainSend.MessageRequest) (response domainSend.MessageResponse, err error) {
err = validations.ValidateSendMessage(request)
err = validations.ValidateSendMessage(ctx, request)
if err != nil { if err != nil {
return response, err return response, err
} }
@ -54,7 +54,7 @@ func (service serviceSend) SendText(ctx context.Context, request domainSend.Mess
} }
func (service serviceSend) SendImage(ctx context.Context, request domainSend.ImageRequest) (response domainSend.ImageResponse, err error) { func (service serviceSend) SendImage(ctx context.Context, request domainSend.ImageRequest) (response domainSend.ImageResponse, err error) {
err = validations.ValidateSendImage(request)
err = validations.ValidateSendImage(ctx, request)
if err != nil { if err != nil {
return response, err return response, err
} }
@ -152,7 +152,7 @@ func (service serviceSend) SendImage(ctx context.Context, request domainSend.Ima
} }
func (service serviceSend) SendFile(ctx context.Context, request domainSend.FileRequest) (response domainSend.FileResponse, err error) { func (service serviceSend) SendFile(ctx context.Context, request domainSend.FileRequest) (response domainSend.FileResponse, err error) {
err = validations.ValidateSendFile(request)
err = validations.ValidateSendFile(ctx, request)
if err != nil { if err != nil {
return response, err return response, err
} }
@ -207,7 +207,7 @@ func (service serviceSend) SendFile(ctx context.Context, request domainSend.File
} }
func (service serviceSend) SendVideo(ctx context.Context, request domainSend.VideoRequest) (response domainSend.VideoResponse, err error) { func (service serviceSend) SendVideo(ctx context.Context, request domainSend.VideoRequest) (response domainSend.VideoResponse, err error) {
err = validations.ValidateSendVideo(request)
err = validations.ValidateSendVideo(ctx, request)
if err != nil { if err != nil {
return response, err return response, err
} }
@ -296,10 +296,13 @@ func (service serviceSend) SendVideo(ctx context.Context, request domainSend.Vid
DirectPath: proto.String(uploaded.DirectPath), DirectPath: proto.String(uploaded.DirectPath),
ViewOnce: proto.Bool(request.ViewOnce), ViewOnce: proto.Bool(request.ViewOnce),
JpegThumbnail: dataWaThumbnail, JpegThumbnail: dataWaThumbnail,
ThumbnailEncSha256: dataWaThumbnail,
ThumbnailSha256: dataWaThumbnail,
ThumbnailDirectPath: proto.String(uploaded.DirectPath),
}} }}
ts, err := service.WaCli.SendMessage(ctx, dataWaRecipient, msgId, msg) ts, err := service.WaCli.SendMessage(ctx, dataWaRecipient, msgId, msg)
go func() { go func() {
errDelete := utils.RemoveFile(0, deletedItems...)
errDelete := utils.RemoveFile(1, deletedItems...)
if errDelete != nil { if errDelete != nil {
fmt.Println(errDelete) fmt.Println(errDelete)
} }
@ -314,7 +317,7 @@ func (service serviceSend) SendVideo(ctx context.Context, request domainSend.Vid
} }
func (service serviceSend) SendContact(ctx context.Context, request domainSend.ContactRequest) (response domainSend.ContactResponse, err error) { func (service serviceSend) SendContact(ctx context.Context, request domainSend.ContactRequest) (response domainSend.ContactResponse, err error) {
err = validations.ValidateSendContact(request)
err = validations.ValidateSendContact(ctx, request)
if err != nil { if err != nil {
return response, err return response, err
} }
@ -341,7 +344,7 @@ func (service serviceSend) SendContact(ctx context.Context, request domainSend.C
} }
func (service serviceSend) SendLink(ctx context.Context, request domainSend.LinkRequest) (response domainSend.LinkResponse, err error) { func (service serviceSend) SendLink(ctx context.Context, request domainSend.LinkRequest) (response domainSend.LinkResponse, err error) {
err = validations.ValidateSendLink(request)
err = validations.ValidateSendLink(ctx, request)
if err != nil { if err != nil {
return response, err return response, err
} }
@ -372,8 +375,38 @@ func (service serviceSend) SendLink(ctx context.Context, request domainSend.Link
return response, nil return response, nil
} }
func (service serviceSend) Revoke(_ context.Context, request domainSend.RevokeRequest) (response domainSend.RevokeResponse, err error) {
err = validations.ValidateRevokeMessage(request)
func (service serviceSend) SendLocation(ctx context.Context, request domainSend.LocationRequest) (response domainSend.LocationResponse, err error) {
err = validations.ValidateSendLocation(ctx, request)
if err != nil {
return response, err
}
dataWaRecipient, err := whatsapp.ValidateJidWithLogin(service.WaCli, request.Phone)
if err != nil {
return response, err
}
// Compose WhatsApp Proto
msgId := whatsmeow.GenerateMessageID()
msg := &waProto.Message{
LocationMessage: &waProto.LocationMessage{
DegreesLatitude: proto.Float64(utils.StrToFloat64(request.Latitude)),
DegreesLongitude: proto.Float64(utils.StrToFloat64(request.Longitude)),
},
}
// Send WhatsApp Message Proto
ts, err := service.WaCli.SendMessage(ctx, dataWaRecipient, msgId, msg)
if err != nil {
return response, err
}
response.MessageID = msgId
response.Status = fmt.Sprintf("Send location success %s (server timestamp: %s)", request.Phone, ts)
return response, nil
}
func (service serviceSend) Revoke(ctx context.Context, request domainSend.RevokeRequest) (response domainSend.RevokeResponse, err error) {
err = validations.ValidateRevokeMessage(ctx, request)
if err != nil { if err != nil {
return response, err return response, err
} }
@ -394,7 +427,7 @@ func (service serviceSend) Revoke(_ context.Context, request domainSend.RevokeRe
} }
func (service serviceSend) UpdateMessage(ctx context.Context, request domainSend.UpdateMessageRequest) (response domainSend.UpdateMessageResponse, err error) { func (service serviceSend) UpdateMessage(ctx context.Context, request domainSend.UpdateMessageRequest) (response domainSend.UpdateMessageResponse, err error) {
err = validations.ValidateUpdateMessage(request)
err = validations.ValidateUpdateMessage(ctx, request)
if err != nil { if err != nil {
return response, err return response, err
} }

10
src/services/user.go

@ -21,8 +21,8 @@ func NewUserService(waCli *whatsmeow.Client) domainUser.IUserService {
} }
} }
func (service userService) Info(_ context.Context, request domainUser.InfoRequest) (response domainUser.InfoResponse, err error) {
err = validations.ValidateUserInfo(request)
func (service userService) Info(ctx context.Context, request domainUser.InfoRequest) (response domainUser.InfoResponse, err error) {
err = validations.ValidateUserInfo(ctx, request)
if err != nil { if err != nil {
return response, err return response, err
} }
@ -64,8 +64,8 @@ func (service userService) Info(_ context.Context, request domainUser.InfoReques
return response, nil return response, nil
} }
func (service userService) Avatar(_ context.Context, request domainUser.AvatarRequest) (response domainUser.AvatarResponse, err error) {
err = validations.ValidateUserAvatar(request)
func (service userService) Avatar(ctx context.Context, request domainUser.AvatarRequest) (response domainUser.AvatarResponse, err error) {
err = validations.ValidateUserAvatar(ctx, request)
if err != nil { if err != nil {
return response, err return response, err
} }
@ -106,7 +106,7 @@ func (service userService) MyListGroups(_ context.Context) (response domainUser.
func (service userService) MyPrivacySetting(_ context.Context) (response domainUser.MyPrivacySettingResponse, err error) { func (service userService) MyPrivacySetting(_ context.Context) (response domainUser.MyPrivacySettingResponse, err error) {
whatsapp.MustLogin(service.WaCli) whatsapp.MustLogin(service.WaCli)
resp, err := service.WaCli.TryFetchPrivacySettings(false)
resp, err := service.WaCli.TryFetchPrivacySettings(true)
if err != nil { if err != nil {
return return
} }

47
src/validations/send_validation.go

@ -1,6 +1,7 @@
package validations package validations
import ( import (
"context"
"fmt" "fmt"
"github.com/aldinokemal/go-whatsapp-web-multidevice/config" "github.com/aldinokemal/go-whatsapp-web-multidevice/config"
domainSend "github.com/aldinokemal/go-whatsapp-web-multidevice/domains/send" domainSend "github.com/aldinokemal/go-whatsapp-web-multidevice/domains/send"
@ -10,8 +11,8 @@ import (
"github.com/go-ozzo/ozzo-validation/v4/is" "github.com/go-ozzo/ozzo-validation/v4/is"
) )
func ValidateSendMessage(request domainSend.MessageRequest) error {
err := validation.ValidateStruct(&request,
func ValidateSendMessage(ctx context.Context, request domainSend.MessageRequest) error {
err := validation.ValidateStructWithContext(ctx, &request,
validation.Field(&request.Phone, validation.Required), validation.Field(&request.Phone, validation.Required),
validation.Field(&request.Message, validation.Required), validation.Field(&request.Message, validation.Required),
) )
@ -22,8 +23,8 @@ func ValidateSendMessage(request domainSend.MessageRequest) error {
return nil return nil
} }
func ValidateSendImage(request domainSend.ImageRequest) error {
err := validation.ValidateStruct(&request,
func ValidateSendImage(ctx context.Context, request domainSend.ImageRequest) error {
err := validation.ValidateStructWithContext(ctx, &request,
validation.Field(&request.Phone, validation.Required), validation.Field(&request.Phone, validation.Required),
validation.Field(&request.Image, validation.Required), validation.Field(&request.Image, validation.Required),
) )
@ -45,8 +46,8 @@ func ValidateSendImage(request domainSend.ImageRequest) error {
return nil return nil
} }
func ValidateSendFile(request domainSend.FileRequest) error {
err := validation.ValidateStruct(&request,
func ValidateSendFile(ctx context.Context, request domainSend.FileRequest) error {
err := validation.ValidateStructWithContext(ctx, &request,
validation.Field(&request.Phone, validation.Required), validation.Field(&request.Phone, validation.Required),
validation.Field(&request.File, validation.Required), validation.Field(&request.File, validation.Required),
) )
@ -63,8 +64,8 @@ func ValidateSendFile(request domainSend.FileRequest) error {
return nil return nil
} }
func ValidateSendVideo(request domainSend.VideoRequest) error {
err := validation.ValidateStruct(&request,
func ValidateSendVideo(ctx context.Context, request domainSend.VideoRequest) error {
err := validation.ValidateStructWithContext(ctx, &request,
validation.Field(&request.Phone, validation.Required), validation.Field(&request.Phone, validation.Required),
validation.Field(&request.Video, validation.Required), validation.Field(&request.Video, validation.Required),
) )
@ -91,8 +92,8 @@ func ValidateSendVideo(request domainSend.VideoRequest) error {
return nil return nil
} }
func ValidateSendContact(request domainSend.ContactRequest) error {
err := validation.ValidateStruct(&request,
func ValidateSendContact(ctx context.Context, request domainSend.ContactRequest) error {
err := validation.ValidateStructWithContext(ctx, &request,
validation.Field(&request.Phone, validation.Required), validation.Field(&request.Phone, validation.Required),
validation.Field(&request.ContactPhone, validation.Required), validation.Field(&request.ContactPhone, validation.Required),
validation.Field(&request.ContactName, validation.Required), validation.Field(&request.ContactName, validation.Required),
@ -105,8 +106,8 @@ func ValidateSendContact(request domainSend.ContactRequest) error {
return nil return nil
} }
func ValidateSendLink(request domainSend.LinkRequest) error {
err := validation.ValidateStruct(&request,
func ValidateSendLink(ctx context.Context, request domainSend.LinkRequest) error {
err := validation.ValidateStructWithContext(ctx, &request,
validation.Field(&request.Phone, validation.Required), validation.Field(&request.Phone, validation.Required),
validation.Field(&request.Link, validation.Required, is.URL), validation.Field(&request.Link, validation.Required, is.URL),
validation.Field(&request.Caption, validation.Required), validation.Field(&request.Caption, validation.Required),
@ -119,8 +120,22 @@ func ValidateSendLink(request domainSend.LinkRequest) error {
return nil return nil
} }
func ValidateRevokeMessage(request domainSend.RevokeRequest) error {
err := validation.ValidateStruct(&request,
func ValidateSendLocation(ctx context.Context, request domainSend.LocationRequest) error {
err := validation.ValidateStructWithContext(ctx, &request,
validation.Field(&request.Phone, validation.Required),
validation.Field(&request.Latitude, validation.Required, is.Latitude),
validation.Field(&request.Longitude, validation.Required, is.Longitude),
)
if err != nil {
return pkgError.ValidationError(err.Error())
}
return nil
}
func ValidateRevokeMessage(ctx context.Context, request domainSend.RevokeRequest) error {
err := validation.ValidateStructWithContext(ctx, &request,
validation.Field(&request.Phone, validation.Required), validation.Field(&request.Phone, validation.Required),
validation.Field(&request.MessageID, validation.Required), validation.Field(&request.MessageID, validation.Required),
) )
@ -132,8 +147,8 @@ func ValidateRevokeMessage(request domainSend.RevokeRequest) error {
return nil return nil
} }
func ValidateUpdateMessage(request domainSend.UpdateMessageRequest) error {
err := validation.ValidateStruct(&request,
func ValidateUpdateMessage(ctx context.Context, request domainSend.UpdateMessageRequest) error {
err := validation.ValidateStructWithContext(ctx, &request,
validation.Field(&request.Phone, validation.Required), validation.Field(&request.Phone, validation.Required),
validation.Field(&request.MessageID, validation.Required), validation.Field(&request.MessageID, validation.Required),
validation.Field(&request.Message, validation.Required), validation.Field(&request.Message, validation.Required),

90
src/validations/send_validation_test.go

@ -1,6 +1,7 @@
package validations package validations
import ( import (
"context"
domainSend "github.com/aldinokemal/go-whatsapp-web-multidevice/domains/send" domainSend "github.com/aldinokemal/go-whatsapp-web-multidevice/domains/send"
pkgError "github.com/aldinokemal/go-whatsapp-web-multidevice/pkg/error" pkgError "github.com/aldinokemal/go-whatsapp-web-multidevice/pkg/error"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -45,7 +46,7 @@ func TestValidateSendMessage(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
err := ValidateSendMessage(tt.args.request)
err := ValidateSendMessage(context.Background(), tt.args.request)
assert.Equal(t, tt.err, err) assert.Equal(t, tt.err, err)
}) })
} }
@ -107,7 +108,7 @@ func TestValidateSendImage(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
err := ValidateSendImage(tt.args.request)
err := ValidateSendImage(context.Background(), tt.args.request)
assert.Equal(t, tt.err, err) assert.Equal(t, tt.err, err)
}) })
} }
@ -156,7 +157,7 @@ func TestValidateSendFile(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
err := ValidateSendFile(tt.args.request)
err := ValidateSendFile(context.Background(), tt.args.request)
assert.Equal(t, tt.err, err) assert.Equal(t, tt.err, err)
}) })
} }
@ -231,7 +232,7 @@ func TestValidateSendVideo(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
err := ValidateSendVideo(tt.args.request)
err := ValidateSendVideo(context.Background(), tt.args.request)
assert.Equal(t, tt.err, err) assert.Equal(t, tt.err, err)
}) })
} }
@ -295,7 +296,7 @@ func TestValidateSendLink(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
err := ValidateSendLink(tt.args.request)
err := ValidateSendLink(context.Background(), tt.args.request)
assert.Equal(t, tt.err, err) assert.Equal(t, tt.err, err)
}) })
} }
@ -338,7 +339,7 @@ func TestValidateRevokeMessage(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
err := ValidateRevokeMessage(tt.args.request)
err := ValidateRevokeMessage(context.Background(), tt.args.request)
assert.Equal(t, tt.err, err) assert.Equal(t, tt.err, err)
}) })
} }
@ -393,7 +394,7 @@ func TestValidateUpdateMessage(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
err := ValidateUpdateMessage(tt.args.request)
err := ValidateUpdateMessage(context.Background(), tt.args.request)
assert.Equal(t, tt.err, err) assert.Equal(t, tt.err, err)
}) })
} }
@ -448,7 +449,80 @@ func TestValidateSendContact(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
err := ValidateSendContact(tt.args.request)
err := ValidateSendContact(context.Background(), tt.args.request)
assert.Equal(t, tt.err, err)
})
}
}
func TestValidateSendLocation(t *testing.T) {
type args struct {
request domainSend.LocationRequest
}
tests := []struct {
name string
args args
err any
}{
{
name: "should success normal condition",
args: args{request: domainSend.LocationRequest{
Phone: "1728937129312@s.whatsapp.net",
Latitude: "-7.797068",
Longitude: "110.370529",
}},
err: nil,
},
{
name: "should error with empty phone",
args: args{request: domainSend.LocationRequest{
Phone: "",
Latitude: "-7.797068",
Longitude: "110.370529",
}},
err: pkgError.ValidationError("phone: cannot be blank."),
},
{
name: "should error with empty latitude",
args: args{request: domainSend.LocationRequest{
Phone: "1728937129312@s.whatsapp.net",
Latitude: "",
Longitude: "110.370529",
}},
err: pkgError.ValidationError("latitude: cannot be blank."),
},
{
name: "should error with empty longitude",
args: args{request: domainSend.LocationRequest{
Phone: "1728937129312@s.whatsapp.net",
Latitude: "-7.797068",
Longitude: "",
}},
err: pkgError.ValidationError("longitude: cannot be blank."),
},
{
name: "should error with invalid latitude",
args: args{request: domainSend.LocationRequest{
Phone: "1728937129312@s.whatsapp.net",
Latitude: "ABCDEF",
Longitude: "110.370529",
}},
err: pkgError.ValidationError("latitude: must be a valid latitude."),
},
{
name: "should error with invalid latitude",
args: args{request: domainSend.LocationRequest{
Phone: "1728937129312@s.whatsapp.net",
Latitude: "-7.797068",
Longitude: "ABCDEF",
}},
err: pkgError.ValidationError("longitude: must be a valid longitude."),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := ValidateSendLocation(context.Background(), tt.args.request)
assert.Equal(t, tt.err, err) assert.Equal(t, tt.err, err)
}) })
} }

9
src/validations/user_validation.go

@ -1,13 +1,14 @@
package validations package validations
import ( import (
"context"
domainUser "github.com/aldinokemal/go-whatsapp-web-multidevice/domains/user" domainUser "github.com/aldinokemal/go-whatsapp-web-multidevice/domains/user"
pkgError "github.com/aldinokemal/go-whatsapp-web-multidevice/pkg/error" pkgError "github.com/aldinokemal/go-whatsapp-web-multidevice/pkg/error"
validation "github.com/go-ozzo/ozzo-validation/v4" validation "github.com/go-ozzo/ozzo-validation/v4"
) )
func ValidateUserInfo(request domainUser.InfoRequest) error {
err := validation.ValidateStruct(&request,
func ValidateUserInfo(ctx context.Context, request domainUser.InfoRequest) error {
err := validation.ValidateStructWithContext(ctx, &request,
validation.Field(&request.Phone, validation.Required), validation.Field(&request.Phone, validation.Required),
) )
@ -17,8 +18,8 @@ func ValidateUserInfo(request domainUser.InfoRequest) error {
return nil return nil
} }
func ValidateUserAvatar(request domainUser.AvatarRequest) error {
err := validation.ValidateStruct(&request,
func ValidateUserAvatar(ctx context.Context, request domainUser.AvatarRequest) error {
err := validation.ValidateStructWithContext(ctx, &request,
validation.Field(&request.Phone, validation.Required), validation.Field(&request.Phone, validation.Required),
) )

5
src/validations/user_validation_test.go

@ -1,6 +1,7 @@
package validations package validations
import ( import (
"context"
domainUser "github.com/aldinokemal/go-whatsapp-web-multidevice/domains/user" domainUser "github.com/aldinokemal/go-whatsapp-web-multidevice/domains/user"
pkgError "github.com/aldinokemal/go-whatsapp-web-multidevice/pkg/error" pkgError "github.com/aldinokemal/go-whatsapp-web-multidevice/pkg/error"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -34,7 +35,7 @@ func TestValidateUserAvatar(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
err := ValidateUserAvatar(tt.args.request)
err := ValidateUserAvatar(context.Background(), tt.args.request)
assert.Equal(t, tt.err, err) assert.Equal(t, tt.err, err)
}) })
} }
@ -67,7 +68,7 @@ func TestValidateUserInfo(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
err := ValidateUserInfo(tt.args.request)
err := ValidateUserInfo(context.Background(), tt.args.request)
assert.Equal(t, tt.err, err) assert.Equal(t, tt.err, err)
}) })
} }

130
src/views/index.html

@ -124,6 +124,15 @@
</div> </div>
</div> </div>
</div> </div>
<div class="blue card" @click="sendLocationModal()" style="cursor: pointer">
<div class="content">
<a class="ui blue right ribbon label">Send</a>
<div class="header">Send Location</div>
<div class="description">
Send location to any whatsapp number
</div>
</div>
</div>
<div class="red card" @click="sendRevokeModal()" style="cursor: pointer"> <div class="red card" @click="sendRevokeModal()" style="cursor: pointer">
<div class="content"> <div class="content">
<a class="ui red right ribbon label">Revoke</a> <a class="ui red right ribbon label">Revoke</a>
@ -274,7 +283,7 @@
<div class="field"> <div class="field">
<label>Compress</label> <label>Compress</label>
<div class="ui toggle checkbox"> <div class="ui toggle checkbox">
<input type="checkbox" aria-label="view once" v-model="image_compress">
<input type="checkbox" aria-label="compress" v-model="image_compress">
<label>Check for compressing image to smaller size</label> <label>Check for compressing image to smaller size</label>
</div> </div>
</div> </div>
@ -379,7 +388,7 @@
<div class="field"> <div class="field">
<label>Compress</label> <label>Compress</label>
<div class="ui toggle checkbox"> <div class="ui toggle checkbox">
<input type="checkbox" aria-label="view once" v-model="video_compress">
<input type="checkbox" aria-label="compress" v-model="video_compress">
<label>Check for compressing image to smaller size</label> <label>Check for compressing image to smaller size</label>
</div> </div>
</div> </div>
@ -426,12 +435,12 @@
<div class="field"> <div class="field">
<label>Contact Name</label> <label>Contact Name</label>
<input v-model="contact_card_name" type="text" placeholder="Please enter contact name" <input v-model="contact_card_name" type="text" placeholder="Please enter contact name"
aria-label="card_name">
aria-label="contact name">
</div> </div>
<div class="field"> <div class="field">
<label>Contact Phone</label> <label>Contact Phone</label>
<input v-model="contact_card_phone" type="text" placeholder="Please enter contact phone" <input v-model="contact_card_phone" type="text" placeholder="Please enter contact phone"
aria-label="card_phone">
aria-label="contact phone">
</div> </div>
</form> </form>
</div> </div>
@ -444,6 +453,48 @@
</div> </div>
</div> </div>
<!-- Modal SendLocation -->
<div class="ui small modal" id="modalSendLocation">
<i class="close icon"></i>
<div class="header">
Send Location
</div>
<div class="content">
<form class="ui form">
<div class="field">
<label>Type</label>
<select name="location_type" v-model="location_type" aria-label="type">
<option value="group">Group Message</option>
<option value="user">Private Message</option>
</select>
</div>
<div class="field">
<label>Phone / Group ID</label>
<input v-model="location_phone" type="text" placeholder="6289..."
aria-label="phone">
<input :value="location_phone_id" disabled aria-label="whatsapp_id">
</div>
<div class="field">
<label>Location Latitude</label>
<input v-model="location_latitude" type="text" placeholder="Please enter latitude"
aria-label="latitude">
</div>
<div class="field">
<label>Location Longitude</label>
<input v-model="location_longitude" type="text" placeholder="Please enter longitude"
aria-label="longitude">
</div>
</form>
</div>
<div class="actions">
<div class="ui approve positive right labeled icon button" :class="{'loading': this.location_loading}"
@click="sendLocationProcess">
Send
<i class="send icon"></i>
</div>
</div>
</div>
<!-- Modal SendRevoke --> <!-- Modal SendRevoke -->
<div class="ui small modal" id="modalSendRevoke"> <div class="ui small modal" id="modalSendRevoke">
<i class="close icon"></i> <i class="close icon"></i>
@ -468,7 +519,7 @@
<div class="field"> <div class="field">
<label>Revoke Message ID</label> <label>Revoke Message ID</label>
<input v-model="revoke_message_id" type="text" placeholder="Please enter your message id" <input v-model="revoke_message_id" type="text" placeholder="Please enter your message id"
aria-label="card_phone">
aria-label="message id">
</div> </div>
</form> </form>
</div> </div>
@ -1112,6 +1163,69 @@
} }
} }
const sendLocation = {
data() {
return {
location_type: 'user',
location_phone: '',
location_latitude: '',
location_longitude: '',
location_loading: false,
}
},
computed: {
location_phone_id() {
return this.location_type === 'user' ? `${this.location_phone}@${this.type_user}` : `${this.location_phone}@${this.type_group}`
}
},
methods: {
sendLocationModal() {
$('#modalSendLocation').modal({
onApprove: function () {
return false;
}
}).modal('show');
},
async sendLocationProcess() {
try {
let response = await this.sendLocationApi()
showSuccessInfo(response)
$('#modalSendLocation').modal('hide');
} catch (err) {
showErrorInfo(err)
}
},
sendLocationApi() {
return new Promise(async (resolve, reject) => {
try {
this.location_loading = true;
let payload = new FormData();
payload.append("phone", this.location_phone_id)
payload.append("latitude", this.location_latitude)
payload.append("longitude", this.location_longitude)
let response = await http.post(`/send/location`, payload)
this.sendLocationReset();
resolve(response.data.message)
} catch (error) {
if (error.response) {
reject(error.response.data.message)
} else {
reject(error.message)
}
} finally {
this.location_loading = false;
}
})
},
sendLocationReset() {
this.location_phone = '';
this.location_latitude = '';
this.location_longitude = '';
this.location_type = 'user';
},
}
}
const userGroups = { const userGroups = {
data() { data() {
return { return {
@ -1352,7 +1466,11 @@
return tanggal.format('LLL'); return tanggal.format('LLL');
} }
}, },
mixins: [login, logout, reconnect, sendMessage, sendImage, sendFile, sendVideo, sendContact, sendRevoke, userGroups, userPrivacy, userAvatar, userInfo]
mixins: [
login, logout, reconnect,
sendMessage, sendImage, sendFile, sendVideo, sendContact, sendRevoke, sendLocation,
userGroups, userPrivacy, userAvatar, userInfo
]
}).mount('#app') }).mount('#app')
</script> </script>
</body> </body>
Loading…
Cancel
Save