whatsapp-multi-devicewhatsapp-apiwhatsapprestgolanggorest-apigolang-whatsapp-apigolang-whatsappbotwhatsapp-web-multi-devicewhatsapp-api-go
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
509 lines
17 KiB
509 lines
17 KiB
package services
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"github.com/aldinokemal/go-whatsapp-web-multidevice/config"
|
|
"github.com/aldinokemal/go-whatsapp-web-multidevice/domains/app"
|
|
domainSend "github.com/aldinokemal/go-whatsapp-web-multidevice/domains/send"
|
|
"github.com/aldinokemal/go-whatsapp-web-multidevice/internal/rest/helpers"
|
|
pkgError "github.com/aldinokemal/go-whatsapp-web-multidevice/pkg/error"
|
|
"github.com/aldinokemal/go-whatsapp-web-multidevice/pkg/utils"
|
|
"github.com/aldinokemal/go-whatsapp-web-multidevice/pkg/whatsapp"
|
|
"github.com/aldinokemal/go-whatsapp-web-multidevice/validations"
|
|
"github.com/disintegration/imaging"
|
|
fiberUtils "github.com/gofiber/fiber/v2/utils"
|
|
"github.com/sirupsen/logrus"
|
|
"github.com/valyala/fasthttp"
|
|
"go.mau.fi/whatsmeow"
|
|
"go.mau.fi/whatsmeow/proto/waE2E"
|
|
"google.golang.org/protobuf/proto"
|
|
"net/http"
|
|
"os"
|
|
"os/exec"
|
|
)
|
|
|
|
type serviceSend struct {
|
|
WaCli *whatsmeow.Client
|
|
appService app.IAppService
|
|
}
|
|
|
|
func NewSendService(waCli *whatsmeow.Client, appService app.IAppService) domainSend.ISendService {
|
|
return &serviceSend{
|
|
WaCli: waCli,
|
|
appService: appService,
|
|
}
|
|
}
|
|
|
|
func (service serviceSend) SendText(ctx context.Context, request domainSend.MessageRequest) (response domainSend.GenericResponse, err error) {
|
|
err = validations.ValidateSendMessage(ctx, request)
|
|
if err != nil {
|
|
return response, err
|
|
}
|
|
dataWaRecipient, err := whatsapp.ValidateJidWithLogin(service.WaCli, request.Phone)
|
|
if err != nil {
|
|
return response, err
|
|
}
|
|
|
|
// Send message
|
|
msg := &waE2E.Message{
|
|
ExtendedTextMessage: &waE2E.ExtendedTextMessage{
|
|
Text: proto.String(request.Message),
|
|
},
|
|
}
|
|
|
|
parsedMentions := service.getMentionFromText(ctx, request.Message)
|
|
if len(parsedMentions) > 0 {
|
|
msg.ExtendedTextMessage.ContextInfo = &waE2E.ContextInfo{
|
|
MentionedJID: parsedMentions,
|
|
}
|
|
}
|
|
|
|
// Reply message
|
|
if request.ReplyMessageID != nil && *request.ReplyMessageID != "" {
|
|
participantJID := dataWaRecipient.String()
|
|
if len(*request.ReplyMessageID) < 28 {
|
|
firstDevice, err := service.appService.FirstDevice(ctx)
|
|
if err != nil {
|
|
return response, err
|
|
}
|
|
participantJID = firstDevice.Device
|
|
}
|
|
|
|
msg.ExtendedTextMessage = &waE2E.ExtendedTextMessage{
|
|
Text: proto.String(request.Message),
|
|
ContextInfo: &waE2E.ContextInfo{
|
|
StanzaID: request.ReplyMessageID,
|
|
Participant: proto.String(participantJID),
|
|
QuotedMessage: &waE2E.Message{
|
|
Conversation: proto.String(request.Message),
|
|
},
|
|
},
|
|
}
|
|
|
|
if len(parsedMentions) > 0 {
|
|
msg.ExtendedTextMessage.ContextInfo.MentionedJID = parsedMentions
|
|
}
|
|
}
|
|
|
|
ts, err := service.WaCli.SendMessage(ctx, dataWaRecipient, msg)
|
|
if err != nil {
|
|
return response, err
|
|
}
|
|
|
|
response.MessageID = ts.ID
|
|
response.Status = fmt.Sprintf("Message sent to %s (server timestamp: %s)", request.Phone, ts.Timestamp.String())
|
|
return response, nil
|
|
}
|
|
|
|
func (service serviceSend) SendImage(ctx context.Context, request domainSend.ImageRequest) (response domainSend.GenericResponse, err error) {
|
|
err = validations.ValidateSendImage(ctx, request)
|
|
if err != nil {
|
|
return response, err
|
|
}
|
|
dataWaRecipient, err := whatsapp.ValidateJidWithLogin(service.WaCli, request.Phone)
|
|
if err != nil {
|
|
return response, err
|
|
}
|
|
|
|
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 size */
|
|
srcImage, err := imaging.Open(oriImagePath)
|
|
if err != nil {
|
|
return response, pkgError.InternalServerError(fmt.Sprintf("failed to open image %v", err))
|
|
}
|
|
|
|
// Resize Thumbnail
|
|
resizedImage := imaging.Resize(srcImage, 100, 0, imaging.Lanczos)
|
|
imageThumbnail = fmt.Sprintf("%s/thumbnails-%s", config.PathSendItems, request.Image.Filename)
|
|
if err = imaging.Save(resizedImage, imageThumbnail); err != nil {
|
|
return response, pkgError.InternalServerError(fmt.Sprintf("failed to save thumbnail %v", err))
|
|
}
|
|
deletedItems = append(deletedItems, imageThumbnail)
|
|
|
|
if request.Compress {
|
|
// Resize image
|
|
openImageBuffer, err := imaging.Open(oriImagePath)
|
|
if err != nil {
|
|
return response, pkgError.InternalServerError(fmt.Sprintf("failed to open image %v", err))
|
|
}
|
|
newImage := imaging.Resize(openImageBuffer, 600, 0, imaging.Lanczos)
|
|
newImagePath := fmt.Sprintf("%s/new-%s", config.PathSendItems, request.Image.Filename)
|
|
if err = imaging.Save(newImage, newImagePath); err != nil {
|
|
return response, pkgError.InternalServerError(fmt.Sprintf("failed to save image %v", err))
|
|
}
|
|
deletedItems = append(deletedItems, newImagePath)
|
|
imagePath = newImagePath
|
|
} else {
|
|
imagePath = oriImagePath
|
|
}
|
|
|
|
// Send to WA server
|
|
dataWaCaption := request.Caption
|
|
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)
|
|
if err != nil {
|
|
return response, pkgError.InternalServerError(fmt.Sprintf("failed to read thumbnail %v", err))
|
|
}
|
|
|
|
msg := &waE2E.Message{ImageMessage: &waE2E.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
|
|
}
|
|
|
|
response.MessageID = ts.ID
|
|
response.Status = fmt.Sprintf("Message sent to %s (server timestamp: %s)", request.Phone, ts.Timestamp.String())
|
|
return response, nil
|
|
}
|
|
|
|
func (service serviceSend) SendFile(ctx context.Context, request domainSend.FileRequest) (response domainSend.GenericResponse, err error) {
|
|
err = validations.ValidateSendFile(ctx, request)
|
|
if err != nil {
|
|
return response, err
|
|
}
|
|
dataWaRecipient, err := whatsapp.ValidateJidWithLogin(service.WaCli, request.Phone)
|
|
if err != nil {
|
|
return response, err
|
|
}
|
|
|
|
fileBytes := helpers.MultipartFormFileHeaderToBytes(request.File)
|
|
fileMimeType := http.DetectContentType(fileBytes)
|
|
|
|
// Send to WA server
|
|
uploadedFile, err := service.WaCli.Upload(context.Background(), fileBytes, whatsmeow.MediaDocument)
|
|
if err != nil {
|
|
fmt.Printf("Failed to upload file: %v", err)
|
|
return response, err
|
|
}
|
|
|
|
msg := &waE2E.Message{DocumentMessage: &waE2E.DocumentMessage{
|
|
URL: proto.String(uploadedFile.URL),
|
|
Mimetype: proto.String(fileMimeType),
|
|
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),
|
|
Caption: proto.String(request.Caption),
|
|
}}
|
|
ts, err := service.WaCli.SendMessage(ctx, dataWaRecipient, msg)
|
|
if err != nil {
|
|
return response, err
|
|
}
|
|
|
|
response.MessageID = ts.ID
|
|
response.Status = fmt.Sprintf("Document sent to %s (server timestamp: %s)", request.Phone, ts.Timestamp.String())
|
|
return response, nil
|
|
}
|
|
|
|
func (service serviceSend) SendVideo(ctx context.Context, request domainSend.VideoRequest) (response domainSend.GenericResponse, err error) {
|
|
err = validations.ValidateSendVideo(ctx, request)
|
|
if err != nil {
|
|
return response, err
|
|
}
|
|
dataWaRecipient, err := whatsapp.ValidateJidWithLogin(service.WaCli, request.Phone)
|
|
if err != nil {
|
|
return response, err
|
|
}
|
|
|
|
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, pkgError.InternalServerError(fmt.Sprintf("failed to store video in server %v", err))
|
|
}
|
|
|
|
// Check if ffmpeg is installed
|
|
_, err = exec.LookPath("ffmpeg")
|
|
if err != nil {
|
|
return response, pkgError.InternalServerError("ffmpeg not installed")
|
|
}
|
|
|
|
// 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()
|
|
if err != nil {
|
|
return response, pkgError.InternalServerError(fmt.Sprintf("failed to create thumbnail %v", err))
|
|
}
|
|
|
|
// Resize Thumbnail
|
|
srcImage, err := imaging.Open(thumbnailVideoPath)
|
|
if err != nil {
|
|
return response, pkgError.InternalServerError(fmt.Sprintf("failed to open image %v", err))
|
|
}
|
|
resizedImage := imaging.Resize(srcImage, 100, 0, imaging.Lanczos)
|
|
thumbnailResizeVideoPath := fmt.Sprintf("%s/thumbnails-%s", config.PathSendItems, generateUUID+".png")
|
|
if err = imaging.Save(resizedImage, thumbnailResizeVideoPath); err != nil {
|
|
return response, pkgError.InternalServerError(fmt.Sprintf("failed to save thumbnail %v", err))
|
|
}
|
|
|
|
deletedItems = append(deletedItems, thumbnailVideoPath)
|
|
deletedItems = append(deletedItems, thumbnailResizeVideoPath)
|
|
videoThumbnail = thumbnailResizeVideoPath
|
|
|
|
if request.Compress {
|
|
compresVideoPath := fmt.Sprintf("%s/%s", config.PathSendItems, generateUUID+".mp4")
|
|
|
|
cmdCompress := exec.Command("ffmpeg", "-i", oriVideoPath, "-strict", "-2", compresVideoPath)
|
|
err = cmdCompress.Run()
|
|
if err != nil {
|
|
return response, pkgError.InternalServerError("failed to compress video")
|
|
}
|
|
|
|
videoPath = compresVideoPath
|
|
deletedItems = append(deletedItems, compresVideoPath)
|
|
} else {
|
|
videoPath = oriVideoPath
|
|
deletedItems = append(deletedItems, oriVideoPath)
|
|
}
|
|
|
|
//Send to WA server
|
|
dataWaVideo, err := os.ReadFile(videoPath)
|
|
if err != nil {
|
|
return response, err
|
|
}
|
|
uploaded, err := service.WaCli.Upload(context.Background(), dataWaVideo, whatsmeow.MediaVideo)
|
|
if err != nil {
|
|
return response, pkgError.InternalServerError(fmt.Sprintf("Failed to upload file: %v", err))
|
|
}
|
|
dataWaThumbnail, err := os.ReadFile(videoThumbnail)
|
|
if err != nil {
|
|
return response, err
|
|
}
|
|
|
|
msg := &waE2E.Message{VideoMessage: &waE2E.VideoMessage{
|
|
URL: proto.String(uploaded.URL),
|
|
Mimetype: proto.String(http.DetectContentType(dataWaVideo)),
|
|
Caption: proto.String(request.Caption),
|
|
FileLength: proto.Uint64(uploaded.FileLength),
|
|
FileSHA256: uploaded.FileSHA256,
|
|
FileEncSHA256: uploaded.FileEncSHA256,
|
|
MediaKey: uploaded.MediaKey,
|
|
DirectPath: proto.String(uploaded.DirectPath),
|
|
ViewOnce: proto.Bool(request.ViewOnce),
|
|
JPEGThumbnail: dataWaThumbnail,
|
|
ThumbnailEncSHA256: dataWaThumbnail,
|
|
ThumbnailSHA256: dataWaThumbnail,
|
|
ThumbnailDirectPath: proto.String(uploaded.DirectPath),
|
|
}}
|
|
ts, err := service.WaCli.SendMessage(ctx, dataWaRecipient, msg)
|
|
go func() {
|
|
errDelete := utils.RemoveFile(1, deletedItems...)
|
|
if errDelete != nil {
|
|
logrus.Infof("error when deleting picture: %v", errDelete)
|
|
}
|
|
}()
|
|
if err != nil {
|
|
return response, err
|
|
}
|
|
|
|
response.MessageID = ts.ID
|
|
response.Status = fmt.Sprintf("Video sent to %s (server timestamp: %s)", request.Phone, ts.Timestamp.String())
|
|
return response, nil
|
|
}
|
|
|
|
func (service serviceSend) SendContact(ctx context.Context, request domainSend.ContactRequest) (response domainSend.GenericResponse, err error) {
|
|
err = validations.ValidateSendContact(ctx, request)
|
|
if err != nil {
|
|
return response, err
|
|
}
|
|
dataWaRecipient, err := whatsapp.ValidateJidWithLogin(service.WaCli, request.Phone)
|
|
if err != nil {
|
|
return response, err
|
|
}
|
|
|
|
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 := &waE2E.Message{ContactMessage: &waE2E.ContactMessage{
|
|
DisplayName: proto.String(request.ContactName),
|
|
Vcard: proto.String(msgVCard),
|
|
}}
|
|
ts, err := service.WaCli.SendMessage(ctx, dataWaRecipient, msg)
|
|
if err != nil {
|
|
return response, err
|
|
}
|
|
|
|
response.MessageID = ts.ID
|
|
response.Status = fmt.Sprintf("Contact sent to %s (server timestamp: %s)", request.Phone, ts.Timestamp.String())
|
|
return response, nil
|
|
}
|
|
|
|
func (service serviceSend) SendLink(ctx context.Context, request domainSend.LinkRequest) (response domainSend.GenericResponse, err error) {
|
|
err = validations.ValidateSendLink(ctx, request)
|
|
if err != nil {
|
|
return response, err
|
|
}
|
|
dataWaRecipient, err := whatsapp.ValidateJidWithLogin(service.WaCli, request.Phone)
|
|
if err != nil {
|
|
return response, err
|
|
}
|
|
|
|
getMetaDataFromURL := utils.GetMetaDataFromURL(request.Link)
|
|
|
|
msg := &waE2E.Message{ExtendedTextMessage: &waE2E.ExtendedTextMessage{
|
|
Text: proto.String(fmt.Sprintf("%s\n%s", request.Caption, request.Link)),
|
|
Title: proto.String(getMetaDataFromURL.Title),
|
|
CanonicalURL: proto.String(request.Link),
|
|
MatchedText: proto.String(request.Link),
|
|
Description: proto.String(getMetaDataFromURL.Description),
|
|
}}
|
|
ts, err := service.WaCli.SendMessage(ctx, dataWaRecipient, msg)
|
|
if err != nil {
|
|
return response, err
|
|
}
|
|
|
|
response.MessageID = ts.ID
|
|
response.Status = fmt.Sprintf("Link sent to %s (server timestamp: %s)", request.Phone, ts.Timestamp.String())
|
|
return response, nil
|
|
}
|
|
|
|
func (service serviceSend) SendLocation(ctx context.Context, request domainSend.LocationRequest) (response domainSend.GenericResponse, err error) {
|
|
err = validations.ValidateSendLocation(ctx, request)
|
|
if err != nil {
|
|
return response, err
|
|
}
|
|
dataWaRecipient, err := whatsapp.ValidateJidWithLogin(service.WaCli, request.Phone)
|
|
if err != nil {
|
|
return response, err
|
|
}
|
|
|
|
// Compose WhatsApp Proto
|
|
msg := &waE2E.Message{
|
|
LocationMessage: &waE2E.LocationMessage{
|
|
DegreesLatitude: proto.Float64(utils.StrToFloat64(request.Latitude)),
|
|
DegreesLongitude: proto.Float64(utils.StrToFloat64(request.Longitude)),
|
|
},
|
|
}
|
|
|
|
// Send WhatsApp Message Proto
|
|
ts, err := service.WaCli.SendMessage(ctx, dataWaRecipient, msg)
|
|
if err != nil {
|
|
return response, err
|
|
}
|
|
|
|
response.MessageID = ts.ID
|
|
response.Status = fmt.Sprintf("Send location success %s (server timestamp: %s)", request.Phone, ts.Timestamp.String())
|
|
return response, nil
|
|
}
|
|
|
|
func (service serviceSend) SendAudio(ctx context.Context, request domainSend.AudioRequest) (response domainSend.GenericResponse, err error) {
|
|
err = validations.ValidateSendAudio(ctx, request)
|
|
if err != nil {
|
|
return response, err
|
|
}
|
|
dataWaRecipient, err := whatsapp.ValidateJidWithLogin(service.WaCli, request.Phone)
|
|
if err != nil {
|
|
return response, err
|
|
}
|
|
|
|
autioBytes := helpers.MultipartFormFileHeaderToBytes(request.Audio)
|
|
audioMimeType := http.DetectContentType(autioBytes)
|
|
|
|
audioUploaded, err := service.WaCli.Upload(ctx, autioBytes, whatsmeow.MediaAudio)
|
|
if err != nil {
|
|
err = pkgError.WaUploadMediaError(fmt.Sprintf("Failed to upload audio: %v", err))
|
|
return response, err
|
|
}
|
|
|
|
msg := &waE2E.Message{
|
|
AudioMessage: &waE2E.AudioMessage{
|
|
URL: proto.String(audioUploaded.URL),
|
|
DirectPath: proto.String(audioUploaded.DirectPath),
|
|
Mimetype: proto.String(audioMimeType),
|
|
FileLength: proto.Uint64(audioUploaded.FileLength),
|
|
FileSHA256: audioUploaded.FileSHA256,
|
|
FileEncSHA256: audioUploaded.FileEncSHA256,
|
|
MediaKey: audioUploaded.MediaKey,
|
|
},
|
|
}
|
|
|
|
ts, err := service.WaCli.SendMessage(ctx, dataWaRecipient, msg)
|
|
if err != nil {
|
|
return response, err
|
|
}
|
|
|
|
response.MessageID = ts.ID
|
|
response.Status = fmt.Sprintf("Send audio success %s (server timestamp: %s)", request.Phone, ts.Timestamp.String())
|
|
return response, nil
|
|
}
|
|
|
|
func (service serviceSend) SendPoll(ctx context.Context, request domainSend.PollRequest) (response domainSend.GenericResponse, err error) {
|
|
err = validations.ValidateSendPoll(ctx, request)
|
|
if err != nil {
|
|
return response, err
|
|
}
|
|
dataWaRecipient, err := whatsapp.ValidateJidWithLogin(service.WaCli, request.Phone)
|
|
if err != nil {
|
|
return response, err
|
|
}
|
|
|
|
ts, err := service.WaCli.SendMessage(ctx, dataWaRecipient, service.WaCli.BuildPollCreation(request.Question, request.Options, request.MaxAnswer))
|
|
if err != nil {
|
|
return response, err
|
|
}
|
|
|
|
response.MessageID = ts.ID
|
|
response.Status = fmt.Sprintf("Send poll success %s (server timestamp: %s)", request.Phone, ts.Timestamp.String())
|
|
return response, nil
|
|
}
|
|
|
|
func (service serviceSend) getMentionFromText(ctx context.Context, messages string) (result []string) {
|
|
mentions := utils.ContainsMention(messages)
|
|
for _, mention := range mentions {
|
|
// Get JID from phone number
|
|
if dataWaRecipient, err := whatsapp.ValidateJidWithLogin(service.WaCli, mention); err == nil {
|
|
result = append(result, dataWaRecipient.String())
|
|
}
|
|
}
|
|
return result
|
|
|
|
}
|