WIP Nostr links
This commit is contained in:
parent
6ec9f51d77
commit
eadc40392a
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
html {
|
html {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
|
@ -32,7 +32,6 @@ button {
|
|||||||
font-family: var(--font-family);
|
font-family: var(--font-family);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
code {
|
code {
|
||||||
background-color: #e8e3da;
|
background-color: #e8e3da;
|
||||||
color: #027739;
|
color: #027739;
|
||||||
@ -49,7 +48,7 @@ pre code {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dl dt {
|
dl dt {
|
||||||
color: var(--text-color-discreet)
|
color: var(--text-color-discreet);
|
||||||
}
|
}
|
||||||
|
|
||||||
hr {
|
hr {
|
||||||
@ -62,7 +61,7 @@ main header .draft-label {
|
|||||||
}
|
}
|
||||||
|
|
||||||
main header .meta .date {
|
main header .meta .date {
|
||||||
color: var(--text-color-discreet)
|
color: var(--text-color-discreet);
|
||||||
}
|
}
|
||||||
|
|
||||||
main header .meta .name a {
|
main header .meta .name a {
|
||||||
@ -70,7 +69,7 @@ main header .meta .name a {
|
|||||||
}
|
}
|
||||||
|
|
||||||
main.profile-page .pubkey {
|
main.profile-page .pubkey {
|
||||||
color: var(--text-color-discreet)
|
color: var(--text-color-discreet);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Dropdown menu */
|
/* Dropdown menu */
|
||||||
|
@ -45,6 +45,7 @@ const config = {
|
|||||||
password: Deno.env.get("LDAP_PASSWORD"),
|
password: Deno.env.get("LDAP_PASSWORD"),
|
||||||
searchDN: Deno.env.get("LDAP_SEARCH_DN"),
|
searchDN: Deno.env.get("LDAP_SEARCH_DN"),
|
||||||
},
|
},
|
||||||
|
njump_url: Deno.env.get("NJUMP_URL") || "https://njump.me",
|
||||||
};
|
};
|
||||||
|
|
||||||
const staticUsersConfigured = Object.keys(staticUsers).length > 0;
|
const staticUsersConfigured = Object.keys(staticUsers).length > 0;
|
||||||
|
15
feeds.ts
15
feeds.ts
@ -2,15 +2,20 @@ import Article from "./models/article.ts";
|
|||||||
import Profile from "./models/profile.ts";
|
import Profile from "./models/profile.ts";
|
||||||
import { isoDate } from "./dates.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 feedId = `tag:${profile.nip05},nostr-p-${profile.pubkey}-k-30023`;
|
||||||
const lastUpdate = articles.sort((a, b) => b.updatedAt - a.updatedAt)[0]
|
const lastUpdate = articles.sort((a, b) => b.updatedAt - a.updatedAt)[0]
|
||||||
?.updatedAt;
|
?.updatedAt;
|
||||||
|
let articlesXml = "";
|
||||||
|
|
||||||
const articlesXml = articles.map((article) => {
|
for (const article of articles) {
|
||||||
|
const contentHtml = await article.buildContentHtml();
|
||||||
const articleId =
|
const articleId =
|
||||||
`tag:${profile.nip05},nostr-d-${article.identifier}-k-30023`;
|
`tag:${profile.nip05},nostr-d-${article.identifier}-k-30023`;
|
||||||
return `
|
articlesXml += `
|
||||||
<entry>
|
<entry>
|
||||||
<id>${articleId}</id>
|
<id>${articleId}</id>
|
||||||
<title>${article.title}</title>
|
<title>${article.title}</title>
|
||||||
@ -19,11 +24,11 @@ export function profileAtomFeed(profile: Profile, articles: Article[]) {
|
|||||||
<published>${isoDate(article.publishedAt)}</published>
|
<published>${isoDate(article.publishedAt)}</published>
|
||||||
<summary>${article.summary}</summary>
|
<summary>${article.summary}</summary>
|
||||||
<content type="html"><![CDATA[
|
<content type="html"><![CDATA[
|
||||||
${cleanContentHtml(article.html)}
|
${cleanContentHtml(contentHtml)}
|
||||||
]]></content>
|
]]></content>
|
||||||
</entry>
|
</entry>
|
||||||
`;
|
`;
|
||||||
}).join("\n");
|
}
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
@ -27,7 +27,7 @@ const userEventHandler = async function (ctx: Context) {
|
|||||||
if (articleEvent && profileEvent) {
|
if (articleEvent && profileEvent) {
|
||||||
const article = new Article(articleEvent);
|
const article = new Article(articleEvent);
|
||||||
const profile = new Profile(profileEvent, username);
|
const profile = new Profile(profileEvent, username);
|
||||||
const html = articleHtml(article, profile);
|
const html = await articleHtml(article, profile);
|
||||||
generateOgProfileImage(profile);
|
generateOgProfileImage(profile);
|
||||||
|
|
||||||
ctx.response.body = html;
|
ctx.response.body = html;
|
||||||
|
7
html.ts
7
html.ts
@ -38,7 +38,10 @@ export function errorPageHtml(statusCode: number, title: string): string {
|
|||||||
return htmlLayout({ title, body });
|
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 publishedAtFormatted = localizeDate(article.publishedAt);
|
||||||
const pageTitle = article.isDraft ? `Draft: ${article.title}` : article.title;
|
const pageTitle = article.isDraft ? `Draft: ${article.title}` : article.title;
|
||||||
let draftLabel = ``;
|
let draftLabel = ``;
|
||||||
@ -60,7 +63,7 @@ export function articleHtml(article: Article, profile: Profile): string {
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<article>
|
<article>
|
||||||
${article.html}
|
${await article.buildContentHtml()}
|
||||||
<footer>
|
<footer>
|
||||||
${openWithNostrAppHtml(article.naddr)}
|
${openWithNostrAppHtml(article.naddr)}
|
||||||
</footer>
|
</footer>
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { render as renderMarkdown } from "@deno/gfm";
|
import { render as renderMarkdown } from "@deno/gfm";
|
||||||
import { nip19 } from "@nostr/tools";
|
import { nip19 } from "@nostr/tools";
|
||||||
import { NostrEvent as NEvent } from "@nostrify/nostrify";
|
import { NostrEvent as NEvent } from "@nostrify/nostrify";
|
||||||
|
import { replaceNostrUris } from "../nostr.ts";
|
||||||
import config from "../config.ts";
|
import config from "../config.ts";
|
||||||
|
|
||||||
export default class Article {
|
export default class Article {
|
||||||
@ -47,10 +48,6 @@ export default class Article {
|
|||||||
return this.event.created_at;
|
return this.event.created_at;
|
||||||
}
|
}
|
||||||
|
|
||||||
get html(): string {
|
|
||||||
return renderMarkdown(this.event.content);
|
|
||||||
}
|
|
||||||
|
|
||||||
get naddr(): string {
|
get naddr(): string {
|
||||||
return nip19.naddrEncode({
|
return nip19.naddrEncode({
|
||||||
identifier: this.identifier,
|
identifier: this.identifier,
|
||||||
@ -59,4 +56,11 @@ export default class Article {
|
|||||||
relays: [config.relay_urls[0]],
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
47
nostr.ts
47
nostr.ts
@ -1,4 +1,6 @@
|
|||||||
import { NPool, NRelay1 } from "@nostrify/nostrify";
|
import { NPool, NRelay1 } from "@nostrify/nostrify";
|
||||||
|
import { nip19 } from "@nostr/tools";
|
||||||
|
import { lookupUsernameByPubkey } from "./directory.ts";
|
||||||
import config from "./config.ts";
|
import config from "./config.ts";
|
||||||
|
|
||||||
const relayPool = new NPool({
|
const relayPool = new NPool({
|
||||||
@ -56,3 +58,48 @@ export async function fetchProfileEvent(pubkey: string) {
|
|||||||
|
|
||||||
return events.length > 0 ? events[0] : null;
|
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;
|
||||||
|
}
|
||||||
|
@ -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", () => {
|
describe("#naddr", () => {
|
||||||
it("returns a bech32 addressable event ID", () => {
|
it("returns a bech32 addressable event ID", () => {
|
||||||
expect(article.naddr).toMatch(
|
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">/);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user