Browse Source

feat: add send video (#15)

* feat: add send rest

* feat: add view_once when sending video

* feat: upgrade body limit

* feat: change statics folder

* feat: remove copyright

* feat: add thumbnails in send video

* fix: deleting file

* feat: add params compress video

* chore: update openapi

* chore: update readme

* chore: update readme for installation

* chore: update docker and document

* feat: fix validation and update dashboard page

* fix: docker config & utils code

* chore: update docs

* chore: update docs
pull/16/head
Aldino Kemal 4 years ago
committed by GitHub
parent
commit
9a301c6068
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 8
      docker/golang.Dockerfile
  2. 108
      docs/openapi.yaml
  3. 42
      readme.md
  4. 2
      src/.air.toml
  5. 10
      src/cmd/root.go
  6. 7
      src/config/settings.go
  7. 31
      src/controllers/send_controller.go
  8. 1
      src/go.mod
  9. 2
      src/go.sum
  10. 4
      src/main.go
  11. 1
      src/services/send_service.go
  12. 111
      src/services/send_service_impl.go
  13. 0
      src/statics/qrcode/.gitignore
  14. 0
      src/statics/senditems/.gitignore
  15. 17
      src/structs/send_struct.go
  16. 6
      src/utils/general.go
  17. 48
      src/validations/send_validation.go
  18. 21
      src/validations/send_validation_test.go
  19. 199
      src/views/index.html

8
docker/golang.Dockerfile

@ -2,22 +2,22 @@
# STEP 1 build executable binary
############################
FROM golang:alpine AS builder
RUN apk update && apk add --no-cache vips-dev gcc musl-dev gcompat
RUN apk update && apk add --no-cache vips-dev gcc musl-dev gcompat ffmpeg
WORKDIR /whatsapp
COPY ./src .
# Fetch dependencies.
RUN go mod download
# Install pkger
RUN go install github.com/gobuffalo/packr/v2/packr2@latest
RUN go install github.com/markbates/pkger/cmd/pkger@latest
# Build the binary.
RUN go build -o /app/whatsapp
RUN pkger && go build -o /app/whatsapp
#############################
## STEP 2 build a smaller image
#############################
FROM alpine
RUN apk update && apk add --no-cache vips-dev
RUN apk update && apk add --no-cache vips-dev ffmpeg
WORKDIR /app
# Copy compiled from builder.
COPY --from=builder /app/whatsapp /app/whatsapp

108
docs/openapi.yaml

@ -1,7 +1,7 @@
openapi: 3.0.0
info:
title: WhatsApp API MultiDevice
version: 2.2.1
version: 2.3.0
description: This API is used for sending whatsapp via API
servers:
- url: http://localhost:3000
@ -606,9 +606,6 @@ paths:
caption:
type: string
example: selamat malam
view_once:
type: boolean
example: 'false'
file:
type: string
format: binary
@ -689,3 +686,106 @@ paths:
code: 500
message: you are not loggin
results: null
/send/video:
post:
tags:
- send
summary: Send Video
requestBody:
content:
multipart/form-data:
schema:
type: object
properties:
phone:
type: integer
example: '6289685024091'
caption:
type: string
example: ini contoh caption video
view_once:
type: boolean
example: 'false'
video:
type: string
format: binary
type:
type: string
example: 'user'
description: 'user/group | default: user'
compress:
type: boolean
example: 'false'
responses:
'200':
description: OK
headers:
Date:
schema:
type: string
example: Fri, 11 Feb 2022 03:42:57 GMT
Content-Type:
schema:
type: string
example: application/json
Content-Length:
schema:
type: integer
example: '123'
content:
application/json:
schema:
type: object
example:
code: 200
message: Success
results:
status: >-
Video sent to 6289685024091@s.whatsapp.net (server timestamp: 2022-05-17 14:39:29 +0700 WIB)
'400':
description: Bad Request
headers:
Date:
schema:
type: string
example: Fri, 11 Feb 2022 03:02:17 GMT
Content-Type:
schema:
type: string
example: application/json
Content-Length:
schema:
type: integer
example: '70'
content:
application/json:
schema:
type: object
example:
code: 400
message: 'phone: cannot be blank.'
results: null
'500':
description: Internal Server Error
headers:
Date:
schema:
type: string
example: Fri, 11 Feb 2022 03:02:48 GMT
Content-Type:
schema:
type: string
example: application/json
Content-Length:
schema:
type: integer
example: '58'
content:
application/json:
schema:
type: object
example:
code: 500
message: you are not loggin
results: null

42
readme.md

@ -3,6 +3,7 @@
### Feature
- Send whatsapp via http API, [docs/openapi.yml](./docs/openapi.yaml) for more details
- Compress image before send
- Compress video before send
- Customizable port and debug mode
- `--port 8000`
- `--debug true`
@ -13,13 +14,16 @@
- Mac OS:
- `brew install vips`
- `brew install ffmpeg`
- `export CGO_CFLAGS_ALLOW="-Xpreprocessor"`
- Linux:
- `sudo apt update`
- `sudo apt install libvips-dev`
- `sudo apt install ffmpeg`
- Windows (not recomended, prefer using [WSL](https://docs.microsoft.com/en-us/windows/wsl/install)):
- install vips library, or you can check here https://www.libvips.org/install.html
- add to [environment variable](https://www.google.com/search?q=windows+add+to+environment+path)
- install ffmpeg, download [here](https://www.ffmpeg.org/download.html#build-windows)
- add to vips & ffmpg to [environment variable](https://www.google.com/search?q=windows+add+to+environment+path)
### How to use
@ -55,7 +59,7 @@
7. open `http://localhost:3000` in browser
### Production Mode (docker)
- `docker run --publish 3000:3000 --restart=always aldinokemal2104/go-whatsapp-web-multidevice`
- `docker run --publish=3000:3000 --name=whatsapp --restart=always --detach aldinokemal2104/go-whatsapp-web-multidevice --autoreply="Dont't reply this message please"`
### Production Mode (binary)
- download binary from [release](https://github.com/aldinokemal/go-whatsapp-web-multidevice/releases)
@ -65,19 +69,20 @@ You can fork or edit this source code !
### Current API
You can check [docs/openapi.yml](./docs/openapi.yaml) for detail API
| Feature | Menu | Method | URL | Payload |
|---------|-------------------------|--------|------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| ✅ | Login | GET | /app/login | |
| ✅ | Logout | GET | /app/logout | |
| ✅ | Reconnect | GET | /app/reconnect | |
| ✅ | User Info | GET | /user/info | <table> <thead> <tr> <th>Param</th> <th>Type</th> <th>Type</th> <th>Example</th> </tr></thead> <tbody> <tr> <td>phone</td><td>string</td><td>querystring</td><td>6289685024099</td></tr></tbody></table> |
| ✅ | User Avatar | GET | /user/avatar | <table> <thead> <tr> <th>Param</th> <th>Type</th> <th>Type</th> <th>Example</th> </tr></thead> <tbody> <tr> <td>phone</td><td>string</td><td>querystring</td><td>6289685024099</td></tr></tbody></table> |
| ✅ | User My Group List | GET | /user/my/groups | |
| ✅ | User My Privacy Setting | GET | /user/my/privacy | |
| ✅ | Send Message (Text) | POST | /send/message | <table> <thead> <tr> <th>Param</th> <th>Data Type</th> <th>Type</th> <th>Example</th> </tr></thead> <tbody> <tr> <td>phone</td><td>string</td><td>form-data</td><td>6289685024099</td></tr><tr> <td>message</td><td>string</td><td>form-data</td><td>Hello guys this is testing</td></tr><tr> <td>type</td><td>string (user/group)</td><td>form-data</td><td>user</td></tr></tbody></table> |
| ✅ | Send Message (Image) | POST | /send/image | <table> <thead> <tr> <th>Param</th> <th>Type</th> <th>Type</th> <th>Example</th> </tr></thead> <tbody> <tr> <td>phone</td><td>string</td><td>form-data</td><td>6289685024099</td></tr><tr> <td>caption</td><td>string</td><td>form-data</td><td>Hello guys this is caption</td></tr><tr> <td>view_once</td><td>bool</td><td>form-data</td><td>false</td></tr><tr> <td>image</td><td>binary</td><td>form-data</td><td>image/jpg,image/jpeg,image/png</td></tr><tr> <td>type</td><td>string (user/group)</td><td>form-data</td><td>user</td></tr></tbody></table> |
| ✅ | Send Message (File) | POST | /send/file | <table><thead><tr><th>Param</th><th>Type</th><th>Type</th><th>Example</th></tr></thead><tbody><tr><td>phone</td><td>string</td><td>form-data</td><td>6289685024099</td></tr><tr><td>file</td><td>binary</td><td>form-data</td><td>any (max: 10MB)</td></tr><tr> <td>type</td><td>string (user/group)</td><td>form-data</td><td>user</td></tr></tbody></table> |
| ❌ | Send Message (Video) | POST | /send/video | <table><thead><tr><th>Param</th><th>Type</th><th>Type</th><th>Example</th></tr></thead><tbody><tr><td>phone</td><td>string</td><td>form-data</td><td>6289685024099</td></tr><tr><td>video</td><td>binary</td><td>form-data</td><td>mp4/avi/mkv</td></tr><tr> <td>type</td><td>string (user/group)</td><td>form-data</td><td>user</td></tr></tbody></table> |
| 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 |
```
✅ = Available
@ -92,9 +97,10 @@ You can check [docs/openapi.yml](./docs/openapi.yaml) for detail API
4. Send Image ![Send Image](https://i.ibb.co/HDVJZSN/Screen-Shot-2022-02-13-at-12-59-06.png)
5. Send File ![Send File](https://i.ibb.co/XxNnsQ8/Screen-Shot-2022-02-13-at-12-59-14.png)
6. User Info ![User Info](https://i.ibb.co/BC0mNT7/Screen-Shot-2022-02-13-at-13-00-57.png)
6. User Avatar ![User Avatar](https://i.ibb.co/TkzPbLZ/Screen-Shot-2022-02-13-at-13-01-39.png)
7. User Privacy ![User My Privacy](https://i.ibb.co/RQcC5m9/Screen-Shot-2022-02-13-at-12-58-47.png)
8. User Group ![List Group](https://i.ibb.co/jfkgKdG/Screen-Shot-2022-05-12-at-21-12-06.png)
7. User Avatar ![User Avatar](https://i.ibb.co/TkzPbLZ/Screen-Shot-2022-02-13-at-13-01-39.png)
8. User Privacy ![User My Privacy](https://i.ibb.co/RQcC5m9/Screen-Shot-2022-02-13-at-12-58-47.png)
9. User Group ![List Group](https://i.ibb.co/jfkgKdG/Screen-Shot-2022-05-12-at-21-12-06.png)
10. Auto Reply ![Auto Reply](https://i.ibb.co/D4rTytX/IMG-20220517-162500.jpg)
### Mac OS NOTE

2
src/.air.toml

@ -2,4 +2,4 @@ root = '.'
tmp_dir = "tmp"
[build]
exclude_dir = ["statics"]
exclude_dir = ["statics", "views"]

10
src/cmd/root.go

@ -7,6 +7,7 @@ import (
"github.com/aldinokemal/go-whatsapp-web-multidevice/middleware"
"github.com/aldinokemal/go-whatsapp-web-multidevice/services"
"github.com/aldinokemal/go-whatsapp-web-multidevice/utils"
"github.com/dustin/go-humanize"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/cors"
"github.com/gofiber/fiber/v2/middleware/logger"
@ -49,7 +50,7 @@ func runRest(cmd *cobra.Command, args []string) {
engine := html.NewFileSystem(pkger.Dir("/views"), ".html")
app := fiber.New(fiber.Config{
Views: engine,
BodyLimit: 10 * 1024 * 1024,
BodyLimit: 50 * 1024 * 1024,
})
app.Static("/statics", "./statics")
app.Use(middleware.Recovery())
@ -79,7 +80,12 @@ func runRest(cmd *cobra.Command, args []string) {
userController.Route(app)
app.Get("/", func(ctx *fiber.Ctx) error {
return ctx.Render("index", fiber.Map{"AppHost": fmt.Sprintf("%s://%s", ctx.Protocol(), ctx.Hostname())})
return ctx.Render("index", fiber.Map{
"AppHost": fmt.Sprintf("%s://%s", ctx.Protocol(), ctx.Hostname()),
"AppVersion": config.AppVersion,
"MaxFileSize": humanize.Bytes(uint64(config.WhatsappSettingMaxFileSize)),
"MaxVideoSize": humanize.Bytes(uint64(config.WhatsappSettingMaxVideoSize)),
})
})
err = app.Listen(":" + config.AppPort)

7
src/config/settings.go

@ -3,14 +3,17 @@ package config
type Browser string
var (
AppVersion string = "3.3.0"
AppPort string = "3000"
AppDebug bool = false
PathQrCode string = "statics/images/qrcode"
PathSendItems string = "statics/images/senditems"
PathQrCode string = "statics/qrcode"
PathSendItems string = "statics/senditems"
DBName string = "hydrogenWaCli.db"
WhatsappLogLevel string = "ERROR"
WhatsappAutoReplyMessage string
WhatsappSettingMaxFileSize int64 = 30000000 // 10MB
WhatsappSettingMaxVideoSize int64 = 30000000 // 30MB
)

31
src/controllers/send_controller.go

@ -20,6 +20,7 @@ func (controller *SendController) Route(app *fiber.App) {
app.Post("/send/message", controller.SendText)
app.Post("/send/image", controller.SendImage)
app.Post("/send/file", controller.SendFile)
app.Post("/send/video", controller.SendVideo)
}
func (controller *SendController) SendText(c *fiber.Ctx) error {
@ -103,3 +104,33 @@ func (controller *SendController) SendFile(c *fiber.Ctx) error {
Results: response,
})
}
func (controller *SendController) SendVideo(c *fiber.Ctx) error {
var request structs.SendVideoRequest
err := c.BodyParser(&request)
utils.PanicIfNeeded(err)
video, err := c.FormFile("video")
utils.PanicIfNeeded(err)
request.Video = video
//add validation send image
validations.ValidateSendVideo(request)
if request.Type == structs.TypeGroup {
request.Phone = request.Phone + "@g.us"
} else {
request.Phone = request.Phone + "@s.whatsapp.net"
}
response, err := controller.Service.SendVideo(c, request)
utils.PanicIfNeeded(err)
return c.JSON(utils.ResponseData{
Code: 200,
Message: response.Status,
Results: response,
})
}

1
src/go.mod

@ -3,6 +3,7 @@ module github.com/aldinokemal/go-whatsapp-web-multidevice
go 1.17
require (
github.com/dustin/go-humanize v1.0.0
github.com/go-ozzo/ozzo-validation/v4 v4.3.0
github.com/gofiber/fiber/v2 v2.26.0
github.com/gofiber/template v1.6.22

2
src/go.sum

@ -106,6 +106,8 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385/go.mod h1:0vRUJqYpeSZifjYj7uP3BG/gKcuzL9xWVV/Y+cK33KM=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=

4
src/main.go

@ -1,7 +1,3 @@
/*
Copyright © 2022 NAME HERE <EMAIL ADDRESS>
*/
package main
import "github.com/aldinokemal/go-whatsapp-web-multidevice/cmd"

1
src/services/send_service.go

@ -9,4 +9,5 @@ type SendService interface {
SendText(c *fiber.Ctx, request structs.SendMessageRequest) (response structs.SendMessageResponse, err error)
SendImage(c *fiber.Ctx, request structs.SendImageRequest) (response structs.SendImageResponse, err error)
SendFile(c *fiber.Ctx, request structs.SendFileRequest) (response structs.SendFileResponse, err error)
SendVideo(c *fiber.Ctx, request structs.SendVideoRequest) (response structs.SendVideoResponse, err error)
}

111
src/services/send_service_impl.go

@ -8,12 +8,14 @@ import (
"github.com/aldinokemal/go-whatsapp-web-multidevice/structs"
"github.com/aldinokemal/go-whatsapp-web-multidevice/utils"
"github.com/gofiber/fiber/v2"
fiberUtils "github.com/gofiber/fiber/v2/utils"
"github.com/h2non/bimg"
"go.mau.fi/whatsmeow"
waProto "go.mau.fi/whatsmeow/binary/proto"
"google.golang.org/protobuf/proto"
"net/http"
"os"
"os/exec"
)
type SendServiceImpl struct {
@ -46,14 +48,15 @@ func (service SendServiceImpl) SendText(_ *fiber.Ctx, request structs.SendMessag
func (service SendServiceImpl) SendImage(c *fiber.Ctx, request structs.SendImageRequest) (response structs.SendImageResponse, err error) {
utils.MustLogin(service.WaCli)
// Resize image
// Save image to server
oriImagePath := fmt.Sprintf("%s/%s", config.PathSendItems, request.Image.Filename)
err = c.SaveFile(request.Image, oriImagePath)
if err != nil {
return response, err
}
// Resize image
openImageBuffer, err := bimg.Read(oriImagePath)
newImage, err := bimg.NewImage(openImageBuffer).Process(bimg.Options{Quality: 90, Width: 600, Height: 600, Embed: true})
newImage, err := bimg.NewImage(openImageBuffer).Process(bimg.Options{Quality: 90, Width: 600, Embed: true})
if err != nil {
return response, err
}
@ -96,7 +99,7 @@ func (service SendServiceImpl) SendImage(c *fiber.Ctx, request structs.SendImage
go func() {
errDelete := utils.RemoveFile(0, oriImagePath, newImagePath)
if errDelete != nil {
fmt.Println(errDelete)
fmt.Println("error when deleting picture: ", errDelete)
}
}()
if err != nil {
@ -110,7 +113,6 @@ func (service SendServiceImpl) SendImage(c *fiber.Ctx, request structs.SendImage
func (service SendServiceImpl) SendFile(c *fiber.Ctx, request structs.SendFileRequest) (response structs.SendFileResponse, err error) {
utils.MustLogin(service.WaCli)
// Resize image
oriFilePath := fmt.Sprintf("%s/%s", config.PathSendItems, request.File.Filename)
err = c.SaveFile(request.File, oriFilePath)
if err != nil {
@ -153,7 +155,106 @@ func (service SendServiceImpl) SendFile(c *fiber.Ctx, request structs.SendFileRe
if err != nil {
return response, err
} else {
response.Status = fmt.Sprintf("Message sent to %s (server timestamp: %s)", request.Phone, ts)
response.Status = fmt.Sprintf("Document sent to %s (server timestamp: %s)", request.Phone, ts)
return response, nil
}
}
func (service SendServiceImpl) SendVideo(c *fiber.Ctx, request structs.SendVideoRequest) (response structs.SendVideoResponse, err error) {
utils.MustLogin(service.WaCli)
var (
videoPath string
videoThumbnail string
deletedItems []string
)
generateUUID := fiberUtils.UUIDv4()
// Save video to server
oriVideoPath := fmt.Sprintf("%s/%s", config.PathSendItems, generateUUID+request.Video.Filename)
err = c.SaveFile(request.Video, oriVideoPath)
if err != nil {
return response, err
}
// Get thumbnail video with ffmpeg
thumbnailVideoPath := fmt.Sprintf("%s/%s", config.PathSendItems, generateUUID+".png")
cmdThumbnail := exec.Command("ffmpeg", "-i", oriVideoPath, "-ss", "00:00:01.000", "-vframes", "1", thumbnailVideoPath)
err = cmdThumbnail.Run()
utils.PanicIfNeeded(err, "error when getting thumbnail")
// Resize Thumbnail
openImageBuffer, err := bimg.Read(thumbnailVideoPath)
resize, err := bimg.NewImage(openImageBuffer).Process(bimg.Options{Quality: 90, Width: 600, Embed: true})
if err != nil {
return response, err
}
thumbnailResizeVideoPath := fmt.Sprintf("%s/%s", config.PathSendItems, generateUUID+"_resize.png")
err = bimg.Write(thumbnailResizeVideoPath, resize)
if err != nil {
return response, err
}
deletedItems = append(deletedItems, thumbnailVideoPath)
deletedItems = append(deletedItems, thumbnailResizeVideoPath)
videoThumbnail = thumbnailResizeVideoPath
if request.Compress {
compresVideoPath := fmt.Sprintf("%s/%s", config.PathSendItems, generateUUID+".mp4")
// Compress video with ffmpeg
cmdCompress := exec.Command("ffmpeg", "-i", oriVideoPath, "-strict", "-2", compresVideoPath)
err = cmdCompress.Run()
utils.PanicIfNeeded(err, "error when compress video")
videoPath = compresVideoPath
deletedItems = append(deletedItems, compresVideoPath)
} else {
videoPath = oriVideoPath
deletedItems = append(deletedItems, oriVideoPath)
}
//Send to WA server
dataWaRecipient, ok := utils.ParseJID(request.Phone)
if !ok {
return response, errors.New("invalid JID " + request.Phone)
}
dataWaVideo, err := os.ReadFile(videoPath)
if err != nil {
return response, err
}
uploadedFile, err := service.WaCli.Upload(context.Background(), dataWaVideo, whatsmeow.MediaVideo)
if err != nil {
fmt.Printf("Failed to upload file: %v", err)
return response, err
}
dataWaThumbnail, err := os.ReadFile(videoThumbnail)
if err != nil {
return response, err
}
msg := &waProto.Message{VideoMessage: &waProto.VideoMessage{
Url: proto.String(uploadedFile.URL),
Mimetype: proto.String(http.DetectContentType(dataWaVideo)),
Caption: proto.String(request.Caption),
FileLength: proto.Uint64(uploadedFile.FileLength),
FileSha256: uploadedFile.FileSHA256,
FileEncSha256: uploadedFile.FileEncSHA256,
MediaKey: uploadedFile.MediaKey,
DirectPath: proto.String(uploadedFile.DirectPath),
ViewOnce: proto.Bool(request.ViewOnce),
JpegThumbnail: dataWaThumbnail,
}}
ts, err := service.WaCli.SendMessage(dataWaRecipient, "", msg)
go func() {
errDelete := utils.RemoveFile(0, deletedItems...)
if errDelete != nil {
fmt.Println(errDelete)
}
}()
if err != nil {
return response, err
} else {
response.Status = fmt.Sprintf("Video sent to %s (server timestamp: %s)", request.Phone, ts)
return response, nil
}
}

0
src/statics/images/qrcode/.gitignore → src/statics/qrcode/.gitignore

0
src/statics/images/senditems/.gitignore → src/statics/senditems/.gitignore

17
src/structs/send_struct.go

@ -24,7 +24,7 @@ type SendImageRequest struct {
Caption string `json:"caption" form:"caption"`
Image *multipart.FileHeader `json:"image" form:"image"`
ViewOnce bool `json:"view_once" form:"view_once"`
Type SendType `json:"type" form:"message"`
Type SendType `json:"type" form:"type"`
}
type SendImageResponse struct {
@ -34,9 +34,22 @@ type SendImageResponse struct {
type SendFileRequest struct {
Phone string `json:"phone" form:"phone"`
File *multipart.FileHeader `json:"file" form:"file"`
Type SendType `json:"type" form:"message"`
Type SendType `json:"type" form:"type"`
}
type SendFileResponse struct {
Status string `json:"status"`
}
type SendVideoRequest struct {
Phone string `json:"phone" form:"phone"`
Caption string `json:"caption" form:"caption"`
Video *multipart.FileHeader `json:"video" form:"video"`
Type SendType `json:"type" form:"type"`
ViewOnce bool `json:"view_once" form:"view_once"`
Compress bool `json:"compress"`
}
type SendVideoResponse struct {
Status string `json:"status"`
}

6
src/utils/general.go

@ -13,9 +13,13 @@ func RemoveFile(delaySecond int, paths ...string) error {
}
for _, path := range paths {
if path != "" {
err := os.Remove(path)
if err != nil {
return err
}
}
}
return nil
}
@ -24,7 +28,9 @@ func CreateFolder(folderPath ...string) error {
for _, folder := range folderPath {
newFolder := filepath.Join(".", folder)
err := os.MkdirAll(newFolder, os.ModePerm)
if err != nil {
return err
}
}
return nil
}

48
src/validations/send_validation.go

@ -1,15 +1,17 @@
package validations
import (
"fmt"
"github.com/aldinokemal/go-whatsapp-web-multidevice/config"
"github.com/aldinokemal/go-whatsapp-web-multidevice/structs"
"github.com/aldinokemal/go-whatsapp-web-multidevice/utils"
"github.com/dustin/go-humanize"
validation "github.com/go-ozzo/ozzo-validation/v4"
"github.com/go-ozzo/ozzo-validation/v4/is"
)
func ValidateSendMessage(request structs.SendMessageRequest) {
err := validation.ValidateStruct(&request,
validation.Field(&request.Phone, validation.Required, is.Digit, validation.Length(10, 25)),
validation.Field(&request.Phone, validation.Required, validation.Length(10, 25)),
validation.Field(&request.Message, validation.Required, validation.Length(1, 50)),
)
@ -22,7 +24,7 @@ func ValidateSendMessage(request structs.SendMessageRequest) {
func ValidateSendImage(request structs.SendImageRequest) {
err := validation.ValidateStruct(&request,
validation.Field(&request.Phone, validation.Required, is.Digit, validation.Length(10, 25)),
validation.Field(&request.Phone, validation.Required, validation.Length(10, 25)),
validation.Field(&request.Caption, validation.When(true, validation.Length(1, 200))),
validation.Field(&request.Image, validation.Required),
)
@ -44,12 +46,11 @@ func ValidateSendImage(request structs.SendImageRequest) {
Message: "your image is not allowed. please use jpg/jpeg/png",
})
}
}
func ValidateSendFile(request structs.SendFileRequest) {
err := validation.ValidateStruct(&request,
validation.Field(&request.Phone, validation.Required, is.Digit, validation.Length(10, 25)),
validation.Field(&request.Phone, validation.Required, validation.Length(10, 25)),
validation.Field(&request.File, validation.Required),
)
@ -59,9 +60,42 @@ func ValidateSendFile(request structs.SendFileRequest) {
})
}
if request.File.Size > 10240000 { // 10MB
if request.File.Size > config.WhatsappSettingMaxFileSize { // 10MB
maxSizeString := humanize.Bytes(uint64(config.WhatsappSettingMaxFileSize))
panic(utils.ValidationError{
Message: fmt.Sprintf("max file upload is %s, please upload in cloud and send via text if your file is higher than %s", maxSizeString, maxSizeString),
})
}
}
func ValidateSendVideo(request structs.SendVideoRequest) {
err := validation.ValidateStruct(&request,
validation.Field(&request.Phone, validation.Required, validation.Length(10, 25)),
validation.Field(&request.Video, validation.Required),
)
if err != nil {
panic(utils.ValidationError{
Message: err.Error(),
})
}
availableMimes := map[string]bool{
"video/mp4": true,
"video/x-matroska": true,
"video/avi": true,
}
if !availableMimes[request.Video.Header.Get("Content-Type")] {
panic(utils.ValidationError{
Message: "your video type is not allowed. please use mp4/mkv",
})
}
if request.Video.Size > config.WhatsappSettingMaxVideoSize { // 30MB
maxSizeString := humanize.Bytes(uint64(config.WhatsappSettingMaxVideoSize))
panic(utils.ValidationError{
Message: "max file upload is 10MB, please upload in cloud and send via text if your file is higher than 10MB",
Message: fmt.Sprintf("max video upload is %s, please upload in cloud and send via text if your file is higher than %s", maxSizeString, maxSizeString),
})
}
}

21
src/validations/send_validation_test.go

@ -2,7 +2,6 @@ package validations
import (
"github.com/aldinokemal/go-whatsapp-web-multidevice/structs"
"github.com/aldinokemal/go-whatsapp-web-multidevice/utils"
"github.com/stretchr/testify/assert"
"testing"
)
@ -24,26 +23,6 @@ func TestValidateSendMessage(t *testing.T) {
}},
err: nil,
},
{
name: "error invalid phone",
args: args{request: structs.SendMessageRequest{
Phone: "some-random-phone",
Message: "Hello this is testing",
}},
err: utils.ValidationError{
Message: "phone: must contain digits only.",
},
},
{
name: "error invalid phone contains dash (-)",
args: args{request: structs.SendMessageRequest{
Phone: "6289-748-291",
Message: "Hello this is testing",
}},
err: utils.ValidationError{
Message: "phone: must contain digits only.",
},
},
}
for _, tt := range tests {

199
src/views/index.html

@ -27,11 +27,10 @@
<h1 class="ui dividing header">[[ app_name ]]</h1>
<h3 class="first">Features</h3>
<div class="ui three cards">
<div class="ui three column stackable grid cards">
<div class="green card" @click="loginModal" style="cursor: pointer">
<div class="content">
<div class="header">Login</div>
<div class="meta">App</div>
<div class="description">
Scan your QRCode and you can use all this API feature
</div>
@ -40,7 +39,6 @@
<div class="green card" @click="logoutProcess" style="cursor: pointer">
<div class="content">
<div class="header">Logout</div>
<div class="meta">App</div>
<div class="description">
Remove your login session in application
</div>
@ -49,7 +47,6 @@
<div class="green card" @click="reconnectProcess" style="cursor: pointer">
<div class="content">
<div class="header">Reconnect</div>
<div class="meta">App</div>
<div class="description">
Reconnect to whatsapp server, please do this if your api doesn't work or your application is down or
restart
@ -58,53 +55,55 @@
</div>
</div>
<div class="ui three cards">
<div class="green card" @click="sendMessageModal('user')" style="cursor: pointer">
<div class="ui four column doubling grid cards">
<div class="green card" @click="infoModal" style="cursor: pointer;">
<div class="content">
<div class="header">Send Message (Text) | Private Message</div>
<div class="meta">App</div>
<div class="header">User Info</div>
<div class="description">
Send any message to any whatsapp number
You can search someone user info by phone
</div>
</div>
</div>
<div class="green card" @click="sendImageModal('user')" style="cursor:pointer;">
<div class="green card" @click="avatarModal" style="cursor: pointer;">
<div class="content">
<div class="header">Send Message (Image) | Private Message</div>
<div class="meta">App</div>
<div class="header">User Avatar</div>
<div class="description">
Send image with
<div class="ui blue horizontal label">jpg/jpeg/png</div>
type
You can search someone avatar by phone
</div>
</div>
</div>
<div class="green card" @click="sendFileModal('user')" style="cursor: pointer">
<div class="green card" @click="groupModal" style="cursor: pointer">
<div class="content">
<div class="header">Send Message (File) | Private Message</div>
<div class="meta">App</div>
<div class="header">User List Groups</div>
<div class="description">
Send any file up to
<div class="ui blue horizontal label">10MB</div>
Display all groups you joined
</div>
</div>
</div>
<div class="green card" @click="privacyModal" style="cursor: pointer">
<div class="content">
<div class="header">User Privacy Setting</div>
<div class="description">
Get your privacy settings
</div>
</div>
</div>
</div>
<div class="ui three cards">
<div class="green card" @click="sendMessageModal('group')" style="cursor: pointer">
<div class="ui four column doubling grid cards">
<div class="green card" @click="sendMessageModal('user')" style="cursor: pointer">
<div class="content">
<div class="header">Send Message (Text) | Group</div>
<div class="meta">App</div>
<a class="ui blue right ribbon label">Private</a>
<div class="header">Send Message </div>
<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="green card" @click="sendImageModal('user')" style="cursor:pointer;">
<div class="content">
<div class="header">Send Message (Image) | Group</div>
<div class="meta">App</div>
<a class="ui blue right ribbon label">Private</a>
<div class="header">Send Image</div>
<div class="description">
Send image with
<div class="ui blue horizontal label">jpg/jpeg/png</div>
@ -112,52 +111,66 @@
</div>
</div>
</div>
<div class="green card" @click="sendFileModal('group')" style="cursor: pointer">
<div class="green card" @click="sendFileModal('user')" style="cursor: pointer">
<div class="content">
<div class="header">Send Message (File) | Group</div>
<div class="meta">App</div>
<a class="ui blue right ribbon label">Private</a>
<div class="header">Send File</div>
<div class="description">
Send any file up to
<div class="ui blue horizontal label">10MB</div>
<div class="ui blue horizontal label">{{ .MaxFileSize }}</div>
</div>
</div>
</div>
<div class="green card" @click="sendVideoModal('user')" style="cursor: pointer">
<div class="content">
<a class="ui blue right ribbon label">Private</a>
<div class="header">Send Video</div>
<div class="description">
Send video <div class="ui blue horizontal label">mp4</div> up to
<div class="ui blue horizontal label">{{ .MaxVideoSize }}</div>
</div>
</div>
</div>
</div>
<div class="ui four cards">
<div class="green card" @click="infoModal" style="cursor: pointer;">
<div class="ui four column doubling grid cards">
<div class="green card" @click="sendMessageModal('group')" style="cursor: pointer">
<div class="content">
<div class="header">User Info</div>
<div class="meta">App</div>
<a class="ui teal right ribbon label">Group</a>
<div class="header">Send Message</div>
<div class="description">
You can search someone user info by phone
Send any message to any whatsapp number
</div>
</div>
</div>
<div class="green card" @click="avatarModal" style="cursor: pointer;">
<div class="green card" @click="sendImageModal('group')" style="cursor:pointer;">
<div class="content">
<div class="header">User Avatar</div>
<div class="meta">App</div>
<a class="ui teal right ribbon label">Group</a>
<div class="header">Send Image</div>
<div class="description">
You can search someone avatar by phone
Send image with
<div class="ui teal horizontal label">jpg/jpeg/png</div>
type
</div>
</div>
</div>
<div class="green card" @click="groupModal" style="cursor: pointer">
<div class="green card" @click="sendFileModal('group')" style="cursor: pointer">
<div class="content">
<div class="header">User List Groups</div>
<div class="meta">App</div>
<a class="ui teal right ribbon label">Group</a>
<div class="header">Send File</div>
<div class="description">
Display all groups you joined
Send any file up to
<div class="ui teal horizontal label">{{ .MaxFileSize }}</div>
</div>
</div>
</div>
<div class="green card" @click="privacyModal" style="cursor: pointer">
<div class="green card" @click="sendVideoModal('group')" style="cursor: pointer">
<div class="content">
<div class="header">User Privacy Setting</div>
<div class="meta">App</div>
<a class="ui teal right ribbon label">Group</a>
<div class="header">Send Video</div>
<div class="description">
Get your privacy settings
Send video <div class="ui teal horizontal label">mp4</div> up to
<div class="ui teal horizontal label">{{ .MaxVideoSize }}</div>
</div>
</div>
</div>
@ -283,6 +296,37 @@
</div>
</div>
<!-- Modal SendFile -->
<div class="ui small modal" id="modalSendVideo">
<i class="close icon"></i>
<div class="header">
Send Video
</div>
<div class="content">
<form class="ui form">
<div class="field">
<label>Phone / Group ID</label>
<input v-model="video_phone" type="text" placeholder="6289..."
aria-label="phone">
</div>
<div class="field" style="padding-bottom: 30px">
<label>Video</label>
<input type="file" class="inputfile" id="video_file" style="display: none" accept="video/*"/>
<label for="video_file" class="ui positive medium green left floated button" style="color: white">
<i class="ui upload icon"></i>
Upload video
</label>
</div>
</form>
</div>
<div class="actions">
<div class="ui positive right labeled icon button" @click="sendVideoProcess">
Send
<i class="send icon"></i>
</div>
</div>
</div>
<!-- Modal UserGroup -->
<div class="ui small modal" id="modalUserGroup">
<i class="close icon"></i>
@ -301,7 +345,7 @@
</thead>
<tbody v-if="data_groups != null">
<tr v-for="g in data_groups">
<td data-label="GID">[[ g.JID.User ]]</td>
<td data-label="GID">[[ g.JID.split('@')[0] ]]</td>
<td data-label="Name">[[ g.Name ]]</td>
<td data-label="Age">[[ g.Participants.length ]]</td>
<td data-label="Job">[[ g.GroupCreated ]]</td>
@ -666,6 +710,59 @@
}
}
const sendVideo = {
data() {
return {
video_type: 'user',
video_phone: '',
}
},
methods: {
sendVideoModal(type) {
this.video_type = type
$('#modalSendVideo').modal('show');
},
async sendVideoProcess() {
try {
let response = await this.sendVideoApi()
showSuccessInfo(response)
} catch (err) {
showErrorInfo(err)
}
},
sendVideoApi() {
return new Promise(async (resolve, reject) => {
try {
let payload = new FormData();
payload.append("phone", this.video_phone)
payload.append("video", $("#video_file")[0].files[0])
payload.append("type", this.video_type)
let response = await axios.post(`${this.app_host}/send/video`, payload, {
// Axios Bug, always content-type that make boundary not set default by browser https://github.com/axios/axios/issues/1603
transformRequest: (data, headers) => {
delete headers.post['Content-Type'];
return data;
}
})
this.sendVideoReset();
resolve(response.data.message)
} catch (error) {
if (error.response) {
reject(error.response.data.message)
} else {
reject(error.message)
}
}
})
},
sendVideoReset() {
this.video_phone = '';
this.video_type = 'user';
$("#video_file").val('');
},
}
}
const userGroups = {
data() {
return {
@ -843,10 +940,10 @@
data() {
return {
app_host: {{ .AppHost }},
app_name: 'Whatsapp API Multi Device App'
app_name: 'Whatsapp API Multi Device App ({{ .AppVersion }})'
}
},
mixins: [login, logout, reconnect, sendMessage, sendImage, sendFile, userGroups, userPrivacy, userAvatar, userInfo]
mixins: [login, logout, reconnect, sendMessage, sendImage, sendFile, sendVideo, userGroups, userPrivacy, userAvatar, userInfo]
}).mount('#app')
</script>
</body>
Loading…
Cancel
Save