content

package
v0.13.0 Latest Latest
Warning

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

Go to latest
Published: Sep 22, 2026 License: MIT Imports: 29 Imported by: 0

Documentation

Overview

Package content is ContentKit's interaction module: posts, comments, reactions, favorites and polls over tenant-scoped content references, stored in the host schema's social_* tables. Everything host-specific lives behind the ports in this file; the package imports no sibling kit and bakes in no host assumption. Content kinds are host-registered, access is an opaque host verdict, ids are opaque text, and every key, index and cursor carries the tenant pinned at construction.

Index

Constants

View Source
const (
	CodeInvalidRequest     = "invalid_request"
	CodeUnauthorized       = "unauthorized"
	CodeForbidden          = "forbidden"
	CodeNotFound           = "not_found"
	CodeConflict           = "conflict"
	CodeModerationRejected = "moderation_rejected"
	CodeUnprocessable      = "unprocessable"
	// CodeNotConfigured: the capability exists but the host never wired its
	// port (MediaStore, AnswerClassifier). Retrying does not help. -> 501
	CodeNotConfigured = "not_configured"
	// CodeTenantMismatch: a host port answered with another tenant's data.
	// A configuration fault, not a client fault; the cause stays in the log.
	CodeTenantMismatch = "tenant_mismatch"
	CodeInternal       = "internal_error"
)

Stable public error codes: the machine-readable half of ContentKit's error body. Clients branch on Code; Error is a human message and may change.

View Source
const (
	ModerationApproved = "approved"
	ModerationHeld     = "held"
	ModerationRejected = "rejected"
)

Moderation states stored in social_comments.moderation / social_posts.moderation.

View Source
const (
	PollMultipleChoice = "multiple_choice"
	PollFreeText       = "free_text"
)

Poll kinds.

View Source
const (
	PreferenceAxisReaction = "reaction" // value -1 dislike / 0 neutral / 1 like
	PreferenceAxisFavorite = "favorite" // value 1 favorited / 0 not
)

Preference axes stored in the snapshot table.

View Source
const (
	KindComment = "comment"
	KindPost    = "post"
)

Reserved content kinds owned by this package: reactions on comments and posts are keyed by them and never pass through the ContentResolver.

View Source
const MaxExportedPreferencesPerBatch = 1000

MaxExportedPreferencesPerBatch bounds one cutover key-reconciliation call.

View Source
const MigratekitApp = "socialkit"

MigratekitApp is the ledger label of the social lineage. It is an identity existing installations already carry, not vocabulary.

Variables

View Source
var (
	// ErrNotFound: the reference does not exist. -> 404
	ErrNotFound = errors.New("content: not found")
	// ErrNotVisible: exists but unpublished or soft-deleted. -> 404 (hidden).
	ErrNotVisible = errors.New("content: not visible")
	// ErrForbidden: visible but the actor may not consume it. -> 403
	ErrForbidden = errors.New("content: not accessible")
	// ErrTenant: a reference of another tenant reached this runtime.
	ErrTenant = errors.New("content: reference belongs to another tenant")
	// ErrNoClassifier: a free-text poll needs an AnswerClassifier. -> 501
	ErrNoClassifier = errors.New("content: free-text polls need an AnswerClassifier")
)

Sentinel errors a ContentResolver returns to gate a target; mapped to HTTP status without leaking which one beyond the code.

View Source
var ErrSubjectErased = fmt.Errorf("content: subject erased")

ErrSubjectErased is the permanent private-content write fence.

Functions

func AssignTenant

func AssignTenant(ctx context.Context, db search.Executor, schema, tenant string) (int64, error)

AssignTenant stamps tenant on every row the content-refs migration converted (tenant_id = ”): a host schema is single-tenant, so this is the one-time adoption step after the migration. Idempotent; returns the rows updated.

func Migrate

func Migrate(ctx context.Context, db *sql.DB, schema string) error

Migrate applies the social lineage (migrations.Social) into the host schema under its ledger label, creating the schema when missing. Idempotent.

Types

type Actor

type Actor struct {
	ID        string // stable subject id (uuid text); empty when Anonymous
	Kind      string // opaque: "user" | "service" | "delegated" | ...
	IP        string // anon fallback key for reactions / poll votes
	Anonymous bool
}

Actor is the already-authenticated caller, read from context by the Identity port. ContentKit never authenticates.

type ActorReaction

type ActorReaction struct {
	contentref.ContentRef
	Value     int16     `json:"value"` // -1 or 1 (neutral rows are excluded)
	CreatedAt time.Time `json:"created_at"`
	UpdatedAt time.Time `json:"updated_at"`
}

ActorReaction is one row of an actor's reaction history for one kind.

type AdminComment

type AdminComment struct {
	Comment
	contentref.ContentRef
	DeletedAt *time.Time `json:"deleted_at,omitempty"`
}

AdminComment is the moderation view: raw body (no tombstoning), deletion and moderation state and the content reference.

type Answer

type Answer struct {
	Tenant     string
	QuestionID string
	AnswerID   string
	Revision   int64  // monotonic per answer; retries retain this revision
	SubjectID  string // opaque authenticated actor, never an IP
	Text       string
}

Answer is one free-text poll answer handed to the AnswerClassifier.

type AnswerClassifier

type AnswerClassifier interface {
	Classify(ctx context.Context, a Answer) (GroupAssignment, error)
}

AnswerClassifier groups free-text poll answers. Classify runs when an answer is stored or edited. Poll results come from source assignments. Without a registered classifier a free-text poll cannot be created. Classify must be idempotent by (Tenant, AnswerID, Revision), ignore older revisions. ContentKit accepts assignments only by source-revision CAS; the stored result is the sole authority for current membership and labels. Provider erasure/lifecycle wiring is a separate host integration obligation.

type AuthorReactions

type AuthorReactions struct {
	Likes    int64 `json:"likes"`
	Dislikes int64 `json:"dislikes"`
}

AuthorReactions is one author's received like/dislike totals across their published comments.

type Authorizer

type Authorizer interface {
	Can(ctx context.Context, actor Actor, perm string) (bool, error)
}

Authorizer answers whether an actor holds an opaque host permission. Callers are fail-closed: an error is never "allowed".

type BasicModerator

type BasicModerator struct {
	AllowLinks  bool          // default false: reject bodies containing URLs
	DupWindow   time.Duration // 0 = 30s; negative disables
	CensorWords []string      // nil = built-in set; empty = none; whole words, case-insensitive
	// contains filtered or unexported fields
}

BasicModerator is the deterministic policy: reject links, near-instant duplicate submissions from one actor and a censor word list. The dup guard is per process; multi-replica hosts wanting global dedup wire their own.

func (*BasicModerator) EraseSubjects

func (m *BasicModerator) EraseSubjects(_ context.Context, tenant string, ids []string) error

func (*BasicModerator) Screen

Screen applies the rules to Title and Text. Edits skip the dup guard.

func (*BasicModerator) StatelessPolicy

func (*BasicModerator) StatelessPolicy()

type Chain

type Chain []ContentModerator

Chain runs moderators in order; the first reject or review verdict wins and an error fails the chain. Put a BasicModerator in front of an AI moderator.

func (Chain) Screen

func (c Chain) Screen(ctx context.Context, in ModerationInput) (Verdict, error)

type ClassificationPage

type ClassificationPage struct {
	Classified int
	Next       string
}

ClassificationPage reports one bounded pending scan. Resume Next even when a provider error is returned; empty Next ends the sweep. Start the next sweep from empty, so failed answers and new revisions behind the cursor are retried.

type Comment

type Comment struct {
	ID         string      `json:"id"`
	ReplyToID  string      `json:"reply_to_id,omitempty"`
	UserID     string      `json:"user_id,omitempty"`
	AnonName   string      `json:"anon_name,omitempty"`
	Body       string      `json:"body"`
	Deleted    bool        `json:"deleted"`
	Likes      int         `json:"likes"`
	Dislikes   int         `json:"dislikes"`
	Mine       int16       `json:"mine"` // caller's own reaction: -1/0/1
	ReplyCount int         `json:"reply_count"`
	Author     *PublicUser `json:"author,omitempty"`
	// Moderation is "held" or "rejected" on the author's own unpublished
	// comments (with the reason); absent on published ones.
	Moderation       string    `json:"moderation,omitempty"`
	ModerationReason string    `json:"moderation_reason,omitempty"`
	CreatedAt        time.Time `json:"created_at"`
	UpdatedAt        time.Time `json:"updated_at"`
}

Comment is the API view of a social_comments row.

type ContentCanonicalizer

type ContentCanonicalizer interface {
	Canonical(ref contentref.ContentRef) (contentref.ContentRef, bool)
}

ContentCanonicalizer maps a resolved reference to the one reference an actor's reaction or favorite is recorded, counted and exported under: per-language routes ("42:en", "42:ja") collapse to one content_id; an explicit content_version_id stays a distinct key; a language suffix never implies a version. ok=false keeps the target out of the preference boundary (its rows keep the resolver's reference and nothing is exported). Comment threads never pass through it. Nil disables export: standalone hosts need no analytics sink.

type ContentCanonicalizerFunc

type ContentCanonicalizerFunc func(ref contentref.ContentRef) (contentref.ContentRef, bool)

ContentCanonicalizerFunc adapts a function to the ContentCanonicalizer port.

func (ContentCanonicalizerFunc) Canonical

type ContentModerator

type ContentModerator interface {
	Screen(ctx context.Context, in ModerationInput) (Verdict, error)
}

ContentModerator screens every comment/post write before it publishes. Absent port: every write publishes. An error or an unknown decision fails closed to review: the submission is kept, held, never published unscreened.

type ContentProcessor

type ContentProcessor interface {
	Sanitize(ctx context.Context, raw string) (string, error)
}

ContentProcessor sanitizes rich text on write. Default: strip tags.

type ContentResolver

type ContentResolver interface {
	Resolve(ctx context.Context, ref contentref.ContentRef, actor Actor) (Resolution, error)
}

ContentResolver is the one mandatory content hook and the whole gating surface: it says whether a ContentRef exists, is visible and is accessible. Report absence either through the sentinel errors (ErrNotFound / ErrNotVisible / ErrForbidden) or through the Resolution flags.

type Counts

type Counts struct {
	Likes        int `json:"likes"`
	Dislikes     int `json:"dislikes"`
	Favorites    int `json:"favorites"`
	CommentCount int `json:"comment_count"`
}

Counts is the denormalized per-reference aggregate (social_entity_counts).

type Decision

type Decision string

Decision is a ContentModerator's outcome for one text write.

const (
	DecisionApprove Decision = "approve" // publish
	DecisionReject  Decision = "reject"  // refuse the write: 422 with the reason, nothing stored
	DecisionReview  Decision = "review"  // store held: author-only until a reviewer resolves it
)

type FavoriteItem

type FavoriteItem struct {
	contentref.ContentRef
	CreatedAt time.Time `json:"created_at"`
}

FavoriteItem is one row of the caller's wishlist (newest-first on list).

type FeedItem

type FeedItem struct {
	Comment
	contentref.ContentRef
}

FeedItem is a Comment plus its canonical content reference, for the cross-content latest feed (hosts hydrate titles/covers from the reference).

type Group

type Group struct {
	ID    string `json:"id"`
	Label string `json:"label"`
	Count int    `json:"count"`
}

Group is one answer group of a free-text poll with its current size. The ContentKit owns current assignments and counts, so delayed provider side effects cannot rewrite results.

type GroupAssignment

type GroupAssignment struct {
	GroupID string
	Label   string
}

GroupAssignment is the group an answer was placed in at store time.

type HeldItem

type HeldItem struct {
	Kind     string `json:"kind"`     // KindComment | KindPost
	Revision int64  `json:"revision"` // required when resolving this exact screened text
	ID       string `json:"id"`
	// Ref is the commented content, or the post's own reference.
	Ref           contentref.ContentRef `json:"ref"`
	AuthorID      string                `json:"author_id,omitempty"`
	AnonName      string                `json:"anon_name,omitempty"`
	Title         string                `json:"title,omitempty"`
	Body          string                `json:"body"`
	Reason        string                `json:"reason"`
	Model         string                `json:"model,omitempty"`
	PromptVersion string                `json:"prompt_version,omitempty"`
	Confidence    float64               `json:"confidence,omitempty"`
	Error         string                `json:"error,omitempty"` // moderator failure that forced the hold
	HeldAt        time.Time             `json:"held_at"`
	CreatedAt     time.Time             `json:"created_at"`
}

HeldItem is one comment or post awaiting review.

type HeldPage

type HeldPage struct {
	Items []HeldItem `json:"items"`
	Next  string     `json:"next,omitempty"`
}

HeldPage is one page of the review queue; Next resumes after the last item.

type Identity

type Identity interface {
	Actor(ctx context.Context) (Actor, bool)
}

Identity reads the authenticated actor from context; the host's middleware populated it upstream.

type MediaStore

type MediaStore interface {
	Put(ctx context.Context, key string, data []byte, contentType string) (url string, err error)
}

MediaStore stores option/cover images. Default: uploads are unsupported.

type ModerationInput

type ModerationInput struct {
	SubjectID string // opaque content author; may differ from editing moderator
	Tenant    string
	Actor     Actor
	// Ref is the content the item belongs to: the commented work for a
	// comment, the post's own reference for a post.
	Ref    contentref.ContentRef
	Kind   string // KindComment | KindPost
	ItemID string // the existing item on an edit; empty on create
	Title  string // posts only
	Text   string
}

ModerationInput is one comment or post body about to publish. The moderator sees sanitized text and opaque ids only.

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 the host schema holding the social_* tables (contentkit.Migrate).
	Schema string
	// Tenant scopes every row, index, cursor and result. Required.
	Tenant string
	// SearchSchema is the keyword-profile schema whose dirty queue receives
	// posts as search documents. Empty: posts are not indexed.
	SearchSchema string

	// Mandatory ports.
	Identity Identity
	Authz    Authorizer
	Resolver ContentResolver

	// Canonicalizer enables the preference boundary (preferences.go): the
	// reaction/favorite row, the counts rollup and the exported snapshot all
	// use the reference it returns. nil = no preference export.
	Canonicalizer ContentCanonicalizer

	// Optional ports (nil -> default).
	Users             UserEnricher     // default: no enrichment (ids only)
	Media             MediaStore       // explicit override; usually leave nil and set Storage
	Processor         ContentProcessor // comments and post excerpts; default: strip tags
	PostBodyProcessor ContentProcessor // post bodies; default: Processor
	// Moderator screens comment/post writes; nil publishes everything.
	// Compose a BasicModerator in front of an AI moderator with Chain.
	Moderator ContentModerator
	// Classifier groups free-text poll answers; nil refuses free-text polls.
	Classifier AnswerClassifier

	// Storage configures the built-in S3-backed media store (poll/post image
	// upload to a public bucket); used when Media is nil. See StorageConfig.
	Storage *StorageConfig

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

	// Perms are the opaque host permission strings gating privileged writes.
	Perms Perms

	// ContentKinds are the commentable/reactable/favoritable kinds the host
	// registers (e.g. "gallery", "video", "post"). Unregistered kinds are 404.
	ContentKinds []string

	// 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, Identity, Authz and Resolver are mandatory; the rest fall back to documented defaults.

type Perms

type Perms struct {
	PostWrite        string // create/update/delete posts
	PollWrite        string // create/update/delete polls + options
	CommentModerate  string // moderator delete/restore of another actor's comment
	ModerationReview string // list and resolve held comments and posts
}

Perms carries the opaque host permission strings checked through Authorizer.Can before privileged writes. An unset gate fails closed.

type PreferenceAck

type PreferenceAck struct {
	PreferenceKey
	Revision int64
}

PreferenceAck acknowledges delivery of exactly Revision for the key.

type PreferenceConflict

type PreferenceConflict struct {
	PreferenceKey
	SourceIDs    []string // the pre-collapse content ids
	SourceValues []int16
	Resolved     int16
}

PreferenceConflict is one (actor, reference, axis) group whose source rows disagreed or were several, resolved by the documented rule.

type PreferenceDelivery

type PreferenceDelivery struct {
	Delivered    int
	Acknowledged int
	Retried      int
	Erased       int
	// Next is the key the sweep stopped at (zero when it ran out); a replay
	// interrupted by maxRows resumes from it.
	Next PreferenceKey
}

PreferenceDelivery reports one sweep.

type PreferenceDisposition

type PreferenceDisposition int

PreferenceDisposition is a sink's verdict for one delivered snapshot. A void callback is not proof of delivery: only PreferenceAccepted acknowledges, and only after the sink durably accepted the row.

const (
	// PreferenceRetry is a transient failure: the row stays pending.
	PreferenceRetry PreferenceDisposition = iota
	// PreferenceAccepted means the sink durably holds this revision.
	PreferenceAccepted
	// PreferenceSubjectErased is terminal: the subject is erased at the sink, so
	// the obligation is purged instead of retried.
	PreferenceSubjectErased
)

type PreferenceKey

type PreferenceKey struct {
	TenantID         string
	ActorID          string
	ContentKind      string
	ContentID        string
	ContentVersionID string
	Axis             string
}

PreferenceKey identifies one snapshot: the tenant, an authenticated actor, the canonical content reference and the axis. Field order is the table's key order (keyset cursors compare it).

func (PreferenceKey) Ref

Ref returns the snapshot's content reference.

type PreferenceMigration

type PreferenceMigration struct {
	ArchivedRows    int64
	ReactionRows    int64 // reaction rows after collapse
	FavoriteRows    int64 // favorite rows after collapse
	Conflicts       []PreferenceConflict
	Seeded          int64 // snapshots written from source truth
	Tombstoned      int64 // zero snapshots for previously exported, now-absent keys
	CountsRewritten int64
	DryRun          bool
}

PreferenceMigration reports what MigratePreferences did.

type PreferenceMigrationOptions

type PreferenceMigrationOptions struct {
	// ExportedKeys are keys a previous exporter already sent. Any with no
	// surviving source row is seeded as a zero snapshot, so a sink cannot keep
	// counting a preference this host no longer holds. OccurredAt for such a
	// row is the cutover time: an explicit reconciliation, not invented history.
	ExportedKeys []PreferenceKey
	// DryRun computes and reports the migration, then rolls it back.
	DryRun bool
}

PreferenceMigrationOptions configures MigratePreferences.

type PreferenceSink

type PreferenceSink interface {
	DeliverPreferences(ctx context.Context, snaps []PreferenceSnapshot) ([]PreferenceDisposition, error)
}

PreferenceSink receives immutable copies of snapshots and reports one disposition per snapshot, in order. An error fails the whole page (every row stays pending). Delivery is at-least-once: a crash after the sink accepted and before the acknowledgement replays the same revision.

type PreferenceSnapshot

type PreferenceSnapshot struct {
	PreferenceKey
	Value             int16
	Revision          int64
	OccurredAt        time.Time
	DeliveredRevision int64
}

PreferenceSnapshot is the immutable copy of one committed preference: the actor's current value, the revision that committed it and that revision's mutation time. DeliveredRevision is the highest acknowledged revision.

func (PreferenceSnapshot) Pending

func (s PreferenceSnapshot) Pending() bool

Pending reports whether the snapshot still owes a delivery.

type PrivateDataEraser

type PrivateDataEraser interface {
	EraseSubjects(ctx context.Context, tenant string, actorIDs []string) error
}

PrivateDataEraser deletes personal data retained by optional moderator and classifier implementations. Success means a durable tenant/subject fence is installed: Classify/Screen calls begun before erasure cannot recreate data when they complete later. Merely queuing a delete is not success. Calls are idempotent; implementations must preserve fences across restore/restart. A host using multiple retaining providers must implement a composite eraser.

type PublicUser

type PublicUser struct {
	ID       string `json:"id"`
	Username string `json:"username"`
	Avatar   string `json:"avatar,omitempty"`
}

PublicUser is display enrichment for an author/actor id.

type RejectedError

type RejectedError struct{ Reason string }

RejectedError is a policy rejection of a text write, answered as 422 with its reason: a ContentModerator's reject verdict.

func (RejectedError) Error

func (e RejectedError) Error() string

type Resolution

type Resolution struct {
	// Ref is the canonical reference every row is stored and read under (an
	// alias or slug resolves to it). A zero Ref keeps the requested one; a Ref
	// of another tenant is an error.
	Ref contentref.ContentRef
	// Visible = published and not soft-deleted.
	Visible bool
	// Accessible = the actor may consume it: an opaque host verdict
	// (entitlement, purchase, ACL, flag). ContentKit imposes no access model.
	Accessible bool
}

Resolution is the host's verdict about a content reference.

type ReviewDecision

type ReviewDecision struct {
	Revision int64 // the revision returned by ListHeld; stale decisions cannot publish edits
	Decision Decision
	Reviewer string
	Reason   string
}

ReviewDecision resolves a held item: approve publishes it, reject keeps it author-only with Reason (or the moderator's reason when empty).

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 social lineage was applied to (Migrate) and wires the module services.

func (*Runtime) AcknowledgePreferences

func (rt *Runtime) AcknowledgePreferences(ctx context.Context, acks []PreferenceAck) error

AcknowledgePreferences records delivery of exactly the revisions sent (batch shaped). A key mutated since stays pending (GREATEST guarded by sent <= revision), an older ack never lowers a higher one, and an ack naming a revision no snapshot of this tenant holds is an error: it can only come from a sink bug. Acknowledgement never touches value/revision/occurred_at.

func (*Runtime) CommentReactionsByAuthor

func (rt *Runtime) CommentReactionsByAuthor(ctx context.Context, userIDs []string) (map[string]AuthorReactions, error)

CommentReactionsByAuthor totals the reactions each author's published comments have received: the profile stat behind "likes my comments got". Like LatestCommentsTotal it takes no actor — held and rejected comments are author-only and never counted, so every reader sees the same totals, and filtering by per-reference visibility would cost one resolver call per distinct reference the author ever commented on. An author with no published comments is absent from the map.

func (*Runtime) Counts

func (rt *Runtime) Counts(ctx context.Context, refs []contentref.ContentRef) (map[contentref.ContentKey]Counts, error)

Counts batch-reads the aggregate counts of refs (O(1) rollup rows). A reference with no engagement yet is absent from the map. Preference counts use the canonical work; comment counts keep the original localized thread.

func (*Runtime) DeliverPreferences

func (rt *Runtime) DeliverPreferences(ctx context.Context, sink PreferenceSink, after PreferenceKey, pageSize, maxRows int) (PreferenceDelivery, error)

DeliverPreferences continues one bounded sweep from after: it pages the tenant's pending snapshots in key order, hands each page to the sink, acknowledges exactly the revisions the sink accepted, purges erased subjects and leaves the rest pending. pageSize bounds each page; maxRows bounds the sweep (<= 0: until the cursor runs out). The cursor is per-sweep, so a key that keeps failing never starves the rest. Resume from Next even after a sink error. Once Next is zero the sweep is exhausted; start the next sweep from zero. Keep this cursor only for the current sweep, never as a persistent high-water mark.

func (*Runtime) EraseSubjects

func (rt *Runtime) EraseSubjects(ctx context.Context, actorIDs []string) error

EraseSubjects fences future authenticated content writes, removes reactions, favorites, poll votes/answers and preference snapshots/archives atomically, and removes private held/rejected/draft/scheduled payloads and moderation provenance. A previously published item retains its last approved payload without publishing it again; its private replacement is erased. Never-published items become tombstones. Published content and reply structure stay under host policy.

The source transaction commits BEFORE calling the external eraser. A provider failure must keep the HOST's downstream erasure obligation pending for retry. AuthKit acknowledgement means durable LOCAL acceptance of that obligation; do not delay that acknowledgement until this method or remote cleanup succeeds. Nil PrivateDataEraser means configured policy ports retain no external personal data. Retaining providers must configure an eraser, including while offline.

func (*Runtime) Handler

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

Handler returns the mountable http.Handler. The host mounts it under a prefix (e.g. "/api/social/") after its own auth middleware populated the identity.

func (*Runtime) IsFavorited

func (rt *Runtime) IsFavorited(ctx context.Context, userID string, refs []contentref.ContentRef) (map[contentref.ContentKey]bool, error)

IsFavorited batch-checks bookmarks for a user (every requested key is present in the map, absent bookmarks => false).

func (*Runtime) KeywordDocuments

func (rt *Runtime) KeywordDocuments(ctx context.Context, tenant, kind, language string, refs []contentref.ContentRef) ([]search.KeywordDocument, error)

KeywordDocuments returns the current keyword document of every published post in refs for language (the worker's BuildKeywordDocuments shape for kind "post"). A draft, unpublished, deleted or other-language post yields no document, which deletes its stale index entry; so does a held or rejected one.

func (*Runtime) LatestComments

func (rt *Runtime) LatestComments(ctx context.Context, actor Actor, limit, offset int) ([]FeedItem, error)

LatestComments is the host-facing feed API: newest comments across all content the actor may see, with canonical references for host hydration.

func (*Runtime) LatestCommentsTotal

func (rt *Runtime) LatestCommentsTotal(ctx context.Context) (int, error)

LatestCommentsTotal is the page total for LatestComments: how many live, approved comments the feed draws from in this tenant. Per-reference visibility is applied per page (a page may under-fill), so this is the upper bound a paged envelope reports, not a per-actor exact count — filtering it exactly would cost one resolver call per distinct reference in the whole table.

func (*Runtime) ListContent

func (rt *Runtime) ListContent(ctx context.Context, tenant, kind, language, cursor string, limit int) ([]contentref.ContentRef, string, bool, error)

ListContent enumerates the tenant's published posts of language in id order (the worker's ListContentPage shape for kind "post"); cursor is the last id.

func (*Runtime) ListFavorites

func (rt *Runtime) ListFavorites(ctx context.Context, userID string, limit, offset int) ([]FavoriteItem, error)

ListFavorites returns userID's bookmarks newest-first: the host-facing Go API a hydrated list route reads its references from. limit <= 0 means all.

func (*Runtime) ListHeld

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

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

func (*Runtime) MigratePreferences

func (rt *Runtime) MigratePreferences(ctx context.Context, opts PreferenceMigrationOptions) (PreferenceMigration, error)

MigratePreferences is the one-time cutover to canonical preference identity: it archives every preference-bearing reaction/favorite row of the tenant, collapses each (actor, canonical reference, axis) group (a dislike wins, else a like, else neutral; a favorite survives if any row had it), recomputes the affected rollup counts from the surviving rows, and seeds a snapshot per surviving row plus a zero snapshot for every previously exported key that no longer has one. Comment reactions and declined targets are untouched.

It runs in one transaction with the writers paused, is idempotent, never overwrites a newer live preference, and reports every conflicting group.

func (*Runtime) MyReactions

func (rt *Runtime) MyReactions(ctx context.Context, actor Actor, refs []contentref.ContentRef) (map[contentref.ContentKey]int16, error)

MyReactions batch-reads the actor's own reaction (-1/0/1) for many refs: the hydration read for list/detail responses, keyed by the caller's references and read under their canonical preference references (the identity the write path stores under). Only nonzero reactions appear.

func (*Runtime) PendingPreferences

func (rt *Runtime) PendingPreferences(ctx context.Context, after PreferenceKey, limit int) ([]PreferenceSnapshot, error)

PendingPreferences pages snapshots of this tenant that still owe a delivery (delivered_revision < revision) in key order; pass the last row's key as `after` to continue the sweep (zero value = from the start). The cursor is per-sweep only: the next sweep starts from the pending rows again, so one key that keeps failing delays nothing else. A retry re-reads the same revision and occurred_at.

func (*Runtime) PurgePreferenceSubjects

func (rt *Runtime) PurgePreferenceSubjects(ctx context.Context, actorIDs []string) (int64, error)

PurgePreferenceSubjects removes the tenant's snapshots and cutover archive of erased actors: the terminal deletion fence, not a delivery outcome. Returns snapshot rows removed.

func (*Runtime) ReactionsByActor

func (rt *Runtime) ReactionsByActor(ctx context.Context, actor Actor, kind string, limit, offset int) ([]ActorReaction, error)

ReactionsByActor lists the actor's nonzero reactions of one kind, newest-first (e.g. a "my tag preferences" page). limit <= 0 means all.

func (*Runtime) ReclassifyPending

func (rt *Runtime) ReclassifyPending(ctx context.Context, after string, limit int) (ClassificationPage, error)

ReclassifyPending processes one page in immutable answer-id order. The cursor is per-sweep only, never a persistent high-water mark. Errors do not strand later keys. Unchanged source revisions retain their classification identity.

func (*Runtime) ReconcileExportedPreferences

func (rt *Runtime) ReconcileExportedPreferences(ctx context.Context, keys []PreferenceKey) (int64, error)

ReconcileExportedPreferences reconciles one bounded page of previously exported keys after MigratePreferences has seeded source truth. Missing keys receive zero snapshots; current snapshots keep their exact value/revision/time. Canonicalization uses the same host rule as live preferences. Fenced subjects are skipped. The inserted count excludes existing snapshots and duplicates.

This is an explicit cutover operation: pause all source/delivery writers and retire old callbacks, seed the revision floor above retained sink/source state, then seed source truth before calling it. Persist the exported-key inventory before retiring old sink identities. Retrying a page is idempotent.

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) ReplayPreferences

func (rt *Runtime) ReplayPreferences(ctx context.Context, sink PreferenceSink, after PreferenceKey, pageSize, maxRows int) (PreferenceDelivery, error)

ReplayPreferences is the bounded full-snapshot replay that repairs sink loss: every snapshot from after, acknowledged rows and zeros included, is delivered with its persisted revision, time and value, and acknowledged on acceptance. Concurrent newer snapshots still win at the sink by revision. Resume an interrupted replay from the returned Next.

func (*Runtime) Resolve

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

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

func (*Runtime) ScanPreferences

func (rt *Runtime) ScanPreferences(ctx context.Context, after PreferenceKey, limit int) ([]PreferenceSnapshot, error)

ScanPreferences pages the tenant's whole snapshot table in key order, zero-valued removals and already-delivered rows included: the replay and reseed authority, reading exactly what live delivery reads.

func (*Runtime) SeedPreferenceRevisionFloor

func (rt *Runtime) SeedPreferenceRevisionFloor(ctx context.Context, floor int64) (int64, error)

SeedPreferenceRevisionFloor raises the schema's revision sequence so every future revision exceeds floor, and returns the resulting floor. It only ever advances (a repeat run, or a floor below the current one, is a no-op) and fails closed outside the safe range. Required before the first export into a sink that holds timestamp-derived revisions: starting at 1 would make every new revision lose to the old domain forever.

func (*Runtime) Tenant

func (rt *Runtime) Tenant() string

Tenant returns the tenant this runtime is scoped to.

type StatelessPolicy

type StatelessPolicy interface{ StatelessPolicy() }

StatelessPolicy explicitly declares that a policy implementation retains no personal data outside this process. Built-in deterministic policies satisfy this marker; retaining providers must instead configure PrivateDataEraser.

type StorageConfig

type StorageConfig struct {
	Bucket          string
	Region          string
	Endpoint        string // custom S3 endpoint (MinIO/R2); empty = AWS default
	AccessKeyID     string
	SecretAccessKey string
	PublicBaseURL   string // public origin serving the bucket, e.g. https://cdn.example.com
	UsePathStyle    bool   // true for MinIO / most self-hosted S3
}

StorageConfig configures ContentKit's built-in, S3-backed media store. When set (Options.Storage), ContentKit owns file upload itself: it writes poll/post images to a PUBLIC bucket and returns the public URL — no presigning. Works with AWS S3, MinIO, or R2 (set Endpoint + UsePathStyle for the latter two).

type UserEnricher

type UserEnricher interface {
	UsersByIDs(ctx context.Context, ids []string) (map[string]PublicUser, error)
}

UserEnricher batch-loads display data for author/actor ids.

type Verdict

type Verdict struct {
	Decision      Decision
	Reason        string
	Model         string
	PromptVersion string
	Confidence    float64
}

Verdict is a moderator's decision with its provenance. Reason is shown to the author on reject and review; Model, PromptVersion and Confidence are kept with a held item for the reviewer.

Jump to

Keyboard shortcuts

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