tinyrelayfile
tinyrelay/internal/records/projections.go/

internal/records/projections.go raw
1package records
2
3import (
4 "context"
5 "database/sql"
6 "encoding/json"
7 "errors"
8 "strings"
9
10 "github.com/FelineStateMachine/tinyrelay/internal/event"
11 "github.com/FelineStateMachine/tinyrelay/internal/policy"
12)
13
14func (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{}, err
18 }
19 defer rows.Close()
20 tags := [][]string{{"-"}, {"d", s.group()}}
21 for rows.Next() {
22 var ref string
23 if err := rows.Scan(&ref); err != nil {
24 return event.Event{}, err
25 }
26 tags = append(tags, []string{"e", ref})
27 }
28 if err := rows.Err(); err != nil {
29 return event.Event{}, err
30 }
31 return s.signed(ctx, event.KIND_GROUP_PINS, tags, "", now)
32}
33
34// 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.Event
37 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, err
41 }
42 out = append(out, e)
43 }
44 group, err := s.groupRecords(ctx, now)
45 if err != nil {
46 return nil, err
47 }
48 out = append(out, group...)
49 return out, nil
50}
51
52// 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, err
57 }
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, err
63 }
64 out = append(out, d)
65 }
66 kind := event.KIND_PUT_USER
67 if c.Added != nil && !*c.Added {
68 kind = event.KIND_REMOVE_USER
69 }
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, err
77 }
78 out = append(out, e)
79 }
80 return out, nil
81}
82
83func (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 err
89}
90
91// 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}
95
96func (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{}, err
103 }
104 defer rows.Close()
105 var values []Presence
106 for rows.Next() {
107 var p Presence
108 if err := rows.Scan(&p.PubKey, &p.SeenAt); err != nil {
109 return event.Event{}, err
110 }
111 values = append(values, p)
112 }
113 b, _ := json.Marshal(values)
114 return s.signedOnly(ctx, event.KIND_PRESENCE, nil, string(b), now)
115}
116
117// 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{}, err
124 }
125 return s.pins(ctx, now)
126}
127
128// ReplacePinsTx replaces the records-owned pin list in a caller-owned
129// transaction. Callers use this when pin metadata and its projection must
130// 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 err
134 }
135 for position, ref := range refs {
136 if strings.TrimSpace(ref) == "" {
137 continue
138 }
139 if _, err := tx.ExecContext(ctx, `INSERT INTO records_pins(position,ref) VALUES(?,?)`, position, ref); err != nil {
140 return err
141 }
142 }
143 return nil
144}
145
146func (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 err
149}

About

ownerc01503e8035f
visibilitypublic
headd730679639bf

Maintainers

  • c01503e8035f owner
  • ba1b5beed5b5 agent hermes

Clone

  • https://012.run/npub1cq2s86qrtadf36eqq3epm38wmhdynrrcwxf84eldk3f94dkv7ttsrulrz5/tinyrelay.git

Refs