feat: add hook system, QR code login, and layered cache
- Implement extensible hook system for content moderation with builtin and AI-powered moderation hooks - Add QR code login feature with SSE for real-time status updates and scan/confirm/cancel workflow - Introduce layered cache with local LRU + Redis backend for improved read performance - Refactor post and comment services to use hook-based moderation instead of direct AI service calls - Add local cache configuration options (size, buckets, ttl)
This commit is contained in:
405
internal/pkg/hook/hook.go
Normal file
405
internal/pkg/hook/hook.go
Normal file
@@ -0,0 +1,405 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user