diff --git a/.github/workflows/deploy-linux.yml b/.github/workflows/deploy-linux.yml index aad8d56..f7f42af 100644 --- a/.github/workflows/deploy-linux.yml +++ b/.github/workflows/deploy-linux.yml @@ -22,7 +22,7 @@ jobs: - name: Golang Installation uses: actions/setup-go@v3 with: - go-version: '1.20' + go-version: '1.21' - name: Golang setup dependency run: | go version diff --git a/.github/workflows/deploy-mac.yml b/.github/workflows/deploy-mac.yml index 5008a90..ab47cfa 100644 --- a/.github/workflows/deploy-mac.yml +++ b/.github/workflows/deploy-mac.yml @@ -21,7 +21,7 @@ jobs: - name: Golang Installation uses: actions/setup-go@v3 with: - go-version: '1.20' + go-version: '1.21' - name: Golang setup dependency run: | go version diff --git a/.github/workflows/deploy-windows.yml b/.github/workflows/deploy-windows.yml index f0ccf87..305783f 100644 --- a/.github/workflows/deploy-windows.yml +++ b/.github/workflows/deploy-windows.yml @@ -34,7 +34,7 @@ jobs: - name: Golang Installation uses: actions/setup-go@v3 with: - go-version: '1.20' + go-version: '1.21' - name: Golang setup dependency run: | go version diff --git a/readme.md b/readme.md index 605519b..f204f8a 100644 --- a/readme.md +++ b/readme.md @@ -104,6 +104,7 @@ API using [openapi-generator](https://openapi-generator.tech/#try) | ✅ | User My Privacy Setting | GET | /user/my/privacy | | ✅ | Send Message | POST | /send/message | | ✅ | Send Image | POST | /send/image | +| ✅ | Send Audio | POST | /send/audio | | ✅ | Send File | POST | /send/file | | ✅ | Send Video | POST | /send/video | | ✅ | Send Contact | POST | /send/contact | diff --git a/src/config/settings.go b/src/config/settings.go index 93ea637..6bd5b03 100644 --- a/src/config/settings.go +++ b/src/config/settings.go @@ -6,7 +6,7 @@ import ( ) var ( - AppVersion = "v4.8.4" + AppVersion = "v4.9.0" AppPort = "3000" AppDebug = false AppOs = fmt.Sprintf("AldinoKemal") diff --git a/src/domains/send/audio.go b/src/domains/send/audio.go new file mode 100644 index 0000000..f2e5063 --- /dev/null +++ b/src/domains/send/audio.go @@ -0,0 +1,8 @@ +package send + +import "mime/multipart" + +type AudioRequest struct { + Phone string `json:"phone" form:"phone"` + Audio *multipart.FileHeader `json:"Audio" form:"Audio"` +} diff --git a/src/domains/send/send.go b/src/domains/send/send.go index 6402588..c848050 100644 --- a/src/domains/send/send.go +++ b/src/domains/send/send.go @@ -12,6 +12,7 @@ type ISendService interface { SendContact(ctx context.Context, request ContactRequest) (response GenericResponse, err error) SendLink(ctx context.Context, request LinkRequest) (response GenericResponse, err error) SendLocation(ctx context.Context, request LocationRequest) (response GenericResponse, err error) + SendAudio(ctx context.Context, request AudioRequest) (response GenericResponse, err error) } type GenericResponse struct { diff --git a/src/go.mod b/src/go.mod index e8244f9..7d393ad 100644 --- a/src/go.mod +++ b/src/go.mod @@ -12,14 +12,14 @@ require ( github.com/google/uuid v1.6.0 github.com/h2non/bimg v1.1.9 github.com/markbates/pkger v0.17.1 - github.com/mattn/go-sqlite3 v1.14.20 + github.com/mattn/go-sqlite3 v1.14.22 github.com/sirupsen/logrus v1.9.3 github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e github.com/spf13/cobra v1.8.0 github.com/stretchr/testify v1.8.4 github.com/valyala/fasthttp v1.51.0 go.mau.fi/libsignal v0.1.0 - go.mau.fi/whatsmeow v0.0.0-20240129221825-0bb41340eb03 + go.mau.fi/whatsmeow v0.0.0-20240201213949-57f290eebe9b google.golang.org/protobuf v1.32.0 ) diff --git a/src/go.sum b/src/go.sum index 8bea583..c8724bc 100644 --- a/src/go.sum +++ b/src/go.sum @@ -78,6 +78,8 @@ github.com/mattn/go-sqlite3 v1.14.19 h1:fhGleo2h1p8tVChob4I9HpmVFIAkKGpiukdrgQbW github.com/mattn/go-sqlite3 v1.14.19/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/mattn/go-sqlite3 v1.14.20 h1:BAZ50Ns0OFBNxdAqFhbZqdPcht1Xlb16pDCqkq1spr0= github.com/mattn/go-sqlite3 v1.14.20/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= @@ -119,6 +121,8 @@ go.mau.fi/util v0.3.0 h1:Lt3lbRXP6ZBqTINK0EieRWor3zEwwwrDT14Z5N8RUCs= go.mau.fi/util v0.3.0/go.mod h1:9dGsBCCbZJstx16YgnVMVi3O2bOizELoKpugLD4FoGs= go.mau.fi/whatsmeow v0.0.0-20240129221825-0bb41340eb03 h1:EWoQvfZydwqrRK6bYPlqQaLYAOJ8MW//ut6a/x2xlyw= go.mau.fi/whatsmeow v0.0.0-20240129221825-0bb41340eb03/go.mod h1:5xTtHNaZpGni6z6aE1iEopjW7wNgsKcolZxZrOujK9M= +go.mau.fi/whatsmeow v0.0.0-20240201213949-57f290eebe9b h1:4d7OK8g0F3T92MAcNySmXRZzEdw0OdsWpWzbxeNjJHM= +go.mau.fi/whatsmeow v0.0.0-20240201213949-57f290eebe9b/go.mod h1:5xTtHNaZpGni6z6aE1iEopjW7wNgsKcolZxZrOujK9M= 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.15.0 h1:frVn1TEaCEaZcn3Tmd7Y2b5KKPaZ+I32Q2OA3kYp5TA= diff --git a/src/internal/rest/helpers/common.go b/src/internal/rest/helpers/common.go index a0686bd..592b4e4 100644 --- a/src/internal/rest/helpers/common.go +++ b/src/internal/rest/helpers/common.go @@ -3,6 +3,7 @@ package helpers import ( "context" domainApp "github.com/aldinokemal/go-whatsapp-web-multidevice/domains/app" + "mime/multipart" "time" ) @@ -10,3 +11,13 @@ func SetAutoConnectAfterBooting(service domainApp.IAppService) { time.Sleep(2 * time.Second) _ = service.Reconnect(context.Background()) } + +func MultipartFormFileHeaderToBytes(fileHeader *multipart.FileHeader) []byte { + file, _ := fileHeader.Open() + defer file.Close() + + fileBytes := make([]byte, fileHeader.Size) + _, _ = file.Read(fileBytes) + + return fileBytes +} diff --git a/src/internal/rest/send.go b/src/internal/rest/send.go index efcec4f..a27cfbe 100644 --- a/src/internal/rest/send.go +++ b/src/internal/rest/send.go @@ -20,6 +20,7 @@ func InitRestSend(app *fiber.App, service domainSend.ISendService) Send { app.Post("/send/contact", rest.SendContact) app.Post("/send/link", rest.SendLink) app.Post("/send/location", rest.SendLocation) + app.Post("/send/audio", rest.SendAudio) return rest } @@ -162,3 +163,25 @@ func (controller *Send) SendLocation(c *fiber.Ctx) error { Results: response, }) } + +func (controller *Send) SendAudio(c *fiber.Ctx) error { + var request domainSend.AudioRequest + err := c.BodyParser(&request) + utils.PanicIfNeeded(err) + + audio, err := c.FormFile("audio") + utils.PanicIfNeeded(err) + + request.Audio = audio + whatsapp.SanitizePhone(&request.Phone) + + response, err := controller.Service.SendAudio(c.UserContext(), request) + utils.PanicIfNeeded(err) + + return c.JSON(utils.ResponseData{ + Status: 200, + Code: "SUCCESS", + Message: response.Status, + Results: response, + }) +} diff --git a/src/pkg/error/whatsapp_error.go b/src/pkg/error/whatsapp_error.go index 2dfd099..db2432a 100644 --- a/src/pkg/error/whatsapp_error.go +++ b/src/pkg/error/whatsapp_error.go @@ -53,6 +53,23 @@ func (e WaCliError) StatusCode() int { return http.StatusInternalServerError } +type WaUploadMediaError string + +// Error for complying the error interface +func (e WaUploadMediaError) Error() string { + return string(e) +} + +// ErrCode will return the error code based on the error data type +func (e WaUploadMediaError) ErrCode() string { + return "UPLOAD_MEDIA_ERROR" +} + +// StatusCode will return the HTTP status code based on the error data type +func (e WaUploadMediaError) StatusCode() int { + return http.StatusInternalServerError +} + const ( ErrInvalidJID = InvalidJID("your JID is invalid") ErrWaCLI = WaCliError("your WhatsApp CLI is invalid or empty") diff --git a/src/services/send.go b/src/services/send.go index ff93a23..57b04c7 100644 --- a/src/services/send.go +++ b/src/services/send.go @@ -6,6 +6,7 @@ import ( "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" @@ -188,18 +189,14 @@ func (service serviceSend) SendFile(ctx context.Context, request domainSend.File return response, err } - oriFilePath := fmt.Sprintf("%s/%s", config.PathSendItems, request.File.Filename) - err = fasthttp.SaveMultipartFile(request.File, oriFilePath) - if err != nil { - return response, err - } + fileBytes := helpers.MultipartFormFileHeaderToBytes(request.File) + fileMimeType := http.DetectContentType(fileBytes) // Send to WA server - dataWaFile, err := os.ReadFile(oriFilePath) if err != nil { return response, err } - uploadedFile, err := service.WaCli.Upload(context.Background(), dataWaFile, whatsmeow.MediaDocument) + uploadedFile, err := service.WaCli.Upload(context.Background(), fileBytes, whatsmeow.MediaDocument) if err != nil { fmt.Printf("Failed to upload file: %v", err) return response, err @@ -207,7 +204,7 @@ func (service serviceSend) SendFile(ctx context.Context, request domainSend.File msg := &waProto.Message{DocumentMessage: &waProto.DocumentMessage{ Url: proto.String(uploadedFile.URL), - Mimetype: proto.String(http.DetectContentType(dataWaFile)), + Mimetype: proto.String(fileMimeType), Title: proto.String(request.File.Filename), FileSha256: uploadedFile.FileSHA256, FileLength: proto.Uint64(uploadedFile.FileLength), @@ -218,12 +215,6 @@ func (service serviceSend) SendFile(ctx context.Context, request domainSend.File Caption: proto.String(request.Caption), }} 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 } @@ -425,3 +416,44 @@ func (service serviceSend) SendLocation(ctx context.Context, request domainSend. 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 := &waProto.Message{ + AudioMessage: &waProto.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 +} diff --git a/src/validations/send_validation.go b/src/validations/send_validation.go index cbef06a..344589c 100644 --- a/src/validations/send_validation.go +++ b/src/validations/send_validation.go @@ -133,3 +133,45 @@ func ValidateSendLocation(ctx context.Context, request domainSend.LocationReques return nil } + +func ValidateSendAudio(ctx context.Context, request domainSend.AudioRequest) error { + err := validation.ValidateStructWithContext(ctx, &request, + validation.Field(&request.Phone, validation.Required), + validation.Field(&request.Audio, validation.Required), + ) + + if err != nil { + return pkgError.ValidationError(err.Error()) + } + + availableMimes := map[string]bool{ + "audio/aac": true, + "audio/amr": true, + "audio/flac": true, + "audio/m4a": true, + "audio/m4r": true, + "audio/mp3": true, + "audio/mpeg": true, + "audio/ogg": true, + + "audio/wma": true, + "audio/x-ms-wma": true, + + "audio/wav": true, + "audio/vnd.wav": true, + "audio/vnd.wave": true, + "audio/wave": true, + "audio/x-pn-wav": true, + "audio/x-wav": true, + } + availableMimesStr := "" + for k := range availableMimes { + availableMimesStr += k + "," + } + + if !availableMimes[request.Audio.Header.Get("Content-Type")] { + return pkgError.ValidationError(fmt.Sprintf("your audio type is not allowed. please use (%s)", availableMimesStr)) + } + + return nil +}