clone

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

feat/ui-updates
tinyrelay / internal / records / records.go
records.go 5.9K
1package records23import (4 "context"5 "errors"6 "fmt"7 "strconv"8 "sync"9 "time"1011 "github.com/FelineStateMachine/tinyrelay/internal/communityread"12 "github.com/FelineStateMachine/tinyrelay/internal/event"13 "github.com/FelineStateMachine/tinyrelay/internal/policy"14 "github.com/FelineStateMachine/tinyrelay/internal/storage"15)1617type Config struct {18 // Store holds the tenant database and records settings.19 Store *storage.Store20 // Policy returns the current durable tenant policy when records evaluates21 // access or builds a projection.22 Policy func() policy.Policy23 // SetPolicy persists a policy change requested by records management.24 SetPolicy func(policy.Policy) error25 RelayURL string26 GroupID string27 // OnGenerated receives a signed event after its records clock transaction28 // commits. The callback commonly persists, publishes or queues the event.29 OnGenerated func(context.Context, event.Event) error30 // DeliverNotification delivers a sealed notification event to its target.31 DeliverNotification func(context.Context, event.Event, string) error32 // PushNotification receives a plaintext summary of each notification for33 // device delivery. The gift wrap itself is opaque to the relay once sealed.34 PushNotification func(context.Context, string, string, string, string) error35 OnTransfer func(context.Context, string, string) error36 // EventVisible optionally applies the host relay's live visibility rules37 // while building the podcast view. Other built-in views retain their38 // existing projection behavior.39 EventVisible func(context.Context, event.Event, policy.Access) bool40 // Community supplies membership and moderation projections through the41 // CommunityReader boundary.42 Community CommunityReader43}4445// CommunityReader is the read-only boundary records needs from community.46// Implementations own the community schema and may use a transaction-backed47// snapshot when the host is publishing a projection.48type CommunityReader interface {49 Members(ctx context.Context) ([]communityread.Member, error)50 ModerationCounts(ctx context.Context, since int64) (communityread.ModerationCounts, error)51 MemberStatus(ctx context.Context, pubkey string) (member bool, memberCount int, err error)52}5354type Member = communityread.Member55type ModerationCounts = communityread.ModerationCounts5657type Service struct {58 store *storage.Store59 policy func() policy.Policy60 setPolicy func(policy.Policy) error61 relayURL string62 groupID string63 secret string64 onGenerated func(context.Context, event.Event) error65 deliverNotification func(context.Context, event.Event, string) error66 pushNotification func(context.Context, string, string, string, string) error67 onTransfer func(context.Context, string, string) error68 community CommunityReader69 eventVisible func(context.Context, event.Event, policy.Access) bool70 mu sync.Mutex71}7273type MembershipChange struct {74 PubKey string75 Added *bool76 Roles []string77}7879const secretSetting = "records.relay_secret"8081const recordsSchema = `82CREATE TABLE IF NOT EXISTS records_audit(83 id INTEGER PRIMARY KEY AUTOINCREMENT, at INTEGER NOT NULL, actor TEXT NOT NULL,84 action TEXT NOT NULL, target TEXT NOT NULL DEFAULT '', detail TEXT NOT NULL DEFAULT '');85CREATE TABLE IF NOT EXISTS records_presence(86 pubkey TEXT PRIMARY KEY, seen_at INTEGER NOT NULL);87CREATE TABLE IF NOT EXISTS records_pins(88 position INTEGER PRIMARY KEY, ref TEXT NOT NULL UNIQUE);89CREATE TABLE IF NOT EXISTS records_view_runs(90 name TEXT NOT NULL, at INTEGER NOT NULL, rows INTEGER NOT NULL);91`9293func New(ctx context.Context, cfg Config) (*Service, error) {94 if cfg.Store == nil {95 return nil, errors.New("records: nil store")96 }97 if cfg.Policy == nil {98 return nil, errors.New("records: nil policy function")99 }100 if cfg.Community == nil {101 return nil, errors.New("records: nil community reader")102 }103 if _, err := cfg.Store.DB().ExecContext(ctx, recordsSchema); err != nil {104 return nil, fmt.Errorf("records schema: %w", err)105 }106 secret, err := loadOrCreateSecret(ctx, cfg.Store)107 if err != nil {108 return nil, err109 }110 groupID := cfg.GroupID111 if groupID == "" {112 groupID = cfg.RelayURL113 }114 return &Service{store: cfg.Store, policy: cfg.Policy, setPolicy: cfg.SetPolicy, relayURL: cfg.RelayURL, groupID: groupID, secret: secret, onGenerated: cfg.OnGenerated, deliverNotification: cfg.DeliverNotification, pushNotification: cfg.PushNotification, onTransfer: cfg.OnTransfer, community: cfg.Community, eventVisible: cfg.EventVisible}, nil115}116117var roleOrder = []string{"owner", "moderator", "member", "agent"}118var rolesAbout = map[string]string{"owner": "relay owner", "moderator": "relay moderator", "member": "relay member", "agent": "agent under an owner-signed grant"}119120type Presence struct {121 PubKey string `json:"pubkey"`122 SeenAt int64 `json:"seen_at"`123}124125type View struct {126 Name string `json:"name"`127 Event event.Event `json:"event"`128 Rows int `json:"rows"`129}130131type ViewRun struct {132 At int64 `json:"at"`133 Rows int `json:"rows"`134}135136var viewNames = []string{"profiles", "relays", "calendar", "moderation", "articles", "podcasts", "zaps", "presence"}137138func calendarStart(e event.Event) int64 {139 s := event.Tag(e, "start")140 if e.Kind == 31922 {141 t, err := time.Parse("2006-01-02", s)142 if err != nil {143 return 0144 }145 return t.Unix()146 }147 n, err := strconv.ParseInt(s, 10, 64)148 if err != nil {149 return 0150 }151 return n152}153func articlePublished(e event.Event) int64 {154 s := event.Tag(e, "published_at")155 if n, err := strconv.ParseInt(s, 10, 64); err == nil {156 return n157 }158 return e.CreatedAt159}160161func minInt(a, b int) int {162 if a < b {163 return a164 }165 return b166}167168func trimString(v any, n int) string {169 s, ok := v.(string)170 if !ok {171 return ""172 }173 if len(s) > n {174 return s[:n]175 }176 return s177}178179type successionLog struct {180 At int64 `json:"at"`181 From string `json:"from"`182 To string `json:"to"`183}184185func maxInt(a, b int) int {186 if a > b {187 return a188 }189 return b190}191192var _ = strconv.IntSize