CI / Test (ubuntu-latest) (push) Canceled after 0s
CI / Test (windows-latest) (push) Canceled after 0s
CI / Storage tests on PostgreSQL (push) Canceled after 0s
CI / Complement (federation off) (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
CI / Smoke test the binary (push) Canceled after 0s
Implements PLAN.md Phase 9. internal/appservice reads registration files (appservice.config_files), matches namespaces the way Synapse does (anchored at the start), and delivers events to each service as transactions read from the roomserver's event stream at a cursor kept in appservice_state. A transaction in flight is recorded, so after a restart it is resent under the same ID; failures back off from 2s to 5m, and a successful ping cuts the backoff short. In the Client-Server API an as_token acts as the service's sender or, with ?user_id=, as any existing user in its namespace. Services register and log in their users with m.login.application_service; exclusive user and alias namespaces are refused to everybody else with M_EXCLUSIVE. Unknown aliases and users in a namespace are asked of the service first. The ping endpoint reports M_URL_NOT_SET, M_BAD_STATUS, M_CONNECTION_FAILED and M_CONNECTION_TIMEOUT. Services' own requests, and their users' when rate_limited is false, skip the rate limiter. Users can change their password (POST /account/password) and deactivate their account (POST /account/deactivate), both behind the password. Deactivation revokes tokens, deletes devices, optionally erases the profile and leaves every room through a new userapi.AccountObserver. The admin API is complete: user detail, the admin flag, deactivation and reactivation, password resets, devices, media quotas (media.max_user_storage_bytes plus per-user overrides), registration tokens with use limits and expiry (registration.require_token), server notices (internal/servernotices, server_notices.*), an editor for the federation allow/deny lists persisted in server_settings, a delivery view of the application services, and an audit log of every change. Migration 0011 adds the registration token, server notice room, media quota, audit log and appservice cursor tables. Policy lists are now returned sorted. The Complement entrypoint loads the registrations Complement mounts in /complement/appservice. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
163 lines
4.9 KiB
Go
163 lines
4.9 KiB
Go
// Package storage defines the interfaces every Warp storage driver
|
|
// implements, plus the registry that turns a config into a Database.
|
|
//
|
|
// Drivers live in sub-packages (sqlite, postgres, pocketbase) and register
|
|
// themselves from an init function, the database/sql way. Nothing in this
|
|
// package imports a driver, so components can depend on the interfaces
|
|
// without pulling in every database engine.
|
|
package storage
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"log/slog"
|
|
"maps"
|
|
"slices"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// Driver names. These are the values accepted by database.driver in the
|
|
// config file.
|
|
const (
|
|
DriverSQLite = "sqlite"
|
|
DriverPostgres = "postgres"
|
|
DriverPocketBase = "pocketbase"
|
|
)
|
|
|
|
// ErrNotImplemented is returned by a driver that is compiled in but not
|
|
// finished — currently PocketBase.
|
|
var ErrNotImplemented = errors.New("not implemented by this storage driver")
|
|
|
|
// ErrNoSuchSetting is returned by SettingsStore.Get for an unknown key.
|
|
var ErrNoSuchSetting = errors.New("no such setting")
|
|
|
|
// Config is the subset of the file configuration a driver needs.
|
|
type Config struct {
|
|
Driver string
|
|
ConnectionString string
|
|
MaxOpenConns int
|
|
MaxIdleConns int
|
|
ConnMaxLifetime time.Duration
|
|
}
|
|
|
|
// Database is the handle every component gets. Component-specific stores
|
|
// (userapi, roomserver, syncapi...) hang off it as they are implemented.
|
|
type Database interface {
|
|
// Driver reports which driver this is, for logs and admin output.
|
|
Driver() string
|
|
|
|
// Ping checks the connection is alive; used by /healthz.
|
|
Ping(ctx context.Context) error
|
|
|
|
// Migrate applies pending migrations.
|
|
Migrate(ctx context.Context) error
|
|
|
|
// PendingMigrations lists migrations that Migrate would apply.
|
|
PendingMigrations(ctx context.Context) ([]string, error)
|
|
|
|
// Settings is the server_settings key/value store backing the runtime
|
|
// settings service (federation and registration switches).
|
|
Settings() SettingsStore
|
|
|
|
// Users is the userapi store: accounts, devices, tokens, account data.
|
|
Users() UserStore
|
|
|
|
// Rooms is the roomserver store: rooms, events, room state, aliases.
|
|
Rooms() RoomStore
|
|
|
|
// Sync is the syncapi store: receipts, to-device messages, presence and
|
|
// uploaded filters.
|
|
Sync() SyncStore
|
|
|
|
// Federation is the federation store: other servers' keys, the
|
|
// outbound queue and its backoff, and the inbound transaction log.
|
|
Federation() FederationStore
|
|
|
|
// Media is the mediaapi store: the metadata of uploaded and cached
|
|
// remote media, and of their thumbnails.
|
|
Media() MediaStore
|
|
|
|
// Keys is the keyserver store: device keys, one-time and fallback
|
|
// keys, cross-signing keys and their signatures, the device list
|
|
// stream and the server-side key backup.
|
|
Keys() KeyStore
|
|
|
|
// Push is the push store: pushers, and the notifications push rules
|
|
// produce with their per-room unread counts.
|
|
Push() PushStore
|
|
|
|
// Admin is the admin API's own state: registration tokens, server
|
|
// notice rooms and the audit log.
|
|
Admin() AdminStore
|
|
|
|
// AppServices is where each application service is in the event
|
|
// stream.
|
|
AppServices() AppServiceStore
|
|
|
|
// Close releases the connection pool.
|
|
Close() error
|
|
}
|
|
|
|
// SettingsStore persists the handful of settings that are mutable at
|
|
// runtime through the admin API.
|
|
type SettingsStore interface {
|
|
// Get returns the raw value for key. The boolean reports presence.
|
|
Get(ctx context.Context, key string) (string, bool, error)
|
|
|
|
// GetAll returns every setting.
|
|
GetAll(ctx context.Context) (map[string]string, error)
|
|
|
|
// Set writes key, overwriting any previous value.
|
|
Set(ctx context.Context, key, value string) error
|
|
|
|
// Delete removes key, reverting it to its configured default. Deleting
|
|
// a key that is not there is not an error.
|
|
Delete(ctx context.Context, key string) error
|
|
}
|
|
|
|
// OpenFunc opens a database for a driver.
|
|
type OpenFunc func(ctx context.Context, cfg Config, log *slog.Logger) (Database, error)
|
|
|
|
var (
|
|
registryMu sync.RWMutex
|
|
registry = map[string]OpenFunc{}
|
|
)
|
|
|
|
// Register makes a driver available to Open. It panics on a duplicate
|
|
// registration, which can only be a programming error.
|
|
func Register(name string, open OpenFunc) {
|
|
registryMu.Lock()
|
|
defer registryMu.Unlock()
|
|
if _, dup := registry[name]; dup {
|
|
panic("storage: driver " + name + " registered twice")
|
|
}
|
|
registry[name] = open
|
|
}
|
|
|
|
// Drivers lists the registered driver names, sorted.
|
|
func Drivers() []string {
|
|
registryMu.RLock()
|
|
defer registryMu.RUnlock()
|
|
return slices.Sorted(maps.Keys(registry))
|
|
}
|
|
|
|
// Open connects using the driver named in cfg.
|
|
func Open(ctx context.Context, cfg Config, log *slog.Logger) (Database, error) {
|
|
registryMu.RLock()
|
|
open, ok := registry[cfg.Driver]
|
|
registryMu.RUnlock()
|
|
if !ok {
|
|
return nil, fmt.Errorf("storage: unknown driver %q (compiled in: %v)", cfg.Driver, Drivers())
|
|
}
|
|
if log == nil {
|
|
log = slog.Default()
|
|
}
|
|
db, err := open(ctx, cfg, log.With("driver", cfg.Driver))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("storage: open %s: %w", cfg.Driver, err)
|
|
}
|
|
return db, nil
|
|
}
|