Files
whatshooked/cmd/server/main.go
2025-12-29 05:42:57 +02:00

195 lines
5.3 KiB
Go

package main
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/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"
)
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
}
// 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) {
// If a path was explicitly provided, check if it exists
if providedPath != "" {
if _, err := os.Stat(providedPath); err == nil {
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)
}
// Check for config.json in current directory
currentDirConfig := "config.json"
if _, err := os.Stat(currentDirConfig); err == nil {
return currentDirConfig, nil
}
// Fall back to user home directory
homeDir, err := os.UserHomeDir()
if err != nil {
return "", fmt.Errorf("failed to get user home directory: %w", err)
}
// Create .whatshooked directory if it doesn't exist
configDir := filepath.Join(homeDir, ".whatshooked")
if err := os.MkdirAll(configDir, 0755); err != nil {
return "", fmt.Errorf("failed to create config directory: %w", err)
}
return filepath.Join(configDir, "config.json"), nil
}
func main() {
flag.Parse()
// Resolve config path
cfgPath, err := resolveConfigPath(*configPath)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to resolve config path: %v\n", err)
os.Exit(1)
}
// Load configuration
cfg, err := config.Load(cfgPath)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to load config 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),
}
// 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)
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
<-sigChan
logging.Info("Shutting down server")
// Graceful shutdown
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if srv.httpServer != nil {
srv.httpServer.Shutdown(shutdownCtx)
}
srv.whatsappMgr.DisconnectAll()
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)
}
}