package handlers import ( "context" "encoding/json" "net/http" "git.warky.dev/wdevs/whatshooked/pkg/config" "git.warky.dev/wdevs/whatshooked/pkg/logging" "git.warky.dev/wdevs/whatshooked/pkg/storage" ) type accountRuntimeStatus struct { AccountID string `json:"account_id"` Type string `json:"type"` Status string `json:"status"` Connected bool `json:"connected"` QRAvailable bool `json:"qr_available"` } type accountConfigWithStatus struct { config.WhatsAppConfig Status string `json:"status"` Connected bool `json:"connected"` QRAvailable bool `json:"qr_available"` } func (h *Handlers) getAccountStatusMapFromDB() map[string]accountRuntimeStatus { result := map[string]accountRuntimeStatus{} db := storage.GetDB() if db == nil { return result } type statusRow struct { ID string `bun:"id"` AccountType string `bun:"account_type"` Status string `bun:"status"` } rows := make([]statusRow, 0) err := db.NewSelect(). Table("whatsapp_account"). Column("id", "account_type", "status"). Scan(context.Background(), &rows) if err != nil { logging.Warn("Failed to load account statuses from database", "error", err) return result } for _, row := range rows { accountID := row.ID status := row.Status if status == "" { status = "disconnected" } result[accountID] = accountRuntimeStatus{ AccountID: accountID, Type: row.AccountType, Status: status, Connected: status == "connected", QRAvailable: status == "pairing", } } return result } // Accounts returns the list of all configured WhatsApp accounts func (h *Handlers) Accounts(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") statusByAccountID := h.getAccountStatusMapFromDB() accounts := make([]accountConfigWithStatus, 0, len(h.config.WhatsApp)) for _, account := range h.config.WhatsApp { status := accountRuntimeStatus{ AccountID: account.ID, Type: account.Type, Status: "disconnected", } if account.Disabled { status.Status = "disconnected" status.Connected = false status.QRAvailable = false } else if fromDB, exists := statusByAccountID[account.ID]; exists { status = fromDB } accounts = append(accounts, accountConfigWithStatus{ WhatsAppConfig: account, Status: status.Status, Connected: status.Connected, QRAvailable: status.QRAvailable, }) } writeJSON(w, accounts) } // AccountStatuses returns status values persisted in the database. // GET /api/accounts/status func (h *Handlers) AccountStatuses(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } statusByAccountID := h.getAccountStatusMapFromDB() statuses := make([]accountRuntimeStatus, 0, len(h.config.WhatsApp)) for _, account := range h.config.WhatsApp { status := accountRuntimeStatus{ AccountID: account.ID, Type: account.Type, Status: "disconnected", } if account.Disabled { status.Status = "disconnected" status.Connected = false status.QRAvailable = false } else if fromDB, exists := statusByAccountID[account.ID]; exists { status = fromDB } statuses = append(statuses, status) } writeJSON(w, map[string]interface{}{ "statuses": statuses, }) } // AddAccount adds a new WhatsApp account to the system func (h *Handlers) AddAccount(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } var account config.WhatsAppConfig if err := json.NewDecoder(r.Body).Decode(&account); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } // Connect to the account if err := h.whatsappMgr.Connect(context.Background(), account); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } if db := storage.GetDB(); db != nil { repo := storage.NewWhatsAppAccountRepository(db) if err := repo.UpdateStatus(context.Background(), account.ID, "connecting"); err != nil { logging.Warn("Failed to set account status to connecting", "account_id", account.ID, "error", err) } } // Update config 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 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 } if db := storage.GetDB(); db != nil { repo := storage.NewWhatsAppAccountRepository(db) if err := repo.UpdateStatus(context.Background(), req.ID, "disconnected"); err != nil { logging.Warn("Failed to set account status to disconnected after removal", "account_id", req.ID, "error", err) } } // 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"}) } // DisableAccount disables a WhatsApp account func (h *Handlers) DisableAccount(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 } // Find the account found := false for i := range h.config.WhatsApp { if h.config.WhatsApp[i].ID == req.ID { found = true // Check if already disabled if h.config.WhatsApp[i].Disabled { logging.Info("Account already disabled", "account_id", req.ID) writeJSON(w, map[string]string{"status": "ok", "message": "account already disabled"}) return } // Disconnect the account if err := h.whatsappMgr.Disconnect(req.ID); err != nil { logging.Warn("Failed to disconnect account during disable", "account_id", req.ID, "error", err) // Continue with disabling even if disconnect fails } // Mark as disabled h.config.WhatsApp[i].Disabled = true logging.Info("Account disabled", "account_id", req.ID) if db := storage.GetDB(); db != nil { repo := storage.NewWhatsAppAccountRepository(db) if err := repo.UpdateStatus(context.Background(), req.ID, "disconnected"); err != nil { logging.Warn("Failed to set account status to disconnected", "account_id", req.ID, "error", err) } } break } } if !found { http.Error(w, "Account not found", http.StatusNotFound) return } // Save config if h.configPath != "" { if err := config.Save(h.configPath, h.config); err != nil { logging.Error("Failed to save config after disabling account", "account_id", req.ID, "error", err) http.Error(w, "Failed to save configuration", http.StatusInternalServerError) return } logging.Info("Config saved after disabling account", "account_id", req.ID) } writeJSON(w, map[string]string{"status": "ok", "account_id": req.ID}) } // EnableAccount enables a WhatsApp account func (h *Handlers) EnableAccount(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 } // Find the account var accountConfig *config.WhatsAppConfig for i := range h.config.WhatsApp { if h.config.WhatsApp[i].ID == req.ID { accountConfig = &h.config.WhatsApp[i] break } } if accountConfig == nil { http.Error(w, "Account not found", http.StatusNotFound) return } // Check if already enabled if !accountConfig.Disabled { logging.Info("Account already enabled", "account_id", req.ID) writeJSON(w, map[string]string{"status": "ok", "message": "account already enabled"}) return } // Mark as enabled accountConfig.Disabled = false // Connect the account if err := h.whatsappMgr.Connect(context.Background(), *accountConfig); err != nil { logging.Error("Failed to connect account during enable", "account_id", req.ID, "error", err) // Revert the disabled flag accountConfig.Disabled = true http.Error(w, "Failed to connect account: "+err.Error(), http.StatusInternalServerError) return } if db := storage.GetDB(); db != nil { repo := storage.NewWhatsAppAccountRepository(db) if err := repo.UpdateStatus(context.Background(), req.ID, "connecting"); err != nil { logging.Warn("Failed to set account status to connecting", "account_id", req.ID, "error", err) } } logging.Info("Account enabled and connected", "account_id", req.ID) // Save config if h.configPath != "" { if err := config.Save(h.configPath, h.config); err != nil { logging.Error("Failed to save config after enabling account", "account_id", req.ID, "error", err) http.Error(w, "Failed to save configuration", http.StatusInternalServerError) return } logging.Info("Config saved after enabling account", "account_id", req.ID) } writeJSON(w, map[string]string{"status": "ok", "account_id": req.ID}) } // UpdateAccount updates an existing WhatsApp account configuration func (h *Handlers) UpdateAccount(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost && r.Method != http.MethodPut { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } var updates config.WhatsAppConfig if err := json.NewDecoder(r.Body).Decode(&updates); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } if updates.ID == "" { http.Error(w, "Account ID is required", http.StatusBadRequest) return } // Find the account found := false var oldConfig config.WhatsAppConfig for i := range h.config.WhatsApp { if h.config.WhatsApp[i].ID == updates.ID { found = true oldConfig = h.config.WhatsApp[i] // Update fields (preserve ID and Type) updates.ID = oldConfig.ID if updates.Type == "" { updates.Type = oldConfig.Type } h.config.WhatsApp[i] = updates logging.Info("Account configuration updated", "account_id", updates.ID) break } } if !found { http.Error(w, "Account not found", http.StatusNotFound) return } // Sync updated config to database if db := storage.GetDB(); db != nil { accountRepo := storage.NewWhatsAppAccountRepository(db) cfgJSON := "" if updates.BusinessAPI != nil { if b, err := json.Marshal(updates.BusinessAPI); err == nil { cfgJSON = string(b) } } if err := accountRepo.UpdateConfig(context.Background(), updates.ID, updates.PhoneNumber, cfgJSON, !updates.Disabled); err != nil { logging.Warn("Failed to sync updated account config to database", "account_id", updates.ID, "error", err) } if updates.Disabled { if err := accountRepo.UpdateStatus(context.Background(), updates.ID, "disconnected"); err != nil { logging.Warn("Failed to set account status to disconnected after update", "account_id", updates.ID, "error", err) } } else if oldConfig.Disabled { if err := accountRepo.UpdateStatus(context.Background(), updates.ID, "connecting"); err != nil { logging.Warn("Failed to set account status to connecting after update", "account_id", updates.ID, "error", err) } } } // If the account was enabled and settings changed, reconnect it if !updates.Disabled { // Disconnect old connection if err := h.whatsappMgr.Disconnect(updates.ID); err != nil { logging.Warn("Failed to disconnect account during update", "account_id", updates.ID, "error", err) } // Reconnect with new settings if err := h.whatsappMgr.Connect(context.Background(), updates); err != nil { logging.Error("Failed to reconnect account after update", "account_id", updates.ID, "error", err) http.Error(w, "Failed to reconnect account: "+err.Error(), http.StatusInternalServerError) return } logging.Info("Account reconnected with new settings", "account_id", updates.ID) } // Save config if h.configPath != "" { if err := config.Save(h.configPath, h.config); err != nil { logging.Error("Failed to save config after updating account", "account_id", updates.ID, "error", err) http.Error(w, "Failed to save configuration", http.StatusInternalServerError) return } logging.Info("Config saved after updating account", "account_id", updates.ID) } writeJSON(w, map[string]string{"status": "ok", "account_id": updates.ID}) }