Compare commits
47 Commits
ec7c775e25
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 8d96b9fbc1 | |||
|
1c37d2dc99
|
|||
|
1e246af26e
|
|||
|
7fe0721416
|
|||
|
d3ad136eab
|
|||
|
cd335a366e
|
|||
|
b307d4de03
|
|||
|
91eff4ad07
|
|||
|
994053e080
|
|||
|
bc37756097
|
|||
|
3a5733eeee
|
|||
|
7c2549cbfe
|
|||
|
e3bd385c96
|
|||
|
038ce15908
|
|||
|
7aebcfc43f
|
|||
|
b7eccde9d0
|
|||
|
9305e9f718
|
|||
|
b907cc2f65
|
|||
|
29e2fca2a5
|
|||
|
d3e908b2b0
|
|||
|
5608176a20
|
|||
|
fa21e72b3f
|
|||
|
5b0397268b
|
|||
|
fb37db8583
|
|||
|
07f881d543
|
|||
|
3f9dad8f9a
|
|||
|
275fb73896
|
|||
| 204b4f44c7 | |||
|
8e802f314a
|
|||
|
f4d1ba897b
|
|||
| 7f975f2dbb | |||
|
5e1e249052
|
|||
|
b376bbd2aa
|
|||
|
51ae16e57f
|
|||
|
2ac3180c0f
|
|||
|
cea96e170d
|
|||
|
5906655902
|
|||
|
5f38355d5c
|
|||
|
010eb3f291
|
|||
|
0b1eca87b2
|
|||
|
f1d6ddbc84
|
|||
|
49d5aa4487
|
|||
|
4c68be19fe
|
|||
|
a6517c61a4
|
|||
|
2624f2cbf8
|
|||
|
cb4a4e06c8
|
|||
|
5f5f024ae7
|
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.5.x
|
||||||
|
- run: "deno task test"
|
||||||
|
- run: "deno lint"
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,3 +1,4 @@
|
|||||||
.env
|
.env
|
||||||
users.yaml
|
users.yaml
|
||||||
substr
|
build/
|
||||||
|
coverage/
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
margin: 4rem 0 0 0;
|
margin: 4rem 0 1.6rem 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
h2, h3, h4 {
|
h2, h3, h4 {
|
||||||
@@ -21,6 +21,10 @@ p, pre, ul, ol, dl, blockquote, hr {
|
|||||||
margin-bottom: 1.6em;
|
margin-bottom: 1.6em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
margin-bottom: 0.8em;
|
||||||
|
}
|
||||||
|
|
||||||
a.anchor {
|
a.anchor {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@@ -41,6 +45,11 @@ pre code {
|
|||||||
padding: 0.6rem 1rem;
|
padding: 0.6rem 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.highlight pre {
|
||||||
|
font-size: 0.9em;
|
||||||
|
padding: 0.6rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
img {
|
img {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
@@ -84,10 +93,6 @@ main header .draft-label {
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
main header h1 {
|
|
||||||
margin-bottom: 1.6rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
main header .meta {
|
main header .meta {
|
||||||
display: flex;
|
display: flex;
|
||||||
column-gap: 1rem;
|
column-gap: 1rem;
|
||||||
@@ -112,11 +117,24 @@ main.profile-page header {
|
|||||||
margin-bottom: 2rem;
|
margin-bottom: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
main.profile-page header h1 {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
main.profile-page header .nip05 {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
main.profile-page img.avatar {
|
main.profile-page img.avatar {
|
||||||
height: 8rem;
|
height: 8rem;
|
||||||
width: 8rem;
|
width: 8rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
main.profile-page details summary {
|
||||||
|
cursor: pointer;
|
||||||
|
margin-bottom: 1.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
main .article-list .item h3 {
|
main .article-list .item h3 {
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
}
|
}
|
||||||
@@ -133,6 +151,14 @@ main article footer {
|
|||||||
margin-top: 5rem;
|
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 menu */
|
||||||
|
|
||||||
.dropdown {
|
.dropdown {
|
||||||
@@ -201,7 +227,7 @@ main article footer {
|
|||||||
margin: 4rem 1rem 8rem 1rem !important;
|
margin: 4rem 1rem 8rem 1rem !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-page h1 {
|
main.profile-page h1 {
|
||||||
margin-top: 2rem;
|
margin-top: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
140
assets/css/prism.css
Normal file
140
assets/css/prism.css
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
/**
|
||||||
|
* prism.js default theme for JavaScript, CSS and HTML
|
||||||
|
* Based on dabblet (http://dabblet.com)
|
||||||
|
* @author Lea Verou
|
||||||
|
*/
|
||||||
|
|
||||||
|
code[class*="language-"],
|
||||||
|
pre[class*="language-"] {
|
||||||
|
color: black;
|
||||||
|
background: none;
|
||||||
|
text-shadow: 0 1px white;
|
||||||
|
font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
|
||||||
|
font-size: 1em;
|
||||||
|
text-align: left;
|
||||||
|
white-space: pre;
|
||||||
|
word-spacing: normal;
|
||||||
|
word-break: normal;
|
||||||
|
word-wrap: normal;
|
||||||
|
line-height: 1.5;
|
||||||
|
|
||||||
|
-moz-tab-size: 4;
|
||||||
|
-o-tab-size: 4;
|
||||||
|
tab-size: 4;
|
||||||
|
|
||||||
|
-webkit-hyphens: none;
|
||||||
|
-moz-hyphens: none;
|
||||||
|
-ms-hyphens: none;
|
||||||
|
hyphens: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre[class*="language-"]::-moz-selection, pre[class*="language-"] ::-moz-selection,
|
||||||
|
code[class*="language-"]::-moz-selection, code[class*="language-"] ::-moz-selection {
|
||||||
|
text-shadow: none;
|
||||||
|
background: #b3d4fc;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre[class*="language-"]::selection, pre[class*="language-"] ::selection,
|
||||||
|
code[class*="language-"]::selection, code[class*="language-"] ::selection {
|
||||||
|
text-shadow: none;
|
||||||
|
background: #b3d4fc;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media print {
|
||||||
|
code[class*="language-"],
|
||||||
|
pre[class*="language-"] {
|
||||||
|
text-shadow: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Code blocks */
|
||||||
|
pre[class*="language-"] {
|
||||||
|
padding: 1em;
|
||||||
|
margin: .5em 0;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
:not(pre) > code[class*="language-"],
|
||||||
|
pre[class*="language-"] {
|
||||||
|
background: #f5f2f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Inline code */
|
||||||
|
:not(pre) > code[class*="language-"] {
|
||||||
|
padding: .1em;
|
||||||
|
border-radius: .3em;
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.comment,
|
||||||
|
.token.prolog,
|
||||||
|
.token.doctype,
|
||||||
|
.token.cdata {
|
||||||
|
color: slategray;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.punctuation {
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.namespace {
|
||||||
|
opacity: .7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.property,
|
||||||
|
.token.tag,
|
||||||
|
.token.boolean,
|
||||||
|
.token.number,
|
||||||
|
.token.constant,
|
||||||
|
.token.symbol,
|
||||||
|
.token.deleted {
|
||||||
|
color: #905;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.selector,
|
||||||
|
.token.attr-name,
|
||||||
|
.token.string,
|
||||||
|
.token.char,
|
||||||
|
.token.builtin,
|
||||||
|
.token.inserted {
|
||||||
|
color: #690;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.operator,
|
||||||
|
.token.entity,
|
||||||
|
.token.url,
|
||||||
|
.language-css .token.string,
|
||||||
|
.style .token.string {
|
||||||
|
color: #9a6e3a;
|
||||||
|
/* This background color was intended by the author of this theme. */
|
||||||
|
background: hsla(0, 0%, 100%, .5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.atrule,
|
||||||
|
.token.attr-value,
|
||||||
|
.token.keyword {
|
||||||
|
color: #07a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.function,
|
||||||
|
.token.class-name {
|
||||||
|
color: #DD4A68;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.regex,
|
||||||
|
.token.important,
|
||||||
|
.token.variable {
|
||||||
|
color: #e90;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.important,
|
||||||
|
.token.bold {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.token.italic {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.entity {
|
||||||
|
cursor: help;
|
||||||
|
}
|
||||||
@@ -47,6 +47,13 @@ pre code {
|
|||||||
color: var(--text-color-dark-bg);
|
color: var(--text-color-dark-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.highlight pre,
|
||||||
|
.highlight code {
|
||||||
|
background-color: var(--background-color-body);
|
||||||
|
border: 2px solid #e8e3da;
|
||||||
|
color: var(--text-color-body);
|
||||||
|
}
|
||||||
|
|
||||||
dl dt {
|
dl dt {
|
||||||
color: var(--text-color-discreet);
|
color: var(--text-color-discreet);
|
||||||
}
|
}
|
||||||
@@ -68,10 +75,24 @@ main header .meta .name a {
|
|||||||
color: var(--text-color-body);
|
color: var(--text-color-body);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
main.profile-page header .nip05 {
|
||||||
|
color: var(--text-color-discreet);
|
||||||
|
}
|
||||||
|
|
||||||
main.profile-page .pubkey {
|
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 */
|
/* Dropdown menu */
|
||||||
|
|
||||||
.dropdown {
|
.dropdown {
|
||||||
|
|||||||
23
config.ts
23
config.ts
@@ -1,18 +1,23 @@
|
|||||||
import { load } from "@std/dotenv";
|
import { load } from "@std/dotenv";
|
||||||
import { parse as parseYaml } from "jsr:@std/yaml";
|
import { parse as parseYaml } from "@std/yaml";
|
||||||
import { checkFileExists } from "./utils.ts";
|
import { checkFileExists } from "./utils.ts";
|
||||||
import { log } from "./log.ts";
|
import { log } from "./log.ts";
|
||||||
|
|
||||||
|
const denoEnv = Deno.env.get("DENO_ENV");
|
||||||
const dirname = Deno.cwd();
|
const dirname = Deno.cwd();
|
||||||
|
|
||||||
await load({ envPath: `${dirname}/.env`, export: true });
|
function envAwarePath(path: string): string {
|
||||||
|
return denoEnv ? `${path}.${denoEnv}` : path;
|
||||||
|
}
|
||||||
|
|
||||||
|
await load({ envPath: envAwarePath(`${dirname}/.env`), export: true });
|
||||||
|
|
||||||
let userConfigPath: string = "";
|
let userConfigPath: string = "";
|
||||||
let staticUsers: { [key: string]: string } = {};
|
let staticUsers: { [key: string]: string } = {};
|
||||||
|
|
||||||
const defaultUserConfigPaths = [
|
const defaultUserConfigPaths = [
|
||||||
"/etc/substr/users.yaml",
|
envAwarePath("/etc/substr/users.yaml"),
|
||||||
`${dirname}/users.yaml`,
|
envAwarePath(`${dirname}/users.yaml`),
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const path of defaultUserConfigPaths) {
|
for (const path of defaultUserConfigPaths) {
|
||||||
@@ -45,7 +50,17 @@ 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"),
|
||||||
},
|
},
|
||||||
|
query_timeout: parseInt(Deno.env.get("RELAY_TIMEOUT_MS") || "5000"),
|
||||||
njump_url: Deno.env.get("NJUMP_URL") || "https://njump.me",
|
njump_url: Deno.env.get("NJUMP_URL") || "https://njump.me",
|
||||||
|
prism: {
|
||||||
|
// TODO make configurable via ENV
|
||||||
|
// Supported languages: https://app.unpkg.com/prismjs@1.29.0/files/components
|
||||||
|
extraLanguages: [
|
||||||
|
"bash",
|
||||||
|
"markdown",
|
||||||
|
"typescript",
|
||||||
|
],
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const staticUsersConfigured = Object.keys(staticUsers).length > 0;
|
const staticUsersConfigured = Object.keys(staticUsers).length > 0;
|
||||||
|
|||||||
51
deno.json
51
deno.json
@@ -1,22 +1,53 @@
|
|||||||
{
|
{
|
||||||
"tasks": {
|
"tasks": {
|
||||||
"dev": "deno run --allow-all --watch server.ts",
|
"dev": "deno run -P --watch server.ts",
|
||||||
"server": "deno run --allow-all server.ts"
|
"server": "deno run -P server.ts",
|
||||||
|
"compile": "deno compile -P --include ./assets/ --exclude ./tests/ --output ./build/substr_x86_64-unknown-linux-gnu server.ts",
|
||||||
|
"test": "DENO_ENV=test deno test -P=test"
|
||||||
},
|
},
|
||||||
"imports": {
|
"imports": {
|
||||||
"@deno/gfm": "jsr:@deno/gfm@^0.9.0",
|
"@deno/gfm": "jsr:@deno/gfm@^0.10.0",
|
||||||
"@nostr/tools": "jsr:@nostr/tools@^2.3.1",
|
"@nostr/tools": "jsr:@nostr/tools@^2.10.4",
|
||||||
"@nostrify/nostrify": "jsr:@nostrify/nostrify@^0.36.1",
|
"@nostrify/nostrify": "jsr:@nostrify/nostrify@^0.36.2",
|
||||||
"@oak/oak": "jsr:@oak/oak@^17.1.0",
|
"@oak/oak": "jsr:@oak/oak@^17.1.3",
|
||||||
"@std/dotenv": "jsr:@std/dotenv@^0.225.2",
|
"@std/dotenv": "jsr:@std/dotenv@^0.225.2",
|
||||||
"@std/expect": "jsr:@std/expect@^1.0.5",
|
"@std/expect": "jsr:@std/expect@^1.0.8",
|
||||||
"@std/testing": "jsr:@std/testing@^1.0.3",
|
"@std/testing": "jsr:@std/testing@^1.0.11",
|
||||||
"@std/yaml": "jsr:@std/yaml@^1.0.5",
|
"@std/yaml": "jsr:@std/yaml@^1.0.5",
|
||||||
"ldapts": "npm:ldapts@^7.2.1"
|
"ldapts": "npm:ldapts@^7.2.2"
|
||||||
},
|
},
|
||||||
"fmt": {
|
"fmt": {
|
||||||
"exclude": [
|
"exclude": [
|
||||||
"magick.ts"
|
"magick.ts",
|
||||||
|
"assets/css/prism.css",
|
||||||
|
"tests/fixtures/"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"permissions": {
|
||||||
|
"default": {
|
||||||
|
"read": [
|
||||||
|
".env",
|
||||||
|
".env.test",
|
||||||
|
"users.yaml",
|
||||||
|
"users.yaml.test",
|
||||||
|
"/etc/substr/users.yaml",
|
||||||
|
"/etc/substr/users.yaml.test",
|
||||||
|
"/tmp/substr/",
|
||||||
|
"assets/"
|
||||||
|
],
|
||||||
|
"write": ["/tmp/substr/"],
|
||||||
|
"run": ["which", "magick", "convert"],
|
||||||
|
"env": true,
|
||||||
|
"net": true
|
||||||
|
},
|
||||||
|
"test": {
|
||||||
|
"read": [
|
||||||
|
"tests/",
|
||||||
|
".env.test",
|
||||||
|
"users.yaml.test",
|
||||||
|
"/etc/substr/users.yaml.test"
|
||||||
|
],
|
||||||
|
"env": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
254
deno.lock
generated
254
deno.lock
generated
@@ -1,60 +1,68 @@
|
|||||||
{
|
{
|
||||||
"version": "4",
|
"version": "4",
|
||||||
"specifiers": {
|
"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:@denosaurs/emoji@0.3": "0.3.1",
|
||||||
"jsr:@nostr/tools@^2.3.1": "2.3.1",
|
"jsr:@nostr/tools@^2.10.4": "2.12.0",
|
||||||
"jsr:@nostrify/nostrify@~0.36.1": "0.36.1",
|
"jsr:@nostrify/nostrify@~0.36.2": "0.36.2",
|
||||||
"jsr:@nostrify/types@0.35": "0.35.0",
|
"jsr:@nostrify/types@0.35": "0.35.0",
|
||||||
"jsr:@oak/commons@1": "1.0.0",
|
"jsr:@oak/commons@1": "1.0.0",
|
||||||
"jsr:@oak/oak@^17.1.0": "17.1.0",
|
"jsr:@oak/oak@^17.1.3": "17.1.3",
|
||||||
"jsr:@std/assert@1": "1.0.6",
|
"jsr:@std/assert@0.224": "0.224.0",
|
||||||
"jsr:@std/assert@^1.0.6": "1.0.6",
|
"jsr:@std/assert@1": "1.0.8",
|
||||||
"jsr:@std/bytes@1": "1.0.2",
|
"jsr:@std/assert@^1.0.12": "1.0.12",
|
||||||
"jsr:@std/bytes@^1.0.2": "1.0.2",
|
"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/crypto@1": "1.0.3",
|
||||||
"jsr:@std/data-structures@^1.0.4": "1.0.4",
|
"jsr:@std/dotenv@~0.225.2": "0.225.3",
|
||||||
"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": "1.0.5",
|
||||||
"jsr:@std/encoding@^1.0.5": "1.0.5",
|
"jsr:@std/encoding@^1.0.5": "1.0.5",
|
||||||
"jsr:@std/encoding@~0.224.1": "0.224.3",
|
"jsr:@std/encoding@~0.224.1": "0.224.3",
|
||||||
"jsr:@std/expect@^1.0.5": "1.0.5",
|
"jsr:@std/expect@^1.0.8": "1.0.15",
|
||||||
"jsr:@std/fs@^1.0.4": "1.0.4",
|
"jsr:@std/http@1": "1.0.10",
|
||||||
"jsr:@std/http@1": "1.0.8",
|
"jsr:@std/internal@^1.0.5": "1.0.5",
|
||||||
"jsr:@std/internal@^1.0.4": "1.0.4",
|
"jsr:@std/internal@^1.0.6": "1.0.6",
|
||||||
"jsr:@std/io@0.224": "0.224.9",
|
"jsr:@std/io@0.224": "0.224.9",
|
||||||
"jsr:@std/media-types@1": "1.0.3",
|
"jsr:@std/media-types@1": "1.1.0",
|
||||||
"jsr:@std/path@1": "1.0.6",
|
"jsr:@std/path@1": "1.0.8",
|
||||||
"jsr:@std/path@^1.0.6": "1.0.6",
|
"jsr:@std/path@^1.0.7": "1.0.8",
|
||||||
"jsr:@std/testing@^1.0.3": "1.0.3",
|
"jsr:@std/path@^1.0.8": "1.0.8",
|
||||||
|
"jsr:@std/testing@^1.0.11": "1.0.11",
|
||||||
"jsr:@std/yaml@*": "1.0.5",
|
"jsr:@std/yaml@*": "1.0.5",
|
||||||
"jsr:@std/yaml@^1.0.5": "1.0.5",
|
"jsr:@std/yaml@^1.0.5": "1.0.5",
|
||||||
"npm:@noble/ciphers@~0.5.1": "0.5.3",
|
"npm:@noble/ciphers@~0.5.1": "0.5.3",
|
||||||
"npm:@noble/curves@1.2.0": "1.2.0",
|
"npm:@noble/curves@1.2.0": "1.2.0",
|
||||||
"npm:@noble/hashes@1.3.1": "1.3.1",
|
"npm:@noble/hashes@1.3.1": "1.3.1",
|
||||||
"npm:@scure/base@1.1.1": "1.1.1",
|
"npm:@scure/base@1.1.1": "1.1.1",
|
||||||
"npm:@scure/bip32@^1.4.0": "1.4.0",
|
"npm:@scure/base@^1.1.6": "1.2.1",
|
||||||
"npm:@scure/bip39@^1.3.0": "1.3.0",
|
"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:github-slugger@2": "2.0.0",
|
||||||
"npm:he@^1.2.0": "1.2.0",
|
"npm:he@^1.2.0": "1.2.0",
|
||||||
"npm:katex@0.16": "0.16.11",
|
"npm:katex@0.16": "0.16.11",
|
||||||
"npm:ldapts@^7.2.1": "7.2.1",
|
"npm:ldapts@^7.2.2": "7.2.2",
|
||||||
"npm:lru-cache@^10.2.0": "10.2.2",
|
"npm:lru-cache@^10.2.0": "10.4.3",
|
||||||
"npm:marked-alert@2": "2.1.0_marked@12.0.2",
|
"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-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-gfm-heading-id@^3.1.0": "3.2.0_marked@12.0.2",
|
||||||
"npm:marked@12": "12.0.2",
|
"npm:marked@12": "12.0.2",
|
||||||
"npm:nostr-tools@^2.7.0": "2.7.0",
|
"npm:nostr-tools@^2.7.0": "2.10.4",
|
||||||
"npm:path-to-regexp@*": "6.2.1",
|
"npm:nostr-wasm@0.1.0": "0.1.0",
|
||||||
"npm:path-to-regexp@6.2.1": "6.2.1",
|
"npm:path-to-regexp@6.2.1": "6.2.1",
|
||||||
|
"npm:prismjs@1.29.0": "1.29.0",
|
||||||
"npm:prismjs@^1.29.0": "1.29.0",
|
"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:websocket-ts@^2.1.5": "2.1.5",
|
||||||
"npm:zod@^3.23.8": "3.23.8"
|
"npm:zod@^3.23.8": "3.23.8"
|
||||||
},
|
},
|
||||||
"jsr": {
|
"jsr": {
|
||||||
"@deno/gfm@0.9.0": {
|
"@deno/gfm@0.10.0": {
|
||||||
"integrity": "9002dbdb6e382e247509edfeae3afdb9232f5ca98a8210ef186d42084e9ded30",
|
"integrity": "51708205e3559a4aeb6afb29d07c5bfafe7941f91bb360351ef6621de9a39527",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"jsr:@denosaurs/emoji",
|
"jsr:@denosaurs/emoji",
|
||||||
"npm:github-slugger",
|
"npm:github-slugger",
|
||||||
@@ -64,29 +72,43 @@
|
|||||||
"npm:marked-alert",
|
"npm:marked-alert",
|
||||||
"npm:marked-footnote",
|
"npm:marked-footnote",
|
||||||
"npm:marked-gfm-heading-id",
|
"npm:marked-gfm-heading-id",
|
||||||
"npm:prismjs",
|
"npm:prismjs@^1.29.0",
|
||||||
"npm:sanitize-html"
|
"npm:sanitize-html"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"@denosaurs/emoji@0.3.1": {
|
"@denosaurs/emoji@0.3.1": {
|
||||||
"integrity": "b0aed5f55dec99e83da7c9637fe0a36d1d6252b7c99deaaa3fc5dea3fcf3da8b"
|
"integrity": "b0aed5f55dec99e83da7c9637fe0a36d1d6252b7c99deaaa3fc5dea3fcf3da8b"
|
||||||
},
|
},
|
||||||
"@nostr/tools@2.3.1": {
|
"@nostr/tools@2.10.4": {
|
||||||
"integrity": "af01dc45cb28784c584d7a0699707196f397bcc53946efa582a01b11ddde4d61",
|
"integrity": "7fda015c96b4f674727843aecb990e2af1989e4724588415ccf6f69066abfd4f",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"npm:@noble/ciphers",
|
"npm:@noble/ciphers",
|
||||||
"npm:@noble/curves",
|
"npm:@noble/curves",
|
||||||
"npm:@noble/hashes",
|
"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": {
|
"@nostr/tools@2.12.0": {
|
||||||
"integrity": "f76c803c0bda5df1c172f25d2313980344b0431df2a973ab3e1dd61e9e7b4b1a",
|
"integrity": "0584d5197682c6eabaded17bae10e765f215ef051ae70aa463f994abf90f295a",
|
||||||
|
"dependencies": [
|
||||||
|
"npm:@noble/ciphers",
|
||||||
|
"npm:@noble/curves",
|
||||||
|
"npm:@noble/hashes",
|
||||||
|
"npm:@scure/base@1.1.1"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"@nostrify/nostrify@0.36.2": {
|
||||||
|
"integrity": "cc4787ca170b623a2e5dfed1baa4426077daa6143af728ea7dd325d58f4d04d6",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"jsr:@nostrify/types",
|
"jsr:@nostrify/types",
|
||||||
|
"jsr:@std/crypto@0.224",
|
||||||
"jsr:@std/encoding@~0.224.1",
|
"jsr:@std/encoding@~0.224.1",
|
||||||
"npm:@scure/bip32",
|
"npm:@scure/base@^1.1.6",
|
||||||
"npm:@scure/bip39",
|
"npm:@scure/bip32@^1.4.0",
|
||||||
|
"npm:@scure/bip39@^1.3.0",
|
||||||
"npm:lru-cache",
|
"npm:lru-cache",
|
||||||
"npm:nostr-tools",
|
"npm:nostr-tools",
|
||||||
"npm:websocket-ts",
|
"npm:websocket-ts",
|
||||||
@@ -101,71 +123,91 @@
|
|||||||
"dependencies": [
|
"dependencies": [
|
||||||
"jsr:@std/assert@1",
|
"jsr:@std/assert@1",
|
||||||
"jsr:@std/bytes@1",
|
"jsr:@std/bytes@1",
|
||||||
"jsr:@std/crypto",
|
"jsr:@std/crypto@1",
|
||||||
"jsr:@std/encoding@1",
|
"jsr:@std/encoding@1",
|
||||||
"jsr:@std/http",
|
"jsr:@std/http",
|
||||||
"jsr:@std/media-types"
|
"jsr:@std/media-types"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"@oak/oak@17.1.0": {
|
"@oak/oak@17.1.3": {
|
||||||
"integrity": "14ffb400c3c268bdc7b3a838664fab782b4ed35bb0dfe7669013c95bb12a9503",
|
"integrity": "d89296c22db91681dd3a2a1e1fd14e258d0d5a9654de55637aee5b661c159f33",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"jsr:@oak/commons",
|
"jsr:@oak/commons",
|
||||||
"jsr:@std/assert@1",
|
"jsr:@std/assert@1",
|
||||||
"jsr:@std/bytes@1",
|
"jsr:@std/bytes@1",
|
||||||
"jsr:@std/crypto",
|
"jsr:@std/crypto@1",
|
||||||
"jsr:@std/http",
|
"jsr:@std/http",
|
||||||
"jsr:@std/io",
|
"jsr:@std/io",
|
||||||
"jsr:@std/media-types",
|
"jsr:@std/media-types",
|
||||||
"jsr:@std/path@1",
|
"jsr:@std/path@1",
|
||||||
"npm:path-to-regexp@6.2.1"
|
"npm:path-to-regexp"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"@std/assert@1.0.6": {
|
"@std/assert@0.224.0": {
|
||||||
"integrity": "1904c05806a25d94fe791d6d883b685c9e2dcd60e4f9fc30f4fc5cf010c72207",
|
"integrity": "8643233ec7aec38a940a8264a6e3eed9bfa44e7a71cc6b3c8874213ff401967f"
|
||||||
|
},
|
||||||
|
"@std/assert@1.0.8": {
|
||||||
|
"integrity": "ebe0bd7eb488ee39686f77003992f389a06c3da1bbd8022184804852b2fa641b",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"jsr:@std/internal"
|
"jsr:@std/internal@^1.0.5"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"@std/bytes@1.0.2": {
|
"@std/assert@1.0.12": {
|
||||||
"integrity": "fbdee322bbd8c599a6af186a1603b3355e59a5fb1baa139f8f4c3c9a1b3e3d57"
|
"integrity": "08009f0926dda9cbd8bef3a35d3b6a4b964b0ab5c3e140a4e0351fbf34af5b9a",
|
||||||
|
"dependencies": [
|
||||||
|
"jsr:@std/internal@^1.0.6"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"@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": {
|
"@std/crypto@1.0.3": {
|
||||||
"integrity": "a2a32f51ddef632d299e3879cd027c630dcd4d1d9a5285d6e6788072f4e51e7f"
|
"integrity": "a2a32f51ddef632d299e3879cd027c630dcd4d1d9a5285d6e6788072f4e51e7f"
|
||||||
},
|
},
|
||||||
"@std/data-structures@1.0.4": {
|
|
||||||
"integrity": "fa0e20c11eb9ba673417450915c750a0001405a784e2a4e0c3725031681684a0"
|
|
||||||
},
|
|
||||||
"@std/dotenv@0.225.2": {
|
"@std/dotenv@0.225.2": {
|
||||||
"integrity": "e2025dce4de6c7bca21dece8baddd4262b09d5187217e231b033e088e0c4dd23"
|
"integrity": "e2025dce4de6c7bca21dece8baddd4262b09d5187217e231b033e088e0c4dd23"
|
||||||
},
|
},
|
||||||
|
"@std/dotenv@0.225.3": {
|
||||||
|
"integrity": "a95e5b812c27b0854c52acbae215856d9cce9d4bbf774d938c51d212711e8d4a"
|
||||||
|
},
|
||||||
"@std/encoding@0.224.3": {
|
"@std/encoding@0.224.3": {
|
||||||
"integrity": "5e861b6d81be5359fad4155e591acf17c0207b595112d1840998bb9f476dbdaf"
|
"integrity": "5e861b6d81be5359fad4155e591acf17c0207b595112d1840998bb9f476dbdaf"
|
||||||
},
|
},
|
||||||
"@std/encoding@1.0.5": {
|
"@std/encoding@1.0.5": {
|
||||||
"integrity": "ecf363d4fc25bd85bd915ff6733a7e79b67e0e7806334af15f4645c569fefc04"
|
"integrity": "ecf363d4fc25bd85bd915ff6733a7e79b67e0e7806334af15f4645c569fefc04"
|
||||||
},
|
},
|
||||||
"@std/expect@1.0.5": {
|
"@std/expect@1.0.8": {
|
||||||
"integrity": "8c7ac797e2ffe57becc6399c0f2fd06230cb9ef124d45229c6e592c563824af1",
|
"integrity": "27e40d8f3aefb372fc6a703fb0b69e34560e72a2f78705178babdffa00119a5f",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"jsr:@std/assert@^1.0.6",
|
"jsr:@std/assert@^1.0.8",
|
||||||
"jsr:@std/internal"
|
"jsr:@std/internal@^1.0.5"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"@std/fs@1.0.4": {
|
"@std/expect@1.0.15": {
|
||||||
"integrity": "2907d32d8d1d9e540588fd5fe0ec21ee638134bd51df327ad4e443aaef07123c",
|
"integrity": "eca360007b5a7f13dbfa1294224baee7fb98dcd460d8461fe64eeae302902945",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"jsr:@std/path@^1.0.6"
|
"jsr:@std/assert@^1.0.12",
|
||||||
|
"jsr:@std/internal@^1.0.6"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"@std/http@1.0.8": {
|
"@std/http@1.0.10": {
|
||||||
"integrity": "6ea1b2e8d33929967754a3b6d6c6f399ad6647d7bbb5a466c1eaf9b294a6ebcd",
|
"integrity": "4e32d11493ab04e3ef09f104f0cb9beb4228b1d4b47c5469573c2c294c0d3692",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"jsr:@std/encoding@^1.0.5"
|
"jsr:@std/encoding@^1.0.5"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"@std/internal@1.0.4": {
|
"@std/internal@1.0.5": {
|
||||||
"integrity": "62e8e4911527e5e4f307741a795c0b0a9e6958d0b3790716ae71ce085f755422"
|
"integrity": "54a546004f769c1ac9e025abd15a76b6671ddc9687e2313b67376125650dc7ba"
|
||||||
|
},
|
||||||
|
"@std/internal@1.0.6": {
|
||||||
|
"integrity": "9533b128f230f73bd209408bb07a4b12f8d4255ab2a4d22a1fd6d87304aca9a4"
|
||||||
},
|
},
|
||||||
"@std/io@0.224.9": {
|
"@std/io@0.224.9": {
|
||||||
"integrity": "4414664b6926f665102e73c969cfda06d2c4c59bd5d0c603fd4f1b1c840d6ee3",
|
"integrity": "4414664b6926f665102e73c969cfda06d2c4c59bd5d0c603fd4f1b1c840d6ee3",
|
||||||
@@ -173,20 +215,17 @@
|
|||||||
"jsr:@std/bytes@^1.0.2"
|
"jsr:@std/bytes@^1.0.2"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"@std/media-types@1.0.3": {
|
"@std/media-types@1.1.0": {
|
||||||
"integrity": "b12d30a7852f7578f4d210622df713bbfd1cbdd9b4ec2eaf5c1845ab70bab159"
|
"integrity": "c9d093f0c05c3512932b330e3cc1fe1d627b301db33a4c2c2185c02471d6eaa4"
|
||||||
},
|
},
|
||||||
"@std/path@1.0.6": {
|
"@std/path@1.0.8": {
|
||||||
"integrity": "ab2c55f902b380cf28e0eec501b4906e4c1960d13f00e11cfbcd21de15f18fed"
|
"integrity": "548fa456bb6a04d3c1a1e7477986b6cffbce95102d0bb447c67c4ee70e0364be"
|
||||||
},
|
},
|
||||||
"@std/testing@1.0.3": {
|
"@std/testing@1.0.11": {
|
||||||
"integrity": "f98c2bee53860a5916727d7e7d3abe920dd6f9edace022e2d059f00d05c2cf42",
|
"integrity": "12b3db12d34f0f385a26248933bde766c0f8c5ad8b6ab34d4d38f528ab852f48",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"jsr:@std/assert@^1.0.6",
|
"jsr:@std/assert@^1.0.12",
|
||||||
"jsr:@std/data-structures",
|
"jsr:@std/internal@^1.0.6"
|
||||||
"jsr:@std/fs",
|
|
||||||
"jsr:@std/internal",
|
|
||||||
"jsr:@std/path@^1.0.6"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"@std/yaml@1.0.5": {
|
"@std/yaml@1.0.5": {
|
||||||
@@ -209,10 +248,10 @@
|
|||||||
"@noble/hashes@1.3.2"
|
"@noble/hashes@1.3.2"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"@noble/curves@1.4.0": {
|
"@noble/curves@1.7.0": {
|
||||||
"integrity": "sha512-p+4cb332SFCrReJkCYe8Xzm0OWi4Jji5jVdIZRL/PmacmDkFNw6MrrV+gGpiPxLHbV+zKFRywUWbaseT+tZRXg==",
|
"integrity": "sha512-UTMhXK9SeDhFJVrHeUJ5uZlI6ajXg10O6Ddocf9S6GjbSBVZsJo88HzKwXznNfGpMTRDyJkqMjNDPYgf0qFWnw==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"@noble/hashes@1.4.0"
|
"@noble/hashes@1.6.0"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"@noble/hashes@1.3.1": {
|
"@noble/hashes@1.3.1": {
|
||||||
@@ -221,43 +260,46 @@
|
|||||||
"@noble/hashes@1.3.2": {
|
"@noble/hashes@1.3.2": {
|
||||||
"integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ=="
|
"integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ=="
|
||||||
},
|
},
|
||||||
"@noble/hashes@1.4.0": {
|
"@noble/hashes@1.6.0": {
|
||||||
"integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg=="
|
"integrity": "sha512-YUULf0Uk4/mAA89w+k3+yUYh6NrEvxZa5T6SY3wlMvE2chHkxFUUIDI8/XW1QSC357iA5pSnqt7XEhvFOqmDyQ=="
|
||||||
|
},
|
||||||
|
"@noble/hashes@1.6.1": {
|
||||||
|
"integrity": "sha512-pq5D8h10hHBjyqX+cfBm0i8JUXJ0UhczFc4r74zbuT9XgewFo2E3J1cOaGtdZynILNmQ685YWGzGE1Zv6io50w=="
|
||||||
},
|
},
|
||||||
"@scure/base@1.1.1": {
|
"@scure/base@1.1.1": {
|
||||||
"integrity": "sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA=="
|
"integrity": "sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA=="
|
||||||
},
|
},
|
||||||
"@scure/base@1.1.7": {
|
"@scure/base@1.2.1": {
|
||||||
"integrity": "sha512-PPNYBslrLNNUQ/Yad37MHYsNQtK67EhWb6WtSvNLLPo7SdVZgkUjD6Dg+5On7zNwmskf8OX7I7Nx5oN+MIWE0g=="
|
"integrity": "sha512-DGmGtC8Tt63J5GfHgfl5CuAXh96VF/LD8K9Hr/Gv0J2lAoRGlPOMpqMpMbCTOoOJMZCk2Xt+DskdDyn6dEFdzQ=="
|
||||||
},
|
},
|
||||||
"@scure/bip32@1.3.1": {
|
"@scure/bip32@1.3.1": {
|
||||||
"integrity": "sha512-osvveYtyzdEVbt3OfwwXFr4P2iVBL5u1Q3q4ONBfDY/UpOuXmOlbgwc1xECEboY8wIays8Yt6onaWMUdUbfl0A==",
|
"integrity": "sha512-osvveYtyzdEVbt3OfwwXFr4P2iVBL5u1Q3q4ONBfDY/UpOuXmOlbgwc1xECEboY8wIays8Yt6onaWMUdUbfl0A==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"@noble/curves@1.1.0",
|
"@noble/curves@1.1.0",
|
||||||
"@noble/hashes@1.3.2",
|
"@noble/hashes@1.3.2",
|
||||||
"@scure/base@1.1.7"
|
"@scure/base@1.1.1"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"@scure/bip32@1.4.0": {
|
"@scure/bip32@1.6.0": {
|
||||||
"integrity": "sha512-sVUpc0Vq3tXCkDGYVWGIZTRfnvu8LoTDaev7vbwh0omSvVORONr960MQWdKqJDCReIEmTj3PAr73O3aoxz7OPg==",
|
"integrity": "sha512-82q1QfklrUUdXJzjuRU7iG7D7XiFx5PHYVS0+oeNKhyDLT7WPqs6pBcM2W5ZdwOwKCwoE1Vy1se+DHjcXwCYnA==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"@noble/curves@1.4.0",
|
"@noble/curves@1.7.0",
|
||||||
"@noble/hashes@1.4.0",
|
"@noble/hashes@1.6.1",
|
||||||
"@scure/base@1.1.7"
|
"@scure/base@1.2.1"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"@scure/bip39@1.2.1": {
|
"@scure/bip39@1.2.1": {
|
||||||
"integrity": "sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg==",
|
"integrity": "sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"@noble/hashes@1.3.2",
|
"@noble/hashes@1.3.2",
|
||||||
"@scure/base@1.1.7"
|
"@scure/base@1.1.1"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"@scure/bip39@1.3.0": {
|
"@scure/bip39@1.5.0": {
|
||||||
"integrity": "sha512-disdg7gHuTDZtY+ZdkmLpPCk7fxZSu3gBiEGuoC1XYxv9cGx3Z6cpTggCgW6odSOOIXCiDjuGejW+aJKCY/pIQ==",
|
"integrity": "sha512-Dop+ASYhnrwm9+HA/HwXg7j2ZqM6yk2fyLWb5znexjctFY3+E+eU8cIWI0Pql0Qx4hPZCijlGq4OL71g+Uz30A==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"@noble/hashes@1.4.0",
|
"@noble/hashes@1.6.1",
|
||||||
"@scure/base@1.1.7"
|
"@scure/base@1.2.1"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"@types/asn1@0.2.4": {
|
"@types/asn1@0.2.4": {
|
||||||
@@ -345,8 +387,8 @@
|
|||||||
"commander"
|
"commander"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"ldapts@7.2.1": {
|
"ldapts@7.2.2": {
|
||||||
"integrity": "sha512-2NSA9drjHdRiApF+TO18c+Hy/uyBLs96OS6Gia4+dPQWPxvqDbu3Ji2beCbNCXTvvgxDj4cLZ0WoOZLt5ojfAg==",
|
"integrity": "sha512-UotAq24/vJEz0m3w/jgwZm7JGNw8M6vexL/5KU5pe3aIZWBkT/HRhjsPw/buRqKSK5Y0vTu5Zv8iyPgQF7ozzg==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"@types/asn1",
|
"@types/asn1",
|
||||||
"asn1",
|
"asn1",
|
||||||
@@ -356,11 +398,11 @@
|
|||||||
"whatwg-url"
|
"whatwg-url"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"lru-cache@10.2.2": {
|
"lru-cache@10.4.3": {
|
||||||
"integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ=="
|
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="
|
||||||
},
|
},
|
||||||
"marked-alert@2.1.0_marked@12.0.2": {
|
"marked-alert@2.1.2_marked@12.0.2": {
|
||||||
"integrity": "sha512-X95Z8PCDgWa0bBfM70GxZG3LD/leUrhXc3cx3w1eFExBhswd1oXn/S4S+9H8ypPdCY7okREb4dItUOc+VJq4jQ==",
|
"integrity": "sha512-EFNRZ08d8L/iEIPLTlQMDjvwIsj03gxWCczYTht6DCiHJIZhMk4NK5gtPY9UqAYb09eV5VGT+jD4lp396E0I+w==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"marked"
|
"marked"
|
||||||
]
|
]
|
||||||
@@ -387,8 +429,8 @@
|
|||||||
"nanoid@3.3.7": {
|
"nanoid@3.3.7": {
|
||||||
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g=="
|
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g=="
|
||||||
},
|
},
|
||||||
"nostr-tools@2.7.0": {
|
"nostr-tools@2.10.4": {
|
||||||
"integrity": "sha512-jJoL2J1CBiKDxaXZww27nY/Wsuxzx7AULxmGKFce4sskDu1tohNyfnzYQ8BvDyvkstU8kNZUAXPL32tre33uig==",
|
"integrity": "sha512-biU7sk+jxHgVASfobg2T5ttxOGGSt69wEVBC51sHHOEaKAAdzHBLV/I2l9Rf61UzClhliZwNouYhqIso4a3HYg==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"@noble/ciphers",
|
"@noble/ciphers",
|
||||||
"@noble/curves@1.2.0",
|
"@noble/curves@1.2.0",
|
||||||
@@ -454,8 +496,8 @@
|
|||||||
"undici-types@6.19.8": {
|
"undici-types@6.19.8": {
|
||||||
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="
|
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="
|
||||||
},
|
},
|
||||||
"uuid@10.0.0": {
|
"uuid@11.0.3": {
|
||||||
"integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ=="
|
"integrity": "sha512-d0z310fCWv5dJwnX1Y/MncBAqGMKEzlBb1AOf7z9K8ALnd0utBX/msg/fA0+sbyN1ihbMsLhrBlnl1ak7Wa0rg=="
|
||||||
},
|
},
|
||||||
"webidl-conversions@7.0.0": {
|
"webidl-conversions@7.0.0": {
|
||||||
"integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g=="
|
"integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g=="
|
||||||
@@ -488,15 +530,15 @@
|
|||||||
},
|
},
|
||||||
"workspace": {
|
"workspace": {
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"jsr:@deno/gfm@0.9",
|
"jsr:@deno/gfm@0.10",
|
||||||
"jsr:@nostr/tools@^2.3.1",
|
"jsr:@nostr/tools@^2.10.4",
|
||||||
"jsr:@nostrify/nostrify@~0.36.1",
|
"jsr:@nostrify/nostrify@~0.36.2",
|
||||||
"jsr:@oak/oak@^17.1.0",
|
"jsr:@oak/oak@^17.1.3",
|
||||||
"jsr:@std/dotenv@~0.225.2",
|
"jsr:@std/dotenv@~0.225.2",
|
||||||
"jsr:@std/expect@^1.0.5",
|
"jsr:@std/expect@^1.0.8",
|
||||||
"jsr:@std/testing@^1.0.3",
|
"jsr:@std/testing@^1.0.11",
|
||||||
"jsr:@std/yaml@^1.0.5",
|
"jsr:@std/yaml@^1.0.5",
|
||||||
"npm:ldapts@^7.2.1"
|
"npm:ldapts@^7.2.2"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
|
// deno-lint-ignore-file require-await
|
||||||
import config from "./config.ts";
|
import config from "./config.ts";
|
||||||
import { lookupUsernameByPubkey as ldapLookupUsername } from "./ldap.ts";
|
import { lookupUsernameByPubkey as ldapLookupUsername } from "./ldap.ts";
|
||||||
import { lookupPubkeyByUsername as ldapLookupPubkey } from "./ldap.ts";
|
import { lookupPubkeyByUsername as ldapLookupPubkey } from "./ldap.ts";
|
||||||
|
|
||||||
export function lookupUsernameByPubkey(pubkey: string) {
|
export async function lookupUsernameByPubkey(
|
||||||
|
pubkey: string,
|
||||||
|
): Promise<string | undefined> {
|
||||||
let username;
|
let username;
|
||||||
for (const [key, value] of Object.entries(config.staticUsers)) {
|
for (const [key, value] of Object.entries(config.staticUsers)) {
|
||||||
if (value === pubkey) {
|
if (value === pubkey) {
|
||||||
@@ -20,7 +23,9 @@ export function lookupUsernameByPubkey(pubkey: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function lookupPubkeyByUsername(username: string) {
|
export async function lookupPubkeyByUsername(
|
||||||
|
username: string,
|
||||||
|
): Promise<string | undefined> {
|
||||||
const pubkey = config.staticUsers[username];
|
const pubkey = config.staticUsers[username];
|
||||||
|
|
||||||
if (pubkey) {
|
if (pubkey) {
|
||||||
|
|||||||
14
feeds.ts
14
feeds.ts
@@ -14,12 +14,14 @@ export async function profileAtomFeed(
|
|||||||
for (const article of articles) {
|
for (const article of articles) {
|
||||||
const contentHtml = await article.buildContentHtml();
|
const contentHtml = await article.buildContentHtml();
|
||||||
const articleId =
|
const articleId =
|
||||||
`tag:${profile.nip05},nostr-d-${article.identifier}-k-30023`;
|
`tag:${profile.nip05},nostr-p-${profile.pubkey}-d-${article.identifier}-k-30023`;
|
||||||
articlesXml += `
|
articlesXml += `
|
||||||
<entry>
|
<entry>
|
||||||
<id>${articleId}</id>
|
<id>${articleId}</id>
|
||||||
<title>${article.title}</title>
|
<title>${article.title}</title>
|
||||||
<link href="${article.url}" />
|
<link href="${article.url}" />
|
||||||
|
<link rel="alternate" type="text/html" href="${article.url}" />
|
||||||
|
<link rel="alternate" type="application/nostr+json" href="nostr:${article.naddr}" />
|
||||||
<updated>${isoDate(article.updatedAt)}</updated>
|
<updated>${isoDate(article.updatedAt)}</updated>
|
||||||
<published>${isoDate(article.publishedAt)}</published>
|
<published>${isoDate(article.publishedAt)}</published>
|
||||||
<summary>${article.summary}</summary>
|
<summary>${article.summary}</summary>
|
||||||
@@ -32,13 +34,17 @@ export async function profileAtomFeed(
|
|||||||
|
|
||||||
return `
|
return `
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<feed xmlns="http://www.w3.org/2005/Atom">
|
<feed xmlns="http://www.w3.org/2005/Atom" xmlns:nostr="urn:nostr:protocol">
|
||||||
<title>${profile.name} on Nostr (Articles)</title>
|
<title>${profile.name} on Nostr (Articles)</title>
|
||||||
|
<link rel="alternate" type="text/html" href="${profile.profileUrl}" />
|
||||||
|
<link rel="alternate" type="application/nostr+json" href="nostr:${profile.npub}" />
|
||||||
<id>${feedId}</id>
|
<id>${feedId}</id>
|
||||||
<updated>${isoDate(lastUpdate)}</updated>
|
<updated>${isoDate(lastUpdate)}</updated>
|
||||||
<icon>${profile.picture}</icon>
|
<icon>${profile.avatarImageUrl}</icon>
|
||||||
<author>
|
<author>
|
||||||
<name>${name}</name>
|
<name>${profile.name}</name>
|
||||||
|
<uri>${profile.profileUrl}</uri>
|
||||||
|
<nostr:uri>nostr:${profile.nprofile}</nostr:uri>
|
||||||
</author>
|
</author>
|
||||||
${articlesXml}
|
${articlesXml}
|
||||||
</feed>
|
</feed>
|
||||||
|
|||||||
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,13 +1,19 @@
|
|||||||
import { Context } from "@oak/oak";
|
import { Context } from "@oak/oak";
|
||||||
import { nip19 } from "@nostr/tools";
|
import { nip19 } from "@nostr/tools";
|
||||||
import { lookupUsernameByPubkey } from "../directory.ts";
|
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 naddrHandler = async function (ctx: Context) {
|
||||||
const naddr = ctx.state.path;
|
const naddr = ctx.state.path;
|
||||||
|
let data: nip19.AddressPointer;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = nip19.decode(naddr).data as nip19.AddressPointer;
|
data = nip19.decode(naddr).data as nip19.AddressPointer;
|
||||||
|
} catch (_e) {
|
||||||
|
notFoundHandler(ctx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const username = await lookupUsernameByPubkey(data.pubkey);
|
const username = await lookupUsernameByPubkey(data.pubkey);
|
||||||
|
|
||||||
if (username && data.identifier) {
|
if (username && data.identifier) {
|
||||||
@@ -15,9 +21,6 @@ const naddrHandler = async function (ctx: Context) {
|
|||||||
} else {
|
} else {
|
||||||
notFoundHandler(ctx);
|
notFoundHandler(ctx);
|
||||||
}
|
}
|
||||||
} catch (_e) {
|
|
||||||
notFoundHandler(ctx);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default naddrHandler;
|
export default naddrHandler;
|
||||||
|
|||||||
@@ -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,13 +1,19 @@
|
|||||||
import { Context } from "@oak/oak";
|
import { Context } from "@oak/oak";
|
||||||
import { nip19 } from "@nostr/tools";
|
import { nip19 } from "@nostr/tools";
|
||||||
import { lookupUsernameByPubkey } from "../directory.ts";
|
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 nprofileHandler = async function (ctx: Context) {
|
||||||
const nprofile = ctx.state.path;
|
const nprofile = ctx.state.path;
|
||||||
|
let data: nip19.ProfilePointer;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = nip19.decode(nprofile).data as nip19.ProfilePointer;
|
data = nip19.decode(nprofile).data as nip19.ProfilePointer;
|
||||||
|
} catch (_e) {
|
||||||
|
notFoundHandler(ctx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const username = await lookupUsernameByPubkey(data.pubkey);
|
const username = await lookupUsernameByPubkey(data.pubkey);
|
||||||
|
|
||||||
if (username) {
|
if (username) {
|
||||||
@@ -15,9 +21,6 @@ const nprofileHandler = async function (ctx: Context) {
|
|||||||
} else {
|
} else {
|
||||||
notFoundHandler(ctx);
|
notFoundHandler(ctx);
|
||||||
}
|
}
|
||||||
} catch (_e) {
|
|
||||||
notFoundHandler(ctx);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nprofileHandler;
|
export default nprofileHandler;
|
||||||
|
|||||||
@@ -1,13 +1,19 @@
|
|||||||
import { Context } from "@oak/oak";
|
import { Context } from "@oak/oak";
|
||||||
import { nip19 } from "@nostr/tools";
|
import { nip19 } from "@nostr/tools";
|
||||||
import { lookupUsernameByPubkey } from "../directory.ts";
|
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 npubHandler = async function (ctx: Context) {
|
||||||
const npub = ctx.state.path;
|
const npub = ctx.state.path;
|
||||||
|
let pubkey: string;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const pubkey = nip19.decode(npub).data as string;
|
pubkey = nip19.decode(npub).data as string;
|
||||||
|
} catch (_e) {
|
||||||
|
notFoundHandler(ctx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const username = await lookupUsernameByPubkey(pubkey);
|
const username = await lookupUsernameByPubkey(pubkey);
|
||||||
|
|
||||||
if (username) {
|
if (username) {
|
||||||
@@ -15,9 +21,6 @@ const npubHandler = async function (ctx: Context) {
|
|||||||
} else {
|
} else {
|
||||||
notFoundHandler(ctx);
|
notFoundHandler(ctx);
|
||||||
}
|
}
|
||||||
} catch (_e) {
|
|
||||||
notFoundHandler(ctx);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default npubHandler;
|
export default npubHandler;
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ import { Context } from "@oak/oak";
|
|||||||
import { lookupPubkeyByUsername } from "../directory.ts";
|
import { lookupPubkeyByUsername } from "../directory.ts";
|
||||||
import { fetchArticlesByAuthor, fetchProfileEvent } from "../nostr.ts";
|
import { fetchArticlesByAuthor, fetchProfileEvent } from "../nostr.ts";
|
||||||
import { profileAtomFeed } from "../feeds.ts";
|
import { profileAtomFeed } from "../feeds.ts";
|
||||||
import Article from "../models/article.ts";
|
|
||||||
import Profile from "../models/profile.ts";
|
import Profile from "../models/profile.ts";
|
||||||
import notFoundHandler from "../handlers/not-found.ts";
|
import { notFoundHandler } from "../handlers/errors.ts";
|
||||||
|
import { generateOgProfileImage } from "../magick.ts";
|
||||||
|
|
||||||
const userAtomFeedHandler = async function (ctx: Context) {
|
const userAtomFeedHandler = async function (ctx: Context) {
|
||||||
const username = ctx.state.username;
|
const username = ctx.state.username;
|
||||||
@@ -15,16 +15,15 @@ const userAtomFeedHandler = async function (ctx: Context) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
|
||||||
const profileEvent = await fetchProfileEvent(pubkey);
|
const profileEvent = await fetchProfileEvent(pubkey);
|
||||||
|
|
||||||
if (profileEvent) {
|
if (profileEvent) {
|
||||||
const profile = new Profile(profileEvent, username);
|
const profile = new Profile(profileEvent, username);
|
||||||
|
|
||||||
if (profile.nip05) {
|
if (profile.nip05) {
|
||||||
const articleEvents = await fetchArticlesByAuthor(pubkey);
|
await generateOgProfileImage(profile);
|
||||||
const articles = articleEvents.map((a) => new Article(a));
|
const articles = await fetchArticlesByAuthor(pubkey, 10);
|
||||||
const atom = profileAtomFeed(profile, articles);
|
const atom = await profileAtomFeed(profile, articles);
|
||||||
|
|
||||||
ctx.response.headers.set("Content-Type", "application/atom+xml");
|
ctx.response.headers.set("Content-Type", "application/atom+xml");
|
||||||
ctx.response.body = atom;
|
ctx.response.body = atom;
|
||||||
@@ -32,9 +31,6 @@ const userAtomFeedHandler = async function (ctx: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
notFoundHandler(ctx);
|
notFoundHandler(ctx);
|
||||||
} catch (_e) {
|
|
||||||
notFoundHandler(ctx);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default userAtomFeedHandler;
|
export default userAtomFeedHandler;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { fetchProfileEvent, fetchReplaceableEvent } from "../nostr.ts";
|
|||||||
import Article from "../models/article.ts";
|
import Article from "../models/article.ts";
|
||||||
import Profile from "../models/profile.ts";
|
import Profile from "../models/profile.ts";
|
||||||
import { articleHtml } from "../html.ts";
|
import { articleHtml } from "../html.ts";
|
||||||
import notFoundHandler from "../handlers/not-found.ts";
|
import { notFoundHandler } from "../handlers/errors.ts";
|
||||||
import { generateOgProfileImage } from "../magick.ts";
|
import { generateOgProfileImage } from "../magick.ts";
|
||||||
|
|
||||||
const userEventHandler = async function (ctx: Context) {
|
const userEventHandler = async function (ctx: Context) {
|
||||||
@@ -17,7 +17,6 @@ const userEventHandler = async function (ctx: Context) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
|
||||||
const articleEvent = await fetchReplaceableEvent(
|
const articleEvent = await fetchReplaceableEvent(
|
||||||
pubkey,
|
pubkey,
|
||||||
identifier,
|
identifier,
|
||||||
@@ -28,15 +27,12 @@ const userEventHandler = async function (ctx: Context) {
|
|||||||
const article = new Article(articleEvent);
|
const article = new Article(articleEvent);
|
||||||
const profile = new Profile(profileEvent, username);
|
const profile = new Profile(profileEvent, username);
|
||||||
const html = await articleHtml(article, profile);
|
const html = await articleHtml(article, profile);
|
||||||
generateOgProfileImage(profile);
|
await generateOgProfileImage(profile);
|
||||||
|
|
||||||
ctx.response.body = html;
|
ctx.response.body = html;
|
||||||
} else {
|
} else {
|
||||||
notFoundHandler(ctx);
|
notFoundHandler(ctx);
|
||||||
}
|
}
|
||||||
} catch (_e) {
|
|
||||||
notFoundHandler(ctx);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default userEventHandler;
|
export default userEventHandler;
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { Context } from "@oak/oak";
|
import { Context } from "@oak/oak";
|
||||||
import { lookupPubkeyByUsername } from "../directory.ts";
|
import { lookupPubkeyByUsername } from "../directory.ts";
|
||||||
import { fetchArticlesByAuthor, fetchProfileEvent } from "../nostr.ts";
|
import { fetchArticlesByAuthor, fetchProfileEvent } from "../nostr.ts";
|
||||||
import Article from "../models/article.ts";
|
|
||||||
import Profile from "../models/profile.ts";
|
import Profile from "../models/profile.ts";
|
||||||
import { profilePageHtml } from "../html.ts";
|
import { profilePageHtml } from "../html.ts";
|
||||||
import notFoundHandler from "../handlers/not-found.ts";
|
import { notFoundHandler } from "../handlers/errors.ts";
|
||||||
import { generateOgProfileImage } from "../magick.ts";
|
import { generateOgProfileImage } from "../magick.ts";
|
||||||
|
|
||||||
const userProfileHandler = async function (ctx: Context) {
|
const userProfileHandler = async function (ctx: Context) {
|
||||||
@@ -16,23 +15,18 @@ const userProfileHandler = async function (ctx: Context) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
|
||||||
const profileEvent = await fetchProfileEvent(pubkey);
|
const profileEvent = await fetchProfileEvent(pubkey);
|
||||||
|
|
||||||
if (profileEvent) {
|
if (profileEvent) {
|
||||||
const profile = new Profile(profileEvent, username);
|
const profile = new Profile(profileEvent, username);
|
||||||
const articleEvents = await fetchArticlesByAuthor(pubkey);
|
const articles = await fetchArticlesByAuthor(pubkey, 210);
|
||||||
const articles = articleEvents.map((a) => new Article(a));
|
const html = await profilePageHtml(profile, articles);
|
||||||
const html = profilePageHtml(profile, articles);
|
await generateOgProfileImage(profile);
|
||||||
generateOgProfileImage(profile);
|
|
||||||
|
|
||||||
ctx.response.body = html;
|
ctx.response.body = html;
|
||||||
} else {
|
} else {
|
||||||
notFoundHandler(ctx);
|
notFoundHandler(ctx);
|
||||||
}
|
}
|
||||||
} catch (_e) {
|
|
||||||
notFoundHandler(ctx);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default userProfileHandler;
|
export default userProfileHandler;
|
||||||
|
|||||||
61
html.ts
61
html.ts
@@ -19,6 +19,7 @@ function htmlLayout({ title, body, metaHtml }: HtmlLayoutOptions): string {
|
|||||||
<title>${title}</title>
|
<title>${title}</title>
|
||||||
${metaHtml || ""}
|
${metaHtml || ""}
|
||||||
<link rel="stylesheet" type="text/css" href="/assets/css/layout.css" />
|
<link rel="stylesheet" type="text/css" href="/assets/css/layout.css" />
|
||||||
|
<link rel="stylesheet" type="text/css" href="/assets/css/prism.css" />
|
||||||
<link rel="stylesheet" type="text/css" href="/assets/css/themes/default-light.css" />
|
<link rel="stylesheet" type="text/css" href="/assets/css/themes/default-light.css" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -53,9 +54,9 @@ export async function articleHtml(
|
|||||||
<main>
|
<main>
|
||||||
<header>
|
<header>
|
||||||
${draftLabel}
|
${draftLabel}
|
||||||
<h1>${article.title}</h1>
|
<h1>${titleHtml(article.title)}</h1>
|
||||||
<div class="meta">
|
<div class="meta">
|
||||||
<img class="avatar" src="${profile.picture}" alt="User Avatar" />
|
<img class="avatar" src="${profile.avatarImageUrl}" alt="User Avatar" />
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<span class="name"><a href="/@${profile.username}">${profile.name}</a></span>
|
<span class="name"><a href="/@${profile.username}">${profile.name}</a></span>
|
||||||
<span class="date">${publishedAtFormatted}</span>
|
<span class="date">${publishedAtFormatted}</span>
|
||||||
@@ -77,12 +78,16 @@ export async function articleHtml(
|
|||||||
return htmlLayout({ title: pageTitle, body, metaHtml });
|
return htmlLayout({ title: pageTitle, body, metaHtml });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function titleHtml(title: string) {
|
||||||
|
return title.replace(/`([^`]+)`/g, "<code>$1</code>");
|
||||||
|
}
|
||||||
|
|
||||||
function articleListItemHtml(article: Article): string {
|
function articleListItemHtml(article: Article): string {
|
||||||
const formattedDate = localizeDate(article.publishedAt);
|
const formattedDate = localizeDate(article.publishedAt);
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="item">
|
<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">
|
<p class="meta">
|
||||||
${formattedDate}
|
${formattedDate}
|
||||||
</p>
|
</p>
|
||||||
@@ -92,10 +97,9 @@ function articleListItemHtml(article: Article): string {
|
|||||||
|
|
||||||
export function articleListHtml(articles: Article[]): string {
|
export function articleListHtml(articles: Article[]): string {
|
||||||
if (articles.length === 0) return "";
|
if (articles.length === 0) return "";
|
||||||
const sortedArticles = articles.sort((a, b) => b.publishedAt - a.publishedAt);
|
|
||||||
let html = "";
|
let html = "";
|
||||||
|
|
||||||
for (const article of sortedArticles) {
|
for (const article of articles) {
|
||||||
html += articleListItemHtml(article);
|
html += articleListItemHtml(article);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,26 +111,58 @@ export function articleListHtml(articles: Article[]): string {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function profilePageHtml(profile: Profile, articles: Article[]): string {
|
function userAddressHtml(profile: Profile) {
|
||||||
|
let html = "";
|
||||||
|
|
||||||
|
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`;
|
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 = `
|
const body = `
|
||||||
<main class="profile-page">
|
<main class="profile-page">
|
||||||
<header>
|
<header>
|
||||||
<img class="avatar" src="${profile.picture}" alt="User Avatar" />
|
<img class="avatar" src="${profile.avatarImageUrl}" alt="User Avatar" />
|
||||||
<div class="bio">
|
<div class="bio">
|
||||||
<h1>${profile.name}</h1>
|
<h1>${profile.name}</h1>
|
||||||
|
${nip05Html}
|
||||||
<p class="about">
|
<p class="about">
|
||||||
${profile.about}
|
${profile.about}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<section>
|
<details>
|
||||||
|
<summary>Details</summary>
|
||||||
<dl>
|
<dl>
|
||||||
<dt>Public key</dt>
|
<dt>Public key</dt>
|
||||||
<dd>${profile.npub}</dd>
|
<dd>${profile.npub}</dd>
|
||||||
|
${userAddressHtml(profile)}
|
||||||
</dl>
|
</dl>
|
||||||
</section>
|
</details>
|
||||||
<section>
|
<section>
|
||||||
${articleListHtml(articles)}
|
${articleListHtml(articles)}
|
||||||
</section>
|
</section>
|
||||||
@@ -173,6 +209,9 @@ function feedLinksHtml(profile: Profile) {
|
|||||||
|
|
||||||
function profileMetaHtml(profile: Profile) {
|
function profileMetaHtml(profile: Profile) {
|
||||||
return `
|
return `
|
||||||
|
<link rel="icon" href="${profile.avatarImageUrl}" type="image/png">
|
||||||
|
<link rel="me" type="application/nostr+json" href="nostr:${profile.nprofile}" title="${profile.name}">
|
||||||
|
<link rel="alternate" type="application/nostr+json" href="nostr:${profile.nprofile}" title="${profile.name} on Nostr">
|
||||||
<meta property="og:url" content="${profile.profileUrl}">
|
<meta property="og:url" content="${profile.profileUrl}">
|
||||||
<meta property="og:type" content="website">
|
<meta property="og:type" content="website">
|
||||||
<meta property="og:title" content="${profile.name} on Nostr">
|
<meta property="og:title" content="${profile.name} on Nostr">
|
||||||
@@ -191,6 +230,10 @@ function articleMetaHtml(article: Article, profile: Profile) {
|
|||||||
const imageUrl = article.image || profile.ogImageUrl;
|
const imageUrl = article.image || profile.ogImageUrl;
|
||||||
|
|
||||||
return `
|
return `
|
||||||
|
<link rel="icon" href="${profile.avatarImageUrl}" type="image/png">
|
||||||
|
<link rel="alternate" type="application/nostr+json" href="nostr:${article.naddr}" title="This article on Nostr">
|
||||||
|
<link rel="author" type="text/html" href="${profile.profileUrl}" title="${profile.name}">
|
||||||
|
<link rel="author" type="application/nostr+json" href="nostr:${profile.nprofile}" title="${profile.name}">
|
||||||
<meta property="og:url" content="${article.url}">
|
<meta property="og:url" content="${article.url}">
|
||||||
<meta property="og:type" content="website">
|
<meta property="og:type" content="website">
|
||||||
<meta property="og:title" content="${article.title}">
|
<meta property="og:title" content="${article.title}">
|
||||||
|
|||||||
30
ldap.ts
30
ldap.ts
@@ -8,8 +8,10 @@ if (ldapEnabled) {
|
|||||||
client = new Client({ url: ldap.url as string });
|
client = new Client({ url: ldap.url as string });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function lookupPubkeyByUsername(username: string) {
|
export async function lookupPubkeyByUsername(
|
||||||
let pubkey;
|
username: string,
|
||||||
|
): Promise<string | undefined> {
|
||||||
|
let pubkey: string | undefined;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await client.bind(ldap.bindDN as string, ldap.password as string);
|
await client.bind(ldap.bindDN as string, ldap.password as string);
|
||||||
@@ -23,19 +25,22 @@ export async function lookupPubkeyByUsername(username: string) {
|
|||||||
searchEntries.length > 0 &&
|
searchEntries.length > 0 &&
|
||||||
typeof searchEntries[0].nostrKey === "string"
|
typeof searchEntries[0].nostrKey === "string"
|
||||||
) {
|
) {
|
||||||
pubkey = searchEntries[0].nostrKey;
|
pubkey = searchEntries[0].nostrKey.toString();
|
||||||
}
|
}
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
} finally {
|
|
||||||
await client.unbind();
|
await client.unbind();
|
||||||
|
} catch (e) {
|
||||||
|
await client.unbind();
|
||||||
|
throw e;
|
||||||
}
|
}
|
||||||
|
|
||||||
return pubkey;
|
return pubkey;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function lookupUsernameByPubkey(pubkey: string) {
|
export async function lookupUsernameByPubkey(
|
||||||
let username;
|
pubkey: string,
|
||||||
|
): Promise<string | undefined> {
|
||||||
|
let username: string | undefined;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await client.bind(ldap.bindDN as string, ldap.password as string);
|
await client.bind(ldap.bindDN as string, ldap.password as string);
|
||||||
@@ -46,12 +51,13 @@ export async function lookupUsernameByPubkey(pubkey: string) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (searchEntries.length > 0) {
|
if (searchEntries.length > 0) {
|
||||||
username = searchEntries[0].cn;
|
username = searchEntries[0].cn.toString();
|
||||||
}
|
}
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
} finally {
|
|
||||||
await client.unbind();
|
await client.unbind();
|
||||||
|
} catch (e) {
|
||||||
|
await client.unbind();
|
||||||
|
throw e;
|
||||||
}
|
}
|
||||||
|
|
||||||
return username;
|
return username;
|
||||||
|
|||||||
52
magick.ts
52
magick.ts
@@ -8,12 +8,31 @@ if (!magick) {
|
|||||||
log("ImageMagick is not installed. Cannot generate preview images", "yellow")
|
log("ImageMagick is not installed. Cannot generate preview images", "yellow")
|
||||||
}
|
}
|
||||||
|
|
||||||
function createRoundedImage(profile: Profile) {
|
function createProfileImage(profile: Profile) {
|
||||||
if (!magick || !profile.picture) return false;
|
if (!magick || !profile.picture) return false;
|
||||||
|
|
||||||
const args = [
|
const args = [
|
||||||
profile.picture,
|
profile.picture,
|
||||||
'-resize', '256x256',
|
'-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',
|
'(', '+clone', '-alpha', 'extract',
|
||||||
'-draw', "fill black polygon 0,0 0,128 128,0 fill white circle 128,128 128,0",
|
'-draw', "fill black polygon 0,0 0,128 128,0 fill white circle 128,128 128,0",
|
||||||
'(', '+clone', '-flip', ')', '-compose', 'Multiply', '-composite',
|
'(', '+clone', '-flip', ')', '-compose', 'Multiply', '-composite',
|
||||||
@@ -26,12 +45,15 @@ function createRoundedImage(profile: Profile) {
|
|||||||
];
|
];
|
||||||
|
|
||||||
return runCommand(magick, args);
|
return runCommand(magick, args);
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createOgImage(profile: Profile, ogImagePath: string, backgroundColor: string) {
|
async function createOgImage(profile: Profile, ogImagePath: string, backgroundColor: string) {
|
||||||
if (!magick) return false;
|
if (!magick) return false;
|
||||||
|
|
||||||
const status = await createRoundedImage(profile);
|
const status = await createRoundedProfileImage(profile);
|
||||||
|
|
||||||
if (status && status.success) {
|
if (status && status.success) {
|
||||||
const args = [
|
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) {
|
export async function generateOgProfileImage(profile: Profile) {
|
||||||
if (!magick || !profile.picture) return false;
|
if (!magick || !profile.picture) return false;
|
||||||
|
|
||||||
@@ -56,10 +97,13 @@ export async function generateOgProfileImage(profile: Profile) {
|
|||||||
const backgroundColor = "#333333";
|
const backgroundColor = "#333333";
|
||||||
const fileExists = await checkFileExists(ogImagePath);
|
const fileExists = await checkFileExists(ogImagePath);
|
||||||
|
|
||||||
if (!fileExists) {
|
if (fileExists) {
|
||||||
|
return { success: true };
|
||||||
|
} else {
|
||||||
const status = await createOgImage(profile, ogImagePath, backgroundColor);
|
const status = await createOgImage(profile, ogImagePath, backgroundColor);
|
||||||
if (status && status.success) {
|
if (status && status.success) {
|
||||||
log(`Created OG image for ${profile.username}: ${ogImagePath}`, "blue")
|
log(`Created OG image for ${profile.username}: ${ogImagePath}`, "blue")
|
||||||
|
return status;
|
||||||
} else {
|
} else {
|
||||||
log(`Could not create OG image for ${profile.username}`, "yellow")
|
log(`Could not create OG image for ${profile.username}`, "yellow")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
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 { replaceNostrUris } from "../nostr/links.ts";
|
||||||
import config from "../config.ts";
|
import config from "../config.ts";
|
||||||
|
|
||||||
|
for (const language of config.prism.extraLanguages) {
|
||||||
|
await import(`npm:prismjs@1.29.0/components/prism-${language}.js`);
|
||||||
|
}
|
||||||
|
|
||||||
export default class Article {
|
export default class Article {
|
||||||
event: NEvent;
|
event: NEvent;
|
||||||
|
|
||||||
@@ -48,6 +52,14 @@ export default class Article {
|
|||||||
return this.event.created_at;
|
return this.event.created_at;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get content(): string {
|
||||||
|
return this.event.content;
|
||||||
|
}
|
||||||
|
|
||||||
|
get isDeleted(): boolean {
|
||||||
|
return !!this.event.tags.find((t) => t[0] === "deleted");
|
||||||
|
}
|
||||||
|
|
||||||
get naddr(): string {
|
get naddr(): string {
|
||||||
return nip19.naddrEncode({
|
return nip19.naddrEncode({
|
||||||
identifier: this.identifier,
|
identifier: this.identifier,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { nip19, NostrEvent as NEvent } from "@nostr/tools";
|
import { nip19, NostrEvent as NEvent } from "@nostr/tools";
|
||||||
|
import { verifyNip05Address } from "../nostr.ts";
|
||||||
import { getImageMagickCommand } from "../utils.ts";
|
import { getImageMagickCommand } from "../utils.ts";
|
||||||
import config from "../config.ts";
|
import config from "../config.ts";
|
||||||
|
|
||||||
@@ -43,7 +44,7 @@ export default class Profile {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get nip05(): string | undefined {
|
get nip05(): string | undefined {
|
||||||
return this.data.nip05;
|
return this.data.nip05?.replace("_@", "");
|
||||||
}
|
}
|
||||||
|
|
||||||
get lud16(): string | undefined {
|
get lud16(): string | undefined {
|
||||||
@@ -58,10 +59,25 @@ export default class Profile {
|
|||||||
return nip19.npubEncode(this.pubkey);
|
return nip19.npubEncode(this.pubkey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get nprofile(): string {
|
||||||
|
return nip19.nprofileEncode({
|
||||||
|
pubkey: this.pubkey,
|
||||||
|
relays: [config.relay_urls[0]],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
get profileUrl(): string {
|
get profileUrl(): string {
|
||||||
return `${config.base_url}/@${this.username}`;
|
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 {
|
get ogImageUrl(): string {
|
||||||
if (magick) {
|
if (magick) {
|
||||||
return `${config.base_url}/assets/g/img/og-p-${this.event.id}.png`;
|
return `${config.base_url}/assets/g/img/og-p-${this.event.id}.png`;
|
||||||
@@ -69,4 +85,12 @@ export default class Profile {
|
|||||||
return this.picture || "";
|
return this.picture || "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
verifyNip05(): Promise<boolean> {
|
||||||
|
if (typeof this.data.nip05 !== "undefined") {
|
||||||
|
return verifyNip05Address(this.data.nip05, this.pubkey);
|
||||||
|
} else {
|
||||||
|
return Promise.resolve(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
97
nostr.ts
97
nostr.ts
@@ -1,7 +1,6 @@
|
|||||||
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 config from "./config.ts";
|
||||||
|
import Article from "./models/article.ts";
|
||||||
|
|
||||||
const relayPool = new NPool({
|
const relayPool = new NPool({
|
||||||
open: (url) => new NRelay1(url),
|
open: (url) => new NRelay1(url),
|
||||||
@@ -14,92 +13,84 @@ const relayPool = new NPool({
|
|||||||
eventRouter: async (_event) => [],
|
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(
|
export async function fetchReplaceableEvent(
|
||||||
pubkey: string,
|
pubkey: string,
|
||||||
identifier: string,
|
identifier: string,
|
||||||
) {
|
) {
|
||||||
let events = await relayPool.query([{
|
let events = await fetchWithTimeout([{
|
||||||
authors: [pubkey],
|
authors: [pubkey],
|
||||||
kinds: [30023],
|
kinds: [30023],
|
||||||
"#d": [identifier],
|
"#d": [identifier],
|
||||||
limit: 1,
|
limit: 1,
|
||||||
}]);
|
}]) as NostrEvent[];
|
||||||
|
|
||||||
if (events.length > 0) {
|
if (events.length > 0) {
|
||||||
return events[0];
|
return events[0];
|
||||||
} else {
|
} else {
|
||||||
events = await relayPool.query([{
|
events = await fetchWithTimeout([{
|
||||||
authors: [pubkey],
|
authors: [pubkey],
|
||||||
kinds: [30024],
|
kinds: [30024],
|
||||||
"#d": [identifier],
|
"#d": [identifier],
|
||||||
limit: 1,
|
limit: 1,
|
||||||
}]);
|
}]) as NostrEvent[];
|
||||||
|
|
||||||
return events.length > 0 ? events[0] : null;
|
return events.length > 0 ? events[0] : null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchArticlesByAuthor(pubkey: string) {
|
export async function fetchArticlesByAuthor(
|
||||||
const events = await relayPool.query([{
|
pubkey: string,
|
||||||
|
limit: number = 10,
|
||||||
|
) {
|
||||||
|
const events = await fetchWithTimeout([{
|
||||||
authors: [pubkey],
|
authors: [pubkey],
|
||||||
kinds: [30023],
|
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) {
|
export async function fetchProfileEvent(pubkey: string) {
|
||||||
const events = await relayPool.query([{
|
const events = await fetchWithTimeout([{
|
||||||
authors: [pubkey],
|
authors: [pubkey],
|
||||||
kinds: [0],
|
kinds: [0],
|
||||||
limit: 1,
|
limit: 1,
|
||||||
}]);
|
}]) as NostrEvent[];
|
||||||
|
|
||||||
return events.length > 0 ? events[0] : null;
|
return events.length > 0 ? events[0] : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function nostrUriToUrl(uri: string): Promise<string> {
|
export async function verifyNip05Address(
|
||||||
const bech32 = uri.replace(/^nostr:/, "");
|
address: string,
|
||||||
|
pubkey: string,
|
||||||
|
): Promise<boolean> {
|
||||||
|
const [username, host] = address.split("@");
|
||||||
|
const url = `https://${host}/.well-known/nostr.json?name=${username}`;
|
||||||
|
|
||||||
if (bech32.match(/^(naddr|nprofile|npub)/)) {
|
|
||||||
try {
|
try {
|
||||||
const r = nip19.decode(bech32);
|
const res = await fetch(url);
|
||||||
let username;
|
if (res.status === 404 || !res.ok) return false;
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
switch (r.type) {
|
return data.names && data.names[username] === pubkey;
|
||||||
case "naddr":
|
} catch (_e) {
|
||||||
username = await lookupUsernameByPubkey(r.data.pubkey);
|
return false;
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|||||||
137
nostr/links.ts
Normal file
137
nostr/links.ts
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
import { lookupUsernameByPubkey } from "../directory.ts";
|
||||||
|
import { nip19 } from "@nostr/tools";
|
||||||
|
import config from "../config.ts";
|
||||||
|
|
||||||
|
const nostrUriRegex = /(nostr:|@)(nprofile|naddr|nevent|npub)[a-z0-9]+/g;
|
||||||
|
|
||||||
|
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 protectedRegex =
|
||||||
|
/(`{3,}[\s\S]*?`{3,})|(`[^`]*`)|(<pre>[\s\S]*?<\/pre>)|(https?:\/\/[^\s<>"']+)/gi;
|
||||||
|
|
||||||
|
// Split text into segments: unprotected text and protected areas (code blocks, URLs)
|
||||||
|
const segments: string[] = [];
|
||||||
|
let lastIndex = 0;
|
||||||
|
|
||||||
|
markdown.replace(
|
||||||
|
protectedRegex,
|
||||||
|
(match, _fencedCode, _inlineCode, _preTag, _url, index) => {
|
||||||
|
segments.push(markdown.slice(lastIndex, index));
|
||||||
|
segments.push(match);
|
||||||
|
lastIndex = index + match.length;
|
||||||
|
return match;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
segments.push(markdown.slice(lastIndex));
|
||||||
|
|
||||||
|
// Process each segment
|
||||||
|
let result = "";
|
||||||
|
for (let i = 0; i < segments.length; i++) {
|
||||||
|
if (i % 2 === 1 || protectedRegex.test(segments[i])) {
|
||||||
|
// Protected segment (code block or URL), leave unchanged
|
||||||
|
result += segments[i];
|
||||||
|
} else {
|
||||||
|
// Unprotected text, replace URIs and handle markdown links
|
||||||
|
result += await processUnprotectedText(segments[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function processUnprotectedText(text: string): Promise<string> {
|
||||||
|
const markdownLinkRegex = /\[([^\]]+)\]\(([^)]+)\)/g;
|
||||||
|
let lastIndex = 0;
|
||||||
|
const parts: string[] = [];
|
||||||
|
|
||||||
|
// Process markdown links first
|
||||||
|
let match;
|
||||||
|
while ((match = markdownLinkRegex.exec(text)) !== null) {
|
||||||
|
const [fullMatch, linkText, target] = match;
|
||||||
|
|
||||||
|
// Add text before the link
|
||||||
|
parts.push(await replaceUrisInText(text.slice(lastIndex, match.index)));
|
||||||
|
|
||||||
|
// Process the link target
|
||||||
|
if (
|
||||||
|
nostrUriRegex.test(target) && target.match(nostrUriRegex)![0] === target
|
||||||
|
) {
|
||||||
|
// Target is a Nostr URI, replace with resolved URL
|
||||||
|
const resolvedUrl = await nostrUriToUrl(target);
|
||||||
|
parts.push(`[${linkText}](${resolvedUrl})`);
|
||||||
|
} else {
|
||||||
|
// Not a Nostr URI, keep the original link
|
||||||
|
parts.push(fullMatch);
|
||||||
|
}
|
||||||
|
|
||||||
|
lastIndex = match.index + fullMatch.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add any remaining text after the last link
|
||||||
|
parts.push(await replaceUrisInText(text.slice(lastIndex)));
|
||||||
|
|
||||||
|
return parts.join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function replaceUrisInText(text: string): Promise<string> {
|
||||||
|
let modifiedText = text;
|
||||||
|
const replacements: { start: number; end: number; replacement: string }[] =
|
||||||
|
[];
|
||||||
|
|
||||||
|
// Collect all replacements for bare Nostr URIs
|
||||||
|
let match;
|
||||||
|
while ((match = nostrUriRegex.exec(modifiedText)) !== null) {
|
||||||
|
const fullUri = match[0];
|
||||||
|
const url = await nostrUriToUrl(fullUri);
|
||||||
|
const linkTitle = cleanUriForTitle(fullUri);
|
||||||
|
const markdownLink = `[${linkTitle}](${url})`;
|
||||||
|
replacements.push({
|
||||||
|
start: match.index,
|
||||||
|
end: match.index + fullUri.length,
|
||||||
|
replacement: markdownLink,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply replacements from right to left to avoid index shifting
|
||||||
|
for (let i = replacements.length - 1; i >= 0; i--) {
|
||||||
|
const { start, end, replacement } = replacements[i];
|
||||||
|
modifiedText = modifiedText.slice(0, start) + replacement +
|
||||||
|
modifiedText.slice(end);
|
||||||
|
}
|
||||||
|
|
||||||
|
return modifiedText;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanUriForTitle(uri: string): string {
|
||||||
|
// Remove "nostr:" prefix, keep "@" for the title
|
||||||
|
return uri.startsWith("nostr:") ? uri.replace(/^nostr:/, "") : uri;
|
||||||
|
}
|
||||||
42
server.ts
42
server.ts
@@ -7,13 +7,18 @@ import npubHandler from "./handlers/npub.ts";
|
|||||||
import userProfileHandler from "./handlers/user-profile.ts";
|
import userProfileHandler from "./handlers/user-profile.ts";
|
||||||
import userEventHandler from "./handlers/user-event.ts";
|
import userEventHandler from "./handlers/user-event.ts";
|
||||||
import userAtomFeedHandler from "./handlers/user-atom-feed.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();
|
const router = new Router();
|
||||||
|
|
||||||
router.get("/:path", async (ctx) => {
|
router.get("/:path", async (ctx) => {
|
||||||
const path = ctx.state.path = ctx.params.path;
|
const path = ctx.state.path = ctx.params.path;
|
||||||
|
|
||||||
|
try {
|
||||||
if (path.startsWith("naddr")) {
|
if (path.startsWith("naddr")) {
|
||||||
await naddrHandler(ctx);
|
await naddrHandler(ctx);
|
||||||
} else if (path.startsWith("nprofile")) {
|
} else if (path.startsWith("nprofile")) {
|
||||||
@@ -25,6 +30,14 @@ router.get("/:path", async (ctx) => {
|
|||||||
} else {
|
} else {
|
||||||
notFoundHandler(ctx);
|
notFoundHandler(ctx);
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
if (e instanceof Error && e.message.match(/^connect|relay timeout/)) {
|
||||||
|
badGatewayHandler(ctx);
|
||||||
|
} else {
|
||||||
|
internalServerErrorHandler(ctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
router.get("/:username/:kind.atom", async (ctx) => {
|
router.get("/:username/:kind.atom", async (ctx) => {
|
||||||
@@ -36,7 +49,16 @@ router.get("/:username/:kind.atom", async (ctx) => {
|
|||||||
(ctx.params.username.startsWith("@") ||
|
(ctx.params.username.startsWith("@") ||
|
||||||
ctx.params.username.startsWith("~"))
|
ctx.params.username.startsWith("~"))
|
||||||
) {
|
) {
|
||||||
|
try {
|
||||||
await userAtomFeedHandler(ctx);
|
await userAtomFeedHandler(ctx);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
if (e instanceof Error && e.message.match(/^connect|relay timeout/)) {
|
||||||
|
badGatewayHandler(ctx);
|
||||||
|
} else {
|
||||||
|
internalServerErrorHandler(ctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
notFoundHandler(ctx);
|
notFoundHandler(ctx);
|
||||||
}
|
}
|
||||||
@@ -47,7 +69,16 @@ router.get("/:username/:identifier", async (ctx) => {
|
|||||||
ctx.state.identifier = ctx.params.identifier;
|
ctx.state.identifier = ctx.params.identifier;
|
||||||
|
|
||||||
if (username.startsWith("@") || username.startsWith("~")) {
|
if (username.startsWith("@") || username.startsWith("~")) {
|
||||||
|
try {
|
||||||
await userEventHandler(ctx);
|
await userEventHandler(ctx);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
if (e instanceof Error && e.message.match(/^connect|relay timeout/)) {
|
||||||
|
badGatewayHandler(ctx);
|
||||||
|
} else {
|
||||||
|
internalServerErrorHandler(ctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
notFoundHandler(ctx);
|
notFoundHandler(ctx);
|
||||||
}
|
}
|
||||||
@@ -62,12 +93,17 @@ router.get("/assets/:path*", async (ctx) => {
|
|||||||
filePath = filePath.replace(/^g\//, "");
|
filePath = filePath.replace(/^g\//, "");
|
||||||
root = "/tmp/substr";
|
root = "/tmp/substr";
|
||||||
} else {
|
} else {
|
||||||
root = `${Deno.cwd()}/assets`;
|
root = `${import.meta.dirname}/assets`;
|
||||||
}
|
}
|
||||||
|
|
||||||
await send(ctx, filePath, { root });
|
await send(ctx, filePath, { root });
|
||||||
} catch (_e) {
|
} catch (e) {
|
||||||
|
if (e instanceof Error && e.name === "NotFoundError") {
|
||||||
notFoundHandler(ctx);
|
notFoundHandler(ctx);
|
||||||
|
} else {
|
||||||
|
console.error(e);
|
||||||
|
badGatewayHandler(ctx);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
41
tests/fixtures/article-2.md
vendored
Normal file
41
tests/fixtures/article-2.md
vendored
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
I think we should agree on an HTML attribute for pointing to the Nostr representation of a document/URL on the Web. We could use the existing one for link relations for example. Something like:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<link rel="alternate" type="application/nostr+json"
|
||||||
|
href="nostr:naddr1qvzqqqr4..."
|
||||||
|
title="This article on Nostr" />
|
||||||
|
```
|
||||||
|
|
||||||
|
This would be useful in multiple ways:
|
||||||
|
|
||||||
|
1. Existing Web publications can retroactively create Nostr versions of their content and easily link the Nostr articles on all of their existing article pages.
|
||||||
|
2. Nostr clients, when fetching meta/preview information for a URL that is linked in a note, can detect that there's a Nostr representation of the content, and then render it in Nostr-native ways (whatever that may be depending on the client)
|
||||||
|
3. User agents, usually a browser or browser extension, when opening a URL on the Web, can offer opening the alternative representation of a page in a Nostr client. And/or they could offer to follow the author's pubkey on Nostr. And/or they could offer to zap the content.
|
||||||
|
4. When publishing a new article on Nostr, authors can share their preferred Web URL everywhere, without having to consider if the recipients' clients support Nostr IDs and content or not. This makes it easy for the reader to share the author's preferred Web URL on any medium, instead of sharing a link to whatever their own Nostr client prefers (usually its own Web UI).
|
||||||
|
|
||||||
|
|
||||||
|
### Testing Nostr IDs
|
||||||
|
|
||||||
|
Receive [special badges](https://badges.page/p/npub1cpmvpsqtzxl4px44dp4544xwgu0ryv2lscl3qexq42dfakuza02s4fsapc)
|
||||||
|
|
||||||
|
raucao scheme 1: nostr:npub1raustrrh5gjwt03zdj8syn9vmt2dwsv9t467m8c3gua636uxu89svgdees
|
||||||
|
raucao at 1: @npub1raustrrh5gjwt03zdj8syn9vmt2dwsv9t467m8c3gua636uxu89svgdees
|
||||||
|
raucao scheme 2: nostr:npub1raustrrh5gjwt03zdj8syn9vmt2dwsv9t467m8c3gua636uxu89svgdees
|
||||||
|
raucao at 2: @npub1raustrrh5gjwt03zdj8syn9vmt2dwsv9t467m8c3gua636uxu89svgdees
|
||||||
|
raucao scheme link 1: [raucao](nostr:npub1raustrrh5gjwt03zdj8syn9vmt2dwsv9t467m8c3gua636uxu89svgdees)
|
||||||
|
raucao scheme link 2: [raucao](nostr:npub1raustrrh5gjwt03zdj8syn9vmt2dwsv9t467m8c3gua636uxu89svgdees)
|
||||||
|
|
||||||
|
Amber scheme 1: nostr:npub1am3ermkr250dywukzqnaug64cred3x5jht6f3kdhfp3h0rgtjlpqecxrv7
|
||||||
|
Amber at 1: @npub1am3ermkr250dywukzqnaug64cred3x5jht6f3kdhfp3h0rgtjlpqecxrv7
|
||||||
|
Amber scheme 2: nostr:npub1am3ermkr250dywukzqnaug64cred3x5jht6f3kdhfp3h0rgtjlpqecxrv7
|
||||||
|
Amber at 2: @npub1am3ermkr250dywukzqnaug64cred3x5jht6f3kdhfp3h0rgtjlpqecxrv7
|
||||||
|
Amber scheme link 1: [Amber](nostr:npub1am3ermkr250dywukzqnaug64cred3x5jht6f3kdhfp3h0rgtjlpqecxrv7)
|
||||||
|
Amber scheme link 2: [Amber](nostr:npub1am3ermkr250dywukzqnaug64cred3x5jht6f3kdhfp3h0rgtjlpqecxrv7)
|
||||||
|
|
||||||
|
## More protected text
|
||||||
|
|
||||||
|
```
|
||||||
|
Follow nostr:npub1am3ermkr250dywukzqnaug64cred3x5jht6f3kdhfp3h0rgtjlpqecxrv7
|
||||||
|
```
|
||||||
|
|
||||||
|
Inline: `raucao: nostr:npub1raustrrh5gjwt03zdj8syn9vmt2dwsv9t467m8c3gua636uxu89svgdees`
|
||||||
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 { beforeAll, describe, it } from "@std/testing/bdd";
|
||||||
import { expect } from "@std/expect";
|
import { expect } from "@std/expect";
|
||||||
import { NostrEvent as NEvent } from "@nostrify/nostrify";
|
|
||||||
import Article from "../../models/article.ts";
|
import Article from "../../models/article.ts";
|
||||||
|
|
||||||
describe("Article", () => {
|
describe("Article", () => {
|
||||||
let articleEvent: NEvent;
|
|
||||||
let article: Article;
|
let article: Article;
|
||||||
|
let deletedArticle: Article;
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
articleEvent = JSON.parse(
|
article = new Article(JSON.parse(
|
||||||
Deno.readTextFileSync("tests/fixtures/article-1.json"),
|
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", () => {
|
describe("#identifier", () => {
|
||||||
@@ -58,10 +59,17 @@ describe("Article", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("#isDeleted", () => {
|
||||||
|
it("returns a boolean based on the 'deleted' tag", () => {
|
||||||
|
expect(article.isDeleted).toEqual(false);
|
||||||
|
expect(deletedArticle.isDeleted).toEqual(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
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(
|
||||||
/naddr1qvzqqqr4gupzq8meqkx80g3yuklzymy0qf/,
|
/^naddr1qvzqqqr4gupzq8meqkx80g3yuklzymy0qf/,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -40,4 +40,12 @@ describe("Profile", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("#nprofile", () => {
|
||||||
|
it("returns a bech32 profile ID", () => {
|
||||||
|
expect(profile.nprofile).toMatch(
|
||||||
|
/^nprofile1qyt8wumn8ghj7mn0wd68ytntdaek6mmn9ehhyecqyq0hjpvvw73zfed7yf/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
97
tests/nostr/links_test.ts
Normal file
97
tests/nostr/links_test.ts
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
import { beforeAll, describe, it } from "@std/testing/bdd";
|
||||||
|
import { expect } from "@std/expect";
|
||||||
|
import { replaceNostrUris } from "../../nostr/links.ts";
|
||||||
|
|
||||||
|
describe("Nostr links", () => {
|
||||||
|
describe("#replaceNostrUris", () => {
|
||||||
|
let mdContent: string;
|
||||||
|
let result: string;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
mdContent = Deno.readTextFileSync("tests/fixtures/article-2.md");
|
||||||
|
result = await replaceNostrUris(mdContent);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not replace URIs in URLs", () => {
|
||||||
|
expect(result).toMatch(
|
||||||
|
new RegExp(
|
||||||
|
"https://badges.page/p/npub1cpmvpsqtzxl4px44dp4544xwgu0ryv2lscl3qexq42dfakuza02s4fsapc",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not replace URIs in fenced code blocks", () => {
|
||||||
|
expect(result).toMatch(
|
||||||
|
new RegExp(
|
||||||
|
"Follow nostr:npub1am3ermkr250dywukzqnaug64cred3x5jht6f3kdhfp3h0rgtjlpqecxrv7",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not replace URIs in inline code blocks", () => {
|
||||||
|
expect(result).toMatch(
|
||||||
|
new RegExp(
|
||||||
|
"raucao: nostr:npub1raustrrh5gjwt03zdj8syn9vmt2dwsv9t467m8c3gua636uxu89svgdees",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("for unknown usernames", () => {
|
||||||
|
it("replaces plain nostr:id URIs with a markdown link", () => {
|
||||||
|
expect(result).toMatch(
|
||||||
|
/Amber scheme 1\: \[npub1am3ermkr250dywukzqnaug64cred3x5jht6f3kdhfp3h0rgtjlpqecxrv7\]\(https\:\/\/njump\.me\/npub1am3ermkr250dywukzqnaug64cred3x5jht6f3kdhfp3h0rgtjlpqecxrv7\)/,
|
||||||
|
);
|
||||||
|
expect(result).toMatch(
|
||||||
|
/Amber scheme 2\: \[npub1am3ermkr250dywukzqnaug64cred3x5jht6f3kdhfp3h0rgtjlpqecxrv7\]\(https\:\/\/njump\.me\/npub1am3ermkr250dywukzqnaug64cred3x5jht6f3kdhfp3h0rgtjlpqecxrv7\)/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("replaces @id URIs with a markdown link", () => {
|
||||||
|
expect(result).toMatch(
|
||||||
|
/Amber at 1\: \[@npub1am3ermkr250dywukzqnaug64cred3x5jht6f3kdhfp3h0rgtjlpqecxrv7\]\(https\:\/\/njump\.me\/npub1am3ermkr250dywukzqnaug64cred3x5jht6f3kdhfp3h0rgtjlpqecxrv7\)/,
|
||||||
|
);
|
||||||
|
expect(result).toMatch(
|
||||||
|
/Amber at 2\: \[@npub1am3ermkr250dywukzqnaug64cred3x5jht6f3kdhfp3h0rgtjlpqecxrv7\]\(https\:\/\/njump\.me\/npub1am3ermkr250dywukzqnaug64cred3x5jht6f3kdhfp3h0rgtjlpqecxrv7\)/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("replaces nostr links with external links", () => {
|
||||||
|
expect(result).toMatch(
|
||||||
|
/Amber scheme link 1\: \[Amber\]\(https\:\/\/njump\.me\/npub1am3ermkr250dywukzqnaug64cred3x5jht6f3kdhfp3h0rgtjlpqecxrv7\)/,
|
||||||
|
);
|
||||||
|
expect(result).toMatch(
|
||||||
|
/Amber scheme link 2\: \[Amber\]\(https\:\/\/njump\.me\/npub1am3ermkr250dywukzqnaug64cred3x5jht6f3kdhfp3h0rgtjlpqecxrv7\)/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("for known usernames", () => {
|
||||||
|
it("replaces plain nostr:id URIs with a markdown link", () => {
|
||||||
|
expect(result).toMatch(
|
||||||
|
/raucao scheme 1\: \[npub1raustrrh5gjwt03zdj8syn9vmt2dwsv9t467m8c3gua636uxu89svgdees\]\(\/@raucao\)/,
|
||||||
|
);
|
||||||
|
expect(result).toMatch(
|
||||||
|
/raucao scheme 2\: \[npub1raustrrh5gjwt03zdj8syn9vmt2dwsv9t467m8c3gua636uxu89svgdees\]\(\/@raucao\)/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("replaces @id URIs with a markdown link", () => {
|
||||||
|
expect(result).toMatch(
|
||||||
|
/raucao at 1\: \[@npub1raustrrh5gjwt03zdj8syn9vmt2dwsv9t467m8c3gua636uxu89svgdees\]\(\/@raucao\)/,
|
||||||
|
);
|
||||||
|
expect(result).toMatch(
|
||||||
|
/raucao at 2\: \[@npub1raustrrh5gjwt03zdj8syn9vmt2dwsv9t467m8c3gua636uxu89svgdees\]\(\/@raucao\)/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("replaces scheme links with internal links", () => {
|
||||||
|
expect(result).toMatch(
|
||||||
|
/raucao scheme link 1\: \[raucao\]\(\/@raucao\)/,
|
||||||
|
);
|
||||||
|
expect(result).toMatch(
|
||||||
|
/raucao scheme link 2\: \[raucao\]\(\/@raucao\)/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
3
users.yaml.test
Normal file
3
users.yaml.test
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
_: b3e1b7c0ef48294bd856203bfd460625de95d3afb894e5f09b14cd1f0e7097cf
|
||||||
|
accounts: b3e1b7c1660b7db0ecb93ec55c09e67961171a5c4e9e2602f1b47477ea61c50a
|
||||||
|
raucao: 1f79058c77a224e5be226c8f024cacdad4d741855d75ed9f11473ba8eb86e1cb
|
||||||
Reference in New Issue
Block a user