diff --git a/index.ts b/index.ts index 6fbe9e2..c1f8047 100644 --- a/index.ts +++ b/index.ts @@ -11,7 +11,9 @@ export * as nip06 from './nip06' export * as nip10 from './nip10' export * as nip13 from './nip13' export * as nip19 from './nip19' +export * as nip21 from './nip21' export * as nip26 from './nip26' +export * as nip27 from './nip27' export * as nip39 from './nip39' export * as nip42 from './nip42' export * as nip57 from './nip57' diff --git a/nip21.test.js b/nip21.test.js new file mode 100644 index 0000000..acc9562 --- /dev/null +++ b/nip21.test.js @@ -0,0 +1,42 @@ +/* eslint-env jest */ +const {nip21} = require('./lib/nostr.cjs') + +test('test', () => { + expect( + nip21.test( + 'nostr:npub108pv4cg5ag52nq082kd5leu9ffrn2gdg6g4xdwatn73y36uzplmq9uyev6' + ) + ).toBe(true) + expect( + nip21.test( + 'nostr:note1gmtnz6q2m55epmlpe3semjdcq987av3jvx4emmjsa8g3s9x7tg4sclreky' + ) + ).toBe(true) + expect( + nip21.test( + ' nostr:npub108pv4cg5ag52nq082kd5leu9ffrn2gdg6g4xdwatn73y36uzplmq9uyev6' + ) + ).toBe(false) + expect(nip21.test('nostr:')).toBe(false) + expect( + nip21.test( + 'nostr:npub108pv4cg5ag52nQq082kd5leu9ffrn2gdg6g4xdwatn73y36uzplmq9uyev6' + ) + ).toBe(false) + expect(nip21.test('gggggg')).toBe(false) +}) + +test('parse', () => { + const result = nip21.parse( + 'nostr:note1gmtnz6q2m55epmlpe3semjdcq987av3jvx4emmjsa8g3s9x7tg4sclreky' + ) + + expect(result).toEqual({ + uri: 'nostr:note1gmtnz6q2m55epmlpe3semjdcq987av3jvx4emmjsa8g3s9x7tg4sclreky', + value: 'note1gmtnz6q2m55epmlpe3semjdcq987av3jvx4emmjsa8g3s9x7tg4sclreky', + decoded: { + type: 'note', + data: '46d731680add2990efe1cc619dc9b8014feeb23261ab9dee50e9d11814de5a2b' + } + }) +}) diff --git a/nip21.ts b/nip21.ts new file mode 100644 index 0000000..a5608a2 --- /dev/null +++ b/nip21.ts @@ -0,0 +1,41 @@ +import * as nip19 from './nip19' +import * as nip21 from './nip21' + +/** + * Bech32 regex. + * @see https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki#bech32 + */ +export const BECH32_REGEX = + /[\x21-\x7E]{1,83}1[023456789acdefghjklmnpqrstuvwxyz]{6,}/ + +/** Nostr URI regex, eg `nostr:npub1...` */ +export const NOSTR_URI_REGEX = new RegExp(`nostr:(${BECH32_REGEX.source})`) + +/** Test whether the value is a Nostr URI. */ +export function test(value: unknown): value is `nostr:${string}` { + return ( + typeof value === 'string' && + new RegExp(`^${NOSTR_URI_REGEX.source}$`).test(value) + ) +} + +/** Parsed Nostr URI data. */ +export interface NostrURI { + /** Full URI including the `nostr:` protocol. */ + uri: `nostr:${string}` + /** The bech32-encoded data (eg `npub1...`). */ + value: string + /** Decoded bech32 string, according to NIP-19. */ + decoded: nip19.DecodeResult +} + +/** Parse and decode a Nostr URI. */ +export function parse(uri: string): NostrURI { + const match = uri.match(new RegExp(`^${nip21.NOSTR_URI_REGEX.source}$`)) + if (!match) throw new Error(`Invalid Nostr URI: ${uri}`) + return { + uri: match[0] as `nostr:${string}`, + value: match[1], + decoded: nip19.decode(match[1]) + } +} diff --git a/nip27.test.js b/nip27.test.js new file mode 100644 index 0000000..24c63ac --- /dev/null +++ b/nip27.test.js @@ -0,0 +1,49 @@ +/* eslint-env jest */ +const {nip27} = require('./lib/nostr.cjs') + +test('matchAll', () => { + const result = nip27.matchAll( + 'Hello nostr:npub108pv4cg5ag52nq082kd5leu9ffrn2gdg6g4xdwatn73y36uzplmq9uyev6!\n\nnostr:note1gmtnz6q2m55epmlpe3semjdcq987av3jvx4emmjsa8g3s9x7tg4sclreky' + ) + + expect([...result]).toEqual([ + { + uri: 'nostr:npub108pv4cg5ag52nq082kd5leu9ffrn2gdg6g4xdwatn73y36uzplmq9uyev6', + value: 'npub108pv4cg5ag52nq082kd5leu9ffrn2gdg6g4xdwatn73y36uzplmq9uyev6', + decoded: { + type: 'npub', + data: '79c2cae114ea28a981e7559b4fe7854a473521a8d22a66bbab9fa248eb820ff6' + }, + start: 6, + end: 75 + }, + { + uri: 'nostr:note1gmtnz6q2m55epmlpe3semjdcq987av3jvx4emmjsa8g3s9x7tg4sclreky', + value: 'note1gmtnz6q2m55epmlpe3semjdcq987av3jvx4emmjsa8g3s9x7tg4sclreky', + decoded: { + type: 'note', + data: '46d731680add2990efe1cc619dc9b8014feeb23261ab9dee50e9d11814de5a2b' + }, + start: 78, + end: 147 + } + ]) +}) + +test('replaceAll', () => { + const content = + 'Hello nostr:npub108pv4cg5ag52nq082kd5leu9ffrn2gdg6g4xdwatn73y36uzplmq9uyev6!\n\nnostr:note1gmtnz6q2m55epmlpe3semjdcq987av3jvx4emmjsa8g3s9x7tg4sclreky' + + const result = nip27.replaceAll(content, ({decoded, value}) => { + switch (decoded.type) { + case 'npub': + return '@alex' + case 'note': + return '!1234' + default: + return value + } + }) + + expect(result).toEqual('Hello @alex!\n\n!1234') +}) diff --git a/nip27.ts b/nip27.ts new file mode 100644 index 0000000..7fa5be8 --- /dev/null +++ b/nip27.ts @@ -0,0 +1,63 @@ +import * as nip19 from './nip19' +import * as nip21 from './nip21' + +/** Regex to find NIP-21 URIs inside event content. */ +export const regex = () => + new RegExp(`\\b${nip21.NOSTR_URI_REGEX.source}\\b`, 'g') + +/** Match result for a Nostr URI in event content. */ +export interface NostrURIMatch extends nip21.NostrURI { + /** Index where the URI begins in the event content. */ + start: number + /** Index where the URI ends in the event content. */ + end: number +} + +/** Find and decode all NIP-21 URIs. */ +export function * matchAll(content: string): Iterable { + const matches = content.matchAll(regex()) + + for (const match of matches) { + const [uri, value] = match + + yield { + uri: uri as `nostr:${string}`, + value, + decoded: nip19.decode(value), + start: match.index!, + end: match.index! + uri.length + } + } +} + +/** + * Replace all occurrences of Nostr URIs in the text. + * + * WARNING: using this on an HTML string is potentially unsafe! + * + * @example + * ```ts + * nip27.replaceAll(event.content, ({ decoded, value }) => { + * switch(decoded.type) { + * case 'npub': + * return renderMention(decoded) + * case 'note': + * return renderNote(decoded) + * default: + * return value + * } + * }) + * ``` + */ +export function replaceAll( + content: string, + replacer: (match: nip21.NostrURI) => string +): string { + return content.replaceAll(regex(), (uri, value) => { + return replacer({ + uri: uri as `nostr:${string}`, + value, + decoded: nip19.decode(value) + }) + }) +}