feat: add hook system, QR code login, and layered cache
All checks were successful
Build Backend / build (push) Successful in 5m54s
Build Backend / build-docker (push) Successful in 4m7s

- 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:
lafay
2026-03-20 12:23:28 +08:00
parent bdfcbdadea
commit 98f0c9f2b6
23 changed files with 2726 additions and 76 deletions

405
internal/pkg/hook/hook.go Normal file
View 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
}

View File

@@ -0,0 +1,122 @@
package hook
import (
"sync"
"sync/atomic"
"time"
)
type Metrics struct {
totalCalls atomic.Int64
failedCalls atomic.Int64
totalTime atomic.Int64
hookMetrics map[string]*hookMetric
mu sync.RWMutex
}
type hookMetric struct {
calls atomic.Int64
failures atomic.Int64
totalTime atomic.Int64
lastCalled atomic.Int64
}
func NewMetrics() *Metrics {
return &Metrics{
hookMetrics: make(map[string]*hookMetric),
}
}
func (m *Metrics) RecordHookExecution(name string, hookType HookType, duration time.Duration, success bool) {
m.totalCalls.Add(1)
m.totalTime.Add(int64(duration))
if !success {
m.failedCalls.Add(1)
}
key := string(hookType) + ":" + name
m.mu.Lock()
metric, ok := m.hookMetrics[key]
if !ok {
metric = &hookMetric{}
m.hookMetrics[key] = metric
}
m.mu.Unlock()
metric.calls.Add(1)
metric.totalTime.Add(int64(duration))
metric.lastCalled.Store(time.Now().Unix())
if !success {
metric.failures.Add(1)
}
}
type MetricSnapshot struct {
Name string
Type HookType
Calls int64
Failures int64
TotalTime time.Duration
AvgTime time.Duration
LastCalled time.Time
}
func (m *Metrics) GetTotalCalls() int64 {
return m.totalCalls.Load()
}
func (m *Metrics) GetFailedCalls() int64 {
return m.failedCalls.Load()
}
func (m *Metrics) GetTotalTime() time.Duration {
return time.Duration(m.totalTime.Load())
}
func (m *Metrics) GetHookMetrics() []MetricSnapshot {
m.mu.RLock()
defer m.mu.RUnlock()
var snapshots []MetricSnapshot
for key, metric := range m.hookMetrics {
calls := metric.calls.Load()
var avgTime time.Duration
if calls > 0 {
avgTime = time.Duration(metric.totalTime.Load() / calls)
}
hookType, name := parseKey(key)
snapshots = append(snapshots, MetricSnapshot{
Name: name,
Type: hookType,
Calls: calls,
Failures: metric.failures.Load(),
TotalTime: time.Duration(metric.totalTime.Load()),
AvgTime: avgTime,
LastCalled: time.Unix(metric.lastCalled.Load(), 0),
})
}
return snapshots
}
func parseKey(key string) (HookType, string) {
for i := 0; i < len(key); i++ {
if key[i] == ':' {
return HookType(key[:i]), key[i+1:]
}
}
return HookType(key), ""
}
func (m *Metrics) Reset() {
m.totalCalls.Store(0)
m.failedCalls.Store(0)
m.totalTime.Store(0)
m.mu.Lock()
m.hookMetrics = make(map[string]*hookMetric)
m.mu.Unlock()
}

View File

@@ -0,0 +1,353 @@
package hook
import (
"context"
"strings"
"time"
"carrot_bbs/internal/repository"
"go.uber.org/zap"
)
type PostModerationHook struct {
name string
postAIService PostAIService
postRepo *repository.PostRepository
strictMode bool
}
type PostAIService interface {
IsEnabled() bool
ModeratePost(ctx context.Context, title, content string, images []string) error
ModerateComment(ctx context.Context, content string, images []string) error
}
func NewPostModerationHook(postAIService PostAIService, postRepo *repository.PostRepository, strictMode bool) *PostModerationHook {
return &PostModerationHook{
name: "moderation.post.ai",
postAIService: postAIService,
postRepo: postRepo,
strictMode: strictMode,
}
}
func (h *PostModerationHook) Register(manager *Manager) {
manager.Register(&Hook{
Name: h.name,
HookType: HookPostPreModerate,
Priority: PriorityHighest,
Func: h.Execute,
Async: false, // 必须同步执行,因为调用方需要审核结果
Timeout: 30 * time.Second,
})
}
func (h *PostModerationHook) Execute(ctx *HookContext) error {
data, ok := ctx.Data.(*PostModerateHookData)
if !ok {
return nil
}
result, ok := ctx.Metadata["result"].(*ModerationResult)
if !ok || result == nil {
return nil
}
if h.postAIService == nil || !h.postAIService.IsEnabled() {
result.Approved = true
result.ReviewedBy = "system"
zap.L().Debug("AI moderation disabled, auto approve post",
zap.String("post_id", data.PostID),
)
return nil
}
err := h.postAIService.ModeratePost(ctx.Ctx, data.Title, data.Content, data.Images)
if err != nil {
if rejectErr, ok := err.(interface{ UserMessage() string }); ok {
result.Approved = false
result.RejectReason = rejectErr.UserMessage()
result.ReviewedBy = "ai"
zap.L().Info("Post rejected by AI moderation",
zap.String("post_id", data.PostID),
zap.String("reason", result.RejectReason),
)
return nil
}
if h.strictMode {
result.Approved = false
result.Error = err
result.ReviewedBy = "ai"
return nil
}
result.Approved = true
result.ReviewedBy = "system"
zap.L().Warn("AI moderation error, fallback approve",
zap.String("post_id", data.PostID),
zap.Error(err),
)
return nil
}
result.Approved = true
result.ReviewedBy = "ai"
zap.L().Debug("Post approved by AI moderation",
zap.String("post_id", data.PostID),
)
return nil
}
type CommentModerationHook struct {
name string
postAIService PostAIService
commentRepo *repository.CommentRepository
strictMode bool
}
func NewCommentModerationHook(postAIService PostAIService, commentRepo *repository.CommentRepository, strictMode bool) *CommentModerationHook {
return &CommentModerationHook{
name: "moderation.comment.ai",
postAIService: postAIService,
commentRepo: commentRepo,
strictMode: strictMode,
}
}
func (h *CommentModerationHook) Register(manager *Manager) {
manager.Register(&Hook{
Name: h.name,
HookType: HookCommentPreModerate,
Priority: PriorityHighest,
Func: h.Execute,
Async: false, // 必须同步执行,因为调用方需要审核结果
Timeout: 30 * time.Second,
})
}
func (h *CommentModerationHook) Execute(ctx *HookContext) error {
data, ok := ctx.Data.(*CommentModerateHookData)
if !ok {
return nil
}
result, ok := ctx.Metadata["result"].(*ModerationResult)
if !ok || result == nil {
return nil
}
if h.postAIService == nil || !h.postAIService.IsEnabled() {
result.Approved = true
result.ReviewedBy = "system"
zap.L().Debug("AI moderation disabled, auto approve comment",
zap.String("comment_id", data.CommentID),
)
return nil
}
err := h.postAIService.ModerateComment(ctx.Ctx, data.Content, data.Images)
if err != nil {
if rejectErr, ok := err.(interface{ UserMessage() string }); ok {
result.Approved = false
result.RejectReason = rejectErr.UserMessage()
result.ReviewedBy = "ai"
zap.L().Info("Comment rejected by AI moderation",
zap.String("comment_id", data.CommentID),
zap.String("reason", result.RejectReason),
)
return nil
}
if h.strictMode {
result.Approved = false
result.Error = err
result.ReviewedBy = "ai"
return nil
}
result.Approved = true
result.ReviewedBy = "system"
zap.L().Warn("AI comment moderation error, fallback approve",
zap.String("comment_id", data.CommentID),
zap.Error(err),
)
return nil
}
result.Approved = true
result.ReviewedBy = "ai"
zap.L().Debug("Comment approved by AI moderation",
zap.String("comment_id", data.CommentID),
)
return nil
}
type SensitiveWordHook struct {
name string
sensitive SensitiveService
replaceStr string
}
type SensitiveService interface {
Check(ctx context.Context, text string) (bool, []string)
Replace(ctx context.Context, text string, repl string) string
}
func NewSensitiveWordHook(sensitive SensitiveService, replaceStr string) *SensitiveWordHook {
return &SensitiveWordHook{
name: "moderation.sensitive_word",
sensitive: sensitive,
replaceStr: replaceStr,
}
}
func (h *SensitiveWordHook) RegisterPostHook(manager *Manager) {
manager.Register(&Hook{
Name: h.name + ".post",
HookType: HookPostPreModerate,
Priority: PriorityHigh,
Func: h.executePost,
Async: false,
})
}
func (h *SensitiveWordHook) RegisterCommentHook(manager *Manager) {
manager.Register(&Hook{
Name: h.name + ".comment",
HookType: HookCommentPreModerate,
Priority: PriorityHigh,
Func: h.executeComment,
Async: false,
})
}
func (h *SensitiveWordHook) executePost(ctx *HookContext) error {
if h.sensitive == nil {
return nil
}
data, ok := ctx.Data.(*PostModerateHookData)
if !ok {
return nil
}
result, ok := ctx.Metadata["result"].(*ModerationResult)
if !ok || result == nil {
return nil
}
if result.RejectReason != "" {
return nil
}
hasSensitive, words := h.sensitive.Check(ctx.Ctx, data.Title+" "+data.Content)
if hasSensitive {
result.Approved = false
result.RejectReason = "内容包含敏感词: " + strings.Join(words, ", ")
result.ReviewedBy = "sensitive_word"
zap.L().Info("Post rejected by sensitive word filter",
zap.String("post_id", data.PostID),
zap.Strings("words", words),
)
}
return nil
}
func (h *SensitiveWordHook) executeComment(ctx *HookContext) error {
if h.sensitive == nil {
return nil
}
data, ok := ctx.Data.(*CommentModerateHookData)
if !ok {
return nil
}
result, ok := ctx.Metadata["result"].(*ModerationResult)
if !ok || result == nil {
return nil
}
if result.RejectReason != "" {
return nil
}
hasSensitive, words := h.sensitive.Check(ctx.Ctx, data.Content)
if hasSensitive {
result.Approved = false
result.RejectReason = "内容包含敏感词: " + strings.Join(words, ", ")
result.ReviewedBy = "sensitive_word"
zap.L().Info("Comment rejected by sensitive word filter",
zap.String("comment_id", data.CommentID),
zap.Strings("words", words),
)
}
return nil
}
type ModerationHooks struct {
postAIService PostAIService
sensitiveService SensitiveService
postRepo *repository.PostRepository
commentRepo *repository.CommentRepository
strictMode bool
replaceStr string
}
func NewModerationHooks(
postAIService PostAIService,
sensitiveService SensitiveService,
postRepo *repository.PostRepository,
commentRepo *repository.CommentRepository,
strictMode bool,
replaceStr string,
) *ModerationHooks {
return &ModerationHooks{
postAIService: postAIService,
sensitiveService: sensitiveService,
postRepo: postRepo,
commentRepo: commentRepo,
strictMode: strictMode,
replaceStr: replaceStr,
}
}
func (m *ModerationHooks) RegisterAll(manager *Manager) {
postHook := NewPostModerationHook(m.postAIService, m.postRepo, m.strictMode)
postHook.Register(manager)
commentHook := NewCommentModerationHook(m.postAIService, m.commentRepo, m.strictMode)
commentHook.Register(manager)
if m.sensitiveService != nil {
sensitiveHook := NewSensitiveWordHook(m.sensitiveService, m.replaceStr)
sensitiveHook.RegisterPostHook(manager)
sensitiveHook.RegisterCommentHook(manager)
}
zap.L().Info("Registered moderation hooks",
zap.Bool("ai_enabled", m.postAIService != nil && m.postAIService.IsEnabled()),
zap.Bool("sensitive_enabled", m.sensitiveService != nil),
zap.Bool("strict_mode", m.strictMode),
)
}
func (m *ModerationHooks) ModeratePost(ctx context.Context, manager *Manager, data *PostModerateHookData) *ModerationResult {
result := &ModerationResult{}
manager.TriggerWithMetadata(ctx, HookPostPreModerate, data.AuthorID, data, map[string]any{
"result": result,
})
return result
}
func (m *ModerationHooks) ModerateComment(ctx context.Context, manager *Manager, data *CommentModerateHookData) *ModerationResult {
result := &ModerationResult{}
manager.TriggerWithMetadata(ctx, HookCommentPreModerate, data.AuthorID, data, map[string]any{
"result": result,
})
return result
}

272
internal/pkg/hook/types.go Normal file
View File

@@ -0,0 +1,272 @@
package hook
import (
"context"
"go.uber.org/zap"
)
type PostHookData struct {
PostID uint
Title string
Content string
AuthorID uint
GroupID *uint
IsPublished bool
}
type CommentHookData struct {
CommentID uint
PostID uint
Content string
AuthorID uint
ParentID *uint
}
type UserHookData struct {
UserID uint
Username string
Email string
Nickname string
}
type GroupHookData struct {
GroupID uint
Name string
CreatorID uint
}
type MessageHookData struct {
MessageID uint
SenderID uint
ReceiverID uint
Content string
}
type NotificationHookData struct {
NotificationID uint
UserID uint
Type string
Title string
Content string
}
type VoteHookData struct {
VoteID uint
PostID uint
VoterID uint
OptionID uint
}
type FileHookData struct {
FileID uint
FileName string
FileSize int64
Uploader uint
MimeType string
}
type PostModerateHookData struct {
PostID string
Title string
Content string
Images []string
AuthorID uint
GroupID *uint
}
type PostModeratedHookData struct {
PostID string
AuthorID uint
Approved bool
RejectReason string
ReviewedBy string
}
type CommentModerateHookData struct {
CommentID string
PostID string
Content string
Images []string
AuthorID uint
ParentID *string
}
type CommentModeratedHookData struct {
CommentID string
PostID string
AuthorID uint
Approved bool
RejectReason string
ReviewedBy string
}
type ModerationResult struct {
Approved bool
RejectReason string
ReviewedBy string
Error error
}
type BuiltinHooks struct {
manager *Manager
}
func NewBuiltinHooks(manager *Manager) *BuiltinHooks {
return &BuiltinHooks{manager: manager}
}
func (b *BuiltinHooks) RegisterAll() {
b.registerLoggingHooks()
b.registerCacheInvalidationHooks()
}
func (b *BuiltinHooks) registerLoggingHooks() {
b.manager.RegisterFunc(HookPostCreated, "log.post.created", func(ctx *HookContext) error {
if data, ok := ctx.Data.(*PostHookData); ok {
zap.L().Info("Post created",
zap.Uint("post_id", data.PostID),
zap.Uint("author_id", data.AuthorID),
zap.String("title", data.Title),
zap.Uint("user_id", ctx.UserID),
)
}
return nil
}, WithAsync(), WithPriority(PriorityLowest))
b.manager.RegisterFunc(HookUserLogin, "log.user.login", func(ctx *HookContext) error {
if data, ok := ctx.Data.(*UserHookData); ok {
zap.L().Info("User logged in",
zap.Uint("user_id", data.UserID),
zap.String("username", data.Username),
)
}
return nil
}, WithAsync())
b.manager.RegisterFunc(HookCommentCreated, "log.comment.created", func(ctx *HookContext) error {
if data, ok := ctx.Data.(*CommentHookData); ok {
zap.L().Info("Comment created",
zap.Uint("comment_id", data.CommentID),
zap.Uint("post_id", data.PostID),
zap.Uint("author_id", data.AuthorID),
)
}
return nil
}, WithAsync(), WithPriority(PriorityLowest))
}
func (b *BuiltinHooks) registerCacheInvalidationHooks() {
}
type HookTriggerHelper struct {
manager *Manager
}
func NewHookTriggerHelper(manager *Manager) *HookTriggerHelper {
return &HookTriggerHelper{manager: manager}
}
func (h *HookTriggerHelper) TriggerPostCreated(ctx context.Context, userID uint, data *PostHookData) {
h.manager.Trigger(ctx, HookPostCreated, userID, data)
}
func (h *HookTriggerHelper) TriggerPostUpdated(ctx context.Context, userID uint, data *PostHookData) {
h.manager.Trigger(ctx, HookPostUpdated, userID, data)
}
func (h *HookTriggerHelper) TriggerPostDeleted(ctx context.Context, userID uint, postID uint) {
h.manager.Trigger(ctx, HookPostDeleted, userID, postID)
}
func (h *HookTriggerHelper) TriggerCommentCreated(ctx context.Context, userID uint, data *CommentHookData) {
h.manager.Trigger(ctx, HookCommentCreated, userID, data)
}
func (h *HookTriggerHelper) TriggerCommentDeleted(ctx context.Context, userID uint, commentID uint) {
h.manager.Trigger(ctx, HookCommentDeleted, userID, commentID)
}
func (h *HookTriggerHelper) TriggerUserCreated(ctx context.Context, data *UserHookData) {
h.manager.Trigger(ctx, HookUserCreated, data.UserID, data)
}
func (h *HookTriggerHelper) TriggerUserUpdated(ctx context.Context, userID uint, data *UserHookData) {
h.manager.Trigger(ctx, HookUserUpdated, userID, data)
}
func (h *HookTriggerHelper) TriggerUserDeleted(ctx context.Context, userID uint) {
h.manager.Trigger(ctx, HookUserDeleted, userID, userID)
}
func (h *HookTriggerHelper) TriggerUserLogin(ctx context.Context, userID uint, data *UserHookData) {
h.manager.Trigger(ctx, HookUserLogin, userID, data)
}
func (h *HookTriggerHelper) TriggerUserLogout(ctx context.Context, userID uint) {
h.manager.Trigger(ctx, HookUserLogout, userID, userID)
}
func (h *HookTriggerHelper) TriggerGroupCreated(ctx context.Context, userID uint, data *GroupHookData) {
h.manager.Trigger(ctx, HookGroupCreated, userID, data)
}
func (h *HookTriggerHelper) TriggerGroupUpdated(ctx context.Context, userID uint, data *GroupHookData) {
h.manager.Trigger(ctx, HookGroupUpdated, userID, data)
}
func (h *HookTriggerHelper) TriggerGroupDeleted(ctx context.Context, userID uint, groupID uint) {
h.manager.Trigger(ctx, HookGroupDeleted, userID, groupID)
}
func (h *HookTriggerHelper) TriggerGroupJoined(ctx context.Context, userID uint, groupID uint) {
h.manager.Trigger(ctx, HookGroupJoined, userID, groupID)
}
func (h *HookTriggerHelper) TriggerGroupLeft(ctx context.Context, userID uint, groupID uint) {
h.manager.Trigger(ctx, HookGroupLeft, userID, groupID)
}
func (h *HookTriggerHelper) TriggerMessageSent(ctx context.Context, userID uint, data *MessageHookData) {
h.manager.Trigger(ctx, HookMessageSent, userID, data)
}
func (h *HookTriggerHelper) TriggerMessageRead(ctx context.Context, userID uint, messageID uint) {
h.manager.Trigger(ctx, HookMessageRead, userID, messageID)
}
func (h *HookTriggerHelper) TriggerNotificationSent(ctx context.Context, userID uint, data *NotificationHookData) {
h.manager.Trigger(ctx, HookNotificationSent, userID, data)
}
func (h *HookTriggerHelper) TriggerVoteCreated(ctx context.Context, userID uint, data *VoteHookData) {
h.manager.Trigger(ctx, HookVoteCreated, userID, data)
}
func (h *HookTriggerHelper) TriggerVoteUpdated(ctx context.Context, userID uint, data *VoteHookData) {
h.manager.Trigger(ctx, HookVoteUpdated, userID, data)
}
func (h *HookTriggerHelper) TriggerFileUploaded(ctx context.Context, userID uint, data *FileHookData) {
h.manager.Trigger(ctx, HookFileUploaded, userID, data)
}
func (h *HookTriggerHelper) TriggerPostPreModerate(ctx context.Context, userID uint, data *PostModerateHookData, result *ModerationResult) {
h.manager.TriggerWithMetadata(ctx, HookPostPreModerate, userID, data, map[string]any{
"result": result,
})
}
func (h *HookTriggerHelper) TriggerPostModerated(ctx context.Context, userID uint, data *PostModeratedHookData) {
h.manager.Trigger(ctx, HookPostModerated, userID, data)
}
func (h *HookTriggerHelper) TriggerCommentPreModerate(ctx context.Context, userID uint, data *CommentModerateHookData, result *ModerationResult) {
h.manager.TriggerWithMetadata(ctx, HookCommentPreModerate, userID, data, map[string]any{
"result": result,
})
}
func (h *HookTriggerHelper) TriggerCommentModerated(ctx context.Context, userID uint, data *CommentModeratedHookData) {
h.manager.Trigger(ctx, HookCommentModerated, userID, data)
}