package main import ( "bytes" "encoding/base64" "encoding/json" "fmt" "io" "net/http" "os" "path/filepath" "strings" "git.warky.dev/wdevs/whatshooked/internal/config" "github.com/spf13/cobra" ) var ( cfgFile string serverURL string cliConfig *CLIConfig ) func main() { if err := rootCmd.Execute(); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } } var rootCmd = &cobra.Command{ Use: "whatshook-cli", Short: "WhatsHooked CLI - Manage WhatsApp webhooks", Long: `A command-line interface for managing WhatsHooked server, hooks, and WhatsApp accounts.`, PersistentPreRun: func(cmd *cobra.Command, args []string) { var err error cliConfig, err = LoadCLIConfig(cfgFile, serverURL) if err != nil { fmt.Fprintf(os.Stderr, "Error loading config: %v\n", err) os.Exit(1) } }, } func init() { rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default: $HOME/.whatshooked/cli.json)") rootCmd.PersistentFlags().StringVar(&serverURL, "server", "", "server URL (default: http://localhost:8080)") rootCmd.AddCommand(healthCmd) rootCmd.AddCommand(hooksCmd) rootCmd.AddCommand(accountsCmd) rootCmd.AddCommand(sendCmd) } // Health command var healthCmd = &cobra.Command{ Use: "health", Short: "Check server health", Run: func(cmd *cobra.Command, args []string) { checkHealth(cliConfig.ServerURL) }, } // Hooks command group var hooksCmd = &cobra.Command{ Use: "hooks", Short: "Manage webhooks", Run: func(cmd *cobra.Command, args []string) { listHooks(cliConfig.ServerURL) }, } var hooksListCmd = &cobra.Command{ Use: "list", Short: "List all hooks", Run: func(cmd *cobra.Command, args []string) { listHooks(cliConfig.ServerURL) }, } var hooksAddCmd = &cobra.Command{ Use: "add", Short: "Add a new hook", Run: func(cmd *cobra.Command, args []string) { addHook(cliConfig.ServerURL) }, } var hooksRemoveCmd = &cobra.Command{ Use: "remove ", Short: "Remove a hook", Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { removeHook(cliConfig.ServerURL, args[0]) }, } func init() { hooksCmd.AddCommand(hooksListCmd) hooksCmd.AddCommand(hooksAddCmd) hooksCmd.AddCommand(hooksRemoveCmd) } // Accounts command group var accountsCmd = &cobra.Command{ Use: "accounts", Short: "Manage WhatsApp accounts", Run: func(cmd *cobra.Command, args []string) { listAccounts(cliConfig.ServerURL) }, } var accountsListCmd = &cobra.Command{ Use: "list", Short: "List all accounts", Run: func(cmd *cobra.Command, args []string) { listAccounts(cliConfig.ServerURL) }, } var accountsAddCmd = &cobra.Command{ Use: "add", Short: "Add a new WhatsApp account", Run: func(cmd *cobra.Command, args []string) { addAccount(cliConfig.ServerURL) }, } func init() { accountsCmd.AddCommand(accountsListCmd) accountsCmd.AddCommand(accountsAddCmd) } // Send command group var sendCmd = &cobra.Command{ Use: "send", Short: "Send messages", } var sendTextCmd = &cobra.Command{ Use: "text", Short: "Send a text message", Run: func(cmd *cobra.Command, args []string) { sendMessage(cliConfig.ServerURL) }, } var sendImageCmd = &cobra.Command{ Use: "image ", Short: "Send an image", Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { sendImage(cliConfig.ServerURL, args[0]) }, } var sendVideoCmd = &cobra.Command{ Use: "video ", Short: "Send a video", Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { sendVideo(cliConfig.ServerURL, args[0]) }, } var sendDocumentCmd = &cobra.Command{ Use: "document ", Short: "Send a document", Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { sendDocument(cliConfig.ServerURL, args[0]) }, } func init() { sendCmd.AddCommand(sendTextCmd) sendCmd.AddCommand(sendImageCmd) sendCmd.AddCommand(sendVideoCmd) sendCmd.AddCommand(sendDocumentCmd) } // Helper functions func checkHealth(serverURL string) { resp, err := http.Get(serverURL + "/health") if err != nil { fmt.Printf("Error: %v\n", err) os.Exit(1) } defer resp.Body.Close() var result map[string]string if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { fmt.Printf("Error decoding response: %v\n", err) os.Exit(1) } fmt.Printf("Server status: %s\n", result["status"]) } func listHooks(serverURL string) { resp, err := http.Get(serverURL + "/api/hooks") if err != nil { fmt.Printf("Error: %v\n", err) os.Exit(1) } defer resp.Body.Close() var hooks []config.Hook if err := json.NewDecoder(resp.Body).Decode(&hooks); err != nil { fmt.Printf("Error decoding response: %v\n", err) os.Exit(1) } if len(hooks) == 0 { fmt.Println("No hooks configured") return } fmt.Printf("Configured hooks (%d):\n\n", len(hooks)) for _, hook := range hooks { status := "inactive" if hook.Active { status = "active" } fmt.Printf("ID: %s\n", hook.ID) fmt.Printf("Name: %s\n", hook.Name) fmt.Printf("URL: %s\n", hook.URL) fmt.Printf("Method: %s\n", hook.Method) fmt.Printf("Status: %s\n", status) if hook.Description != "" { fmt.Printf("Description: %s\n", hook.Description) } fmt.Println() } } func addHook(serverURL string) { var hook config.Hook fmt.Print("Hook ID: ") fmt.Scanln(&hook.ID) fmt.Print("Hook Name: ") fmt.Scanln(&hook.Name) fmt.Print("Webhook URL: ") fmt.Scanln(&hook.URL) fmt.Print("HTTP Method (POST): ") fmt.Scanln(&hook.Method) if hook.Method == "" { hook.Method = "POST" } fmt.Print("Description (optional): ") fmt.Scanln(&hook.Description) hook.Active = true data, err := json.Marshal(hook) if err != nil { fmt.Printf("Error: %v\n", err) os.Exit(1) } resp, err := http.Post(serverURL+"/api/hooks/add", "application/json", bytes.NewReader(data)) if err != nil { fmt.Printf("Error: %v\n", err) os.Exit(1) } defer resp.Body.Close() if resp.StatusCode != http.StatusCreated { body, _ := io.ReadAll(resp.Body) fmt.Printf("Error: %s\n", string(body)) os.Exit(1) } fmt.Println("Hook added successfully") } func removeHook(serverURL string, id string) { req := map[string]string{"id": id} data, err := json.Marshal(req) if err != nil { fmt.Printf("Error: %v\n", err) os.Exit(1) } resp, err := http.Post(serverURL+"/api/hooks/remove", "application/json", bytes.NewReader(data)) if err != nil { fmt.Printf("Error: %v\n", err) os.Exit(1) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) fmt.Printf("Error: %s\n", string(body)) os.Exit(1) } fmt.Println("Hook removed successfully") } func listAccounts(serverURL string) { resp, err := http.Get(serverURL + "/api/accounts") if err != nil { fmt.Printf("Error: %v\n", err) os.Exit(1) } defer resp.Body.Close() var accounts []config.WhatsAppConfig if err := json.NewDecoder(resp.Body).Decode(&accounts); err != nil { fmt.Printf("Error decoding response: %v\n", err) os.Exit(1) } if len(accounts) == 0 { fmt.Println("No accounts configured") return } fmt.Printf("Configured accounts (%d):\n\n", len(accounts)) for _, acc := range accounts { fmt.Printf("ID: %s\n", acc.ID) fmt.Printf("Phone Number: %s\n", acc.PhoneNumber) fmt.Printf("Session Path: %s\n", acc.SessionPath) fmt.Println() } } func addAccount(serverURL string) { var account config.WhatsAppConfig fmt.Print("Account ID: ") fmt.Scanln(&account.ID) fmt.Print("Phone Number (with country code): ") fmt.Scanln(&account.PhoneNumber) fmt.Print("Session Path: ") fmt.Scanln(&account.SessionPath) data, err := json.Marshal(account) if err != nil { fmt.Printf("Error: %v\n", err) os.Exit(1) } resp, err := http.Post(serverURL+"/api/accounts/add", "application/json", bytes.NewReader(data)) if err != nil { fmt.Printf("Error: %v\n", err) os.Exit(1) } defer resp.Body.Close() if resp.StatusCode != http.StatusCreated { body, _ := io.ReadAll(resp.Body) fmt.Printf("Error: %s\n", string(body)) os.Exit(1) } fmt.Println("Account added successfully") fmt.Println("Check server logs for QR code to pair the device") } func sendMessage(serverURL string) { var req struct { AccountID string `json:"account_id"` To string `json:"to"` Text string `json:"text"` } fmt.Print("Account ID: ") fmt.Scanln(&req.AccountID) fmt.Print("Recipient (phone number or JID, e.g., 0834606792 or 1234567890@s.whatsapp.net): ") fmt.Scanln(&req.To) fmt.Print("Message text: ") reader := os.Stdin buf := make([]byte, 1024) n, err := reader.Read(buf) if err != nil { fmt.Printf("Error reading input: %v\n", err) os.Exit(1) } req.Text = string(buf[:n]) data, err := json.Marshal(req) if err != nil { fmt.Printf("Error: %v\n", err) os.Exit(1) } resp, err := http.Post(serverURL+"/api/send", "application/json", bytes.NewReader(data)) if err != nil { fmt.Printf("Error: %v\n", err) os.Exit(1) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) fmt.Printf("Error: %s\n", string(body)) os.Exit(1) } fmt.Println("Message sent successfully") } func sendImage(serverURL string, filePath string) { var req struct { AccountID string `json:"account_id"` To string `json:"to"` Caption string `json:"caption"` MimeType string `json:"mime_type"` ImageData string `json:"image_data"` } fmt.Print("Account ID: ") fmt.Scanln(&req.AccountID) fmt.Print("Recipient (phone number): ") fmt.Scanln(&req.To) fmt.Print("Caption (optional): ") reader := os.Stdin buf := make([]byte, 1024) n, _ := reader.Read(buf) req.Caption = strings.TrimSpace(string(buf[:n])) // Read image file imageData, err := os.ReadFile(filePath) if err != nil { fmt.Printf("Error reading image file: %v\n", err) os.Exit(1) } // Encode to base64 req.ImageData = base64.StdEncoding.EncodeToString(imageData) // Detect mime type from extension ext := strings.ToLower(filepath.Ext(filePath)) switch ext { case ".jpg", ".jpeg": req.MimeType = "image/jpeg" case ".png": req.MimeType = "image/png" case ".gif": req.MimeType = "image/gif" case ".webp": req.MimeType = "image/webp" default: req.MimeType = "image/jpeg" } data, err := json.Marshal(req) if err != nil { fmt.Printf("Error: %v\n", err) os.Exit(1) } resp, err := http.Post(serverURL+"/api/send/image", "application/json", bytes.NewReader(data)) if err != nil { fmt.Printf("Error: %v\n", err) os.Exit(1) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) fmt.Printf("Error: %s\n", string(body)) os.Exit(1) } fmt.Println("Image sent successfully") } func sendVideo(serverURL string, filePath string) { var req struct { AccountID string `json:"account_id"` To string `json:"to"` Caption string `json:"caption"` MimeType string `json:"mime_type"` VideoData string `json:"video_data"` } fmt.Print("Account ID: ") fmt.Scanln(&req.AccountID) fmt.Print("Recipient (phone number): ") fmt.Scanln(&req.To) fmt.Print("Caption (optional): ") reader := os.Stdin buf := make([]byte, 1024) n, _ := reader.Read(buf) req.Caption = strings.TrimSpace(string(buf[:n])) // Read video file videoData, err := os.ReadFile(filePath) if err != nil { fmt.Printf("Error reading video file: %v\n", err) os.Exit(1) } // Encode to base64 req.VideoData = base64.StdEncoding.EncodeToString(videoData) // Detect mime type from extension ext := strings.ToLower(filepath.Ext(filePath)) switch ext { case ".mp4": req.MimeType = "video/mp4" case ".mov": req.MimeType = "video/quicktime" case ".avi": req.MimeType = "video/x-msvideo" case ".webm": req.MimeType = "video/webm" case ".3gp": req.MimeType = "video/3gpp" default: req.MimeType = "video/mp4" } data, err := json.Marshal(req) if err != nil { fmt.Printf("Error: %v\n", err) os.Exit(1) } resp, err := http.Post(serverURL+"/api/send/video", "application/json", bytes.NewReader(data)) if err != nil { fmt.Printf("Error: %v\n", err) os.Exit(1) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) fmt.Printf("Error: %s\n", string(body)) os.Exit(1) } fmt.Println("Video sent successfully") } func sendDocument(serverURL string, filePath string) { var req struct { AccountID string `json:"account_id"` To string `json:"to"` Caption string `json:"caption"` MimeType string `json:"mime_type"` Filename string `json:"filename"` DocumentData string `json:"document_data"` } fmt.Print("Account ID: ") fmt.Scanln(&req.AccountID) fmt.Print("Recipient (phone number): ") fmt.Scanln(&req.To) fmt.Print("Caption (optional): ") reader := os.Stdin buf := make([]byte, 1024) n, _ := reader.Read(buf) req.Caption = strings.TrimSpace(string(buf[:n])) // Read document file documentData, err := os.ReadFile(filePath) if err != nil { fmt.Printf("Error reading document file: %v\n", err) os.Exit(1) } // Encode to base64 req.DocumentData = base64.StdEncoding.EncodeToString(documentData) // Use the original filename req.Filename = filepath.Base(filePath) // Detect mime type from extension ext := strings.ToLower(filepath.Ext(filePath)) switch ext { case ".pdf": req.MimeType = "application/pdf" case ".doc": req.MimeType = "application/msword" case ".docx": req.MimeType = "application/vnd.openxmlformats-officedocument.wordprocessingml.document" case ".xls": req.MimeType = "application/vnd.ms-excel" case ".xlsx": req.MimeType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" case ".txt": req.MimeType = "text/plain" case ".zip": req.MimeType = "application/zip" default: req.MimeType = "application/octet-stream" } data, err := json.Marshal(req) if err != nil { fmt.Printf("Error: %v\n", err) os.Exit(1) } resp, err := http.Post(serverURL+"/api/send/document", "application/json", bytes.NewReader(data)) if err != nil { fmt.Printf("Error: %v\n", err) os.Exit(1) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) fmt.Printf("Error: %s\n", string(body)) os.Exit(1) } fmt.Println("Document sent successfully") }