Help us shape the future of Go! Take the 2026 Go Developer Survey

contentstore

package
v0.135.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 28, 2026 License: MIT Imports: 40 Imported by: 0

Documentation

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrCommentsClosed: the target's comments are closed or hidden. -> 403
	// comments_closed
	ErrCommentsClosed = errors.New("content: comments are closed")
)
View Source
var (
	// ErrCommentsLocked: the actor can't open the content the comments are
	// on, and its kind's locked view hides them. -> 403 comments_locked
	ErrCommentsLocked = errors.New("content: comments are locked")
)
View Source
var (
	ErrConflict = errors.New("content: import conflict")
)
View Source
var (
	// ErrForbidden: visible but the actor may not consume it. -> 403
	ErrForbidden = errors.New("content: not accessible")
)
View Source
var (
	ErrInvalid = errors.New("content: invalid import")
)
View Source
var (
	// ErrNotFound: the reference does not exist. -> 404
	ErrNotFound = errors.New("content: not found")
)
View Source
var (
	// ErrNotVisible: exists but unpublished or soft-deleted. -> 404 (hidden).
	ErrNotVisible = errors.New("content: not visible")
)
View Source
var ErrSubjectErased = fmt.Errorf("content: subject erased")

ErrSubjectErased is returned when an erased account tries to write again.

View Source
var (
	// ErrTenant: a reference of another tenant reached this runtime.
	ErrTenant = errors.New("content: reference belongs to another tenant")
)
View Source
var RateLimitRedisErrors = expvar.NewInt("contentkit_content_ratelimit_redis_errors")

RateLimitRedisErrors counts shared-limit Redis failures, each served by the per-process count; published as expvar "contentkit_content_ratelimit_redis_errors".

TargetTypes are the route families interactions are served under.

Functions

func ChannelScope

func ChannelScope(entityID string) string

ChannelScope is the retained ban scope of a channel and its original posts. The opaque group: prefix preserves existing restrictions.

func OwnerScope

func OwnerScope(ownerID string) string

OwnerScope is the scope of content whose Resolution.Owner is ownerID, managed by that owner.

func RatingSQL

func RatingSQL(likes, dislikes string) string

RatingSQL is the Wilson lower bound (95%) of likes among likes and dislikes: the "best" order of comments and the rating of content. Its arguments are trusted column expressions, never user input.

Types

type Limits

type Limits struct {
	Comment         []content.Rate // default 5 per 5 minutes and 20 per hour
	CommentReaction []content.Rate // default 30 per minute
	Reaction        []content.Rate // default 30 per minute
	Favorite        []content.Rate // default 20 per minute
	PollVote        []content.Rate // default 30 per minute

	// Redis (or Garnet) shares the limits across replicas; pass the host's
	// client. Without it each process counts alone, so N replicas allow N
	// times the limit. Redis errors fall back to the per-process count.
	Redis     redis.UniversalClient
	KeyPrefix string // default "contentkit:content:rl:"; the tenant follows it
	// Disabled turns every limit off.
	Disabled bool
}

Limits are per-actor interaction limits: an actor is its user id, or its IP when anonymous. Every attempt spends one, so undoing (unfavorite, neutral) costs the same as doing. A nil field takes its default; an action with several rates must pass all of them.

type Media

type Media struct {
	Namespace string              // media partition; empty retains the content catalog key
	Images    content.MediaImages // *media.Manifests
	Folders   MediaFolders        // *media.Jobs
	// Reader reads items as the media read API does; the engine fills it
	// from its Reader.
	Reader   content.MediaReader
	PollKind string // media kind of poll folders; default "poll"
}

Media connects poll images to ContentKit media. The browser uploads an image to a Named upload path of the poll's item (the host's media registry declares it and its public preset), and hands its name ("i-{uuid}") to ContentKit, which stores it. Reads resolve names to the images' current public files, one MediaImages lookup per response; poll deletion removes the item. Runtime.CanUpload authorizes those uploads.

type MediaFolders

type MediaFolders interface {
	RetireItemsTx(ctx context.Context, tx pgx.Tx, items ...media.Deletion) error
}

MediaFolders records exact retirement intents in the poll mutation transaction; the existing content-deletion sweep performs storage erasure.

type Options

type Options struct {
	// Pool is the host's shared pgx pool; the runtime does not own its lifecycle.
	Pool *pgxpool.Pool
	// Schema is ContentKit's schema.
	Schema string
	// Tenant scopes every row, index, cursor and result. Required.
	Tenant string

	// Auth says who a request is (authkit/auth; AuthKit's ak.Authenticator()).
	// The routes ask it at most once per request and build the actor from
	// its Verified: anonymous without a credential, 401 for one that fails.
	// Staff hold ContentKit's permissions (access.Permissions) in its root
	// scope (auth.Root), checked by the Verified's Can: declare them in its
	// roles. Required.
	Auth auth.Authenticator
	// UserInfo is the host's directory (authkit/userinfo; AuthKit's
	// ak.UserInfo()): authors' names and usernames, read once per response
	// and never copied. Nil shows authors by id.
	UserInfo userinfo.Lookup
	// Avatars shows authors' avatars (media.Registry.Avatars); nil shows
	// none.
	Avatars content.Avatars
	// Resolver decides interaction targets: content items and their versions
	// (the catalog's) and entities. Required.
	Resolver resolve.Resolver
	// Kinds is the site's content model: interactions target its content
	// kinds and entity kinds, and comments carry one of its languages.
	Kinds kinds.Config

	// PreferenceSyncOverlap bounds how long a preference write may take to
	// commit and still be picked up by SyncPreferences.
	// Default DefaultPreferenceSyncOverlap.
	PreferenceSyncOverlap time.Duration

	// Optional ports (nil -> default).
	Processor content.ContentProcessor // comments; default: trim plain text
	// Moderator screens comment writes, and the catalog's (Screen); nil
	// publishes everything.
	Moderator content.ContentModerator
	// ModeratorTimeout bounds one Screen call (default 5s); a slower
	// moderator counts as failed and the write is held.
	ModeratorTimeout time.Duration
	// ModeratorCooldown is how long writes are held without calling the
	// moderator after it failed repeatedly (default 30s); see CheckModerator.
	ModeratorCooldown time.Duration

	// Media stores poll images in ContentKit media; nil refuses image writes
	// with 501 not_configured. See Media.
	Media *Media

	// ProviderDataEraser is required when moderator ports retain
	// external personal data.
	// Nil explicitly means stateless ports; never remove it during an outage.
	ProviderDataEraser content.ProviderDataEraser

	// Limits are the per-actor interaction rate limits (limits.go); the zero
	// value takes the defaults, counted per process.
	Limits Limits

	// Anonymous is what signed-out visitors may do; the zero value lets them
	// only read.
	Anonymous content.Anonymous
	// CommentMaxLength is the longest comment in characters (runes);
	// default DefaultCommentMaxLength.
	CommentMaxLength int
	// DuplicateWindow refuses identical comments within this duration; zero disables it.
	DuplicateWindow time.Duration

	// NotificationPublisher is AuthKit's scoped account channel. Nil disables
	// user notifications; ContentKit never constructs its own inbox.
	NotificationPublisher notify.Publisher
	// Accounts serves the account lifecycle routes; nil answers them 500.
	Accounts content.AccountLifecycle
	// EraseAccountReceipts connects the existing account cleanup lifecycle.
	// Nil is used by standalone content runtimes without shared account storage.
	EraseAccountReceipts func(context.Context, []string) error

	// Logger receives the access log: each request at DEBUG, a 500 at ERROR
	// with its cause. nil -> slog.Default().
	Logger *slog.Logger
}

Options configures a Runtime. Pool, Schema, Tenant, Auth and Resolver are mandatory; the rest fall back to documented defaults.

type Runtime

type Runtime struct {
	// contains filtered or unexported fields
}

Runtime is one tenant's embedded content module: shared deps + the module services, exposing one mountable http.Handler.

func New

func New(ctx context.Context, opts Options) (*Runtime, error)

New constructs a Runtime over a schema the ContentKit baseline was applied to and wires the module services.

func (*Runtime) AnonymizeAccount

func (rt *Runtime) AnonymizeAccount(ctx context.Context, actorID, anonymousID string) error

AnonymizeAccount permanently removes account attribution without removing comments or contribution rows. Counters are unchanged.

func (*Runtime) CanUpload

func (rt *Runtime) CanUpload(ctx context.Context, actor access.Actor, t media.UploadTarget) (media.UploadGrant, error)

CanUpload is media's UploadAuthorizer for poll items: the actor holds root:polls:write and the poll exists in this tenant. Every other target is refused; other kinds are routed elsewhere.

func (*Runtime) CheckModerator

func (rt *Runtime) CheckModerator(context.Context) error

CheckModerator reports the moderator's state without calling it: an error while repeated failures hold writes for review. Hosts register it as an optional dependency (helpers deps) for statusz and app_dependency_up.

func (*Runtime) Handler

func (rt *Runtime) Handler() http.Handler

Handler returns the content routes, each behind its tier's gate over Options.Auth: the engine's content route group (/v1). Each request runs in an access.WithMemo context, so a Gate resolver reads billing at most once per request.

func (*Runtime) ImportComments

func (rt *Runtime) ImportComments(ctx context.Context, tx pgx.Tx, in []content.ImportComment) error

ImportComments writes comments in tx, inserting new ones and updating stored ones whose import changed (a moderator's decision stays), then recounts their references' comment counts and their threads' replies.

func (*Runtime) ImportDeletes

func (rt *Runtime) ImportDeletes(ctx context.Context, tx pgx.Tx, comments, polls []string) error

ImportDeletes deletes comments and polls in tx as their delete routes do: a comment is tombstoned and its reference's and thread's counts follow; a poll tombstone and exact media retirement intent commit together. The existing content-deletion sweep erases files; deleted rows remain for history.

func (*Runtime) ImportFavorites

func (rt *Runtime) ImportFavorites(ctx context.Context, tx pgx.Tx, in []content.ImportFavorite) error

ImportFavorites writes bookmarks in tx: a new one takes a fresh preference revision. Counts follow.

func (*Runtime) ImportPolls

func (rt *Runtime) ImportPolls(ctx context.Context, tx pgx.Tx, in []content.ImportPoll) error

ImportPolls writes polls and their options in tx; options the import no longer lists go, with their votes.

func (*Runtime) ImportReactions

func (rt *Runtime) ImportReactions(ctx context.Context, tx pgx.Tx, in []content.ImportReaction) error

ImportReactions writes reactions in tx: a new value takes a fresh preference revision, so preference sync exports it. Counts follow.

func (*Runtime) ImportVotes

func (rt *Runtime) ImportVotes(ctx context.Context, tx pgx.Tx, in []content.ImportVote) error

ImportVotes writes votes in tx (one per person, or IP, per poll; the last wins) and recounts their polls' options.

func (*Runtime) ListHeld

func (rt *Runtime) ListHeld(ctx context.Context, kind, cursor string, limit int) (content.HeldPage, error)

ListHeld pages this tenant's held comments oldest first; cursor is the previous page's Next ("" = start). limit is clamped to [1, 100].

func (*Runtime) MediaResolver

func (rt *Runtime) MediaResolver() resolve.Resolver

MediaResolver is the resolve.Resolver of the poll media kind: route the host registry's Hooks.Resolver to it for Media.PollKind, as CanUpload is routed. A poll's folder shows while the poll exists, edited by root:polls:write holders. Other refs are omitted (denied).

func (*Runtime) RaisePreferenceFloor

func (rt *Runtime) RaisePreferenceFloor(ctx context.Context, tx pgx.Tx, floor int64) error

RaisePreferenceFloor advances the preference revision sequence so every later revision exceeds floor: after a Postgres restore, past the newest revision a retained signal plane holds. It never moves it back and fails outside the safe range.

func (*Runtime) Ref

func (rt *Runtime) Ref(contentKind, contentID string) contentref.ContentRef

Ref returns a reference to a work of this runtime's tenant.

func (*Runtime) Resolve

func (rt *Runtime) Resolve(ctx context.Context, kind, id string, d content.ReviewDecision) error

Resolve writes a reviewer's final decision on a held item. Approve publishes it (counts follow); reject keeps it author-visible with the reason. A missing, foreign-tenant or not-held item is ErrNotFound.

func (*Runtime) ResyncPreferences

func (rt *Runtime) ResyncPreferences(ctx context.Context, send content.PreferenceSender) (content.PreferenceSyncReport, error)

ResyncPreferences re-sends every exportable row (zeros included): the periodic safety net for sink loss and slower-than-overlap commits. A sender is required.

func (*Runtime) SetAccountDeleted

func (rt *Runtime) SetAccountDeleted(ctx context.Context, actorID string, deleted bool) error

SetAccountDeleted changes author presentation during the recovery period. It preserves every interaction and never reverses a completed hard deletion.

func (*Runtime) SetAccountHidden

func (rt *Runtime) SetAccountHidden(ctx context.Context, actorID string, hidden bool) error

SetAccountHidden changes comment visibility without changing moderation or account deletion state. Repeating the same state leaves counts unchanged.

func (*Runtime) SyncPreferences

SyncPreferences sends every exportable row whose revision is past the watermark, in revision order, then records a checkpoint. No connection is held across send. Revisions are allocated before commit, so each scan restarts from the newest checkpoint taken at least Options.PreferenceSyncOverlap before the latest one: a row is delivered as long as its transaction commits within the overlap of allocating its revision (ResyncPreferences covers the rest). A checkpoint is recorded only after its own scan was sent, so overlapping syncs stay correct and only repeat sends. send is required.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL