From d79bb4378275a1953aca0ed10cbd18a87db84fd1 Mon Sep 17 00:00:00 2001 From: Aldino Kemal Date: Wed, 5 Mar 2025 23:06:58 +0700 Subject: [PATCH 01/15] feat: Improve image metadata extraction and thumbnail handling - Enhance image dimension detection by decoding actual image data - Add support for detecting image dimensions from downloaded images - Implement fallback to Open Graph tags if image decoding fails - Handle square images by setting width and height to nil - Improve thumbnail upload process for WhatsApp messages - Add logging for image dimensions and thumbnail upload status --- src/pkg/utils/general.go | 69 ++++++++++++++++++++++++++++++---------- src/services/send.go | 36 +++++++++++++++++---- 2 files changed, 82 insertions(+), 23 deletions(-) diff --git a/src/pkg/utils/general.go b/src/pkg/utils/general.go index 99faff9..23b22cc 100644 --- a/src/pkg/utils/general.go +++ b/src/pkg/utils/general.go @@ -1,7 +1,12 @@ package utils import ( + "bytes" "fmt" + "image" + _ "image/gif" // Register GIF format + _ "image/jpeg" // Register JPEG format + _ "image/png" // Register PNG format "io" "log" "net/http" @@ -100,22 +105,6 @@ func GetMetaDataFromURL(url string) (meta Metadata, err error) { meta.Image, _ = element.Attr("content") }) - document.Find("meta[property='og:image:width']").Each(func(index int, element *goquery.Selection) { - if content, exists := element.Attr("content"); exists { - width, _ := strconv.Atoi(content) - widthUint32 := uint32(width) - meta.Width = &widthUint32 - } - }) - - document.Find("meta[property='og:image:height']").Each(func(index int, element *goquery.Selection) { - if content, exists := element.Attr("content"); exists { - height, _ := strconv.Atoi(content) - heightUint32 := uint32(height) - meta.Height = &heightUint32 - } - }) - // If an og:image is found, download it and store its content in ImageThumb if meta.Image != "" { imageResponse, err := http.Get(meta.Image) @@ -123,11 +112,59 @@ func GetMetaDataFromURL(url string) (meta Metadata, err error) { log.Printf("Failed to download image: %v", err) } else { defer imageResponse.Body.Close() + + // Read image data imageData, err := io.ReadAll(imageResponse.Body) if err != nil { log.Printf("Failed to read image data: %v", err) } else { meta.ImageThumb = imageData + + // Get image dimensions from the actual image rather than OG tags + imageReader := bytes.NewReader(imageData) + img, _, err := image.Decode(imageReader) + if err == nil { + bounds := img.Bounds() + width := uint32(bounds.Max.X - bounds.Min.X) + height := uint32(bounds.Max.Y - bounds.Min.Y) + + // Check if image is square (1:1 ratio) + if width == height { + // For 1:1 ratio, leave width and height as nil + meta.Width = nil + meta.Height = nil + } else { + meta.Width = &width + meta.Height = &height + } + + log.Printf("Image dimensions: %dx%d", width, height) + } else { + log.Printf("Failed to decode image to get dimensions: %v", err) + + // Fallback to OG tags if image decoding fails + document.Find("meta[property='og:image:width']").Each(func(index int, element *goquery.Selection) { + if content, exists := element.Attr("content"); exists { + width, _ := strconv.Atoi(content) + widthUint32 := uint32(width) + meta.Width = &widthUint32 + } + }) + + document.Find("meta[property='og:image:height']").Each(func(index int, element *goquery.Selection) { + if content, exists := element.Attr("content"); exists { + height, _ := strconv.Atoi(content) + heightUint32 := uint32(height) + meta.Height = &heightUint32 + } + }) + + // Check if the OG tags indicate a 1:1 ratio + if meta.Width != nil && meta.Height != nil && *meta.Width == *meta.Height { + meta.Width = nil + meta.Height = nil + } + } } } } diff --git a/src/services/send.go b/src/services/send.go index 9a9afa2..c16cc1f 100644 --- a/src/services/send.go +++ b/src/services/send.go @@ -435,16 +435,38 @@ func (service serviceSend) SendLink(ctx context.Context, request domainSend.Link return response, err } + // Log image dimensions if available, otherwise note it's a square image or dimensions not available + if getMetaDataFromURL.Width != nil && getMetaDataFromURL.Height != nil { + fmt.Printf("Image dimensions: %dx%d\n", *getMetaDataFromURL.Width, *getMetaDataFromURL.Height) + } else { + fmt.Println("Image dimensions: Square image or dimensions not available") + } + + // Create the message 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, - ThumbnailHeight: getMetaDataFromURL.Height, - ThumbnailWidth: getMetaDataFromURL.Width, + 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, }} + // If we have a thumbnail image, upload it to WhatsApp's servers + if len(getMetaDataFromURL.ImageThumb) > 0 && getMetaDataFromURL.Height != nil && getMetaDataFromURL.Width != nil { + uploadedThumb, err := service.uploadMedia(ctx, whatsmeow.MediaImage, getMetaDataFromURL.ImageThumb, dataWaRecipient) + if err == nil { + // Update the message with the uploaded thumbnail information + msg.ExtendedTextMessage.ThumbnailDirectPath = proto.String(uploadedThumb.DirectPath) + msg.ExtendedTextMessage.ThumbnailSHA256 = uploadedThumb.FileSHA256 + msg.ExtendedTextMessage.ThumbnailEncSHA256 = uploadedThumb.FileEncSHA256 + msg.ExtendedTextMessage.MediaKey = uploadedThumb.MediaKey + msg.ExtendedTextMessage.ThumbnailHeight = getMetaDataFromURL.Height + msg.ExtendedTextMessage.ThumbnailWidth = getMetaDataFromURL.Width + } else { + logrus.Warnf("Failed to upload thumbnail: %v, continue without uploaded thumbnail", err) + } + } + content := "🔗 " + request.Link if request.Caption != "" { content = "🔗 " + request.Caption From 2efa14c69fcb2e0a3a5ae02d2b8e212f35c57ebd Mon Sep 17 00:00:00 2001 From: Aldino Kemal Date: Thu, 6 Mar 2025 05:42:45 +0700 Subject: [PATCH 02/15] feat: Improve image transparency handling and background processing - Add detection of image transparency across different image formats - Implement white background rendering for images with transparency - Support converting transparent images to JPEG/PNG with white background - Enhance image metadata extraction with transparency checks - Add logging for transparency processing and image conversion --- src/go.mod | 14 +++--- src/go.sum | 18 ++++++++ src/pkg/utils/general.go | 98 ++++++++++++++++++++++++++++++++++++++-- 3 files changed, 118 insertions(+), 12 deletions(-) diff --git a/src/go.mod b/src/go.mod index b657b16..ac19598 100644 --- a/src/go.mod +++ b/src/go.mod @@ -20,7 +20,7 @@ require ( github.com/stretchr/testify v1.10.0 github.com/valyala/fasthttp v1.59.0 go.mau.fi/libsignal v0.1.2 - go.mau.fi/whatsmeow v0.0.0-20250225112721-b7530f3a5056 + go.mau.fi/whatsmeow v0.0.0-20250305175604-af3dc0346412 google.golang.org/protobuf v1.36.5 ) @@ -58,12 +58,12 @@ require ( github.com/valyala/bytebufferpool v1.0.0 // indirect go.mau.fi/util v0.8.5 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/crypto v0.35.0 // indirect - golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa // indirect - golang.org/x/image v0.24.0 // indirect - golang.org/x/net v0.35.0 // indirect - golang.org/x/sys v0.30.0 // indirect - golang.org/x/text v0.22.0 // indirect + golang.org/x/crypto v0.36.0 // indirect + golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect + golang.org/x/image v0.25.0 // indirect + golang.org/x/net v0.37.0 // indirect + golang.org/x/sys v0.31.0 // indirect + golang.org/x/text v0.23.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/src/go.sum b/src/go.sum index 9c88314..f98a419 100644 --- a/src/go.sum +++ b/src/go.sum @@ -128,6 +128,8 @@ go.mau.fi/util v0.8.5 h1:PwCAAtcfK0XxZ4sdErJyfBMkTEWoQU33aB7QqDDzQRI= go.mau.fi/util v0.8.5/go.mod h1:Ycug9mrbztlahHPEJ6H5r8Nu/xqZaWbE5vPHVWmfz6M= go.mau.fi/whatsmeow v0.0.0-20250225112721-b7530f3a5056 h1:1JQUOpYXhFSEQgXMEWD/ZH38FrIe5i1yjxSBwa0aN/Q= go.mau.fi/whatsmeow v0.0.0-20250225112721-b7530f3a5056/go.mod h1:6hRrUtDWI2wTRClOd6m17GwrFE2a8/p5R4pjJsIVn+U= +go.mau.fi/whatsmeow v0.0.0-20250305175604-af3dc0346412 h1:AM+t3vKEho3zTDOW2g6KvxB7iGNPwp0SFZpmx4slVVU= +go.mau.fi/whatsmeow v0.0.0-20250305175604-af3dc0346412/go.mod h1:6hRrUtDWI2wTRClOd6m17GwrFE2a8/p5R4pjJsIVn+U= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -138,11 +140,19 @@ golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs= golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ= +golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa h1:t2QcU6V556bFjYgu4L6C+6VrCPyJZ+eyRsABUPs1mz4= golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa/go.mod h1:BHOTPb3L19zxehTsLoJXVaTktb06DFgmdW6Wb9s8jqk= +golang.org/x/exp v0.0.0-20250228200357-dead58393ab7 h1:aWwlzYV971S4BXRS9AmqwDLAD85ouC6X+pocatKY58c= +golang.org/x/exp v0.0.0-20250228200357-dead58393ab7/go.mod h1:BHOTPb3L19zxehTsLoJXVaTktb06DFgmdW6Wb9s8jqk= +golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw= +golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM= golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ= golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8= +golang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ= +golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs= 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= @@ -159,6 +169,10 @@ golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= +golang.org/x/net v0.36.0 h1:vWF2fRbw4qslQsQzgFqZff+BItCvGFQqKzKIzx1rmoA= +golang.org/x/net v0.36.0/go.mod h1:bFmbeoIPfrw4sMHNhb4J9f6+tPziuGjq7Jk/38fxi1I= +golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= +golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -182,6 +196,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.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 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= @@ -202,6 +218,8 @@ golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= diff --git a/src/pkg/utils/general.go b/src/pkg/utils/general.go index 23b22cc..6f8bfba 100644 --- a/src/pkg/utils/general.go +++ b/src/pkg/utils/general.go @@ -4,9 +4,11 @@ import ( "bytes" "fmt" "image" - _ "image/gif" // Register GIF format - _ "image/jpeg" // Register JPEG format - _ "image/png" // Register PNG format + "image/color" + "image/draw" + _ "image/gif" // Register GIF format + "image/jpeg" // For JPEG encoding + "image/png" // For PNG encoding "io" "log" "net/http" @@ -122,14 +124,100 @@ func GetMetaDataFromURL(url string) (meta Metadata, err error) { // Get image dimensions from the actual image rather than OG tags imageReader := bytes.NewReader(imageData) - img, _, err := image.Decode(imageReader) + img, imgFormat, err := image.Decode(imageReader) if err == nil { bounds := img.Bounds() width := uint32(bounds.Max.X - bounds.Min.X) height := uint32(bounds.Max.Y - bounds.Min.Y) + // Check if image has transparency (alpha channel) + hasTransparency := false + + // Check for transparency by examining image type and pixels + switch v := img.(type) { + case *image.NRGBA: + // NRGBA format - check alpha values + for y := bounds.Min.Y; y < bounds.Max.Y && !hasTransparency; y++ { + for x := bounds.Min.X; x < bounds.Max.X; x++ { + _, _, _, a := v.At(x, y).RGBA() + if a < 0xffff { + hasTransparency = true + break + } + } + } + case *image.RGBA: + // RGBA format - check alpha values + for y := bounds.Min.Y; y < bounds.Max.Y && !hasTransparency; y++ { + for x := bounds.Min.X; x < bounds.Max.X; x++ { + _, _, _, a := v.At(x, y).RGBA() + if a < 0xffff { + hasTransparency = true + break + } + } + } + case *image.NRGBA64: + // NRGBA64 format - check alpha values + for y := bounds.Min.Y; y < bounds.Max.Y && !hasTransparency; y++ { + for x := bounds.Min.X; x < bounds.Max.X; x++ { + _, _, _, a := v.At(x, y).RGBA() + if a < 0xffff { + hasTransparency = true + break + } + } + } + default: + // For other formats, check if the format typically supports transparency + hasTransparency = imgFormat == "png" || imgFormat == "gif" + } + + // If image has transparency, create a new image with white background + if hasTransparency { + log.Printf("Image has transparency, setting white background") + + // Create a new RGBA image with white background + newImg := image.NewRGBA(bounds) + draw.Draw(newImg, bounds, image.NewUniform(color.White), image.Point{}, draw.Src) + + // Draw the original image on top of the white background + draw.Draw(newImg, bounds, img, bounds.Min, draw.Over) + + // Convert the new image back to bytes + var buf bytes.Buffer + switch imgFormat { + case "png": + if err := png.Encode(&buf, newImg); err == nil { + meta.ImageThumb = buf.Bytes() + } else { + log.Printf("Failed to encode PNG image: %v", err) + } + case "jpeg", "jpg": + if err := jpeg.Encode(&buf, newImg, nil); err == nil { + meta.ImageThumb = buf.Bytes() + } else { + log.Printf("Failed to encode JPEG image: %v", err) + } + case "gif": + // Note: Simple conversion to PNG for GIF with transparency + if err := png.Encode(&buf, newImg); err == nil { + meta.ImageThumb = buf.Bytes() + } else { + log.Printf("Failed to encode GIF as PNG: %v", err) + } + default: + // For other formats, try PNG + if err := png.Encode(&buf, newImg); err == nil { + meta.ImageThumb = buf.Bytes() + } else { + log.Printf("Failed to encode image as PNG: %v", err) + } + } + } + // Check if image is square (1:1 ratio) - if width == height { + if width == height && width <= 200 { // For 1:1 ratio, leave width and height as nil meta.Width = nil meta.Height = nil From 151961d58d72ad346958d3ad3f091058886c99f3 Mon Sep 17 00:00:00 2001 From: Aldino Kemal Date: Mon, 10 Mar 2025 14:37:14 +0700 Subject: [PATCH 03/15] feat: Add SendLink component and improve URL metadata extraction - Implement new SendLink Vue.js component for sending links with captions - Enhance URL metadata extraction with more robust image and title detection - Add support for resolving relative image URLs - Improve image download and validation process - Update services and views to support link sending functionality --- src/go.mod | 2 +- src/go.sum | 18 --- src/pkg/utils/general.go | 245 +++++++++++++------------------ src/services/send.go | 22 +-- src/views/components/SendLink.js | 138 +++++++++++++++++ src/views/index.html | 5 +- 6 files changed, 254 insertions(+), 176 deletions(-) create mode 100644 src/views/components/SendLink.js diff --git a/src/go.mod b/src/go.mod index ac19598..b3053eb 100644 --- a/src/go.mod +++ b/src/go.mod @@ -21,6 +21,7 @@ require ( github.com/valyala/fasthttp v1.59.0 go.mau.fi/libsignal v0.1.2 go.mau.fi/whatsmeow v0.0.0-20250305175604-af3dc0346412 + golang.org/x/image v0.25.0 google.golang.org/protobuf v1.36.5 ) @@ -60,7 +61,6 @@ require ( go.uber.org/multierr v1.11.0 // indirect golang.org/x/crypto v0.36.0 // indirect golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect - golang.org/x/image v0.25.0 // indirect golang.org/x/net v0.37.0 // indirect golang.org/x/sys v0.31.0 // indirect golang.org/x/text v0.23.0 // indirect diff --git a/src/go.sum b/src/go.sum index f98a419..ce51e55 100644 --- a/src/go.sum +++ b/src/go.sum @@ -126,8 +126,6 @@ go.mau.fi/libsignal v0.1.2 h1:Vs16DXWxSKyzVtI+EEXLCSy5pVWzzCzp/2eqFGvLyP0= go.mau.fi/libsignal v0.1.2/go.mod h1:JpnLSSJptn/s1sv7I56uEMywvz8x4YzxeF5OzdPb6PE= go.mau.fi/util v0.8.5 h1:PwCAAtcfK0XxZ4sdErJyfBMkTEWoQU33aB7QqDDzQRI= go.mau.fi/util v0.8.5/go.mod h1:Ycug9mrbztlahHPEJ6H5r8Nu/xqZaWbE5vPHVWmfz6M= -go.mau.fi/whatsmeow v0.0.0-20250225112721-b7530f3a5056 h1:1JQUOpYXhFSEQgXMEWD/ZH38FrIe5i1yjxSBwa0aN/Q= -go.mau.fi/whatsmeow v0.0.0-20250225112721-b7530f3a5056/go.mod h1:6hRrUtDWI2wTRClOd6m17GwrFE2a8/p5R4pjJsIVn+U= go.mau.fi/whatsmeow v0.0.0-20250305175604-af3dc0346412 h1:AM+t3vKEho3zTDOW2g6KvxB7iGNPwp0SFZpmx4slVVU= go.mau.fi/whatsmeow v0.0.0-20250305175604-af3dc0346412/go.mod h1:6hRrUtDWI2wTRClOd6m17GwrFE2a8/p5R4pjJsIVn+U= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= @@ -138,19 +136,11 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= -golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs= -golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ= golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= -golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa h1:t2QcU6V556bFjYgu4L6C+6VrCPyJZ+eyRsABUPs1mz4= -golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa/go.mod h1:BHOTPb3L19zxehTsLoJXVaTktb06DFgmdW6Wb9s8jqk= -golang.org/x/exp v0.0.0-20250228200357-dead58393ab7 h1:aWwlzYV971S4BXRS9AmqwDLAD85ouC6X+pocatKY58c= -golang.org/x/exp v0.0.0-20250228200357-dead58393ab7/go.mod h1:BHOTPb3L19zxehTsLoJXVaTktb06DFgmdW6Wb9s8jqk= golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw= golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM= golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ= -golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8= golang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ= golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= @@ -167,10 +157,6 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= -golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= -golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= -golang.org/x/net v0.36.0 h1:vWF2fRbw4qslQsQzgFqZff+BItCvGFQqKzKIzx1rmoA= -golang.org/x/net v0.36.0/go.mod h1:bFmbeoIPfrw4sMHNhb4J9f6+tPziuGjq7Jk/38fxi1I= golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -194,8 +180,6 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 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.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= -golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= @@ -216,8 +200,6 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= -golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= -golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/src/pkg/utils/general.go b/src/pkg/utils/general.go index 6f8bfba..30747a4 100644 --- a/src/pkg/utils/general.go +++ b/src/pkg/utils/general.go @@ -4,14 +4,12 @@ import ( "bytes" "fmt" "image" - "image/color" - "image/draw" - _ "image/gif" // Register GIF format - "image/jpeg" // For JPEG encoding - "image/png" // For PNG encoding + _ "image/gif" // Register GIF format + _ "image/jpeg" // For JPEG encoding + _ "image/png" // For PNG encoding "io" - "log" "net/http" + "net/url" "os" "path/filepath" "regexp" @@ -21,6 +19,8 @@ import ( "github.com/PuerkitoBio/goquery" "github.com/aldinokemal/go-whatsapp-web-multidevice/config" + "github.com/sirupsen/logrus" + _ "golang.org/x/image/webp" // Register WebP format ) // RemoveFile is removing file with delay @@ -80,14 +80,35 @@ type Metadata struct { Width *uint32 } -func GetMetaDataFromURL(url string) (meta Metadata, err error) { +func GetMetaDataFromURL(urlStr string) (meta Metadata, err error) { + // Create HTTP client with timeout + client := &http.Client{ + Timeout: 15 * time.Second, + CheckRedirect: func(req *http.Request, via []*http.Request) error { + if len(via) >= 10 { + return fmt.Errorf("too many redirects") + } + return nil + }, + } + + // Parse the base URL for resolving relative URLs later + baseURL, err := url.Parse(urlStr) + if err != nil { + return meta, fmt.Errorf("invalid URL: %v", err) + } + // Send an HTTP GET request to the website - response, err := http.Get(url) + response, err := client.Get(urlStr) if err != nil { return meta, err } defer response.Body.Close() + if response.StatusCode != http.StatusOK { + return meta, fmt.Errorf("HTTP request failed with status: %s", response.Status) + } + // Parse the HTML document document, err := goquery.NewDocumentFromReader(response.Body) if err != nil { @@ -98,159 +119,93 @@ func GetMetaDataFromURL(url string) (meta Metadata, err error) { meta.Description, _ = element.Attr("content") }) - // find title - document.Find("title").Each(func(index int, element *goquery.Selection) { - meta.Title = element.Text() + // find title - try multiple sources + // First try og:title + document.Find("meta[property='og:title']").Each(func(index int, element *goquery.Selection) { + if content, exists := element.Attr("content"); exists && content != "" { + meta.Title = content + } }) + // If og:title not found, try regular title tag + if meta.Title == "" { + document.Find("title").Each(func(index int, element *goquery.Selection) { + meta.Title = element.Text() + }) + } + // Try to find image URL from various sources + // First try og:image document.Find("meta[property='og:image']").Each(func(index int, element *goquery.Selection) { - meta.Image, _ = element.Attr("content") + if content, exists := element.Attr("content"); exists && content != "" { + meta.Image = content + } }) - // If an og:image is found, download it and store its content in ImageThumb + // If og:image not found, try twitter:image + if meta.Image == "" { + document.Find("meta[name='twitter:image']").Each(func(index int, element *goquery.Selection) { + if content, exists := element.Attr("content"); exists && content != "" { + meta.Image = content + } + }) + } + + // If an image URL is found, resolve it if it's relative if meta.Image != "" { - imageResponse, err := http.Get(meta.Image) + imgURL, err := url.Parse(meta.Image) if err != nil { - log.Printf("Failed to download image: %v", err) + logrus.Warnf("Invalid image URL: %v", err) } else { - defer imageResponse.Body.Close() - - // Read image data - imageData, err := io.ReadAll(imageResponse.Body) - if err != nil { - log.Printf("Failed to read image data: %v", err) - } else { - meta.ImageThumb = imageData - - // Get image dimensions from the actual image rather than OG tags - imageReader := bytes.NewReader(imageData) - img, imgFormat, err := image.Decode(imageReader) - if err == nil { - bounds := img.Bounds() - width := uint32(bounds.Max.X - bounds.Min.X) - height := uint32(bounds.Max.Y - bounds.Min.Y) - - // Check if image has transparency (alpha channel) - hasTransparency := false - - // Check for transparency by examining image type and pixels - switch v := img.(type) { - case *image.NRGBA: - // NRGBA format - check alpha values - for y := bounds.Min.Y; y < bounds.Max.Y && !hasTransparency; y++ { - for x := bounds.Min.X; x < bounds.Max.X; x++ { - _, _, _, a := v.At(x, y).RGBA() - if a < 0xffff { - hasTransparency = true - break - } - } - } - case *image.RGBA: - // RGBA format - check alpha values - for y := bounds.Min.Y; y < bounds.Max.Y && !hasTransparency; y++ { - for x := bounds.Min.X; x < bounds.Max.X; x++ { - _, _, _, a := v.At(x, y).RGBA() - if a < 0xffff { - hasTransparency = true - break - } - } - } - case *image.NRGBA64: - // NRGBA64 format - check alpha values - for y := bounds.Min.Y; y < bounds.Max.Y && !hasTransparency; y++ { - for x := bounds.Min.X; x < bounds.Max.X; x++ { - _, _, _, a := v.At(x, y).RGBA() - if a < 0xffff { - hasTransparency = true - break - } - } - } - default: - // For other formats, check if the format typically supports transparency - hasTransparency = imgFormat == "png" || imgFormat == "gif" - } + // Resolve relative URLs against the base URL + meta.Image = baseURL.ResolveReference(imgURL).String() + } - // If image has transparency, create a new image with white background - if hasTransparency { - log.Printf("Image has transparency, setting white background") + // Download the image + imgResponse, err := client.Get(meta.Image) + if err != nil { + logrus.Warnf("Failed to download image: %v", err) + } else { + defer imgResponse.Body.Close() - // Create a new RGBA image with white background - newImg := image.NewRGBA(bounds) - draw.Draw(newImg, bounds, image.NewUniform(color.White), image.Point{}, draw.Src) + if imgResponse.StatusCode != http.StatusOK { + logrus.Warnf("Image download failed with status: %s", imgResponse.Status) + } else { + // Check content type + contentType := imgResponse.Header.Get("Content-Type") + if !strings.HasPrefix(contentType, "image/") { + logrus.Warnf("URL returned non-image content type: %s", contentType) + } else { + // Read image data with size limit + imageData, err := io.ReadAll(io.LimitReader(imgResponse.Body, int64(config.WhatsappSettingMaxImageSize))) + if err != nil { + logrus.Warnf("Failed to read image data: %v", err) + } else if len(imageData) == 0 { + logrus.Warn("Downloaded image data is empty") + } else { + meta.ImageThumb = imageData - // Draw the original image on top of the white background - draw.Draw(newImg, bounds, img, bounds.Min, draw.Over) + // Validate image by decoding it + imageReader := bytes.NewReader(imageData) + img, _, err := image.Decode(imageReader) + if err != nil { + logrus.Warnf("Failed to decode image: %v", err) + } else { + bounds := img.Bounds() + width := uint32(bounds.Max.X - bounds.Min.X) + height := uint32(bounds.Max.Y - bounds.Min.Y) - // Convert the new image back to bytes - var buf bytes.Buffer - switch imgFormat { - case "png": - if err := png.Encode(&buf, newImg); err == nil { - meta.ImageThumb = buf.Bytes() + // Check if image is square (1:1 ratio) + if width == height && width <= 200 { + // For small square images, leave width and height as nil + meta.Width = nil + meta.Height = nil } else { - log.Printf("Failed to encode PNG image: %v", err) + meta.Width = &width + meta.Height = &height } - case "jpeg", "jpg": - if err := jpeg.Encode(&buf, newImg, nil); err == nil { - meta.ImageThumb = buf.Bytes() - } else { - log.Printf("Failed to encode JPEG image: %v", err) - } - case "gif": - // Note: Simple conversion to PNG for GIF with transparency - if err := png.Encode(&buf, newImg); err == nil { - meta.ImageThumb = buf.Bytes() - } else { - log.Printf("Failed to encode GIF as PNG: %v", err) - } - default: - // For other formats, try PNG - if err := png.Encode(&buf, newImg); err == nil { - meta.ImageThumb = buf.Bytes() - } else { - log.Printf("Failed to encode image as PNG: %v", err) - } - } - } - // Check if image is square (1:1 ratio) - if width == height && width <= 200 { - // For 1:1 ratio, leave width and height as nil - meta.Width = nil - meta.Height = nil - } else { - meta.Width = &width - meta.Height = &height - } - - log.Printf("Image dimensions: %dx%d", width, height) - } else { - log.Printf("Failed to decode image to get dimensions: %v", err) - - // Fallback to OG tags if image decoding fails - document.Find("meta[property='og:image:width']").Each(func(index int, element *goquery.Selection) { - if content, exists := element.Attr("content"); exists { - width, _ := strconv.Atoi(content) - widthUint32 := uint32(width) - meta.Width = &widthUint32 + logrus.Debugf("Image dimensions: %dx%d", width, height) } - }) - - document.Find("meta[property='og:image:height']").Each(func(index int, element *goquery.Selection) { - if content, exists := element.Attr("content"); exists { - height, _ := strconv.Atoi(content) - heightUint32 := uint32(height) - meta.Height = &heightUint32 - } - }) - - // Check if the OG tags indicate a 1:1 ratio - if meta.Width != nil && meta.Height != nil && *meta.Width == *meta.Height { - meta.Width = nil - meta.Height = nil } } } diff --git a/src/services/send.go b/src/services/send.go index c16cc1f..bdfdd10 100644 --- a/src/services/send.go +++ b/src/services/send.go @@ -430,38 +430,38 @@ func (service serviceSend) SendLink(ctx context.Context, request domainSend.Link return response, err } - getMetaDataFromURL, err := utils.GetMetaDataFromURL(request.Link) + metadata, err := utils.GetMetaDataFromURL(request.Link) if err != nil { return response, err } // Log image dimensions if available, otherwise note it's a square image or dimensions not available - if getMetaDataFromURL.Width != nil && getMetaDataFromURL.Height != nil { - fmt.Printf("Image dimensions: %dx%d\n", *getMetaDataFromURL.Width, *getMetaDataFromURL.Height) + if metadata.Width != nil && metadata.Height != nil { + logrus.Debugf("Image dimensions: %dx%d", *metadata.Width, *metadata.Height) } else { - fmt.Println("Image dimensions: Square image or dimensions not available") + logrus.Debugf("Image dimensions: Square image or dimensions not available") } // Create the message msg := &waE2E.Message{ExtendedTextMessage: &waE2E.ExtendedTextMessage{ Text: proto.String(fmt.Sprintf("%s\n%s", request.Caption, request.Link)), - Title: proto.String(getMetaDataFromURL.Title), + Title: proto.String(metadata.Title), MatchedText: proto.String(request.Link), - Description: proto.String(getMetaDataFromURL.Description), - JPEGThumbnail: getMetaDataFromURL.ImageThumb, + Description: proto.String(metadata.Description), + JPEGThumbnail: metadata.ImageThumb, }} // If we have a thumbnail image, upload it to WhatsApp's servers - if len(getMetaDataFromURL.ImageThumb) > 0 && getMetaDataFromURL.Height != nil && getMetaDataFromURL.Width != nil { - uploadedThumb, err := service.uploadMedia(ctx, whatsmeow.MediaImage, getMetaDataFromURL.ImageThumb, dataWaRecipient) + if len(metadata.ImageThumb) > 0 && metadata.Height != nil && metadata.Width != nil { + uploadedThumb, err := service.uploadMedia(ctx, whatsmeow.MediaLinkThumbnail, metadata.ImageThumb, dataWaRecipient) if err == nil { // Update the message with the uploaded thumbnail information msg.ExtendedTextMessage.ThumbnailDirectPath = proto.String(uploadedThumb.DirectPath) msg.ExtendedTextMessage.ThumbnailSHA256 = uploadedThumb.FileSHA256 msg.ExtendedTextMessage.ThumbnailEncSHA256 = uploadedThumb.FileEncSHA256 msg.ExtendedTextMessage.MediaKey = uploadedThumb.MediaKey - msg.ExtendedTextMessage.ThumbnailHeight = getMetaDataFromURL.Height - msg.ExtendedTextMessage.ThumbnailWidth = getMetaDataFromURL.Width + msg.ExtendedTextMessage.ThumbnailHeight = metadata.Height + msg.ExtendedTextMessage.ThumbnailWidth = metadata.Width } else { logrus.Warnf("Failed to upload thumbnail: %v, continue without uploaded thumbnail", err) } diff --git a/src/views/components/SendLink.js b/src/views/components/SendLink.js new file mode 100644 index 0000000..7b68ab4 --- /dev/null +++ b/src/views/components/SendLink.js @@ -0,0 +1,138 @@ +import FormRecipient from "./generic/FormRecipient.js"; + +export default { + name: 'SendLink', + components: { + FormRecipient + }, + data() { + return { + type: window.TYPEUSER, + phone: '', + link: '', + caption: '', + reply_message_id: '', + loading: false, + } + }, + computed: { + phone_id() { + return this.phone + this.type; + }, + }, + methods: { + openModal() { + $('#modalSendLink').modal({ + onApprove: function () { + return false; + } + }).modal('show'); + }, + isShowReplyId() { + return this.type !== window.TYPESTATUS; + }, + isValidForm() { + // Validate phone number is not empty except for status type + const isPhoneValid = this.type === window.TYPESTATUS || this.phone.trim().length > 0; + + // Validate link is not empty and has reasonable length + const isLinkValid = this.link.trim().length > 0 && this.link.length <= 4096; + + // Validate caption is not empty and has reasonable length + const isCaptionValid = this.caption.trim().length > 0 && this.caption.length <= 4096; + + return isPhoneValid && isLinkValid && isCaptionValid + }, + async handleSubmit() { + // Add validation check here to prevent submission when form is invalid + if (!this.isValidForm() || this.loading) { + return; + } + try { + const response = await this.submitApi(); + showSuccessInfo(response); + $('#modalSendLink').modal('hide'); + } catch (err) { + showErrorInfo(err); + } + }, + async submitApi() { + this.loading = true; + try { + const payload = { + phone: this.phone_id, + link: this.link.trim(), + caption: this.caption.trim(), + }; + if (this.reply_message_id !== '') { + payload.reply_message_id = this.reply_message_id; + } + + const response = await window.http.post('/send/link', payload); + this.handleReset(); + return response.data.message; + } catch (error) { + if (error.response?.data?.message) { + throw new Error(error.response.data.message); + } + throw error; + } finally { + this.loading = false; + } + }, + handleReset() { + this.phone = ''; + this.link = ''; + this.caption = ''; + this.reply_message_id = ''; + }, + }, + template: ` +
+
+ Send +
Send Link
+
+ Send link to user or group +
+
+
+ + + + ` +} \ No newline at end of file diff --git a/src/views/index.html b/src/views/index.html index ff1c486..97a2d6f 100644 --- a/src/views/index.html +++ b/src/views/index.html @@ -116,6 +116,8 @@ + +
@@ -201,6 +203,7 @@ import SendImage from "./components/SendImage.js"; import SendFile from "./components/SendFile.js"; import SendVideo from "./components/SendVideo.js"; + import SendLink from "./components/SendLink.js"; import SendContact from "./components/SendContact.js"; import SendLocation from "./components/SendLocation.js"; import SendAudio from "./components/SendAudio.js"; @@ -254,7 +257,7 @@ Vue.createApp({ components: { AppLogin, AppLoginWithCode, AppLogout, AppReconnect, - SendMessage, SendImage, SendFile, SendVideo, SendContact, SendLocation, SendAudio, SendPoll, SendPresence, + SendMessage, SendImage, SendFile, SendVideo, SendLink, SendContact, SendLocation, SendAudio, SendPoll, SendPresence, MessageDelete, MessageUpdate, MessageReact, MessageRevoke, GroupList, GroupCreate, GroupJoinWithLink, GroupAddParticipants, NewsletterList, From 44db95c5953f08a4962ecfd7cc40f3a588025a94 Mon Sep 17 00:00:00 2001 From: Aldino Kemal Date: Mon, 10 Mar 2025 14:38:56 +0700 Subject: [PATCH 04/15] feat: package version --- src/config/settings.go | 2 +- src/go.mod | 2 +- src/go.sum | 2 ++ 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/config/settings.go b/src/config/settings.go index 369cdc3..f35f3dd 100644 --- a/src/config/settings.go +++ b/src/config/settings.go @@ -5,7 +5,7 @@ import ( ) var ( - AppVersion = "v5.3.0" + AppVersion = "v5.3.1" AppPort = "3000" AppDebug = false AppOs = "AldinoKemal" diff --git a/src/go.mod b/src/go.mod index b3053eb..6488cfc 100644 --- a/src/go.mod +++ b/src/go.mod @@ -20,7 +20,7 @@ require ( github.com/stretchr/testify v1.10.0 github.com/valyala/fasthttp v1.59.0 go.mau.fi/libsignal v0.1.2 - go.mau.fi/whatsmeow v0.0.0-20250305175604-af3dc0346412 + go.mau.fi/whatsmeow v0.0.0-20250307203951-daf102be9698 golang.org/x/image v0.25.0 google.golang.org/protobuf v1.36.5 ) diff --git a/src/go.sum b/src/go.sum index ce51e55..e38fa1c 100644 --- a/src/go.sum +++ b/src/go.sum @@ -128,6 +128,8 @@ go.mau.fi/util v0.8.5 h1:PwCAAtcfK0XxZ4sdErJyfBMkTEWoQU33aB7QqDDzQRI= go.mau.fi/util v0.8.5/go.mod h1:Ycug9mrbztlahHPEJ6H5r8Nu/xqZaWbE5vPHVWmfz6M= go.mau.fi/whatsmeow v0.0.0-20250305175604-af3dc0346412 h1:AM+t3vKEho3zTDOW2g6KvxB7iGNPwp0SFZpmx4slVVU= go.mau.fi/whatsmeow v0.0.0-20250305175604-af3dc0346412/go.mod h1:6hRrUtDWI2wTRClOd6m17GwrFE2a8/p5R4pjJsIVn+U= +go.mau.fi/whatsmeow v0.0.0-20250307203951-daf102be9698 h1:JRng1Qa5ZyOx59Cprle+DNf8LN0MAT2WJZis38hwuHQ= +go.mau.fi/whatsmeow v0.0.0-20250307203951-daf102be9698/go.mod h1:6hRrUtDWI2wTRClOd6m17GwrFE2a8/p5R4pjJsIVn+U= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= From 17cf27c808b58947e76bc3640a4e688aab71da56 Mon Sep 17 00:00:00 2001 From: Aldino Kemal Date: Tue, 11 Mar 2025 04:46:22 +0700 Subject: [PATCH 05/15] docs: Add Patreon support badge and funding information to README --- readme.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/readme.md b/readme.md index 87f1551..955f64e 100644 --- a/readme.md +++ b/readme.md @@ -1,5 +1,10 @@ # WhatsApp API Multi Device Version +[![Patreon](https://img.shields.io/badge/Support%20on-Patreon-orange.svg)](https://www.patreon.com/c/aldinokemal) +**If you're using this library to generate income, consider supporting its development by becoming a Patreon member!** +Your support helps ensure the library stays maintained and receives regular updates. Join our community of supporters today! +___ + ![release version](https://img.shields.io/github/v/release/aldinokemal/go-whatsapp-web-multidevice) ![Build Image](https://github.com/aldinokemal/go-whatsapp-web-multidevice/actions/workflows/build-docker-image.yaml/badge.svg) From 3f324cb6d04e056db6ce109152032d748e5411ab Mon Sep 17 00:00:00 2001 From: Aldino Kemal Date: Tue, 11 Mar 2025 04:49:21 +0700 Subject: [PATCH 06/15] docs: Add Patreon support badge and funding information to README --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index 955f64e..ab14e6c 100644 --- a/readme.md +++ b/readme.md @@ -1,7 +1,7 @@ # WhatsApp API Multi Device Version [![Patreon](https://img.shields.io/badge/Support%20on-Patreon-orange.svg)](https://www.patreon.com/c/aldinokemal) -**If you're using this library to generate income, consider supporting its development by becoming a Patreon member!** +**If you're using this tools to generate income, consider supporting its development by becoming a Patreon member!** Your support helps ensure the library stays maintained and receives regular updates. Join our community of supporters today! ___ From 15df63f0bd6123f93fbeaddb9c10622126d9dbdb Mon Sep 17 00:00:00 2001 From: Aldino Kemal Date: Sun, 23 Mar 2025 17:21:02 +0700 Subject: [PATCH 07/15] feat: Add CSV export functionality for contacts --- src/config/settings.go | 2 +- src/go.mod | 19 +++++------- src/go.sum | 42 +++++++++----------------- src/views/components/AccountContact.js | 35 +++++++++++++++++++++ 4 files changed, 58 insertions(+), 40 deletions(-) diff --git a/src/config/settings.go b/src/config/settings.go index f35f3dd..b04457e 100644 --- a/src/config/settings.go +++ b/src/config/settings.go @@ -5,7 +5,7 @@ import ( ) var ( - AppVersion = "v5.3.1" + AppVersion = "v5.4.0" AppPort = "3000" AppDebug = false AppOs = "AldinoKemal" diff --git a/src/go.mod b/src/go.mod index 6488cfc..3df0d92 100644 --- a/src/go.mod +++ b/src/go.mod @@ -16,11 +16,11 @@ require ( github.com/sirupsen/logrus v1.9.3 github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e github.com/spf13/cobra v1.9.1 - github.com/spf13/viper v1.19.0 + github.com/spf13/viper v1.20.0 github.com/stretchr/testify v1.10.0 github.com/valyala/fasthttp v1.59.0 go.mau.fi/libsignal v0.1.2 - go.mau.fi/whatsmeow v0.0.0-20250307203951-daf102be9698 + go.mau.fi/whatsmeow v0.0.0-20250318233852-06705625cf82 golang.org/x/image v0.25.0 google.golang.org/protobuf v1.36.5 ) @@ -33,37 +33,32 @@ require ( github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/fasthttp/websocket v1.5.12 // indirect github.com/fsnotify/fsnotify v1.8.0 // indirect + github.com/go-viper/mapstructure/v2 v2.2.1 // indirect github.com/gofiber/template v1.8.3 // indirect github.com/gofiber/utils v1.1.0 // indirect github.com/gorilla/websocket v1.5.3 // indirect - github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/klauspost/compress v1.18.0 // indirect - github.com/magiconair/properties v1.8.9 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect - github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/pelletier/go-toml/v2 v2.2.3 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/rivo/uniseg v0.4.7 // indirect - github.com/rs/zerolog v1.33.0 // indirect - github.com/sagikazarmark/locafero v0.7.0 // indirect - github.com/sagikazarmark/slog-shim v0.1.0 // indirect + github.com/rs/zerolog v1.34.0 // indirect + github.com/sagikazarmark/locafero v0.8.0 // indirect github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38 // indirect github.com/sourcegraph/conc v0.3.0 // indirect - github.com/spf13/afero v1.12.0 // indirect + github.com/spf13/afero v1.14.0 // indirect github.com/spf13/cast v1.7.1 // indirect github.com/spf13/pflag v1.0.6 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect - go.mau.fi/util v0.8.5 // indirect + go.mau.fi/util v0.8.6 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/crypto v0.36.0 // indirect - golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect golang.org/x/net v0.37.0 // indirect golang.org/x/sys v0.31.0 // indirect golang.org/x/text v0.23.0 // indirect - gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/src/go.sum b/src/go.sum index e38fa1c..435f8c2 100644 --- a/src/go.sum +++ b/src/go.sum @@ -27,6 +27,8 @@ github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/ github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/go-ozzo/ozzo-validation/v4 v4.3.0 h1:byhDUpfEwjsVQb1vBunvIjh2BHQ9ead57VkAEY4V+Es= github.com/go-ozzo/ozzo-validation/v4 v4.3.0/go.mod h1:2NKgrcHl3z6cJs+3Oo940FPRiTzuqKbvfrL2RxCj6Ew= +github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= +github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gofiber/fiber/v2 v2.52.6 h1:Rfp+ILPiYSvvVuIPvxrBns+HJp8qGLDnLJawAu27XVI= github.com/gofiber/fiber/v2 v2.52.6/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw= @@ -44,8 +46,6 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= -github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= @@ -56,8 +56,6 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/magiconair/properties v1.8.9 h1:nWcCbLq1N2v/cpNsy5WvQ37Fb+YElfq20WJ/a8RkpQM= -github.com/magiconair/properties v1.8.9/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= @@ -69,8 +67,6 @@ github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6T github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= -github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= -github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -82,14 +78,12 @@ github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= -github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= -github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= -github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= +github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= +github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= +github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo= -github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k= -github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= -github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= +github.com/sagikazarmark/locafero v0.8.0 h1:mXaMVw7IqxNBxfv3LdWt9MDmcWDQ1fagDH918lOdVaQ= +github.com/sagikazarmark/locafero v0.8.0/go.mod h1:UBUyz37V+EdMS3hDF3QWIiVr/2dPrx49OMO0Bn0hJqk= github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38 h1:D0vL7YNisV2yqE55+q0lFuGse6U8lxlg7fYTctlT5Gc= github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38/go.mod h1:sM7Mt7uEoCeFSCBM+qBrqvEo+/9vdmj19wzp3yzUhmg= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= @@ -98,16 +92,16 @@ github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1 github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= -github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs= -github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4= +github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA= +github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo= github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= -github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= +github.com/spf13/viper v1.20.0 h1:zrxIyR3RQIOsarIrgL8+sAvALXul9jeEPa06Y0Ph6vY= +github.com/spf13/viper v1.20.0/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -124,12 +118,10 @@ github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3i github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.mau.fi/libsignal v0.1.2 h1:Vs16DXWxSKyzVtI+EEXLCSy5pVWzzCzp/2eqFGvLyP0= go.mau.fi/libsignal v0.1.2/go.mod h1:JpnLSSJptn/s1sv7I56uEMywvz8x4YzxeF5OzdPb6PE= -go.mau.fi/util v0.8.5 h1:PwCAAtcfK0XxZ4sdErJyfBMkTEWoQU33aB7QqDDzQRI= -go.mau.fi/util v0.8.5/go.mod h1:Ycug9mrbztlahHPEJ6H5r8Nu/xqZaWbE5vPHVWmfz6M= -go.mau.fi/whatsmeow v0.0.0-20250305175604-af3dc0346412 h1:AM+t3vKEho3zTDOW2g6KvxB7iGNPwp0SFZpmx4slVVU= -go.mau.fi/whatsmeow v0.0.0-20250305175604-af3dc0346412/go.mod h1:6hRrUtDWI2wTRClOd6m17GwrFE2a8/p5R4pjJsIVn+U= -go.mau.fi/whatsmeow v0.0.0-20250307203951-daf102be9698 h1:JRng1Qa5ZyOx59Cprle+DNf8LN0MAT2WJZis38hwuHQ= -go.mau.fi/whatsmeow v0.0.0-20250307203951-daf102be9698/go.mod h1:6hRrUtDWI2wTRClOd6m17GwrFE2a8/p5R4pjJsIVn+U= +go.mau.fi/util v0.8.6 h1:AEK13rfgtiZJL2YsNK+W4ihhYCuukcRom8WPP/w/L54= +go.mau.fi/util v0.8.6/go.mod h1:uNB3UTXFbkpp7xL1M/WvQks90B/L4gvbLpbS0603KOE= +go.mau.fi/whatsmeow v0.0.0-20250318233852-06705625cf82 h1:AZlDkXHgoQNW4gd2hnTCvPH7hYznmwc3gPaYqGZ5w8A= +go.mau.fi/whatsmeow v0.0.0-20250318233852-06705625cf82/go.mod h1:WNhj4JeQ6YR6dUOEiCXKqmE4LavSFkwRoKmu4atRrRs= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -140,8 +132,6 @@ golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= -golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw= -golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM= golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ= golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs= @@ -216,8 +206,6 @@ google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojt gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= -gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/src/views/components/AccountContact.js b/src/views/components/AccountContact.js index a907e05..6e2fbf6 100644 --- a/src/views/components/AccountContact.js +++ b/src/views/components/AccountContact.js @@ -39,6 +39,38 @@ export default { }, getPhoneNumber(jid) { return jid.split('@')[0]; + }, + exportToCSV() { + if (!this.contacts || this.contacts.length === 0) { + showErrorInfo("No contacts to export"); + return; + } + + // Create CSV content with headers + let csvContent = "Phone Number,Name\n"; + + // Add each contact as a row + this.contacts.forEach(contact => { + const phoneNumber = this.getPhoneNumber(contact.jid); + // Escape commas and quotes in the name field + const escapedName = contact.name ? contact.name.replace(/"/g, '""') : ""; + csvContent += `${phoneNumber},"${escapedName}"\n`; + }); + + // Create a Blob with the CSV data + const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); + + // Create a download link and trigger download + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.setAttribute('href', url); + link.setAttribute('download', 'contacts.csv'); + link.style.visibility = 'hidden'; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + + showSuccessInfo("Contacts exported to CSV"); } }, template: ` @@ -57,6 +89,9 @@ export default {
My Contacts +
From 1ef8f42e1490cf8ee43d05fc8ac8fedca5017159 Mon Sep 17 00:00:00 2001 From: Aldino Kemal Date: Mon, 24 Mar 2025 14:57:31 +0700 Subject: [PATCH 08/15] style: Update background color to enhance visual consistency --- src/views/assets/app.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/views/assets/app.css b/src/views/assets/app.css index f9ce165..7b7fe6f 100644 --- a/src/views/assets/app.css +++ b/src/views/assets/app.css @@ -2,7 +2,7 @@ --primary-color: #00A884; /* WhatsApp's new brand green */ --secondary-color: #008069; /* WhatsApp's darker green */ --tertiary-color: #075E54; /* WhatsApp's darkest green */ - --background-color: #EFEAE2; /* WhatsApp's authentic background */ + --background-color: #FCF5EB; /* WhatsApp's authentic background */ --card-hover-color: #ffffff; --text-color: #111B21; /* WhatsApp's text color */ --gradient-start: #00A884; From 534e8977be1aefb00e07308c1524590cf320f521 Mon Sep 17 00:00:00 2001 From: Aldino Kemal Date: Mon, 24 Mar 2025 14:59:16 +0700 Subject: [PATCH 09/15] =?UTF-8?q?feat:=20Add=20IsForwarded=20field=20to=20?= =?UTF-8?q?request=20types=20for=20tracking=20forwarded=20m=E2=80=A6=20(#2?= =?UTF-8?q?71)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit feat: Add IsForwarded field to request types for tracking forwarded messages --- src/domains/send/audio.go | 5 +- src/domains/send/contact.go | 1 + src/domains/send/file.go | 7 +-- src/domains/send/image.go | 13 +++--- src/domains/send/link.go | 7 +-- src/domains/send/location.go | 7 +-- src/domains/send/presence.go | 3 +- src/domains/send/text.go | 1 + src/domains/send/video.go | 11 +++-- src/services/send.go | 70 +++++++++++++++++++++++++--- src/views/components/SendAudio.js | 14 +++++- src/views/components/SendContact.js | 20 ++++++-- src/views/components/SendFile.js | 15 +++++- src/views/components/SendImage.js | 20 +++++++- src/views/components/SendLink.js | 10 ++++ src/views/components/SendLocation.js | 15 +++++- src/views/components/SendMessage.js | 10 ++++ src/views/components/SendVideo.js | 18 +++++++ 18 files changed, 208 insertions(+), 39 deletions(-) diff --git a/src/domains/send/audio.go b/src/domains/send/audio.go index 16da723..5d37080 100644 --- a/src/domains/send/audio.go +++ b/src/domains/send/audio.go @@ -3,6 +3,7 @@ package send import "mime/multipart" type AudioRequest struct { - Phone string `json:"phone" form:"phone"` - Audio *multipart.FileHeader `json:"audio" form:"audio"` + Phone string `json:"phone" form:"phone"` + Audio *multipart.FileHeader `json:"audio" form:"audio"` + IsForwarded bool `json:"is_forwarded" form:"is_forwarded"` } diff --git a/src/domains/send/contact.go b/src/domains/send/contact.go index dacadcf..8840e80 100644 --- a/src/domains/send/contact.go +++ b/src/domains/send/contact.go @@ -4,4 +4,5 @@ type ContactRequest struct { Phone string `json:"phone" form:"phone"` ContactName string `json:"contact_name" form:"contact_name"` ContactPhone string `json:"contact_phone" form:"contact_phone"` + IsForwarded bool `json:"is_forwarded" form:"is_forwarded"` } diff --git a/src/domains/send/file.go b/src/domains/send/file.go index ca57790..663fea2 100644 --- a/src/domains/send/file.go +++ b/src/domains/send/file.go @@ -3,7 +3,8 @@ package send import "mime/multipart" type FileRequest struct { - Phone string `json:"phone" form:"phone"` - File *multipart.FileHeader `json:"file" form:"file"` - Caption string `json:"caption" form:"caption"` + Phone string `json:"phone" form:"phone"` + File *multipart.FileHeader `json:"file" form:"file"` + Caption string `json:"caption" form:"caption"` + IsForwarded bool `json:"is_forwarded" form:"is_forwarded"` } diff --git a/src/domains/send/image.go b/src/domains/send/image.go index b6baf95..18450b2 100644 --- a/src/domains/send/image.go +++ b/src/domains/send/image.go @@ -3,10 +3,11 @@ package send import "mime/multipart" 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"` + 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"` + IsForwarded bool `json:"is_forwarded" form:"is_forwarded"` } diff --git a/src/domains/send/link.go b/src/domains/send/link.go index c0bf49e..7979bf1 100644 --- a/src/domains/send/link.go +++ b/src/domains/send/link.go @@ -1,7 +1,8 @@ package send type LinkRequest struct { - Phone string `json:"phone" form:"phone"` - Caption string `json:"caption"` - Link string `json:"link"` + Phone string `json:"phone" form:"phone"` + Caption string `json:"caption"` + Link string `json:"link"` + IsForwarded bool `json:"is_forwarded" form:"is_forwarded"` } diff --git a/src/domains/send/location.go b/src/domains/send/location.go index 43660bd..3d8a661 100644 --- a/src/domains/send/location.go +++ b/src/domains/send/location.go @@ -1,7 +1,8 @@ package send type LocationRequest struct { - Phone string `json:"phone" form:"phone"` - Latitude string `json:"latitude" form:"latitude"` - Longitude string `json:"longitude" form:"longitude"` + Phone string `json:"phone" form:"phone"` + Latitude string `json:"latitude" form:"latitude"` + Longitude string `json:"longitude" form:"longitude"` + IsForwarded bool `json:"is_forwarded" form:"is_forwarded"` } diff --git a/src/domains/send/presence.go b/src/domains/send/presence.go index 301e3e3..baeed08 100644 --- a/src/domains/send/presence.go +++ b/src/domains/send/presence.go @@ -1,5 +1,6 @@ package send type PresenceRequest struct { - Type string `json:"type" form:"type"` + Type string `json:"type" form:"type"` + IsForwarded bool `json:"is_forwarded" form:"is_forwarded"` } diff --git a/src/domains/send/text.go b/src/domains/send/text.go index 332762f..d5a15c3 100644 --- a/src/domains/send/text.go +++ b/src/domains/send/text.go @@ -3,5 +3,6 @@ package send type MessageRequest struct { Phone string `json:"phone" form:"phone"` Message string `json:"message" form:"message"` + IsForwarded bool `json:"is_forwarded" form:"is_forwarded"` ReplyMessageID *string `json:"reply_message_id" form:"reply_message_id"` } diff --git a/src/domains/send/video.go b/src/domains/send/video.go index 5ba739d..818a9e4 100644 --- a/src/domains/send/video.go +++ b/src/domains/send/video.go @@ -3,9 +3,10 @@ package send import "mime/multipart" type VideoRequest struct { - Phone string `json:"phone" form:"phone"` - Caption string `json:"caption" form:"caption"` - Video *multipart.FileHeader `json:"video" form:"video"` - ViewOnce bool `json:"view_once" form:"view_once"` - Compress bool `json:"compress"` + Phone string `json:"phone" form:"phone"` + Caption string `json:"caption" form:"caption"` + Video *multipart.FileHeader `json:"video" form:"video"` + ViewOnce bool `json:"view_once" form:"view_once"` + Compress bool `json:"compress"` + IsForwarded bool `json:"is_forwarded" form:"is_forwarded"` } diff --git a/src/services/send.go b/src/services/send.go index bdfdd10..48a0383 100644 --- a/src/services/send.go +++ b/src/services/send.go @@ -59,18 +59,23 @@ func (service serviceSend) SendText(ctx context.Context, request domainSend.Mess return response, err } - // Send message + // Create base message msg := &waE2E.Message{ ExtendedTextMessage: &waE2E.ExtendedTextMessage{ - Text: proto.String(request.Message), + Text: proto.String(request.Message), + ContextInfo: &waE2E.ContextInfo{}, }, } + // Add forwarding context if IsForwarded is true + if request.IsForwarded { + msg.ExtendedTextMessage.ContextInfo.IsForwarded = proto.Bool(true) + msg.ExtendedTextMessage.ContextInfo.ForwardingScore = proto.Uint32(100) + } + parsedMentions := service.getMentionFromText(ctx, request.Message) if len(parsedMentions) > 0 { - msg.ExtendedTextMessage.ContextInfo = &waE2E.ContextInfo{ - MentionedJID: parsedMentions, - } + msg.ExtendedTextMessage.ContextInfo.MentionedJID = parsedMentions } // Reply message @@ -207,6 +212,13 @@ func (service serviceSend) SendImage(ctx context.Context, request domainSend.Ima ViewOnce: proto.Bool(request.ViewOnce), }} + if request.IsForwarded { + msg.ImageMessage.ContextInfo = &waE2E.ContextInfo{ + IsForwarded: proto.Bool(true), + ForwardingScore: proto.Uint32(100), + } + } + caption := "🖼️ Image" if request.Caption != "" { caption = "🖼️ " + request.Caption @@ -259,6 +271,14 @@ func (service serviceSend) SendFile(ctx context.Context, request domainSend.File DirectPath: proto.String(uploadedFile.DirectPath), Caption: proto.String(request.Caption), }} + + if request.IsForwarded { + msg.DocumentMessage.ContextInfo = &waE2E.ContextInfo{ + IsForwarded: proto.Bool(true), + ForwardingScore: proto.Uint32(100), + } + } + caption := "📄 Document" if request.Caption != "" { caption = "📄 " + request.Caption @@ -371,6 +391,14 @@ func (service serviceSend) SendVideo(ctx context.Context, request domainSend.Vid ThumbnailSHA256: dataWaThumbnail, ThumbnailDirectPath: proto.String(uploaded.DirectPath), }} + + if request.IsForwarded { + msg.VideoMessage.ContextInfo = &waE2E.ContextInfo{ + IsForwarded: proto.Bool(true), + ForwardingScore: proto.Uint32(100), + } + } + caption := "🎥 Video" if request.Caption != "" { caption = "🎥 " + request.Caption @@ -408,6 +436,13 @@ func (service serviceSend) SendContact(ctx context.Context, request domainSend.C Vcard: proto.String(msgVCard), }} + if request.IsForwarded { + msg.ContactMessage.ContextInfo = &waE2E.ContextInfo{ + IsForwarded: proto.Bool(true), + ForwardingScore: proto.Uint32(100), + } + } + content := "👤 " + request.ContactName ts, err := service.wrapSendMessage(ctx, dataWaRecipient, msg, content) @@ -451,6 +486,13 @@ func (service serviceSend) SendLink(ctx context.Context, request domainSend.Link JPEGThumbnail: metadata.ImageThumb, }} + if request.IsForwarded { + msg.ExtendedTextMessage.ContextInfo = &waE2E.ContextInfo{ + IsForwarded: proto.Bool(true), + ForwardingScore: proto.Uint32(100), + } + } + // If we have a thumbnail image, upload it to WhatsApp's servers if len(metadata.ImageThumb) > 0 && metadata.Height != nil && metadata.Width != nil { uploadedThumb, err := service.uploadMedia(ctx, whatsmeow.MediaLinkThumbnail, metadata.ImageThumb, dataWaRecipient) @@ -499,6 +541,13 @@ func (service serviceSend) SendLocation(ctx context.Context, request domainSend. }, } + if request.IsForwarded { + msg.LocationMessage.ContextInfo = &waE2E.ContextInfo{ + IsForwarded: proto.Bool(true), + ForwardingScore: proto.Uint32(100), + } + } + content := "📍 " + request.Latitude + ", " + request.Longitude // Send WhatsApp Message Proto @@ -543,6 +592,13 @@ func (service serviceSend) SendAudio(ctx context.Context, request domainSend.Aud }, } + if request.IsForwarded { + msg.AudioMessage.ContextInfo = &waE2E.ContextInfo{ + IsForwarded: proto.Bool(true), + ForwardingScore: proto.Uint32(100), + } + } + content := "🎵 Audio" ts, err := service.wrapSendMessage(ctx, dataWaRecipient, msg, content) @@ -567,7 +623,9 @@ func (service serviceSend) SendPoll(ctx context.Context, request domainSend.Poll content := "📊 " + request.Question - ts, err := service.wrapSendMessage(ctx, dataWaRecipient, service.WaCli.BuildPollCreation(request.Question, request.Options, request.MaxAnswer), content) + msg := service.WaCli.BuildPollCreation(request.Question, request.Options, request.MaxAnswer) + + ts, err := service.wrapSendMessage(ctx, dataWaRecipient, msg, content) if err != nil { return response, err } diff --git a/src/views/components/SendAudio.js b/src/views/components/SendAudio.js index b68f63e..ca4452d 100644 --- a/src/views/components/SendAudio.js +++ b/src/views/components/SendAudio.js @@ -10,7 +10,8 @@ export default { phone: '', type: window.TYPEUSER, loading: false, - selectedFileName: null + selectedFileName: null, + is_forwarded: false } }, computed: { @@ -27,7 +28,7 @@ export default { }).modal('show'); }, isValidForm() { - if (this.type !== window.TYPESTATUS && !this.phone.trim()) { + if (this.type !== window.TYPEUSER && !this.phone.trim()) { return false; } @@ -55,6 +56,7 @@ export default { try { let payload = new FormData(); payload.append("phone", this.phone_id) + payload.append("is_forwarded", this.is_forwarded) payload.append("audio", $("#file_audio")[0].files[0]) const response = await window.http.post(`/send/audio`, payload) this.handleReset(); @@ -71,6 +73,7 @@ export default { handleReset() { this.phone = ''; this.type = window.TYPEUSER; + this.is_forwarded = false; $("#file_audio").val(''); this.selectedFileName = null; }, @@ -101,6 +104,13 @@ export default {
+
+ +
+ + +
+
diff --git a/src/views/components/SendContact.js b/src/views/components/SendContact.js index 7758302..6dce006 100644 --- a/src/views/components/SendContact.js +++ b/src/views/components/SendContact.js @@ -12,6 +12,7 @@ export default { card_name: '', card_phone: '', loading: false, + is_forwarded: false } }, computed: { @@ -27,6 +28,9 @@ export default { } }).modal('show'); }, + isShowAttributes() { + return this.type !== window.TYPESTATUS; + }, isValidForm() { if (this.type !== window.TYPESTATUS && !this.phone.trim()) { return false; @@ -44,18 +48,15 @@ export default { }, async handleSubmit() { try { - this.loading = true; let response = await this.submitApi() showSuccessInfo(response) $('#modalSendContact').modal('hide'); } catch (err) { showErrorInfo(err) - } finally { - this.loading = false; } }, async submitApi() { - if (!this.isValidForm() || this.loading) { + if (!this.isValidForm()) { return; } @@ -64,7 +65,8 @@ export default { const payload = { phone: this.phone_id, contact_name: this.card_name, - contact_phone: this.card_phone + contact_phone: this.card_phone, + is_forwarded: this.is_forwarded } let response = await window.http.post(`/send/contact`, payload) this.handleReset(); @@ -83,6 +85,7 @@ export default { this.card_name = ''; this.card_phone = ''; this.type = window.TYPEUSER; + this.is_forwarded = false; }, }, template: ` @@ -116,6 +119,13 @@ export default {
+
+ +
+ + +
+
diff --git a/src/views/components/SendFile.js b/src/views/components/SendFile.js index a5de1dc..22bbf0b 100644 --- a/src/views/components/SendFile.js +++ b/src/views/components/SendFile.js @@ -17,7 +17,8 @@ export default { type: window.TYPEUSER, phone: '', loading: false, - selectedFileName: null + selectedFileName: null, + is_forwarded: false } }, computed: { @@ -33,6 +34,9 @@ export default { } }).modal('show'); }, + isShowAttributes() { + return this.type !== window.TYPESTATUS; + }, isValidForm() { if (this.type !== window.TYPESTATUS && !this.phone.trim()) { return false; @@ -63,6 +67,7 @@ export default { let payload = new FormData(); payload.append("caption", this.caption) payload.append("phone", this.phone_id) + payload.append("is_forwarded", this.is_forwarded) payload.append("file", $("#file_file")[0].files[0]) let response = await window.http.post(`/send/file`, payload) this.handleReset(); @@ -81,6 +86,7 @@ export default { this.phone = ''; this.type = window.TYPEUSER; this.selectedFileName = null; + this.is_forwarded = false; $("#file_file").val(''); }, handleFileChange(event) { @@ -117,6 +123,13 @@ export default {
+
+ +
+ + +
+
diff --git a/src/views/components/SendImage.js b/src/views/components/SendImage.js index 0827842..722df8e 100644 --- a/src/views/components/SendImage.js +++ b/src/views/components/SendImage.js @@ -15,7 +15,8 @@ export default { loading: false, selected_file: null, image_url: null, - preview_url: null + preview_url: null, + is_forwarded: false } }, computed: { @@ -23,6 +24,14 @@ export default { return this.phone + this.type; }, }, + watch: { + view_once(newValue) { + // If view_once is set to true, set is_forwarded to false + if (newValue === true) { + this.is_forwarded = false; + } + } + }, methods: { openModal() { $('#modalSendImage').modal({ @@ -66,6 +75,7 @@ export default { payload.append("view_once", this.view_once) payload.append("compress", this.compress) payload.append("caption", this.caption) + payload.append("is_forwarded", this.is_forwarded) const fileInput = $("#file_image"); if (fileInput.length > 0 && fileInput[0].files.length > 0) { @@ -96,6 +106,7 @@ export default { this.preview_url = null; this.selected_file = null; this.image_url = null; + this.is_forwarded = false; $("#file_image").val(''); }, handleImageChange(event) { @@ -155,6 +166,13 @@ export default {
+
+ +
+ + +
+
+
+ +
+ + +
+
diff --git a/src/views/components/SendLocation.js b/src/views/components/SendLocation.js index 226bbc1..1021f80 100644 --- a/src/views/components/SendLocation.js +++ b/src/views/components/SendLocation.js @@ -12,6 +12,7 @@ export default { latitude: '', longitude: '', loading: false, + is_forwarded: false } }, computed: { @@ -43,6 +44,9 @@ export default { } }).modal('show'); }, + isShowAttributes() { + return this.type !== window.TYPESTATUS; + }, async handleSubmit() { try { let response = await this.submitApi() @@ -58,7 +62,8 @@ export default { const payload = { phone: this.phone_id, latitude: this.latitude, - longitude: this.longitude + longitude: this.longitude, + is_forwarded: this.is_forwarded }; const response = await window.http.post(`/send/location`, payload); @@ -78,6 +83,7 @@ export default { this.latitude = ''; this.longitude = ''; this.type = window.TYPEUSER; + this.is_forwarded = false; }, }, template: ` @@ -111,6 +117,13 @@ export default {
+
+ +
+ + +
+
diff --git a/src/views/components/SendMessage.js b/src/views/components/SendMessage.js index 2fffa25..e7993b3 100644 --- a/src/views/components/SendMessage.js +++ b/src/views/components/SendMessage.js @@ -11,6 +11,7 @@ export default { phone: '', text: '', reply_message_id: '', + is_forwarded: false, loading: false, } }, @@ -58,6 +59,7 @@ export default { const payload = { phone: this.phone_id, message: this.text.trim(), + is_forwarded: this.is_forwarded }; if (this.reply_message_id !== '') { payload.reply_message_id = this.reply_message_id; @@ -79,6 +81,7 @@ export default { this.phone = ''; this.text = ''; this.reply_message_id = ''; + this.is_forwarded = false; }, }, template: ` @@ -112,6 +115,13 @@ export default {
+
+ +
+ + +
+
diff --git a/src/views/components/SendVideo.js b/src/views/components/SendVideo.js index d00dc60..d4b0ea1 100644 --- a/src/views/components/SendVideo.js +++ b/src/views/components/SendVideo.js @@ -20,6 +20,7 @@ export default { phone: '', loading: false, selectedFileName: null, + is_forwarded: false } }, computed: { @@ -27,6 +28,14 @@ export default { return this.phone + this.type; }, }, + watch: { + view_once(newValue) { + // If view_once is set to true, set is_forwarded to false + if (newValue === true) { + this.is_forwarded = false; + } + } + }, methods: { openModal() { $('#modalSendVideo').modal({ @@ -81,6 +90,7 @@ export default { payload.append("caption", this.caption.trim()) payload.append("view_once", this.view_once) payload.append("compress", this.compress) + payload.append("is_forwarded", this.is_forwarded) payload.append('video', $("#file_video")[0].files[0]) let response = await window.http.post(`/send/video`, payload) this.handleReset(); @@ -100,6 +110,7 @@ export default { this.compress = false; this.phone = ''; this.selectedFileName = null; + this.is_forwarded = false; $("#file_video").val(''); }, handleFileChange(event) { @@ -152,6 +163,13 @@ export default {
+
+ +
+ + +
+
From 978e3068f41cf24a3b5b8d0cab782e0a5a4d178b Mon Sep 17 00:00:00 2001 From: Aldino Kemal Date: Mon, 24 Mar 2025 14:59:32 +0700 Subject: [PATCH 10/15] =?UTF-8?q?feat:=20Implement=20user=20push=20name=20?= =?UTF-8?q?change=20functionality=20and=20update=20API=20do=E2=80=A6=20(#2?= =?UTF-8?q?70)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit feat: Implement user push name change functionality and update API documentation --- docs/openapi.yaml | 40 +++++++- readme.md | 1 + src/config/settings.go | 2 +- src/domains/user/account.go | 4 + src/domains/user/user.go | 1 + src/internal/rest/user.go | 16 ++++ src/services/user.go | 12 +++ src/views/components/AccountChangePushName.js | 96 +++++++++++++++++++ src/views/index.html | 4 +- 9 files changed, 173 insertions(+), 3 deletions(-) create mode 100644 src/views/components/AccountChangePushName.js diff --git a/docs/openapi.yaml b/docs/openapi.yaml index 2a9d583..0ba4426 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -1,7 +1,7 @@ openapi: "3.0.0" info: title: WhatsApp API MultiDevice - version: 5.2.0 + version: 5.3.0 description: This API is used for sending whatsapp via API servers: - url: http://localhost:3000 @@ -225,6 +225,44 @@ paths: application/json: schema: $ref: '#/components/schemas/ErrorInternalServer' + /user/pushname: + post: + operationId: userChangePushName + tags: + - user + summary: User Change Push Name + description: Update the display name (push name) shown to others in WhatsApp + requestBody: + content: + application/json: + schema: + type: object + properties: + push_name: + type: string + example: 'John Doe' + description: The new display name to set + required: + - push_name + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/GenericResponse' + '400': + description: Bad Request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorBadRequest' + '500': + description: Internal Server Error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorInternalServer' /user/my/privacy: get: operationId: userMyPrivacy diff --git a/readme.md b/readme.md index ab14e6c..488bad9 100644 --- a/readme.md +++ b/readme.md @@ -191,6 +191,7 @@ You can fork or edit this source code ! | ✅ | User Info | GET | /user/info | | ✅ | User Avatar | GET | /user/avatar | | ✅ | User Change Avatar | POST | /user/avatar | +| ✅ | User Change PushName | POST | /user/pushname | | ✅ | User My Groups | GET | /user/my/groups | | ✅ | User My Newsletter | GET | /user/my/newsletters | | ✅ | User My Privacy Setting | GET | /user/my/privacy | diff --git a/src/config/settings.go b/src/config/settings.go index b04457e..712a431 100644 --- a/src/config/settings.go +++ b/src/config/settings.go @@ -5,7 +5,7 @@ import ( ) var ( - AppVersion = "v5.4.0" + AppVersion = "v5.5.0" AppPort = "3000" AppDebug = false AppOs = "AldinoKemal" diff --git a/src/domains/user/account.go b/src/domains/user/account.go index fba3b0b..c0fb6b5 100644 --- a/src/domains/user/account.go +++ b/src/domains/user/account.go @@ -69,3 +69,7 @@ type MyListContactsResponseData struct { JID types.JID `json:"jid"` Name string `json:"name"` } + +type ChangePushNameRequest struct { + PushName string `json:"push_name" form:"push_name"` +} diff --git a/src/domains/user/user.go b/src/domains/user/user.go index 0b46309..ef78619 100644 --- a/src/domains/user/user.go +++ b/src/domains/user/user.go @@ -8,6 +8,7 @@ type IUserService interface { Info(ctx context.Context, request InfoRequest) (response InfoResponse, err error) Avatar(ctx context.Context, request AvatarRequest) (response AvatarResponse, err error) ChangeAvatar(ctx context.Context, request ChangeAvatarRequest) (err error) + ChangePushName(ctx context.Context, request ChangePushNameRequest) (err error) MyListGroups(ctx context.Context) (response MyListGroupsResponse, err error) MyListNewsletter(ctx context.Context) (response MyListNewsletterResponse, err error) MyPrivacySetting(ctx context.Context) (response MyPrivacySettingResponse, err error) diff --git a/src/internal/rest/user.go b/src/internal/rest/user.go index 10596a5..1dc244b 100644 --- a/src/internal/rest/user.go +++ b/src/internal/rest/user.go @@ -16,6 +16,7 @@ func InitRestUser(app *fiber.App, service domainUser.IUserService) User { app.Get("/user/info", rest.UserInfo) app.Get("/user/avatar", rest.UserAvatar) app.Post("/user/avatar", rest.UserChangeAvatar) + app.Post("/user/pushname", rest.UserChangePushName) app.Get("/user/my/privacy", rest.UserMyPrivacySetting) app.Get("/user/my/groups", rest.UserMyListGroups) app.Get("/user/my/newsletters", rest.UserMyListNewsletter) @@ -125,3 +126,18 @@ func (controller *User) UserMyListContacts(c *fiber.Ctx) error { Results: response, }) } + +func (controller *User) UserChangePushName(c *fiber.Ctx) error { + var request domainUser.ChangePushNameRequest + err := c.BodyParser(&request) + utils.PanicIfNeeded(err) + + err = controller.Service.ChangePushName(c.UserContext(), request) + utils.PanicIfNeeded(err) + + return c.JSON(utils.ResponseData{ + Status: 200, + Code: "SUCCESS", + Message: "Success change push name", + }) +} diff --git a/src/services/user.go b/src/services/user.go index e1d0c94..b49562d 100644 --- a/src/services/user.go +++ b/src/services/user.go @@ -14,6 +14,7 @@ import ( "github.com/aldinokemal/go-whatsapp-web-multidevice/validations" "github.com/disintegration/imaging" "go.mau.fi/whatsmeow" + "go.mau.fi/whatsmeow/appstate" "go.mau.fi/whatsmeow/types" ) @@ -231,3 +232,14 @@ func (service userService) ChangeAvatar(ctx context.Context, request domainUser. return nil } + +// ChangePushName implements user.IUserService. +func (service *userService) ChangePushName(ctx context.Context, request domainUser.ChangePushNameRequest) (err error) { + whatsapp.MustLogin(service.WaCli) + + err = service.WaCli.SendAppState(appstate.BuildSettingPushName(request.PushName)) + if err != nil { + return err + } + return nil +} diff --git a/src/views/components/AccountChangePushName.js b/src/views/components/AccountChangePushName.js new file mode 100644 index 0000000..467ff4a --- /dev/null +++ b/src/views/components/AccountChangePushName.js @@ -0,0 +1,96 @@ +export default { + name: 'AccountChangePushName', + data() { + return { + loading: false, + push_name: '' + } + }, + methods: { + openModal() { + $('#modalChangePushName').modal({ + onApprove: function () { + return false; + } + }).modal('show'); + }, + isValidForm() { + return this.push_name.trim() !== ''; + }, + async handleSubmit() { + if (!this.isValidForm() || this.loading) { + return; + } + + try { + let response = await this.submitApi() + showSuccessInfo(response) + $('#modalChangePushName').modal('hide'); + } catch (err) { + showErrorInfo(err) + } + }, + async submitApi() { + this.loading = true; + try { + let payload = { + push_name: this.push_name + } + + let response = await window.http.post(`/user/pushname`, payload) + this.handleReset(); + return response.data.message; + } catch (error) { + if (error.response) { + throw new Error(error.response.data.message); + } + throw new Error(error.message); + } finally { + this.loading = false; + } + }, + handleReset() { + this.push_name = ''; + } + }, + template: ` +
+
+ Account +
Change Push Name
+
+ Update your WhatsApp display name +
+
+
+ + + + ` +} diff --git a/src/views/index.html b/src/views/index.html index 97a2d6f..b79dcc4 100644 --- a/src/views/index.html +++ b/src/views/index.html @@ -157,6 +157,7 @@
+ @@ -219,6 +220,7 @@ import GroupAddParticipants from "./components/GroupManageParticipants.js"; import AccountAvatar from "./components/AccountAvatar.js"; import AccountChangeAvatar from "./components/AccountChangeAvatar.js"; + import AccountChangePushName from "./components/AccountChangePushName.js"; import AccountUserInfo from "./components/AccountUserInfo.js"; import AccountPrivacy from "./components/AccountPrivacy.js"; import NewsletterList from "./components/NewsletterList.js"; @@ -261,7 +263,7 @@ MessageDelete, MessageUpdate, MessageReact, MessageRevoke, GroupList, GroupCreate, GroupJoinWithLink, GroupAddParticipants, NewsletterList, - AccountAvatar, AccountUserInfo, AccountPrivacy, AccountChangeAvatar, AccountContact + AccountAvatar, AccountUserInfo, AccountPrivacy, AccountChangeAvatar, AccountContact, AccountChangePushName }, delimiters: ['[[', ']]'], data() { From 069ed9ca1cd3acac59d37f4c0d836ca5e1882475 Mon Sep 17 00:00:00 2001 From: Aldino Kemal Date: Mon, 24 Mar 2025 17:16:17 +0700 Subject: [PATCH 11/15] test: Add unit tests for chat storage and environment utilities --- src/pkg/utils/chat_storage_test.go | 155 ++++++++++++++++++++++++ src/pkg/utils/environment_test.go | 185 +++++++++++++++++++++++++++++ 2 files changed, 340 insertions(+) create mode 100644 src/pkg/utils/chat_storage_test.go create mode 100644 src/pkg/utils/environment_test.go diff --git a/src/pkg/utils/chat_storage_test.go b/src/pkg/utils/chat_storage_test.go new file mode 100644 index 0000000..73ce445 --- /dev/null +++ b/src/pkg/utils/chat_storage_test.go @@ -0,0 +1,155 @@ +package utils_test + +import ( + "encoding/csv" + "os" + "path/filepath" + "testing" + + "github.com/aldinokemal/go-whatsapp-web-multidevice/config" + . "github.com/aldinokemal/go-whatsapp-web-multidevice/pkg/utils" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" +) + +type ChatStorageTestSuite struct { + suite.Suite + tempDir string + origStorage bool + origPath string +} + +func (suite *ChatStorageTestSuite) SetupTest() { + // Create a temporary directory for test files + tempDir, err := os.MkdirTemp("", "chat_storage_test") + assert.NoError(suite.T(), err) + suite.tempDir = tempDir + + // Save original config values + suite.origStorage = config.WhatsappChatStorage + suite.origPath = config.PathChatStorage + + // Set test config values + config.WhatsappChatStorage = true + config.PathChatStorage = filepath.Join(tempDir, "chat_storage.csv") +} + +func (suite *ChatStorageTestSuite) TearDownTest() { + // Restore original config values + config.WhatsappChatStorage = suite.origStorage + config.PathChatStorage = suite.origPath + + // Clean up temp directory + os.RemoveAll(suite.tempDir) +} + +func (suite *ChatStorageTestSuite) createTestData() { + // Create test CSV data + file, err := os.Create(config.PathChatStorage) + assert.NoError(suite.T(), err) + defer file.Close() + + writer := csv.NewWriter(file) + defer writer.Flush() + + testData := [][]string{ + {"msg1", "user1@test.com", "Hello world"}, + {"msg2", "user2@test.com", "Test message"}, + } + + err = writer.WriteAll(testData) + assert.NoError(suite.T(), err) +} + +func (suite *ChatStorageTestSuite) TestFindRecordFromStorage() { + // Test case: Record found + suite.createTestData() + record, err := FindRecordFromStorage("msg1") + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), "msg1", record.MessageID) + assert.Equal(suite.T(), "user1@test.com", record.JID) + assert.Equal(suite.T(), "Hello world", record.MessageContent) + + // Test case: Record not found + _, err = FindRecordFromStorage("non_existent") + assert.Error(suite.T(), err) + assert.Contains(suite.T(), err.Error(), "not found in storage") + + // Test case: Empty file - should still report message not found + os.Remove(config.PathChatStorage) + _, err = FindRecordFromStorage("msg1") + assert.Error(suite.T(), err) + assert.Contains(suite.T(), err.Error(), "not found in storage") + + // Test case: Corrupted CSV file - should return CSV parsing error + err = os.WriteFile(config.PathChatStorage, []byte("corrupted,csv,data\nwith,no,proper,format"), 0644) + assert.NoError(suite.T(), err) + _, err = FindRecordFromStorage("msg1") + assert.Error(suite.T(), err) + assert.Contains(suite.T(), err.Error(), "failed to read CSV records") + + // Test case: File permissions issue + // Create an unreadable directory for testing file permission issues + unreadableDir := filepath.Join(suite.tempDir, "unreadable") + err = os.Mkdir(unreadableDir, 0000) + assert.NoError(suite.T(), err) + defer os.Chmod(unreadableDir, 0755) // So it can be deleted during teardown + + // Temporarily change path to unreadable location + origPath := config.PathChatStorage + config.PathChatStorage = filepath.Join(unreadableDir, "inaccessible.csv") + _, err = FindRecordFromStorage("anything") + assert.Error(suite.T(), err) + assert.Contains(suite.T(), err.Error(), "failed to open storage file") + + // Restore path + config.PathChatStorage = origPath +} + +func (suite *ChatStorageTestSuite) TestRecordMessage() { + // Test case: Normal recording + err := RecordMessage("newMsg", "user@test.com", "New test message") + assert.NoError(suite.T(), err) + + // Verify the message was recorded + record, err := FindRecordFromStorage("newMsg") + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), "newMsg", record.MessageID) + assert.Equal(suite.T(), "user@test.com", record.JID) + assert.Equal(suite.T(), "New test message", record.MessageContent) + + // Test case: Duplicate message ID + err = RecordMessage("newMsg", "user@test.com", "Duplicate message") + assert.NoError(suite.T(), err) + + // Verify the duplicate wasn't added + record, err = FindRecordFromStorage("newMsg") + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), "New test message", record.MessageContent, "Should not update existing record") + + // Test case: Disabled storage + config.WhatsappChatStorage = false + err = RecordMessage("anotherMsg", "user@test.com", "Should not be stored") + assert.NoError(suite.T(), err) + + config.WhatsappChatStorage = true // Re-enable for next tests + _, err = FindRecordFromStorage("anotherMsg") + assert.Error(suite.T(), err, "Message should not be found when storage is disabled") + + // Test case: Write permission error - Alternative approach to avoid platform-specific issues + // Instead of creating an unwritable file, we'll temporarily set PathChatStorage to a non-existent directory + nonExistentPath := filepath.Join(suite.tempDir, "non-existent-dir", "test.csv") + origPath := config.PathChatStorage + config.PathChatStorage = nonExistentPath + + err = RecordMessage("failMsg", "user@test.com", "Should fail to write") + assert.Error(suite.T(), err) + assert.Contains(suite.T(), err.Error(), "failed to open file for writing") + + // Restore path + config.PathChatStorage = origPath +} + +func TestChatStorageTestSuite(t *testing.T) { + suite.Run(t, new(ChatStorageTestSuite)) +} diff --git a/src/pkg/utils/environment_test.go b/src/pkg/utils/environment_test.go new file mode 100644 index 0000000..fc3c4b4 --- /dev/null +++ b/src/pkg/utils/environment_test.go @@ -0,0 +1,185 @@ +package utils_test + +import ( + "os" + "testing" + "time" + + "github.com/aldinokemal/go-whatsapp-web-multidevice/pkg/utils" + "github.com/spf13/viper" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" +) + +type EnvironmentTestSuite struct { + suite.Suite +} + +func (suite *EnvironmentTestSuite) SetupTest() { + // Clear any existing viper configs + viper.Reset() + // Set up automatic environment variable reading + viper.AutomaticEnv() +} + +func (suite *EnvironmentTestSuite) TearDownTest() { + viper.Reset() +} + +func (suite *EnvironmentTestSuite) TestIsLocal() { + tests := []struct { + name string + envValue string + expected bool + }{ + {"Production environment", "production", false}, + {"Staging environment", "staging", false}, + {"Integration environment", "integration", false}, + {"Development environment", "development", true}, + {"Local environment", "local", true}, + } + + for _, tt := range tests { + suite.T().Run(tt.name, func(t *testing.T) { + // Set the environment value + if tt.envValue != "" { + viper.Set("APP_ENV", tt.envValue) + } else { + viper.Set("APP_ENV", nil) // Explicitly clear the value + } + + result := utils.IsLocal() + assert.Equal(t, tt.expected, result) + }) + } +} + +func (suite *EnvironmentTestSuite) TestEnv() { + // Test with existing value + viper.Set("TEST_KEY", "test_value") + result := utils.Env[string]("TEST_KEY") + assert.Equal(suite.T(), "test_value", result) + + // Test with default value + result = utils.Env("NON_EXISTENT_KEY", "default_value") + assert.Equal(suite.T(), "default_value", result) + + // Test with integer + viper.Set("TEST_INT", 42) + intResult := utils.Env[int]("TEST_INT") + assert.Equal(suite.T(), 42, intResult) + + // Test with default integer + intResult = utils.Env("NON_EXISTENT_INT", 100) + assert.Equal(suite.T(), 100, intResult) + + // Test with boolean + viper.Set("TEST_BOOL", true) + boolResult := utils.Env[bool]("TEST_BOOL") + assert.Equal(suite.T(), true, boolResult) +} + +func (suite *EnvironmentTestSuite) TestMustHaveEnv() { + // Test with value present + viper.Set("REQUIRED_ENV", "required_value") + result := utils.MustHaveEnv("REQUIRED_ENV") + assert.Equal(suite.T(), "required_value", result) + + // Create a temporary .env file for testing + tempEnvContent := []byte("ENV_FROM_FILE=env_file_value\n") + err := os.WriteFile(".env", tempEnvContent, 0644) + assert.NoError(suite.T(), err) + defer os.Remove(".env") + + // Test reading from .env file + result = utils.MustHaveEnv("ENV_FROM_FILE") + assert.Equal(suite.T(), "env_file_value", result) + + // We can't easily test the fatal log scenario in a unit test + // as it would terminate the program +} + +func (suite *EnvironmentTestSuite) TestMustHaveEnvBool() { + // Test true value + viper.Set("BOOL_TRUE", "true") + result := utils.MustHaveEnvBool("BOOL_TRUE") + assert.True(suite.T(), result) + + // Test false value + viper.Set("BOOL_FALSE", "false") + result = utils.MustHaveEnvBool("BOOL_FALSE") + assert.False(suite.T(), result) +} + +func (suite *EnvironmentTestSuite) TestMustHaveEnvInt() { + // Test valid integer + viper.Set("INT_VALUE", "42") + result := utils.MustHaveEnvInt("INT_VALUE") + assert.Equal(suite.T(), 42, result) + + // Test zero + viper.Set("ZERO_INT", "0") + result = utils.MustHaveEnvInt("ZERO_INT") + assert.Equal(suite.T(), 0, result) + + // Test negative number + viper.Set("NEG_INT", "-10") + result = utils.MustHaveEnvInt("NEG_INT") + assert.Equal(suite.T(), -10, result) + + // We can't easily test the fatal log scenario with invalid int + // as it would terminate the program +} + +func (suite *EnvironmentTestSuite) TestMustHaveEnvMinuteDuration() { + // Test valid duration + viper.Set("DURATION_MIN", "5") + result := utils.MustHaveEnvMinuteDuration("DURATION_MIN") + assert.Equal(suite.T(), 5*time.Minute, result) + + // Test zero duration + viper.Set("ZERO_DURATION", "0") + result = utils.MustHaveEnvMinuteDuration("ZERO_DURATION") + assert.Equal(suite.T(), 0*time.Minute, result) + + // We can't easily test the fatal log scenario with invalid duration + // as it would terminate the program +} + +func (suite *EnvironmentTestSuite) TestLoadConfig() { + // Create a temporary config file for testing + tempDir, err := os.MkdirTemp("", "config_test") + assert.NoError(suite.T(), err) + defer os.RemoveAll(tempDir) + + // Create test config file + configContent := []byte("TEST_CONFIG=config_value\n") + configPath := tempDir + "/.env" + err = os.WriteFile(configPath, configContent, 0644) + assert.NoError(suite.T(), err) + + // Test loading config with default name + err = utils.LoadConfig(tempDir) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), "config_value", viper.GetString("TEST_CONFIG")) + + // Test loading config with custom name + customConfigContent := []byte("CUSTOM_CONFIG=custom_value\n") + customConfigPath := tempDir + "/custom.env" + err = os.WriteFile(customConfigPath, customConfigContent, 0644) + assert.NoError(suite.T(), err) + + viper.Reset() + err = utils.LoadConfig(tempDir, "custom") + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), "custom_value", viper.GetString("CUSTOM_CONFIG")) + + // Test error case - non-existent directory + viper.Reset() + err = utils.LoadConfig("/non/existent/directory") + assert.Error(suite.T(), err) +} + +func TestEnvironmentTestSuite(t *testing.T) { + suite.Run(t, new(EnvironmentTestSuite)) +} From deb092207884358dff0edc788cf263c33f119495 Mon Sep 17 00:00:00 2001 From: Aldino Kemal Date: Mon, 24 Mar 2025 21:13:56 +0700 Subject: [PATCH 12/15] fix: Update excluded directories in .air.toml configuration --- src/.air.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/.air.toml b/src/.air.toml index 5b2841a..9d157d7 100644 --- a/src/.air.toml +++ b/src/.air.toml @@ -2,4 +2,4 @@ root = '.' tmp_dir = "tmp" [build] -exclude_dir = ["statics", "views"] \ No newline at end of file +exclude_dir = ["statics", "storages"] \ No newline at end of file From b81332c98cde931f5f60b37db8cafc77d47269ae Mon Sep 17 00:00:00 2001 From: Aldino Kemal Date: Sun, 6 Apr 2025 18:37:48 +0700 Subject: [PATCH 13/15] chore: Update dependencies in go.mod and go.sum - Bump versions for several dependencies: - github.com/mattn/go-sqlite3 from v1.14.24 to v1.14.27 - github.com/spf13/viper from v1.20.0 to v1.20.1 - github.com/valyala/fasthttp from v1.59.0 to v1.60.0 - google.golang.org/protobuf from v1.36.5 to v1.36.6 - go.mau.fi/whatsmeow to a new version - github.com/fsnotify/fsnotify from v1.8.0 to v1.9.0 - github.com/sagikazarmark/locafero from v0.8.0 to v0.9.0 - golang.org/x/net from v0.37.0 to v0.38.0 - golang.org/x/sys from v0.31.0 to v0.32.0 - golang.org/x/text from v0.23.0 to v0.24.0 --- src/go.mod | 20 ++++++++++---------- src/go.sum | 20 ++++++++++++++++++++ 2 files changed, 30 insertions(+), 10 deletions(-) diff --git a/src/go.mod b/src/go.mod index 3df0d92..a5f24f0 100644 --- a/src/go.mod +++ b/src/go.mod @@ -12,17 +12,17 @@ require ( github.com/gofiber/websocket/v2 v2.2.1 github.com/google/uuid v1.6.0 github.com/lib/pq v1.10.9 - github.com/mattn/go-sqlite3 v1.14.24 + github.com/mattn/go-sqlite3 v1.14.27 github.com/sirupsen/logrus v1.9.3 github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e github.com/spf13/cobra v1.9.1 - github.com/spf13/viper v1.20.0 + github.com/spf13/viper v1.20.1 github.com/stretchr/testify v1.10.0 - github.com/valyala/fasthttp v1.59.0 + github.com/valyala/fasthttp v1.60.0 go.mau.fi/libsignal v0.1.2 - go.mau.fi/whatsmeow v0.0.0-20250318233852-06705625cf82 + go.mau.fi/whatsmeow v0.0.0-20250402091807-b0caa1b76088 golang.org/x/image v0.25.0 - google.golang.org/protobuf v1.36.5 + google.golang.org/protobuf v1.36.6 ) require ( @@ -32,7 +32,7 @@ require ( github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/fasthttp/websocket v1.5.12 // indirect - github.com/fsnotify/fsnotify v1.8.0 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/go-viper/mapstructure/v2 v2.2.1 // indirect github.com/gofiber/template v1.8.3 // indirect github.com/gofiber/utils v1.1.0 // indirect @@ -46,7 +46,7 @@ require ( github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/rs/zerolog v1.34.0 // indirect - github.com/sagikazarmark/locafero v0.8.0 // indirect + github.com/sagikazarmark/locafero v0.9.0 // indirect github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.14.0 // indirect @@ -57,8 +57,8 @@ require ( go.mau.fi/util v0.8.6 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/crypto v0.36.0 // indirect - golang.org/x/net v0.37.0 // indirect - golang.org/x/sys v0.31.0 // indirect - golang.org/x/text v0.23.0 // indirect + golang.org/x/net v0.38.0 // indirect + golang.org/x/sys v0.32.0 // indirect + golang.org/x/text v0.24.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/src/go.sum b/src/go.sum index 435f8c2..5fd4c2b 100644 --- a/src/go.sum +++ b/src/go.sum @@ -25,6 +25,8 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/go-ozzo/ozzo-validation/v4 v4.3.0 h1:byhDUpfEwjsVQb1vBunvIjh2BHQ9ead57VkAEY4V+Es= github.com/go-ozzo/ozzo-validation/v4 v4.3.0/go.mod h1:2NKgrcHl3z6cJs+3Oo940FPRiTzuqKbvfrL2RxCj6Ew= github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= @@ -67,6 +69,8 @@ github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6T github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/mattn/go-sqlite3 v1.14.27 h1:drZCnuvf37yPfs95E5jd9s3XhdVWLal+6BOK6qrv6IU= +github.com/mattn/go-sqlite3 v1.14.27/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -84,6 +88,8 @@ github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sagikazarmark/locafero v0.8.0 h1:mXaMVw7IqxNBxfv3LdWt9MDmcWDQ1fagDH918lOdVaQ= github.com/sagikazarmark/locafero v0.8.0/go.mod h1:UBUyz37V+EdMS3hDF3QWIiVr/2dPrx49OMO0Bn0hJqk= +github.com/sagikazarmark/locafero v0.9.0 h1:GbgQGNtTrEmddYDSAH9QLRyfAHY12md+8YFTqyMTC9k= +github.com/sagikazarmark/locafero v0.9.0/go.mod h1:UBUyz37V+EdMS3hDF3QWIiVr/2dPrx49OMO0Bn0hJqk= github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38 h1:D0vL7YNisV2yqE55+q0lFuGse6U8lxlg7fYTctlT5Gc= github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38/go.mod h1:sM7Mt7uEoCeFSCBM+qBrqvEo+/9vdmj19wzp3yzUhmg= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= @@ -102,6 +108,8 @@ github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.20.0 h1:zrxIyR3RQIOsarIrgL8+sAvALXul9jeEPa06Y0Ph6vY= github.com/spf13/viper v1.20.0/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4= +github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4= +github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -113,6 +121,8 @@ github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6Kllzaw github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.59.0 h1:Qu0qYHfXvPk1mSLNqcFtEk6DpxgA26hy6bmydotDpRI= github.com/valyala/fasthttp v1.59.0/go.mod h1:GTxNb9Bc6r2a9D0TWNSPwDz78UxnTGBViY3xZNEqyYU= +github.com/valyala/fasthttp v1.60.0 h1:kBRYS0lOhVJ6V+bYN8PqAHELKHtXqwq9zNMLKx1MBsw= +github.com/valyala/fasthttp v1.60.0/go.mod h1:iY4kDgV3Gc6EqhRZ8icqcmlG6bqhcDXfuHgTO4FXCvc= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= @@ -122,6 +132,8 @@ go.mau.fi/util v0.8.6 h1:AEK13rfgtiZJL2YsNK+W4ihhYCuukcRom8WPP/w/L54= go.mau.fi/util v0.8.6/go.mod h1:uNB3UTXFbkpp7xL1M/WvQks90B/L4gvbLpbS0603KOE= go.mau.fi/whatsmeow v0.0.0-20250318233852-06705625cf82 h1:AZlDkXHgoQNW4gd2hnTCvPH7hYznmwc3gPaYqGZ5w8A= go.mau.fi/whatsmeow v0.0.0-20250318233852-06705625cf82/go.mod h1:WNhj4JeQ6YR6dUOEiCXKqmE4LavSFkwRoKmu4atRrRs= +go.mau.fi/whatsmeow v0.0.0-20250402091807-b0caa1b76088 h1:ns6nk2NjqdaQnCKrp+Qqwpf+3OI7+nnH56D71+7XzOM= +go.mau.fi/whatsmeow v0.0.0-20250402091807-b0caa1b76088/go.mod h1:WNhj4JeQ6YR6dUOEiCXKqmE4LavSFkwRoKmu4atRrRs= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -151,6 +163,8 @@ golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= +golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -174,6 +188,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.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= +golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 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= @@ -194,6 +210,8 @@ golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= +golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= @@ -203,6 +221,8 @@ golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxb golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= From d43aa43e3dbbfded21bc003f387956f7227bb6af Mon Sep 17 00:00:00 2001 From: Aldino Kemal Date: Sat, 19 Apr 2025 19:23:08 +0700 Subject: [PATCH 14/15] feat: approval requested participant group (#276) * feat: Add group participant request management functionality - Introduced new endpoints for handling requested participants in groups: - GET /group/participants/requested to retrieve requested participants. - POST /group/participants/requested/approve to approve participant requests. - POST /group/participants/requested/reject to reject participant requests. - Added corresponding methods in the group service to manage participant requests. - Implemented validation for new request types to ensure proper data handling. * feat: Update API version and enhance group participant request management - Bumped API version from 5.3.0 to 5.4.0. - Added new endpoints for managing group participant requests: - GET /group/participant-requests to retrieve pending requests. - POST /group/participant-requests/approve to approve requests. - POST /group/participant-requests/reject to reject requests. - Updated group service methods to handle new request types and responses. - Enhanced front-end components to support requested member management. * chore: Update dependency versions in go.mod and go.sum - Bumped versions for several dependencies: - github.com/PuerkitoBio/goquery from v1.10.2 to v1.10.3 - github.com/mattn/go-sqlite3 from v1.14.27 to v1.14.28 - go.mau.fi/whatsmeow to a new version - golang.org/x/image from v0.25.0 to v0.26.0 - golang.org/x/net from v0.38.0 to v0.39.0 - github.com/pelletier/go-toml/v2 from v2.2.3 to v2.2.4 - golang.org/x/crypto from v0.36.0 to v0.37.0 - Added new indirect dependencies for petermattis/goid and updated existing ones. * refactor: Change receiver type for ChangePushName method in userService * refactor: Rename group participant request endpoints and streamline request handling - Updated API endpoints for managing group participant requests: - Changed GET /group/participants/requested to GET /group/participant-requests - Changed POST /group/participants/requested/approve to POST /group/participant-requests/approve - Changed POST /group/participants/requested/reject to POST /group/participant-requests/reject - Refactored front-end methods to handle the new endpoint structure and consolidate approval/rejection logic. * feat: Add validation for empty Group ID in participant request listing - Implemented a check to ensure Group ID is not empty in the ListParticipantRequests method. - Returns a 400 Bad Request response with an appropriate error message if Group ID is missing. * feat: Enhance participant request management with error handling - Updated ManageGroupRequestParticipants method to include error handling for participant requests. - Added validation for the Action field in participant management requests to ensure it is not empty and is one of the allowed values. * refactor: Update message handling to use domainMessage consistently - Removed redundant import of message package. - Updated ReactMessage method to use domainMessage types for request and response, ensuring consistency across the service. --- docs/openapi.yaml | 156 +++++++++++++++++++++++++++- readme.md | 93 +++++++++-------- src/config/settings.go | 4 +- src/domains/group/group.go | 19 ++++ src/go.mod | 18 ++-- src/go.sum | 20 ++++ src/internal/rest/group.go | 83 ++++++++------- src/services/group.go | 66 ++++++++++++ src/services/message.go | 3 +- src/services/user.go | 3 +- src/validations/group_validation.go | 29 ++++++ src/views/assets/app.css | 2 +- src/views/components/GroupList.js | 136 +++++++++++++++++++++++- src/views/index.html | 4 +- 14 files changed, 534 insertions(+), 102 deletions(-) diff --git a/docs/openapi.yaml b/docs/openapi.yaml index 0ba4426..3b34482 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -1,7 +1,7 @@ openapi: "3.0.0" info: title: WhatsApp API MultiDevice - version: 5.3.0 + version: 5.4.0 description: This API is used for sending whatsapp via API servers: - url: http://localhost:3000 @@ -18,6 +18,9 @@ tags: description: Group setting - name: newsletter description: newsletter setting +security: + - basicAuth: [] + paths: /app/login: get: @@ -1217,6 +1220,123 @@ paths: application/json: schema: $ref: '#/components/schemas/ErrorInternalServer' + /group/participant-requests: + get: + operationId: getGroupParticipantRequests + tags: + - group + summary: Get list of participant requests to join group + parameters: + - name: group_id + in: query + required: true + schema: + type: string + example: '120363024512399999@g.us' + description: The group ID to get participant requests for + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/GroupParticipantRequestListResponse' + '400': + description: Bad Request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorBadRequest' + '500': + description: Internal Server Error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorInternalServer' + /group/participant-requests/approve: + post: + operationId: approveGroupParticipantRequest + tags: + - group + summary: Approve participant request to join group + requestBody: + content: + application/json: + schema: + type: object + properties: + group_id: + type: string + example: '120363024512399999@g.us' + description: The group ID + participant_id: + type: string + example: '6281234567890' + description: The participant's WhatsApp ID to approve + required: + - group_id + - participant_id + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/GenericResponse' + '400': + description: Bad Request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorBadRequest' + '500': + description: Internal Server Error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorInternalServer' + /group/participant-requests/reject: + post: + operationId: rejectGroupParticipantRequest + tags: + - group + summary: Reject participant request to join group + requestBody: + content: + application/json: + schema: + type: object + properties: + group_id: + type: string + example: '120363024512399999@g.us' + description: The group ID + participant_id: + type: string + example: '6281234567890' + description: The participant's WhatsApp ID to reject + required: + - group_id + - participant_id + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/GenericResponse' + '400': + description: Bad Request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorBadRequest' + '500': + description: Internal Server Error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorInternalServer' /group/leave: post: operationId: leaveGroup @@ -1287,6 +1407,10 @@ paths: $ref: '#/components/schemas/ErrorInternalServer' components: + securitySchemes: + basicAuth: + type: http + scheme: basic schemas: CreateGroupResponse: type: object @@ -1319,6 +1443,7 @@ components: - '6839241294719274' ManageParticipantResponse: type: object + additionalProperties: false properties: code: type: string @@ -1329,6 +1454,8 @@ components: results: type: array items: + type: object + additionalProperties: false properties: participant: type: string @@ -1851,4 +1978,29 @@ components: example: 0 AddRequest: type: string - example: null \ No newline at end of file + example: null + + GroupParticipantRequestListResponse: + type: object + properties: + code: + type: string + example: "SUCCESS" + message: + type: string + example: "Success getting list requested participants" + results: + type: object + properties: + data: + type: array + items: + type: object + properties: + jid: + type: string + example: "6289685024091@s.whatsapp.net" + requested_at: + type: string + format: date-time + example: "2024-10-11T21:27:29+07:00" \ No newline at end of file diff --git a/readme.md b/readme.md index 488bad9..7c25343 100644 --- a/readme.md +++ b/readme.md @@ -2,7 +2,7 @@ [![Patreon](https://img.shields.io/badge/Support%20on-Patreon-orange.svg)](https://www.patreon.com/c/aldinokemal) **If you're using this tools to generate income, consider supporting its development by becoming a Patreon member!** -Your support helps ensure the library stays maintained and receives regular updates. Join our community of supporters today! +Your support helps ensure the library stays maintained and receives regular updates! ___ ![release version](https://img.shields.io/github/v/release/aldinokemal/go-whatsapp-web-multidevice) @@ -44,13 +44,14 @@ Now that we support ARM64 for Linux: - `-w="http://yourwebhook.site/handler"` - Webhook Secret Our webhook will be sent to you with an HMAC header and a sha256 default key `secret`. - + You may modify this by using the option below: - `--webhook-secret="secret"` ## Configuration -You can configure the application using either command-line flags (shown above) or environment variables. Configuration can be set in three ways (in order of priority): +You can configure the application using either command-line flags (shown above) or environment variables. Configuration +can be set in three ways (in order of priority): 1. Command-line flags (highest priority) 2. Environment variables @@ -181,45 +182,48 @@ You can fork or edit this source code ! - Use [SwaggerEditor](https://editor.swagger.io) to visualize the API. - Generate HTTP clients using [openapi-generator](https://openapi-generator.tech/#try). -| Feature | Menu | Method | URL | -|---------|------------------------------|--------|-------------------------------| -| ✅ | Login with Scan QR | GET | /app/login | -| ✅ | Login With Pair Code | GET | /app/login-with-code | -| ✅ | Logout | GET | /app/logout | -| ✅ | Reconnect | GET | /app/reconnect | -| ✅ | Devices | GET | /app/devices | -| ✅ | User Info | GET | /user/info | -| ✅ | User Avatar | GET | /user/avatar | -| ✅ | User Change Avatar | POST | /user/avatar | -| ✅ | User Change PushName | POST | /user/pushname | -| ✅ | User My Groups | GET | /user/my/groups | -| ✅ | User My Newsletter | GET | /user/my/newsletters | -| ✅ | User My Privacy Setting | GET | /user/my/privacy | -| ✅ | User My Contacts | GET | /user/my/contacts | -| ✅ | 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 | -| ✅ | Send Link | POST | /send/link | -| ✅ | Send Location | POST | /send/location | -| ✅ | Send Poll / Vote | POST | /send/poll | -| ✅ | Send Presence | POST | /send/presence | -| ✅ | Revoke Message | POST | /message/:message_id/revoke | -| ✅ | React Message | POST | /message/:message_id/reaction | -| ✅ | Delete Message | POST | /message/:message_id/delete | -| ✅ | Edit Message | POST | /message/:message_id/update | -| ✅ | Read Message (DM) | POST | /message/:message_id/read | -| ❌ | Star Message | POST | /message/:message_id/star | -| ✅ | Join Group With Link | POST | /group/join-with-link | -| ✅ | Leave Group | POST | /group/leave | -| ✅ | Create Group | POST | /group | -| ✅ | Add Participants in Group | POST | /group/participants | -| ✅ | Remove Participant in Group | POST | /group/participants/remove | -| ✅ | Promote Participant in Group | POST | /group/participants/promote | -| ✅ | Demote Participant in Group | POST | /group/participants/demote | -| ✅ | Unfollow Newsletter | POST | /newsletter/unfollow | +| Feature | Menu | Method | URL | +|---------|----------------------------------------|--------|---------------------------------------| +| ✅ | Login with Scan QR | GET | /app/login | +| ✅ | Login With Pair Code | GET | /app/login-with-code | +| ✅ | Logout | GET | /app/logout | +| ✅ | Reconnect | GET | /app/reconnect | +| ✅ | Devices | GET | /app/devices | +| ✅ | User Info | GET | /user/info | +| ✅ | User Avatar | GET | /user/avatar | +| ✅ | User Change Avatar | POST | /user/avatar | +| ✅ | User Change PushName | POST | /user/pushname | +| ✅ | User My Groups | GET | /user/my/groups | +| ✅ | User My Newsletter | GET | /user/my/newsletters | +| ✅ | User My Privacy Setting | GET | /user/my/privacy | +| ✅ | User My Contacts | GET | /user/my/contacts | +| ✅ | 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 | +| ✅ | Send Link | POST | /send/link | +| ✅ | Send Location | POST | /send/location | +| ✅ | Send Poll / Vote | POST | /send/poll | +| ✅ | Send Presence | POST | /send/presence | +| ✅ | Revoke Message | POST | /message/:message_id/revoke | +| ✅ | React Message | POST | /message/:message_id/reaction | +| ✅ | Delete Message | POST | /message/:message_id/delete | +| ✅ | Edit Message | POST | /message/:message_id/update | +| ✅ | Read Message (DM) | POST | /message/:message_id/read | +| ✅ | Star Message | POST | /message/:message_id/star | +| ✅ | Join Group With Link | POST | /group/join-with-link | +| ✅ | Leave Group | POST | /group/leave | +| ✅ | Create Group | POST | /group | +| ✅ | Add Participants in Group | POST | /group/participants | +| ✅ | Remove Participant in Group | POST | /group/participants/remove | +| ✅ | Promote Participant in Group | POST | /group/participants/promote | +| ✅ | Demote Participant in Group | POST | /group/participants/demote | +| ✅ | List Requested Participants in Group | POST | /group/participants/requested | +| ✅ | Approve Requested Participant in Group | POST | /group/participants/requested/approve | +| ✅ | Reject Requested Participant in Group | POST | /group/participants/requested/reject | +| ✅ | Unfollow Newsletter | POST | /newsletter/unfollow | ```txt ✅ = Available @@ -261,3 +265,8 @@ You can fork or edit this source code ! - Please do this if you have an error (invalid flag in pkg-config --cflags: -Xpreprocessor) `export CGO_CFLAGS_ALLOW="-Xpreprocessor"` + +## Important + +- This project is unofficial and not affiliated with WhatsApp. +- Please use official WhatsApp API to avoid any issues. diff --git a/src/config/settings.go b/src/config/settings.go index 712a431..7815f1d 100644 --- a/src/config/settings.go +++ b/src/config/settings.go @@ -5,7 +5,7 @@ import ( ) var ( - AppVersion = "v5.5.0" + AppVersion = "v5.6.0" AppPort = "3000" AppDebug = false AppOs = "AldinoKemal" @@ -19,7 +19,7 @@ var ( PathStorages = "storages" PathChatStorage = "storages/chat.csv" - DBURI = "file:storages/whatsapp.db?_foreign_keys=off" + DBURI = "file:storages/whatsapp.db?_foreign_keys=on" WhatsappAutoReplyMessage string WhatsappWebhook []string diff --git a/src/domains/group/group.go b/src/domains/group/group.go index 59d6764..36e8db4 100644 --- a/src/domains/group/group.go +++ b/src/domains/group/group.go @@ -2,6 +2,8 @@ package group import ( "context" + "time" + "go.mau.fi/whatsmeow" ) @@ -10,6 +12,8 @@ type IGroupService interface { LeaveGroup(ctx context.Context, request LeaveGroupRequest) (err error) CreateGroup(ctx context.Context, request CreateGroupRequest) (groupID string, err error) ManageParticipant(ctx context.Context, request ParticipantRequest) (result []ParticipantStatus, err error) + GetGroupRequestParticipants(ctx context.Context, request GetGroupRequestParticipantsRequest) (result []GetGroupRequestParticipantsResponse, err error) + ManageGroupRequestParticipants(ctx context.Context, request GroupRequestParticipantsRequest) (result []ParticipantStatus, err error) } type JoinGroupWithLinkRequest struct { @@ -36,3 +40,18 @@ type ParticipantStatus struct { Status string `json:"status"` Message string `json:"message"` } + +type GetGroupRequestParticipantsRequest struct { + GroupID string `json:"group_id" query:"group_id"` +} + +type GetGroupRequestParticipantsResponse struct { + JID string `json:"jid"` + RequestedAt time.Time `json:"requested_at"` +} + +type GroupRequestParticipantsRequest struct { + GroupID string `json:"group_id" form:"group_id"` + Participants []string `json:"participants" form:"participants"` + Action whatsmeow.ParticipantRequestChange `json:"action" form:"action"` +} diff --git a/src/go.mod b/src/go.mod index a5f24f0..dda8e1a 100644 --- a/src/go.mod +++ b/src/go.mod @@ -3,7 +3,7 @@ module github.com/aldinokemal/go-whatsapp-web-multidevice go 1.24.0 require ( - github.com/PuerkitoBio/goquery v1.10.2 + github.com/PuerkitoBio/goquery v1.10.3 github.com/disintegration/imaging v1.6.2 github.com/dustin/go-humanize v1.0.1 github.com/go-ozzo/ozzo-validation/v4 v4.3.0 @@ -12,7 +12,7 @@ require ( github.com/gofiber/websocket/v2 v2.2.1 github.com/google/uuid v1.6.0 github.com/lib/pq v1.10.9 - github.com/mattn/go-sqlite3 v1.14.27 + github.com/mattn/go-sqlite3 v1.14.28 github.com/sirupsen/logrus v1.9.3 github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e github.com/spf13/cobra v1.9.1 @@ -20,8 +20,8 @@ require ( github.com/stretchr/testify v1.10.0 github.com/valyala/fasthttp v1.60.0 go.mau.fi/libsignal v0.1.2 - go.mau.fi/whatsmeow v0.0.0-20250402091807-b0caa1b76088 - golang.org/x/image v0.25.0 + go.mau.fi/whatsmeow v0.0.0-20250417131650-164ddf482526 + golang.org/x/image v0.26.0 google.golang.org/protobuf v1.36.6 ) @@ -42,12 +42,13 @@ require ( github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect - github.com/pelletier/go-toml/v2 v2.2.3 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/petermattis/goid v0.0.0-20250319124200-ccd6737f222a // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/rs/zerolog v1.34.0 // indirect github.com/sagikazarmark/locafero v0.9.0 // indirect - github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38 // indirect + github.com/savsgio/gotils v0.0.0-20250408102913-196191ec6287 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.14.0 // indirect github.com/spf13/cast v1.7.1 // indirect @@ -56,8 +57,9 @@ require ( github.com/valyala/bytebufferpool v1.0.0 // indirect go.mau.fi/util v0.8.6 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/crypto v0.36.0 // indirect - golang.org/x/net v0.38.0 // indirect + golang.org/x/crypto v0.37.0 // indirect + golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect + golang.org/x/net v0.39.0 // indirect golang.org/x/sys v0.32.0 // indirect golang.org/x/text v0.24.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/src/go.sum b/src/go.sum index 5fd4c2b..b2b1136 100644 --- a/src/go.sum +++ b/src/go.sum @@ -2,6 +2,8 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/PuerkitoBio/goquery v1.10.2 h1:7fh2BdHcG6VFZsK7toXBT/Bh1z5Wmy8Q9MV9HqT2AM8= github.com/PuerkitoBio/goquery v1.10.2/go.mod h1:0guWGjcLu9AYC7C1GHnpysHy056u9aEkUHwhdnePMCU= +github.com/PuerkitoBio/goquery v1.10.3 h1:pFYcNSqHxBD06Fpj/KsbStFRsgRATgnf3LeXiUkhzPo= +github.com/PuerkitoBio/goquery v1.10.3/go.mod h1:tMUX0zDMHXYlAQk6p35XxQMqMweEKB7iK7iLNd4RH4Y= github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM= @@ -71,8 +73,14 @@ github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBW github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mattn/go-sqlite3 v1.14.27 h1:drZCnuvf37yPfs95E5jd9s3XhdVWLal+6BOK6qrv6IU= github.com/mattn/go-sqlite3 v1.14.27/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A= +github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/petermattis/goid v0.0.0-20250319124200-ccd6737f222a h1:S+AGcmAESQ0pXCUNnRH7V+bOUIgkSX5qVt2cNKCrm0Q= +github.com/petermattis/goid v0.0.0-20250319124200-ccd6737f222a/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= @@ -92,6 +100,8 @@ github.com/sagikazarmark/locafero v0.9.0 h1:GbgQGNtTrEmddYDSAH9QLRyfAHY12md+8YFT github.com/sagikazarmark/locafero v0.9.0/go.mod h1:UBUyz37V+EdMS3hDF3QWIiVr/2dPrx49OMO0Bn0hJqk= github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38 h1:D0vL7YNisV2yqE55+q0lFuGse6U8lxlg7fYTctlT5Gc= github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38/go.mod h1:sM7Mt7uEoCeFSCBM+qBrqvEo+/9vdmj19wzp3yzUhmg= +github.com/savsgio/gotils v0.0.0-20250408102913-196191ec6287 h1:qIQ0tWF9vxGtkJa24bR+2i53WBCz1nW/Pc47oVYauC4= +github.com/savsgio/gotils v0.0.0-20250408102913-196191ec6287/go.mod h1:sM7Mt7uEoCeFSCBM+qBrqvEo+/9vdmj19wzp3yzUhmg= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= @@ -134,6 +144,8 @@ go.mau.fi/whatsmeow v0.0.0-20250318233852-06705625cf82 h1:AZlDkXHgoQNW4gd2hnTCvP go.mau.fi/whatsmeow v0.0.0-20250318233852-06705625cf82/go.mod h1:WNhj4JeQ6YR6dUOEiCXKqmE4LavSFkwRoKmu4atRrRs= go.mau.fi/whatsmeow v0.0.0-20250402091807-b0caa1b76088 h1:ns6nk2NjqdaQnCKrp+Qqwpf+3OI7+nnH56D71+7XzOM= go.mau.fi/whatsmeow v0.0.0-20250402091807-b0caa1b76088/go.mod h1:WNhj4JeQ6YR6dUOEiCXKqmE4LavSFkwRoKmu4atRrRs= +go.mau.fi/whatsmeow v0.0.0-20250417131650-164ddf482526 h1:i9w16FdM3zmOWdF5nh1l2MlmE/wK7ulL6rbT02WBBJs= +go.mau.fi/whatsmeow v0.0.0-20250417131650-164ddf482526/go.mod h1:NlPtoLdpX3RnltqCTCZQ6kIUfprqLirtSK1gHvwoNx0= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -144,9 +156,15 @@ golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= +golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= +golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM= +golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8= golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ= golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs= +golang.org/x/image v0.26.0 h1:4XjIFEZWQmCZi6Wv8BoxsDhRU3RVnLX04dToTDAEPlY= +golang.org/x/image v0.26.0/go.mod h1:lcxbMFAovzpnJxzXS3nyL83K27tmqtKzIJpctK8YO5c= 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= @@ -165,6 +183,8 @@ golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= +golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= diff --git a/src/internal/rest/group.go b/src/internal/rest/group.go index 53f98bd..b365862 100644 --- a/src/internal/rest/group.go +++ b/src/internal/rest/group.go @@ -2,6 +2,7 @@ package rest import ( "fmt" + domainGroup "github.com/aldinokemal/go-whatsapp-web-multidevice/domains/group" "github.com/aldinokemal/go-whatsapp-web-multidevice/pkg/utils" "github.com/aldinokemal/go-whatsapp-web-multidevice/pkg/whatsapp" @@ -22,6 +23,9 @@ func InitRestGroup(app *fiber.App, service domainGroup.IGroupService) Group { app.Post("/group/participants/remove", rest.DeleteParticipants) app.Post("/group/participants/promote", rest.PromoteParticipants) app.Post("/group/participants/demote", rest.DemoteParticipants) + app.Get("/group/participant-requests", rest.ListParticipantRequests) + app.Post("/group/participant-requests/approve", rest.ApproveParticipantRequests) + app.Post("/group/participant-requests/reject", rest.RejectParticipantRequests) return rest } @@ -77,83 +81,86 @@ func (controller *Group) CreateGroup(c *fiber.Ctx) error { }, }) } - func (controller *Group) AddParticipants(c *fiber.Ctx) error { - var request domainGroup.ParticipantRequest - err := c.BodyParser(&request) - utils.PanicIfNeeded(err) - - whatsapp.SanitizePhone(&request.GroupID) + return controller.manageParticipants(c, whatsmeow.ParticipantChangeAdd, "Success add participants") +} - request.Action = whatsmeow.ParticipantChangeAdd +func (controller *Group) DeleteParticipants(c *fiber.Ctx) error { + return controller.manageParticipants(c, whatsmeow.ParticipantChangeRemove, "Success delete participants") +} - result, err := controller.Service.ManageParticipant(c.UserContext(), request) - utils.PanicIfNeeded(err) +func (controller *Group) PromoteParticipants(c *fiber.Ctx) error { + return controller.manageParticipants(c, whatsmeow.ParticipantChangePromote, "Success promote participants") +} - return c.JSON(utils.ResponseData{ - Status: 200, - Code: "SUCCESS", - Message: "Success add participants", - Results: result, - }) +func (controller *Group) DemoteParticipants(c *fiber.Ctx) error { + return controller.manageParticipants(c, whatsmeow.ParticipantChangeDemote, "Success demote participants") } -func (controller *Group) DeleteParticipants(c *fiber.Ctx) error { - var request domainGroup.ParticipantRequest - err := c.BodyParser(&request) +func (controller *Group) ListParticipantRequests(c *fiber.Ctx) error { + var request domainGroup.GetGroupRequestParticipantsRequest + err := c.QueryParser(&request) utils.PanicIfNeeded(err) - whatsapp.SanitizePhone(&request.GroupID) + if request.GroupID == "" { + return c.Status(fiber.StatusBadRequest).JSON(utils.ResponseData{ + Status: 400, + Code: "INVALID_GROUP_ID", + Message: "Group ID cannot be empty", + }) + } - request.Action = whatsmeow.ParticipantChangeRemove + whatsapp.SanitizePhone(&request.GroupID) - result, err := controller.Service.ManageParticipant(c.UserContext(), request) + result, err := controller.Service.GetGroupRequestParticipants(c.UserContext(), request) utils.PanicIfNeeded(err) return c.JSON(utils.ResponseData{ Status: 200, Code: "SUCCESS", - Message: "Success delete participants", + Message: "Success getting list requested participants", Results: result, }) } -func (controller *Group) PromoteParticipants(c *fiber.Ctx) error { +func (controller *Group) ApproveParticipantRequests(c *fiber.Ctx) error { + return controller.handleRequestedParticipants(c, whatsmeow.ParticipantChangeApprove, "Success approve requested participants") +} + +func (controller *Group) RejectParticipantRequests(c *fiber.Ctx) error { + return controller.handleRequestedParticipants(c, whatsmeow.ParticipantChangeReject, "Success reject requested participants") +} + +// Generalized participant management handler +func (controller *Group) manageParticipants(c *fiber.Ctx, action whatsmeow.ParticipantChange, successMsg string) error { var request domainGroup.ParticipantRequest err := c.BodyParser(&request) utils.PanicIfNeeded(err) - whatsapp.SanitizePhone(&request.GroupID) - - request.Action = whatsmeow.ParticipantChangePromote - + request.Action = action result, err := controller.Service.ManageParticipant(c.UserContext(), request) utils.PanicIfNeeded(err) - return c.JSON(utils.ResponseData{ Status: 200, Code: "SUCCESS", - Message: "Success promote participants", + Message: successMsg, Results: result, }) } -func (controller *Group) DemoteParticipants(c *fiber.Ctx) error { - var request domainGroup.ParticipantRequest +// Generalized requested participants handler +func (controller *Group) handleRequestedParticipants(c *fiber.Ctx, action whatsmeow.ParticipantRequestChange, successMsg string) error { + var request domainGroup.GroupRequestParticipantsRequest err := c.BodyParser(&request) utils.PanicIfNeeded(err) - whatsapp.SanitizePhone(&request.GroupID) - - request.Action = whatsmeow.ParticipantChangeDemote - - result, err := controller.Service.ManageParticipant(c.UserContext(), request) + request.Action = action + result, err := controller.Service.ManageGroupRequestParticipants(c.UserContext(), request) utils.PanicIfNeeded(err) - return c.JSON(utils.ResponseData{ Status: 200, Code: "SUCCESS", - Message: "Success demote participants", + Message: successMsg, Results: result, }) } diff --git a/src/services/group.go b/src/services/group.go index afe2aa1..2c31bb6 100644 --- a/src/services/group.go +++ b/src/services/group.go @@ -2,6 +2,8 @@ package services import ( "context" + "fmt" + "github.com/aldinokemal/go-whatsapp-web-multidevice/config" domainGroup "github.com/aldinokemal/go-whatsapp-web-multidevice/domains/group" pkgError "github.com/aldinokemal/go-whatsapp-web-multidevice/pkg/error" @@ -113,6 +115,70 @@ func (service groupService) ManageParticipant(ctx context.Context, request domai return result, nil } +func (service groupService) GetGroupRequestParticipants(ctx context.Context, request domainGroup.GetGroupRequestParticipantsRequest) (result []domainGroup.GetGroupRequestParticipantsResponse, err error) { + if err = validations.ValidateGetGroupRequestParticipants(ctx, request); err != nil { + return result, err + } + + groupJID, err := whatsapp.ValidateJidWithLogin(service.WaCli, request.GroupID) + if err != nil { + return result, err + } + + participants, err := service.WaCli.GetGroupRequestParticipants(groupJID) + if err != nil { + return result, err + } + + for _, participant := range participants { + result = append(result, domainGroup.GetGroupRequestParticipantsResponse{ + JID: participant.JID.String(), + RequestedAt: participant.RequestedAt, + }) + } + + return result, nil +} + +func (service groupService) ManageGroupRequestParticipants(ctx context.Context, request domainGroup.GroupRequestParticipantsRequest) (result []domainGroup.ParticipantStatus, err error) { + if err = validations.ValidateManageGroupRequestParticipants(ctx, request); err != nil { + return result, err + } + + groupJID, err := whatsapp.ValidateJidWithLogin(service.WaCli, request.GroupID) + if err != nil { + return result, err + } + + participantsJID, err := service.participantToJID(request.Participants) + if err != nil { + return result, err + } + + participants, err := service.WaCli.UpdateGroupRequestParticipants(groupJID, participantsJID, request.Action) + if err != nil { + return result, err + } + + for _, participant := range participants { + if participant.Error != 0 { + result = append(result, domainGroup.ParticipantStatus{ + Participant: participant.JID.String(), + Status: "error", + Message: fmt.Sprintf("Action %s failed (code %d)", request.Action, participant.Error), + }) + } else { + result = append(result, domainGroup.ParticipantStatus{ + Participant: participant.JID.String(), + Status: "success", + Message: fmt.Sprintf("Action %s success", request.Action), + }) + } + } + + return result, nil +} + func (service groupService) participantToJID(participants []string) ([]types.JID, error) { var participantsJID []types.JID for _, participant := range participants { diff --git a/src/services/message.go b/src/services/message.go index 1a9eaaa..9d16da1 100644 --- a/src/services/message.go +++ b/src/services/message.go @@ -5,7 +5,6 @@ import ( "fmt" "time" - "github.com/aldinokemal/go-whatsapp-web-multidevice/domains/message" domainMessage "github.com/aldinokemal/go-whatsapp-web-multidevice/domains/message" "github.com/aldinokemal/go-whatsapp-web-multidevice/pkg/whatsapp" "github.com/aldinokemal/go-whatsapp-web-multidevice/validations" @@ -55,7 +54,7 @@ func (service serviceMessage) MarkAsRead(ctx context.Context, request domainMess return response, nil } -func (service serviceMessage) ReactMessage(ctx context.Context, request message.ReactionRequest) (response message.GenericResponse, err error) { +func (service serviceMessage) ReactMessage(ctx context.Context, request domainMessage.ReactionRequest) (response domainMessage.GenericResponse, err error) { if err = validations.ValidateReactMessage(ctx, request); err != nil { return response, err } diff --git a/src/services/user.go b/src/services/user.go index b49562d..12e7f1f 100644 --- a/src/services/user.go +++ b/src/services/user.go @@ -233,8 +233,7 @@ func (service userService) ChangeAvatar(ctx context.Context, request domainUser. return nil } -// ChangePushName implements user.IUserService. -func (service *userService) ChangePushName(ctx context.Context, request domainUser.ChangePushNameRequest) (err error) { +func (service userService) ChangePushName(ctx context.Context, request domainUser.ChangePushNameRequest) (err error) { whatsapp.MustLogin(service.WaCli) err = service.WaCli.SendAppState(appstate.BuildSettingPushName(request.PushName)) diff --git a/src/validations/group_validation.go b/src/validations/group_validation.go index 0758c1f..cd6ab5a 100644 --- a/src/validations/group_validation.go +++ b/src/validations/group_validation.go @@ -2,9 +2,11 @@ package validations import ( "context" + domainGroup "github.com/aldinokemal/go-whatsapp-web-multidevice/domains/group" pkgError "github.com/aldinokemal/go-whatsapp-web-multidevice/pkg/error" validation "github.com/go-ozzo/ozzo-validation/v4" + "go.mau.fi/whatsmeow" ) func ValidateJoinGroupWithLink(ctx context.Context, request domainGroup.JoinGroupWithLinkRequest) error { @@ -58,3 +60,30 @@ func ValidateParticipant(ctx context.Context, request domainGroup.ParticipantReq return nil } + +func ValidateGetGroupRequestParticipants(ctx context.Context, request domainGroup.GetGroupRequestParticipantsRequest) error { + err := validation.ValidateStructWithContext(ctx, &request, + validation.Field(&request.GroupID, validation.Required), + ) + + if err != nil { + return pkgError.ValidationError(err.Error()) + } + + return nil +} + +func ValidateManageGroupRequestParticipants(ctx context.Context, request domainGroup.GroupRequestParticipantsRequest) error { + err := validation.ValidateStructWithContext(ctx, &request, + validation.Field(&request.GroupID, validation.Required), + validation.Field(&request.Participants, validation.Required), + validation.Field(&request.Participants, validation.Each(validation.Required)), + validation.Field(&request.Action, validation.Required, validation.In(whatsmeow.ParticipantChangeApprove, whatsmeow.ParticipantChangeReject)), + ) + + if err != nil { + return pkgError.ValidationError(err.Error()) + } + + return nil +} diff --git a/src/views/assets/app.css b/src/views/assets/app.css index 7b7fe6f..9c41e99 100644 --- a/src/views/assets/app.css +++ b/src/views/assets/app.css @@ -204,7 +204,7 @@ body { border-radius: 12px !important; transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275) !important; /* background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%) !important; */ - background: var(--primary-color) !important; + /* background: var(--primary-color) !important; */ color: white !important; font-weight: 600 !important; letter-spacing: 0.5px; diff --git a/src/views/components/GroupList.js b/src/views/components/GroupList.js index 38cb2d5..3fe44c2 100644 --- a/src/views/components/GroupList.js +++ b/src/views/components/GroupList.js @@ -1,8 +1,20 @@ export default { name: 'ListGroup', + props: ['connected'], data() { return { - groups: [] + groups: [], + selectedGroupId: null, + requestedMembers: [], + loadingRequestedMembers: false, + processingMember: null + } + }, + computed: { + currentUserId() { + if (!this.connected || this.connected.length === 0) return null; + const device = this.connected[0].device; + return device.split('@')[0].split(':')[0]; } }, methods: { @@ -67,6 +79,69 @@ export default { formatDate: function (value) { if (!value) return '' return moment(value).format('LLL'); + }, + isAdmin(ownerJID) { + const owner = ownerJID.split('@')[0]; + return owner === this.currentUserId; + }, + async handleSeeRequestedMember(group_id) { + this.selectedGroupId = group_id; + this.loadingRequestedMembers = true; + this.requestedMembers = []; + + try { + const response = await window.http.get(`/group/participant-requests?group_id=${group_id}`); + this.requestedMembers = response.data.results || []; + this.loadingRequestedMembers = false; + $('#modalRequestedMembers').modal('show'); + } catch (error) { + this.loadingRequestedMembers = false; + let errorMessage = "Failed to fetch requested members"; + if (error.response) { + errorMessage = error.response.data.message || errorMessage; + } + showErrorInfo(errorMessage); + } + }, + formatJID(jid) { + return jid ? jid.split('@')[0] : ''; + }, + closeRequestedMembersModal() { + $('#modalRequestedMembers').modal('hide'); + // open modal again + this.openModal(); + }, + async handleProcessRequest(member, action) { + if (!this.selectedGroupId || !member) return; + + const actionText = action === 'approve' ? 'approve' : 'reject'; + const confirmMsg = `Are you sure you want to ${actionText} this member request?`; + const ok = confirm(confirmMsg); + if (!ok) return; + + try { + this.processingMember = member.jid; + + const payload = { + group_id: this.selectedGroupId, + participants: [this.formatJID(member.jid)] + }; + + await window.http.post(`/group/participant-requests/${action}`, payload); + + // Remove the processed member from the list + this.requestedMembers = this.requestedMembers.filter(m => m.jid !== member.jid); + + showSuccessInfo(`Member request ${actionText}d`); + this.processingMember = null; + } catch (error) { + this.processingMember = null; + let errorMessage = `Failed to ${actionText} member request`; + if (error.response) { + errorMessage = error.response.data.message || errorMessage; + } + showErrorInfo(errorMessage); + } } }, template: ` @@ -81,7 +156,7 @@ export default {
-
{{ g.Participants.length }} {{ formatDate(g.GroupCreated) }} - +
+ + +
+ + + ` } \ No newline at end of file diff --git a/src/views/index.html b/src/views/index.html index b79dcc4..7635eea 100644 --- a/src/views/index.html +++ b/src/views/index.html @@ -136,7 +136,7 @@
- + @@ -218,12 +218,12 @@ import GroupCreate from "./components/GroupCreate.js"; import GroupJoinWithLink from "./components/GroupJoinWithLink.js"; import GroupAddParticipants from "./components/GroupManageParticipants.js"; + import NewsletterList from "./components/NewsletterList.js"; import AccountAvatar from "./components/AccountAvatar.js"; import AccountChangeAvatar from "./components/AccountChangeAvatar.js"; import AccountChangePushName from "./components/AccountChangePushName.js"; import AccountUserInfo from "./components/AccountUserInfo.js"; import AccountPrivacy from "./components/AccountPrivacy.js"; - import NewsletterList from "./components/NewsletterList.js"; import AccountContact from "./components/AccountContact.js"; const showErrorInfo = (message) => { From adb0746c44982575464d9b35f66d8b0b8bba0aa5 Mon Sep 17 00:00:00 2001 From: Aldino Kemal Date: Fri, 9 May 2025 21:36:54 +0700 Subject: [PATCH 15/15] feat: update proto --- src/config/settings.go | 2 +- src/go.mod | 20 ++++++++++---------- src/go.sum | 20 ++++++++++++++++++++ 3 files changed, 31 insertions(+), 11 deletions(-) diff --git a/src/config/settings.go b/src/config/settings.go index 7815f1d..e5aaa6f 100644 --- a/src/config/settings.go +++ b/src/config/settings.go @@ -5,7 +5,7 @@ import ( ) var ( - AppVersion = "v5.6.0" + AppVersion = "v5.6.1" AppPort = "3000" AppDebug = false AppOs = "AldinoKemal" diff --git a/src/go.mod b/src/go.mod index dda8e1a..9cd323f 100644 --- a/src/go.mod +++ b/src/go.mod @@ -18,10 +18,10 @@ require ( github.com/spf13/cobra v1.9.1 github.com/spf13/viper v1.20.1 github.com/stretchr/testify v1.10.0 - github.com/valyala/fasthttp v1.60.0 + github.com/valyala/fasthttp v1.62.0 go.mau.fi/libsignal v0.1.2 - go.mau.fi/whatsmeow v0.0.0-20250417131650-164ddf482526 - golang.org/x/image v0.26.0 + go.mau.fi/whatsmeow v0.0.0-20250501130609-4c93ee4e6efa + golang.org/x/image v0.27.0 google.golang.org/protobuf v1.36.6 ) @@ -43,7 +43,7 @@ require ( github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect - github.com/petermattis/goid v0.0.0-20250319124200-ccd6737f222a // indirect + github.com/petermattis/goid v0.0.0-20250508124226-395b08cebbdb // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/rs/zerolog v1.34.0 // indirect @@ -51,16 +51,16 @@ require ( github.com/savsgio/gotils v0.0.0-20250408102913-196191ec6287 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.14.0 // indirect - github.com/spf13/cast v1.7.1 // indirect + github.com/spf13/cast v1.8.0 // indirect github.com/spf13/pflag v1.0.6 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect go.mau.fi/util v0.8.6 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/crypto v0.37.0 // indirect - golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect - golang.org/x/net v0.39.0 // indirect - golang.org/x/sys v0.32.0 // indirect - golang.org/x/text v0.24.0 // indirect + golang.org/x/crypto v0.38.0 // indirect + golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 // indirect + golang.org/x/net v0.40.0 // indirect + golang.org/x/sys v0.33.0 // indirect + golang.org/x/text v0.25.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/src/go.sum b/src/go.sum index b2b1136..7693be3 100644 --- a/src/go.sum +++ b/src/go.sum @@ -81,6 +81,8 @@ github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0 github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/petermattis/goid v0.0.0-20250319124200-ccd6737f222a h1:S+AGcmAESQ0pXCUNnRH7V+bOUIgkSX5qVt2cNKCrm0Q= github.com/petermattis/goid v0.0.0-20250319124200-ccd6737f222a/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= +github.com/petermattis/goid v0.0.0-20250508124226-395b08cebbdb h1:3PrKuO92dUTMrQ9dx0YNejC6U/Si6jqKmyQ9vWjwqR4= +github.com/petermattis/goid v0.0.0-20250508124226-395b08cebbdb/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= @@ -112,6 +114,8 @@ github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA= github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo= github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/cast v1.8.0 h1:gEN9K4b8Xws4EX0+a0reLmhq8moKn7ntRlQYgjPeCDk= +github.com/spf13/cast v1.8.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= @@ -133,6 +137,8 @@ github.com/valyala/fasthttp v1.59.0 h1:Qu0qYHfXvPk1mSLNqcFtEk6DpxgA26hy6bmydotDp github.com/valyala/fasthttp v1.59.0/go.mod h1:GTxNb9Bc6r2a9D0TWNSPwDz78UxnTGBViY3xZNEqyYU= github.com/valyala/fasthttp v1.60.0 h1:kBRYS0lOhVJ6V+bYN8PqAHELKHtXqwq9zNMLKx1MBsw= github.com/valyala/fasthttp v1.60.0/go.mod h1:iY4kDgV3Gc6EqhRZ8icqcmlG6bqhcDXfuHgTO4FXCvc= +github.com/valyala/fasthttp v1.62.0 h1:8dKRBX/y2rCzyc6903Zu1+3qN0H/d2MsxPPmVNamiH0= +github.com/valyala/fasthttp v1.62.0/go.mod h1:FCINgr4GKdKqV8Q0xv8b+UxPV+H/O5nNFo3D+r54Htg= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= @@ -146,6 +152,8 @@ go.mau.fi/whatsmeow v0.0.0-20250402091807-b0caa1b76088 h1:ns6nk2NjqdaQnCKrp+Qqwp go.mau.fi/whatsmeow v0.0.0-20250402091807-b0caa1b76088/go.mod h1:WNhj4JeQ6YR6dUOEiCXKqmE4LavSFkwRoKmu4atRrRs= go.mau.fi/whatsmeow v0.0.0-20250417131650-164ddf482526 h1:i9w16FdM3zmOWdF5nh1l2MlmE/wK7ulL6rbT02WBBJs= go.mau.fi/whatsmeow v0.0.0-20250417131650-164ddf482526/go.mod h1:NlPtoLdpX3RnltqCTCZQ6kIUfprqLirtSK1gHvwoNx0= +go.mau.fi/whatsmeow v0.0.0-20250501130609-4c93ee4e6efa h1:+bQKfMtnhX2jVoCSaneH4Ctk51IVT1K2gvjyqfFjVW0= +go.mau.fi/whatsmeow v0.0.0-20250501130609-4c93ee4e6efa/go.mod h1:NlPtoLdpX3RnltqCTCZQ6kIUfprqLirtSK1gHvwoNx0= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -158,13 +166,19 @@ golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= +golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= +golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM= golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8= +golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 h1:y5zboxd6LQAqYIhHnB48p0ByQ/GnQx2BE33L8BOHQkI= +golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ= golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ= golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs= golang.org/x/image v0.26.0 h1:4XjIFEZWQmCZi6Wv8BoxsDhRU3RVnLX04dToTDAEPlY= golang.org/x/image v0.26.0/go.mod h1:lcxbMFAovzpnJxzXS3nyL83K27tmqtKzIJpctK8YO5c= +golang.org/x/image v0.27.0 h1:C8gA4oWU/tKkdCfYT6T2u4faJu3MeNS5O8UPWlPF61w= +golang.org/x/image v0.27.0/go.mod h1:xbdrClrAUway1MUTEZDq9mz/UpRwYAkFFNUslZtcB+g= 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= @@ -185,6 +199,8 @@ golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= +golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= +golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -210,6 +226,8 @@ golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 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= @@ -232,6 +250,8 @@ golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= +golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= +golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=