1package records23import (4 "context"5 "database/sql"6 "errors"78 "github.com/FelineStateMachine/tinyrelay/internal/policy"9 "github.com/FelineStateMachine/tinyrelay/internal/storage"10)1112// 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 err17 }18 if p.Succession == nil || p.Succession.Heir == "" || p.Owner == "" {19 return nil20 }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 err25 }26 if memberCount > 0 && !member {27 if s.setPolicy != nil {28 next := p29 next.Succession = nil30 if err := s.setPolicy(next); err != nil {31 return err32 }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 int6439 if err := s.store.GetSetting(ctx, "records.owner_seen", &heartbeat); err != nil && !errors.Is(err, sql.ErrNoRows) {40 return err41 }42 if heartbeat == 0 {43 heartbeat = now44 if err := s.store.PutSetting(ctx, "records.owner_seen", heartbeat); err != nil {45 return err46 }47 return nil48 }49 delay := int64(p.Succession.AfterDays) * 8640050 if now < heartbeat+delay {51 return nil52 }53 var warned int6454 _ = 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 err58 }59 if err := s.store.PutSetting(ctx, "records.succession_notify", now); err != nil {60 return err61 }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*8640065 if now < warnUntil {66 var last int6467 _ = 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 nil73 }74 old, heir := p.Owner, p.Succession.Heir75 if s.setPolicy == nil {76 return errors.New("records: succession policy writer unavailable")77 }78 next := p79 next.Owner = heir80 next.Succession = nil81 if err := s.setPolicy(next); err != nil {82 return err83 }84 if err := appendSuccessionLog(ctx, s.store, successionLog{At: now, From: old, To: heir}); err != nil {85 return err86 }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 err93 }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 err97 }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}100101func appendSuccessionLog(ctx context.Context, st *storage.Store, entry successionLog) error {102 var log []successionLog103 _ = 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}110111func (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 continue117 }118 if mode == "daily" {119 period = 86400120 }121 if mode == "hourly" {122 period = 3600123 }124 key := "records.view." + name + ".at"125 var last int64126 if err := s.store.GetSetting(ctx, key, &last); err != nil && !errors.Is(err, sql.ErrNoRows) {127 return err128 }129 var dirty int64130 if mode == "write" {131 if err := s.store.GetSetting(ctx, "records.view."+name+".dirty", &dirty); err != nil && !errors.Is(err, sql.ErrNoRows) {132 return err133 }134 if dirty != 0 && now-dirty >= 10 {135 // A write burst is coalesced into one publication.136 } else if dirty != 0 {137 continue138 } else if last != 0 && now-last < period-300 {139 continue140 }141 } else if last != 0 && now-last < period-300 {142 continue143 }144 if mode == "hourly" {145 fp, err := s.viewFingerprint(ctx, name, now)146 if err != nil {147 return err148 }149 var previous string150 if err := s.store.GetSetting(ctx, "records.view."+name+".fingerprint", &previous); err != nil && !errors.Is(err, sql.ErrNoRows) {151 return err152 }153 if previous != "" && previous == fp {154 if err := s.store.PutSetting(ctx, key, now); err != nil {155 return err156 }157 continue158 }159 if err := s.store.PutSetting(ctx, "records.view."+name+".fingerprint", fp); err != nil {160 return err161 }162 }163 e, err := s.view(ctx, name, policy.Access{Owner: true}, now)164 if err != nil {165 return err166 }167 if err := s.store.PutSetting(ctx, key, now); err != nil {168 return err169 }170 if err := s.recordViewRun(ctx, name, now, maxInt(0, len(e.Tags)-3)); err != nil {171 return err172 }173 if mode == "write" && dirty != 0 {174 if err := s.store.PutSetting(ctx, "records.view."+name+".dirty", int64(0)); err != nil {175 return err176 }177 }178 }179 return nil180}181182func (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 err188 }189 _ = s.store.PutSetting(ctx, "records.succession_warning", int64(0))190 return s.store.PutSetting(ctx, "records.succession_notify", int64(0))191}