Browse Source

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
pull/271/head
Aldino Kemal 1 year ago
parent
commit
151961d58d
  1. 2
      src/go.mod
  2. 18
      src/go.sum
  3. 215
      src/pkg/utils/general.go
  4. 22
      src/services/send.go
  5. 138
      src/views/components/SendLink.js
  6. 5
      src/views/index.html

2
src/go.mod

@ -21,6 +21,7 @@ require (
github.com/valyala/fasthttp v1.59.0 github.com/valyala/fasthttp v1.59.0
go.mau.fi/libsignal v0.1.2 go.mau.fi/libsignal v0.1.2
go.mau.fi/whatsmeow v0.0.0-20250305175604-af3dc0346412 go.mau.fi/whatsmeow v0.0.0-20250305175604-af3dc0346412
golang.org/x/image v0.25.0
google.golang.org/protobuf v1.36.5 google.golang.org/protobuf v1.36.5
) )
@ -60,7 +61,6 @@ require (
go.uber.org/multierr v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect
golang.org/x/crypto v0.36.0 // indirect golang.org/x/crypto v0.36.0 // indirect
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // 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/net v0.37.0 // indirect
golang.org/x/sys v0.31.0 // indirect golang.org/x/sys v0.31.0 // indirect
golang.org/x/text v0.23.0 // indirect golang.org/x/text v0.23.0 // indirect

18
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/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 h1:PwCAAtcfK0XxZ4sdErJyfBMkTEWoQU33aB7QqDDzQRI=
go.mau.fi/util v0.8.5/go.mod h1:Ycug9mrbztlahHPEJ6H5r8Nu/xqZaWbE5vPHVWmfz6M= 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 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-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 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.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.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= 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 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= 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 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/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.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 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ=
golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs= 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.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.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= 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.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 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= 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-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.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.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.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 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 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/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.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.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.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 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= 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-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=

215
src/pkg/utils/general.go

@ -4,14 +4,12 @@ import (
"bytes" "bytes"
"fmt" "fmt"
"image" "image"
"image/color"
"image/draw"
_ "image/gif" // Register GIF format _ "image/gif" // Register GIF format
"image/jpeg" // For JPEG encoding
"image/png" // For PNG encoding
_ "image/jpeg" // For JPEG encoding
_ "image/png" // For PNG encoding
"io" "io"
"log"
"net/http" "net/http"
"net/url"
"os" "os"
"path/filepath" "path/filepath"
"regexp" "regexp"
@ -21,6 +19,8 @@ import (
"github.com/PuerkitoBio/goquery" "github.com/PuerkitoBio/goquery"
"github.com/aldinokemal/go-whatsapp-web-multidevice/config" "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 // RemoveFile is removing file with delay
@ -80,14 +80,35 @@ type Metadata struct {
Width *uint32 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 // Send an HTTP GET request to the website
response, err := http.Get(url)
response, err := client.Get(urlStr)
if err != nil { if err != nil {
return meta, err return meta, err
} }
defer response.Body.Close() 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 // Parse the HTML document
document, err := goquery.NewDocumentFromReader(response.Body) document, err := goquery.NewDocumentFromReader(response.Body)
if err != nil { if err != nil {
@ -98,127 +119,84 @@ func GetMetaDataFromURL(url string) (meta Metadata, err error) {
meta.Description, _ = element.Attr("content") meta.Description, _ = element.Attr("content")
}) })
// find title
// 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) { document.Find("title").Each(func(index int, element *goquery.Selection) {
meta.Title = element.Text() 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) { 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 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 og:image is found, download it and store its content in ImageThumb
// If an image URL is found, resolve it if it's relative
if meta.Image != "" { if meta.Image != "" {
imageResponse, err := http.Get(meta.Image)
imgURL, err := url.Parse(meta.Image)
if err != nil {
logrus.Warnf("Invalid image URL: %v", err)
} else {
// Resolve relative URLs against the base URL
meta.Image = baseURL.ResolveReference(imgURL).String()
}
// Download the image
imgResponse, err := client.Get(meta.Image)
if err != nil { if err != nil {
log.Printf("Failed to download image: %v", err)
logrus.Warnf("Failed to download image: %v", err)
} else { } else {
defer imageResponse.Body.Close()
defer imgResponse.Body.Close()
// Read image data
imageData, err := io.ReadAll(imageResponse.Body)
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 { if err != nil {
log.Printf("Failed to read image data: %v", err)
logrus.Warnf("Failed to read image data: %v", err)
} else if len(imageData) == 0 {
logrus.Warn("Downloaded image data is empty")
} else { } else {
meta.ImageThumb = imageData meta.ImageThumb = imageData
// Get image dimensions from the actual image rather than OG tags
// Validate image by decoding it
imageReader := bytes.NewReader(imageData) imageReader := bytes.NewReader(imageData)
img, imgFormat, err := image.Decode(imageReader)
if err == nil {
img, _, err := image.Decode(imageReader)
if err != nil {
logrus.Warnf("Failed to decode image: %v", err)
} else {
bounds := img.Bounds() bounds := img.Bounds()
width := uint32(bounds.Max.X - bounds.Min.X) width := uint32(bounds.Max.X - bounds.Min.X)
height := uint32(bounds.Max.Y - bounds.Min.Y) 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) // Check if image is square (1:1 ratio)
if width == height && width <= 200 { if width == height && width <= 200 {
// For 1:1 ratio, leave width and height as nil
// For small square images, leave width and height as nil
meta.Width = nil meta.Width = nil
meta.Height = nil meta.Height = nil
} else { } else {
@ -226,31 +204,8 @@ func GetMetaDataFromURL(url string) (meta Metadata, err error) {
meta.Height = &height 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
logrus.Debugf("Image dimensions: %dx%d", width, height)
} }
})
// 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
} }
} }
} }

22
src/services/send.go

@ -430,38 +430,38 @@ func (service serviceSend) SendLink(ctx context.Context, request domainSend.Link
return response, err return response, err
} }
getMetaDataFromURL, err := utils.GetMetaDataFromURL(request.Link)
metadata, err := utils.GetMetaDataFromURL(request.Link)
if err != nil { if err != nil {
return response, err return response, err
} }
// Log image dimensions if available, otherwise note it's a square image or dimensions not available // 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 { } else {
fmt.Println("Image dimensions: Square image or dimensions not available")
logrus.Debugf("Image dimensions: Square image or dimensions not available")
} }
// Create the message // Create the message
msg := &waE2E.Message{ExtendedTextMessage: &waE2E.ExtendedTextMessage{ msg := &waE2E.Message{ExtendedTextMessage: &waE2E.ExtendedTextMessage{
Text: proto.String(fmt.Sprintf("%s\n%s", request.Caption, request.Link)), 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), 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 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 { if err == nil {
// Update the message with the uploaded thumbnail information // Update the message with the uploaded thumbnail information
msg.ExtendedTextMessage.ThumbnailDirectPath = proto.String(uploadedThumb.DirectPath) msg.ExtendedTextMessage.ThumbnailDirectPath = proto.String(uploadedThumb.DirectPath)
msg.ExtendedTextMessage.ThumbnailSHA256 = uploadedThumb.FileSHA256 msg.ExtendedTextMessage.ThumbnailSHA256 = uploadedThumb.FileSHA256
msg.ExtendedTextMessage.ThumbnailEncSHA256 = uploadedThumb.FileEncSHA256 msg.ExtendedTextMessage.ThumbnailEncSHA256 = uploadedThumb.FileEncSHA256
msg.ExtendedTextMessage.MediaKey = uploadedThumb.MediaKey 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 { } else {
logrus.Warnf("Failed to upload thumbnail: %v, continue without uploaded thumbnail", err) logrus.Warnf("Failed to upload thumbnail: %v, continue without uploaded thumbnail", err)
} }

138
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: `
<div class="blue card" @click="openModal()" style="cursor: pointer">
<div class="content">
<a class="ui blue right ribbon label">Send</a>
<div class="header">Send Link</div>
<div class="description">
Send link to user or group
</div>
</div>
</div>
<!-- Modal SendLink -->
<div class="ui small modal" id="modalSendLink">
<i class="close icon"></i>
<div class="header">
Send Link
</div>
<div class="content">
<form class="ui form">
<FormRecipient v-model:type="type" v-model:phone="phone" :show-status="true"/>
<div class="field" v-if="isShowReplyId()">
<label>Reply Message ID</label>
<input v-model="reply_message_id" type="text"
placeholder="Optional: 57D29F74B7FC62F57D8AC2C840279B5B/3EB0288F008D32FCD0A424"
aria-label="reply_message_id">
</div>
<div class="field">
<label>Link</label>
<input v-model="link" type="text" placeholder="https://www.google.com"
aria-label="link">
</div>
<div class="field">
<label>Caption</label>
<textarea v-model="caption" placeholder="Hello this is caption"
aria-label="caption"></textarea>
</div>
</form>
</div>
<div class="actions">
<button class="ui approve positive right labeled icon button"
:class="{'disabled': !isValidForm() || loading}"
@click.prevent="handleSubmit">
Send
<i class="send icon"></i>
</button>
</div>
</div>
`
}

5
src/views/index.html

@ -116,6 +116,8 @@
<send-audio></send-audio> <send-audio></send-audio>
<send-poll></send-poll> <send-poll></send-poll>
<send-presence></send-presence> <send-presence></send-presence>
<send-link></send-link>
</div> </div>
<div class="ui horizontal divider"> <div class="ui horizontal divider">
@ -201,6 +203,7 @@
import SendImage from "./components/SendImage.js"; import SendImage from "./components/SendImage.js";
import SendFile from "./components/SendFile.js"; import SendFile from "./components/SendFile.js";
import SendVideo from "./components/SendVideo.js"; import SendVideo from "./components/SendVideo.js";
import SendLink from "./components/SendLink.js";
import SendContact from "./components/SendContact.js"; import SendContact from "./components/SendContact.js";
import SendLocation from "./components/SendLocation.js"; import SendLocation from "./components/SendLocation.js";
import SendAudio from "./components/SendAudio.js"; import SendAudio from "./components/SendAudio.js";
@ -254,7 +257,7 @@
Vue.createApp({ Vue.createApp({
components: { components: {
AppLogin, AppLoginWithCode, AppLogout, AppReconnect, 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, MessageDelete, MessageUpdate, MessageReact, MessageRevoke,
GroupList, GroupCreate, GroupJoinWithLink, GroupAddParticipants, GroupList, GroupCreate, GroupJoinWithLink, GroupAddParticipants,
NewsletterList, NewsletterList,

Loading…
Cancel
Save