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,13 +1,28 @@
package main
import (
"context"
"encoding/json"
"fmt"
"os"
"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"
)
// accountsCmd is the parent command for account management
var (
accountUser string
accountPhoneNumber string
accountType string
accountSessionPath string
accountDisplayName string
)
var accountsCmd = &cobra.Command{
Use: "accounts",
Short: "Manage WhatsApp accounts",
@@ -35,17 +50,47 @@ var accountsAddCmd = &cobra.Command{
},
}
var accountsRemoveCmd = &cobra.Command{
Use: "remove <id>",
Short: "Remove a WhatsApp account by ID",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
client := NewClient(cliConfig)
removeAccount(client, args[0])
},
}
func init() {
accountsAddCmd.Flags().StringVarP(&accountPhoneNumber, "phone", "p", "", "Phone number (with country code)")
accountsAddCmd.Flags().StringVarP(&accountType, "type", "t", "whatsmeow", "Account type (whatsmeow/business-api)")
accountsAddCmd.Flags().StringVarP(&accountSessionPath, "session-path", "s", "", "Session path (auto-generated if omitted)")
accountsAddCmd.Flags().StringVarP(&accountDisplayName, "display-name", "d", "", "Display name")
accountsAddCmd.Flags().StringVarP(&accountUser, "user", "u", "", "Owner username for DB mode (default: first admin)")
accountsCmd.AddCommand(accountsListCmd)
accountsCmd.AddCommand(accountsAddCmd)
accountsCmd.AddCommand(accountsRemoveCmd)
}
func listAccounts(client *Client) {
if serverAvailable(client) {
listAccountsHTTP(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
}
listAccountsDB()
}
}
func listAccountsHTTP(client *Client) {
resp, err := client.Get("/api/accounts")
checkError(err)
defer resp.Body.Close()
var accounts []config.WhatsAppConfig
var accounts []map[string]interface{}
checkError(decodeJSON(resp, &accounts))
if len(accounts) == 0 {
@@ -53,37 +98,140 @@ func listAccounts(client *Client) {
return
}
fmt.Printf("Configured accounts (%d):\n\n", len(accounts))
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintln(w, "ID\tPHONE\tTYPE\tSTATUS\tACTIVE")
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()
fmt.Fprintf(w, "%v\t%v\t%v\t%v\t%v\n",
acc["id"], acc["phone_number"], acc["account_type"], acc["status"], acc["active"])
}
w.Flush()
}
func listAccountsDB() {
var accounts []models.ModelPublicWhatsappAccount
err := storage.DB.NewSelect().Model(&accounts).OrderExpr("created_at ASC").Scan(context.Background())
checkError(err)
if len(accounts) == 0 {
fmt.Println("No accounts configured")
return
}
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintln(w, "ID\tPHONE\tTYPE\tSTATUS\tACTIVE")
for _, acc := range accounts {
active := "yes"
if !acc.Active {
active = "no"
}
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n",
acc.ID.String(),
acc.PhoneNumber.String(),
acc.AccountType.String(),
acc.Status.String(),
active,
)
}
w.Flush()
}
func addAccount(client *Client) {
var account config.WhatsAppConfig
fmt.Print("Account ID: ")
if _, err := fmt.Scanln(&account.ID); err != nil {
checkError(fmt.Errorf("error reading account ID: %v", err))
phone := accountPhoneNumber
if phone == "" {
phone = promptRequired("Phone Number (with country code)")
}
fmt.Print("Phone Number (with country code): ")
if _, err := fmt.Scanln(&account.PhoneNumber); err != nil {
checkError(fmt.Errorf("error reading phone number: %v", err))
acctType := accountType
if acctType == "" {
acctType = promptLine("Account type (whatsmeow/business-api)", "whatsmeow")
}
fmt.Print("Session Path: ")
if _, err := fmt.Scanln(&account.SessionPath); err != nil {
checkError(fmt.Errorf("error reading session path: %v", err))
displayName := accountDisplayName
if displayName == "" {
displayName = promptLine("Display name (optional)", "")
}
resp, err := client.Post("/api/accounts/add", account)
if serverAvailable(client) {
addAccountHTTP(client, phone, acctType, displayName)
} 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
}
addAccountDB(phone, acctType, displayName)
}
}
func addAccountHTTP(client *Client, phone, acctType, displayName string) {
payload := map[string]interface{}{
"phone_number": phone,
"account_type": acctType,
"display_name": displayName,
}
resp, err := client.Post("/api/accounts/add", payload)
checkError(err)
defer resp.Body.Close()
fmt.Println("Account added successfully")
fmt.Println("Check server logs for QR code to pair the device")
}
func addAccountDB(phone, acctType, displayName string) {
userID := dbOwnerUserID(accountUser)
if userID == "" {
fmt.Println("Error: no users found in database. Create a user first with: users add")
return
}
id := uuid.New().String()
sessionPath := accountSessionPath
if sessionPath == "" {
sessionPath = fmt.Sprintf("./sessions/%s", id)
}
now := time.Now()
var cfgJSON string
if acctType == "business-api" {
b, _ := json.Marshal(map[string]string{})
cfgJSON = string(b)
}
account := &models.ModelPublicWhatsappAccount{
ID: resolvespec_common.NewSqlString(id),
PhoneNumber: resolvespec_common.NewSqlString(phone),
AccountType: resolvespec_common.NewSqlString(acctType),
DisplayName: resolvespec_common.NewSqlString(displayName),
SessionPath: resolvespec_common.NewSqlString(sessionPath),
Config: resolvespec_common.NewSqlString(cfgJSON),
Status: resolvespec_common.NewSqlString("disconnected"),
UserID: resolvespec_common.NewSqlString(userID),
Active: true,
CreatedAt: resolvespec_common.NewSqlTimeStamp(now),
UpdatedAt: resolvespec_common.NewSqlTimeStamp(now),
}
repo := storage.NewWhatsAppAccountRepository(storage.DB)
checkError(repo.Create(context.Background(), account))
fmt.Printf("Account '%s' added (ID: %s)\n", phone, id)
fmt.Println("Start the server to connect and pair the device")
}
func removeAccount(client *Client, id string) {
if serverAvailable(client) {
resp, err := client.Post("/api/accounts/remove", map[string]string{"id": id})
checkError(err)
defer resp.Body.Close()
fmt.Println("Account 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.NewWhatsAppAccountRepository(storage.DB)
checkError(repo.Delete(context.Background(), id))
fmt.Printf("Account '%s' removed\n", id)
}
}