7 Commits

Author SHA1 Message Date
5608176a20 Add some margin to list items
All checks were successful
CI / Test and lint (push) Successful in 16s
2025-04-23 15:30:07 +04:00
fa21e72b3f Add alternative nostr links to HTML meta tags
All checks were successful
CI / Test and lint (push) Successful in 19s
2025-04-22 13:09:03 +04:00
5b0397268b Improve Nostr link handling
Some checks failed
CI / Test and lint (push) Failing after 10m22s
Fixes a bunch of problems with how Nostr links are created and replaced
in Markdown content
2025-04-21 16:53:22 +04:00
fb37db8583 Typing 2025-04-21 16:44:13 +04:00
07f881d543 Load test configs when running tests 2025-04-21 16:44:13 +04:00
3f9dad8f9a Don't link nrelay in content
All checks were successful
CI / Test and lint (push) Successful in 19s
It's deprecated
2024-12-17 14:40:44 +04:00
275fb73896 Move executables to build/ dir
All checks were successful
CI / Test and lint (push) Successful in 17s
2024-12-09 18:58:17 +04:00
15 changed files with 302 additions and 83 deletions

1
.env.test Normal file
View File

@@ -0,0 +1 @@
RELAY_URLS=wss://nostr.kosmos.org

2
.gitignore vendored
View File

@@ -1,3 +1,3 @@
.env
users.yaml
substr
build/

View File

@@ -21,6 +21,10 @@ p, pre, ul, ol, dl, blockquote, hr {
margin-bottom: 1.6em;
}
li {
margin-bottom: 0.8em;
}
a.anchor {
display: none;
}

View File

@@ -3,16 +3,21 @@ 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();
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 staticUsers: { [key: string]: string } = {};
const defaultUserConfigPaths = [
"/etc/substr/users.yaml",
`${dirname}/users.yaml`,
envAwarePath("/etc/substr/users.yaml"),
envAwarePath(`${dirname}/users.yaml`),
];
for (const path of defaultUserConfigPaths) {

View File

@@ -2,8 +2,8 @@
"tasks": {
"dev": "deno run --allow-all --watch server.ts",
"server": "deno run --allow-all server.ts",
"compile": "deno compile --allow-all --include ./assets/ --output substr server.ts",
"test": "deno test --allow-read --allow-env"
"compile": "deno compile --allow-all --include ./assets/ --output ./build/substr_x86_64-unknown-linux-gnu server.ts",
"test": "DENO_ENV=test deno test --allow-read --allow-env"
},
"imports": {
"@deno/gfm": "jsr:@deno/gfm@^0.10.0",
@@ -12,7 +12,7 @@
"@oak/oak": "jsr:@oak/oak@^17.1.3",
"@std/dotenv": "jsr:@std/dotenv@^0.225.2",
"@std/expect": "jsr:@std/expect@^1.0.8",
"@std/testing": "jsr:@std/testing@^1.0.5",
"@std/testing": "jsr:@std/testing@^1.0.11",
"@std/yaml": "jsr:@std/yaml@^1.0.5",
"ldapts": "npm:ldapts@^7.2.2"
},

60
deno.lock generated
View File

@@ -3,34 +3,34 @@
"specifiers": {
"jsr:@deno/gfm@0.10": "0.10.0",
"jsr:@denosaurs/emoji@0.3": "0.3.1",
"jsr:@nostr/tools@^2.10.4": "2.10.4",
"jsr:@nostr/tools@^2.10.4": "2.12.0",
"jsr:@nostrify/nostrify@~0.36.2": "0.36.2",
"jsr:@nostrify/types@0.35": "0.35.0",
"jsr:@oak/commons@1": "1.0.0",
"jsr:@oak/oak@^17.1.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:@std/crypto@1": "1.0.3",
"jsr:@std/data-structures@^1.0.4": "1.0.4",
"jsr:@std/dotenv@~0.225.2": "0.225.2",
"jsr:@std/dotenv@~0.225.2": "0.225.3",
"jsr:@std/encoding@0.224": "0.224.3",
"jsr:@std/encoding@1": "1.0.5",
"jsr:@std/encoding@^1.0.5": "1.0.5",
"jsr:@std/encoding@~0.224.1": "0.224.3",
"jsr:@std/expect@^1.0.8": "1.0.8",
"jsr:@std/fs@^1.0.5": "1.0.5",
"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/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.5": "1.0.5",
"jsr:@std/testing@^1.0.11": "1.0.11",
"jsr:@std/yaml@*": "1.0.5",
"jsr:@std/yaml@^1.0.5": "1.0.5",
"npm:@noble/ciphers@~0.5.1": "0.5.3",
@@ -90,6 +90,15 @@
"npm:nostr-wasm"
]
},
"@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",
"dependencies": [
@@ -139,7 +148,13 @@
"@std/assert@1.0.8": {
"integrity": "ebe0bd7eb488ee39686f77003992f389a06c3da1bbd8022184804852b2fa641b",
"dependencies": [
"jsr:@std/internal"
"jsr:@std/internal@^1.0.5"
]
},
"@std/assert@1.0.12": {
"integrity": "08009f0926dda9cbd8bef3a35d3b6a4b964b0ab5c3e140a4e0351fbf34af5b9a",
"dependencies": [
"jsr:@std/internal@^1.0.6"
]
},
"@std/bytes@1.0.4": {
@@ -155,12 +170,12 @@
"@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"
},
@@ -171,13 +186,14 @@
"integrity": "27e40d8f3aefb372fc6a703fb0b69e34560e72a2f78705178babdffa00119a5f",
"dependencies": [
"jsr:@std/assert@^1.0.8",
"jsr:@std/internal"
"jsr:@std/internal@^1.0.5"
]
},
"@std/fs@1.0.5": {
"integrity": "41806ad6823d0b5f275f9849a2640d87e4ef67c51ee1b8fb02426f55e02fd44e",
"@std/expect@1.0.15": {
"integrity": "eca360007b5a7f13dbfa1294224baee7fb98dcd460d8461fe64eeae302902945",
"dependencies": [
"jsr:@std/path@^1.0.7"
"jsr:@std/assert@^1.0.12",
"jsr:@std/internal@^1.0.6"
]
},
"@std/http@1.0.10": {
@@ -189,6 +205,9 @@
"@std/internal@1.0.5": {
"integrity": "54a546004f769c1ac9e025abd15a76b6671ddc9687e2313b67376125650dc7ba"
},
"@std/internal@1.0.6": {
"integrity": "9533b128f230f73bd209408bb07a4b12f8d4255ab2a4d22a1fd6d87304aca9a4"
},
"@std/io@0.224.9": {
"integrity": "4414664b6926f665102e73c969cfda06d2c4c59bd5d0c603fd4f1b1c840d6ee3",
"dependencies": [
@@ -201,14 +220,11 @@
"@std/path@1.0.8": {
"integrity": "548fa456bb6a04d3c1a1e7477986b6cffbce95102d0bb447c67c4ee70e0364be"
},
"@std/testing@1.0.5": {
"integrity": "6e693cbec94c81a1ad3df668685c7ba8e20742bb10305bc7137faa5cf16d2ec4",
"@std/testing@1.0.11": {
"integrity": "12b3db12d34f0f385a26248933bde766c0f8c5ad8b6ab34d4d38f528ab852f48",
"dependencies": [
"jsr:@std/assert@^1.0.8",
"jsr:@std/data-structures",
"jsr:@std/fs",
"jsr:@std/internal",
"jsr:@std/path@^1.0.8"
"jsr:@std/assert@^1.0.12",
"jsr:@std/internal@^1.0.6"
]
},
"@std/yaml@1.0.5": {
@@ -519,7 +535,7 @@
"jsr:@oak/oak@^17.1.3",
"jsr:@std/dotenv@~0.225.2",
"jsr:@std/expect@^1.0.8",
"jsr:@std/testing@^1.0.5",
"jsr:@std/testing@^1.0.11",
"jsr:@std/yaml@^1.0.5",
"npm:ldapts@^7.2.2"
]

View File

@@ -1,8 +1,9 @@
// 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 function lookupUsernameByPubkey(pubkey: string) {
export async function lookupUsernameByPubkey(pubkey: string): Promise<string | undefined> {
let username;
for (const [key, value] of Object.entries(config.staticUsers)) {
if (value === pubkey) {
@@ -20,7 +21,7 @@ export function lookupUsernameByPubkey(pubkey: string) {
}
}
export function lookupPubkeyByUsername(username: string) {
export async function lookupPubkeyByUsername(username: string): Promise<string | undefined> {
const pubkey = config.staticUsers[username];
if (pubkey) {

View File

@@ -212,6 +212,7 @@ function feedLinksHtml(profile: Profile) {
function profileMetaHtml(profile: Profile) {
return `
<link rel="icon" href="${profile.avatarImageUrl}" type="image/png">
<link rel="alternate" type="application/nostr+json" href="nostr:${profile.npub}" 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">
@@ -231,6 +232,7 @@ function articleMetaHtml(article: Article, profile: Profile) {
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">
<meta property="og:url" content="${article.url}">
<meta property="og:type" content="website">
<meta property="og:title" content="${article.title}">

12
ldap.ts
View File

@@ -8,8 +8,8 @@ if (ldapEnabled) {
client = new Client({ url: ldap.url as string });
}
export async function lookupPubkeyByUsername(username: string) {
let pubkey;
export async function lookupPubkeyByUsername(username: string): Promise<string | undefined> {
let pubkey: string | undefined;
try {
await client.bind(ldap.bindDN as string, ldap.password as string);
@@ -23,7 +23,7 @@ export async function lookupPubkeyByUsername(username: string) {
searchEntries.length > 0 &&
typeof searchEntries[0].nostrKey === "string"
) {
pubkey = searchEntries[0].nostrKey;
pubkey = searchEntries[0].nostrKey.toString();
}
await client.unbind();
@@ -35,8 +35,8 @@ export async function lookupPubkeyByUsername(username: string) {
return pubkey;
}
export async function lookupUsernameByPubkey(pubkey: string) {
let username;
export async function lookupUsernameByPubkey(pubkey: string): Promise<string | undefined> {
let username: string | undefined;
try {
await client.bind(ldap.bindDN as string, ldap.password as string);
@@ -47,7 +47,7 @@ export async function lookupUsernameByPubkey(pubkey: string) {
});
if (searchEntries.length > 0) {
username = searchEntries[0].cn;
username = searchEntries[0].cn.toString();
}
} catch (e) {
await client.unbind();

View File

@@ -1,7 +1,7 @@
import { render as renderMarkdown } from "@deno/gfm";
import { nip19 } from "@nostr/tools";
import { NostrEvent as NEvent } from "@nostrify/nostrify";
import { replaceNostrUris } from "../nostr.ts";
import { replaceNostrUris } from "../nostr/links.ts";
import config from "../config.ts";
export default class Article {

View File

@@ -1,6 +1,4 @@
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";
@@ -79,49 +77,6 @@ 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,

129
nostr/links.ts Normal file
View File

@@ -0,0 +1,129 @@
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;
}

41
tests/fixtures/article-2.md vendored Normal file
View 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`

62
tests/nostr/links_test.ts Normal file
View File

@@ -0,0 +1,62 @@
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
View File

@@ -0,0 +1,3 @@
_: b3e1b7c0ef48294bd856203bfd460625de95d3afb894e5f09b14cd1f0e7097cf
accounts: b3e1b7c1660b7db0ecb93ec55c09e67961171a5c4e9e2602f1b47477ea61c50a
raucao: 1f79058c77a224e5be226c8f024cacdad4d741855d75ed9f11473ba8eb86e1cb