Major refactor to library
This commit is contained in:
133
pkg/whatshooked/options.go
Normal file
133
pkg/whatshooked/options.go
Normal file
@@ -0,0 +1,133 @@
|
||||
package whatshooked
|
||||
|
||||
import "git.warky.dev/wdevs/whatshooked/pkg/config"
|
||||
|
||||
// Option is a functional option for configuring WhatsHooked
|
||||
type Option func(*config.Config)
|
||||
|
||||
// WithServer configures server settings
|
||||
func WithServer(host string, port int) Option {
|
||||
return func(c *config.Config) {
|
||||
c.Server.Host = host
|
||||
c.Server.Port = port
|
||||
}
|
||||
}
|
||||
|
||||
// WithAuth configures authentication
|
||||
func WithAuth(apiKey, username, password string) Option {
|
||||
return func(c *config.Config) {
|
||||
c.Server.AuthKey = apiKey
|
||||
c.Server.Username = username
|
||||
c.Server.Password = password
|
||||
}
|
||||
}
|
||||
|
||||
// WithDefaultCountryCode sets the default country code for phone numbers
|
||||
func WithDefaultCountryCode(code string) Option {
|
||||
return func(c *config.Config) {
|
||||
c.Server.DefaultCountryCode = code
|
||||
}
|
||||
}
|
||||
|
||||
// WithWhatsAppAccount adds a WhatsApp account
|
||||
func WithWhatsAppAccount(account config.WhatsAppConfig) Option {
|
||||
return func(c *config.Config) {
|
||||
c.WhatsApp = append(c.WhatsApp, account)
|
||||
}
|
||||
}
|
||||
|
||||
// WithWhatsmeowAccount adds a Whatsmeow account (convenience)
|
||||
func WithWhatsmeowAccount(id, phoneNumber, sessionPath string, showQR bool) Option {
|
||||
return func(c *config.Config) {
|
||||
c.WhatsApp = append(c.WhatsApp, config.WhatsAppConfig{
|
||||
ID: id,
|
||||
Type: "whatsmeow",
|
||||
PhoneNumber: phoneNumber,
|
||||
SessionPath: sessionPath,
|
||||
ShowQR: showQR,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// WithBusinessAPIAccount adds a Business API account (convenience)
|
||||
func WithBusinessAPIAccount(id, phoneNumber, phoneNumberID, accessToken, verifyToken string) Option {
|
||||
return func(c *config.Config) {
|
||||
c.WhatsApp = append(c.WhatsApp, config.WhatsAppConfig{
|
||||
ID: id,
|
||||
Type: "business-api",
|
||||
PhoneNumber: phoneNumber,
|
||||
BusinessAPI: &config.BusinessAPIConfig{
|
||||
PhoneNumberID: phoneNumberID,
|
||||
AccessToken: accessToken,
|
||||
VerifyToken: verifyToken,
|
||||
APIVersion: "v21.0",
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// WithHook adds a webhook
|
||||
func WithHook(hook config.Hook) Option {
|
||||
return func(c *config.Config) {
|
||||
c.Hooks = append(c.Hooks, hook)
|
||||
}
|
||||
}
|
||||
|
||||
// WithEventLogger enables event logging
|
||||
func WithEventLogger(targets []string, fileDir string) Option {
|
||||
return func(c *config.Config) {
|
||||
c.EventLogger.Enabled = true
|
||||
c.EventLogger.Targets = targets
|
||||
c.EventLogger.FileDir = fileDir
|
||||
}
|
||||
}
|
||||
|
||||
// WithEventLoggerConfig configures event logger with full config
|
||||
func WithEventLoggerConfig(cfg config.EventLoggerConfig) Option {
|
||||
return func(c *config.Config) {
|
||||
c.EventLogger = cfg
|
||||
}
|
||||
}
|
||||
|
||||
// WithMedia configures media settings
|
||||
func WithMedia(dataPath, mode, baseURL string) Option {
|
||||
return func(c *config.Config) {
|
||||
c.Media.DataPath = dataPath
|
||||
c.Media.Mode = mode
|
||||
c.Media.BaseURL = baseURL
|
||||
}
|
||||
}
|
||||
|
||||
// WithMediaConfig configures media with full config
|
||||
func WithMediaConfig(cfg config.MediaConfig) Option {
|
||||
return func(c *config.Config) {
|
||||
c.Media = cfg
|
||||
}
|
||||
}
|
||||
|
||||
// WithLogLevel sets the log level
|
||||
func WithLogLevel(level string) Option {
|
||||
return func(c *config.Config) {
|
||||
c.LogLevel = level
|
||||
}
|
||||
}
|
||||
|
||||
// WithDatabase configures database settings
|
||||
func WithDatabase(dbType, host string, port int, username, password, database string) Option {
|
||||
return func(c *config.Config) {
|
||||
c.Database.Type = dbType
|
||||
c.Database.Host = host
|
||||
c.Database.Port = port
|
||||
c.Database.Username = username
|
||||
c.Database.Password = password
|
||||
c.Database.Database = database
|
||||
}
|
||||
}
|
||||
|
||||
// WithSQLiteDatabase configures SQLite database
|
||||
func WithSQLiteDatabase(sqlitePath string) Option {
|
||||
return func(c *config.Config) {
|
||||
c.Database.Type = "sqlite"
|
||||
c.Database.SQLitePath = sqlitePath
|
||||
}
|
||||
}
|
||||
157
pkg/whatshooked/server.go
Normal file
157
pkg/whatshooked/server.go
Normal file
@@ -0,0 +1,157 @@
|
||||
package whatshooked
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
// 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 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,
|
||||
}
|
||||
|
||||
logging.Info("Starting HTTP server",
|
||||
"host", s.wh.config.Server.Host,
|
||||
"port", s.wh.config.Server.Port,
|
||||
"address", addr)
|
||||
|
||||
// Connect to WhatsApp accounts
|
||||
go func() {
|
||||
time.Sleep(100 * time.Millisecond) // Give HTTP server a moment to start
|
||||
logging.Info("HTTP 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 (blocking)
|
||||
if err := s.httpServer.ListenAndServe(); 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))
|
||||
|
||||
// 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)
|
||||
|
||||
// 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")
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
169
pkg/whatshooked/whatshooked.go
Normal file
169
pkg/whatshooked/whatshooked.go
Normal file
@@ -0,0 +1,169 @@
|
||||
package whatshooked
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.warky.dev/wdevs/whatshooked/pkg/config"
|
||||
"git.warky.dev/wdevs/whatshooked/pkg/eventlogger"
|
||||
"git.warky.dev/wdevs/whatshooked/pkg/events"
|
||||
"git.warky.dev/wdevs/whatshooked/pkg/handlers"
|
||||
"git.warky.dev/wdevs/whatshooked/pkg/hooks"
|
||||
"git.warky.dev/wdevs/whatshooked/pkg/logging"
|
||||
"git.warky.dev/wdevs/whatshooked/pkg/whatsapp"
|
||||
)
|
||||
|
||||
// WhatsHooked is the main library instance
|
||||
type WhatsHooked struct {
|
||||
config *config.Config
|
||||
configPath string
|
||||
eventBus *events.EventBus
|
||||
whatsappMgr *whatsapp.Manager
|
||||
hookMgr *hooks.Manager
|
||||
eventLogger *eventlogger.Logger
|
||||
handlers *handlers.Handlers
|
||||
server *Server // Optional built-in server
|
||||
}
|
||||
|
||||
// NewFromFile creates a WhatsHooked instance from a config file
|
||||
func NewFromFile(configPath string) (*WhatsHooked, error) {
|
||||
cfg, err := config.Load(configPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Initialize logging from config
|
||||
logging.Init(cfg.LogLevel)
|
||||
|
||||
return newWithConfig(cfg, configPath)
|
||||
}
|
||||
|
||||
// New creates a WhatsHooked instance with programmatic config
|
||||
func New(opts ...Option) (*WhatsHooked, error) {
|
||||
cfg := &config.Config{
|
||||
Server: config.ServerConfig{
|
||||
Host: "localhost",
|
||||
Port: 8080,
|
||||
},
|
||||
Media: config.MediaConfig{
|
||||
DataPath: "./data/media",
|
||||
Mode: "link",
|
||||
},
|
||||
LogLevel: "info",
|
||||
}
|
||||
|
||||
// Apply options
|
||||
for _, opt := range opts {
|
||||
opt(cfg)
|
||||
}
|
||||
|
||||
// Initialize logging from config
|
||||
logging.Init(cfg.LogLevel)
|
||||
|
||||
return newWithConfig(cfg, "")
|
||||
}
|
||||
|
||||
// newWithConfig is the internal constructor
|
||||
func newWithConfig(cfg *config.Config, configPath string) (*WhatsHooked, error) {
|
||||
wh := &WhatsHooked{
|
||||
config: cfg,
|
||||
configPath: configPath,
|
||||
eventBus: events.NewEventBus(),
|
||||
}
|
||||
|
||||
// Initialize WhatsApp manager
|
||||
wh.whatsappMgr = whatsapp.NewManager(
|
||||
wh.eventBus,
|
||||
cfg.Media,
|
||||
cfg,
|
||||
configPath,
|
||||
func(updatedCfg *config.Config) error {
|
||||
if configPath != "" {
|
||||
return config.Save(configPath, updatedCfg)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
)
|
||||
|
||||
// Initialize hook manager
|
||||
wh.hookMgr = hooks.NewManager(wh.eventBus)
|
||||
wh.hookMgr.LoadHooks(cfg.Hooks)
|
||||
wh.hookMgr.Start()
|
||||
|
||||
// Initialize event logger if enabled
|
||||
if cfg.EventLogger.Enabled && len(cfg.EventLogger.Targets) > 0 {
|
||||
logger, err := eventlogger.NewLogger(cfg.EventLogger, cfg.Database)
|
||||
if err == nil {
|
||||
wh.eventLogger = logger
|
||||
wh.eventBus.SubscribeAll(func(event events.Event) {
|
||||
wh.eventLogger.Log(event)
|
||||
})
|
||||
logging.Info("Event logger initialized", "targets", cfg.EventLogger.Targets)
|
||||
} else {
|
||||
logging.Error("Failed to initialize event logger", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Create handlers
|
||||
wh.handlers = handlers.New(wh.whatsappMgr, wh.hookMgr, cfg, configPath)
|
||||
|
||||
return wh, nil
|
||||
}
|
||||
|
||||
// ConnectAll connects to all configured WhatsApp accounts
|
||||
func (wh *WhatsHooked) ConnectAll(ctx context.Context) error {
|
||||
for _, waCfg := range wh.config.WhatsApp {
|
||||
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
|
||||
}
|
||||
|
||||
// Handlers returns the HTTP handlers instance
|
||||
func (wh *WhatsHooked) Handlers() *handlers.Handlers {
|
||||
return wh.handlers
|
||||
}
|
||||
|
||||
// Manager returns the WhatsApp manager
|
||||
func (wh *WhatsHooked) Manager() *whatsapp.Manager {
|
||||
return wh.whatsappMgr
|
||||
}
|
||||
|
||||
// EventBus returns the event bus
|
||||
func (wh *WhatsHooked) EventBus() *events.EventBus {
|
||||
return wh.eventBus
|
||||
}
|
||||
|
||||
// HookManager returns the hook manager
|
||||
func (wh *WhatsHooked) HookManager() *hooks.Manager {
|
||||
return wh.hookMgr
|
||||
}
|
||||
|
||||
// Config returns the configuration
|
||||
func (wh *WhatsHooked) Config() *config.Config {
|
||||
return wh.config
|
||||
}
|
||||
|
||||
// Close shuts down all components gracefully
|
||||
func (wh *WhatsHooked) Close() error {
|
||||
wh.whatsappMgr.DisconnectAll()
|
||||
if wh.eventLogger != nil {
|
||||
return wh.eventLogger.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// StartServer starts the built-in HTTP server (convenience method)
|
||||
func (wh *WhatsHooked) StartServer() error {
|
||||
wh.server = NewServer(wh)
|
||||
return wh.server.Start()
|
||||
}
|
||||
|
||||
// StopServer stops the built-in HTTP server
|
||||
func (wh *WhatsHooked) StopServer(ctx context.Context) error {
|
||||
if wh.server != nil {
|
||||
return wh.server.Stop(ctx)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user