CLI refactor
This commit is contained in:
111
cmd/cli/client.go
Normal file
111
cmd/cli/client.go
Normal file
@@ -0,0 +1,111 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
)
|
||||
|
||||
// Client wraps the HTTP client with authentication support
|
||||
type Client struct {
|
||||
baseURL string
|
||||
authKey string
|
||||
username string
|
||||
password string
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
// NewClient creates a new authenticated HTTP client
|
||||
func NewClient(cfg *CLIConfig) *Client {
|
||||
return &Client{
|
||||
baseURL: cfg.ServerURL,
|
||||
authKey: cfg.AuthKey,
|
||||
username: cfg.Username,
|
||||
password: cfg.Password,
|
||||
client: &http.Client{},
|
||||
}
|
||||
}
|
||||
|
||||
// addAuth adds authentication headers to the request
|
||||
func (c *Client) addAuth(req *http.Request) {
|
||||
// Priority 1: API Key (as Bearer token or x-api-key header)
|
||||
if c.authKey != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+c.authKey)
|
||||
req.Header.Set("x-api-key", c.authKey)
|
||||
return
|
||||
}
|
||||
|
||||
// Priority 2: Basic Auth (username/password)
|
||||
if c.username != "" && c.password != "" {
|
||||
req.SetBasicAuth(c.username, c.password)
|
||||
}
|
||||
}
|
||||
|
||||
// Get performs an authenticated GET request
|
||||
func (c *Client) Get(path string) (*http.Response, error) {
|
||||
req, err := http.NewRequest("GET", c.baseURL+path, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c.addAuth(req)
|
||||
resp, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Check for HTTP error status codes
|
||||
if resp.StatusCode >= 400 {
|
||||
defer resp.Body.Close()
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// Post performs an authenticated POST request with JSON data
|
||||
func (c *Client) Post(path string, data interface{}) (*http.Response, error) {
|
||||
jsonData, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", c.baseURL+path, bytes.NewReader(jsonData))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
c.addAuth(req)
|
||||
resp, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Check for HTTP error status codes
|
||||
if resp.StatusCode >= 400 {
|
||||
defer resp.Body.Close()
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// decodeJSON decodes JSON response into target
|
||||
func decodeJSON(resp *http.Response, target interface{}) error {
|
||||
defer resp.Body.Close()
|
||||
return json.NewDecoder(resp.Body).Decode(target)
|
||||
}
|
||||
|
||||
// checkError prints error and exits if error is not nil
|
||||
func checkError(err error) {
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
83
cmd/cli/commands_accounts.go
Normal file
83
cmd/cli/commands_accounts.go
Normal file
@@ -0,0 +1,83 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.warky.dev/wdevs/whatshooked/internal/config"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// accountsCmd is the parent command for account management
|
||||
var accountsCmd = &cobra.Command{
|
||||
Use: "accounts",
|
||||
Short: "Manage WhatsApp accounts",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
client := NewClient(cliConfig)
|
||||
listAccounts(client)
|
||||
},
|
||||
}
|
||||
|
||||
var accountsListCmd = &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List all accounts",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
client := NewClient(cliConfig)
|
||||
listAccounts(client)
|
||||
},
|
||||
}
|
||||
|
||||
var accountsAddCmd = &cobra.Command{
|
||||
Use: "add",
|
||||
Short: "Add a new WhatsApp account",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
client := NewClient(cliConfig)
|
||||
addAccount(client)
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
accountsCmd.AddCommand(accountsListCmd)
|
||||
accountsCmd.AddCommand(accountsAddCmd)
|
||||
}
|
||||
|
||||
func listAccounts(client *Client) {
|
||||
resp, err := client.Get("/api/accounts")
|
||||
checkError(err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
var accounts []config.WhatsAppConfig
|
||||
checkError(decodeJSON(resp, &accounts))
|
||||
|
||||
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(client *Client) {
|
||||
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)
|
||||
|
||||
resp, err := client.Post("/api/accounts/add", account)
|
||||
checkError(err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
fmt.Println("Account added successfully")
|
||||
fmt.Println("Check server logs for QR code to pair the device")
|
||||
}
|
||||
28
cmd/cli/commands_health.go
Normal file
28
cmd/cli/commands_health.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// healthCmd checks server health
|
||||
var healthCmd = &cobra.Command{
|
||||
Use: "health",
|
||||
Short: "Check server health",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
client := NewClient(cliConfig)
|
||||
checkHealth(client)
|
||||
},
|
||||
}
|
||||
|
||||
func checkHealth(client *Client) {
|
||||
resp, err := client.Get("/health")
|
||||
checkError(err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
var result map[string]string
|
||||
checkError(decodeJSON(resp, &result))
|
||||
|
||||
fmt.Printf("Server status: %s\n", result["status"])
|
||||
}
|
||||
155
cmd/cli/commands_hooks.go
Normal file
155
cmd/cli/commands_hooks.go
Normal file
@@ -0,0 +1,155 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"git.warky.dev/wdevs/whatshooked/internal/config"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// hooksCmd is the parent command for hook management
|
||||
var hooksCmd = &cobra.Command{
|
||||
Use: "hooks",
|
||||
Short: "Manage webhooks",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
client := NewClient(cliConfig)
|
||||
listHooks(client)
|
||||
},
|
||||
}
|
||||
|
||||
var hooksListCmd = &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List all hooks",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
client := NewClient(cliConfig)
|
||||
listHooks(client)
|
||||
},
|
||||
}
|
||||
|
||||
var hooksAddCmd = &cobra.Command{
|
||||
Use: "add",
|
||||
Short: "Add a new hook",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
client := NewClient(cliConfig)
|
||||
addHook(client)
|
||||
},
|
||||
}
|
||||
|
||||
var hooksRemoveCmd = &cobra.Command{
|
||||
Use: "remove <hook_id>",
|
||||
Short: "Remove a hook",
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
client := NewClient(cliConfig)
|
||||
removeHook(client, args[0])
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
hooksCmd.AddCommand(hooksListCmd)
|
||||
hooksCmd.AddCommand(hooksAddCmd)
|
||||
hooksCmd.AddCommand(hooksRemoveCmd)
|
||||
}
|
||||
|
||||
func listHooks(client *Client) {
|
||||
resp, err := client.Get("/api/hooks")
|
||||
checkError(err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
var hooks []config.Hook
|
||||
checkError(decodeJSON(resp, &hooks))
|
||||
|
||||
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 len(hook.Events) > 0 {
|
||||
fmt.Printf("Events: %v\n", hook.Events)
|
||||
} else {
|
||||
fmt.Printf("Events: all (no filter)\n")
|
||||
}
|
||||
if hook.Description != "" {
|
||||
fmt.Printf("Description: %s\n", hook.Description)
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
}
|
||||
|
||||
func addHook(client *Client) {
|
||||
var hook config.Hook
|
||||
scanner := bufio.NewScanner(os.Stdin)
|
||||
|
||||
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"
|
||||
}
|
||||
|
||||
// Prompt for events with helpful examples
|
||||
fmt.Println("\nAvailable events:")
|
||||
fmt.Println(" WhatsApp: whatsapp.connected, whatsapp.disconnected, whatsapp.qr.code")
|
||||
fmt.Println(" Messages: message.received, message.sent, message.delivered, message.read")
|
||||
fmt.Println(" Hooks: hook.triggered, hook.success, hook.failed")
|
||||
fmt.Print("\nEvents (comma-separated, or press Enter for all): ")
|
||||
|
||||
scanner.Scan()
|
||||
eventsInput := strings.TrimSpace(scanner.Text())
|
||||
|
||||
if eventsInput != "" {
|
||||
// Split by comma and trim whitespace
|
||||
eventsList := strings.Split(eventsInput, ",")
|
||||
hook.Events = make([]string, 0, len(eventsList))
|
||||
for _, event := range eventsList {
|
||||
trimmed := strings.TrimSpace(event)
|
||||
if trimmed != "" {
|
||||
hook.Events = append(hook.Events, trimmed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Print("\nDescription (optional): ")
|
||||
scanner.Scan()
|
||||
hook.Description = strings.TrimSpace(scanner.Text())
|
||||
|
||||
hook.Active = true
|
||||
|
||||
resp, err := client.Post("/api/hooks/add", hook)
|
||||
checkError(err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
fmt.Println("Hook added successfully")
|
||||
}
|
||||
|
||||
func removeHook(client *Client, id string) {
|
||||
req := map[string]string{"id": id}
|
||||
|
||||
resp, err := client.Post("/api/hooks/remove", req)
|
||||
checkError(err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
fmt.Println("Hook removed successfully")
|
||||
}
|
||||
254
cmd/cli/commands_send.go
Normal file
254
cmd/cli/commands_send.go
Normal file
@@ -0,0 +1,254 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// sendCmd is the parent command for sending messages
|
||||
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) {
|
||||
client := NewClient(cliConfig)
|
||||
sendMessage(client)
|
||||
},
|
||||
}
|
||||
|
||||
var sendImageCmd = &cobra.Command{
|
||||
Use: "image <file_path>",
|
||||
Short: "Send an image",
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
client := NewClient(cliConfig)
|
||||
sendImage(client, args[0])
|
||||
},
|
||||
}
|
||||
|
||||
var sendVideoCmd = &cobra.Command{
|
||||
Use: "video <file_path>",
|
||||
Short: "Send a video",
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
client := NewClient(cliConfig)
|
||||
sendVideo(client, args[0])
|
||||
},
|
||||
}
|
||||
|
||||
var sendDocumentCmd = &cobra.Command{
|
||||
Use: "document <file_path>",
|
||||
Short: "Send a document",
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
client := NewClient(cliConfig)
|
||||
sendDocument(client, args[0])
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
sendCmd.AddCommand(sendTextCmd)
|
||||
sendCmd.AddCommand(sendImageCmd)
|
||||
sendCmd.AddCommand(sendVideoCmd)
|
||||
sendCmd.AddCommand(sendDocumentCmd)
|
||||
}
|
||||
|
||||
func sendMessage(client *Client) {
|
||||
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 {
|
||||
checkError(fmt.Errorf("error reading input: %v", err))
|
||||
}
|
||||
req.Text = string(buf[:n])
|
||||
|
||||
resp, err := client.Post("/api/send", req)
|
||||
checkError(err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
fmt.Println("Message sent successfully")
|
||||
}
|
||||
|
||||
func sendImage(client *Client, 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)
|
||||
checkError(err)
|
||||
|
||||
// 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"
|
||||
}
|
||||
|
||||
resp, err := client.Post("/api/send/image", req)
|
||||
checkError(err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
fmt.Println("Image sent successfully")
|
||||
}
|
||||
|
||||
func sendVideo(client *Client, 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)
|
||||
checkError(err)
|
||||
|
||||
// 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"
|
||||
}
|
||||
|
||||
resp, err := client.Post("/api/send/video", req)
|
||||
checkError(err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
fmt.Println("Video sent successfully")
|
||||
}
|
||||
|
||||
func sendDocument(client *Client, 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)
|
||||
checkError(err)
|
||||
|
||||
// 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"
|
||||
}
|
||||
|
||||
resp, err := client.Post("/api/send/document", req)
|
||||
checkError(err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
fmt.Println("Document sent successfully")
|
||||
}
|
||||
@@ -10,6 +10,9 @@ import (
|
||||
// CLIConfig holds the CLI configuration
|
||||
type CLIConfig struct {
|
||||
ServerURL string
|
||||
AuthKey string
|
||||
Username string
|
||||
Password string
|
||||
}
|
||||
|
||||
// LoadCLIConfig loads configuration with priority: config file → ENV → flag
|
||||
@@ -18,6 +21,9 @@ func LoadCLIConfig(configFile string, serverFlag string) (*CLIConfig, error) {
|
||||
|
||||
// Set defaults
|
||||
v.SetDefault("server_url", "http://localhost:8080")
|
||||
v.SetDefault("auth_key", "")
|
||||
v.SetDefault("username", "")
|
||||
v.SetDefault("password", "")
|
||||
|
||||
// 1. Load from config file (lowest priority)
|
||||
if configFile != "" {
|
||||
@@ -50,6 +56,9 @@ func LoadCLIConfig(configFile string, serverFlag string) (*CLIConfig, error) {
|
||||
|
||||
cfg := &CLIConfig{
|
||||
ServerURL: v.GetString("server_url"),
|
||||
AuthKey: v.GetString("auth_key"),
|
||||
Username: v.GetString("username"),
|
||||
Password: v.GetString("password"),
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
|
||||
585
cmd/cli/main.go
585
cmd/cli/main.go
@@ -1,17 +1,9 @@
|
||||
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"
|
||||
)
|
||||
|
||||
@@ -46,584 +38,9 @@ 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)")
|
||||
|
||||
// Add all command groups
|
||||
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 <hook_id>",
|
||||
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 <file_path>",
|
||||
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 <file_path>",
|
||||
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 <file_path>",
|
||||
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")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user