diff --git a/.gitignore b/.gitignore index 15d3a08..3889688 100644 --- a/.gitignore +++ b/.gitignore @@ -47,3 +47,6 @@ sessions/ .DS_Store Thumbs.db /server + +# Web directory (files are embedded in pkg/handlers/static/) +web/ diff --git a/ACCOUNT_MANAGEMENT.md b/ACCOUNT_MANAGEMENT.md new file mode 100644 index 0000000..1283f91 --- /dev/null +++ b/ACCOUNT_MANAGEMENT.md @@ -0,0 +1,430 @@ +# Account Management API + +This document describes the API endpoints for managing WhatsApp accounts in WhatsHooked. + +## Authentication + +All account management endpoints require authentication using one of the following methods: + +- **API Key**: `X-API-Key: your-api-key-here` +- **Basic Auth**: `Authorization: Basic ` + +## Endpoints + +### 1. List All Accounts + +Get a list of all configured WhatsApp accounts. + +**Endpoint:** `GET /api/accounts` + +**Response:** +```json +[ + { + "id": "business", + "type": "business-api", + "phone_number": "+27663602295", + "disabled": false, + "business_api": { + "phone_number_id": "889966060876308", + "access_token": "...", + "business_account_id": "1602055757491196", + "api_version": "v21.0", + "verify_token": "..." + } + } +] +``` + +--- + +### 2. Add Account + +Add a new WhatsApp account to the system. + +**Endpoint:** `POST /api/accounts/add` + +**Request Body (WhatsApp Web/WhatsMe ow):** +```json +{ + "id": "my-account", + "type": "whatsmeow", + "phone_number": "+1234567890", + "session_path": "./sessions/my-account", + "show_qr": true, + "disabled": false +} +``` + +**Request Body (Business API):** +```json +{ + "id": "business-account", + "type": "business-api", + "phone_number": "+1234567890", + "disabled": false, + "business_api": { + "phone_number_id": "123456789", + "access_token": "YOUR_ACCESS_TOKEN", + "business_account_id": "987654321", + "api_version": "v21.0", + "verify_token": "your-verify-token" + } +} +``` + +**Response:** +```json +{ + "status": "ok", + "account_id": "my-account" +} +``` + +**Status Codes:** +- `201 Created` - Account added successfully +- `400 Bad Request` - Invalid request body +- `500 Internal Server Error` - Failed to connect or save config + +--- + +### 3. Update Account + +Update an existing WhatsApp account configuration. + +**Endpoint:** `POST /api/accounts/update` or `PUT /api/accounts/update` + +**Request Body:** +```json +{ + "id": "business-account", + "type": "business-api", + "phone_number": "+1234567890", + "disabled": false, + "business_api": { + "phone_number_id": "123456789", + "access_token": "NEW_ACCESS_TOKEN", + "business_account_id": "987654321", + "api_version": "v21.0", + "verify_token": "new-verify-token" + } +} +``` + +**Response:** +```json +{ + "status": "ok", + "account_id": "business-account" +} +``` + +**Notes:** +- The `id` field is required to identify which account to update +- The `type` field cannot be changed (preserved from original) +- If the account is enabled, it will be disconnected and reconnected with new settings +- Configuration is saved to disk after successful update + +**Status Codes:** +- `200 OK` - Account updated successfully +- `400 Bad Request` - Invalid request or missing ID +- `404 Not Found` - Account not found +- `500 Internal Server Error` - Failed to reconnect or save config + +--- + +### 4. Disable Account + +Disable a WhatsApp account (disconnect and prevent auto-connect on restart). + +**Endpoint:** `POST /api/accounts/disable` + +**Request Body:** +```json +{ + "id": "my-account" +} +``` + +**Response:** +```json +{ + "status": "ok", + "account_id": "my-account" +} +``` + +**Behavior:** +- Disconnects the account immediately +- Sets `disabled: true` in config +- Account will not auto-connect on server restart +- Account remains in config (can be re-enabled) + +**Status Codes:** +- `200 OK` - Account disabled successfully +- `400 Bad Request` - Invalid request body +- `404 Not Found` - Account not found +- `500 Internal Server Error` - Failed to save config + +--- + +### 5. Enable Account + +Enable a previously disabled WhatsApp account. + +**Endpoint:** `POST /api/accounts/enable` + +**Request Body:** +```json +{ + "id": "my-account" +} +``` + +**Response:** +```json +{ + "status": "ok", + "account_id": "my-account" +} +``` + +**Behavior:** +- Sets `disabled: false` in config +- Connects the account immediately +- Account will auto-connect on server restart +- If connection fails, account remains disabled + +**Status Codes:** +- `200 OK` - Account enabled and connected successfully +- `400 Bad Request` - Invalid request body +- `404 Not Found` - Account not found +- `500 Internal Server Error` - Failed to connect or save config + +--- + +### 6. Remove Account + +Permanently remove a WhatsApp account from the system. + +**Endpoint:** `POST /api/accounts/remove` or `DELETE /api/accounts/remove` + +**Request Body:** +```json +{ + "id": "my-account" +} +``` + +**Response:** +```json +{ + "status": "ok" +} +``` + +**Behavior:** +- Disconnects the account +- Removes from config permanently +- Session data is NOT deleted (manual cleanup required) + +**Status Codes:** +- `200 OK` - Account removed successfully +- `400 Bad Request` - Invalid request body +- `404 Not Found` - Account not found +- `500 Internal Server Error` - Failed to save config + +--- + +## Configuration File + +Account settings are stored in `config.json`: + +```json +{ + "whatsapp": [ + { + "id": "business", + "type": "business-api", + "phone_number": "+27663602295", + "disabled": false, + "business_api": { + "phone_number_id": "889966060876308", + "access_token": "...", + "business_account_id": "1602055757491196", + "api_version": "v21.0", + "verify_token": "..." + } + }, + { + "id": "personal", + "type": "whatsmeow", + "phone_number": "+1234567890", + "session_path": "./sessions/personal", + "show_qr": true, + "disabled": true + } + ] +} +``` + +### Configuration Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `id` | string | Yes | Unique identifier for the account | +| `type` | string | Yes | Account type: `whatsmeow` or `business-api` | +| `phone_number` | string | Yes | Phone number with country code | +| `disabled` | boolean | No | If `true`, account won't connect (default: `false`) | +| `session_path` | string | No | Session storage path (whatsmeow only) | +| `show_qr` | boolean | No | Display QR code in logs (whatsmeow only) | +| `business_api` | object | Conditional | Required for `business-api` type | + +### Business API Configuration + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `phone_number_id` | string | Yes | WhatsApp Business phone number ID | +| `access_token` | string | Yes | Meta Graph API access token | +| `business_account_id` | string | No | Business account ID | +| `api_version` | string | No | API version (default: `v21.0`) | +| `verify_token` | string | No | Webhook verification token | + +--- + +## Usage Examples + +### cURL Examples + +**List accounts:** +```bash +curl -u username:password http://localhost:8080/api/accounts +``` + +**Add account:** +```bash +curl -u username:password \ + -X POST \ + -H "Content-Type: application/json" \ + -d '{ + "id": "new-account", + "type": "whatsmeow", + "phone_number": "+1234567890", + "session_path": "./sessions/new-account", + "show_qr": true + }' \ + http://localhost:8080/api/accounts/add +``` + +**Update account:** +```bash +curl -u username:password \ + -X POST \ + -H "Content-Type: application/json" \ + -d '{ + "id": "business", + "type": "business-api", + "phone_number": "+27663602295", + "business_api": { + "phone_number_id": "889966060876308", + "access_token": "NEW_TOKEN_HERE", + "api_version": "v21.0" + } + }' \ + http://localhost:8080/api/accounts/update +``` + +**Disable account:** +```bash +curl -u username:password \ + -X POST \ + -H "Content-Type: application/json" \ + -d '{"id": "my-account"}' \ + http://localhost:8080/api/accounts/disable +``` + +**Enable account:** +```bash +curl -u username:password \ + -X POST \ + -H "Content-Type: application/json" \ + -d '{"id": "my-account"}' \ + http://localhost:8080/api/accounts/enable +``` + +**Remove account:** +```bash +curl -u username:password \ + -X POST \ + -H "Content-Type: application/json" \ + -d '{"id": "my-account"}' \ + http://localhost:8080/api/accounts/remove +``` + +--- + +## Server Logs + +When managing accounts, you'll see these log messages: + +```log +INFO Account disabled account_id=business +INFO Config saved after disabling account account_id=business + +INFO Account enabled and connected account_id=business +INFO Config saved after enabling account account_id=business + +INFO Account configuration updated account_id=business +INFO Account reconnected with new settings account_id=business + +INFO Skipping disabled account account_id=business +``` + +--- + +## Best Practices + +1. **Disable vs Remove**: + - Use **disable** for temporary disconnection (maintenance, testing) + - Use **remove** for permanent deletion + +2. **Update Process**: + - Always provide complete configuration when updating + - Account will be reconnected automatically if enabled + +3. **Session Management**: + - WhatsMe ow sessions are stored in `session_path` + - Removing an account doesn't delete session files + - Clean up manually if needed + +4. **Server Restart**: + - Only enabled accounts (`disabled: false`) connect on startup + - Disabled accounts remain in config but stay disconnected + +5. **Configuration Backup**: + - Config is automatically saved after each change + - Keep backups before making bulk changes + - Test changes with one account first + +--- + +## Error Handling + +All endpoints return proper HTTP status codes and JSON error messages: + +```json +{ + "error": "Account not found" +} +``` + +Or plain text for simple errors: +``` +Account ID required in path +``` + +Common error scenarios: +- Account already exists (when adding) +- Account not found (when updating/removing) +- Connection failed (when enabling) +- Configuration save failed (any operation) diff --git a/pkg/config/config.go b/pkg/config/config.go index ca7f64d..f491d6a 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -52,6 +52,7 @@ type WhatsAppConfig struct { PhoneNumber string `json:"phone_number"` SessionPath string `json:"session_path,omitempty"` ShowQR bool `json:"show_qr,omitempty"` + Disabled bool `json:"disabled,omitempty"` // If true, account won't be connected BusinessAPI *BusinessAPIConfig `json:"business_api,omitempty"` } diff --git a/pkg/handlers/accounts.go b/pkg/handlers/accounts.go index d090868..4ea90bc 100644 --- a/pkg/handlers/accounts.go +++ b/pkg/handlers/accounts.go @@ -98,3 +98,197 @@ func (h *Handlers) RemoveAccount(w http.ResponseWriter, r *http.Request) { 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) + 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 + } + + 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 + } + + // 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}) +} diff --git a/pkg/handlers/static.go b/pkg/handlers/static.go new file mode 100644 index 0000000..eeab97a --- /dev/null +++ b/pkg/handlers/static.go @@ -0,0 +1,66 @@ +package handlers + +import ( + "embed" + "net/http" + "path/filepath" + + "git.warky.dev/wdevs/whatshooked/pkg/logging" +) + +//go:embed static/* +var staticFiles embed.FS + +// ServeIndex serves the landing page +func (h *Handlers) ServeIndex(w http.ResponseWriter, r *http.Request) { + // Only serve index on root path + if r.URL.Path != "/" { + http.NotFound(w, r) + return + } + + content, err := staticFiles.ReadFile("static/index.html") + if err != nil { + logging.Error("Failed to read index.html", "error", err) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.WriteHeader(http.StatusOK) + writeBytes(w, content) +} + +// ServeStatic serves static files (logo, etc.) +func (h *Handlers) ServeStatic(w http.ResponseWriter, r *http.Request) { + // Get the file path from URL + filename := filepath.Base(r.URL.Path) + filePath := filepath.Join("static", filename) + + content, err := staticFiles.ReadFile(filePath) + if err != nil { + logging.Error("Failed to read static file", "path", filePath, "error", err) + http.NotFound(w, r) + return + } + + // Set content type based on file extension + ext := filepath.Ext(filePath) + switch ext { + case ".png": + w.Header().Set("Content-Type", "image/png") + case ".jpg", ".jpeg": + w.Header().Set("Content-Type", "image/jpeg") + case ".svg": + w.Header().Set("Content-Type", "image/svg+xml") + case ".css": + w.Header().Set("Content-Type", "text/css") + case ".js": + w.Header().Set("Content-Type", "application/javascript") + } + + // Cache static assets for 1 hour + w.Header().Set("Cache-Control", "public, max-age=3600") + w.WriteHeader(http.StatusOK) + writeBytes(w, content) +} diff --git a/pkg/handlers/static/README.md b/pkg/handlers/static/README.md new file mode 100644 index 0000000..7a52da5 --- /dev/null +++ b/pkg/handlers/static/README.md @@ -0,0 +1,82 @@ +# Static Files + +This directory contains the embedded static files for the WhatsHooked landing page. + +## Files + +- `index.html` - Landing page with API documentation +- `logo.png` - WhatsHooked logo (from `assets/image/whatshooked_tp.png`) + +## How It Works + +These files are embedded into the Go binary using `go:embed` directive in `static.go`. + +When you build the server: +```bash +go build ./cmd/server/ +``` + +The files in this directory are compiled directly into the binary, so the server can run without any external files. + +## Updating the Landing Page + +1. **Edit the HTML:** + ```bash + vim pkg/handlers/static/index.html + ``` + +2. **Rebuild the server:** + ```bash + go build ./cmd/server/ + ``` + +3. **Restart the server:** + ```bash + ./server -config bin/config.json + ``` + +The changes will be embedded in the new binary. + +## Updating the Logo + +1. **Replace the logo:** + ```bash + cp path/to/new-logo.png pkg/handlers/static/logo.png + ``` + +2. **Rebuild:** + ```bash + go build ./cmd/server/ + ``` + +## Routes + +- `GET /` - Serves `index.html` +- `GET /static/logo.png` - Serves `logo.png` +- `GET /static/*` - Serves any file in this directory + +## Development Tips + +- Files are cached with `Cache-Control: public, max-age=3600` (1 hour) +- Force refresh in browser: `Ctrl+Shift+R` or `Cmd+Shift+R` +- Changes require rebuild - no hot reload +- Keep files small - they're embedded in the binary + +## File Structure + +``` +pkg/handlers/ +├── static.go # Handler with go:embed directive +├── static/ +│ ├── index.html # Landing page +│ ├── logo.png # Logo image +│ └── README.md # This file +``` + +## Benefits of Embedded Files + +✅ **Single binary deployment** - No external dependencies +✅ **Fast serving** - Files loaded from memory +✅ **No file system access** - Works in restricted environments +✅ **Portable** - Binary includes everything +✅ **Version controlled** - Static assets tracked with code diff --git a/pkg/handlers/static/index.html b/pkg/handlers/static/index.html new file mode 100644 index 0000000..bb25a69 --- /dev/null +++ b/pkg/handlers/static/index.html @@ -0,0 +1,424 @@ + + + + + + WhatsHooked - WhatsApp Webhook Bridge + + + +
+
+ +

WhatsHooked

+

Bridge your WhatsApp messages to webhooks

+
+ +
+ + Server is running +
+ +
+
+
📱
+
WhatsApp Integration
+
Connect via WhatsApp Web or Business API
+
+
+
🔗
+
Webhook Bridge
+
Forward messages to your custom endpoints
+
+
+
+
Real-time Events
+
Instant message delivery and status updates
+
+
+
💾
+
Message Cache
+
Never lose messages with persistent storage
+
+
+ +
+

Available API Endpoints

+ +
+

📊 Status & Health

+
+ GET + /health +
+
+ +
+

🔌 Webhooks

+
+ GET + /api/hooks +
+
+ POST + /api/hooks/add +
+
+ DELETE + /api/hooks/remove +
+
+ +
+

👤 Accounts

+
+ GET + /api/accounts +
+
+ POST + /api/accounts/add +
+
+ POST + /api/accounts/update +
+
+ POST + /api/accounts/disable +
+
+ POST + /api/accounts/enable +
+
+ DELETE + /api/accounts/remove +
+
+ +
+

💬 Send Messages

+
+ POST + /api/send +
+
+ POST + /api/send/image +
+
+ POST + /api/send/video +
+
+ POST + /api/send/document +
+
+ +
+

💾 Message Cache

+
+ GET + /api/cache +
+
+ GET + /api/cache/stats +
+
+ POST + /api/cache/replay +
+
+ +
+

🔔 WhatsApp Business API

+
+ GET + /webhooks/whatsapp/{account_id} +
+
+ POST + /webhooks/whatsapp/{account_id} +
+
+
+ + +
+ + diff --git a/pkg/handlers/static/logo.png b/pkg/handlers/static/logo.png new file mode 100644 index 0000000..1607a9b Binary files /dev/null and b/pkg/handlers/static/logo.png differ diff --git a/pkg/whatshooked/server.go b/pkg/whatshooked/server.go index 3194854..705903c 100644 --- a/pkg/whatshooked/server.go +++ b/pkg/whatshooked/server.go @@ -204,6 +204,12 @@ func (s *Server) setupRoutes() *http.ServeMux { mux := http.NewServeMux() h := s.wh.Handlers() + // Landing page (no auth required) + mux.HandleFunc("/", h.ServeIndex) + + // Static files (no auth required) + mux.HandleFunc("/static/", h.ServeStatic) + // Health check (no auth required) mux.HandleFunc("/health", h.Health) @@ -215,7 +221,10 @@ 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/update", h.Auth(h.UpdateAccount)) mux.HandleFunc("/api/accounts/remove", h.Auth(h.RemoveAccount)) + mux.HandleFunc("/api/accounts/disable", h.Auth(h.DisableAccount)) + mux.HandleFunc("/api/accounts/enable", h.Auth(h.EnableAccount)) // Send messages (with auth) mux.HandleFunc("/api/send", h.Auth(h.SendMessage)) @@ -242,11 +251,14 @@ func (s *Server) setupRoutes() *http.ServeMux { mux.HandleFunc("/api/cache/clear", h.Auth(h.ClearCache)) // DELETE with ?confirm=true logging.Info("HTTP server endpoints configured", + "index", "/", + "static", "/static/*", "health", "/health", "hooks", "/api/hooks", "accounts", "/api/accounts", "send", "/api/send", "cache", "/api/cache", + "webhooks", "/webhooks/whatsapp/*", "qr", "/api/qr") return mux diff --git a/pkg/whatshooked/whatshooked.go b/pkg/whatshooked/whatshooked.go index 37066aa..cdcf4df 100644 --- a/pkg/whatshooked/whatshooked.go +++ b/pkg/whatshooked/whatshooked.go @@ -131,6 +131,12 @@ func newWithConfig(cfg *config.Config, configPath string) (*WhatsHooked, error) // ConnectAll connects to all configured WhatsApp accounts func (wh *WhatsHooked) ConnectAll(ctx context.Context) error { for _, waCfg := range wh.config.WhatsApp { + // Skip disabled accounts + if waCfg.Disabled { + logging.Info("Skipping disabled account", "account_id", waCfg.ID) + continue + } + if err := wh.whatsappMgr.Connect(ctx, waCfg); err != nil { logging.Error("Failed to connect to WhatsApp", "account_id", waCfg.ID, "error", err) // Continue connecting to other accounts even if one fails