Server qr fixes.
This commit is contained in:
@@ -79,6 +79,10 @@ func (eb *EventBus) SubscribeAll(subscriber Subscriber) {
|
||||
EventWhatsAppDisconnected,
|
||||
EventWhatsAppPairSuccess,
|
||||
EventWhatsAppPairFailed,
|
||||
EventWhatsAppQRCode,
|
||||
EventWhatsAppQRTimeout,
|
||||
EventWhatsAppQRError,
|
||||
EventWhatsAppPairEvent,
|
||||
EventMessageReceived,
|
||||
EventMessageSent,
|
||||
EventMessageFailed,
|
||||
|
||||
@@ -38,10 +38,63 @@ func (h *Handlers) AddAccount(w http.ResponseWriter, r *http.Request) {
|
||||
h.config.WhatsApp = append(h.config.WhatsApp, account)
|
||||
if h.configPath != "" {
|
||||
if err := config.Save(h.configPath, h.config); err != nil {
|
||||
logging.Error("Failed to save config", "error", err)
|
||||
logging.Error("Failed to save config after adding account", "account_id", account.ID, "error", err)
|
||||
} else {
|
||||
logging.Info("Config saved after adding account", "account_id", account.ID)
|
||||
}
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
writeJSON(w, map[string]string{"status": "ok", "account_id": account.ID})
|
||||
}
|
||||
|
||||
// RemoveAccount removes a WhatsApp account from the system
|
||||
func (h *Handlers) RemoveAccount(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
ID string `json:"id"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Disconnect the account
|
||||
if err := h.whatsappMgr.Disconnect(req.ID); err != nil {
|
||||
logging.Warn("Failed to disconnect account during removal", "account_id", req.ID, "error", err)
|
||||
// Continue with removal even if disconnect fails
|
||||
}
|
||||
|
||||
// Remove from config
|
||||
found := false
|
||||
newAccounts := make([]config.WhatsAppConfig, 0)
|
||||
for _, acc := range h.config.WhatsApp {
|
||||
if acc.ID != req.ID {
|
||||
newAccounts = append(newAccounts, acc)
|
||||
} else {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
http.Error(w, "Account not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
h.config.WhatsApp = newAccounts
|
||||
|
||||
// Save config
|
||||
if h.configPath != "" {
|
||||
if err := config.Save(h.configPath, h.config); err != nil {
|
||||
logging.Error("Failed to save config after removing account", "account_id", req.ID, "error", err)
|
||||
} else {
|
||||
logging.Info("Config saved after removing account", "account_id", req.ID)
|
||||
}
|
||||
}
|
||||
|
||||
writeJSON(w, map[string]string{"status": "ok"})
|
||||
}
|
||||
|
||||
@@ -34,12 +34,14 @@ func (h *Handlers) AddHook(w http.ResponseWriter, r *http.Request) {
|
||||
h.config.Hooks = h.hookMgr.ListHooks()
|
||||
if h.configPath != "" {
|
||||
if err := config.Save(h.configPath, h.config); err != nil {
|
||||
logging.Error("Failed to save config", "error", err)
|
||||
logging.Error("Failed to save config after adding hook", "hook_id", hook.ID, "error", err)
|
||||
} else {
|
||||
logging.Info("Config saved after adding hook", "hook_id", hook.ID)
|
||||
}
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
writeJSON(w, map[string]string{"status": "ok"})
|
||||
writeJSON(w, map[string]string{"status": "ok", "hook_id": hook.ID})
|
||||
}
|
||||
|
||||
// RemoveHook removes a hook from the system
|
||||
@@ -66,7 +68,9 @@ func (h *Handlers) RemoveHook(w http.ResponseWriter, r *http.Request) {
|
||||
h.config.Hooks = h.hookMgr.ListHooks()
|
||||
if h.configPath != "" {
|
||||
if err := config.Save(h.configPath, h.config); err != nil {
|
||||
logging.Error("Failed to save config", "error", err)
|
||||
logging.Error("Failed to save config after removing hook", "hook_id", req.ID, "error", err)
|
||||
} else {
|
||||
logging.Info("Config saved after removing hook", "hook_id", req.ID)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
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)
|
||||
}
|
||||
@@ -64,6 +65,7 @@ func NewClient(cfg config.WhatsAppConfig, eventBus *events.EventBus, mediaConfig
|
||||
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
|
||||
}
|
||||
|
||||
@@ -215,6 +215,7 @@ func (s *Server) setupRoutes() *http.ServeMux {
|
||||
// Account management (with auth)
|
||||
mux.HandleFunc("/api/accounts", h.Auth(h.Accounts))
|
||||
mux.HandleFunc("/api/accounts/add", h.Auth(h.AddAccount))
|
||||
mux.HandleFunc("/api/accounts/remove", h.Auth(h.RemoveAccount))
|
||||
|
||||
// Send messages (with auth)
|
||||
mux.HandleFunc("/api/send", h.Auth(h.SendMessage))
|
||||
|
||||
Reference in New Issue
Block a user