store

package
v0.1.2 Latest Latest
Warning

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

Go to latest
Published: Oct 1, 2026 License: MIT Imports: 10 Imported by: 0

Documentation

Overview

Package store is the persistence layer (PRD §4.2): SQLite behind a Store interface so a move to Postgres is a driver swap, not a rewrite.

Index

Constants

This section is empty.

Variables

View Source
var ErrDomainTaken = errors.New("domain already mapped to another account")

ErrDomainTaken means a domain is already mapped to a different member.

View Source
var ErrKeyMismatch = errors.New("username registered with a different key")

ErrKeyMismatch means a username is already registered with another key.

View Source
var ErrQuotaExceeded = errors.New("qrypt invite quota exceeded")

ErrQuotaExceeded means a member has hit their qrypt.chat invite quota.

Functions

This section is empty.

Types

type AdminAction

type AdminAction struct {
	Admin  string
	Action string
	Target string
	Detail string
	At     time.Time
}

AdminAction is one entry in the admin audit log.

type ChatMessage

type ChatMessage struct {
	Role string // "user" or "agent"
	Text string
	At   time.Time
}

ChatMessage is one line of an agent@ conversation.

type ChatRow

type ChatRow struct {
	Username string
	Role     string
	Text     string
	At       time.Time
}

ChatRow is one agent@ message with its author, for moderation review.

type DomainMap

type DomainMap struct {
	Domain   string
	Username string
	At       time.Time
}

DomainMap binds a custom domain to a member's homepage (the public_html that is also served at /~name). Used to serve, e.g., https://chovy.com from users/chovy/public_html.

type FilesAccess added in v0.1.1

type FilesAccess struct {
	QuotaBytes int64
	Revoked    bool
}

FilesAccess is a user's SFTP access record. QuotaBytes is a per-user override (0 means "use the server default"); Revoked blocks SFTP without affecting the BBS login.

type MatchRow

type MatchRow struct {
	ID          int64
	Game        string
	P0          string
	P1          string
	Winner      int
	Reason      string
	Moves       []games.Move
	RatingAfter [2]float64
	StartedAt   time.Time
	EndedAt     time.Time
}

MatchRow is a recorded match, including its moves for replay.

type Message added in v0.1.1

type Message struct {
	ID   int64
	From string
	To   string
	Body string
	Read bool
	At   time.Time
}

Message is one member-to-member note in the store-and-forward inbox.

type NewsArticle

type NewsArticle struct {
	Group     string
	Num       int64
	MsgID     string
	Subject   string
	From      string // the From: header (stamped to the posting member)
	Refs      string // the References: header
	Date      string // the Date: header as posted (RFC1123Z)
	Body      string
	Lines     int
	Bytes     int
	CreatedAt time.Time
}

NewsArticle is one stored article within a group. Headers beyond these are reconstructed at serve time (Message-ID, Newsgroups, Path) from these fields.

type NewsGroup

type NewsGroup struct {
	Name        string
	Description string
	Posting     bool
	Count       int64
	Low         int64
	High        int64
	CreatedAt   time.Time
}

NewsGroup is a newsgroup plus the cached article-number bounds NNTP clients expect (Low/High/Count). Empty groups report Low=1, High=0, Count=0.

type RatingRow

type RatingRow struct {
	User   string
	Rating float64
	Played int
}

RatingRow is one ladder entry.

type Score

type Score struct {
	User  string
	Game  string
	Score int64
	At    time.Time
}

Score is one leaderboard entry.

type SessionRow

type SessionRow struct {
	ID         int64
	Username   string
	Remote     string
	Route      string
	Started    time.Time
	Ended      time.Time
	EndedValid bool // false while the session is still open
}

SessionRow is one connection record from the audit trail.

type Store

type Store interface {
	// EnsureUser returns the user with this name, creating it with the given
	// kind and key fingerprint on first sight. If the name exists with a
	// different fingerprint, ErrKeyMismatch is returned.
	EnsureUser(name, kind, pubkeyFP string) (User, error)
	// UserByFingerprint finds an account by SSH key fingerprint.
	UserByFingerprint(fp string) (User, bool, error)
	// UserByName finds an account by exact username (no creation).
	UserByName(name string) (User, bool, error)
	// LastSeen reports the start of the user's most recent session.
	LastSeen(userID int64) (time.Time, bool, error)

	// SetEmailVerification records the account's email and a fresh
	// confirmation token (a link token or a short code), marking it unverified
	// until the token is consumed.
	SetEmailVerification(userID int64, email, token string) error
	// VerifyEmail consumes a confirmation token: on match it marks the
	// account verified, clears the token, and returns the account.
	VerifyEmail(token string) (User, bool, error)
	// ConfirmEmailCode is the interactive (join@) counterpart to VerifyEmail:
	// it matches the code against the one stored for THIS user (codes are
	// short and not globally unique), and on match marks the account verified
	// and clears the code. Returns ok=false on a wrong/empty code.
	ConfirmEmailCode(userID int64, code string) (User, bool, error)

	// SetPremiumPayment records the CoinPay payment id of a pending premium
	// charge so a later visit can verify whether it settled.
	SetPremiumPayment(userID int64, payID string) error
	// GrantPremium marks the account as a lifetime premium member (the $99
	// Founding Lifetime membership), recording the CoinPay payment reference. Idempotent.
	GrantPremium(userID int64, paymentRef string) error

	RecordSession(userID int64, username, remote, route string) (int64, error)
	EndSession(sessionID int64) error

	AddScore(userID int64, game string, score int64) error
	TopScores(game string, n int) ([]Score, error)

	// Pod subscription (paid membership, e.g. $1/mo via CoinPay).
	PodPaidUntil(userID int64) (time.Time, bool, error)
	GrantPod(userID int64, until time.Time, paymentRef string) error

	// Chat transcripts for the agent@ surface.
	AddChat(userID int64, username, role, text string) error
	RecentChats(username string, n int) ([]ChatMessage, error)

	// SendMessage leaves a note from→to in the recipient's inbox.
	SendMessage(from, to, body string) error
	// SendMessageMulti delivers one body to every recipient in to, in a single
	// transaction (used for group + broadcast messages so a partial failure
	// doesn't leave some inboxes written and others not). Duplicate and empty
	// recipients are skipped; an empty list is a no-op. Returns the number of
	// inboxes written.
	SendMessageMulti(from string, to []string, body string) (int, error)
	// Inbox returns up to n messages addressed to username, newest first.
	Inbox(username string, n int) ([]Message, error)
	// UnreadCount reports how many unread messages username has waiting.
	UnreadCount(username string) (int, error)
	// MarkRead marks the given message ids read (scoped to username so a member
	// can only clear their own mail). Empty ids is a no-op.
	MarkRead(username string, ids []int64) error
	// OnlineUsers reports the set of usernames with an open session (no
	// ended_at), for the members directory presence dots.
	OnlineUsers() (map[string]bool, error)

	// Custom domains mapped to a member's homepage (public_html).
	// MapDomain binds domain→username, returning whether a new row was created.
	// It returns ErrDomainTaken if the domain is already claimed by someone else;
	// re-binding to the same owner is a no-op and returns false.
	MapDomain(domain, username string) (bool, error)
	UnmapDomain(domain, username string) error
	DomainUser(domain string) (string, bool, error)
	DomainsForUser(username string) ([]string, error)
	AllDomains() ([]DomainMap, error)

	// ListUsers returns up to limit accounts, most recently created first.
	ListUsers(limit int) ([]User, error)
	// SetBanned suspends (or restores) an account; banned accounts are blocked
	// at login by the SSH routes.
	SetBanned(userID int64, banned bool) error
	// RecentSessions returns the last n session rows (the audit trail).
	RecentSessions(n int) ([]SessionRow, error)
	// LogAdminAction records one privileged action for the audit log.
	LogAdminAction(admin, action, target, detail string) error
	// RecentAdminActions returns the last n logged admin actions.
	RecentAdminActions(n int) ([]AdminAction, error)
	// RecentChatsAll returns the last n agent@ messages across all users, for
	// moderation review.
	RecentChatsAll(n int) ([]ChatRow, error)
	// DisabledPlugins reports the set of plugin IDs currently switched off.
	DisabledPlugins() (map[string]bool, error)
	// SetPluginDisabled enables or disables a plugin by ID. Idempotent.
	SetPluginDisabled(id string, disabled bool) error

	// Rating returns a player's current rating for a game, or
	// games.DefaultRating if they have no rated history there. It satisfies
	// games.Store so the matchmaker can read ratings.
	Rating(user, game string) (float64, error)
	// SaveMatch records a finished match and upserts both players' ratings.
	// It satisfies games.Store.
	SaveMatch(games.FinishedMatch) error
	// TopRatings returns the n highest-rated players for a game.
	TopRatings(game string, n int) ([]RatingRow, error)
	// RecentMatches returns the last n matches for a game, newest first.
	RecentMatches(game string, n int) ([]MatchRow, error)
	// MatchByID returns one match (with its moves, for replay).
	MatchByID(id int64) (MatchRow, bool, error)

	// MintAPIToken creates and stores a fresh bearer token for the WebSocket
	// game endpoint, bound to username. Returns the token.
	MintAPIToken(username string) (string, error)
	// UserByToken resolves an API token to its account name.
	UserByToken(token string) (string, bool, error)

	// QryptInviteCount reports how many qrypt.chat invites username has issued.
	QryptInviteCount(username string) (int, error)
	// RecordQryptInvite records one issued invite (its jti, for audit and as
	// the per-member quota counter) against username. It returns
	// ErrQuotaExceeded if the member is already at or above quota.
	RecordQryptInvite(username, jti string, quota int) error

	// EnsureNewsGroup creates a newsgroup if absent (idempotent), setting the
	// description only on first creation.
	EnsureNewsGroup(name, description string) error
	// NewsGroups lists every group with its article counts, name-sorted.
	NewsGroups() ([]NewsGroup, error)
	// NewsGroup returns one group (with counts), or ok=false if unknown.
	NewsGroup(name string) (NewsGroup, bool, error)
	// NewsArticleByNum fetches an article by its per-group sequence number.
	NewsArticleByNum(group string, num int64) (NewsArticle, bool, error)
	// NewsArticleByMsgID fetches the first article with this Message-ID (any
	// group it was posted to).
	NewsArticleByMsgID(msgID string) (NewsArticle, bool, error)
	// NewsArticlesRange returns articles in [from,to] (inclusive) for a group,
	// ordered by number, for OVER/XOVER.
	NewsArticlesRange(group string, from, to int64) ([]NewsArticle, error)
	// InsertNewsArticle stores an article in a group, assigning the next
	// per-group number, and returns the stored row (with its number).
	InsertNewsArticle(a NewsArticle) (NewsArticle, error)

	// FilesAccess returns a user's SFTP access record (per-user quota override
	// and revoked flag). A user with no row reports the zero value
	// (QuotaBytes 0 = use the server default, Revoked false).
	FilesAccess(userID int64) (FilesAccess, error)
	// SetFilesQuota sets a per-user quota override in bytes (0 clears the
	// override, falling back to the server default). Idempotent upsert.
	SetFilesQuota(userID, bytes int64) error
	// SetFilesRevoked revokes (or restores) a user's SFTP access without
	// touching their BBS login. Idempotent upsert.
	SetFilesRevoked(userID int64, revoked bool) error
	// FilesSetting reads a Files service setting (e.g. the public-write mode).
	FilesSetting(key string) (string, bool, error)
	// SetFilesSetting writes a Files service setting. Idempotent upsert.
	SetFilesSetting(key, value string) error

	Close() error
}

Store is the persistence contract shared by all plugins.

func Open

func Open(path string) (Store, error)

Open opens (and migrates) the SQLite store at path.

type User

type User struct {
	ID            int64
	Name          string
	Kind          string
	PubKeyFP      string
	Email         string
	EmailVerified bool
	Premium       bool   // paid the one-time lifetime membership
	PremiumPayID  string // CoinPay payment id of the pending/settled premium charge
	Banned        bool   // suspended by an admin (blocked at login)
	CreatedAt     time.Time
}

User is a persisted account (member or agent; guests are never stored).

Jump to

Keyboard shortcuts

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