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.
 
 
 
 
 

570 lines
19 KiB

package services
import (
"context"
"fmt"
"net/http"
"os"
"os/exec"
"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"
"go.mau.fi/whatsmeow/types"
"google.golang.org/protobuf/proto"
)
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,
}
}
// wrapSendMessage wraps the message sending process with message ID saving
func (service serviceSend) wrapSendMessage(ctx context.Context, recipient types.JID, msg *waE2E.Message, content string) (whatsmeow.SendResponse, error) {
ts, err := service.WaCli.SendMessage(ctx, recipient, msg)
if err != nil {
return whatsmeow.SendResponse{}, err
}
utils.RecordMessage(ts.ID, service.WaCli.Store.ID.String(), content)
return ts, nil
}
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 != "" {
record, err := utils.FindRecordFromStorage(*request.ReplyMessageID)
if err == nil { // Only set reply context if we found the message ID
msg.ExtendedTextMessage = &waE2E.ExtendedTextMessage{
Text: proto.String(request.Message),
ContextInfo: &waE2E.ContextInfo{
StanzaID: request.ReplyMessageID,
Participant: proto.String(record.JID),
QuotedMessage: &waE2E.Message{
Conversation: proto.String(record.MessageContent),
},
},
}
if len(parsedMentions) > 0 {
msg.ExtendedTextMessage.ContextInfo.MentionedJID = parsedMentions
}
} else {
logrus.Warnf("Reply message ID %s not found in storage, continuing without reply context", *request.ReplyMessageID)
}
}
ts, err := service.wrapSendMessage(ctx, dataWaRecipient, msg, request.Message)
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.uploadMedia(ctx, whatsmeow.MediaImage, dataWaImage, dataWaRecipient)
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),
}}
caption := "🖼️ Image"
if request.Caption != "" {
caption = "🖼️ " + request.Caption
}
ts, err := service.wrapSendMessage(ctx, dataWaRecipient, msg, caption)
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.uploadMedia(ctx, whatsmeow.MediaDocument, fileBytes, dataWaRecipient)
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),
}}
caption := "📄 Document"
if request.Caption != "" {
caption = "📄 " + request.Caption
}
ts, err := service.wrapSendMessage(ctx, dataWaRecipient, msg, caption)
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.uploadMedia(ctx, whatsmeow.MediaVideo, dataWaVideo, dataWaRecipient)
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),
}}
caption := "🎥 Video"
if request.Caption != "" {
caption = "🎥 " + request.Caption
}
ts, err := service.wrapSendMessage(ctx, dataWaRecipient, msg, caption)
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),
}}
content := "👤 " + request.ContactName
ts, err := service.wrapSendMessage(ctx, dataWaRecipient, msg, content)
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),
MatchedText: proto.String(request.Link),
Description: proto.String(getMetaDataFromURL.Description),
JPEGThumbnail: getMetaDataFromURL.ImageThumb,
}}
content := "🔗 " + request.Link
if request.Caption != "" {
content = "🔗 " + request.Caption
}
ts, err := service.wrapSendMessage(ctx, dataWaRecipient, msg, content)
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)),
},
}
content := "📍 " + request.Latitude + ", " + request.Longitude
// Send WhatsApp Message Proto
ts, err := service.wrapSendMessage(ctx, dataWaRecipient, msg, content)
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.uploadMedia(ctx, whatsmeow.MediaAudio, autioBytes, dataWaRecipient)
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,
},
}
content := "🎵 Audio"
ts, err := service.wrapSendMessage(ctx, dataWaRecipient, msg, content)
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
}
content := "📊 " + request.Question
ts, err := service.wrapSendMessage(ctx, dataWaRecipient, service.WaCli.BuildPollCreation(request.Question, request.Options, request.MaxAnswer), content)
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) SendPresence(ctx context.Context, request domainSend.PresenceRequest) (response domainSend.GenericResponse, err error) {
err = validations.ValidateSendPresence(ctx, request)
if err != nil {
return response, err
}
err = service.WaCli.SendPresence(types.Presence(request.Type))
if err != nil {
return response, err
}
response.MessageID = "presence"
response.Status = fmt.Sprintf("Send presence success %s", request.Type)
return response, nil
}
func (service serviceSend) getMentionFromText(_ 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
}
func (service serviceSend) uploadMedia(ctx context.Context, mediaType whatsmeow.MediaType, media []byte, recipient types.JID) (uploaded whatsmeow.UploadResponse, err error) {
if recipient.Server == types.NewsletterServer {
uploaded, err = service.WaCli.UploadNewsletter(ctx, media, mediaType)
} else {
uploaded, err = service.WaCli.Upload(ctx, media, mediaType)
}
return uploaded, err
}