blowater/app/UI/config-other.ts
2024-06-17 15:42:51 +08:00

203 lines
6.3 KiB
TypeScript

import { prepareEncryptedNostrEvent } from "../../libs/nostr.ts/event.ts";
import { NostrAccountContext, NostrEvent, NostrKind, verifyEvent } from "../../libs/nostr.ts/nostr.ts";
import { parseJSON } from "../features/profile.ts";
import { PinConversationRelay, UnpinConversationRelay } from "../nostr.ts";
import { LamportTime } from "../time.ts";
import { PinListGetter } from "./conversation-list.tsx";
export type NostrEventAdder = {
addEvent(event: NostrEvent): Promise<undefined | Error>;
};
export class OtherConfig implements PinListGetter, NostrEventAdder {
static Empty(ctx: NostrAccountContext, lamport: LamportTime) {
return new OtherConfig(ctx, lamport);
}
static async FromLocalStorage(
ctx: NostrAccountContext,
lamport: LamportTime,
) {
const item = localStorage.getItem(`${OtherConfig.name}:${ctx.publicKey.bech32()}`);
if (item == null) {
return OtherConfig.Empty(ctx, lamport);
}
const event = parseJSON<NostrEvent>(item);
if (event instanceof Error) {
console.error(event);
return OtherConfig.Empty(ctx, lamport);
}
const ok = await verifyEvent(event);
if (!ok) {
return OtherConfig.Empty(ctx, lamport);
}
if (event.kind == NostrKind.Encrypted_Custom_App_Data) {
const config = await OtherConfig.FromNostrEvent(
{
...event,
kind: event.kind,
},
ctx,
lamport,
);
if (config instanceof Error) {
return OtherConfig.Empty(ctx, lamport);
}
return config;
}
return OtherConfig.Empty(ctx, lamport);
}
private constructor(
private readonly ctx: NostrAccountContext,
private readonly lamport: LamportTime,
) {}
private pinList = new Map<string, PinConversationRelay | UnpinConversationRelay>(); // set of pubkeys in npub format
getPinList(): Set<string> {
const set = new Set<string>();
for (const event of this.pinList.values()) {
if (event.type == "PinConversation") {
set.add(event.pubkey);
}
}
return set;
}
async addPin(pubkey: string) {
const currentPin = this.pinList.get(pubkey);
if (currentPin && currentPin.type == "PinConversation") {
return;
}
const pin: PinConversationRelay = {
pubkey,
type: "PinConversation",
lamport: this.lamport.now(),
};
const event = await prepareEncryptedNostrEvent(this.ctx, {
content: JSON.stringify(pin),
encryptKey: this.ctx.publicKey,
kind: NostrKind.Encrypted_Custom_App_Data,
});
if (event instanceof Error) {
return event;
}
this.pinList.set(pubkey, pin);
const err = await this.saveToLocalStorage();
if (err instanceof Error) {
return err;
}
}
async removePin(pubkey: string) {
const exist = this.pinList.delete(pubkey);
if (!exist) {
return;
}
const unpin: UnpinConversationRelay = {
pubkey,
type: "UnpinConversation",
lamport: this.lamport.now(),
};
const event = await prepareEncryptedNostrEvent(this.ctx, {
content: JSON.stringify(unpin),
encryptKey: this.ctx.publicKey,
kind: NostrKind.Encrypted_Custom_App_Data,
});
if (event instanceof Error) {
return event;
}
this.pinList.set(pubkey, unpin);
const err = await this.saveToLocalStorage();
if (err instanceof Error) {
return err;
}
}
static async FromNostrEvent(
event: NostrEvent<NostrKind.Encrypted_Custom_App_Data>,
ctx: NostrAccountContext,
lamport: LamportTime,
) {
const decrypted = await ctx.decrypt(ctx.publicKey.hex, event.content, "nip4");
if (decrypted instanceof Error) {
return decrypted;
}
const pinListArray = parseJSON<string[]>(decrypted);
if (pinListArray instanceof Error) {
return pinListArray;
}
let pinList;
try {
pinList = new Set<string>(pinListArray);
} catch (e) {
console.error(pinListArray, e);
pinList = [];
}
const c = new OtherConfig(ctx, lamport);
for (const pin of pinList) {
const err = await c.addPin(pin);
if (err instanceof Error) {
return err;
}
}
return c;
}
private async toNostrEvent(ctx: NostrAccountContext) {
const event = await prepareEncryptedNostrEvent(ctx, {
encryptKey: ctx.publicKey,
content: JSON.stringify(Array.from(this.getPinList())),
kind: NostrKind.Encrypted_Custom_App_Data,
tags: [],
});
return event;
}
private async saveToLocalStorage() {
const event = await this.toNostrEvent(this.ctx);
if (event instanceof Error) {
return event;
}
localStorage.setItem(`${OtherConfig.name}:${this.ctx.publicKey.bech32()}`, JSON.stringify(event));
}
async addEvent(event: NostrEvent) {
if (event.kind != NostrKind.Encrypted_Custom_App_Data) {
return;
}
const decrypted = await this.ctx.decrypt(this.ctx.publicKey.hex, event.content, "nip44");
if (decrypted instanceof Error) {
return decrypted;
}
const pin = parseJSON<ConfigEvent>(decrypted);
if (pin instanceof Error) {
return pin;
}
if (pin.type == "PinConversation" || pin.type == "UnpinConversation") {
const currentEvent = this.pinList.get(pin.pubkey);
if (currentEvent && pin.lamport < currentEvent.lamport) {
return; // ignore because the current event is newer
}
this.lamport.set(pin.lamport);
this.pinList.set(pin.pubkey, pin);
const err = await this.saveToLocalStorage();
if (err instanceof Error) {
return err;
}
}
}
}
export type ConfigEvent = PinConversationRelay | UnpinConversationRelay;