Use NostrLink everywhere

This commit is contained in:
Kieran 2023-09-19 09:30:01 +01:00
parent a1cd56292a
commit 9fb6f0dfee
Signed by: Kieran
GPG Key ID: DE71CEB3925BE941
24 changed files with 164 additions and 220 deletions

View File

@ -1,13 +1,17 @@
import { NostrLink } from "@snort/system";
import { useArticles } from "Feed/ArticlesFeed";
import { orderDescending } from "SnortUtils";
import Note from "../Note";
import { useReactions } from "Feed/FeedReactions";
export default function Articles() {
const data = useArticles();
const related = useReactions("articles:reactions", data.data?.map(v => NostrLink.fromEvent(v)) ?? []);
return (
<>
{orderDescending(data.data ?? []).map(a => (
<Note data={a} key={a.id} related={[]} />
<Note data={a} key={a.id} related={related.data ?? []} />
))}
</>
);

View File

@ -1,11 +1,10 @@
import { NostrEvent, NostrPrefix, encodeTLV } from "@snort/system";
import { findTag, unwrap } from "SnortUtils";
import { NostrEvent, NostrLink } from "@snort/system";
import { findTag } from "SnortUtils";
import { FormattedMessage } from "react-intl";
import { Link } from "react-router-dom";
export function LiveEvent({ ev }: { ev: NostrEvent }) {
const title = findTag(ev, "title");
const d = unwrap(findTag(ev, "d"));
return (
<div className="text">
<div className="flex card">
@ -13,7 +12,7 @@ export function LiveEvent({ ev }: { ev: NostrEvent }) {
<h3>{title}</h3>
</div>
<div>
<Link to={`https://zap.stream/${encodeTLV(NostrPrefix.Address, d, undefined, ev.kind, ev.pubkey)}`}>
<Link to={`https://zap.stream/${NostrLink.fromEvent(ev).encode()}`}>
<button className="primary" type="button">
<FormattedMessage defaultMessage="Watch Live!" />
</button>

View File

@ -1,5 +1,5 @@
import "./LiveStreams.css";
import { NostrEvent, NostrPrefix, encodeTLV } from "@snort/system";
import { NostrEvent, NostrLink } from "@snort/system";
import { findTag } from "SnortUtils";
import { CSSProperties, useMemo } from "react";
import { Link } from "react-router-dom";
@ -32,7 +32,7 @@ function LiveStreamEvent({ ev }: { ev: NostrEvent }) {
const image = findTag(ev, "image");
const status = findTag(ev, "status");
const link = encodeTLV(NostrPrefix.Address, findTag(ev, "d") ?? "", undefined, ev.kind, ev.pubkey);
const link = NostrLink.fromEvent(ev).encode();
const imageProxy = proxy(image ?? "");
return (

View File

@ -11,8 +11,7 @@ import {
Lists,
EventExt,
parseZap,
tagToNostrLink,
createNostrLinkToEvent,
NostrLink
} from "@snort/system";
import { System } from "index";
@ -47,9 +46,9 @@ import Reactions from "Element/Reactions";
import { ZapGoal } from "Element/ZapGoal";
import NoteReaction from "Element/NoteReaction";
import ProfilePreview from "Element/ProfilePreview";
import { ProxyImg } from "Element/ProxyImg";
import messages from "./messages";
import { ProxyImg } from "./ProxyImg";
export interface NoteProps {
data: TaggedNostrEvent;
@ -299,7 +298,7 @@ export function NoteInner(props: NoteProps) {
return;
}
const link = createNostrLinkToEvent(eTarget);
const link = NostrLink.fromEvent(eTarget);
// detect cmd key and open in new tab
if (e.metaKey) {
window.open(`/e/${link.encode()}`, "_blank");
@ -319,7 +318,7 @@ export function NoteInner(props: NoteProps) {
const maxMentions = 2;
const replyTo = thread?.replyTo ?? thread?.root;
const replyLink = replyTo
? tagToNostrLink(
? NostrLink.fromTag(
[replyTo.key, replyTo.value ?? "", replyTo.relay ?? "", replyTo.marker ?? ""].filter(a => a.length > 0),
)
: undefined;

View File

@ -1,5 +1,5 @@
import { FormattedMessage, useIntl } from "react-intl";
import { HexKey, Lists, NostrPrefix, TaggedNostrEvent, encodeTLV } from "@snort/system";
import { HexKey, Lists, NostrLink, TaggedNostrEvent } from "@snort/system";
import { Menu, MenuItem } from "@szhsin/react-menu";
import { useDispatch, useSelector } from "react-redux";
@ -56,7 +56,7 @@ export function NoteContextMenu({ ev, ...props }: NosteContextMenuProps) {
}
async function share() {
const link = encodeTLV(NostrPrefix.Event, ev.id, ev.relays);
const link = NostrLink.fromEvent(ev).encode();
const url = `${window.location.protocol}//${window.location.host}/e/${link}`;
if ("share" in window.navigator) {
await window.navigator.share({
@ -92,7 +92,7 @@ export function NoteContextMenu({ ev, ...props }: NosteContextMenuProps) {
}
async function copyId() {
const link = encodeTLV(NostrPrefix.Event, ev.id, ev.relays);
const link = NostrLink.fromEvent(ev).encode();
await navigator.clipboard.writeText(link);
}

View File

@ -1,7 +1,7 @@
import "./NoteCreator.css";
import { FormattedMessage, useIntl } from "react-intl";
import { useDispatch, useSelector } from "react-redux";
import { encodeTLV, EventKind, NostrPrefix, TaggedNostrEvent, EventBuilder, tryParseNostrLink } from "@snort/system";
import { EventKind, NostrPrefix, TaggedNostrEvent, EventBuilder, tryParseNostrLink, NostrLink } from "@snort/system";
import Icon from "Icons/Icon";
import useEventPublisher from "Feed/EventPublisher";
@ -172,7 +172,7 @@ export function NoteCreator() {
if (file) {
const rx = await uploader.upload(file, file.name);
if (rx.header) {
const link = `nostr:${encodeTLV(NostrPrefix.Event, rx.header.id, undefined, rx.header.kind)}`;
const link = `nostr:${new NostrLink(NostrPrefix.Event, rx.header.id, rx.header.kind).encode()}`;
dispatch(setNote(`${note ? `${note}\n` : ""}${link}`));
dispatch(setOtherEvents([...otherEvents, rx.header]));
} else if (rx.url) {

View File

@ -2,7 +2,7 @@ import React, { HTMLProps, useContext, useEffect, useState } from "react";
import { useSelector, useDispatch } from "react-redux";
import { useIntl } from "react-intl";
import { useLongPress } from "use-long-press";
import { TaggedNostrEvent, ParsedZap, countLeadingZeros, createNostrLinkToEvent } from "@snort/system";
import { TaggedNostrEvent, ParsedZap, countLeadingZeros, NostrLink } from "@snort/system";
import { SnortContext, useUserProfile } from "@snort/system-react";
import { formatShort } from "Number";
@ -120,7 +120,7 @@ export default function NoteFooter(props: NoteFooterProps) {
name: getDisplayName(author, ev.pubkey),
zap: {
pubkey: ev.pubkey,
event: createNostrLinkToEvent(ev),
event: NostrLink.fromEvent(ev),
},
} as ZapTarget,
];

View File

@ -1,7 +1,7 @@
import "./Timeline.css";
import { ReactNode, useCallback, useContext, useMemo, useState, useSyncExternalStore } from "react";
import { FormattedMessage } from "react-intl";
import { TaggedNostrEvent, EventKind, u256, NostrEvent } from "@snort/system";
import { TaggedNostrEvent, EventKind, u256, NostrEvent, NostrLink } from "@snort/system";
import { unixNow } from "@snort/shared";
import { SnortContext } from "@snort/system-react";
import { useInView } from "react-intersection-observer";
@ -36,7 +36,7 @@ const TimelineFollows = (props: TimelineFollowsProps) => {
);
const reactions = useReactions(
"follows-feed-reactions",
feed.map(a => a.id),
feed.map(a => NostrLink.fromEvent(a)),
);
const system = useContext(SnortContext);
const login = useLogin();

View File

@ -1,5 +1,5 @@
import { useEffect, useState } from "react";
import { NostrEvent, TaggedNostrEvent } from "@snort/system";
import { NostrEvent, NostrLink, TaggedNostrEvent } from "@snort/system";
import PageSpinner from "Element/PageSpinner";
import Note from "Element/Note";
@ -8,7 +8,7 @@ import { useReactions } from "Feed/FeedReactions";
export default function TrendingNotes() {
const [posts, setPosts] = useState<Array<NostrEvent>>();
const related = useReactions("trending", posts?.map(a => a.id) ?? []);
const related = useReactions("trending", posts?.map(a => NostrLink.fromEvent(a)) ?? []);
async function loadTrendingNotes() {
const api = new NostrBandApi();

View File

@ -1,4 +1,4 @@
import { encodeTLV, NostrPrefix, NostrEvent } from "@snort/system";
import { NostrPrefix, NostrEvent, NostrLink } from "@snort/system";
import useEventPublisher from "Feed/EventPublisher";
import Icon from "Icons/Icon";
import Spinner from "Icons/Spinner";
@ -37,7 +37,7 @@ export default function WriteMessage({ chat }: { chat: Chat }) {
if (file) {
const rx = await uploader.upload(file, file.name);
if (rx.header) {
const link = `nostr:${encodeTLV(NostrPrefix.Event, rx.header.id, undefined, rx.header.kind)}`;
const link = `nostr:${new NostrLink(NostrPrefix.Event, rx.header.id, rx.header.kind).encode()}`;
setMsg(`${msg ? `${msg}\n` : ""}${link}`);
setOtherEvents([...otherEvents, rx.header]);
} else if (rx.url) {

View File

@ -1,6 +1,6 @@
import "./ZapGoal.css";
import { CSSProperties, useState } from "react";
import { NostrEvent, NostrPrefix, createNostrLink } from "@snort/system";
import { NostrEvent, NostrLink } from "@snort/system";
import useZapsFeed from "Feed/ZapsFeed";
import { formatShort } from "Number";
import { findTag } from "SnortUtils";
@ -10,7 +10,7 @@ import { Zapper } from "Zapper";
export function ZapGoal({ ev }: { ev: NostrEvent }) {
const [zap, setZap] = useState(false);
const zaps = useZapsFeed(createNostrLink(NostrPrefix.Note, ev.id));
const zaps = useZapsFeed(NostrLink.fromEvent(ev));
const target = Number(findTag(ev, "amount"));
const amount = zaps.reduce((acc, v) => (acc += v.amount * 1000), 0);
const progress = 100 * (amount / target);

View File

@ -1,6 +1,6 @@
import "./ZapstrEmbed.css";
import { Link } from "react-router-dom";
import { encodeTLV, NostrPrefix, NostrEvent } from "@snort/system";
import { NostrEvent, NostrLink } from "@snort/system";
import { ProxyImg } from "Element/ProxyImg";
import ProfileImage from "Element/ProfileImage";
@ -12,13 +12,7 @@ export default function ZapstrEmbed({ ev }: { ev: NostrEvent }) {
const subject = ev.tags.find(a => a[0] === "subject");
const refPersons = ev.tags.filter(a => a[0] === "p");
const link = encodeTLV(
NostrPrefix.Address,
ev.tags.find(a => a[0] === "d")?.[1] ?? "",
undefined,
ev.kind,
ev.pubkey,
);
const link = NostrLink.fromEvent(ev).encode();
return (
<>
<div className="flex zapstr mb10 card">

View File

@ -1,21 +1,30 @@
import { RequestBuilder, EventKind, NoteCollection } from "@snort/system";
import { RequestBuilder, EventKind, NoteCollection, NostrLink, NostrPrefix } from "@snort/system";
import { useRequestBuilder } from "@snort/system-react";
import useLogin from "Hooks/useLogin";
import { useMemo } from "react";
export function useReactions(subId: string, ids: Array<string>, others?: (rb: RequestBuilder) => void) {
export function useReactions(subId: string, ids: Array<NostrLink>, others?: (rb: RequestBuilder) => void) {
const { preferences: pref } = useLogin();
const sub = useMemo(() => {
const rb = new RequestBuilder(subId);
if (ids.length > 0) {
rb.withFilter()
const eTags = ids.filter(a => a.type === NostrPrefix.Note || a.type === NostrPrefix.Event);
const aTags = ids.filter(a => a.type === NostrPrefix.Address);
if (aTags.length > 0 || eTags.length > 0) {
const f = rb.withFilter()
.kinds(
pref.enableReactions
? [EventKind.Reaction, EventKind.Repost, EventKind.ZapReceipt]
: [EventKind.ZapReceipt, EventKind.Repost],
)
.tag("e", ids);
);
if(aTags.length > 0) {
f.tag("a", aTags.map(v => `${v.kind}:${v.author}:${v.id}`));
}
if(eTags.length > 0) {
f.tag("e", eTags.map(v => v.id));
}
}
others?.(rb);
return rb.numFilters > 0 ? rb : null;

View File

@ -3,11 +3,9 @@ import { EventKind, NoteCollection, RequestBuilder } from "@snort/system";
import { useRequestBuilder } from "@snort/system-react";
import { unixNow } from "@snort/shared";
import { unwrap, tagFilterOfTextRepost } from "SnortUtils";
import useTimelineWindow from "Hooks/useTimelineWindow";
import useLogin from "Hooks/useLogin";
import { SearchRelays } from "Const";
import { useReactions } from "./FeedReactions";
export interface TimelineFeedOptions {
method: "TIME_RANGE" | "LIMIT_UNTIL";
@ -140,36 +138,9 @@ export default function useTimelineFeed(subject: TimelineSubject, options: Timel
latest.clear();
}, [subject.relay]);
function getParentEvents() {
if (main.data) {
const repostsByKind6 = main.data
.filter(a => a.kind === EventKind.Repost && a.content === "")
.map(a => a.tags.find(b => b[0] === "e"))
.filter(a => a)
.map(a => unwrap(a)[1]);
const repostsByKind1 = main.data
.filter(
a => (a.kind === EventKind.Repost || a.kind === EventKind.TextNote) && a.tags.some(tagFilterOfTextRepost(a)),
)
.map(a => a.tags.find(tagFilterOfTextRepost(a)))
.filter(a => a)
.map(a => unwrap(a)[1]);
return [...repostsByKind6, ...repostsByKind1];
}
return [];
}
const trackingEvents = main.data?.map(a => a.id) ?? [];
const related = useReactions(`timeline-related:${subject.type}:${subject.discriminator}`, trackingEvents, rb => {
const trackingParentEvents = getParentEvents();
if (trackingParentEvents.length > 0) {
rb.withFilter().ids(trackingParentEvents);
}
});
return {
main: main.data,
related: related.data,
related: [],
latest: latest.data,
loading: main.loading(),
loadMore: () => {

View File

@ -1,7 +1,6 @@
import { unwrap } from "@snort/shared";
import {
EventExt,
EventKind,
NostrLink,
NostrPrefix,
TaggedNostrEvent,
@ -32,8 +31,7 @@ export function ThreadContextWrapper({ link, children }: { link: NostrLink; chil
const chains = new Map<u256, Array<TaggedNostrEvent>>();
if (thread.data) {
thread.data
?.filter(a => a.kind === EventKind.TextNote)
.sort((a, b) => b.created_at - a.created_at)
?.sort((a, b) => b.created_at - a.created_at)
.forEach(v => {
const t = EventExt.extractThread(v);
let replyTo = t?.replyTo?.value ?? t?.root?.value;

View File

@ -2,7 +2,7 @@ import "./Deck.css";
import { CSSProperties, createContext, useContext, useEffect, useState } from "react";
import { Outlet, useNavigate } from "react-router-dom";
import { FormattedMessage } from "react-intl";
import { NostrPrefix, createNostrLink } from "@snort/system";
import { NostrLink } from "@snort/system";
import { DeckNav } from "Element/Deck/Nav";
import useLoginFeed from "Feed/LoginFeed";
@ -25,8 +25,8 @@ import useLogin from "Hooks/useLogin";
type Cols = "notes" | "articles" | "media" | "streams" | "notifications";
interface DeckScope {
thread?: string;
setThread: (e?: string) => void;
thread?: NostrLink,
setThread: (e?: NostrLink) => void
}
export const DeckContext = createContext<DeckScope | undefined>(undefined);
@ -35,7 +35,7 @@ export function SnortDeckLayout() {
const login = useLogin();
const navigate = useNavigate();
const [deckScope, setDeckScope] = useState<DeckScope>({
setThread: (e?: string) => setDeckScope(s => ({ ...s, thread: e })),
setThread: (e?: NostrLink) => setDeckScope(s => ({ ...s, thread: e }))
});
useLoginFeed();
@ -71,7 +71,7 @@ export function SnortDeckLayout() {
{deckScope.thread && (
<>
<Modal onClose={() => deckScope.setThread(undefined)} className="thread-overlay">
<ThreadContextWrapper link={createNostrLink(NostrPrefix.Note, deckScope.thread)}>
<ThreadContextWrapper link={deckScope.thread}>
<SpotlightFromThread onClose={() => deckScope.setThread(undefined)} />
<div>
<Thread onBack={() => 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 (
<div>
@ -158,7 +158,7 @@ function MediaCol({ setThread }: { setThread: (e: string) => void }) {
"--img": `url(${proxy(images[0].content)})`,
} as CSSProperties
}
onClick={() => setThread(e.id)}></div>
onClick={() => setThread(NostrLink.fromEvent(e))}></div>
);
}}
/>

View File

@ -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;
}
}

View File

@ -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);
}
}
}

View File

@ -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 (
<div className="main-content">

View File

@ -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 = () => {
<>
<FollowsHint />
<TaskList />
<TimelineFollows
postsOnly={true}
noteOnClick={
deckContext
? ev => {
deckContext.setThread(ev.id);
}
: undefined
}
/>
<TimelineFollows postsOnly={true} noteOnClick={deckContext ? (ev) => {
deckContext.setThread(NostrLink.fromEvent(ev));
} : undefined} />
</>
);
};

View File

@ -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);
}

View File

@ -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",

View File

@ -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<string>;
encode(): string;
}
export class NostrLink {
constructor(
readonly type: NostrPrefix,
readonly id: string,
readonly kind?: number,
readonly author?: string,
readonly relays?: Array<string>
) { }
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<string>) {
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<string>) {
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");
}

View File

@ -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
*/