Something went wrong. Try again.
A community based topic aggregation platform built on atproto
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183package posts
import ( "errors" "fmt" "strings"
coreerrors "Coves/internal/core/errors" "Coves/internal/core/richtext")
// Sentinel errors for common post operationsvar ( // ErrCommunityNotFound is returned when the community doesn't exist in AppView ErrCommunityNotFound = errors.New("community not found")
// ErrNotAuthorized is returned when user isn't authorized to post in community // (e.g., banned, private community without membership - Beta) ErrNotAuthorized = errors.New("user not authorized to post in this community")
// ErrBanned is returned when user is banned from community (Beta) ErrBanned = errors.New("user is banned from this community")
// ErrInvalidContent is returned for general content violations ErrInvalidContent = errors.New("invalid post content")
// ErrNotFound is returned when a post is not found by URI ErrNotFound = errors.New("post not found")
// ErrRateLimitExceeded is returned when a submission is refused for being // over quota — primarily the per-author, per-community submission limit of // docs/POSTS.md §8 (DecisionRateLimitExceeded). The handler // maps it to a 429. (An aggregator over its OWN hourly quota is refused // through the aggregators package's sentinel instead, so the boundary can // tell the two apart.) ErrRateLimitExceeded = errors.New("rate limit exceeded")
// ErrInvalidCursor is returned when a pagination cursor is malformed ErrInvalidCursor = errors.New("invalid pagination cursor")
// ErrActorNotFound is returned when the requested actor does not exist ErrActorNotFound = errors.New("actor not found")
// ErrDuplicateSubmission is returned when an author resubmits content // identical to something already on the submission ledger for the current // dedupe window (docs/POSTS.md §8). // // THE WORDING IS LOAD-BEARING. IsConflict below classifies an error by // looking for "duplicate key", "already exists" or "already indexed" in its // text, because a genuine index conflict arrives from the driver as a // string rather than as a typed error. This sentinel must therefore avoid // all three phrasings: a duplicate SUBMISSION is a client being refused at // the admission gate, while a conflict is the indexer meeting a record it // already has, and collapsing them would let a refused post be reported as // successfully indexed. ErrDuplicateSubmission = errors.New("an identical submission from this author to this community was refused as a repeat")
// ErrNoAuthorCredentials is returned when the AppView cannot open the // AUTHOR's repository because it holds nothing to authenticate as them // with: no OAuth session on the request and no stored session to resume // (an aggregator whose tokens were never granted, or were revoked). // // IT IS ITS OWN SENTINEL RATHER THAN A GENERIC FAILURE because the two // audiences need opposite things from it. A human's missing session is // "sign in again" — a 401 the client can act on. An aggregator's revoked // tokens are an operator problem: the service is running, correctly // configured and completely unable to post, and a 500 saying "failed to // write post to PDS" would have that diagnosed as a PDS outage. Posts used // to be written with the COMMUNITY's credentials, so this class of failure // did not exist before the write path flipped to the author's repo. ErrNoAuthorCredentials = errors.New("no credentials to write to the author's repository")
// ErrConcurrentModification is returned when an update's swap guard fires: // the record changed between the read that shaped the edit and the write // that would have applied it. // // The API boundary maps it to 409. Retrying is the client's decision, not // the server's, because the edit was composed against content that no // longer stands — silently re-reading and re-applying would let a second // device's edit be overwritten by a first device that never saw it. ErrConcurrentModification = errors.New("the post was modified concurrently")
// ErrInvalidEmbed marks a post refused because its embed breaks the // record lexicon or the embed write rules (XRPC InvalidEmbed, 400). ErrInvalidEmbed = errors.New("invalid embed")
// ErrTooManyImages marks a post refused because its embed carries more // than eight images (XRPC TooManyImages, 400). It does not match // ErrInvalidEmbed. ErrTooManyImages = errors.New("too many images")
// ErrInvalidImageFacet marks a post refused because an #image facet breaks // a write rule (XRPC InvalidImageFacet, 400). It is richtext's sentinel, so // the rule check's own error matches it without re-wrapping. ErrInvalidImageFacet = richtext.ErrInvalidImageFacet)
// ValidationError is the shared validation error type. It is aliased rather// than redefined so that one errors.As at the API boundary matches validation// failures from every domain package, instead of each handler needing to know// which domains it might hear from.type ValidationError = coreerrors.ValidationError
// NewValidationError creates a new validation errorfunc NewValidationError(field, message string) error { return coreerrors.NewValidationError(field, message)}
// NewValidationErrorFrom creates a validation error that keeps cause matchable// via errors.Is while presenting a field-scoped message to the client.func NewValidationErrorFrom(field string, cause error) error { return coreerrors.NewValidationErrorFrom(field, cause)}
// IsValidationError checks if error is a validation errorfunc IsValidationError(err error) bool { return coreerrors.IsValidationError(err)}
// ContentRuleViolation represents a violation of community content rules// (Deferred to Beta - included here for future compatibility)type ContentRuleViolation struct { Rule string // e.g., "requireText", "allowedEmbedTypes" Message string // Human-readable explanation}
func (e *ContentRuleViolation) Error() string { return fmt.Sprintf("content rule violation (%s): %s", e.Rule, e.Message)}
// NewContentRuleViolation creates a new content rule violation errorfunc NewContentRuleViolation(rule, message string) error { return &ContentRuleViolation{ Rule: rule, Message: message, }}
// IsContentRuleViolation checks if error is a content rule violationfunc IsContentRuleViolation(err error) bool { var violation *ContentRuleViolation return errors.As(err, &violation)}
// NotFoundError is the shared not-found error type, aliased for the same// reason as ValidationError above.type NotFoundError = coreerrors.NotFoundError
// NewNotFoundError creates a new not found errorfunc NewNotFoundError(resource, id string) error { return coreerrors.NewNotFoundError(resource, id)}
// IsNotFound checks if error is a not found error.//// errors.Is rather than == : these sentinels travel up through service layers// that wrap them with %w for context, and an == comparison stops matching the// moment anyone adds that context — silently turning a 404 into a 500.func IsNotFound(err error) bool { return coreerrors.IsNotFound(err) || errors.Is(err, ErrCommunityNotFound) || errors.Is(err, ErrNotFound)}
// IsConflict checks if error is due to duplicate/conflict.//// This inspects the message because the conflict usually originates in the// PostgreSQL driver as a unique-violation string rather than as a typed error// the repository translates. Typed conflicts (coreerrors.ConflictError) are// checked first so callers that do return one are matched exactly.func IsConflict(err error) bool { if err == nil { return false } if coreerrors.IsConflict(err) { return true } message := err.Error() return strings.Contains(message, "already indexed") || strings.Contains(message, "duplicate key") || strings.Contains(message, "already exists")}