diff --git a/src/app/TopNav.svelte b/src/app/TopNav.svelte index 265bf0b8..41e8b1b6 100644 --- a/src/app/TopNav.svelte +++ b/src/app/TopNav.svelte @@ -24,8 +24,9 @@ hasNewNotfications, getUserRelayUrls, searchableRelays, + Subscription, } from "src/engine2" - import engine, {Keys, Directory, Network} from "src/app/engine" + import engine, {Keys, Directory} from "src/app/engine" const {keyState, canUseGiftWrap} = Keys const logoUrl = import.meta.env.VITE_LOGO_URL || "/images/logo.png" @@ -71,15 +72,15 @@ // If we have a query, search using nostr.band. If not, ask for random profiles. // This allows us to populate results even if search isn't supported by forced urls if (term.length > 2) { - Network.subscribe({ + new Subscription({ relays: $searchableRelays, - filter: [{kinds: [0], search, limit: 10}], + filters: [{kinds: [0], search, limit: 10}], timeout: 3000, }) } else if (Directory.profiles.get().length < 50) { - Network.subscribe({ + new Subscription({ relays: getUserRelayUrls("read"), - filter: [{kinds: [0], limit: 50}], + filters: [{kinds: [0], limit: 50}], timeout: 3000, }) } diff --git a/src/app/engine.ts b/src/app/engine.ts index 98d143d8..343557e5 100644 --- a/src/app/engine.ts +++ b/src/app/engine.ts @@ -47,8 +47,4 @@ export const Crypt = engine.Crypt export const Directory = engine.Directory export const Events = engine.Events export const Keys = engine.Keys -export const Network = engine.Network export const Nip04 = engine.Nip04 -export const Nip05 = engine.Nip05 -export const Nip28 = engine.Nip28 -export const Nip44 = engine.Nip44 diff --git a/src/app/shared/Compose.svelte b/src/app/shared/Compose.svelte index 9ad788b2..18ccbbcb 100644 --- a/src/app/shared/Compose.svelte +++ b/src/app/shared/Compose.svelte @@ -6,8 +6,15 @@ import PersonBadge from "src/app/shared/PersonBadge.svelte" import ContentEditable from "src/partials/ContentEditable.svelte" import Suggestions from "src/partials/Suggestions.svelte" - import {isFollowing, searchableRelays, getPubkeyHints} from "src/engine2" - import {Network, Directory} from "src/app/engine" + import { + people, + displayPerson, + Subscription, + isFollowing, + searchableRelays, + getPubkeyHints, + getPeopleSearch, + } from "src/engine2" export let onSubmit @@ -28,16 +35,17 @@ }, } - const {searchProfiles} = Directory + const searchPeople = people.derived(getPeopleSearch) - const loadProfiles = debounce(500, search => { + const loadPeople = debounce(500, search => { if (search.length > 2 && search.startsWith("@")) { - Network.subscribe({ + const sub = new Subscription({ timeout: 3000, relays: $searchableRelays, - filter: [{kinds: [0], search, limit: 10}], - onEvent: debounce(100, () => applySearch(getInfo().word)), + filters: [{kinds: [0], search, limit: 10}], }) + + sub.on("event", () => applySearch(getInfo().word)) } }) @@ -46,7 +54,7 @@ if (word.length > 1 && word.startsWith("@")) { const [followed, notFollowed] = partition( p => isFollowing(p.pubkey).get(), - $searchProfiles(word.slice(1)) + $searchPeople(word.slice(1)) ) results = followed.concat(notFollowed) @@ -64,7 +72,7 @@ return {selection, node, offset, word} } - const autocomplete = ({profile = null, force = false} = {}) => { + const autocomplete = ({person = null, force = false} = {}) => { let completed = false const {selection, node, offset, word} = getInfo() @@ -98,8 +106,8 @@ } // Mentions - if ((force || word.length > 1) && word.startsWith("@") && profile) { - annotate("@", Directory.displayProfile(profile).trim(), pubkeyEncoder.encode(profile.pubkey)) + if ((force || word.length > 1) && word.startsWith("@") && person) { + annotate("@", displayPerson(person).trim(), pubkeyEncoder.encode(person.pubkey)) } // Topics @@ -130,7 +138,7 @@ // Enter adds a newline, so do it on key down if (["Enter"].includes(e.code)) { - autocomplete({profile: suggestions.get()}) + autocomplete({person: suggestions.get()}) } // Only autocomplete topics on space @@ -145,11 +153,11 @@ const {word} = getInfo() // Populate search data - loadProfiles(word) + loadPeople(word) applySearch(word) if (["Tab"].includes(e.code)) { - autocomplete({profile: suggestions.get()}) + autocomplete({person: suggestions.get()}) } if (["Escape", "Space"].includes(e.code)) { @@ -167,7 +175,7 @@ dispatch("keyup", e) } - export const mention = profile => { + export const mention = person => { const input = contenteditable.getInput() const selection = window.getSelection() const textNode = document.createTextNode("@") @@ -179,7 +187,7 @@ selection.getRangeAt(0).insertNode(spaceNode) selection.collapse(input, 1) - autocomplete({profile, force: true}) + autocomplete({person, force: true}) } const createNewLines = (n = 1) => { @@ -258,7 +266,7 @@ - autocomplete({profile})}> + autocomplete({person})}>
diff --git a/src/app/shared/NoteContentKind40.svelte b/src/app/shared/NoteContentKind40.svelte index 6c9eb124..b72baf2f 100644 --- a/src/app/shared/NoteContentKind40.svelte +++ b/src/app/shared/NoteContentKind40.svelte @@ -7,27 +7,27 @@ import Card from "src/partials/Card.svelte" import Content from "src/partials/Content.svelte" import ImageCircle from "src/partials/ImageCircle.svelte" - import {Nip28} from "src/app/engine" + import {channels} from "src/engine2" export let note const {name, picture, about} = tryJson(() => JSON.parse(note.content)) - const channel = Nip28.channels + const channel = channels .key(note.id) - .derived(defaultTo({id: note.id, name, picture, about})) + .derived(defaultTo({id: note.id, meta: {name, picture, about}})) const noteId = nip19.noteEncode(note.kind === 40 ? note.id : Tags.from(note).getMeta("e")) navigate(`/chat/${noteId}`)}>
- {#if $channel.picture} - + {#if $channel.meta?.picture} + {/if} -

{$channel.name}

+

{$channel.meta?.name}

- {#if $channel.about} -

{$channel.about}

+ {#if $channel.meta?.about} +

{$channel.meta?.about}

{/if}
diff --git a/src/app/shared/NoteContentQuote.svelte b/src/app/shared/NoteContentQuote.svelte index c68a4bca..0e916b97 100644 --- a/src/app/shared/NoteContentQuote.svelte +++ b/src/app/shared/NoteContentQuote.svelte @@ -9,8 +9,8 @@ import Card from "src/partials/Card.svelte" import Spinner from "src/partials/Spinner.svelte" import PersonCircle from "src/app/shared/PersonCircle.svelte" - import {getSetting, isEventMuted, getEventHints, mergeHints} from "src/engine2" - import {Directory, Network} from "src/app/engine" + import {getSetting, Subscription, isEventMuted, getEventHints, mergeHints} from "src/engine2" + import {Directory} from "src/app/engine" export let note export let value @@ -29,23 +29,24 @@ getEventHints(getSetting("relay_limit"), note), ]) - const filter = ( + const filters = [ id ? {ids: [id]} : filterVals(xs => xs.length > 0, { "#d": [identifier], kinds: [kind], authors: [pubkey], - }) - ) as Filter + }), + ] as Filter[] - const onEvent = event => { + const sub = new Subscription({timeout: 30000, relays, filters}) + + sub.on("event", event => { loading = false muted = isEventMuted(event).get() quote = event - } - - const sub = Network.subscribe({timeout: 30000, relays, filter, onEvent}) + sub.close() + }) const openQuote = e => { const noteId = id || quote?.id diff --git a/src/app/shared/PersonHandle.svelte b/src/app/shared/PersonHandle.svelte index 1215b9cd..4dbc3d05 100644 --- a/src/app/shared/PersonHandle.svelte +++ b/src/app/shared/PersonHandle.svelte @@ -1,13 +1,13 @@
- {$handle ? Nip05.displayHandle($handle) : npub} + {$person?.handle ? displayHandle($person.handle) : npub}
diff --git a/src/app/shared/PersonList.svelte b/src/app/shared/PersonList.svelte index cad1a9cf..a8b87fc4 100644 --- a/src/app/shared/PersonList.svelte +++ b/src/app/shared/PersonList.svelte @@ -5,9 +5,8 @@ import Content from "src/partials/Content.svelte" import Spinner from "src/partials/Spinner.svelte" import PersonSummary from "src/app/shared/PersonSummary.svelte" - import {loadPubkeys} from "src/engine2" - import {getSetting, getPubkeyHints, follows} from "src/engine2" - import {Network} from "src/app/engine" + import type {Event} from "src/engine2" + import {Subscription, getSetting, loadPubkeys, getPubkeyHints, follows} from "src/engine2" export let type export let pubkey @@ -18,17 +17,21 @@ if (type === "follows") { pubkeys = $follows } else { - const sub = Network.subscribe({ + const sub = new Subscription({ relays: getPubkeyHints(getSetting("relay_limit"), pubkey, "read"), - filter: {kinds: [3], "#p": [pubkey]}, - onEvent: batch(500, events => { + filters: [{kinds: [3], "#p": [pubkey]}], + }) + + sub.on( + "event", + batch(500, (events: Event[]) => { const newPubkeys = pluck("pubkey", events) loadPubkeys(newPubkeys) pubkeys = uniq(pubkeys.concat(newPubkeys)) - }), - }) + }) + ) return sub.close } diff --git a/src/app/shared/PersonStats.svelte b/src/app/shared/PersonStats.svelte index 0dcc00f1..d35c81d1 100644 --- a/src/app/shared/PersonStats.svelte +++ b/src/app/shared/PersonStats.svelte @@ -5,8 +5,9 @@ import {tweened} from "svelte/motion" import {numberFmt} from "src/util/misc" import {modal} from "src/partials/state" - import {people, getPubkeyHints} from "src/engine2" - import {Keys, Network} from "src/app/engine" + import type {Event} from "src/engine2" + import {people, count, Subscription, getPubkeyHints} from "src/engine2" + import {Keys} from "src/app/engine" export let pubkey @@ -25,26 +26,30 @@ canLoadFollowers = false // Get our followers count - const count = await Network.count({kinds: [3], "#p": [pubkey]}) + const total = await count([{kinds: [3], "#p": [pubkey]}]) - if (count) { - followersCount.set(count) + if (total) { + followersCount.set(total) } else { const followers = new Set() - sub = Network.subscribe({ + sub = new Subscription({ timeout: 30_000, - shouldProcess: false, + ephemeral: true, relays: getPubkeyHints(3, Keys.pubkey.get(), "read"), - filter: [{kinds: [3], "#p": [pubkey]}], - onEvent: batch(300, events => { + filters: [{kinds: [3], "#p": [pubkey]}], + }) + + sub.on( + "event", + batch(300, (events: Event[]) => { for (const e of events) { followers.add(e.pubkey) } followersCount.set(followers.size) - }), - }) + }) + ) } } diff --git a/src/app/shared/RelayStatus.svelte b/src/app/shared/RelayStatus.svelte index 96ad4ab3..71f2d395 100644 --- a/src/app/shared/RelayStatus.svelte +++ b/src/app/shared/RelayStatus.svelte @@ -1,7 +1,7 @@ - +
-
{$channel.name || ""}
-
{$channel.about || ""}
+
{$channel?.meta?.name || ""}
+
{$channel?.meta?.about || ""}
- {#if $channel.pubkey === Keys.pubkey.get()} + {#if $channel?.nip28?.owner === Keys.pubkey.get()} {/if} - {#if $channel.joined} + {#if $channel?.nip28?.joined} Leave diff --git a/src/app/views/Explore.svelte b/src/app/views/Explore.svelte index efa07af2..38456350 100644 --- a/src/app/views/Explore.svelte +++ b/src/app/views/Explore.svelte @@ -11,8 +11,8 @@ import Content from "src/partials/Content.svelte" import NoteById from "src/app/shared/NoteById.svelte" import PersonBadgeSmall from "src/app/shared/PersonBadgeSmall.svelte" - import {getUserRelayUrls, follows} from "src/engine2" - import engine, {Network, Keys} from "src/app/engine" + import {getUserRelayUrls, follows, Subscription} from "src/engine2" + import engine, {Keys} from "src/app/engine" type LabelGroup = { label: string @@ -63,13 +63,15 @@ const showGroup = ({label, ids, hints}) => modal.push({type: "label/detail", label, ids, hints}) onMount(() => { - const sub = Network.subscribe({ + const sub = new Subscription({ relays: getUserRelayUrls("read"), - filter: { - kinds: [1985], - "#L": ["#t", "ugc"], - authors: $follows.concat($pubkey), - }, + filters: [ + { + kinds: [1985], + "#L": ["#t", "ugc"], + authors: $follows.concat($pubkey), + }, + ], }) return () => sub.close() diff --git a/src/app/views/LoginConnect.svelte b/src/app/views/LoginConnect.svelte index 14e8c9d8..ee1b84d8 100644 --- a/src/app/views/LoginConnect.svelte +++ b/src/app/views/LoginConnect.svelte @@ -12,8 +12,8 @@ import Anchor from "src/partials/Anchor.svelte" import Modal from "src/partials/Modal.svelte" import RelayCard from "src/app/shared/RelayCard.svelte" - import {relays, loadPubkeys, getUserRelayUrls} from "src/engine2" - import {Env, Keys, Network} from "src/app/engine" + import {relays, pool, loadPubkeys, getUserRelayUrls} from "src/engine2" + import {Env, Keys} from "src/app/engine" import {loadAppData} from "src/app/state" const pubkey = Keys.pubkey.get() @@ -75,7 +75,7 @@ navigate("/notes") } else { - Network.pool.remove(relay.url) + pool.remove(relay.url) } }) } diff --git a/src/app/views/NoteCreate.svelte b/src/app/views/NoteCreate.svelte index 1e3cb5aa..0c17acb1 100644 --- a/src/app/views/NoteCreate.svelte +++ b/src/app/views/NoteCreate.svelte @@ -16,8 +16,8 @@ import RelayCard from "src/app/shared/RelayCard.svelte" import NoteContent from "src/app/shared/NoteContent.svelte" import RelaySearch from "src/app/shared/RelaySearch.svelte" - import {publishNote, displayRelay, getUserRelayUrls, mention} from "src/engine2" - import {Directory, Network, Keys} from "src/app/engine" + import {Publisher, publishNote, displayRelay, getUserRelayUrls, mention} from "src/engine2" + import {Directory, Keys} from "src/app/engine" import {modal} from "src/partials/state" import {toastProgress} from "src/app/state" @@ -45,7 +45,7 @@ tags.push(mention(quote.pubkey)) // Re-broadcast the note we're quoting - Network.publish({relays: $relays, event: quote}) + Publisher.publish({relays: $relays, event: quote}) } publishNote(content, tags, $relays).on("progress", toastProgress) diff --git a/src/app/views/PersonInfo.svelte b/src/app/views/PersonInfo.svelte index 85bbeb02..ad6ed725 100644 --- a/src/app/views/PersonInfo.svelte +++ b/src/app/views/PersonInfo.svelte @@ -4,12 +4,11 @@ import Content from "src/partials/Content.svelte" import CopyValue from "src/partials/CopyValue.svelte" import RelayCard from "src/app/shared/RelayCard.svelte" - import {getPubkeyHints} from "src/engine2" - import {Nip05} from "src/app/engine" + import {getPubkeyHints, displayHandle, people} from "src/engine2" export let pubkey - const handle = Nip05.getHandle(pubkey) + const person = people.key(pubkey) const relays = getPubkeyHints(3, pubkey, "write") const nprofile = nip19.nprofileEncode({pubkey, relays}) @@ -19,12 +18,12 @@

Details

- {#if handle} - {@const display = Nip05.displayHandle(handle)} + {#if $person?.handle} + {@const display = displayHandle($person.handle)} Nostr Address Relays - {#each handle.profile.relays || [] as url} + {#each $person.handle.profile.relays || [] as url} {:else}

diff --git a/src/engine/Engine.ts b/src/engine/Engine.ts index dcabe7e6..847d1013 100644 --- a/src/engine/Engine.ts +++ b/src/engine/Engine.ts @@ -4,12 +4,7 @@ import {Crypt} from "./components/Crypt" import {Directory} from "./components/Directory" import {Events} from "./components/Events" import {Keys} from "./components/Keys" -import {Network} from "./components/Network" import {Nip04} from "./components/Nip04" -import {Nip05} from "./components/Nip05" -import {Nip28} from "./components/Nip28" -import {Nip44} from "./components/Nip44" -import {Settings} from "./components/Settings" export class Engine { Env: Env @@ -18,12 +13,7 @@ export class Engine { Directory = new Directory() Events = new Events() Keys = new Keys() - Network = new Network() Nip04 = new Nip04() - Nip05 = new Nip05() - Nip28 = new Nip28() - Nip44 = new Nip44() - Settings = new Settings() constructor(Env: Env) { this.Env = Env diff --git a/src/engine/components/Network.ts b/src/engine/components/Network.ts deleted file mode 100644 index 679bbed0..00000000 --- a/src/engine/components/Network.ts +++ /dev/null @@ -1,215 +0,0 @@ -import {omit} from "ramda" -import {Pool, Plex, Relays, Executor} from "paravel" -import {noop, ensurePlural, union, difference} from "hurdak" -import {warn, error, info} from "src/util/logger" -import {normalizeRelayUrl} from "src/util/nostr" -import type {Event, Filter} from "src/engine/types" -import type {Engine} from "src/engine/Engine" -import {Subscription} from "src/engine/util/Subscription" - -export type Progress = { - event: Event - succeeded: Set - failed: Set - timeouts: Set - completed: Set - pending: Set -} - -export type PublishOpts = { - event: Event - relays: string[] - onProgress?: (p: Progress) => void - timeout?: number - verb?: string -} - -export type SubscribeOpts = { - relays: string[] - filter: Filter[] | Filter - onEvent?: (event: Event) => void - onEose?: (url: string) => void - onClose?: () => void - timeout?: number - shouldProcess?: boolean -} - -export class Network { - engine: Engine - pool = new Pool() - authHandler: (url: string, challenge: string) => void - - relayIsLowQuality = (url: string) => this.pool.get(url, {autoConnect: false})?.meta?.quality < 0.6 - - getUrls = (relays: string[]) => { - if (this.engine.Env.FORCE_RELAYS?.length > 0) { - return this.engine.Env.FORCE_RELAYS - } - - if (relays.length === 0) { - error(`Attempted to connect to zero urls`) - } - - const urls = new Set(relays.map(normalizeRelayUrl)) - - if (urls.size !== relays.length) { - warn(`Attempted to connect to non-unique relays`) - } - - return Array.from(urls) - } - - getExecutor = (urls: string[], {bypassBoot = false} = {}) => { - let target - - const muxUrl = this.engine.Settings.getSetting("multiplextr_url") - - // Try to use our multiplexer, but if it fails to connect fall back to relays. If - // we're only connecting to a single relay, just do it directly, unless we already - // have a connection to the multiplexer open, in which case we're probably doing - // AUTH with a single relay. - if (muxUrl && (urls.length > 1 || this.pool.has(muxUrl))) { - const connection = this.pool.get(muxUrl) - - if (connection.socket.isHealthy()) { - target = new Plex(urls, connection) - } - } - - if (!target) { - target = new Relays(urls.map(url => this.pool.get(url))) - } - - const executor = new Executor(target) - - if (this.authHandler) { - executor.handleAuth({onAuth: this.authHandler, onOk: noop}) - } - - return executor - } - - publish = ({ - relays, - event, - onProgress, - timeout = 3000, - verb = "EVENT", - }: PublishOpts): Promise => { - const urls = this.getUrls(relays) - const executor = this.getExecutor(urls, {bypassBoot: verb === "AUTH"}) - - if (event.wrap) { - throw new Error("Can't publish unwrapped event") - } - - info(`Publishing to ${urls.length} relays`, event, urls) - - return new Promise(resolve => { - const timeouts = new Set() - const succeeded = new Set() - const failed = new Set() - - const getProgress = () => { - const completed = union(timeouts, succeeded, failed) - const pending = difference(new Set(urls), completed) - - return {event, succeeded, failed, timeouts, completed, pending} - } - - const attemptToResolve = () => { - const progress = getProgress() - - if (progress.pending.size === 0) { - resolve(progress) - sub.unsubscribe() - executor.target.cleanup() - } - - onProgress?.(progress) - } - - setTimeout(() => { - for (const url of urls) { - if (!succeeded.has(url) && !failed.has(url)) { - timeouts.add(url) - } - } - - attemptToResolve() - }, timeout) - - const sub = executor.publish(omit(["seen_on"], event), { - verb, - onOk: (url: string) => { - succeeded.add(url) - timeouts.delete(url) - failed.delete(url) - attemptToResolve() - }, - onError: (url: string) => { - failed.add(url) - timeouts.delete(url) - attemptToResolve() - }, - }) - - // Report progress to start - attemptToResolve() - }) - } - - subscribe = ({ - relays, - filter, - onEose, - onEvent, - onClose, - timeout, - shouldProcess = true, - }: SubscribeOpts) => { - const urls = this.getUrls(relays) - const subscription = new Subscription({ - executor: this.getExecutor(urls), - filters: ensurePlural(filter), - relays: urls, - timeout, - }) - - info(`Starting subscription with ${urls.length} relays`, {filter, urls}) - - if (onEose) subscription.on("eose", onEose) - if (onClose) subscription.on("close", onClose) - - subscription.on("event", event => { - if (shouldProcess) { - this.engine.Events.queue.push(event) - } - - onEvent?.(event) - }) - - return subscription - } - - count = async (filter: Filter | Filter[]) => { - const filters = ensurePlural(filter) - const executor = this.getExecutor(this.getUrls(this.engine.Env.COUNT_RELAYS)) - - return new Promise(resolve => { - const sub = executor.count(filters, { - onCount: (url: string, {count}: {count: number}) => resolve(count), - }) - - setTimeout(() => { - resolve(0) - sub.unsubscribe() - executor.target.cleanup() - }, 3000) - }) - } - - initialize(engine: Engine) { - this.engine = engine - } -} diff --git a/src/engine/components/Nip05.ts b/src/engine/components/Nip05.ts deleted file mode 100644 index 69dc2c8a..00000000 --- a/src/engine/components/Nip05.ts +++ /dev/null @@ -1,46 +0,0 @@ -import {last} from "ramda" -import {tryFunc, Fetch} from "hurdak" -import {now, tryJson} from "src/util/misc" -import type {Handle} from "src/engine/types" -import type {Engine} from "src/engine/Engine" -import {collection} from "src/engine/util/store" - -export class Nip05 { - handles = collection("pubkey") - - getHandle = (pubkey: string) => this.handles.key(pubkey).get() - - displayHandle = (handle: Handle) => - handle.address.startsWith("_@") ? last(handle.address.split("@")) : handle.address - - initialize(engine: Engine) { - engine.Events.addHandler(0, e => { - tryJson(async () => { - const kind0 = JSON.parse(e.content) - const handle = this.handles.key(e.pubkey) - - if (!kind0.nip05 || e.created_at < (handle.get()?.created_at || 0)) { - return - } - - const body = {handle: kind0.nip05} - const url = engine.Settings.dufflepud("handle/info") - const profile = (await tryFunc(() => Fetch.postJson(url, body))) as null | { - pubkey: string - } - - if (profile?.pubkey !== e.pubkey) { - return - } - - handle.set({ - profile: profile, - pubkey: e.pubkey, - address: kind0.nip05, - created_at: e.created_at, - updated_at: now(), - }) - }) - }) - } -} diff --git a/src/engine/components/Nip44.ts b/src/engine/components/Nip44.ts deleted file mode 100644 index 05391ca7..00000000 --- a/src/engine/components/Nip44.ts +++ /dev/null @@ -1,81 +0,0 @@ -import {switcherFn, tryFunc} from "hurdak" -import {tryJson} from "src/util/misc" -import {LRUCache} from "src/util/lruCache" -import type {Engine} from "src/engine/Engine" -import * as nip44 from "src/engine/util/nip44" - -export type Nip44Opts = { - pk?: string - sk?: string -} - -const sharedSecretCache = new LRUCache(100) - -// Deriving shared secret is an expensive computation -function getSharedSecret(sk: string, pk: string) { - const cacheKey = `${sk.slice(0, 8)}:${pk}` - - let result = sharedSecretCache.get(cacheKey) - - if (!result) { - result = nip44.getSharedSecret(sk, pk) - - sharedSecretCache.set(cacheKey, result) - } - - return result -} - -export class Nip44 { - engine: Engine - - async encrypt(message: string, {pk, sk}: Nip44Opts = {}) { - if (!pk) { - pk = this.engine.Keys.pubkey.get() - } - - if (sk) { - return nip44.encrypt(getSharedSecret(sk, pk), message) - } - - return switcherFn(this.engine.Keys.method.get(), { - privkey: () => { - const privkey = this.engine.Keys.privkey.get() as string - const secret = getSharedSecret(privkey, pk) - - return nip44.encrypt(secret, message) - }, - }) - } - - async decrypt(message, {pk, sk}: Nip44Opts = {}) { - if (!pk) { - pk = this.engine.Keys.pubkey.get() - } - - if (sk) { - return nip44.decrypt(getSharedSecret(sk, pk), message) - } - - return switcherFn(this.engine.Keys.method.get(), { - privkey: () => { - const privkey = this.engine.Keys.privkey.get() as string - const secret = getSharedSecret(privkey, pk) - - return tryFunc(() => nip44.decrypt(secret, message)) || `` - }, - }) - } - - async encryptJson(data: any, opts?: Nip44Opts) { - return this.encrypt(JSON.stringify(data), opts) - } - - async decryptJson(data: string, opts?: Nip44Opts) { - return tryJson(async () => JSON.parse(await this.decrypt(data, opts))) - } - - initialize(engine: Engine) { - this.engine = engine - } -} diff --git a/src/engine/components/Settings.ts b/src/engine/components/Settings.ts deleted file mode 100644 index 3bf4b52f..00000000 --- a/src/engine/components/Settings.ts +++ /dev/null @@ -1,60 +0,0 @@ -import {Tags, appDataKeys} from "src/util/nostr" -import {writable} from "src/engine/util/store" -import type {Writable} from "src/engine/util/store" -import type {Engine} from "src/engine/Engine" - -export class Settings { - engine: Engine - settings: Writable> - - getSetting = (k: string) => this.settings.get()[k] - - imgproxy = (url: string, {w = 640, h = 1024} = {}) => { - const base = this.getSetting("imgproxy_url") - - if (!url || url.match("gif$")) { - return url - } - - try { - return base && url ? `${base}/x/s:${w}:${h}/${btoa(url)}` : url - } catch (e) { - return url - } - } - - dufflepud = (path: string) => `${this.getSetting("dufflepud_url")}/${path}` - - initialize(engine: Engine) { - this.engine = engine - - this.settings = writable>({ - last_updated: 0, - relay_limit: 10, - default_zap: 21, - show_media: true, - report_analytics: true, - imgproxy_url: engine.Env.IMGPROXY_URL, - dufflepud_url: engine.Env.DUFFLEPUD_URL, - multiplextr_url: engine.Env.MULTIPLEXTR_URL, - }) - - engine.Events.addHandler(30078, async e => { - if ( - engine.Keys.canSign.get() && - Tags.from(e).getMeta("d") === appDataKeys.USER_SETTINGS && - e.created_at > this.getSetting("last_updated") - ) { - const updates = await engine.Crypt.decryptJson(e.content) - - if (updates) { - this.settings.update($settings => ({ - ...$settings, - ...updates, - last_updated: e.created_at, - })) - } - } - }) - } -} diff --git a/src/engine/index.ts b/src/engine/index.ts index 2f93539e..14e92cc7 100644 --- a/src/engine/index.ts +++ b/src/engine/index.ts @@ -6,8 +6,4 @@ export {Crypt} from "./components/Crypt" export {Directory} from "./components/Directory" export {Events} from "./components/Events" export {Keys} from "./components/Keys" -export {Network} from "./components/Network" export {Nip04} from "./components/Nip04" -export {Nip05} from "./components/Nip05" -export {Nip28} from "./components/Nip28" -export {Nip44} from "./components/Nip44" diff --git a/src/engine/util/nip44.ts b/src/engine/util/nip44.ts deleted file mode 100644 index eaef47f7..00000000 --- a/src/engine/util/nip44.ts +++ /dev/null @@ -1,43 +0,0 @@ -import {base64} from "@scure/base" -import {randomBytes} from "@noble/hashes/utils" -import {secp256k1} from "@noble/curves/secp256k1" -import {sha256} from "@noble/hashes/sha256" -import {xchacha20} from "@noble/ciphers/chacha" - -export const utf8Decoder = new TextDecoder() - -export const utf8Encoder = new TextEncoder() - -export const getSharedSecret = (privkey: string, pubkey: string): Uint8Array => - sha256(secp256k1.getSharedSecret(privkey, "02" + pubkey).subarray(1, 33)) - -export function encrypt(key: Uint8Array, text: string, v = 1) { - if (v !== 1) { - throw new Error("NIP44: unknown encryption version") - } - - const nonce = randomBytes(24) - const plaintext = utf8Encoder.encode(text) - const ciphertext = xchacha20(key, nonce, plaintext) - - const payload = new Uint8Array(25 + ciphertext.length) - payload.set([v], 0) - payload.set(nonce, 1) - payload.set(ciphertext, 25) - - return base64.encode(payload) -} - -export function decrypt(key: Uint8Array, payload: string) { - const data = base64.decode(payload) - - if (data[0] !== 1) { - throw new Error(`NIP44: unknown encryption version: ${data[0]}`) - } - - const nonce = data.slice(1, 25) - const ciphertext = data.slice(25) - const plaintext = xchacha20(key, nonce, ciphertext) - - return utf8Decoder.decode(plaintext) -} diff --git a/src/engine2/queries/nip65.ts b/src/engine2/queries/nip65.ts index 439dd2e3..4f2432d3 100644 --- a/src/engine2/queries/nip65.ts +++ b/src/engine2/queries/nip65.ts @@ -21,11 +21,9 @@ export const displayRelay = ({url}: Relay) => last(url.split("://")) export const searchRelays = relays.derived($relays => fuzzy($relays, {keys: ["url"]})) export const searchableRelays = relays.derived($relays => { - const searchableRelayUrls = $relays - .filter(r => (r.info?.supported_nips || []).includes(50)) - .map(prop("url")) + const urls = $relays.filter(r => (r.info?.supported_nips || []).includes(50)).map(prop("url")) - return uniq(env.get().SEARCH_RELAYS.concat(searchableRelayUrls)).slice(0, 8) as string[] + return uniq(env.get().SEARCH_RELAYS.concat(urls)).slice(0, 8) as string[] }) export const getPubkeyRelays = (pubkey: string, mode: string = null) => { diff --git a/src/engine2/requests/count.ts b/src/engine2/requests/count.ts new file mode 100644 index 00000000..d2686f0a --- /dev/null +++ b/src/engine2/requests/count.ts @@ -0,0 +1,18 @@ +import type {Filter} from "src/engine2/model" +import {getUrls, getExecutor, searchableRelays} from "src/engine2/queries" + +export const count = async (filters: Filter[]) => { + const executor = getExecutor(getUrls(searchableRelays.get())) + + return new Promise(resolve => { + const sub = executor.count(filters, { + onCount: (url: string, {count}: {count: number}) => resolve(count), + }) + + setTimeout(() => { + resolve(0) + sub.unsubscribe() + executor.target.cleanup() + }, 3000) + }) +} diff --git a/src/engine2/requests/index.ts b/src/engine2/requests/index.ts index 796e03b4..f99be962 100644 --- a/src/engine2/requests/index.ts +++ b/src/engine2/requests/index.ts @@ -2,6 +2,8 @@ export * from "./context" export * from "./cursor" export * from "./feed" export * from "./pubkeys" +export * from "./count" +export * from "./load" export * from "./subscription" export * from "./thread" export * from "./nip04" diff --git a/src/engine2/requests/load.ts b/src/engine2/requests/load.ts new file mode 100644 index 00000000..1030f07d --- /dev/null +++ b/src/engine2/requests/load.ts @@ -0,0 +1,76 @@ +import {matchFilters} from "nostr-tools" +import {prop, groupBy, uniq} from "ramda" +import {batch} from "hurdak" +import {subscribe} from "./subscription" +import type {Event, Filter} from "src/engine2/model" + +export type LoadOpts = { + relays: string[] + filters: Filter[] + onEvent?: (e: Event) => void + onEose?: (url: string) => void + onClose?: (events: Event[]) => void +} + +export const calculateGroup = ({since, until, ...filter}: Filter) => { + const group = Object.keys(filter) + + if (since) { + group.push(`since:${since}`) + } + + if (until) { + group.push(`until:${until}`) + } + + return group.sort().join("-") +} + +export const combineFilters = filters => { + const result = [] + + for (const group of Object.values(groupBy(calculateGroup, filters))) { + const newFilter = {} + + for (const k of Object.keys(group[0])) { + newFilter[k] = uniq(group.flatMap(prop(k))) + } + + result.push(newFilter) + } + + return result +} + +export const load = batch(500, (requests: LoadOpts[]) => { + const relays = uniq(requests.flatMap(prop("relays"))) + const filters = combineFilters(requests.flatMap(prop("filters"))) + + const sub = subscribe({relays, filters, timeout: 3000}) + + sub.on("event", (e: Event) => { + for (const req of requests) { + if (!req.onEvent) { + continue + } + + if (matchFilters(req.filters, e)) { + req.onEvent(e) + } + } + }) + + sub.on("eose", url => { + for (const req of requests) { + req.onEose?.(url) + } + }) + + sub.on("close", events => { + for (const req of requests) { + req.onClose?.(events) + } + }) + + return sub +}) diff --git a/src/engine2/requests/subscription.ts b/src/engine2/requests/subscription.ts index 1ea390d6..9bcd8118 100644 --- a/src/engine2/requests/subscription.ts +++ b/src/engine2/requests/subscription.ts @@ -7,7 +7,7 @@ import type {Event, Filter} from "src/engine2/model" import {getUrls, getExecutor} from "src/engine2/queries" import {projections} from "src/engine2/projections" -type SubscriptionOpts = { +export type SubscriptionOpts = { relays: string[] filters: Filter[] timeout?: number @@ -102,3 +102,19 @@ export class Subscription extends EventEmitter { } } } + +export type SubscribeOpts = SubscriptionOpts & { + onEvent?: (e: Event) => void + onEose?: (url: string) => void + onClose?: (events: Event[]) => void +} + +export const subscribe = (opts: SubscribeOpts) => { + const sub = new Subscription(opts) + + if (opts.onEvent) sub.on("event", opts.onEvent) + if (opts.onEose) sub.on("eose", opts.onEose) + if (opts.onClose) sub.on("close", opts.onClose) + + return sub +}