Server qr fixes.
Some checks failed
CI / Test (1.23) (push) Failing after -25m23s
CI / Test (1.22) (push) Failing after -25m21s
CI / Build (push) Failing after -25m59s
CI / Lint (push) Successful in -25m51s

This commit is contained in:
2025-12-29 22:44:10 +02:00
parent 94fc899bab
commit fd2527219e
7 changed files with 202 additions and 76 deletions

View File

@@ -1,14 +1,11 @@
package whatsmeow
import (
"bytes"
"context"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"fmt"
"image"
"image/png"
"os"
"path/filepath"
"sync"
@@ -20,7 +17,9 @@ import (
qrterminal "github.com/mdp/qrterminal/v3"
"go.mau.fi/whatsmeow"
"go.mau.fi/whatsmeow/proto/waCompanionReg"
"go.mau.fi/whatsmeow/proto/waE2E"
"go.mau.fi/whatsmeow/store"
"go.mau.fi/whatsmeow/store/sqlstore"
"go.mau.fi/whatsmeow/types"
waEvents "go.mau.fi/whatsmeow/types/events"
@@ -43,11 +42,13 @@ type Client struct {
showQR bool
keepAliveCancel context.CancelFunc
qrCode string
qrCodePNG []byte // Cached PNG data
qrCodeMutex sync.RWMutex
onPhoneUpdate func(accountID, phoneNumber string) // Callback when phone number is updated
}
// NewClient creates a new whatsmeow client
func NewClient(cfg config.WhatsAppConfig, eventBus *events.EventBus, mediaConfig config.MediaConfig) (*Client, error) {
func NewClient(cfg config.WhatsAppConfig, eventBus *events.EventBus, mediaConfig config.MediaConfig, onPhoneUpdate func(accountID, phoneNumber string)) (*Client, error) {
if cfg.Type != "whatsmeow" && cfg.Type != "" {
return nil, fmt.Errorf("invalid client type for whatsmeow: %s", cfg.Type)
}
@@ -58,12 +59,13 @@ func NewClient(cfg config.WhatsAppConfig, eventBus *events.EventBus, mediaConfig
}
return &Client{
id: cfg.ID,
phoneNumber: cfg.PhoneNumber,
sessionPath: sessionPath,
eventBus: eventBus,
mediaConfig: mediaConfig,
showQR: cfg.ShowQR,
id: cfg.ID,
phoneNumber: cfg.PhoneNumber,
sessionPath: sessionPath,
eventBus: eventBus,
mediaConfig: mediaConfig,
showQR: cfg.ShowQR,
onPhoneUpdate: onPhoneUpdate,
}, nil
}
@@ -73,6 +75,10 @@ func (c *Client) Connect(ctx context.Context) error {
if err := os.MkdirAll(c.sessionPath, 0700); err != nil {
return fmt.Errorf("failed to create session directory: %w", err)
}
// store.SetOSInfo("Linux", store.GetWAVersion())
store.DeviceProps.PlatformType = waCompanionReg.DeviceProps_CLOUD_API.Enum()
store.DeviceProps.Os = proto.String("whatshooked.warky.dev")
store.DeviceProps.RequireFullSync = proto.Bool(false)
// Create database container for session storage
dbPath := filepath.Join(c.sessionPath, "session.db")
@@ -89,9 +95,18 @@ func (c *Client) Connect(ctx context.Context) error {
return fmt.Errorf("failed to get device: %w", err)
}
// Set custom client information
deviceStore.Platform = "WhatsHooked"
deviceStore.BusinessName = "git.warky.dev/wdevs/whatshooked"
// Set custom client information that will be shown in WhatsApp
deviceStore.Platform = "git.warky.dev/wdevs/whatshooked"
// Set PushName BEFORE pairing - this is what shows up in WhatsApp linked devices
if deviceStore.PushName == "" {
deviceStore.PushName = fmt.Sprintf("WhatsHooked %s", c.phoneNumber)
}
// Save device store to persist device info before pairing
if err := deviceStore.Save(ctx); err != nil {
logging.Warn("Failed to save device store", "account_id", c.id, "error", err)
}
// Create client
clientLog := waLog.Stdout("Client", "ERROR", true)
@@ -118,11 +133,21 @@ func (c *Client) Connect(ctx context.Context) error {
case "code":
logging.Info("QR code received for pairing", "account_id", c.id)
// Store QR code
// Generate PNG (this regenerates on each new QR code)
pngData, err := c.generateQRCodePNG(evt.Code)
if err != nil {
logging.Error("Failed to generate QR code PNG", "account_id", c.id, "error", err)
}
// Store QR code and PNG (updates cached version)
c.qrCodeMutex.Lock()
c.qrCode = evt.Code
c.qrCodePNG = pngData
qrCodeSize := len(pngData)
c.qrCodeMutex.Unlock()
logging.Debug("QR code PNG updated", "account_id", c.id, "size_bytes", qrCodeSize)
// Generate QR code URL
qrURL := c.generateQRCodeURL()
@@ -143,10 +168,24 @@ func (c *Client) Connect(ctx context.Context) error {
case "success":
logging.Info("Pairing successful", "account_id", c.id, "phone", c.phoneNumber)
// Clear cached QR code after successful pairing
c.qrCodeMutex.Lock()
c.qrCode = ""
c.qrCodePNG = nil
c.qrCodeMutex.Unlock()
c.eventBus.Publish(events.WhatsAppPairSuccessEvent(ctx, c.id))
case "timeout":
logging.Warn("QR code timeout", "account_id", c.id)
// Clear cached QR code on timeout
c.qrCodeMutex.Lock()
c.qrCode = ""
c.qrCodePNG = nil
c.qrCodeMutex.Unlock()
c.eventBus.Publish(events.WhatsAppQRTimeoutEvent(ctx, c.id))
case "error":
@@ -167,12 +206,7 @@ func (c *Client) Connect(ctx context.Context) error {
}
}
if deviceStore.PushName == "" {
deviceStore.PushName = fmt.Sprintf("WhatsHooked %s", c.phoneNumber)
if err := deviceStore.Save(ctx); err != nil {
logging.Error("failed to save device store", "account_id", c.id)
}
}
// PushName is already set before pairing, no need to set it again here
if client.IsConnected() {
err := client.SendPresence(ctx, types.PresenceAvailable)
@@ -521,6 +555,11 @@ func (c *Client) handleEvent(evt interface{}) {
if c.phoneNumber != phoneNumber {
c.phoneNumber = phoneNumber
logging.Info("Updated phone number from WhatsApp", "account_id", c.id, "phone", phoneNumber)
// Trigger config update callback to persist the phone number
if c.onPhoneUpdate != nil {
c.onPhoneUpdate(c.id, phoneNumber)
}
}
} else if c.phoneNumber != "" {
phoneNumber = c.phoneNumber
@@ -703,49 +742,31 @@ func (c *Client) generateQRCodeURL() string {
return fmt.Sprintf("%s/api/qr/%s", baseURL, c.id)
}
// GetQRCodePNG generates a PNG image of the current QR code
// GetQRCodePNG returns the cached PNG image of the current QR code
func (c *Client) GetQRCodePNG() ([]byte, error) {
c.qrCodeMutex.RLock()
qrCodeData := c.qrCode
c.qrCodeMutex.RUnlock()
defer c.qrCodeMutex.RUnlock()
if qrCodeData == "" {
if c.qrCode == "" {
return nil, fmt.Errorf("no QR code available")
}
if c.qrCodePNG == nil {
return nil, fmt.Errorf("QR code PNG not yet generated")
}
return c.qrCodePNG, nil
}
// generateQRCodePNG generates a PNG image from QR code data
func (c *Client) generateQRCodePNG(qrCodeData string) ([]byte, error) {
// Generate QR code using rsc.io/qr
code, err := qr.Encode(qrCodeData, qr.L)
code, err := qr.Encode(qrCodeData, qr.H)
if err != nil {
return nil, fmt.Errorf("failed to encode QR code: %w", err)
}
// Scale the QR code for better visibility (8x scale)
img := code.Image()
scale := 8
bounds := img.Bounds()
scaledWidth := bounds.Dx() * scale
scaledHeight := bounds.Dy() * scale
// Create a new image with scaled dimensions
scaledImg := image.NewRGBA(image.Rect(0, 0, scaledWidth, scaledHeight))
// Scale the image
for y := 0; y < bounds.Dy(); y++ {
for x := 0; x < bounds.Dx(); x++ {
pixel := img.At(x, y)
for sy := 0; sy < scale; sy++ {
for sx := 0; sx < scale; sx++ {
scaledImg.Set(x*scale+sx, y*scale+sy, pixel)
}
}
}
}
// Encode to PNG
var buf bytes.Buffer
if err := png.Encode(&buf, scaledImg); err != nil {
return nil, fmt.Errorf("failed to encode PNG: %w", err)
}
return buf.Bytes(), nil
// Use the library's built-in PNG method with a scale factor
// Scale of 8 means each QR module is 8x8 pixels
return code.PNG(), nil
}