Files
backend/internal/pkg/hook/moderation_hooks.go

384 lines
9.5 KiB
Go
Raw Normal View History

package hook
import (
"context"
"strings"
"time"
"carrot_bbs/internal/repository"
"go.uber.org/zap"
)
type reviewableError interface {
IsReview() 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
}
type PostModerationHook struct {
name string
postAIService PostAIService
postRepo repository.PostRepository
strictMode bool
}
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: PriorityHigh,
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 result.RejectReason != "" || result.NeedsReview {
return nil
}
if h.postAIService == nil || !h.postAIService.IsEnabled() {
result.Approved = true
result.ReviewedBy = "system"
return nil
}
err := h.postAIService.ModeratePost(ctx.Ctx, data.Title, data.Content, data.Images)
if err != nil {
if userErr, ok := err.(interface{ UserMessage() string }); ok {
if revErr, ok := err.(reviewableError); ok && revErr.IsReview() {
result.Approved = false
result.NeedsReview = true
result.RejectReason = userErr.UserMessage()
result.ReviewedBy = "ai"
zap.L().Info("Post flagged for manual review by AI",
zap.String("post_id", data.PostID),
zap.String("reason", result.RejectReason),
)
return nil
}
result.Approved = false
result.RejectReason = userErr.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"
zap.L().Error("AI post moderation failed in strict mode, reject post",
zap.String("post_id", data.PostID),
zap.Error(err),
)
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"
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: PriorityHigh,
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 result.RejectReason != "" || result.NeedsReview {
return nil
}
if h.postAIService == nil || !h.postAIService.IsEnabled() {
result.Approved = true
result.ReviewedBy = "system"
return nil
}
err := h.postAIService.ModerateComment(ctx.Ctx, data.Content, data.Images)
if err != nil {
if userErr, ok := err.(interface{ UserMessage() string }); ok {
if revErr, ok := err.(reviewableError); ok && revErr.IsReview() {
result.Approved = false
result.NeedsReview = true
result.RejectReason = userErr.UserMessage()
result.ReviewedBy = "ai"
zap.L().Info("Comment flagged for manual review by AI",
zap.String("comment_id", data.CommentID),
zap.String("reason", result.RejectReason),
)
return nil
}
result.Approved = false
result.RejectReason = userErr.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"
zap.L().Error("AI comment moderation failed in strict mode, reject comment",
zap.String("comment_id", data.CommentID),
zap.Error(err),
)
return nil
}
result.Approved = true
result.ReviewedBy = "system"
zap.L().Warn("AI moderation error, fallback approve",
zap.String("comment_id", data.CommentID),
zap.Error(err),
)
return nil
}
result.Approved = true
result.ReviewedBy = "ai"
return nil
}
type SensitiveService interface {
Check(ctx context.Context, text string) (bool, []string)
Replace(ctx context.Context, text string, repl string) string
}
type SensitiveWordHook struct {
name string
sensitive SensitiveService
replaceStr 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: PriorityHighest,
Func: h.executePost,
Async: false,
})
}
func (h *SensitiveWordHook) RegisterCommentHook(manager *Manager) {
manager.Register(&Hook{
Name: h.name + ".comment",
HookType: HookCommentPreModerate,
Priority: PriorityHighest,
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) {
if m.sensitiveService != nil {
sensitiveHook := NewSensitiveWordHook(m.sensitiveService, m.replaceStr)
sensitiveHook.RegisterPostHook(manager)
sensitiveHook.RegisterCommentHook(manager)
}
postHook := NewPostModerationHook(m.postAIService, m.postRepo, m.strictMode)
postHook.Register(manager)
commentHook := NewCommentModerationHook(m.postAIService, m.commentRepo, m.strictMode)
commentHook.Register(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
}