refactor(UI): 🏗️ Ui changes and API changes
Some checks failed
CI / Test (1.23) (push) Failing after -22m40s
CI / Test (1.22) (push) Failing after -22m36s
CI / Build (push) Failing after -23m32s
CI / Lint (push) Failing after -23m7s

This commit is contained in:
Hein
2026-02-06 17:03:28 +02:00
parent 8b1eed6c42
commit 35a548e7e2
10 changed files with 648 additions and 20 deletions

View File

@@ -7,6 +7,8 @@ import (
"net/http"
"time"
"github.com/google/uuid"
"git.warky.dev/wdevs/whatshooked/pkg/config"
"git.warky.dev/wdevs/whatshooked/pkg/handlers"
"git.warky.dev/wdevs/whatshooked/pkg/models"
@@ -397,6 +399,60 @@ func handleQueryCreate(w http.ResponseWriter, r *http.Request, db *bun.DB, req Q
return
}
// Initialize data map if needed
if req.Data == nil {
req.Data = make(map[string]interface{})
}
// Auto-generate UUID for id field if not provided
generatedID := ""
if _, exists := req.Data["id"]; !exists {
generatedID = uuid.New().String()
req.Data["id"] = generatedID
}
// Auto-inject user_id for tables that need it
if userCtx != nil && userCtx.Claims != nil {
// Get user_id from claims (it's stored as UUID string)
if userIDClaim, ok := userCtx.Claims["user_id"]; ok {
if userID, ok := userIDClaim.(string); ok && userID != "" {
// Add user_id to data if the table requires it and it's not already set
tablesWithUserID := map[string]bool{
"hooks": true,
"whatsapp_accounts": true,
"api_keys": true,
"event_logs": true,
}
if tablesWithUserID[req.Table] {
// Only set user_id if not already provided
if _, exists := req.Data["user_id"]; !exists {
req.Data["user_id"] = userID
}
}
}
}
}
// Auto-generate session_path for WhatsApp accounts if not provided
if req.Table == "whatsapp_accounts" {
// Set session_path if not already provided
if _, exists := req.Data["session_path"]; !exists {
// Use account_id if provided, otherwise use generated id
sessionID := ""
if accountID, ok := req.Data["account_id"].(string); ok && accountID != "" {
sessionID = accountID
} else if generatedID != "" {
sessionID = generatedID
} else if id, ok := req.Data["id"].(string); ok && id != "" {
sessionID = id
}
if sessionID != "" {
req.Data["session_path"] = fmt.Sprintf("./sessions/%s", sessionID)
}
}
}
// Convert data map to model using JSON marshaling
dataJSON, err := json.Marshal(req.Data)
if err != nil {
@@ -409,6 +465,22 @@ func handleQueryCreate(w http.ResponseWriter, r *http.Request, db *bun.DB, req Q
return
}
// Ensure ID is set after unmarshaling by using model-specific handling
if generatedID != "" {
switch m := model.(type) {
case *models.ModelPublicWhatsappAccount:
m.ID.FromString(generatedID)
case *models.ModelPublicHook:
m.ID.FromString(generatedID)
case *models.ModelPublicAPIKey:
m.ID.FromString(generatedID)
case *models.ModelPublicEventLog:
m.ID.FromString(generatedID)
case *models.ModelPublicUser:
m.ID.FromString(generatedID)
}
}
// Insert into database
_, err = db.NewInsert().Model(model).Exec(r.Context())
if err != nil {

View File

@@ -9,8 +9,9 @@ import (
type ModelPublicWhatsappAccount struct {
bun.BaseModel `bun:"table:whatsapp_accounts,alias:whatsapp_accounts"`
ID resolvespec_common.SqlString `bun:"id,type:varchar(36),pk," json:"id"` // UUID
AccountType resolvespec_common.SqlString `bun:"account_type,type:varchar(50),notnull," json:"account_type"` // whatsmeow or business-api
ID resolvespec_common.SqlString `bun:"id,type:varchar(36),pk," json:"id"` // UUID
AccountID resolvespec_common.SqlString `bun:"account_id,type:varchar(100),unique,nullzero," json:"account_id"` // User-friendly unique identifier
AccountType resolvespec_common.SqlString `bun:"account_type,type:varchar(50),notnull," json:"account_type"` // whatsmeow or business-api
Active bool `bun:"active,type:boolean,default:true,notnull," json:"active"`
Config resolvespec_common.SqlString `bun:"config,type:text,nullzero," json:"config"` // JSON encoded additional config
CreatedAt resolvespec_common.SqlTime `bun:"created_at,type:timestamp,default:now(),notnull," json:"created_at"`

View File

@@ -2,6 +2,7 @@ package whatshooked
import (
"context"
"encoding/json"
"time"
"git.warky.dev/wdevs/whatshooked/pkg/api"
@@ -29,6 +30,7 @@ type WhatsHooked struct {
messageCache *cache.MessageCache
handlers *handlers.Handlers
apiServer *api.Server // ResolveSpec unified server
dbReady bool // Flag to indicate if database is ready
}
// NewFromFile creates a WhatsHooked instance from a config file
@@ -109,7 +111,7 @@ func newWithConfig(cfg *config.Config, configPath string) (*WhatsHooked, error)
// Initialize hook manager
wh.hookMgr = hooks.NewManager(wh.eventBus, wh.messageCache)
wh.hookMgr.LoadHooks(cfg.Hooks)
// Don't load hooks here - will be loaded from database after it's initialized
wh.hookMgr.Start()
// Initialize event logger if enabled
@@ -134,6 +136,59 @@ func newWithConfig(cfg *config.Config, configPath string) (*WhatsHooked, error)
// ConnectAll connects to all configured WhatsApp accounts
func (wh *WhatsHooked) ConnectAll(ctx context.Context) error {
// If database is ready, load accounts from database
if wh.dbReady {
return wh.connectFromDatabase(ctx)
}
// Otherwise, fall back to config file (legacy)
return wh.connectFromConfig(ctx)
}
// connectFromDatabase loads and connects WhatsApp accounts from the database
func (wh *WhatsHooked) connectFromDatabase(ctx context.Context) error {
db := storage.GetDB()
if db == nil {
logging.Warn("Database not available, skipping account connections")
return nil
}
// Load active WhatsApp accounts from database
accountRepo := storage.NewWhatsAppAccountRepository(db)
accounts, err := accountRepo.List(ctx, map[string]interface{}{"active": true})
if err != nil {
logging.Error("Failed to load WhatsApp accounts from database", "error", err)
return err
}
logging.Info("Loading WhatsApp accounts from database", "count", len(accounts))
for _, account := range accounts {
// Skip if account_id is not set
accountID := account.AccountID.String()
if accountID == "" {
accountID = account.ID.String() // Fall back to UUID if account_id not set
}
// Convert database model to config format
waCfg := config.WhatsAppConfig{
ID: accountID,
PhoneNumber: account.PhoneNumber.String(),
Type: account.AccountType.String(),
SessionPath: account.SessionPath.String(),
Disabled: !account.Active,
}
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
}
}
return nil
}
// connectFromConfig loads and connects WhatsApp accounts from config file (legacy)
func (wh *WhatsHooked) connectFromConfig(ctx context.Context) error {
for _, waCfg := range wh.config.WhatsApp {
// Skip disabled accounts
if waCfg.Disabled {
@@ -149,6 +204,59 @@ func (wh *WhatsHooked) ConnectAll(ctx context.Context) error {
return nil
}
// loadHooksFromDatabase loads webhooks from the database
func (wh *WhatsHooked) loadHooksFromDatabase(ctx context.Context) error {
db := storage.GetDB()
if db == nil {
logging.Warn("Database not available, skipping hook loading")
return nil
}
// Load active hooks from database
hookRepo := storage.NewHookRepository(db)
dbHooks, err := hookRepo.List(ctx, map[string]interface{}{"active": true})
if err != nil {
logging.Error("Failed to load hooks from database", "error", err)
return err
}
logging.Info("Loading hooks from database", "count", len(dbHooks))
// Convert database models to config format
configHooks := make([]config.Hook, 0, len(dbHooks))
for _, dbHook := range dbHooks {
hook := config.Hook{
ID: dbHook.ID.String(),
Name: dbHook.Name.String(),
URL: dbHook.URL.String(),
Method: dbHook.Method.String(),
Description: dbHook.Description.String(),
Active: dbHook.Active,
}
// Parse headers JSON if present
if headersStr := dbHook.Headers.String(); headersStr != "" {
hook.Headers = make(map[string]string)
if err := json.Unmarshal([]byte(headersStr), &hook.Headers); err != nil {
logging.Warn("Failed to parse hook headers", "hook_id", hook.ID, "error", err)
}
}
// Parse events JSON if present
if eventsStr := dbHook.Events.String(); eventsStr != "" {
if err := json.Unmarshal([]byte(eventsStr), &hook.Events); err != nil {
logging.Warn("Failed to parse hook events", "hook_id", hook.ID, "error", err)
}
}
configHooks = append(configHooks, hook)
}
// Load hooks into the hook manager
wh.hookMgr.LoadHooks(configHooks)
return nil
}
// Handlers returns the HTTP handlers instance
func (wh *WhatsHooked) Handlers() *handlers.Handlers {
return wh.handlers
@@ -218,6 +326,15 @@ func (wh *WhatsHooked) StartAPIServer(ctx context.Context) error {
return err
}
// Mark database as ready for account/hook loading
wh.dbReady = true
// Load hooks from database
if err := wh.loadHooksFromDatabase(ctx); err != nil {
logging.Error("Failed to load hooks from database", "error", err)
// Continue anyway, hooks can be added later
}
// Create unified server
logging.Info("Creating ResolveSpec server", "host", wh.config.Server.Host, "port", wh.config.Server.Port)
apiServer, err := api.NewServer(wh.config, db, wh)