Show list of linked articles on profile page

This commit is contained in:
Râu Cao 2024-10-21 00:06:15 +02:00
parent e14adffbc8
commit 18f3e888a6
Signed by: raucao
GPG Key ID: 37036C356E56CC51
8 changed files with 93 additions and 32 deletions

View File

@ -1,11 +1,8 @@
import { Context } from "@oak/oak"; import { Context } from "@oak/oak";
import { nip19 } from "@nostr/tools"; import { nip19 } from "@nostr/tools";
import { log } from "../log.ts"; import { log } from "../log.ts";
import { articleHtml } from "../html.ts" import { articleHtml } from "../html.ts";
import { import { fetchProfileEvent, fetchReplaceableEvent } from "../nostr.ts";
fetchReplaceableEvent,
fetchProfileEvent
} from "../nostr.ts";
const naddrHandler = async function (ctx: Context) { const naddrHandler = async function (ctx: Context) {
const { request } = ctx; const { request } = ctx;
@ -13,7 +10,10 @@ const naddrHandler = async function (ctx: Context) {
try { try {
const r = nip19.decode(naddr); const r = nip19.decode(naddr);
const articleEvent = await fetchReplaceableEvent(r.data.pubkey, r.data.identifier); const articleEvent = await fetchReplaceableEvent(
r.data.pubkey,
r.data.identifier,
);
const profileEvent = await fetchProfileEvent(r.data.pubkey); const profileEvent = await fetchProfileEvent(r.data.pubkey);
let profile; let profile;

View File

@ -3,7 +3,7 @@ import { nip19 } from "@nostr/tools";
import { log } from "../log.ts"; import { log } from "../log.ts";
import { lookupUsernameByPubkey } from "../ldap.ts"; import { lookupUsernameByPubkey } from "../ldap.ts";
import { fetchProfileEvent } from "../nostr.ts"; import { fetchProfileEvent } from "../nostr.ts";
import { profilePageHtml } from "../html.ts" import { profilePageHtml } from "../html.ts";
const nprofileHandler = async function (ctx: Context) { const nprofileHandler = async function (ctx: Context) {
const { request } = ctx; const { request } = ctx;

View File

@ -3,7 +3,7 @@ import { nip19 } from "@nostr/tools";
import { log } from "../log.ts"; import { log } from "../log.ts";
import { lookupUsernameByPubkey } from "../ldap.ts"; import { lookupUsernameByPubkey } from "../ldap.ts";
import { fetchProfileEvent } from "../nostr.ts"; import { fetchProfileEvent } from "../nostr.ts";
import { profilePageHtml } from "../html.ts" import { profilePageHtml } from "../html.ts";
const npubHandler = async function (ctx: Context) { const npubHandler = async function (ctx: Context) {
const { request } = ctx; const { request } = ctx;

View File

@ -2,8 +2,8 @@ import { Context } from "@oak/oak";
import { nip19 } from "@nostr/tools"; import { nip19 } from "@nostr/tools";
import { log } from "../log.ts"; import { log } from "../log.ts";
import { lookupPubkeyByUsername } from "../ldap.ts"; import { lookupPubkeyByUsername } from "../ldap.ts";
import { fetchProfileEvent } from "../nostr.ts"; import { fetchArticlesByAuthor, fetchProfileEvent } from "../nostr.ts";
import { profilePageHtml } from "../html.ts" import { profilePageHtml } from "../html.ts";
const usernameHandler = async function (ctx: Context) { const usernameHandler = async function (ctx: Context) {
const { request } = ctx; const { request } = ctx;
@ -20,7 +20,8 @@ const usernameHandler = async function (ctx: Context) {
const profileEvent = await fetchProfileEvent(pubkey); const profileEvent = await fetchProfileEvent(pubkey);
if (profileEvent) { if (profileEvent) {
const html = profilePageHtml(profileEvent); const articleEvents = await fetchArticlesByAuthor(pubkey);
const html = profilePageHtml(profileEvent, articleEvents);
ctx.response.body = html; ctx.response.body = html;
} else { } else {

64
html.ts
View File

@ -1,4 +1,5 @@
import { render as renderMarkdown } from "@deno/gfm"; import { render as renderMarkdown } from "@deno/gfm";
import { nip19 } from "@nostr/tools";
import { log } from "./log.ts"; import { log } from "./log.ts";
export function htmlLayout(title: string, body: string) { export function htmlLayout(title: string, body: string) {
@ -47,6 +48,7 @@ export function htmlLayout(title: string, body: string) {
h2, h3, h4 { h2, h3, h4 {
margin-top: 2em; margin-top: 2em;
margin-bottom: 2rem; margin-bottom: 2rem;
line-height: 1.6em;
} }
h1, h2, h3, h4 { h1, h2, h3, h4 {
@ -110,6 +112,14 @@ export function htmlLayout(title: string, body: string) {
p.meta .date { p.meta .date {
color: #888; color: #888;
} }
.article-list .item {
margin-bottom: 3rem;
}
.article-list .item h3 {
margin-bottom: 1rem;
}
</style> </style>
</head> </head>
<body> <body>
@ -120,7 +130,7 @@ export function htmlLayout(title: string, body: string) {
} }
export function articleHtml(articleEvent: object, profile: object) { export function articleHtml(articleEvent: object, profile: object) {
const titleTag = articleEvent.tags.find(t => t[0] === "title"); const titleTag = articleEvent.tags.find((t) => t[0] === "title");
const title = titleTag ? titleTag[1] : "Untitled"; const title = titleTag ? titleTag[1] : "Untitled";
const content = renderMarkdown(articleEvent.content); const content = renderMarkdown(articleEvent.content);
const date = new Date(articleEvent.created_at * 1000); const date = new Date(articleEvent.created_at * 1000);
@ -149,16 +159,50 @@ export function articleHtml(articleEvent: object, profile: object) {
return htmlLayout(title, body); return htmlLayout(title, body);
} }
export function profilePageHtml(profileEvent: object) { function articleListItemHtml(articleEvent: object) {
const identifier = articleEvent.tags.find((t) => t[0] === "d")[1];
const naddr = nip19.naddrEncode({
identifier: identifier,
pubkey: articleEvent.pubkey,
kind: articleEvent.kind
});
const titleTag = articleEvent.tags.find((t) => t[0] === "title");
const title = titleTag ? titleTag[1] : "Untitled";
const date = new Date(articleEvent.created_at * 1000);
const formattedDate = date.toLocaleDateString("en-US", {
year: "numeric",
month: "long",
day: "numeric",
});
return `
<div class="item">
<h3><a href="/${naddr}">${title}</a></h3>
<p>${formattedDate}</p>
</div>
`;
}
export function articleListHtml(articleEvents: object[]) {
if (articleEvents.length === 0) return "";
let html = "";
for (const articleEvent of articleEvents) {
html += articleListItemHtml(articleEvent);
}
return `
<h2>Articles</h2>
<div class="article-list">
${html}
</div>
`;
}
export function profilePageHtml(profileEvent: object, articleEvents: object[]) {
const profile = JSON.parse(profileEvent.content); const profile = JSON.parse(profileEvent.content);
const name = profile.name || "Anonymous"; const name = profile.name || "Anonymous";
const title = `${name} on Nostr` const title = `${name} on Nostr`;
// const date = new Date(articleEvent.created_at * 1000);
// const formattedDate = date.toLocaleDateString("en-US", {
// year: "numeric",
// month: "long",
// day: "numeric",
// });
const body = ` const body = `
<main class="profile-page"> <main class="profile-page">
@ -171,7 +215,7 @@ export function profilePageHtml(profileEvent: object) {
</p> </p>
</div> </div>
</header> </header>
<h2>Articles</h2> ${articleListHtml(articleEvents)}
</main> </main>
`; `;

16
ldap.ts
View File

@ -1,20 +1,20 @@
import { load } from "@std/dotenv"; import { load } from "@std/dotenv";
import { Client } from 'ldapts'; import { Client } from "ldapts";
import { log } from "./log.ts"; import { log } from "./log.ts";
const dirname = new URL('.', import.meta.url).pathname; const dirname = new URL(".", import.meta.url).pathname;
await load({ envPath: `${dirname}/.env`, export: true }); await load({ envPath: `${dirname}/.env`, export: true });
const config = { const config = {
url: Deno.env.get("LDAP_URL"), url: Deno.env.get("LDAP_URL"),
bindDN: Deno.env.get("LDAP_BIND_DN"), bindDN: Deno.env.get("LDAP_BIND_DN"),
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"),
} };
const client = new Client({ url: config.url }); const client = new Client({ url: config.url });
export async function lookupPubkeyByUsername (username: string) { export async function lookupPubkeyByUsername(username: string) {
let pubkey; let pubkey;
try { try {
@ -22,7 +22,7 @@ export async function lookupPubkeyByUsername (username: string) {
const { searchEntries } = await client.search(config.searchDN, { const { searchEntries } = await client.search(config.searchDN, {
filter: `(cn=${username})`, filter: `(cn=${username})`,
attributes: ['nostrKey'] attributes: ["nostrKey"],
}); });
pubkey = searchEntries[0]?.nostrKey; pubkey = searchEntries[0]?.nostrKey;
@ -34,7 +34,7 @@ export async function lookupPubkeyByUsername (username: string) {
} }
} }
export async function lookupUsernameByPubkey (pubkey: string) { export async function lookupUsernameByPubkey(pubkey: string) {
let username; let username;
try { try {
@ -42,7 +42,7 @@ export async function lookupUsernameByPubkey (pubkey: string) {
const { searchEntries } = await client.search(config.searchDN, { const { searchEntries } = await client.search(config.searchDN, {
filter: `(nostrKey=${pubkey})`, filter: `(nostrKey=${pubkey})`,
attributes: ['cn'] attributes: ["cn"],
}); });
username = searchEntries[0]?.cn; username = searchEntries[0]?.cn;

View File

@ -23,7 +23,10 @@ router.get("/:path", async (ctx: ctx) => {
ctx.response.body = "Not Found"; ctx.response.body = "Not Found";
} }
log(`${ctx.request.method} ${ctx.request.url} - ${ctx.response.status}`, "gray"); log(
`${ctx.request.method} ${ctx.request.url} - ${ctx.response.status}`,
"gray",
);
}); });
const app = new Application(); const app = new Application();
@ -33,4 +36,4 @@ app.use(router.allowedMethods());
const PORT = 8000; const PORT = 8000;
app.listen({ port: PORT }); app.listen({ port: PORT });
console.log(`App listening on http://localhost:${PORT}`) console.log(`App listening on http://localhost:${PORT}`);

View File

@ -2,7 +2,10 @@ import { NRelay1 } from "@nostrify/nostrify";
export const relay = new NRelay1("wss://nostr.kosmos.org"); export const relay = new NRelay1("wss://nostr.kosmos.org");
export async function fetchReplaceableEvent(pubkey: string, identifier: string) { export async function fetchReplaceableEvent(
pubkey: string,
identifier: string,
) {
const events = await relay.query([{ const events = await relay.query([{
authors: [pubkey], authors: [pubkey],
kinds: [30023], kinds: [30023],
@ -13,6 +16,16 @@ export async function fetchReplaceableEvent(pubkey: string, identifier: string)
return events.length > 0 ? events[0] : null; return events.length > 0 ? events[0] : null;
} }
export async function fetchArticlesByAuthor(pubkey: string) {
const events = await relay.query([{
authors: [pubkey],
kinds: [30023],
limit: 10,
}]);
return events;
}
export async function fetchProfileEvent(pubkey: string) { export async function fetchProfileEvent(pubkey: string) {
const events = await relay.query([{ const events = await relay.query([{
authors: [pubkey], authors: [pubkey],