catalog

package
v0.85.0 Latest Latest
Warning

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

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

Documentation

Overview

Package catalog is ContentKit's own content: content items (a gallery, a movie, a creator or blog post), their per-language text, creators, versions with their text and segments, the state machine (draft, published, scheduled, deleted), the listing projection (content_facets), the one visibility rule every read applies, the events outbox and the catalog totals. Kinds are configuration (package kinds); nothing here names a host's nouns.

Index

Constants

View Source
const (
	EventContentCreated     = "content.created"
	EventContentUpdated     = "content.updated"
	EventContentScheduled   = "content.scheduled"
	EventContentPublished   = "content.published"
	EventContentUnpublished = "content.unpublished"
	EventContentDeleted     = "content.deleted"
	EventContentRestored    = "content.restored"
	EventContentPurged      = "content.purged"
	EventVersionCreated     = "version.created"
	EventVersionUpdated     = "version.updated"
	EventVersionPublished   = "version.published"
	EventVersionDeleted     = "version.deleted"
)

Event types written to the outbox.

View Source
const (
	StateDraft     = "draft"
	StatePublished = "published"
	StateDeleted   = "deleted"

	ListingListed   = "listed"
	ListingMembers  = "members"
	ListingUnlisted = "unlisted"
)

Content states and listings.

View Source
const DefaultDeletedRetention = 30 * 24 * time.Hour

DefaultDeletedRetention is how long deleted, never-sold content keeps its media and can be restored before PurgeDeleted erases it.

View Source
const HeldKind = "content"

HeldKind is the review queue's kind for held content (content.Runtime's /moderation/held?kind=content).

View Source
const ImageScheme = "contentkit:"

ImageScheme is the URL scheme of an inline image reference in a body.

View Source
const Queue = "contentkit_catalog"

Queue is the River queue of the catalog's jobs.

Variables

View Source
var (
	ErrInvalid   = errors.New("catalog: invalid request")
	ErrNotFound  = errors.New("catalog: not found")
	ErrForbidden = errors.New("catalog: forbidden")
	ErrConflict  = errors.New("catalog: conflict")
)

Errors; match with errors.Is.

Functions

func ImageRef

func ImageRef(name string) string

ImageRef is the body reference to an inline image name ("i-{uuid}").

func RefreshCountsJob

func RefreshCountsJob() river.JobArgs

RefreshCountsJob is RefreshCounts' job, for Queue: an import queues it.

Types

type AuthError

type AuthError struct{ Err error }

AuthError is a permission check the auth could not answer: 401 when the credential ended, else 503. It never grants.

func (*AuthError) Error

func (e *AuthError) Error() string

func (*AuthError) Unwrap

func (e *AuthError) Unwrap() error

type Content

type Content struct {
	ID   string `json:"id"`
	Kind string `json:"kind"`
	// Code and Path are its page: [/{lang}]/{route}/{code}/{slug}.
	Code string `json:"code"`
	Path string `json:"path"`
	// Language is the language of the text below.
	Language    string `json:"language"`
	Title       string `json:"title"`
	Slug        string `json:"slug,omitempty"`
	Description string `json:"description,omitempty"`
	// Body shows each inline image with a current public file as its URL;
	// only editors and viewers Decision allows get it. Description is the
	// teaser everyone reads.
	Body string `json:"body,omitempty"`
	// Decision is the viewer's access to the item: allowed when they open any
	// of its versions, else its first version's paywall.
	Decision Decision `json:"decision"`
	// Images maps each resolved image name of Body and the cover to its URL.
	Images  map[string]string `json:"images,omitempty"`
	Cover   *Cover            `json:"cover,omitempty"`
	State   string            `json:"state"`
	Listing string            `json:"listing"`
	// Comments is open, closed or hidden.
	Comments     string         `json:"comments"`
	PublishAt    *time.Time     `json:"publish_at,omitempty"`
	PublishedAt  *time.Time     `json:"published_at,omitempty"`
	Live         bool           `json:"live"`
	Access       access.Rule    `json:"access"`
	Restrictions []string       `json:"restrictions"`
	Attributes   map[string]any `json:"attributes"`
	OwnerGroupID string         `json:"owner_group_id,omitempty"`
	// Owner is the owner group's card (a channel's name, path, avatar and
	// cover) while it is active.
	Owner    *groups.GroupCard `json:"owner,omitempty"`
	Creators []Creator         `json:"creators"`
	Groups   []GroupSummary    `json:"groups"`
	Versions []Version         `json:"versions"`
	// Views counts its distinct viewers of all time plus its imported legacy
	// view count, as cards' counts.views do.
	Views int64 `json:"views"`
	// Viewer is the signed-in reader's standing with the item, to resume
	// it; each version carries its own.
	Viewer *ViewerState `json:"viewer,omitempty"`
	// Editor: the viewer may edit it; the fields below are theirs.
	Editor           bool            `json:"editor"`
	Text             map[string]Text `json:"text,omitempty"`
	Moderation       string          `json:"moderation,omitempty"`
	ModerationReason string          `json:"moderation_reason,omitempty"`
	EverSold         bool            `json:"ever_sold,omitempty"`
	// Offer is the item's current offer, for its offer editors.
	Offer     *offers.Offer `json:"offer,omitempty"`
	Revision  int64         `json:"revision"`
	CreatedAt time.Time     `json:"created_at"`
	UpdatedAt time.Time     `json:"updated_at"`
	DeletedAt *time.Time    `json:"deleted_at,omitempty"`
}

Content is a content item as a viewer sees it: its text in the viewer's language (falling back), its live versions (every version for editors), groups and attributes.

type ContentInput

type ContentInput struct {
	Kind string `json:"kind"`
	// Text is the text per language.
	Text    map[string]Text `json:"text,omitempty"`
	Listing string          `json:"listing,omitempty"` // default listed
	// Comments is open (the default), closed (existing ones show, new ones
	// are refused) or hidden.
	Comments string `json:"comments,omitempty"`
	// Access defaults to public.
	Access       *access.Rule   `json:"access,omitempty"`
	Restrictions []string       `json:"restrictions,omitempty"`
	Attributes   map[string]any `json:"attributes,omitempty"`
	// OwnerGroupID is the home group (a channel); its editors edit the item.
	// It never changes.
	OwnerGroupID string `json:"owner_group_id,omitempty"`
	// Creators nil credits the creating person as its author.
	Creators *[]Creator `json:"creators,omitempty"`
	// Versions: at least one, except for a kind with hidden versions, which
	// gets its one version automatically.
	Versions []VersionInput `json:"versions,omitempty"`
}

ContentInput creates a content item, a draft until published.

type ContentPatch

type ContentPatch struct {
	Listing      *string        `json:"listing,omitempty"`
	Comments     *string        `json:"comments,omitempty"`
	Access       *access.Rule   `json:"access,omitempty"`
	Restrictions *[]string      `json:"restrictions,omitempty"`
	Attributes   map[string]any `json:"attributes,omitempty"` // replaces all
	Creators     *[]Creator     `json:"creators,omitempty"`
	// Cover is an inline image name of the content's folder; "" clears.
	Cover *string `json:"cover,omitempty"`
	// Revision, when set, refuses the edit (409) unless it is current.
	Revision *int64 `json:"revision,omitempty"`
}

ContentPatch edits a content item; nil fields stay.

type Cover

type Cover struct {
	Image string  `json:"image"`
	URL   *string `json:"url,omitempty"`
}

Cover is a content item's cover: an inline image of its media folder.

type Creator

type Creator struct {
	UserID   string `json:"user_id"`
	Role     string `json:"role,omitempty"`
	Position int    `json:"position"`
}

Creator is a person credited on a content item; creators edit it.

type CreatorsInput

type CreatorsInput struct {
	Creators []Creator `json:"creators"`
}

CreatorsInput replaces the creators.

type Decision

type Decision struct {
	Allowed bool `json:"allowed"`
	// Unknown: entitlements could not be read; denied, nothing offered.
	Unknown bool `json:"unknown,omitempty"`
	// Sell is the keys a paywall offers; Requires the key a buyer holds first.
	Sell     []string `json:"sell,omitempty"`
	Requires string   `json:"requires,omitempty"`
	// Preview is what a locked viewer still reads; FreeAfter when it opens to all.
	Preview   *access.Preview `json:"preview,omitempty"`
	FreeAfter *time.Time      `json:"free_after,omitempty"`
	// Offers are the purchasable offers of Sell's keys; tiers are the host's
	// to label.
	Offers []offers.Sale `json:"offers,omitempty"`
}

Decision is a viewer's access verdict on a version.

type Event

type Event struct {
	ID        int64     `json:"id"`
	Type      string    `json:"type"`
	Kind      string    `json:"kind"`
	ContentID string    `json:"content_id"`
	VersionID string    `json:"version_id,omitempty"`
	CreatedAt time.Time `json:"created_at"`
}

Event is one outbox entry: a change to a content item or one of its versions, delivered at least once in order of ID.

type GroupSummary

type GroupSummary struct {
	ID   string `json:"id"`
	Kind string `json:"kind"`
	Slug string `json:"slug"`
	Name string `json:"name"`
	Code string `json:"code,omitempty"`
	Path string `json:"path,omitempty"`
}

GroupSummary names one group a content item belongs to; Code and Path are its page, as content's are, when its kind has one.

type Groups

type Groups interface {
	EffectiveGroups(ctx context.Context, q groups.Querier, versionIDs []string) (map[string][]string, error)
	Cards(ctx context.Context, ids []groups.GroupID, o groups.CardOptions) ([]groups.GroupCard, error)
}

Groups answers each version's effective group ids (its content's memberships, its own and what they imply), read in q's transaction, and owner groups' cards: *groups.Store.

type Images

type Images interface {
	Images(ctx context.Context, qs ...media.ImageQuery) ([][]media.PublicImage, error)
}

Images answers image queries from current publications in one read.

type ImportContent

type ImportContent struct {
	ID          string
	Kind        string
	State       string
	PublishAt   *time.Time
	PublishedAt *time.Time
	DeletedAt   *time.Time
	Listing     string
	// Comments is open (when empty), closed or hidden.
	Comments string
	// ViewBaseline is the legacy all-time view count, added to the viewers
	// the signal plane counts.
	ViewBaseline int64
	Access       *access.Rule
	Restrictions []string
	Attributes   map[string]any
	OwnerGroupID string
	Cover        string
	Creators     []Creator
	Text         map[string]Text
	Versions     []ImportVersion
	Code         string
	CreatedAt    *time.Time
	UpdatedAt    *time.Time
}

ImportContent is a content item as the bulk import writes it: its id, state and times kept, trusted (not screened). Nil Creators, Text and a version's Text and Segments keep what is stored; given ones replace it.

type ImportVersion

type ImportVersion struct {
	ID          string
	Languages   []string
	Traits      []string
	Label       string
	State       string
	PublishAt   *time.Time
	PublishedAt *time.Time
	DeletedAt   *time.Time
	Access      *access.Rule
	Attributes  map[string]any
	Ordinal     *int
	Text        map[string]VersionText
	Segments    []Segment
	CreatedAt   *time.Time
	UpdatedAt   *time.Time
}

ImportVersion is one version of an ImportContent, positioned as listed.

type InlineImage

type InlineImage struct {
	Ref string  `json:"ref"`
	URL *string `json:"url"`
}

InlineImage is an inline image of a body: Ref goes where its URL would ("contentkit:i-{uuid}"); URL shows it to the editor now (null until it has a file to show).

type LanguageTotal

type LanguageTotal struct {
	Public int64            `json:"public"`
	Tiers  map[string]int64 `json:"tiers"`
}

LanguageTotal sizes one kind in one language, as Total does.

type ManageQuery

type ManageQuery struct {
	Kind string
	// State is draft, scheduled, published (live) or deleted; "" is every
	// state but deleted.
	State      string
	OwnerGroup string
	// Text matches title, description or body, ignoring case.
	Text          string
	Limit, Offset int
}

ManageQuery selects content items to manage.

type Media

type Media struct {
	Images Images // *media.Manifests
	// Reader gives editors an unpublished item's image previews.
	Reader   Reader
	Versions VersionMedia // *versions.Service
}

Media connects content items to ContentKit media, built over the catalog's Resolver and RefreshTx (media/versions): bind it with BindMedia before serving. A content item's media item is (kind, content), holding its cover; its body images are its body item's ({kind}.body, content), shown only to viewers who open it; each version's is (kind, content, version).

type Moderation

type Moderation interface {
	Screen(ctx context.Context, in content.ModerationInput) (content.Screening, error)
}

Moderation screens text before it publishes: content.Runtime.

type Offers

type Offers interface {
	Resync(ctx context.Context, tx pgx.Tx, targets []offers.Target) error
	Sales(ctx context.Context, keys []string) (map[string]offers.Sale, error)
	Current(ctx context.Context, a auth.Authenticator, actor access.Actor, targets []offers.Target) ([]*offers.Offer, error)
}

Offers is *offers.Service: Resync re-syncs the targets' products in the writing transaction; Sales and Current fill reads' paywalls and editors' offers.

type Options

type Options struct {
	Pool   *pgxpool.Pool
	Schema string
	Tenant string
	// Kinds is the site's content model. Required.
	Kinds kinds.Config
	// Auth gates the routes and answers staff and owner-group permissions.
	Auth auth.Authenticator
	// Gate answers viewers' entitlements; nil builds one without billing
	// (only free rules and claimed tiers open).
	Gate *access.Gate
	// Offers renames the products of content whose title changes and answers
	// paywalls' offers; nil when nothing is sold.
	Offers Offers
	// Groups answers effective group ids for content_facets; nil: none. Wire
	// its Options.ContentChanged to Store.ContentChanged.
	Groups Groups
	// Moderation screens text of content that is or goes live; nil approves
	// unless contentkit.NewRuntime binds its content runtime (BindModeration).
	Moderation Moderation
	// Body sanitizes text bodies; nil strips tags.
	Body Sanitizer
	// DeletedRetention: see DefaultDeletedRetention.
	DeletedRetention time.Duration
	// DocumentsChanged runs in the writing transaction after a write queued
	// search documents, so the engine can sync them at once. Optional.
	DocumentsChanged func(ctx context.Context, q Querier) error
	Now              func() time.Time
	Logger           *slog.Logger
}

Options configures a Store.

type OrderInput

type OrderInput struct {
	IDs []string `json:"ids"`
}

OrderInput orders versions.

type Publish

type Publish struct {
	ContentID string
	At        *time.Time
}

Publish publishes a content item now, or at At when it is in the future (scheduled).

type PublishInput

type PublishInput struct {
	PublishAt *time.Time `json:"publish_at,omitempty"`
}

PublishInput publishes now, or at PublishAt when it is in the future.

type Querier

type Querier interface {
	Query(ctx context.Context, sql string, args ...any) (pgx.Rows, error)
	QueryRow(ctx context.Context, sql string, args ...any) pgx.Row
	Exec(ctx context.Context, sql string, args ...any) (pgconn.CommandTag, error)
}

Querier is a pool or a transaction.

type Reader

type Reader interface {
	Read(ctx context.Context, ref contentref.ContentRef, actor access.Actor, o media.ReadOptions) (*media.ReadResult, error)
	// NamedURLs signs an opened item's named images that have no public files.
	NamedURLs(ctx context.Context, ref contentref.ContentRef, actor access.Actor, names []string) (map[string]string, error)
}

Reader reads an item as the media read API does.

type Sanitizer

type Sanitizer = content.ContentProcessor

Sanitizer cleans rich text on write: content.ContentProcessor.

type Segment

type Segment struct {
	Position  int        `json:"position"`
	Title     string     `json:"title,omitempty"`
	Start     int        `json:"start"`
	End       int        `json:"end"`
	ReleaseAt *time.Time `json:"release_at,omitempty"`
	// Released is computed on reads.
	Released bool `json:"released"`
}

Segment is an ordered, individually released range of a version (a chapter): items [Start, End). A segment is released once it and every earlier one are past ReleaseAt.

type SegmentsInput

type SegmentsInput struct {
	Segments []Segment `json:"segments"`
}

SegmentsInput replaces a version's segments.

type Standing added in v0.85.0

type Standing interface {
	Standing(ctx context.Context, actor access.Actor, refs []contentref.ContentRef) ([]ViewerState, error)
}

Standing reads a signed-in person's standing with content items and versions, per ref in order: *discovery.Service.

type Store

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

Store is one tenant's catalog.

func New

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

New validates opts and checks the schema has the catalog tables.

func (*Store) BindMedia

func (s *Store) BindMedia(m Media) error

BindMedia attaches media; call it once, before serving.

func (*Store) BindModeration

func (s *Store) BindModeration(m Moderation)

BindModeration screens through m when Options.Moderation is nil, so one moderator serves comments and catalog text; call it before serving.

func (*Store) BindStanding added in v0.85.0

func (s *Store) BindStanding(st Standing)

BindStanding fills detail reads' viewer state from st; call it once, before serving.

func (*Store) BuildDocuments

func (s *Store) BuildDocuments(ctx context.Context, kind, language string, refs []contentref.ContentRef) ([]search.KeywordDocument, error)

BuildDocuments returns the current document of each published item of refs with text in language; the others get none, which deletes theirs.

func (*Store) Close

func (s *Store) Close()

Close releases the totals cache.

func (*Store) ContentChanged

func (s *Store) ContentChanged(ctx context.Context, q groups.Querier, contentIDs []string) error

ContentChanged is groups.Options.ContentChanged: after the groups module re-derived the items' group_ids in q's transaction, it queues their search documents (group names are keywords) and records the change.

func (*Store) Contents

func (s *Store) Contents(ctx context.Context, v *Viewer, ids []string) ([]*Content, error)

Contents reads content items as v sees them (spec §4): an item the viewer may not see, or that does not exist, is nil. Text is in the viewer's language, else the site's first language, else the first it has; an item whose edit awaits review shows its last approved text to everyone but its editors. Editors also get every language's text, every version and the moderation state.

func (*Store) Create

func (s *Store) Create(ctx context.Context, actor access.Actor, in []ContentInput) ([]string, error)

Create creates content items as drafts and returns their ids in order. actor needs root:content:write, or to edit each item's owner group.

func (*Store) Delete

func (s *Store) Delete(ctx context.Context, actor access.Actor, ids []string) error

Delete deletes content items. A deleted item stays readable to holders of its own key; one never sold is erased, media included, after the retention (PurgeDeleted), until when Restore brings it back.

func (*Store) DeleteOwned

func (s *Store) DeleteOwned(ctx context.Context, groups []string) error

DeleteOwned deletes the live content of the deleted user-owned groups among groups (every one with nil), recording each group as its content's cause (groups.OwnedContent): holders of a deleted item's key still read it, and one never sold is erased after the retention.

func (*Store) DeleteVersions

func (s *Store) DeleteVersions(ctx context.Context, actor access.Actor, keys []VersionKey) error

DeleteVersions deletes versions; an item keeps at least one.

func (*Store) DeliverEvents

func (s *Store) DeliverEvents(ctx context.Context) (int, error)

DeliverEvents hands undelivered events to the subscribers in batches, in order, and marks each batch delivered once every subscriber took it; it stops at the first failure, which it returns. Delivered events older than a week are pruned. It returns how many events were delivered.

func (*Store) DocumentKinds

func (s *Store) DocumentKinds() []string

DocumentKinds are the content kinds the catalog documents.

func (*Store) ExpireDrafts

func (s *Store) ExpireDrafts(ctx context.Context) (int, error)

ExpireDrafts erases the drafts of kinds with a DraftTTL that never went live and were created longer than it ago: their media folders and their rows. It returns how many it erased.

func (*Store) Handler

func (s *Store) Handler() http.Handler

Handler serves the catalog routes behind their gates over Options.Auth, each request in an access.WithMemo context (one billing read). Its paths are under the module's prefix (httpapi.Contents, /v1): mount it with that prefix stripped.

func (*Store) Held

func (s *Store) Held(ctx context.Context, after time.Time, afterID string, limit int) ([]content.HeldItem, error)

Held pages held content items oldest first, for content's review queue (content.HeldSource under HeldKind): the text shown is the site's first language's, else any.

func (*Store) Import

func (s *Store) Import(ctx context.Context, tx pgx.Tx, in []ImportContent) error

Import writes content items in tx: new ones are inserted, stored ones updated where the import changes them. Then what depends on them follows, as after every write: codes, facets, search documents, events, media exposure and publications, and offers' product names. A version of another content item or a kind change is ErrInvalid.

func (*Store) ImportDeletes added in v0.85.0

func (s *Store) ImportDeletes(ctx context.Context, tx pgx.Tx, contents, versions []string) error

ImportDeletes deletes content items and versions in tx as Delete and DeleteVersions do: holders of a deleted item's key still read it, and one never sold is erased after the retention. Deleted ones stay as they are; a kind's one hidden version goes only with its item.

func (*Store) Kinds

func (s *Store) Kinds() kinds.Config

Kinds is the store's content model.

func (*Store) ListDocuments

func (s *Store) ListDocuments(ctx context.Context, kind, language, cursor string, limit int) ([]contentref.ContentRef, string, bool, error)

ListDocuments pages the published items of kind with text in language, in id order, for backfill.

func (*Store) Manage

func (s *Store) Manage(ctx context.Context, actor access.Actor, q ManageQuery) ([]string, error)

Manage lists the ids of items actor manages, newest first: every item for root:content:write holders; else what they created, or with OwnerGroup the group's items when they edit it.

func (*Store) OrderVersions

func (s *Store) OrderVersions(ctx context.Context, actor access.Actor, contentID string, ids []string) error

OrderVersions sets the order of a content item's versions: ids lists every version not deleted, first first; deleted ones follow.

func (*Store) Publish

func (s *Store) Publish(ctx context.Context, actor access.Actor, in []Publish) error

Publish publishes content items now, or schedules those with a future At. A published item is screened (all its text); a hold leaves it unpublished until a reviewer approves it. Rescheduling a scheduled item moves its time; a live item is left as it is.

func (*Store) PublishDue

func (s *Store) PublishDue(ctx context.Context) (int, error)

PublishDue promotes scheduled content items and versions whose time came: it stamps published_at and fires the side effects of going live (facets, search documents, media exposure, events). Visibility itself never waits for it: a scheduled item is live from its time. It returns how many items and versions it promoted.

func (*Store) PurgeDeleted

func (s *Store) PurgeDeleted(ctx context.Context) (int, error)

PurgeDeleted erases content items deleted longer than the retention that were never sold: their media folders and their rows. Sold ones stay for their holders. It returns how many it erased.

func (*Store) PutText

func (s *Store) PutText(ctx context.Context, actor access.Actor, in []TextPut) error

PutText sets or removes text per language. Text of a published item is screened first: a rejection writes nothing (content.RejectedError), a hold keeps the item's last approved text public until a reviewer decides.

func (*Store) PutVersions

func (s *Store) PutVersions(ctx context.Context, actor access.Actor, in []VersionPut) ([]string, error)

PutVersions creates (VersionID "") or edits versions and returns their ids in order.

func (*Store) Ref

func (s *Store) Ref(kind, id string) contentref.ContentRef

Ref is a content item's reference.

func (*Store) RefreshCounts

func (s *Store) RefreshCounts(ctx context.Context) (int, error)

RefreshCounts copies each content item's interaction counts into its facets: likes, dislikes, favorites and rating (the Wilson lower bound of likes over likes and dislikes). It returns how many facet rows changed.

func (*Store) RefreshFacets

func (s *Store) RefreshFacets(ctx context.Context, q Querier, contentIDs []string) error

RefreshFacets rewrites the content_facets rows of every version of the content items, and queues their search documents, in q's transaction. live_from is when a version went (or goes) live, kept through deletion so holders of a deleted item still read what was live; deleted marks either deletion. Every write to a content item, its versions, memberships, access, listing, restrictions, traits or attributes calls it before committing; the groups module calls it after membership changes.

func (*Store) RefreshTx

func (s *Store) RefreshTx(ctx context.Context, tx pgx.Tx, versionIDs []string) error

RefreshTx is versions.Facets: it refreshes the facets of the versions' content items after their media facts changed.

func (*Store) ReleaseFree

func (s *Store) ReleaseFree(ctx context.Context) (int, error)

ReleaseFree runs what follows a free_after time passing: the content items whose versions turned free since the last run get their facets and documents re-derived and their media, body images included, exposed to everyone, as a write would. It returns how many items it released.

func (*Store) ResolveHeld

func (s *Store) ResolveHeld(ctx context.Context, id string, d content.ReviewDecision) error

ResolveHeld applies a reviewer's decision to a held content item at the revision the queue listed: approval publishes it (or its edit), rejection keeps it from going live (an edit of a live item leaves its last approved text public).

func (*Store) Resolver

func (s *Store) Resolver() resolve.Resolver

Resolver is the resolve.Resolver of content items for media (media/versions) and interactions: a content reference (kind, content) or one of its versions is visible by the rule for direct reads (live, deleted and held by its own keys, or edited by the viewer), accessible when the viewer opens it (a content reference: any of its visible versions), with staff, creators and owner-group editors as Editor and the first creator as Owner. A reference it omits is unseen.

func (*Store) Restore

func (s *Store) Restore(ctx context.Context, actor access.Actor, ids []string) error

Restore brings deleted content items back as they were: published (or scheduled) when they had been, else drafts.

func (*Store) RestoreOwned

func (s *Store) RestoreOwned(ctx context.Context, groups []string) error

RestoreOwned restores the content the active groups among groups (every one with nil) deleted when they were deleted (groups.OwnedContent), as Restore does; content deleted for another reason stays deleted, and content purged since is gone.

func (*Store) RiverJobs

func (s *Store) RiverJobs() riverhelpers.Contribution

RiverJobs contributes the catalog's periodic jobs to the engine's River client: scheduled publication and free_after releases (every 30s), event delivery (5s), interaction counts (5m), purging deleted content (1h) and expiring drafts (10m).

func (*Store) Subscribe

func (s *Store) Subscribe(sub Subscriber)

Subscribe registers an in-process subscriber; DeliverEvents calls every subscriber with every batch.

func (*Store) Tenant

func (s *Store) Tenant() string

Tenant is the store's tenant.

func (*Store) Totals

func (s *Store) Totals(ctx context.Context, q TotalsQuery) (Totals, error)

Totals counts the listed content of each kind by who opens it, cached per site and kind: a count reloads in the background after ten minutes, and the last good one serves while reloads fail, for up to a day.

func (*Store) Unpublish

func (s *Store) Unpublish(ctx context.Context, actor access.Actor, ids []string) error

Unpublish returns published or scheduled content items to drafts.

func (*Store) Update

func (s *Store) Update(ctx context.Context, actor access.Actor, in []Update) error

Update edits content items.

func (*Store) Viewer

func (s *Store) Viewer(ctx context.Context, o ViewerOptions) (*Viewer, error)

Viewer resolves o once per request: the site, staff rights, entitlements (one billing read, fail-closed when unavailable), the site's required groups and the viewer's content filters (config defaults merged with their own).

func (*Store) Visibility

func (s *Store) Visibility(r *http.Request, links []contenturl.Link) ([]contenturl.Visibility, error)

Visibility is contenturl.RouterOptions.Visibility for content kinds: a page shows while its item is live for an anonymous visitor of the request's site, is Gone once a live item was deleted (or erased), and is Hidden otherwise. Other kinds are Visible.

type Subscriber

type Subscriber func(ctx context.Context, events []Event) error

Subscriber receives a batch of events in order. An error redelivers the batch later, so a subscriber must be idempotent.

type Text

type Text struct {
	Title       string   `json:"title"`
	Slug        string   `json:"slug,omitempty"`
	Description string   `json:"description,omitempty"`
	Body        string   `json:"body,omitempty"`
	SortKey     string   `json:"sort_key,omitempty"`
	Aliases     []string `json:"aliases,omitempty"`
}

Text is a content item's text in one language. Body is rich text whose inline images are "contentkit:i-{uuid}" references (ImageRef).

type TextPut

type TextPut struct {
	ContentID string
	Language  string
	Text      *Text
}

TextPut sets (Text non-nil) or removes one language's text.

type Total

type Total struct {
	Public    int64                    `json:"public"`
	Tiers     map[string]int64         `json:"tiers"`
	Languages map[string]LanguageTotal `json:"languages"`
}

Total sizes one kind. Public counts what an anonymous visitor's listing shows and opens; Tiers[t] every eligible item a holder of t opens, members-listed ones included. An item counts once in the kind's total and once per language any of its eligible versions serves, never per version. The site's scope and the default content filters apply.

type Totals

type Totals struct {
	Kinds map[string]Total `json:"kinds"`
}

Totals is the catalog's size by content kind.

type TotalsClient

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

TotalsClient reads another deployment's catalog totals (GET /v1/catalog/totals), such as a sibling site's, server to server, cached per kind like Store.Totals.

func NewTotalsClient

func NewTotalsClient(baseURL string, hc *http.Client) (*TotalsClient, error)

NewTotalsClient reads the deployment whose ContentKit is mounted at baseURL, e.g. "https://hentai0.com/api/v1/contentkit". A nil hc has a ten-second timeout.

func (*TotalsClient) Close

func (c *TotalsClient) Close()

Close releases the cache's goroutines.

func (*TotalsClient) Totals

func (c *TotalsClient) Totals(ctx context.Context, q TotalsQuery) (Totals, error)

Totals reads the remote's totals as Store.Totals answers them, in the site the remote derives from its host: q.Kinds is required and q.Site empty.

type TotalsQuery

type TotalsQuery struct {
	// Kinds are content kinds; empty means all of them.
	Kinds []string
	// Languages are the languages Total.Languages reports; empty means every
	// language with a counted item. Kind totals ignore it.
	Languages []string
	// Site is the site whose scope applies (its name); empty is the first
	// site. The HTTP route takes it from the request's host.
	Site string
}

TotalsQuery selects the kinds and languages to total.

type Update

type Update struct {
	ID string
	ContentPatch
}

Update is one ContentPatch.

type Version

type Version struct {
	ID        string   `json:"id"`
	Position  int      `json:"position"`
	Languages []string `json:"languages"`
	Traits    []string `json:"traits"`
	Label     string   `json:"label,omitempty"`
	// Title and Description are the override in the detail's language.
	Title       string `json:"title,omitempty"`
	Description string `json:"description,omitempty"`
	// State is the editors' to see.
	State       string     `json:"state,omitempty"`
	PublishAt   *time.Time `json:"publish_at,omitempty"`
	PublishedAt *time.Time `json:"published_at,omitempty"`
	// Live: published, past PublishAt, its content live.
	Live bool `json:"live"`
	// Access is the version's own rule; nil inherits the content's.
	Access     *access.Rule   `json:"access,omitempty"`
	Attributes map[string]any `json:"attributes"`
	Pages      *int           `json:"pages,omitempty"`
	DurationMS *int64         `json:"duration_ms,omitempty"`
	Width      *int           `json:"width,omitempty"`
	Height     *int           `json:"height,omitempty"`
	HasAudio   bool           `json:"has_audio"`
	Ordinal    *int           `json:"ordinal,omitempty"`
	Segments   []Segment      `json:"segments"`
	Decision   Decision       `json:"decision"`
	// Text is every language's override, for editors.
	Text map[string]VersionText `json:"text,omitempty"`
	// Offer is the version's current offer, for its offer editors.
	Offer *offers.Offer `json:"offer,omitempty"`
	// Viewer is the signed-in reader's standing with the version.
	Viewer *ViewerState `json:"viewer,omitempty"`
}

Version is what a viewer opens.

type VersionFork

type VersionFork struct {
	From     string `json:"from"`
	Playback bool   `json:"playback,omitempty"`
	Segments bool   `json:"segments,omitempty"`
}

VersionFork copies a sibling version's media into a new version: its uploads and reader metadata, with Playback its audio, with Segments its segments.

type VersionInput

type VersionInput struct {
	Languages []string  `json:"languages,omitempty"`
	Traits    *[]string `json:"traits,omitempty"`
	Label     *string   `json:"label,omitempty"`
	// State is draft or published (the default for a new version): a
	// published version is live once its content is and PublishAt passed.
	State     string     `json:"state,omitempty"`
	PublishAt *time.Time `json:"publish_at,omitempty"`
	// Access overrides the content's rule; ClearAccess inherits it again.
	Access      *access.Rule           `json:"access,omitempty"`
	ClearAccess bool                   `json:"clear_access,omitempty"`
	Attributes  map[string]any         `json:"attributes,omitempty"` // replaces all
	Text        map[string]VersionText `json:"text,omitempty"`       // replaces all
	Segments    *[]Segment             `json:"segments,omitempty"`   // replaces all
	Ordinal     *int                   `json:"ordinal,omitempty"`
	// Fork fills a new version's media from a sibling (creation only).
	Fork *VersionFork `json:"fork,omitempty"`
}

VersionInput creates or edits a version; nil fields stay (or default).

type VersionKey

type VersionKey struct{ ContentID, VersionID string }

VersionKey names one version.

type VersionMedia

type VersionMedia interface {
	// ExposeTx follows a visibility change.
	ExposeTx(ctx context.Context, tx pgx.Tx, targets []versions.Target) error
	// RepublishTx follows a change of a version's effective rule or segments.
	RepublishTx(ctx context.Context, tx pgx.Tx, versions []versions.Target) error
	// DeleteTx deletes media before the rows go.
	DeleteTx(ctx context.Context, tx pgx.Tx, targets []versions.Target) error
	// ForkTx fills new versions' media from siblings.
	ForkTx(ctx context.Context, tx pgx.Tx, forks []versions.Fork) error
}

VersionMedia is the media calls catalog writes make in their transaction: a target is a content item (its item and every version's) or one version.

type VersionPut

type VersionPut struct {
	ContentID string
	VersionID string
	VersionInput
}

VersionPut creates (VersionID "") or edits a version.

type VersionText

type VersionText struct {
	Title       string `json:"title,omitempty"`
	Description string `json:"description,omitempty"`
}

VersionText overrides a version's title and description in one language.

type Viewer

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

Viewer is the one visibility rule (ContentKit #117 spec §4) for one request: live content and versions, the listing rule, the site scope, the viewer's content filters and entitlements, and the staff, creators and owner-group editors who see everything. Listed renders it as SQL over content_facets for lists and searches; Store.Contents and Resolver decide items with it.

func (*Viewer) Actor

func (v *Viewer) Actor() access.Actor

Actor is the viewer's actor.

func (*Viewer) Editors

func (v *Viewer) Editors(ctx context.Context, groups []string) map[string]bool

Editors reports, per owner group, whether the viewer edits its content and so opens it: staff edit every group's; anyone else is asked once per group per request, and a failed check reads as not an editor.

func (*Viewer) Indexable added in v0.85.0

func (v *Viewer) Indexable(alias string) (string, pgx.NamedArgs)

Indexable is a boolean SQL predicate over the content_facets row aliased alias: the versions a site lists to anyone, as its sitemaps name them: live, not deleted, listed and in the viewer's site scope. Access, the language and a reader's content filters do not apply. Args are pgx named args prefixed ci_.

func (*Viewer) Listed

func (v *Viewer) Listed(alias string) (string, pgx.NamedArgs)

Listed is a boolean SQL predicate over the content_facets row aliased alias: the versions the viewer's listing shows. A list shows one card per content item among its rows. In Hide mode, and for members-listed rows, rows a truncated keyspace may unlock are admitted as candidates: decide them with Contents (or access.Gate.Settle) before showing. Args are pgx named args prefixed cv_ and access_.

func (*Viewer) Site

func (v *Viewer) Site() kinds.Site

Site is the site the viewer is on.

func (*Viewer) Staff

func (v *Viewer) Staff() bool

Staff reports a viewer holding root:content:view-restricted or root:content:write: they see everything.

type ViewerOptions

type ViewerOptions struct {
	Actor access.Actor
	// Host is the request's host; it picks the site (kinds.Config.SiteFor).
	Host string
	// Language, when set, lists only content with a live version serving it.
	Language string
	// Mode defaults to access.Lock.
	Mode access.Mode
	// EditorGroups are owner groups (channels) whose editors see their whole
	// content in lists; the viewer's editorship of each is checked.
	EditorGroups []string
}

ViewerOptions describes one request's reader.

type ViewerState added in v0.85.0

type ViewerState struct {
	Favorited bool `json:"favorited"`
	// Seen, Completed, Progress, ProgressMax and Resume (the client's own
	// pointer, such as a page) come from the signal plane.
	Seen        bool       `json:"seen"`
	Completed   bool       `json:"completed"`
	Progress    uint32     `json:"progress"`
	ProgressMax uint32     `json:"progress_max"`
	Resume      string     `json:"resume,omitempty"`
	LastViewAt  *time.Time `json:"last_view_at,omitempty"`
}

ViewerState is a signed-in reader's own standing with a content item or version: their favorite and, with a signal plane, where they are in it.

Jump to

Keyboard shortcuts

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