clone

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

feat/ui-updates
tinyrelay / internal / records / projections.go
projections.go 4.5K
1package records23import (4 "context"5 "database/sql"6 "encoding/json"7 "errors"8 "strings"910 "github.com/FelineStateMachine/tinyrelay/internal/event"11 "github.com/FelineStateMachine/tinyrelay/internal/policy"12)1314func (s *Service) pins(ctx context.Context, now int64) (event.Event, error) {15 rows, err := s.store.DB().QueryContext(ctx, `SELECT ref FROM records_pins ORDER BY position`)16 if err != nil {17 return event.Event{}, err18 }19 defer rows.Close()20 tags := [][]string{{"-"}, {"d", s.group()}}21 for rows.Next() {22 var ref string23 if err := rows.Scan(&ref); err != nil {24 return event.Event{}, err25 }26 tags = append(tags, []string{"e", ref})27 }28 if err := rows.Err(); err != nil {29 return event.Event{}, err30 }31 return s.signed(ctx, event.KIND_GROUP_PINS, tags, "", now)32}3334// PublishMembership emits relay-owned identity and group records deterministically.35func (s *Service) PublishMembership(ctx context.Context, now int64) ([]event.Event, error) {36 var out []event.Event37 for _, fn := range []func(context.Context, int64) (event.Event, error){s.profile, s.discovery, s.roster, s.groupState} {38 e, err := fn(ctx, now)39 if err != nil {40 return nil, err41 }42 out = append(out, e)43 }44 group, err := s.groupRecords(ctx, now)45 if err != nil {46 return nil, err47 }48 out = append(out, group...)49 return out, nil50}5152// PublishMembershipChanges extends the projection with membership changes.53func (s *Service) PublishMembershipChanges(ctx context.Context, changes []MembershipChange, now int64) ([]event.Event, error) {54 out, err := s.PublishMembership(ctx, now)55 if err != nil {56 return nil, err57 }58 for _, c := range changes {59 if c.Added != nil {60 d, err := s.MembershipDelta(ctx, c.PubKey, *c.Added, now)61 if err != nil {62 return nil, err63 }64 out = append(out, d)65 }66 kind := event.KIND_PUT_USER67 if c.Added != nil && !*c.Added {68 kind = event.KIND_REMOVE_USER69 }70 tags := [][]string{{"-"}, {"h", s.group()}, {"p", c.PubKey}}71 for _, role := range c.Roles {72 tags[1] = append(tags[1], role)73 }74 e, err := s.signed(ctx, kind, tags, "", now)75 if err != nil {76 return nil, err77 }78 out = append(out, e)79 }80 return out, nil81}8283func (s *Service) RecordPresence(ctx context.Context, pubkey string, now int64) error {84 if pubkey == "" {85 return errors.New("presence: empty pubkey")86 }87 _, err := s.store.DB().ExecContext(ctx, `INSERT INTO records_presence(pubkey,seen_at) VALUES(?,?) ON CONFLICT(pubkey) DO UPDATE SET seen_at=excluded.seen_at`, pubkey, now)88 return err89}9091// NotePresence records presence from authenticated host paths.92func (s *Service) NotePresence(ctx context.Context, pubkey string, now int64) error {93 return s.RecordPresence(ctx, pubkey, now)94}9596func (s *Service) Presence(ctx context.Context, caller policy.Access, now int64) (event.Event, error) {97 if !policy.CanRead(s.policy(), event.Event{Kind: event.KIND_PRESENCE}, caller) {98 return event.Event{}, errors.New("restricted: presence")99 }100 rows, err := s.store.DB().QueryContext(ctx, `SELECT pubkey,seen_at FROM records_presence WHERE seen_at>=? ORDER BY pubkey`, now-15*60)101 if err != nil {102 return event.Event{}, err103 }104 defer rows.Close()105 var values []Presence106 for rows.Next() {107 var p Presence108 if err := rows.Scan(&p.PubKey, &p.SeenAt); err != nil {109 return event.Event{}, err110 }111 values = append(values, p)112 }113 b, _ := json.Marshal(values)114 return s.signedOnly(ctx, event.KIND_PRESENCE, nil, string(b), now)115}116117// SetPins persists ordered references and emits the signed pin record.118func (s *Service) SetPins(ctx context.Context, refs []string, now int64) (event.Event, error) {119 err := s.store.WithTx(ctx, func(tx *sql.Tx) error {120 return s.ReplacePinsTx(ctx, tx, refs)121 })122 if err != nil {123 return event.Event{}, err124 }125 return s.pins(ctx, now)126}127128// ReplacePinsTx replaces the records-owned pin list in a caller-owned129// transaction. Callers use this when pin metadata and its projection must130// commit together.131func (s *Service) ReplacePinsTx(ctx context.Context, tx *sql.Tx, refs []string) error {132 if _, err := tx.ExecContext(ctx, `DELETE FROM records_pins`); err != nil {133 return err134 }135 for position, ref := range refs {136 if strings.TrimSpace(ref) == "" {137 continue138 }139 if _, err := tx.ExecContext(ctx, `INSERT INTO records_pins(position,ref) VALUES(?,?)`, position, ref); err != nil {140 return err141 }142 }143 return nil144}145146func (s *Service) audit(ctx context.Context, actor, action, target, detail string, now int64) error {147 _, err := s.store.DB().ExecContext(ctx, `INSERT INTO records_audit(at,actor,action,target,detail) VALUES(?,?,?,?,?)`, now, actor, action, target, detail)148 return err149}