mirror of
https://github.com/BlowaterNostr/blowater.git
synced 2024-10-18 07:33:22 +00:00
Relay Config Persistance to Local Storage & Sync With the Nostr Network(#136)
This commit is contained in:
parent
050a937cec
commit
3df997b679
33
UI/app.tsx
33
UI/app.tsx
@ -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);
|
||||
// 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
|
||||
}
|
||||
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>
|
||||
);
|
||||
|
@ -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();
|
||||
|
@ -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());
|
||||
});
|
||||
|
@ -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,
|
||||
);
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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
|
@ -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(
|
||||
|
9
nostr.ts
9
nostr.ts
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user