Something went wrong. Try again.
A community based topic aggregation platform built on atproto
Something went wrong. Try again.
7.0 kB · 192 lines
Go
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193package moderation
import ( "context" "errors" "fmt" "time"
"Coves/internal/core/imageproxy"
"github.com/bluesky-social/indigo/atproto/syntax")
const ( illegalContentReason = "social.coves.moderation.defs#reasonIllegalContent" doxingReason = "social.coves.moderation.defs#reasonDoxing")
// removeReasons is every known moderation reason. removeContent and// restoreContent require one; labelContent and retractContentLabel accept one// optionally. Restore, label and retract-label reject the illegal-content and// doxing reasons on top of this set.var removeReasons = map[string]struct{}{ "social.coves.moderation.defs#reasonSpam": {}, "social.coves.moderation.defs#reasonHarassment": {}, doxingReason: {}, illegalContentReason: {}, "social.coves.moderation.defs#reasonRuleViolation": {}, "social.coves.moderation.defs#reasonModeratorDiscretion": {},}
func validateRemoveRequest(request RemoveContentRequest) error { uri, err := syntax.ParseATURI(request.Subject.URI) if err != nil || !uri.Authority().IsDID() || uri.RecordKey().String() == "" { return fmt.Errorf("%w: expected a content record URI with a DID authority", ErrInvalidSubject) } switch uri.Collection().String() { case CommentCollection, PostV2Collection, LegacyPostCollection: default: return fmt.Errorf("%w: unsupported subject collection", ErrInvalidSubject) } if _, err := syntax.ParseCID(request.Subject.CID); err != nil { return fmt.Errorf("%w: invalid subject CID", ErrInvalidRequest) } return validateMutationFields(request.IdempotencyKey, request.ExpectedVersion, request.Reason, request.PrivateNote)}
func (s *service) removeContent(ctx context.Context, actorDID string, request RemoveContentRequest) (*MutationResult, error) { if err := validateRemoveRequest(request); err != nil { return nil, err } now := time.Now() if s.config.Now != nil { now = s.config.Now() } fingerprint := mutationFingerprint(ActionRemove, "", request.Subject.URI, request.Subject.CID, request.ExpectedVersion, request.Reason, "", request.PrivateNote) var result *MutationResult var newlyBlocked []MediaBlock var purgeBlobs []imageproxy.BlockedBlob var ruleError error err := s.store.InTransaction(ctx, func(ctx context.Context, tx Transaction) error { fail := func(err error) error { ruleError = err return err } unavailable := func(err error) error { return fmt.Errorf("%w: %w", ErrModerationUnavailable, err) } if err := tx.LockActor(ctx, actorDID); err != nil { return unavailable(err) } stored, err := tx.LiveIdempotencyRecord(ctx, actorDID, s.config.InstanceDID, request.IdempotencyKey, now) if err != nil { return unavailable(err) } if stored != nil { if stored.Fingerprint != fingerprint { return fail(ErrIdempotencyConflict) } copy := stored.Result result = © return nil } count, err := tx.CountLiveIdempotencyKeys(ctx, actorDID, now) if err != nil { return unavailable(err) } if count >= s.config.MaxLiveIdempotencyKeys { return fail(fmt.Errorf("%w: live idempotency key limit %d reached", ErrInvalidRequest, s.config.MaxLiveIdempotencyKeys)) } version, err := tx.LockSubject(ctx, request.Subject.URI) if err != nil { return unavailable(err) } if request.ExpectedVersion != versionToken(version) { return fail(ErrStateConflict) } subject, err := readIndexedSubject(ctx, tx, request.Subject.URI) if errors.Is(err, ErrSubjectNotIndexed) { return fail(ErrSubjectNotFound) } if err != nil { return unavailable(err) } if subject == nil { return unavailable(errors.New("indexed subject missing")) } if !subject.AuthorDeleted && subject.CID != request.Subject.CID { return fail(ErrContentChanged) } active, err := tx.ActiveRemoval(ctx, s.config.InstanceDID, request.Subject.URI) if err != nil { return unavailable(err) } activeLabels, err := tx.ActiveLabels(ctx, s.config.InstanceDID, request.Subject.URI) if err != nil { return unavailable(err) } recordState := RecordStatePresent var current *StrongRef if subject.AuthorDeleted { recordState = RecordStateDeleted } else { current = &StrongRef{URI: subject.URI, CID: subject.CID} } if active != nil { state, err := newSubjectState(request.Subject.URI, version, recordState, current, active, activeLabels, s.config.InstanceDID) if err != nil { return err } result = &MutationResult{Outcome: OutcomeUnchanged, State: state} } else { action, err := tx.InsertAction(ctx, Action{ ActorDID: actorDID, AuthorityDID: s.config.InstanceDID, ScopeKind: ScopeInstance, SubjectURI: request.Subject.URI, SubjectCollection: subject.Collection, SubjectCommunityDID: subject.CommunityDID, ObservedCID: subject.CID, Action: ActionRemove, Reason: request.Reason, PrivateNote: request.PrivateNote, Origin: OriginLocal, CreatedAt: now, }) if err != nil { return unavailable(err) } if action == nil || action.ID == "" { return unavailable(errors.New("action insert returned no identifier")) } if err := tx.SetRemovalDecision(ctx, s.config.InstanceDID, request.Subject.URI, action.ID, true); err != nil { return unavailable(err) } if err := tx.SetSubjectVersion(ctx, request.Subject.URI, version+1); err != nil { return unavailable(err) } // An empty owner DID on a media block means every owner, so a // subject that lost its owner is refused rather than blocking its // images for everyone. if subject.OwnerDID == "" && len(subject.BlobCIDs) > 0 { return unavailable(errors.New("indexed subject has images but no owner DID")) } newlyBlocked = imageMediaBlocks(subject, action) if len(newlyBlocked) > 0 { if err := tx.InsertMediaBlocks(ctx, newlyBlocked); err != nil { return unavailable(err) } if s.config.CDNPurgeTargets != nil { purgeBlobs = ownerCDNPurgeBlobs(newlyBlocked) if len(purgeBlobs) > 0 { if err := tx.RecordCDNPurgeTargets(ctx, purgeBlobs); err != nil { return unavailable(err) } } } } state, err := newSubjectState(request.Subject.URI, version+1, recordState, current, action, activeLabels, s.config.InstanceDID) if err != nil { return err } result = &MutationResult{Outcome: OutcomeApplied, State: state, Action: action} } return tx.SaveIdempotencyRecord(ctx, IdempotencyRecord{ ActorDID: actorDID, AuthorityDID: s.config.InstanceDID, Key: request.IdempotencyKey, Fingerprint: fingerprint, Result: *result, CreatedAt: now, ExpiresAt: now.Add(s.config.IdempotencyRetention), }) }) if err != nil { if ruleError != nil && errors.Is(err, ruleError) { return nil, ruleError } return nil, fmt.Errorf("%w: %w", ErrModerationUnavailable, err) } purgeMediaBlocks(ctx, s.config.Purger, newlyBlocked) if s.config.CDNPurgeTargets != nil && len(purgeBlobs) > 0 { s.config.CDNPurgeTargets.PurgeAfterCommit(ctx, purgeBlobs) } return result, nil}