deckScope.setThread(undefined)} />
deckScope.setThread(undefined)} />
@@ -128,7 +128,7 @@ function ArticlesCol() {
);
}
-function MediaCol({ setThread }: { setThread: (e: string) => void }) {
+function MediaCol({ setThread }: { setThread: (e: NostrLink) => void }) {
const { proxy } = useImgProxy();
return (
@@ -158,7 +158,7 @@ function MediaCol({ setThread }: { setThread: (e: string) => void }) {
"--img": `url(${proxy(images[0].content)})`,
} as CSSProperties
}
- onClick={() => setThread(e.id)}>
+ onClick={() => setThread(NostrLink.fromEvent(e))}>
);
}}
/>
diff --git a/packages/app/src/Pages/Layout.tsx b/packages/app/src/Pages/Layout.tsx
index 10864cde..4de52fde 100644
--- a/packages/app/src/Pages/Layout.tsx
+++ b/packages/app/src/Pages/Layout.tsx
@@ -4,7 +4,7 @@ import { useDispatch, useSelector } from "react-redux";
import { Link, Outlet, useLocation, useNavigate } from "react-router-dom";
import { FormattedMessage, useIntl } from "react-intl";
import { useUserProfile } from "@snort/system-react";
-import { NostrPrefix, createNostrLink, tryParseNostrLink } from "@snort/system";
+import { NostrLink, NostrPrefix, tryParseNostrLink } from "@snort/system";
import messages from "./messages";
@@ -125,7 +125,7 @@ const AccountHeader = () => {
const [handle, domain] = search.split("@");
const pk = await fetchNip05Pubkey(handle, domain);
if (pk) {
- navigate(`/${createNostrLink(NostrPrefix.PublicKey, pk).encode()}`);
+ navigate(`/${new NostrLink(NostrPrefix.PublicKey, pk).encode()}`);
return;
}
}
diff --git a/packages/app/src/Pages/Notifications.tsx b/packages/app/src/Pages/Notifications.tsx
index 23cefcd7..70de123e 100644
--- a/packages/app/src/Pages/Notifications.tsx
+++ b/packages/app/src/Pages/Notifications.tsx
@@ -7,7 +7,6 @@ import {
NostrLink,
NostrPrefix,
TaggedNostrEvent,
- createNostrLink,
parseZap,
} from "@snort/system";
import { unwrap } from "@snort/shared";
@@ -33,15 +32,15 @@ function notificationContext(ev: TaggedNostrEvent) {
const aTag = findTag(ev, "a");
if (aTag) {
const [kind, author, d] = aTag.split(":");
- return createNostrLink(NostrPrefix.Address, d, undefined, Number(kind), author);
+ return new NostrLink(NostrPrefix.Address, d, Number(kind), author);
}
const eTag = findTag(ev, "e");
if (eTag) {
- return createNostrLink(NostrPrefix.Event, eTag);
+ return new NostrLink(NostrPrefix.Event, eTag);
}
const pTag = ev.tags.filter(a => a[0] === "p").slice(-1)?.[0];
if (pTag) {
- return createNostrLink(NostrPrefix.PublicKey, pTag[1]);
+ return new NostrLink(NostrPrefix.PublicKey, pTag[1]);
}
break;
}
@@ -50,16 +49,16 @@ function notificationContext(ev: TaggedNostrEvent) {
const thread = EventExt.extractThread(ev);
const tag = unwrap(thread?.replyTo ?? thread?.root ?? { value: ev.id, key: "e" });
if (tag.key === "e") {
- return createNostrLink(NostrPrefix.Event, unwrap(tag.value));
+ return new NostrLink(NostrPrefix.Event, unwrap(tag.value));
} else if (tag.key === "a") {
const [kind, author, d] = unwrap(tag.value).split(":");
- return createNostrLink(NostrPrefix.Address, d, undefined, Number(kind), author);
+ return new NostrLink(NostrPrefix.Address, d, Number(kind), author);
} else {
throw new Error("Unknown thread context");
}
}
case EventKind.TextNote: {
- return createNostrLink(NostrPrefix.Note, ev.id);
+ return new NostrLink(NostrPrefix.Note, ev.id);
}
}
}
diff --git a/packages/app/src/Pages/ProfilePage.tsx b/packages/app/src/Pages/ProfilePage.tsx
index 45769a91..905c4620 100644
--- a/packages/app/src/Pages/ProfilePage.tsx
+++ b/packages/app/src/Pages/ProfilePage.tsx
@@ -3,11 +3,11 @@ import { useEffect, useState } from "react";
import { FormattedMessage } from "react-intl";
import { useNavigate, useParams } from "react-router-dom";
import {
- createNostrLink,
encodeTLV,
encodeTLVEntries,
EventKind,
HexKey,
+ NostrLink,
NostrPrefix,
TLVEntryType,
tryParseNostrLink,
@@ -70,7 +70,7 @@ const RELAYS = 7;
const BOOKMARKS = 8;
function ZapsProfileTab({ id }: { id: HexKey }) {
- const zaps = useZapsFeed(createNostrLink(NostrPrefix.PublicKey, id));
+ const zaps = useZapsFeed(new NostrLink(NostrPrefix.PublicKey, id));
const zapsTotal = zaps.reduce((acc, z) => acc + z.amount, 0);
return (
diff --git a/packages/app/src/Pages/Root.tsx b/packages/app/src/Pages/Root.tsx
index 09c263a2..9a599133 100644
--- a/packages/app/src/Pages/Root.tsx
+++ b/packages/app/src/Pages/Root.tsx
@@ -2,6 +2,7 @@ import { useContext, useEffect, useState } from "react";
import { Link, Outlet, RouteObject, useParams } from "react-router-dom";
import { FormattedMessage } from "react-intl";
import { unixNow } from "@snort/shared";
+import { NostrLink } from "@snort/system";
import Timeline from "Element/Timeline";
import { System } from "index";
@@ -141,16 +142,9 @@ export const NotesTab = () => {
<>
- {
- deckContext.setThread(ev.id);
- }
- : undefined
- }
- />
+ {
+ deckContext.setThread(NostrLink.fromEvent(ev));
+ } : undefined} />
>
);
};
diff --git a/packages/app/src/Zapper.ts b/packages/app/src/Zapper.ts
index a6c158e1..d89655b3 100644
--- a/packages/app/src/Zapper.ts
+++ b/packages/app/src/Zapper.ts
@@ -3,9 +3,7 @@ import {
EventPublisher,
NostrEvent,
NostrLink,
- SystemInterface,
- createNostrLinkToEvent,
- linkToEventTag,
+ SystemInterface
} from "@snort/system";
import { generateRandomKey } from "Login";
import { isHex } from "SnortUtils";
@@ -63,7 +61,7 @@ export class Zapper {
weight: Number(v[3] ?? 0),
zap: {
pubkey: v[1],
- event: createNostrLinkToEvent(ev),
+ event: NostrLink.fromEvent(ev),
},
} as ZapTarget;
} else {
@@ -74,7 +72,7 @@ export class Zapper {
weight: 1,
zap: {
pubkey: ev.pubkey,
- event: createNostrLinkToEvent(ev),
+ event: NostrLink.fromEvent(ev),
},
} as ZapTarget;
}
@@ -103,7 +101,7 @@ export class Zapper {
t.zap && svc.canZap
? await pub?.zap(toSend * 1000, t.zap.pubkey, relays, undefined, t.memo, eb => {
if (t.zap?.event) {
- const tag = linkToEventTag(t.zap.event);
+ const tag = t.zap.event.toEventTag();
if (tag) {
eb.tag(tag);
}
diff --git a/packages/system/src/links.ts b/packages/system/src/links.ts
index 5a8a230c..5d8bd366 100644
--- a/packages/system/src/links.ts
+++ b/packages/system/src/links.ts
@@ -2,7 +2,7 @@ import * as utils from "@noble/curves/abstract/utils";
import { bech32 } from "@scure/base";
import { HexKey } from "./nostr";
-export enum NostrPrefix {
+export const enum NostrPrefix {
PublicKey = "npub",
PrivateKey = "nsec",
Note = "note",
diff --git a/packages/system/src/nostr-link.ts b/packages/system/src/nostr-link.ts
index b832ad8f..aedac1a7 100644
--- a/packages/system/src/nostr-link.ts
+++ b/packages/system/src/nostr-link.ts
@@ -2,82 +2,73 @@ import { bech32ToHex, hexToBech32, unwrap } from "@snort/shared";
import { NostrPrefix, decodeTLV, TLVEntryType, encodeTLV, NostrEvent, TaggedNostrEvent } from ".";
import { findTag } from "./utils";
-export interface NostrLink {
- type: NostrPrefix;
- id: string;
- kind?: number;
- author?: string;
- relays?: Array;
- encode(): string;
-}
+export class NostrLink {
+ constructor(
+ readonly type: NostrPrefix,
+ readonly id: string,
+ readonly kind?: number,
+ readonly author?: string,
+ readonly relays?: Array
+ ) { }
-export function linkToEventTag(link: NostrLink) {
- const relayEntry = link.relays ? [link.relays[0]] : [];
- if (link.type === NostrPrefix.PublicKey) {
- return ["p", link.id];
- } else if (link.type === NostrPrefix.Note || link.type === NostrPrefix.Event) {
- return ["e", link.id];
- } else if (link.type === NostrPrefix.Address) {
- return ["a", `${link.kind}:${link.author}:${link.id}`, ...relayEntry];
- }
-}
-
-export function tagToNostrLink(tag: Array) {
- switch (tag[0]) {
- case "e": {
- return createNostrLink(NostrPrefix.Event, tag[1], tag.slice(2));
- }
- case "p": {
- return createNostrLink(NostrPrefix.Profile, tag[1], tag.slice(2));
- }
- case "a": {
- const [kind, author, dTag] = tag[1].split(":");
- return createNostrLink(NostrPrefix.Address, dTag, tag.slice(2), Number(kind), author);
+ encode(): string {
+ if(this.type === NostrPrefix.Note || this.type === NostrPrefix.PrivateKey || this.type === NostrPrefix.PublicKey) {
+ return hexToBech32(this.type, this.id);
+ } else {
+ return encodeTLV(this.type, this.id, this.relays, this.kind, this.author);
}
}
- throw new Error(`Unknown tag kind ${tag[0]}`);
-}
-export function createNostrLinkToEvent(ev: TaggedNostrEvent | NostrEvent) {
- const relays = "relays" in ev ? ev.relays : undefined;
-
- if (ev.kind >= 30_000 && ev.kind < 40_000) {
- const dTag = unwrap(findTag(ev, "d"));
- return createNostrLink(NostrPrefix.Address, dTag, relays, ev.kind, ev.pubkey);
- }
- return createNostrLink(NostrPrefix.Event, ev.id, relays, ev.kind, ev.pubkey);
-}
-
-export function linkMatch(link: NostrLink, ev: NostrEvent) {
- if (link.type === NostrPrefix.Address) {
- const dTag = findTag(ev, "d");
- if (dTag && dTag === link.id && unwrap(link.author) === ev.pubkey && unwrap(link.kind) === ev.kind) {
- return true;
+ toEventTag() {
+ const relayEntry = this.relays ? [this.relays[0]] : [];
+ if (this.type === NostrPrefix.PublicKey) {
+ return ["p", this.id];
+ } else if (this.type === NostrPrefix.Note || this.type === NostrPrefix.Event) {
+ return ["e", this.id, ...relayEntry];
+ } else if (this.type === NostrPrefix.Address) {
+ return ["a", `${this.kind}:${this.author}:${this.id}`, ...relayEntry];
}
- } else if (link.type === NostrPrefix.Event || link.type === NostrPrefix.Note) {
- return link.id === ev.id;
}
- return false;
-}
-
-export function createNostrLink(prefix: NostrPrefix, id: string, relays?: string[], kind?: number, author?: string) {
- return {
- type: prefix,
- id,
- relays,
- kind,
- author,
- encode: () => {
- if (prefix === NostrPrefix.Note || prefix === NostrPrefix.PublicKey) {
- return hexToBech32(prefix, id);
+ matchesEvent(ev: NostrEvent) {
+ if (this.type === NostrPrefix.Address) {
+ const dTag = findTag(ev, "d");
+ if (dTag && dTag === this.id && unwrap(this.author) === ev.pubkey && unwrap(this.kind) === ev.kind) {
+ return true;
}
- if (prefix === NostrPrefix.Address || prefix === NostrPrefix.Event || prefix === NostrPrefix.Profile) {
- return encodeTLV(prefix, id, relays, kind, author);
+ } else if (this.type === NostrPrefix.Event || this.type === NostrPrefix.Note) {
+ return this.id === ev.id;
+ }
+
+ return false;
+ }
+
+ static fromTag(tag: Array) {
+ const relays = tag.length > 2 ? [tag[2]]: undefined;
+ switch (tag[0]) {
+ case "e": {
+ return new NostrLink(NostrPrefix.Event, tag[1], undefined, undefined, relays);
}
- return "";
- },
- } as NostrLink;
+ case "p": {
+ return new NostrLink(NostrPrefix.Profile, tag[1], undefined, undefined, relays);
+ }
+ case "a": {
+ const [kind, author, dTag] = tag[1].split(":");
+ return new NostrLink(NostrPrefix.Address, dTag, Number(kind), author, relays);
+ }
+ }
+ throw new Error(`Unknown tag kind ${tag[0]}`);
+ }
+
+ static fromEvent(ev: TaggedNostrEvent | NostrEvent) {
+ const relays = "relays" in ev ? ev.relays : undefined;
+
+ if (ev.kind >= 30_000 && ev.kind < 40_000) {
+ const dTag = unwrap(findTag(ev, "d"));
+ return new NostrLink(NostrPrefix.Address, dTag, ev.kind, ev.pubkey, relays);
+ }
+ return new NostrLink(NostrPrefix.Event, ev.id, ev.kind, ev.pubkey, relays);
+ }
}
export function validateNostrLink(link: string): boolean {
@@ -114,19 +105,11 @@ export function parseNostrLink(link: string, prefixHint?: NostrPrefix): NostrLin
if (isPrefix(NostrPrefix.PublicKey)) {
const id = bech32ToHex(entity);
if (id.length !== 64) throw new Error("Invalid nostr link, must contain 32 byte id");
- return {
- type: NostrPrefix.PublicKey,
- id: id,
- encode: () => hexToBech32(NostrPrefix.PublicKey, id),
- };
+ return new NostrLink(NostrPrefix.PublicKey, id);
} else if (isPrefix(NostrPrefix.Note)) {
const id = bech32ToHex(entity);
if (id.length !== 64) throw new Error("Invalid nostr link, must contain 32 byte id");
- return {
- type: NostrPrefix.Note,
- id: id,
- encode: () => hexToBech32(NostrPrefix.Note, id),
- };
+ return new NostrLink(NostrPrefix.Note, id);
} else if (isPrefix(NostrPrefix.Profile) || isPrefix(NostrPrefix.Event) || isPrefix(NostrPrefix.Address)) {
const decoded = decodeTLV(entity);
@@ -135,45 +118,17 @@ export function parseNostrLink(link: string, prefixHint?: NostrPrefix): NostrLin
const author = decoded.find(a => a.type === TLVEntryType.Author)?.value as string;
const kind = decoded.find(a => a.type === TLVEntryType.Kind)?.value as number;
- const encode = () => {
- return entity; // return original
- };
if (isPrefix(NostrPrefix.Profile)) {
if (id.length !== 64) throw new Error("Invalid nostr link, must contain 32 byte id");
- return {
- type: NostrPrefix.Profile,
- id,
- relays,
- kind,
- author,
- encode,
- };
+ return new NostrLink(NostrPrefix.Profile, id, kind, author, relays);
} else if (isPrefix(NostrPrefix.Event)) {
if (id.length !== 64) throw new Error("Invalid nostr link, must contain 32 byte id");
- return {
- type: NostrPrefix.Event,
- id,
- relays,
- kind,
- author,
- encode,
- };
+ return new NostrLink(NostrPrefix.Event, id, kind, author, relays);
} else if (isPrefix(NostrPrefix.Address)) {
- return {
- type: NostrPrefix.Address,
- id,
- relays,
- kind,
- author,
- encode,
- };
+ return new NostrLink(NostrPrefix.Address, id, kind, author, relays);
}
} else if (prefixHint) {
- return {
- type: prefixHint,
- id: link,
- encode: () => hexToBech32(prefixHint, link),
- };
+ return new NostrLink(prefixHint, link);
}
throw new Error("Invalid nostr link");
}
diff --git a/packages/system/src/request-builder.ts b/packages/system/src/request-builder.ts
index b5991895..03a0538f 100644
--- a/packages/system/src/request-builder.ts
+++ b/packages/system/src/request-builder.ts
@@ -1,9 +1,9 @@
import debug from "debug";
import { v4 as uuid } from "uuid";
-import { appendDedupe, sanitizeRelayUrl, unixNowMs } from "@snort/shared";
+import { appendDedupe, sanitizeRelayUrl, unixNowMs, unwrap } from "@snort/shared";
import EventKind from "./event-kind";
-import { SystemInterface } from "index";
+import { NostrLink, NostrPrefix, SystemInterface } from "index";
import { ReqFilter, u256, HexKey } from "./nostr";
import { RelayCache, splitByWriteRelays, splitFlatByWriteRelays } from "./gossip-model";
@@ -229,6 +229,30 @@ export class RequestFilterBuilder {
return this;
}
+ /**
+ * Get event from link
+ */
+ link(link: NostrLink) {
+ if(link.type === NostrPrefix.Address) {
+ return this.tag("d", [link.id])
+ .kinds([unwrap(link.kind)])
+ .authors([unwrap(link.author)]);
+ } else {
+ return this.ids([link.id]);
+ }
+ }
+
+ /**
+ * Get replies to link with e/a tags
+ */
+ replyToLink(link: NostrLink) {
+ if(link.type === NostrPrefix.Address) {
+ return this.tag("a", [`${link.kind}:${link.author}:${link.id}`]);
+ } else {
+ return this.tag("e", [link.id]);
+ }
+ }
+
/**
* Build/expand this filter into a set of relay specific queries
*/