Browse Source
feat: introduce domain (#28)
feat: introduce domain (#28)
* feat: introduce domain * fix: context parsing * feat: add fetch all connected devices * feat: move controller to transport layer rest * feat: change struct service name * feat: add middleware JID * feat: remove unsed packagepull/31/head v3.9.0
committed by
GitHub
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 898 additions and 833 deletions
-
31src/cmd/root.go
-
8src/config/settings.go
-
58src/controllers/app_controller.go
-
12src/domains/app/app.go
-
6src/domains/app/devices.go
-
6src/domains/app/login.go
-
12src/domains/send/contact.go
-
13src/domains/send/file.go
-
16src/domains/send/image.go
-
18src/domains/send/send.go
-
11src/domains/send/text.go
-
16src/domains/send/video.go
-
48src/domains/user/account.go
-
12src/domains/user/user.go
-
2src/go.mod
-
6src/go.sum
-
69src/internal/rest/app_rest.go
-
66src/internal/rest/send_controller.go
-
41src/internal/rest/user_controller.go
-
21src/middleware/select_jid.go
-
145src/services/app_service.go
-
118src/services/app_service_impl.go
-
312src/services/send_service.go
-
310src/services/send_service_impl.go
-
115src/services/user_service.go
-
114src/services/user_service_impl.go
-
67src/structs/send_struct.go
-
48src/structs/user_struct.go
-
2src/utils/whatsapp.go
-
12src/validations/send_validation.go
-
6src/validations/send_validation_test.go
-
6src/validations/user_validation.go
-
4src/views/index.html
@ -1,58 +0,0 @@ |
|||||
package controllers |
|
||||
|
|
||||
import ( |
|
||||
"fmt" |
|
||||
"github.com/aldinokemal/go-whatsapp-web-multidevice/services" |
|
||||
"github.com/aldinokemal/go-whatsapp-web-multidevice/utils" |
|
||||
"github.com/gofiber/fiber/v2" |
|
||||
) |
|
||||
|
|
||||
type AppController struct { |
|
||||
Service services.AppService |
|
||||
} |
|
||||
|
|
||||
func NewAppController(service services.AppService) AppController { |
|
||||
return AppController{Service: service} |
|
||||
} |
|
||||
|
|
||||
func (controller *AppController) Route(app *fiber.App) { |
|
||||
app.Get("/app/login", controller.Login) |
|
||||
app.Get("/app/logout", controller.Logout) |
|
||||
app.Get("/app/reconnect", controller.Reconnect) |
|
||||
} |
|
||||
|
|
||||
func (controller *AppController) Login(c *fiber.Ctx) error { |
|
||||
response, err := controller.Service.Login(c) |
|
||||
utils.PanicIfNeeded(err) |
|
||||
|
|
||||
return c.JSON(utils.ResponseData{ |
|
||||
Code: 200, |
|
||||
Message: "Success", |
|
||||
Results: map[string]interface{}{ |
|
||||
"qr_link": fmt.Sprintf("%s://%s/%s", c.Protocol(), c.Hostname(), response.ImagePath), |
|
||||
"qr_duration": response.Duration, |
|
||||
}, |
|
||||
}) |
|
||||
} |
|
||||
|
|
||||
func (controller *AppController) Logout(c *fiber.Ctx) error { |
|
||||
err := controller.Service.Logout(c) |
|
||||
utils.PanicIfNeeded(err) |
|
||||
|
|
||||
return c.JSON(utils.ResponseData{ |
|
||||
Code: 200, |
|
||||
Message: "Success logout", |
|
||||
Results: nil, |
|
||||
}) |
|
||||
} |
|
||||
|
|
||||
func (controller *AppController) Reconnect(c *fiber.Ctx) error { |
|
||||
err := controller.Service.Reconnect(c) |
|
||||
utils.PanicIfNeeded(err) |
|
||||
|
|
||||
return c.JSON(utils.ResponseData{ |
|
||||
Code: 200, |
|
||||
Message: "Reconnect success", |
|
||||
Results: nil, |
|
||||
}) |
|
||||
} |
|
||||
@ -0,0 +1,12 @@ |
|||||
|
package app |
||||
|
|
||||
|
import ( |
||||
|
"context" |
||||
|
) |
||||
|
|
||||
|
type IAppService interface { |
||||
|
Login(ctx context.Context) (response LoginResponse, err error) |
||||
|
Logout(ctx context.Context) (err error) |
||||
|
Reconnect(ctx context.Context) (err error) |
||||
|
FetchDevices(ctx context.Context) (response []FetchDevicesResponse, err error) |
||||
|
} |
||||
@ -0,0 +1,6 @@ |
|||||
|
package app |
||||
|
|
||||
|
type FetchDevicesResponse struct { |
||||
|
Name string |
||||
|
Device string |
||||
|
} |
||||
@ -1,8 +1,6 @@ |
|||||
package structs |
|
||||
|
package app |
||||
|
|
||||
import ( |
|
||||
"time" |
|
||||
) |
|
||||
|
import "time" |
||||
|
|
||||
type LoginResponse struct { |
type LoginResponse struct { |
||||
ImagePath string `json:"image_path"` |
ImagePath string `json:"image_path"` |
||||
@ -0,0 +1,12 @@ |
|||||
|
package send |
||||
|
|
||||
|
type ContactRequest struct { |
||||
|
Phone string `json:"phone" form:"phone"` |
||||
|
ContactName string `json:"contact_name" form:"contact_name"` |
||||
|
ContactPhone string `json:"contact_phone" form:"contact_phone"` |
||||
|
Type Type `json:"type" form:"type"` |
||||
|
} |
||||
|
|
||||
|
type ContactResponse struct { |
||||
|
Status string `json:"status"` |
||||
|
} |
||||
@ -0,0 +1,13 @@ |
|||||
|
package send |
||||
|
|
||||
|
import "mime/multipart" |
||||
|
|
||||
|
type FileRequest struct { |
||||
|
Phone string `json:"phone" form:"phone"` |
||||
|
File *multipart.FileHeader `json:"file" form:"file"` |
||||
|
Type Type `json:"type" form:"type"` |
||||
|
} |
||||
|
|
||||
|
type FileResponse struct { |
||||
|
Status string `json:"status"` |
||||
|
} |
||||
@ -0,0 +1,16 @@ |
|||||
|
package send |
||||
|
|
||||
|
import "mime/multipart" |
||||
|
|
||||
|
type ImageRequest struct { |
||||
|
Phone string `json:"phone" form:"phone"` |
||||
|
Caption string `json:"caption" form:"caption"` |
||||
|
Image *multipart.FileHeader `json:"image" form:"image"` |
||||
|
ViewOnce bool `json:"view_once" form:"view_once"` |
||||
|
Type Type `json:"type" form:"type"` |
||||
|
Compress bool `json:"compress"` |
||||
|
} |
||||
|
|
||||
|
type ImageResponse struct { |
||||
|
Status string `json:"status"` |
||||
|
} |
||||
@ -0,0 +1,18 @@ |
|||||
|
package send |
||||
|
|
||||
|
import ( |
||||
|
"context" |
||||
|
) |
||||
|
|
||||
|
type Type string |
||||
|
|
||||
|
const TypeUser Type = "user" |
||||
|
const TypeGroup Type = "group" |
||||
|
|
||||
|
type ISendService interface { |
||||
|
SendText(ctx context.Context, request MessageRequest) (response MessageResponse, err error) |
||||
|
SendImage(ctx context.Context, request ImageRequest) (response ImageResponse, err error) |
||||
|
SendFile(ctx context.Context, request FileRequest) (response FileResponse, err error) |
||||
|
SendVideo(ctx context.Context, request VideoRequest) (response VideoResponse, err error) |
||||
|
SendContact(ctx context.Context, request ContactRequest) (response ContactResponse, err error) |
||||
|
} |
||||
@ -0,0 +1,11 @@ |
|||||
|
package send |
||||
|
|
||||
|
type MessageRequest struct { |
||||
|
Phone string `json:"phone" form:"phone"` |
||||
|
Message string `json:"message" form:"message"` |
||||
|
Type Type `json:"type" form:"type"` |
||||
|
} |
||||
|
|
||||
|
type MessageResponse struct { |
||||
|
Status string `json:"status"` |
||||
|
} |
||||
@ -0,0 +1,16 @@ |
|||||
|
package send |
||||
|
|
||||
|
import "mime/multipart" |
||||
|
|
||||
|
type VideoRequest struct { |
||||
|
Phone string `json:"phone" form:"phone"` |
||||
|
Caption string `json:"caption" form:"caption"` |
||||
|
Video *multipart.FileHeader `json:"video" form:"video"` |
||||
|
Type Type `json:"type" form:"type"` |
||||
|
ViewOnce bool `json:"view_once" form:"view_once"` |
||||
|
Compress bool `json:"compress"` |
||||
|
} |
||||
|
|
||||
|
type VideoResponse struct { |
||||
|
Status string `json:"status"` |
||||
|
} |
||||
@ -0,0 +1,48 @@ |
|||||
|
package user |
||||
|
|
||||
|
import "go.mau.fi/whatsmeow/types" |
||||
|
|
||||
|
type InfoRequest struct { |
||||
|
Phone string `json:"phone" query:"phone"` |
||||
|
} |
||||
|
|
||||
|
type InfoResponseDataDevice struct { |
||||
|
User string |
||||
|
Agent uint8 |
||||
|
Device string |
||||
|
Server string |
||||
|
AD bool |
||||
|
} |
||||
|
|
||||
|
type InfoResponseData struct { |
||||
|
VerifiedName string `json:"verified_name"` |
||||
|
Status string `json:"status"` |
||||
|
PictureID string `json:"picture_id"` |
||||
|
Devices []InfoResponseDataDevice `json:"devices"` |
||||
|
} |
||||
|
|
||||
|
type InfoResponse struct { |
||||
|
Data []InfoResponseData `json:"data"` |
||||
|
} |
||||
|
|
||||
|
type AvatarRequest struct { |
||||
|
Phone string `json:"phone" query:"phone"` |
||||
|
} |
||||
|
|
||||
|
type AvatarResponse struct { |
||||
|
URL string `json:"url"` |
||||
|
ID string `json:"id"` |
||||
|
Type string `json:"type"` |
||||
|
} |
||||
|
|
||||
|
type MyPrivacySettingResponse struct { |
||||
|
GroupAdd string `json:"group_add"` |
||||
|
LastSeen string `json:"last_seen"` |
||||
|
Status string `json:"status"` |
||||
|
Profile string `json:"profile"` |
||||
|
ReadReceipts string `json:"read_receipts"` |
||||
|
} |
||||
|
|
||||
|
type MyListGroupsResponse struct { |
||||
|
Data []types.GroupInfo `json:"data"` |
||||
|
} |
||||
@ -0,0 +1,12 @@ |
|||||
|
package user |
||||
|
|
||||
|
import ( |
||||
|
"context" |
||||
|
) |
||||
|
|
||||
|
type IUserService interface { |
||||
|
Info(ctx context.Context, request InfoRequest) (response InfoResponse, err error) |
||||
|
Avatar(ctx context.Context, request AvatarRequest) (response AvatarResponse, err error) |
||||
|
MyListGroups(ctx context.Context) (response MyListGroupsResponse, err error) |
||||
|
MyPrivacySetting(ctx context.Context) (response MyPrivacySettingResponse, err error) |
||||
|
} |
||||
@ -0,0 +1,69 @@ |
|||||
|
package rest |
||||
|
|
||||
|
import ( |
||||
|
"fmt" |
||||
|
domainApp "github.com/aldinokemal/go-whatsapp-web-multidevice/domains/app" |
||||
|
"github.com/aldinokemal/go-whatsapp-web-multidevice/utils" |
||||
|
"github.com/gofiber/fiber/v2" |
||||
|
) |
||||
|
|
||||
|
type App struct { |
||||
|
Service domainApp.IAppService |
||||
|
} |
||||
|
|
||||
|
func InitRestApp(app *fiber.App, service domainApp.IAppService) App { |
||||
|
rest := App{Service: service} |
||||
|
app.Get("/app/login", rest.Login) |
||||
|
app.Get("/app/logout", rest.Logout) |
||||
|
app.Get("/app/reconnect", rest.Reconnect) |
||||
|
app.Get("/app/devices", rest.Devices) |
||||
|
|
||||
|
return App{Service: service} |
||||
|
} |
||||
|
|
||||
|
func (controller *App) Login(c *fiber.Ctx) error { |
||||
|
response, err := controller.Service.Login(c.UserContext()) |
||||
|
utils.PanicIfNeeded(err) |
||||
|
|
||||
|
return c.JSON(utils.ResponseData{ |
||||
|
Code: 200, |
||||
|
Message: "Success", |
||||
|
Results: map[string]interface{}{ |
||||
|
"qr_link": fmt.Sprintf("%s://%s/%s", c.Protocol(), c.Hostname(), response.ImagePath), |
||||
|
"qr_duration": response.Duration, |
||||
|
}, |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
func (controller *App) Logout(c *fiber.Ctx) error { |
||||
|
err := controller.Service.Logout(c.UserContext()) |
||||
|
utils.PanicIfNeeded(err) |
||||
|
|
||||
|
return c.JSON(utils.ResponseData{ |
||||
|
Code: 200, |
||||
|
Message: "Success logout", |
||||
|
Results: nil, |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
func (controller *App) Reconnect(c *fiber.Ctx) error { |
||||
|
err := controller.Service.Reconnect(c.UserContext()) |
||||
|
utils.PanicIfNeeded(err) |
||||
|
|
||||
|
return c.JSON(utils.ResponseData{ |
||||
|
Code: 200, |
||||
|
Message: "Reconnect success", |
||||
|
Results: nil, |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
func (controller *App) Devices(c *fiber.Ctx) error { |
||||
|
devices, err := controller.Service.FetchDevices(c.UserContext()) |
||||
|
utils.PanicIfNeeded(err) |
||||
|
|
||||
|
return c.JSON(utils.ResponseData{ |
||||
|
Code: 200, |
||||
|
Message: "Fetch device success", |
||||
|
Results: devices, |
||||
|
}) |
||||
|
} |
||||
@ -0,0 +1,21 @@ |
|||||
|
package middleware |
||||
|
|
||||
|
import ( |
||||
|
"context" |
||||
|
"github.com/aldinokemal/go-whatsapp-web-multidevice/config" |
||||
|
"github.com/gofiber/fiber/v2" |
||||
|
) |
||||
|
|
||||
|
func SelectJid() fiber.Handler { |
||||
|
return func(c *fiber.Ctx) error { |
||||
|
selectedJid := string(c.Request().Header.Peek("jid")) |
||||
|
if selectedJid == "" { |
||||
|
selectedJid = config.AppDefaultDevice |
||||
|
} |
||||
|
|
||||
|
ctx := context.WithValue(c.Context(), config.AppSelectedDeviceKey, selectedJid) |
||||
|
c.SetUserContext(ctx) |
||||
|
|
||||
|
return c.Next() |
||||
|
} |
||||
|
} |
||||
@ -1,12 +1,145 @@ |
|||||
package services |
package services |
||||
|
|
||||
import ( |
import ( |
||||
"github.com/aldinokemal/go-whatsapp-web-multidevice/structs" |
|
||||
"github.com/gofiber/fiber/v2" |
|
||||
|
"context" |
||||
|
"errors" |
||||
|
"fmt" |
||||
|
"github.com/aldinokemal/go-whatsapp-web-multidevice/config" |
||||
|
domainApp "github.com/aldinokemal/go-whatsapp-web-multidevice/domains/app" |
||||
|
fiberUtils "github.com/gofiber/fiber/v2/utils" |
||||
|
"github.com/skip2/go-qrcode" |
||||
|
"go.mau.fi/whatsmeow" |
||||
|
"go.mau.fi/whatsmeow/store/sqlstore" |
||||
|
"os" |
||||
|
"path/filepath" |
||||
|
"time" |
||||
) |
) |
||||
|
|
||||
type AppService interface { |
|
||||
Login(c *fiber.Ctx) (response structs.LoginResponse, err error) |
|
||||
Logout(c *fiber.Ctx) (err error) |
|
||||
Reconnect(c *fiber.Ctx) (err error) |
|
||||
|
type serviceApp struct { |
||||
|
WaCli *whatsmeow.Client |
||||
|
db *sqlstore.Container |
||||
|
} |
||||
|
|
||||
|
func NewAppService(waCli *whatsmeow.Client, db *sqlstore.Container) domainApp.IAppService { |
||||
|
return &serviceApp{ |
||||
|
WaCli: waCli, |
||||
|
db: db, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func (service serviceApp) Login(_ context.Context) (response domainApp.LoginResponse, err error) { |
||||
|
if service.WaCli == nil { |
||||
|
return response, errors.New("wa cli nil cok") |
||||
|
} |
||||
|
|
||||
|
// Disconnect for reconnecting
|
||||
|
service.WaCli.Disconnect() |
||||
|
|
||||
|
chImage := make(chan string) |
||||
|
|
||||
|
ch, err := service.WaCli.GetQRChannel(context.Background()) |
||||
|
if err != nil { |
||||
|
// This error means that we're already logged in, so ignore it.
|
||||
|
if errors.Is(err, whatsmeow.ErrQRStoreContainsID) { |
||||
|
_ = service.WaCli.Connect() // just connect to websocket
|
||||
|
if service.WaCli.IsLoggedIn() { |
||||
|
return response, errors.New("you already logged in :)") |
||||
|
} |
||||
|
return response, errors.New("your session have been saved, please wait to connect 2 second and refresh again") |
||||
|
} else { |
||||
|
return response, errors.New("Error when GetQRChannel:" + err.Error()) |
||||
|
} |
||||
|
} else { |
||||
|
go func() { |
||||
|
for evt := range ch { |
||||
|
response.Code = evt.Code |
||||
|
response.Duration = evt.Timeout / time.Second / 2 |
||||
|
if evt.Event == "code" { |
||||
|
qrPath := fmt.Sprintf("%s/scan-qr-%s.png", config.PathQrCode, fiberUtils.UUIDv4()) |
||||
|
err = qrcode.WriteFile(evt.Code, qrcode.Medium, 512, qrPath) |
||||
|
if err != nil { |
||||
|
fmt.Println("error when write qrImage file", err.Error()) |
||||
|
} |
||||
|
go func() { |
||||
|
time.Sleep(response.Duration * time.Second) |
||||
|
err := os.Remove(qrPath) |
||||
|
if err != nil { |
||||
|
fmt.Println("Failed to remove qrPath " + qrPath) |
||||
|
} |
||||
|
}() |
||||
|
chImage <- qrPath |
||||
|
} else { |
||||
|
fmt.Printf("QR channel result: %s", evt.Event) |
||||
|
} |
||||
|
} |
||||
|
}() |
||||
|
} |
||||
|
|
||||
|
err = service.WaCli.Connect() |
||||
|
if err != nil { |
||||
|
return response, errors.New("Failed to connect bro " + err.Error()) |
||||
|
} |
||||
|
response.ImagePath = <-chImage |
||||
|
|
||||
|
return response, nil |
||||
|
} |
||||
|
|
||||
|
func (service serviceApp) Logout(_ context.Context) (err error) { |
||||
|
// delete history
|
||||
|
files, err := filepath.Glob("./history-*") |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
|
||||
|
for _, f := range files { |
||||
|
err = os.Remove(f) |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
} |
||||
|
// delete qr images
|
||||
|
qrImages, err := filepath.Glob("./statics/images/qrcode/scan-*") |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
|
||||
|
for _, f := range qrImages { |
||||
|
err = os.Remove(f) |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
err = service.WaCli.Logout() |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
func (service serviceApp) Reconnect(_ context.Context) (err error) { |
||||
|
service.WaCli.Disconnect() |
||||
|
return service.WaCli.Connect() |
||||
|
} |
||||
|
|
||||
|
func (service serviceApp) FetchDevices(ctx context.Context) (response []domainApp.FetchDevicesResponse, err error) { |
||||
|
if service.WaCli == nil { |
||||
|
return response, errors.New("wa cli nil cok") |
||||
|
} |
||||
|
|
||||
|
devices, err := service.db.GetAllDevices() |
||||
|
if err != nil { |
||||
|
return nil, err |
||||
|
} |
||||
|
|
||||
|
for _, device := range devices { |
||||
|
var d domainApp.FetchDevicesResponse |
||||
|
d.Device = device.ID.String() |
||||
|
if device.PushName != "" { |
||||
|
d.Name = device.PushName |
||||
|
} else { |
||||
|
d.Name = device.BusinessName |
||||
|
} |
||||
|
|
||||
|
response = append(response, d) |
||||
|
} |
||||
|
|
||||
|
return response, nil |
||||
} |
} |
||||
@ -1,118 +0,0 @@ |
|||||
package services |
|
||||
|
|
||||
import ( |
|
||||
"context" |
|
||||
"errors" |
|
||||
"fmt" |
|
||||
"github.com/aldinokemal/go-whatsapp-web-multidevice/config" |
|
||||
"github.com/aldinokemal/go-whatsapp-web-multidevice/structs" |
|
||||
"github.com/gofiber/fiber/v2" |
|
||||
fiberutils "github.com/gofiber/fiber/v2/utils" |
|
||||
"github.com/skip2/go-qrcode" |
|
||||
"go.mau.fi/whatsmeow" |
|
||||
"os" |
|
||||
"path/filepath" |
|
||||
"time" |
|
||||
) |
|
||||
|
|
||||
type AppServiceImpl struct { |
|
||||
WaCli *whatsmeow.Client |
|
||||
} |
|
||||
|
|
||||
func NewAppService(waCli *whatsmeow.Client) AppService { |
|
||||
return &AppServiceImpl{ |
|
||||
WaCli: waCli, |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
func (service AppServiceImpl) Login(c *fiber.Ctx) (response structs.LoginResponse, err error) { |
|
||||
if service.WaCli == nil { |
|
||||
return response, errors.New("wa cli nil cok") |
|
||||
} |
|
||||
|
|
||||
// Disconnect for reconnecting
|
|
||||
service.WaCli.Disconnect() |
|
||||
|
|
||||
chImage := make(chan string) |
|
||||
|
|
||||
ch, err := service.WaCli.GetQRChannel(context.Background()) |
|
||||
if err != nil { |
|
||||
// This error means that we're already logged in, so ignore it.
|
|
||||
if errors.Is(err, whatsmeow.ErrQRStoreContainsID) { |
|
||||
_ = service.WaCli.Connect() // just connect to websocket
|
|
||||
if service.WaCli.IsLoggedIn() { |
|
||||
return response, errors.New("you already logged in :)") |
|
||||
} |
|
||||
return response, errors.New("your session have been saved, please wait to connect 2 second and refresh again") |
|
||||
} else { |
|
||||
return response, errors.New("Error when GetQRChannel:" + err.Error()) |
|
||||
} |
|
||||
} else { |
|
||||
go func() { |
|
||||
for evt := range ch { |
|
||||
response.Code = evt.Code |
|
||||
response.Duration = evt.Timeout / time.Second / 2 |
|
||||
if evt.Event == "code" { |
|
||||
qrPath := fmt.Sprintf("%s/scan-qr-%s.png", config.PathQrCode, fiberutils.UUIDv4()) |
|
||||
err = qrcode.WriteFile(evt.Code, qrcode.Medium, 512, qrPath) |
|
||||
if err != nil { |
|
||||
fmt.Println("error when write qrImage file", err.Error()) |
|
||||
} |
|
||||
go func() { |
|
||||
time.Sleep(response.Duration * time.Second) |
|
||||
err := os.Remove(qrPath) |
|
||||
if err != nil { |
|
||||
fmt.Println("Failed to remove qrPath " + qrPath) |
|
||||
} |
|
||||
}() |
|
||||
chImage <- qrPath |
|
||||
} else { |
|
||||
fmt.Printf("QR channel result: %s", evt.Event) |
|
||||
} |
|
||||
} |
|
||||
}() |
|
||||
} |
|
||||
|
|
||||
err = service.WaCli.Connect() |
|
||||
if err != nil { |
|
||||
return response, errors.New("Failed to connect bro " + err.Error()) |
|
||||
} |
|
||||
response.ImagePath = <-chImage |
|
||||
|
|
||||
return response, nil |
|
||||
} |
|
||||
|
|
||||
func (service AppServiceImpl) Logout(c *fiber.Ctx) (err error) { |
|
||||
// delete history
|
|
||||
files, err := filepath.Glob("./history-*") |
|
||||
if err != nil { |
|
||||
panic(err) |
|
||||
} |
|
||||
|
|
||||
for _, f := range files { |
|
||||
err = os.Remove(f) |
|
||||
if err != nil { |
|
||||
return err |
|
||||
} |
|
||||
} |
|
||||
// delete qr images
|
|
||||
qrImages, err := filepath.Glob("./statics/images/qrcode/scan-*") |
|
||||
if err != nil { |
|
||||
panic(err) |
|
||||
} |
|
||||
|
|
||||
for _, f := range qrImages { |
|
||||
err = os.Remove(f) |
|
||||
if err != nil { |
|
||||
return err |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
err = service.WaCli.Logout() |
|
||||
return |
|
||||
} |
|
||||
|
|
||||
func (service AppServiceImpl) Reconnect(c *fiber.Ctx) (err error) { |
|
||||
service.WaCli.Disconnect() |
|
||||
return service.WaCli.Connect() |
|
||||
} |
|
||||
@ -1,14 +1,310 @@ |
|||||
package services |
package services |
||||
|
|
||||
import ( |
import ( |
||||
"github.com/aldinokemal/go-whatsapp-web-multidevice/structs" |
|
||||
"github.com/gofiber/fiber/v2" |
|
||||
|
"context" |
||||
|
"errors" |
||||
|
"fmt" |
||||
|
"github.com/aldinokemal/go-whatsapp-web-multidevice/config" |
||||
|
domainSend "github.com/aldinokemal/go-whatsapp-web-multidevice/domains/send" |
||||
|
"github.com/aldinokemal/go-whatsapp-web-multidevice/utils" |
||||
|
fiberUtils "github.com/gofiber/fiber/v2/utils" |
||||
|
"github.com/h2non/bimg" |
||||
|
"github.com/valyala/fasthttp" |
||||
|
"go.mau.fi/whatsmeow" |
||||
|
waProto "go.mau.fi/whatsmeow/binary/proto" |
||||
|
"google.golang.org/protobuf/proto" |
||||
|
"net/http" |
||||
|
"os" |
||||
|
"os/exec" |
||||
) |
) |
||||
|
|
||||
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) |
|
||||
SendContact(c *fiber.Ctx, request structs.SendContactRequest) (response structs.SendContactResponse, err error) |
|
||||
|
type serviceSend struct { |
||||
|
WaCli *whatsmeow.Client |
||||
|
} |
||||
|
|
||||
|
func NewSendService(waCli *whatsmeow.Client) domainSend.ISendService { |
||||
|
return &serviceSend{ |
||||
|
WaCli: waCli, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func (service serviceSend) SendText(ctx context.Context, request domainSend.MessageRequest) (response domainSend.MessageResponse, err error) { |
||||
|
utils.MustLogin(service.WaCli) |
||||
|
|
||||
|
recipient, ok := utils.ParseJID(request.Phone) |
||||
|
if !ok { |
||||
|
return response, errors.New("invalid JID " + request.Phone) |
||||
|
} |
||||
|
msg := &waProto.Message{Conversation: proto.String(request.Message)} |
||||
|
ts, err := service.WaCli.SendMessage(ctx, recipient, "", msg) |
||||
|
if err != nil { |
||||
|
return response, err |
||||
|
} else { |
||||
|
response.Status = fmt.Sprintf("Message sent to %s (server timestamp: %s)", request.Phone, ts) |
||||
|
} |
||||
|
return response, nil |
||||
|
} |
||||
|
|
||||
|
func (service serviceSend) SendImage(ctx context.Context, request domainSend.ImageRequest) (response domainSend.ImageResponse, err error) { |
||||
|
utils.MustLogin(service.WaCli) |
||||
|
|
||||
|
var ( |
||||
|
imagePath string |
||||
|
imageThumbnail string |
||||
|
deletedItems []string |
||||
|
) |
||||
|
|
||||
|
// Save image to server
|
||||
|
oriImagePath := fmt.Sprintf("%s/%s", config.PathSendItems, request.Image.Filename) |
||||
|
err = fasthttp.SaveMultipartFile(request.Image, oriImagePath) |
||||
|
if err != nil { |
||||
|
return response, err |
||||
|
} |
||||
|
deletedItems = append(deletedItems, oriImagePath) |
||||
|
|
||||
|
// Generate thumbnail with smalled image
|
||||
|
openThumbnailBuffer, err := bimg.Read(oriImagePath) |
||||
|
imageThumbnail = fmt.Sprintf("%s/thumbnails-%s", config.PathSendItems, request.Image.Filename) |
||||
|
thumbnailImage, err := bimg.NewImage(openThumbnailBuffer).Process(bimg.Options{Quality: 90, Width: 100, Embed: true}) |
||||
|
if err != nil { |
||||
|
return response, err |
||||
|
} |
||||
|
err = bimg.Write(imageThumbnail, thumbnailImage) |
||||
|
if err != nil { |
||||
|
return response, err |
||||
|
} |
||||
|
deletedItems = append(deletedItems, imageThumbnail) |
||||
|
|
||||
|
if request.Compress { |
||||
|
// Resize image
|
||||
|
openImageBuffer, err := bimg.Read(oriImagePath) |
||||
|
newImage, err := bimg.NewImage(openImageBuffer).Process(bimg.Options{Quality: 90, Width: 600, Embed: true}) |
||||
|
if err != nil { |
||||
|
return response, err |
||||
|
} |
||||
|
|
||||
|
newImagePath := fmt.Sprintf("%s/new-%s", config.PathSendItems, request.Image.Filename) |
||||
|
err = bimg.Write(newImagePath, newImage) |
||||
|
if err != nil { |
||||
|
return response, err |
||||
|
} |
||||
|
deletedItems = append(deletedItems, newImagePath) |
||||
|
imagePath = newImagePath |
||||
|
} else { |
||||
|
imagePath = oriImagePath |
||||
|
} |
||||
|
|
||||
|
// Send to WA server
|
||||
|
dataWaCaption := request.Caption |
||||
|
dataWaRecipient, ok := utils.ParseJID(request.Phone) |
||||
|
if !ok { |
||||
|
return response, errors.New("invalid JID " + request.Phone) |
||||
|
} |
||||
|
dataWaImage, err := os.ReadFile(imagePath) |
||||
|
if err != nil { |
||||
|
return response, err |
||||
|
} |
||||
|
uploadedImage, err := service.WaCli.Upload(context.Background(), dataWaImage, whatsmeow.MediaImage) |
||||
|
if err != nil { |
||||
|
fmt.Printf("Failed to upload file: %v", err) |
||||
|
return response, err |
||||
|
} |
||||
|
dataWaThumbnail, err := os.ReadFile(imageThumbnail) |
||||
|
|
||||
|
msg := &waProto.Message{ImageMessage: &waProto.ImageMessage{ |
||||
|
JpegThumbnail: dataWaThumbnail, |
||||
|
Caption: proto.String(dataWaCaption), |
||||
|
Url: proto.String(uploadedImage.URL), |
||||
|
DirectPath: proto.String(uploadedImage.DirectPath), |
||||
|
MediaKey: uploadedImage.MediaKey, |
||||
|
Mimetype: proto.String(http.DetectContentType(dataWaImage)), |
||||
|
FileEncSha256: uploadedImage.FileEncSHA256, |
||||
|
FileSha256: uploadedImage.FileSHA256, |
||||
|
FileLength: proto.Uint64(uint64(len(dataWaImage))), |
||||
|
ViewOnce: proto.Bool(request.ViewOnce), |
||||
|
}} |
||||
|
ts, err := service.WaCli.SendMessage(ctx, dataWaRecipient, "", msg) |
||||
|
go func() { |
||||
|
errDelete := utils.RemoveFile(0, deletedItems...) |
||||
|
if errDelete != nil { |
||||
|
fmt.Println("error when deleting picture: ", errDelete) |
||||
|
} |
||||
|
}() |
||||
|
if err != nil { |
||||
|
return response, err |
||||
|
} else { |
||||
|
response.Status = fmt.Sprintf("Message sent to %s (server timestamp: %s)", request.Phone, ts) |
||||
|
return response, nil |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func (service serviceSend) SendFile(ctx context.Context, request domainSend.FileRequest) (response domainSend.FileResponse, err error) { |
||||
|
utils.MustLogin(service.WaCli) |
||||
|
|
||||
|
oriFilePath := fmt.Sprintf("%s/%s", config.PathSendItems, request.File.Filename) |
||||
|
err = fasthttp.SaveMultipartFile(request.File, oriFilePath) |
||||
|
if err != nil { |
||||
|
return response, err |
||||
|
} |
||||
|
|
||||
|
// Send to WA server
|
||||
|
dataWaRecipient, ok := utils.ParseJID(request.Phone) |
||||
|
if !ok { |
||||
|
return response, errors.New("invalid JID " + request.Phone) |
||||
|
} |
||||
|
dataWaFile, err := os.ReadFile(oriFilePath) |
||||
|
if err != nil { |
||||
|
return response, err |
||||
|
} |
||||
|
uploadedFile, err := service.WaCli.Upload(context.Background(), dataWaFile, whatsmeow.MediaDocument) |
||||
|
if err != nil { |
||||
|
fmt.Printf("Failed to upload file: %v", err) |
||||
|
return response, err |
||||
|
} |
||||
|
|
||||
|
msg := &waProto.Message{DocumentMessage: &waProto.DocumentMessage{ |
||||
|
Url: proto.String(uploadedFile.URL), |
||||
|
Mimetype: proto.String(http.DetectContentType(dataWaFile)), |
||||
|
Title: proto.String(request.File.Filename), |
||||
|
FileSha256: uploadedFile.FileSHA256, |
||||
|
FileLength: proto.Uint64(uploadedFile.FileLength), |
||||
|
MediaKey: uploadedFile.MediaKey, |
||||
|
FileName: proto.String(request.File.Filename), |
||||
|
FileEncSha256: uploadedFile.FileEncSHA256, |
||||
|
DirectPath: proto.String(uploadedFile.DirectPath), |
||||
|
}} |
||||
|
ts, err := service.WaCli.SendMessage(ctx, dataWaRecipient, "", msg) |
||||
|
go func() { |
||||
|
errDelete := utils.RemoveFile(0, oriFilePath) |
||||
|
if errDelete != nil { |
||||
|
fmt.Println(errDelete) |
||||
|
} |
||||
|
}() |
||||
|
if err != nil { |
||||
|
return response, err |
||||
|
} else { |
||||
|
response.Status = fmt.Sprintf("Document sent to %s (server timestamp: %s)", request.Phone, ts) |
||||
|
return response, nil |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func (service serviceSend) SendVideo(ctx context.Context, request domainSend.VideoRequest) (response domainSend.VideoResponse, err error) { |
||||
|
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 = fasthttp.SaveMultipartFile(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(ctx, 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 |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func (service serviceSend) SendContact(ctx context.Context, request domainSend.ContactRequest) (response domainSend.ContactResponse, err error) { |
||||
|
utils.MustLogin(service.WaCli) |
||||
|
|
||||
|
recipient, ok := utils.ParseJID(request.Phone) |
||||
|
if !ok { |
||||
|
return response, errors.New("invalid JID " + request.Phone) |
||||
|
} |
||||
|
msgVCard := fmt.Sprintf("BEGIN:VCARD\nVERSION:3.0\nN:;%v;;;\nFN:%v\nTEL;type=CELL;waid=%v:+%v\nEND:VCARD", |
||||
|
request.ContactName, request.ContactName, request.ContactPhone, request.ContactPhone) |
||||
|
msg := &waProto.Message{ContactMessage: &waProto.ContactMessage{ |
||||
|
DisplayName: proto.String(request.ContactName), |
||||
|
Vcard: proto.String(msgVCard), |
||||
|
}} |
||||
|
ts, err := service.WaCli.SendMessage(ctx, recipient, "", msg) |
||||
|
if err != nil { |
||||
|
return response, err |
||||
|
} else { |
||||
|
response.Status = fmt.Sprintf("Contact sent to %s (server timestamp: %s)", request.Phone, ts) |
||||
|
} |
||||
|
return response, nil |
||||
} |
} |
||||
@ -1,310 +0,0 @@ |
|||||
package services |
|
||||
|
|
||||
import ( |
|
||||
"context" |
|
||||
"errors" |
|
||||
"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/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 { |
|
||||
WaCli *whatsmeow.Client |
|
||||
} |
|
||||
|
|
||||
func NewSendService(waCli *whatsmeow.Client) SendService { |
|
||||
return &SendServiceImpl{ |
|
||||
WaCli: waCli, |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
func (service SendServiceImpl) SendText(c *fiber.Ctx, request structs.SendMessageRequest) (response structs.SendMessageResponse, err error) { |
|
||||
utils.MustLogin(service.WaCli) |
|
||||
|
|
||||
recipient, ok := utils.ParseJID(request.Phone) |
|
||||
if !ok { |
|
||||
return response, errors.New("invalid JID " + request.Phone) |
|
||||
} |
|
||||
msg := &waProto.Message{Conversation: proto.String(request.Message)} |
|
||||
ts, err := service.WaCli.SendMessage(c.Context(), recipient, "", msg) |
|
||||
if err != nil { |
|
||||
return response, err |
|
||||
} else { |
|
||||
response.Status = fmt.Sprintf("Message sent to %s (server timestamp: %s)", request.Phone, ts) |
|
||||
} |
|
||||
return response, nil |
|
||||
} |
|
||||
|
|
||||
func (service SendServiceImpl) SendImage(c *fiber.Ctx, request structs.SendImageRequest) (response structs.SendImageResponse, err error) { |
|
||||
utils.MustLogin(service.WaCli) |
|
||||
|
|
||||
var ( |
|
||||
imagePath string |
|
||||
imageThumbnail string |
|
||||
deletedItems []string |
|
||||
) |
|
||||
|
|
||||
// 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 |
|
||||
} |
|
||||
deletedItems = append(deletedItems, oriImagePath) |
|
||||
|
|
||||
// Generate thumbnail with smalled image
|
|
||||
openThumbnailBuffer, err := bimg.Read(oriImagePath) |
|
||||
imageThumbnail = fmt.Sprintf("%s/thumbnails-%s", config.PathSendItems, request.Image.Filename) |
|
||||
thumbnailImage, err := bimg.NewImage(openThumbnailBuffer).Process(bimg.Options{Quality: 90, Width: 100, Embed: true}) |
|
||||
if err != nil { |
|
||||
return response, err |
|
||||
} |
|
||||
err = bimg.Write(imageThumbnail, thumbnailImage) |
|
||||
if err != nil { |
|
||||
return response, err |
|
||||
} |
|
||||
deletedItems = append(deletedItems, imageThumbnail) |
|
||||
|
|
||||
if request.Compress { |
|
||||
// Resize image
|
|
||||
openImageBuffer, err := bimg.Read(oriImagePath) |
|
||||
newImage, err := bimg.NewImage(openImageBuffer).Process(bimg.Options{Quality: 90, Width: 600, Embed: true}) |
|
||||
if err != nil { |
|
||||
return response, err |
|
||||
} |
|
||||
|
|
||||
newImagePath := fmt.Sprintf("%s/new-%s", config.PathSendItems, request.Image.Filename) |
|
||||
err = bimg.Write(newImagePath, newImage) |
|
||||
if err != nil { |
|
||||
return response, err |
|
||||
} |
|
||||
deletedItems = append(deletedItems, newImagePath) |
|
||||
imagePath = newImagePath |
|
||||
} else { |
|
||||
imagePath = oriImagePath |
|
||||
} |
|
||||
|
|
||||
// Send to WA server
|
|
||||
dataWaCaption := request.Caption |
|
||||
dataWaRecipient, ok := utils.ParseJID(request.Phone) |
|
||||
if !ok { |
|
||||
return response, errors.New("invalid JID " + request.Phone) |
|
||||
} |
|
||||
dataWaImage, err := os.ReadFile(imagePath) |
|
||||
if err != nil { |
|
||||
return response, err |
|
||||
} |
|
||||
uploadedImage, err := service.WaCli.Upload(context.Background(), dataWaImage, whatsmeow.MediaImage) |
|
||||
if err != nil { |
|
||||
fmt.Printf("Failed to upload file: %v", err) |
|
||||
return response, err |
|
||||
} |
|
||||
dataWaThumbnail, err := os.ReadFile(imageThumbnail) |
|
||||
|
|
||||
msg := &waProto.Message{ImageMessage: &waProto.ImageMessage{ |
|
||||
JpegThumbnail: dataWaThumbnail, |
|
||||
Caption: proto.String(dataWaCaption), |
|
||||
Url: proto.String(uploadedImage.URL), |
|
||||
DirectPath: proto.String(uploadedImage.DirectPath), |
|
||||
MediaKey: uploadedImage.MediaKey, |
|
||||
Mimetype: proto.String(http.DetectContentType(dataWaImage)), |
|
||||
FileEncSha256: uploadedImage.FileEncSHA256, |
|
||||
FileSha256: uploadedImage.FileSHA256, |
|
||||
FileLength: proto.Uint64(uint64(len(dataWaImage))), |
|
||||
ViewOnce: proto.Bool(request.ViewOnce), |
|
||||
}} |
|
||||
ts, err := service.WaCli.SendMessage(c.Context(), dataWaRecipient, "", msg) |
|
||||
go func() { |
|
||||
errDelete := utils.RemoveFile(0, deletedItems...) |
|
||||
if errDelete != nil { |
|
||||
fmt.Println("error when deleting picture: ", errDelete) |
|
||||
} |
|
||||
}() |
|
||||
if err != nil { |
|
||||
return response, err |
|
||||
} else { |
|
||||
response.Status = fmt.Sprintf("Message sent to %s (server timestamp: %s)", request.Phone, ts) |
|
||||
return response, nil |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
func (service SendServiceImpl) SendFile(c *fiber.Ctx, request structs.SendFileRequest) (response structs.SendFileResponse, err error) { |
|
||||
utils.MustLogin(service.WaCli) |
|
||||
|
|
||||
oriFilePath := fmt.Sprintf("%s/%s", config.PathSendItems, request.File.Filename) |
|
||||
err = c.SaveFile(request.File, oriFilePath) |
|
||||
if err != nil { |
|
||||
return response, err |
|
||||
} |
|
||||
|
|
||||
// Send to WA server
|
|
||||
dataWaRecipient, ok := utils.ParseJID(request.Phone) |
|
||||
if !ok { |
|
||||
return response, errors.New("invalid JID " + request.Phone) |
|
||||
} |
|
||||
dataWaFile, err := os.ReadFile(oriFilePath) |
|
||||
if err != nil { |
|
||||
return response, err |
|
||||
} |
|
||||
uploadedFile, err := service.WaCli.Upload(context.Background(), dataWaFile, whatsmeow.MediaDocument) |
|
||||
if err != nil { |
|
||||
fmt.Printf("Failed to upload file: %v", err) |
|
||||
return response, err |
|
||||
} |
|
||||
|
|
||||
msg := &waProto.Message{DocumentMessage: &waProto.DocumentMessage{ |
|
||||
Url: proto.String(uploadedFile.URL), |
|
||||
Mimetype: proto.String(http.DetectContentType(dataWaFile)), |
|
||||
Title: proto.String(request.File.Filename), |
|
||||
FileSha256: uploadedFile.FileSHA256, |
|
||||
FileLength: proto.Uint64(uploadedFile.FileLength), |
|
||||
MediaKey: uploadedFile.MediaKey, |
|
||||
FileName: proto.String(request.File.Filename), |
|
||||
FileEncSha256: uploadedFile.FileEncSHA256, |
|
||||
DirectPath: proto.String(uploadedFile.DirectPath), |
|
||||
}} |
|
||||
ts, err := service.WaCli.SendMessage(c.Context(), dataWaRecipient, "", msg) |
|
||||
go func() { |
|
||||
errDelete := utils.RemoveFile(0, oriFilePath) |
|
||||
if errDelete != nil { |
|
||||
fmt.Println(errDelete) |
|
||||
} |
|
||||
}() |
|
||||
if err != nil { |
|
||||
return response, err |
|
||||
} else { |
|
||||
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(c.Context(), 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 |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
func (service SendServiceImpl) SendContact(c *fiber.Ctx, request structs.SendContactRequest) (response structs.SendContactResponse, err error) { |
|
||||
utils.MustLogin(service.WaCli) |
|
||||
|
|
||||
recipient, ok := utils.ParseJID(request.Phone) |
|
||||
if !ok { |
|
||||
return response, errors.New("invalid JID " + request.Phone) |
|
||||
} |
|
||||
msgVCard := fmt.Sprintf("BEGIN:VCARD\nVERSION:3.0\nN:;%v;;;\nFN:%v\nTEL;type=CELL;waid=%v:+%v\nEND:VCARD", |
|
||||
request.ContactName, request.ContactName, request.ContactPhone, request.ContactPhone) |
|
||||
msg := &waProto.Message{ContactMessage: &waProto.ContactMessage{ |
|
||||
DisplayName: proto.String(request.ContactName), |
|
||||
Vcard: proto.String(msgVCard), |
|
||||
}} |
|
||||
ts, err := service.WaCli.SendMessage(c.Context(), recipient, "", msg) |
|
||||
if err != nil { |
|
||||
return response, err |
|
||||
} else { |
|
||||
response.Status = fmt.Sprintf("Contact sent to %s (server timestamp: %s)", request.Phone, ts) |
|
||||
} |
|
||||
return response, nil |
|
||||
} |
|
||||
@ -1,13 +1,114 @@ |
|||||
package services |
package services |
||||
|
|
||||
import ( |
import ( |
||||
"github.com/aldinokemal/go-whatsapp-web-multidevice/structs" |
|
||||
"github.com/gofiber/fiber/v2" |
|
||||
|
"context" |
||||
|
"errors" |
||||
|
"fmt" |
||||
|
domainUser "github.com/aldinokemal/go-whatsapp-web-multidevice/domains/user" |
||||
|
"github.com/aldinokemal/go-whatsapp-web-multidevice/utils" |
||||
|
"go.mau.fi/whatsmeow" |
||||
|
"go.mau.fi/whatsmeow/types" |
||||
) |
) |
||||
|
|
||||
type UserService interface { |
|
||||
UserInfo(c *fiber.Ctx, request structs.UserInfoRequest) (response structs.UserInfoResponse, err error) |
|
||||
UserAvatar(c *fiber.Ctx, request structs.UserAvatarRequest) (response structs.UserAvatarResponse, err error) |
|
||||
UserMyListGroups(c *fiber.Ctx) (response structs.UserMyListGroupsResponse, err error) |
|
||||
UserMyPrivacySetting(c *fiber.Ctx) (response structs.UserMyPrivacySettingResponse, err error) |
|
||||
|
type userService struct { |
||||
|
WaCli *whatsmeow.Client |
||||
|
} |
||||
|
|
||||
|
func NewUserService(waCli *whatsmeow.Client) domainUser.IUserService { |
||||
|
return &userService{ |
||||
|
WaCli: waCli, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func (service userService) Info(_ context.Context, request domainUser.InfoRequest) (response domainUser.InfoResponse, err error) { |
||||
|
utils.MustLogin(service.WaCli) |
||||
|
|
||||
|
var jids []types.JID |
||||
|
jid, ok := utils.ParseJID(request.Phone) |
||||
|
if !ok { |
||||
|
return response, errors.New("invalid JID " + request.Phone) |
||||
|
} |
||||
|
|
||||
|
jids = append(jids, jid) |
||||
|
resp, err := service.WaCli.GetUserInfo(jids) |
||||
|
if err != nil { |
||||
|
return response, err |
||||
|
} |
||||
|
|
||||
|
for _, userInfo := range resp { |
||||
|
var device []domainUser.InfoResponseDataDevice |
||||
|
for _, j := range userInfo.Devices { |
||||
|
device = append(device, domainUser.InfoResponseDataDevice{ |
||||
|
User: j.User, |
||||
|
Agent: j.Agent, |
||||
|
Device: utils.GetPlatformName(int(j.Device)), |
||||
|
Server: j.Server, |
||||
|
AD: j.AD, |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
data := domainUser.InfoResponseData{ |
||||
|
Status: userInfo.Status, |
||||
|
PictureID: userInfo.PictureID, |
||||
|
Devices: device, |
||||
|
} |
||||
|
if userInfo.VerifiedName != nil { |
||||
|
data.VerifiedName = fmt.Sprintf("%v", *userInfo.VerifiedName) |
||||
|
} |
||||
|
response.Data = append(response.Data, data) |
||||
|
} |
||||
|
|
||||
|
return response, nil |
||||
|
} |
||||
|
|
||||
|
func (service userService) Avatar(_ context.Context, request domainUser.AvatarRequest) (response domainUser.AvatarResponse, err error) { |
||||
|
utils.MustLogin(service.WaCli) |
||||
|
|
||||
|
jid, ok := utils.ParseJID(request.Phone) |
||||
|
if !ok { |
||||
|
return response, errors.New("invalid JID " + request.Phone) |
||||
|
} |
||||
|
pic, err := service.WaCli.GetProfilePictureInfo(jid, false, "") |
||||
|
if err != nil { |
||||
|
return response, err |
||||
|
} else if pic == nil { |
||||
|
return response, errors.New("no avatar found") |
||||
|
} else { |
||||
|
response.URL = pic.URL |
||||
|
response.ID = pic.ID |
||||
|
response.Type = pic.Type |
||||
|
|
||||
|
return response, nil |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func (service userService) MyListGroups(_ context.Context) (response domainUser.MyListGroupsResponse, err error) { |
||||
|
utils.MustLogin(service.WaCli) |
||||
|
|
||||
|
groups, err := service.WaCli.GetJoinedGroups() |
||||
|
if err != nil { |
||||
|
return |
||||
|
} |
||||
|
fmt.Printf("%+v\n", groups) |
||||
|
if groups != nil { |
||||
|
for _, group := range groups { |
||||
|
response.Data = append(response.Data, *group) |
||||
|
} |
||||
|
} |
||||
|
return response, nil |
||||
|
} |
||||
|
|
||||
|
func (service userService) MyPrivacySetting(_ context.Context) (response domainUser.MyPrivacySettingResponse, err error) { |
||||
|
utils.MustLogin(service.WaCli) |
||||
|
|
||||
|
resp, err := service.WaCli.TryFetchPrivacySettings(false) |
||||
|
if err != nil { |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
response.GroupAdd = string(resp.GroupAdd) |
||||
|
response.Status = string(resp.Status) |
||||
|
response.ReadReceipts = string(resp.ReadReceipts) |
||||
|
response.Profile = string(resp.Profile) |
||||
|
return response, nil |
||||
} |
} |
||||
@ -1,114 +0,0 @@ |
|||||
package services |
|
||||
|
|
||||
import ( |
|
||||
"errors" |
|
||||
"fmt" |
|
||||
"github.com/aldinokemal/go-whatsapp-web-multidevice/structs" |
|
||||
"github.com/aldinokemal/go-whatsapp-web-multidevice/utils" |
|
||||
"github.com/gofiber/fiber/v2" |
|
||||
"go.mau.fi/whatsmeow" |
|
||||
"go.mau.fi/whatsmeow/types" |
|
||||
) |
|
||||
|
|
||||
type UserServiceImpl struct { |
|
||||
WaCli *whatsmeow.Client |
|
||||
} |
|
||||
|
|
||||
func NewUserService(waCli *whatsmeow.Client) UserService { |
|
||||
return &UserServiceImpl{ |
|
||||
WaCli: waCli, |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
func (service UserServiceImpl) UserInfo(_ *fiber.Ctx, request structs.UserInfoRequest) (response structs.UserInfoResponse, err error) { |
|
||||
utils.MustLogin(service.WaCli) |
|
||||
|
|
||||
var jids []types.JID |
|
||||
jid, ok := utils.ParseJID(request.Phone) |
|
||||
if !ok { |
|
||||
return response, errors.New("invalid JID " + request.Phone) |
|
||||
} |
|
||||
|
|
||||
jids = append(jids, jid) |
|
||||
resp, err := service.WaCli.GetUserInfo(jids) |
|
||||
if err != nil { |
|
||||
return response, err |
|
||||
} |
|
||||
|
|
||||
for _, userInfo := range resp { |
|
||||
var device []structs.UserInfoResponseDataDevice |
|
||||
for _, j := range userInfo.Devices { |
|
||||
device = append(device, structs.UserInfoResponseDataDevice{ |
|
||||
User: j.User, |
|
||||
Agent: j.Agent, |
|
||||
Device: utils.GetPlatformName(int(j.Device)), |
|
||||
Server: j.Server, |
|
||||
AD: j.AD, |
|
||||
}) |
|
||||
} |
|
||||
|
|
||||
data := structs.UserInfoResponseData{ |
|
||||
Status: userInfo.Status, |
|
||||
PictureID: userInfo.PictureID, |
|
||||
Devices: device, |
|
||||
} |
|
||||
if userInfo.VerifiedName != nil { |
|
||||
data.VerifiedName = fmt.Sprintf("%v", *userInfo.VerifiedName) |
|
||||
} |
|
||||
response.Data = append(response.Data, data) |
|
||||
} |
|
||||
|
|
||||
return response, nil |
|
||||
} |
|
||||
|
|
||||
func (service UserServiceImpl) UserAvatar(c *fiber.Ctx, request structs.UserAvatarRequest) (response structs.UserAvatarResponse, err error) { |
|
||||
utils.MustLogin(service.WaCli) |
|
||||
|
|
||||
jid, ok := utils.ParseJID(request.Phone) |
|
||||
if !ok { |
|
||||
return response, errors.New("invalid JID " + request.Phone) |
|
||||
} |
|
||||
pic, err := service.WaCli.GetProfilePictureInfo(jid, false, "") |
|
||||
if err != nil { |
|
||||
return response, err |
|
||||
} else if pic == nil { |
|
||||
return response, errors.New("no avatar found") |
|
||||
} else { |
|
||||
response.URL = pic.URL |
|
||||
response.ID = pic.ID |
|
||||
response.Type = pic.Type |
|
||||
|
|
||||
return response, nil |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
func (service UserServiceImpl) UserMyListGroups(_ *fiber.Ctx) (response structs.UserMyListGroupsResponse, err error) { |
|
||||
utils.MustLogin(service.WaCli) |
|
||||
|
|
||||
groups, err := service.WaCli.GetJoinedGroups() |
|
||||
if err != nil { |
|
||||
return |
|
||||
} |
|
||||
fmt.Printf("%+v\n", groups) |
|
||||
if groups != nil { |
|
||||
for _, group := range groups { |
|
||||
response.Data = append(response.Data, *group) |
|
||||
} |
|
||||
} |
|
||||
return response, nil |
|
||||
} |
|
||||
|
|
||||
func (service UserServiceImpl) UserMyPrivacySetting(_ *fiber.Ctx) (response structs.UserMyPrivacySettingResponse, err error) { |
|
||||
utils.MustLogin(service.WaCli) |
|
||||
|
|
||||
resp, err := service.WaCli.TryFetchPrivacySettings(false) |
|
||||
if err != nil { |
|
||||
return |
|
||||
} |
|
||||
|
|
||||
response.GroupAdd = string(resp.GroupAdd) |
|
||||
response.Status = string(resp.Status) |
|
||||
response.ReadReceipts = string(resp.ReadReceipts) |
|
||||
response.Profile = string(resp.Profile) |
|
||||
return response, nil |
|
||||
} |
|
||||
@ -1,67 +0,0 @@ |
|||||
package structs |
|
||||
|
|
||||
import ( |
|
||||
"mime/multipart" |
|
||||
) |
|
||||
|
|
||||
type SendType string |
|
||||
|
|
||||
const TypeUser SendType = "user" |
|
||||
const TypeGroup SendType = "group" |
|
||||
|
|
||||
type SendMessageRequest struct { |
|
||||
Phone string `json:"phone" form:"phone"` |
|
||||
Message string `json:"message" form:"message"` |
|
||||
Type SendType `json:"type" form:"type"` |
|
||||
} |
|
||||
|
|
||||
type SendMessageResponse struct { |
|
||||
Status string `json:"status"` |
|
||||
} |
|
||||
|
|
||||
type SendImageRequest struct { |
|
||||
Phone string `json:"phone" form:"phone"` |
|
||||
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:"type"` |
|
||||
Compress bool `json:"compress"` |
|
||||
} |
|
||||
|
|
||||
type SendImageResponse struct { |
|
||||
Status string `json:"status"` |
|
||||
} |
|
||||
|
|
||||
type SendFileRequest struct { |
|
||||
Phone string `json:"phone" form:"phone"` |
|
||||
File *multipart.FileHeader `json:"file" form:"file"` |
|
||||
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"` |
|
||||
} |
|
||||
|
|
||||
type SendContactRequest struct { |
|
||||
Phone string `json:"phone" form:"phone"` |
|
||||
ContactName string `json:"contact_name" form:"contact_name"` |
|
||||
ContactPhone string `json:"contact_phone" form:"contact_phone"` |
|
||||
Type SendType `json:"type" form:"type"` |
|
||||
} |
|
||||
|
|
||||
type SendContactResponse struct { |
|
||||
Status string `json:"status"` |
|
||||
} |
|
||||
@ -1,48 +0,0 @@ |
|||||
package structs |
|
||||
|
|
||||
import "go.mau.fi/whatsmeow/types" |
|
||||
|
|
||||
type UserInfoRequest struct { |
|
||||
Phone string `json:"phone" query:"phone"` |
|
||||
} |
|
||||
|
|
||||
type UserInfoResponseDataDevice struct { |
|
||||
User string |
|
||||
Agent uint8 |
|
||||
Device string |
|
||||
Server string |
|
||||
AD bool |
|
||||
} |
|
||||
|
|
||||
type UserInfoResponseData struct { |
|
||||
VerifiedName string `json:"verified_name"` |
|
||||
Status string `json:"status"` |
|
||||
PictureID string `json:"picture_id"` |
|
||||
Devices []UserInfoResponseDataDevice `json:"devices"` |
|
||||
} |
|
||||
|
|
||||
type UserInfoResponse struct { |
|
||||
Data []UserInfoResponseData `json:"data"` |
|
||||
} |
|
||||
|
|
||||
type UserAvatarRequest struct { |
|
||||
Phone string `json:"phone" query:"phone"` |
|
||||
} |
|
||||
|
|
||||
type UserAvatarResponse struct { |
|
||||
URL string `json:"url"` |
|
||||
ID string `json:"id"` |
|
||||
Type string `json:"type"` |
|
||||
} |
|
||||
|
|
||||
type UserMyPrivacySettingResponse struct { |
|
||||
GroupAdd string `json:"group_add"` |
|
||||
LastSeen string `json:"last_seen"` |
|
||||
Status string `json:"status"` |
|
||||
Profile string `json:"profile"` |
|
||||
ReadReceipts string `json:"read_receipts"` |
|
||||
} |
|
||||
|
|
||||
type UserMyListGroupsResponse struct { |
|
||||
Data []types.GroupInfo `json:"data"` |
|
||||
} |
|
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue