Compare commits
26 Commits
afcb99356c
...
v1.0.0-alp
| Author | SHA1 | Date | |
|---|---|---|---|
|
2c9edf4ddc
|
|||
|
275fb73896
|
|||
| 204b4f44c7 | |||
|
8e802f314a
|
|||
|
f4d1ba897b
|
|||
| 7f975f2dbb | |||
|
5e1e249052
|
|||
|
b376bbd2aa
|
|||
|
51ae16e57f
|
|||
|
2ac3180c0f
|
|||
|
cea96e170d
|
|||
|
5906655902
|
|||
|
5f38355d5c
|
|||
|
010eb3f291
|
|||
|
0b1eca87b2
|
|||
|
f1d6ddbc84
|
|||
|
49d5aa4487
|
|||
|
4c68be19fe
|
|||
|
a6517c61a4
|
|||
|
2624f2cbf8
|
|||
|
cb4a4e06c8
|
|||
|
5f5f024ae7
|
|||
|
ec7c775e25
|
|||
|
28e9d14aae
|
|||
|
eadc40392a
|
|||
|
6ec9f51d77
|
18
.gitea/workflows/ci.yml
Normal file
18
.gitea/workflows/ci.yml
Normal file
@@ -0,0 +1,18 @@
|
||||
name: CI
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
jobs:
|
||||
test_and_lint:
|
||||
name: Test and lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
- uses: denoland/setup-deno@v2
|
||||
with:
|
||||
deno-version: v2.1.x
|
||||
- run: "deno task test"
|
||||
- run: "deno lint"
|
||||
24
.gitea/workflows/release.yml
Normal file
24
.gitea/workflows/release.yml
Normal file
@@ -0,0 +1,24 @@
|
||||
name: Release
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- v**
|
||||
jobs:
|
||||
release:
|
||||
name: Release tagged version
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
- uses: denoland/setup-deno@v2
|
||||
with:
|
||||
deno-version: v2.1.x
|
||||
- run: "deno task compile:release"
|
||||
- uses: akkuman/gitea-release-action@v1
|
||||
with:
|
||||
files: |-
|
||||
build/**
|
||||
draft: true
|
||||
sha256sum: true
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,3 +1,3 @@
|
||||
.env
|
||||
users.yaml
|
||||
substr
|
||||
build/
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
html {
|
||||
font-size: 16px;
|
||||
}
|
||||
@@ -8,7 +7,7 @@ body {
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 4rem 0 0 0;
|
||||
margin: 4rem 0 1.6rem 0;
|
||||
}
|
||||
|
||||
h2, h3, h4 {
|
||||
@@ -27,7 +26,7 @@ a.anchor {
|
||||
}
|
||||
|
||||
code {
|
||||
font-size: 1rem;
|
||||
font-size: 0.9em;
|
||||
padding: 0.1em 0.3em;
|
||||
}
|
||||
|
||||
@@ -85,10 +84,6 @@ main header .draft-label {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
main header h1 {
|
||||
margin-bottom: 1.6rem;
|
||||
}
|
||||
|
||||
main header .meta {
|
||||
display: flex;
|
||||
column-gap: 1rem;
|
||||
@@ -113,11 +108,24 @@ main.profile-page header {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
main.profile-page header h1 {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
main.profile-page header .nip05 {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
main.profile-page img.avatar {
|
||||
height: 8rem;
|
||||
width: 8rem;
|
||||
}
|
||||
|
||||
main.profile-page details summary {
|
||||
cursor: pointer;
|
||||
margin-bottom: 1.6rem;
|
||||
}
|
||||
|
||||
main .article-list .item h3 {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
@@ -134,6 +142,14 @@ main article footer {
|
||||
margin-top: 5rem;
|
||||
}
|
||||
|
||||
.nip05 .verified,
|
||||
.nip05 .not-verified {
|
||||
margin-left: 0.3rem;
|
||||
padding: 0.1em 0.3em;
|
||||
border-radius: 5px;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/* Dropdown menu */
|
||||
|
||||
.dropdown {
|
||||
@@ -202,7 +218,7 @@ main article footer {
|
||||
margin: 4rem 1rem 8rem 1rem !important;
|
||||
}
|
||||
|
||||
.profile-page h1 {
|
||||
main.profile-page h1 {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,15 +61,29 @@ main header .draft-label {
|
||||
}
|
||||
|
||||
main header .meta .date {
|
||||
color: var(--text-color-discreet)
|
||||
color: var(--text-color-discreet);
|
||||
}
|
||||
|
||||
main header .meta .name a {
|
||||
color: var(--text-color-body);
|
||||
}
|
||||
|
||||
main.profile-page header .nip05 {
|
||||
color: var(--text-color-discreet);
|
||||
}
|
||||
|
||||
main.profile-page .pubkey {
|
||||
color: var(--text-color-discreet)
|
||||
color: var(--text-color-discreet);
|
||||
}
|
||||
|
||||
.nip05 .verified {
|
||||
background-color: #e8e3da;
|
||||
color: #027739;
|
||||
}
|
||||
|
||||
.nip05 .not-verified {
|
||||
background-color: #e8e3da;
|
||||
color: #770202;
|
||||
}
|
||||
|
||||
/* Dropdown menu */
|
||||
|
||||
@@ -45,6 +45,8 @@ const config = {
|
||||
password: Deno.env.get("LDAP_PASSWORD"),
|
||||
searchDN: Deno.env.get("LDAP_SEARCH_DN"),
|
||||
},
|
||||
query_timeout: parseInt(Deno.env.get("RELAY_TIMEOUT_MS") || "5000"),
|
||||
njump_url: Deno.env.get("NJUMP_URL") || "https://njump.me",
|
||||
};
|
||||
|
||||
const staticUsersConfigured = Object.keys(staticUsers).length > 0;
|
||||
@@ -56,6 +58,8 @@ export function ensureNecessaryConfigs() {
|
||||
"yellow",
|
||||
);
|
||||
Deno.exit(1);
|
||||
} else {
|
||||
log(`Relays: ${config.relay_urls.join(", ")}`, "green");
|
||||
}
|
||||
|
||||
if (staticUsersConfigured) {
|
||||
|
||||
27
deno.json
27
deno.json
@@ -1,18 +1,29 @@
|
||||
{
|
||||
"tasks": {
|
||||
"dev": "deno run --allow-all --watch server.ts",
|
||||
"server": "deno run --allow-all server.ts"
|
||||
"server": "deno run --allow-all server.ts",
|
||||
"lint": "deno lint",
|
||||
"test": "deno test --allow-read --allow-env",
|
||||
"create-build-dir": "mkdir -p build",
|
||||
"compile": {
|
||||
"command": "deno compile --allow-all --include ./assets/ --output ./build/substr_x86_64-unknown-linux-gnu server.ts",
|
||||
"dependencies": ["create-build-dir"]
|
||||
},
|
||||
"compile:release": {
|
||||
"command": "echo LFG",
|
||||
"dependencies": ["lint", "test", "compile"]
|
||||
}
|
||||
},
|
||||
"imports": {
|
||||
"@deno/gfm": "jsr:@deno/gfm@^0.9.0",
|
||||
"@nostr/tools": "jsr:@nostr/tools@^2.3.1",
|
||||
"@nostrify/nostrify": "jsr:@nostrify/nostrify@^0.36.1",
|
||||
"@oak/oak": "jsr:@oak/oak@^17.1.0",
|
||||
"@deno/gfm": "jsr:@deno/gfm@^0.10.0",
|
||||
"@nostr/tools": "jsr:@nostr/tools@^2.10.4",
|
||||
"@nostrify/nostrify": "jsr:@nostrify/nostrify@^0.36.2",
|
||||
"@oak/oak": "jsr:@oak/oak@^17.1.3",
|
||||
"@std/dotenv": "jsr:@std/dotenv@^0.225.2",
|
||||
"@std/expect": "jsr:@std/expect@^1.0.5",
|
||||
"@std/testing": "jsr:@std/testing@^1.0.3",
|
||||
"@std/expect": "jsr:@std/expect@^1.0.8",
|
||||
"@std/testing": "jsr:@std/testing@^1.0.5",
|
||||
"@std/yaml": "jsr:@std/yaml@^1.0.5",
|
||||
"ldapts": "npm:ldapts@^7.2.1"
|
||||
"ldapts": "npm:ldapts@^7.2.2"
|
||||
},
|
||||
"fmt": {
|
||||
"exclude": [
|
||||
|
||||
215
deno.lock
generated
215
deno.lock
generated
@@ -1,60 +1,67 @@
|
||||
{
|
||||
"version": "4",
|
||||
"specifiers": {
|
||||
"jsr:@deno/gfm@0.9": "0.9.0",
|
||||
"jsr:@deno/gfm@0.10": "0.10.0",
|
||||
"jsr:@denosaurs/emoji@0.3": "0.3.1",
|
||||
"jsr:@nostr/tools@^2.3.1": "2.3.1",
|
||||
"jsr:@nostrify/nostrify@~0.36.1": "0.36.1",
|
||||
"jsr:@nostr/tools@^2.10.4": "2.10.4",
|
||||
"jsr:@nostrify/nostrify@~0.36.2": "0.36.2",
|
||||
"jsr:@nostrify/types@0.35": "0.35.0",
|
||||
"jsr:@oak/commons@1": "1.0.0",
|
||||
"jsr:@oak/oak@^17.1.0": "17.1.0",
|
||||
"jsr:@std/assert@1": "1.0.6",
|
||||
"jsr:@std/assert@^1.0.6": "1.0.6",
|
||||
"jsr:@std/bytes@1": "1.0.2",
|
||||
"jsr:@std/bytes@^1.0.2": "1.0.2",
|
||||
"jsr:@oak/oak@^17.1.3": "17.1.3",
|
||||
"jsr:@std/assert@0.224": "0.224.0",
|
||||
"jsr:@std/assert@1": "1.0.8",
|
||||
"jsr:@std/assert@^1.0.8": "1.0.8",
|
||||
"jsr:@std/bytes@1": "1.0.4",
|
||||
"jsr:@std/bytes@^1.0.2": "1.0.4",
|
||||
"jsr:@std/crypto@0.224": "0.224.0",
|
||||
"jsr:@std/crypto@1": "1.0.3",
|
||||
"jsr:@std/data-structures@^1.0.4": "1.0.4",
|
||||
"jsr:@std/dotenv@~0.225.2": "0.225.2",
|
||||
"jsr:@std/encoding@0.224": "0.224.3",
|
||||
"jsr:@std/encoding@1": "1.0.5",
|
||||
"jsr:@std/encoding@^1.0.5": "1.0.5",
|
||||
"jsr:@std/encoding@~0.224.1": "0.224.3",
|
||||
"jsr:@std/expect@^1.0.5": "1.0.5",
|
||||
"jsr:@std/fs@^1.0.4": "1.0.4",
|
||||
"jsr:@std/http@1": "1.0.8",
|
||||
"jsr:@std/internal@^1.0.4": "1.0.4",
|
||||
"jsr:@std/expect@^1.0.8": "1.0.8",
|
||||
"jsr:@std/fs@^1.0.5": "1.0.5",
|
||||
"jsr:@std/http@1": "1.0.10",
|
||||
"jsr:@std/internal@^1.0.5": "1.0.5",
|
||||
"jsr:@std/io@0.224": "0.224.9",
|
||||
"jsr:@std/media-types@1": "1.0.3",
|
||||
"jsr:@std/path@1": "1.0.6",
|
||||
"jsr:@std/path@^1.0.6": "1.0.6",
|
||||
"jsr:@std/testing@^1.0.3": "1.0.3",
|
||||
"jsr:@std/media-types@1": "1.1.0",
|
||||
"jsr:@std/path@1": "1.0.8",
|
||||
"jsr:@std/path@^1.0.7": "1.0.8",
|
||||
"jsr:@std/path@^1.0.8": "1.0.8",
|
||||
"jsr:@std/testing@^1.0.5": "1.0.5",
|
||||
"jsr:@std/yaml@*": "1.0.5",
|
||||
"jsr:@std/yaml@^1.0.5": "1.0.5",
|
||||
"npm:@noble/ciphers@~0.5.1": "0.5.3",
|
||||
"npm:@noble/curves@1.2.0": "1.2.0",
|
||||
"npm:@noble/hashes@1.3.1": "1.3.1",
|
||||
"npm:@scure/base@1.1.1": "1.1.1",
|
||||
"npm:@scure/bip32@^1.4.0": "1.4.0",
|
||||
"npm:@scure/bip39@^1.3.0": "1.3.0",
|
||||
"npm:@scure/base@^1.1.6": "1.2.1",
|
||||
"npm:@scure/bip32@1.3.1": "1.3.1",
|
||||
"npm:@scure/bip32@^1.4.0": "1.6.0",
|
||||
"npm:@scure/bip39@1.2.1": "1.2.1",
|
||||
"npm:@scure/bip39@^1.3.0": "1.5.0",
|
||||
"npm:github-slugger@2": "2.0.0",
|
||||
"npm:he@^1.2.0": "1.2.0",
|
||||
"npm:katex@0.16": "0.16.11",
|
||||
"npm:ldapts@^7.2.1": "7.2.1",
|
||||
"npm:lru-cache@^10.2.0": "10.2.2",
|
||||
"npm:marked-alert@2": "2.1.0_marked@12.0.2",
|
||||
"npm:ldapts@^7.2.2": "7.2.2",
|
||||
"npm:lru-cache@^10.2.0": "10.4.3",
|
||||
"npm:marked-alert@2": "2.1.2_marked@12.0.2",
|
||||
"npm:marked-footnote@^1.2.0": "1.2.4_marked@12.0.2",
|
||||
"npm:marked-gfm-heading-id@^3.1.0": "3.2.0_marked@12.0.2",
|
||||
"npm:marked@12": "12.0.2",
|
||||
"npm:nostr-tools@^2.7.0": "2.7.0",
|
||||
"npm:path-to-regexp@*": "6.2.1",
|
||||
"npm:nostr-tools@^2.7.0": "2.10.4",
|
||||
"npm:nostr-wasm@0.1.0": "0.1.0",
|
||||
"npm:path-to-regexp@6.2.1": "6.2.1",
|
||||
"npm:prismjs@^1.29.0": "1.29.0",
|
||||
"npm:sanitize-html@^2.11.0": "2.13.1",
|
||||
"npm:sanitize-html@^2.13.0": "2.13.1",
|
||||
"npm:websocket-ts@^2.1.5": "2.1.5",
|
||||
"npm:zod@^3.23.8": "3.23.8"
|
||||
},
|
||||
"jsr": {
|
||||
"@deno/gfm@0.9.0": {
|
||||
"integrity": "9002dbdb6e382e247509edfeae3afdb9232f5ca98a8210ef186d42084e9ded30",
|
||||
"@deno/gfm@0.10.0": {
|
||||
"integrity": "51708205e3559a4aeb6afb29d07c5bfafe7941f91bb360351ef6621de9a39527",
|
||||
"dependencies": [
|
||||
"jsr:@denosaurs/emoji",
|
||||
"npm:github-slugger",
|
||||
@@ -71,22 +78,27 @@
|
||||
"@denosaurs/emoji@0.3.1": {
|
||||
"integrity": "b0aed5f55dec99e83da7c9637fe0a36d1d6252b7c99deaaa3fc5dea3fcf3da8b"
|
||||
},
|
||||
"@nostr/tools@2.3.1": {
|
||||
"integrity": "af01dc45cb28784c584d7a0699707196f397bcc53946efa582a01b11ddde4d61",
|
||||
"@nostr/tools@2.10.4": {
|
||||
"integrity": "7fda015c96b4f674727843aecb990e2af1989e4724588415ccf6f69066abfd4f",
|
||||
"dependencies": [
|
||||
"npm:@noble/ciphers",
|
||||
"npm:@noble/curves",
|
||||
"npm:@noble/hashes",
|
||||
"npm:@scure/base"
|
||||
"npm:@scure/base@1.1.1",
|
||||
"npm:@scure/bip32@1.3.1",
|
||||
"npm:@scure/bip39@1.2.1",
|
||||
"npm:nostr-wasm"
|
||||
]
|
||||
},
|
||||
"@nostrify/nostrify@0.36.1": {
|
||||
"integrity": "f76c803c0bda5df1c172f25d2313980344b0431df2a973ab3e1dd61e9e7b4b1a",
|
||||
"@nostrify/nostrify@0.36.2": {
|
||||
"integrity": "cc4787ca170b623a2e5dfed1baa4426077daa6143af728ea7dd325d58f4d04d6",
|
||||
"dependencies": [
|
||||
"jsr:@nostrify/types",
|
||||
"jsr:@std/crypto@0.224",
|
||||
"jsr:@std/encoding@~0.224.1",
|
||||
"npm:@scure/bip32",
|
||||
"npm:@scure/bip39",
|
||||
"npm:@scure/base@^1.1.6",
|
||||
"npm:@scure/bip32@^1.4.0",
|
||||
"npm:@scure/bip39@^1.3.0",
|
||||
"npm:lru-cache",
|
||||
"npm:nostr-tools",
|
||||
"npm:websocket-ts",
|
||||
@@ -101,34 +113,44 @@
|
||||
"dependencies": [
|
||||
"jsr:@std/assert@1",
|
||||
"jsr:@std/bytes@1",
|
||||
"jsr:@std/crypto",
|
||||
"jsr:@std/crypto@1",
|
||||
"jsr:@std/encoding@1",
|
||||
"jsr:@std/http",
|
||||
"jsr:@std/media-types"
|
||||
]
|
||||
},
|
||||
"@oak/oak@17.1.0": {
|
||||
"integrity": "14ffb400c3c268bdc7b3a838664fab782b4ed35bb0dfe7669013c95bb12a9503",
|
||||
"@oak/oak@17.1.3": {
|
||||
"integrity": "d89296c22db91681dd3a2a1e1fd14e258d0d5a9654de55637aee5b661c159f33",
|
||||
"dependencies": [
|
||||
"jsr:@oak/commons",
|
||||
"jsr:@std/assert@1",
|
||||
"jsr:@std/bytes@1",
|
||||
"jsr:@std/crypto",
|
||||
"jsr:@std/crypto@1",
|
||||
"jsr:@std/http",
|
||||
"jsr:@std/io",
|
||||
"jsr:@std/media-types",
|
||||
"jsr:@std/path@1",
|
||||
"npm:path-to-regexp@6.2.1"
|
||||
"npm:path-to-regexp"
|
||||
]
|
||||
},
|
||||
"@std/assert@1.0.6": {
|
||||
"integrity": "1904c05806a25d94fe791d6d883b685c9e2dcd60e4f9fc30f4fc5cf010c72207",
|
||||
"@std/assert@0.224.0": {
|
||||
"integrity": "8643233ec7aec38a940a8264a6e3eed9bfa44e7a71cc6b3c8874213ff401967f"
|
||||
},
|
||||
"@std/assert@1.0.8": {
|
||||
"integrity": "ebe0bd7eb488ee39686f77003992f389a06c3da1bbd8022184804852b2fa641b",
|
||||
"dependencies": [
|
||||
"jsr:@std/internal"
|
||||
]
|
||||
},
|
||||
"@std/bytes@1.0.2": {
|
||||
"integrity": "fbdee322bbd8c599a6af186a1603b3355e59a5fb1baa139f8f4c3c9a1b3e3d57"
|
||||
"@std/bytes@1.0.4": {
|
||||
"integrity": "11a0debe522707c95c7b7ef89b478c13fb1583a7cfb9a85674cd2cc2e3a28abc"
|
||||
},
|
||||
"@std/crypto@0.224.0": {
|
||||
"integrity": "154ef3ff08ef535562ef1a718718c5b2c5fc3808f0f9100daad69e829bfcdf2d",
|
||||
"dependencies": [
|
||||
"jsr:@std/assert@0.224",
|
||||
"jsr:@std/encoding@0.224"
|
||||
]
|
||||
},
|
||||
"@std/crypto@1.0.3": {
|
||||
"integrity": "a2a32f51ddef632d299e3879cd027c630dcd4d1d9a5285d6e6788072f4e51e7f"
|
||||
@@ -145,27 +167,27 @@
|
||||
"@std/encoding@1.0.5": {
|
||||
"integrity": "ecf363d4fc25bd85bd915ff6733a7e79b67e0e7806334af15f4645c569fefc04"
|
||||
},
|
||||
"@std/expect@1.0.5": {
|
||||
"integrity": "8c7ac797e2ffe57becc6399c0f2fd06230cb9ef124d45229c6e592c563824af1",
|
||||
"@std/expect@1.0.8": {
|
||||
"integrity": "27e40d8f3aefb372fc6a703fb0b69e34560e72a2f78705178babdffa00119a5f",
|
||||
"dependencies": [
|
||||
"jsr:@std/assert@^1.0.6",
|
||||
"jsr:@std/assert@^1.0.8",
|
||||
"jsr:@std/internal"
|
||||
]
|
||||
},
|
||||
"@std/fs@1.0.4": {
|
||||
"integrity": "2907d32d8d1d9e540588fd5fe0ec21ee638134bd51df327ad4e443aaef07123c",
|
||||
"@std/fs@1.0.5": {
|
||||
"integrity": "41806ad6823d0b5f275f9849a2640d87e4ef67c51ee1b8fb02426f55e02fd44e",
|
||||
"dependencies": [
|
||||
"jsr:@std/path@^1.0.6"
|
||||
"jsr:@std/path@^1.0.7"
|
||||
]
|
||||
},
|
||||
"@std/http@1.0.8": {
|
||||
"integrity": "6ea1b2e8d33929967754a3b6d6c6f399ad6647d7bbb5a466c1eaf9b294a6ebcd",
|
||||
"@std/http@1.0.10": {
|
||||
"integrity": "4e32d11493ab04e3ef09f104f0cb9beb4228b1d4b47c5469573c2c294c0d3692",
|
||||
"dependencies": [
|
||||
"jsr:@std/encoding@^1.0.5"
|
||||
]
|
||||
},
|
||||
"@std/internal@1.0.4": {
|
||||
"integrity": "62e8e4911527e5e4f307741a795c0b0a9e6958d0b3790716ae71ce085f755422"
|
||||
"@std/internal@1.0.5": {
|
||||
"integrity": "54a546004f769c1ac9e025abd15a76b6671ddc9687e2313b67376125650dc7ba"
|
||||
},
|
||||
"@std/io@0.224.9": {
|
||||
"integrity": "4414664b6926f665102e73c969cfda06d2c4c59bd5d0c603fd4f1b1c840d6ee3",
|
||||
@@ -173,20 +195,20 @@
|
||||
"jsr:@std/bytes@^1.0.2"
|
||||
]
|
||||
},
|
||||
"@std/media-types@1.0.3": {
|
||||
"integrity": "b12d30a7852f7578f4d210622df713bbfd1cbdd9b4ec2eaf5c1845ab70bab159"
|
||||
"@std/media-types@1.1.0": {
|
||||
"integrity": "c9d093f0c05c3512932b330e3cc1fe1d627b301db33a4c2c2185c02471d6eaa4"
|
||||
},
|
||||
"@std/path@1.0.6": {
|
||||
"integrity": "ab2c55f902b380cf28e0eec501b4906e4c1960d13f00e11cfbcd21de15f18fed"
|
||||
"@std/path@1.0.8": {
|
||||
"integrity": "548fa456bb6a04d3c1a1e7477986b6cffbce95102d0bb447c67c4ee70e0364be"
|
||||
},
|
||||
"@std/testing@1.0.3": {
|
||||
"integrity": "f98c2bee53860a5916727d7e7d3abe920dd6f9edace022e2d059f00d05c2cf42",
|
||||
"@std/testing@1.0.5": {
|
||||
"integrity": "6e693cbec94c81a1ad3df668685c7ba8e20742bb10305bc7137faa5cf16d2ec4",
|
||||
"dependencies": [
|
||||
"jsr:@std/assert@^1.0.6",
|
||||
"jsr:@std/assert@^1.0.8",
|
||||
"jsr:@std/data-structures",
|
||||
"jsr:@std/fs",
|
||||
"jsr:@std/internal",
|
||||
"jsr:@std/path@^1.0.6"
|
||||
"jsr:@std/path@^1.0.8"
|
||||
]
|
||||
},
|
||||
"@std/yaml@1.0.5": {
|
||||
@@ -209,10 +231,10 @@
|
||||
"@noble/hashes@1.3.2"
|
||||
]
|
||||
},
|
||||
"@noble/curves@1.4.0": {
|
||||
"integrity": "sha512-p+4cb332SFCrReJkCYe8Xzm0OWi4Jji5jVdIZRL/PmacmDkFNw6MrrV+gGpiPxLHbV+zKFRywUWbaseT+tZRXg==",
|
||||
"@noble/curves@1.7.0": {
|
||||
"integrity": "sha512-UTMhXK9SeDhFJVrHeUJ5uZlI6ajXg10O6Ddocf9S6GjbSBVZsJo88HzKwXznNfGpMTRDyJkqMjNDPYgf0qFWnw==",
|
||||
"dependencies": [
|
||||
"@noble/hashes@1.4.0"
|
||||
"@noble/hashes@1.6.0"
|
||||
]
|
||||
},
|
||||
"@noble/hashes@1.3.1": {
|
||||
@@ -221,43 +243,46 @@
|
||||
"@noble/hashes@1.3.2": {
|
||||
"integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ=="
|
||||
},
|
||||
"@noble/hashes@1.4.0": {
|
||||
"integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg=="
|
||||
"@noble/hashes@1.6.0": {
|
||||
"integrity": "sha512-YUULf0Uk4/mAA89w+k3+yUYh6NrEvxZa5T6SY3wlMvE2chHkxFUUIDI8/XW1QSC357iA5pSnqt7XEhvFOqmDyQ=="
|
||||
},
|
||||
"@noble/hashes@1.6.1": {
|
||||
"integrity": "sha512-pq5D8h10hHBjyqX+cfBm0i8JUXJ0UhczFc4r74zbuT9XgewFo2E3J1cOaGtdZynILNmQ685YWGzGE1Zv6io50w=="
|
||||
},
|
||||
"@scure/base@1.1.1": {
|
||||
"integrity": "sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA=="
|
||||
},
|
||||
"@scure/base@1.1.7": {
|
||||
"integrity": "sha512-PPNYBslrLNNUQ/Yad37MHYsNQtK67EhWb6WtSvNLLPo7SdVZgkUjD6Dg+5On7zNwmskf8OX7I7Nx5oN+MIWE0g=="
|
||||
"@scure/base@1.2.1": {
|
||||
"integrity": "sha512-DGmGtC8Tt63J5GfHgfl5CuAXh96VF/LD8K9Hr/Gv0J2lAoRGlPOMpqMpMbCTOoOJMZCk2Xt+DskdDyn6dEFdzQ=="
|
||||
},
|
||||
"@scure/bip32@1.3.1": {
|
||||
"integrity": "sha512-osvveYtyzdEVbt3OfwwXFr4P2iVBL5u1Q3q4ONBfDY/UpOuXmOlbgwc1xECEboY8wIays8Yt6onaWMUdUbfl0A==",
|
||||
"dependencies": [
|
||||
"@noble/curves@1.1.0",
|
||||
"@noble/hashes@1.3.2",
|
||||
"@scure/base@1.1.7"
|
||||
"@scure/base@1.1.1"
|
||||
]
|
||||
},
|
||||
"@scure/bip32@1.4.0": {
|
||||
"integrity": "sha512-sVUpc0Vq3tXCkDGYVWGIZTRfnvu8LoTDaev7vbwh0omSvVORONr960MQWdKqJDCReIEmTj3PAr73O3aoxz7OPg==",
|
||||
"@scure/bip32@1.6.0": {
|
||||
"integrity": "sha512-82q1QfklrUUdXJzjuRU7iG7D7XiFx5PHYVS0+oeNKhyDLT7WPqs6pBcM2W5ZdwOwKCwoE1Vy1se+DHjcXwCYnA==",
|
||||
"dependencies": [
|
||||
"@noble/curves@1.4.0",
|
||||
"@noble/hashes@1.4.0",
|
||||
"@scure/base@1.1.7"
|
||||
"@noble/curves@1.7.0",
|
||||
"@noble/hashes@1.6.1",
|
||||
"@scure/base@1.2.1"
|
||||
]
|
||||
},
|
||||
"@scure/bip39@1.2.1": {
|
||||
"integrity": "sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg==",
|
||||
"dependencies": [
|
||||
"@noble/hashes@1.3.2",
|
||||
"@scure/base@1.1.7"
|
||||
"@scure/base@1.1.1"
|
||||
]
|
||||
},
|
||||
"@scure/bip39@1.3.0": {
|
||||
"integrity": "sha512-disdg7gHuTDZtY+ZdkmLpPCk7fxZSu3gBiEGuoC1XYxv9cGx3Z6cpTggCgW6odSOOIXCiDjuGejW+aJKCY/pIQ==",
|
||||
"@scure/bip39@1.5.0": {
|
||||
"integrity": "sha512-Dop+ASYhnrwm9+HA/HwXg7j2ZqM6yk2fyLWb5znexjctFY3+E+eU8cIWI0Pql0Qx4hPZCijlGq4OL71g+Uz30A==",
|
||||
"dependencies": [
|
||||
"@noble/hashes@1.4.0",
|
||||
"@scure/base@1.1.7"
|
||||
"@noble/hashes@1.6.1",
|
||||
"@scure/base@1.2.1"
|
||||
]
|
||||
},
|
||||
"@types/asn1@0.2.4": {
|
||||
@@ -345,8 +370,8 @@
|
||||
"commander"
|
||||
]
|
||||
},
|
||||
"ldapts@7.2.1": {
|
||||
"integrity": "sha512-2NSA9drjHdRiApF+TO18c+Hy/uyBLs96OS6Gia4+dPQWPxvqDbu3Ji2beCbNCXTvvgxDj4cLZ0WoOZLt5ojfAg==",
|
||||
"ldapts@7.2.2": {
|
||||
"integrity": "sha512-UotAq24/vJEz0m3w/jgwZm7JGNw8M6vexL/5KU5pe3aIZWBkT/HRhjsPw/buRqKSK5Y0vTu5Zv8iyPgQF7ozzg==",
|
||||
"dependencies": [
|
||||
"@types/asn1",
|
||||
"asn1",
|
||||
@@ -356,11 +381,11 @@
|
||||
"whatwg-url"
|
||||
]
|
||||
},
|
||||
"lru-cache@10.2.2": {
|
||||
"integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ=="
|
||||
"lru-cache@10.4.3": {
|
||||
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="
|
||||
},
|
||||
"marked-alert@2.1.0_marked@12.0.2": {
|
||||
"integrity": "sha512-X95Z8PCDgWa0bBfM70GxZG3LD/leUrhXc3cx3w1eFExBhswd1oXn/S4S+9H8ypPdCY7okREb4dItUOc+VJq4jQ==",
|
||||
"marked-alert@2.1.2_marked@12.0.2": {
|
||||
"integrity": "sha512-EFNRZ08d8L/iEIPLTlQMDjvwIsj03gxWCczYTht6DCiHJIZhMk4NK5gtPY9UqAYb09eV5VGT+jD4lp396E0I+w==",
|
||||
"dependencies": [
|
||||
"marked"
|
||||
]
|
||||
@@ -387,8 +412,8 @@
|
||||
"nanoid@3.3.7": {
|
||||
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g=="
|
||||
},
|
||||
"nostr-tools@2.7.0": {
|
||||
"integrity": "sha512-jJoL2J1CBiKDxaXZww27nY/Wsuxzx7AULxmGKFce4sskDu1tohNyfnzYQ8BvDyvkstU8kNZUAXPL32tre33uig==",
|
||||
"nostr-tools@2.10.4": {
|
||||
"integrity": "sha512-biU7sk+jxHgVASfobg2T5ttxOGGSt69wEVBC51sHHOEaKAAdzHBLV/I2l9Rf61UzClhliZwNouYhqIso4a3HYg==",
|
||||
"dependencies": [
|
||||
"@noble/ciphers",
|
||||
"@noble/curves@1.2.0",
|
||||
@@ -454,8 +479,8 @@
|
||||
"undici-types@6.19.8": {
|
||||
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="
|
||||
},
|
||||
"uuid@10.0.0": {
|
||||
"integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ=="
|
||||
"uuid@11.0.3": {
|
||||
"integrity": "sha512-d0z310fCWv5dJwnX1Y/MncBAqGMKEzlBb1AOf7z9K8ALnd0utBX/msg/fA0+sbyN1ihbMsLhrBlnl1ak7Wa0rg=="
|
||||
},
|
||||
"webidl-conversions@7.0.0": {
|
||||
"integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g=="
|
||||
@@ -488,15 +513,15 @@
|
||||
},
|
||||
"workspace": {
|
||||
"dependencies": [
|
||||
"jsr:@deno/gfm@0.9",
|
||||
"jsr:@nostr/tools@^2.3.1",
|
||||
"jsr:@nostrify/nostrify@~0.36.1",
|
||||
"jsr:@oak/oak@^17.1.0",
|
||||
"jsr:@deno/gfm@0.10",
|
||||
"jsr:@nostr/tools@^2.10.4",
|
||||
"jsr:@nostrify/nostrify@~0.36.2",
|
||||
"jsr:@oak/oak@^17.1.3",
|
||||
"jsr:@std/dotenv@~0.225.2",
|
||||
"jsr:@std/expect@^1.0.5",
|
||||
"jsr:@std/testing@^1.0.3",
|
||||
"jsr:@std/expect@^1.0.8",
|
||||
"jsr:@std/testing@^1.0.5",
|
||||
"jsr:@std/yaml@^1.0.5",
|
||||
"npm:ldapts@^7.2.1"
|
||||
"npm:ldapts@^7.2.2"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
19
feeds.ts
19
feeds.ts
@@ -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 `
|
||||
`tag:${profile.nip05},nostr-p-${profile.pubkey}-d-${article.identifier}-k-30023`;
|
||||
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"?>
|
||||
@@ -31,7 +36,7 @@ export function profileAtomFeed(profile: Profile, articles: Article[]) {
|
||||
<title>${profile.name} on Nostr (Articles)</title>
|
||||
<id>${feedId}</id>
|
||||
<updated>${isoDate(lastUpdate)}</updated>
|
||||
<icon>${profile.picture}</icon>
|
||||
<icon>${profile.avatarImageUrl}</icon>
|
||||
<author>
|
||||
<name>${name}</name>
|
||||
</author>
|
||||
|
||||
20
handlers/errors.ts
Normal file
20
handlers/errors.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { Context } from "@oak/oak";
|
||||
import { errorPageHtml } from "../html.ts";
|
||||
|
||||
export const notFoundHandler = function (ctx: Context) {
|
||||
const html = errorPageHtml(404, "Not found");
|
||||
ctx.response.body = html;
|
||||
ctx.response.status = 404;
|
||||
};
|
||||
|
||||
export const badGatewayHandler = function (ctx: Context) {
|
||||
const html = errorPageHtml(502, "Bad gateway");
|
||||
ctx.response.body = html;
|
||||
ctx.response.status = 502;
|
||||
};
|
||||
|
||||
export const internalServerErrorHandler = function (ctx: Context) {
|
||||
const html = errorPageHtml(500, "Internal server error");
|
||||
ctx.response.body = html;
|
||||
ctx.response.status = 500;
|
||||
};
|
||||
@@ -1,22 +1,25 @@
|
||||
import { Context } from "@oak/oak";
|
||||
import { nip19 } from "@nostr/tools";
|
||||
import { lookupUsernameByPubkey } from "../directory.ts";
|
||||
import notFoundHandler from "../handlers/not-found.ts";
|
||||
import { notFoundHandler } from "../handlers/errors.ts";
|
||||
|
||||
const naddrHandler = async function (ctx: Context) {
|
||||
const naddr = ctx.state.path;
|
||||
let data: nip19.AddressPointer;
|
||||
|
||||
try {
|
||||
const data = nip19.decode(naddr).data as nip19.AddressPointer;
|
||||
const username = await lookupUsernameByPubkey(data.pubkey);
|
||||
|
||||
if (username && data.identifier) {
|
||||
ctx.response.redirect(`/@${username}/${data.identifier}`);
|
||||
} else {
|
||||
notFoundHandler(ctx);
|
||||
}
|
||||
data = nip19.decode(naddr).data as nip19.AddressPointer;
|
||||
} catch (_e) {
|
||||
notFoundHandler(ctx);
|
||||
return;
|
||||
}
|
||||
|
||||
const username = await lookupUsernameByPubkey(data.pubkey);
|
||||
|
||||
if (username && data.identifier) {
|
||||
ctx.response.redirect(`/@${username}/${data.identifier}`);
|
||||
} else {
|
||||
notFoundHandler(ctx);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
import { Context } from "@oak/oak";
|
||||
import { errorPageHtml } from "../html.ts";
|
||||
|
||||
const notFoundHandler = function (ctx: Context) {
|
||||
const html = errorPageHtml(404, "Not found");
|
||||
ctx.response.body = html;
|
||||
ctx.response.status = 404;
|
||||
};
|
||||
|
||||
export default notFoundHandler;
|
||||
@@ -1,22 +1,25 @@
|
||||
import { Context } from "@oak/oak";
|
||||
import { nip19 } from "@nostr/tools";
|
||||
import { lookupUsernameByPubkey } from "../directory.ts";
|
||||
import notFoundHandler from "../handlers/not-found.ts";
|
||||
import { notFoundHandler } from "../handlers/errors.ts";
|
||||
|
||||
const nprofileHandler = async function (ctx: Context) {
|
||||
const nprofile = ctx.state.path;
|
||||
let data: nip19.ProfilePointer;
|
||||
|
||||
try {
|
||||
const data = nip19.decode(nprofile).data as nip19.ProfilePointer;
|
||||
const username = await lookupUsernameByPubkey(data.pubkey);
|
||||
|
||||
if (username) {
|
||||
ctx.response.redirect(`/@${username}`);
|
||||
} else {
|
||||
notFoundHandler(ctx);
|
||||
}
|
||||
data = nip19.decode(nprofile).data as nip19.ProfilePointer;
|
||||
} catch (_e) {
|
||||
notFoundHandler(ctx);
|
||||
return;
|
||||
}
|
||||
|
||||
const username = await lookupUsernameByPubkey(data.pubkey);
|
||||
|
||||
if (username) {
|
||||
ctx.response.redirect(`/@${username}`);
|
||||
} else {
|
||||
notFoundHandler(ctx);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,22 +1,25 @@
|
||||
import { Context } from "@oak/oak";
|
||||
import { nip19 } from "@nostr/tools";
|
||||
import { lookupUsernameByPubkey } from "../directory.ts";
|
||||
import notFoundHandler from "../handlers/not-found.ts";
|
||||
import { notFoundHandler } from "../handlers/errors.ts";
|
||||
|
||||
const npubHandler = async function (ctx: Context) {
|
||||
const npub = ctx.state.path;
|
||||
let pubkey: string;
|
||||
|
||||
try {
|
||||
const pubkey = nip19.decode(npub).data as string;
|
||||
const username = await lookupUsernameByPubkey(pubkey);
|
||||
|
||||
if (username) {
|
||||
ctx.response.redirect(`/@${username}`);
|
||||
} else {
|
||||
notFoundHandler(ctx);
|
||||
}
|
||||
pubkey = nip19.decode(npub).data as string;
|
||||
} catch (_e) {
|
||||
notFoundHandler(ctx);
|
||||
return;
|
||||
}
|
||||
|
||||
const username = await lookupUsernameByPubkey(pubkey);
|
||||
|
||||
if (username) {
|
||||
ctx.response.redirect(`/@${username}`);
|
||||
} else {
|
||||
notFoundHandler(ctx);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -2,9 +2,8 @@ import { Context } from "@oak/oak";
|
||||
import { lookupPubkeyByUsername } from "../directory.ts";
|
||||
import { fetchArticlesByAuthor, fetchProfileEvent } from "../nostr.ts";
|
||||
import { profileAtomFeed } from "../feeds.ts";
|
||||
import Article from "../models/article.ts";
|
||||
import Profile from "../models/profile.ts";
|
||||
import notFoundHandler from "../handlers/not-found.ts";
|
||||
import { notFoundHandler } from "../handlers/errors.ts";
|
||||
|
||||
const userAtomFeedHandler = async function (ctx: Context) {
|
||||
const username = ctx.state.username;
|
||||
@@ -15,26 +14,21 @@ const userAtomFeedHandler = async function (ctx: Context) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const profileEvent = await fetchProfileEvent(pubkey);
|
||||
const profileEvent = await fetchProfileEvent(pubkey);
|
||||
|
||||
if (profileEvent) {
|
||||
const profile = new Profile(profileEvent, username);
|
||||
if (profileEvent) {
|
||||
const profile = new Profile(profileEvent, username);
|
||||
|
||||
if (profile.nip05) {
|
||||
const articleEvents = await fetchArticlesByAuthor(pubkey);
|
||||
const articles = articleEvents.map((a) => new Article(a));
|
||||
const atom = profileAtomFeed(profile, articles);
|
||||
if (profile.nip05) {
|
||||
const articles = await fetchArticlesByAuthor(pubkey, 10);
|
||||
const atom = await profileAtomFeed(profile, articles);
|
||||
|
||||
ctx.response.headers.set("Content-Type", "application/atom+xml");
|
||||
ctx.response.body = atom;
|
||||
return;
|
||||
}
|
||||
ctx.response.headers.set("Content-Type", "application/atom+xml");
|
||||
ctx.response.body = atom;
|
||||
return;
|
||||
}
|
||||
notFoundHandler(ctx);
|
||||
} catch (_e) {
|
||||
notFoundHandler(ctx);
|
||||
}
|
||||
notFoundHandler(ctx);
|
||||
};
|
||||
|
||||
export default userAtomFeedHandler;
|
||||
|
||||
@@ -4,7 +4,7 @@ import { fetchProfileEvent, fetchReplaceableEvent } from "../nostr.ts";
|
||||
import Article from "../models/article.ts";
|
||||
import Profile from "../models/profile.ts";
|
||||
import { articleHtml } from "../html.ts";
|
||||
import notFoundHandler from "../handlers/not-found.ts";
|
||||
import { notFoundHandler } from "../handlers/errors.ts";
|
||||
import { generateOgProfileImage } from "../magick.ts";
|
||||
|
||||
const userEventHandler = async function (ctx: Context) {
|
||||
@@ -17,24 +17,20 @@ const userEventHandler = async function (ctx: Context) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const articleEvent = await fetchReplaceableEvent(
|
||||
pubkey,
|
||||
identifier,
|
||||
);
|
||||
const profileEvent = await fetchProfileEvent(pubkey);
|
||||
const articleEvent = await fetchReplaceableEvent(
|
||||
pubkey,
|
||||
identifier,
|
||||
);
|
||||
const profileEvent = await fetchProfileEvent(pubkey);
|
||||
|
||||
if (articleEvent && profileEvent) {
|
||||
const article = new Article(articleEvent);
|
||||
const profile = new Profile(profileEvent, username);
|
||||
const html = articleHtml(article, profile);
|
||||
generateOgProfileImage(profile);
|
||||
if (articleEvent && profileEvent) {
|
||||
const article = new Article(articleEvent);
|
||||
const profile = new Profile(profileEvent, username);
|
||||
const html = await articleHtml(article, profile);
|
||||
await generateOgProfileImage(profile);
|
||||
|
||||
ctx.response.body = html;
|
||||
} else {
|
||||
notFoundHandler(ctx);
|
||||
}
|
||||
} catch (_e) {
|
||||
ctx.response.body = html;
|
||||
} else {
|
||||
notFoundHandler(ctx);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { Context } from "@oak/oak";
|
||||
import { lookupPubkeyByUsername } from "../directory.ts";
|
||||
import { fetchArticlesByAuthor, fetchProfileEvent } from "../nostr.ts";
|
||||
import Article from "../models/article.ts";
|
||||
import Profile from "../models/profile.ts";
|
||||
import { profilePageHtml } from "../html.ts";
|
||||
import notFoundHandler from "../handlers/not-found.ts";
|
||||
import { notFoundHandler } from "../handlers/errors.ts";
|
||||
import { generateOgProfileImage } from "../magick.ts";
|
||||
|
||||
const userProfileHandler = async function (ctx: Context) {
|
||||
@@ -16,21 +15,16 @@ const userProfileHandler = async function (ctx: Context) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const profileEvent = await fetchProfileEvent(pubkey);
|
||||
const profileEvent = await fetchProfileEvent(pubkey);
|
||||
|
||||
if (profileEvent) {
|
||||
const profile = new Profile(profileEvent, username);
|
||||
const articleEvents = await fetchArticlesByAuthor(pubkey);
|
||||
const articles = articleEvents.map((a) => new Article(a));
|
||||
const html = profilePageHtml(profile, articles);
|
||||
generateOgProfileImage(profile);
|
||||
if (profileEvent) {
|
||||
const profile = new Profile(profileEvent, username);
|
||||
const articles = await fetchArticlesByAuthor(pubkey, 210);
|
||||
const html = await profilePageHtml(profile, articles);
|
||||
await generateOgProfileImage(profile);
|
||||
|
||||
ctx.response.body = html;
|
||||
} else {
|
||||
notFoundHandler(ctx);
|
||||
}
|
||||
} catch (_e) {
|
||||
ctx.response.body = html;
|
||||
} else {
|
||||
notFoundHandler(ctx);
|
||||
}
|
||||
};
|
||||
|
||||
66
html.ts
66
html.ts
@@ -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 = ``;
|
||||
@@ -50,9 +53,9 @@ export function articleHtml(article: Article, profile: Profile): string {
|
||||
<main>
|
||||
<header>
|
||||
${draftLabel}
|
||||
<h1>${article.title}</h1>
|
||||
<h1>${titleHtml(article.title)}</h1>
|
||||
<div class="meta">
|
||||
<img class="avatar" src="${profile.picture}" alt="User Avatar" />
|
||||
<img class="avatar" src="${profile.avatarImageUrl}" alt="User Avatar" />
|
||||
<div class="content">
|
||||
<span class="name"><a href="/@${profile.username}">${profile.name}</a></span>
|
||||
<span class="date">${publishedAtFormatted}</span>
|
||||
@@ -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>
|
||||
@@ -74,12 +77,16 @@ export function articleHtml(article: Article, profile: Profile): string {
|
||||
return htmlLayout({ title: pageTitle, body, metaHtml });
|
||||
}
|
||||
|
||||
function titleHtml(title: string) {
|
||||
return title.replace(/`([^`]+)`/g, "<code>$1</code>");
|
||||
}
|
||||
|
||||
function articleListItemHtml(article: Article): string {
|
||||
const formattedDate = localizeDate(article.publishedAt);
|
||||
|
||||
return `
|
||||
<div class="item">
|
||||
<h3><a href="/${article.naddr}">${article.title}</a></h3>
|
||||
<h3><a href="/${article.naddr}">${titleHtml(article.title)}</a></h3>
|
||||
<p class="meta">
|
||||
${formattedDate}
|
||||
</p>
|
||||
@@ -89,10 +96,9 @@ function articleListItemHtml(article: Article): string {
|
||||
|
||||
export function articleListHtml(articles: Article[]): string {
|
||||
if (articles.length === 0) return "";
|
||||
const sortedArticles = articles.sort((a, b) => b.publishedAt - a.publishedAt);
|
||||
let html = "";
|
||||
|
||||
for (const article of sortedArticles) {
|
||||
for (const article of articles) {
|
||||
html += articleListItemHtml(article);
|
||||
}
|
||||
|
||||
@@ -104,26 +110,61 @@ export function articleListHtml(articles: Article[]): string {
|
||||
`;
|
||||
}
|
||||
|
||||
export function profilePageHtml(profile: Profile, articles: Article[]): string {
|
||||
function userAddressHtml(profile: Profile) {
|
||||
let html = "";
|
||||
|
||||
if (profile.nip05) {
|
||||
html += `<dt>Nostr address</dt><dd>${profile.nip05}</dd>\n`;
|
||||
}
|
||||
if (profile.lud16) {
|
||||
html += `<dt>Lightning address</dt><dd>${profile.lud16}</dd>\n`;
|
||||
}
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
function nip05VerifiedHtml(verified: boolean): string {
|
||||
if (verified) {
|
||||
return ` <span class="verified" title="Verified">✔</span>`;
|
||||
} else {
|
||||
return ` <span class="not-verified" title="Verification failed">✕</span>`;
|
||||
}
|
||||
}
|
||||
|
||||
export async function profilePageHtml(
|
||||
profile: Profile,
|
||||
articles: Article[],
|
||||
): Promise<string> {
|
||||
const title = `${profile.name} on Nostr`;
|
||||
let nip05Html = "";
|
||||
|
||||
if (profile.nip05) {
|
||||
const nip05Verified = await profile.verifyNip05();
|
||||
nip05Html += `<p class="nip05">${profile.nip05}${
|
||||
nip05VerifiedHtml(nip05Verified)
|
||||
}</p>\n`;
|
||||
}
|
||||
|
||||
const body = `
|
||||
<main class="profile-page">
|
||||
<header>
|
||||
<img class="avatar" src="${profile.picture}" alt="User Avatar" />
|
||||
<img class="avatar" src="${profile.avatarImageUrl}" alt="User Avatar" />
|
||||
<div class="bio">
|
||||
<h1>${profile.name}</h1>
|
||||
${nip05Html}
|
||||
<p class="about">
|
||||
${profile.about}
|
||||
</p>
|
||||
</div>
|
||||
</header>
|
||||
<section>
|
||||
<details>
|
||||
<summary>Details</summary>
|
||||
<dl>
|
||||
<dt>Public key</dt>
|
||||
<dd>${profile.npub}</dd>
|
||||
${userAddressHtml(profile)}
|
||||
</dl>
|
||||
</section>
|
||||
</details>
|
||||
<section>
|
||||
${articleListHtml(articles)}
|
||||
</section>
|
||||
@@ -145,6 +186,7 @@ function openWithNostrAppHtml(bech32Id: string): string {
|
||||
href: `https://nostrudel.ninja/#/articles/${bech32Id}`,
|
||||
},
|
||||
{ title: "Coracle", href: `https://coracle.social/${bech32Id}` },
|
||||
{ title: "YakiHonne", href: `https://yakihonne.com/article/${bech32Id}` },
|
||||
];
|
||||
|
||||
for (const link of appLinks) {
|
||||
@@ -169,6 +211,7 @@ function feedLinksHtml(profile: Profile) {
|
||||
|
||||
function profileMetaHtml(profile: Profile) {
|
||||
return `
|
||||
<link rel="icon" href="${profile.avatarImageUrl}" type="image/png">
|
||||
<meta property="og:url" content="${profile.profileUrl}">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:title" content="${profile.name} on Nostr">
|
||||
@@ -187,6 +230,7 @@ function articleMetaHtml(article: Article, profile: Profile) {
|
||||
const imageUrl = article.image || profile.ogImageUrl;
|
||||
|
||||
return `
|
||||
<link rel="icon" href="${profile.avatarImageUrl}" type="image/png">
|
||||
<meta property="og:url" content="${article.url}">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:title" content="${article.title}">
|
||||
|
||||
10
ldap.ts
10
ldap.ts
@@ -25,10 +25,11 @@ export async function lookupPubkeyByUsername(username: string) {
|
||||
) {
|
||||
pubkey = searchEntries[0].nostrKey;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
|
||||
await client.unbind();
|
||||
} catch (e) {
|
||||
await client.unbind();
|
||||
throw e;
|
||||
}
|
||||
|
||||
return pubkey;
|
||||
@@ -49,9 +50,8 @@ export async function lookupUsernameByPubkey(pubkey: string) {
|
||||
username = searchEntries[0].cn;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
await client.unbind();
|
||||
throw e;
|
||||
}
|
||||
|
||||
return username;
|
||||
|
||||
70
magick.ts
70
magick.ts
@@ -8,30 +8,52 @@ if (!magick) {
|
||||
log("ImageMagick is not installed. Cannot generate preview images", "yellow")
|
||||
}
|
||||
|
||||
function createRoundedImage(profile: Profile) {
|
||||
function createProfileImage(profile: Profile) {
|
||||
if (!magick || !profile.picture) return false;
|
||||
|
||||
const args = [
|
||||
profile.picture,
|
||||
'-resize', '256x256',
|
||||
'(', '+clone', '-alpha', 'extract',
|
||||
'-draw', "fill black polygon 0,0 0,128 128,0 fill white circle 128,128 128,0",
|
||||
'(', '+clone', '-flip', ')', '-compose', 'Multiply', '-composite',
|
||||
'(', '+clone', '-flop', ')', '-compose', 'Multiply', '-composite',
|
||||
')',
|
||||
'-alpha', 'off',
|
||||
'-compose', 'CopyOpacity',
|
||||
'-composite',
|
||||
`${tmpImgDir}/p-${profile.event.id}-rounded.png`
|
||||
'-resize', '256x256^',
|
||||
'-gravity', 'center',
|
||||
'-extent', '256x256',
|
||||
`${tmpImgDir}/p-${profile.event.id}.png`
|
||||
];
|
||||
|
||||
return runCommand(magick, args);
|
||||
}
|
||||
|
||||
async function createRoundedProfileImage(profile: Profile) {
|
||||
if (!magick || !profile.picture) return false;
|
||||
|
||||
const status = await generateProfileImage(profile);
|
||||
|
||||
if (status && status.success) {
|
||||
const args = [
|
||||
`${tmpImgDir}/p-${profile.event.id}.png`,
|
||||
'-resize', '256x256^',
|
||||
'-gravity', 'center',
|
||||
'-extent', '256x256',
|
||||
'(', '+clone', '-alpha', 'extract',
|
||||
'-draw', "fill black polygon 0,0 0,128 128,0 fill white circle 128,128 128,0",
|
||||
'(', '+clone', '-flip', ')', '-compose', 'Multiply', '-composite',
|
||||
'(', '+clone', '-flop', ')', '-compose', 'Multiply', '-composite',
|
||||
')',
|
||||
'-alpha', 'off',
|
||||
'-compose', 'CopyOpacity',
|
||||
'-composite',
|
||||
`${tmpImgDir}/p-${profile.event.id}-rounded.png`
|
||||
];
|
||||
|
||||
return runCommand(magick, args);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function createOgImage(profile: Profile, ogImagePath: string, backgroundColor: string) {
|
||||
if (!magick) return false;
|
||||
|
||||
const status = await createRoundedImage(profile);
|
||||
const status = await createRoundedProfileImage(profile);
|
||||
|
||||
if (status && status.success) {
|
||||
const args = [
|
||||
@@ -49,6 +71,25 @@ async function createOgImage(profile: Profile, ogImagePath: string, backgroundCo
|
||||
}
|
||||
};
|
||||
|
||||
export async function generateProfileImage(profile: Profile) {
|
||||
if (!magick || !profile.picture) return false;
|
||||
|
||||
const imagePath = `${tmpImgDir}/p-${profile.event.id}.png`;
|
||||
const fileExists = await checkFileExists(imagePath);
|
||||
|
||||
if (fileExists) {
|
||||
return { success: true };
|
||||
} else {
|
||||
const status = await createProfileImage(profile);
|
||||
if (status && status.success) {
|
||||
log(`Created avatar image for ${profile.username}: ${imagePath}`, "blue")
|
||||
return status;
|
||||
} else {
|
||||
log(`Could not create avatar image for ${profile.username}`, "yellow")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function generateOgProfileImage(profile: Profile) {
|
||||
if (!magick || !profile.picture) return false;
|
||||
|
||||
@@ -56,10 +97,13 @@ export async function generateOgProfileImage(profile: Profile) {
|
||||
const backgroundColor = "#333333";
|
||||
const fileExists = await checkFileExists(ogImagePath);
|
||||
|
||||
if (!fileExists) {
|
||||
if (fileExists) {
|
||||
return { success: true };
|
||||
} else {
|
||||
const status = await createOgImage(profile, ogImagePath, backgroundColor);
|
||||
if (status && status.success) {
|
||||
log(`Created OG image for ${profile.username}: ${ogImagePath}`, "blue")
|
||||
return status;
|
||||
} else {
|
||||
log(`Could not create OG image for ${profile.username}`, "yellow")
|
||||
}
|
||||
|
||||
@@ -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,8 +48,12 @@ export default class Article {
|
||||
return this.event.created_at;
|
||||
}
|
||||
|
||||
get html(): string {
|
||||
return renderMarkdown(this.event.content);
|
||||
get content(): string {
|
||||
return this.event.content;
|
||||
}
|
||||
|
||||
get isDeleted(): boolean {
|
||||
return !!this.event.tags.find((t) => t[0] === "deleted");
|
||||
}
|
||||
|
||||
get naddr(): string {
|
||||
@@ -59,4 +64,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { nip19, NostrEvent as NEvent } from "@nostr/tools";
|
||||
import { verifyNip05Address } from "../nostr.ts";
|
||||
import { getImageMagickCommand } from "../utils.ts";
|
||||
import config from "../config.ts";
|
||||
|
||||
@@ -43,7 +44,7 @@ export default class Profile {
|
||||
}
|
||||
|
||||
get nip05(): string | undefined {
|
||||
return this.data.nip05;
|
||||
return this.data.nip05?.replace("_@", "");
|
||||
}
|
||||
|
||||
get lud16(): string | undefined {
|
||||
@@ -62,6 +63,14 @@ export default class Profile {
|
||||
return `${config.base_url}/@${this.username}`;
|
||||
}
|
||||
|
||||
get avatarImageUrl(): string {
|
||||
if (magick) {
|
||||
return `${config.base_url}/assets/g/img/p-${this.event.id}.png`;
|
||||
} else {
|
||||
return this.picture || "";
|
||||
}
|
||||
}
|
||||
|
||||
get ogImageUrl(): string {
|
||||
if (magick) {
|
||||
return `${config.base_url}/assets/g/img/og-p-${this.event.id}.png`;
|
||||
@@ -69,4 +78,12 @@ export default class Profile {
|
||||
return this.picture || "";
|
||||
}
|
||||
}
|
||||
|
||||
verifyNip05(): Promise<boolean> {
|
||||
if (typeof this.data.nip05 !== "undefined") {
|
||||
return verifyNip05Address(this.data.nip05, this.pubkey);
|
||||
} else {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
107
nostr.ts
107
nostr.ts
@@ -1,5 +1,8 @@
|
||||
import { NPool, NRelay1 } from "@nostrify/nostrify";
|
||||
import { NostrEvent, NostrFilter, NPool, NRelay1 } from "@nostrify/nostrify";
|
||||
import { nip19 } from "@nostr/tools";
|
||||
import { lookupUsernameByPubkey } from "./directory.ts";
|
||||
import config from "./config.ts";
|
||||
import Article from "./models/article.ts";
|
||||
|
||||
const relayPool = new NPool({
|
||||
open: (url) => new NRelay1(url),
|
||||
@@ -12,47 +15,127 @@ const relayPool = new NPool({
|
||||
eventRouter: async (_event) => [],
|
||||
});
|
||||
|
||||
async function fetchWithTimeout(filters: NostrFilter[]) {
|
||||
const timeoutPromise = new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new Error("relay timeout")), config.query_timeout)
|
||||
);
|
||||
const eventsPromise = relayPool.query(filters);
|
||||
|
||||
const events = await Promise.race([eventsPromise, timeoutPromise]);
|
||||
return events;
|
||||
}
|
||||
|
||||
export async function fetchReplaceableEvent(
|
||||
pubkey: string,
|
||||
identifier: string,
|
||||
) {
|
||||
let events = await relayPool.query([{
|
||||
let events = await fetchWithTimeout([{
|
||||
authors: [pubkey],
|
||||
kinds: [30023],
|
||||
"#d": [identifier],
|
||||
limit: 1,
|
||||
}]);
|
||||
}]) as NostrEvent[];
|
||||
|
||||
if (events.length > 0) {
|
||||
return events[0];
|
||||
} else {
|
||||
events = await relayPool.query([{
|
||||
events = await fetchWithTimeout([{
|
||||
authors: [pubkey],
|
||||
kinds: [30024],
|
||||
"#d": [identifier],
|
||||
limit: 1,
|
||||
}]);
|
||||
}]) as NostrEvent[];
|
||||
|
||||
return events.length > 0 ? events[0] : null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchArticlesByAuthor(pubkey: string) {
|
||||
const events = await relayPool.query([{
|
||||
export async function fetchArticlesByAuthor(
|
||||
pubkey: string,
|
||||
limit: number = 10,
|
||||
) {
|
||||
const events = await fetchWithTimeout([{
|
||||
authors: [pubkey],
|
||||
kinds: [30023],
|
||||
limit: 10,
|
||||
}]);
|
||||
limit: limit,
|
||||
}]) as NostrEvent[];
|
||||
|
||||
return events;
|
||||
const articles = events.map((a) => new Article(a));
|
||||
|
||||
return articles
|
||||
.filter((a) => !a.isDeleted)
|
||||
.filter((a) => a.content.trim() !== "")
|
||||
.sort((a, b) => b.publishedAt - a.publishedAt)
|
||||
.slice(0, limit); // The limit seems to apply per relay, not per pool query
|
||||
}
|
||||
|
||||
export async function fetchProfileEvent(pubkey: string) {
|
||||
const events = await relayPool.query([{
|
||||
const events = await fetchWithTimeout([{
|
||||
authors: [pubkey],
|
||||
kinds: [0],
|
||||
limit: 1,
|
||||
}]);
|
||||
}]) as NostrEvent[];
|
||||
|
||||
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 uri of matches) {
|
||||
const url = await nostrUriToUrl(uri);
|
||||
markdown = markdown.replace(uri, url);
|
||||
}
|
||||
|
||||
return markdown;
|
||||
}
|
||||
|
||||
export async function verifyNip05Address(
|
||||
address: string,
|
||||
pubkey: string,
|
||||
): Promise<boolean> {
|
||||
const [username, host] = address.split("@");
|
||||
const url = `https://${host}/.well-known/nostr.json?name=${username}`;
|
||||
|
||||
try {
|
||||
const res = await fetch(url);
|
||||
if (res.status === 404 || !res.ok) return false;
|
||||
const data = await res.json();
|
||||
|
||||
return data.names && data.names[username] === pubkey;
|
||||
} catch (_e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
68
server.ts
68
server.ts
@@ -7,23 +7,36 @@ import npubHandler from "./handlers/npub.ts";
|
||||
import userProfileHandler from "./handlers/user-profile.ts";
|
||||
import userEventHandler from "./handlers/user-event.ts";
|
||||
import userAtomFeedHandler from "./handlers/user-atom-feed.ts";
|
||||
import notFoundHandler from "./handlers/not-found.ts";
|
||||
import {
|
||||
badGatewayHandler,
|
||||
internalServerErrorHandler,
|
||||
notFoundHandler,
|
||||
} from "./handlers/errors.ts";
|
||||
|
||||
const router = new Router();
|
||||
|
||||
router.get("/:path", async (ctx) => {
|
||||
const path = ctx.state.path = ctx.params.path;
|
||||
|
||||
if (path.startsWith("naddr")) {
|
||||
await naddrHandler(ctx);
|
||||
} else if (path.startsWith("nprofile")) {
|
||||
await nprofileHandler(ctx);
|
||||
} else if (path.startsWith("npub")) {
|
||||
await npubHandler(ctx);
|
||||
} else if (path.startsWith("@") || path.startsWith("~")) {
|
||||
await userProfileHandler(ctx);
|
||||
} else {
|
||||
notFoundHandler(ctx);
|
||||
try {
|
||||
if (path.startsWith("naddr")) {
|
||||
await naddrHandler(ctx);
|
||||
} else if (path.startsWith("nprofile")) {
|
||||
await nprofileHandler(ctx);
|
||||
} else if (path.startsWith("npub")) {
|
||||
await npubHandler(ctx);
|
||||
} else if (path.startsWith("@") || path.startsWith("~")) {
|
||||
await userProfileHandler(ctx);
|
||||
} else {
|
||||
notFoundHandler(ctx);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
if (e instanceof Error && e.message.match(/^connect|relay timeout/)) {
|
||||
badGatewayHandler(ctx);
|
||||
} else {
|
||||
internalServerErrorHandler(ctx);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -36,7 +49,16 @@ router.get("/:username/:kind.atom", async (ctx) => {
|
||||
(ctx.params.username.startsWith("@") ||
|
||||
ctx.params.username.startsWith("~"))
|
||||
) {
|
||||
await userAtomFeedHandler(ctx);
|
||||
try {
|
||||
await userAtomFeedHandler(ctx);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
if (e instanceof Error && e.message.match(/^connect|relay timeout/)) {
|
||||
badGatewayHandler(ctx);
|
||||
} else {
|
||||
internalServerErrorHandler(ctx);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
notFoundHandler(ctx);
|
||||
}
|
||||
@@ -47,7 +69,16 @@ router.get("/:username/:identifier", async (ctx) => {
|
||||
ctx.state.identifier = ctx.params.identifier;
|
||||
|
||||
if (username.startsWith("@") || username.startsWith("~")) {
|
||||
await userEventHandler(ctx);
|
||||
try {
|
||||
await userEventHandler(ctx);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
if (e instanceof Error && e.message.match(/^connect|relay timeout/)) {
|
||||
badGatewayHandler(ctx);
|
||||
} else {
|
||||
internalServerErrorHandler(ctx);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
notFoundHandler(ctx);
|
||||
}
|
||||
@@ -62,12 +93,17 @@ router.get("/assets/:path*", async (ctx) => {
|
||||
filePath = filePath.replace(/^g\//, "");
|
||||
root = "/tmp/substr";
|
||||
} else {
|
||||
root = `${Deno.cwd()}/assets`;
|
||||
root = `${import.meta.dirname}/assets`;
|
||||
}
|
||||
|
||||
await send(ctx, filePath, { root });
|
||||
} catch (_e) {
|
||||
notFoundHandler(ctx);
|
||||
} catch (e) {
|
||||
if (e instanceof Error && e.name === "NotFoundError") {
|
||||
notFoundHandler(ctx);
|
||||
} else {
|
||||
console.error(e);
|
||||
badGatewayHandler(ctx);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
9
tests/fixtures/article-deleted.json
vendored
Normal file
9
tests/fixtures/article-deleted.json
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"content": "",
|
||||
"created_at": 1716761766,
|
||||
"id": "ae83c3e23e11db5fd0e6dacaece38847451e81d1429e4182a0cadd409bdce30f",
|
||||
"kind": 30023,
|
||||
"pubkey": "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d",
|
||||
"sig": "6c21eba5324af302dfbfb9dadfc2d067646a3594ffed02d2528cca8ee0f7c16b9ffb3dc640420304882be94e789d93191d830108ac52d57b2e72445025e433b2",
|
||||
"tags": [["d", "66674915"], ["deleted"]]
|
||||
}
|
||||
@@ -1,17 +1,18 @@
|
||||
import { beforeAll, describe, it } from "@std/testing/bdd";
|
||||
import { expect } from "@std/expect";
|
||||
import { NostrEvent as NEvent } from "@nostrify/nostrify";
|
||||
import Article from "../../models/article.ts";
|
||||
|
||||
describe("Article", () => {
|
||||
let articleEvent: NEvent;
|
||||
let article: Article;
|
||||
let deletedArticle: Article;
|
||||
|
||||
beforeAll(() => {
|
||||
articleEvent = JSON.parse(
|
||||
article = new Article(JSON.parse(
|
||||
Deno.readTextFileSync("tests/fixtures/article-1.json"),
|
||||
);
|
||||
article = new Article(articleEvent);
|
||||
));
|
||||
deletedArticle = new Article(JSON.parse(
|
||||
Deno.readTextFileSync("tests/fixtures/article-deleted.json"),
|
||||
));
|
||||
});
|
||||
|
||||
describe("#identifier", () => {
|
||||
@@ -58,9 +59,10 @@ describe("Article", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("#html", () => {
|
||||
it("returns a rendered HTML version of the 'content'", () => {
|
||||
expect(article.html).toMatch(/<h2 id="the-solution">/);
|
||||
describe("#isDeleted", () => {
|
||||
it("returns a boolean based on the 'deleted' tag", () => {
|
||||
expect(article.isDeleted).toEqual(false);
|
||||
expect(deletedArticle.isDeleted).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -71,4 +73,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">/);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user