diff --git a/.drone.yml b/.drone.yml index b4a064939..d9b03b43f 100644 --- a/.drone.yml +++ b/.drone.yml @@ -6,7 +6,7 @@ concurrency: limit: 1 trigger: branch: - - main + - main metadata: namespace: git steps: @@ -34,9 +34,9 @@ steps: - img build -t voidic/snort:latest --platform linux/amd64,linux/arm64 -f Dockerfile.prebuilt . - img push voidic/snort:latest volumes: -- name: cache - claim: - name: docker-cache + - name: cache + claim: + name: docker-cache --- kind: pipeline type: kubernetes @@ -60,9 +60,9 @@ steps: - yarn workspace @snort/app eslint - yarn workspace @snort/app prettier --check . volumes: -- name: cache - claim: - name: docker-cache + - name: cache + claim: + name: docker-cache --- kind: pipeline type: kubernetes @@ -71,7 +71,7 @@ concurrency: limit: 1 trigger: branch: - - main + - main metadata: namespace: git steps: @@ -98,9 +98,9 @@ steps: - 'git commit -a -m "chore: Update translations"' - git push -u origin main volumes: -- name: cache - claim: - name: docker-cache + - name: cache + claim: + name: docker-cache --- kind: pipeline type: kubernetes @@ -109,7 +109,7 @@ concurrency: limit: 1 trigger: event: - - tag + - tag metadata: namespace: git steps: @@ -137,6 +137,6 @@ steps: - img build -t voidic/snort:$DRONE_TAG --platform linux/amd64,linux/arm64 -f Dockerfile.prebuilt . - img push voidic/snort:$DRONE_TAG volumes: -- name: cache - claim: - name: docker-cache \ No newline at end of file + - name: cache + claim: + name: docker-cache diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0e05e72ca..661d1a92b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -29,13 +29,13 @@ jobs: - name: Rust cache uses: swatinem/rust-cache@v2 with: - workspaces: './src-tauri -> target' + workspaces: "./src-tauri -> target" - name: Sync node version and setup cache uses: actions/setup-node@v3 with: - node-version: '16' - cache: 'yarn' + node-version: "16" + cache: "yarn" - name: Install frontend dependencies run: yarn install - name: Build the app @@ -44,7 +44,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: tagName: ${{ github.ref_name }} - releaseName: 'Snort v__VERSION__' - releaseBody: 'See the assets to download and install this version.' + releaseName: "Snort v__VERSION__" + releaseBody: "See the assets to download and install this version." releaseDraft: true - prerelease: false \ No newline at end of file + prerelease: false diff --git a/.vscode/settings.json b/.vscode/settings.json index 2291b0962..fd67ac9d2 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,11 +1,11 @@ { - "files.exclude": { - "**/.git": true, - "**/.svn": true, - "**/.hg": true, - "**/CVS": true, - "**/.DS_Store": true, - "**/Thumbs.db": true, - "**/node_modules": true - } -} \ No newline at end of file + "files.exclude": { + "**/.git": true, + "**/.svn": true, + "**/.hg": true, + "**/CVS": true, + "**/.DS_Store": true, + "**/Thumbs.db": true, + "**/node_modules": true + } +} diff --git a/README.md b/README.md index d38ac3609..6a0e2888a 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,7 @@ $ yarn build Translations are managed on [Crowdin](https://crowdin.com/project/snort) To extract translations run: + ```bash yarn workspace @snort/app intl-extract yarn workspace @snort/app intl-compile diff --git a/functions/e/[id].ts b/functions/e/[id].ts index d41b80b3a..519a0dc9a 100644 --- a/functions/e/[id].ts +++ b/functions/e/[id].ts @@ -1,7 +1,6 @@ -interface Env { -} +interface Env {} -export const onRequest: PagesFunction = async (context) => { +export const onRequest: PagesFunction = async context => { const id = context.params.id as string; const next = await context.next(); @@ -11,16 +10,16 @@ export const onRequest: PagesFunction = async (context) => { body: await next.arrayBuffer(), headers: { "user-agent": "Snort-Functions/1.0 (https://snort.social)", - "content-type": "text/plain" - } + "content-type": "text/plain", + }, }); if (rsp.ok) { const body = await rsp.text(); if (body.length > 0) { return new Response(body, { headers: { - "content-type": "text/html" - } + "content-type": "text/html", + }, }); } } @@ -28,4 +27,4 @@ export const onRequest: PagesFunction = async (context) => { // ignore } return next; -} \ No newline at end of file +}; diff --git a/functions/p/[id].ts b/functions/p/[id].ts index ced38bc90..dfe501058 100644 --- a/functions/p/[id].ts +++ b/functions/p/[id].ts @@ -1,7 +1,6 @@ -interface Env { -} +interface Env {} -export const onRequest: PagesFunction = async (context) => { +export const onRequest: PagesFunction = async context => { const id = context.params.id as string; const next = await context.next(); @@ -11,16 +10,16 @@ export const onRequest: PagesFunction = async (context) => { body: await next.arrayBuffer(), headers: { "user-agent": "Snort-Functions/1.0 (https://snort.social)", - "content-type": "text/plain" - } + "content-type": "text/plain", + }, }); if (rsp.ok) { const body = await rsp.text(); if (body.length > 0) { return new Response(body, { headers: { - "content-type": "text/html" - } + "content-type": "text/html", + }, }); } } @@ -28,4 +27,4 @@ export const onRequest: PagesFunction = async (context) => { // ignore } return next; -} \ No newline at end of file +}; diff --git a/functions/tsconfig.json b/functions/tsconfig.json index e0b6870ed..da95421c9 100644 --- a/functions/tsconfig.json +++ b/functions/tsconfig.json @@ -1,8 +1,8 @@ { - "compilerOptions": { - "target": "esnext", - "module": "esnext", - "lib": ["esnext"], - "types": ["@cloudflare/workers-types"] - } - } \ No newline at end of file + "compilerOptions": { + "target": "esnext", + "module": "esnext", + "lib": ["esnext"], + "types": ["@cloudflare/workers-types"] + } +} diff --git a/package.json b/package.json index d53cfa79d..dbf87f14d 100644 --- a/package.json +++ b/package.json @@ -11,5 +11,10 @@ "devDependencies": { "@tauri-apps/cli": "^1.2.3", "@cloudflare/workers-types": "^4.20230307.0" + }, + "prettier": { + "printWidth": 120, + "bracketSameLine": true, + "arrowParens": "avoid" } -} \ No newline at end of file +} diff --git a/packages/app/.prettierrc.json b/packages/app/.prettierrc.json deleted file mode 100644 index e1280035d..000000000 --- a/packages/app/.prettierrc.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "printWidth": 120, - "bracketSameLine": true, - "arrowParens": "avoid" -} diff --git a/packages/nostr/package.json b/packages/nostr/package.json index f107e8e17..b3daa4dfa 100644 --- a/packages/nostr/package.json +++ b/packages/nostr/package.json @@ -65,4 +65,4 @@ "author": "", "license": "ISC", "description": "" -} \ No newline at end of file +} diff --git a/packages/nostr/src/client/index.ts b/packages/nostr/src/client/index.ts index 2f7ccf661..4799ac174 100644 --- a/packages/nostr/src/client/index.ts +++ b/packages/nostr/src/client/index.ts @@ -1,7 +1,7 @@ import { NostrError } from "../common" import { RawEvent, parseEvent } from "../event" import { Conn } from "./conn" -import * as utils from "@noble/curves/abstract/utils"; +import * as utils from "@noble/curves/abstract/utils" import { EventEmitter } from "./emitter" import { fetchRelayInfo, ReadyState, Relay } from "./relay" import { Filters } from "../filters" @@ -71,9 +71,9 @@ export class Nostr extends EventEmitter { opts?.fetchInfo === false ? Promise.resolve({}) : fetchRelayInfo(relayUrl).catch((e) => { - this.#error(e) - return {} - }) + this.#error(e) + return {} + }) // If there is no existing connection, open a new one. const conn = new Conn({ @@ -128,7 +128,8 @@ export class Nostr extends EventEmitter { if (conn.relay.readyState !== ReadyState.CONNECTING) { this.#error( new NostrError( - `bug: expected connection to ${relayUrl.toString()} to have readyState CONNECTING, got ${conn.relay.readyState + `bug: expected connection to ${relayUrl.toString()} to have readyState CONNECTING, got ${ + conn.relay.readyState }` ) ) @@ -293,7 +294,7 @@ export class Nostr extends EventEmitter { relay.info === undefined ? undefined : // Deep copy of the info. - JSON.parse(JSON.stringify(relay.info)) + JSON.parse(JSON.stringify(relay.info)) return { ...relay, info } } }) diff --git a/packages/nostr/src/crypto.ts b/packages/nostr/src/crypto.ts index d68aeffa8..0a1edfae8 100644 --- a/packages/nostr/src/crypto.ts +++ b/packages/nostr/src/crypto.ts @@ -1,6 +1,6 @@ import * as secp from "@noble/curves/secp256k1" -import * as utils from "@noble/curves/abstract/utils"; -import {sha256 as sha} from "@noble/hashes/sha256"; +import * as utils from "@noble/curves/abstract/utils" +import { sha256 as sha } from "@noble/hashes/sha256" import base64 from "base64-js" import { bech32 } from "bech32" @@ -92,11 +92,7 @@ export function schnorrSign(data: Hex, priv: PrivateKey): Hex { /** * Verify that the elliptic curve signature is correct. */ -export function schnorrVerify( - sig: Hex, - data: Hex, - key: PublicKey -): boolean { +export function schnorrVerify(sig: Hex, data: Hex, key: PublicKey): boolean { return secp.schnorr.verify(sig.toString(), data.toString(), key.toString()) } diff --git a/packages/nostr/src/event/index.ts b/packages/nostr/src/event/index.ts index 1cd8c2030..6bfc32400 100644 --- a/packages/nostr/src/event/index.ts +++ b/packages/nostr/src/event/index.ts @@ -159,14 +159,14 @@ export async function signEvent( * Parse an event from its raw format. */ export function parseEvent(event: RawEvent): Event { - if (event.id !== (serializeEventId(event))) { + if (event.id !== serializeEventId(event)) { throw new NostrError( `invalid id ${event.id} for event ${JSON.stringify( event )}, expected ${serializeEventId(event)}` ) } - if (!(schnorrVerify(event.sig, event.id, event.pubkey))) { + if (!schnorrVerify(event.sig, event.id, event.pubkey)) { throw new NostrError(`invalid signature for event ${JSON.stringify(event)}`) } @@ -221,9 +221,7 @@ export function parseEvent(event: RawEvent): Event { } } -function serializeEventId( - event: UnsignedWithPubkey -): EventId { +function serializeEventId(event: UnsignedWithPubkey): EventId { const serialized = JSON.stringify([ 0, event.pubkey, diff --git a/packages/nostr/webpack.config.js b/packages/nostr/webpack.config.js index d75c71b27..e4f006cc8 100644 --- a/packages/nostr/webpack.config.js +++ b/packages/nostr/webpack.config.js @@ -1,6 +1,6 @@ const fs = require("fs") -const isProduction = process.env.NODE_ENV == "production"; +const isProduction = process.env.NODE_ENV == "production" const entry = { lib: "./src/index.ts", diff --git a/packages/shared/src/const.ts b/packages/shared/src/const.ts index 051e6842e..cba08f804 100644 --- a/packages/shared/src/const.ts +++ b/packages/shared/src/const.ts @@ -1,8 +1,6 @@ - /** * Regex to match email address */ - export const EmailRegex = - // eslint-disable-next-line no-useless-escape - /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; - \ No newline at end of file +export const EmailRegex = + // eslint-disable-next-line no-useless-escape + /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; diff --git a/packages/shared/src/d.ts b/packages/shared/src/d.ts index 7d1eb7b3a..eaf4717b4 100644 --- a/packages/shared/src/d.ts +++ b/packages/shared/src/d.ts @@ -1,14 +1,13 @@ - declare module "light-bolt11-decoder" { - export function decode(pr?: string): ParsedInvoice; + export function decode(pr?: string): ParsedInvoice; - export interface ParsedInvoice { - paymentRequest: string; - sections: Section[]; - } + export interface ParsedInvoice { + paymentRequest: string; + sections: Section[]; + } - export interface Section { - name: string; - value: string | Uint8Array | number | undefined; - } + export interface Section { + name: string; + value: string | Uint8Array | number | undefined; + } } diff --git a/packages/shared/src/feed-cache.ts b/packages/shared/src/feed-cache.ts index 22be7384b..bfa4a0330 100644 --- a/packages/shared/src/feed-cache.ts +++ b/packages/shared/src/feed-cache.ts @@ -38,7 +38,7 @@ export abstract class FeedCache { } async preload() { - const keys = await this.table?.toCollection().primaryKeys() ?? []; + const keys = (await this.table?.toCollection().primaryKeys()) ?? []; this.onTable = new Set(keys.map(a => a as string)); } diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 9d7fa865d..4e284bb91 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -3,4 +3,4 @@ export * from "./lnurl"; export * from "./utils"; export * from "./work-queue"; export * from "./feed-cache"; -export * from "./invoices"; \ No newline at end of file +export * from "./invoices"; diff --git a/packages/shared/src/invoices.ts b/packages/shared/src/invoices.ts index 050a470e0..fb9660253 100644 --- a/packages/shared/src/invoices.ts +++ b/packages/shared/src/invoices.ts @@ -1,48 +1,55 @@ - import { bytesToHex } from "@noble/hashes/utils"; import { decode as invoiceDecode } from "light-bolt11-decoder"; export interface InvoiceDetails { - amount?: number; - expire?: number; - timestamp?: number; - description?: string; - descriptionHash?: string; - paymentHash?: string; - expired: boolean; - pr: string; + amount?: number; + expire?: number; + timestamp?: number; + description?: string; + descriptionHash?: string; + paymentHash?: string; + expired: boolean; + pr: string; } export function decodeInvoice(pr: string): InvoiceDetails | undefined { - try { - const parsed = invoiceDecode(pr); + try { + const parsed = invoiceDecode(pr); - const amountSection = parsed.sections.find(a => a.name === "amount"); - const amount = amountSection ? Number(amountSection.value as number | string) : undefined; + const amountSection = parsed.sections.find(a => a.name === "amount"); + const amount = amountSection ? Number(amountSection.value as number | string) : undefined; - const timestampSection = parsed.sections.find(a => a.name === "timestamp"); - const timestamp = timestampSection ? Number(timestampSection.value as number | string) : undefined; + const timestampSection = parsed.sections.find(a => a.name === "timestamp"); + const timestamp = timestampSection ? Number(timestampSection.value as number | string) : undefined; - const expirySection = parsed.sections.find(a => a.name === "expiry"); - const expire = expirySection ? Number(expirySection.value as number | string) : undefined; - const descriptionSection = parsed.sections.find(a => a.name === "description")?.value; - const descriptionHashSection = parsed.sections.find(a => a.name === "description_hash")?.value; - const paymentHashSection = parsed.sections.find(a => a.name === "payment_hash")?.value; - const ret = { - pr, - amount: amount, - expire: timestamp && expire ? timestamp + expire : undefined, - timestamp: timestamp, - description: descriptionSection as string | undefined, - descriptionHash: descriptionHashSection ? (typeof descriptionHashSection === "string" ? descriptionHashSection as string : bytesToHex(descriptionHashSection as Uint8Array)) : undefined, - paymentHash: paymentHashSection ? (typeof paymentHashSection === "string" ? paymentHashSection as string : bytesToHex(paymentHashSection as Uint8Array)) : undefined, - expired: false, - }; - if (ret.expire) { - ret.expired = ret.expire < new Date().getTime() / 1000; - } - return ret; - } catch (e) { - console.error(e); + const expirySection = parsed.sections.find(a => a.name === "expiry"); + const expire = expirySection ? Number(expirySection.value as number | string) : undefined; + const descriptionSection = parsed.sections.find(a => a.name === "description")?.value; + const descriptionHashSection = parsed.sections.find(a => a.name === "description_hash")?.value; + const paymentHashSection = parsed.sections.find(a => a.name === "payment_hash")?.value; + const ret = { + pr, + amount: amount, + expire: timestamp && expire ? timestamp + expire : undefined, + timestamp: timestamp, + description: descriptionSection as string | undefined, + descriptionHash: descriptionHashSection + ? typeof descriptionHashSection === "string" + ? (descriptionHashSection as string) + : bytesToHex(descriptionHashSection as Uint8Array) + : undefined, + paymentHash: paymentHashSection + ? typeof paymentHashSection === "string" + ? (paymentHashSection as string) + : bytesToHex(paymentHashSection as Uint8Array) + : undefined, + expired: false, + }; + if (ret.expire) { + ret.expired = ret.expire < new Date().getTime() / 1000; } + return ret; + } catch (e) { + console.error(e); + } } diff --git a/packages/shared/src/lnurl.ts b/packages/shared/src/lnurl.ts index bd63dc2fc..34b3c8fc2 100644 --- a/packages/shared/src/lnurl.ts +++ b/packages/shared/src/lnurl.ts @@ -205,26 +205,26 @@ export class LNURL { } export interface LNURLService { - tag: string - nostrPubkey?: string - minSendable?: number - maxSendable?: number - metadata: string - callback: string - commentAllowed?: number + tag: string; + nostrPubkey?: string; + minSendable?: number; + maxSendable?: number; + metadata: string; + callback: string; + commentAllowed?: number; } export interface LNURLStatus { - status: "SUCCESS" | "ERROR" - reason?: string + status: "SUCCESS" | "ERROR"; + reason?: string; } export interface LNURLInvoice extends LNURLStatus { - pr?: string - successAction?: LNURLSuccessAction + pr?: string; + successAction?: LNURLSuccessAction; } export interface LNURLSuccessAction { - description?: string - url?: string + description?: string; + url?: string; } diff --git a/packages/shared/src/utils.ts b/packages/shared/src/utils.ts index f0faec9ff..d32a683a7 100644 --- a/packages/shared/src/utils.ts +++ b/packages/shared/src/utils.ts @@ -71,7 +71,10 @@ export function countMembers(a: any) { return ret; } -export function equalProp(a: string | number | Array | undefined, b: string | number | Array | undefined) { +export function equalProp( + a: string | number | Array | undefined, + b: string | number | Array | undefined +) { if ((a !== undefined && b === undefined) || (a === undefined && b !== undefined)) { return false; } @@ -130,7 +133,7 @@ export function appendDedupe(a?: Array, b?: Array) { export const sha256 = (str: string | Uint8Array): string => { return utils.bytesToHex(sha2(str)); -} +}; export function getPublicKey(privKey: string) { return utils.bytesToHex(secp.schnorr.getPublicKey(privKey)); diff --git a/packages/system-react/README.md b/packages/system-react/README.md index 001b3af20..fbb237b10 100644 --- a/packages/system-react/README.md +++ b/packages/system-react/README.md @@ -3,54 +3,49 @@ React hooks for @snort/system Sample: + ```js -import { useMemo } from "react" +import { useMemo } from "react"; import { useRequestBuilder, useUserProfile } from "@snort/system-react"; -import { FlatNoteStore, NostrSystem, RequestBuilder, TaggedRawEvent } from "@snort/system" +import { FlatNoteStore, NostrSystem, RequestBuilder, TaggedRawEvent } from "@snort/system"; // singleton nostr system class const System = new NostrSystem({}); // some bootstrap relays -[ - "wss://relay.snort.social", - "wss://nos.lol" -].forEach(r => System.ConnectToRelay(r, { read: true, write: false })); +["wss://relay.snort.social", "wss://nos.lol"].forEach(r => System.ConnectToRelay(r, { read: true, write: false })); export function Note({ ev }: { ev: TaggedRawEvent }) { - // get profile from cache or request a profile from relays - const profile = useUserProfile(System, ev.pubkey); + // get profile from cache or request a profile from relays + const profile = useUserProfile(System, ev.pubkey); - return
- Post by: {profile.name ?? profile.display_name} -

- {ev.content} -

+ return ( +
+ Post by: {profile.name ?? profile.display_name} +

{ev.content}

+ ); } export function UserPosts(props: { pubkey: string }) { - const sub = useMemo(() => { - const rb = new RequestBuilder("get-posts"); - rb.withFilter() - .authors([props.pubkey]) - .kinds([1]) - .limit(10); + const sub = useMemo(() => { + const rb = new RequestBuilder("get-posts"); + rb.withFilter().authors([props.pubkey]).kinds([1]).limit(10); - return rb; - }, [props.pubkey]); + return rb; + }, [props.pubkey]); - const data = useRequestBuilder(System, FlatNoteStore, sub); - return ( - <> - {data.data.map(a => )} - - ) + const data = useRequestBuilder < FlatNoteStore > (System, FlatNoteStore, sub); + return ( + <> + {data.data.map(a => ( + + ))} + + ); } export function MyApp() { - return ( - - ) + return ; } -``` \ No newline at end of file +``` diff --git a/packages/system-react/example/example.tsx b/packages/system-react/example/example.tsx index abbba635f..04086e680 100644 --- a/packages/system-react/example/example.tsx +++ b/packages/system-react/example/example.tsx @@ -1,48 +1,42 @@ -import { useMemo } from "react" +import { useMemo } from "react"; import { useRequestBuilder, useUserProfile } from "../src"; -import { FlatNoteStore, NostrSystem, RequestBuilder, TaggedRawEvent } from "@snort/system" +import { FlatNoteStore, NostrSystem, RequestBuilder, TaggedRawEvent } from "@snort/system"; const System = new NostrSystem({}); // some bootstrap relays -[ - "wss://relay.snort.social", - "wss://nos.lol" -].forEach(r => System.ConnectToRelay(r, { read: true, write: false })); +["wss://relay.snort.social", "wss://nos.lol"].forEach(r => System.ConnectToRelay(r, { read: true, write: false })); export function Note({ ev }: { ev: TaggedRawEvent }) { - const profile = useUserProfile(System, ev.pubkey); + const profile = useUserProfile(System, ev.pubkey); - return
- Post by: {profile.name ?? profile.display_name} -

- {ev.content} -

+ return ( +
+ Post by: {profile.name ?? profile.display_name} +

{ev.content}

+ ); } export function UserPosts(props: { pubkey: string }) { - const sub = useMemo(() => { - const rb = new RequestBuilder("get-posts"); - rb.withFilter() - .authors([props.pubkey]) - .kinds([1]) - .limit(10); + const sub = useMemo(() => { + const rb = new RequestBuilder("get-posts"); + rb.withFilter().authors([props.pubkey]).kinds([1]).limit(10); - return rb; - }, [props.pubkey]); + return rb; + }, [props.pubkey]); - const data = useRequestBuilder(System, FlatNoteStore, sub); - return ( - <> - {data.data.map(a => )} - - ) + const data = useRequestBuilder(System, FlatNoteStore, sub); + return ( + <> + {data.data.map(a => ( + + ))} + + ); } export function MyApp() { - return ( - - ) -} \ No newline at end of file + return ; +} diff --git a/packages/system-react/package.json b/packages/system-react/package.json index 19a00e874..f6143489f 100644 --- a/packages/system-react/package.json +++ b/packages/system-react/package.json @@ -20,4 +20,4 @@ "@snort/system": "^1.0.16", "@snort/shared": "^1.0.4" } -} \ No newline at end of file +} diff --git a/packages/system-react/src/index.ts b/packages/system-react/src/index.ts index 035aa6f2f..354bf0c5a 100644 --- a/packages/system-react/src/index.ts +++ b/packages/system-react/src/index.ts @@ -1,3 +1,3 @@ export * from "./useRequestBuilder"; export * from "./useSystemState"; -export * from "./useUserProfile"; \ No newline at end of file +export * from "./useUserProfile"; diff --git a/packages/system-react/src/useRequestBuilder.tsx b/packages/system-react/src/useRequestBuilder.tsx index a27dcf90d..e63995bd2 100644 --- a/packages/system-react/src/useRequestBuilder.tsx +++ b/packages/system-react/src/useRequestBuilder.tsx @@ -7,7 +7,7 @@ import { unwrap } from "@snort/shared"; */ const useRequestBuilder = >( system: SystemInterface, - type: { new(): TStore }, + type: { new (): TStore }, rb: RequestBuilder | null ) => { const subscribe = (onChanged: () => void) => { @@ -37,4 +37,4 @@ const useRequestBuilder = system.ProfileLoader.Cache.getFromCache(pubKey) ); diff --git a/packages/system/README.md b/packages/system/README.md index ad7bf86e6..4fc14b90c 100644 --- a/packages/system/README.md +++ b/packages/system/README.md @@ -3,14 +3,15 @@ A collection of caching and querying techniquies used by https://snort.social to serve all content from the nostr protocol. Simple example: + ```js -import { - NostrSystem, - EventPublisher, - UserRelaysCache, - RequestBuilder, - FlatNoteStore, - StoreSnapshot +import { + NostrSystem, + EventPublisher, + UserRelaysCache, + RequestBuilder, + FlatNoteStore, + StoreSnapshot } from "@snort/system" // Provided in-memory / indexedDb cache for relays @@ -63,4 +64,4 @@ const System = new NostrSystem({ // these patterns will be managed in @snort/system-react to make it easier to use react or other UI frameworks // release(); })(); -``` \ No newline at end of file +``` diff --git a/packages/system/examples/simple.ts b/packages/system/examples/simple.ts index 700a9eb80..b42cca116 100644 --- a/packages/system/examples/simple.ts +++ b/packages/system/examples/simple.ts @@ -1,4 +1,4 @@ -import { NostrSystem, EventPublisher, UserRelaysCache, RequestBuilder, FlatNoteStore, StoreSnapshot } from "../src" +import { NostrSystem, EventPublisher, UserRelaysCache, RequestBuilder, FlatNoteStore, StoreSnapshot } from "../src"; // Provided in-memory / indexedDb cache for relays // You can also implement your own with "RelayCache" interface @@ -6,47 +6,47 @@ const RelaysCache = new UserRelaysCache(); // example auth handler using NIP-07 const AuthHandler = async (challenge: string, relay: string) => { - const pub = await EventPublisher.nip7(); - if (pub) { - return await pub.nip42Auth(challenge, relay); - } -} + const pub = await EventPublisher.nip7(); + if (pub) { + return await pub.nip42Auth(challenge, relay); + } +}; // Singleton instance to store all connections and access query fetching system const System = new NostrSystem({ - relayCache: RelaysCache, - authHandler: AuthHandler // can be left undefined if you dont care about NIP-42 Auth + relayCache: RelaysCache, + authHandler: AuthHandler, // can be left undefined if you dont care about NIP-42 Auth }); (async () => { - // connec to one "bootstrap" relay to pull profiles/relay lists from - // also used as a fallback relay when gossip model doesnt know which relays to pick, or "authors" are not provided in the request - await System.ConnectToRelay("wss://relay.snort.social", { read: true, write: false }); + // connec to one "bootstrap" relay to pull profiles/relay lists from + // also used as a fallback relay when gossip model doesnt know which relays to pick, or "authors" are not provided in the request + await System.ConnectToRelay("wss://relay.snort.social", { read: true, write: false }); - // ID should be unique to the use case, this is important as all data fetched from this ID will be merged into the same NoteStore - const rb = new RequestBuilder("get-posts"); - rb.withFilter() - .authors(["63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed"]) // Kieran pubkey - .kinds([1]) - .limit(10); + // ID should be unique to the use case, this is important as all data fetched from this ID will be merged into the same NoteStore + const rb = new RequestBuilder("get-posts"); + rb.withFilter() + .authors(["63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed"]) // Kieran pubkey + .kinds([1]) + .limit(10); - const q = System.Query(FlatNoteStore, rb); - // basic usage using "onEvent", fired for every event added to the store - q.onEvent = (sub, e) => { - console.debug(sub, e); - } + const q = System.Query(FlatNoteStore, rb); + // basic usage using "onEvent", fired for every event added to the store + q.onEvent = (sub, e) => { + console.debug(sub, e); + }; - // Hookable type using change notification, limited to every 500ms - const release = q.feed.hook(() => { - // since we use the FlatNoteStore we expect NostrEvent[] - // other stores provide different data, like a single event instead of an array (latest version) - const state = q.feed.snapshot as StoreSnapshot>; + // Hookable type using change notification, limited to every 500ms + const release = q.feed.hook(() => { + // since we use the FlatNoteStore we expect NostrEvent[] + // other stores provide different data, like a single event instead of an array (latest version) + const state = q.feed.snapshot as StoreSnapshot>; - // do something with snapshot of store - console.log(`We have ${state.data.length} events now!`) - }); + // do something with snapshot of store + console.log(`We have ${state.data.length} events now!`); + }); - // release the hook when its not needed anymore - // these patterns will be managed in @snort/system-react to make it easier to use react or other UI frameworks - // release(); -})(); \ No newline at end of file + // release the hook when its not needed anymore + // these patterns will be managed in @snort/system-react to make it easier to use react or other UI frameworks + // release(); +})(); diff --git a/packages/system/package.json b/packages/system/package.json index 8b688a2fb..41993eb32 100644 --- a/packages/system/package.json +++ b/packages/system/package.json @@ -33,4 +33,4 @@ "dexie": "^3.2.4", "uuid": "^9.0.0" } -} \ No newline at end of file +} diff --git a/packages/system/src/cache/db.ts b/packages/system/src/cache/db.ts index 1a1d07f3e..afb8ce840 100644 --- a/packages/system/src/cache/db.ts +++ b/packages/system/src/cache/db.ts @@ -6,37 +6,37 @@ const NAME = "snort-system"; const VERSION = 2; const STORES = { - users: "++pubkey, name, display_name, picture, nip05, npub", - relayMetrics: "++addr", - userRelays: "++pubkey", - events: "++id, pubkey, created_at" + users: "++pubkey, name, display_name, picture, nip05, npub", + relayMetrics: "++addr", + userRelays: "++pubkey", + events: "++id, pubkey, created_at", }; export class SnortSystemDb extends Dexie { - ready = false; - users!: Table; - relayMetrics!: Table; - userRelays!: Table; - events!: Table; - dms!: Table; + ready = false; + users!: Table; + relayMetrics!: Table; + userRelays!: Table; + events!: Table; + dms!: Table; - constructor() { - super(NAME); - this.version(VERSION).stores(STORES); - } + constructor() { + super(NAME); + this.version(VERSION).stores(STORES); + } - isAvailable() { - if ("indexedDB" in window) { - return new Promise(resolve => { - const req = window.indexedDB.open("dummy", 1); - req.onsuccess = () => { - resolve(true); - }; - req.onerror = () => { - resolve(false); - }; - }); - } - return Promise.resolve(false); + isAvailable() { + if ("indexedDB" in window) { + return new Promise(resolve => { + const req = window.indexedDB.open("dummy", 1); + req.onsuccess = () => { + resolve(true); + }; + req.onerror = () => { + resolve(false); + }; + }); } -} \ No newline at end of file + return Promise.resolve(false); + } +} diff --git a/packages/system/src/cache/index.ts b/packages/system/src/cache/index.ts index 42bd6bf35..5065a7400 100644 --- a/packages/system/src/cache/index.ts +++ b/packages/system/src/cache/index.ts @@ -70,4 +70,4 @@ export function mapEventToProfile(ev: NostrEvent) { } catch (e) { console.error("Failed to parse JSON", ev, e); } -} \ No newline at end of file +} diff --git a/packages/system/src/cache/relay-metric.ts b/packages/system/src/cache/relay-metric.ts index 8dd6a9c7a..2eb735bd1 100644 --- a/packages/system/src/cache/relay-metric.ts +++ b/packages/system/src/cache/relay-metric.ts @@ -2,21 +2,21 @@ import { db, RelayMetrics } from "."; import { FeedCache } from "@snort/shared"; export class RelayMetricCache extends FeedCache { - constructor() { - super("RelayMetrics", db.relayMetrics); - } + constructor() { + super("RelayMetrics", db.relayMetrics); + } - key(of: RelayMetrics): string { - return of.addr; - } + key(of: RelayMetrics): string { + return of.addr; + } - override async preload(): Promise { - await super.preload(); - // load everything - await this.buffer([...this.onTable]); - } + override async preload(): Promise { + await super.preload(); + // load everything + await this.buffer([...this.onTable]); + } - takeSnapshot(): Array { - return [...this.cache.values()]; - } -} \ No newline at end of file + takeSnapshot(): Array { + return [...this.cache.values()]; + } +} diff --git a/packages/system/src/cache/user-metadata.ts b/packages/system/src/cache/user-metadata.ts index df1aa0639..51fcbd78f 100644 --- a/packages/system/src/cache/user-metadata.ts +++ b/packages/system/src/cache/user-metadata.ts @@ -145,4 +145,4 @@ export class UserProfileCache extends FeedCache { } } } -} \ No newline at end of file +} diff --git a/packages/system/src/cache/user-relays.ts b/packages/system/src/cache/user-relays.ts index 24bed9290..7381e3411 100644 --- a/packages/system/src/cache/user-relays.ts +++ b/packages/system/src/cache/user-relays.ts @@ -26,4 +26,4 @@ export class UserRelaysCache extends FeedCache { takeSnapshot(): Array { return [...this.cache.values()]; } -} \ No newline at end of file +} diff --git a/packages/system/src/connection.ts b/packages/system/src/connection.ts index d99a2e3c4..517faeb7d 100644 --- a/packages/system/src/connection.ts +++ b/packages/system/src/connection.ts @@ -421,7 +421,12 @@ export class Connection extends ExternalStore { const lastActivity = unixNowMs() - this.#activity; if (lastActivity > 30_000 && !this.IsClosed) { if (this.ActiveRequests.size > 0) { - this.#log("%s Inactive connection has %d active requests! %O", this.Address, this.ActiveRequests.size, this.ActiveRequests); + this.#log( + "%s Inactive connection has %d active requests! %O", + this.Address, + this.ActiveRequests.size, + this.ActiveRequests + ); } else { this.Close(); } diff --git a/packages/system/src/const.ts b/packages/system/src/const.ts index 85168944a..15889551a 100644 --- a/packages/system/src/const.ts +++ b/packages/system/src/const.ts @@ -9,7 +9,6 @@ export const DefaultConnectTimeout = 2000; // eslint-disable-next-line no-useless-escape export const HashtagRegex = /(#[^\s!@#$%^&*()=+.\/,\[{\]};:'"?><]+)/g; - /** * How long profile cache should be considered valid for */ diff --git a/packages/system/src/event-ext.ts b/packages/system/src/event-ext.ts index 6a631f45c..af4c6b405 100644 --- a/packages/system/src/event-ext.ts +++ b/packages/system/src/event-ext.ts @@ -6,17 +6,17 @@ import { EventKind, HexKey, NostrEvent } from "."; import { Nip4WebCryptoEncryptor } from "./impl/nip4"; export interface Tag { - key: string - value?: string - relay?: string - marker?: string // NIP-10 + key: string; + value?: string; + relay?: string; + marker?: string; // NIP-10 } export interface Thread { - root?: Tag - replyTo?: Tag - mentions: Array - pubKeys: Array + root?: Tag; + replyTo?: Tag; + mentions: Array; + pubKeys: Array; } export abstract class EventExt { @@ -41,7 +41,7 @@ export abstract class EventExt { const sig = secp.schnorr.sign(e.id, key); e.sig = utils.bytesToHex(sig); - if (!(secp.schnorr.verify(e.sig, e.id, e.pubkey))) { + if (!secp.schnorr.verify(e.sig, e.id, e.pubkey)) { throw new Error("Signing failed"); } } @@ -84,12 +84,12 @@ export abstract class EventExt { static parseTag(tag: Array) { if (tag.length < 1) { - throw new Error("Invalid tag, must have more than 2 items") + throw new Error("Invalid tag, must have more than 2 items"); } const ret = { key: tag[0], - value: tag[1] + value: tag[1], } as Tag; switch (ret.key) { case "e": { diff --git a/packages/system/src/event-publisher.ts b/packages/system/src/event-publisher.ts index 699fc5e4c..fef650690 100644 --- a/packages/system/src/event-publisher.ts +++ b/packages/system/src/event-publisher.ts @@ -95,7 +95,7 @@ export class EventPublisher { * Create an EventPublisher for a private key */ static privateKey(privateKey: string) { - const signer = new PrivateKeySigner(privateKey) + const signer = new PrivateKeySigner(privateKey); return new EventPublisher(signer, signer.getPubKey()); } diff --git a/packages/system/src/gossip-model.ts b/packages/system/src/gossip-model.ts index bc6d7d6d1..efda9a077 100644 --- a/packages/system/src/gossip-model.ts +++ b/packages/system/src/gossip-model.ts @@ -1,6 +1,7 @@ import { ReqFilter, UsersRelays } from "."; -import { unwrap } from "@snort/shared"; +import { dedupe, unwrap } from "@snort/shared"; import debug from "debug"; +import { FlatReqFilter } from "request-expander"; const PickNRelays = 2; @@ -9,6 +10,11 @@ export interface RelayTaggedFilter { filter: ReqFilter; } +export interface RelayTaggedFlatFilters { + relay: string; + filters: Array; +} + export interface RelayTaggedFilters { relay: string; filters: Array; @@ -43,11 +49,10 @@ export function splitAllByWriteRelays(cache: RelayCache, filters: Array { - if ((filter.authors?.length ?? 0) === 0) { + const authors = filter.authors; + if ((authors?.length ?? 0) === 0) { return [ { relay: "", @@ -56,10 +61,13 @@ export function splitByWriteRelays(cache: RelayCache, filter: ReqFilter): Array< ]; } - const allRelays = unwrap(filter.authors).map(a => { + const allRelays = unwrap(authors).map(a => { return { key: a, - relays: cache.getFromCache(a)?.relays?.filter(a => a.settings.write).sort(() => Math.random() < 0.5 ? 1 : -1), + relays: cache + .getFromCache(a) + ?.relays?.filter(a => a.settings.write) + .sort(() => (Math.random() < 0.5 ? 1 : -1)), }; }); @@ -83,7 +91,7 @@ export function splitByWriteRelays(cache: RelayCache, filter: ReqFilter): Array< // - pick n top relays // - map keys per relay (for subscription filter) - const userPickedRelays = unwrap(filter.authors).map(k => { + const userPickedRelays = unwrap(authors).map(k => { // pick top 3 relays for this key const relaysForKey = topRelays .filter(([, v]) => v.has(k)) @@ -116,3 +124,98 @@ export function splitByWriteRelays(cache: RelayCache, filter: ReqFilter): Array< debug("GOSSIP")("Picked %o", picked); return picked; } + +/** + * Split filters by author + */ +export function splitFlatByWriteRelays(cache: RelayCache, input: Array): Array { + const authors = input.filter(a => a.authors).map(a => unwrap(a.authors)); + if (authors.length === 0) { + return [ + { + relay: "", + filters: input, + }, + ]; + } + const topRelays = pickTopRelays(cache, authors, PickNRelays); + const pickedRelays = dedupe(topRelays.flatMap(a => a.relays)); + + const picked = pickedRelays.map(a => { + const keysOnPickedRelay = new Set(userPickedRelays.filter(b => b.relaysForKey.includes(a)).map(b => b.k)); + return { + relay: a, + filter: { + ...filter, + authors: [...keysOnPickedRelay], + }, + } as RelayTaggedFilter; + }); + if (missing.length > 0) { + picked.push({ + relay: "", + filter: { + ...filter, + authors: missing.map(a => a.key), + }, + }); + } + debug("GOSSIP")("Picked %o", picked); + return picked; +} + +/** + * Pick most popular relays for each authors + */ +function pickTopRelays(cache: RelayCache, authors: Array, n: number) { + // map of pubkey -> [write relays] + const allRelays = authors.map(a => { + return { + key: a, + relays: cache + .getFromCache(a) + ?.relays?.filter(a => a.settings.write) + .sort(() => (Math.random() < 0.5 ? 1 : -1)), + }; + }); + + const missing = allRelays.filter(a => a.relays === undefined || a.relays.length === 0); + const hasRelays = allRelays.filter(a => a.relays !== undefined && a.relays.length > 0); + + // map of relay -> [pubkeys] + const relayUserMap = hasRelays.reduce((acc, v) => { + for (const r of unwrap(v.relays)) { + if (!acc.has(r.url)) { + acc.set(r.url, new Set([v.key])); + } else { + unwrap(acc.get(r.url)).add(v.key); + } + } + return acc; + }, new Map>()); + + // selection algo will just pick relays with the most users + const topRelays = [...relayUserMap.entries()].sort(([, v], [, v1]) => v1.size - v.size); + + // - count keys per relay + // - pick n top relays + // - map keys per relay (for subscription filter) + + return hasRelays + .map(k => { + // pick top N relays for this key + const relaysForKey = topRelays + .filter(([, v]) => v.has(k.key)) + .slice(0, n) + .map(([k]) => k); + return { key: k.key, relays: relaysForKey }; + }) + .concat( + missing.map(a => { + return { + key: a.key, + relays: [], + }; + }) + ); +} diff --git a/packages/system/src/impl/nip4.ts b/packages/system/src/impl/nip4.ts index 2a648c4f4..8a4a6b957 100644 --- a/packages/system/src/impl/nip4.ts +++ b/packages/system/src/impl/nip4.ts @@ -4,49 +4,49 @@ import { base64 } from "@scure/base"; import { secp256k1 } from "@noble/curves/secp256k1"; export class Nip4WebCryptoEncryptor implements MessageEncryptor { - getSharedSecret(privateKey: string, publicKey: string) { - const sharedPoint = secp256k1.getSharedSecret(privateKey, "02" + publicKey); - const sharedX = sharedPoint.slice(1, 33); - return sharedX; - } + getSharedSecret(privateKey: string, publicKey: string) { + const sharedPoint = secp256k1.getSharedSecret(privateKey, "02" + publicKey); + const sharedX = sharedPoint.slice(1, 33); + return sharedX; + } - async encryptData(content: string, sharedSecet: Uint8Array) { - const key = await this.#importKey(sharedSecet); - const iv = window.crypto.getRandomValues(new Uint8Array(16)); - const data = new TextEncoder().encode(content); - const result = await window.crypto.subtle.encrypt( - { - name: "AES-CBC", - iv: iv, - }, - key, - data - ); - const uData = new Uint8Array(result); - return `${base64.encode(uData)}?iv=${base64.encode(iv)}`; - } + async encryptData(content: string, sharedSecet: Uint8Array) { + const key = await this.#importKey(sharedSecet); + const iv = window.crypto.getRandomValues(new Uint8Array(16)); + const data = new TextEncoder().encode(content); + const result = await window.crypto.subtle.encrypt( + { + name: "AES-CBC", + iv: iv, + }, + key, + data + ); + const uData = new Uint8Array(result); + return `${base64.encode(uData)}?iv=${base64.encode(iv)}`; + } - /** - * Decrypt the content of the message - */ - async decryptData(cyphertext: string, sharedSecet: Uint8Array) { - const key = await this.#importKey(sharedSecet); - const cSplit = cyphertext.split("?iv="); - const data = base64.decode(cSplit[0]); - const iv = base64.decode(cSplit[1]); + /** + * Decrypt the content of the message + */ + async decryptData(cyphertext: string, sharedSecet: Uint8Array) { + const key = await this.#importKey(sharedSecet); + const cSplit = cyphertext.split("?iv="); + const data = base64.decode(cSplit[0]); + const iv = base64.decode(cSplit[1]); - const result = await window.crypto.subtle.decrypt( - { - name: "AES-CBC", - iv: iv, - }, - key, - data - ); - return new TextDecoder().decode(result); - } + const result = await window.crypto.subtle.decrypt( + { + name: "AES-CBC", + iv: iv, + }, + key, + data + ); + return new TextDecoder().decode(result); + } - async #importKey(sharedSecet: Uint8Array) { - return await window.crypto.subtle.importKey("raw", sharedSecet, { name: "AES-CBC" }, false, ["encrypt", "decrypt"]); - } -} \ No newline at end of file + async #importKey(sharedSecet: Uint8Array) { + return await window.crypto.subtle.importKey("raw", sharedSecet, { name: "AES-CBC" }, false, ["encrypt", "decrypt"]); + } +} diff --git a/packages/system/src/impl/nip44.ts b/packages/system/src/impl/nip44.ts index 871c6db6f..770506588 100644 --- a/packages/system/src/impl/nip44.ts +++ b/packages/system/src/impl/nip44.ts @@ -1,40 +1,39 @@ import { MessageEncryptor } from "index"; import { base64 } from "@scure/base"; -import { randomBytes } from '@noble/hashes/utils' -import { streamXOR as xchacha20 } from '@stablelib/xchacha20' +import { randomBytes } from "@noble/hashes/utils"; +import { streamXOR as xchacha20 } from "@stablelib/xchacha20"; import { secp256k1 } from "@noble/curves/secp256k1"; -import { sha256 } from '@noble/hashes/sha256' +import { sha256 } from "@noble/hashes/sha256"; export enum Nip44Version { - Reserved = 0x00, - XChaCha20 = 0x01 + Reserved = 0x00, + XChaCha20 = 0x01, } export class Nip44Encryptor implements MessageEncryptor { - getSharedSecret(privateKey: string, publicKey: string) { - const key = secp256k1.getSharedSecret(privateKey, '02' + publicKey) - return sha256(key.slice(1, 33)); - } + getSharedSecret(privateKey: string, publicKey: string) { + const key = secp256k1.getSharedSecret(privateKey, "02" + publicKey); + return sha256(key.slice(1, 33)); + } - encryptData(content: string, sharedSecret: Uint8Array) { - const nonce = randomBytes(24) - const plaintext = new TextEncoder().encode(content) - const ciphertext = xchacha20(sharedSecret, nonce, plaintext, plaintext); - const ctb64 = base64.encode(Uint8Array.from(ciphertext)) - const nonceb64 = base64.encode(nonce) - return JSON.stringify({ ciphertext: ctb64, nonce: nonceb64, v: Nip44Version.XChaCha20 }) - } + encryptData(content: string, sharedSecret: Uint8Array) { + const nonce = randomBytes(24); + const plaintext = new TextEncoder().encode(content); + const ciphertext = xchacha20(sharedSecret, nonce, plaintext, plaintext); + const ctb64 = base64.encode(Uint8Array.from(ciphertext)); + const nonceb64 = base64.encode(nonce); + return JSON.stringify({ ciphertext: ctb64, nonce: nonceb64, v: Nip44Version.XChaCha20 }); + } - decryptData(cyphertext: string, sharedSecret: Uint8Array) { - const dt = JSON.parse(cyphertext) - if (dt.v !== 1) throw new Error('NIP44: unknown encryption version') + decryptData(cyphertext: string, sharedSecret: Uint8Array) { + const dt = JSON.parse(cyphertext); + if (dt.v !== 1) throw new Error("NIP44: unknown encryption version"); - const ciphertext = base64.decode(dt.ciphertext) - const nonce = base64.decode(dt.nonce) - const plaintext = xchacha20(sharedSecret, nonce, ciphertext, ciphertext) - const text = new TextDecoder().decode(plaintext) - return text; - } - -} \ No newline at end of file + const ciphertext = base64.decode(dt.ciphertext); + const nonce = base64.decode(dt.nonce); + const plaintext = xchacha20(sharedSecret, nonce, ciphertext, ciphertext); + const text = new TextDecoder().decode(plaintext); + return text; + } +} diff --git a/packages/system/src/impl/nip46.ts b/packages/system/src/impl/nip46.ts index 46da0cdfc..b42e99cb0 100644 --- a/packages/system/src/impl/nip46.ts +++ b/packages/system/src/impl/nip46.ts @@ -12,204 +12,213 @@ import EventKind from "../event-kind"; const NIP46_KIND = 24_133; interface Nip46Metadata { - name: string - url?: string - description?: string - icons?: Array + name: string; + url?: string; + description?: string; + icons?: Array; } interface Nip46Request { - id: string - method: string - params: Array + id: string; + method: string; + params: Array; } interface Nip46Response { - id: string - result: any - error: string + id: string; + result: any; + error: string; } interface QueueObj { - resolve: (o: any) => void; - reject: (e: Error) => void; + resolve: (o: any) => void; + reject: (e: Error) => void; } export class Nip46Signer implements EventSigner { - #conn?: Connection; - #relay: string; - #localPubkey: string; - #remotePubkey?: string; - #token?: string; - #insideSigner: EventSigner; - #commandQueue: Map = new Map(); - #log = debug("NIP-46"); - #proto: string; - #didInit: boolean = false; + #conn?: Connection; + #relay: string; + #localPubkey: string; + #remotePubkey?: string; + #token?: string; + #insideSigner: EventSigner; + #commandQueue: Map = new Map(); + #log = debug("NIP-46"); + #proto: string; + #didInit: boolean = false; - constructor(config: string, insideSigner?: EventSigner) { - const u = new URL(config); - this.#proto = u.protocol; - this.#localPubkey = u.pathname.substring(2); + constructor(config: string, insideSigner?: EventSigner) { + const u = new URL(config); + this.#proto = u.protocol; + this.#localPubkey = u.pathname.substring(2); - if (u.hash.length > 1) { - this.#token = u.hash.substring(1); - } - if (this.#localPubkey.startsWith("npub")) { - this.#localPubkey = bech32ToHex(this.#localPubkey); - } - - this.#relay = unwrap(u.searchParams.get("relay")); - this.#insideSigner = insideSigner ?? new PrivateKeySigner(secp256k1.utils.randomPrivateKey()) + if (u.hash.length > 1) { + this.#token = u.hash.substring(1); + } + if (this.#localPubkey.startsWith("npub")) { + this.#localPubkey = bech32ToHex(this.#localPubkey); } - get relays() { - return [this.#relay]; - } + this.#relay = unwrap(u.searchParams.get("relay")); + this.#insideSigner = insideSigner ?? new PrivateKeySigner(secp256k1.utils.randomPrivateKey()); + } - get privateKey() { - if(this.#insideSigner instanceof PrivateKeySigner) { - return this.#insideSigner.privateKey; - } - } + get relays() { + return [this.#relay]; + } + + get privateKey() { + if (this.#insideSigner instanceof PrivateKeySigner) { + return this.#insideSigner.privateKey; + } + } + + async init() { + const isBunker = this.#proto === "bunker:"; + if (isBunker) { + this.#remotePubkey = this.#localPubkey; + this.#localPubkey = await this.#insideSigner.getPubKey(); + } + return await new Promise((resolve, reject) => { + this.#conn = new Connection(this.#relay, { read: true, write: true }); + this.#conn.OnEvent = async (sub, e) => { + await this.#onReply(e); + }; + this.#conn.OnConnected = async () => { + this.#conn!.QueueReq( + [ + "REQ", + "reply", + { + kinds: [NIP46_KIND], + "#p": [this.#localPubkey], + }, + ], + () => {} + ); - async init() { - const isBunker = this.#proto === "bunker:"; if (isBunker) { - this.#remotePubkey = this.#localPubkey; - this.#localPubkey = await this.#insideSigner.getPubKey(); + await this.#connect(unwrap(this.#remotePubkey)); + resolve(); + } else { + this.#commandQueue.set("connect", { + reject, + resolve, + }); } - return await new Promise((resolve, reject) => { - this.#conn = new Connection(this.#relay, { read: true, write: true }); - this.#conn.OnEvent = async (sub, e) => { - await this.#onReply(e); - } - this.#conn.OnConnected = async () => { - this.#conn!.QueueReq(["REQ", "reply", { - kinds: [NIP46_KIND], - "#p": [this.#localPubkey] - }], () => { }); + }; + this.#conn.Connect(); + this.#didInit = true; + }); + } - if (isBunker) { - await this.#connect(unwrap(this.#remotePubkey)); - resolve(); - } else { - this.#commandQueue.set("connect", { - reject, - resolve - }) - } - } - this.#conn.Connect(); - this.#didInit = true; - }) + async close() { + if (this.#conn) { + await this.#disconnect(); + this.#conn.CloseReq("reply"); + this.#conn.Close(); + this.#conn = undefined; + this.#didInit = false; + } + } + async describe() { + return await this.#rpc>("describe", []); + } + + async getPubKey() { + return await this.#rpc("get_public_key", []); + } + + async nip4Encrypt(content: string, otherKey: string) { + return await this.#rpc("nip04_encrypt", [otherKey, content]); + } + + async nip4Decrypt(content: string, otherKey: string) { + return await this.#rpc("nip04_decrypt", [otherKey, content]); + } + + async sign(ev: NostrEvent) { + const evStr = await this.#rpc("sign_event", [JSON.stringify(ev)]); + return JSON.parse(evStr); + } + + async #disconnect() { + return await this.#rpc("disconnect", []); + } + + async #connect(pk: string) { + const connectParams = [pk]; + if (this.#token) { + connectParams.push(this.#token); + } + return await this.#rpc("connect", connectParams); + } + + async #onReply(e: NostrEvent) { + if (e.kind !== NIP46_KIND) { + throw new Error("Unknown event kind"); } - async close() { - if (this.#conn) { - await this.#disconnect(); - this.#conn.CloseReq("reply"); - this.#conn.Close(); - this.#conn = undefined; - this.#didInit = false; - } + const decryptedContent = await this.#insideSigner.nip4Decrypt(e.content, e.pubkey); + const reply = JSON.parse(decryptedContent) as Nip46Request | Nip46Response; + + let id = reply.id; + this.#log("Recv: %O", reply); + if ("method" in reply && reply.method === "connect") { + this.#remotePubkey = reply.params[0]; + await this.#sendCommand( + { + id: reply.id, + result: "ack", + error: "", + }, + unwrap(this.#remotePubkey) + ); + id = "connect"; + } + const pending = this.#commandQueue.get(id); + if (!pending) { + throw new Error("No pending command found"); } - async describe() { - return await this.#rpc>("describe", []); + pending.resolve(reply); + this.#commandQueue.delete(reply.id); + } + + async #rpc(method: string, params: Array) { + if (!this.#didInit) { + await this.init(); } + if (!this.#conn) throw new Error("Connection error"); - async getPubKey() { - return await this.#rpc("get_public_key", []); - } + const payload = { + id: uuid(), + method, + params, + } as Nip46Request; - async nip4Encrypt(content: string, otherKey: string) { - return await this.#rpc("nip04_encrypt", [otherKey, content]); - } + this.#sendCommand(payload, unwrap(this.#remotePubkey)); + return await new Promise((resolve, reject) => { + this.#commandQueue.set(payload.id, { + resolve: async (o: Nip46Response) => { + resolve(o.result as T); + }, + reject, + }); + }); + } - async nip4Decrypt(content: string, otherKey: string) { - return await this.#rpc("nip04_decrypt", [otherKey, content]); - } + async #sendCommand(payload: Nip46Request | Nip46Response, target: string) { + if (!this.#conn) return; - async sign(ev: NostrEvent) { - const evStr = await this.#rpc("sign_event", [JSON.stringify(ev)]); - return JSON.parse(evStr); - } + const eb = new EventBuilder(); + eb.kind(NIP46_KIND as EventKind) + .content(await this.#insideSigner.nip4Encrypt(JSON.stringify(payload), target)) + .tag(["p", target]); - async #disconnect() { - return await this.#rpc("disconnect", []); - } - - async #connect(pk: string) { - const connectParams = [pk]; - if (this.#token) { - connectParams.push(this.#token); - } - return await this.#rpc("connect", connectParams); - } - - async #onReply(e: NostrEvent) { - if (e.kind !== NIP46_KIND) { - throw new Error("Unknown event kind"); - } - - const decryptedContent = await this.#insideSigner.nip4Decrypt(e.content, e.pubkey); - const reply = JSON.parse(decryptedContent) as Nip46Request | Nip46Response; - - let id = reply.id; - this.#log("Recv: %O", reply); - if ("method" in reply && reply.method === "connect") { - this.#remotePubkey = reply.params[0]; - await this.#sendCommand({ - id: reply.id, - result: "ack", - error: "" - }, unwrap(this.#remotePubkey)); - id = "connect"; - } - const pending = this.#commandQueue.get(id); - if (!pending) { - throw new Error("No pending command found"); - } - - pending.resolve(reply); - this.#commandQueue.delete(reply.id); - } - - async #rpc(method: string, params: Array) { - if (!this.#didInit) { - await this.init(); - } - if (!this.#conn) throw new Error("Connection error"); - - const payload = { - id: uuid(), - method, - params, - } as Nip46Request; - - this.#sendCommand(payload, unwrap(this.#remotePubkey)); - return await new Promise((resolve, reject) => { - this.#commandQueue.set(payload.id, { - resolve: async (o: Nip46Response) => { - resolve(o.result as T); - }, - reject, - }); - }); - } - - async #sendCommand(payload: Nip46Request | Nip46Response, target: string) { - if (!this.#conn) return; - - const eb = new EventBuilder(); - eb.kind(NIP46_KIND as EventKind) - .content(await this.#insideSigner.nip4Encrypt(JSON.stringify(payload), target)) - .tag(["p", target]); - - this.#log("Send: %O", payload); - const evCommand = await eb.buildAndSign(this.#insideSigner); - await this.#conn.SendAsync(evCommand); - } + this.#log("Send: %O", payload); + const evCommand = await eb.buildAndSign(this.#insideSigner); + await this.#conn.SendAsync(evCommand); + } } diff --git a/packages/system/src/impl/nip7.ts b/packages/system/src/impl/nip7.ts index 6d6c92c36..1114916c6 100644 --- a/packages/system/src/impl/nip7.ts +++ b/packages/system/src/impl/nip7.ts @@ -6,56 +6,55 @@ const Nip7Queue: Array = []; processWorkQueue(Nip7Queue); declare global { - interface Window { - nostr?: { - getPublicKey: () => Promise; - signEvent: (event: T) => Promise; + interface Window { + nostr?: { + getPublicKey: () => Promise; + signEvent: (event: T) => Promise; - getRelays?: () => Promise>; + getRelays?: () => Promise>; - nip04?: { - encrypt?: (pubkey: HexKey, plaintext: string) => Promise; - decrypt?: (pubkey: HexKey, ciphertext: string) => Promise; - }; - }; - } + nip04?: { + encrypt?: (pubkey: HexKey, plaintext: string) => Promise; + decrypt?: (pubkey: HexKey, ciphertext: string) => Promise; + }; + }; + } } export class Nip7Signer implements EventSigner { - init(): Promise { - return Promise.resolve(); - } + init(): Promise { + return Promise.resolve(); + } - async getPubKey(): Promise { - if (!window.nostr) { - throw new Error("Cannot use NIP-07 signer, not found!"); - } - return await barrierQueue(Nip7Queue, () => unwrap(window.nostr).getPublicKey()); + async getPubKey(): Promise { + if (!window.nostr) { + throw new Error("Cannot use NIP-07 signer, not found!"); } + return await barrierQueue(Nip7Queue, () => unwrap(window.nostr).getPublicKey()); + } - async nip4Encrypt(content: string, key: string): Promise { - if (!window.nostr) { - throw new Error("Cannot use NIP-07 signer, not found!"); - } - return await barrierQueue(Nip7Queue, () => - unwrap(window.nostr?.nip04?.encrypt).call(window.nostr?.nip04, key, content) - ); + async nip4Encrypt(content: string, key: string): Promise { + if (!window.nostr) { + throw new Error("Cannot use NIP-07 signer, not found!"); } + return await barrierQueue(Nip7Queue, () => + unwrap(window.nostr?.nip04?.encrypt).call(window.nostr?.nip04, key, content) + ); + } - async nip4Decrypt(content: string, otherKey: string): Promise { - if (!window.nostr) { - throw new Error("Cannot use NIP-07 signer, not found!"); - } - return await barrierQueue(Nip7Queue, () => - unwrap(window.nostr?.nip04?.decrypt).call(window.nostr?.nip04, otherKey, content) - ); + async nip4Decrypt(content: string, otherKey: string): Promise { + if (!window.nostr) { + throw new Error("Cannot use NIP-07 signer, not found!"); } + return await barrierQueue(Nip7Queue, () => + unwrap(window.nostr?.nip04?.decrypt).call(window.nostr?.nip04, otherKey, content) + ); + } - async sign(ev: NostrEvent): Promise { - if (!window.nostr) { - throw new Error("Cannot use NIP-07 signer, not found!"); - } - return await barrierQueue(Nip7Queue, () => unwrap(window.nostr).signEvent(ev)); + async sign(ev: NostrEvent): Promise { + if (!window.nostr) { + throw new Error("Cannot use NIP-07 signer, not found!"); } - -} \ No newline at end of file + return await barrierQueue(Nip7Queue, () => unwrap(window.nostr).signEvent(ev)); + } +} diff --git a/packages/system/src/index.ts b/packages/system/src/index.ts index a41460487..677dce489 100644 --- a/packages/system/src/index.ts +++ b/packages/system/src/index.ts @@ -37,7 +37,7 @@ export interface SystemInterface { HandleAuth?: AuthHandler; get Sockets(): Array; GetQuery(id: string): Query | undefined; - Query(type: { new(): T }, req: RequestBuilder | null): Query; + Query(type: { new (): T }, req: RequestBuilder | null): Query; ConnectToRelay(address: string, options: RelaySettings): Promise; DisconnectRelay(address: string): void; BroadcastEvent(ev: NostrEvent): void; @@ -53,7 +53,7 @@ export interface SystemSnapshot { } export interface MessageEncryptor { - getSharedSecret(privateKey: string, publicKey: string): Promise | Uint8Array - encryptData(plaintext: string, sharedSecet: Uint8Array): Promise | string - decryptData(cyphertext: string, sharedSecet: Uint8Array): Promise | string -} \ No newline at end of file + getSharedSecret(privateKey: string, publicKey: string): Promise | Uint8Array; + encryptData(plaintext: string, sharedSecet: Uint8Array): Promise | string; + decryptData(cyphertext: string, sharedSecet: Uint8Array): Promise | string; +} diff --git a/packages/system/src/nostr-link.ts b/packages/system/src/nostr-link.ts index 39968eb5d..2f572f6ef 100644 --- a/packages/system/src/nostr-link.ts +++ b/packages/system/src/nostr-link.ts @@ -2,109 +2,108 @@ import { bech32ToHex, hexToBech32 } from "@snort/shared"; import { NostrPrefix, decodeTLV, TLVEntryType } from "."; export interface NostrLink { - type: NostrPrefix; - id: string; - kind?: number; - author?: string; - relays?: Array; - encode(): string; - } - - export function validateNostrLink(link: string): boolean { - try { - const parsedLink = parseNostrLink(link); - if (!parsedLink) { - return false; - } - if (parsedLink.type === NostrPrefix.PublicKey || parsedLink.type === NostrPrefix.Note) { - return parsedLink.id.length === 64; - } - - return true; - } catch { + type: NostrPrefix; + id: string; + kind?: number; + author?: string; + relays?: Array; + encode(): string; +} + +export function validateNostrLink(link: string): boolean { + try { + const parsedLink = parseNostrLink(link); + if (!parsedLink) { return false; } - } - - export function tryParseNostrLink(link: string, prefixHint?: NostrPrefix): NostrLink | undefined { - try { - return parseNostrLink(link, prefixHint); - } catch { - return undefined; + if (parsedLink.type === NostrPrefix.PublicKey || parsedLink.type === NostrPrefix.Note) { + return parsedLink.id.length === 64; } + + return true; + } catch { + return false; } - - export function parseNostrLink(link: string, prefixHint?: NostrPrefix): NostrLink { - const entity = link.startsWith("web+nostr:") || link.startsWith("nostr:") ? link.split(":")[1] : link; - - const isPrefix = (prefix: NostrPrefix) => { - return entity.startsWith(prefix); +} + +export function tryParseNostrLink(link: string, prefixHint?: NostrPrefix): NostrLink | undefined { + try { + return parseNostrLink(link, prefixHint); + } catch { + return undefined; + } +} + +export function parseNostrLink(link: string, prefixHint?: NostrPrefix): NostrLink { + const entity = link.startsWith("web+nostr:") || link.startsWith("nostr:") ? link.split(":")[1] : link; + + const isPrefix = (prefix: NostrPrefix) => { + return entity.startsWith(prefix); + }; + + if (isPrefix(NostrPrefix.PublicKey)) { + const id = bech32ToHex(entity); + if (id.length !== 64) throw new Error("Invalid nostr link, must contain 32 byte id"); + return { + type: NostrPrefix.PublicKey, + id: id, + encode: () => hexToBech32(NostrPrefix.PublicKey, id), }; - - if (isPrefix(NostrPrefix.PublicKey)) { - const id = bech32ToHex(entity); + } else if (isPrefix(NostrPrefix.Note)) { + const id = bech32ToHex(entity); + if (id.length !== 64) throw new Error("Invalid nostr link, must contain 32 byte id"); + return { + type: NostrPrefix.Note, + id: id, + encode: () => hexToBech32(NostrPrefix.Note, id), + }; + } else if (isPrefix(NostrPrefix.Profile) || isPrefix(NostrPrefix.Event) || isPrefix(NostrPrefix.Address)) { + const decoded = decodeTLV(entity); + + const id = decoded.find(a => a.type === TLVEntryType.Special)?.value as string; + const relays = decoded.filter(a => a.type === TLVEntryType.Relay).map(a => a.value as string); + const author = decoded.find(a => a.type === TLVEntryType.Author)?.value as string; + const kind = decoded.find(a => a.type === TLVEntryType.Kind)?.value as number; + + const encode = () => { + return entity; // return original + }; + if (isPrefix(NostrPrefix.Profile)) { if (id.length !== 64) throw new Error("Invalid nostr link, must contain 32 byte id"); return { - type: NostrPrefix.PublicKey, - id: id, - encode: () => hexToBech32(NostrPrefix.PublicKey, id), + type: NostrPrefix.Profile, + id, + relays, + kind, + author, + encode, }; - } else if (isPrefix(NostrPrefix.Note)) { - const id = bech32ToHex(entity); + } else if (isPrefix(NostrPrefix.Event)) { if (id.length !== 64) throw new Error("Invalid nostr link, must contain 32 byte id"); return { - type: NostrPrefix.Note, - id: id, - encode: () => hexToBech32(NostrPrefix.Note, id), + type: NostrPrefix.Event, + id, + relays, + kind, + author, + encode, }; - } else if (isPrefix(NostrPrefix.Profile) || isPrefix(NostrPrefix.Event) || isPrefix(NostrPrefix.Address)) { - const decoded = decodeTLV(entity); - - const id = decoded.find(a => a.type === TLVEntryType.Special)?.value as string; - const relays = decoded.filter(a => a.type === TLVEntryType.Relay).map(a => a.value as string); - const author = decoded.find(a => a.type === TLVEntryType.Author)?.value as string; - const kind = decoded.find(a => a.type === TLVEntryType.Kind)?.value as number; - - const encode = () => { - return entity; // return original - }; - if (isPrefix(NostrPrefix.Profile)) { - if (id.length !== 64) throw new Error("Invalid nostr link, must contain 32 byte id"); - return { - type: NostrPrefix.Profile, - id, - relays, - kind, - author, - encode, - }; - } else if (isPrefix(NostrPrefix.Event)) { - if (id.length !== 64) throw new Error("Invalid nostr link, must contain 32 byte id"); - return { - type: NostrPrefix.Event, - id, - relays, - kind, - author, - encode, - }; - } else if (isPrefix(NostrPrefix.Address)) { - return { - type: NostrPrefix.Address, - id, - relays, - kind, - author, - encode, - }; - } - } else if (prefixHint) { + } else if (isPrefix(NostrPrefix.Address)) { return { - type: prefixHint, - id: link, - encode: () => hexToBech32(prefixHint, link), + type: NostrPrefix.Address, + id, + relays, + kind, + author, + encode, }; } - throw new Error("Invalid nostr link"); + } else if (prefixHint) { + return { + type: prefixHint, + id: link, + encode: () => hexToBech32(prefixHint, link), + }; } - \ No newline at end of file + throw new Error("Invalid nostr link"); +} diff --git a/packages/system/src/nostr-system.ts b/packages/system/src/nostr-system.ts index 5e4a8596f..118fde5bf 100644 --- a/packages/system/src/nostr-system.ts +++ b/packages/system/src/nostr-system.ts @@ -17,7 +17,7 @@ import { UserRelaysCache, RelayMetricCache, db, - UsersRelays + UsersRelays, } from "."; /** @@ -67,10 +67,10 @@ export class NostrSystem extends ExternalStore implements System #relayMetrics: RelayMetricHandler; constructor(props: { - authHandler?: AuthHandler, - relayCache?: FeedCache, - profileCache?: FeedCache - relayMetrics?: FeedCache + authHandler?: AuthHandler; + relayCache?: FeedCache; + profileCache?: FeedCache; + relayMetrics?: FeedCache; }) { super(); this.#handleAuth = props.authHandler; @@ -99,11 +99,7 @@ export class NostrSystem extends ExternalStore implements System */ async Init() { db.ready = await db.isAvailable(); - const t = [ - this.#relayCache.preload(), - this.#profileCache.preload(), - this.#relayMetricsCache.preload() - ]; + const t = [this.#relayCache.preload(), this.#profileCache.preload(), this.#relayMetricsCache.preload()]; await Promise.all(t); } @@ -118,8 +114,8 @@ export class NostrSystem extends ExternalStore implements System this.#sockets.set(addr, c); c.OnEvent = (s, e) => this.OnEvent(s, e); c.OnEose = s => this.OnEndOfStoredEvents(c, s); - c.OnDisconnect = (code) => this.OnRelayDisconnect(c, code); - c.OnConnected = (r) => this.OnRelayConnected(c, r); + c.OnDisconnect = code => this.OnRelayDisconnect(c, code); + c.OnConnected = r => this.OnRelayConnected(c, r); await c.Connect(); } else { // update settings if already connected @@ -170,7 +166,7 @@ export class NostrSystem extends ExternalStore implements System c.OnEvent = (s, e) => this.OnEvent(s, e); c.OnEose = s => this.OnEndOfStoredEvents(c, s); c.OnDisconnect = code => this.OnRelayDisconnect(c, code); - c.OnConnected = (r) => this.OnRelayConnected(c, r); + c.OnConnected = r => this.OnRelayConnected(c, r); await c.Connect(); return c; } @@ -194,7 +190,7 @@ export class NostrSystem extends ExternalStore implements System return this.Queries.get(id); } - Query(type: { new(): T }, req: RequestBuilder): Query { + Query(type: { new (): T }, req: RequestBuilder): Query { const existing = this.Queries.get(req.id); if (existing) { // if same instance, just return query diff --git a/packages/system/src/nostr.ts b/packages/system/src/nostr.ts index 336346d1b..c818255f1 100644 --- a/packages/system/src/nostr.ts +++ b/packages/system/src/nostr.ts @@ -38,21 +38,21 @@ export type ReqCommand = [cmd: "REQ", id: string, ...filters: Array]; * Raw REQ filter object */ export interface ReqFilter { - ids?: u256[] - authors?: u256[] - kinds?: number[] - "#e"?: u256[] - "#p"?: u256[] - "#t"?: string[] - "#d"?: string[] - "#r"?: string[] - "#a"?: string[] - "#g"?: string[] - search?: string - since?: number - until?: number - limit?: number - [key: string]: Array | Array | string | number | undefined + ids?: u256[]; + authors?: u256[]; + kinds?: number[]; + "#e"?: u256[]; + "#p"?: u256[]; + "#t"?: string[]; + "#d"?: string[]; + "#r"?: string[]; + "#a"?: string[]; + "#g"?: string[]; + search?: string; + since?: number; + until?: number; + limit?: number; + [key: string]: Array | Array | string | number | undefined; } /** diff --git a/packages/system/src/note-collection.ts b/packages/system/src/note-collection.ts index 238f69ede..1137e8692 100644 --- a/packages/system/src/note-collection.ts +++ b/packages/system/src/note-collection.ts @@ -249,8 +249,8 @@ export class ReplaceableNoteStore extends HookedNoteStore { - const legacyReplaceable = [0, 3, 41] + super(e => { + const legacyReplaceable = [0, 3, 41]; if (e.kind >= 30_000 && e.kind < 40_000) { return `${e.kind}:${e.pubkey}:${findTag(e, "d")}`; // Parameterized replaceable } else if (e.kind >= 10_000 && e.kind < 20_000) { @@ -263,6 +263,6 @@ export class NoteCollection extends KeyedReplaceableNoteStore { // unknown kind return e.id; } - }) + }); } -} \ No newline at end of file +} diff --git a/packages/system/src/profile-cache.ts b/packages/system/src/profile-cache.ts index 4e45aa88e..aac74026b 100644 --- a/packages/system/src/profile-cache.ts +++ b/packages/system/src/profile-cache.ts @@ -1,13 +1,10 @@ - import debug from "debug"; import { unixNowMs, FeedCache } from "@snort/shared"; import { EventKind, HexKey, SystemInterface, TaggedRawEvent, NoteCollection, RequestBuilder } from "."; import { ProfileCacheExpire } from "./const"; import { mapEventToProfile, MetadataCache } from "./cache"; -const MetadataRelays = [ - "wss://purplepag.es" -] +const MetadataRelays = ["wss://purplepag.es"]; export class ProfileLoaderService { #system: SystemInterface; @@ -88,7 +85,8 @@ export class ProfileLoaderService { .authors([...missing]); if (this.#missingLastRun.size > 0) { - const fMissing = sub.withFilter() + const fMissing = sub + .withFilter() .kinds([EventKind.SetMetadata]) .authors([...this.#missingLastRun]); MetadataRelays.forEach(r => fMissing.relay(r)); diff --git a/packages/system/src/query.ts b/packages/system/src/query.ts index 90b0a114a..a7421f9e5 100644 --- a/packages/system/src/query.ts +++ b/packages/system/src/query.ts @@ -3,11 +3,10 @@ import debug from "debug"; import { unixNowMs, unwrap } from "@snort/shared"; import { Connection, ReqFilter, Nips, TaggedRawEvent } from "."; -import { reqFilterEq } from "./utils"; import { NoteStore } from "./note-collection"; import { flatMerge } from "./request-merger"; import { BuiltRawReqFilter } from "./request-builder"; -import { expandFilter } from "./request-expander"; +import { FlatReqFilter, expandFilter } from "./request-expander"; /** * Tracing for relay query status @@ -19,6 +18,7 @@ class QueryTrace { eose?: number; close?: number; #wasForceClosed = false; + readonly flatFilters: Array; readonly #fnClose: (id: string) => void; readonly #fnProgress: () => void; @@ -33,6 +33,7 @@ class QueryTrace { this.start = unixNowMs(); this.#fnClose = fnClose; this.#fnProgress = fnProgress; + this.flatFilters = filters.flatMap(expandFilter); } sentToRelay() { @@ -168,13 +169,7 @@ export class Query implements QueryBase { } get flatFilters() { - const f: Array = []; - for (const x of this.#tracing.flatMap(a => a.filters)) { - if (!f.some(a => reqFilterEq(a, x))) { - f.push(x); - } - } - return f.flatMap(expandFilter); + return this.#tracing.flatMap(a => a.flatFilters); } get feed() { diff --git a/packages/system/src/relay-metric-handler.ts b/packages/system/src/relay-metric-handler.ts index b5c723c4b..8a8cb3000 100644 --- a/packages/system/src/relay-metric-handler.ts +++ b/packages/system/src/relay-metric-handler.ts @@ -3,13 +3,11 @@ import { Connection } from "connection"; import { RelayMetrics } from "cache"; export class RelayMetricHandler { - readonly #cache: FeedCache; + readonly #cache: FeedCache; - constructor(cache: FeedCache) { - this.#cache = cache; - } + constructor(cache: FeedCache) { + this.#cache = cache; + } - onDisconnect(c: Connection, code: number) { - - } -} \ No newline at end of file + onDisconnect(c: Connection, code: number) {} +} diff --git a/packages/system/src/request-builder.ts b/packages/system/src/request-builder.ts index b744f4aef..07f0ac1d5 100644 --- a/packages/system/src/request-builder.ts +++ b/packages/system/src/request-builder.ts @@ -108,10 +108,9 @@ export class RequestBuilder { const next = this.#builders.flatMap(f => expandFilter(f.filter)); const diff = diffFilters(prev, next); - const ts = (unixNowMs() - start); + const ts = unixNowMs() - start; this.#log("buildDiff %s %d ms", this.id, ts); if (diff.changed) { - this.#log(diff); return splitAllByWriteRelays(relays, diff.added).map(a => { return { strategy: RequestStrategy.AuthorsRelays, diff --git a/packages/system/src/request-expander.ts b/packages/system/src/request-expander.ts index 978a0a165..669c55a34 100644 --- a/packages/system/src/request-expander.ts +++ b/packages/system/src/request-expander.ts @@ -1,19 +1,19 @@ import { ReqFilter } from "./nostr"; export interface FlatReqFilter { - keys: number - ids?: string - authors?: string - kinds?: number - "#e"?: string - "#p"?: string - "#t"?: string - "#d"?: string - "#r"?: string - search?: string - since?: number - until?: number - limit?: number + keys: number; + ids?: string; + authors?: string; + kinds?: number; + "#e"?: string; + "#p"?: string; + "#t"?: string; + "#d"?: string; + "#r"?: string; + search?: string; + since?: number; + until?: number; + limit?: number; } /** diff --git a/packages/system/src/request-merger.ts b/packages/system/src/request-merger.ts index f32f1515a..a345ef034 100644 --- a/packages/system/src/request-merger.ts +++ b/packages/system/src/request-merger.ts @@ -114,7 +114,7 @@ export function flatMerge(all: Array): Array { acc[k].push(v); } } - }) + }); return acc; }, {} as any) as ReqFilter; } diff --git a/packages/system/src/request-splitter.ts b/packages/system/src/request-splitter.ts index eed73218b..5dd625792 100644 --- a/packages/system/src/request-splitter.ts +++ b/packages/system/src/request-splitter.ts @@ -28,8 +28,8 @@ export function diffFilters(prev: Array, next: Array 0 || removed.length > 0; return { - added: changed ? flatMerge(added) : [], - removed: changed ? flatMerge(removed) : [], + added: changed ? added : [], + removed: changed ? removed : [], changed, }; } diff --git a/packages/system/src/utils.ts b/packages/system/src/utils.ts index cb8f6c09f..697a6b14a 100644 --- a/packages/system/src/utils.ts +++ b/packages/system/src/utils.ts @@ -1,42 +1,45 @@ - import { equalProp } from "@snort/shared"; import { FlatReqFilter } from "./request-expander"; import { NostrEvent, ReqFilter } from "./nostr"; export function findTag(e: NostrEvent, tag: string) { - const maybeTag = e.tags.find(evTag => { - return evTag[0] === tag; - }); - return maybeTag && maybeTag[1]; + const maybeTag = e.tags.find(evTag => { + return evTag[0] === tag; + }); + return maybeTag && maybeTag[1]; } export function reqFilterEq(a: FlatReqFilter | ReqFilter, b: FlatReqFilter | ReqFilter): boolean { - return equalProp(a.ids, b.ids) - && equalProp(a.kinds, b.kinds) - && equalProp(a.authors, b.authors) - && equalProp(a.limit, b.limit) - && equalProp(a.since, b.since) - && equalProp(a.until, b.until) - && equalProp(a.search, b.search) - && equalProp(a["#e"], b["#e"]) - && equalProp(a["#p"], b["#p"]) - && equalProp(a["#t"], b["#t"]) - && equalProp(a["#d"], b["#d"]) - && equalProp(a["#r"], b["#r"]); + return ( + equalProp(a.ids, b.ids) && + equalProp(a.kinds, b.kinds) && + equalProp(a.authors, b.authors) && + equalProp(a.limit, b.limit) && + equalProp(a.since, b.since) && + equalProp(a.until, b.until) && + equalProp(a.search, b.search) && + equalProp(a["#e"], b["#e"]) && + equalProp(a["#p"], b["#p"]) && + equalProp(a["#t"], b["#t"]) && + equalProp(a["#d"], b["#d"]) && + equalProp(a["#r"], b["#r"]) + ); } export function flatFilterEq(a: FlatReqFilter, b: FlatReqFilter): boolean { - return a.keys === b.keys - && a.since === b.since - && a.until === b.until - && a.limit === b.limit - && a.search === b.search - && a.ids === b.ids - && a.kinds === b.kinds - && a.authors === b.authors - && a["#e"] === b["#e"] - && a["#p"] === b["#p"] - && a["#t"] === b["#t"] - && a["#d"] === b["#d"] - && a["#r"] === b["#r"]; -} \ No newline at end of file + return ( + a.keys === b.keys && + a.since === b.since && + a.until === b.until && + a.limit === b.limit && + a.search === b.search && + a.ids === b.ids && + a.kinds === b.kinds && + a.authors === b.authors && + a["#e"] === b["#e"] && + a["#p"] === b["#p"] && + a["#t"] === b["#t"] && + a["#d"] === b["#d"] && + a["#r"] === b["#r"] + ); +} diff --git a/packages/system/src/zaps.ts b/packages/system/src/zaps.ts index c31476f3f..57c3b3551 100644 --- a/packages/system/src/zaps.ts +++ b/packages/system/src/zaps.ts @@ -5,88 +5,88 @@ import { findTag } from "./utils"; import { MetadataCache } from "./cache"; function getInvoice(zap: NostrEvent): InvoiceDetails | undefined { - const bolt11 = findTag(zap, "bolt11"); - if (!bolt11) { - throw new Error("Invalid zap, missing bolt11 tag"); - } - return decodeInvoice(bolt11); + const bolt11 = findTag(zap, "bolt11"); + if (!bolt11) { + throw new Error("Invalid zap, missing bolt11 tag"); + } + return decodeInvoice(bolt11); } export function parseZap(zapReceipt: NostrEvent, userCache: FeedCache, refNote?: NostrEvent): ParsedZap { - let innerZapJson = findTag(zapReceipt, "description"); - if (innerZapJson) { - try { - const invoice = getInvoice(zapReceipt); - if (innerZapJson.startsWith("%")) { - innerZapJson = decodeURIComponent(innerZapJson); - } - const zapRequest: NostrEvent = JSON.parse(innerZapJson); - if (Array.isArray(zapRequest)) { - // old format, ignored - throw new Error("deprecated zap format"); - } - const isForwardedZap = refNote?.tags.some(a => a[0] === "zap") ?? false; - const anonZap = zapRequest.tags.find(a => a[0] === "anon"); - const metaHash = sha256(innerZapJson); - const pollOpt = zapRequest.tags.find(a => a[0] === "poll_option")?.[1]; - const ret: ParsedZap = { - id: zapReceipt.id, - zapService: zapReceipt.pubkey, - amount: (invoice?.amount ?? 0) / 1000, - event: findTag(zapRequest, "e"), - sender: zapRequest.pubkey, - receiver: findTag(zapRequest, "p"), - valid: true, - anonZap: anonZap !== undefined, - content: zapRequest.content, - errors: [], - pollOption: pollOpt ? Number(pollOpt) : undefined, - }; - if (invoice?.descriptionHash !== metaHash) { - ret.valid = false; - ret.errors.push("description_hash does not match zap request"); - } - if (findTag(zapRequest, "p") !== findTag(zapReceipt, "p")) { - ret.valid = false; - ret.errors.push("p tags dont match"); - } - if (ret.event && ret.event !== findTag(zapReceipt, "e")) { - ret.valid = false; - ret.errors.push("e tags dont match"); - } - if (findTag(zapRequest, "amount") === invoice?.amount) { - ret.valid = false; - ret.errors.push("amount tag does not match invoice amount"); - } - if (userCache.getFromCache(ret.receiver)?.zapService !== ret.zapService && !isForwardedZap) { - ret.valid = false; - ret.errors.push("zap service pubkey doesn't match"); - } - return ret; - } catch (e) { - // ignored: console.debug("Invalid zap", zapReceipt, e); - } - } - return { + let innerZapJson = findTag(zapReceipt, "description"); + if (innerZapJson) { + try { + const invoice = getInvoice(zapReceipt); + if (innerZapJson.startsWith("%")) { + innerZapJson = decodeURIComponent(innerZapJson); + } + const zapRequest: NostrEvent = JSON.parse(innerZapJson); + if (Array.isArray(zapRequest)) { + // old format, ignored + throw new Error("deprecated zap format"); + } + const isForwardedZap = refNote?.tags.some(a => a[0] === "zap") ?? false; + const anonZap = zapRequest.tags.find(a => a[0] === "anon"); + const metaHash = sha256(innerZapJson); + const pollOpt = zapRequest.tags.find(a => a[0] === "poll_option")?.[1]; + const ret: ParsedZap = { id: zapReceipt.id, zapService: zapReceipt.pubkey, - amount: 0, - valid: false, - anonZap: false, - errors: ["invalid zap, parsing failed"], - }; + amount: (invoice?.amount ?? 0) / 1000, + event: findTag(zapRequest, "e"), + sender: zapRequest.pubkey, + receiver: findTag(zapRequest, "p"), + valid: true, + anonZap: anonZap !== undefined, + content: zapRequest.content, + errors: [], + pollOption: pollOpt ? Number(pollOpt) : undefined, + }; + if (invoice?.descriptionHash !== metaHash) { + ret.valid = false; + ret.errors.push("description_hash does not match zap request"); + } + if (findTag(zapRequest, "p") !== findTag(zapReceipt, "p")) { + ret.valid = false; + ret.errors.push("p tags dont match"); + } + if (ret.event && ret.event !== findTag(zapReceipt, "e")) { + ret.valid = false; + ret.errors.push("e tags dont match"); + } + if (findTag(zapRequest, "amount") === invoice?.amount) { + ret.valid = false; + ret.errors.push("amount tag does not match invoice amount"); + } + if (userCache.getFromCache(ret.receiver)?.zapService !== ret.zapService && !isForwardedZap) { + ret.valid = false; + ret.errors.push("zap service pubkey doesn't match"); + } + return ret; + } catch (e) { + // ignored: console.debug("Invalid zap", zapReceipt, e); + } + } + return { + id: zapReceipt.id, + zapService: zapReceipt.pubkey, + amount: 0, + valid: false, + anonZap: false, + errors: ["invalid zap, parsing failed"], + }; } export interface ParsedZap { - id: HexKey; - event?: HexKey; - receiver?: HexKey; - amount: number; - content?: string; - sender?: HexKey; - valid: boolean; - zapService: HexKey; - anonZap: boolean; - errors: Array; - pollOption?: number; + id: HexKey; + event?: HexKey; + receiver?: HexKey; + amount: number; + content?: string; + sender?: HexKey; + valid: boolean; + zapService: HexKey; + anonZap: boolean; + errors: Array; + pollOption?: number; } diff --git a/packages/system/tests/EventExt.test.ts b/packages/system/tests/EventExt.test.ts index c6bec8d11..b12f8b0f9 100644 --- a/packages/system/tests/EventExt.test.ts +++ b/packages/system/tests/EventExt.test.ts @@ -1,62 +1,62 @@ import { EventExt } from "../src/EventExt"; describe("NIP-10", () => { - it("should extract thread", () => { - const a = { - content: "This is the problem with Lightning....", - id: "868187063f...", - kind: 1, - created_at: 1, - pubkey: "test", - sig: "test", - "tags": [ - ["e", "cbf2375078..."], - ["e", "977ac5d3b6..."], - ["e", "8f99ca1363..."], - ] - } + it("should extract thread", () => { + const a = { + content: "This is the problem with Lightning....", + id: "868187063f...", + kind: 1, + created_at: 1, + pubkey: "test", + sig: "test", + tags: [ + ["e", "cbf2375078..."], + ["e", "977ac5d3b6..."], + ["e", "8f99ca1363..."], + ], + }; - const b = { - "content": "This is a good point, but your ...", - "id": "434ad4a646...", - kind: 1, - created_at: 1, - pubkey: "test", - sig: "test", - "tags": [ - ["e", "cbf2375078..."], - ["e", "868187063f..."], - ["e", "6834ffc491..."], - ] - } + const b = { + content: "This is a good point, but your ...", + id: "434ad4a646...", + kind: 1, + created_at: 1, + pubkey: "test", + sig: "test", + tags: [ + ["e", "cbf2375078..."], + ["e", "868187063f..."], + ["e", "6834ffc491..."], + ], + }; - const c = { - "content": "There is some middle ground ...", - "id": "6834ffc491...", - kind: 1, - created_at: 1, - pubkey: "test", - sig: "test", - "tags": [ - ["e", "cbf2375078...", "", "root"], - ["e", "868187063f...", "", "reply"], - ] - } + const c = { + content: "There is some middle ground ...", + id: "6834ffc491...", + kind: 1, + created_at: 1, + pubkey: "test", + sig: "test", + tags: [ + ["e", "cbf2375078...", "", "root"], + ["e", "868187063f...", "", "reply"], + ], + }; - expect(EventExt.extractThread(a)).toMatchObject({ - root: { key: "e", value: "cbf2375078...", marker: "root" }, - replyTo: { key: "e", value: "8f99ca1363...", marker: "reply" }, - mentions: [{ key: "e", value: "977ac5d3b6...", marker: "mention" }] - }) - expect(EventExt.extractThread(b)).toMatchObject({ - root: { key: "e", value: "cbf2375078...", marker: "root" }, - replyTo: { key: "e", value: "6834ffc491...", marker: "reply" }, - mentions: [{ key: "e", value: "868187063f...", marker: "mention" }] - }) - expect(EventExt.extractThread(c)).toMatchObject({ - root: { key: "e", value: "cbf2375078...", relay: "", marker: "root" }, - replyTo: { key: "e", value: "868187063f...", relay: "", marker: "reply" }, - mentions: [] - }) - }) -}) \ No newline at end of file + expect(EventExt.extractThread(a)).toMatchObject({ + root: { key: "e", value: "cbf2375078...", marker: "root" }, + replyTo: { key: "e", value: "8f99ca1363...", marker: "reply" }, + mentions: [{ key: "e", value: "977ac5d3b6...", marker: "mention" }], + }); + expect(EventExt.extractThread(b)).toMatchObject({ + root: { key: "e", value: "cbf2375078...", marker: "root" }, + replyTo: { key: "e", value: "6834ffc491...", marker: "reply" }, + mentions: [{ key: "e", value: "868187063f...", marker: "mention" }], + }); + expect(EventExt.extractThread(c)).toMatchObject({ + root: { key: "e", value: "cbf2375078...", relay: "", marker: "root" }, + replyTo: { key: "e", value: "868187063f...", relay: "", marker: "reply" }, + mentions: [], + }); + }); +}); diff --git a/packages/system/tests/GossipModel.test.ts b/packages/system/tests/GossipModel.test.ts index eaf9f20ff..72a9b8a99 100644 --- a/packages/system/tests/GossipModel.test.ts +++ b/packages/system/tests/GossipModel.test.ts @@ -1,37 +1,33 @@ -import { splitAllByWriteRelays } from "../src/GossipModel" +import { splitAllByWriteRelays } from "../src/GossipModel"; describe("GossipModel", () => { - it("should not output empty", () => { - const Relays = { - getFromCache: (pk?: string) => { - if (pk) { - return { - pubkey: pk, - created_at: 0, - relays: [] - }; - } - } + it("should not output empty", () => { + const Relays = { + getFromCache: (pk?: string) => { + if (pk) { + return { + pubkey: pk, + created_at: 0, + relays: [], + }; } - const a = [{ - "until": 1686651693, - "limit": 200, - "kinds": [ - 1, - 6, - 6969 - ], - "authors": [ - "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d" - ] - }]; + }, + }; + const a = [ + { + until: 1686651693, + limit: 200, + kinds: [1, 6, 6969], + authors: ["3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d"], + }, + ]; - const output = splitAllByWriteRelays(Relays, a); - expect(output).toEqual([ - { - relay: "", - filters: a - } - ]) - }) -}) \ No newline at end of file + const output = splitAllByWriteRelays(Relays, a); + expect(output).toEqual([ + { + relay: "", + filters: a, + }, + ]); + }); +}); diff --git a/packages/system/tests/Impl.test.ts b/packages/system/tests/Impl.test.ts index 77d73592a..d37459986 100644 --- a/packages/system/tests/Impl.test.ts +++ b/packages/system/tests/Impl.test.ts @@ -1,4 +1,3 @@ - import { schnorr, secp256k1 } from "@noble/curves/secp256k1"; import { Nip4WebCryptoEncryptor } from "../src/impl/nip4"; import { Nip44Encryptor } from "../src/impl/nip44"; @@ -10,36 +9,36 @@ const bKey = secp256k1.utils.randomPrivateKey(); const bPubKey = schnorr.getPublicKey(bKey); describe("NIP-04", () => { - it("should encrypt/decrypt", async () => { - const msg = "test hello, 123"; - const enc = new Nip4WebCryptoEncryptor(); - const sec = enc.getSharedSecret(bytesToHex(aKey), bytesToHex(bPubKey)); + it("should encrypt/decrypt", async () => { + const msg = "test hello, 123"; + const enc = new Nip4WebCryptoEncryptor(); + const sec = enc.getSharedSecret(bytesToHex(aKey), bytesToHex(bPubKey)); - const ciphertext = await enc.encryptData(msg, sec); - expect(ciphertext).toMatch(/^.*\?iv=.*$/i); + const ciphertext = await enc.encryptData(msg, sec); + expect(ciphertext).toMatch(/^.*\?iv=.*$/i); - const dec = new Nip4WebCryptoEncryptor(); - const sec2 = enc.getSharedSecret(bytesToHex(bKey), bytesToHex(aPubKey)); - const plaintext = await dec.decryptData(ciphertext, sec2); - expect(plaintext).toEqual(msg); - }) -}) + const dec = new Nip4WebCryptoEncryptor(); + const sec2 = enc.getSharedSecret(bytesToHex(bKey), bytesToHex(aPubKey)); + const plaintext = await dec.decryptData(ciphertext, sec2); + expect(plaintext).toEqual(msg); + }); +}); describe("NIP-44", () => { - it("should encrypt/decrypt", () => { - const msg = "test hello, 123"; - const enc = new Nip44Encryptor(); - const sec = enc.getSharedSecret(bytesToHex(aKey), bytesToHex(bPubKey)); + it("should encrypt/decrypt", () => { + const msg = "test hello, 123"; + const enc = new Nip44Encryptor(); + const sec = enc.getSharedSecret(bytesToHex(aKey), bytesToHex(bPubKey)); - const ciphertext = enc.encryptData(msg, sec); - const jObj = JSON.parse(ciphertext); - expect(jObj).toHaveProperty("ciphertext") - expect(jObj).toHaveProperty("nonce") - expect(jObj.v).toBe(1); + const ciphertext = enc.encryptData(msg, sec); + const jObj = JSON.parse(ciphertext); + expect(jObj).toHaveProperty("ciphertext"); + expect(jObj).toHaveProperty("nonce"); + expect(jObj.v).toBe(1); - const dec = new Nip44Encryptor(); - const sec2 = enc.getSharedSecret(bytesToHex(bKey), bytesToHex(aPubKey)); - const plaintext = dec.decryptData(ciphertext, sec2); - expect(plaintext).toEqual(msg); - }) -}) \ No newline at end of file + const dec = new Nip44Encryptor(); + const sec2 = enc.getSharedSecret(bytesToHex(bKey), bytesToHex(aPubKey)); + const plaintext = dec.decryptData(ciphertext, sec2); + expect(plaintext).toEqual(msg); + }); +}); diff --git a/packages/system/tests/RequestBuilder.test.ts b/packages/system/tests/RequestBuilder.test.ts index 5ab725c7a..0fa3a78c9 100644 --- a/packages/system/tests/RequestBuilder.test.ts +++ b/packages/system/tests/RequestBuilder.test.ts @@ -20,7 +20,7 @@ const DummyCache = { write: true, }, }, - ] + ], }; }, } as RelayCache; @@ -181,23 +181,24 @@ describe("build diff, large follow list", () => { const start = unixNowMs(); const a = rb.build(DummyCache); - expect(a).toEqual(f.map(a => { - return { - strategy: RequestStrategy.AuthorsRelays, - relay: `wss://${a}.com/`, - filters: [ - { - kinds: [1, 6, 10002, 3, 6969], - authors: [a], - } - ], - } - })); + expect(a).toEqual( + f.map(a => { + return { + strategy: RequestStrategy.AuthorsRelays, + relay: `wss://${a}.com/`, + filters: [ + { + kinds: [1, 6, 10002, 3, 6969], + authors: [a], + }, + ], + }; + }) + ); expect(unixNowMs() - start).toBeLessThan(500); const start2 = unixNowMs(); const b = rb.buildDiff(DummyCache, rb.buildRaw().flatMap(expandFilter)); expect(b).toEqual([]); expect(unixNowMs() - start2).toBeLessThan(100); - -}) \ No newline at end of file +}); diff --git a/packages/system/tests/RequestMerger.test.ts b/packages/system/tests/RequestMerger.test.ts index f3cefda99..30ff3a1a0 100644 --- a/packages/system/tests/RequestMerger.test.ts +++ b/packages/system/tests/RequestMerger.test.ts @@ -108,50 +108,50 @@ describe("flatMerge", () => { }); }); -describe('canMerge', () => { +describe("canMerge", () => { it("should have 0 distance", () => { const a = { ids: "a", - keys: 1 + keys: 1, }; const b = { ids: "a", - keys: 1 + keys: 1, }; expect(canMergeFilters(a, b)).toEqual(true); }); it("should have 1 distance", () => { const a = { ids: "a", - keys: 1 + keys: 1, }; const b = { ids: "b", - keys: 1 + keys: 1, }; expect(canMergeFilters(a, b)).toEqual(true); }); it("should have 10 distance", () => { const a = { ids: "a", - keys: 1 + keys: 1, }; const b = { ids: "a", kinds: 1, - keys: 2 + keys: 2, }; expect(canMergeFilters(a, b)).toEqual(false); }); it("should have 11 distance", () => { const a = { ids: "a", - keys: 1 + keys: 1, }; const b = { ids: "b", kinds: 1, - keys: 2 + keys: 2, }; expect(canMergeFilters(a, b)).toEqual(false); }); @@ -160,13 +160,13 @@ describe('canMerge', () => { since: 1, until: 100, kinds: [1], - authors: ["kieran", "snort", "c", "d", "e"] + authors: ["kieran", "snort", "c", "d", "e"], }; const b = { since: 1, until: 100, kinds: [6969], - authors: ["kieran", "snort", "c", "d", "e"] + authors: ["kieran", "snort", "c", "d", "e"], }; expect(canMergeFilters(a, b)).toEqual(true); }); @@ -175,14 +175,14 @@ describe('canMerge', () => { since: 1, until: 100, kinds: [1], - authors: ["f", "kieran", "snort", "c", "d"] + authors: ["f", "kieran", "snort", "c", "d"], }; const b = { since: 1, until: 100, kinds: [1], - authors: ["kieran", "snort", "c", "d", "e"] + authors: ["kieran", "snort", "c", "d", "e"], }; expect(canMergeFilters(a, b)).toEqual(true); }); -}) \ No newline at end of file +}); diff --git a/packages/system/tests/setupTests.ts b/packages/system/tests/setupTests.ts index 59d570e55..12e28cff2 100644 --- a/packages/system/tests/setupTests.ts +++ b/packages/system/tests/setupTests.ts @@ -2,4 +2,4 @@ import { TextEncoder, TextDecoder } from "util"; import { Crypto } from "@peculiar/webcrypto"; Object.assign(global, { TextDecoder, TextEncoder }); -Object.assign(globalThis.window.crypto, new Crypto()); \ No newline at end of file +Object.assign(globalThis.window.crypto, new Crypto()); diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 7251f9322..adccb2983 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -22,10 +22,7 @@ "depends": [] }, "externalBin": [], - "icon": [ - "icons/128x128.png", - "icons/128x128@2x.png" - ], + "icon": ["icons/128x128.png", "icons/128x128@2x.png"], "identifier": "social.snort.app", "longDescription": "", "macOS": {