clone

a self-hosted multitenant nostr relay with sites, file storage, git hosting, and inbox and outbox delivery

main
tinyrelay / tinyrelay / relay.go
relay.go 5.6K
1// Package tinyrelay provides an embeddable Nostr relay and a standalone server.2// OpenServer owns a durable event store. New uses a host backend so an integrated3// application can commit event projections and required work atomically before4// publication. Both use the same authentication, subscription and synchronization5// transport. Neither constructor starts an HTTP listener.6package tinyrelay78import (9 "context"10 "errors"11 "net/http"12 "time"1314 "github.com/FelineStateMachine/tinyrelay/internal/relay"15 "github.com/FelineStateMachine/tinyrelay/protocol/auth"16 "github.com/FelineStateMachine/tinyrelay/protocol/nostr"17)1819type Session = auth.Session20type SyncItem = nostr.EventRef2122// Backend owns admission, persistence and authorized query results. Publish23// must commit required durable work before returning success. A successful24// duplicate returns a reason beginning with "duplicate:" and is not broadcast.25// New borrows the backend; its caller retains lifecycle ownership.26type Backend interface {27 Publish(context.Context, nostr.Event, Session) (string, error)28 Query(context.Context, []nostr.Filter, Session) ([]nostr.Event, error)29 Count(context.Context, []nostr.Filter, Session) (any, error)30}3132// Reader applies current access policy to historical and live event delivery.33type Reader interface {34 CanRead(nostr.Event, Session) bool35}3637// FilterReader additionally supplies the subscription filter to access policy.38type FilterReader interface {39 CanReadFilter(nostr.Event, Session, *nostr.Filter) bool40}4142// QueryHints adds completion and authentication hints to a historical query.43type QueryHints interface {44 QueryHints(context.Context, []nostr.Filter, Session) ([]nostr.Event, []string, error)45}4647// PolicyChanges releases host subscription state after access is invalidated.48type PolicyChanges interface {49 CloseSubscriptions(Session)50}5152// SyncBackend supplies an authorized snapshot for NIP-77 reconciliation.53type SyncBackend interface {54 Sync(context.Context, nostr.Filter, Session) ([]SyncItem, error)55}5657// Config controls connection identity, limits, authorization and observability.58// Zero MaxMessageBytes leaves input unbounded; a negative value selects the59// transport default. OpenServer supplies a bounded standalone default instead.60type Config struct {61 RelayURL string62 RequestRelayURL func(*http.Request) string63 OnAuthenticate func(context.Context, Session) error64 MaxMessageBytes int6465 MaxPendingBytes int66 ReadLimit time.Duration67 WriteLimit time.Duration68 PingInterval time.Duration69 OriginPatterns []string70 OnConnection func(int)71 OnSubscription func(int)72 // ChangesAccess identifies host-specific events that invalidate active73 // subscriptions. Standalone relays leave this unset.74 ChangesAccess func(nostr.Event) bool75}7677// Relay owns live connections and serializes publication with query snapshots.78// Stop accepting HTTP requests before Close, which drains its connections.79type Relay struct {80 router *relay.Router81}8283var _ http.Handler = (*Relay)(nil)8485// New embeds the relay protocol engine around a caller-owned backend.86func New(backend Backend, cfg Config) (*Relay, error) {87 if backend == nil {88 return nil, errors.New("tinyrelay: backend is required")89 }90 router := relay.New(backend, relay.Config{91 RelayURL: cfg.RelayURL, RequestRelayURL: cfg.RequestRelayURL,92 OnAuthenticate: cfg.OnAuthenticate, MaxMessageBytes: cfg.MaxMessageBytes,93 MaxPendingBytes: cfg.MaxPendingBytes, ReadLimit: cfg.ReadLimit,94 WriteLimit: cfg.WriteLimit, PingInterval: cfg.PingInterval,95 OriginPatterns: append([]string(nil), cfg.OriginPatterns...),96 OnConnection: cfg.OnConnection, OnSubscription: cfg.OnSubscription,97 ChangesAccess: cfg.ChangesAccess,98 })99 return &Relay{router: router}, nil100}101102// ServeHTTP upgrades a request to the Nostr WebSocket transport.103func (r *Relay) ServeHTTP(w http.ResponseWriter, req *http.Request) {104 r.router.HandleHTTP(w, req)105}106107// Publish commits through the backend before delivering to live subscribers.108func (r *Relay) Publish(ctx context.Context, e nostr.Event, s Session) (string, error) {109 return r.router.Publish(ctx, e, s)110}111112// Listen observes accepted events before per-reader access checks. The callback113// runs under the publication fence and must not block or publish recursively.114// Call the returned function to unregister the observer.115func (r *Relay) Listen(fn func(nostr.Event)) func() { return r.router.Listen(fn) }116117// BroadcastGenerated delivers an event that has already been durably accepted.118// Use CommitGenerated when persistence must share the historical query fence.119func (r *Relay) BroadcastGenerated(e nostr.Event) { r.router.BroadcastGenerated(e) }120121// CommitGenerated commits an imported/generated event under the publication122// fence, then broadcasts it. Failed commits never reach subscribers.123func (r *Relay) CommitGenerated(ctx context.Context, commit func(context.Context) (nostr.Event, error)) (nostr.Event, error) {124 return r.router.CommitGenerated(ctx, commit)125}126127// ApplyACLChange applies an access mutation and invalidates subscriptions while128// holding the same fence used by publication and reconciliation snapshots.129func (r *Relay) ApplyACLChange(ctx context.Context, change func(context.Context) (any, error), reason string) (any, error) {130 return r.router.ApplyACLChange(ctx, change, reason)131}132133// CloseSubscriptions invalidates current subscriptions after a policy revision.134func (r *Relay) CloseSubscriptions(reason string) { r.router.CloseSubscriptions(reason) }135136// Close stops and joins live connections without closing the borrowed backend.137func (r *Relay) Close(ctx context.Context) error { return r.router.Close(ctx) }