diff --git a/client-web/package.json b/client-web/package.json index ae413d8..870bd79 100644 --- a/client-web/package.json +++ b/client-web/package.json @@ -21,7 +21,7 @@ "framer-motion": "^10.15.0", "idb": "^7.1.1", "mdi-react": "^9.2.0", - "nanoid": "^4.0.2", + "nanoid": "^3.0.0", "react": "^18.2.0", "react-dom": "^18.2.0", "react-router-dom": "^6.15.0", diff --git a/client-web/src/components/bottom-bar.tsx b/client-web/src/components/bottom-bar.tsx index dd941da..708d841 100644 --- a/client-web/src/components/bottom-bar.tsx +++ b/client-web/src/components/bottom-bar.tsx @@ -51,7 +51,7 @@ export function BottomBar() { } else if (event.data[0] === RELAY_MESSAGE_TYPE.EOSE) { // Ignore for now return; - description = `Loaded all requested events for subscription ${event.data[1]}`; + // description = `Loaded all requested events for subscription ${event.data[1]}`; } else if (event.data[0] === RELAY_MESSAGE_TYPE.COUNT) { description = `Relay ${event.data[1]}: ${JSON.stringify( event.data[2] @@ -73,15 +73,7 @@ export function BottomBar() { }, [relayEvents]); return ( - + <> diff --git a/client-web/src/components/create-event-form.tsx b/client-web/src/components/create-event-form.tsx index ce46bce..ec50806 100644 --- a/client-web/src/components/create-event-form.tsx +++ b/client-web/src/components/create-event-form.tsx @@ -224,7 +224,13 @@ export const CreateEventForm = () => { Type: {translateNameToLabel(newEventName)} {users.map((user) => ( - + ))} diff --git a/client-web/src/components/event.tsx b/client-web/src/components/event.tsx index 1cacf70..88a7e6e 100644 --- a/client-web/src/components/event.tsx +++ b/client-web/src/components/event.tsx @@ -34,6 +34,7 @@ import ThumbDownIcon from "mdi-react/ThumbDownIcon"; import RepeatIcon from "mdi-react/RepeatIcon"; import { unixTimeToRelative } from "../lib/relative-time"; import { excerpt } from "../lib/excerpt"; +import { UserIcon } from "./user-icon"; export interface EventProps extends NEventWithUserBase { userComponent?: JSX.Element; @@ -44,6 +45,8 @@ export function Event({ event, reactions, reposts, + mentions, + replies, eventRelayUrls, }: EventProps) { const [isReady] = useNClient((state) => [ @@ -60,17 +63,22 @@ export function Event({ }>({}); useEffect(() => { - setDownVotesCount(reactions?.filter((r) => r.content === "-").length || 0); - setUpVotesCount(reactions?.filter((r) => r.content === "+").length || 0); + setDownVotesCount( + reactions?.filter((r) => r.event.content === "-").length || 0 + ); + setUpVotesCount( + reactions?.filter((r) => r.event.content === "+").length || 0 + ); setRepostsCount(reposts?.length || 0); setReactionsWithCount( reactions - ?.filter((r) => r.content !== "+" && r.content !== "-") + ?.filter((r) => r.event.content !== "+" && r.event.content !== "-") .reduce((acc, r) => { - if (acc[r.content]) { - acc[r.content] += 1; - } else { - acc[r.content] = 1; + const content = r.event?.content ? r.event.content : undefined; + if (content && acc[content]) { + acc[content] += 1; + } else if (content) { + acc[content] = 1; } return acc; }, {} as { [key: string]: number }) || {} @@ -287,56 +295,171 @@ export function Event({ ); - return ( - <> - - - - {images && images?.length > 0 && ( - - {images.map((i, index) => ( - } - fallbackStrategy="onError" - alt="" - onClick={() => openImage(i)} - /> - ))} - - )} - - {userComponent && userComponent} + function makeLinksClickable(text: string) { + if (!text) return ""; + const urlRegex = /(https?:\/\/[^\s]+)/g; + return text.replace( + urlRegex, + (url) => + `${excerpt( + url, + 20 + )}` + ); + } + + const EventCard = ( + + + + {images && images?.length > 0 && ( + + {images.map((i, index) => ( + } + fallbackStrategy="onError" + alt="" + onClick={() => openImage(i)} + /> + ))} + )} + + {userComponent && userComponent} - - - {event.content} - - {unixTimeToRelative(event.created_at)} - - - - - + + + + + + {unixTimeToRelative(event.created_at)} + + + + + - - Relay {eventRelayUrls[0]} + + {eventRelayUrls[0]} - - - - + + + + + ); + + return ( + <> + {EventCard} + + {reactions && ( + <> + Reactions + {reactions.map((r, index) => { + const user = r.user || { pubkey: r.event.pubkey }; + return ( + + + + ); + })} + + )} + {reposts && ( + <> + Reposts + {reposts.map((r) => { + const user = r.user ? r.user : { pubkey: r.event.pubkey }; + return ( + + + + + ); + })} + + )} + { + // TODO: Relay urls + mentions && ( + <> + Mentions + {mentions.map((u) => ( + + + + ))} + + ) + } + + {replies && + replies.map((r) => { + const user = r.user ? r.user : { pubkey: r.event.pubkey }; + return ( + + + } + eventRelayUrls={eventRelayUrls} + /> + + ); + })} {ImageModal} {EventModal} diff --git a/client-web/src/components/events.tsx b/client-web/src/components/events.tsx index 2385b9b..1800314 100644 --- a/client-web/src/components/events.tsx +++ b/client-web/src/components/events.tsx @@ -38,7 +38,7 @@ export function Events(props: { }; return ( - + {events.map((event) => { return ( @@ -47,6 +47,8 @@ export function Events(props: { user={event.user} reactions={event.reactions} reposts={event.reposts} + mentions={event.mentions} + replies={event.replies} eventRelayUrls={event.eventRelayUrls} key={event.event.id} userComponent={ @@ -54,14 +56,19 @@ export function Events(props: { event.user && event.user.pubkey ? ( ) : ( ) ) : undefined diff --git a/client-web/src/components/user-icon.tsx b/client-web/src/components/user-icon.tsx new file mode 100644 index 0000000..88fb7da --- /dev/null +++ b/client-web/src/components/user-icon.tsx @@ -0,0 +1,95 @@ +import { + Avatar, + Box, + Text, + Popover, + PopoverArrow, + PopoverBody, + PopoverCloseButton, + PopoverContent, + PopoverHeader, + PopoverTrigger, +} from "@chakra-ui/react"; +import { UserBase } from "@nostr-ts/common"; +import { useNClient } from "../state/client"; +import { UserInfo } from "./user-info"; +import { UserOptions } from "../lib/user-properties"; + +export function UserIcon({ + user, + options: { + showAbout, + showBanner, + showFollowing, + relayUrls, + title, + reaction, + avatarSize, + }, +}: { + user: UserBase; + options: UserOptions; +}) { + const [following] = useNClient((state) => [ + state.followingUserIds.find((f) => f === user.pubkey), + ]); + + const picture = user.data && user.data.picture ? user.data.picture : ""; + + return ( + <> + + + {reaction ? ( + + + + + {reaction} + + + ) : ( + + )} + + + + + {title} + + + + + + + ); +} diff --git a/client-web/src/components/user-info.tsx b/client-web/src/components/user-info.tsx new file mode 100644 index 0000000..66b1f84 --- /dev/null +++ b/client-web/src/components/user-info.tsx @@ -0,0 +1,69 @@ +import { + Flex, + Avatar, + Heading, + Spacer, + Button, + Box, + Image, + Text, +} from "@chakra-ui/react"; +import { Link } from "react-router-dom"; +import { useNClient } from "../state/client"; +import { UserInfoProps } from "../lib/user-properties"; + +export function UserInfo({ + user: { pubkey, data }, + opts: { showAbout, showBanner, following, showFollowing, relayUrls }, +}: UserInfoProps) { + const name = data && data.name ? data.name : "Anonymous"; + const displayName = + data && data.display_name ? data.display_name : "Anonymous"; + const picture = data && data.picture ? data.picture : ""; + const banner = data && data.banner ? data.banner : undefined; + const about = data && data.about ? data.about : undefined; + + const profileLink = `/p/${pubkey}?relays=${relayUrls.join(",")}`; + // const mentionsLink = `/mentions/${user.pubkey}?relays=${relayUrls.join(",")}`; + + return ( + <> + {showBanner && banner && ( + + banner + + )} + + + + + + + + + {displayName} + {name} + + {showAbout && about && {about}} + + + {showFollowing && ( + + )} + + + ); +} diff --git a/client-web/src/components/user.tsx b/client-web/src/components/user.tsx index 2200a62..0fce4a0 100644 --- a/client-web/src/components/user.tsx +++ b/client-web/src/components/user.tsx @@ -1,81 +1,29 @@ -import { - Flex, - Avatar, - Heading, - Box, - Text, - Button, - Spacer, - Image, -} from "@chakra-ui/react"; import { UserBase } from "@nostr-ts/common"; import { useNClient } from "../state/client"; -import { Link } from "react-router-dom"; +import { UserInfo } from "./user-info"; +import { UserOptions } from "../lib/user-properties"; export function User({ user, - relayUrls, - hideFollow, - showBanner, - showAbout, + options: { showAbout, showBanner, showFollowing, relayUrls }, }: { user: UserBase; - relayUrls: string[]; - hideFollow?: boolean; - showBanner?: boolean; - showAbout?: boolean; + options: UserOptions; }) { const [following] = useNClient((state) => [ state.followingUserIds.find((f) => f === user.pubkey), ]); - const data = user.data ? user.data : null; - const display_name = - data && data.display_name ? data.display_name : "Anonymous"; - const name = data && data.name ? data.name : "Anonymous"; - const picture = data && data.picture ? data.picture : "/no-image.png"; - const banner = data && data.banner ? data.banner : undefined; - const about = data && data.about ? data.about : undefined; - - const profileLink = `/p/${user.pubkey}?relays=${relayUrls.join(",")}`; - // const mentionsLink = `/mentions/${user.pubkey}?relays=${relayUrls.join(",")}`; - return ( - <> - {showBanner && banner && ( - - banner - - )} - - - - - - - {display_name} - {name} - - {showAbout && about && {about}} - - - {!hideFollow && ( - - )} - - + ); } diff --git a/client-web/src/index.css b/client-web/src/index.css index 95ef22f..3f73ada 100644 --- a/client-web/src/index.css +++ b/client-web/src/index.css @@ -26,4 +26,7 @@ aspect-ratio: auto; } } - \ No newline at end of file + + a.is-inline { + text-decoration: underline + } \ No newline at end of file diff --git a/client-web/src/layouts/primary.tsx b/client-web/src/layouts/primary.tsx index ca0a74a..08ff33d 100644 --- a/client-web/src/layouts/primary.tsx +++ b/client-web/src/layouts/primary.tsx @@ -151,7 +151,7 @@ export function PrimaryLayout() { {Sidebar} - + diff --git a/client-web/src/lib/user-properties.ts b/client-web/src/lib/user-properties.ts new file mode 100644 index 0000000..570d5b3 --- /dev/null +++ b/client-web/src/lib/user-properties.ts @@ -0,0 +1,24 @@ +import { UserBase } from "@nostr-ts/common"; + +export interface UserOptions { + showAbout?: boolean; + showBanner?: boolean; + following?: boolean; + showFollowing?: boolean; + relayUrls: string[]; + + /** + * For pop-over + */ + title?: string; + /** + * For reactions + */ + reaction?: string; + avatarSize?: "sm" | "md" | "lg" | "xl" | "2xl" | "2xs" | "xs" | "full"; +} + +export interface UserInfoProps { + user: UserBase; + opts: UserOptions; +} diff --git a/client-web/src/routes/following.tsx b/client-web/src/routes/following.tsx index 79fa33a..fbcd13c 100644 --- a/client-web/src/routes/following.tsx +++ b/client-web/src/routes/following.tsx @@ -36,7 +36,10 @@ export function FollowingRoute() { ))} diff --git a/client-web/src/routes/mentions.tsx b/client-web/src/routes/mentions.tsx index 8945883..76f201c 100644 --- a/client-web/src/routes/mentions.tsx +++ b/client-web/src/routes/mentions.tsx @@ -86,7 +86,14 @@ export function UserMentionsRoute() { Profile - {userRecord && } + {userRecord && ( + + )} {connected ? ( diff --git a/client-web/src/routes/profile.tsx b/client-web/src/routes/profile.tsx index 4232360..6f76149 100644 --- a/client-web/src/routes/profile.tsx +++ b/client-web/src/routes/profile.tsx @@ -30,56 +30,59 @@ export function UserProfileRoute() { kinds: [NEVENT_KIND.SHORT_TEXT_NOTE, NEVENT_KIND.LONG_FORM_CONTENT], }); - /** - * Handle initial load - */ - useEffect(() => { - const init = async () => { - if (!connected) return; + const init = async () => { + if (!connected) return; + // USER + + if (!userRecord || userRecord.user.pubkey !== pubkey) { await useNClient.getState().clearEvents(); await useNClient.getState().setViewSubscription(view, defaultFilters); - // USER - - if (!userRecord) { - const dbUser = await useNClient.getState().getUser(pubkey); - if (dbUser) { - setUserRecord(dbUser); - setrelayUrls(dbUser.relayUrls); - } else { - setUserRecord({ - user: new NUser({ - pubkey, - }), - relayUrls, - }); - - for (const params of searchParams.entries()) { - if (params[0] === "relays") { - setrelayUrls(params[1].split(",")); - } + const dbUser = await useNClient.getState().getUser(pubkey); + if (dbUser) { + setUserRecord(dbUser); + setrelayUrls(dbUser.relayUrls); + } else { + setUserRecord({ + user: new NUser({ + pubkey, + }), + relayUrls, + }); + + for (const params of searchParams.entries()) { + if (params[0] === "relays") { + setrelayUrls(params[1].split(",")); } } } + } - console.log(relayUrls); - - await useNClient.getState().count({ - type: CLIENT_MESSAGE_TYPE.COUNT, - filters: new NFilters({ - kinds: [3], - "#p": [pubkey], - }), - options: { - timeout: 10000, - timeoutAt: Date.now() + 10000, - }, - }); - }; + await useNClient.getState().count({ + type: CLIENT_MESSAGE_TYPE.COUNT, + filters: new NFilters({ + kinds: [3], + "#p": [pubkey], + }), + options: { + timeout: 10000, + timeoutAt: Date.now() + 10000, + }, + }); + }; + + /** + * Handle initial load + */ + useEffect(() => { init(); }, []); + useEffect(() => { + init(); + }, [pubkey]); + /** * Remove subscription when we hit the limit */ @@ -98,13 +101,15 @@ export function UserProfileRoute() { - Profile {userRecord && ( )} diff --git a/client-web/src/routes/welcome.tsx b/client-web/src/routes/welcome.tsx index 5b06e34..d40a0e6 100644 --- a/client-web/src/routes/welcome.tsx +++ b/client-web/src/routes/welcome.tsx @@ -28,6 +28,7 @@ export function WelcomeRoute() { if (!connected || initDone.current) return; initDone.current = true; await useNClient.getState().clearEvents(); + defaultFilters.until = Date.now() - 10000; await useNClient .getState() .setViewSubscription("welcome", defaultFilters); @@ -42,6 +43,7 @@ export function WelcomeRoute() { const init = async () => { if (!connected || initDone.current) return; initDone.current = true; + defaultFilters.until = Math.round(Date.now() / 1000 - 120); await useNClient .getState() .setViewSubscription("welcome", defaultFilters); @@ -65,38 +67,36 @@ export function WelcomeRoute() { return ( - - - {connected ? ( - - ) : ( - - About Nostr - - Tldr: Nostr is a decentralized social network. - - - Nostr is anything you can imagine. A new reddit, Twitter, - Facebook, Mastodon - Craigstslist or Ebay? It's only a matter of - what the interface looks like, the underlying network is the - same, and so is your identity - so you get to access it all, - without giving up yourself. - - Connect to get started - - You don't need an account to browse or follow users. All data is - saved in your browser. To interact with events, generate or - supply a keypair. - - nos2x and nos2x-fox should be working too. - - )} - + + {connected ? ( + + ) : ( + + About Nostr + + Tldr: Nostr is a decentralized social network. + + + Nostr is anything you can imagine. A new reddit, Twitter, + Facebook, Mastodon - Craigstslist or Ebay? It's only a matter of + what the interface looks like, the underlying network is the same, + and so is your identity - so you get to access it all, without + giving up yourself. + + Connect to get started + + You don't need an account to browse or follow users. All data is + saved in your browser. To interact with events, generate or supply + a keypair. + + nos2x and nos2x-fox should be working too. + + )} diff --git a/client-web/src/state/client.ts b/client-web/src/state/client.ts index c06c061..7bf560f 100644 --- a/client-web/src/state/client.ts +++ b/client-web/src/state/client.ts @@ -553,13 +553,18 @@ export const useNClient = create((set, get) => ({ }, }); - setTimeout(async () => { + const process = async () => { const eventUserPubkeys: { pubkey: string; relayUrls: string[]; }[] = []; - const eventIds: { + // const eventIds: { + // id: string; + // relayUrls: string[]; + // }[] = []; + + const relEventIds: { id: string; relayUrls: string[]; }[] = []; @@ -572,8 +577,21 @@ export const useNClient = create((set, get) => ({ relayUrls: ev.eventRelayUrls, }); } + // if (!ev.inResponseTo) { + // const tags = eventHasEventTags(ev.event); + // if (tags) { + // for (const tag of tags) { + // if (tag.marker === "root") { + // eventIds.push({ + // id: tag.eventId, + // relayUrls: tag.relayUrl ? [tag.relayUrl] : ev.eventRelayUrls, + // }); + // } + // } + // } + // } if (!ev.reactions) { - eventIds.push({ + relEventIds.push({ id: ev.event.id, relayUrls: ev.eventRelayUrls, }); @@ -601,30 +619,51 @@ export const useNClient = create((set, get) => ({ }; }); + // const relayUrlToEventIdsMap: Record> = {}; + + // for (const ev of eventIds) { + // for (const relayUrl of ev.relayUrls) { + // if (!relayUrlToEventIdsMap[relayUrl]) { + // relayUrlToEventIdsMap[relayUrl] = new Set(); + // } + // relayUrlToEventIdsMap[relayUrl].add(ev.id); + // } + // } + + // const reqEvents: RelaysWithIdsOrKeys[] = Object.entries( + // relayUrlToEventIdsMap + // ).map(([relayUrl, eventIdsSet]) => { + // return { + // source: "events", + // relayUrl, + // idsOrKeys: [...eventIdsSet], + // }; + // }); + // This map will keep track of relayUrls and their associated eventIds. - const relayUrlToEventIdsMap: Record> = {}; + const relayUrlToRelEventIdsMap: Record> = {}; - for (const ev of eventIds) { + for (const ev of relEventIds) { for (const relayUrl of ev.relayUrls) { - if (!relayUrlToEventIdsMap[relayUrl]) { - relayUrlToEventIdsMap[relayUrl] = new Set(); + if (!relayUrlToRelEventIdsMap[relayUrl]) { + relayUrlToRelEventIdsMap[relayUrl] = new Set(); } - relayUrlToEventIdsMap[relayUrl].add(ev.id); + relayUrlToRelEventIdsMap[relayUrl].add(ev.id); } } - const reqEvents: RelaysWithIdsOrKeys[] = Object.entries( - relayUrlToEventIdsMap + const reqRelEvents: RelaysWithIdsOrKeys[] = Object.entries( + relayUrlToRelEventIdsMap ).map(([relayUrl, eventIdsSet]) => { return { - source: "events", + source: "events:related", relayUrl, idsOrKeys: [...eventIdsSet], }; }); const infoRequestPromises = []; - for (const item of [...reqUsers, ...reqEvents]) { + for (const item of [...reqUsers, ...reqRelEvents]) { infoRequestPromises.push( await get().requestInformation(item, { timeout: 10000, @@ -633,7 +672,11 @@ export const useNClient = create((set, get) => ({ }) ); } - }, 4000); + }; + + // TODO: This is not accurate + setTimeout(process, 3000); + setTimeout(process, 8000); }, /** diff --git a/client-web/src/state/worker.ts b/client-web/src/state/worker.ts index bd84999..354073b 100644 --- a/client-web/src/state/worker.ts +++ b/client-web/src/state/worker.ts @@ -349,33 +349,111 @@ class WorkerClass implements NClientWorker { // Check if event already exists const exists = event.id ? this.eventsMap.has(event.id) : false; - if (!exists) { - if (event.pubkey) { - const newEvent: NEventWithUserBase = { - event: new NEvent(event), - eventRelayUrls: [payload.meta.url as string], - }; - - const data = await this.getUser(event.pubkey); - if (data) { - newEvent.user = data.user; + if (exists) { + return; + } + + if (!event.pubkey) { + return; + } + + const newEvent: NEventWithUserBase = { + event: new NEvent(event), + eventRelayUrls: [payload.meta.url as string], + }; + + const data = await this.getUser(event.pubkey); + if (data) { + newEvent.user = data.user; + } + + const mentions = newEvent.event.hasMentions(); + if (mentions) { + for (const mention of mentions) { + const user = await this.getUser(mention); + if (user) { + if (newEvent.mentions) { + newEvent.mentions.push(user.user); + } else { + newEvent.mentions = [user.user]; + } + } else { + if (newEvent.mentions) { + newEvent.mentions.push(new NUserBase({ pubkey: mention })); + } else { + newEvent.mentions = [new NUserBase({ pubkey: mention })]; + } } + } + } - const mentions = newEvent.event.hasMentions(); - if (mentions) { - for (const mention of mentions) { - const user = await this.getUser(mention); - if (user) { - if (newEvent.mentions) { - newEvent.mentions.push(user.user); - } else { - newEvent.mentions = [user.user]; - } + // Check if event is a response to another event + const eventTags = newEvent.event.hasEventTags(); + if (eventTags) { + const rootTag = eventTags.find((tag) => tag.marker === "root"); + // let replyTag = eventTags.find((tag) => tag.marker === "reply"); + + if (rootTag) { + const eventId = rootTag.eventId; + + const origEvent = this.eventsMap.get(eventId); + if (origEvent) { + if (origEvent.replies) { + const exist = origEvent.replies.find( + (reply) => reply.event.id === newEvent.event.id + ); + if (!exist) { + origEvent.replies.push({ + ...newEvent, + }); } + } else { + origEvent.replies = [ + { + ...newEvent, + }, + ]; } + this.updateEvent(origEvent); + console.log(`Reply event added to event ${origEvent.event.id}`); + return; } + } else { + console.log(`No root tag found for event ${event.id}`); + } + } + + this.addEvent(newEvent); + }); + } else if (kind === NEVENT_KIND.ZAP_RECEIPT) { + incomingQueue.enqueueBackground(async () => { + const event = payload.data[2] as EventBase; + + if (!event.pubkey) { + return; + } - this.addEvent(newEvent); + const ev = new NEvent(event); + const user = await this.getUser(ev.pubkey); + const tags = ev.hasEventTags(); + const hasRootTag = tags?.find((tag) => tag.marker === "root"); + if (hasRootTag) { + const rootEvent = this.eventsMap.get(hasRootTag.eventId); + if (rootEvent) { + if (rootEvent.lightningReceipts) { + rootEvent.lightningReceipts.push({ + event: ev, + user: user?.user, + }); + } else { + rootEvent.lightningReceipts = [ + { + event: ev, + user: user?.user, + }, + ]; + } + this.updateEvent(rootEvent); } } }); @@ -424,6 +502,7 @@ class WorkerClass implements NClientWorker { // TODO: Support users return; } + const data = await this.getUser(ev.pubkey); const eventIds = inResponse .filter((tag) => tag.eventId) @@ -433,9 +512,17 @@ class WorkerClass implements NClientWorker { const event = this.eventsMap.get(id); if (event) { if (event.reactions && event.reactions.length) { - event.reactions.push(ev); + event.reactions.push({ + event: ev, + user: data?.user, + }); } else { - event.reactions = [ev]; + event.reactions = [ + { + event: ev, + user: data?.user, + }, + ]; } console.log(`Reaction event added to event ${event.event.id}`); this.updateEvent(event); @@ -452,6 +539,7 @@ class WorkerClass implements NClientWorker { // TODO: Support users return; } + const data = await this.getUser(ev.pubkey); const eventIds = inResponse .filter((tag) => tag.eventId) @@ -461,9 +549,17 @@ class WorkerClass implements NClientWorker { const event = this.eventsMap.get(id); if (event) { if (event.reposts) { - event.reposts.push(ev); + event.reposts.push({ + event: ev, + user: data?.user, + }); } else { - event.reposts = [ev]; + event.reposts = [ + { + event: ev, + user: data?.user, + }, + ]; } console.log(`Repost event added to event ${event.event.id}`); this.updateEvent(event); @@ -642,7 +738,7 @@ class WorkerClass implements NClientWorker { const timeout = options?.timeout || 10000; let filtered: string[] = []; - if (payload.source === "events") { + if (payload.source === "events" || payload.source === "events:related") { filtered = payload.idsOrKeys.filter( (id) => !this.checkedEvents.includes(id) ); @@ -670,10 +766,26 @@ class WorkerClass implements NClientWorker { for (let i = 0; i < filtered.length; i += 25) { const keys = filtered.slice(i, i + 25); let filters: NFilters; + if (payload.source === "events") { filters = new NFilters({ - kinds: [NEVENT_KIND.REACTION, NEVENT_KIND.REPOST], - "#e": filtered, + kinds: [ + NEVENT_KIND.SHORT_TEXT_NOTE, + NEVENT_KIND.LONG_FORM_CONTENT, + NEVENT_KIND.REACTION, + NEVENT_KIND.REPOST, + NEVENT_KIND.ZAP_RECEIPT, + ], + ids: keys, + }); + } else if (payload.source === "events:related") { + filters = new NFilters({ + kinds: [ + NEVENT_KIND.SHORT_TEXT_NOTE, + NEVENT_KIND.REACTION, + NEVENT_KIND.REPOST, + ], + "#e": keys, }); } else if (payload.source === "users") { filters = new NFilters({ diff --git a/packages/common/package.json b/packages/common/package.json index 95e483b..f53a956 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -26,11 +26,12 @@ "@noble/hashes": "^1.3.1", "bech32": "^2.0.0", "light-bolt11-decoder": "^3.0.0", - "nanoid": "^4.0.2" + "nanoid": "^3.0.0" }, "devDependencies": { "@anatine/esbuild-decorators": "^0.2.19", "@types/jest": "^29.5.3", + "crypto-browserify": "^3.12.0", "esbuild": "^0.17.18", "esbuild-node-externals": "^1.7.0", "jest": "^29.6.2", diff --git a/packages/common/src/classes/event.test.ts b/packages/common/src/classes/event.test.ts index 15c3dbc..0b89fe6 100644 --- a/packages/common/src/classes/event.test.ts +++ b/packages/common/src/classes/event.test.ts @@ -1,21 +1,14 @@ import { NEvent, - NREPORT_KIND, NewLongFormContent, NewReaction, NewShortTextNote, NewShortTextNoteResponse, - Report, - createEventContent, eventHasExpiration, eventHasLabels, eventHasNonce, eventHasRelayRecommendation, - eventHasReport, - extractEventContent, generateClientKeys, - generateReportTags, - isValidEventContent, } from ".."; const keypair_one = generateClientKeys(); @@ -65,7 +58,7 @@ test("NewShortTextNoteResponse", () => { const eventTags = evR.tags?.filter((tag) => tag[0] === "e"); expect(eventTags?.length).toEqual(1); - expect(eventTags?.[0]).toEqual(["e", ev.id]); + expect(eventTags?.[0]).toEqual(["e", ev.id, "", "root"]); const publicKeyTags = evR.tags?.filter((tag) => tag[0] === "p"); expect(publicKeyTags?.length).toEqual(1); @@ -135,78 +128,23 @@ test("eventHasContentWarning type 2", () => { expect(hasContentWarning).toEqual("reason"); }); -test("extractEventContent: relay", () => { - const content = "wss://relay.example.com"; - const res = extractEventContent(content); - expect(res).toEqual({ - type: "relayUrl", - relayUrl: "wss://relay.example.com", - }); -}); - -test("extractEventContent: nostr", () => { - const content = - "Profile is impersonating nostr:2234567890123456789012345678901234567890123456789012345678901234"; - const res = extractEventContent(content); - expect(res).toEqual({ - message: "Profile is impersonating", - type: "nostr", - publicKeys: [ - "2234567890123456789012345678901234567890123456789012345678901234", - ], - }); -}); - -test("extractEventContent: nostr x2", () => { - const content = - "Checkout these guys nostr:2234567890123456789012345678901234567890123456789012345678901234 nostr:2334567890123456789012345678901234567890123456789012345678901234 later"; - const res = extractEventContent(content); - expect(res).toEqual({ - message: "Checkout these guys later", - type: "nostr", - publicKeys: [ - "2234567890123456789012345678901234567890123456789012345678901234", - "2334567890123456789012345678901234567890123456789012345678901234", - ], - }); -}); - -test("extractEventContent: nothing", () => { - const content = "Here's whats on nostr: cool stuff"; - const res = extractEventContent(content); - expect(res).toEqual(undefined); -}); - -test("createEventContent", () => { - const content = "Here's whats on nostr: cool stuff"; - const res = createEventContent({ - message: content, - }); - expect(res).toEqual("Here's whats on nostr: cool stuff"); -}); - -test("createEventContent: relay", () => { - const content = "wss://relay.example.com"; - const res = createEventContent({ - type: "relayUrl", - relayUrl: "wss://relay.example.com", - }); - expect(res).toEqual("wss://relay.example.com"); -}); - -test("createEventContent: nostr", () => { - const content = "Profile is impersonating"; - const res = createEventContent({ - message: "Profile is impersonating", - type: "nostr", - publicKeys: [ - "2234567890123456789012345678901234567890123456789012345678901234", - ], - }); - expect(res).toEqual( - "Profile is impersonating nostr:2234567890123456789012345678901234567890123456789012345678901234" - ); -}); +// test("createEventContent: nostr", () => { +// const content = "Profile is impersonating"; +// const res = createEventContent({ +// message: "Profile is impersonating", +// nurls: [ +// { +// type: "npub", +// publicKeys: [ +// "2234567890123456789012345678901234567890123456789012345678901234", +// ], +// }, +// ], +// }); +// expect(res).toEqual( +// "Profile is impersonating nostr:2234567890123456789012345678901234567890123456789012345678901234" +// ); +// }); test("eventHasExpiration", () => { const ev = new NEvent({ @@ -272,75 +210,3 @@ test("eventHasNonce", () => { expect(hasNonce).toEqual([64, 2]); expect(hasNonce).toEqual(ev.hasNonceTag()); }); - -test("eventHasReport", () => { - const ev = new NEvent({ - kind: 1984, - content: "Broke local law", - tags: [ - [ - "e", - "1234567890123456789012345678901234567890123456789012345678901234", - "illegal", - ], - ["p", "1234567890123456789012345678901234567890123456789012345678901234"], - ], - }); - const hasReport = eventHasReport(ev); - expect(hasReport).toEqual({ - eventId: "1234567890123456789012345678901234567890123456789012345678901234", - kind: "illegal", - publicKey: - "1234567890123456789012345678901234567890123456789012345678901234", - content: "Broke local law", - }); - expect(hasReport).toEqual(ev.hasReportTags()); -}); - -test("eventHasReport: impersonation", () => { - const ev = new NEvent({ - kind: 1984, - content: - "Profile is impersonating nostr:2234567890123456789012345678901234567890123456789012345678901234", - tags: [ - [ - "p", - "1234567890123456789012345678901234567890123456789012345678901234", - "impersonation", - ], - ], - }); - const hasReport = eventHasReport(ev); - expect(hasReport).toEqual({ - kind: "impersonation", - publicKey: - "1234567890123456789012345678901234567890123456789012345678901234", - content: - "Profile is impersonating nostr:2234567890123456789012345678901234567890123456789012345678901234", - }); - expect(hasReport).toEqual(ev.hasReportTags()); -}); - -test("generateReportTags: impersonation", () => { - const report: Report = { - kind: NREPORT_KIND.IMPERSONATION, - publicKey: - "1234567890123456789012345678901234567890123456789012345678901234", - }; - const tags = generateReportTags(report); - expect(tags).toEqual([["p", report.publicKey, report.kind]]); -}); - -test("generateReportTags: event", () => { - const report: Report = { - kind: NREPORT_KIND.ILLEGAL, - publicKey: - "1234567890123456789012345678901234567890123456789012345678901234", - eventId: "1234567890123456789012345678901234567890123456789012345678901234", - }; - const tags = generateReportTags(report); - expect(tags).toEqual([ - ["e", report.eventId, report.kind], - ["p", report.publicKey], - ]); -}); diff --git a/packages/common/src/classes/event.ts b/packages/common/src/classes/event.ts index 3e4a94e..f8c116a 100644 --- a/packages/common/src/classes/event.ts +++ b/packages/common/src/classes/event.ts @@ -17,6 +17,7 @@ import { iNewZAPReceipt, iNewEventDeletion, EventCoordinatesTag, + EventEventTag, } from "../types"; import { hash, @@ -49,6 +50,11 @@ import { eventHasAmountTags, makeEventCoordinatesTag, countLeadingZeroes, + eventAddNonceTag, + eventReplaceNonceTag, + eventHasEventTags, + eventHasPositionalEventTag, + eventHasPositionalEventTags, } from "../utils"; import { eventHasExternalIdentityClaim, @@ -149,7 +155,12 @@ export class NEvent implements EventBase { if (!extracted) { this.content = createEventContent({ message: this.content, - publicKeys, + nurls: [ + { + type: "npub", + publicKeys, + }, + ], }); } else { throw new Error("Already has motified content"); @@ -162,14 +173,11 @@ export class NEvent implements EventBase { */ public hasMentions(): string[] | undefined { const extracted = this.extractContent(); - if ( - !extracted || - !extracted.publicKeys || - extracted.publicKeys.length < 1 - ) { - return; + if (!extracted) { + return undefined; } - return extracted.publicKeys; + const publicKeys = extracted?.nurls.filter((n) => n.type === "npub"); + return publicKeys.length > 0 ? publicKeys[0].publicKeys : undefined; } /** @@ -208,34 +216,24 @@ export class NEvent implements EventBase { * Standard tag * https://github.com/nostr-protocol/nips/blob/master/README.md#standardized-tags */ - public addEventTag(eventId: string, relayUrl?: string) { - const tag = ["e", eventId]; - if (relayUrl) { - tag[1] = `${tag[1]}, ${relayUrl}`; + public addEventTag(data: EventEventTag) { + const relayUrl = data.relayUrl ? data.relayUrl : ""; + let tag = ["e", data.eventId]; + if (data.marker) { + tag = [...tag, relayUrl, data.marker]; + } else if (data.relayUrl) { + tag = [...tag, data.relayUrl]; } this.addTag(tag); } - public hasEventTags(): - | { - eventId: string; - relayUrl?: string; - }[] - | undefined { - const tags = this.tags.filter((tag) => tag[0] === "e"); - if (tags.length === 0) { - return; + public hasEventTags(): EventEventTag[] | undefined { + const hasPositional = eventHasPositionalEventTag(this); + if (hasPositional) { + return eventHasPositionalEventTags(this); + } else { + return eventHasEventTags(this); } - - return tags.map((tag) => { - const parts = tag[1].split(","); - const eventId = parts[0].trim(); - const relayUrl = parts[1] ? parts[1].trim() : undefined; - return { - eventId, - relayUrl, - }; - }); } /** @@ -383,18 +381,8 @@ export class NEvent implements EventBase { * https://github.com/nostr-protocol/nips/blob/master/13.md */ public addNonceTag(nonce: number[]) { - if (this.hasNonceTag()) { - throw new Error("Event already has a nonce."); - } - if (nonce.length !== 2) { - throw new Error( - "Nonce must be an array of 2 numbers: [miningResult, difficulty]" - ); - } - const miningResult = nonce[0].toString(); - const difficulty = nonce[1].toString(); - - this.addTag(["nonce", miningResult, difficulty]); + const update = eventAddNonceTag(this, nonce); + this.tags = update.tags; } /** @@ -409,11 +397,8 @@ export class NEvent implements EventBase { * @param nonce */ public replaceNonceTag(nonce: number[]) { - const hasTag = this.hasNonceTag(); - if (hasTag) { - this.tags = this.tags.filter((tag) => tag[0] !== "nonce"); - } - this.addNonceTag(nonce); + const update = eventReplaceNonceTag(this, nonce); + this.tags = update.tags; } /** @@ -585,7 +570,9 @@ export function NewLongFormContent(opts: iNewLongFormContent) { : NEVENT_KIND.LONG_FORM_CONTENT, }); if (opts.identifier) { - newEvent.addEventTag(opts.identifier); + newEvent.addEventTag({ + eventId: opts.identifier, + }); } return newEvent; } @@ -621,9 +608,46 @@ export function NewShortTextNoteResponse( console.log("Event you are responding to does not have a subject."); } - // Append tags - newEvent.addEventTag(inResponseToEvent.id, opts.relayUrl); - newEvent.addPublicKeyTag(inResponseToEvent.pubkey, opts.relayUrl); + /** + * Event tags + */ + const origEventTags = inResponseToEvent.hasEventTags(); + const rootTag = origEventTags + ? origEventTags.find((t) => t.marker === "root") + : undefined; + if (rootTag) { + // If the event we're responding to is a response itself, it should have a root tag + newEvent.addEventTag({ + eventId: rootTag.eventId, + relayUrl: opts.relayUrl, + marker: "root", + }); + newEvent.addEventTag({ + eventId: opts.inResponseTo.id, + relayUrl: opts.relayUrl, + marker: "reply", + }); + } else { + // If there's no root tag, this is a response, to the root event + newEvent.addEventTag({ + eventId: opts.inResponseTo.id, + relayUrl: opts.relayUrl, + marker: "root", + }); + } + + /** + * Public key tags + */ + const origPublicKeyTags = inResponseToEvent.hasPublicKeyTags(); + const pubKeyTags = []; + if (origPublicKeyTags) { + pubKeyTags.push(origPublicKeyTags); + } + pubKeyTags.push([inResponseToEvent.pubkey, opts.relayUrl]); + for (const tag of pubKeyTags) { + newEvent.addPublicKeyTag(tag[0], tag[1]); + } return newEvent; } @@ -649,7 +673,10 @@ export function NewReaction(opts: iNewReaction) { tags: [], }); - nEv.addEventTag(opts.inResponseTo.id, opts.relayUrl); + nEv.addEventTag({ + eventId: opts.inResponseTo.id, + relayUrl: opts.relayUrl, + }); nEv.addPublicKeyTag(opts.inResponseTo.pubkey, opts.relayUrl); return nEv; @@ -675,7 +702,10 @@ export function NewQuoteRepost(opts: iNewQuoteRepost) { kind: NEVENT_KIND.REPOST, }); - nEv.addEventTag(opts.inResponseTo.id, opts.relayUrl); + nEv.addEventTag({ + eventId: opts.inResponseTo.id, + relayUrl: opts.relayUrl, + }); nEv.addPublicKeyTag(opts.inResponseTo.pubkey, opts.relayUrl); return nEv; @@ -696,7 +726,10 @@ export function NewGenericRepost(opts: iNewGenericRepost) { kind: NEVENT_KIND.GENERIC_REPOST, }); - nEv.addEventTag(opts.inResponseTo.id, opts.relayUrl); + nEv.addEventTag({ + eventId: opts.inResponseTo.id, + relayUrl: opts.relayUrl, + }); nEv.addPublicKeyTag(opts.inResponseTo.pubkey, opts.relayUrl); nEv.addKindTag(opts.inResponseTo.kind); @@ -791,7 +824,9 @@ export function NewZapRequest(opts: iNewZAPRequest) { nEv.addPublicKeyTag(opts.recipientPubkey); if (opts.eventId) { - nEv.addEventTag(opts.eventId); + nEv.addEventTag({ + eventId: opts.eventId, + }); } return nEv; } @@ -857,7 +892,9 @@ export function NewZapReceipt(opts: iNewZAPReceipt) { }); if (eTag) { - nEv.addEventTag(eTag[1]); + nEv.addEventTag({ + eventId: eTag[1], + }); } if (opts.preimage) { @@ -893,7 +930,9 @@ export function NewEventDeletion(opts: iNewEventDeletion) { } } else { for (const ev of opts.events) { - nEv.addEventTag(ev.id); + nEv.addEventTag({ + eventId: ev.id, + }); } } diff --git a/packages/common/src/types/content.ts b/packages/common/src/types/content.ts index 3ddf782..27ebf7f 100644 --- a/packages/common/src/types/content.ts +++ b/packages/common/src/types/content.ts @@ -5,7 +5,10 @@ */ export interface NEventContent { message?: string; - type?: "nostr" | "relayUrl"; - publicKeys?: string[]; relayUrl?: string; + nurls?: { + type?: "npub" | "nsec" | "note" | "lnurl" | "nprofile" | "nevent"; + // TODO: Not really accurate + publicKeys?: string[]; + }[]; } diff --git a/packages/common/src/types/event-with-user.ts b/packages/common/src/types/event-with-user.ts index ce1183a..3aa5aac 100644 --- a/packages/common/src/types/event-with-user.ts +++ b/packages/common/src/types/event-with-user.ts @@ -1,22 +1,34 @@ -import { NEvent } from "../classes"; -import { EventBase } from "./event"; +import { NEvent } from "../classes/event"; +import { NUserBase } from "../classes/user"; import { UserBase } from "./user"; +export interface EventWithUser { + user?: UserBase; + event: NEvent; +} + export interface EventBaseWithUserBase { user?: UserBase; - event: EventBase; - eventRelayIds: string[]; + event: NEvent; + eventRelayUrls: string[]; // 7 - reactions?: NEvent[]; - reactionsCount?: number; + reactions?: EventWithUser[]; // 6 - reposts?: NEvent[]; - repostsCount?: number; + reposts?: EventWithUser[]; // 8 badgeAwards?: NEvent[]; - badgeAwardsCount?: number; - mentions?: UserBase[]; + // Mentions and replies + mentions?: NUserBase[]; + + // Mentions and replies + replies?: EventWithUser[]; + + // TODO: Implement + // In Response to + // inResponseTo?: EventWithUser[]; + + lightningReceipts?: EventWithUser[]; } export interface NEventWithUserBase extends EventBaseWithUserBase { @@ -25,18 +37,18 @@ export interface NEventWithUserBase extends EventBaseWithUserBase { } export interface idOrKey { - source: "events" | "users"; + source: "events" | "events:related" | "users"; idOrKey: string; } -export interface idOrKeyWithRelayIds { - source: "events" | "users"; +export interface idOrKeyWithrelayUrls { + source: "events" | "events:related" | "users"; idOrKey: string; - relayIds: string[]; + relayUrls: string[]; } export interface RelaysWithIdsOrKeys { - source: "events" | "users"; - relayId: string; + source: "events" | "events:related" | "users"; + relayUrl: string; idsOrKeys: string[]; } diff --git a/packages/common/src/types/event.ts b/packages/common/src/types/event.ts index 83a89cd..c385433 100644 --- a/packages/common/src/types/event.ts +++ b/packages/common/src/types/event.ts @@ -1,4 +1,4 @@ -import { ExternalIdentityClaim } from "src/classes"; +import { ExternalIdentityClaim } from "../classes/identity-claim"; import { NEVENT_KIND } from "./event-kind"; import { UserMetadata } from "./user-metadata"; import { Report } from "./report"; @@ -16,19 +16,24 @@ export interface EventBase { sig?: string; } -export interface iNewShortTextNote { +export interface iNewBase { + pow?: string; + relayUrls?: string[]; +} + +export interface iNewShortTextNote extends iNewBase { text: string; subject?: string; } -export interface iNewLongFormContent { +export interface iNewLongFormContent extends iNewBase { text: string; isDraft?: boolean; // d identifier to make it replaceable identifier?: string; } -interface inResponse { +interface inResponse extends iNewBase { /** * The event that this is in response to */ @@ -125,7 +130,7 @@ export interface iNewZAPReceipt { zapRequest?: EventBase; } -export interface iNewEventDeletion { +export interface iNewEventDeletion extends iNewBase { text: string; events: EventBase[]; diff --git a/packages/common/src/utils/event-content.test.ts b/packages/common/src/utils/event-content.test.ts index a93a2e6..46fc1b3 100644 --- a/packages/common/src/utils/event-content.test.ts +++ b/packages/common/src/utils/event-content.test.ts @@ -1,5 +1,13 @@ import { NEVENT_KIND } from "../types"; -import { isValidEventContent } from "./event-content"; +import { + createEventContent, + extractEventContent, + isValidEventContent, +} from "./event-content"; + +/** + * VALID + */ test("Websocket url", () => { const content = "wss://example.com"; @@ -43,8 +51,9 @@ test("Test with HTML", () => { test("Test with line breaks", () => { const content = "Checkout\n"; + // TODO: According to spec this should be false but it seems other clients don't care const report = isValidEventContent(content); - expect(report.isValid).toBe(false); + expect(report.isValid).toBe(true); }); test("Test with unicode", () => { @@ -53,9 +62,98 @@ test("Test with unicode", () => { expect(report.isValid).toBe(true); }); -test("Test with nostr: link and unicode", () => { - const content = - "Gracias nostr:npub1kade5vf37snr4hv5hgstav6j5ygry6z09kkq0flp47p8cmeuz5zs7zz2an ! ⚡️⚡️⚡️"; +test("Test real content", () => { + const content = `Not our duty. There are agencies we pay to do that. Do you actively check people in the streets to see if they are talking about something illegal? Why would you do that here? All we have to do here is not interact with your “bad actors” and in exceptional cases notify the authorities. + I respect your point of view, but it dangerously adheres to the political doctrine that we all feel as a yoke.`; + const report = isValidEventContent(content); + // TODO: According to spec this should be false but it seems other clients don't care + expect(report.isValid).toBe(true); +}); + +test("Test real content #2", () => { + const content = `Not our duty. There are agencies we pay to do that. Do you actively check people in the streets to see if they are talking about something illegal? Why would you do that here? All we have to do here is not interact with your “bad actors” and in exceptional cases notify the authorities. + I respect your point of view, but it dangerously adheres to the political doctrine that we all feel as a yoke.`; const report = isValidEventContent(content); + // TODO: According to spec this should be false but it seems other clients don't care expect(report.isValid).toBe(true); }); + +/** + * EXTRACT + */ + +test("extractEventContent: relay", () => { + const content = "wss://relay.example.com"; + const res = extractEventContent(content); + expect(res).toEqual({ + message: undefined, + nurls: [], + relayUrl: "wss://relay.example.com", + }); +}); + +test("Test extract without result", () => { + const content = "Here's whats on nostr: cool stuff"; + const res = extractEventContent(content); + expect(res).toEqual(undefined); +}); + +test("extractEventContent: nostr", () => { + const content = + "Profile is impersonating nostr:npub14vgamf2zucjlrxrp9tuudutklyt9ny8at3g5t6d4z7zs22g7gjqsujasrr"; + const res = extractEventContent(content); + expect(res).toEqual({ + message: + "Profile is impersonating ab11dda542e625f198612af9c6f176f9165990fd5c5145e9b5178505291e4481", + + nurls: [ + { + type: "npub", + publicKeys: [ + "ab11dda542e625f198612af9c6f176f9165990fd5c5145e9b5178505291e4481", + ], + }, + ], + }); +}); + +test("extractEventContent: nostr x2", () => { + const content = + "Checkout these guys nostr:npub14vgamf2zucjlrxrp9tuudutklyt9ny8at3g5t6d4z7zs22g7gjqsujasrr nostr:npub1stemstrls4f5plqeqkeq43gtjhtycuqd9w25v5r5z5ygaq2n2sjsd6mul5 later"; + const res = extractEventContent(content); + + expect(res).toEqual({ + message: + "Checkout these guys ab11dda542e625f198612af9c6f176f9165990fd5c5145e9b5178505291e4481 82f3b82c7f855340fc1905b20ac50b95d64c700d2b9546507415088e81535425 later", + nurls: [ + { + type: "npub", + publicKeys: [ + "ab11dda542e625f198612af9c6f176f9165990fd5c5145e9b5178505291e4481", + "82f3b82c7f855340fc1905b20ac50b95d64c700d2b9546507415088e81535425", + ], + }, + ], + relayUrl: undefined, + }); +}); + +/** + * CREATE + */ + +test("createEventContent", () => { + const content = "Here's whats on nostr: cool stuff"; + const res = createEventContent({ + message: content, + }); + expect(res).toEqual("Here's whats on nostr: cool stuff"); +}); + +test("createEventContent: relay", () => { + const content = "wss://relay.example.com"; + const res = createEventContent({ + relayUrl: "wss://relay.example.com", + }); + expect(res).toEqual("wss://relay.example.com"); +}); diff --git a/packages/common/src/utils/event-content.ts b/packages/common/src/utils/event-content.ts index bf55316..38b219c 100644 --- a/packages/common/src/utils/event-content.ts +++ b/packages/common/src/utils/event-content.ts @@ -1,4 +1,10 @@ import { NEVENT_KIND, NEventContent } from "../types"; +import { + NOSTR_URL_REGEX_GLOBAL, + decodeNostrUrl, + encodePublicKeyToNostrUrl, + isNostrUrl, +} from "./nostr-url"; import { isValidWebSocketUrl } from "./websocket-url"; /** @@ -6,6 +12,7 @@ import { isValidWebSocketUrl } from "./websocket-url"; * - for ex "wss://relay.example.com" * - for ex "Profile is impersonating nostr:2234567890123456789012345678901234567890123456789012345678901234" * - for ex "Checkout nostr:2234567890123456789012345678901234567890123456789012345678901234" + * - for ex "Gracias nostr:npub1kade5vf37snr4hv5hgstav6j5ygry6z09kkq0flp47p8cmeuz5zs7zz2an ! ⚡️⚡️⚡️"; * - for ex "Checkout nostr:2234567890123456789012345678901234567890123456789012345678901234 nostr:2234567890123456789012345678901234567890123456789012345678901234" * - for ex. "Checkout this pic" // no match * @param content @@ -14,58 +21,85 @@ export function extractEventContent( content: string, kind?: NEVENT_KIND ): NEventContent | undefined { - if (!isValidEventContent(content, kind)) { - return; + const validationResult = isValidEventContent(content, kind); + + if (!validationResult.isValid) { + console.log("Invalid content"); + return undefined; } const urlRegex = /(.*)?(wss:\/\/[a-zA-Z0-9.-]+)/; - const nostrRegex = /(nostr:[a-fA-F0-9]{64})/g; - - let match; + const evc: NEventContent = { + message: undefined, + relayUrl: undefined, + nurls: [], + }; + + const urlMatch = urlRegex.exec(content); + if (urlMatch) { + evc.message = urlMatch[1] ? urlMatch[1].trim() : undefined; + evc.relayUrl = urlMatch[2]; + return evc; + } - match = urlRegex.exec(content); - if (match) { - const message = match[1] ? match[1].trim() : undefined; - return { type: "relayUrl", relayUrl: match[2], message }; + const publicKeys: string[] = []; + let nostrMatch; + const replaceIndexes = []; + + while ((nostrMatch = NOSTR_URL_REGEX_GLOBAL.exec(content)) !== null) { + const item = nostrMatch[0]; + if (isNostrUrl(item)) { + const decoded = decodeNostrUrl(item); + if (decoded.prefix === "npub" && decoded.tlvItems.length > 0) { + const publicKey = decoded.tlvItems[0].value as string; + publicKeys.push(publicKey); + replaceIndexes.push({ + index: nostrMatch.index, + length: item.length, + replaceWith: publicKey, + }); + } + } } - let result; - let publicKeys = []; - while ((result = nostrRegex.exec(content)) !== null) { - publicKeys.push(result[1].split(":")[1]); + let offset = 0; + for (const { index, length, replaceWith } of replaceIndexes) { + const realIndex = index + offset; + content = + content.slice(0, realIndex) + + replaceWith + + content.slice(realIndex + length); + offset += replaceWith.length - length; } if (publicKeys.length > 0) { - const message = content.replace(nostrRegex, "").trim(); - return { type: "nostr", publicKeys, message }; + evc.message = content.trim(); + evc.nurls = [ + { + type: "npub", + publicKeys, + }, + ]; + return evc; } - // If there was no match, return undefined - return; + return undefined; } export function createEventContent(content: NEventContent) { - if (!content.type) { - return content.message; - } else if (content.type === "relayUrl") { - return content.relayUrl; - } else if (content.type === "nostr") { - if (!content.publicKeys) return content.message; - - for (const key of content.publicKeys) { - // check length - if (key.length !== 64) { - throw new Error(`Invalid key length: ${key}`); - } + if (!content) return ""; + let newContent = content.message ? `${content.message} ` : ""; + if (content.relayUrl) return `${newContent} ${content.relayUrl}`.trim(); + if (!content.nurls || content.nurls.length === 0) return newContent.trim(); + for (const nurl of content.nurls) { + if (nurl.type === "npub") { + // TODO: handle multiple npub + newContent += `${encodePublicKeyToNostrUrl(nurl.publicKeys[0])} `; + } else { + throw new Error(`Unsupported nurl type ${nurl.type}`); } - - const publicKeysStr = content.publicKeys - .map((key) => `nostr:${key}`) - .join(" "); - return content.message - ? `${content.message} ${publicKeysStr}` - : publicKeysStr; } + return newContent.trim(); } /** @@ -113,9 +147,10 @@ export function isValidEventContent( return { isValid: false, error: "HTML tags are not allowed" }; } - if (containsLineBreaks(content)) { - return { isValid: false, error: "Line breaks are not allowed" }; - } + // TODO: Many users use line breaks to format their content. + // if (containsLineBreaks(content)) { + // return { isValid: false, error: "Line breaks are not allowed" }; + // } // If none of the negative conditions are met return { isValid: true }; diff --git a/packages/common/src/utils/event-even-tag.test.ts b/packages/common/src/utils/event-even-tag.test.ts new file mode 100644 index 0000000..6ba1fad --- /dev/null +++ b/packages/common/src/utils/event-even-tag.test.ts @@ -0,0 +1,154 @@ +import { NEvent } from "../classes/event"; +import { + eventHasPositionalEventTag, + eventHasPositionalEventTags, +} from "./event-event"; + +test("eventHasEventTags", () => { + const orig = { + id: "834208b49d2c50557d568263364ced175bb2562274b67c3f80a0d72791414177", + pubkey: "78e47dfe683faf1869a5378aa4b4a52d768d799bd8eb38099c0b194f56727f35", + created_at: 1692825846, + kind: 1, + tags: [ + [ + "e", + "de64876043e27b1176f7b23c23eaeca155886bab696c9a8a277b24f02c93be8c", + "", + "root", + ], + [ + "e", + "ae2eee89b07b0c0707d2d795cbb4d07947fa9868e7529442212d1b196c0cf47c", + "", + "reply", + ], + ["p", "78e47dfe683faf1869a5378aa4b4a52d768d799bd8eb38099c0b194f56727f35"], + ["p", "fcf70a45cfa817eaa813b9ba8a375d713d3169f4a27f3dcac3d49112df67d37e"], + ], + content: + "I agree but thats why I said, too many people underestimate the enemy.\n\nThey have started wars and assassinated presidents over this power, this will not go smoothly (if indeed this is real rebellion)", + sig: "36a3442cc38c6367ffb80bd9988a263a37aaedc4103aa3640bae58715cc7b8be7890140fa72cd01bad3b198c865e80964f3ee40beb1a458e3b7141f44ecfd9d9", + }; + + const ev = new NEvent(orig); + // Check for deprecated tags + const hasPositionalTag = eventHasPositionalEventTag(ev); + expect(hasPositionalTag).toEqual(false); + + // Check for new tags + const hasEventTag = ev.hasEventTags(); + expect(hasEventTag).toEqual([ + { + eventId: + "de64876043e27b1176f7b23c23eaeca155886bab696c9a8a277b24f02c93be8c", + relayUrl: "", + marker: "root", + }, + { + eventId: + "ae2eee89b07b0c0707d2d795cbb4d07947fa9868e7529442212d1b196c0cf47c", + relayUrl: "", + marker: "reply", + }, + ]); + + const hasPublicKeyTags = ev.hasPublicKeyTags(); + expect(hasPublicKeyTags).toEqual([ + ["78e47dfe683faf1869a5378aa4b4a52d768d799bd8eb38099c0b194f56727f35"], + ["fcf70a45cfa817eaa813b9ba8a375d713d3169f4a27f3dcac3d49112df67d37e"], + ]); +}); + +test("eventHasEventTags: #2", () => { + const orig = { + id: "ae7d77a1ecde851d14e40d7acb3e49e69bc658bae4eea8f056e805ce904cedb2", + pubkey: "8c59239319637f97e007dad0d681e65ce35b1ace333b629e2d33f9465c132608", + created_at: 1692267918, + kind: 1, + tags: [ + [ + "e", + "4c7339c9ea23adff5140b0747b58d646dc41e5575900630e9a019a12e331a764", + "", + "root", + ], + ["p", "8c59239319637f97e007dad0d681e65ce35b1ace333b629e2d33f9465c132608"], + ["r", "https://nnn.ed.jp/news/blog/archives/10023/"], + ], + content: + "これだ。期間限定だったのか。結構中身良かった記憶あります https://nnn.ed.jp/news/blog/archives/10023/", + sig: "a9b3f7c993eaef0d28a62f7a965811348e7003c1c96356c27c4abbf4c8fa0ffed2aeb3a22c9440c2e4fbfa7fba0338c1bf93cff0ea09477d5fe1ae2460213769", + }; + + const ev = new NEvent(orig); + // Check for deprecated tags + const hasPositionalTag = eventHasPositionalEventTag(ev); + expect(hasPositionalTag).toBe(false); + + // Check for new tags + const hasEventTag = ev.hasEventTags(); + expect(hasEventTag).toEqual([ + { + eventId: + "4c7339c9ea23adff5140b0747b58d646dc41e5575900630e9a019a12e331a764", + relayUrl: "", + marker: "root", + }, + ]); +}); + +test("eventHasEventTags: legacy", () => { + const orig = { + id: "6b9da693a00ce41815129c6fa85bf5efafba42e3f91c10dadb64ba5caf133916", + pubkey: "09d49f47081c1a06f04afda62988e3253247a8c96c1d4ef025dc7619dbc23942", + created_at: 1692958312, + kind: 1, + tags: [ + ["e", "80578fa54977b958e687f47e9bde5e9c000e52fd5ad4a4792860e0282015c3ee"], + ["p", "a723805cda67251191c8786f4da58f797e6977582301354ba8e91bcb0342dc9c"], + ], + content: "Good morning Seth 🤙", + sig: "15a9e1954929109f7a2b1e91765f24508b16bdd6bd95ed89127f18d36a549536b3359024d9d0491858890b998fcac9249cc7e75988ac83a1506e18c51491a445", + }; + + const ev = new NEvent(orig); + // Check for deprecated tags + const hasPositionalTag = eventHasPositionalEventTag(ev); + expect(hasPositionalTag).toBe(true); + + const tags = eventHasPositionalEventTags(ev); + expect(tags).toEqual([ + { + eventId: + "80578fa54977b958e687f47e9bde5e9c000e52fd5ad4a4792860e0282015c3ee", + relayUrl: "", + marker: "root", + }, + ]); +}); + +test("eventHasEventTags: no tags", () => { + const orig = { + id: "834208b49d2c50557d568263364ced175bb2562274b67c3f80a0d72791414177", + pubkey: "78e47dfe683faf1869a5378aa4b4a52d768d799bd8eb38099c0b194f56727f35", + created_at: 1692825846, + kind: 1, + tags: [], + content: + "I agree but thats why I said, too many people underestimate the enemy.\n\nThey have started wars and assassinated presidents over this power, this will not go smoothly (if indeed this is real rebellion)", + sig: "36a3442cc38c6367ffb80bd9988a263a37aaedc4103aa3640bae58715cc7b8be7890140fa72cd01bad3b198c865e80964f3ee40beb1a458e3b7141f44ecfd9d9", + }; + + const ev = new NEvent(orig); + // Check for deprecated tags + const hasPositionalTag = eventHasPositionalEventTag(ev); + expect(hasPositionalTag).toBe(false); + + // Check for new tags + const hasEventTag = ev.hasEventTags(); + expect(hasEventTag).toBe(undefined); + + const hasPublicKeyTags = ev.hasPublicKeyTags(); + expect(hasPublicKeyTags).toBe(undefined); +}); diff --git a/packages/common/src/utils/event-event.ts b/packages/common/src/utils/event-event.ts index 547ed39..95007b5 100644 --- a/packages/common/src/utils/event-event.ts +++ b/packages/common/src/utils/event-event.ts @@ -1,6 +1,9 @@ -import { EventBase, EventEventTag } from "../types"; +import { EventBase } from "../types/event"; +import { EventEventTag } from "../types/event-event-tag"; /** + * Marked "e" tags (PREFERRED) + * * ["e", ] * ["e", , ] * ['e', 'eventId', 'relayUrl', 'marker'] @@ -32,3 +35,63 @@ export function eventHasEventTags( } return evTags && evTags.length > 0 ? evTags : undefined; } + +/** + * Get event positional "e" tags (DEPRECATED) + */ +export function eventHasPositionalEventTags(event: EventBase) { + const tags = event.tags.filter((tag) => tag[0] === "e"); + if (tags.length === 0) return undefined; + const evTags: EventEventTag[] = []; + + for (let i = 0; i < tags.length; i++) { + if (i === 0) { + evTags.push({ + eventId: tags[i][1], + relayUrl: "", + marker: "root", + }); + } + + if (tags.length === 2) { + if (i === 1) { + evTags.push({ + eventId: tags[i][1], + relayUrl: "", + marker: "reply", + }); + } + } else if (tags.length > 2) { + if (i === 1) { + evTags.push({ + eventId: tags[i][1], + relayUrl: "", + marker: "mention", + }); + } else if (i > 1) { + evTags.push({ + eventId: tags[i][1], + relayUrl: "", + marker: "reply", + }); + } + } + } + + return evTags && evTags.length > 0 ? evTags : undefined; +} + +/** + * Positional "e" tags (DEPRECATED) + * Basically checks whether the event is using the deprecated format + * + * This is a boolean check that doesn't return the tags + * I use this primarily to differentiate between the formats + */ +export function eventHasPositionalEventTag(event: EventBase): boolean { + if (!event.tags || event.tags.length === 0) return false; + const tags = event.tags.filter((tag) => tag[0] === "e" && tag.length > 2); + // If none have been found, + if (tags.length === 0) return true; + return false; +} diff --git a/packages/common/src/utils/event-nonce.ts b/packages/common/src/utils/event-nonce.ts index 8a41eff..5c5a254 100644 --- a/packages/common/src/utils/event-nonce.ts +++ b/packages/common/src/utils/event-nonce.ts @@ -1,10 +1,9 @@ -import { EventBase } from "../types"; +import { NEvent } from "../classes/event"; +import { EventBase } from "../types/event"; /** * Check array of tags if any nonce tags are present * https://github.com/nostr-protocol/nips/blob/master/13.md - * - * @returns nonce or undefined */ export function eventHasNonce(event: EventBase): [number, number] | undefined { const nonceTags = event.tags.filter((tag) => tag[0] === "nonce"); @@ -13,3 +12,32 @@ export function eventHasNonce(event: EventBase): [number, number] | undefined { } return [parseInt(nonceTags[0][1]), parseInt(nonceTags[0][2])]; } + +/** + * Add nonce tag to event + */ +export function eventAddNonceTag(event: NEvent, nonce: number[]) { + if (event.hasNonceTag()) { + throw new Error("Event already has a nonce."); + } + if (nonce.length !== 2) { + throw new Error( + "Nonce must be an array of 2 numbers: [miningResult, difficulty]" + ); + } + const miningResult = nonce[0].toString(); + const difficulty = nonce[1].toString(); + + event.addTag(["nonce", miningResult, difficulty]); + + return event; +} + +/** + * Replace nonce tag on event + */ +export function eventReplaceNonceTag(event: NEvent, nonce: number[]) { + event.tags = event.tags.filter((tag) => tag[0] !== "nonce"); + event.addNonceTag(nonce); + return event; +} diff --git a/packages/common/src/utils/event-reporting.test.ts b/packages/common/src/utils/event-reporting.test.ts new file mode 100644 index 0000000..a6d96f6 --- /dev/null +++ b/packages/common/src/utils/event-reporting.test.ts @@ -0,0 +1,75 @@ +import { Report, NREPORT_KIND } from "../types"; +import { NEvent } from "../classes"; +import { eventHasReport, generateReportTags } from "./event-reporting"; + +test("eventHasReport", () => { + const ev = new NEvent({ + kind: 1984, + content: "Broke local law", + tags: [ + [ + "e", + "1234567890123456789012345678901234567890123456789012345678901234", + "illegal", + ], + ["p", "1234567890123456789012345678901234567890123456789012345678901234"], + ], + }); + const hasReport = eventHasReport(ev); + expect(hasReport).toEqual({ + eventId: "1234567890123456789012345678901234567890123456789012345678901234", + kind: "illegal", + publicKey: + "1234567890123456789012345678901234567890123456789012345678901234", + content: "Broke local law", + }); + expect(hasReport).toEqual(ev.hasReportTags()); +}); + +test("eventHasReport: impersonation", () => { + const ev = new NEvent({ + kind: 1984, + content: + "Profile is impersonating nostr:2234567890123456789012345678901234567890123456789012345678901234", + tags: [ + [ + "p", + "1234567890123456789012345678901234567890123456789012345678901234", + "impersonation", + ], + ], + }); + const hasReport = eventHasReport(ev); + expect(hasReport).toEqual({ + kind: "impersonation", + publicKey: + "1234567890123456789012345678901234567890123456789012345678901234", + content: + "Profile is impersonating nostr:2234567890123456789012345678901234567890123456789012345678901234", + }); + expect(hasReport).toEqual(ev.hasReportTags()); +}); + +test("generateReportTags: impersonation", () => { + const report: Report = { + kind: NREPORT_KIND.IMPERSONATION, + publicKey: + "1234567890123456789012345678901234567890123456789012345678901234", + }; + const tags = generateReportTags(report); + expect(tags).toEqual([["p", report.publicKey, report.kind]]); +}); + +test("generateReportTags: event", () => { + const report: Report = { + kind: NREPORT_KIND.ILLEGAL, + publicKey: + "1234567890123456789012345678901234567890123456789012345678901234", + eventId: "1234567890123456789012345678901234567890123456789012345678901234", + }; + const tags = generateReportTags(report); + expect(tags).toEqual([ + ["e", report.eventId, report.kind], + ["p", report.publicKey], + ]); +}); diff --git a/packages/common/src/utils/event-zap.ts b/packages/common/src/utils/event-zap.ts index 916bd18..eefdb6d 100644 --- a/packages/common/src/utils/event-zap.ts +++ b/packages/common/src/utils/event-zap.ts @@ -1,10 +1,8 @@ -import { EventBase, ZapTag } from "src/types"; +import { EventBase, ZapTag } from "../types"; /** * Check if event has zap tags * https://github.com/nostr-protocol/nips/blob/master/57.md#appendix-g-zap-tag-on-other-events - * @param event - * @returns */ export function eventHasZapTags(event: EventBase): ZapTag[] | undefined { if (!event.tags) { @@ -26,6 +24,9 @@ export function eventHasZapTags(event: EventBase): ZapTag[] | undefined { return zapTags; } +/** + * Generate a new zap tag + */ export function createEventZapTag(opts: ZapTag) { return [ "zap", diff --git a/packages/common/src/utils/event.nonce.test.ts b/packages/common/src/utils/event.nonce.test.ts new file mode 100644 index 0000000..16f8556 --- /dev/null +++ b/packages/common/src/utils/event.nonce.test.ts @@ -0,0 +1,33 @@ +import { NEvent, NewShortTextNote } from "../classes/event"; +import { eventHasNonce } from "./event-nonce"; + +test("eventHasNonce", () => { + const ev = new NEvent({ + kind: 1, + content: "Hello", + tags: [["nonce", "64", "2"]], + }); + const hasNonce = eventHasNonce(ev); + expect(hasNonce).toEqual([64, 2]); + expect(hasNonce).toEqual(ev.hasNonceTag()); +}); + +test("eventAddNonce", () => { + const ev = NewShortTextNote({ + text: "Hello", + }); + const nonce = [64, 2]; + ev.addNonceTag(nonce); + expect(ev.hasNonceTag()).toEqual(nonce); +}); + +test("eventAddNonce: already has nonce", () => { + const ev = NewShortTextNote({ + text: "Hello", + }); + const nonce = [64, 2]; + ev.addNonceTag(nonce); + expect(() => ev.addNonceTag(nonce)).toThrowError( + "Event already has a nonce." + ); +}); diff --git a/packages/common/src/utils/index.ts b/packages/common/src/utils/index.ts index 8bfb06b..8652f2e 100644 --- a/packages/common/src/utils/index.ts +++ b/packages/common/src/utils/index.ts @@ -5,6 +5,7 @@ export * from "./event-amount"; export * from "./event-content-warning"; export * from "./event-content"; export * from "./event-coordinates"; +export * from "./event-event"; export * from "./event-expiration"; export * from "./event-identifier"; export * from "./event-labels"; diff --git a/packages/common/src/utils/nostr-url.test.ts b/packages/common/src/utils/nostr-url.test.ts new file mode 100644 index 0000000..b9c0bf9 --- /dev/null +++ b/packages/common/src/utils/nostr-url.test.ts @@ -0,0 +1,14 @@ +import { BECH32_PREFIX } from "../types"; +import { encodeNostrUrl } from "./nostr-url"; + +test("encodeNostrUrl", () => { + const url = encodeNostrUrl(BECH32_PREFIX.PublicKeys, [ + { + type: 0, + value: "b75b9a3131f4263add94ba20beb352a11032684f2dac07a7e1af827c6f3c1505", + }, + ]); + expect(url).toEqual( + "nostr:npub1kade5vf37snr4hv5hgstav6j5ygry6z09kkq0flp47p8cmeuz5zs7zz2an" + ); +}); diff --git a/packages/common/src/utils/nostr-url.ts b/packages/common/src/utils/nostr-url.ts index a598864..3a7e5c6 100644 --- a/packages/common/src/utils/nostr-url.ts +++ b/packages/common/src/utils/nostr-url.ts @@ -1,10 +1,19 @@ -import { decodeBech32 } from "./bech32"; +import { BECH32_PREFIX } from "../types"; +import { decodeBech32, encodeBech32 } from "./bech32"; -export function parseNostrURL(input: string) { - const regex = - /(?:nostr:)?(npub|nsec|note|lnurl|nprofile|nevent)([a-zA-Z0-9]+)/; +export const NOSTR_URL_REGEX = + /(?:nostr:)?(npub|nsec|note|lnurl|nprofile|nevent)([a-zA-Z0-9]+)/; - const match = input.match(regex); +export const NOSTR_URL_REGEX_GLOBAL = + /(?:nostr:)?(npub|nsec|note|lnurl|nprofile|nevent)([a-zA-Z0-9]+)/g; +// /(nostr:npub[0-9a-fA-F]{64})/g; + +function parseNostrURL(input: string) { + if (typeof input !== "string") { + return null; + } + + const match = input.match(NOSTR_URL_REGEX); if (match && match.length === 3) { return { @@ -16,10 +25,20 @@ export function parseNostrURL(input: string) { } } +/** + * Check if string is a nostr url + * @param url nostr:npub1kade5vf37snr4hv5hgstav6j5ygry6z09kkq0flp47p8cmeuz5zs7zz2an + * @returns + */ export function isNostrUrl(url: string) { return parseNostrURL(url) !== null; } +/** + * Decode nostr url + * @param url nostr:npub1kade5vf37snr4hv5hgstav6j5ygry6z09kkq0flp47p8cmeuz5zs7zz2an + * @returns + */ export function decodeNostrUrl(url: string) { const decoded = parseNostrURL(url); if (decoded === null) { @@ -27,3 +46,33 @@ export function decodeNostrUrl(url: string) { } return decodeBech32(decoded.bech32); } + +/** + * Encode nostr url + * @param prefix + * @param tlvItems + * @returns + */ +export function encodeNostrUrl( + prefix: BECH32_PREFIX, + tlvItems: { + type: number; + value: string | number; + }[] +) { + return `nostr:${encodeBech32(prefix, tlvItems)}`; +} + +/** + * Encode a private key as nostr url + * @param publicKey + * @returns + */ +export function encodePublicKeyToNostrUrl(publicKey: string) { + return encodeNostrUrl(BECH32_PREFIX.PublicKeys, [ + { + type: 0, + value: publicKey, + }, + ]); +} diff --git a/packages/common/src/utils/proof-of-work.test.ts b/packages/common/src/utils/proof-of-work.test.ts index a3a7079..0cc99fe 100644 --- a/packages/common/src/utils/proof-of-work.test.ts +++ b/packages/common/src/utils/proof-of-work.test.ts @@ -35,39 +35,4 @@ describe("Proof of Work Performance", () => { `10 bits proof of work took ${endTime - startTime} ms - event function` ); }); - - // test("15 bits proof of work", async () => { - // const startTime = Date.now(); - // proofOfWork(sampleEvent, 15); - // const endTime = Date.now(); - - // console.log(`15 bits proof of work took ${endTime - startTime} ms`); - // }); - - // test("20 bits proof of work", async () => { - // const startTime = Date.now(); - // proofOfWork(sampleEvent, 20); - // const endTime = Date.now(); - - // console.log(`20 bits proof of work took ${endTime - startTime} ms`); - // }); - - // test("20 bits proof of work - event function", async () => { - // const ev = new NEvent(sampleEvent); - // const startTime = Date.now(); - // ev.proofOfWork(20); - // const endTime = Date.now(); - - // console.log( - // `20 bits proof of work took ${endTime - startTime} ms - event function` - // ); - // }); - - // test("25 bits proof of work", async () => { - // const startTime = Date.now(); - // proofOfWork(sampleEvent, 25); - // const endTime = Date.now(); - - // console.log(`25 bits proof of work took ${endTime - startTime} ms`); - // }); }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 697cf70..b5790db 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -63,8 +63,8 @@ importers: specifier: ^9.2.0 version: 9.2.0(react@18.2.0) nanoid: - specifier: ^4.0.2 - version: 4.0.2 + specifier: ^3.0.0 + version: 3.3.6 react: specifier: ^18.2.0 version: 18.2.0 @@ -124,8 +124,8 @@ importers: specifier: ^3.0.0 version: 3.0.0 nanoid: - specifier: ^4.0.2 - version: 4.0.2 + specifier: ^3.0.0 + version: 3.3.6 devDependencies: '@anatine/esbuild-decorators': specifier: ^0.2.19 @@ -133,6 +133,9 @@ importers: '@types/jest': specifier: ^29.5.3 version: 29.5.3 + crypto-browserify: + specifier: ^3.12.0 + version: 3.12.0 esbuild: specifier: ^0.17.18 version: 0.17.18 @@ -3099,6 +3102,15 @@ packages: engines: {node: '>=8'} dev: true + /asn1.js@5.4.1: + resolution: {integrity: sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==} + dependencies: + bn.js: 4.12.0 + inherits: 2.0.4 + minimalistic-assert: 1.0.1 + safer-buffer: 2.1.2 + dev: true + /babel-jest@29.6.2(@babel/core@7.22.9): resolution: {integrity: sha512-BYCzImLos6J3BH/+HvUCHG1dTf2MzmAB4jaVxHV+29RZLjR29XuYTmsf2sdDwkrb+FczkGo3kOhE7ga6sI0P4A==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -3188,6 +3200,14 @@ packages: resolution: {integrity: sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg==} dev: false + /bn.js@4.12.0: + resolution: {integrity: sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==} + dev: true + + /bn.js@5.2.1: + resolution: {integrity: sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==} + dev: true + /brace-expansion@1.1.11: resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} dependencies: @@ -3202,6 +3222,59 @@ packages: fill-range: 7.0.1 dev: true + /brorand@1.1.0: + resolution: {integrity: sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==} + dev: true + + /browserify-aes@1.2.0: + resolution: {integrity: sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==} + dependencies: + buffer-xor: 1.0.3 + cipher-base: 1.0.4 + create-hash: 1.2.0 + evp_bytestokey: 1.0.3 + inherits: 2.0.4 + safe-buffer: 5.2.1 + dev: true + + /browserify-cipher@1.0.1: + resolution: {integrity: sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==} + dependencies: + browserify-aes: 1.2.0 + browserify-des: 1.0.2 + evp_bytestokey: 1.0.3 + dev: true + + /browserify-des@1.0.2: + resolution: {integrity: sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==} + dependencies: + cipher-base: 1.0.4 + des.js: 1.1.0 + inherits: 2.0.4 + safe-buffer: 5.2.1 + dev: true + + /browserify-rsa@4.1.0: + resolution: {integrity: sha512-AdEER0Hkspgno2aR97SAf6vi0y0k8NuOpGnVH3O99rcA5Q6sh8QxcngtHuJ6uXwnfAXNM4Gn1Gb7/MV1+Ymbog==} + dependencies: + bn.js: 5.2.1 + randombytes: 2.1.0 + dev: true + + /browserify-sign@4.2.1: + resolution: {integrity: sha512-/vrA5fguVAKKAVTNJjgSm1tRQDHUU6DbwO9IROu/0WAzC8PKhucDSh18J0RMvVeHAn5puMd+QHC2erPRNf8lmg==} + dependencies: + bn.js: 5.2.1 + browserify-rsa: 4.1.0 + create-hash: 1.2.0 + create-hmac: 1.1.7 + elliptic: 6.5.4 + inherits: 2.0.4 + parse-asn1: 5.1.6 + readable-stream: 3.6.2 + safe-buffer: 5.2.1 + dev: true + /browserslist@4.21.10: resolution: {integrity: sha512-bipEBdZfVH5/pwrvqc+Ub0kUPVfGUhlKxbvfD+z1BDnPEO/X98ruXGA1WP5ASpAFKan7Qr6j736IacbZQuAlKQ==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} @@ -3230,6 +3303,10 @@ packages: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} dev: true + /buffer-xor@1.0.3: + resolution: {integrity: sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ==} + dev: true + /callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} @@ -3274,6 +3351,13 @@ packages: engines: {node: '>=8'} dev: true + /cipher-base@1.0.4: + resolution: {integrity: sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==} + dependencies: + inherits: 2.0.4 + safe-buffer: 5.2.1 + dev: true + /cjs-module-lexer@1.2.3: resolution: {integrity: sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==} dev: true @@ -3355,6 +3439,34 @@ packages: yaml: 1.10.2 dev: false + /create-ecdh@4.0.4: + resolution: {integrity: sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==} + dependencies: + bn.js: 4.12.0 + elliptic: 6.5.4 + dev: true + + /create-hash@1.2.0: + resolution: {integrity: sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==} + dependencies: + cipher-base: 1.0.4 + inherits: 2.0.4 + md5.js: 1.3.5 + ripemd160: 2.0.2 + sha.js: 2.4.11 + dev: true + + /create-hmac@1.1.7: + resolution: {integrity: sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==} + dependencies: + cipher-base: 1.0.4 + create-hash: 1.2.0 + inherits: 2.0.4 + ripemd160: 2.0.2 + safe-buffer: 5.2.1 + sha.js: 2.4.11 + dev: true + /create-require@1.1.1: resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} dev: true @@ -3368,6 +3480,22 @@ packages: which: 2.0.2 dev: true + /crypto-browserify@3.12.0: + resolution: {integrity: sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==} + dependencies: + browserify-cipher: 1.0.1 + browserify-sign: 4.2.1 + create-ecdh: 4.0.4 + create-hash: 1.2.0 + create-hmac: 1.1.7 + diffie-hellman: 5.0.3 + inherits: 2.0.4 + pbkdf2: 3.1.2 + public-encrypt: 4.0.3 + randombytes: 2.1.0 + randomfill: 1.0.4 + dev: true + /css-box-model@1.2.1: resolution: {integrity: sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==} dependencies: @@ -3412,6 +3540,13 @@ packages: engines: {node: '>=0.10.0'} dev: true + /des.js@1.1.0: + resolution: {integrity: sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg==} + dependencies: + inherits: 2.0.4 + minimalistic-assert: 1.0.1 + dev: true + /detect-newline@3.1.0: resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} engines: {node: '>=8'} @@ -3431,6 +3566,14 @@ packages: engines: {node: '>=0.3.1'} dev: true + /diffie-hellman@5.0.3: + resolution: {integrity: sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==} + dependencies: + bn.js: 4.12.0 + miller-rabin: 4.0.1 + randombytes: 2.1.0 + dev: true + /dir-glob@3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} @@ -3449,6 +3592,18 @@ packages: resolution: {integrity: sha512-nO3ZEomTK2PO/3TUXgEx0A97xZTpKVf4p427lABHuCpT1IQ2N+njVh29DkQkCk6Q4m2wjU+faK4xAcfFndwjvw==} dev: true + /elliptic@6.5.4: + resolution: {integrity: sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==} + dependencies: + bn.js: 4.12.0 + brorand: 1.1.0 + hash.js: 1.1.7 + hmac-drbg: 1.0.1 + inherits: 2.0.4 + minimalistic-assert: 1.0.1 + minimalistic-crypto-utils: 1.0.1 + dev: true + /emittery@0.13.1: resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} engines: {node: '>=12'} @@ -3680,6 +3835,13 @@ packages: engines: {node: '>=0.10.0'} dev: true + /evp_bytestokey@1.0.3: + resolution: {integrity: sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==} + dependencies: + md5.js: 1.3.5 + safe-buffer: 5.2.1 + dev: true + /execa@5.1.1: resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} engines: {node: '>=10'} @@ -3955,6 +4117,30 @@ packages: dependencies: function-bind: 1.1.1 + /hash-base@3.1.0: + resolution: {integrity: sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==} + engines: {node: '>=4'} + dependencies: + inherits: 2.0.4 + readable-stream: 3.6.2 + safe-buffer: 5.2.1 + dev: true + + /hash.js@1.1.7: + resolution: {integrity: sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==} + dependencies: + inherits: 2.0.4 + minimalistic-assert: 1.0.1 + dev: true + + /hmac-drbg@1.0.1: + resolution: {integrity: sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==} + dependencies: + hash.js: 1.1.7 + minimalistic-assert: 1.0.1 + minimalistic-crypto-utils: 1.0.1 + dev: true + /hoist-non-react-statics@3.3.2: resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} dependencies: @@ -4654,6 +4840,14 @@ packages: tmpl: 1.0.5 dev: true + /md5.js@1.3.5: + resolution: {integrity: sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==} + dependencies: + hash-base: 3.1.0 + inherits: 2.0.4 + safe-buffer: 5.2.1 + dev: true + /mdi-react@9.2.0(react@18.2.0): resolution: {integrity: sha512-NI1sIvu142SQKkOMe4bjA9KuvMXpT8PfNOkCblSEuUeakFpgciMuKV9rEyJbQkvFBppDLx1f1xmbcIc06oRyAQ==} peerDependencies: @@ -4679,11 +4873,27 @@ packages: picomatch: 2.3.1 dev: true + /miller-rabin@4.0.1: + resolution: {integrity: sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==} + hasBin: true + dependencies: + bn.js: 4.12.0 + brorand: 1.1.0 + dev: true + /mimic-fn@2.1.0: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} dev: true + /minimalistic-assert@1.0.1: + resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==} + dev: true + + /minimalistic-crypto-utils@1.0.1: + resolution: {integrity: sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==} + dev: true + /minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} dependencies: @@ -4698,13 +4908,6 @@ packages: resolution: {integrity: sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true - dev: true - - /nanoid@4.0.2: - resolution: {integrity: sha512-7ZtY5KTCNheRGfEFxnedV5zFiORN1+Y1N6zvPTnHQd8ENUvfaDBeuJDZb2bN/oXwXxu3qkTXDzy57W5vAmDTBw==} - engines: {node: ^14 || ^16 || >=18} - hasBin: true - dev: false /natural-compare-lite@1.4.0: resolution: {integrity: sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==} @@ -4817,6 +5020,16 @@ packages: dependencies: callsites: 3.1.0 + /parse-asn1@5.1.6: + resolution: {integrity: sha512-RnZRo1EPU6JBnra2vGHj0yhp6ebyjBZpmUCLHWiFhxlzvBCCpAuZ7elsBp1PVAbQN0/04VD/19rfzlBSwLstMw==} + dependencies: + asn1.js: 5.4.1 + browserify-aes: 1.2.0 + evp_bytestokey: 1.0.3 + pbkdf2: 3.1.2 + safe-buffer: 5.2.1 + dev: true + /parse-json@5.2.0: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} @@ -4848,6 +5061,17 @@ packages: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} + /pbkdf2@3.1.2: + resolution: {integrity: sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==} + engines: {node: '>=0.12'} + dependencies: + create-hash: 1.2.0 + create-hmac: 1.1.7 + ripemd160: 2.0.2 + safe-buffer: 5.2.1 + sha.js: 2.4.11 + dev: true + /picocolors@1.0.0: resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} dev: true @@ -4908,6 +5132,17 @@ packages: react-is: 16.13.1 dev: false + /public-encrypt@4.0.3: + resolution: {integrity: sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==} + dependencies: + bn.js: 4.12.0 + browserify-rsa: 4.1.0 + create-hash: 1.2.0 + parse-asn1: 5.1.6 + randombytes: 2.1.0 + safe-buffer: 5.2.1 + dev: true + /punycode@2.3.0: resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==} engines: {node: '>=6'} @@ -4921,6 +5156,19 @@ packages: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} dev: true + /randombytes@2.1.0: + resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} + dependencies: + safe-buffer: 5.2.1 + dev: true + + /randomfill@1.0.4: + resolution: {integrity: sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==} + dependencies: + randombytes: 2.1.0 + safe-buffer: 5.2.1 + dev: true + /react-clientside-effect@1.2.6(react@18.2.0): resolution: {integrity: sha512-XGGGRQAKY+q25Lz9a/4EPqom7WRjz3z9R2k4jhVKA/puQFH/5Nt27vFZYql4m4NVNdUvX8PS3O7r/Zzm7cjUlg==} peerDependencies: @@ -5053,6 +5301,15 @@ packages: loose-envify: 1.4.0 dev: false + /readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + dev: true + /regenerator-runtime@0.13.11: resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==} dev: false @@ -5103,6 +5360,13 @@ packages: glob: 7.2.3 dev: true + /ripemd160@2.0.2: + resolution: {integrity: sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==} + dependencies: + hash-base: 3.1.0 + inherits: 2.0.4 + dev: true + /rollup@3.27.2: resolution: {integrity: sha512-YGwmHf7h2oUHkVBT248x0yt6vZkYQ3/rvE5iQuVBh3WO8GcJ6BNeOkpoX1yMHIiBm18EMLjBPIoUDkhgnyxGOQ==} engines: {node: '>=14.18.0', npm: '>=8.0.0'} @@ -5117,6 +5381,14 @@ packages: queue-microtask: 1.2.3 dev: true + /safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + dev: true + + /safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + dev: true + /scheduler@0.23.0: resolution: {integrity: sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==} dependencies: @@ -5136,6 +5408,14 @@ packages: lru-cache: 6.0.0 dev: true + /sha.js@2.4.11: + resolution: {integrity: sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==} + hasBin: true + dependencies: + inherits: 2.0.4 + safe-buffer: 5.2.1 + dev: true + /shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -5211,6 +5491,12 @@ packages: strip-ansi: 6.0.1 dev: true + /string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + dependencies: + safe-buffer: 5.2.1 + dev: true + /strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} @@ -5463,6 +5749,10 @@ packages: react: 18.2.0 dev: false + /util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + dev: true + /v8-compile-cache-lib@3.0.1: resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} dev: true