package hook import ( "context" "sync" "time" "go.uber.org/zap" ) type HookType string const ( HookPostCreated HookType = "post.created" HookPostUpdated HookType = "post.updated" HookPostDeleted HookType = "post.deleted" HookPostPreModerate HookType = "post.pre_moderate" HookPostModerated HookType = "post.moderated" HookCommentCreated HookType = "comment.created" HookCommentDeleted HookType = "comment.deleted" HookCommentPreModerate HookType = "comment.pre_moderate" HookCommentModerated HookType = "comment.moderated" HookUserCreated HookType = "user.created" HookUserUpdated HookType = "user.updated" HookUserDeleted HookType = "user.deleted" HookUserLogin HookType = "user.login" HookUserLogout HookType = "user.logout" HookGroupCreated HookType = "group.created" HookGroupUpdated HookType = "group.updated" HookGroupDeleted HookType = "group.deleted" HookGroupJoined HookType = "group.joined" HookGroupLeft HookType = "group.left" HookMessageSent HookType = "message.sent" HookMessageRead HookType = "message.read" HookNotificationSent HookType = "notification.sent" HookVoteCreated HookType = "vote.created" HookVoteUpdated HookType = "vote.updated" HookFileUploaded HookType = "file.uploaded" ) type Priority int const ( PriorityLowest Priority = 0 PriorityLow Priority = 25 PriorityNormal Priority = 50 PriorityHigh Priority = 75 PriorityHighest Priority = 100 ) type HookContext struct { Ctx context.Context HookType HookType UserID uint Data any Metadata map[string]any } type HookFunc func(ctx *HookContext) error type Hook struct { Name string HookType HookType Func HookFunc Priority Priority Async bool Timeout time.Duration Enabled bool } type HookStats struct { TotalCalls int64 FailedCalls int64 TotalTime time.Duration AverageTime time.Duration LastCalledAt time.Time } type hookEntry struct { hook *Hook stats HookStats mu sync.Mutex } type Manager struct { hooks map[HookType][]*hookEntry mu sync.RWMutex asyncPool chan struct{} enabled bool metrics *Metrics } type ManagerOptions struct { MaxConcurrentAsync int Enabled bool } func NewManager(opts *ManagerOptions) *Manager { if opts == nil { opts = &ManagerOptions{ MaxConcurrentAsync: 100, Enabled: true, } } if opts.MaxConcurrentAsync <= 0 { opts.MaxConcurrentAsync = 100 } return &Manager{ hooks: make(map[HookType][]*hookEntry), asyncPool: make(chan struct{}, opts.MaxConcurrentAsync), enabled: opts.Enabled, metrics: NewMetrics(), } } func (m *Manager) Register(hook *Hook) { if hook == nil || hook.Func == nil { return } if hook.Priority == 0 { hook.Priority = PriorityNormal } if hook.Timeout == 0 { hook.Timeout = 30 * time.Second } hook.Enabled = true entry := &hookEntry{ hook: hook, stats: HookStats{}, } m.mu.Lock() defer m.mu.Unlock() hooks := m.hooks[hook.HookType] hooks = append(hooks, entry) for i := len(hooks) - 1; i > 0; i-- { if hooks[i].hook.Priority > hooks[i-1].hook.Priority { hooks[i], hooks[i-1] = hooks[i-1], hooks[i] } else { break } } m.hooks[hook.HookType] = hooks zap.L().Debug("Hook registered", zap.String("name", hook.Name), zap.String("type", string(hook.HookType)), zap.Int("priority", int(hook.Priority)), ) } func (m *Manager) RegisterFunc(hookType HookType, name string, fn HookFunc, opts ...HookOption) { hook := &Hook{ Name: name, HookType: hookType, Func: fn, } for _, opt := range opts { opt(hook) } m.Register(hook) } type HookOption func(*Hook) func WithPriority(p Priority) HookOption { return func(h *Hook) { h.Priority = p } } func WithAsync() HookOption { return func(h *Hook) { h.Async = true } } func WithTimeout(timeout time.Duration) HookOption { return func(h *Hook) { h.Timeout = timeout } } func (m *Manager) Trigger(ctx context.Context, hookType HookType, userID uint, data any) error { return m.TriggerWithMetadata(ctx, hookType, userID, data, nil) } func (m *Manager) TriggerWithMetadata(ctx context.Context, hookType HookType, userID uint, data any, metadata map[string]any) error { if !m.enabled { return nil } m.mu.RLock() entries := m.hooks[hookType] m.mu.RUnlock() if len(entries) == 0 { return nil } hookCtx := &HookContext{ Ctx: ctx, HookType: hookType, UserID: userID, Data: data, Metadata: metadata, } var asyncHooks []*hookEntry for _, entry := range entries { if !entry.hook.Enabled { continue } if entry.hook.Async { asyncHooks = append(asyncHooks, entry) continue } if err := m.executeHook(entry, hookCtx); err != nil { zap.L().Error("Hook execution failed", zap.String("name", entry.hook.Name), zap.String("type", string(hookType)), zap.Error(err), ) } } for _, entry := range asyncHooks { entry := entry go func() { select { case m.asyncPool <- struct{}{}: defer func() { <-m.asyncPool }() if err := m.executeHook(entry, hookCtx); err != nil { zap.L().Error("Async hook execution failed", zap.String("name", entry.hook.Name), zap.String("type", string(hookType)), zap.Error(err), ) } default: zap.L().Warn("Async hook pool full, skipping hook", zap.String("name", entry.hook.Name), zap.String("type", string(hookType)), ) } }() } return nil } func (m *Manager) executeHook(entry *hookEntry, ctx *HookContext) error { start := time.Now() var err error if entry.hook.Timeout > 0 { done := make(chan error, 1) go func() { defer func() { if r := recover(); r != nil { zap.L().Error("Hook panic recovered", zap.String("name", entry.hook.Name), zap.Any("panic", r), ) done <- nil } }() done <- entry.hook.Func(ctx) }() select { case err = <-done: case <-time.After(entry.hook.Timeout): err = context.DeadlineExceeded zap.L().Warn("Hook timeout", zap.String("name", entry.hook.Name), zap.Duration("timeout", entry.hook.Timeout), ) } } else { err = entry.hook.Func(ctx) } duration := time.Since(start) entry.mu.Lock() entry.stats.TotalCalls++ entry.stats.TotalTime += duration entry.stats.AverageTime = time.Duration(int64(entry.stats.TotalTime) / entry.stats.TotalCalls) entry.stats.LastCalledAt = time.Now() if err != nil { entry.stats.FailedCalls++ } entry.mu.Unlock() m.metrics.RecordHookExecution(entry.hook.Name, entry.hook.HookType, duration, err == nil) return err } func (m *Manager) Unregister(hookType HookType, name string) { m.mu.Lock() defer m.mu.Unlock() entries := m.hooks[hookType] for i, entry := range entries { if entry.hook.Name == name { m.hooks[hookType] = append(entries[:i], entries[i+1:]...) zap.L().Debug("Hook unregistered", zap.String("name", name), zap.String("type", string(hookType)), ) break } } } func (m *Manager) Enable(hookType HookType, name string) { m.mu.RLock() entries := m.hooks[hookType] m.mu.RUnlock() for _, entry := range entries { if entry.hook.Name == name { entry.hook.Enabled = true break } } } func (m *Manager) Disable(hookType HookType, name string) { m.mu.RLock() entries := m.hooks[hookType] m.mu.RUnlock() for _, entry := range entries { if entry.hook.Name == name { entry.hook.Enabled = false break } } } func (m *Manager) GetStats(hookType HookType, name string) *HookStats { m.mu.RLock() entries := m.hooks[hookType] m.mu.RUnlock() for _, entry := range entries { if entry.hook.Name == name { entry.mu.Lock() stats := entry.stats entry.mu.Unlock() return &stats } } return nil } func (m *Manager) GetAllStats() map[HookType]map[string]HookStats { m.mu.RLock() defer m.mu.RUnlock() result := make(map[HookType]map[string]HookStats) for hookType, entries := range m.hooks { result[hookType] = make(map[string]HookStats) for _, entry := range entries { entry.mu.Lock() result[hookType][entry.hook.Name] = entry.stats entry.mu.Unlock() } } return result } func (m *Manager) GetMetrics() *Metrics { return m.metrics } func (m *Manager) Clear(hookType HookType) { m.mu.Lock() defer m.mu.Unlock() delete(m.hooks, hookType) } func (m *Manager) ClearAll() { m.mu.Lock() defer m.mu.Unlock() m.hooks = make(map[HookType][]*hookEntry) } func (m *Manager) SetEnabled(enabled bool) { m.mu.Lock() defer m.mu.Unlock() m.enabled = enabled }