blowater/UI/relay-config.ts
BlowaterNostr 1cf773d27c
Bring Back Pin Conversation List(#205)
without CRDT yet
2023-10-01 22:21:55 +00:00

139 lines
4.3 KiB
TypeScript

import * as Automerge from "https://deno.land/x/automerge@2.1.0-alpha.12/index.ts";
import { NostrAccountContext, NostrEvent, NostrKind } from "../lib/nostr-ts/nostr.ts";
import * as secp256k1 from "../lib/nostr-ts/vendor/secp256k1.js";
import { ConnectionPool, RelayAlreadyRegistered } from "../lib/nostr-ts/relay.ts";
import { prepareParameterizedEvent } from "../lib/nostr-ts/event.ts";
export const defaultRelays = [
"wss://nos.lol",
"wss://relay.damus.io",
"wss://relay.nostr.wirednet.jp",
];
type Config = {
[key: string]: boolean;
};
export class RelayConfig {
// This is a state based CRDT based on Vector Clock
// see https://www.youtube.com/watch?v=OOlnp2bZVRs
private config: Automerge.next.Doc<Config> = Automerge.init();
private constructor() {}
static Empty() {
return new RelayConfig();
}
// The the relay config of this account from local storage
static FromLocalStorage(ctx: NostrAccountContext) {
const encodedConfigStr = localStorage.getItem(this.localStorageKey(ctx));
if (encodedConfigStr == null) {
return RelayConfig.Empty();
}
const config = Automerge.load<Config>(secp256k1.utils.hexToBytes(encodedConfigStr));
const relayConfig = new RelayConfig();
relayConfig.config = config;
return relayConfig;
}
static localStorageKey(ctx: NostrAccountContext) {
return `${RelayConfig.name}-${ctx.publicKey.bech32()}`;
}
/////////////////////////////
// Nostr Encoding Decoding //
/////////////////////////////
static async FromNostrEvent(event: NostrEvent, ctx: NostrAccountContext) {
const decrypted = await ctx.decrypt(ctx.publicKey.hex, event.content);
if (decrypted instanceof Error) {
return decrypted;
}
const json = JSON.parse(decrypted);
const relayConfig = new RelayConfig();
relayConfig.merge(secp256k1.utils.hexToBytes(json.data));
return relayConfig;
}
async toNostrEvent(ctx: NostrAccountContext) {
const configJSON = JSON.stringify({
type: RelayConfig.name,
data: this.saveAsHex(),
});
const encrypted = await ctx.encrypt(ctx.publicKey.hex, configJSON);
if (encrypted instanceof Error) {
return encrypted;
}
const event = await prepareParameterizedEvent(ctx, {
content: encrypted,
d: RelayConfig.name,
kind: NostrKind.Custom_App_Data,
});
return event;
}
getRelayURLs() {
return new Set(Object.keys(this.config));
}
save() {
return Automerge.save(this.config);
}
saveAsHex() {
const bytes = this.save();
return secp256k1.utils.bytesToHex(bytes);
}
saveToLocalStorage(ctx: NostrAccountContext) {
const hex = this.saveAsHex();
localStorage.setItem(RelayConfig.localStorageKey(ctx), hex);
}
merge(bytes: Uint8Array) {
const otherDoc = Automerge.load<Config>(bytes);
this.config = Automerge.merge(this.config, otherDoc);
}
async add(url: string) {
if (this.config[url] != undefined) {
return;
}
if (!url.startsWith("wss://") && !url.startsWith("ws://")) {
url = "wss://" + url;
}
this.config = Automerge.change(this.config, "add", (config) => {
config[url] = true;
});
}
async remove(url: string) {
this.config = Automerge.change(this.config, "remove", (config) => {
delete config[url];
});
}
async syncWithPool(pool: ConnectionPool) {
const errors = [];
for (const url of Object.keys(this.config)) {
const err = await pool.addRelayURL(url);
if (err instanceof RelayAlreadyRegistered) {
continue;
} else if (err instanceof Error) {
errors.push(err);
}
}
for (const r of pool.getRelays()) {
if (this.config[r.url] == undefined) {
await pool.removeRelay(r.url);
}
}
if (errors.length > 0) {
return errors;
}
}
}
export function applyPoolToRelayConfig(pool: ConnectionPool, relayConfig: RelayConfig) {
for (const relay of pool.getRelays()) {
relayConfig.add(relay.url);
}
}