292 lines
8.3 KiB
Go
292 lines
8.3 KiB
Go
package whatshooked
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"fmt"
|
|
"net/http"
|
|
"time"
|
|
|
|
"git.warky.dev/wdevs/whatshooked/pkg/config"
|
|
"git.warky.dev/wdevs/whatshooked/pkg/events"
|
|
"git.warky.dev/wdevs/whatshooked/pkg/hooks"
|
|
"git.warky.dev/wdevs/whatshooked/pkg/logging"
|
|
"git.warky.dev/wdevs/whatshooked/pkg/utils"
|
|
"go.mau.fi/whatsmeow/types"
|
|
"golang.org/x/crypto/acme/autocert"
|
|
)
|
|
|
|
// Server is the optional built-in HTTP server
|
|
type Server struct {
|
|
wh *WhatsHooked
|
|
httpServer *http.Server
|
|
}
|
|
|
|
// NewServer creates a new HTTP server instance
|
|
func NewServer(wh *WhatsHooked) *Server {
|
|
return &Server{
|
|
wh: wh,
|
|
}
|
|
}
|
|
|
|
// Start starts the HTTP/HTTPS server
|
|
func (s *Server) Start() error {
|
|
// Subscribe to hook success events for two-way communication
|
|
s.wh.EventBus().Subscribe(events.EventHookSuccess, s.handleHookResponse)
|
|
|
|
// Setup routes
|
|
mux := s.setupRoutes()
|
|
|
|
addr := fmt.Sprintf("%s:%d", s.wh.config.Server.Host, s.wh.config.Server.Port)
|
|
s.httpServer = &http.Server{
|
|
Addr: addr,
|
|
Handler: mux,
|
|
}
|
|
|
|
// Connect to WhatsApp accounts after server starts
|
|
go func() {
|
|
time.Sleep(100 * time.Millisecond) // Give server a moment to start
|
|
logging.Info("Server ready, connecting to WhatsApp accounts")
|
|
if err := s.wh.ConnectAll(context.Background()); err != nil {
|
|
logging.Error("Failed to connect to WhatsApp accounts", "error", err)
|
|
}
|
|
}()
|
|
|
|
// Start server with or without TLS
|
|
if s.wh.config.Server.TLS.Enabled {
|
|
return s.startTLS()
|
|
}
|
|
|
|
logging.Info("Starting HTTP server",
|
|
"host", s.wh.config.Server.Host,
|
|
"port", s.wh.config.Server.Port,
|
|
"address", addr)
|
|
|
|
// Start server (blocking)
|
|
if err := s.httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// startTLS starts the server with TLS based on the configured mode
|
|
func (s *Server) startTLS() error {
|
|
tlsConfig := &s.wh.config.Server.TLS
|
|
addr := fmt.Sprintf("%s:%d", s.wh.config.Server.Host, s.wh.config.Server.Port)
|
|
|
|
switch tlsConfig.Mode {
|
|
case "self-signed":
|
|
return s.startSelfSignedTLS(tlsConfig, addr)
|
|
case "custom":
|
|
return s.startCustomTLS(tlsConfig, addr)
|
|
case "autocert":
|
|
return s.startAutocertTLS(tlsConfig, addr)
|
|
default:
|
|
return fmt.Errorf("invalid TLS mode: %s (must be 'self-signed', 'custom', or 'autocert')", tlsConfig.Mode)
|
|
}
|
|
}
|
|
|
|
// startSelfSignedTLS starts the server with a self-signed certificate
|
|
func (s *Server) startSelfSignedTLS(tlsConfig *config.TLSConfig, addr string) error {
|
|
logging.Info("Generating/loading self-signed certificate",
|
|
"cert_dir", tlsConfig.CertDir,
|
|
"host", s.wh.config.Server.Host)
|
|
|
|
certPath, keyPath, err := utils.GenerateSelfSignedCert(tlsConfig.CertDir, s.wh.config.Server.Host)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to generate self-signed certificate: %w", err)
|
|
}
|
|
|
|
logging.Info("Starting HTTPS server with self-signed certificate",
|
|
"host", s.wh.config.Server.Host,
|
|
"port", s.wh.config.Server.Port,
|
|
"address", addr,
|
|
"cert", certPath,
|
|
"key", keyPath)
|
|
|
|
if err := s.httpServer.ListenAndServeTLS(certPath, keyPath); err != nil && err != http.ErrServerClosed {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// startCustomTLS starts the server with custom certificate files
|
|
func (s *Server) startCustomTLS(tlsConfig *config.TLSConfig, addr string) error {
|
|
if tlsConfig.CertFile == "" || tlsConfig.KeyFile == "" {
|
|
return fmt.Errorf("custom TLS mode requires cert_file and key_file to be specified")
|
|
}
|
|
|
|
logging.Info("Validating custom TLS certificates",
|
|
"cert", tlsConfig.CertFile,
|
|
"key", tlsConfig.KeyFile)
|
|
|
|
// Validate certificate files
|
|
if err := utils.ValidateCertificateFiles(tlsConfig.CertFile, tlsConfig.KeyFile); err != nil {
|
|
return fmt.Errorf("invalid certificate files: %w", err)
|
|
}
|
|
|
|
logging.Info("Starting HTTPS server with custom certificate",
|
|
"host", s.wh.config.Server.Host,
|
|
"port", s.wh.config.Server.Port,
|
|
"address", addr,
|
|
"cert", tlsConfig.CertFile,
|
|
"key", tlsConfig.KeyFile)
|
|
|
|
if err := s.httpServer.ListenAndServeTLS(tlsConfig.CertFile, tlsConfig.KeyFile); err != nil && err != http.ErrServerClosed {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// startAutocertTLS starts the server with Let's Encrypt autocert
|
|
func (s *Server) startAutocertTLS(tlsConfig *config.TLSConfig, addr string) error {
|
|
if tlsConfig.Domain == "" {
|
|
return fmt.Errorf("autocert mode requires domain to be specified")
|
|
}
|
|
|
|
logging.Info("Setting up Let's Encrypt autocert",
|
|
"domain", tlsConfig.Domain,
|
|
"email", tlsConfig.Email,
|
|
"cache_dir", tlsConfig.CacheDir,
|
|
"production", tlsConfig.Production)
|
|
|
|
// Create autocert manager
|
|
certManager := &autocert.Manager{
|
|
Prompt: autocert.AcceptTOS,
|
|
HostPolicy: autocert.HostWhitelist(tlsConfig.Domain),
|
|
Cache: autocert.DirCache(tlsConfig.CacheDir),
|
|
Email: tlsConfig.Email,
|
|
}
|
|
|
|
// Configure TLS
|
|
s.httpServer.TLSConfig = &tls.Config{
|
|
GetCertificate: certManager.GetCertificate,
|
|
MinVersion: tls.VersionTLS12,
|
|
}
|
|
|
|
// Start HTTP-01 challenge server on port 80 if we're listening on 443
|
|
if s.wh.config.Server.Port == 443 {
|
|
go func() {
|
|
httpAddr := fmt.Sprintf("%s:80", s.wh.config.Server.Host)
|
|
logging.Info("Starting HTTP server for ACME challenges", "address", httpAddr)
|
|
if err := http.ListenAndServe(httpAddr, certManager.HTTPHandler(nil)); err != nil {
|
|
logging.Error("Failed to start HTTP challenge server", "error", err)
|
|
}
|
|
}()
|
|
}
|
|
|
|
logging.Info("Starting HTTPS server with Let's Encrypt",
|
|
"host", s.wh.config.Server.Host,
|
|
"port", s.wh.config.Server.Port,
|
|
"address", addr,
|
|
"domain", tlsConfig.Domain)
|
|
|
|
if err := s.httpServer.ListenAndServeTLS("", ""); err != nil && err != http.ErrServerClosed {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Stop stops the HTTP server gracefully
|
|
func (s *Server) Stop(ctx context.Context) error {
|
|
if s.httpServer != nil {
|
|
return s.httpServer.Shutdown(ctx)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// setupRoutes configures all HTTP routes
|
|
func (s *Server) setupRoutes() *http.ServeMux {
|
|
mux := http.NewServeMux()
|
|
h := s.wh.Handlers()
|
|
|
|
// Health check (no auth required)
|
|
mux.HandleFunc("/health", h.Health)
|
|
|
|
// Hook management (with auth)
|
|
mux.HandleFunc("/api/hooks", h.Auth(h.Hooks))
|
|
mux.HandleFunc("/api/hooks/add", h.Auth(h.AddHook))
|
|
mux.HandleFunc("/api/hooks/remove", h.Auth(h.RemoveHook))
|
|
|
|
// Account management (with auth)
|
|
mux.HandleFunc("/api/accounts", h.Auth(h.Accounts))
|
|
mux.HandleFunc("/api/accounts/add", h.Auth(h.AddAccount))
|
|
mux.HandleFunc("/api/accounts/remove", h.Auth(h.RemoveAccount))
|
|
|
|
// Send messages (with auth)
|
|
mux.HandleFunc("/api/send", h.Auth(h.SendMessage))
|
|
mux.HandleFunc("/api/send/image", h.Auth(h.SendImage))
|
|
mux.HandleFunc("/api/send/video", h.Auth(h.SendVideo))
|
|
mux.HandleFunc("/api/send/document", h.Auth(h.SendDocument))
|
|
|
|
// Serve media files (with auth)
|
|
mux.HandleFunc("/api/media/", h.ServeMedia)
|
|
|
|
// Serve QR codes (no auth - needed during pairing)
|
|
mux.HandleFunc("/api/qr/", h.ServeQRCode)
|
|
|
|
// Business API webhooks (no auth - Meta validates via verify_token)
|
|
mux.HandleFunc("/webhooks/whatsapp/", h.BusinessAPIWebhook)
|
|
|
|
logging.Info("HTTP server endpoints configured",
|
|
"health", "/health",
|
|
"hooks", "/api/hooks",
|
|
"accounts", "/api/accounts",
|
|
"send", "/api/send",
|
|
"qr", "/api/qr")
|
|
|
|
return mux
|
|
}
|
|
|
|
// handleHookResponse processes hook success events for 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.wh.config.WhatsApp) > 0 {
|
|
targetAccountID = s.wh.config.WhatsApp[0].ID
|
|
}
|
|
|
|
// Format phone number to JID format
|
|
formattedJID := utils.FormatPhoneToJID(resp.To, s.wh.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.wh.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)
|
|
}
|
|
}
|