clone

nostr relays on demand at <name>.bind.ws: claim one with your key, pay in zaps past the free allowance

dependabot/npm_and_yarn/npm-f01a459b0f
bindws / src / succession.ts
succession.ts 5.7K
1// Succession: an heir and a dead-man's switch. The owner's presence is2// recorded on signed actions; silence past the delay starts a month of3// weekly warnings; silence through the month hands the relay to the heir.4// The bookkeeping lives in storage next to the policy; the warning in5// flight is mirrored here so NIP-11 can say so without a read.6import type { Relay } from "./relay.ts";7import { now } from "./event.ts";8import { SUCCESSION_WARN_DAYS } from "./settings.ts";9import { notify } from "./notify.ts";1011export interface SuccessionWarn {12 since: number;13 lastNotified: number;14}15export interface SuccessionLog {16 at: number;17 from: string;18 to: string;19}2021export class Succession {22 // A warning month in flight, or null.23 warn: SuccessionWarn | null = null;24 // When the presence write last happened (ms); at most hourly.25 seenWrite = 0;2627 constructor(private relay: Relay) {}2829 async load() {30 this.warn = (await this.relay.storage.get<SuccessionWarn>("succession_warn")) ?? null;31 }3233 // seen records that the owner acted, at most once an hour, and calls34 // off a succession warning in flight. Anyone else is ignored.35 async seen(pubkey: string) {36 if (!this.relay.settings.isOwner(pubkey)) return;37 const ms = Date.now();38 if (ms - this.seenWrite < 3600_000) return;39 this.seenWrite = ms;40 await this.relay.storage.put("ownerSeenAt", now());41 if (this.warn) {42 this.warn = null;43 await this.relay.storage.delete("succession_warn");44 }45 }4647 // seenNow starts the clock afresh: claim, transfer, naming an heir.48 async seenNow() {49 this.seenWrite = Date.now();50 await this.relay.storage.put("ownerSeenAt", now());51 this.warn = null;52 await this.relay.storage.delete("succession_warn");53 }5455 async status() {56 const t = now();57 const seen = (await this.relay.storage.get<number>("ownerSeenAt")) ?? 0;58 const log = (await this.relay.storage.get<SuccessionLog[]>("succession_log")) ?? [];59 const sc = this.relay.settings.policy.succession;60 const from = seen || t;61 const handoverAt = !sc ? 0 : this.warn ? this.warn.since + SUCCESSION_WARN_DAYS * 86400 : from + (sc.afterDays + SUCCESSION_WARN_DAYS) * 86400;62 return { succession: sc, ownerSeenAt: seen, silentDays: seen ? Math.floor((t - seen) / 86400) : 0, warning: this.warn, handoverAt, log };63 }6465 // tick runs from the daily alarm. Silence past the delay starts66 // a month of weekly warnings; silence through the month hands the relay67 // to the heir. Any owner action in between calls it off (seen).68 async tick(t: number) {69 const sc = this.relay.settings.policy.succession;70 const dropWarning = async () => {71 if (!this.warn) return;72 this.warn = null;73 await this.relay.storage.delete("succession_warn");74 };75 if (!sc || this.relay.settings.policy.owner === "" || this.relay.settings.isLeased()) return dropWarning();76 const subject = "succession on " + this.relay.slug;77 if (!this.relay.settings.member(sc.heir)) {78 // The heir left. Nobody to hand to; say so and stop.79 this.relay.settings.update({ succession: null });80 await dropWarning();81 await notify(this.relay, "succession", `Your heir ${sc.heir.slice(0, 8)} is no longer a member of ${this.relay.slug}, so the handover plan is off. Name another heir if you still want one.`, subject);82 return;83 }84 let seen = (await this.relay.storage.get<number>("ownerSeenAt")) ?? 0;85 if (seen === 0) {86 // Relays from before presence was recorded start the clock today.87 seen = t;88 await this.relay.storage.put("ownerSeenAt", t);89 }90 if (t - seen < sc.afterDays * 86400) return dropWarning();91 const day = (at: number) => new Date(at * 1000).toISOString().slice(0, 10);92 if (!this.warn) {93 this.warn = { since: t, lastNotified: t };94 await this.relay.storage.put("succession_warn", this.warn);95 await notify(this.relay, "succession", `You have not signed in to ${this.relay.slug} for ${Math.floor((t - seen) / 86400)} days. Unless you do, it goes to your heir ${sc.heir.slice(0, 8)} on ${day(t + SUCCESSION_WARN_DAYS * 86400)}. Any signed action on the relay calls this off.`, subject);96 return;97 }98 if (t - this.warn.since < SUCCESSION_WARN_DAYS * 86400) {99 if (t - this.warn.lastNotified >= 7 * 86400) {100 this.warn = { ...this.warn, lastNotified: t };101 await this.relay.storage.put("succession_warn", this.warn);102 await notify(this.relay, "succession", `Still no sign of you on ${this.relay.slug}. It goes to ${sc.heir.slice(0, 8)} on ${day(this.warn.since + SUCCESSION_WARN_DAYS * 86400)} unless you sign in.`, subject);103 }104 return;105 }106 // The month is up: hand over.107 const old = this.relay.settings.policy.owner;108 const err = this.relay.settings.transferOwner(sc.heir);109 if (err) {110 this.relay.settings.update({ succession: null });111 await dropWarning();112 await notify(this.relay, "succession", `The handover of ${this.relay.slug} to ${sc.heir.slice(0, 8)} could not happen: ${err}. The plan is off.`, subject);113 return;114 }115 const log = [...((await this.relay.storage.get<SuccessionLog[]>("succession_log")) ?? []), { at: t, from: old, to: sc.heir }].slice(-10);116 await this.relay.storage.put("succession_log", log);117 await this.seenNow();118 await this.relay.publishMembership({ pubkey: sc.heir }, { pubkey: old });119 await notify(this.relay, "succession", `${this.relay.slug} now belongs to ${sc.heir.slice(0, 8)}, as you planned. You stay on as a moderator.`, subject, old);120 await notify(this.relay, "succession", `${this.relay.slug} is yours now. Its owner named you heir and has been away for ${sc.afterDays + SUCCESSION_WARN_DAYS} days. Open https://${this.relay.slug}.${this.relay.domain}/ and sign in.`, subject, sc.heir);121 }122}