Compare commits
1 Commits
master
...
feature/ta
Author | SHA1 | Date | |
---|---|---|---|
1e081c83e5 |
@ -1,18 +0,0 @@
|
||||
name: CI
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
jobs:
|
||||
test_and_lint:
|
||||
name: Test and lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
- uses: denoland/setup-deno@v2
|
||||
with:
|
||||
deno-version: v2.1.x
|
||||
- run: "deno task test"
|
||||
- run: "deno lint"
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,4 +1,3 @@
|
||||
.env
|
||||
users.yaml
|
||||
build/
|
||||
coverage/
|
||||
substr
|
||||
|
@ -21,10 +21,6 @@ p, pre, ul, ol, dl, blockquote, hr {
|
||||
margin-bottom: 1.6em;
|
||||
}
|
||||
|
||||
li {
|
||||
margin-bottom: 0.8em;
|
||||
}
|
||||
|
||||
a.anchor {
|
||||
display: none;
|
||||
}
|
||||
@ -45,11 +41,6 @@ pre code {
|
||||
padding: 0.6rem 1rem;
|
||||
}
|
||||
|
||||
.highlight pre {
|
||||
font-size: 0.9em;
|
||||
padding: 0.6rem 1rem;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
}
|
||||
@ -151,6 +142,14 @@ main article footer {
|
||||
margin-top: 5rem;
|
||||
}
|
||||
|
||||
main article footer .actions {
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
main article footer p {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.nip05 .verified,
|
||||
.nip05 .not-verified {
|
||||
margin-left: 0.3rem;
|
||||
|
@ -1,140 +0,0 @@
|
||||
/**
|
||||
* 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,13 +47,6 @@ pre code {
|
||||
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 {
|
||||
color: var(--text-color-discreet);
|
||||
}
|
||||
@ -83,6 +76,17 @@ main.profile-page .pubkey {
|
||||
color: var(--text-color-discreet);
|
||||
}
|
||||
|
||||
main article footer a,
|
||||
main article footer a:visited {
|
||||
text-decoration: none;
|
||||
color: var(--text-color-discreet);
|
||||
}
|
||||
|
||||
main article footer a:hover,
|
||||
main article footer a:active {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.nip05 .verified {
|
||||
background-color: #e8e3da;
|
||||
color: #027739;
|
||||
|
20
config.ts
20
config.ts
@ -3,21 +3,16 @@ import { parse as parseYaml } from "jsr:@std/yaml";
|
||||
import { checkFileExists } from "./utils.ts";
|
||||
import { log } from "./log.ts";
|
||||
|
||||
const denoEnv = Deno.env.get("DENO_ENV");
|
||||
const dirname = Deno.cwd();
|
||||
|
||||
function envAwarePath(path: string): string {
|
||||
return denoEnv ? `${path}.${denoEnv}` : path;
|
||||
}
|
||||
|
||||
await load({ envPath: envAwarePath(`${dirname}/.env`), export: true });
|
||||
await load({ envPath: `${dirname}/.env`, export: true });
|
||||
|
||||
let userConfigPath: string = "";
|
||||
let staticUsers: { [key: string]: string } = {};
|
||||
|
||||
const defaultUserConfigPaths = [
|
||||
envAwarePath("/etc/substr/users.yaml"),
|
||||
envAwarePath(`${dirname}/users.yaml`),
|
||||
"/etc/substr/users.yaml",
|
||||
`${dirname}/users.yaml`,
|
||||
];
|
||||
|
||||
for (const path of defaultUserConfigPaths) {
|
||||
@ -52,15 +47,6 @@ const config = {
|
||||
},
|
||||
query_timeout: parseInt(Deno.env.get("RELAY_TIMEOUT_MS") || "5000"),
|
||||
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;
|
||||
|
22
deno.json
22
deno.json
@ -1,26 +1,22 @@
|
||||
{
|
||||
"tasks": {
|
||||
"dev": "deno run --allow-all --watch server.ts",
|
||||
"server": "deno run --allow-all server.ts",
|
||||
"compile": "deno compile --allow-all --include ./assets/ --exclude ./tests/ --output ./build/substr_x86_64-unknown-linux-gnu server.ts",
|
||||
"test": "DENO_ENV=test deno test --allow-read --allow-env"
|
||||
"server": "deno run --allow-all server.ts"
|
||||
},
|
||||
"imports": {
|
||||
"@deno/gfm": "jsr:@deno/gfm@^0.10.0",
|
||||
"@nostr/tools": "jsr:@nostr/tools@^2.10.4",
|
||||
"@nostrify/nostrify": "jsr:@nostrify/nostrify@^0.36.2",
|
||||
"@oak/oak": "jsr:@oak/oak@^17.1.3",
|
||||
"@deno/gfm": "jsr:@deno/gfm@^0.9.0",
|
||||
"@nostr/tools": "jsr:@nostr/tools@^2.3.1",
|
||||
"@nostrify/nostrify": "jsr:@nostrify/nostrify@^0.36.1",
|
||||
"@oak/oak": "jsr:@oak/oak@^17.1.0",
|
||||
"@std/dotenv": "jsr:@std/dotenv@^0.225.2",
|
||||
"@std/expect": "jsr:@std/expect@^1.0.8",
|
||||
"@std/testing": "jsr:@std/testing@^1.0.11",
|
||||
"@std/expect": "jsr:@std/expect@^1.0.5",
|
||||
"@std/testing": "jsr:@std/testing@^1.0.3",
|
||||
"@std/yaml": "jsr:@std/yaml@^1.0.5",
|
||||
"ldapts": "npm:ldapts@^7.2.2"
|
||||
"ldapts": "npm:ldapts@^7.2.1"
|
||||
},
|
||||
"fmt": {
|
||||
"exclude": [
|
||||
"magick.ts",
|
||||
"assets/css/prism.css",
|
||||
"tests/fixtures/"
|
||||
"magick.ts"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
254
deno.lock
generated
254
deno.lock
generated
@ -1,68 +1,60 @@
|
||||
{
|
||||
"version": "4",
|
||||
"specifiers": {
|
||||
"jsr:@deno/gfm@0.10": "0.10.0",
|
||||
"jsr:@deno/gfm@0.9": "0.9.0",
|
||||
"jsr:@denosaurs/emoji@0.3": "0.3.1",
|
||||
"jsr:@nostr/tools@^2.10.4": "2.12.0",
|
||||
"jsr:@nostrify/nostrify@~0.36.2": "0.36.2",
|
||||
"jsr:@nostr/tools@^2.3.1": "2.3.1",
|
||||
"jsr:@nostrify/nostrify@~0.36.1": "0.36.1",
|
||||
"jsr:@nostrify/types@0.35": "0.35.0",
|
||||
"jsr:@oak/commons@1": "1.0.0",
|
||||
"jsr:@oak/oak@^17.1.3": "17.1.3",
|
||||
"jsr:@std/assert@0.224": "0.224.0",
|
||||
"jsr:@std/assert@1": "1.0.8",
|
||||
"jsr:@std/assert@^1.0.12": "1.0.12",
|
||||
"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:@oak/oak@^17.1.0": "17.1.0",
|
||||
"jsr:@std/assert@1": "1.0.6",
|
||||
"jsr:@std/assert@^1.0.6": "1.0.6",
|
||||
"jsr:@std/bytes@1": "1.0.2",
|
||||
"jsr:@std/bytes@^1.0.2": "1.0.2",
|
||||
"jsr:@std/crypto@1": "1.0.3",
|
||||
"jsr:@std/dotenv@~0.225.2": "0.225.3",
|
||||
"jsr:@std/encoding@0.224": "0.224.3",
|
||||
"jsr:@std/data-structures@^1.0.4": "1.0.4",
|
||||
"jsr:@std/dotenv@~0.225.2": "0.225.2",
|
||||
"jsr:@std/encoding@1": "1.0.5",
|
||||
"jsr:@std/encoding@^1.0.5": "1.0.5",
|
||||
"jsr:@std/encoding@~0.224.1": "0.224.3",
|
||||
"jsr:@std/expect@^1.0.8": "1.0.15",
|
||||
"jsr:@std/http@1": "1.0.10",
|
||||
"jsr:@std/internal@^1.0.5": "1.0.5",
|
||||
"jsr:@std/internal@^1.0.6": "1.0.6",
|
||||
"jsr:@std/expect@^1.0.5": "1.0.5",
|
||||
"jsr:@std/fs@^1.0.4": "1.0.4",
|
||||
"jsr:@std/http@1": "1.0.8",
|
||||
"jsr:@std/internal@^1.0.4": "1.0.4",
|
||||
"jsr:@std/io@0.224": "0.224.9",
|
||||
"jsr:@std/media-types@1": "1.1.0",
|
||||
"jsr:@std/path@1": "1.0.8",
|
||||
"jsr:@std/path@^1.0.7": "1.0.8",
|
||||
"jsr:@std/path@^1.0.8": "1.0.8",
|
||||
"jsr:@std/testing@^1.0.11": "1.0.11",
|
||||
"jsr:@std/media-types@1": "1.0.3",
|
||||
"jsr:@std/path@1": "1.0.6",
|
||||
"jsr:@std/path@^1.0.6": "1.0.6",
|
||||
"jsr:@std/testing@^1.0.3": "1.0.3",
|
||||
"jsr:@std/yaml@*": "1.0.5",
|
||||
"jsr:@std/yaml@^1.0.5": "1.0.5",
|
||||
"npm:@noble/ciphers@~0.5.1": "0.5.3",
|
||||
"npm:@noble/curves@1.2.0": "1.2.0",
|
||||
"npm:@noble/hashes@1.3.1": "1.3.1",
|
||||
"npm:@scure/base@1.1.1": "1.1.1",
|
||||
"npm:@scure/base@^1.1.6": "1.2.1",
|
||||
"npm:@scure/bip32@1.3.1": "1.3.1",
|
||||
"npm:@scure/bip32@^1.4.0": "1.6.0",
|
||||
"npm:@scure/bip39@1.2.1": "1.2.1",
|
||||
"npm:@scure/bip39@^1.3.0": "1.5.0",
|
||||
"npm:@scure/bip32@^1.4.0": "1.4.0",
|
||||
"npm:@scure/bip39@^1.3.0": "1.3.0",
|
||||
"npm:github-slugger@2": "2.0.0",
|
||||
"npm:he@^1.2.0": "1.2.0",
|
||||
"npm:katex@0.16": "0.16.11",
|
||||
"npm:ldapts@^7.2.2": "7.2.2",
|
||||
"npm:lru-cache@^10.2.0": "10.4.3",
|
||||
"npm:marked-alert@2": "2.1.2_marked@12.0.2",
|
||||
"npm:ldapts@^7.2.1": "7.2.1",
|
||||
"npm:lru-cache@^10.2.0": "10.2.2",
|
||||
"npm:marked-alert@2": "2.1.0_marked@12.0.2",
|
||||
"npm:marked-footnote@^1.2.0": "1.2.4_marked@12.0.2",
|
||||
"npm:marked-gfm-heading-id@^3.1.0": "3.2.0_marked@12.0.2",
|
||||
"npm:marked@12": "12.0.2",
|
||||
"npm:nostr-tools@^2.7.0": "2.10.4",
|
||||
"npm:nostr-wasm@0.1.0": "0.1.0",
|
||||
"npm:nostr-tools@^2.7.0": "2.7.0",
|
||||
"npm:path-to-regexp@*": "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:sanitize-html@^2.13.0": "2.13.1",
|
||||
"npm:sanitize-html@^2.11.0": "2.13.1",
|
||||
"npm:websocket-ts@^2.1.5": "2.1.5",
|
||||
"npm:zod@^3.23.8": "3.23.8"
|
||||
},
|
||||
"jsr": {
|
||||
"@deno/gfm@0.10.0": {
|
||||
"integrity": "51708205e3559a4aeb6afb29d07c5bfafe7941f91bb360351ef6621de9a39527",
|
||||
"@deno/gfm@0.9.0": {
|
||||
"integrity": "9002dbdb6e382e247509edfeae3afdb9232f5ca98a8210ef186d42084e9ded30",
|
||||
"dependencies": [
|
||||
"jsr:@denosaurs/emoji",
|
||||
"npm:github-slugger",
|
||||
@ -72,43 +64,29 @@
|
||||
"npm:marked-alert",
|
||||
"npm:marked-footnote",
|
||||
"npm:marked-gfm-heading-id",
|
||||
"npm:prismjs@^1.29.0",
|
||||
"npm:prismjs",
|
||||
"npm:sanitize-html"
|
||||
]
|
||||
},
|
||||
"@denosaurs/emoji@0.3.1": {
|
||||
"integrity": "b0aed5f55dec99e83da7c9637fe0a36d1d6252b7c99deaaa3fc5dea3fcf3da8b"
|
||||
},
|
||||
"@nostr/tools@2.10.4": {
|
||||
"integrity": "7fda015c96b4f674727843aecb990e2af1989e4724588415ccf6f69066abfd4f",
|
||||
"@nostr/tools@2.3.1": {
|
||||
"integrity": "af01dc45cb28784c584d7a0699707196f397bcc53946efa582a01b11ddde4d61",
|
||||
"dependencies": [
|
||||
"npm:@noble/ciphers",
|
||||
"npm:@noble/curves",
|
||||
"npm:@noble/hashes",
|
||||
"npm:@scure/base@1.1.1",
|
||||
"npm:@scure/bip32@1.3.1",
|
||||
"npm:@scure/bip39@1.2.1",
|
||||
"npm:nostr-wasm"
|
||||
"npm:@scure/base"
|
||||
]
|
||||
},
|
||||
"@nostr/tools@2.12.0": {
|
||||
"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",
|
||||
"@nostrify/nostrify@0.36.1": {
|
||||
"integrity": "f76c803c0bda5df1c172f25d2313980344b0431df2a973ab3e1dd61e9e7b4b1a",
|
||||
"dependencies": [
|
||||
"jsr:@nostrify/types",
|
||||
"jsr:@std/crypto@0.224",
|
||||
"jsr:@std/encoding@~0.224.1",
|
||||
"npm:@scure/base@^1.1.6",
|
||||
"npm:@scure/bip32@^1.4.0",
|
||||
"npm:@scure/bip39@^1.3.0",
|
||||
"npm:@scure/bip32",
|
||||
"npm:@scure/bip39",
|
||||
"npm:lru-cache",
|
||||
"npm:nostr-tools",
|
||||
"npm:websocket-ts",
|
||||
@ -123,91 +101,71 @@
|
||||
"dependencies": [
|
||||
"jsr:@std/assert@1",
|
||||
"jsr:@std/bytes@1",
|
||||
"jsr:@std/crypto@1",
|
||||
"jsr:@std/crypto",
|
||||
"jsr:@std/encoding@1",
|
||||
"jsr:@std/http",
|
||||
"jsr:@std/media-types"
|
||||
]
|
||||
},
|
||||
"@oak/oak@17.1.3": {
|
||||
"integrity": "d89296c22db91681dd3a2a1e1fd14e258d0d5a9654de55637aee5b661c159f33",
|
||||
"@oak/oak@17.1.0": {
|
||||
"integrity": "14ffb400c3c268bdc7b3a838664fab782b4ed35bb0dfe7669013c95bb12a9503",
|
||||
"dependencies": [
|
||||
"jsr:@oak/commons",
|
||||
"jsr:@std/assert@1",
|
||||
"jsr:@std/bytes@1",
|
||||
"jsr:@std/crypto@1",
|
||||
"jsr:@std/crypto",
|
||||
"jsr:@std/http",
|
||||
"jsr:@std/io",
|
||||
"jsr:@std/media-types",
|
||||
"jsr:@std/path@1",
|
||||
"npm:path-to-regexp"
|
||||
"npm:path-to-regexp@6.2.1"
|
||||
]
|
||||
},
|
||||
"@std/assert@0.224.0": {
|
||||
"integrity": "8643233ec7aec38a940a8264a6e3eed9bfa44e7a71cc6b3c8874213ff401967f"
|
||||
},
|
||||
"@std/assert@1.0.8": {
|
||||
"integrity": "ebe0bd7eb488ee39686f77003992f389a06c3da1bbd8022184804852b2fa641b",
|
||||
"@std/assert@1.0.6": {
|
||||
"integrity": "1904c05806a25d94fe791d6d883b685c9e2dcd60e4f9fc30f4fc5cf010c72207",
|
||||
"dependencies": [
|
||||
"jsr:@std/internal@^1.0.5"
|
||||
"jsr:@std/internal"
|
||||
]
|
||||
},
|
||||
"@std/assert@1.0.12": {
|
||||
"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/bytes@1.0.2": {
|
||||
"integrity": "fbdee322bbd8c599a6af186a1603b3355e59a5fb1baa139f8f4c3c9a1b3e3d57"
|
||||
},
|
||||
"@std/crypto@1.0.3": {
|
||||
"integrity": "a2a32f51ddef632d299e3879cd027c630dcd4d1d9a5285d6e6788072f4e51e7f"
|
||||
},
|
||||
"@std/data-structures@1.0.4": {
|
||||
"integrity": "fa0e20c11eb9ba673417450915c750a0001405a784e2a4e0c3725031681684a0"
|
||||
},
|
||||
"@std/dotenv@0.225.2": {
|
||||
"integrity": "e2025dce4de6c7bca21dece8baddd4262b09d5187217e231b033e088e0c4dd23"
|
||||
},
|
||||
"@std/dotenv@0.225.3": {
|
||||
"integrity": "a95e5b812c27b0854c52acbae215856d9cce9d4bbf774d938c51d212711e8d4a"
|
||||
},
|
||||
"@std/encoding@0.224.3": {
|
||||
"integrity": "5e861b6d81be5359fad4155e591acf17c0207b595112d1840998bb9f476dbdaf"
|
||||
},
|
||||
"@std/encoding@1.0.5": {
|
||||
"integrity": "ecf363d4fc25bd85bd915ff6733a7e79b67e0e7806334af15f4645c569fefc04"
|
||||
},
|
||||
"@std/expect@1.0.8": {
|
||||
"integrity": "27e40d8f3aefb372fc6a703fb0b69e34560e72a2f78705178babdffa00119a5f",
|
||||
"@std/expect@1.0.5": {
|
||||
"integrity": "8c7ac797e2ffe57becc6399c0f2fd06230cb9ef124d45229c6e592c563824af1",
|
||||
"dependencies": [
|
||||
"jsr:@std/assert@^1.0.8",
|
||||
"jsr:@std/internal@^1.0.5"
|
||||
"jsr:@std/assert@^1.0.6",
|
||||
"jsr:@std/internal"
|
||||
]
|
||||
},
|
||||
"@std/expect@1.0.15": {
|
||||
"integrity": "eca360007b5a7f13dbfa1294224baee7fb98dcd460d8461fe64eeae302902945",
|
||||
"@std/fs@1.0.4": {
|
||||
"integrity": "2907d32d8d1d9e540588fd5fe0ec21ee638134bd51df327ad4e443aaef07123c",
|
||||
"dependencies": [
|
||||
"jsr:@std/assert@^1.0.12",
|
||||
"jsr:@std/internal@^1.0.6"
|
||||
"jsr:@std/path@^1.0.6"
|
||||
]
|
||||
},
|
||||
"@std/http@1.0.10": {
|
||||
"integrity": "4e32d11493ab04e3ef09f104f0cb9beb4228b1d4b47c5469573c2c294c0d3692",
|
||||
"@std/http@1.0.8": {
|
||||
"integrity": "6ea1b2e8d33929967754a3b6d6c6f399ad6647d7bbb5a466c1eaf9b294a6ebcd",
|
||||
"dependencies": [
|
||||
"jsr:@std/encoding@^1.0.5"
|
||||
]
|
||||
},
|
||||
"@std/internal@1.0.5": {
|
||||
"integrity": "54a546004f769c1ac9e025abd15a76b6671ddc9687e2313b67376125650dc7ba"
|
||||
},
|
||||
"@std/internal@1.0.6": {
|
||||
"integrity": "9533b128f230f73bd209408bb07a4b12f8d4255ab2a4d22a1fd6d87304aca9a4"
|
||||
"@std/internal@1.0.4": {
|
||||
"integrity": "62e8e4911527e5e4f307741a795c0b0a9e6958d0b3790716ae71ce085f755422"
|
||||
},
|
||||
"@std/io@0.224.9": {
|
||||
"integrity": "4414664b6926f665102e73c969cfda06d2c4c59bd5d0c603fd4f1b1c840d6ee3",
|
||||
@ -215,17 +173,20 @@
|
||||
"jsr:@std/bytes@^1.0.2"
|
||||
]
|
||||
},
|
||||
"@std/media-types@1.1.0": {
|
||||
"integrity": "c9d093f0c05c3512932b330e3cc1fe1d627b301db33a4c2c2185c02471d6eaa4"
|
||||
"@std/media-types@1.0.3": {
|
||||
"integrity": "b12d30a7852f7578f4d210622df713bbfd1cbdd9b4ec2eaf5c1845ab70bab159"
|
||||
},
|
||||
"@std/path@1.0.8": {
|
||||
"integrity": "548fa456bb6a04d3c1a1e7477986b6cffbce95102d0bb447c67c4ee70e0364be"
|
||||
"@std/path@1.0.6": {
|
||||
"integrity": "ab2c55f902b380cf28e0eec501b4906e4c1960d13f00e11cfbcd21de15f18fed"
|
||||
},
|
||||
"@std/testing@1.0.11": {
|
||||
"integrity": "12b3db12d34f0f385a26248933bde766c0f8c5ad8b6ab34d4d38f528ab852f48",
|
||||
"@std/testing@1.0.3": {
|
||||
"integrity": "f98c2bee53860a5916727d7e7d3abe920dd6f9edace022e2d059f00d05c2cf42",
|
||||
"dependencies": [
|
||||
"jsr:@std/assert@^1.0.12",
|
||||
"jsr:@std/internal@^1.0.6"
|
||||
"jsr:@std/assert@^1.0.6",
|
||||
"jsr:@std/data-structures",
|
||||
"jsr:@std/fs",
|
||||
"jsr:@std/internal",
|
||||
"jsr:@std/path@^1.0.6"
|
||||
]
|
||||
},
|
||||
"@std/yaml@1.0.5": {
|
||||
@ -248,10 +209,10 @@
|
||||
"@noble/hashes@1.3.2"
|
||||
]
|
||||
},
|
||||
"@noble/curves@1.7.0": {
|
||||
"integrity": "sha512-UTMhXK9SeDhFJVrHeUJ5uZlI6ajXg10O6Ddocf9S6GjbSBVZsJo88HzKwXznNfGpMTRDyJkqMjNDPYgf0qFWnw==",
|
||||
"@noble/curves@1.4.0": {
|
||||
"integrity": "sha512-p+4cb332SFCrReJkCYe8Xzm0OWi4Jji5jVdIZRL/PmacmDkFNw6MrrV+gGpiPxLHbV+zKFRywUWbaseT+tZRXg==",
|
||||
"dependencies": [
|
||||
"@noble/hashes@1.6.0"
|
||||
"@noble/hashes@1.4.0"
|
||||
]
|
||||
},
|
||||
"@noble/hashes@1.3.1": {
|
||||
@ -260,46 +221,43 @@
|
||||
"@noble/hashes@1.3.2": {
|
||||
"integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ=="
|
||||
},
|
||||
"@noble/hashes@1.6.0": {
|
||||
"integrity": "sha512-YUULf0Uk4/mAA89w+k3+yUYh6NrEvxZa5T6SY3wlMvE2chHkxFUUIDI8/XW1QSC357iA5pSnqt7XEhvFOqmDyQ=="
|
||||
},
|
||||
"@noble/hashes@1.6.1": {
|
||||
"integrity": "sha512-pq5D8h10hHBjyqX+cfBm0i8JUXJ0UhczFc4r74zbuT9XgewFo2E3J1cOaGtdZynILNmQ685YWGzGE1Zv6io50w=="
|
||||
"@noble/hashes@1.4.0": {
|
||||
"integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg=="
|
||||
},
|
||||
"@scure/base@1.1.1": {
|
||||
"integrity": "sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA=="
|
||||
},
|
||||
"@scure/base@1.2.1": {
|
||||
"integrity": "sha512-DGmGtC8Tt63J5GfHgfl5CuAXh96VF/LD8K9Hr/Gv0J2lAoRGlPOMpqMpMbCTOoOJMZCk2Xt+DskdDyn6dEFdzQ=="
|
||||
"@scure/base@1.1.7": {
|
||||
"integrity": "sha512-PPNYBslrLNNUQ/Yad37MHYsNQtK67EhWb6WtSvNLLPo7SdVZgkUjD6Dg+5On7zNwmskf8OX7I7Nx5oN+MIWE0g=="
|
||||
},
|
||||
"@scure/bip32@1.3.1": {
|
||||
"integrity": "sha512-osvveYtyzdEVbt3OfwwXFr4P2iVBL5u1Q3q4ONBfDY/UpOuXmOlbgwc1xECEboY8wIays8Yt6onaWMUdUbfl0A==",
|
||||
"dependencies": [
|
||||
"@noble/curves@1.1.0",
|
||||
"@noble/hashes@1.3.2",
|
||||
"@scure/base@1.1.1"
|
||||
"@scure/base@1.1.7"
|
||||
]
|
||||
},
|
||||
"@scure/bip32@1.6.0": {
|
||||
"integrity": "sha512-82q1QfklrUUdXJzjuRU7iG7D7XiFx5PHYVS0+oeNKhyDLT7WPqs6pBcM2W5ZdwOwKCwoE1Vy1se+DHjcXwCYnA==",
|
||||
"@scure/bip32@1.4.0": {
|
||||
"integrity": "sha512-sVUpc0Vq3tXCkDGYVWGIZTRfnvu8LoTDaev7vbwh0omSvVORONr960MQWdKqJDCReIEmTj3PAr73O3aoxz7OPg==",
|
||||
"dependencies": [
|
||||
"@noble/curves@1.7.0",
|
||||
"@noble/hashes@1.6.1",
|
||||
"@scure/base@1.2.1"
|
||||
"@noble/curves@1.4.0",
|
||||
"@noble/hashes@1.4.0",
|
||||
"@scure/base@1.1.7"
|
||||
]
|
||||
},
|
||||
"@scure/bip39@1.2.1": {
|
||||
"integrity": "sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg==",
|
||||
"dependencies": [
|
||||
"@noble/hashes@1.3.2",
|
||||
"@scure/base@1.1.1"
|
||||
"@scure/base@1.1.7"
|
||||
]
|
||||
},
|
||||
"@scure/bip39@1.5.0": {
|
||||
"integrity": "sha512-Dop+ASYhnrwm9+HA/HwXg7j2ZqM6yk2fyLWb5znexjctFY3+E+eU8cIWI0Pql0Qx4hPZCijlGq4OL71g+Uz30A==",
|
||||
"@scure/bip39@1.3.0": {
|
||||
"integrity": "sha512-disdg7gHuTDZtY+ZdkmLpPCk7fxZSu3gBiEGuoC1XYxv9cGx3Z6cpTggCgW6odSOOIXCiDjuGejW+aJKCY/pIQ==",
|
||||
"dependencies": [
|
||||
"@noble/hashes@1.6.1",
|
||||
"@scure/base@1.2.1"
|
||||
"@noble/hashes@1.4.0",
|
||||
"@scure/base@1.1.7"
|
||||
]
|
||||
},
|
||||
"@types/asn1@0.2.4": {
|
||||
@ -387,8 +345,8 @@
|
||||
"commander"
|
||||
]
|
||||
},
|
||||
"ldapts@7.2.2": {
|
||||
"integrity": "sha512-UotAq24/vJEz0m3w/jgwZm7JGNw8M6vexL/5KU5pe3aIZWBkT/HRhjsPw/buRqKSK5Y0vTu5Zv8iyPgQF7ozzg==",
|
||||
"ldapts@7.2.1": {
|
||||
"integrity": "sha512-2NSA9drjHdRiApF+TO18c+Hy/uyBLs96OS6Gia4+dPQWPxvqDbu3Ji2beCbNCXTvvgxDj4cLZ0WoOZLt5ojfAg==",
|
||||
"dependencies": [
|
||||
"@types/asn1",
|
||||
"asn1",
|
||||
@ -398,11 +356,11 @@
|
||||
"whatwg-url"
|
||||
]
|
||||
},
|
||||
"lru-cache@10.4.3": {
|
||||
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="
|
||||
"lru-cache@10.2.2": {
|
||||
"integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ=="
|
||||
},
|
||||
"marked-alert@2.1.2_marked@12.0.2": {
|
||||
"integrity": "sha512-EFNRZ08d8L/iEIPLTlQMDjvwIsj03gxWCczYTht6DCiHJIZhMk4NK5gtPY9UqAYb09eV5VGT+jD4lp396E0I+w==",
|
||||
"marked-alert@2.1.0_marked@12.0.2": {
|
||||
"integrity": "sha512-X95Z8PCDgWa0bBfM70GxZG3LD/leUrhXc3cx3w1eFExBhswd1oXn/S4S+9H8ypPdCY7okREb4dItUOc+VJq4jQ==",
|
||||
"dependencies": [
|
||||
"marked"
|
||||
]
|
||||
@ -429,8 +387,8 @@
|
||||
"nanoid@3.3.7": {
|
||||
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g=="
|
||||
},
|
||||
"nostr-tools@2.10.4": {
|
||||
"integrity": "sha512-biU7sk+jxHgVASfobg2T5ttxOGGSt69wEVBC51sHHOEaKAAdzHBLV/I2l9Rf61UzClhliZwNouYhqIso4a3HYg==",
|
||||
"nostr-tools@2.7.0": {
|
||||
"integrity": "sha512-jJoL2J1CBiKDxaXZww27nY/Wsuxzx7AULxmGKFce4sskDu1tohNyfnzYQ8BvDyvkstU8kNZUAXPL32tre33uig==",
|
||||
"dependencies": [
|
||||
"@noble/ciphers",
|
||||
"@noble/curves@1.2.0",
|
||||
@ -496,8 +454,8 @@
|
||||
"undici-types@6.19.8": {
|
||||
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="
|
||||
},
|
||||
"uuid@11.0.3": {
|
||||
"integrity": "sha512-d0z310fCWv5dJwnX1Y/MncBAqGMKEzlBb1AOf7z9K8ALnd0utBX/msg/fA0+sbyN1ihbMsLhrBlnl1ak7Wa0rg=="
|
||||
"uuid@10.0.0": {
|
||||
"integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ=="
|
||||
},
|
||||
"webidl-conversions@7.0.0": {
|
||||
"integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g=="
|
||||
@ -530,15 +488,15 @@
|
||||
},
|
||||
"workspace": {
|
||||
"dependencies": [
|
||||
"jsr:@deno/gfm@0.10",
|
||||
"jsr:@nostr/tools@^2.10.4",
|
||||
"jsr:@nostrify/nostrify@~0.36.2",
|
||||
"jsr:@oak/oak@^17.1.3",
|
||||
"jsr:@deno/gfm@0.9",
|
||||
"jsr:@nostr/tools@^2.3.1",
|
||||
"jsr:@nostrify/nostrify@~0.36.1",
|
||||
"jsr:@oak/oak@^17.1.0",
|
||||
"jsr:@std/dotenv@~0.225.2",
|
||||
"jsr:@std/expect@^1.0.8",
|
||||
"jsr:@std/testing@^1.0.11",
|
||||
"jsr:@std/expect@^1.0.5",
|
||||
"jsr:@std/testing@^1.0.3",
|
||||
"jsr:@std/yaml@^1.0.5",
|
||||
"npm:ldapts@^7.2.2"
|
||||
"npm:ldapts@^7.2.1"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,8 @@
|
||||
// deno-lint-ignore-file require-await
|
||||
import config from "./config.ts";
|
||||
import { lookupUsernameByPubkey as ldapLookupUsername } from "./ldap.ts";
|
||||
import { lookupPubkeyByUsername as ldapLookupPubkey } from "./ldap.ts";
|
||||
|
||||
export async function lookupUsernameByPubkey(
|
||||
pubkey: string,
|
||||
): Promise<string | undefined> {
|
||||
export function lookupUsernameByPubkey(pubkey: string) {
|
||||
let username;
|
||||
for (const [key, value] of Object.entries(config.staticUsers)) {
|
||||
if (value === pubkey) {
|
||||
@ -23,9 +20,7 @@ export async function lookupUsernameByPubkey(
|
||||
}
|
||||
}
|
||||
|
||||
export async function lookupPubkeyByUsername(
|
||||
username: string,
|
||||
): Promise<string | undefined> {
|
||||
export function lookupPubkeyByUsername(username: string) {
|
||||
const pubkey = config.staticUsers[username];
|
||||
|
||||
if (pubkey) {
|
||||
|
12
feeds.ts
12
feeds.ts
@ -20,8 +20,6 @@ export async function profileAtomFeed(
|
||||
<id>${articleId}</id>
|
||||
<title>${article.title}</title>
|
||||
<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>
|
||||
<published>${isoDate(article.publishedAt)}</published>
|
||||
<summary>${article.summary}</summary>
|
||||
@ -34,17 +32,13 @@ export async function profileAtomFeed(
|
||||
|
||||
return `
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<feed xmlns="http://www.w3.org/2005/Atom" xmlns:nostr="urn:nostr:protocol">
|
||||
<feed xmlns="http://www.w3.org/2005/Atom">
|
||||
<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>
|
||||
<updated>${isoDate(lastUpdate)}</updated>
|
||||
<icon>${profile.avatarImageUrl}</icon>
|
||||
<icon>${profile.picture}</icon>
|
||||
<author>
|
||||
<name>${profile.name}</name>
|
||||
<uri>${profile.profileUrl}</uri>
|
||||
<nostr:uri>nostr:${profile.nprofile}</nostr:uri>
|
||||
<name>${name}</name>
|
||||
</author>
|
||||
${articlesXml}
|
||||
</feed>
|
||||
|
@ -9,6 +9,7 @@ const nprofileHandler = async function (ctx: Context) {
|
||||
|
||||
try {
|
||||
data = nip19.decode(nprofile).data as nip19.ProfilePointer;
|
||||
console.log(data);
|
||||
} catch (_e) {
|
||||
notFoundHandler(ctx);
|
||||
return;
|
||||
|
@ -4,7 +4,6 @@ import { fetchArticlesByAuthor, fetchProfileEvent } from "../nostr.ts";
|
||||
import { profileAtomFeed } from "../feeds.ts";
|
||||
import Profile from "../models/profile.ts";
|
||||
import { notFoundHandler } from "../handlers/errors.ts";
|
||||
import { generateOgProfileImage } from "../magick.ts";
|
||||
|
||||
const userAtomFeedHandler = async function (ctx: Context) {
|
||||
const username = ctx.state.username;
|
||||
@ -21,7 +20,6 @@ const userAtomFeedHandler = async function (ctx: Context) {
|
||||
const profile = new Profile(profileEvent, username);
|
||||
|
||||
if (profile.nip05) {
|
||||
await generateOgProfileImage(profile);
|
||||
const articles = await fetchArticlesByAuthor(pubkey, 10);
|
||||
const atom = await profileAtomFeed(profile, articles);
|
||||
|
||||
|
@ -27,7 +27,7 @@ const userEventHandler = async function (ctx: Context) {
|
||||
const article = new Article(articleEvent);
|
||||
const profile = new Profile(profileEvent, username);
|
||||
const html = await articleHtml(article, profile);
|
||||
await generateOgProfileImage(profile);
|
||||
generateOgProfileImage(profile);
|
||||
|
||||
ctx.response.body = html;
|
||||
} else {
|
||||
|
@ -19,9 +19,11 @@ const userProfileHandler = async function (ctx: Context) {
|
||||
|
||||
if (profileEvent) {
|
||||
const profile = new Profile(profileEvent, username);
|
||||
const articles = await fetchArticlesByAuthor(pubkey, 210);
|
||||
|
||||
const articles = await fetchArticlesByAuthor(pubkey, 210, ctx.state.tags);
|
||||
|
||||
const html = await profilePageHtml(profile, articles);
|
||||
await generateOgProfileImage(profile);
|
||||
generateOgProfileImage(profile);
|
||||
|
||||
ctx.response.body = html;
|
||||
} else {
|
||||
|
32
html.ts
32
html.ts
@ -19,7 +19,6 @@ function htmlLayout({ title, body, metaHtml }: HtmlLayoutOptions): string {
|
||||
<title>${title}</title>
|
||||
${metaHtml || ""}
|
||||
<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" />
|
||||
</head>
|
||||
<body>
|
||||
@ -39,6 +38,16 @@ export function errorPageHtml(statusCode: number, title: string): string {
|
||||
return htmlLayout({ title, body });
|
||||
}
|
||||
|
||||
function articleTagsHtml(article: Article, profile: Profile): string {
|
||||
if (article.tags.length === 0) return "";
|
||||
|
||||
const tags = article.tags.map((tag) => {
|
||||
return `<a href="/@${profile.username}?tags=${tag}">${tag}</a>`;
|
||||
});
|
||||
|
||||
return `Tags: ${tags.join(", ")}</p>\n`;
|
||||
}
|
||||
|
||||
export async function articleHtml(
|
||||
article: Article,
|
||||
profile: Profile,
|
||||
@ -56,7 +65,7 @@ export async function articleHtml(
|
||||
${draftLabel}
|
||||
<h1>${titleHtml(article.title)}</h1>
|
||||
<div class="meta">
|
||||
<img class="avatar" src="${profile.avatarImageUrl}" alt="User Avatar" />
|
||||
<img class="avatar" src="${profile.picture}" alt="User Avatar" />
|
||||
<div class="content">
|
||||
<span class="name"><a href="/@${profile.username}">${profile.name}</a></span>
|
||||
<span class="date">${publishedAtFormatted}</span>
|
||||
@ -66,7 +75,12 @@ export async function articleHtml(
|
||||
<article>
|
||||
${await article.buildContentHtml()}
|
||||
<footer>
|
||||
${openWithNostrAppHtml(article.naddr)}
|
||||
<div class="actions">
|
||||
${openWithNostrAppHtml(article.naddr)}
|
||||
</div>
|
||||
<p class="tags">
|
||||
${articleTagsHtml(article, profile)}
|
||||
</p>
|
||||
</footer>
|
||||
</article>
|
||||
</main>
|
||||
@ -114,6 +128,9 @@ export function articleListHtml(articles: Article[]): string {
|
||||
function userAddressHtml(profile: Profile) {
|
||||
let html = "";
|
||||
|
||||
if (profile.nip05) {
|
||||
html += `<dt>Nostr address</dt><dd>${profile.nip05}</dd>\n`;
|
||||
}
|
||||
if (profile.lud16) {
|
||||
html += `<dt>Lightning address</dt><dd>${profile.lud16}</dd>\n`;
|
||||
}
|
||||
@ -146,7 +163,7 @@ export async function profilePageHtml(
|
||||
const body = `
|
||||
<main class="profile-page">
|
||||
<header>
|
||||
<img class="avatar" src="${profile.avatarImageUrl}" alt="User Avatar" />
|
||||
<img class="avatar" src="${profile.picture}" alt="User Avatar" />
|
||||
<div class="bio">
|
||||
<h1>${profile.name}</h1>
|
||||
${nip05Html}
|
||||
@ -209,9 +226,6 @@ function feedLinksHtml(profile: Profile) {
|
||||
|
||||
function profileMetaHtml(profile: Profile) {
|
||||
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:type" content="website">
|
||||
<meta property="og:title" content="${profile.name} on Nostr">
|
||||
@ -230,10 +244,6 @@ function articleMetaHtml(article: Article, profile: Profile) {
|
||||
const imageUrl = article.image || profile.ogImageUrl;
|
||||
|
||||
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:type" content="website">
|
||||
<meta property="og:title" content="${article.title}">
|
||||
|
18
ldap.ts
18
ldap.ts
@ -8,10 +8,8 @@ if (ldapEnabled) {
|
||||
client = new Client({ url: ldap.url as string });
|
||||
}
|
||||
|
||||
export async function lookupPubkeyByUsername(
|
||||
username: string,
|
||||
): Promise<string | undefined> {
|
||||
let pubkey: string | undefined;
|
||||
export async function lookupPubkeyByUsername(username: string) {
|
||||
let pubkey;
|
||||
|
||||
try {
|
||||
await client.bind(ldap.bindDN as string, ldap.password as string);
|
||||
@ -25,7 +23,7 @@ export async function lookupPubkeyByUsername(
|
||||
searchEntries.length > 0 &&
|
||||
typeof searchEntries[0].nostrKey === "string"
|
||||
) {
|
||||
pubkey = searchEntries[0].nostrKey.toString();
|
||||
pubkey = searchEntries[0].nostrKey;
|
||||
}
|
||||
|
||||
await client.unbind();
|
||||
@ -37,10 +35,8 @@ export async function lookupPubkeyByUsername(
|
||||
return pubkey;
|
||||
}
|
||||
|
||||
export async function lookupUsernameByPubkey(
|
||||
pubkey: string,
|
||||
): Promise<string | undefined> {
|
||||
let username: string | undefined;
|
||||
export async function lookupUsernameByPubkey(pubkey: string) {
|
||||
let username;
|
||||
|
||||
try {
|
||||
await client.bind(ldap.bindDN as string, ldap.password as string);
|
||||
@ -51,10 +47,8 @@ export async function lookupUsernameByPubkey(
|
||||
});
|
||||
|
||||
if (searchEntries.length > 0) {
|
||||
username = searchEntries[0].cn.toString();
|
||||
username = searchEntries[0].cn;
|
||||
}
|
||||
|
||||
await client.unbind();
|
||||
} catch (e) {
|
||||
await client.unbind();
|
||||
throw e;
|
||||
|
66
magick.ts
66
magick.ts
@ -8,7 +8,7 @@ if (!magick) {
|
||||
log("ImageMagick is not installed. Cannot generate preview images", "yellow")
|
||||
}
|
||||
|
||||
function createProfileImage(profile: Profile) {
|
||||
function createRoundedImage(profile: Profile) {
|
||||
if (!magick || !profile.picture) return false;
|
||||
|
||||
const args = [
|
||||
@ -16,44 +16,24 @@ function createProfileImage(profile: Profile) {
|
||||
'-resize', '256x256^',
|
||||
'-gravity', 'center',
|
||||
'-extent', '256x256',
|
||||
`${tmpImgDir}/p-${profile.event.id}.png`
|
||||
'(', '+clone', '-alpha', 'extract',
|
||||
'-draw', "fill black polygon 0,0 0,128 128,0 fill white circle 128,128 128,0",
|
||||
'(', '+clone', '-flip', ')', '-compose', 'Multiply', '-composite',
|
||||
'(', '+clone', '-flop', ')', '-compose', 'Multiply', '-composite',
|
||||
')',
|
||||
'-alpha', 'off',
|
||||
'-compose', 'CopyOpacity',
|
||||
'-composite',
|
||||
`${tmpImgDir}/p-${profile.event.id}-rounded.png`
|
||||
];
|
||||
|
||||
return runCommand(magick, args);
|
||||
}
|
||||
|
||||
async function createRoundedProfileImage(profile: Profile) {
|
||||
if (!magick || !profile.picture) return false;
|
||||
|
||||
const status = await generateProfileImage(profile);
|
||||
|
||||
if (status && status.success) {
|
||||
const args = [
|
||||
`${tmpImgDir}/p-${profile.event.id}.png`,
|
||||
'-resize', '256x256^',
|
||||
'-gravity', 'center',
|
||||
'-extent', '256x256',
|
||||
'(', '+clone', '-alpha', 'extract',
|
||||
'-draw', "fill black polygon 0,0 0,128 128,0 fill white circle 128,128 128,0",
|
||||
'(', '+clone', '-flip', ')', '-compose', 'Multiply', '-composite',
|
||||
'(', '+clone', '-flop', ')', '-compose', 'Multiply', '-composite',
|
||||
')',
|
||||
'-alpha', 'off',
|
||||
'-compose', 'CopyOpacity',
|
||||
'-composite',
|
||||
`${tmpImgDir}/p-${profile.event.id}-rounded.png`
|
||||
];
|
||||
|
||||
return runCommand(magick, args);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function createOgImage(profile: Profile, ogImagePath: string, backgroundColor: string) {
|
||||
if (!magick) return false;
|
||||
|
||||
const status = await createRoundedProfileImage(profile);
|
||||
const status = await createRoundedImage(profile);
|
||||
|
||||
if (status && status.success) {
|
||||
const args = [
|
||||
@ -71,25 +51,6 @@ async function createOgImage(profile: Profile, ogImagePath: string, backgroundCo
|
||||
}
|
||||
};
|
||||
|
||||
export async function generateProfileImage(profile: Profile) {
|
||||
if (!magick || !profile.picture) return false;
|
||||
|
||||
const imagePath = `${tmpImgDir}/p-${profile.event.id}.png`;
|
||||
const fileExists = await checkFileExists(imagePath);
|
||||
|
||||
if (fileExists) {
|
||||
return { success: true };
|
||||
} else {
|
||||
const status = await createProfileImage(profile);
|
||||
if (status && status.success) {
|
||||
log(`Created avatar image for ${profile.username}: ${imagePath}`, "blue")
|
||||
return status;
|
||||
} else {
|
||||
log(`Could not create avatar image for ${profile.username}`, "yellow")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function generateOgProfileImage(profile: Profile) {
|
||||
if (!magick || !profile.picture) return false;
|
||||
|
||||
@ -97,13 +58,10 @@ export async function generateOgProfileImage(profile: Profile) {
|
||||
const backgroundColor = "#333333";
|
||||
const fileExists = await checkFileExists(ogImagePath);
|
||||
|
||||
if (fileExists) {
|
||||
return { success: true };
|
||||
} else {
|
||||
if (!fileExists) {
|
||||
const status = await createOgImage(profile, ogImagePath, backgroundColor);
|
||||
if (status && status.success) {
|
||||
log(`Created OG image for ${profile.username}: ${ogImagePath}`, "blue")
|
||||
return status;
|
||||
} else {
|
||||
log(`Could not create OG image for ${profile.username}`, "yellow")
|
||||
}
|
||||
|
@ -1,13 +1,9 @@
|
||||
import { render as renderMarkdown } from "@deno/gfm";
|
||||
import { nip19 } from "@nostr/tools";
|
||||
import { NostrEvent as NEvent } from "@nostrify/nostrify";
|
||||
import { replaceNostrUris } from "../nostr/links.ts";
|
||||
import { replaceNostrUris } from "../nostr.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 {
|
||||
event: NEvent;
|
||||
|
||||
@ -43,6 +39,13 @@ export default class Article {
|
||||
return tag ? tag[1] : "";
|
||||
}
|
||||
|
||||
get tags(): string[] {
|
||||
return this.event.tags
|
||||
.filter((t) => t[0] === "t")
|
||||
.filter((t) => t[1] !== "")
|
||||
.map((t) => t[1]);
|
||||
}
|
||||
|
||||
get publishedAt(): number {
|
||||
const tag = this.event.tags.find((t) => t[0] === "published_at");
|
||||
return tag ? parseInt(tag[1]) : this.event.created_at;
|
||||
|
@ -59,25 +59,10 @@ export default class Profile {
|
||||
return nip19.npubEncode(this.pubkey);
|
||||
}
|
||||
|
||||
get nprofile(): string {
|
||||
return nip19.nprofileEncode({
|
||||
pubkey: this.pubkey,
|
||||
relays: [config.relay_urls[0]],
|
||||
});
|
||||
}
|
||||
|
||||
get profileUrl(): string {
|
||||
return `${config.base_url}/@${this.username}`;
|
||||
}
|
||||
|
||||
get avatarImageUrl(): string {
|
||||
if (magick) {
|
||||
return `${config.base_url}/assets/g/img/p-${this.event.id}.png`;
|
||||
} else {
|
||||
return this.picture || "";
|
||||
}
|
||||
}
|
||||
|
||||
get ogImageUrl(): string {
|
||||
if (magick) {
|
||||
return `${config.base_url}/assets/g/img/og-p-${this.event.id}.png`;
|
||||
|
63
nostr.ts
63
nostr.ts
@ -1,4 +1,6 @@
|
||||
import { NostrEvent, NostrFilter, NPool, NRelay1 } from "@nostrify/nostrify";
|
||||
import { nip19 } from "@nostr/tools";
|
||||
import { lookupUsernameByPubkey } from "./directory.ts";
|
||||
import config from "./config.ts";
|
||||
import Article from "./models/article.ts";
|
||||
|
||||
@ -13,7 +15,7 @@ const relayPool = new NPool({
|
||||
eventRouter: async (_event) => [],
|
||||
});
|
||||
|
||||
async function fetchWithTimeout(filters: NostrFilter[]) {
|
||||
export async function fetchWithTimeout(filters: NostrFilter[]) {
|
||||
const timeoutPromise = new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new Error("relay timeout")), config.query_timeout)
|
||||
);
|
||||
@ -48,16 +50,28 @@ export async function fetchReplaceableEvent(
|
||||
}
|
||||
}
|
||||
|
||||
export function createTagList(
|
||||
articles: Article[],
|
||||
): Record<string, number> {
|
||||
return articles.flatMap((a) => a.tags).reduce((acc, tag) => {
|
||||
acc[tag] = (acc[tag] || 0) + 1;
|
||||
return acc;
|
||||
}, {} as Record<string, number>);
|
||||
}
|
||||
|
||||
export async function fetchArticlesByAuthor(
|
||||
pubkey: string,
|
||||
limit: number = 10,
|
||||
tags?: string[],
|
||||
) {
|
||||
const events = await fetchWithTimeout([{
|
||||
const filter = {
|
||||
authors: [pubkey],
|
||||
kinds: [30023],
|
||||
limit: limit,
|
||||
}]) as NostrEvent[];
|
||||
};
|
||||
if (typeof tags !== "undefined") filter["#t"] = tags;
|
||||
|
||||
const events = await fetchWithTimeout([filter]) as NostrEvent[];
|
||||
const articles = events.map((a) => new Article(a));
|
||||
|
||||
return articles
|
||||
@ -77,6 +91,49 @@ export async function fetchProfileEvent(pubkey: string) {
|
||||
return events.length > 0 ? events[0] : null;
|
||||
}
|
||||
|
||||
export async function nostrUriToUrl(uri: string): Promise<string> {
|
||||
const bech32 = uri.replace(/^nostr:/, "");
|
||||
|
||||
if (bech32.match(/^(naddr|nprofile|npub)/)) {
|
||||
try {
|
||||
const r = nip19.decode(bech32);
|
||||
let username;
|
||||
|
||||
switch (r.type) {
|
||||
case "naddr":
|
||||
username = await lookupUsernameByPubkey(r.data.pubkey);
|
||||
if (username) return `/${bech32}`;
|
||||
break;
|
||||
case "nprofile":
|
||||
username = await lookupUsernameByPubkey(r.data.pubkey);
|
||||
if (username) return `/@${username}`;
|
||||
break;
|
||||
case "npub":
|
||||
username = await lookupUsernameByPubkey(r.data);
|
||||
if (username) return `/@${username}`;
|
||||
break;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
return `${config.njump_url}/${bech32}`;
|
||||
}
|
||||
|
||||
export async function replaceNostrUris(markdown: string): Promise<string> {
|
||||
const nostrUriRegex = /(nostr:|nprofile|naddr|nevent|nrelay|npub)[a-z0-9]+/g;
|
||||
const matches = markdown.match(nostrUriRegex);
|
||||
if (!matches) return markdown;
|
||||
|
||||
for (const uri of matches) {
|
||||
const url = await nostrUriToUrl(uri);
|
||||
markdown = markdown.replace(uri, url);
|
||||
}
|
||||
|
||||
return markdown;
|
||||
}
|
||||
|
||||
export async function verifyNip05Address(
|
||||
address: string,
|
||||
pubkey: string,
|
||||
|
137
nostr/links.ts
137
nostr/links.ts
@ -1,137 +0,0 @@
|
||||
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;
|
||||
}
|
@ -26,6 +26,10 @@ router.get("/:path", async (ctx) => {
|
||||
} else if (path.startsWith("npub")) {
|
||||
await npubHandler(ctx);
|
||||
} else if (path.startsWith("@") || path.startsWith("~")) {
|
||||
const tags = ctx.request.url.searchParams.get("tags");
|
||||
if (typeof tags === "string" && tags !== "") {
|
||||
ctx.state.tags = tags.split(",");
|
||||
}
|
||||
await userProfileHandler(ctx);
|
||||
} else {
|
||||
notFoundHandler(ctx);
|
||||
@ -93,7 +97,7 @@ router.get("/assets/:path*", async (ctx) => {
|
||||
filePath = filePath.replace(/^g\//, "");
|
||||
root = "/tmp/substr";
|
||||
} else {
|
||||
root = `${import.meta.dirname}/assets`;
|
||||
root = `${Deno.cwd()}/assets`;
|
||||
}
|
||||
|
||||
await send(ctx, filePath, { root });
|
||||
|
41
tests/fixtures/article-2.md
vendored
41
tests/fixtures/article-2.md
vendored
@ -1,41 +0,0 @@
|
||||
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`
|
@ -47,6 +47,16 @@ describe("Article", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("#tags", () => {
|
||||
it("returns a flattened tag list", () => {
|
||||
expect(article.tags).toEqual([
|
||||
"lightning",
|
||||
"lightning network",
|
||||
"howto",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#publishedAt", () => {
|
||||
it("returns the value of the first 'published_at' tag", () => {
|
||||
expect(article.publishedAt).toEqual(1726402055);
|
||||
@ -69,7 +79,7 @@ describe("Article", () => {
|
||||
describe("#naddr", () => {
|
||||
it("returns a bech32 addressable event ID", () => {
|
||||
expect(article.naddr).toMatch(
|
||||
/^naddr1qvzqqqr4gupzq8meqkx80g3yuklzymy0qf/,
|
||||
/naddr1qvzqqqr4gupzq8meqkx80g3yuklzymy0qf/,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@ -40,12 +40,4 @@ describe("Profile", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#nprofile", () => {
|
||||
it("returns a bech32 profile ID", () => {
|
||||
expect(profile.nprofile).toMatch(
|
||||
/^nprofile1qyt8wumn8ghj7mn0wd68ytntdaek6mmn9ehhyecqyq0hjpvvw73zfed7yf/,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1,97 +0,0 @@
|
||||
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\)/,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
27
tests/nostr_test.ts
Normal file
27
tests/nostr_test.ts
Normal file
@ -0,0 +1,27 @@
|
||||
// import { describe, it } from "@std/testing/bdd";
|
||||
// import { stub } from "@std/testing/mock";
|
||||
// import { expect } from "@std/expect";
|
||||
// import { NostrEvent, NostrFilter } from "@nostrify/nostrify";
|
||||
// import * as nostr from "../nostr.ts";
|
||||
//
|
||||
// async function fetchWithTimeout(filters: NostrFilter[]) {
|
||||
// console.log("================")
|
||||
// const events = [];
|
||||
// const fixtures = [ "article-1.json", "article-deleted.json" ]
|
||||
// for (const filename of fixtures) {
|
||||
// const event = JSON.parse(Deno.readTextFileSync(`tests/fixtures/${filename}`));
|
||||
// events.push(event);
|
||||
// }
|
||||
// return Promise.resolve(events);
|
||||
// }
|
||||
//
|
||||
// describe("Nostr", () => {
|
||||
// describe("#fetchArticlesByAuthor", () => {
|
||||
// it("removes the anchor links for headlines", async () => {
|
||||
// stub(nostr, "fetchArticlesByAuthor");
|
||||
//
|
||||
// const articles = await nostr.fetchArticlesByAuthor("123456abcdef");
|
||||
// expect(articles.length).toEqual(2);
|
||||
// });
|
||||
// });
|
||||
// });
|
@ -1,3 +0,0 @@
|
||||
_: b3e1b7c0ef48294bd856203bfd460625de95d3afb894e5f09b14cd1f0e7097cf
|
||||
accounts: b3e1b7c1660b7db0ecb93ec55c09e67961171a5c4e9e2602f1b47477ea61c50a
|
||||
raucao: 1f79058c77a224e5be226c8f024cacdad4d741855d75ed9f11473ba8eb86e1cb
|
Loading…
x
Reference in New Issue
Block a user