diff --git a/.gitignore b/.gitignore index 5e4f252..2a2c0ff 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ coverage test.sqlite *.db node_modules +.idea diff --git a/channel.ts b/channel.ts index 8fb0e5c..e55850a 100644 --- a/channel.ts +++ b/channel.ts @@ -1,4 +1,4 @@ -import { ChannelCreation, ChannelEdition, Kind_V2 } from "./events.ts"; +import { ChannelCreation, ChannelEdition, Kind_V2 } from "./nostr.ts/nostr.ts"; import { DB, SqliteError } from "https://deno.land/x/sqlite@v3.8/mod.ts"; export type func_GetChannelByName = ( diff --git a/deno.test.lock b/deno.test.lock index e0909fd..134230b 100644 --- a/deno.test.lock +++ b/deno.test.lock @@ -37,9 +37,58 @@ "https://deno.land/std@0.202.0/assert/unreachable.ts": "4600dc0baf7d9c15a7f7d234f00c23bca8f3eba8b140286aaca7aa998cf9a536", "https://deno.land/std@0.202.0/encoding/hex.ts": "b4e346619c0535a5db48d6bdb6e057c8d1179c64a2b0ba8d3b0b8a79ae361a90", "https://deno.land/std@0.202.0/fmt/colors.ts": "c51c4642678eb690dcf5ffee5918b675bf01a33fba82acf303701ae1a4f8c8d9", + "https://deno.land/std@0.220.1/assert/_constants.ts": "a271e8ef5a573f1df8e822a6eb9d09df064ad66a4390f21b3e31f820a38e0975", + "https://deno.land/std@0.220.1/assert/_diff.ts": "4bf42969aa8b1a33aaf23eb8e478b011bfaa31b82d85d2ff4b5c4662d8780d2b", + "https://deno.land/std@0.220.1/assert/_format.ts": "0ba808961bf678437fb486b56405b6fefad2cf87b5809667c781ddee8c32aff4", + "https://deno.land/std@0.220.1/assert/assert_equals.ts": "4497c56fe7d2993b0d447926702802fc0becb44e319079e8eca39b482ee01b4e", + "https://deno.land/std@0.220.1/assert/assertion_error.ts": "9f689a101ee586c4ce92f52fa7ddd362e86434ffdf1f848e45987dc7689976b8", + "https://deno.land/std@0.220.1/assert/equal.ts": "fae5e8a52a11d3ac694bbe1a53e13a7969e3f60791262312e91a3e741ae519e2", + "https://deno.land/std@0.220.1/fmt/colors.ts": "d239d84620b921ea520125d778947881f62c50e78deef2657073840b8af9559a", + "https://deno.land/std@0.224.0/assert/_constants.ts": "a271e8ef5a573f1df8e822a6eb9d09df064ad66a4390f21b3e31f820a38e0975", "https://deno.land/std@0.224.0/assert/assert.ts": "09d30564c09de846855b7b071e62b5974b001bb72a4b797958fe0660e7849834", + "https://deno.land/std@0.224.0/assert/assert_almost_equals.ts": "9e416114322012c9a21fa68e187637ce2d7df25bcbdbfd957cd639e65d3cf293", + "https://deno.land/std@0.224.0/assert/assert_array_includes.ts": "14c5094471bc8e4a7895fc6aa5a184300d8a1879606574cb1cd715ef36a4a3c7", + "https://deno.land/std@0.224.0/assert/assert_equals.ts": "3bbca947d85b9d374a108687b1a8ba3785a7850436b5a8930d81f34a32cb8c74", + "https://deno.land/std@0.224.0/assert/assert_exists.ts": "43420cf7f956748ae6ed1230646567b3593cb7a36c5a5327269279c870c5ddfd", + "https://deno.land/std@0.224.0/assert/assert_false.ts": "3e9be8e33275db00d952e9acb0cd29481a44fa0a4af6d37239ff58d79e8edeff", + "https://deno.land/std@0.224.0/assert/assert_greater.ts": "5e57b201fd51b64ced36c828e3dfd773412c1a6120c1a5a99066c9b261974e46", + "https://deno.land/std@0.224.0/assert/assert_greater_or_equal.ts": "9870030f997a08361b6f63400273c2fb1856f5db86c0c3852aab2a002e425c5b", + "https://deno.land/std@0.224.0/assert/assert_instance_of.ts": "e22343c1fdcacfaea8f37784ad782683ec1cf599ae9b1b618954e9c22f376f2c", + "https://deno.land/std@0.224.0/assert/assert_is_error.ts": "f856b3bc978a7aa6a601f3fec6603491ab6255118afa6baa84b04426dd3cc491", + "https://deno.land/std@0.224.0/assert/assert_less.ts": "60b61e13a1982865a72726a5fa86c24fad7eb27c3c08b13883fb68882b307f68", + "https://deno.land/std@0.224.0/assert/assert_less_or_equal.ts": "d2c84e17faba4afe085e6c9123a63395accf4f9e00150db899c46e67420e0ec3", + "https://deno.land/std@0.224.0/assert/assert_match.ts": "ace1710dd3b2811c391946954234b5da910c5665aed817943d086d4d4871a8b7", + "https://deno.land/std@0.224.0/assert/assert_not_equals.ts": "78d45dd46133d76ce624b2c6c09392f6110f0df9b73f911d20208a68dee2ef29", + "https://deno.land/std@0.224.0/assert/assert_not_instance_of.ts": "3434a669b4d20cdcc5359779301a0588f941ffdc2ad68803c31eabdb4890cf7a", + "https://deno.land/std@0.224.0/assert/assert_not_match.ts": "df30417240aa2d35b1ea44df7e541991348a063d9ee823430e0b58079a72242a", + "https://deno.land/std@0.224.0/assert/assert_not_strict_equals.ts": "37f73880bd672709373d6dc2c5f148691119bed161f3020fff3548a0496f71b8", + "https://deno.land/std@0.224.0/assert/assert_object_match.ts": "411450fd194fdaabc0089ae68f916b545a49d7b7e6d0026e84a54c9e7eed2693", + "https://deno.land/std@0.224.0/assert/assert_rejects.ts": "4bee1d6d565a5b623146a14668da8f9eb1f026a4f338bbf92b37e43e0aa53c31", + "https://deno.land/std@0.224.0/assert/assert_strict_equals.ts": "b4f45f0fd2e54d9029171876bd0b42dd9ed0efd8f853ab92a3f50127acfa54f5", + "https://deno.land/std@0.224.0/assert/assert_string_includes.ts": "496b9ecad84deab72c8718735373feb6cdaa071eb91a98206f6f3cb4285e71b8", + "https://deno.land/std@0.224.0/assert/assert_throws.ts": "c6508b2879d465898dab2798009299867e67c570d7d34c90a2d235e4553906eb", "https://deno.land/std@0.224.0/assert/assertion_error.ts": "ba8752bd27ebc51f723702fac2f54d3e94447598f54264a6653d6413738a8917", + "https://deno.land/std@0.224.0/assert/equal.ts": "bddf07bb5fc718e10bb72d5dc2c36c1ce5a8bdd3b647069b6319e07af181ac47", + "https://deno.land/std@0.224.0/assert/fail.ts": "0eba674ffb47dff083f02ced76d5130460bff1a9a68c6514ebe0cdea4abadb68", + "https://deno.land/std@0.224.0/assert/mod.ts": "48b8cb8a619ea0b7958ad7ee9376500fe902284bb36f0e32c598c3dc34cbd6f3", + "https://deno.land/std@0.224.0/assert/unimplemented.ts": "8c55a5793e9147b4f1ef68cd66496b7d5ba7a9e7ca30c6da070c1a58da723d73", + "https://deno.land/std@0.224.0/assert/unreachable.ts": "5ae3dbf63ef988615b93eb08d395dda771c96546565f9e521ed86f6510c29e19", + "https://deno.land/std@0.224.0/datetime/_date_time_formatter.ts": "b810c4c0d8f7aafe765fb963efdbf704acceb0730f5b242a7ec46df37834307c", + "https://deno.land/std@0.224.0/datetime/constants.ts": "5df80a84e301da6db5793804122c034d2d090da31f1e1c5fdaa831fc70a45da7", + "https://deno.land/std@0.224.0/datetime/day_of_year.ts": "5e513e239bd473e86d6a869d154720e9b6b8c86e1f14330ed5bb97575d9fa07b", + "https://deno.land/std@0.224.0/datetime/difference.ts": "8da08374910cae7d316cd7669912b70465f36d4e159e2c9f525a441f91fa9c7a", + "https://deno.land/std@0.224.0/datetime/format.ts": "8a550406530533c4aba101d471695f7f96c0eae61c1346748708eb333290d5f8", + "https://deno.land/std@0.224.0/datetime/is_leap.ts": "77f6eb9a4b7ea2a2dedbfecf75fd0a3f8f0e1610f708480e692f401f29e72650", + "https://deno.land/std@0.224.0/datetime/mod.ts": "6a49163dd0c7986658ed425b3555da5312252e5f5156bced6ae7199e651aa83b", + "https://deno.land/std@0.224.0/datetime/parse.ts": "d6dea8b3e394908dfd016bbe4545f260da4032493294aee1d10a3cf194c191cb", + "https://deno.land/std@0.224.0/datetime/week_of_year.ts": "ecaff056953753db3dabc9a769f80ff7ea7be79ae1e34edb5e2bda336f685757", + "https://deno.land/std@0.224.0/encoding/_util.ts": "beacef316c1255da9bc8e95afb1fa56ed69baef919c88dc06ae6cb7a6103d376", + "https://deno.land/std@0.224.0/encoding/hex.ts": "6270f25e5d85f99fcf315278670ba012b04b7c94b67715b53f30d03249687c07", + "https://deno.land/std@0.224.0/fmt/colors.ts": "508563c0659dd7198ba4bbf87e97f654af3c34eb56ba790260f252ad8012e1c5", "https://deno.land/std@0.224.0/http/cookie.ts": "a377fa60175ba5f61dd4b8a70b34f2bbfbc70782dfd5faf36d314c42e4306006", + "https://deno.land/std@0.224.0/internal/diff.ts": "6234a4b493ebe65dc67a18a0eb97ef683626a1166a1906232ce186ae9f65f4e6", + "https://deno.land/std@0.224.0/internal/format.ts": "0a98ee226fd3d43450245b1844b47003419d34d210fa989900861c79820d21c2", + "https://deno.land/std@0.224.0/internal/mod.ts": "534125398c8e7426183e12dc255bb635d94e06d0f93c60a297723abe69d3b22e", "https://deno.land/x/graphql_tag@0.1.2/deps.ts": "5696461c8bb42db7c83486db452e125f7cfdc62a2c628bb470a4447d934b90b3", "https://deno.land/x/graphql_tag@0.1.2/mod.ts": "57fd56de5f7cbc66e23ce896cc8e99521d286e89969d83e09d960642b0a9d652", "https://deno.land/x/sqlite@v3.8/build/sqlite.js": "72f63689fffcb9bb5ae10b1e8f7db09ea845cdf713e0e3a9693d8416a28f92a6", diff --git a/events.ts b/events.ts deleted file mode 100644 index 3dd4626..0000000 --- a/events.ts +++ /dev/null @@ -1,33 +0,0 @@ -type Event_Base = { - pubkey: string; - id: string; - sig: string; -}; - -export enum Kind_V2 { - ChannelCreation = "ChannelCreation", - ChannelEdition = "ChannelEdition", - RelayMember = "RelayMember", -} - -export type ChannelCreation = Event_Base & { - kind: Kind_V2.ChannelCreation; - name: string; - scope: "server"; -}; - -// EditChannel is a different type from CreateChannel because -// a channel only has one creator but could have multiple admin to modify it -export type ChannelEdition = Event_Base & { - kind: Kind_V2.ChannelEdition; - channel_id: string; - 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 49f0e9b..8e71044 100644 --- a/graphql-schema.ts +++ b/graphql-schema.ts @@ -13,7 +13,6 @@ export const typeDefs = gql` relayInformation: RelayInformation channel(name: String!): Channel deleted_events: [String!]! - members: [String!]! } type Mutation { diff --git a/main.ts b/main.ts index abff69c..d76b5dd 100644 --- a/main.ts +++ b/main.ts @@ -1,46 +1,52 @@ import { typeDefs } from "./graphql-schema.ts"; -import { func_IsMember, SubscriptionMap, ws_handler } from "./ws.ts"; +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 { func_GetRelayMembers, Policy } from "./resolvers/policy.ts"; -import { func_ResolvePolicyByKind } from "./resolvers/policy.ts"; -import { PolicyStore } from "./resolvers/policy.ts"; -import { Policies } from "./resolvers/policy.ts"; +import { + add_space_member, + func_AddSpaceMember, + func_GetSpaceMembers, + func_IsSpaceMember, + func_ResolvePolicyByKind, + get_space_members, + is_space_member, + Policies, + Policy, + PolicyStore, +} from "./resolvers/policy.ts"; import { event_schema_sqlite, func_GetEventCount, + func_GetEventsByFilter, + func_WriteRegularEvent, func_WriteReplaceableEvent, + get_event_count_sqlite, + get_events_by_filter_sqlite, write_regular_event_sqlite, write_replaceable_event_sqlite, } from "./resolvers/event.ts"; import Landing from "./routes/landing.tsx"; import Error404 from "./routes/_404.tsx"; import { RelayInfomationBase, RelayInformation, RelayInformationStore } from "./resolvers/nip11.ts"; -import { func_GetEventsByFilter, func_WriteRegularEvent } from "./resolvers/event.ts"; import { Cookie, getCookies, setCookie } from "https://deno.land/std@0.224.0/http/cookie.ts"; -import { Event_V2, Kind_V2 } from "./events.ts"; +import { Event_V2, Kind_V2, NostrEvent, NostrKind, verify_event_v2, verifyEvent } from "./nostr.ts/nostr.ts"; import { create_channel_sqlite, edit_channel_sqlite, func_CreateChannel, func_EditChannel, + func_GetChannelByID, get_channel_by_id_sqlite, sqlite_schema, } 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"; -import { get_events_by_filter_sqlite } from "./resolvers/event.ts"; -import { get_event_count_sqlite } from "./resolvers/event.ts"; import { + delete_event_sqlite, func_DeleteEvent, func_DeleteEventsFromPubkey, func_GetDeletedEventIDs, } from "./resolvers/event_deletion.ts"; -import { delete_event_sqlite } from "./resolvers/event_deletion.ts"; - -import { NostrEvent, NostrKind, verify_event_v2, verifyEvent } from "./nostr.ts/nostr.ts"; import { PublicKey } from "./nostr.ts/key.ts"; import { parseJSON } from "./nostr.ts/_helper.ts"; @@ -55,7 +61,6 @@ export type Relay = { ws_url: string; http_url: string; shutdown: () => Promise; - get_event: (id: string) => Promise; set_relay_information: (args: { name?: string; @@ -76,6 +81,10 @@ export type Relay = { }) => Promise; // channel get_channel_by_id: func_GetChannelByID; + // space member + get_space_members: func_GetSpaceMembers; + is_space_member: func_IsSpaceMember; + add_space_member: func_AddSpaceMember; [Symbol.asyncDispose]: () => Promise; }; @@ -83,7 +92,7 @@ export const ENV_relayed_pubkey = "relayed_pubkey"; export async function run(args: { port?: number; - admin?: PublicKey; + admin?: PublicKey | string; auth_required: boolean; default_policy: DefaultPolicy; default_information?: RelayInfomationBase; @@ -122,6 +131,12 @@ export async function run(args: { return p; } args.admin = p; + } else { + if (typeof args.admin == "string") { + const p = PublicKey.FromString(args.admin); + if (p instanceof Error) return p; + args.admin = p; + } } // Relay Key // let system_key: string | PrivateKey | Error = args.system_key; @@ -169,7 +184,6 @@ export async function run(args: { }, root_handler({ ...args, - is_member: is_member({ admin: args.admin, policyStore }), // deletion delete_event: delete_event_sqlite(db), delete_events_from_pubkey: async () => { @@ -186,7 +200,11 @@ export async function run(args: { write_replaceable_event, policyStore, relayInformationStore, - get_relay_members: get_relay_members(db), + // space member + get_space_members: get_space_members(db), + add_space_member: add_space_member({ admin: args.admin, db }), + is_space_member: is_space_member({ admin: args.admin, db }), + // channel create_channel: create_channel_sqlite(db), edit_channel: edit_channel_sqlite(db), kv: kv, @@ -228,6 +246,10 @@ export async function run(args: { get_channel_by_id: (id: string) => { return get_channel_by_id(id); }, + // space member + get_space_members: get_space_members(db), + is_space_member: is_space_member({ admin: args.admin, db }), + add_space_member: add_space_member({ admin: args.admin, db }), [Symbol.asyncDispose]() { return shutdown(); }, @@ -241,7 +263,6 @@ const root_handler = ( resolvePolicyByKind: func_ResolvePolicyByKind; policyStore: PolicyStore; relayInformationStore: RelayInformationStore; - is_member: func_IsMember; // channel create_channel: func_CreateChannel; edit_channel: func_EditChannel; @@ -255,8 +276,10 @@ const root_handler = ( // write write_regular_event: func_WriteRegularEvent; write_replaceable_event: func_WriteReplaceableEvent; - // relay - get_relay_members: func_GetRelayMembers; + // space member + get_space_members: func_GetSpaceMembers; + add_space_member: func_AddSpaceMember; + is_space_member: func_IsSpaceMember; // config auth_required: boolean; kv: Deno.Kv; @@ -269,28 +292,10 @@ async (req: Request, info: Deno.ServeHandlerInfo) => { } const url = new URL(req.url); if (url.pathname === "/api/auth/login") { - const body = await req.json(); - if (!body) { - return new Response(`{"errors":"request body is null"}`, { status: 400 }); - } - const error = await verifyToken(body, args.relayInformationStore); - if (error instanceof Error) { - return new Response(JSON.stringify(error.message), { status: 400 }); - } else { - const auth = btoa(JSON.stringify(body)); - const headers = new Headers(); - const cookie: Cookie = { - name: "token", - value: auth, - path: "/", - secure: true, - httpOnly: true, - sameSite: "Strict", - }; - setCookie(headers, cookie); - const resp = new Response("", { status: 200, headers }); - return resp; - } + return graphql_login_handler(args)(req); + } + if (url.pathname == "/api/members") { + return members_handler(args); } if (url.pathname == "/api") { return graphql_handler(args)(req); @@ -315,33 +320,7 @@ async (req: Request, info: Deno.ServeHandlerInfo) => { } } } else if (req.method == "POST") { - const text = await req.text(); - const event = parseJSON(text); - if (event instanceof Error) { - return new Response(event.message, { - status: 400, - }); - } - const ok = await verify_event_v2(event); - if (!ok) { - return new Response("event is not valid", { status: 400 }); - } - if (event.kind == Kind_V2.ChannelCreation) { - const ok = await args.create_channel(event); - if (ok) { - return new Response(); - } else { - return new Response("failed to write event", { status: 400 }); - } - } else if (event.kind == Kind_V2.ChannelEdition) { - const res = await args.edit_channel(event); - if (res instanceof Error) { - return new Response(res.message, { status: 400 }); - } - return new Response(); - } else { - return new Response(`not a recognizable event`, { status: 400 }); - } + return event_v2_handler(args)(req); } } const resp = new Response(render(Error404()), { status: 404 }); @@ -358,6 +337,7 @@ const graphql_handler = ( // get get_events_by_filter: func_GetEventsByFilter; get_event_count: func_GetEventCount; + get_space_members: func_GetSpaceMembers; // write write_regular_event: func_WriteRegularEvent; write_replaceable_event: func_WriteReplaceableEvent; @@ -365,8 +345,6 @@ const graphql_handler = ( delete_event: func_DeleteEvent; delete_events_from_pubkey: func_DeleteEventsFromPubkey; get_deleted_event_ids: func_GetDeletedEventIDs; - // relay members - get_relay_members: func_GetRelayMembers; // kv kv: Deno.Kv; }, @@ -423,6 +401,46 @@ async (req: Request) => { export const supported_nips = [1, 2, 11]; export const software = "https://github.com/BlowaterNostr/relayed"; +const graphql_login_handler = + (args: { relayInformationStore: RelayInformationStore }) => async (req: Request) => { + const body = await req.json(); + if (!body) { + return new Response(`{"errors":"request body is null"}`, { status: 400 }); + } + const error = await verifyToken(body, args.relayInformationStore); + if (error instanceof Error) { + return new Response(error.message, { status: 400 }); + } else { + const auth = btoa(JSON.stringify(body)); + const headers = new Headers(); + const cookie: Cookie = { + name: "token", + value: auth, + path: "/", + secure: true, + httpOnly: true, + sameSite: "Strict", + }; + setCookie(headers, cookie); + const resp = new Response("", { status: 200, headers }); + return resp; + } + }; + +const members_handler = async (args: { get_space_members: func_GetSpaceMembers }) => { + const members = await args.get_space_members(); + if (members instanceof Error) { + console.error(members); + return new Response("", { status: 500 }); + } + const resp = new Response(JSON.stringify(members), { status: 200 }); + resp.headers.set("content-type", "application/json; charset=utf-8"); + resp.headers.set("Access-Control-Allow-Origin", "*"); + resp.headers.set("Access-Control-Allow-Methods", "GET"); + resp.headers.set("Access-Control-Allow-Headers", "accept,content-type"); + return resp; +}; + const landing_handler = async (args: { relayInformationStore: RelayInformationStore }) => { const storeInformation = await args.relayInformationStore.resolveRelayInformation(); if (storeInformation instanceof Error) { @@ -450,6 +468,49 @@ const information_handler = async (args: { relayInformationStore: RelayInformati return resp; }; +const event_v2_handler = (args: { + create_channel: func_CreateChannel; + edit_channel: func_EditChannel; + add_space_member: func_AddSpaceMember; +}) => +async (req: Request) => { + const text = await req.text(); + const event = parseJSON(text); + if (event instanceof Error) { + return new Response(event.message, { + status: 400, + }); + } + const ok = await verify_event_v2(event); + if (!ok) { + console.error("event", event); + return new Response("event is not valid", { status: 400 }); + } + if (event.kind == Kind_V2.ChannelCreation) { + const ok = await args.create_channel(event); + if (ok) { + return new Response(); + } else { + return new Response("failed to write event", { status: 400 }); + } + } else if (event.kind == Kind_V2.ChannelEdition) { + const res = await args.edit_channel(event); + if (res instanceof Error) { + return new Response(res.message, { status: 400 }); + } + return new Response(); + } else if (event.kind == Kind_V2.SpaceMember) { + const res = await args.add_space_member(event); + if (res instanceof Error) { + console.error(res); + return new Response(res.message, { status: 400 }); + } + return new Response(); + } else { + return new Response(`not a recognizable event`, { status: 400 }); + } +}; + async function verifyToken(event: NostrEvent, relayInformationStore: RelayInformationStore) { if (!await verifyEvent(event)) { return new Error("token not verified"); @@ -467,28 +528,6 @@ async function verifyToken(event: NostrEvent, relayInformationStore: RelayInform } } -const is_member = (args: { - admin: PublicKey; - policyStore: PolicyStore; -}): func_IsMember => -async (pubkey: string) => { - const { admin, policyStore } = args; - const key = PublicKey.FromString(pubkey); - if (key instanceof Error) { - return key; - } - if (key.hex == admin.hex) { - return true; - } - const policy = await policyStore.resolvePolicyByKind(NostrKind.TEXT_NOTE); - if (policy instanceof Error) { - return policy; - } - const policyAllow = policy.allow.has(pubkey); - const policyBlock = policy.block.has(pubkey); - return policyAllow && !policyBlock; -}; - const graphiql = `