tinyrelayfile
tinyrelay/internal/records/succession.go/

internal/records/succession.go raw
1package records
2
3import (
4 "context"
5 "database/sql"
6 "errors"
7
8 "github.com/FelineStateMachine/tinyrelay/internal/policy"
9 "github.com/FelineStateMachine/tinyrelay/internal/storage"
10)
11
12// Tick is called by the host scheduler and does not start a goroutine.
13func (s *Service) Tick(ctx context.Context, now int64) error {
14 p := s.policy()
15 if err := s.tickViews(ctx, p, now); err != nil {
16 return err
17 }
18 if p.Succession == nil || p.Succession.Heir == "" || p.Owner == "" {
19 return nil
20 }
21 // The heir must remain a member while the plan is armed.
22 member, memberCount, err := s.memberStatus(ctx, p.Succession.Heir)
23 if err != nil {
24 return err
25 }
26 if memberCount > 0 && !member {
27 if s.setPolicy != nil {
28 next := p
29 next.Succession = nil
30 if err := s.setPolicy(next); err != nil {
31 return err
32 }
33 }
34 _ = s.store.PutSetting(ctx, "records.succession_warning", int64(0))
35 _ = s.store.PutSetting(ctx, "records.succession_notify", int64(0))
36 return s.notifyText(ctx, "succession", "Your heir is no longer a member, so the handover plan is off. Name another heir if you still want one.", "succession on relay", p.Owner, now)
37 }
38 var heartbeat int64
39 if err := s.store.GetSetting(ctx, "records.owner_seen", &heartbeat); err != nil && !errors.Is(err, sql.ErrNoRows) {
40 return err
41 }
42 if heartbeat == 0 {
43 heartbeat = now
44 if err := s.store.PutSetting(ctx, "records.owner_seen", heartbeat); err != nil {
45 return err
46 }
47 return nil
48 }
49 delay := int64(p.Succession.AfterDays) * 86400
50 if now < heartbeat+delay {
51 return nil
52 }
53 var warned int64
54 _ = s.store.GetSetting(ctx, "records.succession_warning", &warned)
55 if warned == 0 {
56 if err := s.store.PutSetting(ctx, "records.succession_warning", now); err != nil {
57 return err
58 }
59 if err := s.store.PutSetting(ctx, "records.succession_notify", now); err != nil {
60 return err
61 }
62 return s.notifyText(ctx, "succession", "You have been away long enough to start the succession warning period. Any signed action cancels it.", "succession on relay", p.Owner, now)
63 }
64 warnUntil := warned + 30*86400
65 if now < warnUntil {
66 var last int64
67 _ = s.store.GetSetting(ctx, "records.succession_notify", &last)
68 if now-last >= 7*86400 {
69 _ = s.store.PutSetting(ctx, "records.succession_notify", now)
70 return s.notifyText(ctx, "succession", "Still no sign of you. The relay will go to your heir unless you sign in.", "succession on relay", p.Owner, now)
71 }
72 return nil
73 }
74 old, heir := p.Owner, p.Succession.Heir
75 if s.setPolicy == nil {
76 return errors.New("records: succession policy writer unavailable")
77 }
78 next := p
79 next.Owner = heir
80 next.Succession = nil
81 if err := s.setPolicy(next); err != nil {
82 return err
83 }
84 if err := appendSuccessionLog(ctx, s.store, successionLog{At: now, From: old, To: heir}); err != nil {
85 return err
86 }
87 _ = s.store.PutSetting(ctx, "records.owner_seen", now)
88 _ = s.store.PutSetting(ctx, "records.succession_warning", int64(0))
89 _ = s.store.PutSetting(ctx, "records.succession_notify", int64(0))
90 if s.onTransfer != nil {
91 if err := s.onTransfer(ctx, old, heir); err != nil {
92 return err
93 }
94 }
95 if err := s.notifyText(ctx, "succession", "The relay now belongs to your heir, as you planned. You stay on as a moderator.", "succession on relay", old, now); err != nil {
96 return err
97 }
98 return s.notifyText(ctx, "succession", "The relay is yours now. Its owner named you heir and has been away through the warning period.", "succession on relay", heir, now)
99}
100
101func appendSuccessionLog(ctx context.Context, st *storage.Store, entry successionLog) error {
102 var log []successionLog
103 _ = st.GetSetting(ctx, "records.succession_log", &log)
104 log = append(log, entry)
105 if len(log) > 10 {
106 log = log[len(log)-10:]
107 }
108 return st.PutSetting(ctx, "records.succession_log", log)
109}
110
111func (s *Service) tickViews(ctx context.Context, p policy.Policy, now int64) error {
112 defaults := map[string]int64{"profiles": 86400, "relays": 86400, "calendar": 3600, "moderation": 86400, "articles": 86400, "zaps": 3600}
113 for name, period := range defaults {
114 mode := effectiveViewTrigger(p, name)
115 if mode == "off" || !viewStored(p, name) {
116 continue
117 }
118 if mode == "daily" {
119 period = 86400
120 }
121 if mode == "hourly" {
122 period = 3600
123 }
124 key := "records.view." + name + ".at"
125 var last int64
126 if err := s.store.GetSetting(ctx, key, &last); err != nil && !errors.Is(err, sql.ErrNoRows) {
127 return err
128 }
129 var dirty int64
130 if mode == "write" {
131 if err := s.store.GetSetting(ctx, "records.view."+name+".dirty", &dirty); err != nil && !errors.Is(err, sql.ErrNoRows) {
132 return err
133 }
134 if dirty != 0 && now-dirty >= 10 {
135 // A write burst is coalesced into one publication.
136 } else if dirty != 0 {
137 continue
138 } else if last != 0 && now-last < period-300 {
139 continue
140 }
141 } else if last != 0 && now-last < period-300 {
142 continue
143 }
144 if mode == "hourly" {
145 fp, err := s.viewFingerprint(ctx, name, now)
146 if err != nil {
147 return err
148 }
149 var previous string
150 if err := s.store.GetSetting(ctx, "records.view."+name+".fingerprint", &previous); err != nil && !errors.Is(err, sql.ErrNoRows) {
151 return err
152 }
153 if previous != "" && previous == fp {
154 if err := s.store.PutSetting(ctx, key, now); err != nil {
155 return err
156 }
157 continue
158 }
159 if err := s.store.PutSetting(ctx, "records.view."+name+".fingerprint", fp); err != nil {
160 return err
161 }
162 }
163 e, err := s.view(ctx, name, policy.Access{Owner: true}, now)
164 if err != nil {
165 return err
166 }
167 if err := s.store.PutSetting(ctx, key, now); err != nil {
168 return err
169 }
170 if err := s.recordViewRun(ctx, name, now, maxInt(0, len(e.Tags)-3)); err != nil {
171 return err
172 }
173 if mode == "write" && dirty != 0 {
174 if err := s.store.PutSetting(ctx, "records.view."+name+".dirty", int64(0)); err != nil {
175 return err
176 }
177 }
178 }
179 return nil
180}
181
182func (s *Service) Heartbeat(ctx context.Context, owner string, now int64) error {
183 if owner == "" || owner != s.policy().Owner {
184 return errors.New("restricted: owner heartbeat")
185 }
186 if err := s.store.PutSetting(ctx, "records.owner_seen", now); err != nil {
187 return err
188 }
189 _ = s.store.PutSetting(ctx, "records.succession_warning", int64(0))
190 return s.store.PutSetting(ctx, "records.succession_notify", int64(0))
191}

About

ownerc01503e8035f
visibilitypublic
headd730679639bf

Maintainers

  • c01503e8035f owner
  • ba1b5beed5b5 agent hermes

Clone

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

Refs