diff --git a/events.ts b/events.ts index ee90668..3dd4626 100644 --- a/events.ts +++ b/events.ts @@ -7,6 +7,7 @@ type Event_Base = { export enum Kind_V2 { ChannelCreation = "ChannelCreation", ChannelEdition = "ChannelEdition", + RelayMember = "RelayMember", } export type ChannelCreation = Event_Base & { @@ -23,4 +24,10 @@ export type ChannelEdition = Event_Base & { name: string; }; +export type EventRelayMembers = Event_Base & { + kind: Kind_V2.RelayMember; + created_at: number; + members: string[]; // the pubkey of members +}; + export type Event_V2 = ChannelCreation | ChannelEdition; diff --git a/graphql-schema.ts b/graphql-schema.ts index 8e71044..49f0e9b 100644 --- a/graphql-schema.ts +++ b/graphql-schema.ts @@ -13,6 +13,7 @@ export const typeDefs = gql` relayInformation: RelayInformation channel(name: String!): Channel deleted_events: [String!]! + members: [String!]! } type Mutation { diff --git a/main.tsx b/main.tsx index 901f4da..60c679e 100644 --- a/main.tsx +++ b/main.tsx @@ -3,13 +3,21 @@ import { SubscriptionMap, ws_handler } from "./ws.ts"; import { render } from "https://esm.sh/preact-render-to-string@6.4.1"; 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_GetRelayMembers, Policy } from "./resolvers/policy.ts"; import { func_ResolvePolicyByKind } from "./resolvers/policy.ts"; -import { NostrEvent, NostrKind, parseJSON, PublicKey, verify_event_v2, verifyEvent } from "./_libs.ts"; +import { + NostrEvent, + NostrKind, + parseJSON, + PrivateKey, + PublicKey, + verify_event_v2, + verifyEvent, +} from "./_libs.ts"; import { PolicyStore } from "./resolvers/policy.ts"; import { Policies } from "./resolvers/policy.ts"; import { - event_v1_schema_sqlite, + event_schema_sqlite, func_DeleteEventsFromPubkey, func_GetDeletedEventIDs, func_GetEventCount, @@ -44,6 +52,7 @@ import { } from "./channel.ts"; import { func_GetChannelByID } from "./channel.ts"; import { DB } from "https://deno.land/x/sqlite@v3.8/mod.ts"; +import { get_relay_members } from "./resolvers/policy.ts"; const schema = gql.buildSchema(gql.print(typeDefs)); @@ -86,24 +95,30 @@ export async function run(args: { admin?: PublicKey; default_policy: DefaultPolicy; default_information?: RelayInformationStringify; + system_key: string | PrivateKey; kv?: Deno.Kv; _debug?: boolean; }): Promise { const isDenoDeploy = Deno.env.get("DENO_DEPLOYMENT_ID") !== undefined; + + //------------------ // argument checking + // Deno KV let kv = args.kv; if (kv == undefined) { kv = await Deno.openKv(); } + // SQLite Database let db: DB | undefined; let get_channel_by_id: func_GetChannelByID; if (!isDenoDeploy) { db = new DB("relayed.db"); - db.execute(`${sqlite_schema}${event_v1_schema_sqlite}`); + db.execute(`${sqlite_schema}${event_schema_sqlite}`); get_channel_by_id = get_channel_by_id_sqlite(db); } + // Administrator Keys let admin_pubkey: string | undefined | PublicKey | Error = args.default_information?.pubkey; if (admin_pubkey == undefined) { const env_pubkey = Deno.env.get(ENV_relayed_pubkey); @@ -120,6 +135,17 @@ export async function run(args: { return admin_pubkey; } + // Relay Key + let system_key: string | PrivateKey | Error = args.system_key; + if (typeof system_key == "string") { + system_key = PrivateKey.FromString(system_key); + if (system_key instanceof Error) { + return system_key; + } + } + // argument checking done + //----------------------- + const { default_policy } = args; /////////////// const connections = new Map(); @@ -129,7 +155,13 @@ export async function run(args: { }); const get_all_policies = Policies(kv); - const policyStore = new PolicyStore(default_policy, kv, await get_all_policies()); + const policyStore = new PolicyStore({ + default_policy, + kv, + system_account: system_key, + initial_policies: await get_all_policies(), + db, + }); const relayInformationStore = new RelayInformationStore( kv, { @@ -168,6 +200,7 @@ export async function run(args: { write_replaceable_event: eventStore.write_replaceable_event, policyStore, relayInformationStore, + get_relay_members: get_relay_members(db), kv, db: db, // get_channel_by_name: get_channel_by_name(db) @@ -224,6 +257,7 @@ const root_handler = ( policyStore: PolicyStore; relayInformationStore: RelayInformationStore; // get_channel_by_name: func_GetChannelByName; + get_relay_members: func_GetRelayMembers; kv: Deno.Kv; db?: DB; _debug: boolean; @@ -323,6 +357,7 @@ const graphql_handler = ( delete_event: func_DeleteEvent; delete_events_from_pubkey: func_DeleteEventsFromPubkey; get_deleted_event_ids: func_GetDeletedEventIDs; + get_relay_members: func_GetRelayMembers; }, ) => async (req: Request) => { diff --git a/resolvers/event.ts b/resolvers/event.ts index bfec485..a724e14 100644 --- a/resolvers/event.ts +++ b/resolvers/event.ts @@ -281,7 +281,7 @@ function isMatched(event: NostrEvent, filter: NostrFilter) { ps.length == 0 && es.length == 0); } -export const event_v1_schema_sqlite = ` +export const event_schema_sqlite = ` CREATE TABLE IF NOT exists events_v1 ( id TEXT PRIMARY KEY, pubkey TEXT NOT NULL, @@ -290,6 +290,13 @@ CREATE TABLE IF NOT exists events_v1 ( created_at INTEGER NOT NULL, event JSON NOT NULL ); + +CREATE TABLE IF NOT exists events_v2 ( + id TEXT PRIMARY KEY, + pubkey TEXT NOT NULL, + kind TEXT NOT NULL, + event JSON NOT NULL +); `; // export const get_events_with_filter = (db: DB) => (filter: NostrFilter) => { diff --git a/resolvers/policy.ts b/resolvers/policy.ts index 88a4dcb..9c69f3a 100644 --- a/resolvers/policy.ts +++ b/resolvers/policy.ts @@ -1,5 +1,7 @@ -import { NostrKind, PublicKey } from "../_libs.ts"; +import { DB } from "https://deno.land/x/sqlite@v3.8/mod.ts"; +import { NostrAccountContext, NostrKind, parseJSON, PrivateKey, PublicKey, sign_event_v2 } from "../_libs.ts"; import { DefaultPolicy } from "../main.tsx"; +import { Event_V2, EventRelayMembers, Kind_V2 } from "../events.ts"; export const Policies = (kv: Deno.Kv) => async function () { @@ -25,11 +27,15 @@ export class PolicyStore { policies = new Map(); constructor( - private default_policy: DefaultPolicy, - private kv: Deno.Kv, - initial_policies: Policy[], + private readonly args: { + default_policy: DefaultPolicy; + kv: Deno.Kv; + system_account: PrivateKey; + initial_policies: Policy[]; + db?: DB; + }, ) { - for (const policy of initial_policies) { + for (const policy of args.initial_policies) { this.policies.set(policy.kind, policy); } } @@ -37,7 +43,7 @@ export class PolicyStore { resolvePolicyByKind = async (kind: NostrKind): Promise => { const policy = this.policies.get(kind); if (policy == undefined) { - const default_policy = this.default_policy; + const default_policy = this.args.default_policy; let allow_this_kind: boolean; if (default_policy.allowed_kinds == "all") { allow_this_kind = true; @@ -90,7 +96,61 @@ export class PolicyStore { policy.block = blocks; } this.policies.set(args.kind, policy); - await this.kv.set(["policy", args.kind], policy); + await this.args.kv.set(["policy", args.kind], policy); + + { + // WIP + // generate new relay member list + if (this.args.db) { + if (args.kind == NostrKind.TEXT_NOTE) { + const event = await sign_event_v2(this.args.system_account, { + pubkey: this.args.system_account.toPublicKey().hex, + kind: Kind_V2.RelayMember, + members: Array.from(policy.allow), + created_at: Date.now(), + }); + // warn: could throw + try { + this.args.db.query( + `INSERT INTO events_v2 (id, pubkey, kind, event) VALUES (?, ?, ?, ?);`, + [event.id, event.pubkey, event.kind, JSON.stringify(event)], + ); + } catch (e) { + console.log(event); + console.error(e); + } + } + } else { + console.log("Feature Not Supported in this environment: Relay Members"); + } + } return policy; }; } + +export type func_GetRelayMembers = () => Promise; +export const get_relay_members = (db?: DB): func_GetRelayMembers => async () => { + if (!db) { + return new Error("get_relay_members is not supported"); + } + const rows = db.query<[string]>( + "select event from events_v2 where kind = (?)", + [Kind_V2.RelayMember], + ); + + const events = [] as EventRelayMembers[]; + for (const row of rows) { + const relay_member_event = parseJSON(row[0]); + if (relay_member_event instanceof Error) { + return relay_member_event; + } + events.push(relay_member_event); + } + if (events.length == 0) { + return; + } + + const event = events.sort((e1, e2) => e1.created_at - e2.created_at)[0]; + console.log(event); + return event; +}; diff --git a/resolvers/root.ts b/resolvers/root.ts index 5adaf45..6ae8ac9 100644 --- a/resolvers/root.ts +++ b/resolvers/root.ts @@ -1,4 +1,4 @@ -import { PolicyStore } from "./policy.ts"; +import { func_GetRelayMembers, PolicyStore } from "./policy.ts"; import { Policies } from "./policy.ts"; import { RelayInformationStore } from "./nip11.ts"; import { @@ -24,6 +24,7 @@ export function RootResolver({ deps }: { delete_event: func_DeleteEvent; delete_events_from_pubkey: func_DeleteEventsFromPubkey; get_deleted_event_ids: func_GetDeletedEventIDs; + get_relay_members: func_GetRelayMembers; }; }) { return { @@ -41,6 +42,7 @@ function Queries(deps: { get_events_by_kinds: func_GetEventsByKinds; // get_channel_by_name: func_GetChannelByName; get_deleted_event_ids: func_GetDeletedEventIDs; + get_relay_members: func_GetRelayMembers; }) { return { policies: Policies(deps.kv), @@ -70,6 +72,17 @@ function Queries(deps: { deleted_events: async () => { return deps.get_deleted_event_ids(); }, + members: async () => { + const members = await deps.get_relay_members(); + if (members instanceof Error) { + console.error(members); + throw members; + } + if (members == undefined) { + return []; + } + return members.members; + }, }; }