diff --git a/.gitignore b/.gitignore index 1073944..863280c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .env users.yaml +substr diff --git a/config.ts b/config.ts index f4f8aed..22b84ac 100644 --- a/config.ts +++ b/config.ts @@ -5,24 +5,24 @@ import { log } from "./log.ts"; const dirname = new URL(".", import.meta.url).pathname; await load({ envPath: `${dirname}/.env`, export: true }); -let staticUsers; +let staticUsers: { [key: string]: string } = {}; try { const yamlContent = await Deno.readTextFile(`${dirname}/users.yaml`); - staticUsers = parse(yamlContent); - log(`Serving content for ${Object.keys(staticUsers).length} pubkeys from users.yaml`, "blue"); + const parsedContent = parse(yamlContent); + if (parsedContent !== null && typeof parsedContent === "object") { + staticUsers = parsedContent as { [key: string]: string }; + log(`Serving content for pubkeys in users.yaml`, "blue"); + } } catch { - staticUsers = {}; log(`Could not find or parse a users.yaml config`, "yellow"); } -const relay_urls = Deno.env.get("RELAY_URLS")?.split(","); - const config = { - port: Deno.env.get("PORT") || 8000, + port: parseInt(Deno.env.get("PORT") || "8000"), base_url: Deno.env.get("BASE_URL") || `http://localhost:8000`, - relay_urls, - staticUsers: staticUsers, + relay_urls: Deno.env.get("RELAY_URLS")?.split(",") || [], + staticUsers, ldapEnabled: !!Deno.env.get("LDAP_URL"), ldap: { url: Deno.env.get("LDAP_URL"), @@ -32,8 +32,21 @@ const config = { }, }; -if (config.ldapEnabled && config.ldap.url) { - log(`Serving content for pubkeys from ${config.ldap.url}`, "blue"); +if (config.relay_urls.length === 0) { + log(`No relays configured. Please add at least one relay to RELAY_URLS.`); + Deno.exit(1); +} + +if (config.ldapEnabled) { + if ( + config.ldap.url && config.ldap.bindDN && config.ldap.password && + config.ldap.searchDN + ) { + log(`Serving content for pubkeys from ${config.ldap.url}`, "blue"); + } else { + log(`The LDAP config is incomplete`); + Deno.exit(1); + } } else { log(`LDAP not enabled`, "blue"); } @@ -43,11 +56,4 @@ if (Object.keys(staticUsers).length === 0 && !config.ldapEnabled) { Deno.exit(1); } -if (config.relay_urls?.length > 0) { - log(`Relays: ${config.relay_urls.join(", ")}`, "green"); -} else { - log(`No relays configured. Please add at least one relay to RELAY_URLS.`); - Deno.exit(1); -} - export default config; diff --git a/handlers/naddr.ts b/handlers/naddr.ts index babed74..fb6ad5f 100644 --- a/handlers/naddr.ts +++ b/handlers/naddr.ts @@ -1,23 +1,21 @@ import { Context } from "@oak/oak"; import { nip19 } from "@nostr/tools"; -import { log } from "../log.ts"; import { lookupUsernameByPubkey } from "../directory.ts"; import notFoundHandler from "../handlers/not-found.ts"; const naddrHandler = async function (ctx: Context) { - const naddr = ctx.params.path; + const naddr = ctx.state.path; try { - const r = nip19.decode(naddr); - const username = await lookupUsernameByPubkey(r.data.pubkey); + const data = nip19.decode(naddr).data as nip19.AddressPointer; + const username = await lookupUsernameByPubkey(data.pubkey); - if (username && r.data.identifier) { - ctx.response.redirect(`/@${username}/${r.data.identifier}`); + if (username && data.identifier) { + ctx.response.redirect(`/@${username}/${data.identifier}`); } else { notFoundHandler(ctx); } - } catch (e) { - log(e, "yellow"); + } catch (_e) { notFoundHandler(ctx); } }; diff --git a/handlers/nprofile.ts b/handlers/nprofile.ts index aefba0d..04a7c75 100644 --- a/handlers/nprofile.ts +++ b/handlers/nprofile.ts @@ -1,23 +1,21 @@ import { Context } from "@oak/oak"; import { nip19 } from "@nostr/tools"; -import { log } from "../log.ts"; import { lookupUsernameByPubkey } from "../directory.ts"; import notFoundHandler from "../handlers/not-found.ts"; const nprofileHandler = async function (ctx: Context) { - const nprofile = ctx.params.path; + const nprofile = ctx.state.path; try { - const r = nip19.decode(nprofile); - const username = await lookupUsernameByPubkey(r.data.pubkey); + const data = nip19.decode(nprofile).data as nip19.ProfilePointer; + const username = await lookupUsernameByPubkey(data.pubkey); if (username) { ctx.response.redirect(`/@${username}`); } else { notFoundHandler(ctx); } - } catch (e) { - log(e, "yellow"); + } catch (_e) { notFoundHandler(ctx); } }; diff --git a/handlers/npub.ts b/handlers/npub.ts index ecbb354..315bd3f 100644 --- a/handlers/npub.ts +++ b/handlers/npub.ts @@ -1,23 +1,21 @@ import { Context } from "@oak/oak"; import { nip19 } from "@nostr/tools"; -import { log } from "../log.ts"; import { lookupUsernameByPubkey } from "../directory.ts"; import notFoundHandler from "../handlers/not-found.ts"; const npubHandler = async function (ctx: Context) { - const npub = ctx.params.path; + const npub = ctx.state.path; try { - const r = nip19.decode(npub); - const username = await lookupUsernameByPubkey(r.data); + const pubkey = nip19.decode(npub).data as string; + const username = await lookupUsernameByPubkey(pubkey); if (username) { ctx.response.redirect(`/@${username}`); } else { notFoundHandler(ctx); } - } catch (e) { - log(e, "yellow"); + } catch (_e) { notFoundHandler(ctx); } }; diff --git a/handlers/user-atom-feed.ts b/handlers/user-atom-feed.ts index efc6a81..a464847 100644 --- a/handlers/user-atom-feed.ts +++ b/handlers/user-atom-feed.ts @@ -1,5 +1,4 @@ import { Context } from "@oak/oak"; -import { log } from "../log.ts"; import { lookupPubkeyByUsername } from "../directory.ts"; import { fetchArticlesByAuthor, fetchProfileEvent } from "../nostr.ts"; import { profileAtomFeed } from "../feeds.ts"; @@ -8,8 +7,8 @@ import Profile from "../models/profile.ts"; import notFoundHandler from "../handlers/not-found.ts"; const userAtomFeedHandler = async function (ctx: Context) { - const username = ctx.params.user.replace(/^(@|~)/, ""); - const pubkey = await lookupPubkeyByUsername(username); + const username = ctx.state.username; + const pubkey = await lookupPubkeyByUsername(ctx.state.username); if (!pubkey) { notFoundHandler(ctx); @@ -18,21 +17,22 @@ const userAtomFeedHandler = async function (ctx: Context) { try { const profileEvent = await fetchProfileEvent(pubkey); - const profile = new Profile(profileEvent, username); - if (profileEvent && profile.nip05) { - const articleEvents = await fetchArticlesByAuthor(pubkey); - const articles = articleEvents.map((a) => new Article(a)); - const atom = profileAtomFeed(profile, articles); + if (profileEvent) { + const profile = new Profile(profileEvent, username); - ctx.response.headers.set("Content-Type", "application/atom+xml"); - ctx.response.body = atom; - } else { - ctx.response.status = 404; - ctx.response.body = "Not Found"; + if (profile.nip05) { + const articleEvents = await fetchArticlesByAuthor(pubkey); + const articles = articleEvents.map((a) => new Article(a)); + const atom = profileAtomFeed(profile, articles); + + ctx.response.headers.set("Content-Type", "application/atom+xml"); + ctx.response.body = atom; + return; + } } - } catch (e) { - log(e, "yellow"); + notFoundHandler(ctx); + } catch (_e) { notFoundHandler(ctx); } }; diff --git a/handlers/user-event.ts b/handlers/user-event.ts index 469861c..8b581ba 100644 --- a/handlers/user-event.ts +++ b/handlers/user-event.ts @@ -1,5 +1,4 @@ import { Context } from "@oak/oak"; -import { log } from "../log.ts"; import { lookupPubkeyByUsername } from "../directory.ts"; import { fetchProfileEvent, fetchReplaceableEvent } from "../nostr.ts"; import Article from "../models/article.ts"; @@ -8,8 +7,8 @@ import { articleHtml } from "../html.ts"; import notFoundHandler from "../handlers/not-found.ts"; const userEventHandler = async function (ctx: Context) { - const username = ctx.params.user.replace(/^(@|~)/, ""); - const identifier = ctx.params.identifier; + const username = ctx.state.username.replace(/^(@|~)/, ""); + const identifier = ctx.state.identifier; const pubkey = await lookupPubkeyByUsername(username); if (!pubkey) { @@ -33,8 +32,7 @@ const userEventHandler = async function (ctx: Context) { } else { notFoundHandler(ctx); } - } catch (e) { - log(e, "yellow"); + } catch (_e) { notFoundHandler(ctx); } }; diff --git a/handlers/user-profile.ts b/handlers/user-profile.ts index 17b77e5..b12c743 100644 --- a/handlers/user-profile.ts +++ b/handlers/user-profile.ts @@ -1,5 +1,4 @@ import { Context } from "@oak/oak"; -import { log } from "../log.ts"; import { lookupPubkeyByUsername } from "../directory.ts"; import { fetchArticlesByAuthor, fetchProfileEvent } from "../nostr.ts"; import Article from "../models/article.ts"; @@ -8,7 +7,7 @@ import { profilePageHtml } from "../html.ts"; import notFoundHandler from "../handlers/not-found.ts"; const userProfileHandler = async function (ctx: Context) { - const username = ctx.params.path.replace(/^(@|~)/, ""); + const username = ctx.state.path.replace(/^(@|~)/, ""); const pubkey = await lookupPubkeyByUsername(username); if (!pubkey) { @@ -27,11 +26,9 @@ const userProfileHandler = async function (ctx: Context) { ctx.response.body = html; } else { - log(`No profile event found for @${username}`, "yellow"); notFoundHandler(ctx); } - } catch (e) { - log(e, "yellow"); + } catch (_e) { notFoundHandler(ctx); } }; diff --git a/ldap.ts b/ldap.ts index d201430..07867b9 100644 --- a/ldap.ts +++ b/ldap.ts @@ -1,28 +1,27 @@ import { Client } from "ldapts"; -import { log } from "./log.ts"; import config from "./config.ts"; const { ldap, ldapEnabled } = config; let client: Client; if (ldapEnabled) { - client = new Client({ url: ldap.url }); + client = new Client({ url: ldap.url as string }); } export async function lookupPubkeyByUsername(username: string) { let pubkey; try { - await client.bind(ldap.bindDN, ldap.password); + await client.bind(ldap.bindDN as string, ldap.password as string); - const { searchEntries } = await client.search(ldap.searchDN, { + const { searchEntries } = await client.search(ldap.searchDN as string, { filter: `(cn=${username})`, attributes: ["nostrKey"], }); - pubkey = searchEntries[0]?.nostrKey; - } catch (ex) { - log(ex, "red"); + pubkey = searchEntries[0]?.nostrKey as string; + } catch (e) { + console.error(e); } finally { await client.unbind(); } @@ -34,16 +33,16 @@ export async function lookupUsernameByPubkey(pubkey: string) { let username; try { - await client.bind(ldap.bindDN, ldap.password); + await client.bind(ldap.bindDN as string, ldap.password as string); - const { searchEntries } = await client.search(ldap.searchDN, { + const { searchEntries } = await client.search(ldap.searchDN as string, { filter: `(nostrKey=${pubkey})`, attributes: ["cn"], }); username = searchEntries[0]?.cn; - } catch (ex) { - log(ex, "red"); + } catch (e) { + console.error(e); } finally { await client.unbind(); } diff --git a/models/article.ts b/models/article.ts index 58e6a9b..7e2249b 100644 --- a/models/article.ts +++ b/models/article.ts @@ -1,6 +1,6 @@ import { render as renderMarkdown } from "@deno/gfm"; import { nip19 } from "@nostr/tools"; -import { NEvent } from "../nostr.ts"; +import { NostrEvent as NEvent } from "@nostrify/nostrify"; import config from "../config.ts"; export default class Article { @@ -51,7 +51,7 @@ export default class Article { identifier: this.identifier, pubkey: this.event.pubkey, kind: this.event.kind, - relays: [config.home_relay_url], + relays: [config.relay_urls[0]], }); } } diff --git a/models/profile.ts b/models/profile.ts index 478e3a4..8e29606 100644 --- a/models/profile.ts +++ b/models/profile.ts @@ -1,5 +1,5 @@ -import { nip19 } from "@nostr/tools"; -import { NEvent } from "../nostr.ts"; +import { nip19, NostrEvent as NEvent } from "@nostr/tools"; +// import { NEvent } from "../nostr.ts"; export interface ProfileData { name?: string; @@ -12,8 +12,8 @@ export interface ProfileData { } export default class Profile { - event: NEvent; private data: ProfileData; + event: NEvent; username?: string; constructor(event: NEvent, username?: string) { diff --git a/nostr.ts b/nostr.ts index 3ceb321..f1e8ad6 100644 --- a/nostr.ts +++ b/nostr.ts @@ -1,21 +1,15 @@ import { NPool, NRelay1 } from "@nostrify/nostrify"; import config from "./config.ts"; -export interface NEvent { - content: string; - created_at: number; - id: string; - kind: number; - pubkey: string; - sig: string; - tags: Array<[string, string, string?]>; -} - const relayPool = new NPool({ open: (url) => new NRelay1(url), - reqRouter: async (filters) => new Map( - config.relay_urls.map(url => [ url, filters ]) - ) + // deno-lint-ignore require-await + reqRouter: async (filters) => + new Map( + config.relay_urls.map((url) => [url, filters]), + ), + // deno-lint-ignore require-await + eventRouter: async (_event) => [], }); export async function fetchReplaceableEvent( diff --git a/server.ts b/server.ts index c32e1dd..22ab0a2 100644 --- a/server.ts +++ b/server.ts @@ -1,4 +1,4 @@ -import { Application, Context, Router, send } from "@oak/oak"; +import { Application, Router, send } from "@oak/oak"; import config from "./config.ts"; import naddrHandler from "./handlers/naddr.ts"; import nprofileHandler from "./handlers/nprofile.ts"; @@ -10,8 +10,8 @@ import notFoundHandler from "./handlers/not-found.ts"; const router = new Router(); -router.get("/:path", async (ctx: Context) => { - const { path } = ctx.params; +router.get("/:path", async (ctx) => { + const path = ctx.state.path = ctx.params.path; if (path.startsWith("naddr")) { await naddrHandler(ctx); @@ -26,12 +26,14 @@ router.get("/:path", async (ctx: Context) => { } }); -router.get("/:user/:kind.atom", async (ctx: Context) => { - const { user, kind } = ctx.params; +router.get("/:username/:kind.atom", async (ctx) => { + ctx.state.username = ctx.params.username.replace(/^(@|~)/, ""); + ctx.state.kind = ctx.params.kind; if ( - kind === "articles" && - (user.startsWith("@") || user.startsWith("~")) + ctx.state.kind === "articles" && + (ctx.params.username.startsWith("@") || + ctx.params.username.startsWith("~")) ) { await userAtomFeedHandler(ctx); } else { @@ -39,10 +41,11 @@ router.get("/:user/:kind.atom", async (ctx: Context) => { } }); -router.get("/:user/:identifier", async (ctx: Context) => { - const { user } = ctx.params; +router.get("/:username/:identifier", async (ctx) => { + const username = ctx.state.username = ctx.params.username; + ctx.state.identifier = ctx.params.identifier; - if (user.startsWith("@") || user.startsWith("~")) { + if (username.startsWith("@") || username.startsWith("~")) { await userEventHandler(ctx); } else { notFoundHandler(ctx); @@ -51,7 +54,7 @@ router.get("/:user/:identifier", async (ctx: Context) => { router.get("/assets/:path*", async (ctx) => { try { - const filePath = ctx.params.path || ''; + const filePath = ctx.params.path || ""; await send(ctx, filePath, { root: `${Deno.cwd()}/assets`, diff --git a/tests/models/article_test.ts b/tests/models/article_test.ts index 6f9d6a8..c2a0894 100644 --- a/tests/models/article_test.ts +++ b/tests/models/article_test.ts @@ -1,6 +1,6 @@ import { beforeAll, describe, it } from "@std/testing/bdd"; import { expect } from "@std/expect"; -import { NEvent } from "../../nostr.ts"; +import { NostrEvent as NEvent } from "@nostrify/nostrify"; import Article from "../../models/article.ts"; describe("Article", () => { diff --git a/tests/models/profile_test.ts b/tests/models/profile_test.ts index a4cd09a..42d2e54 100644 --- a/tests/models/profile_test.ts +++ b/tests/models/profile_test.ts @@ -1,6 +1,6 @@ import { beforeAll, describe, it } from "@std/testing/bdd"; import { expect } from "@std/expect"; -import { NEvent } from "../../nostr.ts"; +import { NostrEvent as NEvent } from "@nostrify/nostrify"; import Profile from "../../models/profile.ts"; describe("Profile", () => {