move to pkg

This commit is contained in:
2023-06-08 12:45:23 +02:00
parent 2b80109e3b
commit 81ccb95d82
256 changed files with 4856 additions and 529 deletions

View File

@ -1,4 +1,4 @@
import { NostrEvent } from "System";
import { NostrEvent } from "@snort/system";
import { db } from "Db";
import FeedCache from "./FeedCache";

View File

@ -1,6 +1,6 @@
import FeedCache from "Cache/FeedCache";
import { db } from "Db";
import { MetadataCache } from "Cache";
import { MetadataCache } from "@snort/system";
import { LNURL } from "LNURL";
import { fetchNip05Pubkey } from "Nip05/Verifier";

View File

@ -1,57 +1,8 @@
import { HexKey, NostrEvent, UserMetadata } from "System";
import { hexToBech32, unixNowMs } from "SnortUtils";
import { DmCache } from "./DMCache";
import { InteractionCache } from "./EventInteractionCache";
import { UserCache } from "./UserCache";
import { UserRelays } from "./UserRelayCache";
export interface MetadataCache extends UserMetadata {
/**
* When the object was saved in cache
*/
loaded: number;
/**
* When the source metadata event was created
*/
created: number;
/**
* The pubkey of the owner of this metadata
*/
pubkey: HexKey;
/**
* The bech32 encoded pubkey
*/
npub: string;
/**
* Pubkey of zapper service
*/
zapService?: HexKey;
/**
* If the nip05 is valid for this user
*/
isNostrAddressValid: boolean;
}
export function mapEventToProfile(ev: NostrEvent) {
try {
const data: UserMetadata = JSON.parse(ev.content);
return {
...data,
pubkey: ev.pubkey,
npub: hexToBech32("npub", ev.pubkey),
created: ev.created_at,
loaded: unixNowMs(),
} as MetadataCache;
} catch (e) {
console.error("Failed to parse JSON", ev, e);
}
}
export async function preload(follows?: Array<string>) {
const preloads = [
UserCache.preload(follows),

View File

@ -1,6 +1,4 @@
import { UserRelays } from "Cache/UserRelayCache";
import { NostrSystem, RelaySettings } from "System";
import { ProfileLoaderService } from "System/ProfileCache";
import { RelaySettings } from "@snort/system";
/**
* Add-on api for snort features

View File

@ -1,6 +1,5 @@
import Dexie, { Table } from "dexie";
import { FullRelaySettings, HexKey, NostrEvent, u256 } from "System";
import { MetadataCache } from "Cache";
import { FullRelaySettings, HexKey, NostrEvent, u256, MetadataCache } from "@snort/system";
export const NAME = "snortDB";
export const VERSION = 8;

View File

@ -2,7 +2,7 @@ import "./Avatar.css";
import Nostrich from "nostrich.webp";
import { CSSProperties, useEffect, useState } from "react";
import type { UserMetadata } from "System";
import type { UserMetadata } from "@snort/system";
import useImgProxy from "Hooks/useImgProxy";

View File

@ -3,7 +3,7 @@ import "./BadgeList.css";
import { useState } from "react";
import { FormattedMessage } from "react-intl";
import { TaggedRawEvent } from "System";
import { TaggedRawEvent } from "@snort/system";
import { ProxyImg } from "Element/ProxyImg";
import Icon from "Icons/Icon";

View File

@ -1,5 +1,5 @@
import { FormattedMessage } from "react-intl";
import { HexKey } from "System";
import { HexKey } from "@snort/system";
import useModeration from "Hooks/useModeration";
import messages from "./messages";

View File

@ -1,6 +1,6 @@
import { useState, useMemo, ChangeEvent } from "react";
import { FormattedMessage } from "react-intl";
import { HexKey, TaggedRawEvent } from "System";
import { HexKey, TaggedRawEvent } from "@snort/system";
import Note from "Element/Note";
import useLogin from "Hooks/useLogin";

View File

@ -2,7 +2,7 @@ import "./DM.css";
import { useEffect, useState } from "react";
import { useIntl } from "react-intl";
import { useInView } from "react-intersection-observer";
import { TaggedRawEvent } from "System";
import { TaggedRawEvent } from "@snort/system";
import useEventPublisher from "Feed/EventPublisher";
import NoteTime from "Element/NoteTime";

View File

@ -1,6 +1,6 @@
import "./DmWindow.css";
import { useEffect, useMemo, useRef } from "react";
import { TaggedRawEvent } from "System";
import { TaggedRawEvent } from "@snort/system";
import ProfileImage from "Element/ProfileImage";
import DM from "Element/DM";

View File

@ -1,6 +1,6 @@
import "./FollowButton.css";
import { FormattedMessage } from "react-intl";
import { HexKey } from "System";
import { HexKey } from "@snort/system";
import useEventPublisher from "Feed/EventPublisher";
import { parseId } from "SnortUtils";

View File

@ -1,6 +1,6 @@
import { ReactNode } from "react";
import { FormattedMessage } from "react-intl";
import { HexKey } from "System";
import { HexKey } from "@snort/system";
import useEventPublisher from "Feed/EventPublisher";
import ProfilePreview from "Element/ProfilePreview";

View File

@ -1,6 +1,6 @@
import { useMemo } from "react";
import { Link } from "react-router-dom";
import { HexKey } from "System";
import { HexKey } from "@snort/system";
import { useUserProfile } from "Hooks/useUserProfile";
import { profileLink } from "SnortUtils";

View File

@ -1,5 +1,5 @@
import { FormattedMessage } from "react-intl";
import { HexKey } from "System";
import { HexKey } from "@snort/system";
import useModeration from "Hooks/useModeration";
import messages from "./messages";

View File

@ -1,5 +1,5 @@
import { FormattedMessage } from "react-intl";
import { HexKey } from "System";
import { HexKey } from "@snort/system";
import MuteButton from "Element/MuteButton";
import ProfilePreview from "Element/ProfilePreview";
import useModeration from "Hooks/useModeration";

View File

@ -1,5 +1,5 @@
import "./Nip05.css";
import { HexKey } from "System";
import { HexKey } from "@snort/system";
import Icon from "Icons/Icon";
import { useUserProfile } from "Hooks/useUserProfile";

View File

@ -1,7 +1,7 @@
import { useEffect, useMemo, useState, ChangeEvent } from "react";
import { useIntl, FormattedMessage } from "react-intl";
import { useNavigate } from "react-router-dom";
import { UserMetadata } from "System";
import { UserMetadata, mapEventToProfile } from "@snort/system";
import { unwrap } from "SnortUtils";
import { formatShort } from "Number";
@ -22,7 +22,7 @@ import useEventPublisher from "Feed/EventPublisher";
import { debounce } from "SnortUtils";
import useLogin from "Hooks/useLogin";
import SnortServiceProvider from "Nip05/SnortServiceProvider";
import { mapEventToProfile, UserCache } from "Cache";
import { UserCache } from "Cache";
import messages from "./messages";

View File

@ -1,7 +1,7 @@
import { FormattedMessage } from "react-intl";
import { NostrEvent } from "System";
import { NostrEvent, NostrLink } from "@snort/system";
import { findTag, NostrLink } from "SnortUtils";
import { findTag } from "SnortUtils";
import useEventFeed from "Feed/EventFeed";
import PageSpinner from "Element/PageSpinner";
import Reveal from "Element/Reveal";

View File

@ -1,8 +1,7 @@
import { Link } from "react-router-dom";
import { NostrPrefix } from "System";
import { NostrPrefix, parseNostrLink } from "@snort/system";
import Mention from "Element/Mention";
import { parseNostrLink } from "SnortUtils";
import NoteQuote from "Element/NoteQuote";
export default function NostrLink({ link, depth }: { link: string; depth?: number }) {

View File

@ -3,7 +3,7 @@ import React, { useMemo, useState, useLayoutEffect, ReactNode } from "react";
import { useNavigate, Link } from "react-router-dom";
import { useInView } from "react-intersection-observer";
import { useIntl, FormattedMessage } from "react-intl";
import { TaggedRawEvent, HexKey, EventKind, NostrPrefix, Lists } from "System";
import { TaggedRawEvent, HexKey, EventKind, NostrPrefix, Lists, EventExt } from "@snort/system";
import useEventPublisher from "Feed/EventPublisher";
import Icon from "Icons/Icon";
@ -26,7 +26,6 @@ import Reveal from "Element/Reveal";
import useModeration from "Hooks/useModeration";
import { UserCache } from "Cache/UserCache";
import Poll from "Element/Poll";
import { EventExt } from "System/EventExt";
import useLogin from "Hooks/useLogin";
import { setBookmarked, setPinned } from "Login";
import { NostrFileElement } from "Element/NostrFileHeader";

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, TaggedRawEvent } from "System";
import { encodeTLV, EventKind, NostrPrefix, TaggedRawEvent, EventBuilder } from "@snort/system";
import Icon from "Icons/Icon";
import useEventPublisher from "Feed/EventPublisher";
@ -31,7 +31,6 @@ import { LNURL } from "LNURL";
import messages from "./messages";
import { ClipboardEventHandler, useState } from "react";
import Spinner from "Icons/Spinner";
import { EventBuilder } from "System";
import { Menu, MenuItem } from "@szhsin/react-menu";
import { LoginStore } from "Login";
import { getCurrentSubscription } from "Subscription";

View File

@ -3,7 +3,7 @@ import { useSelector, useDispatch } from "react-redux";
import { useIntl, FormattedMessage } from "react-intl";
import { Menu, MenuItem } from "@szhsin/react-menu";
import { useLongPress } from "use-long-press";
import { TaggedRawEvent, HexKey, u256, encodeTLV, NostrPrefix, Lists } from "System";
import { TaggedRawEvent, HexKey, u256, encodeTLV, NostrPrefix, Lists } from "@snort/system";
import Icon from "Icons/Icon";
import Spinner from "Icons/Spinner";

View File

@ -1,5 +1,5 @@
import useEventFeed from "Feed/EventFeed";
import { NostrLink } from "SnortUtils";
import { NostrLink } from "@snort/system";
import Note from "Element/Note";
import PageSpinner from "Element/PageSpinner";

View File

@ -1,14 +1,13 @@
import "./NoteReaction.css";
import { Link } from "react-router-dom";
import { useMemo } from "react";
import { EventKind, NostrEvent, TaggedRawEvent, NostrPrefix } from "System";
import { EventKind, NostrEvent, TaggedRawEvent, NostrPrefix, EventExt } from "@snort/system";
import Note from "Element/Note";
import ProfileImage from "Element/ProfileImage";
import { eventLink, hexToBech32 } from "SnortUtils";
import NoteTime from "Element/NoteTime";
import useModeration from "Hooks/useModeration";
import { EventExt } from "System/EventExt";
export interface NoteReactionProps {
data: TaggedRawEvent;

View File

@ -1,4 +1,4 @@
import { TaggedRawEvent } from "System";
import { TaggedRawEvent } from "@snort/system";
import { useState } from "react";
import { FormattedMessage, FormattedNumber, useIntl } from "react-intl";

View File

@ -1,13 +1,12 @@
import "./ProfileImage.css";
import React, { useMemo } from "react";
import { HexKey, NostrPrefix } from "System";
import { HexKey, NostrPrefix, MetadataCache } from "@snort/system";
import { useUserProfile } from "Hooks/useUserProfile";
import { hexToBech32, profileLink } from "SnortUtils";
import Avatar from "Element/Avatar";
import Nip05 from "Element/Nip05";
import { MetadataCache } from "Cache";
import { Link } from "react-router-dom";
export interface ProfileImageProps {

View File

@ -4,7 +4,7 @@ import { ReactNode } from "react";
import ProfileImage from "Element/ProfileImage";
import FollowButton from "Element/FollowButton";
import { useUserProfile } from "Hooks/useUserProfile";
import { HexKey } from "System";
import { HexKey } from "@snort/system";
import { useInView } from "react-intersection-observer";
export interface ProfilePreviewProps {

View File

@ -1,4 +1,4 @@
import { NostrEvent } from "System";
import { NostrEvent } from "@snort/system";
import { dedupe } from "SnortUtils";
import FollowListBase from "./FollowListBase";

View File

@ -2,7 +2,7 @@ import "./Reactions.css";
import { useState, useMemo, useEffect } from "react";
import { useIntl, FormattedMessage } from "react-intl";
import { TaggedRawEvent } from "System";
import { TaggedRawEvent } from "@snort/system";
import { formatShort } from "Number";
import Icon from "Icons/Icon";

View File

@ -2,7 +2,7 @@ import "./Relay.css";
import { useMemo } from "react";
import { FormattedMessage } from "react-intl";
import { useNavigate } from "react-router-dom";
import { RelaySettings } from "System";
import { RelaySettings } from "@snort/system";
import useRelayState from "Feed/RelayState";
import { System } from "index";

View File

@ -2,7 +2,7 @@ import "./RelaysMetadata.css";
import Nostrich from "nostrich.webp";
import { useState } from "react";
import { FullRelaySettings } from "System";
import { FullRelaySettings } from "@snort/system";
import Icon from "Icons/Icon";
const RelayFavicon = ({ url }: { url: string }) => {

View File

@ -2,7 +2,7 @@ import "./SendSats.css";
import React, { useEffect, useMemo, useState } from "react";
import { useIntl, FormattedMessage } from "react-intl";
import { HexKey, NostrEvent } from "System";
import { HexKey, NostrEvent, EventPublisher } from "@snort/system";
import { System } from "index";
import { formatShort } from "Number";
import Icon from "Icons/Icon";
@ -16,7 +16,6 @@ import { chunks, debounce } from "SnortUtils";
import { useWallet } from "Wallet";
import useLogin from "Hooks/useLogin";
import { generateRandomKey } from "Login";
import { EventPublisher } from "System/EventPublisher";
import { ZapPoolController } from "ZapPoolController";
import messages from "./messages";

View File

@ -5,7 +5,7 @@ import useRelayState from "Feed/RelayState";
import Tabs, { Tab } from "Element/Tabs";
import { unwrap } from "SnortUtils";
import useSystemState from "Hooks/useSystemState";
import { ReqFilter } from "System";
import { ReqFilter } from "@snort/system";
import { useCopy } from "useCopy";
import { System } from "index";

View File

@ -1,5 +1,5 @@
import { useEffect, useState } from "react";
import { HexKey, NostrPrefix } from "System";
import { HexKey, NostrPrefix } from "@snort/system";
import { FormattedMessage } from "react-intl";
import FollowListBase from "Element/FollowListBase";

View File

@ -1,10 +1,10 @@
import "./Text.css";
import { useMemo } from "react";
import { Link, useLocation } from "react-router-dom";
import { HexKey, NostrPrefix } from "System";
import { HexKey, NostrPrefix, validateNostrLink } from "@snort/system";
import { MentionRegex, InvoiceRegex, HashtagRegex, CashuRegex } from "Const";
import { eventLink, hexToBech32, splitByUrl, validateNostrLink } from "SnortUtils";
import { eventLink, hexToBech32, splitByUrl } from "SnortUtils";
import Invoice from "Element/Invoice";
import Hashtag from "Element/Hashtag";
import Mention from "Element/Mention";

View File

@ -4,12 +4,11 @@ import "./Textarea.css";
import { useIntl } from "react-intl";
import ReactTextareaAutocomplete from "@webscopeio/react-textarea-autocomplete";
import TextareaAutosize from "react-textarea-autosize";
import { NostrPrefix } from "System";
import { NostrPrefix, MetadataCache } from "@snort/system";
import Avatar from "Element/Avatar";
import Nip05 from "Element/Nip05";
import { hexToBech32 } from "SnortUtils";
import { MetadataCache } from "Cache";
import { UserCache } from "Cache/UserCache";
import messages from "./messages";

View File

@ -2,10 +2,17 @@ import "./Thread.css";
import { useMemo, useState, ReactNode } from "react";
import { useIntl } from "react-intl";
import { useNavigate, useLocation, Link, useParams } from "react-router-dom";
import { TaggedRawEvent, u256, EventKind, NostrPrefix } from "System";
import { EventExt, Thread as ThreadInfo } from "System/EventExt";
import {
TaggedRawEvent,
u256,
EventKind,
NostrPrefix,
EventExt,
Thread as ThreadInfo,
parseNostrLink,
} from "@snort/system";
import { eventLink, unwrap, getReactions, parseNostrLink, getAllReactions, findTag } from "SnortUtils";
import { eventLink, unwrap, getReactions, getAllReactions, findTag } from "SnortUtils";
import BackButton from "Element/BackButton";
import Note from "Element/Note";
import NoteGhost from "Element/NoteGhost";

View File

@ -2,7 +2,7 @@ import "./Timeline.css";
import { FormattedMessage } from "react-intl";
import { useCallback, useMemo } from "react";
import { useInView } from "react-intersection-observer";
import { TaggedRawEvent, EventKind, u256 } from "System";
import { TaggedRawEvent, EventKind, u256 } from "@snort/system";
import Icon from "Icons/Icon";
import { dedupeByPubkey, findTag, tagFilterOfTextRepost } from "SnortUtils";

View File

@ -1,5 +1,5 @@
import { useEffect, useState } from "react";
import { NostrEvent, TaggedRawEvent } from "System";
import { NostrEvent, TaggedRawEvent } from "@snort/system";
import { FormattedMessage } from "react-intl";
import PageSpinner from "Element/PageSpinner";

View File

@ -1,5 +1,5 @@
import { useEffect, useState } from "react";
import { HexKey } from "System";
import { HexKey } from "@snort/system";
import { FormattedMessage } from "react-intl";
import FollowListBase from "Element/FollowListBase";

View File

@ -1,7 +1,7 @@
import { MouseEvent } from "react";
import { useNavigate, Link } from "react-router-dom";
import { HexKey } from "System";
import { HexKey } from "@snort/system";
import { useUserProfile } from "Hooks/useUserProfile";
import { profileLink } from "SnortUtils";

View File

@ -1,4 +1,4 @@
import { encodeTLV, NostrPrefix, NostrEvent } from "System";
import { encodeTLV, NostrPrefix, NostrEvent } from "@snort/system";
import useEventPublisher from "Feed/EventPublisher";
import Icon from "Icons/Icon";
import Spinner from "Icons/Spinner";

View File

@ -1,7 +1,7 @@
import "./Zap.css";
import { useMemo } from "react";
import { FormattedMessage, useIntl } from "react-intl";
import { HexKey, TaggedRawEvent } from "System";
import { HexKey, TaggedRawEvent } from "@snort/system";
import { decodeInvoice, InvoiceDetails, sha256, unwrap } from "SnortUtils";
import { formatShort } from "Number";

View File

@ -1,6 +1,6 @@
import "./ZapButton.css";
import { useState } from "react";
import { HexKey } from "System";
import { HexKey } from "@snort/system";
import { useUserProfile } from "Hooks/useUserProfile";
import SendSats from "Element/SendSats";

View File

@ -1,6 +1,6 @@
import "./ZapstrEmbed.css";
import { Link } from "react-router-dom";
import { encodeTLV, NostrPrefix, NostrEvent } from "System";
import { encodeTLV, NostrPrefix, NostrEvent } from "@snort/system";
import { ProxyImg } from "Element/ProxyImg";
import ProfileImage from "Element/ProfileImage";

View File

@ -1,4 +1,4 @@
import { NostrEvent } from "System";
import { NostrEvent } from "@snort/system";
export interface TrendingUser {
pubkey: string;

View File

@ -1,9 +1,7 @@
import { useMemo } from "react";
import { EventKind, HexKey, Lists } from "System";
import { EventKind, HexKey, Lists, RequestBuilder, FlatNoteStore, ReplaceableNoteStore } from "@snort/system";
import { unwrap, findTag, chunks } from "SnortUtils";
import { RequestBuilder } from "System";
import { FlatNoteStore, ReplaceableNoteStore } from "System/NoteCollection";
import useRequestBuilder from "Hooks/useRequestBuilder";
type BadgeAwards = {

View File

@ -1,4 +1,4 @@
import { HexKey, Lists } from "System";
import { HexKey, Lists } from "@snort/system";
import useNotelistSubscription from "Hooks/useNotelistSubscription";
import useLogin from "Hooks/useLogin";

View File

@ -1,9 +1,8 @@
import { useMemo } from "react";
import { NostrPrefix } from "System";
import { NostrPrefix, RequestBuilder, ReplaceableNoteStore, NostrLink } from "@snort/system";
import useRequestBuilder from "Hooks/useRequestBuilder";
import { RequestBuilder, ReplaceableNoteStore } from "System";
import { NostrLink, unwrap } from "SnortUtils";
import { unwrap } from "SnortUtils";
export default function useEventFeed(link: NostrLink) {
const sub = useMemo(() => {

View File

@ -1,6 +1,6 @@
import { useMemo } from "react";
import useLogin from "Hooks/useLogin";
import { EventPublisher } from "System/EventPublisher";
import { EventPublisher } from "@snort/system";
import { System } from "index";
export default function useEventPublisher() {

View File

@ -1,7 +1,6 @@
import { useMemo } from "react";
import { HexKey, EventKind } from "System";
import { HexKey, EventKind, PubkeyReplaceableNoteStore, RequestBuilder } from "@snort/system";
import { PubkeyReplaceableNoteStore, RequestBuilder } from "System";
import useRequestBuilder from "Hooks/useRequestBuilder";
export default function useFollowersFeed(pubkey?: HexKey) {

View File

@ -1,7 +1,6 @@
import { useMemo } from "react";
import { HexKey, TaggedRawEvent, EventKind } from "System";
import { HexKey, TaggedRawEvent, EventKind, PubkeyReplaceableNoteStore, RequestBuilder } from "@snort/system";
import { PubkeyReplaceableNoteStore, RequestBuilder } from "System";
import useRequestBuilder from "Hooks/useRequestBuilder";
import useLogin from "Hooks/useLogin";

View File

@ -1,5 +1,5 @@
import { useEffect, useMemo } from "react";
import { TaggedRawEvent, Lists, EventKind } from "System";
import { TaggedRawEvent, Lists, EventKind, FlatNoteStore, RequestBuilder } from "@snort/system";
import debug from "debug";
import { bech32ToHex, getNewest, getNewestEventTagsByKey, unwrap } from "SnortUtils";
@ -7,7 +7,6 @@ import { makeNotification, sendNotification } from "Notifications";
import useEventPublisher from "Feed/EventPublisher";
import { getMutedKeys } from "Feed/MuteList";
import useModeration from "Hooks/useModeration";
import { FlatNoteStore, RequestBuilder } from "System";
import useRequestBuilder from "Hooks/useRequestBuilder";
import { DmCache } from "Cache";
import useLogin from "Hooks/useLogin";

View File

@ -1,8 +1,14 @@
import { useMemo } from "react";
import { HexKey, TaggedRawEvent, Lists, EventKind } from "System";
import {
HexKey,
TaggedRawEvent,
Lists,
EventKind,
ParameterizedReplaceableNoteStore,
RequestBuilder,
} from "@snort/system";
import { getNewest } from "SnortUtils";
import { ParameterizedReplaceableNoteStore, RequestBuilder } from "System";
import useRequestBuilder from "Hooks/useRequestBuilder";
import useLogin from "Hooks/useLogin";

View File

@ -1,4 +1,4 @@
import { HexKey, Lists } from "System";
import { HexKey, Lists } from "@snort/system";
import useNotelistSubscription from "Hooks/useNotelistSubscription";
import useLogin from "Hooks/useLogin";

View File

@ -1,8 +1,6 @@
import { useMemo } from "react";
import { HexKey, FullRelaySettings, EventKind } from "System";
import { HexKey, FullRelaySettings, EventKind, RequestBuilder, ReplaceableNoteStore } from "@snort/system";
import { RequestBuilder } from "System";
import { ReplaceableNoteStore } from "System/NoteCollection";
import useRequestBuilder from "Hooks/useRequestBuilder";
export default function useRelaysFeed(pubkey?: HexKey) {

View File

@ -1,9 +1,16 @@
import { useMemo } from "react";
import { HexKey, FullRelaySettings, TaggedRawEvent, RelaySettings, EventKind } from "System";
import {
HexKey,
FullRelaySettings,
TaggedRawEvent,
RelaySettings,
EventKind,
PubkeyReplaceableNoteStore,
RequestBuilder,
} from "@snort/system";
import debug from "debug";
import { sanitizeRelayUrl } from "SnortUtils";
import { PubkeyReplaceableNoteStore, RequestBuilder } from "System";
import useRequestBuilder from "Hooks/useRequestBuilder";
import { UserRelays } from "Cache/UserRelayCache";

View File

@ -1,8 +1,7 @@
import { useEffect, useMemo, useState } from "react";
import { u256, EventKind } from "System";
import { u256, EventKind, NostrLink, FlatNoteStore, RequestBuilder } from "@snort/system";
import { appendDedupe, NostrLink } from "SnortUtils";
import { FlatNoteStore, RequestBuilder } from "System";
import { appendDedupe } from "SnortUtils";
import useRequestBuilder from "Hooks/useRequestBuilder";
import useLogin from "Hooks/useLogin";

View File

@ -1,8 +1,7 @@
import { useCallback, useEffect, useMemo, useState } from "react";
import { EventKind, u256 } from "System";
import { EventKind, u256, FlatNoteStore, RequestBuilder } from "@snort/system";
import { unixNow, unwrap, tagFilterOfTextRepost } from "SnortUtils";
import { FlatNoteStore, RequestBuilder } from "System";
import useRequestBuilder from "Hooks/useRequestBuilder";
import useTimelineWindow from "Hooks/useTimelineWindow";
import useLogin from "Hooks/useLogin";

View File

@ -1,8 +1,7 @@
import { useMemo } from "react";
import { HexKey, EventKind } from "System";
import { HexKey, EventKind, FlatNoteStore, RequestBuilder } from "@snort/system";
import { parseZap } from "Element/Zap";
import { FlatNoteStore, RequestBuilder } from "System";
import useRequestBuilder from "Hooks/useRequestBuilder";
export default function useZapsFeed(pubkey?: HexKey) {

View File

@ -1,5 +1,5 @@
import { useSyncExternalStore } from "react";
import { HexKey, u256 } from "System";
import { HexKey, u256 } from "@snort/system";
import { InteractionCache } from "Cache/EventInteractionCache";
import { EventInteraction } from "Db";

View File

@ -1,4 +1,4 @@
import { HexKey } from "System";
import { HexKey } from "@snort/system";
import useEventPublisher from "Feed/EventPublisher";
import useLogin from "Hooks/useLogin";
import { setBlocked, setMuted } from "Login";

View File

@ -1,7 +1,13 @@
import { useMemo } from "react";
import { HexKey, Lists, EventKind } from "System";
import {
HexKey,
Lists,
EventKind,
FlatNoteStore,
ParameterizedReplaceableNoteStore,
RequestBuilder,
} from "@snort/system";
import { FlatNoteStore, ParameterizedReplaceableNoteStore, RequestBuilder } from "System";
import useRequestBuilder from "Hooks/useRequestBuilder";
import useLogin from "Hooks/useLogin";

View File

@ -1,6 +1,5 @@
import { useSyncExternalStore } from "react";
import { RequestBuilder } from "System";
import { EmptySnapshot, NoteStore, StoreSnapshot } from "System/NoteCollection";
import { RequestBuilder, EmptySnapshot, NoteStore, StoreSnapshot } from "@snort/system";
import { unwrap } from "SnortUtils";
import { System } from "index";

View File

@ -1,5 +1,5 @@
import { useSyncExternalStore } from "react";
import { SystemSnapshot } from "System";
import { SystemSnapshot } from "@snort/system";
import { System } from "index";
export default function useSystemState() {

View File

@ -1,7 +1,6 @@
import { useEffect, useSyncExternalStore } from "react";
import { HexKey } from "System";
import { MetadataCache } from "Cache";
import { HexKey, MetadataCache } from "@snort/system";
import { UserCache } from "Cache/UserCache";
import { ProfileLoader } from "index";

View File

@ -1,4 +1,4 @@
import { HexKey, NostrEvent } from "System";
import { HexKey, NostrEvent } from "@snort/system";
import { EmailRegex } from "Const";
import { bech32ToText, unwrap } from "SnortUtils";

View File

@ -1,4 +1,4 @@
import { HexKey, RelaySettings } from "System";
import { HexKey, RelaySettings, EventPublisher } from "@snort/system";
import * as secp from "@noble/curves/secp256k1";
import * as utils from "@noble/curves/abstract/utils";
@ -7,7 +7,6 @@ import { LoginStore, UserPreferences, LoginSession } from "Login";
import { generateBip39Entropy, entropyToPrivateKey } from "nip6";
import { bech32ToHex, dedupeById, randomSample, sanitizeRelayUrl, unixNowMs, unwrap } from "SnortUtils";
import { SubscriptionEvent } from "Subscription";
import { EventPublisher } from "System/EventPublisher";
import { System } from "index";
export function setRelays(state: LoginSession, relays: Record<string, RelaySettings>, createdAt: number) {

View File

@ -1,4 +1,4 @@
import { HexKey, RelaySettings, u256 } from "System";
import { HexKey, RelaySettings, u256 } from "@snort/system";
import { UserPreferences } from "Login";
import { SubscriptionEvent } from "Subscription";

View File

@ -1,7 +1,7 @@
import * as secp from "@noble/curves/secp256k1";
import * as utils from "@noble/curves/abstract/utils";
import { HexKey, RelaySettings } from "System";
import { HexKey, RelaySettings } from "@snort/system";
import { DefaultRelays } from "Const";
import ExternalStore from "ExternalStore";

View File

@ -1,5 +1,4 @@
import { EventKind } from "System";
import { EventPublisher } from "System/EventPublisher";
import { EventKind, EventPublisher } from "@snort/system";
import { ServiceError, ServiceProvider } from "./ServiceProvider";
export interface ManageHandle {

View File

@ -1,7 +1,6 @@
import Nostrich from "nostrich.webp";
import { TaggedRawEvent, EventKind } from "System";
import { MetadataCache } from "Cache";
import { TaggedRawEvent, EventKind, MetadataCache } from "@snort/system";
import { getDisplayName } from "Element/ProfileImage";
import { MentionRegex } from "Const";
import { tagFilterOfTextRepost, unwrap } from "SnortUtils";

View File

@ -1,6 +1,6 @@
import { useEffect, useState } from "react";
import { FormattedMessage } from "react-intl";
import { HexKey } from "System";
import { HexKey } from "@snort/system";
import { ApiHost, KieranPubKey, SnortPubKey } from "Const";
import ProfilePreview from "Element/ProfilePreview";

View File

@ -3,7 +3,7 @@ import "./LoginPage.css";
import { CSSProperties, useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { useIntl, FormattedMessage } from "react-intl";
import { HexKey } from "System";
import { HexKey } from "@snort/system";
import { bech32ToHex, unwrap } from "SnortUtils";
import ZapButton from "Element/ZapButton";

View File

@ -1,7 +1,7 @@
import React, { useMemo, useState } from "react";
import { FormattedMessage, useIntl } from "react-intl";
import { useNavigate } from "react-router-dom";
import { HexKey, NostrEvent, NostrPrefix } from "System";
import { HexKey, NostrEvent, NostrPrefix } from "@snort/system";
import UnreadCount from "Element/UnreadCount";
import ProfileImage, { getDisplayName } from "Element/ProfileImage";

View File

@ -1,10 +1,10 @@
import { NostrPrefix } from "System";
import { NostrPrefix, parseNostrLink } from "@snort/system";
import { useEffect, useState } from "react";
import { FormattedMessage } from "react-intl";
import { useNavigate, useParams } from "react-router-dom";
import Spinner from "Icons/Spinner";
import { parseNostrLink, profileLink } from "SnortUtils";
import { profileLink } from "SnortUtils";
import { getNip05PubKey } from "Pages/LoginPage";
export default function NostrLinkHandler() {

View File

@ -2,9 +2,9 @@ import "./ProfilePage.css";
import { useEffect, useState } from "react";
import { useIntl, FormattedMessage } from "react-intl";
import { useNavigate, useParams } from "react-router-dom";
import { encodeTLV, EventKind, HexKey, NostrPrefix } from "System";
import { encodeTLV, EventKind, HexKey, NostrPrefix, parseNostrLink } from "@snort/system";
import { parseNostrLink, getReactions, unwrap } from "SnortUtils";
import { getReactions, unwrap } from "SnortUtils";
import { formatShort } from "Number";
import Note from "Element/Note";
import Bookmarks from "Element/Bookmarks";

View File

@ -1,12 +1,13 @@
import { useEffect, useState } from "react";
import { useIntl, FormattedMessage } from "react-intl";
import { useNavigate } from "react-router-dom";
import { mapEventToProfile } from "@snort/system";
import Logo from "Element/Logo";
import useEventPublisher from "Feed/EventPublisher";
import useLogin from "Hooks/useLogin";
import { useUserProfile } from "Hooks/useUserProfile";
import { mapEventToProfile, UserCache } from "Cache";
import { UserCache } from "Cache";
import AvatarEditor from "Element/AvatarEditor";
import messages from "./messages";

View File

@ -1,6 +1,6 @@
import "./Keys.css";
import { FormattedMessage } from "react-intl";
import { encodeTLV, NostrPrefix } from "System";
import { encodeTLV, NostrPrefix } from "@snort/system";
import Copy from "Element/Copy";
import useLogin from "Hooks/useLogin";

View File

@ -3,13 +3,14 @@ import Nostrich from "nostrich.webp";
import { useEffect, useState } from "react";
import { FormattedMessage } from "react-intl";
import { useNavigate } from "react-router-dom";
import { mapEventToProfile } from "@snort/system";
import useEventPublisher from "Feed/EventPublisher";
import { useUserProfile } from "Hooks/useUserProfile";
import { openFile } from "SnortUtils";
import useFileUpload from "Upload";
import AsyncButton from "Element/AsyncButton";
import { mapEventToProfile, UserCache } from "Cache";
import { UserCache } from "Cache";
import useLogin from "Hooks/useLogin";
import AvatarEditor from "Element/AvatarEditor";
import Icon from "Icons/Icon";

View File

@ -23,7 +23,6 @@ const RelaySettingsPage = () => {
if (publisher) {
const ev = await publisher.contactList(login.follows.item, login.relays.item);
publisher.broadcast(ev);
publisher.broadcastForBootstrap(ev);
try {
const onlineRelays = await fetch("https://api.nostr.watch/v1/online").then(r => r.json());
const relayList = await publisher.relayList(login.relays.item);

View File

@ -1,7 +1,6 @@
import { EventKind } from "System";
import { EventKind, EventPublisher } from "@snort/system";
import { ApiHost } from "Const";
import { SubscriptionType } from "Subscription";
import { EventPublisher } from "System/EventPublisher";
export interface RevenueToday {
donations: number;

View File

@ -1,5 +1,4 @@
import { NostrPrefix } from "System";
import { parseNostrLink, tryParseNostrLink } from ".";
import { NostrPrefix } from "@snort/system";
import { splitByUrl, magnetURIDecode, getRelayName } from ".";
import { describe, expect } from "@jest/globals";
@ -93,47 +92,3 @@ describe("getRelayName", () => {
expect(output).toEqual("relay.example2.com?broadcast=true");
});
});
describe("tryParseNostrLink", () => {
it("is a valid nostr link", () => {
expect(parseNostrLink("nostr:npub10elfcs4fr0l0r8af98jlmgdh9c8tcxjvz9qkw038js35mp4dma8qzvjptg")).toMatchObject({
id: "7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e",
type: NostrPrefix.PublicKey,
});
expect(parseNostrLink("web+nostr:npub10elfcs4fr0l0r8af98jlmgdh9c8tcxjvz9qkw038js35mp4dma8qzvjptg")).toMatchObject({
id: "7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e",
type: NostrPrefix.PublicKey,
});
expect(parseNostrLink("nostr:note15449edq4qa5wzgqvh8td0q0dp6hwtes4pknsrm7eygeenhlj99xsq94wu9")).toMatchObject({
id: "a56a5cb4150768e1200cb9d6d781ed0eaee5e6150da701efd9223399dff2294d",
type: NostrPrefix.Note,
});
expect(
parseNostrLink(
"nostr:nprofile1qqsrhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8gpp4mhxue69uhhytnc9e3k7mgpz4mhxue69uhkg6nzv9ejuumpv34kytnrdaksjlyr9p"
)
).toMatchObject({
id: "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d",
type: NostrPrefix.Profile,
relays: ["wss://r.x.com", "wss://djbas.sadkb.com"],
});
expect(parseNostrLink("nostr:nevent1qqs226juks2sw68pyqxtn4khs8ksath9uc2smfcpalvjyvuemlezjngrd87dq")).toMatchObject({
id: "a56a5cb4150768e1200cb9d6d781ed0eaee5e6150da701efd9223399dff2294d",
type: NostrPrefix.Event,
});
expect(
parseNostrLink(
"nostr:naddr1qqzkjurnw4ksz9thwden5te0wfjkccte9ehx7um5wghx7un8qgs2d90kkcq3nk2jry62dyf50k0h36rhpdtd594my40w9pkal876jxgrqsqqqa28pccpzu"
)
).toMatchObject({
id: "ipsum",
type: NostrPrefix.Address,
relays: ["wss://relay.nostr.org"],
author: "a695f6b60119d9521934a691347d9f78e8770b56da16bb255ee286ddf9fda919",
kind: 30023,
});
});
test.each(["nostr:npub", "web+nostr:npub", "nostr:nevent1xxx"])("should return false for invalid nostr links", lb => {
expect(tryParseNostrLink(lb)).toBeUndefined();
});
});

View File

@ -13,12 +13,9 @@ import {
EventKind,
encodeTLV,
NostrPrefix,
decodeTLV,
TLVEntryType,
NostrEvent,
} from "System";
import { MetadataCache } from "Cache";
import NostrLink from "Element/NostrLink";
MetadataCache,
} from "@snort/system";
export const sha256 = (str: string | Uint8Array): u256 => {
return utils.bytesToHex(hash(str));
@ -506,113 +503,6 @@ export function getUrlHostname(url?: string) {
}
}
export interface NostrLink {
type: NostrPrefix;
id: string;
kind?: number;
author?: string;
relays?: Array<string>;
encode(): string;
}
export function validateNostrLink(link: string): boolean {
try {
const parsedLink = parseNostrLink(link);
if (!parsedLink) {
return false;
}
if (parsedLink.type === NostrPrefix.PublicKey || parsedLink.type === NostrPrefix.Note) {
return parsedLink.id.length === 64;
}
return true;
} catch {
return false;
}
}
export function tryParseNostrLink(link: string, prefixHint?: NostrPrefix): NostrLink | undefined {
try {
return parseNostrLink(link, prefixHint);
} catch {
return undefined;
}
}
export function parseNostrLink(link: string, prefixHint?: NostrPrefix): NostrLink {
const entity = link.startsWith("web+nostr:") || link.startsWith("nostr:") ? link.split(":")[1] : link;
const isPrefix = (prefix: NostrPrefix) => {
return entity.startsWith(prefix);
};
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),
};
} 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),
};
} else if (isPrefix(NostrPrefix.Profile) || isPrefix(NostrPrefix.Event) || isPrefix(NostrPrefix.Address)) {
const decoded = decodeTLV(entity);
const id = decoded.find(a => a.type === TLVEntryType.Special)?.value as string;
const relays = decoded.filter(a => a.type === TLVEntryType.Relay).map(a => a.value as string);
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,
};
} 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,
};
} else if (isPrefix(NostrPrefix.Address)) {
return {
type: NostrPrefix.Address,
id,
relays,
kind,
author,
encode,
};
}
} else if (prefixHint) {
return {
type: prefixHint,
id: link,
encode: () => hexToBech32(prefixHint, link),
};
}
throw new Error("Invalid nostr link");
}
export function sanitizeRelayUrl(url: string) {
try {
return new URL(url).toString();

View File

@ -1,5 +1,5 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { NostrEvent, TaggedRawEvent } from "System";
import { NostrEvent, TaggedRawEvent } from "@snort/system";
interface NoteCreatorStore {
show: boolean;

View File

@ -1,5 +1,5 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { NostrEvent } from "System";
import { NostrEvent } from "@snort/system";
interface ReBroadcastStore {
show: boolean;

View File

@ -1,415 +0,0 @@
import { v4 as uuid } from "uuid";
import { DefaultConnectTimeout } from "./Const";
import { ConnectionStats } from "./ConnectionStats";
import { NostrEvent, ReqCommand, TaggedRawEvent, u256 } from "./Nostr";
import { RelayInfo } from "./RelayInfo";
import { unwrap } from "./Util";
import ExternalStore from "ExternalStore";
export type AuthHandler = (challenge: string, relay: string) => Promise<NostrEvent | undefined>;
/**
* Relay settings
*/
export interface RelaySettings {
read: boolean;
write: boolean;
}
/**
* Snapshot of connection stats
*/
export interface ConnectionStateSnapshot {
connected: boolean;
disconnects: number;
avgLatency: number;
events: {
received: number;
send: number;
};
settings?: RelaySettings;
info?: RelayInfo;
pendingRequests: Array<string>;
activeRequests: Array<string>;
id: string;
ephemeral: boolean;
address: string;
}
export class Connection extends ExternalStore<ConnectionStateSnapshot> {
Id: string;
Address: string;
Socket: WebSocket | null = null;
PendingRaw: Array<object> = [];
PendingRequests: Array<{
cmd: ReqCommand;
cb: () => void;
}> = [];
ActiveRequests = new Set<string>();
Settings: RelaySettings;
Info?: RelayInfo;
ConnectTimeout: number = DefaultConnectTimeout;
Stats: ConnectionStats = new ConnectionStats();
HasStateChange: boolean = true;
IsClosed: boolean;
ReconnectTimer: ReturnType<typeof setTimeout> | null;
EventsCallback: Map<u256, (msg: boolean[]) => void>;
OnConnected?: () => void;
OnEvent?: (sub: string, e: TaggedRawEvent) => void;
OnEose?: (sub: string) => void;
OnDisconnect?: (id: string) => void;
Auth?: AuthHandler;
AwaitingAuth: Map<string, boolean>;
Authed = false;
Ephemeral: boolean;
EphemeralTimeout: ReturnType<typeof setTimeout> | undefined;
Down = true;
constructor(addr: string, options: RelaySettings, auth?: AuthHandler, ephemeral: boolean = false) {
super();
this.Id = uuid();
this.Address = addr;
this.Settings = options;
this.IsClosed = false;
this.ReconnectTimer = null;
this.EventsCallback = new Map();
this.AwaitingAuth = new Map();
this.Auth = auth;
this.Ephemeral = ephemeral;
}
ResetEphemeralTimeout() {
if (this.EphemeralTimeout) {
clearTimeout(this.EphemeralTimeout);
}
if (this.Ephemeral) {
this.EphemeralTimeout = setTimeout(() => {
this.Close();
}, 30_000);
}
}
async Connect() {
try {
if (this.Info === undefined) {
const u = new URL(this.Address);
const rsp = await fetch(`${u.protocol === "wss:" ? "https:" : "http:"}//${u.host}`, {
headers: {
accept: "application/nostr+json",
},
});
if (rsp.ok) {
const data = await rsp.json();
for (const [k, v] of Object.entries(data)) {
if (v === "unset" || v === "" || v === "~") {
data[k] = undefined;
}
}
this.Info = data;
}
}
} catch (e) {
console.warn("Could not load relay information", e);
}
if (this.Socket) {
this.Id = uuid();
this.Socket.onopen = null;
this.Socket.onmessage = null;
this.Socket.onerror = null;
this.Socket.onclose = null;
}
this.IsClosed = false;
this.Socket = new WebSocket(this.Address);
this.Socket.onopen = () => this.OnOpen();
this.Socket.onmessage = e => this.OnMessage(e);
this.Socket.onerror = e => this.OnError(e);
this.Socket.onclose = e => this.OnClose(e);
}
Close() {
this.IsClosed = true;
if (this.ReconnectTimer !== null) {
clearTimeout(this.ReconnectTimer);
this.ReconnectTimer = null;
}
this.Socket?.close();
this.notifyChange();
}
OnOpen() {
this.ConnectTimeout = DefaultConnectTimeout;
console.log(`[${this.Address}] Open!`);
this.Down = false;
if (this.Ephemeral) {
this.ResetEphemeralTimeout();
}
this.OnConnected?.();
this.#sendPendingRaw();
}
OnClose(e: CloseEvent) {
if (!this.IsClosed) {
this.ConnectTimeout = this.ConnectTimeout * 2;
console.log(
`[${this.Address}] Closed (${e.reason}), trying again in ${(this.ConnectTimeout / 1000)
.toFixed(0)
.toLocaleString()} sec`
);
this.ReconnectTimer = setTimeout(() => {
this.Connect();
}, this.ConnectTimeout);
this.Stats.Disconnects++;
} else {
console.log(`[${this.Address}] Closed!`);
this.ReconnectTimer = null;
}
this.OnDisconnect?.(this.Id);
this.#ResetQueues();
// reset connection Id on disconnect, for query-tracking
this.Id = uuid();
this.notifyChange();
}
OnMessage(e: MessageEvent) {
if (e.data.length > 0) {
const msg = JSON.parse(e.data);
const tag = msg[0];
switch (tag) {
case "AUTH": {
this._OnAuthAsync(msg[1])
.then(() => this.#sendPendingRaw())
.catch(console.error);
this.Stats.EventsReceived++;
this.notifyChange();
break;
}
case "EVENT": {
this.OnEvent?.(msg[1], {
...msg[2],
relays: [this.Address],
});
this.Stats.EventsReceived++;
this.notifyChange();
break;
}
case "EOSE": {
this.OnEose?.(msg[1]);
break;
}
case "OK": {
// feedback to broadcast call
console.debug(`${this.Address} OK: `, msg);
const id = msg[1];
if (this.EventsCallback.has(id)) {
const cb = unwrap(this.EventsCallback.get(id));
this.EventsCallback.delete(id);
cb(msg);
}
break;
}
case "NOTICE": {
console.warn(`[${this.Address}] NOTICE: ${msg[1]}`);
break;
}
default: {
console.warn(`Unknown tag: ${tag}`);
break;
}
}
}
}
OnError(e: Event) {
console.error(e);
this.notifyChange();
}
/**
* Send event on this connection
*/
SendEvent(e: NostrEvent) {
if (!this.Settings.write) {
return;
}
const req = ["EVENT", e];
this.#SendJson(req);
this.Stats.EventsSent++;
this.notifyChange();
}
/**
* Send event on this connection and wait for OK response
*/
async SendAsync(e: NostrEvent, timeout = 5000) {
return new Promise<void>(resolve => {
if (!this.Settings.write) {
resolve();
return;
}
const t = setTimeout(() => {
resolve();
}, timeout);
this.EventsCallback.set(e.id, () => {
clearTimeout(t);
resolve();
});
const req = ["EVENT", e];
this.#SendJson(req);
this.Stats.EventsSent++;
this.notifyChange();
});
}
/**
* Using relay document to determine if this relay supports a feature
*/
SupportsNip(n: number) {
return this.Info?.supported_nips?.some(a => a === n) ?? false;
}
/**
* Queue or send command to the relay
* @param cmd The REQ to send to the server
*/
QueueReq(cmd: ReqCommand, cbSent: () => void) {
if (this.ActiveRequests.size >= this.#maxSubscriptions) {
this.PendingRequests.push({
cmd,
cb: cbSent,
});
console.debug("Queuing:", this.Address, cmd);
} else {
this.ActiveRequests.add(cmd[1]);
this.#SendJson(cmd);
cbSent();
}
this.notifyChange();
}
CloseReq(id: string) {
if (this.ActiveRequests.delete(id)) {
this.#SendJson(["CLOSE", id]);
this.OnEose?.(id);
this.#SendQueuedRequests();
}
this.notifyChange();
}
takeSnapshot(): ConnectionStateSnapshot {
return {
connected: this.Socket?.readyState === WebSocket.OPEN,
events: {
received: this.Stats.EventsReceived,
send: this.Stats.EventsSent,
},
avgLatency:
this.Stats.Latency.length > 0
? this.Stats.Latency.reduce((acc, v) => acc + v, 0) / this.Stats.Latency.length
: 0,
disconnects: this.Stats.Disconnects,
info: this.Info,
id: this.Id,
pendingRequests: [...this.PendingRequests.map(a => a.cmd[1])],
activeRequests: [...this.ActiveRequests],
ephemeral: this.Ephemeral,
address: this.Address,
};
}
#SendQueuedRequests() {
const canSend = this.#maxSubscriptions - this.ActiveRequests.size;
if (canSend > 0) {
for (let x = 0; x < canSend; x++) {
const p = this.PendingRequests.shift();
if (p) {
this.ActiveRequests.add(p.cmd[1]);
this.#SendJson(p.cmd);
p.cb();
console.debug("Sent pending REQ", this.Address, p.cmd);
}
}
}
}
#ResetQueues() {
this.ActiveRequests.clear();
this.PendingRequests = [];
this.PendingRaw = [];
this.notifyChange();
}
#SendJson(obj: object) {
const authPending = !this.Authed && (this.AwaitingAuth.size > 0 || this.Info?.limitation?.auth_required === true);
if (this.Socket?.readyState !== WebSocket.OPEN || authPending) {
this.PendingRaw.push(obj);
if (this.Socket?.readyState === WebSocket.CLOSED && this.Ephemeral && this.IsClosed) {
this.Connect();
}
return false;
}
this.#sendPendingRaw();
this.#sendOnWire(obj);
}
#sendPendingRaw() {
while (this.PendingRaw.length > 0) {
const next = this.PendingRaw.shift();
if (next) {
this.#sendOnWire(next);
}
}
}
#sendOnWire(obj: unknown) {
if (this.Socket?.readyState !== WebSocket.OPEN) {
throw new Error(`Socket is not open, state is ${this.Socket?.readyState}`);
}
const json = JSON.stringify(obj);
this.Socket.send(json);
return true;
}
async _OnAuthAsync(challenge: string): Promise<void> {
const authCleanup = () => {
this.AwaitingAuth.delete(challenge);
};
if (!this.Auth) {
throw new Error("Auth hook not registered");
}
this.AwaitingAuth.set(challenge, true);
const authEvent = await this.Auth(challenge, this.Address);
return new Promise(resolve => {
if (!authEvent) {
authCleanup();
return Promise.reject("no event");
}
const t = setTimeout(() => {
authCleanup();
resolve();
}, 10_000);
this.EventsCallback.set(authEvent.id, (msg: boolean[]) => {
clearTimeout(t);
authCleanup();
if (msg.length > 3 && msg[2] === true) {
this.Authed = true;
}
resolve();
});
this.#sendOnWire(["AUTH", authEvent]);
});
}
get #maxSubscriptions() {
return this.Info?.limitation?.max_subscriptions ?? 25;
}
}

View File

@ -1,34 +0,0 @@
/**
* Stats class for tracking metrics per connection
*/
export class ConnectionStats {
/**
* Last n records of how long between REQ->EOSE
*/
Latency: number[] = [];
/**
* Total number of REQ's sent on this connection
*/
Subs: number = 0;
/**
* Count of REQ which took too long and where abandoned
*/
SubsTimeout: number = 0;
/**
* Total number of EVENT messages received
*/
EventsReceived: number = 0;
/**
* Total number of EVENT messages sent
*/
EventsSent: number = 0;
/**
* Total number of times this connection was lost
*/
Disconnects: number = 0;
}

View File

@ -1,4 +0,0 @@
/**
* Websocket re-connect timeout
*/
export const DefaultConnectTimeout = 2000;

View File

@ -1,107 +0,0 @@
import { EventKind, HexKey, NostrPrefix, NostrEvent } from "System";
import { HashtagRegex } from "Const";
import { getPublicKey, parseNostrLink, unixNow } from "SnortUtils";
import { EventExt } from "./EventExt";
export class EventBuilder {
#kind?: EventKind;
#content?: string;
#createdAt?: number;
#pubkey?: string;
#tags: Array<Array<string>> = [];
kind(k: EventKind) {
this.#kind = k;
return this;
}
content(c: string) {
this.#content = c;
return this;
}
createdAt(n: number) {
this.#createdAt = n;
return this;
}
pubKey(k: string) {
this.#pubkey = k;
return this;
}
tag(t: Array<string>): EventBuilder {
const duplicate = this.#tags.some(a => a.length === t.length && a.every((b, i) => b !== a[i]));
if (duplicate) return this;
this.#tags.push(t);
return this;
}
/**
* Extract mentions
*/
processContent() {
if (this.#content) {
this.#content = this.#content.replace(/@n(pub|profile|event|ote|addr|)1[acdefghjklmnpqrstuvwxyz023456789]+/g, m =>
this.#replaceMention(m)
);
const hashTags = [...this.#content.matchAll(HashtagRegex)];
hashTags.map(hashTag => {
this.#addHashtag(hashTag[0]);
});
}
return this;
}
build() {
this.#validate();
const ev = {
id: "",
pubkey: this.#pubkey ?? "",
content: this.#content ?? "",
kind: this.#kind,
created_at: this.#createdAt ?? unixNow(),
tags: this.#tags,
} as NostrEvent;
ev.id = EventExt.createId(ev);
return ev;
}
/**
* Build and sign event
* @param pk Private key to sign event with
*/
async buildAndSign(pk: HexKey) {
const ev = this.pubKey(getPublicKey(pk)).build();
await EventExt.sign(ev, pk);
return ev;
}
#validate() {
if (this.#kind === undefined) {
throw new Error("Kind must be set");
}
if (this.#pubkey === undefined) {
throw new Error("Pubkey must be set");
}
}
#replaceMention(match: string) {
const npub = match.slice(1);
const link = parseNostrLink(npub);
if (link) {
if (link.type === NostrPrefix.Profile || link.type === NostrPrefix.PublicKey) {
this.tag(["p", link.id]);
}
return `nostr:${link.encode()}`;
} else {
return match;
}
}
#addHashtag(match: string) {
const tag = match.slice(1);
this.tag(["t", tag.toLowerCase()]);
}
}

View File

@ -1,166 +0,0 @@
import * as secp from "@noble/curves/secp256k1";
import * as utils from "@noble/curves/abstract/utils";
import { EventKind, HexKey, NostrEvent, Tag } from "System";
import base64 from "@protobufjs/base64";
import { sha256, unixNow } from "SnortUtils";
export interface Thread {
root?: Tag;
replyTo?: Tag;
mentions: Array<Tag>;
pubKeys: Array<HexKey>;
}
export abstract class EventExt {
/**
* Get the pub key of the creator of this event NIP-26
*/
static getRootPubKey(e: NostrEvent): HexKey {
const delegation = e.tags.find(a => a[0] === "delegation");
if (delegation?.[1]) {
return delegation[1];
}
return e.pubkey;
}
/**
* Sign this message with a private key
*/
static async sign(e: NostrEvent, key: HexKey) {
e.id = this.createId(e);
const sig = await secp.schnorr.sign(e.id, key);
e.sig = utils.bytesToHex(sig);
if (!(await secp.schnorr.verify(e.sig, e.id, e.pubkey))) {
throw new Error("Signing failed");
}
}
/**
* Check the signature of this message
* @returns True if valid signature
*/
static async verify(e: NostrEvent) {
const id = this.createId(e);
const result = await secp.schnorr.verify(e.sig, id, e.pubkey);
return result;
}
static createId(e: NostrEvent) {
const payload = [0, e.pubkey, e.created_at, e.kind, e.tags, e.content];
const hash = sha256(JSON.stringify(payload));
if (e.id !== "" && hash !== e.id) {
console.debug(payload);
throw new Error("ID doesnt match!");
}
return hash;
}
/**
* Create a new event for a specific pubkey
*/
static forPubKey(pk: HexKey, kind: EventKind) {
return {
pubkey: pk,
kind: kind,
created_at: unixNow(),
content: "",
tags: [],
id: "",
sig: "",
} as NostrEvent;
}
static extractThread(ev: NostrEvent) {
const isThread = ev.tags.some(a => (a[0] === "e" && a[3] !== "mention") || a[0] == "a");
if (!isThread) {
return undefined;
}
const shouldWriteMarkers = ev.kind === EventKind.TextNote;
const ret = {
mentions: [],
pubKeys: [],
} as Thread;
const eTags = ev.tags.filter(a => a[0] === "e" || a[0] === "a").map((v, i) => new Tag(v, i));
const marked = eTags.some(a => a.Marker !== undefined);
if (!marked) {
ret.root = eTags[0];
ret.root.Marker = shouldWriteMarkers ? "root" : undefined;
if (eTags.length > 1) {
ret.replyTo = eTags[1];
ret.replyTo.Marker = shouldWriteMarkers ? "reply" : undefined;
}
if (eTags.length > 2) {
ret.mentions = eTags.slice(2);
if (shouldWriteMarkers) {
ret.mentions.forEach(a => (a.Marker = "mention"));
}
}
} else {
const root = eTags.find(a => a.Marker === "root");
const reply = eTags.find(a => a.Marker === "reply");
ret.root = root;
ret.replyTo = reply;
ret.mentions = eTags.filter(a => a.Marker === "mention");
}
ret.pubKeys = Array.from(new Set(ev.tags.filter(a => a[0] === "p").map(a => a[1])));
return ret;
}
/**
* Encrypt the given message content
*/
static async encryptData(content: string, pubkey: HexKey, privkey: HexKey) {
const key = await this.#getDmSharedKey(pubkey, privkey);
const iv = window.crypto.getRandomValues(new Uint8Array(16));
const data = new TextEncoder().encode(content);
const result = await window.crypto.subtle.encrypt(
{
name: "AES-CBC",
iv: iv,
},
key,
data
);
const uData = new Uint8Array(result);
return `${base64.encode(uData, 0, result.byteLength)}?iv=${base64.encode(iv, 0, 16)}`;
}
/**
* Decrypt the content of the message
*/
static async decryptData(cyphertext: string, privkey: HexKey, pubkey: HexKey) {
const key = await this.#getDmSharedKey(pubkey, privkey);
const cSplit = cyphertext.split("?iv=");
const data = new Uint8Array(base64.length(cSplit[0]));
base64.decode(cSplit[0], data, 0);
const iv = new Uint8Array(base64.length(cSplit[1]));
base64.decode(cSplit[1], iv, 0);
const result = await window.crypto.subtle.decrypt(
{
name: "AES-CBC",
iv: iv,
},
key,
data
);
return new TextDecoder().decode(result);
}
/**
* Decrypt the content of this message in place
*/
static async decryptDm(content: string, privkey: HexKey, pubkey: HexKey) {
return await this.decryptData(content, privkey, pubkey);
}
static async #getDmSharedKey(pubkey: HexKey, privkey: HexKey) {
const sharedPoint = secp.secp256k1.getSharedSecret(privkey, "02" + pubkey);
const sharedX = sharedPoint.slice(1, 33);
return await window.crypto.subtle.importKey("raw", sharedX, { name: "AES-CBC" }, false, ["encrypt", "decrypt"]);
}
}

View File

@ -1,29 +0,0 @@
enum EventKind {
Unknown = -1,
SetMetadata = 0,
TextNote = 1,
RecommendServer = 2,
ContactList = 3, // NIP-02
DirectMessage = 4, // NIP-04
Deletion = 5, // NIP-09
Repost = 6, // NIP-18
Reaction = 7, // NIP-25
BadgeAward = 8, // NIP-58
SnortSubscriptions = 1000, // NIP-XX
Polls = 6969, // NIP-69
FileHeader = 1063, // NIP-94
Relays = 10002, // NIP-65
Ephemeral = 20_000,
Auth = 22242, // NIP-42
PubkeyLists = 30000, // NIP-51a
NoteLists = 30001, // NIP-51b
TagLists = 30002, // NIP-51c
Badge = 30009, // NIP-58
ProfileBadges = 30008, // NIP-58
ZapstrTrack = 31337,
ZapRequest = 9734, // NIP 57
ZapReceipt = 9735, // NIP 57
HttpAuthentication = 27235, // NIP XX - HTTP Authentication
}
export default EventKind;

View File

@ -1,329 +0,0 @@
import * as secp from "@noble/curves/secp256k1";
import * as utils from "@noble/curves/abstract/utils";
import {
EventKind,
FullRelaySettings,
HexKey,
Lists,
NostrEvent,
RelaySettings,
SystemInterface,
TaggedRawEvent,
u256,
UserMetadata,
} from "System";
import { DefaultRelays } from "Const";
import { unwrap } from "SnortUtils";
import { EventBuilder } from "./EventBuilder";
import { EventExt } from "./EventExt";
import { barrierQueue, processWorkQueue, WorkQueueItem } from "WorkQueue";
const Nip7Queue: Array<WorkQueueItem> = [];
processWorkQueue(Nip7Queue);
export type EventBuilderHook = (ev: EventBuilder) => EventBuilder;
declare global {
interface Window {
nostr?: {
getPublicKey: () => Promise<HexKey>;
signEvent: <T extends NostrEvent>(event: T) => Promise<T>;
getRelays?: () => Promise<Record<string, { read: boolean; write: boolean }>>;
nip04?: {
encrypt?: (pubkey: HexKey, plaintext: string) => Promise<string>;
decrypt?: (pubkey: HexKey, ciphertext: string) => Promise<string>;
};
};
}
}
export class EventPublisher {
#system: SystemInterface;
#pubKey: string;
#privateKey?: string;
constructor(system: SystemInterface, pubKey: string, privKey?: string) {
this.#system = system;
if (privKey) {
this.#privateKey = privKey;
this.#pubKey = utils.bytesToHex(secp.schnorr.getPublicKey(privKey));
} else {
this.#pubKey = pubKey;
}
}
get #hasNip07() {
return "nostr" in window;
}
#eb(k: EventKind) {
const eb = new EventBuilder();
return eb.pubKey(this.#pubKey).kind(k);
}
async #sign(eb: EventBuilder) {
if (this.#hasNip07 && !this.#privateKey) {
const nip7PubKey = await barrierQueue(Nip7Queue, () => unwrap(window.nostr).getPublicKey());
if (nip7PubKey !== this.#pubKey) {
throw new Error("Can't sign event, NIP-07 pubkey does not match");
}
const ev = eb.build();
return await barrierQueue(Nip7Queue, () => unwrap(window.nostr).signEvent(ev));
} else if (this.#privateKey) {
return await eb.buildAndSign(this.#privateKey);
} else {
throw new Error("Can't sign event, no private keys available");
}
}
async nip4Encrypt(content: string, key: HexKey) {
if (this.#hasNip07 && !this.#privateKey) {
const nip7PubKey = await barrierQueue(Nip7Queue, () => unwrap(window.nostr).getPublicKey());
if (nip7PubKey !== this.#pubKey) {
throw new Error("Can't encrypt content, NIP-07 pubkey does not match");
}
return await barrierQueue(Nip7Queue, () =>
unwrap(window.nostr?.nip04?.encrypt).call(window.nostr?.nip04, key, content)
);
} else if (this.#privateKey) {
return await EventExt.encryptData(content, key, this.#privateKey);
} else {
throw new Error("Can't encrypt content, no private keys available");
}
}
async nip4Decrypt(content: string, otherKey: HexKey) {
if (this.#hasNip07 && !this.#privateKey && window.nostr?.nip04?.decrypt) {
return await barrierQueue(Nip7Queue, () =>
unwrap(window.nostr?.nip04?.decrypt).call(window.nostr?.nip04, otherKey, content)
);
} else if (this.#privateKey) {
return await EventExt.decryptDm(content, this.#privateKey, otherKey);
} else {
throw new Error("Can't decrypt content, no private keys available");
}
}
async nip42Auth(challenge: string, relay: string) {
const eb = this.#eb(EventKind.Auth);
eb.tag(["relay", relay]);
eb.tag(["challenge", challenge]);
return await this.#sign(eb);
}
broadcast(ev: NostrEvent) {
console.debug(ev);
this.#system.BroadcastEvent(ev);
}
/**
* Write event to DefaultRelays, this is important for profiles / relay lists to prevent bugs
* If a user removes all the DefaultRelays from their relay list and saves that relay list,
* When they open the site again we wont see that updated relay list and so it will appear to reset back to the previous state
*/
broadcastForBootstrap(ev: NostrEvent) {
for (const [k] of DefaultRelays) {
this.#system.WriteOnceToRelay(k, ev);
}
}
/**
* Write event to all given relays.
*/
broadcastAll(ev: NostrEvent, relays: string[]) {
for (const k of relays) {
this.#system.WriteOnceToRelay(k, ev);
}
}
async muted(keys: HexKey[], priv: HexKey[]) {
const eb = this.#eb(EventKind.PubkeyLists);
eb.tag(["d", Lists.Muted]);
keys.forEach(p => {
eb.tag(["p", p]);
});
if (priv.length > 0) {
const ps = priv.map(p => ["p", p]);
const plaintext = JSON.stringify(ps);
eb.content(await this.nip4Encrypt(plaintext, this.#pubKey));
}
return await this.#sign(eb);
}
async noteList(notes: u256[], list: Lists) {
const eb = this.#eb(EventKind.NoteLists);
eb.tag(["d", list]);
notes.forEach(n => {
eb.tag(["e", n]);
});
return await this.#sign(eb);
}
async tags(tags: string[]) {
const eb = this.#eb(EventKind.TagLists);
eb.tag(["d", Lists.Followed]);
tags.forEach(t => {
eb.tag(["t", t]);
});
return await this.#sign(eb);
}
async metadata(obj: UserMetadata) {
const eb = this.#eb(EventKind.SetMetadata);
eb.content(JSON.stringify(obj));
return await this.#sign(eb);
}
/**
* Create a basic text note
*/
async note(msg: string, fnExtra?: EventBuilderHook) {
const eb = this.#eb(EventKind.TextNote);
eb.content(msg);
eb.processContent();
fnExtra?.(eb);
return await this.#sign(eb);
}
/**
* Create a zap request event for a given target event/profile
* @param amount Millisats amout!
* @param author Author pubkey to tag in the zap
* @param note Note Id to tag in the zap
* @param msg Custom message to be included in the zap
*/
async zap(
amount: number,
author: HexKey,
relays: Array<string>,
note?: HexKey,
msg?: string,
fnExtra?: EventBuilderHook
) {
const eb = this.#eb(EventKind.ZapRequest);
eb.content(msg ?? "");
if (note) {
eb.tag(["e", note]);
}
eb.tag(["p", author]);
eb.tag(["relays", ...relays.map(a => a.trim())]);
eb.tag(["amount", amount.toString()]);
eb.processContent();
fnExtra?.(eb);
return await this.#sign(eb);
}
/**
* Reply to a note
*/
async reply(replyTo: TaggedRawEvent, msg: string, fnExtra?: EventBuilderHook) {
const eb = this.#eb(EventKind.TextNote);
eb.content(msg);
const thread = EventExt.extractThread(replyTo);
if (thread) {
if (thread.root || thread.replyTo) {
eb.tag(["e", thread.root?.Event ?? thread.replyTo?.Event ?? "", "", "root"]);
}
eb.tag(["e", replyTo.id, replyTo.relays?.[0] ?? "", "reply"]);
eb.tag(["p", replyTo.pubkey]);
for (const pk of thread.pubKeys) {
if (pk === this.#pubKey) {
continue;
}
eb.tag(["p", pk]);
}
} else {
eb.tag(["e", replyTo.id, "", "reply"]);
// dont tag self in replies
if (replyTo.pubkey !== this.#pubKey) {
eb.tag(["p", replyTo.pubkey]);
}
}
eb.processContent();
fnExtra?.(eb);
return await this.#sign(eb);
}
async react(evRef: NostrEvent, content = "+") {
const eb = this.#eb(EventKind.Reaction);
eb.content(content);
eb.tag(["e", evRef.id]);
eb.tag(["p", evRef.pubkey]);
return await this.#sign(eb);
}
async relayList(relays: Array<FullRelaySettings> | Record<string, RelaySettings>) {
if (!Array.isArray(relays)) {
relays = Object.entries(relays).map(([k, v]) => ({
url: k,
settings: v,
}));
}
const eb = this.#eb(EventKind.Relays);
for (const rx of relays) {
const rTag = ["r", rx.url];
if (rx.settings.read && !rx.settings.write) {
rTag.push("read");
}
if (rx.settings.write && !rx.settings.read) {
rTag.push("write");
}
eb.tag(rTag);
}
return await this.#sign(eb);
}
async contactList(follows: Array<HexKey>, relays: Record<string, RelaySettings>) {
const eb = this.#eb(EventKind.ContactList);
eb.content(JSON.stringify(relays));
const temp = new Set(follows.filter(a => a.length === 64).map(a => a.toLowerCase()));
temp.forEach(a => eb.tag(["p", a]));
return await this.#sign(eb);
}
/**
* Delete an event (NIP-09)
*/
async delete(id: u256) {
const eb = this.#eb(EventKind.Deletion);
eb.tag(["e", id]);
return await this.#sign(eb);
}
/**
* Repost a note (NIP-18)
*/
async repost(note: NostrEvent) {
const eb = this.#eb(EventKind.Repost);
eb.tag(["e", note.id, ""]);
eb.tag(["p", note.pubkey]);
return await this.#sign(eb);
}
async decryptDm(note: NostrEvent) {
if (note.pubkey !== this.#pubKey && !note.tags.some(a => a[1] === this.#pubKey)) {
throw new Error("Can't decrypt, DM does not belong to this user");
}
const otherPubKey = note.pubkey === this.#pubKey ? unwrap(note.tags.find(a => a[0] === "p")?.[1]) : note.pubkey;
return await this.nip4Decrypt(note.content, otherPubKey);
}
async sendDm(content: string, to: HexKey) {
const eb = this.#eb(EventKind.DirectMessage);
eb.content(await this.nip4Encrypt(content, to));
eb.tag(["p", to]);
return await this.#sign(eb);
}
async generic(fnHook: EventBuilderHook) {
const eb = new EventBuilder();
eb.pubKey(this.#pubKey);
fnHook(eb);
return await this.#sign(eb);
}
}

View File

@ -1,117 +0,0 @@
import { FullRelaySettings, ReqFilter } from "System";
import { unwrap } from "SnortUtils";
import debug from "debug";
const PickNRelays = 2;
export interface RelayTaggedFilter {
relay: string;
filter: ReqFilter;
}
export interface RelayTaggedFilters {
relay: string;
filters: Array<ReqFilter>;
}
export interface RelayCache {
get(pubkey?: string): Array<FullRelaySettings> | undefined;
}
export function splitAllByWriteRelays(cache: RelayCache, filters: Array<ReqFilter>) {
const allSplit = filters
.map(a => splitByWriteRelays(cache, a))
.reduce((acc, v) => {
for (const vn of v) {
const existing = acc.get(vn.relay);
if (existing) {
existing.push(vn.filter);
} else {
acc.set(vn.relay, [vn.filter]);
}
}
return acc;
}, new Map<string, Array<ReqFilter>>());
return [...allSplit.entries()].map(([k, v]) => {
return {
relay: k,
filters: v,
} as RelayTaggedFilters;
});
}
/**
* Split filters by authors
* @param filter
* @returns
*/
export function splitByWriteRelays(cache: RelayCache, filter: ReqFilter): Array<RelayTaggedFilter> {
if ((filter.authors?.length ?? 0) === 0)
return [
{
relay: "",
filter,
},
];
const allRelays = unwrap(filter.authors).map(a => {
return {
key: a,
relays: cache.get(a)?.filter(a => a.settings.write),
};
});
const missing = allRelays.filter(a => a.relays === undefined);
const hasRelays = allRelays.filter(a => a.relays !== undefined);
const relayUserMap = hasRelays.reduce((acc, v) => {
for (const r of unwrap(v.relays)) {
if (!acc.has(r.url)) {
acc.set(r.url, new Set([v.key]));
} else {
unwrap(acc.get(r.url)).add(v.key);
}
}
return acc;
}, new Map<string, Set<string>>());
// selection algo will just pick relays with the most users
const topRelays = [...relayUserMap.entries()].sort(([, v], [, v1]) => v1.size - v.size);
// <relay, key[]> - count keys per relay
// <key, relay[]> - pick n top relays
// <relay, key[]> - map keys per relay (for subscription filter)
const userPickedRelays = unwrap(filter.authors).map(k => {
// pick top 3 relays for this key
const relaysForKey = topRelays
.filter(([, v]) => v.has(k))
.slice(0, PickNRelays)
.map(([k]) => k);
return { k, relaysForKey };
});
const pickedRelays = new Set(userPickedRelays.map(a => a.relaysForKey).flat());
const picked = [...pickedRelays].map(a => {
const keysOnPickedRelay = new Set(userPickedRelays.filter(b => b.relaysForKey.includes(a)).map(b => b.k));
return {
relay: a,
filter: {
...filter,
authors: [...keysOnPickedRelay],
},
} as RelayTaggedFilter;
});
if (missing.length > 0) {
picked.push({
relay: "",
filter: {
...filter,
authors: missing.map(a => a.key),
},
});
}
debug("GOSSIP")("Picked %o", picked);
return picked;
}

View File

@ -1,88 +0,0 @@
import * as utils from "@noble/curves/abstract/utils";
import { bech32 } from "bech32";
import { HexKey } from "./Nostr";
export enum NostrPrefix {
PublicKey = "npub",
PrivateKey = "nsec",
Note = "note",
// TLV prefixes
Profile = "nprofile",
Event = "nevent",
Relay = "nrelay",
Address = "naddr",
}
export enum TLVEntryType {
Special = 0,
Relay = 1,
Author = 2,
Kind = 3,
}
export interface TLVEntry {
type: TLVEntryType;
length: number;
value: string | HexKey | number;
}
export function encodeTLV(prefix: NostrPrefix, id: string, relays?: string[], kind?: number, author?: string) {
const enc = new TextEncoder();
const buf = prefix === NostrPrefix.Address ? enc.encode(id) : utils.hexToBytes(id);
const tl0 = [0, buf.length, ...buf];
const tl1 =
relays
?.map(a => {
const data = enc.encode(a);
return [1, data.length, ...data];
})
.flat() ?? [];
const tl2 = author ? [2, 32, ...utils.hexToBytes(author)] : [];
const tl3 = kind ? [3, 4, ...new Uint8Array(new Uint32Array([kind]).buffer).reverse()] : [];
return bech32.encode(prefix, bech32.toWords([...tl0, ...tl1, ...tl2, ...tl3]), 1_000);
}
export function decodeTLV(str: string) {
const decoded = bech32.decode(str, 1_000);
const data = bech32.fromWords(decoded.words);
const entries: TLVEntry[] = [];
let x = 0;
while (x < data.length) {
const t = data[x];
const l = data[x + 1];
const v = data.slice(x + 2, x + 2 + l);
entries.push({
type: t,
length: l,
value: decodeTLVEntry(t, decoded.prefix, new Uint8Array(v)),
});
x += 2 + l;
}
return entries;
}
function decodeTLVEntry(type: TLVEntryType, prefix: string, data: Uint8Array) {
switch (type) {
case TLVEntryType.Special: {
if (prefix === NostrPrefix.Address) {
return new TextDecoder("ASCII").decode(data);
} else {
return utils.bytesToHex(data);
}
}
case TLVEntryType.Author: {
return utils.bytesToHex(data);
}
case TLVEntryType.Kind: {
return new Uint32Array(new Uint8Array(data.reverse()).buffer)[0];
}
case TLVEntryType.Relay: {
return new TextDecoder("ASCII").decode(data);
}
}
}

View File

@ -1,3 +0,0 @@
export enum Nips {
Search = 50,
}

View File

@ -1,84 +0,0 @@
import { RelaySettings } from "./Connection";
export interface NostrEvent {
id: u256;
pubkey: HexKey;
created_at: number;
kind: number;
tags: Array<Array<string>>;
content: string;
sig: string;
}
export interface TaggedRawEvent extends NostrEvent {
/**
* A list of relays this event was seen on
*/
relays: string[];
}
/**
* Basic raw key as hex
*/
export type HexKey = string;
/**
* Optional HexKey
*/
export type MaybeHexKey = HexKey | undefined;
/**
* A 256bit hex id
*/
export type u256 = string;
export type ReqCommand = [cmd: "REQ", id: string, ...filters: Array<ReqFilter>];
/**
* Raw REQ filter object
*/
export interface ReqFilter {
ids?: u256[];
authors?: u256[];
kinds?: number[];
"#e"?: u256[];
"#p"?: u256[];
"#t"?: string[];
"#d"?: string[];
"#r"?: string[];
search?: string;
since?: number;
until?: number;
limit?: number;
}
/**
* Medatadata event content
*/
export type UserMetadata = {
name?: string;
display_name?: string;
about?: string;
picture?: string;
website?: string;
banner?: string;
nip05?: string;
lud06?: string;
lud16?: string;
};
/**
* NIP-51 list types
*/
export enum Lists {
Muted = "mute",
Pinned = "pin",
Bookmarked = "bookmark",
Followed = "follow",
Badges = "profile_badges",
}
export interface FullRelaySettings {
url: string;
settings: RelaySettings;
}

View File

@ -1,244 +0,0 @@
import debug from "debug";
import { v4 as uuid } from "uuid";
import ExternalStore from "ExternalStore";
import { NostrEvent, ReqFilter, TaggedRawEvent } from "./Nostr";
import { AuthHandler, Connection, RelaySettings, ConnectionStateSnapshot } from "./Connection";
import { Query, QueryBase } from "./Query";
import { RelayCache } from "./GossipModel";
import { NoteStore } from "./NoteCollection";
import { BuiltRawReqFilter, RequestBuilder } from "./RequestBuilder";
import { unwrap, sanitizeRelayUrl, unixNowMs } from "./Util";
import { SystemInterface, SystemSnapshot } from "System";
/**
* Manages nostr content retrieval system
*/
export class NostrSystem extends ExternalStore<SystemSnapshot> implements SystemInterface {
/**
* All currently connected websockets
*/
#sockets = new Map<string, Connection>();
/**
* All active queries
*/
Queries: Map<string, Query> = new Map();
/**
* Handler function for NIP-42
*/
HandleAuth?: AuthHandler;
#log = debug("System");
#relayCache: RelayCache;
constructor(relayCache: RelayCache) {
super();
this.#relayCache = relayCache;
this.#cleanup();
}
get Sockets(): ConnectionStateSnapshot[] {
return [...this.#sockets.values()].map(a => a.snapshot());
}
/**
* Connect to a NOSTR relay if not already connected
*/
async ConnectToRelay(address: string, options: RelaySettings) {
try {
const addr = unwrap(sanitizeRelayUrl(address));
if (!this.#sockets.has(addr)) {
const c = new Connection(addr, options, this.HandleAuth?.bind(this));
this.#sockets.set(addr, c);
c.OnEvent = (s, e) => this.OnEvent(s, e);
c.OnEose = s => this.OnEndOfStoredEvents(c, s);
c.OnDisconnect = id => this.OnRelayDisconnect(id);
await c.Connect();
} else {
// update settings if already connected
unwrap(this.#sockets.get(addr)).Settings = options;
}
} catch (e) {
console.error(e);
}
}
OnRelayDisconnect(id: string) {
for (const [, q] of this.Queries) {
q.connectionLost(id);
}
}
OnEndOfStoredEvents(c: Readonly<Connection>, sub: string) {
for (const [, v] of this.Queries) {
v.eose(sub, c);
}
}
OnEvent(sub: string, ev: TaggedRawEvent) {
for (const [, v] of this.Queries) {
v.onEvent(sub, ev);
}
}
/**
*
* @param address Relay address URL
*/
async ConnectEphemeralRelay(address: string): Promise<Connection | undefined> {
try {
const addr = unwrap(sanitizeRelayUrl(address));
if (!this.#sockets.has(addr)) {
const c = new Connection(addr, { read: true, write: false }, this.HandleAuth?.bind(this), true);
this.#sockets.set(addr, c);
c.OnEvent = (s, e) => this.OnEvent(s, e);
c.OnEose = s => this.OnEndOfStoredEvents(c, s);
c.OnDisconnect = id => this.OnRelayDisconnect(id);
await c.Connect();
return c;
}
} catch (e) {
console.error(e);
}
}
/**
* Disconnect from a relay
*/
DisconnectRelay(address: string) {
const c = this.#sockets.get(address);
if (c) {
this.#sockets.delete(address);
c.Close();
}
}
GetQuery(id: string): Query | undefined {
return this.Queries.get(id);
}
Query<T extends NoteStore>(type: { new (): T }, req: RequestBuilder): Query {
const existing = this.Queries.get(req.id);
if (existing) {
const filters = !req.options?.skipDiff
? req.buildDiff(this.#relayCache, existing.filters)
: req.build(this.#relayCache);
if (filters.length === 0 && !!req.options?.skipDiff) {
return existing;
} else {
for (const subQ of filters) {
this.SendQuery(existing, subQ).then(qta =>
qta.forEach(v => this.#log("New QT from diff %s %s %O from: %O", req.id, v.id, v.filters, existing.filters))
);
}
this.notifyChange();
return existing;
}
} else {
const store = new type();
const filters = req.build(this.#relayCache);
const q = new Query(req.id, store, req.options?.leaveOpen);
this.Queries.set(req.id, q);
for (const subQ of filters) {
this.SendQuery(q, subQ).then(qta =>
qta.forEach(v => this.#log("New QT from diff %s %s %O", req.id, v.id, v.filters))
);
}
this.notifyChange();
return q;
}
}
async SendQuery(q: Query, qSend: BuiltRawReqFilter) {
if (qSend.relay) {
this.#log("Sending query to %s %O", qSend.relay, qSend);
const s = this.#sockets.get(qSend.relay);
if (s) {
const qt = q.sendToRelay(s, qSend);
if (qt) {
return [qt];
}
} else {
const nc = await this.ConnectEphemeralRelay(qSend.relay);
if (nc) {
const qt = q.sendToRelay(nc, qSend);
if (qt) {
return [qt];
}
} else {
console.warn("Failed to connect to new relay for:", qSend.relay, q);
}
}
} else {
const ret = [];
for (const [, s] of this.#sockets) {
if (!s.Ephemeral) {
const qt = q.sendToRelay(s, qSend);
if (qt) {
ret.push(qt);
}
}
}
return ret;
}
return [];
}
/**
* Send events to writable relays
*/
BroadcastEvent(ev: NostrEvent) {
for (const [, s] of this.#sockets) {
s.SendEvent(ev);
}
}
/**
* Write an event to a relay then disconnect
*/
async WriteOnceToRelay(address: string, ev: NostrEvent) {
return new Promise<void>((resolve, reject) => {
const c = new Connection(address, { write: true, read: false }, this.HandleAuth, true);
const t = setTimeout(reject, 5_000);
c.OnConnected = async () => {
clearTimeout(t);
await c.SendAsync(ev);
c.Close();
resolve();
};
c.Connect();
});
}
takeSnapshot(): SystemSnapshot {
return {
queries: [...this.Queries.values()].map(a => {
return {
id: a.id,
filters: a.filters,
subFilters: [],
};
}),
};
}
#cleanup() {
let changed = false;
for (const [k, v] of this.Queries) {
if (v.canRemove()) {
v.sendClose();
this.Queries.delete(k);
this.#log("Deleted query %s", k);
changed = true;
}
}
if (changed) {
this.notifyChange();
}
setTimeout(() => this.#cleanup(), 1_000);
}
}

View File

@ -1,53 +0,0 @@
import { TaggedRawEvent } from "System";
import { describe, expect } from "@jest/globals";
import { FlatNoteStore, ReplaceableNoteStore } from "./NoteCollection";
describe("NoteStore", () => {
describe("flat", () => {
test("one event", () => {
const ev = { id: "one" } as TaggedRawEvent;
const c = new FlatNoteStore();
c.add(ev);
expect(c.getSnapshotData()).toEqual([ev]);
});
test("still one event", () => {
const ev = { id: "one" } as TaggedRawEvent;
const c = new FlatNoteStore();
c.add(ev);
c.add(ev);
expect(c.getSnapshotData()).toEqual([ev]);
});
test("clears", () => {
const ev = { id: "one" } as TaggedRawEvent;
const c = new FlatNoteStore();
c.add(ev);
expect(c.getSnapshotData()).toEqual([ev]);
c.clear();
expect(c.getSnapshotData()).toEqual([]);
});
});
describe("replacable", () => {
test("one event", () => {
const ev = { id: "test", created_at: 69 } as TaggedRawEvent;
const c = new ReplaceableNoteStore();
c.add(ev);
expect(c.getSnapshotData()).toEqual(ev);
});
test("dont replace with older", () => {
const ev = { id: "test", created_at: 69 } as TaggedRawEvent;
const evOlder = { id: "test2", created_at: 68 } as TaggedRawEvent;
const c = new ReplaceableNoteStore();
c.add(ev);
c.add(evOlder);
expect(c.getSnapshotData()).toEqual(ev);
});
test("replace with newer", () => {
const ev = { id: "test", created_at: 69 } as TaggedRawEvent;
const evNewer = { id: "test2", created_at: 70 } as TaggedRawEvent;
const c = new ReplaceableNoteStore();
c.add(ev);
c.add(evNewer);
expect(c.getSnapshotData()).toEqual(evNewer);
});
});
});

Some files were not shown because too many files have changed in this diff Show More