Files
backend/internal/pkg/hook/user_profile_moderation_hooks.go
lafay 2f584c1f2c
All checks were successful
Build Backend / build (push) Successful in 5m10s
Build Backend / build-docker (push) Successful in 2m22s
This is a breaking change that renames the entire project from "Carrot BBS" to "WithYou".
The Go module name has been changed from `carrot_bbs` to `with_you`, which requires
all import paths to be updated throughout the codebase and by external consumers.

BREAKING CHANGE: Module name changed from carrot_bbs to with_you. All imports
and dependencies must be updated from carrot_bbs/* to with_you/*.
2026-04-22 16:01:59 +08:00

318 lines
7.9 KiB
Go

package hook
import (
"strings"
"time"
"with_you/internal/repository"
"go.uber.org/zap"
)
type UserAvatarModerationHook struct {
name string
postAIService PostAIService
userRepo repository.UserRepository
strictMode bool
}
func NewUserAvatarModerationHook(postAIService PostAIService, userRepo repository.UserRepository, strictMode bool) *UserAvatarModerationHook {
return &UserAvatarModerationHook{
name: "moderation.user.avatar.ai",
postAIService: postAIService,
userRepo: userRepo,
strictMode: strictMode,
}
}
func (h *UserAvatarModerationHook) Register(manager *Manager) {
manager.Register(&Hook{
Name: h.name,
HookType: HookUserAvatarPreModerate,
Priority: PriorityHigh,
Func: h.Execute,
Async: false,
Timeout: 30 * time.Second,
})
}
func (h *UserAvatarModerationHook) Execute(ctx *HookContext) error {
data, ok := ctx.Data.(*UserAvatarModerateHookData)
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.ModerateImage(ctx.Ctx, data.AvatarURL)
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("User avatar flagged for manual review by AI",
zap.String("user_id", data.UserID),
zap.String("reason", result.RejectReason),
)
return nil
}
result.Approved = false
result.RejectReason = userErr.UserMessage()
result.ReviewedBy = "ai"
zap.L().Info("User avatar rejected by AI moderation",
zap.String("user_id", data.UserID),
zap.String("reason", result.RejectReason),
)
return nil
}
if h.strictMode {
result.Approved = false
result.Error = err
result.ReviewedBy = "ai"
zap.L().Error("AI avatar moderation failed in strict mode, reject",
zap.String("user_id", data.UserID),
zap.Error(err),
)
return nil
}
result.Approved = true
result.ReviewedBy = "system"
zap.L().Warn("AI moderation error, fallback approve avatar",
zap.String("user_id", data.UserID),
zap.Error(err),
)
return nil
}
result.Approved = true
result.ReviewedBy = "ai"
return nil
}
type UserCoverModerationHook struct {
name string
postAIService PostAIService
userRepo repository.UserRepository
strictMode bool
}
func NewUserCoverModerationHook(postAIService PostAIService, userRepo repository.UserRepository, strictMode bool) *UserCoverModerationHook {
return &UserCoverModerationHook{
name: "moderation.user.cover.ai",
postAIService: postAIService,
userRepo: userRepo,
strictMode: strictMode,
}
}
func (h *UserCoverModerationHook) Register(manager *Manager) {
manager.Register(&Hook{
Name: h.name,
HookType: HookUserCoverPreModerate,
Priority: PriorityHigh,
Func: h.Execute,
Async: false,
Timeout: 30 * time.Second,
})
}
func (h *UserCoverModerationHook) Execute(ctx *HookContext) error {
data, ok := ctx.Data.(*UserCoverModerateHookData)
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.ModerateImage(ctx.Ctx, data.CoverURL)
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("User cover flagged for manual review by AI",
zap.String("user_id", data.UserID),
zap.String("reason", result.RejectReason),
)
return nil
}
result.Approved = false
result.RejectReason = userErr.UserMessage()
result.ReviewedBy = "ai"
zap.L().Info("User cover rejected by AI moderation",
zap.String("user_id", data.UserID),
zap.String("reason", result.RejectReason),
)
return nil
}
if h.strictMode {
result.Approved = false
result.Error = err
result.ReviewedBy = "ai"
zap.L().Error("AI cover moderation failed in strict mode, reject",
zap.String("user_id", data.UserID),
zap.Error(err),
)
return nil
}
result.Approved = true
result.ReviewedBy = "system"
zap.L().Warn("AI moderation error, fallback approve cover",
zap.String("user_id", data.UserID),
zap.Error(err),
)
return nil
}
result.Approved = true
result.ReviewedBy = "ai"
return nil
}
type UserBioModerationHook struct {
name string
postAIService PostAIService
sensitiveService SensitiveService
userRepo repository.UserRepository
strictMode bool
}
func NewUserBioModerationHook(postAIService PostAIService, sensitiveService SensitiveService, userRepo repository.UserRepository, strictMode bool) *UserBioModerationHook {
return &UserBioModerationHook{
name: "moderation.user.bio.ai",
postAIService: postAIService,
sensitiveService: sensitiveService,
userRepo: userRepo,
strictMode: strictMode,
}
}
func (h *UserBioModerationHook) Register(manager *Manager) {
manager.Register(&Hook{
Name: h.name,
HookType: HookUserBioPreModerate,
Priority: PriorityHigh,
Func: h.Execute,
Async: false,
Timeout: 30 * time.Second,
})
}
func (h *UserBioModerationHook) Execute(ctx *HookContext) error {
data, ok := ctx.Data.(*UserBioModerateHookData)
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.sensitiveService != nil {
hasSensitive, words := h.sensitiveService.Check(ctx.Ctx, data.Bio)
if hasSensitive {
result.Approved = false
result.RejectReason = "内容包含敏感词: " + strings.Join(words, ", ")
result.ReviewedBy = "sensitive_word"
zap.L().Info("User bio rejected by sensitive word filter",
zap.String("user_id", data.UserID),
zap.Strings("words", words),
)
return nil
}
}
if h.postAIService == nil || !h.postAIService.IsEnabled() {
result.Approved = true
result.ReviewedBy = "system"
return nil
}
err := h.postAIService.ModerateBio(ctx.Ctx, data.Bio)
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("User bio flagged for manual review by AI",
zap.String("user_id", data.UserID),
zap.String("reason", result.RejectReason),
)
return nil
}
result.Approved = false
result.RejectReason = userErr.UserMessage()
result.ReviewedBy = "ai"
zap.L().Info("User bio rejected by AI moderation",
zap.String("user_id", data.UserID),
zap.String("reason", result.RejectReason),
)
return nil
}
if h.strictMode {
result.Approved = false
result.Error = err
result.ReviewedBy = "ai"
zap.L().Error("AI bio moderation failed in strict mode, reject",
zap.String("user_id", data.UserID),
zap.Error(err),
)
return nil
}
result.Approved = true
result.ReviewedBy = "system"
zap.L().Warn("AI moderation error, fallback approve bio",
zap.String("user_id", data.UserID),
zap.Error(err),
)
return nil
}
result.Approved = true
result.ReviewedBy = "ai"
return nil
}