Show list of linked articles on profile page
This commit is contained in:
parent
e14adffbc8
commit
18f3e888a6
@ -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;
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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
64
html.ts
@ -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>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
12
ldap.ts
12
ldap.ts
@ -1,16 +1,16 @@
|
|||||||
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 });
|
||||||
|
|
||||||
@ -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;
|
||||||
@ -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;
|
||||||
|
7
main.ts
7
main.ts
@ -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}`);
|
||||||
|
15
nostr.ts
15
nostr.ts
@ -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],
|
||||||
|
Loading…
x
Reference in New Issue
Block a user