WIP Nostr links

This commit is contained in:
Râu Cao 2024-10-25 01:10:15 +02:00
parent 6ec9f51d77
commit eadc40392a
Signed by: raucao
GPG Key ID: 37036C356E56CC51
9 changed files with 82 additions and 23 deletions

View File

@ -1,4 +1,3 @@
html {
font-size: 16px;
}

View File

@ -32,7 +32,6 @@ button {
font-family: var(--font-family);
}
code {
background-color: #e8e3da;
color: #027739;
@ -49,7 +48,7 @@ pre code {
}
dl dt {
color: var(--text-color-discreet)
color: var(--text-color-discreet);
}
hr {
@ -62,7 +61,7 @@ main header .draft-label {
}
main header .meta .date {
color: var(--text-color-discreet)
color: var(--text-color-discreet);
}
main header .meta .name a {
@ -70,7 +69,7 @@ main header .meta .name a {
}
main.profile-page .pubkey {
color: var(--text-color-discreet)
color: var(--text-color-discreet);
}
/* Dropdown menu */

View File

@ -45,6 +45,7 @@ const config = {
password: Deno.env.get("LDAP_PASSWORD"),
searchDN: Deno.env.get("LDAP_SEARCH_DN"),
},
njump_url: Deno.env.get("NJUMP_URL") || "https://njump.me",
};
const staticUsersConfigured = Object.keys(staticUsers).length > 0;

View File

@ -2,15 +2,20 @@ import Article from "./models/article.ts";
import Profile from "./models/profile.ts";
import { isoDate } from "./dates.ts";
export function profileAtomFeed(profile: Profile, articles: Article[]) {
export async function profileAtomFeed(
profile: Profile,
articles: Article[],
): Promise<string> {
const feedId = `tag:${profile.nip05},nostr-p-${profile.pubkey}-k-30023`;
const lastUpdate = articles.sort((a, b) => b.updatedAt - a.updatedAt)[0]
?.updatedAt;
let articlesXml = "";
const articlesXml = articles.map((article) => {
for (const article of articles) {
const contentHtml = await article.buildContentHtml();
const articleId =
`tag:${profile.nip05},nostr-d-${article.identifier}-k-30023`;
return `
articlesXml += `
<entry>
<id>${articleId}</id>
<title>${article.title}</title>
@ -19,11 +24,11 @@ export function profileAtomFeed(profile: Profile, articles: Article[]) {
<published>${isoDate(article.publishedAt)}</published>
<summary>${article.summary}</summary>
<content type="html"><![CDATA[
${cleanContentHtml(article.html)}
${cleanContentHtml(contentHtml)}
]]></content>
</entry>
`;
}).join("\n");
}
return `
<?xml version="1.0" encoding="utf-8"?>

View File

@ -27,7 +27,7 @@ const userEventHandler = async function (ctx: Context) {
if (articleEvent && profileEvent) {
const article = new Article(articleEvent);
const profile = new Profile(profileEvent, username);
const html = articleHtml(article, profile);
const html = await articleHtml(article, profile);
generateOgProfileImage(profile);
ctx.response.body = html;

View File

@ -38,7 +38,10 @@ export function errorPageHtml(statusCode: number, title: string): string {
return htmlLayout({ title, body });
}
export function articleHtml(article: Article, profile: Profile): string {
export async function articleHtml(
article: Article,
profile: Profile,
): Promise<string> {
const publishedAtFormatted = localizeDate(article.publishedAt);
const pageTitle = article.isDraft ? `Draft: ${article.title}` : article.title;
let draftLabel = ``;
@ -60,7 +63,7 @@ export function articleHtml(article: Article, profile: Profile): string {
</div>
</header>
<article>
${article.html}
${await article.buildContentHtml()}
<footer>
${openWithNostrAppHtml(article.naddr)}
</footer>

View File

@ -1,6 +1,7 @@
import { render as renderMarkdown } from "@deno/gfm";
import { nip19 } from "@nostr/tools";
import { NostrEvent as NEvent } from "@nostrify/nostrify";
import { replaceNostrUris } from "../nostr.ts";
import config from "../config.ts";
export default class Article {
@ -47,10 +48,6 @@ export default class Article {
return this.event.created_at;
}
get html(): string {
return renderMarkdown(this.event.content);
}
get naddr(): string {
return nip19.naddrEncode({
identifier: this.identifier,
@ -59,4 +56,11 @@ export default class Article {
relays: [config.relay_urls[0]],
});
}
async buildContentHtml(): Promise<string> {
let md = this.event.content.trim();
md = md.replace(`# ${this.title}\n`, "");
md = await replaceNostrUris(md);
return renderMarkdown(md);
}
}

View File

@ -1,4 +1,6 @@
import { NPool, NRelay1 } from "@nostrify/nostrify";
import { nip19 } from "@nostr/tools";
import { lookupUsernameByPubkey } from "./directory.ts";
import config from "./config.ts";
const relayPool = new NPool({
@ -56,3 +58,48 @@ export async function fetchProfileEvent(pubkey: string) {
return events.length > 0 ? events[0] : null;
}
export async function nostrUriToUrl(uri: string): Promise<string> {
const bech32 = uri.replace(/^nostr:/, "");
if (bech32.match(/^(naddr|nprofile|npub)/)) {
try {
const r = nip19.decode(bech32);
let username;
switch (r.type) {
case "naddr":
username = await lookupUsernameByPubkey(r.data.pubkey);
if (username) return `/${bech32}`;
break;
case "nprofile":
username = await lookupUsernameByPubkey(r.data.pubkey);
if (username) return `/@${username}`;
break;
case "npub":
username = await lookupUsernameByPubkey(r.data);
if (username) return `/@${username}`;
break;
}
} catch (e) {
console.error(e);
}
}
return `${config.njump_url}/${bech32}`;
}
export async function replaceNostrUris(markdown: string): Promise<string> {
const nostrUriRegex =
/\((nostr:|nprofile|naddr|nevent|nrelay|npub)[a-z0-9]+\)/g;
const matches = markdown.match(nostrUriRegex);
if (!matches) return markdown;
for (const match of matches) {
const uri = match.slice(1, -1);
const url = await nostrUriToUrl(uri);
markdown = markdown.replace(match, `(${url})`);
}
return markdown;
}

View File

@ -58,12 +58,6 @@ describe("Article", () => {
});
});
describe("#html", () => {
it("returns a rendered HTML version of the 'content'", () => {
expect(article.html).toMatch(/<h2 id="the-solution">/);
});
});
describe("#naddr", () => {
it("returns a bech32 addressable event ID", () => {
expect(article.naddr).toMatch(
@ -71,4 +65,11 @@ describe("Article", () => {
);
});
});
describe("#buildContentHtml", () => {
it("returns a rendered HTML version of the 'content'", async () => {
const html = await article.buildContentHtml();
expect(html).toMatch(/<h2 id="the-solution">/);
});
});
});