feat(cli): enhance user and hook management with new commands and flags
Some checks failed
CI / Test (1.22) (push) Failing after -22m38s
CI / Test (1.23) (push) Failing after -22m21s
CI / Lint (push) Failing after -22m42s
CI / Build (push) Failing after -23m0s

This commit is contained in:
2026-02-20 21:17:09 +02:00
parent 500db67c72
commit b81febafc9
5 changed files with 529 additions and 171 deletions

View File

@@ -1,16 +1,23 @@
package main
import (
"bufio"
"context"
"encoding/json"
"fmt"
"os"
"strings"
"text/tabwriter"
"time"
"git.warky.dev/wdevs/whatshooked/pkg/config"
"git.warky.dev/wdevs/whatshooked/pkg/models"
"git.warky.dev/wdevs/whatshooked/pkg/storage"
resolvespec_common "github.com/bitechdev/ResolveSpec/pkg/spectypes"
"github.com/google/uuid"
"github.com/spf13/cobra"
)
// hooksCmd is the parent command for hook management
var hookUser string
var hooksCmd = &cobra.Command{
Use: "hooks",
Short: "Manage webhooks",
@@ -40,7 +47,7 @@ var hooksAddCmd = &cobra.Command{
var hooksRemoveCmd = &cobra.Command{
Use: "remove <hook_id>",
Short: "Remove a hook",
Short: "Remove a hook by ID",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
client := NewClient(cliConfig)
@@ -49,17 +56,31 @@ var hooksRemoveCmd = &cobra.Command{
}
func init() {
hooksAddCmd.Flags().StringVarP(&hookUser, "user", "u", "", "Owner username for DB mode (default: first admin)")
hooksCmd.AddCommand(hooksListCmd)
hooksCmd.AddCommand(hooksAddCmd)
hooksCmd.AddCommand(hooksRemoveCmd)
}
func listHooks(client *Client) {
if serverAvailable(client) {
listHooksHTTP(client)
} else {
fmt.Println("[server unavailable, reading from database]")
if !tryInitDB() {
fmt.Println("Error: server unreachable and no database config found. Use --server-config to specify config path.")
return
}
listHooksDB()
}
}
func listHooksHTTP(client *Client) {
resp, err := client.Get("/api/hooks")
checkError(err)
defer resp.Body.Close()
var hooks []config.Hook
var hooks []map[string]interface{}
checkError(decodeJSON(resp, &hooks))
if len(hooks) == 0 {
@@ -67,98 +88,153 @@ func listHooks(client *Client) {
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()
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintln(w, "ID\tNAME\tURL\tMETHOD\tACTIVE")
for _, h := range hooks {
fmt.Fprintf(w, "%v\t%v\t%v\t%v\t%v\n",
h["id"], h["name"], h["url"], h["method"], h["active"])
}
w.Flush()
}
func listHooksDB() {
var hooks []models.ModelPublicHook
err := storage.DB.NewSelect().Model(&hooks).OrderExpr("created_at ASC").Scan(context.Background())
checkError(err)
if len(hooks) == 0 {
fmt.Println("No hooks configured")
return
}
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintln(w, "ID\tNAME\tURL\tMETHOD\tACTIVE\tEVENTS")
for _, h := range hooks {
active := "yes"
if !h.Active {
active = "no"
}
events := parseEventsJSON(h.Events.String())
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\n",
h.ID.String(),
h.Name.String(),
h.URL.String(),
h.Method.String(),
active,
events,
)
}
w.Flush()
}
func addHook(client *Client) {
var hook config.Hook
scanner := bufio.NewScanner(os.Stdin)
name := promptRequired("Hook Name")
url := promptRequired("Webhook URL")
method := promptLine("HTTP Method", "POST")
fmt.Print("Hook ID: ")
if _, err := fmt.Scanln(&hook.ID); err != nil {
checkError(fmt.Errorf("error reading hook ID: %v", err))
}
fmt.Print("Hook Name: ")
if _, err := fmt.Scanln(&hook.Name); err != nil {
checkError(fmt.Errorf("error reading hook name: %v", err))
}
fmt.Print("Webhook URL: ")
if _, err := fmt.Scanln(&hook.URL); err != nil {
checkError(fmt.Errorf("error reading webhook URL: %v", err))
}
fmt.Print("HTTP Method (POST): ")
if _, err := fmt.Scanln(&hook.Method); err == nil {
// Successfully read input
fmt.Printf("Selected Method %s", 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): ")
fmt.Println(" whatsapp.connected, whatsapp.disconnected, whatsapp.qr.code")
fmt.Println(" message.received, message.sent, message.delivered, message.read")
fmt.Println(" hook.triggered, hook.success, hook.failed")
eventsRaw := promptLine("\nEvents (comma-separated, or Enter for all)", "")
description := promptLine("Description (optional)", "")
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)
var events []string
if eventsRaw != "" {
for _, e := range strings.Split(eventsRaw, ",") {
if t := strings.TrimSpace(e); t != "" {
events = append(events, t)
}
}
}
fmt.Print("\nDescription (optional): ")
scanner.Scan()
hook.Description = strings.TrimSpace(scanner.Text())
if serverAvailable(client) {
addHookHTTP(client, name, url, method, description, events)
} else {
fmt.Println("[server unavailable, writing to database]")
if !tryInitDB() {
fmt.Println("Error: server unreachable and no database config found. Use --server-config to specify config path.")
return
}
addHookDB(name, url, method, description, events)
}
}
hook.Active = true
resp, err := client.Post("/api/hooks/add", hook)
func addHookHTTP(client *Client, name, url, method, description string, events []string) {
payload := map[string]interface{}{
"name": name,
"url": url,
"method": method,
"description": description,
"events": events,
"active": true,
}
resp, err := client.Post("/api/hooks/add", payload)
checkError(err)
defer resp.Body.Close()
fmt.Println("Hook added successfully")
}
func removeHook(client *Client, id string) {
req := map[string]string{"id": id}
func addHookDB(name, url, method, description string, events []string) {
userID := dbOwnerUserID(hookUser)
if userID == "" {
fmt.Println("Error: no users found in database. Create a user first with: users add")
return
}
resp, err := client.Post("/api/hooks/remove", req)
checkError(err)
defer resp.Body.Close()
eventsJSON := "[]"
if len(events) > 0 {
b, _ := json.Marshal(events)
eventsJSON = string(b)
}
fmt.Println("Hook removed successfully")
id := uuid.New().String()
now := time.Now()
hook := &models.ModelPublicHook{
ID: resolvespec_common.NewSqlString(id),
Name: resolvespec_common.NewSqlString(name),
URL: resolvespec_common.NewSqlString(url),
Method: resolvespec_common.NewSqlString(method),
Description: resolvespec_common.NewSqlString(description),
Events: resolvespec_common.NewSqlString(eventsJSON),
UserID: resolvespec_common.NewSqlString(userID),
Active: true,
CreatedAt: resolvespec_common.NewSqlTimeStamp(now),
UpdatedAt: resolvespec_common.NewSqlTimeStamp(now),
}
repo := storage.NewHookRepository(storage.DB)
checkError(repo.Create(context.Background(), hook))
fmt.Printf("Hook '%s' added (ID: %s)\n", name, id)
}
func removeHook(client *Client, id string) {
if serverAvailable(client) {
resp, err := client.Post("/api/hooks/remove", map[string]string{"id": id})
checkError(err)
defer resp.Body.Close()
fmt.Println("Hook removed successfully")
} else {
fmt.Println("[server unavailable, removing from database]")
if !tryInitDB() {
fmt.Println("Error: server unreachable and no database config found. Use --server-config to specify config path.")
return
}
repo := storage.NewHookRepository(storage.DB)
checkError(repo.Delete(context.Background(), id))
fmt.Printf("Hook '%s' removed\n", id)
}
}
// parseEventsJSON parses a JSON events string into a comma-separated display string.
func parseEventsJSON(raw string) string {
if raw == "" || raw == "[]" {
return "all"
}
var events []string
if err := json.Unmarshal([]byte(raw), &events); err != nil {
return raw
}
return strings.Join(events, ", ")
}