diff --git a/LICENCE.txt b/LICENCE.txt new file mode 100644 index 0000000..34fbff4 --- /dev/null +++ b/LICENCE.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Aldino Kemal + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/docs/openapi.yaml b/docs/openapi.yaml index 7c102c6..a81d805 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -1,7 +1,7 @@ openapi: 3.0.0 info: title: WhatsApp API MultiDevice - version: 3.6.0 + version: 3.7.0 description: This API is used for sending whatsapp via API servers: - url: http://localhost:3000 @@ -214,6 +214,10 @@ paths: type: string example: This is a *test* _message_ 😊 description: Message to send + reply_message_id: + type: string + example: 3EB089B9D6ADD58153C561 + description: Message ID that you want reply responses: '200': description: OK diff --git a/src/cmd/root.go b/src/cmd/root.go index b3162a2..efe8e31 100644 --- a/src/cmd/root.go +++ b/src/cmd/root.go @@ -99,7 +99,7 @@ func runRest(_ *cobra.Command, _ []string) { // Service appService := services.NewAppService(cli, db) - sendService := services.NewSendService(cli) + sendService := services.NewSendService(cli, appService) userService := services.NewUserService(cli) messageService := services.NewMessageService(cli) groupService := services.NewGroupService(cli) diff --git a/src/config/settings.go b/src/config/settings.go index f0b3a49..319ca6d 100644 --- a/src/config/settings.go +++ b/src/config/settings.go @@ -6,7 +6,7 @@ import ( ) var ( - AppVersion = "v4.7.4" + AppVersion = "v4.8.0" AppPort = "3000" AppDebug = false AppOs = fmt.Sprintf("AldinoKemal") diff --git a/src/domains/app/app.go b/src/domains/app/app.go index 03b7be0..8c00e8f 100644 --- a/src/domains/app/app.go +++ b/src/domains/app/app.go @@ -2,11 +2,24 @@ package app import ( "context" + "time" ) 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) + FirstDevice(ctx context.Context) (response DevicesResponse, err error) + FetchDevices(ctx context.Context) (response []DevicesResponse, err error) +} + +type DevicesResponse struct { + Name string `json:"name"` + Device string `json:"device"` +} + +type LoginResponse struct { + ImagePath string `json:"image_path"` + Duration time.Duration `json:"duration"` + Code string `json:"code"` } diff --git a/src/domains/app/devices.go b/src/domains/app/devices.go deleted file mode 100644 index 34c6406..0000000 --- a/src/domains/app/devices.go +++ /dev/null @@ -1,6 +0,0 @@ -package app - -type FetchDevicesResponse struct { - Name string `json:"name"` - Device string `json:"device"` -} diff --git a/src/domains/app/login.go b/src/domains/app/login.go deleted file mode 100644 index fc8f98b..0000000 --- a/src/domains/app/login.go +++ /dev/null @@ -1,9 +0,0 @@ -package app - -import "time" - -type LoginResponse struct { - ImagePath string `json:"image_path"` - Duration time.Duration `json:"duration"` - Code string `json:"code"` -} diff --git a/src/domains/send/text.go b/src/domains/send/text.go index 90484b0..c572f36 100644 --- a/src/domains/send/text.go +++ b/src/domains/send/text.go @@ -1,8 +1,9 @@ package send type MessageRequest struct { - Phone string `json:"phone" form:"phone"` - Message string `json:"message" form:"message"` + Phone string `json:"phone" form:"phone"` + Message string `json:"message" form:"message"` + ReplyMessageID *string `json:"reply_message_id" form:"reply_message_id"` } type MessageResponse struct { diff --git a/src/go.mod b/src/go.mod index c93b13d..b7e7b6c 100644 --- a/src/go.mod +++ b/src/go.mod @@ -1,6 +1,6 @@ module github.com/aldinokemal/go-whatsapp-web-multidevice -go 1.20 +go 1.21 require ( github.com/PuerkitoBio/goquery v1.8.1 @@ -9,7 +9,7 @@ require ( github.com/gofiber/fiber/v2 v2.48.0 github.com/gofiber/template/html/v2 v2.0.5 github.com/gofiber/websocket/v2 v2.2.1 - github.com/google/uuid v1.3.0 + github.com/google/uuid v1.3.1 github.com/h2non/bimg v1.1.9 github.com/markbates/pkger v0.17.1 github.com/mattn/go-sqlite3 v1.14.17 @@ -19,7 +19,7 @@ require ( github.com/stretchr/testify v1.8.4 github.com/valyala/fasthttp v1.48.0 go.mau.fi/libsignal v0.1.0 - go.mau.fi/whatsmeow v0.0.0-20230916142552-a743fdc23bf1 + go.mau.fi/whatsmeow v0.0.0-20231006094259-c6eaf52f1357 google.golang.org/protobuf v1.31.0 gopkg.in/yaml.v2 v2.4.0 github.com/json-iterator/go v1.1.12 diff --git a/src/go.sum b/src/go.sum index d28effb..2d93606 100644 --- a/src/go.sum +++ b/src/go.sum @@ -35,8 +35,8 @@ github.com/gofiber/websocket/v2 v2.2.1/go.mod h1:Ao/+nyNnX5u/hIFPuHl28a+NIkrqK7P github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= +github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/h2non/bimg v1.1.9 h1:WH20Nxko9l/HFm4kZCA3Phbgu2cbHvYzxwxn9YROEGg= @@ -96,14 +96,10 @@ go.mau.fi/libsignal v0.1.0 h1:vAKI/nJ5tMhdzke4cTK1fb0idJzz1JuEIpmjprueC+c= go.mau.fi/libsignal v0.1.0/go.mod h1:R8ovrTezxtUNzCQE5PH30StOQWWeBskBsWE55vMfY9I= go.mau.fi/util v0.1.0 h1:BwIFWIOEeO7lsiI2eWKFkWTfc5yQmoe+0FYyOFVyaoE= go.mau.fi/util v0.1.0/go.mod h1:AxuJUMCxpzgJ5eV9JbPWKRH8aAJJidxetNdUj7qcb84= -go.mau.fi/whatsmeow v0.0.0-20230718190209-efef6f1cec8e h1:i2atPgn2MRLGxisk+EZM1b1RfPh+4dZxSc8OdyvzutY= -go.mau.fi/whatsmeow v0.0.0-20230718190209-efef6f1cec8e/go.mod h1:+ObGpFE6cbbY4hKc1FmQH9MVfqaemmlXGXSnwDvCOyE= -go.mau.fi/whatsmeow v0.0.0-20230916142552-a743fdc23bf1 h1:tfVqib0PAAgMJrZu/Ko25J436e91HKgZepwdhgPmeHM= -go.mau.fi/whatsmeow v0.0.0-20230916142552-a743fdc23bf1/go.mod h1:1xFS2b5zqsg53ApsYB4FDtko7xG7r+gVgBjh9k+9/GE= +go.mau.fi/whatsmeow v0.0.0-20231006094259-c6eaf52f1357 h1:/le5a7Gzr9MhiZghMs/Aq3az0vyknJSqWUKdQvRaAIg= +go.mau.fi/whatsmeow v0.0.0-20231006094259-c6eaf52f1357/go.mod h1:1xFS2b5zqsg53ApsYB4FDtko7xG7r+gVgBjh9k+9/GE= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= -golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck= golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= @@ -126,8 +122,6 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= -golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= diff --git a/src/pkg/whatsapp/whatsapp.go b/src/pkg/whatsapp/whatsapp.go index b61ddf5..cd52dca 100644 --- a/src/pkg/whatsapp/whatsapp.go +++ b/src/pkg/whatsapp/whatsapp.go @@ -35,6 +35,22 @@ var ( startupTime = time.Now().Unix() ) +type ExtractedMedia struct { + MediaPath string `json:"media_path"` + MimeType string `json:"mime_type"` + Caption string `json:"caption"` +} + +type evtReaction struct { + ID string `json:"id"` + Message string `json:"message"` +} + +type evtMessage struct { + ID string `json:"id"` + Text string `json:"text"` +} + func SanitizePhone(phone *string) { if phone != nil && len(*phone) > 0 && !strings.Contains(*phone, "@") { if len(*phone) <= 15 { @@ -201,7 +217,7 @@ func handler(rawEvt interface{}) { img := evt.Message.GetImageMessage() if img != nil { - path, err := DownloadMedia(config.PathStorages, img) + path, err := ExtractMedia(config.PathStorages, img) if err != nil { log.Errorf("Failed to download image: %v", err) } else { @@ -270,9 +286,11 @@ func forwardToWebhook(evt *events.Message) error { audioMedia := evt.Message.GetAudioMessage() documentMedia := evt.Message.GetDocumentMessage() - message := evt.Message.GetConversation() + var message evtMessage + message.Text = evt.Message.GetConversation() + message.ID = evt.Info.ID if extendedMessage := evt.Message.ExtendedTextMessage.GetText(); extendedMessage != "" { - message = extendedMessage + message.Text = extendedMessage } var quotedmessage any @@ -289,62 +307,62 @@ func forwardToWebhook(evt *events.Message) error { } } - var reactionMessage any + var waReaction *evtReaction if evt.Message.ReactionMessage != nil { - reactionMessage = *evt.Message.ReactionMessage.Text + waReaction.Message = evt.Message.ReactionMessage.GetText() + waReaction.ID = evt.Message.ReactionMessage.GetKey().GetId() } body := map[string]interface{}{ - "audio": audioMedia, - "contact": evt.Message.GetContactMessage(), - "document": documentMedia, - "forwarded": forwarded, - "from": evt.Info.SourceString(), - "image": imageMedia, - "list": evt.Message.GetListMessage(), - "live_location": evt.Message.GetLiveLocationMessage(), - "location": evt.Message.GetLocationMessage(), - "message": message, - "message_id": evt.Info.ID, - "order": evt.Message.GetOrderMessage(), - "pushname": evt.Info.PushName, - "quoted_message": quotedmessage, - "reaction_message": reactionMessage, - "sticker": stickerMedia, - "video": videoMedia, - "view_once": evt.Message.GetViewOnceMessage(), + "audio": audioMedia, + "contact": evt.Message.GetContactMessage(), + "document": documentMedia, + "forwarded": forwarded, + "from": evt.Info.SourceString(), + "image": imageMedia, + "list": evt.Message.GetListMessage(), + "live_location": evt.Message.GetLiveLocationMessage(), + "location": evt.Message.GetLocationMessage(), + "message": message, + "order": evt.Message.GetOrderMessage(), + "pushname": evt.Info.PushName, + "quoted_message": quotedmessage, + "reaction": waReaction, + "sticker": stickerMedia, + "video": videoMedia, + "view_once": evt.Message.GetViewOnceMessage(), } if imageMedia != nil { - path, err := DownloadMedia(config.PathMedia, imageMedia) + path, err := ExtractMedia(config.PathMedia, imageMedia) if err != nil { return pkgError.WebhookError(fmt.Sprintf("Failed to download image: %v", err)) } body["image"] = path } if stickerMedia != nil { - path, err := DownloadMedia(config.PathMedia, stickerMedia) + path, err := ExtractMedia(config.PathMedia, stickerMedia) if err != nil { return pkgError.WebhookError(fmt.Sprintf("Failed to download sticker: %v", err)) } body["sticker"] = path } if videoMedia != nil { - path, err := DownloadMedia(config.PathMedia, videoMedia) + path, err := ExtractMedia(config.PathMedia, videoMedia) if err != nil { return pkgError.WebhookError(fmt.Sprintf("Failed to download video: %v", err)) } body["video"] = path } if audioMedia != nil { - path, err := DownloadMedia(config.PathMedia, audioMedia) + path, err := ExtractMedia(config.PathMedia, audioMedia) if err != nil { return pkgError.WebhookError(fmt.Sprintf("Failed to download audio: %v", err)) } body["audio"] = path } if documentMedia != nil { - path, err := DownloadMedia(config.PathMedia, documentMedia) + path, err := ExtractMedia(config.PathMedia, documentMedia) if err != nil { return pkgError.WebhookError(fmt.Sprintf("Failed to download document: %v", err)) } @@ -390,37 +408,39 @@ func extractPhoneNumber(jid string) string { return "" } -// DownloadMedia is a helper function to download media from whatsapp -func DownloadMedia(storageLocation string, mediaFile whatsmeow.DownloadableMessage) (path string, err error) { +// ExtractMedia is a helper function to extract media from whatsapp +func ExtractMedia(storageLocation string, mediaFile whatsmeow.DownloadableMessage) (extractedMedia ExtractedMedia, err error) { if mediaFile == nil { logrus.Info("Skip download because data is nil") - return "", nil + return extractedMedia, nil } data, err := cli.Download(mediaFile) if err != nil { - return path, err + return extractedMedia, err } - var mimeType string switch media := mediaFile.(type) { case *waProto.ImageMessage: - mimeType = media.GetMimetype() + extractedMedia.MimeType = media.GetMimetype() + extractedMedia.Caption = media.GetCaption() case *waProto.AudioMessage: - mimeType = media.GetMimetype() + extractedMedia.MimeType = media.GetMimetype() case *waProto.VideoMessage: - mimeType = media.GetMimetype() + extractedMedia.MimeType = media.GetMimetype() + extractedMedia.Caption = media.GetCaption() case *waProto.StickerMessage: - mimeType = media.GetMimetype() + extractedMedia.MimeType = media.GetMimetype() case *waProto.DocumentMessage: - mimeType = media.GetMimetype() + extractedMedia.MimeType = media.GetMimetype() + extractedMedia.Caption = media.GetCaption() } - extensions, _ := mime.ExtensionsByType(mimeType) - path = fmt.Sprintf("%s/%d-%s%s", storageLocation, time.Now().Unix(), uuid.NewString(), extensions[0]) - err = os.WriteFile(path, data, 0600) + extensions, _ := mime.ExtensionsByType(extractedMedia.MimeType) + extractedMedia.MediaPath = fmt.Sprintf("%s/%d-%s%s", storageLocation, time.Now().Unix(), uuid.NewString(), extensions[0]) + err = os.WriteFile(extractedMedia.MediaPath, data, 0600) if err != nil { - return path, err + return extractedMedia, err } - return path, nil + return extractedMedia, nil } diff --git a/src/services/app.go b/src/services/app.go index 27c0993..9cf9d40 100644 --- a/src/services/app.go +++ b/src/services/app.go @@ -140,7 +140,27 @@ func (service serviceApp) Reconnect(_ context.Context) (err error) { return service.WaCli.Connect() } -func (service serviceApp) FetchDevices(_ context.Context) (response []domainApp.FetchDevicesResponse, err error) { +func (service serviceApp) FirstDevice(ctx context.Context) (response domainApp.DevicesResponse, err error) { + if service.WaCli == nil { + return response, pkgError.ErrWaCLI + } + + devices, err := service.db.GetFirstDevice() + if err != nil { + return response, err + } + + response.Device = devices.ID.String() + if devices.PushName != "" { + response.Name = devices.PushName + } else { + response.Name = devices.BusinessName + } + + return response, nil +} + +func (service serviceApp) FetchDevices(_ context.Context) (response []domainApp.DevicesResponse, err error) { if service.WaCli == nil { return response, pkgError.ErrWaCLI } @@ -151,7 +171,7 @@ func (service serviceApp) FetchDevices(_ context.Context) (response []domainApp. } for _, device := range devices { - var d domainApp.FetchDevicesResponse + var d domainApp.DevicesResponse d.Device = device.ID.String() if device.PushName != "" { d.Name = device.PushName diff --git a/src/services/send.go b/src/services/send.go index b1170d2..7e090fc 100644 --- a/src/services/send.go +++ b/src/services/send.go @@ -4,6 +4,7 @@ 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" pkgError "github.com/aldinokemal/go-whatsapp-web-multidevice/pkg/error" "github.com/aldinokemal/go-whatsapp-web-multidevice/pkg/utils" @@ -21,7 +22,8 @@ import ( ) type serviceSend struct { - WaCli *whatsmeow.Client + WaCli *whatsmeow.Client + appService app.IAppService } type metadata struct { @@ -29,9 +31,10 @@ type metadata struct { Content string } -func NewSendService(waCli *whatsmeow.Client) domainSend.ISendService { +func NewSendService(waCli *whatsmeow.Client, appService app.IAppService) domainSend.ISendService { return &serviceSend{ - WaCli: waCli, + WaCli: waCli, + appService: appService, } } @@ -45,7 +48,34 @@ func (service serviceSend) SendText(ctx context.Context, request domainSend.Mess return response, err } + // Send message msg := &waProto.Message{Conversation: proto.String(request.Message)} + + // 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 = &waProto.Message{ + ExtendedTextMessage: &waProto.ExtendedTextMessage{ + Text: proto.String(request.Message), + ContextInfo: &waProto.ContextInfo{ + StanzaId: request.ReplyMessageID, + Participant: proto.String(participantJID), + QuotedMessage: &waProto.Message{ + Conversation: proto.String(request.Message), + }, + }, + }, + } + } + ts, err := service.WaCli.SendMessage(ctx, dataWaRecipient, msg) if err != nil { return response, err diff --git a/src/views/index.html b/src/views/index.html index 600378c..d1ae713 100644 --- a/src/views/index.html +++ b/src/views/index.html @@ -248,6 +248,11 @@ aria-label="phone"> +
+ + +