// Package logic is sensitive-word scan, penalties, and admin word CRUD. package logic import ( "context" "errors" "time" modentity "spark-server/internal/module/moderation/entity" ) var ErrNotReady = errors.New("moderation: not ready") type Repository interface { Ready() bool ReloadMatcher(ctx context.Context) error ListWords(ctx context.Context, q string, category int, enabled *bool, page, size int) ([]modentity.Word, int64, error) CreateWord(ctx context.Context, w *modentity.Word) error UpdateWord(ctx context.Context, id int64, patch WordPatch) error SoftDeleteWord(ctx context.Context, id int64) error InsertHits(ctx context.Context, hits []HitRecord) error ListHits(ctx context.Context, userID int64, category int, since, until *time.Time, page, size int) ([]HitRecord, int64, error) LookupWord(id int64) *modentity.Word MatcherFind(text string) ([]MatchDetail, string, bool) } type WordPatch func(w *modentity.Word) error type HitRecord struct { ID int64 `json:"id"` UserID int64 `json:"user_id"` ConversationID int64 `json:"conversation_id"` WordID int64 `json:"word_id"` Category modentity.Category `json:"category"` Action modentity.Action `json:"action"` Excerpt string `json:"excerpt"` RealIP string `json:"real_ip"` CreatedAt time.Time `json:"created_at"` } type MatchDetail struct { WordID int64 Category modentity.Category Action modentity.Action Excerpt string From int To int } type UserModeration interface { AssertCanSend(ctx context.Context, userID int64) error DisableAccount(ctx context.Context, userID int64) error ExtendSendMutedUntil(ctx context.Context, userID int64, until time.Time) error } type IDGen interface { NextID() (int64, error) } type Service struct { repo Repository users UserModeration ids IDGen } func NewService(repo Repository, users UserModeration, ids IDGen) *Service { return &Service{repo: repo, users: users, ids: ids} } func (s *Service) Ready() bool { return s != nil && s.repo != nil && s.repo.Ready() } func (s *Service) Reload(ctx context.Context) error { if !s.Ready() { return ErrNotReady } return s.repo.ReloadMatcher(ctx) }