diff --git a/docs/openapi.yaml b/docs/openapi.yaml index d274d61..cdcd0ed 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -1,7 +1,7 @@ openapi: "3.0.0" info: title: WhatsApp API MultiDevice - version: 5.0.0 + version: 5.1.0 description: This API is used for sending whatsapp via API servers: - url: http://localhost:3000 @@ -131,8 +131,9 @@ paths: - name: phone in: query schema: - type: integer + type: string example: '6289685028129@s.whatsapp.net' + description: Phone number with country code responses: '200': description: OK @@ -162,13 +163,15 @@ paths: - name: phone in: query schema: - type: integer + type: string example: '6289685028129@s.whatsapp.net' + description: Phone number with country code - name: is_preview in: query schema: type: boolean example: true + description: Whether to fetch a preview of the avatar responses: '200': description: OK @@ -350,6 +353,10 @@ paths: type: string format: binary description: Image to send + image_url: + type: string + example: https://example.com/image.jpg + description: Image URL to send compress: type: boolean example: false @@ -1320,13 +1327,17 @@ components: type: number UserInfoResponse: type: object + required: + - code + - message + - results properties: code: type: string example: SUCCESS message: type: string - example: + example: Success results: type: object properties: @@ -1574,7 +1585,7 @@ components: properties: text: type: string - example: "WhatsApp’s official channel. Follow for our latest feature launches, updates, exclusive drops and more." + example: "WhatsApp's official channel. Follow for our latest feature launches, updates, exclusive drops and more." id: type: string example: "1689653839450668" diff --git a/src/config/settings.go b/src/config/settings.go index 3e2e64e..f35b027 100644 --- a/src/config/settings.go +++ b/src/config/settings.go @@ -5,7 +5,7 @@ import ( ) var ( - AppVersion = "v5.0.0" + AppVersion = "v5.1.0" AppPort = "3000" AppDebug = false AppOs = "AldinoKemal" @@ -24,6 +24,7 @@ var ( WhatsappWebhook []string WhatsappWebhookSecret = "secret" WhatsappLogLevel = "ERROR" + WhatsappSettingMaxImageSize int64 = 20000000 // 20MB WhatsappSettingMaxFileSize int64 = 50000000 // 50MB WhatsappSettingMaxVideoSize int64 = 100000000 // 100MB WhatsappSettingMaxDownloadSize int64 = 500000000 // 500MB diff --git a/src/domains/send/audio.go b/src/domains/send/audio.go index f2e5063..16da723 100644 --- a/src/domains/send/audio.go +++ b/src/domains/send/audio.go @@ -4,5 +4,5 @@ import "mime/multipart" type AudioRequest struct { Phone string `json:"phone" form:"phone"` - Audio *multipart.FileHeader `json:"Audio" form:"Audio"` + Audio *multipart.FileHeader `json:"audio" form:"audio"` } diff --git a/src/domains/send/image.go b/src/domains/send/image.go index 783b030..b6baf95 100644 --- a/src/domains/send/image.go +++ b/src/domains/send/image.go @@ -6,6 +6,7 @@ type ImageRequest struct { Phone string `json:"phone" form:"phone"` Caption string `json:"caption" form:"caption"` Image *multipart.FileHeader `json:"image" form:"image"` + ImageURL *string `json:"image_url" form:"image_url"` ViewOnce bool `json:"view_once" form:"view_once"` Compress bool `json:"compress"` } diff --git a/src/go.mod b/src/go.mod index bd2a599..023cd50 100644 --- a/src/go.mod +++ b/src/go.mod @@ -21,7 +21,7 @@ require ( github.com/stretchr/testify v1.10.0 github.com/valyala/fasthttp v1.58.0 go.mau.fi/libsignal v0.1.1 - go.mau.fi/whatsmeow v0.0.0-20250130221717-faf72d668860 + go.mau.fi/whatsmeow v0.0.0-20250204095649-a75587ab11d7 google.golang.org/protobuf v1.36.4 ) @@ -50,9 +50,9 @@ require ( github.com/valyala/tcplisten v1.0.0 // indirect go.mau.fi/util v0.8.4 // indirect golang.org/x/crypto v0.32.0 // indirect - golang.org/x/image v0.23.0 // indirect + golang.org/x/image v0.24.0 // indirect golang.org/x/net v0.34.0 // indirect - golang.org/x/sys v0.29.0 // indirect + golang.org/x/sys v0.30.0 // indirect gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/src/go.sum b/src/go.sum index d39c350..99b6be9 100644 --- a/src/go.sum +++ b/src/go.sum @@ -105,6 +105,8 @@ go.mau.fi/whatsmeow v0.0.0-20250104105216-918c879fcd19 h1:uVS+Zct5fF8rSXV9lfs87z go.mau.fi/whatsmeow v0.0.0-20250104105216-918c879fcd19/go.mod h1:TLzm2XkwgufONEmiVAsFny+9uBqyEZnUoPrQAfMyuSU= go.mau.fi/whatsmeow v0.0.0-20250130221717-faf72d668860 h1:jQhAJJGC42rwZ562nz6V9SXBCVz+QhORusd0r9cxiww= go.mau.fi/whatsmeow v0.0.0-20250130221717-faf72d668860/go.mod h1:PG1x7fBW66I9q/e8a9mU2qF9M94+kK32MceMWgxBoiw= +go.mau.fi/whatsmeow v0.0.0-20250204095649-a75587ab11d7 h1:eLT0TKTpSeNcszoyasSylaxFCNU82XCP72DjbInFpzg= +go.mau.fi/whatsmeow v0.0.0-20250204095649-a75587ab11d7/go.mod h1:PG1x7fBW66I9q/e8a9mU2qF9M94+kK32MceMWgxBoiw= 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.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= @@ -116,6 +118,8 @@ golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ug golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.23.0 h1:HseQ7c2OpPKTPVzNjG5fwJsOTCiiwS4QdsYi5XU6H68= golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY= +golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ= +golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= @@ -155,6 +159,8 @@ golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= diff --git a/src/internal/rest/send.go b/src/internal/rest/send.go index 637c32b..e30e9e4 100644 --- a/src/internal/rest/send.go +++ b/src/internal/rest/send.go @@ -52,9 +52,10 @@ func (controller *Send) SendImage(c *fiber.Ctx) error { utils.PanicIfNeeded(err) file, err := c.FormFile("image") - utils.PanicIfNeeded(err) + if err == nil { + request.Image = file + } - request.Image = file whatsapp.SanitizePhone(&request.Phone) response, err := controller.Service.SendImage(c.UserContext(), request) diff --git a/src/pkg/utils/general.go b/src/pkg/utils/general.go index 71685f6..90c8b80 100644 --- a/src/pkg/utils/general.go +++ b/src/pkg/utils/general.go @@ -13,6 +13,7 @@ import ( "time" "github.com/PuerkitoBio/goquery" + "github.com/aldinokemal/go-whatsapp-web-multidevice/config" ) // RemoveFile is removing file with delay @@ -131,3 +132,50 @@ func ContainsMention(message string) []string { } return phoneNumbers } + +func DownloadImageFromURL(url string) ([]byte, string, error) { + client := &http.Client{ + Timeout: 30 * time.Second, + CheckRedirect: func(req *http.Request, via []*http.Request) error { + if len(via) >= 10 { + return fmt.Errorf("too many redirects") + } + return nil + }, + } + response, err := client.Get(url) + if err != nil { + return nil, "", err + } + defer response.Body.Close() + contentType := response.Header.Get("Content-Type") + if !strings.HasPrefix(contentType, "image/") { + return nil, "", fmt.Errorf("invalid content type: %s", contentType) + } + // Check content length if available + if contentLength := response.ContentLength; contentLength > int64(config.WhatsappSettingMaxImageSize) { + return nil, "", fmt.Errorf("image size %d exceeds maximum allowed size %d", contentLength, config.WhatsappSettingMaxImageSize) + } + // Limit the size from config + reader := io.LimitReader(response.Body, int64(config.WhatsappSettingMaxImageSize)) + // Extract the file name from the URL and remove query parameters if present + segments := strings.Split(url, "/") + fileName := segments[len(segments)-1] + fileName = strings.Split(fileName, "?")[0] + // Check if the file extension is supported + allowedExtensions := map[string]bool{ + ".jpg": true, + ".jpeg": true, + ".png": true, + ".webp": true, + } + extension := strings.ToLower(filepath.Ext(fileName)) + if !allowedExtensions[extension] { + return nil, "", fmt.Errorf("unsupported file type: %s", extension) + } + imageData, err := io.ReadAll(reader) + if err != nil { + return nil, "", err + } + return imageData, fileName, nil +} diff --git a/src/pkg/utils/general_test.go b/src/pkg/utils/general_test.go index 6c80a29..b16c2bf 100644 --- a/src/pkg/utils/general_test.go +++ b/src/pkg/utils/general_test.go @@ -1,12 +1,21 @@ package utils_test import ( + "net/http" + "net/http/httptest" + "os" + "testing" + "github.com/aldinokemal/go-whatsapp-web-multidevice/pkg/utils" "github.com/stretchr/testify/assert" - "testing" + "github.com/stretchr/testify/suite" ) -func TestContainsMention(t *testing.T) { +type UtilsTestSuite struct { + suite.Suite +} + +func (suite *UtilsTestSuite) TestContainsMention() { type args struct { message string } @@ -32,9 +41,82 @@ func TestContainsMention(t *testing.T) { }, } for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { + suite.T().Run(tt.name, func(t *testing.T) { got := utils.ContainsMention(tt.args.message) assert.Equal(t, tt.want, got) }) } } + +func (suite *UtilsTestSuite) TestRemoveFile() { + tempFile, err := os.CreateTemp("", "testfile") + assert.NoError(suite.T(), err) + tempFilePath := tempFile.Name() + tempFile.Close() + + err = utils.RemoveFile(0, tempFilePath) + assert.NoError(suite.T(), err) + _, err = os.Stat(tempFilePath) + assert.True(suite.T(), os.IsNotExist(err)) +} + +func (suite *UtilsTestSuite) TestCreateFolder() { + tempDir := "testdir" + err := utils.CreateFolder(tempDir) + assert.NoError(suite.T(), err) + _, err = os.Stat(tempDir) + assert.NoError(suite.T(), err) + assert.True(suite.T(), err == nil) + os.RemoveAll(tempDir) +} + +func (suite *UtilsTestSuite) TestPanicIfNeeded() { + assert.PanicsWithValue(suite.T(), "test error", func() { + utils.PanicIfNeeded("test error") + }) + + assert.NotPanics(suite.T(), func() { + utils.PanicIfNeeded(nil) + }) +} + +func (suite *UtilsTestSuite) TestStrToFloat64() { + assert.Equal(suite.T(), 123.45, utils.StrToFloat64("123.45")) + assert.Equal(suite.T(), 0.0, utils.StrToFloat64("invalid")) + assert.Equal(suite.T(), 0.0, utils.StrToFloat64("")) +} + +func (suite *UtilsTestSuite) TestGetMetaDataFromURL() { + // Use httptest.NewServer to mock HTTP server + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(`Test Title`)) + })) + defer server.Close() // Ensure the server is closed when the test ends + + meta := utils.GetMetaDataFromURL(server.URL) + assert.Equal(suite.T(), "Test Title", meta.Title) + assert.Equal(suite.T(), "Test Description", meta.Description) + assert.Equal(suite.T(), "http://example.com/image.jpg", meta.Image) +} + +func (suite *UtilsTestSuite) TestDownloadImageFromURL() { + // Use httptest.NewServer to mock HTTP server + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/image.jpg" { + w.Header().Set("Content-Type", "image/jpeg") // Set content type to image + w.Write([]byte("image data")) + } else { + http.NotFound(w, r) + } + })) + defer server.Close() // Ensure the server is closed when the test ends + + imageData, fileName, err := utils.DownloadImageFromURL(server.URL + "/image.jpg") + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), []byte("image data"), imageData) + assert.Equal(suite.T(), "image.jpg", fileName) +} + +func TestUtilsTestSuite(t *testing.T) { + suite.Run(t, new(UtilsTestSuite)) +} diff --git a/src/services/send.go b/src/services/send.go index aa8f9a4..0e82961 100644 --- a/src/services/send.go +++ b/src/services/send.go @@ -119,14 +119,31 @@ func (service serviceSend) SendImage(ctx context.Context, request domainSend.Ima var ( imagePath string imageThumbnail string + imageName string deletedItems []string + oriImagePath 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 + if request.ImageURL != nil && *request.ImageURL != "" { + // Download image from URL + imageData, fileName, err := utils.DownloadImageFromURL(*request.ImageURL) + oriImagePath = fmt.Sprintf("%s/%s", config.PathSendItems, fileName) + if err != nil { + return response, pkgError.InternalServerError(fmt.Sprintf("failed to download image from URL %v", err)) + } + imageName = fileName + err = os.WriteFile(oriImagePath, imageData, 0644) + if err != nil { + return response, pkgError.InternalServerError(fmt.Sprintf("failed to save downloaded image %v", err)) + } + } else if request.Image != nil { + // 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 + } + imageName = request.Image.Filename } deletedItems = append(deletedItems, oriImagePath) @@ -138,7 +155,7 @@ func (service serviceSend) SendImage(ctx context.Context, request domainSend.Ima // Resize Thumbnail resizedImage := imaging.Resize(srcImage, 100, 0, imaging.Lanczos) - imageThumbnail = fmt.Sprintf("%s/thumbnails-%s", config.PathSendItems, request.Image.Filename) + imageThumbnail = fmt.Sprintf("%s/thumbnails-%s", config.PathSendItems, imageName) if err = imaging.Save(resizedImage, imageThumbnail); err != nil { return response, pkgError.InternalServerError(fmt.Sprintf("failed to save thumbnail %v", err)) } @@ -151,7 +168,7 @@ func (service serviceSend) SendImage(ctx context.Context, request domainSend.Ima 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) + newImagePath := fmt.Sprintf("%s/new-%s", config.PathSendItems, imageName) if err = imaging.Save(newImage, newImagePath); err != nil { return response, pkgError.InternalServerError(fmt.Sprintf("failed to save image %v", err)) } diff --git a/src/validations/send_validation.go b/src/validations/send_validation.go index ee67368..9304faf 100644 --- a/src/validations/send_validation.go +++ b/src/validations/send_validation.go @@ -3,6 +3,7 @@ package validations import ( "context" "fmt" + "sort" "github.com/aldinokemal/go-whatsapp-web-multidevice/config" domainSend "github.com/aldinokemal/go-whatsapp-web-multidevice/domains/send" @@ -27,21 +28,37 @@ func ValidateSendMessage(ctx context.Context, request domainSend.MessageRequest) func ValidateSendImage(ctx context.Context, request domainSend.ImageRequest) error { err := validation.ValidateStructWithContext(ctx, &request, validation.Field(&request.Phone, validation.Required), - validation.Field(&request.Image, validation.Required), ) if err != nil { return pkgError.ValidationError(err.Error()) } - availableMimes := map[string]bool{ - "image/jpeg": true, - "image/jpg": true, - "image/png": true, + if request.Image == nil && (request.ImageURL == nil || *request.ImageURL == "") { + return pkgError.ValidationError("either Image or ImageURL must be provided") } - if !availableMimes[request.Image.Header.Get("Content-Type")] { - return pkgError.ValidationError("your image is not allowed. please use jpg/jpeg/png") + if request.Image != nil { + availableMimes := map[string]bool{ + "image/jpeg": true, + "image/jpg": true, + "image/png": true, + } + + if !availableMimes[request.Image.Header.Get("Content-Type")] { + return pkgError.ValidationError("your image is not allowed. please use jpg/jpeg/png") + } + } + + if request.ImageURL != nil { + if *request.ImageURL == "" { + return pkgError.ValidationError("ImageURL cannot be empty") + } + + err := validation.Validate(*request.ImageURL, is.URL) + if err != nil { + return pkgError.ValidationError("ImageURL must be a valid URL") + } } return nil @@ -146,18 +163,16 @@ func ValidateSendAudio(ctx context.Context, request domainSend.AudioRequest) err } 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/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, @@ -166,7 +181,15 @@ func ValidateSendAudio(ctx context.Context, request domainSend.AudioRequest) err "audio/x-wav": true, } availableMimesStr := "" + + // Sort MIME types for consistent error message order + mimeKeys := make([]string, 0, len(availableMimes)) for k := range availableMimes { + mimeKeys = append(mimeKeys, k) + } + sort.Strings(mimeKeys) + + for _, k := range mimeKeys { availableMimesStr += k + "," } @@ -178,11 +201,15 @@ func ValidateSendAudio(ctx context.Context, request domainSend.AudioRequest) err } func ValidateSendPoll(ctx context.Context, request domainSend.PollRequest) error { + // Validate options first to ensure it is not blank before validating MaxAnswer + if len(request.Options) == 0 { + return pkgError.ValidationError("options: cannot be blank.") + } + err := validation.ValidateStructWithContext(ctx, &request, validation.Field(&request.Phone, validation.Required), validation.Field(&request.Question, validation.Required), - validation.Field(&request.Options, validation.Required), validation.Field(&request.Options, validation.Each(validation.Required)), validation.Field(&request.MaxAnswer, validation.Required), @@ -204,7 +231,6 @@ func ValidateSendPoll(ctx context.Context, request domainSend.PollRequest) error } return nil - } func ValidateSendPresence(ctx context.Context, request domainSend.PresenceRequest) error { diff --git a/src/validations/send_validation_test.go b/src/validations/send_validation_test.go index a9810cd..cccf463 100644 --- a/src/validations/send_validation_test.go +++ b/src/validations/send_validation_test.go @@ -2,12 +2,13 @@ package validations import ( "context" + "mime/multipart" + "testing" + domainMessage "github.com/aldinokemal/go-whatsapp-web-multidevice/domains/message" domainSend "github.com/aldinokemal/go-whatsapp-web-multidevice/domains/send" pkgError "github.com/aldinokemal/go-whatsapp-web-multidevice/pkg/error" "github.com/stretchr/testify/assert" - "mime/multipart" - "testing" ) func TestValidateSendMessage(t *testing.T) { @@ -91,7 +92,7 @@ func TestValidateSendImage(t *testing.T) { Phone: "1728937129312@s.whatsapp.net", Image: nil, }}, - err: pkgError.ValidationError("image: cannot be blank."), + err: pkgError.ValidationError("either Image or ImageURL must be provided"), }, { name: "should error with invalid image type", @@ -528,3 +529,183 @@ func TestValidateSendLocation(t *testing.T) { }) } } + +func TestValidateSendAudio(t *testing.T) { + audio := &multipart.FileHeader{ + Filename: "sample-audio.mp3", + Size: 100, + Header: map[string][]string{"Content-Type": {"audio/mp3"}}, + } + + type args struct { + request domainSend.AudioRequest + } + tests := []struct { + name string + args args + err any + }{ + { + name: "should success with normal condition", + args: args{request: domainSend.AudioRequest{ + Phone: "1728937129312@s.whatsapp.net", + Audio: audio, + }}, + err: nil, + }, + { + name: "should error with empty phone", + args: args{request: domainSend.AudioRequest{ + Phone: "", + Audio: audio, + }}, + err: pkgError.ValidationError("phone: cannot be blank."), + }, + { + name: "should error with empty audio", + args: args{request: domainSend.AudioRequest{ + Phone: "1728937129312@s.whatsapp.net", + Audio: nil, + }}, + err: pkgError.ValidationError("audio: cannot be blank."), + }, + { + name: "should error with invalid audio type", + args: args{request: domainSend.AudioRequest{ + Phone: "1728937129312@s.whatsapp.net", + Audio: &multipart.FileHeader{ + Filename: "sample-audio.txt", + Size: 100, + Header: map[string][]string{"Content-Type": {"text/plain"}}, + }, + }}, + err: pkgError.ValidationError("your audio type is not allowed. please use (audio/aac,audio/amr,audio/flac,audio/m4a,audio/m4r,audio/mp3,audio/mpeg,audio/ogg,audio/vnd.wav,audio/vnd.wave,audio/wav,audio/wave,audio/wma,audio/x-ms-wma,audio/x-pn-wav,audio/x-wav,)"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := ValidateSendAudio(context.Background(), tt.args.request) + assert.Equal(t, tt.err, err) + }) + } +} + +func TestValidateSendPoll(t *testing.T) { + type args struct { + request domainSend.PollRequest + } + tests := []struct { + name string + args args + err any + }{ + { + name: "should success with normal condition", + args: args{request: domainSend.PollRequest{ + Phone: "1728937129312@s.whatsapp.net", + Question: "What is your favorite color?", + Options: []string{"Red", "Blue", "Green"}, + MaxAnswer: 1, + }}, + err: nil, + }, + { + name: "should error with empty phone", + args: args{request: domainSend.PollRequest{ + Phone: "", + Question: "What is your favorite color?", + Options: []string{"Red", "Blue", "Green"}, + MaxAnswer: 1, + }}, + err: pkgError.ValidationError("phone: cannot be blank."), + }, + { + name: "should error with empty question", + args: args{request: domainSend.PollRequest{ + Phone: "1728937129312@s.whatsapp.net", + Question: "", + Options: []string{"Red", "Blue", "Green"}, + MaxAnswer: 1, + }}, + err: pkgError.ValidationError("question: cannot be blank."), + }, + { + name: "should error with empty options", + args: args{request: domainSend.PollRequest{ + Phone: "1728937129312@s.whatsapp.net", + Question: "What is your favorite color?", + Options: []string{}, + MaxAnswer: 5, + }}, + err: pkgError.ValidationError("options: cannot be blank."), + }, + { + name: "should error with duplicate options", + args: args{request: domainSend.PollRequest{ + Phone: "1728937129312@s.whatsapp.net", + Question: "What is your favorite color?", + Options: []string{"Red", "Red", "Green"}, + MaxAnswer: 1, + }}, + err: pkgError.ValidationError("options should be unique"), + }, + { + name: "should error with max answer greater than options", + args: args{request: domainSend.PollRequest{ + Phone: "1728937129312@s.whatsapp.net", + Question: "What is your favorite color?", + Options: []string{"Red", "Blue", "Green"}, + MaxAnswer: 5, + }}, + err: pkgError.ValidationError("max_answer: must be no greater than 3."), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := ValidateSendPoll(context.Background(), tt.args.request) + assert.Equal(t, tt.err, err) + }) + } +} + +func TestValidateSendPresence(t *testing.T) { + type args struct { + request domainSend.PresenceRequest + } + tests := []struct { + name string + args args + err any + }{ + { + name: "should success with available type", + args: args{request: domainSend.PresenceRequest{ + Type: "available", + }}, + err: nil, + }, + { + name: "should success with unavailable type", + args: args{request: domainSend.PresenceRequest{ + Type: "unavailable", + }}, + err: nil, + }, + { + name: "should error with invalid type", + args: args{request: domainSend.PresenceRequest{ + Type: "invalid", + }}, + err: pkgError.ValidationError("type: must be a valid value."), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := ValidateSendPresence(context.Background(), tt.args.request) + assert.Equal(t, tt.err, err) + }) + } +} diff --git a/src/views/components/SendImage.js b/src/views/components/SendImage.js index 35682f2..0827842 100644 --- a/src/views/components/SendImage.js +++ b/src/views/components/SendImage.js @@ -14,6 +14,7 @@ export default { type: window.TYPEUSER, loading: false, selected_file: null, + image_url: null, preview_url: null } }, @@ -38,7 +39,7 @@ export default { return false; } - if (!this.selected_file) { + if (!this.selected_file && !this.image_url) { return false; } @@ -65,8 +66,16 @@ export default { payload.append("view_once", this.view_once) payload.append("compress", this.compress) payload.append("caption", this.caption) - payload.append('image', $("#file_image")[0].files[0]) - + + const fileInput = $("#file_image"); + if (fileInput.length > 0 && fileInput[0].files.length > 0) { + const file = fileInput[0].files[0]; + payload.append('image', file); + } + if (this.image_url) { + payload.append('image_url', this.image_url) + } + let response = await window.http.post(`/send/image`, payload) this.handleReset(); return response.data.message; @@ -86,6 +95,7 @@ export default { this.caption = ''; this.preview_url = null; this.selected_file = null; + this.image_url = null; $("#file_image").val(''); }, handleImageChange(event) { @@ -145,6 +155,12 @@ export default { +
+ + +
+
or you can upload image from your device