Something went wrong. Try again.
A community based topic aggregation platform built on atproto
Something went wrong. Try again.
10 kB · 247 lines
Go
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248package moderation
import ( "context" "time"
"Coves/internal/core/imageproxy")
// Action kinds, scope kinds and origins recorded on moderation actions.const ( ActionRemove = "remove" ActionRestore = "restore" ActionLabel = "label" ActionRetractLabel = "retract-label"
// LabelNSFW is the only content label value the local classification // procedures accept. LabelNSFW = "nsfw"
ScopeInstance = "instance"
OriginLocal = "local"
// ModerationStateRemoved is the effective state of a subject with an // active removal decision. ModerationStateRemoved = "removed"
OutcomeApplied = "applied" OutcomeUnchanged = "unchanged")
// Action is one immutable row of the moderation action log.type Action struct { ID string ActorDID string AuthorityDID string ScopeKind string ScopeCommunityDID string SubjectURI string SubjectCollection string SubjectCommunityDID string SubjectRootURI string `json:",omitempty"` // omitempty keeps stored idempotency result JSON (pinned in idempotency_golden_test.go) unchanged for actions without a root. ObservedCID string Action string LabelValue string Reason string PrivateNote string ReversesActionID string // ReversedActionReason is the reason of the action named by ReversesActionID, // read with the row rather than stored as a column. Store.ListActions must // populate it: it decides whether the action is hidden, and an empty value // reads as not hidden. ReversedActionReason string Origin string CreatedAt time.Time // SubjectAccess is whether the public may know the subject exists, decided // when the log is read. Store.ListActions must set it on every row; the // service refuses a row that leaves it unset. It is read-time state, so it // stays out of stored idempotency results. SubjectAccess SubjectAccess `json:"-"` // ContentSnapshot is read-time state too: only Store.ListActions sets it, // so mutation results and stored idempotency results never carry it. ContentSnapshot *ContentSnapshot `json:"-"` // SubjectAuthorDeleted is read-time state set by Store.ListActions: the author has deleted the subject. // Any reader that sets ContentSnapshot must set it too, or the public log shows text the author deleted. SubjectAuthorDeleted bool `json:"-"` // SubjectRootWithheld is read-time state set by Store.ListActions: the public log must not name the // subject's root post, which has a hidden-reason action or is no longer indexed. SubjectRootWithheld bool `json:"-"`}
// ContentSnapshot is the text of a removed subject, as indexed when it was// removed. CapturedCID is the version it was read from.type ContentSnapshot struct { Title string Text string LinkURL string CapturedCID string}
// SubjectAccess says whether the public log may name an action's subject.// Removals are public by default, whatever the subject's community admission// status (decided 2026-10-03): a subject is public while its own post or// comment row is indexed, soft-deleted or not. One whose row is gone, as after// account erasure, is restricted.type SubjectAccess string
const ( SubjectAccessPublic SubjectAccess = "public" SubjectAccessRestricted SubjectAccess = "restricted")
// IndexedComment is the indexed comment row a mutation inspects, read under a// share lock so consumer writes serialize against the CID check.type IndexedComment struct { URI string CID string AuthorDeleted bool // OwnerDID is the repository holding the comment and its blobs. OwnerDID string // CommunityDID is the root post's community, empty when the root post is // not indexed. CommunityDID string // ImageCIDs are the blob CIDs of the comment's indexed image embed. ImageCIDs []string // RootURI is the post at the root of the comment's thread. RootURI string Text string}
// IndexedPost is the indexed post row a mutation inspects, read under a share// lock so consumer writes serialize against the CID check.type IndexedPost struct { URI string CID string AuthorDeleted bool // OwnerDID is the repository holding the post's blobs: the author for // postv2, the community for legacy posts. OwnerDID string CommunityDID string // BlobCIDs are the canonical CIDs of the post's proxy-served blobs. BlobCIDs []string // AuthorDID is the person who wrote the post, which differs from OwnerDID // for legacy posts. AuthorDID string Title string Text string // LinkURL is the URI of an external link embed. LinkURL string}
// MediaBlock suppresses Coves-served bytes of a blob. An empty OwnerDID// blocks the CID for every owner.type MediaBlock struct { OwnerDID string BlobCID string ActionID string}
// IdempotencyRecord is a stored mutation result bound to a request// fingerprint, scoped to (actor DID, authority DID, key).type IdempotencyRecord struct { ActorDID string AuthorityDID string Key string Fingerprint string Result MutationResult CreatedAt time.Time ExpiresAt time.Time}
// SubjectModeration is the stored moderation state of a subject.type SubjectModeration struct { Version int64 ActiveRemoval *Action ActiveLabels []Action}
// Store persists moderation state. Mutations run inside InTransaction.type Store interface { InTransaction(ctx context.Context, fn func(ctx context.Context, tx Transaction) error) error SubjectModeration(ctx context.Context, authorityDID, subjectURI string) (*SubjectModeration, error) // ListActions returns up to query.Limit actions, newest first, each with // ReversedActionReason and SubjectAccess populated. Under ExcludeHidden it // must omit every hidden action, under ExcludeLabelActions every action // whose kind is in PublicExcludedActions, and under ExcludeRestricted every // action whose subject is not SubjectAccessPublic; the service fails closed // if any of them comes back. ListActions(ctx context.Context, query ActionListQuery) ([]Action, error)}
// ActionListQuery selects a page of the action log, newest first.type ActionListQuery struct { // Limit is the page size plus one look-ahead row, which tells the service // whether a next page exists. Limit int Before *ActionKey SubjectURI string SubjectCollection string Action string Origin string AuthorityDID string ActorDID string CommunityDID string ActionID string Since *time.Time Until *time.Time ExcludeHidden bool // ExcludeRestricted omits every action whose subject is restricted. ExcludeRestricted bool // ExcludeLabelActions omits every action kind in PublicExcludedActions. ExcludeLabelActions bool}
// ActionKey is an action log position: rows strictly older than it follow.type ActionKey struct { CreatedAt time.Time ID string}
// Transaction is the set of operations a mutation performs atomically.type Transaction interface { // LockActor serializes idempotency admission for one actor. LockActor(ctx context.Context, actorDID string) error // LiveIdempotencyRecord returns the unexpired record for the key, or nil. LiveIdempotencyRecord(ctx context.Context, actorDID, authorityDID, key string, now time.Time) (*IdempotencyRecord, error) CountLiveIdempotencyKeys(ctx context.Context, actorDID string, now time.Time) (int, error) // SaveIdempotencyRecord stores the record, replacing an expired one. SaveIdempotencyRecord(ctx context.Context, record IdempotencyRecord) error // LockSubject locks the subject's version row, creating it at 0. LockSubject(ctx context.Context, subjectURI string) (int64, error) // ReadIndexedComment returns ErrSubjectNotIndexed for a never-indexed URI. ReadIndexedComment(ctx context.Context, subjectURI string) (*IndexedComment, error) // ReadIndexedPost returns ErrSubjectNotIndexed for a never-indexed URI. ReadIndexedPost(ctx context.Context, subjectURI string) (*IndexedPost, error) // GetAction returns ErrDecisionNotFound for an unknown id. GetAction(ctx context.Context, actionID string) (*Action, error) // ActiveRemoval returns the active removal action, or nil. ActiveRemoval(ctx context.Context, authorityDID, subjectURI string) (*Action, error) // ActiveLabels returns active instance-scope label actions, ordered by value. ActiveLabels(ctx context.Context, authorityDID, subjectURI string) ([]Action, error) InsertAction(ctx context.Context, action Action) (*Action, error) // InsertContentSnapshot stores the snapshot of a removal's subject, // written by authorDID, keyed to the removal's action. InsertContentSnapshot(ctx context.Context, actionID, authorDID string, snapshot ContentSnapshot) error // DeleteSubjectContentSnapshots deletes the snapshots of every action on subjectURI. DeleteSubjectContentSnapshots(ctx context.Context, subjectURI string) error SetRemovalDecision(ctx context.Context, authorityDID, subjectURI, actionID string, active bool) error // SetLabelDecision changes the instance-scope label decision for // (authorityDID, subjectURI, value). With active true it activates the // decision keyed to actionID, inserting it or reactivating an inactive row, // and fails if the decision is already active. With active false it // deactivates exactly one active decision whose action is actionID, and // fails if there is none. SetLabelDecision(ctx context.Context, authorityDID, subjectURI, value, actionID string, active bool) error SetSubjectVersion(ctx context.Context, subjectURI string, version int64) error InsertMediaBlocks(ctx context.Context, blocks []MediaBlock) error DeactivateMediaBlocks(ctx context.Context, actionID string) error RecordCDNPurgeTargets(ctx context.Context, blobs []imageproxy.BlockedBlob) error}
// MediaPurger removes cached bytes of newly blocked blobs.type MediaPurger interface { PurgeOwnerBlob(ctx context.Context, ownerDID, blobCID string) error PurgeBlob(ctx context.Context, blobCID string) error}