Major refactor to library

This commit is contained in:
2025-12-29 09:51:16 +02:00
parent ae169f81e4
commit 767a9e211f
38 changed files with 1073 additions and 492 deletions

View File

@@ -4,37 +4,20 @@ import (
"context"
"flag"
"fmt"
"net/http"
"os"
"os/signal"
"path/filepath"
"syscall"
"time"
"git.warky.dev/wdevs/whatshooked/internal/config"
"git.warky.dev/wdevs/whatshooked/internal/eventlogger"
"git.warky.dev/wdevs/whatshooked/internal/events"
"git.warky.dev/wdevs/whatshooked/internal/hooks"
"git.warky.dev/wdevs/whatshooked/internal/logging"
"git.warky.dev/wdevs/whatshooked/internal/utils"
"git.warky.dev/wdevs/whatshooked/internal/whatsapp"
"go.mau.fi/whatsmeow/types"
"git.warky.dev/wdevs/whatshooked/pkg/logging"
"git.warky.dev/wdevs/whatshooked/pkg/whatshooked"
)
var (
configPath = flag.String("config", "", "Path to configuration file (optional, defaults to user home directory)")
)
type Server struct {
config *config.Config
configPath string
whatsappMgr *whatsapp.Manager
hookMgr *hooks.Manager
httpServer *http.Server
eventBus *events.EventBus
eventLogger *eventlogger.Logger
}
// resolveConfigPath determines the config file path to use
// Priority: 1) provided path (if exists), 2) config.json in current dir, 3) .whatshooked/config.json in user home
func resolveConfigPath(providedPath string) (string, error) {
@@ -44,7 +27,7 @@ func resolveConfigPath(providedPath string) (string, error) {
return providedPath, nil
}
// Directory doesn't exist, fall through to default locations
logging.Info("Provided config path directory does not exist, using default locations", "path", providedPath)
fmt.Fprintf(os.Stderr, "Provided config path directory does not exist, using default locations: %s\n", providedPath)
}
// Check for config.json in current directory
@@ -78,70 +61,21 @@ func main() {
os.Exit(1)
}
// Load configuration
cfg, err := config.Load(cfgPath)
// Create WhatsHooked instance from config file
wh, err := whatshooked.NewFromFile(cfgPath)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to load config from %s: %v\n", cfgPath, err)
fmt.Fprintf(os.Stderr, "Failed to initialize WhatsHooked from %s: %v\n", cfgPath, err)
os.Exit(1)
}
// Initialize logging
logging.Init(cfg.LogLevel)
logging.Info("Starting WhatsHooked server", "config_path", cfgPath)
// Create event bus
eventBus := events.NewEventBus()
// Create server with config update callback
srv := &Server{
config: cfg,
configPath: cfgPath,
eventBus: eventBus,
whatsappMgr: whatsapp.NewManager(eventBus, cfg.Media, cfg, cfgPath, func(updatedCfg *config.Config) error {
return config.Save(cfgPath, updatedCfg)
}),
hookMgr: hooks.NewManager(eventBus),
}
// Initialize event logger if enabled
if cfg.EventLogger.Enabled && len(cfg.EventLogger.Targets) > 0 {
evtLogger, err := eventlogger.NewLogger(cfg.EventLogger, cfg.Database)
if err != nil {
logging.Error("Failed to initialize event logger", "error", err)
} else {
srv.eventLogger = evtLogger
// Subscribe to all events
srv.eventBus.SubscribeAll(func(event events.Event) {
srv.eventLogger.Log(event)
})
logging.Info("Event logger initialized", "targets", cfg.EventLogger.Targets)
// Start the built-in HTTP server (non-blocking goroutine)
go func() {
if err := wh.StartServer(); err != nil {
logging.Error("HTTP server error", "error", err)
}
}
// Load hooks
srv.hookMgr.LoadHooks(cfg.Hooks)
// Start hook manager to listen for events
srv.hookMgr.Start()
// Subscribe to hook success events to handle webhook responses
srv.eventBus.Subscribe(events.EventHookSuccess, srv.handleHookResponse)
// Start HTTP server for CLI BEFORE connecting to WhatsApp
// This ensures all infrastructure is ready before events start flowing
srv.startHTTPServer()
// Give HTTP server a moment to start
time.Sleep(100 * time.Millisecond)
logging.Info("HTTP server ready, connecting to WhatsApp accounts")
// Connect to WhatsApp accounts
ctx := context.Background()
for _, waCfg := range cfg.WhatsApp {
if err := srv.whatsappMgr.Connect(ctx, waCfg); err != nil {
logging.Error("Failed to connect to WhatsApp", "account_id", waCfg.ID, "error", err)
}
}
}()
// Wait for interrupt signal
sigChan := make(chan os.Signal, 1)
@@ -154,66 +88,15 @@ func main() {
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if srv.httpServer != nil {
srv.httpServer.Shutdown(shutdownCtx)
// Stop server
if err := wh.StopServer(shutdownCtx); err != nil {
logging.Error("Error stopping server", "error", err)
}
srv.whatsappMgr.DisconnectAll()
// Close event logger
if srv.eventLogger != nil {
if err := srv.eventLogger.Close(); err != nil {
logging.Error("Failed to close event logger", "error", err)
}
// Close WhatsHooked (disconnects WhatsApp, closes event logger, etc.)
if err := wh.Close(); err != nil {
logging.Error("Error closing WhatsHooked", "error", err)
}
logging.Info("Server stopped")
}
// handleHookResponse processes hook success events to handle two-way communication
func (s *Server) handleHookResponse(event events.Event) {
// Use event context for sending message
ctx := event.Context
if ctx == nil {
ctx = context.Background()
}
// Extract response from event data
responseData, ok := event.Data["response"]
if !ok || responseData == nil {
return
}
// Try to cast to HookResponse
resp, ok := responseData.(hooks.HookResponse)
if !ok {
return
}
if !resp.SendMessage {
return
}
// Determine which account to use - default to first available if not specified
targetAccountID := resp.AccountID
if targetAccountID == "" && len(s.config.WhatsApp) > 0 {
targetAccountID = s.config.WhatsApp[0].ID
}
// Format phone number to JID format
formattedJID := utils.FormatPhoneToJID(resp.To, s.config.Server.DefaultCountryCode)
// Parse JID
jid, err := types.ParseJID(formattedJID)
if err != nil {
logging.Error("Invalid JID in hook response", "jid", formattedJID, "error", err)
return
}
// Send message with context
if err := s.whatsappMgr.SendTextMessage(ctx, targetAccountID, jid, resp.Text); err != nil {
logging.Error("Failed to send message from hook response", "error", err)
} else {
logging.Info("Message sent from hook response", "account_id", targetAccountID, "to", resp.To)
}
}