Browse Source

feat: redesign UI 4.0 (#36)

* feat: remove send group

* feat: remove type in send payload

* feat: add loading when submit

* feat: update docs

* chore: update docs

* fix: update UI

* feat: add more intuitive UI

* chore: update docs
pull/38/head v4.0.0
Aldino Kemal 3 years ago
committed by GitHub
parent
commit
c4328a316e
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 122
      docs/openapi.yaml
  2. 65
      readme.md
  3. 2
      src/cmd/root.go
  4. 2
      src/config/settings.go
  5. 4
      src/domains/app/devices.go
  6. 1
      src/domains/send/contact.go
  7. 1
      src/domains/send/file.go
  8. 1
      src/domains/send/image.go
  9. 1
      src/domains/send/link.go
  10. 2
      src/domains/send/message.go
  11. 5
      src/domains/send/send.go
  12. 1
      src/domains/send/text.go
  13. 1
      src/domains/send/video.go
  14. 2
      src/internal/rest/app_rest.go
  15. 39
      src/internal/rest/send_rest.go
  16. 4
      src/internal/rest/user_rest.go
  17. 15
      src/internal/websocket/websocket.go
  18. 2
      src/services/app_service.go
  19. 2
      src/utils/errors.go
  20. 6
      src/utils/response.go
  21. 7
      src/utils/whatsapp.go
  22. 17
      src/validations/send_validation.go
  23. 4
      src/validations/send_validation_test.go
  24. 5
      src/validations/user_validation.go
  25. 436
      src/views/index.html

122
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: 2.5.0
version: 3.0.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
@ -10,8 +10,8 @@ tags:
description: Initial Connection to Whatsapp server description: Initial Connection to Whatsapp server
- name: user - name: user
description: Getting information description: Getting information
- name: send
description: Send Message (Text/Image/File/Video)
- name: message
description: Send or Manipulate Message (Text/Image/File/Video).
paths: paths:
/app/login: /app/login:
get: get:
@ -118,7 +118,7 @@ paths:
in: query in: query
schema: schema:
type: integer type: integer
example: '6289685024091'
example: '6289685028129@s.whatsapp.net'
responses: responses:
'200': '200':
description: OK description: OK
@ -155,17 +155,17 @@ paths:
status: you are blocked status: you are blocked
picture_id: '1635239861' picture_id: '1635239861'
devices: devices:
- User: '6289685024091'
- User: '6289685028129@s.whatsapp.net'
Agent: 0 Agent: 0
Device: UNKNOWN Device: UNKNOWN
Server: s.whatsapp.net Server: s.whatsapp.net
AD: true AD: true
- User: '6289685024091'
- User: '6289685028129@s.whatsapp.net'
Agent: 0 Agent: 0
Device: SAFARI Device: SAFARI
Server: s.whatsapp.net Server: s.whatsapp.net
AD: true AD: true
- User: '6289685024091'
- User: '6289685028129@s.whatsapp.net'
Agent: 0 Agent: 0
Device: IPAD Device: IPAD
Server: s.whatsapp.net Server: s.whatsapp.net
@ -193,7 +193,7 @@ paths:
in: query in: query
schema: schema:
type: integer type: integer
example: '6289685024091'
example: '6289685028129@s.whatsapp.net'
responses: responses:
'200': '200':
description: OK description: OK
@ -300,7 +300,7 @@ paths:
post: post:
operationId: sendMessage operationId: sendMessage
tags: tags:
- send
- message
summary: Send Message summary: Send Message
requestBody: requestBody:
content: content:
@ -310,14 +310,12 @@ paths:
properties: properties:
phone: phone:
type: integer type: integer
example: '6289685024091'
example: '6289685028129@s.whatsapp.net'
description: Phone number with country code
message: message:
type: string type: string
example: selamat malam example: selamat malam
type:
type: string
example: 'user'
description: 'user/group | default: user'
description: Message to send
responses: responses:
'200': '200':
description: OK description: OK
@ -341,7 +339,7 @@ paths:
post: post:
operationId: sendImage operationId: sendImage
tags: tags:
- send
- message
summary: Send Image summary: Send Image
requestBody: requestBody:
content: content:
@ -351,23 +349,24 @@ paths:
properties: properties:
phone: phone:
type: integer type: integer
example: '6289685024091'
example: '6289685028129@s.whatsapp.net'
description: Phone number with country code
caption: caption:
type: string type: string
example: selamat malam example: selamat malam
description: Caption to send
view_once: view_once:
type: boolean type: boolean
example: false example: false
description: View once
image: image:
type: string type: string
format: binary format: binary
type:
type: string
example: 'user'
description: 'user/group | default: user'
description: Image to send
compress: compress:
type: boolean type: boolean
example: false example: false
description: Compress image
responses: responses:
'200': '200':
description: OK description: OK
@ -391,7 +390,7 @@ paths:
post: post:
operationId: sendFile operationId: sendFile
tags: tags:
- send
- message
summary: Send File summary: Send File
requestBody: requestBody:
content: content:
@ -401,17 +400,16 @@ paths:
properties: properties:
phone: phone:
type: integer type: integer
example: '6289685024091'
example: '6289685028129@s.whatsapp.net'
description: Phone number with country code
caption: caption:
type: string type: string
example: selamat malam example: selamat malam
description: Caption to send
file: file:
type: string type: string
format: binary format: binary
type:
type: string
example: 'user'
description: 'user/group | default: user'
description: File to send
responses: responses:
'200': '200':
description: OK description: OK
@ -435,7 +433,7 @@ paths:
post: post:
operationId: sendVideo operationId: sendVideo
tags: tags:
- send
- message
summary: Send Video summary: Send Video
requestBody: requestBody:
content: content:
@ -445,23 +443,24 @@ paths:
properties: properties:
phone: phone:
type: integer type: integer
example: '6289685024091'
example: '6289685028129@s.whatsapp.net'
description: Phone number with country code
caption: caption:
type: string type: string
example: ini contoh caption video example: ini contoh caption video
description: Caption to send
view_once: view_once:
type: boolean type: boolean
example: 'false' example: 'false'
description: View once
video: video:
type: string type: string
format: binary format: binary
type:
type: string
example: 'user'
description: 'user/group | default: user'
description: Video to send
compress: compress:
type: boolean type: boolean
example: 'false' example: 'false'
description: Compress video
responses: responses:
'200': '200':
description: OK description: OK
@ -485,7 +484,7 @@ paths:
post: post:
operationId: sendContact operationId: sendContact
tags: tags:
- send
- message
summary: Send Contact summary: Send Contact
requestBody: requestBody:
content: content:
@ -495,17 +494,16 @@ paths:
properties: properties:
phone: phone:
type: integer type: integer
example: '6289685024051'
example: '6289685024051@s.whatsapp.net'
description: Phone number with country code
contact_name: contact_name:
type: string type: string
example: Aldino Kemal example: Aldino Kemal
description: Contact name
contact_phone: contact_phone:
type: string type: string
example: '6289685024992' example: '6289685024992'
type:
type: string
example: 'user'
description: 'user/group | default: user'
description: Contact phone number
responses: responses:
'200': '200':
description: OK description: OK
@ -529,7 +527,7 @@ paths:
post: post:
operationId: sendLink operationId: sendLink
tags: tags:
- send
- message
summary: Send Link summary: Send Link
requestBody: requestBody:
content: content:
@ -539,17 +537,55 @@ paths:
properties: properties:
phone: phone:
type: integer type: integer
example: '6289685024051'
example: '6289685024051@s.whatsapp.net'
description: Phone number with country code
link: link:
type: string type: string
example: "https://google.com" example: "https://google.com"
description: Link to send
caption: caption:
type: string type: string
example: 'Halo ini contoh caption' example: 'Halo ini contoh caption'
type:
description: Caption to send
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:
post:
operationId: revokeMessage
tags:
- message
summary: Send Link
requestBody:
content:
multipart/form-data:
schema:
type: object
properties:
phone:
type: integer
example: '6289685024051@s.whatsapp.net'
description: Phone number with country code
message_id:
type: string type: string
example: 'user'
description: 'user/group | default: user'
example: "3EB007D87686670344D7"
description: Message ID
responses: responses:
'200': '200':
description: OK description: OK

65
readme.md

@ -1,6 +1,8 @@
## Go Whatsapp API Multi Device Version ## 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) [![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 version](https://img.shields.io/github/v/release/aldinokemal/go-whatsapp-web-multidevice "release version")](https://github.com/aldinokemal/go-whatsapp-web-multidevice/releases)
<br>
[![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 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) [![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)
[![release macos](https://github.com/aldinokemal/go-whatsapp-web-multidevice/actions/workflows/deploy-mac.yml/badge.svg "release macos")](https://github.com/aldinokemal/go-whatsapp-web-multidevice/actions/workflows/deploy-mac.yml) [![release macos](https://github.com/aldinokemal/go-whatsapp-web-multidevice/actions/workflows/deploy-mac.yml/badge.svg "release macos")](https://github.com/aldinokemal/go-whatsapp-web-multidevice/actions/workflows/deploy-mac.yml)
@ -86,23 +88,25 @@ You can fork or edit this source code !
### Current API ### Current API
You can check [docs/openapi.yml](./docs/openapi.yaml) for detail API
| Feature | Menu | Method | URL |
|---------|-------------------------|--------|------------------|
| ✅ | Login | GET | /app/login |
| ✅ | Logout | GET | /app/logout |
| ✅ | Reconnect | GET | /app/reconnect |
| ✅ | User Info | GET | /user/info |
| ✅ | User Avatar | GET | /user/avatar |
| ✅ | User My Group List | GET | /user/my/groups |
| ✅ | User My Privacy Setting | GET | /user/my/privacy |
| ✅ | Send Message | POST | /send/message |
| ✅ | Send Image | POST | /send/image |
| ✅ | Send File | POST | /send/file |
| ✅ | Send Video | POST | /send/video |
| ✅ | Send Contact | POST | /send/contact |
| ✅ | Send Link | POST | /send/link |
You can check [docs/openapi.yml](./docs/openapi.yaml) for detail API, furthermore you can generate HTTP Client from this
API using [openapi-generator](https://openapi-generator.tech/#try)
| Feature | Menu | Method | URL |
|---------|-------------------------|--------|-----------------------------|
| ✅ | Login | GET | /app/login |
| ✅ | Logout | GET | /app/logout |
| ✅ | Reconnect | GET | /app/reconnect |
| ✅ | User Info | GET | /user/info |
| ✅ | User Avatar | GET | /user/avatar |
| ✅ | User My Group List | GET | /user/my/groups |
| ✅ | User My Privacy Setting | GET | /user/my/privacy |
| ✅ | Send Message | POST | /send/message |
| ✅ | Send Image | POST | /send/image |
| ✅ | Send File | POST | /send/file |
| ✅ | Send Video | POST | /send/video |
| ✅ | Send Contact | POST | /send/contact |
| ✅ | Send Link | POST | /send/link |
| ✅ | Revoke Messave | POST | /message/:message_id/revoke |
``` ```
✅ = Available ✅ = Available
@ -111,19 +115,20 @@ You can check [docs/openapi.yml](./docs/openapi.yaml) for detail API
### App User Interface ### App User Interface
1. Homepage ![Homepage](https://i.ibb.co/xjZ7KLr/Screenshot-2022-11-20-at-20-54-52.png)
2. Login ![Login](https://i.ibb.co/Yp3YJKM/Screen-Shot-2022-02-13-at-12-55-54.png)
3. Send Message ![Send Message](https://i.ibb.co/Bng57Ry/Screen-Shot-2022-05-22-at-13-51-13.png)
4. Send Image ![Send Image](https://i.ibb.co/gJj3SrQ/Screen-Shot-2022-05-22-at-13-49-21.png)
5. Send File ![Send File](https://i.ibb.co/nCwhysd/Screen-Shot-2022-05-22-at-13-43-16.png)
6. Send Video ![Send File](https://i.ibb.co/yBXsWXX/Screen-Shot-2022-05-22-at-13-43-24.png)
7. Send Contact ![Send File](https://i.ibb.co/fqwwGK5/Screen-Shot-2022-07-07-at-07-59-26.png)
8. User Info ![User Info](https://i.ibb.co/BC0mNT7/Screen-Shot-2022-02-13-at-13-00-57.png)
9. User Avatar ![User Avatar](https://i.ibb.co/TkzPbLZ/Screen-Shot-2022-02-13-at-13-01-39.png)
10. User Privacy ![User My Privacy](https://i.ibb.co/RQcC5m9/Screen-Shot-2022-02-13-at-12-58-47.png)
11. User Group ![List Group](https://i.ibb.co/jfkgKdG/Screen-Shot-2022-05-12-at-21-12-06.png)
12. Auto Reply ![Auto Reply](https://i.ibb.co/D4rTytX/IMG-20220517-162500.jpg)
13. Basic Auth Prompt ![Basic Auth](https://i.ibb.co/PDjQ92W/Screenshot-2022-11-06-at-14-06-29.png)
1. Homepage ![Homepage](https://i.ibb.co/NNX2wWY/home.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)
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)
### Mac OS NOTE ### Mac OS NOTE

2
src/cmd/root.go

@ -113,7 +113,7 @@ func runRest(_ *cobra.Command, _ []string) {
}) })
}) })
websocket.RegisterRoutes(app)
websocket.RegisterRoutes(app, appService)
go websocket.RunHub() go websocket.RunHub()
// Set auto reconnect to whatsapp server after booting // Set auto reconnect to whatsapp server after booting

2
src/config/settings.go

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

4
src/domains/app/devices.go

@ -1,6 +1,6 @@
package app package app
type FetchDevicesResponse struct { type FetchDevicesResponse struct {
Name string
Device string
Name string `json:"name"`
Device string `json:"device"`
} }

1
src/domains/send/contact.go

@ -4,7 +4,6 @@ type ContactRequest struct {
Phone string `json:"phone" form:"phone"` Phone string `json:"phone" form:"phone"`
ContactName string `json:"contact_name" form:"contact_name"` ContactName string `json:"contact_name" form:"contact_name"`
ContactPhone string `json:"contact_phone" form:"contact_phone"` ContactPhone string `json:"contact_phone" form:"contact_phone"`
Type Type `json:"type" form:"type"`
} }
type ContactResponse struct { type ContactResponse struct {

1
src/domains/send/file.go

@ -5,7 +5,6 @@ import "mime/multipart"
type FileRequest struct { type FileRequest struct {
Phone string `json:"phone" form:"phone"` Phone string `json:"phone" form:"phone"`
File *multipart.FileHeader `json:"file" form:"file"` File *multipart.FileHeader `json:"file" form:"file"`
Type Type `json:"type" form:"type"`
} }
type FileResponse struct { type FileResponse struct {

1
src/domains/send/image.go

@ -7,7 +7,6 @@ type ImageRequest struct {
Caption string `json:"caption" form:"caption"` Caption string `json:"caption" form:"caption"`
Image *multipart.FileHeader `json:"image" form:"image"` Image *multipart.FileHeader `json:"image" form:"image"`
ViewOnce bool `json:"view_once" form:"view_once"` ViewOnce bool `json:"view_once" form:"view_once"`
Type Type `json:"type" form:"type"`
Compress bool `json:"compress"` Compress bool `json:"compress"`
} }

1
src/domains/send/link.go

@ -4,7 +4,6 @@ type LinkRequest struct {
Phone string `json:"phone" form:"phone"` Phone string `json:"phone" form:"phone"`
Caption string `json:"caption"` Caption string `json:"caption"`
Link string `json:"link"` Link string `json:"link"`
Type Type `json:"type" form:"type"`
} }
type LinkResponse struct { type LinkResponse struct {

2
src/domains/send/message.go

@ -3,7 +3,6 @@ package send
type RevokeRequest struct { type RevokeRequest struct {
MessageID string `json:"message_id" uri:"message_id"` MessageID string `json:"message_id" uri:"message_id"`
Phone string `json:"phone" form:"phone"` Phone string `json:"phone" form:"phone"`
Type Type `json:"type" form:"type"`
} }
type RevokeResponse struct { type RevokeResponse struct {
@ -15,7 +14,6 @@ type UpdateMessageRequest struct {
MessageID string `json:"message_id" uri:"message_id"` MessageID string `json:"message_id" uri:"message_id"`
Message string `json:"message" form:"message"` Message string `json:"message" form:"message"`
Phone string `json:"phone" form:"phone"` Phone string `json:"phone" form:"phone"`
Type Type `json:"type" form:"type"`
} }
type UpdateMessageResponse struct { type UpdateMessageResponse struct {

5
src/domains/send/send.go

@ -4,11 +4,6 @@ import (
"context" "context"
) )
type Type string
const TypeUser Type = "user"
const TypeGroup Type = "group"
type ISendService interface { type ISendService interface {
SendText(ctx context.Context, request MessageRequest) (response MessageResponse, err error) SendText(ctx context.Context, request MessageRequest) (response MessageResponse, err error)
SendImage(ctx context.Context, request ImageRequest) (response ImageResponse, err error) SendImage(ctx context.Context, request ImageRequest) (response ImageResponse, err error)

1
src/domains/send/text.go

@ -3,7 +3,6 @@ package send
type MessageRequest struct { type MessageRequest struct {
Phone string `json:"phone" form:"phone"` Phone string `json:"phone" form:"phone"`
Message string `json:"message" form:"message"` Message string `json:"message" form:"message"`
Type Type `json:"type" form:"type"`
} }
type MessageResponse struct { type MessageResponse struct {

1
src/domains/send/video.go

@ -6,7 +6,6 @@ type VideoRequest struct {
Phone string `json:"phone" form:"phone"` Phone string `json:"phone" form:"phone"`
Caption string `json:"caption" form:"caption"` Caption string `json:"caption" form:"caption"`
Video *multipart.FileHeader `json:"video" form:"video"` Video *multipart.FileHeader `json:"video" form:"video"`
Type Type `json:"type" form:"type"`
ViewOnce bool `json:"view_once" form:"view_once"` ViewOnce bool `json:"view_once" form:"view_once"`
Compress bool `json:"compress"` Compress bool `json:"compress"`
} }

2
src/internal/rest/app_rest.go

@ -28,7 +28,7 @@ func (controller *App) Login(c *fiber.Ctx) error {
return c.JSON(utils.ResponseData{ return c.JSON(utils.ResponseData{
Code: 200, Code: 200,
Message: "Success", Message: "Success",
Results: map[string]interface{}{
Results: map[string]any{
"qr_link": fmt.Sprintf("%s://%s/%s", c.Protocol(), c.Hostname(), response.ImagePath), "qr_link": fmt.Sprintf("%s://%s/%s", c.Protocol(), c.Hostname(), response.ImagePath),
"qr_duration": response.Duration, "qr_duration": response.Duration,
}, },

39
src/internal/rest/send_rest.go

@ -1,7 +1,6 @@
package rest package rest
import ( import (
"fmt"
domainSend "github.com/aldinokemal/go-whatsapp-web-multidevice/domains/send" domainSend "github.com/aldinokemal/go-whatsapp-web-multidevice/domains/send"
"github.com/aldinokemal/go-whatsapp-web-multidevice/utils" "github.com/aldinokemal/go-whatsapp-web-multidevice/utils"
"github.com/aldinokemal/go-whatsapp-web-multidevice/validations" "github.com/aldinokemal/go-whatsapp-web-multidevice/validations"
@ -33,12 +32,6 @@ func (controller *Send) SendText(c *fiber.Ctx) error {
// add validation send message // add validation send message
validations.ValidateSendMessage(request) validations.ValidateSendMessage(request)
if request.Type == domainSend.TypeGroup {
request.Phone = request.Phone + "@g.us"
} else {
request.Phone = request.Phone + "@s.whatsapp.net"
}
response, err := controller.Service.SendText(c.UserContext(), request) response, err := controller.Service.SendText(c.UserContext(), request)
utils.PanicIfNeeded(err) utils.PanicIfNeeded(err)
@ -64,12 +57,6 @@ func (controller *Send) SendImage(c *fiber.Ctx) error {
//add validation send image //add validation send image
validations.ValidateSendImage(request) validations.ValidateSendImage(request)
if request.Type == domainSend.TypeGroup {
request.Phone = request.Phone + "@g.us"
} else {
request.Phone = request.Phone + "@s.whatsapp.net"
}
response, err := controller.Service.SendImage(c.UserContext(), request) response, err := controller.Service.SendImage(c.UserContext(), request)
utils.PanicIfNeeded(err) utils.PanicIfNeeded(err)
@ -93,12 +80,6 @@ func (controller *Send) SendFile(c *fiber.Ctx) error {
//add validation send image //add validation send image
validations.ValidateSendFile(request) validations.ValidateSendFile(request)
if request.Type == domainSend.TypeGroup {
request.Phone = request.Phone + "@g.us"
} else {
request.Phone = request.Phone + "@s.whatsapp.net"
}
response, err := controller.Service.SendFile(c.UserContext(), request) response, err := controller.Service.SendFile(c.UserContext(), request)
utils.PanicIfNeeded(err) utils.PanicIfNeeded(err)
@ -122,12 +103,6 @@ func (controller *Send) SendVideo(c *fiber.Ctx) error {
//add validation send image //add validation send image
validations.ValidateSendVideo(request) validations.ValidateSendVideo(request)
if request.Type == domainSend.TypeGroup {
request.Phone = request.Phone + "@g.us"
} else {
request.Phone = request.Phone + "@s.whatsapp.net"
}
response, err := controller.Service.SendVideo(c.UserContext(), request) response, err := controller.Service.SendVideo(c.UserContext(), request)
utils.PanicIfNeeded(err) utils.PanicIfNeeded(err)
@ -146,12 +121,6 @@ func (controller *Send) SendContact(c *fiber.Ctx) error {
// add validation send contect // add validation send contect
validations.ValidateSendContact(request) validations.ValidateSendContact(request)
if request.Type == domainSend.TypeGroup {
request.Phone = request.Phone + "@g.us"
} else {
request.Phone = request.Phone + "@s.whatsapp.net"
}
response, err := controller.Service.SendContact(c.UserContext(), request) response, err := controller.Service.SendContact(c.UserContext(), request)
utils.PanicIfNeeded(err) utils.PanicIfNeeded(err)
@ -170,12 +139,6 @@ func (controller *Send) SendLink(c *fiber.Ctx) error {
err = validations.ValidateSendLink(request) err = validations.ValidateSendLink(request)
utils.PanicIfNeeded(err) 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) response, err := controller.Service.SendLink(c.UserContext(), request)
utils.PanicIfNeeded(err) utils.PanicIfNeeded(err)
@ -194,7 +157,6 @@ func (controller *Send) RevokeMessage(c *fiber.Ctx) error {
err = validations.ValidateRevokeMessage(request) err = validations.ValidateRevokeMessage(request)
utils.PanicIfNeeded(err) utils.PanicIfNeeded(err)
fmt.Println(request)
response, err := controller.Service.Revoke(c.UserContext(), request) response, err := controller.Service.Revoke(c.UserContext(), request)
utils.PanicIfNeeded(err) utils.PanicIfNeeded(err)
@ -214,7 +176,6 @@ func (controller *Send) UpdateMessage(c *fiber.Ctx) error {
err = validations.ValidateUpdateMessage(request) err = validations.ValidateUpdateMessage(request)
utils.PanicIfNeeded(err) utils.PanicIfNeeded(err)
fmt.Println(request)
response, err := controller.Service.UpdateMessage(c.UserContext(), request) response, err := controller.Service.UpdateMessage(c.UserContext(), request)
utils.PanicIfNeeded(err) utils.PanicIfNeeded(err)

4
src/internal/rest/user_rest.go

@ -35,8 +35,7 @@ func (controller *User) UserInfo(c *fiber.Ctx) error {
// add validation send message // add validation send message
validations.ValidateUserInfo(request) validations.ValidateUserInfo(request)
request.Phone = request.Phone + "@s.whatsapp.net"
response, err := controller.Service.Info(c.Context(), request) response, err := controller.Service.Info(c.Context(), request)
utils.PanicIfNeeded(err) utils.PanicIfNeeded(err)
@ -55,7 +54,6 @@ func (controller *User) UserAvatar(c *fiber.Ctx) error {
// add validation send message // add validation send message
validations.ValidateUserAvatar(request) validations.ValidateUserAvatar(request)
request.Phone = request.Phone + "@s.whatsapp.net"
response, err := controller.Service.Avatar(c.Context(), request) response, err := controller.Service.Avatar(c.Context(), request)
utils.PanicIfNeeded(err) utils.PanicIfNeeded(err)

15
src/internal/websocket/websocket.go

@ -1,7 +1,9 @@
package websocket package websocket
import ( import (
"context"
"encoding/json" "encoding/json"
domainApp "github.com/aldinokemal/go-whatsapp-web-multidevice/domains/app"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
"github.com/gofiber/websocket/v2" "github.com/gofiber/websocket/v2"
"log" "log"
@ -11,6 +13,7 @@ type client struct{} // Add more data to this type if needed
type BroadcastMessage struct { type BroadcastMessage struct {
Code string `json:"code"` Code string `json:"code"`
Message string `json:"message"` Message string `json:"message"`
Result any `json:"result"`
} }
var Clients = make(map[*websocket.Conn]client) // Note: although large maps with pointer-like types (e.g. strings) as keys are slow, using pointers themselves as keys is acceptable and fast var Clients = make(map[*websocket.Conn]client) // Note: although large maps with pointer-like types (e.g. strings) as keys are slow, using pointers themselves as keys is acceptable and fast
@ -61,7 +64,7 @@ func RunHub() {
} }
} }
func RegisterRoutes(app *fiber.App) {
func RegisterRoutes(app *fiber.App, service domainApp.IAppService) {
app.Use("/ws", func(c *fiber.Ctx) error { app.Use("/ws", func(c *fiber.Ctx) error {
if websocket.IsWebSocketUpgrade(c) { // Returns true if the client requested upgrade to the WebSocket protocol if websocket.IsWebSocketUpgrade(c) { // Returns true if the client requested upgrade to the WebSocket protocol
return c.Next() return c.Next()
@ -96,7 +99,15 @@ func RegisterRoutes(app *fiber.App) {
log.Println("error unmarshal message:", err) log.Println("error unmarshal message:", err)
return return
} }
Broadcast <- messageData
if messageData.Code == "FETCH_DEVICES" {
devices, _ := service.FetchDevices(context.Background())
Broadcast <- BroadcastMessage{
Code: "LIST_DEVICES",
Message: "Device found",
Result: devices,
}
}
} else { } else {
log.Println("websocket message received of type", messageType) log.Println("websocket message received of type", messageType)
} }

2
src/services/app_service.go

@ -119,7 +119,7 @@ func (service serviceApp) Reconnect(_ context.Context) (err error) {
return service.WaCli.Connect() return service.WaCli.Connect()
} }
func (service serviceApp) FetchDevices(ctx context.Context) (response []domainApp.FetchDevicesResponse, err error) {
func (service serviceApp) FetchDevices(_ context.Context) (response []domainApp.FetchDevicesResponse, err error) {
if service.WaCli == nil { if service.WaCli == nil {
return response, errors.New("wa cli nil cok") return response, errors.New("wa cli nil cok")
} }

2
src/utils/errors.go

@ -2,7 +2,7 @@ package utils
import "fmt" import "fmt"
func PanicIfNeeded(err interface{}, message ...string) {
func PanicIfNeeded(err any, message ...string) {
if err != nil { if err != nil {
if fmt.Sprintf("%s", err) == "record not found" && len(message) > 0 { if fmt.Sprintf("%s", err) == "record not found" && len(message) > 0 {
panic(message[0]) panic(message[0])

6
src/utils/response.go

@ -1,7 +1,7 @@
package utils package utils
type ResponseData struct { type ResponseData struct {
Code int `json:"code"`
Message string `json:"message"`
Results interface{} `json:"results"`
Code int `json:"code"`
Message string `json:"message"`
Results any `json:"results"`
} }

7
src/utils/whatsapp.go

@ -140,6 +140,11 @@ func handler(rawEvt interface{}) {
Code: "LOGIN_SUCCESS", Code: "LOGIN_SUCCESS",
Message: fmt.Sprintf("Successfully pair with %s", evt.ID.String()), Message: fmt.Sprintf("Successfully pair with %s", evt.ID.String()),
} }
case *events.LoggedOut:
websocket.Broadcast <- websocket.BroadcastMessage{
Code: "LIST_DEVICES",
Result: nil,
}
case *events.Connected, *events.PushNameSetting: case *events.Connected, *events.PushNameSetting:
if len(cli.Store.PushName) == 0 { if len(cli.Store.PushName) == 0 {
return return
@ -238,7 +243,7 @@ func handler(rawEvt interface{}) {
func sendAutoReplyWebhook(evt *events.Message) error { func sendAutoReplyWebhook(evt *events.Message) error {
client := &http.Client{Timeout: 10 * time.Second} client := &http.Client{Timeout: 10 * time.Second}
body := map[string]interface{}{
body := map[string]any{
"from": evt.Info.SourceString(), "from": evt.Info.SourceString(),
"message": evt.Message.GetConversation(), "message": evt.Message.GetConversation(),
"image": evt.Message.GetImageMessage(), "image": evt.Message.GetImageMessage(),

17
src/validations/send_validation.go

@ -11,8 +11,7 @@ import (
func ValidateSendMessage(request domainSend.MessageRequest) { func ValidateSendMessage(request domainSend.MessageRequest) {
err := validation.ValidateStruct(&request, err := validation.ValidateStruct(&request,
validation.Field(&request.Phone, validation.Required, validation.Length(10, 25)),
validation.Field(&request.Message, validation.Required, validation.Length(1, 1000)),
validation.Field(&request.Message, validation.Required),
) )
if err != nil { if err != nil {
@ -24,8 +23,6 @@ func ValidateSendMessage(request domainSend.MessageRequest) {
func ValidateSendImage(request domainSend.ImageRequest) { func ValidateSendImage(request domainSend.ImageRequest) {
err := validation.ValidateStruct(&request, err := validation.ValidateStruct(&request,
validation.Field(&request.Phone, validation.Required, validation.Length(10, 25)),
validation.Field(&request.Caption, validation.When(true, validation.Length(1, 1000))),
validation.Field(&request.Image, validation.Required), validation.Field(&request.Image, validation.Required),
) )
@ -50,7 +47,6 @@ func ValidateSendImage(request domainSend.ImageRequest) {
func ValidateSendFile(request domainSend.FileRequest) { func ValidateSendFile(request domainSend.FileRequest) {
err := validation.ValidateStruct(&request, err := validation.ValidateStruct(&request,
validation.Field(&request.Phone, validation.Required, validation.Length(10, 25)),
validation.Field(&request.File, validation.Required), validation.Field(&request.File, validation.Required),
) )
@ -70,7 +66,6 @@ func ValidateSendFile(request domainSend.FileRequest) {
func ValidateSendVideo(request domainSend.VideoRequest) { func ValidateSendVideo(request domainSend.VideoRequest) {
err := validation.ValidateStruct(&request, err := validation.ValidateStruct(&request,
validation.Field(&request.Phone, validation.Required, validation.Length(10, 25)),
validation.Field(&request.Video, validation.Required), validation.Field(&request.Video, validation.Required),
) )
@ -102,7 +97,6 @@ func ValidateSendVideo(request domainSend.VideoRequest) {
func ValidateSendContact(request domainSend.ContactRequest) { func ValidateSendContact(request domainSend.ContactRequest) {
err := validation.ValidateStruct(&request, err := validation.ValidateStruct(&request,
validation.Field(&request.Phone, validation.Required, validation.Length(10, 25)),
validation.Field(&request.ContactName, validation.Required), validation.Field(&request.ContactName, validation.Required),
validation.Field(&request.ContactPhone, validation.Required), validation.Field(&request.ContactPhone, validation.Required),
) )
@ -116,7 +110,6 @@ func ValidateSendContact(request domainSend.ContactRequest) {
func ValidateSendLink(request domainSend.LinkRequest) error { func ValidateSendLink(request domainSend.LinkRequest) error {
err := validation.ValidateStruct(&request, err := validation.ValidateStruct(&request,
validation.Field(&request.Phone, validation.Required, validation.Length(10, 25)),
validation.Field(&request.Link, validation.Required), validation.Field(&request.Link, validation.Required),
validation.Field(&request.Caption, validation.Required), validation.Field(&request.Caption, validation.Required),
) )
@ -132,8 +125,7 @@ func ValidateSendLink(request domainSend.LinkRequest) error {
func ValidateRevokeMessage(request domainSend.RevokeRequest) error { func ValidateRevokeMessage(request domainSend.RevokeRequest) error {
err := validation.ValidateStruct(&request, err := validation.ValidateStruct(&request,
validation.Field(&request.Phone, validation.Required, validation.Length(10, 25)),
validation.Field(&request.MessageID, validation.Required, validation.Length(20, 25)),
validation.Field(&request.MessageID, validation.Required),
) )
if err != nil { if err != nil {
@ -147,9 +139,8 @@ func ValidateRevokeMessage(request domainSend.RevokeRequest) error {
func ValidateUpdateMessage(request domainSend.UpdateMessageRequest) error { func ValidateUpdateMessage(request domainSend.UpdateMessageRequest) error {
err := validation.ValidateStruct(&request, err := validation.ValidateStruct(&request,
validation.Field(&request.Phone, validation.Required, validation.Length(10, 25)),
validation.Field(&request.MessageID, validation.Required, validation.Length(20, 25)),
validation.Field(&request.Message, validation.Required, validation.Length(1, 1000)),
validation.Field(&request.MessageID, validation.Required),
validation.Field(&request.Message, validation.Required),
) )
if err != nil { if err != nil {

4
src/validations/send_validation_test.go

@ -13,12 +13,12 @@ func TestValidateSendMessage(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
args args args args
err interface{}
err any
}{ }{
{ {
name: "success phone & message normal", name: "success phone & message normal",
args: args{request: domainSend.MessageRequest{ args: args{request: domainSend.MessageRequest{
Phone: "6289685024091",
Phone: "1728937129312@s.whatsapp.net",
Message: "Hello this is testing", Message: "Hello this is testing",
}}, }},
err: nil, err: nil,

5
src/validations/user_validation.go

@ -4,12 +4,11 @@ import (
domainUser "github.com/aldinokemal/go-whatsapp-web-multidevice/domains/user" domainUser "github.com/aldinokemal/go-whatsapp-web-multidevice/domains/user"
"github.com/aldinokemal/go-whatsapp-web-multidevice/utils" "github.com/aldinokemal/go-whatsapp-web-multidevice/utils"
validation "github.com/go-ozzo/ozzo-validation/v4" validation "github.com/go-ozzo/ozzo-validation/v4"
"github.com/go-ozzo/ozzo-validation/v4/is"
) )
func ValidateUserInfo(request domainUser.InfoRequest) { func ValidateUserInfo(request domainUser.InfoRequest) {
err := validation.ValidateStruct(&request, err := validation.ValidateStruct(&request,
validation.Field(&request.Phone, validation.Required, is.E164, validation.Length(10, 15)),
validation.Field(&request.Phone, validation.Required),
) )
if err != nil { if err != nil {
@ -20,7 +19,7 @@ func ValidateUserInfo(request domainUser.InfoRequest) {
} }
func ValidateUserAvatar(request domainUser.AvatarRequest) { func ValidateUserAvatar(request domainUser.AvatarRequest) {
err := validation.ValidateStruct(&request, err := validation.ValidateStruct(&request,
validation.Field(&request.Phone, validation.Required, is.E164, validation.Length(10, 15)),
validation.Field(&request.Phone, validation.Required),
) )
if err != nil { if err != nil {

436
src/views/index.html

@ -27,6 +27,15 @@
<div class="ui container" id="app"> <div class="ui container" id="app">
<h1 class="ui header center aligned">[[ app_name ]]</h1> <h1 class="ui header center aligned">[[ app_name ]]</h1>
<div class="ui success message" v-if="connected_devices != null">
<div class="header">
Device is connected
</div>
<p>
Device ID: <b>[[ connected_devices[0].device ]]</b>
</p>
</div>
<div class="ui horizontal divider"> <div class="ui horizontal divider">
App App
</div> </div>
@ -60,22 +69,22 @@
</div> </div>
<div class="ui horizontal divider"> <div class="ui horizontal divider">
Send Private Message
Message
</div> </div>
<div class="ui three column doubling grid cards"> <div class="ui three column doubling grid cards">
<div class="green card" @click="sendMessageModal('user')" style="cursor: pointer">
<div class="blue card" @click="sendMessageModal()" style="cursor: pointer">
<div class="content"> <div class="content">
<a class="ui blue right ribbon label">Private</a>
<a class="ui blue right ribbon label">Send</a>
<div class="header">Send Message</div> <div class="header">Send Message</div>
<div class="description"> <div class="description">
Send any message to any whatsapp number Send any message to any whatsapp number
</div> </div>
</div> </div>
</div> </div>
<div class="green card" @click="sendImageModal('user')" style="cursor:pointer;">
<div class="blue card" @click="sendImageModal()" style="cursor:pointer;">
<div class="content"> <div class="content">
<a class="ui blue right ribbon label">Private</a>
<a class="ui blue right ribbon label">Send</a>
<div class="header">Send Image</div> <div class="header">Send Image</div>
<div class="description"> <div class="description">
Send image with Send image with
@ -84,9 +93,9 @@
</div> </div>
</div> </div>
</div> </div>
<div class="green card" @click="sendFileModal('user')" style="cursor: pointer">
<div class="blue card" @click="sendFileModal()" style="cursor: pointer">
<div class="content"> <div class="content">
<a class="ui blue right ribbon label">Private</a>
<a class="ui blue right ribbon label">Send</a>
<div class="header">Send File</div> <div class="header">Send File</div>
<div class="description"> <div class="description">
Send any file up to Send any file up to
@ -94,9 +103,9 @@
</div> </div>
</div> </div>
</div> </div>
<div class="green card" @click="sendVideoModal('user')" style="cursor: pointer">
<div class="blue card" @click="sendVideoModal()" style="cursor: pointer">
<div class="content"> <div class="content">
<a class="ui blue right ribbon label">Private</a>
<a class="ui blue right ribbon label">Send</a>
<div class="header">Send Video</div> <div class="header">Send Video</div>
<div class="description"> <div class="description">
Send video Send video
@ -106,99 +115,50 @@
</div> </div>
</div> </div>
</div> </div>
<div class="green card" @click="sendContactModal('user')" style="cursor: pointer">
<div class="blue card" @click="sendContactModal()" style="cursor: pointer">
<div class="content"> <div class="content">
<a class="ui blue right ribbon label">Private</a>
<a class="ui blue right ribbon label">Send</a>
<div class="header">Send Contact</div> <div class="header">Send Contact</div>
<div class="description"> <div class="description">
Send contact to any whatsapp number Send contact to any whatsapp number
</div> </div>
</div> </div>
</div> </div>
</div>
<div class="ui horizontal divider">
Send Group Message
</div>
<div class="ui three column doubling grid cards">
<div class="green card" @click="sendMessageModal('group')" style="cursor: pointer">
<div class="red card" @click="sendRevokeModal()" style="cursor: pointer">
<div class="content"> <div class="content">
<a class="ui teal right ribbon label">Group</a>
<div class="header">Send Message</div>
<a class="ui red right ribbon label">Revoke</a>
<div class="header">Revoke Message</div>
<div class="description"> <div class="description">
Send any message to any whatsapp number
</div>
</div>
</div>
<div class="green card" @click="sendImageModal('group')" style="cursor:pointer;">
<div class="content">
<a class="ui teal right ribbon label">Group</a>
<div class="header">Send Image</div>
<div class="description">
Send image with
<div class="ui teal horizontal label">jpg/jpeg/png</div>
type
</div>
</div>
</div>
<div class="green card" @click="sendFileModal('group')" style="cursor: pointer">
<div class="content">
<a class="ui teal right ribbon label">Group</a>
<div class="header">Send File</div>
<div class="description">
Send any file up to
<div class="ui teal horizontal label">{{ .MaxFileSize }}</div>
</div>
</div>
</div>
<div class="green card" @click="sendVideoModal('group')" style="cursor: pointer">
<div class="content">
<a class="ui teal right ribbon label">Group</a>
<div class="header">Send Video</div>
<div class="description">
Send video
<div class="ui teal horizontal label">mp4</div>
up to
<div class="ui teal horizontal label">{{ .MaxVideoSize }}</div>
</div>
</div>
</div>
<div class="green card" @click="sendContactModal('group')" style="cursor: pointer">
<div class="content">
<a class="ui teal right ribbon label">Group</a>
<div class="header">Send Contact</div>
<div class="description">
Send contact to any whatsapp number
Revoke any message in private or group chat
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="ui horizontal divider"> <div class="ui horizontal divider">
User
Account
</div> </div>
<div class="ui four column doubling grid cards"> <div class="ui four column doubling grid cards">
<div class="green card" @click="infoModal" style="cursor: pointer;">
<div class="green card" @click="avatarModal" style="cursor: pointer;">
<div class="content"> <div class="content">
<div class="header">User Info</div>
<div class="header">Avatar</div>
<div class="description"> <div class="description">
You can search someone user info by phone
You can search someone avatar by phone
</div> </div>
</div> </div>
</div> </div>
<div class="green card" @click="avatarModal" style="cursor: pointer;">
<div class="green card" @click="infoModal" style="cursor: pointer;">
<div class="content"> <div class="content">
<div class="header">User Avatar</div>
<div class="header">User Info</div>
<div class="description"> <div class="description">
You can search someone avatar by phone
You can search someone user info by phone
</div> </div>
</div> </div>
</div> </div>
<div class="green card" @click="groupModal" style="cursor: pointer"> <div class="green card" @click="groupModal" style="cursor: pointer">
<div class="content"> <div class="content">
<div class="header">User List Groups</div>
<div class="header">My List Groups</div>
<div class="description"> <div class="description">
Display all groups you joined Display all groups you joined
</div> </div>
@ -206,7 +166,7 @@
</div> </div>
<div class="green card" @click="privacyModal" style="cursor: pointer"> <div class="green card" @click="privacyModal" style="cursor: pointer">
<div class="content"> <div class="content">
<div class="header">User Privacy Setting</div>
<div class="header">My Privacy Setting</div>
<div class="description"> <div class="description">
Get your privacy settings Get your privacy settings
</div> </div>
@ -249,10 +209,18 @@
</div> </div>
<div class="content"> <div class="content">
<form class="ui form"> <form class="ui form">
<div class="field">
<label>Type</label>
<select name="message_type" v-model="message_type" aria-label="type">
<option value="group">Group Message</option>
<option value="user">Private Message</option>
</select>
</div>
<div class="field"> <div class="field">
<label>Phone / Group ID</label> <label>Phone / Group ID</label>
<input v-model="message_phone" type="text" placeholder="6289..." <input v-model="message_phone" type="text" placeholder="6289..."
aria-label="phone"> aria-label="phone">
<input :value="message_phone_id" disabled aria-label="whatsapp_id">
</div> </div>
<div class="field"> <div class="field">
<label>Message</label> <label>Message</label>
@ -262,7 +230,8 @@
</form> </form>
</div> </div>
<div class="actions"> <div class="actions">
<div class="ui positive right labeled icon button" @click="sendMessageProcess">
<div class="ui approve positive right labeled icon button" :class="{'loading': this.message_loading}"
@click="sendMessageProcess">
Send Send
<i class="send icon"></i> <i class="send icon"></i>
</div> </div>
@ -277,10 +246,18 @@
</div> </div>
<div class="content"> <div class="content">
<form class="ui form"> <form class="ui form">
<div class="field">
<label>Type</label>
<select name="image_type" v-model="image_type" aria-label="type">
<option value="group">Group Message</option>
<option value="user">Private Message</option>
</select>
</div>
<div class="field"> <div class="field">
<label>Phone / Group ID</label> <label>Phone / Group ID</label>
<input v-model="image_phone" type="text" placeholder="6289..." <input v-model="image_phone" type="text" placeholder="6289..."
aria-label="phone"> aria-label="phone">
<input :value="image_phone_id" disabled aria-label="whatsapp_id">
</div> </div>
<div class="field"> <div class="field">
<label>Caption</label> <label>Caption</label>
@ -313,7 +290,8 @@
</form> </form>
</div> </div>
<div class="actions"> <div class="actions">
<div class="ui positive right labeled icon button" @click="sendImageProcess">
<div class="ui approve positive right labeled icon button" :class="{'loading': this.image_loading}"
@click="sendImageProcess">
Send Send
<i class="send icon"></i> <i class="send icon"></i>
</div> </div>
@ -328,10 +306,18 @@
</div> </div>
<div class="content"> <div class="content">
<form class="ui form"> <form class="ui form">
<div class="field">
<label>Type</label>
<select name="file_type" v-model="file_type" aria-label="type">
<option value="group">Group Message</option>
<option value="user">Private Message</option>
</select>
</div>
<div class="field"> <div class="field">
<label>Phone / Group ID</label> <label>Phone / Group ID</label>
<input v-model="file_phone" type="text" placeholder="6289..." <input v-model="file_phone" type="text" placeholder="6289..."
aria-label="phone"> aria-label="phone">
<input :value="file_phone_id" disabled aria-label="whatsapp_id">
</div> </div>
<div class="field"> <div class="field">
<label>Caption</label> <label>Caption</label>
@ -349,14 +335,15 @@
</form> </form>
</div> </div>
<div class="actions"> <div class="actions">
<div class="ui positive right labeled icon button" @click="sendFileProcess">
<div class="ui approve positive right labeled icon button" :class="{'loading': this.file_loading}"
@click="sendFileProcess">
Send Send
<i class="send icon"></i> <i class="send icon"></i>
</div> </div>
</div> </div>
</div> </div>
<!-- Modal SendFile -->
<!-- Modal SendVideo -->
<div class="ui small modal" id="modalSendVideo"> <div class="ui small modal" id="modalSendVideo">
<i class="close icon"></i> <i class="close icon"></i>
<div class="header"> <div class="header">
@ -364,6 +351,13 @@
</div> </div>
<div class="content"> <div class="content">
<form class="ui form"> <form class="ui form">
<div class="field">
<label>Type</label>
<select name="video_type" v-model="video_type" aria-label="type">
<option value="group">Group Message</option>
<option value="user">Private Message</option>
</select>
</div>
<div class="field"> <div class="field">
<label>Phone / Group ID</label> <label>Phone / Group ID</label>
<input v-model="video_phone" type="text" placeholder="6289..." <input v-model="video_phone" type="text" placeholder="6289..."
@ -373,6 +367,7 @@
<label>Caption</label> <label>Caption</label>
<input v-model="video_caption" type="text" placeholder="Type some caption (optional)..." <input v-model="video_caption" type="text" placeholder="Type some caption (optional)..."
aria-label="caption"> aria-label="caption">
<input :value="video_phone_id" disabled aria-label="whatsapp_id">
</div> </div>
<div class="field"> <div class="field">
<label>View Once</label> <label>View Once</label>
@ -399,7 +394,8 @@
</form> </form>
</div> </div>
<div class="actions"> <div class="actions">
<div class="ui positive right labeled icon button" @click="sendVideoProcess">
<div class="ui approve positive right labeled icon button" :class="{'loading': this.video_loading}"
@click="sendVideoProcess">
Send Send
<i class="send icon"></i> <i class="send icon"></i>
</div> </div>
@ -414,10 +410,18 @@
</div> </div>
<div class="content"> <div class="content">
<form class="ui form"> <form class="ui form">
<div class="field">
<label>Type</label>
<select name="contact_type" v-model="contact_type" aria-label="type">
<option value="group">Group Message</option>
<option value="user">Private Message</option>
</select>
</div>
<div class="field"> <div class="field">
<label>Phone / Group ID</label> <label>Phone / Group ID</label>
<input v-model="contact_phone" type="text" placeholder="6289..." <input v-model="contact_phone" type="text" placeholder="6289..."
aria-label="phone"> aria-label="phone">
<input :value="contact_phone_id" disabled aria-label="whatsapp_id">
</div> </div>
<div class="field"> <div class="field">
<label>Contact Name</label> <label>Contact Name</label>
@ -432,7 +436,45 @@
</form> </form>
</div> </div>
<div class="actions"> <div class="actions">
<div class="ui positive right labeled icon button" @click="sendContactProcess">
<div class="ui approve positive right labeled icon button" :class="{'loading': this.contact_loading}"
@click="sendContactProcess">
Send
<i class="send icon"></i>
</div>
</div>
</div>
<!-- Modal SendRevoke -->
<div class="ui small modal" id="modalSendRevoke">
<i class="close icon"></i>
<div class="header">
Send Revoke
</div>
<div class="content">
<form class="ui form">
<div class="field">
<label>Type</label>
<select name="revoke_type" v-model="revoke_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="revoke_phone" type="text" placeholder="6289..."
aria-label="phone">
<input :value="revoke_phone_id" disabled aria-label="whatsapp_id">
</div>
<div class="field">
<label>Revoke Message ID</label>
<input v-model="revoke_message_id" type="text" placeholder="Please enter your message id"
aria-label="card_phone">
</div>
</form>
</div>
<div class="actions">
<div class="ui approve positive right labeled icon button" :class="{'loading': this.revoke_loading}"
@click="sendRevokeProcess">
Send Send
<i class="send icon"></i> <i class="send icon"></i>
</div> </div>
@ -492,10 +534,18 @@
</div> </div>
<div class="content"> <div class="content">
<form class="ui form"> <form class="ui form">
<div class="field">
<label>Type</label>
<select name="avatar_type" v-model="avatar_type" aria-label="type">
<option value="group">Group Message</option>
<option value="user">Private Message</option>
</select>
</div>
<div class="field"> <div class="field">
<label>Phone</label> <label>Phone</label>
<input v-model="avatar_phone" type="text" placeholder="6289..." <input v-model="avatar_phone" type="text" placeholder="6289..."
aria-label="phone"> aria-label="phone">
<input :value="avatar_phone_id" disabled aria-label="whatsapp_id">
</div> </div>
<button type="button" class="ui primary button" :class="{'loading': avatar_loading}" <button type="button" class="ui primary button" :class="{'loading': avatar_loading}"
@ -505,7 +555,7 @@
</form> </form>
<div v-if="avatar_image != null" class="center"> <div v-if="avatar_image != null" class="center">
<img :src="avatar_image" alt="profile picture" style="padding-top: 10px;">
<img :src="avatar_image" alt="profile picture" style="padding-top: 10px; max-height: 200px">
</div> </div>
</div> </div>
</div> </div>
@ -518,10 +568,17 @@
</div> </div>
<div class="content"> <div class="content">
<form class="ui form"> <form class="ui form">
<div class="field">
<label>Type</label>
<select name="info_type" v-model="info_type" aria-label="type">
<option value="user">Private Message</option>
</select>
</div>
<div class="field"> <div class="field">
<label>Phone</label> <label>Phone</label>
<input v-model="info_phone" type="text" placeholder="6289..." <input v-model="info_phone" type="text" placeholder="6289..."
aria-label="phone"> aria-label="phone">
<input :value="info_phone_id" disabled aria-label="whatsapp_id">
</div> </div>
<button type="button" class="ui primary button" :class="{'loading': info_loading}" <button type="button" class="ui primary button" :class="{'loading': info_loading}"
@ -620,6 +677,9 @@
try { try {
await this.logoutApi() await this.logoutApi()
showSuccessInfo("Logout success") showSuccessInfo("Logout success")
// fetch devices
this.app_ws.send(JSON.stringify({"code": "FETCH_DEVICES"}))
} catch (err) { } catch (err) {
showErrorInfo(err) showErrorInfo(err)
} }
@ -648,6 +708,8 @@
try { try {
await this.reconnectApi() await this.reconnectApi()
showSuccessInfo("Reconnect success") showSuccessInfo("Reconnect success")
// fetch devices
this.app_ws.send(JSON.stringify({"code": "FETCH_DEVICES"}))
} catch (err) { } catch (err) {
showErrorInfo(err) showErrorInfo(err)
} }
@ -676,17 +738,27 @@
message_type: 'user', message_type: 'user',
message_phone: '', message_phone: '',
message_text: '', message_text: '',
message_loading: false,
}
},
computed: {
message_phone_id() {
return this.message_type === 'user' ? `${this.message_phone}@${this.type_user}` : `${this.message_phone}@${this.type_group}`
} }
}, },
methods: { methods: {
sendMessageModal(type) {
this.message_type = type
$('#modalSendMessage').modal('show');
sendMessageModal() {
$('#modalSendMessage').modal({
onApprove: function () {
return false;
}
}).modal('show');
}, },
async sendMessageProcess() { async sendMessageProcess() {
try { try {
let response = await this.sendMessageApi() let response = await this.sendMessageApi()
showSuccessInfo(response) showSuccessInfo(response)
$('#modalSendMessage').modal('hide');
} catch (err) { } catch (err) {
showErrorInfo(err) showErrorInfo(err)
} }
@ -694,10 +766,10 @@
sendMessageApi() { sendMessageApi() {
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {
try { try {
this.message_loading = true;
let payload = new FormData(); let payload = new FormData();
payload.append("phone", this.message_phone)
payload.append("phone", this.message_phone_id)
payload.append("message", this.message_text) payload.append("message", this.message_text)
payload.append("type", this.message_type)
let response = await http.post(`/send/message`, payload) let response = await http.post(`/send/message`, payload)
this.sendMessageReset(); this.sendMessageReset();
resolve(response.data.message) resolve(response.data.message)
@ -707,6 +779,8 @@
} else { } else {
reject(error.message) reject(error.message)
} }
} finally {
this.message_loading = false;
} }
}) })
}, },
@ -726,17 +800,27 @@
image_compress: false, image_compress: false,
image_caption: '', image_caption: '',
image_type: 'user', image_type: 'user',
image_loading: false,
}
},
computed: {
image_phone_id() {
return this.image_type === 'user' ? `${this.image_phone}@${this.type_user}` : `${this.image_phone}@${this.type_group}`
} }
}, },
methods: { methods: {
sendImageModal(type) {
this.image_type = type
$('#modalSendImage').modal('show');
sendImageModal() {
$('#modalSendImage').modal({
onApprove: function () {
return false;
}
}).modal('show');
}, },
async sendImageProcess() { async sendImageProcess() {
try { try {
let response = await this.sendImageApi() let response = await this.sendImageApi()
showSuccessInfo(response) showSuccessInfo(response)
$('#modalSendImage').modal('hide');
} catch (err) { } catch (err) {
showErrorInfo(err) showErrorInfo(err)
} }
@ -744,13 +828,13 @@
sendImageApi() { sendImageApi() {
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {
try { try {
this.image_loading = true;
let payload = new FormData(); let payload = new FormData();
payload.append("phone", this.image_phone)
payload.append("phone", this.image_phone_id)
payload.append("view_once", this.image_view_once) payload.append("view_once", this.image_view_once)
payload.append("compress", this.image_compress) payload.append("compress", this.image_compress)
payload.append("caption", this.image_caption) payload.append("caption", this.image_caption)
payload.append("image", $("#image_file")[0].files[0]) payload.append("image", $("#image_file")[0].files[0])
payload.append("type", this.image_type)
let response = await http.post(`/send/image`, payload) let response = await http.post(`/send/image`, payload)
this.sendImageReset(); this.sendImageReset();
resolve(response.data.message) resolve(response.data.message)
@ -760,6 +844,8 @@
} else { } else {
reject(error.message) reject(error.message)
} }
} finally {
this.image_loading = false;
} }
}) })
}, },
@ -780,17 +866,27 @@
file_caption: '', file_caption: '',
file_type: 'user', file_type: 'user',
file_phone: '', file_phone: '',
file_loading: false,
}
},
computed: {
file_phone_id() {
return this.file_type === 'user' ? `${this.file_phone}@${this.type_user}` : `${this.file_phone}@${this.type_group}`
} }
}, },
methods: { methods: {
sendFileModal(type) {
this.file_type = type
$('#modalSendFile').modal('show');
sendFileModal() {
$('#modalSendFile').modal({
onApprove: function () {
return false;
}
}).modal('show');
}, },
async sendFileProcess() { async sendFileProcess() {
try { try {
let response = await this.sendFileApi() let response = await this.sendFileApi()
showSuccessInfo(response) showSuccessInfo(response)
$('#modalSendFile').modal('hide');
} catch (err) { } catch (err) {
showErrorInfo(err) showErrorInfo(err)
} }
@ -798,11 +894,11 @@
sendFileApi() { sendFileApi() {
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {
try { try {
this.file_loading = true;
let payload = new FormData(); let payload = new FormData();
payload.append("caption", this.file_caption) payload.append("caption", this.file_caption)
payload.append("phone", this.file_phone)
payload.append("phone", this.file_phone_id)
payload.append("file", $("#file_file")[0].files[0]) payload.append("file", $("#file_file")[0].files[0])
payload.append("type", this.file_type)
let response = await http.post(`/send/file`, payload) let response = await http.post(`/send/file`, payload)
this.sendFileReset(); this.sendFileReset();
resolve(response.data.message) resolve(response.data.message)
@ -812,6 +908,8 @@
} else { } else {
reject(error.message) reject(error.message)
} }
} finally {
this.file_loading = false;
} }
}) })
}, },
@ -832,17 +930,27 @@
video_compress: false, video_compress: false,
video_type: 'user', video_type: 'user',
video_phone: '', video_phone: '',
video_loading: false,
}
},
computed: {
video_phone_id() {
return this.video_type === 'user' ? `${this.video_phone}@${this.type_user}` : `${this.video_phone}@${this.type_group}`
} }
}, },
methods: { methods: {
sendVideoModal(type) {
this.video_type = type
$('#modalSendVideo').modal('show');
sendVideoModal() {
$('#modalSendVideo').modal({
onApprove: function () {
return false;
}
}).modal('show');
}, },
async sendVideoProcess() { async sendVideoProcess() {
try { try {
let response = await this.sendVideoApi() let response = await this.sendVideoApi()
showSuccessInfo(response) showSuccessInfo(response)
$('#modalSendVideo').modal('hide');
} catch (err) { } catch (err) {
showErrorInfo(err) showErrorInfo(err)
} }
@ -850,13 +958,13 @@
sendVideoApi() { sendVideoApi() {
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {
try { try {
this.video_loading = true;
let payload = new FormData(); let payload = new FormData();
payload.append("phone", this.video_phone)
payload.append("phone", this.video_phone_id)
payload.append("caption", this.video_caption) payload.append("caption", this.video_caption)
payload.append("view_once", this.video_view_once) payload.append("view_once", this.video_view_once)
payload.append("compress", this.video_compress) payload.append("compress", this.video_compress)
payload.append("video", $("#video_file")[0].files[0]) payload.append("video", $("#video_file")[0].files[0])
payload.append("type", this.video_type)
let response = await http.post(`/send/video`, payload) let response = await http.post(`/send/video`, payload)
this.sendVideoReset(); this.sendVideoReset();
resolve(response.data.message) resolve(response.data.message)
@ -866,6 +974,8 @@
} else { } else {
reject(error.message) reject(error.message)
} }
} finally {
this.video_loading = false;
} }
}) })
}, },
@ -887,29 +997,41 @@
contact_phone: '', contact_phone: '',
contact_card_name: '', contact_card_name: '',
contact_card_phone: '', contact_card_phone: '',
contact_loading: false,
}
},
computed: {
contact_phone_id() {
return this.contact_type === 'user' ? `${this.contact_phone}@${this.type_user}` : `${this.contact_phone}@${this.type_group}`
} }
}, },
methods: { methods: {
sendContactModal(type) {
this.contact_type = type
$('#modalSendContact').modal('show');
sendContactModal() {
$('#modalSendContact').modal({
onApprove: function () {
return false;
}
}).modal('show');
}, },
async sendContactProcess() { async sendContactProcess() {
try { try {
this.contact_loading = true;
let response = await this.sendContactApi() let response = await this.sendContactApi()
showSuccessInfo(response) showSuccessInfo(response)
$('#modalSendContact').modal('hide');
} catch (err) { } catch (err) {
showErrorInfo(err) showErrorInfo(err)
} finally {
this.contact_loading = false;
} }
}, },
sendContactApi() { sendContactApi() {
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {
try { try {
let payload = new FormData(); let payload = new FormData();
payload.append("phone", this.contact_phone)
payload.append("phone", this.contact_phone_id)
payload.append("contact_name", this.contact_card_name) payload.append("contact_name", this.contact_card_name)
payload.append("contact_phone", this.contact_card_phone) payload.append("contact_phone", this.contact_card_phone)
payload.append("type", this.contact_type)
let response = await http.post(`/send/contact`, payload) let response = await http.post(`/send/contact`, payload)
this.sendContactReset(); this.sendContactReset();
resolve(response.data.message) resolve(response.data.message)
@ -926,7 +1048,66 @@
this.contact_phone = ''; this.contact_phone = '';
this.contact_card_name = ''; this.contact_card_name = '';
this.contact_card_phone = ''; this.contact_card_phone = '';
this.message_type = 'user';
this.contact_type = 'user';
},
}
}
const sendRevoke = {
data() {
return {
revoke_type: 'user',
revoke_phone: '',
revoke_message_id: '',
revoke_loading: false,
}
},
computed: {
revoke_phone_id() {
return this.revoke_type === 'user' ? `${this.revoke_phone}@${this.type_user}` : `${this.revoke_phone}@${this.type_group}`
}
},
methods: {
sendRevokeModal() {
$('#modalSendRevoke').modal({
onApprove: function () {
return false;
}
}).modal('show');
},
async sendRevokeProcess() {
try {
let response = await this.sendRevokeApi()
showSuccessInfo(response)
$('#modalSendRevoke').modal('hide');
} catch (err) {
showErrorInfo(err)
}
},
sendRevokeApi() {
return new Promise(async (resolve, reject) => {
try {
this.revoke_loading = true;
let payload = new FormData();
payload.append("phone", this.revoke_phone_id)
let response = await http.post(`/message/${this.revoke_message_id}/revoke`, payload)
this.sendRevokeReset();
resolve(response.data.message)
} catch (error) {
if (error.response) {
reject(error.response.data.message)
} else {
reject(error.message)
}
} finally {
this.revoke_loading = false;
}
})
},
sendRevokeReset() {
this.revoke_phone = '';
this.revoke_message_id = '';
this.revoke_type = 'user';
}, },
} }
} }
@ -1006,11 +1187,17 @@
const userAvatar = { const userAvatar = {
data() { data() {
return { return {
avatar_type: 'user',
avatar_phone: '', avatar_phone: '',
avatar_image: null, avatar_image: null,
avatar_loading: false, avatar_loading: false,
} }
}, },
computed: {
avatar_phone_id() {
return this.avatar_type === 'user' ? `${this.avatar_phone}@${this.type_user}` : `${this.avatar_phone}@${this.type_group}`
}
},
methods: { methods: {
async avatarModal() { async avatarModal() {
this.avatarReset(); this.avatarReset();
@ -1030,7 +1217,7 @@
avatarApi() { avatarApi() {
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {
try { try {
let response = await http.get(`/user/avatar?phone=${this.avatar_phone}`)
let response = await http.get(`/user/avatar?phone=${this.avatar_phone_id}`)
this.avatar_image = response.data.results.url; this.avatar_image = response.data.results.url;
resolve() resolve()
} catch (error) { } catch (error) {
@ -1045,6 +1232,7 @@
avatarReset() { avatarReset() {
this.avatar_phone = ''; this.avatar_phone = '';
this.avatar_image = null; this.avatar_image = null;
this.avatar_type = 'user';
} }
} }
} }
@ -1052,6 +1240,7 @@
const userInfo = { const userInfo = {
data() { data() {
return { return {
info_type: 'user',
info_phone: '', info_phone: '',
// //
info_name: null, info_name: null,
@ -1061,6 +1250,12 @@
info_loading: false, info_loading: false,
} }
}, },
computed: {
info_phone_id() {
return this.info_type === 'user' ? `${this.info_phone}@${this.type_user}` : `${this.info_phone}@${this.type_group}`
}
},
methods: { methods: {
async infoModal() { async infoModal() {
this.infoReset(); this.infoReset();
@ -1080,7 +1275,7 @@
infoApi() { infoApi() {
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {
try { try {
let response = await http.get(`/user/info?phone=${this.info_phone}`)
let response = await http.get(`/user/info?phone=${this.info_phone_id}`)
this.info_name = response.data.results.verified_name; this.info_name = response.data.results.verified_name;
this.info_status = response.data.results.status; this.info_status = response.data.results.status;
this.info_devices = response.data.results.devices; this.info_devices = response.data.results.devices;
@ -1099,6 +1294,7 @@
this.info_name = null; this.info_name = null;
this.info_status = null; this.info_status = null;
this.info_devices = []; this.info_devices = [];
this.info_type = 'user';
} }
} }
} }
@ -1107,27 +1303,39 @@
delimiters: ['[[', ']]'], delimiters: ['[[', ']]'],
data() { data() {
return { return {
app_ws: null,
app_host: {{ .AppHost }}, app_host: {{ .AppHost }},
app_name: 'Whatsapp API Multi Device ({{ .AppVersion }})', app_name: 'Whatsapp API Multi Device ({{ .AppVersion }})',
is_logged_in: false, is_logged_in: false,
connected_devices: null,
type_group: "g.us",
type_user: "s.whatsapp.net"
} }
}, },
mounted() { mounted() {
if (window["WebSocket"]) { if (window["WebSocket"]) {
let wsType = location.protocol !== 'https:' ? 'ws://' : 'wss://'; let wsType = location.protocol !== 'https:' ? 'ws://' : 'wss://';
let conn = new WebSocket(wsType + document.location.host + "/ws");
this.app_ws = new WebSocket(wsType + document.location.host + "/ws");
conn.onclose = (evt) => {
console.log(evt)
this.app_ws.onopen = (evt) => {
this.app_ws.send(JSON.stringify({
"code": "FETCH_DEVICES",
"message": "List device"
}))
}; };
conn.onmessage = (evt) => {
console.log(evt)
this.app_ws.onmessage = (evt) => {
const message = JSON.parse(evt.data) const message = JSON.parse(evt.data)
switch (message.code) { switch (message.code) {
case 'LOGIN_SUCCESS': case 'LOGIN_SUCCESS':
showSuccessInfo(message.message) showSuccessInfo(message.message)
$('#modalLogin').modal('hide'); $('#modalLogin').modal('hide');
// fetch devices
this.app_ws.send(JSON.stringify({"code": "FETCH_DEVICES"}))
break;
case 'LIST_DEVICES':
this.connected_devices = message.result
break; break;
default: default:
console.log(message) console.log(message)
@ -1144,7 +1352,7 @@
return tanggal.format('LLL'); return tanggal.format('LLL');
} }
}, },
mixins: [login, logout, reconnect, sendMessage, sendImage, sendFile, sendVideo, sendContact, userGroups, userPrivacy, userAvatar, userInfo]
mixins: [login, logout, reconnect, sendMessage, sendImage, sendFile, sendVideo, sendContact, sendRevoke, userGroups, userPrivacy, userAvatar, userInfo]
}).mount('#app') }).mount('#app')
</script> </script>
</body> </body>
Loading…
Cancel
Save