Relay Config Persistance to Local Storage & Sync With the Nostr Network(#136)

This commit is contained in:
BlowaterNostr 2023-09-06 21:04:25 +00:00 committed by GitHub
parent 050a937cec
commit 3df997b679
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 134 additions and 45 deletions

View File

@ -158,7 +158,7 @@ export class App {
) {
this.eventSyncer = new EventSyncer(relayPool, this.database);
this.allUsersInfo = new AllUsersInformation(myAccountContext);
this.relayConfig = new RelayConfig();
this.relayConfig = RelayConfig.FromLocalStorage(myAccountContext);
if (this.relayConfig.getRelayURLs().size == 0) {
for (const url of defaultRelays) {
this.relayConfig.add(url);
@ -186,22 +186,42 @@ export class App {
///////////////////////////////////
// Add relays to Connection Pool //
///////////////////////////////////
const events = [];
const events_CustomAppData = [];
for (const e of this.database.events) {
if (e.kind == NostrKind.CustomAppData) {
events.push(e);
events_CustomAppData.push(e);
} else if (e.kind == NostrKind.Custom_App_Data) {
}
}
{
// relay config synchronization
for (const e of events) {
const _relayConfig = await RelayConfig.FromNostrEvent(e, this.myAccountContext);
if (_relayConfig instanceof Error) {
console.log(_relayConfig.message);
continue;
// relay config synchronization, need to refactor later
(async () => {
const stream = await pool.newSub("relay config", {
"#d": ["RelayConfig"],
authors: [accountContext.publicKey.hex],
kinds: [NostrKind.Custom_App_Data],
});
if (stream instanceof Error) {
throw stream; // impossible
}
this.relayConfig.merge(_relayConfig.save());
}
for await (const msg of stream.chan) {
if (msg.res.type == "EOSE") {
continue;
}
console.log(msg.res);
RelayConfig.FromNostrEvent(msg.res.event, accountContext);
const _relayConfig = await RelayConfig.FromNostrEvent(
msg.res.event,
this.myAccountContext,
);
if (_relayConfig instanceof Error) {
console.log(_relayConfig.message);
continue;
}
this.relayConfig.merge(_relayConfig.save());
this.relayConfig.saveToLocalStorage(accountContext);
}
})();
}
}
@ -364,6 +384,7 @@ export function AppComponent(props: {
relayConfig: app.relayConfig,
myAccountContext: myAccountCtx,
relayPool: props.pool,
emit: props.eventBus.emit,
})}
</div>
);

View File

@ -19,7 +19,7 @@ import { Model } from "./app_model.ts";
import { SearchUpdate, SelectProfile } from "./search_model.ts";
import { fromEvents, LamportTime } from "../time.ts";
import { PublicKey } from "../lib/nostr-ts/key.ts";
import { NostrAccountContext, NostrKind, prepareCustomAppDataEvent } from "../lib/nostr-ts/nostr.ts";
import { NostrAccountContext, NostrKind } from "../lib/nostr-ts/nostr.ts";
import { ConnectionPool } from "../lib/nostr-ts/relay.ts";
import { SignInEvent, signInWithExtension, signInWithPrivateKey } from "./signIn.tsx";
import {
@ -36,6 +36,9 @@ import { DexieDatabase } from "./dexie-db.ts";
import { getSocialPosts } from "../features/social.ts";
import { RelayConfig } from "./setting.ts";
import { SocialUpdates } from "./social.tsx";
import { RelayConfigChange } from "./setting.tsx";
import { prepareCustomAppDataEvent, prepareParameterizedEvent } from "../lib/nostr-ts/event.ts";
import { relays } from "../lib/nostr-ts/relay-list.test.ts";
export type UI_Interaction_Event =
| SearchUpdate
@ -48,7 +51,8 @@ export type UI_Interaction_Event =
| PinContact
| UnpinContact
| SignInEvent
| SocialUpdates;
| SocialUpdates
| RelayConfigChange;
type BackToContactList = {
type: "BackToContactList";
@ -375,6 +379,13 @@ export async function* UI_Interaction_Update(args: {
model.social.filter.adding_author = event.value;
} else if (event.type == "SocialFilterChanged_remove_author") {
model.social.filter.author.delete(event.value);
} else if (event.type == "RelayConfigChange") {
const e = await model.app.relayConfig.toNostrEvent(model.app.myAccountContext, true);
if (e instanceof Error) {
throw e; // impossible
}
pool.sendEvent(e);
model.app.relayConfig.saveToLocalStorage(model.app.myAccountContext);
}
yield model;
}
@ -581,7 +592,7 @@ export async function* Database_Update(
///////////
export async function* Relay_Update(relayPool: ConnectionPool, relayConfig: RelayConfig) {
for (;;) {
await csp.sleep(1000 * 2.5); // every 2.5 sec
await csp.sleep(1000 * 10); // every 2.5 sec
console.log(`Relay: checking connections`);
// first, remove closed relays
const relays = relayPool.getRelays();

View File

@ -5,7 +5,7 @@ import { defaultRelays, RelayConfig } from "./setting.ts";
import { assertEquals, assertNotInstanceOf, fail } from "https://deno.land/std@0.176.0/testing/asserts.ts";
Deno.test("Relay Config", async () => {
const relayConfig = new RelayConfig();
const relayConfig = RelayConfig.Empty();
{
const urls = relayConfig.getRelayURLs();
assertEquals(urls.size, 0);
@ -18,7 +18,7 @@ Deno.test("Relay Config", async () => {
assertEquals(relayConfig.getRelayURLs(), new Set(["wss://nos.lol"]));
}
const relayConfig2 = new RelayConfig();
const relayConfig2 = RelayConfig.Empty();
{
const urls = relayConfig2.getRelayURLs();
assertEquals(urls.size, 0);
@ -93,3 +93,18 @@ Deno.test("Relay Config", async () => {
await pool.close();
}
});
Deno.test("RelayConfig: Nostr Encoding Decoding", async () => {
const config = RelayConfig.Empty();
config.add("something");
const ctx = InMemoryAccountContext.New(PrivateKey.Generate());
const event = await config.toNostrEvent(ctx, true);
if (event instanceof Error) fail(event.message);
const config2 = await RelayConfig.FromNostrEvent(event, ctx);
if (config2 instanceof Error) fail(config2.message);
console.log(config.getRelayURLs(), config2.getRelayURLs());
assertEquals(config.getRelayURLs(), config2.getRelayURLs());
});

View File

@ -5,13 +5,16 @@ import { ConnectionPool } from "../lib/nostr-ts/relay.ts";
import { defaultRelays, RelayConfig } from "./setting.ts";
import { InMemoryAccountContext } from "../lib/nostr-ts/nostr.ts";
import { PrivateKey } from "../lib/nostr-ts/key.ts";
import { EventBus } from "../event-bus.ts";
const pool = new ConnectionPool();
const ctx = InMemoryAccountContext.New(PrivateKey.Generate());
const relayConfig = new RelayConfig();
const relayConfig = RelayConfig.Empty();
for (const url of defaultRelays) {
relayConfig.add(url);
}
const emitter = new EventBus();
render(
Setting({
relayConfig: relayConfig,
@ -20,6 +23,7 @@ render(
logout: () => {
console.log("logout is clicked");
},
emit: emitter.emit,
}),
document.body,
);

View File

@ -1,12 +1,8 @@
import * as Automerge from "https://deno.land/x/automerge@2.1.0-alpha.12/index.ts";
import {
NostrAccountContext,
NostrEvent,
NostrKind,
prepareCustomAppDataEvent,
} from "../lib/nostr-ts/nostr.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 { prepareCustomAppDataEvent, prepareParameterizedEvent } from "../lib/nostr-ts/event.ts";
export const defaultRelays = [
"wss://nos.lol",
@ -22,12 +18,36 @@ 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 async FromNostrEvent(event: NostrEvent<NostrKind.CustomAppData>, ctx: NostrAccountContext) {
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));
@ -36,10 +56,18 @@ export class RelayConfig {
async toNostrEvent(ctx: NostrAccountContext, needEncryption: boolean) {
if (needEncryption) {
const hex = secp256k1.utils.bytesToHex(this.save());
const event = await prepareCustomAppDataEvent(ctx, {
type: "relayConfig",
data: hex,
const configJSON = JSON.stringify({
type: RelayConfig.name,
data: secp256k1.utils.bytesToHex(this.save()),
});
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;
}
@ -54,6 +82,14 @@ export class RelayConfig {
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);

View File

@ -27,12 +27,14 @@ import { RelayIcon } from "./icons2/relay-icon.tsx";
import { DeleteIcon } from "./icons2/delete-icon.tsx";
import { RelayConfig } from "./setting.ts";
import { ConnectionPool } from "../lib/nostr-ts/relay.ts";
import { emit, EventEmitter } from "../event-bus.ts";
export interface SettingProps {
logout: () => void;
relayConfig: RelayConfig;
relayPool: ConnectionPool;
myAccountContext: NostrAccountContext;
emit: emit<RelayConfigChange>;
}
const colors = {
@ -76,9 +78,13 @@ export const Setting = (props: SettingProps) => {
const error = signal("");
const addRelayInput = signal("");
const relayStatus = signal<{ url: string; status: keyof typeof colors }[]>([]);
export type RelayConfigChange = {
type: "RelayConfigChange";
};
export function RelaySetting(props: {
relayConfig: RelayConfig;
relayPool: ConnectionPool;
emit: emit<RelayConfigChange>;
}) {
function computeRelayStatus() {
const _relayStatus: { url: string; status: keyof typeof colors }[] = [];
@ -119,6 +125,7 @@ export function RelaySetting(props: {
error.value = err.map((e) => e.message).join("\n");
}
relayStatus.value = computeRelayStatus();
props.emit({ type: "RelayConfigChange" });
}
};
return (
@ -178,7 +185,7 @@ export function RelaySetting(props: {
<button
class={tw`w-[2rem] h-[2rem] rounded-lg bg-transparent hover:bg-[${DividerBackgroundColor}] ${CenterClass} ${NoOutlineClass}`}
onClick={async () => {
onClick={async function remove() {
props.relayConfig.remove(r.url);
relayStatus.value = computeRelayStatus();
const err = await props.relayConfig.syncWithPool(props.relayPool);
@ -186,7 +193,7 @@ export function RelaySetting(props: {
error.value = err.map((e) => e.message).join("\n");
}
relayStatus.value = computeRelayStatus();
console.log(relayStatus.value);
props.emit({ type: "RelayConfigChange" });
}}
>
<DeleteIcon

View File

@ -4,9 +4,9 @@ export class EventBus<T> implements EventEmitter<T> {
private readonly c = chan<T>();
private readonly caster = multi<T>(this.c);
async emit(event: T) {
emit = async (event: T) => {
await this.c.put(event);
}
};
onChange() {
return this.caster.copy();
@ -16,3 +16,4 @@ export class EventBus<T> implements EventEmitter<T> {
export type EventEmitter<T> = {
emit: (event: T) => void;
};
export type emit<T extends { type: string }> = (event: T) => void;

View File

@ -6,13 +6,12 @@ import {
NostrAccountContext,
NostrEvent,
NostrKind,
prepareEncryptedNostrEvent,
prepareNormalNostrEvent,
RelayResponse_Event,
} from "../lib/nostr-ts/nostr.ts";
import { ConnectionPool } from "../lib/nostr-ts/relay.ts";
import { prepareNostrImageEvents, Tag } from "../nostr.ts";
import { PublicKey } from "../lib/nostr-ts/key.ts";
import { prepareEncryptedNostrEvent, prepareNormalNostrEvent } from "../lib/nostr-ts/event.ts";
export async function sendDMandImages(args: {
sender: NostrAccountContext;

View File

@ -1,8 +1,9 @@
import { Database_Contextual_View } from "../database.ts";
import { ConnectionPool } from "../lib/nostr-ts/relay.ts";
import { PublicKey } from "../lib/nostr-ts/key.ts";
import { groupBy, NostrAccountContext, NostrKind, prepareNormalNostrEvent } from "../lib/nostr-ts/nostr.ts";
import { groupBy, NostrAccountContext, NostrKind } from "../lib/nostr-ts/nostr.ts";
import { Parsed_Event, Profile_Nostr_Event } from "../nostr.ts";
import { prepareNormalNostrEvent } from "../lib/nostr-ts/event.ts";
export class ProfilesSyncer {
readonly userSet = new Set<string>();

@ -1 +1 @@
Subproject commit f7cdd6f4352193d873371d79e74d720c811a1703
Subproject commit c8ff9a73a718c5fd6cebeaeb8df154e34b0f0e81

View File

@ -10,7 +10,6 @@ import {
InMemoryAccountContext,
NostrEvent,
NostrKind,
prepareNormalNostrEvent,
} from "./lib/nostr-ts/nostr.ts";
import {
computeThreads,
@ -25,6 +24,7 @@ import {
import { LamportTime } from "./time.ts";
import { PrivateKey, PublicKey } from "./lib/nostr-ts/key.ts";
import { utf8Decode } from "./lib/nostr-ts/ende.ts";
import { prepareNormalNostrEvent } from "./lib/nostr-ts/event.ts";
Deno.test("prepareNostrImageEvents", async (t) => {
const pri = PrivateKey.Generate();
@ -145,7 +145,6 @@ Deno.test("groupImageEvents", async () => {
Deno.test("Generate reply event", async () => {
const userAPrivateKey = PrivateKey.Generate();
const userBPrivateKey = PrivateKey.Generate();
const userAContext = InMemoryAccountContext.New(userAPrivateKey);
const message1 = await prepareNormalNostrEvent(

View File

@ -3,15 +3,10 @@
*/
import { PrivateKey, PublicKey } from "./lib/nostr-ts/key.ts";
import * as nostr from "./lib/nostr-ts/nostr.ts";
import {
groupBy,
NostrKind,
prepareEncryptedNostrEvent,
prepareNormalNostrEvent,
TagPubKey,
} from "./lib/nostr-ts/nostr.ts";
import { groupBy, NostrKind, TagPubKey } from "./lib/nostr-ts/nostr.ts";
import { ProfileData } from "./features/profile.ts";
import { ContentItem } from "./UI/message.ts";
import { prepareEncryptedNostrEvent, prepareNormalNostrEvent } from "./lib/nostr-ts/event.ts";
type TotolChunks = string;
type ChunkIndex = string; // 0-indexed