package moderation import ( "context" "fmt" "sort" "time" "telesrv/internal/domain" ) func (s *Service) ListCases(ctx context.Context, filter domain.ModerationCaseFilter) ([]domain.ModerationCase, error) { if s == nil || s.cases == nil { return nil, fmt.Errorf("moderation case store is not configured") } return s.cases.ListModerationCases(ctx, filter) } func (s *Service) Case(ctx context.Context, caseID int64) (domain.ModerationCaseDetail, bool, error) { if s == nil || s.cases == nil { return domain.ModerationCaseDetail{}, false, fmt.Errorf("moderation case store is not configured") } return s.cases.GetModerationCase(ctx, caseID) } func (s *Service) ClaimCase(ctx context.Context, caseID, expectedVersion int64, actor string, now time.Time) (domain.ModerationCase, error) { if s == nil || s.cases == nil { return domain.ModerationCase{}, fmt.Errorf("moderation case store is not configured") } return s.cases.ClaimModerationCase(ctx, caseID, expectedVersion, actor, now) } func (s *Service) DecideCase(ctx context.Context, request domain.ModerationDecisionRequest) (domain.ModerationCaseDetail, bool, error) { if s == nil || s.cases == nil { return domain.ModerationCaseDetail{}, false, fmt.Errorf("moderation case store is not configured") } prepared, err := domain.NewModerationDecisionRequest(request) if err != nil { return domain.ModerationCaseDetail{}, false, err } detail, found, err := s.cases.GetModerationCase(ctx, prepared.CaseID) if err != nil { return domain.ModerationCaseDetail{}, false, err } if !found { return domain.ModerationCaseDetail{}, false, domain.ErrModerationCaseNotFound } if err := s.validateDecisionActions(ctx, detail, prepared.Actions); err != nil { return domain.ModerationCaseDetail{}, false, err } return s.cases.DecideModerationCase(ctx, prepared) } func (s *Service) SubmitAppeal(ctx context.Context, caseID, appellantUserID int64, text string, now time.Time) (domain.ModerationAppeal, bool, error) { if s == nil || s.cases == nil { return domain.ModerationAppeal{}, false, fmt.Errorf("moderation case store is not configured") } detail, found, err := s.cases.GetModerationCase(ctx, caseID) if err != nil { return domain.ModerationAppeal{}, false, err } if !found { return domain.ModerationAppeal{}, false, domain.ErrModerationCaseNotFound } switch detail.Case.Target.Type { case domain.PeerTypeUser: if detail.Case.Target.ID != appellantUserID { return domain.ModerationAppeal{}, false, domain.ErrModerationPermissionDenied } case domain.PeerTypeChannel: if s.channels == nil { return domain.ModerationAppeal{}, false, domain.ErrModerationPermissionDenied } view, err := s.channels.ResolveChannel(ctx, appellantUserID, detail.Case.Target.ID) if err != nil || view.Forbidden || (view.Self.Role != domain.ChannelRoleCreator && view.Self.Role != domain.ChannelRoleAdmin) { return domain.ModerationAppeal{}, false, domain.ErrModerationPermissionDenied } default: return domain.ModerationAppeal{}, false, domain.ErrModerationCaseInvalid } appeal, err := domain.NewModerationAppeal( caseID, appellantUserID, detail.Case.Status, text, now, ) if err != nil { return domain.ModerationAppeal{}, false, err } return s.cases.CreateModerationAppeal(ctx, appeal) } func (s *Service) ReviewAppeal(ctx context.Context, request domain.ModerationDecisionRequest) (domain.ModerationCaseDetail, bool, error) { if s == nil || s.cases == nil { return domain.ModerationCaseDetail{}, false, fmt.Errorf("moderation case store is not configured") } prepared, err := domain.NewModerationDecisionRequest(request) if err != nil { return domain.ModerationCaseDetail{}, false, err } if prepared.AppealID <= 0 || (prepared.Kind != domain.ModerationDecisionAppealGrant && prepared.Kind != domain.ModerationDecisionAppealDeny) { return domain.ModerationCaseDetail{}, false, domain.ErrModerationCaseInvalid } detail, found, err := s.cases.GetModerationCase(ctx, prepared.CaseID) if err != nil { return domain.ModerationCaseDetail{}, false, err } if !found { return domain.ModerationCaseDetail{}, false, domain.ErrModerationCaseNotFound } appealFound := false for _, appeal := range detail.Appeals { if appeal.ID == prepared.AppealID && appeal.Status == domain.ModerationAppealPending { appealFound = true break } } if !appealFound { return domain.ModerationCaseDetail{}, false, domain.ErrModerationCaseNotFound } if err := s.validateDecisionActions(ctx, detail, prepared.Actions); err != nil { return domain.ModerationCaseDetail{}, false, err } if prepared.Kind == domain.ModerationDecisionAppealGrant { if err := validateAppealRemedyActions(detail, prepared.Actions); err != nil { return domain.ModerationCaseDetail{}, false, err } } return s.cases.ReviewModerationAppeal(ctx, prepared) } func validateAppealRemedyActions(detail domain.ModerationCaseDetail, actions []domain.ModerationActionDraft) error { history := append([]domain.ModerationAction(nil), detail.Actions...) sort.Slice(history, func(i, j int) bool { return history[i].ID < history[j].ID }) var flagsActive, freezeActive, irreversible bool for _, action := range history { if action.Status != domain.ModerationActionSucceeded { continue } switch action.Kind { case domain.ModerationActionMarkScam, domain.ModerationActionMarkFake: flagsActive = true case domain.ModerationActionClearPeerFlags: flagsActive = false case domain.ModerationActionFreezeAccount: freezeActive = true case domain.ModerationActionUnfreezeAccount: freezeActive = false case domain.ModerationActionDeletePrivateMessage, domain.ModerationActionDeleteChannelMessage, domain.ModerationActionDeleteAccount: irreversible = true } } if irreversible { return domain.ErrModerationActionInvalid } expected := make(map[domain.ModerationActionKind]bool, 2) if flagsActive { expected[domain.ModerationActionClearPeerFlags] = true } if freezeActive { expected[domain.ModerationActionUnfreezeAccount] = true } if len(actions) != len(expected) { return domain.ErrModerationActionInvalid } for _, action := range actions { if !expected[action.Kind] { return domain.ErrModerationActionInvalid } delete(expected, action.Kind) } if len(expected) != 0 { return domain.ErrModerationActionInvalid } return nil }