From f028de7c04cc3a4822b116699567a10a4f851532 Mon Sep 17 00:00:00 2001 From: kieran Date: Wed, 12 Mar 2025 11:26:46 +0000 Subject: [PATCH] chore: cleanup login state --- packages/app/src/Feed/LoginFeed.ts | 1 + packages/app/src/Utils/Login/LoginSession.ts | 3 + .../app/src/Utils/Login/MultiAccountStore.ts | 77 +++++------ packages/shared/src/utils.ts | 12 +- packages/system/src/nostr-link.ts | 2 +- packages/system/src/request-builder.ts | 5 +- packages/system/src/user-state.ts | 121 ++++++++++-------- 7 files changed, 117 insertions(+), 104 deletions(-) diff --git a/packages/app/src/Feed/LoginFeed.ts b/packages/app/src/Feed/LoginFeed.ts index 6d0ff66f..f977fb2d 100644 --- a/packages/app/src/Feed/LoginFeed.ts +++ b/packages/app/src/Feed/LoginFeed.ts @@ -37,6 +37,7 @@ export default function useLoginFeed() { }, [pubKey]); useEffect(() => { + console.debug("UserState: start init from LoginFeed", login.state.didInit); login.state.init(publisher?.signer, system).catch(console.error); }, [login, publisher, system]); diff --git a/packages/app/src/Utils/Login/LoginSession.ts b/packages/app/src/Utils/Login/LoginSession.ts index 94626afe..1f6c23e4 100644 --- a/packages/app/src/Utils/Login/LoginSession.ts +++ b/packages/app/src/Utils/Login/LoginSession.ts @@ -62,6 +62,9 @@ export interface LoginSession { */ publicKey?: HexKey; + /** + * Login state for the current user + */ state: UserState; /** diff --git a/packages/app/src/Utils/Login/MultiAccountStore.ts b/packages/app/src/Utils/Login/MultiAccountStore.ts index 41ba560b..7da29808 100644 --- a/packages/app/src/Utils/Login/MultiAccountStore.ts +++ b/packages/app/src/Utils/Login/MultiAccountStore.ts @@ -7,7 +7,6 @@ import { EventPublisher, HexKey, KeyStorage, - NotEncrypted, RelaySettings, UserState, UserStateObject, @@ -63,6 +62,7 @@ const LoggedOut = { export class MultiAccountStore extends ExternalStore { #activeAccount?: HexKey; + #saveDebounce?: ReturnType; #accounts: Map = new Map(); #publishers = new Map(); @@ -109,6 +109,10 @@ export class MultiAccountStore extends ExternalStore { ); stateClass.checkIsStandardList(EventKind.StorageServerList); // track nip96 list stateClass.on("change", () => this.#save()); + if (v.state instanceof UserState) { + v.state.destroy(); + } + console.debug("UserState assign = ", stateClass); v.state = stateClass; // always activate signer @@ -117,7 +121,6 @@ export class MultiAccountStore extends ExternalStore { this.#publishers.set(v.id, signer); } } - this.#loadIrisKeyIfExists(); } getSessions() { @@ -191,8 +194,8 @@ export class MultiAccountStore extends ExternalStore { stalker: stalker ?? false, } as LoginSession; - newSession.state.checkIsStandardList(EventKind.StorageServerList); // track nip96 list - newSession.state.on("change", () => this.#save()); + newSession.state!.checkIsStandardList(EventKind.StorageServerList); // track nip96 list + newSession.state!.on("change", () => this.#save()); const pub = createPublisher(newSession); if (pub) { this.#publishers.set(newSession.id, pub); @@ -240,8 +243,8 @@ export class MultiAccountStore extends ExternalStore { appdataId: "snort", }), } as LoginSession; - newSession.state.checkIsStandardList(EventKind.StorageServerList); // track nip96 list - newSession.state.on("change", () => this.#save()); + newSession.state!.checkIsStandardList(EventKind.StorageServerList); // track nip96 list + newSession.state!.on("change", () => this.#save()); if ("nostr_os" in window && window?.nostr_os) { window?.nostr_os.saveKey(key.value); @@ -280,22 +283,6 @@ export class MultiAccountStore extends ExternalStore { return { ...s }; } - #loadIrisKeyIfExists() { - try { - const irisKeyJSON = window.localStorage.getItem("iris.myKey"); - if (irisKeyJSON) { - const irisKeyObj = JSON.parse(irisKeyJSON); - if (irisKeyObj.priv) { - const privateKey = new NotEncrypted(irisKeyObj.priv); - this.loginWithPrivateKey(privateKey); - window.localStorage.removeItem("iris.myKey"); - } - } - } catch (e) { - console.error("Failed to load iris key", e); - } - } - #migrate() { let didMigrate = false; @@ -367,27 +354,33 @@ export class MultiAccountStore extends ExternalStore { } #save() { - if (!this.#activeAccount && this.#accounts.size > 0) { - this.#activeAccount = this.#accounts.keys().next().value; + if (this.#saveDebounce !== undefined) { + clearTimeout(this.#saveDebounce); } - const toSave = []; - for (const v of this.#accounts.values()) { - if (v.privateKeyData instanceof KeyStorage) { - toSave.push({ - ...v, - state: v.state instanceof UserState ? v.state.serialize() : v.state, - privateKeyData: v.privateKeyData.toPayload(), - }); - } else { - toSave.push({ - ...v, - state: v.state instanceof UserState ? v.state.serialize() : v.state, - }); - } - } - - console.debug("Trying to save", toSave); - window.localStorage.setItem(AccountStoreKey, JSON.stringify(toSave)); this.notifyChange(); + this.#saveDebounce = setTimeout(() => { + if (!this.#activeAccount && this.#accounts.size > 0) { + this.#activeAccount = this.#accounts.keys().next().value; + } + const toSave = []; + for (const v of this.#accounts.values()) { + if (v.privateKeyData instanceof KeyStorage) { + toSave.push({ + ...v, + state: v.state instanceof UserState ? v.state.serialize() : v.state, + privateKeyData: v.privateKeyData.toPayload(), + }); + } else { + toSave.push({ + ...v, + state: v.state instanceof UserState ? v.state.serialize() : v.state, + }); + } + } + + console.debug("Trying to save", toSave); + window.localStorage.setItem(AccountStoreKey, JSON.stringify(toSave)); + this.#saveDebounce = undefined; + }, 2000); } } diff --git a/packages/shared/src/utils.ts b/packages/shared/src/utils.ts index 636a2231..cdac8d4d 100644 --- a/packages/shared/src/utils.ts +++ b/packages/shared/src/utils.ts @@ -226,7 +226,7 @@ export function normalizeReaction(content: string) { } } -export class OfflineError extends Error {} +export class OfflineError extends Error { } export function throwIfOffline() { if (isOffline()) { @@ -238,11 +238,13 @@ export function isOffline() { return !("navigator" in globalThis && globalThis.navigator.onLine); } -export function isHex(s: string) { +export function isHex(s?: string) { + if (!s) return false; // 48-57 = 0-9 // 65-90 = A-Z // 97-122 = a-z - return [...s] - .map(v => v.charCodeAt(0)) - .every(v => (v >= 48 && v <= 57) || (v >= 65 && v <= 90) || v >= 97 || v <= 122); + return s.length % 2 == 0 && + [...s] + .map(v => v.charCodeAt(0)) + .every(v => (v >= 48 && v <= 57) || (v >= 65 && v <= 90) || v >= 97 || v <= 122); } diff --git a/packages/system/src/nostr-link.ts b/packages/system/src/nostr-link.ts index 995d05c4..955e884d 100644 --- a/packages/system/src/nostr-link.ts +++ b/packages/system/src/nostr-link.ts @@ -150,7 +150,7 @@ export class NostrLink implements ToNostrEventTag { const ifSetCheck = (a: T | undefined, b: T) => { return !Boolean(a) || a === b; }; - return ifSetCheck(this.id, ev.id) && ifSetCheck(this.author, ev.pubkey) && ifSetCheck(this.kind, ev.kind); + return (EventExt.isReplaceable(ev.kind) || ifSetCheck(this.id, ev.id)) && ifSetCheck(this.author, ev.pubkey) && ifSetCheck(this.kind, ev.kind); } return false; diff --git a/packages/system/src/request-builder.ts b/packages/system/src/request-builder.ts index 6e21f155..888fe91f 100644 --- a/packages/system/src/request-builder.ts +++ b/packages/system/src/request-builder.ts @@ -2,7 +2,7 @@ import { v4 as uuid } from "uuid"; import { appendDedupe, dedupe, removeUndefined, sanitizeRelayUrl, unwrap } from "@snort/shared"; import EventKind from "./event-kind"; -import { NostrLink, NostrPrefix, ToNostrEventTag } from "."; +import { EventExt, NostrLink, NostrPrefix, ToNostrEventTag } from "."; import { ReqFilter, u256, HexKey, TaggedNostrEvent } from "./nostr"; import { RequestRouter } from "./request-router"; @@ -208,7 +208,8 @@ export class RequestFilterBuilder { .kinds([unwrap(link.kind)]) .authors([unwrap(link.author)]); } else { - if (link.id) { + // dont use id if link is replaceable kind + if (link.id && (link.kind === undefined || !EventExt.isReplaceable(link.kind))) { this.ids([link.id]); } if (link.author) { diff --git a/packages/system/src/user-state.ts b/packages/system/src/user-state.ts index 033d1aef..3695bb1f 100644 --- a/packages/system/src/user-state.ts +++ b/packages/system/src/user-state.ts @@ -50,11 +50,11 @@ export interface UserStateEvents { */ export class UserState extends EventEmitter { #log = debug("UserState"); - #profile: JsonEventSync; // kind 0 - #contacts: DiffSyncTags; // kind 3 - #relays: DiffSyncTags; // kind 10_003 + #profile?: JsonEventSync; // kind 0 + #contacts?: DiffSyncTags; // kind 3 + #relays?: DiffSyncTags; // kind 10_003 #appdata?: JsonEventSync; // kind 30_0078 - #standardLists: Map; // NIP-51 lists + #standardLists?: Map; // NIP-51 lists // init vars #signer?: EventSigner; @@ -72,15 +72,15 @@ export class UserState extends EventEmitter { ) { super(); this.#stateObj = stateObj; - this.#standardLists = new Map(); + this.#standardLists = pubkey ? new Map() : undefined; - this.#profile = new JsonEventSync( + this.#profile = pubkey ? new JsonEventSync( undefined, - new NostrLink(NostrPrefix.Event, "", EventKind.SetMetadata, pubkey), + new NostrLink(NostrPrefix.Event, pubkey, EventKind.SetMetadata, pubkey), false, - ); - this.#contacts = new DiffSyncTags(new NostrLink(NostrPrefix.Event, "", EventKind.ContactList, pubkey), false); - this.#relays = new DiffSyncTags(new NostrLink(NostrPrefix.Event, "", EventKind.Relays, pubkey), false); + ) : undefined; + this.#contacts = pubkey ? new DiffSyncTags(new NostrLink(NostrPrefix.Event, pubkey, EventKind.ContactList, pubkey), false) : undefined; + this.#relays = pubkey ? new DiffSyncTags(new NostrLink(NostrPrefix.Event, pubkey, EventKind.Relays, pubkey), false) : undefined; if (options?.appdataId && options.initAppdata) { const link = new NostrLink(NostrPrefix.Address, options.appdataId, EventKind.AppData, pubkey); this.#appdata = new JsonEventSync(options.initAppdata, link, options.encryptAppdata ?? false); @@ -90,12 +90,21 @@ export class UserState extends EventEmitter { // always track mute list this.checkIsStandardList(EventKind.MuteList); - this.#profile.on("change", () => this.emit("change", UserStateChangeType.Profile)); - this.#contacts.on("change", () => this.emit("change", UserStateChangeType.Contacts)); - this.#relays.on("change", () => this.emit("change", UserStateChangeType.Relays)); + this.#profile?.on("change", () => this.emit("change", UserStateChangeType.Profile)); + this.#contacts?.on("change", () => this.emit("change", UserStateChangeType.Contacts)); + this.#relays?.on("change", () => this.emit("change", UserStateChangeType.Relays)); this.on("change", () => this.#version++); } + get didInit() { + return this.#didInit; + } + + destroy() { + this.#log("Shutdown"); + this.removeAllListeners(); + } + async init(signer: EventSigner | undefined, system: SystemInterface) { if (this.#didInit) { return; @@ -105,35 +114,37 @@ export class UserState extends EventEmitter { this.#signer = signer; this.#system = system; const tasks = [ - this.#profile.sync(signer, system), - this.#contacts.sync(signer, system), - this.#relays.sync(signer, system), + this.#profile?.sync(signer, system), + this.#contacts?.sync(signer, system), + this.#relays?.sync(signer, system), ]; if (this.#appdata) { tasks.push(this.#appdata.sync(signer, system)); } - for (const list of this.#standardLists.values()) { - tasks.push(list.sync(signer, system)); + if (this.#standardLists) { + for (const list of this.#standardLists.values()) { + tasks.push(list.sync(signer, system)); + } } await Promise.all(tasks); this.#log( "Init results: signer=%s, profile=%O, contacts=%O, relays=%O, appdata=%O, lists=%O", signer ? "yes" : "no", - this.#profile.json, - this.#contacts.value, - this.#relays.value, + this.#profile?.json, + this.#contacts?.value, + this.#relays?.value, this.#appdata?.json, - [...this.#standardLists.values()].map(a => [a.value, a.encryptedTags]), + [...(this.#standardLists?.values() ?? [])].map(a => [a.link.kind, a.value, a.encryptedTags]), ); // update relay metadata with value from contact list if not found - if (this.#relays.value === undefined && this.#contacts.value?.content !== undefined && signer) { + if (this.#relays?.value === undefined && this.#contacts?.value?.content !== undefined && signer) { this.#log("Saving relays to NIP-65 relay list using %O", this.relays); for (const r of this.relays ?? []) { await this.addRelay(r.url, r.settings, false); } - await this.#relays.persist(signer, system); + await this.#relays?.persist(signer, system); } } @@ -149,16 +160,16 @@ export class UserState extends EventEmitter { * Users profile */ get profile() { - return this.#profile.json ?? this.#stateObj?.profile; + return this.#profile?.json ?? this.#stateObj?.profile; } /** * Users configured relays */ get relays() { - if (this.#relays.value) { + if (this.#relays?.value) { return parseRelayTags(this.#relays.tags); - } else if (this.#contacts.value) { + } else if (this.#contacts?.value) { return parseRelaysFromKind(this.#contacts.value); } else { return this.#stateObj?.relays; @@ -169,7 +180,7 @@ export class UserState extends EventEmitter { * Followed pubkeys */ get follows() { - if (this.#contacts.value) { + if (this.#contacts?.value) { const pTags = this.#contacts.tags.filter(a => a[0] === "p" && a[1].length === 64).map(a => a[1]) ?? []; return dedupe(pTags); } else { @@ -188,7 +199,7 @@ export class UserState extends EventEmitter { * Get the standard mute list */ get muted() { - const list = this.#standardLists.get(EventKind.MuteList); + const list = this.#standardLists?.get(EventKind.MuteList); if (list) { return NostrLink.fromAllTags(list.encryptedTags); } @@ -202,12 +213,12 @@ export class UserState extends EventEmitter { } const tag = link.toEventTag(); - if (tag) { + if (tag && this.#contacts) { this.#contacts.add(tag); if (autoCommit) { await this.saveContacts(); } - } else { + } else if (!tag) { throw new Error("Invalid link"); } } @@ -219,12 +230,12 @@ export class UserState extends EventEmitter { } const tag = link.toEventTag(); - if (tag) { + if (tag && this.#contacts) { this.#contacts.remove(tag); if (autoCommit) { await this.saveContacts(); } - } else { + } else if (!tag) { throw new Error("Invalid link"); } } @@ -235,10 +246,12 @@ export class UserState extends EventEmitter { throw new Error("Cannot follow this type of link"); } - const tags = removeUndefined(links.map(link => link.toEventTag())); - this.#contacts.replace(tags); - if (autoCommit) { - await this.saveContacts(); + if (this.#contacts) { + const tags = removeUndefined(links.map(link => link.toEventTag())); + this.#contacts.replace(tags); + if (autoCommit) { + await this.saveContacts(); + } } } @@ -250,7 +263,7 @@ export class UserState extends EventEmitter { async saveContacts() { this.#checkInit(); const content = JSON.stringify(this.#relaysObject()); - await this.#contacts.persist(this.#signer!, this.#system!, content); + await this.#contacts?.persist(this.#signer!, this.#system!, content); } async addRelay(addr: string, settings: RelaySettings, autoCommit = false) { @@ -260,12 +273,12 @@ export class UserState extends EventEmitter { url: addr, settings, }); - if (tag) { + if (tag && this.#relays) { this.#relays.add(tag); if (autoCommit) { await this.saveRelays(); } - } else { + } else if (!tag) { throw new Error("Invalid relay options"); } } @@ -274,12 +287,12 @@ export class UserState extends EventEmitter { this.#checkInit(); const url = sanitizeRelayUrl(addr); - if (url) { + if (url && this.#relays) { this.#relays.remove(["r", url]); if (autoCommit) { await this.saveRelays(); } - } else { + } else if (!url) { throw new Error("Invalid relay options"); } } @@ -292,12 +305,12 @@ export class UserState extends EventEmitter { settings, }); const url = sanitizeRelayUrl(addr); - if (url && tag) { + if (url && tag && this.#relays) { this.#relays.update(tag); if (autoCommit) { await this.saveRelays(); } - } else { + } else if (!url && !tag) { throw new Error("Invalid relay options"); } } @@ -309,7 +322,7 @@ export class UserState extends EventEmitter { */ async saveRelays() { this.#checkInit(); - await this.#relays.persist(this.#signer!, this.#system!); + await this.#relays?.persist(this.#signer!, this.#system!); } async setAppData(data: TAppData) { @@ -336,7 +349,7 @@ export class UserState extends EventEmitter { ) { this.checkIsStandardList(kind); this.#checkInit(); - const list = this.#standardLists.get(kind); + const list = this.#standardLists?.get(kind); const tags = removeUndefined(Array.isArray(links) ? links.map(a => a.toEventTag()) : [links.toEventTag()]); if (list && tags.length > 0) { list.add(tags, encrypted); @@ -361,7 +374,7 @@ export class UserState extends EventEmitter { ) { this.checkIsStandardList(kind); this.#checkInit(); - const list = this.#standardLists.get(kind); + const list = this.#standardLists?.get(kind); const tags = removeUndefined(Array.isArray(links) ? links.map(a => a.toEventTag()) : [links.toEventTag()]); if (list && tags.length > 0) { list.remove(tags, encrypted); @@ -377,7 +390,7 @@ export class UserState extends EventEmitter { * used with `autocommit = false` */ async saveList(kind: EventKind, content?: string) { - const list = this.#standardLists.get(kind); + const list = this.#standardLists?.get(kind); await list?.persist(this.#signer!, this.#system!, content); } @@ -390,7 +403,7 @@ export class UserState extends EventEmitter { } isOnList(kind: EventKind, link: ToNostrEventTag) { - const list = this.#standardLists.get(kind); + const list = this.#standardLists?.get(kind); const tag = link.toEventTag(); if (list && tag) { return list.tags.some(a => a[0] === tag[0] && a[1] === tag[1]); @@ -399,7 +412,7 @@ export class UserState extends EventEmitter { } getList(kind: EventKind): Array { - const list = this.#standardLists.get(kind); + const list = this.#standardLists?.get(kind); return NostrLink.fromAllTags(list?.tags ?? []); } @@ -414,12 +427,12 @@ export class UserState extends EventEmitter { checkIsStandardList(kind: EventKind) { if (!(kind >= 10_000 && kind < 20_000)) { - throw new Error("Not a standar list"); + throw new Error("Not a standard list"); } - if (!this.#standardLists.has(kind)) { - const list = new DiffSyncTags(new NostrLink(NostrPrefix.Event, "", kind, this.pubkey), true); + if (this.#standardLists?.has(kind) === false) { + const list = new DiffSyncTags(new NostrLink(NostrPrefix.Event, this.pubkey, kind, this.pubkey), true); list.on("change", () => this.emit("change", UserStateChangeType.GenericList)); - this.#standardLists.set(kind, list); + this.#standardLists?.set(kind, list); } }