Server qr fixes.
This commit is contained in:
@@ -40,9 +40,8 @@ func NewManager(eventBus *events.EventBus, mediaConfig config.MediaConfig, cfg *
|
||||
// Connect establishes a connection to a WhatsApp account using the appropriate client type
|
||||
func (m *Manager) Connect(ctx context.Context, cfg config.WhatsAppConfig) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
if _, exists := m.clients[cfg.ID]; exists {
|
||||
m.mu.Unlock()
|
||||
return fmt.Errorf("client %s already connected", cfg.ID)
|
||||
}
|
||||
|
||||
@@ -54,21 +53,39 @@ func (m *Manager) Connect(ctx context.Context, cfg config.WhatsAppConfig) error
|
||||
case "business-api":
|
||||
client, err = businessapi.NewClient(cfg, m.eventBus, m.mediaConfig)
|
||||
case "whatsmeow", "":
|
||||
client, err = whatsmeow.NewClient(cfg, m.eventBus, m.mediaConfig)
|
||||
// Create callback for phone number updates
|
||||
onPhoneUpdate := func(accountID, phoneNumber string) {
|
||||
m.updatePhoneNumberInConfig(accountID, phoneNumber)
|
||||
}
|
||||
client, err = whatsmeow.NewClient(cfg, m.eventBus, m.mediaConfig, onPhoneUpdate)
|
||||
default:
|
||||
m.mu.Unlock()
|
||||
return fmt.Errorf("unknown client type: %s", cfg.Type)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
m.mu.Unlock()
|
||||
return fmt.Errorf("failed to create client: %w", err)
|
||||
}
|
||||
|
||||
if err := client.Connect(ctx); err != nil {
|
||||
return fmt.Errorf("failed to connect: %w", err)
|
||||
}
|
||||
|
||||
// Register client immediately so it's available for QR code serving during pairing
|
||||
m.clients[cfg.ID] = client
|
||||
logging.Info("Client connected", "account_id", cfg.ID, "type", client.GetType())
|
||||
m.mu.Unlock()
|
||||
|
||||
// Connect in background (this can block during QR pairing)
|
||||
go func() {
|
||||
if err := client.Connect(ctx); err != nil {
|
||||
logging.Error("Failed to connect client", "account_id", cfg.ID, "error", err)
|
||||
// Remove client if connection fails
|
||||
m.mu.Lock()
|
||||
delete(m.clients, cfg.ID)
|
||||
m.mu.Unlock()
|
||||
m.eventBus.Publish(events.WhatsAppPairFailedEvent(ctx, cfg.ID, err))
|
||||
} else {
|
||||
logging.Info("Client connected", "account_id", cfg.ID, "type", client.GetType())
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -169,3 +186,29 @@ func (m *Manager) GetClient(id string) (Client, bool) {
|
||||
client, exists := m.clients[id]
|
||||
return client, exists
|
||||
}
|
||||
|
||||
// updatePhoneNumberInConfig updates the phone number for an account in config and saves it
|
||||
func (m *Manager) updatePhoneNumberInConfig(accountID, phoneNumber string) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
// Find and update the account in config
|
||||
for i := range m.config.WhatsApp {
|
||||
if m.config.WhatsApp[i].ID == accountID {
|
||||
if m.config.WhatsApp[i].PhoneNumber != phoneNumber {
|
||||
m.config.WhatsApp[i].PhoneNumber = phoneNumber
|
||||
logging.Info("Updated phone number in config", "account_id", accountID, "phone", phoneNumber)
|
||||
|
||||
// Save config if callback is available
|
||||
if m.onConfigUpdate != nil {
|
||||
if err := m.onConfigUpdate(m.config); err != nil {
|
||||
logging.Error("Failed to save config after phone update", "account_id", accountID, "error", err)
|
||||
} else {
|
||||
logging.Debug("Config saved with updated phone number", "account_id", accountID)
|
||||
}
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user