From 348d4a680e999d330d025e9b808174f65c7587e3 Mon Sep 17 00:00:00 2001 From: BlowaterNostr <127284497+BlowaterNostr@users.noreply.github.com> Date: Sun, 14 Apr 2024 14:20:38 +0800 Subject: [PATCH] Replaceable event (#8) --- main.tsx | 33 ++++++++------------ makefile | 1 - resolvers/event.ts | 64 +++++++++++++++++++++++++++++++------ test.ts | 22 +++++++++++-- ws.ts | 78 ++++++++++++++++++++++++++++++---------------- 5 files changed, 139 insertions(+), 59 deletions(-) diff --git a/main.tsx b/main.tsx index abb2b85..5b00ba9 100644 --- a/main.tsx +++ b/main.tsx @@ -5,10 +5,14 @@ import { RootResolver } from "./resolvers/root.ts"; import * as gql from "https://esm.sh/graphql@16.8.1"; import { Policy } from "./resolvers/policy.ts"; import { func_ResolvePolicyByKind } from "./resolvers/policy.ts"; -import { NostrEvent, NostrKind, parseJSON, PublicKey, verifyEvent } from "./_libs.ts"; +import { NostrKind, PublicKey } from "./_libs.ts"; import { PolicyStore } from "./resolvers/policy.ts"; import { Policies } from "./resolvers/policy.ts"; -import { interface_GetEventsByAuthors } from "./resolvers/event.ts"; +import { + func_GetReplaceableEvents, + func_WriteReplaceableEvent, + interface_GetEventsByAuthors, +} from "./resolvers/event.ts"; import Landing from "./routes/landing.tsx"; import Error404 from "./routes/_404.tsx"; import { RelayInformation, RelayInformationStore } from "./resolvers/nip11.ts"; @@ -18,7 +22,7 @@ import { func_GetEventsByIDs, func_GetEventsByKinds, func_MarkEventDeleted, - func_WriteEvent, + func_WriteRegularEvent, } from "./resolvers/event.ts"; const schema = gql.buildSchema(gql.print(typeDefs)); @@ -94,12 +98,14 @@ export async function run(args: { password, connections, resolvePolicyByKind: policyStore.resolvePolicyByKind, - write_event: eventStore.write_event.bind(eventStore), get_events_by_IDs: eventStore.get_events_by_IDs.bind(eventStore), get_events_by_kinds: eventStore.get_events_by_kinds.bind(eventStore), get_events_by_authors: eventStore.get_events_by_authors.bind(eventStore), get_events_by_filter: eventStore.get_events_by_filter.bind(eventStore), + get_replaceable_events: eventStore.get_replaceable_events.bind(eventStore), mark_event_deleted: eventStore.mark_event_deleted, + write_regular_event: eventStore.write_regular_event.bind(eventStore), + write_replaceable_event: eventStore.write_replaceable_event, policyStore, relayInformationStore, kv: args.kv, @@ -123,11 +129,13 @@ export async function run(args: { } export type EventReadWriter = { - write_event: func_WriteEvent; + write_regular_event: func_WriteRegularEvent; + write_replaceable_event: func_WriteReplaceableEvent; get_events_by_IDs: func_GetEventsByIDs; get_events_by_kinds: func_GetEventsByKinds; get_events_by_filter: func_GetEventsByFilter; mark_event_deleted: func_MarkEventDeleted; + get_replaceable_events: func_GetReplaceableEvents; } & interface_GetEventsByAuthors; const root_handler = ( @@ -177,31 +185,16 @@ async (req: Request) => { const { password, policyStore } = args; if (req.method == "POST") { const query = await req.json(); - const nip42 = req.headers.get("nip42"); - console.log("nip42 header", nip42); - const pw = req.headers.get("password"); if (pw != password) { return new Response(`{"errors":"incorrect password"}`); } - - if (nip42) { - const auth_event = parseJSON(nip42); - if (auth_event instanceof Error) { - return new Response(`{errors:["no auth"]}`); - } - const ok = await verifyEvent(auth_event); - if (!ok) { - return new Response(`{"errors":["no auth"]}`); - } - } const result = await gql.graphql({ schema: schema, source: query.query, variableValues: query.variables, rootValue: RootResolver(args), }); - console.log(result); return new Response(JSON.stringify(result)); } else if (req.method == "GET") { const res = new Response(graphiql); diff --git a/makefile b/makefile index 60baee5..2c11bb7 100644 --- a/makefile +++ b/makefile @@ -6,5 +6,4 @@ fmt: test: fmt deno test --allow-net --unstable --allow-read --allow-write \ - --filter main \ --coverage test.ts diff --git a/resolvers/event.ts b/resolvers/event.ts index 301793a..b8d58ab 100644 --- a/resolvers/event.ts +++ b/resolvers/event.ts @@ -38,10 +38,13 @@ export type interface_GetEventsByAuthors = { export type func_GetEventsByFilter = (filter: NostrFilter) => AsyncIterable; -export type func_WriteEvent = (event: NostrEvent) => Promise; -export type interface_WriteEvent = { - write_event: func_WriteEvent; -}; +export type func_WriteRegularEvent = (event: NostrEvent) => Promise; + +export type func_WriteReplaceableEvent = (event: NostrEvent) => Promise; +export type func_GetReplaceableEvents = (args: { + kinds: NostrKind[]; + authors: string[]; +}) => AsyncIterable; export type func_MarkEventDeleted = (event: NostrEvent | NoteID) => Promise; @@ -97,7 +100,28 @@ export class EventStore implements EventReadWriter { } } - async write_event(event: NostrEvent) { + async *get_replaceable_events(args: { + kinds: NostrKind[]; + authors: string[]; + }) { + const keys: Deno.KvKey[] = []; + for (const kind of args.kinds) { + for (const author of args.authors) { + keys.push(["event", kind, author]); + } + } + const results = await this.kv.getMany(keys); + for (const result of results) { + if (result.value) { + yield result.value; + } + } + } + + write_regular_event = async (event: NostrEvent) => { + if (isReplaceableEvent(event.kind)) { + return false; + } console.log("write_event", event); const result = await this.kv.atomic() .set(["event", event.id], event) @@ -110,14 +134,32 @@ export class EventStore implements EventReadWriter { } return result.ok; - } + }; + + write_replaceable_event = async (event: NostrEvent) => { + const kind = event.kind; + if (!isReplaceableEvent(kind)) { + return false; + } + console.log("write_replaceable_event", event); + const result = await this.kv.atomic() + .set(["event", event.kind, event.pubkey], event) + .set(["event", event.pubkey, event.kind], event) + .commit(); + + if (result.ok) { + this.events.set(event.id, event); + } + + return result.ok; + }; mark_event_deleted = async (event_or_id: NostrEvent | NoteID) => { let id: string; - if(event_or_id instanceof NoteID) { - id = event_or_id.hex + if (event_or_id instanceof NoteID) { + id = event_or_id.hex; } else { - id = event_or_id.id + id = event_or_id.id; } const result = await this.kv.set(["event", "deleted", id], id); if (result.ok) { @@ -127,6 +169,10 @@ export class EventStore implements EventReadWriter { }; } +export function isReplaceableEvent(kind: NostrKind) { + return kind == NostrKind.META_DATA || kind == NostrKind.CONTACTS || (10000 <= kind && kind < 20000); +} + function isMatched(event: NostrEvent, filter: NostrFilter) { const kinds = filter.kinds || []; const authors = filter.authors || []; diff --git a/test.ts b/test.ts index c0a70c2..b9ccb57 100644 --- a/test.ts +++ b/test.ts @@ -152,6 +152,24 @@ Deno.test("main", async (t) => { await relay.shutdown(); }); +// https://github.com/nostr-protocol/nips/blob/master/01.md#kinds +Deno.test("replaceable events", async (t) => { + const relay = await run({ + password: "123", + port: 8080, + default_policy: { + allowed_kinds: "all", + }, + kv: await test_kv(), + }) as Relay; + const client = SingleRelayConnection.New(relay.url, { log: false }); + await t.step("get_replaceable_event", async () => { + await client_test.get_replaceable_event(relay.url)(); + }); + await client.close(); + await relay.shutdown(); +}); + // https://github.com/nostr-protocol/nips/blob/master/09.md Deno.test("NIP-9: Deletion", async () => { const relay = await run({ @@ -164,7 +182,7 @@ Deno.test("NIP-9: Deletion", async () => { }) as Relay; const client = SingleRelayConnection.New(relay.url, { log: false }); { - await client_nip9_test.send_deletion_event(relay.url)(); + await client_nip9_test.store_deletion_event(relay.url)(); } await client.close(); await relay.shutdown(); @@ -186,8 +204,6 @@ Deno.test("NIP-11: Relay Information Document", async (t) => { await t.step("get relay information", async () => { const information = await relay.get_relay_information(); - console.log(`information`, information); - assertEquals(information, { name: "Nostr Relay", ...not_modifiable_information }); }); diff --git a/ws.ts b/ws.ts index 12aadb1..fcb7eab 100644 --- a/ws.ts +++ b/ws.ts @@ -2,12 +2,10 @@ import { func_ResolvePolicyByKind } from "./resolvers/policy.ts"; import { DefaultPolicy, EventReadWriter } from "./main.tsx"; import { - func_GetEventsByAuthors, - func_GetEventsByFilter, - func_GetEventsByIDs, - func_GetEventsByKinds, func_MarkEventDeleted, - func_WriteEvent, + func_WriteRegularEvent, + func_WriteReplaceableEvent, + isReplaceableEvent, } from "./resolvers/event.ts"; import { _RelayResponse_EOSE, @@ -114,17 +112,20 @@ async function handle_cmd_event(args: { connections: Map; nostr_ws_msg: ClientRequest_Event; resolvePolicyByKind: func_ResolvePolicyByKind; - write_event: func_WriteEvent; + write_regular_event: func_WriteRegularEvent; + write_replaceable_event: func_WriteReplaceableEvent; mark_event_deleted: func_MarkEventDeleted; }) { - const { this_socket, connections, nostr_ws_msg, resolvePolicyByKind, write_event } = args; + const { this_socket, connections, nostr_ws_msg, resolvePolicyByKind } = args; const event = nostr_ws_msg[1]; - const ok = await verifyEvent(event); - if (!ok) { - return send( - this_socket, - JSON.stringify(respond_ok(event, false, "invalid event")), - ); + { + const ok = await verifyEvent(event); + if (!ok) { + return send( + this_socket, + JSON.stringify(respond_ok(event, false, "invalid event")), + ); + } } const policy = await resolvePolicyByKind(event.kind); @@ -161,14 +162,24 @@ async function handle_cmd_event(args: { if (event.kind == NostrKind.DELETE) { for (const e of getTags(event).e) { - const ok = await args.mark_event_deleted(NoteID.FromHex(e)) - if(!ok) { - console.error("failed to delete", e) + const ok = await args.mark_event_deleted(NoteID.FromHex(e)); + if (!ok) { + console.error("failed to delete", e); } } } - const _ok = await write_event(event); - if (_ok) { + + let ok: boolean; + if ( + event.kind == NostrKind.META_DATA || event.kind == NostrKind.CONTACTS || + (10000 <= event.kind && event.kind < 20000) + ) { + ok = await args.write_replaceable_event(event); + } else { + ok = await args.write_regular_event(event); + } + + if (ok) { send(this_socket, JSON.stringify(respond_ok(event, true, ""))); } else { send(this_socket, JSON.stringify(respond_ok(event, false, ""))); @@ -220,16 +231,31 @@ async function handle_cmd_req( return send(this_socket, JSON.stringify(respond_eose(sub_id))); } -async function handle_filter(args: { - filter: NostrFilter; - get_events_by_IDs: func_GetEventsByIDs; - get_events_by_kinds: func_GetEventsByKinds; - get_events_by_authors: func_GetEventsByAuthors; - get_events_by_filter: func_GetEventsByFilter; - resolvePolicyByKind: func_ResolvePolicyByKind; -}) { +async function handle_filter( + args: { + filter: NostrFilter; + resolvePolicyByKind: func_ResolvePolicyByKind; + } & EventReadWriter, +) { const event_candidates = new Map(); const { filter, get_events_by_IDs, resolvePolicyByKind, get_events_by_kinds } = args; + + if (filter.kinds) { + const replaceable_kinds: NostrKind[] = []; + for (const kind of filter.kinds) { + if (isReplaceableEvent(kind)) { + replaceable_kinds.push(kind); + } + } + const events = args.get_replaceable_events({ + authors: filter.authors || [], + kinds: replaceable_kinds, + }); + for await (const event of events) { + event_candidates.set(event.id, event); + } + } + if (filter.ids) { const events = get_events_by_IDs(new Set(filter.ids)); for await (const event of events) {