Merge pull request 'Gossip tweaks' (#609) from new-gossip into main
Reviewed-on: #609
This commit is contained in:
commit
b334f764dc
30
.drone.yml
30
.drone.yml
@ -6,7 +6,7 @@ concurrency:
|
|||||||
limit: 1
|
limit: 1
|
||||||
trigger:
|
trigger:
|
||||||
branch:
|
branch:
|
||||||
- main
|
- main
|
||||||
metadata:
|
metadata:
|
||||||
namespace: git
|
namespace: git
|
||||||
steps:
|
steps:
|
||||||
@ -34,9 +34,9 @@ steps:
|
|||||||
- img build -t voidic/snort:latest --platform linux/amd64,linux/arm64 -f Dockerfile.prebuilt .
|
- img build -t voidic/snort:latest --platform linux/amd64,linux/arm64 -f Dockerfile.prebuilt .
|
||||||
- img push voidic/snort:latest
|
- img push voidic/snort:latest
|
||||||
volumes:
|
volumes:
|
||||||
- name: cache
|
- name: cache
|
||||||
claim:
|
claim:
|
||||||
name: docker-cache
|
name: docker-cache
|
||||||
---
|
---
|
||||||
kind: pipeline
|
kind: pipeline
|
||||||
type: kubernetes
|
type: kubernetes
|
||||||
@ -60,9 +60,9 @@ steps:
|
|||||||
- yarn workspace @snort/app eslint
|
- yarn workspace @snort/app eslint
|
||||||
- yarn workspace @snort/app prettier --check .
|
- yarn workspace @snort/app prettier --check .
|
||||||
volumes:
|
volumes:
|
||||||
- name: cache
|
- name: cache
|
||||||
claim:
|
claim:
|
||||||
name: docker-cache
|
name: docker-cache
|
||||||
---
|
---
|
||||||
kind: pipeline
|
kind: pipeline
|
||||||
type: kubernetes
|
type: kubernetes
|
||||||
@ -71,7 +71,7 @@ concurrency:
|
|||||||
limit: 1
|
limit: 1
|
||||||
trigger:
|
trigger:
|
||||||
branch:
|
branch:
|
||||||
- main
|
- main
|
||||||
metadata:
|
metadata:
|
||||||
namespace: git
|
namespace: git
|
||||||
steps:
|
steps:
|
||||||
@ -98,9 +98,9 @@ steps:
|
|||||||
- 'git commit -a -m "chore: Update translations"'
|
- 'git commit -a -m "chore: Update translations"'
|
||||||
- git push -u origin main
|
- git push -u origin main
|
||||||
volumes:
|
volumes:
|
||||||
- name: cache
|
- name: cache
|
||||||
claim:
|
claim:
|
||||||
name: docker-cache
|
name: docker-cache
|
||||||
---
|
---
|
||||||
kind: pipeline
|
kind: pipeline
|
||||||
type: kubernetes
|
type: kubernetes
|
||||||
@ -109,7 +109,7 @@ concurrency:
|
|||||||
limit: 1
|
limit: 1
|
||||||
trigger:
|
trigger:
|
||||||
event:
|
event:
|
||||||
- tag
|
- tag
|
||||||
metadata:
|
metadata:
|
||||||
namespace: git
|
namespace: git
|
||||||
steps:
|
steps:
|
||||||
@ -137,6 +137,6 @@ steps:
|
|||||||
- img build -t voidic/snort:$DRONE_TAG --platform linux/amd64,linux/arm64 -f Dockerfile.prebuilt .
|
- img build -t voidic/snort:$DRONE_TAG --platform linux/amd64,linux/arm64 -f Dockerfile.prebuilt .
|
||||||
- img push voidic/snort:$DRONE_TAG
|
- img push voidic/snort:$DRONE_TAG
|
||||||
volumes:
|
volumes:
|
||||||
- name: cache
|
- name: cache
|
||||||
claim:
|
claim:
|
||||||
name: docker-cache
|
name: docker-cache
|
||||||
|
12
.github/workflows/release.yml
vendored
12
.github/workflows/release.yml
vendored
@ -29,13 +29,13 @@ jobs:
|
|||||||
- name: Rust cache
|
- name: Rust cache
|
||||||
uses: swatinem/rust-cache@v2
|
uses: swatinem/rust-cache@v2
|
||||||
with:
|
with:
|
||||||
workspaces: './src-tauri -> target'
|
workspaces: "./src-tauri -> target"
|
||||||
|
|
||||||
- name: Sync node version and setup cache
|
- name: Sync node version and setup cache
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: '16'
|
node-version: "16"
|
||||||
cache: 'yarn'
|
cache: "yarn"
|
||||||
- name: Install frontend dependencies
|
- name: Install frontend dependencies
|
||||||
run: yarn install
|
run: yarn install
|
||||||
- name: Build the app
|
- name: Build the app
|
||||||
@ -44,7 +44,7 @@ jobs:
|
|||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
with:
|
||||||
tagName: ${{ github.ref_name }}
|
tagName: ${{ github.ref_name }}
|
||||||
releaseName: 'Snort v__VERSION__'
|
releaseName: "Snort v__VERSION__"
|
||||||
releaseBody: 'See the assets to download and install this version.'
|
releaseBody: "See the assets to download and install this version."
|
||||||
releaseDraft: true
|
releaseDraft: true
|
||||||
prerelease: false
|
prerelease: false
|
||||||
|
20
.vscode/settings.json
vendored
20
.vscode/settings.json
vendored
@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"files.exclude": {
|
"files.exclude": {
|
||||||
"**/.git": true,
|
"**/.git": true,
|
||||||
"**/.svn": true,
|
"**/.svn": true,
|
||||||
"**/.hg": true,
|
"**/.hg": true,
|
||||||
"**/CVS": true,
|
"**/CVS": true,
|
||||||
"**/.DS_Store": true,
|
"**/.DS_Store": true,
|
||||||
"**/Thumbs.db": true,
|
"**/Thumbs.db": true,
|
||||||
"**/node_modules": true
|
"**/node_modules": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -63,6 +63,7 @@ $ yarn build
|
|||||||
Translations are managed on [Crowdin](https://crowdin.com/project/snort)
|
Translations are managed on [Crowdin](https://crowdin.com/project/snort)
|
||||||
|
|
||||||
To extract translations run:
|
To extract translations run:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
yarn workspace @snort/app intl-extract
|
yarn workspace @snort/app intl-extract
|
||||||
yarn workspace @snort/app intl-compile
|
yarn workspace @snort/app intl-compile
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
interface Env {
|
interface Env {}
|
||||||
}
|
|
||||||
|
|
||||||
export const onRequest: PagesFunction<Env> = async (context) => {
|
export const onRequest: PagesFunction<Env> = async context => {
|
||||||
const id = context.params.id as string;
|
const id = context.params.id as string;
|
||||||
|
|
||||||
const next = await context.next();
|
const next = await context.next();
|
||||||
@ -11,16 +10,16 @@ export const onRequest: PagesFunction<Env> = async (context) => {
|
|||||||
body: await next.arrayBuffer(),
|
body: await next.arrayBuffer(),
|
||||||
headers: {
|
headers: {
|
||||||
"user-agent": "Snort-Functions/1.0 (https://snort.social)",
|
"user-agent": "Snort-Functions/1.0 (https://snort.social)",
|
||||||
"content-type": "text/plain"
|
"content-type": "text/plain",
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
if (rsp.ok) {
|
if (rsp.ok) {
|
||||||
const body = await rsp.text();
|
const body = await rsp.text();
|
||||||
if (body.length > 0) {
|
if (body.length > 0) {
|
||||||
return new Response(body, {
|
return new Response(body, {
|
||||||
headers: {
|
headers: {
|
||||||
"content-type": "text/html"
|
"content-type": "text/html",
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -28,4 +27,4 @@ export const onRequest: PagesFunction<Env> = async (context) => {
|
|||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
return next;
|
return next;
|
||||||
}
|
};
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
interface Env {
|
interface Env {}
|
||||||
}
|
|
||||||
|
|
||||||
export const onRequest: PagesFunction<Env> = async (context) => {
|
export const onRequest: PagesFunction<Env> = async context => {
|
||||||
const id = context.params.id as string;
|
const id = context.params.id as string;
|
||||||
|
|
||||||
const next = await context.next();
|
const next = await context.next();
|
||||||
@ -11,16 +10,16 @@ export const onRequest: PagesFunction<Env> = async (context) => {
|
|||||||
body: await next.arrayBuffer(),
|
body: await next.arrayBuffer(),
|
||||||
headers: {
|
headers: {
|
||||||
"user-agent": "Snort-Functions/1.0 (https://snort.social)",
|
"user-agent": "Snort-Functions/1.0 (https://snort.social)",
|
||||||
"content-type": "text/plain"
|
"content-type": "text/plain",
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
if (rsp.ok) {
|
if (rsp.ok) {
|
||||||
const body = await rsp.text();
|
const body = await rsp.text();
|
||||||
if (body.length > 0) {
|
if (body.length > 0) {
|
||||||
return new Response(body, {
|
return new Response(body, {
|
||||||
headers: {
|
headers: {
|
||||||
"content-type": "text/html"
|
"content-type": "text/html",
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -28,4 +27,4 @@ export const onRequest: PagesFunction<Env> = async (context) => {
|
|||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
return next;
|
return next;
|
||||||
}
|
};
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "esnext",
|
"target": "esnext",
|
||||||
"module": "esnext",
|
"module": "esnext",
|
||||||
"lib": ["esnext"],
|
"lib": ["esnext"],
|
||||||
"types": ["@cloudflare/workers-types"]
|
"types": ["@cloudflare/workers-types"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,5 +11,10 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tauri-apps/cli": "^1.2.3",
|
"@tauri-apps/cli": "^1.2.3",
|
||||||
"@cloudflare/workers-types": "^4.20230307.0"
|
"@cloudflare/workers-types": "^4.20230307.0"
|
||||||
|
},
|
||||||
|
"prettier": {
|
||||||
|
"printWidth": 120,
|
||||||
|
"bracketSameLine": true,
|
||||||
|
"arrowParens": "avoid"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"printWidth": 120,
|
|
||||||
"bracketSameLine": true,
|
|
||||||
"arrowParens": "avoid"
|
|
||||||
}
|
|
@ -8,13 +8,11 @@ import Icon from "Icons/Icon";
|
|||||||
import { dedupeByPubkey, findTag, tagFilterOfTextRepost } from "SnortUtils";
|
import { dedupeByPubkey, findTag, tagFilterOfTextRepost } from "SnortUtils";
|
||||||
import ProfileImage from "Element/ProfileImage";
|
import ProfileImage from "Element/ProfileImage";
|
||||||
import useTimelineFeed, { TimelineFeed, TimelineSubject } from "Feed/TimelineFeed";
|
import useTimelineFeed, { TimelineFeed, TimelineSubject } from "Feed/TimelineFeed";
|
||||||
import LoadMore from "Element/LoadMore";
|
|
||||||
import Zap from "Element/Zap";
|
import Zap from "Element/Zap";
|
||||||
import Note from "Element/Note";
|
import Note from "Element/Note";
|
||||||
import NoteReaction from "Element/NoteReaction";
|
import NoteReaction from "Element/NoteReaction";
|
||||||
import useModeration from "Hooks/useModeration";
|
import useModeration from "Hooks/useModeration";
|
||||||
import ProfilePreview from "Element/ProfilePreview";
|
import ProfilePreview from "Element/ProfilePreview";
|
||||||
import Skeleton from "Element/Skeleton";
|
|
||||||
import { UserCache } from "Cache";
|
import { UserCache } from "Cache";
|
||||||
|
|
||||||
export interface TimelineProps {
|
export interface TimelineProps {
|
||||||
@ -142,11 +140,11 @@ const Timeline = (props: TimelineProps) => {
|
|||||||
)}
|
)}
|
||||||
{mainFeed.map(eventElement)}
|
{mainFeed.map(eventElement)}
|
||||||
{(props.loadMore === undefined || props.loadMore === true) && (
|
{(props.loadMore === undefined || props.loadMore === true) && (
|
||||||
<LoadMore onLoadMore={() => feed.loadMore()} shouldLoadMore={!feed.loading}>
|
<div className="flex f-center">
|
||||||
<Skeleton width="100%" height="120px" margin="0 0 16px 0" />
|
<button type="button" onClick={() => feed.loadMore()}>
|
||||||
<Skeleton width="100%" height="120px" margin="0 0 16px 0" />
|
<FormattedMessage defaultMessage="Load more" />
|
||||||
<Skeleton width="100%" height="120px" margin="0 0 16px 0" />
|
</button>
|
||||||
</LoadMore>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { useCallback, useEffect, useMemo } from "react";
|
import { useCallback, useEffect, useMemo } from "react";
|
||||||
import { EventKind, u256, FlatNoteStore, RequestBuilder } from "@snort/system";
|
import { EventKind, FlatNoteStore, RequestBuilder } from "@snort/system";
|
||||||
import { useRequestBuilder } from "@snort/system-react";
|
import { useRequestBuilder } from "@snort/system-react";
|
||||||
|
|
||||||
import { unixNow, unwrap, tagFilterOfTextRepost } from "SnortUtils";
|
import { unixNow, unwrap, tagFilterOfTextRepost } from "SnortUtils";
|
||||||
|
@ -65,4 +65,4 @@
|
|||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"description": ""
|
"description": ""
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { NostrError } from "../common"
|
import { NostrError } from "../common"
|
||||||
import { RawEvent, parseEvent } from "../event"
|
import { RawEvent, parseEvent } from "../event"
|
||||||
import { Conn } from "./conn"
|
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 { EventEmitter } from "./emitter"
|
||||||
import { fetchRelayInfo, ReadyState, Relay } from "./relay"
|
import { fetchRelayInfo, ReadyState, Relay } from "./relay"
|
||||||
import { Filters } from "../filters"
|
import { Filters } from "../filters"
|
||||||
@ -71,9 +71,9 @@ export class Nostr extends EventEmitter {
|
|||||||
opts?.fetchInfo === false
|
opts?.fetchInfo === false
|
||||||
? Promise.resolve({})
|
? Promise.resolve({})
|
||||||
: fetchRelayInfo(relayUrl).catch((e) => {
|
: fetchRelayInfo(relayUrl).catch((e) => {
|
||||||
this.#error(e)
|
this.#error(e)
|
||||||
return {}
|
return {}
|
||||||
})
|
})
|
||||||
|
|
||||||
// If there is no existing connection, open a new one.
|
// If there is no existing connection, open a new one.
|
||||||
const conn = new Conn({
|
const conn = new Conn({
|
||||||
@ -128,7 +128,8 @@ export class Nostr extends EventEmitter {
|
|||||||
if (conn.relay.readyState !== ReadyState.CONNECTING) {
|
if (conn.relay.readyState !== ReadyState.CONNECTING) {
|
||||||
this.#error(
|
this.#error(
|
||||||
new NostrError(
|
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
|
relay.info === undefined
|
||||||
? undefined
|
? undefined
|
||||||
: // Deep copy of the info.
|
: // Deep copy of the info.
|
||||||
JSON.parse(JSON.stringify(relay.info))
|
JSON.parse(JSON.stringify(relay.info))
|
||||||
return { ...relay, info }
|
return { ...relay, info }
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import * as secp from "@noble/curves/secp256k1"
|
import * as secp from "@noble/curves/secp256k1"
|
||||||
import * as utils from "@noble/curves/abstract/utils";
|
import * as utils from "@noble/curves/abstract/utils"
|
||||||
import {sha256 as sha} from "@noble/hashes/sha256";
|
import { sha256 as sha } from "@noble/hashes/sha256"
|
||||||
import base64 from "base64-js"
|
import base64 from "base64-js"
|
||||||
import { bech32 } from "bech32"
|
import { bech32 } from "bech32"
|
||||||
|
|
||||||
@ -92,11 +92,7 @@ export function schnorrSign(data: Hex, priv: PrivateKey): Hex {
|
|||||||
/**
|
/**
|
||||||
* Verify that the elliptic curve signature is correct.
|
* Verify that the elliptic curve signature is correct.
|
||||||
*/
|
*/
|
||||||
export function schnorrVerify(
|
export function schnorrVerify(sig: Hex, data: Hex, key: PublicKey): boolean {
|
||||||
sig: Hex,
|
|
||||||
data: Hex,
|
|
||||||
key: PublicKey
|
|
||||||
): boolean {
|
|
||||||
return secp.schnorr.verify(sig.toString(), data.toString(), key.toString())
|
return secp.schnorr.verify(sig.toString(), data.toString(), key.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -159,14 +159,14 @@ export async function signEvent<T extends RawEvent>(
|
|||||||
* Parse an event from its raw format.
|
* Parse an event from its raw format.
|
||||||
*/
|
*/
|
||||||
export function parseEvent(event: RawEvent): Event {
|
export function parseEvent(event: RawEvent): Event {
|
||||||
if (event.id !== (serializeEventId(event))) {
|
if (event.id !== serializeEventId(event)) {
|
||||||
throw new NostrError(
|
throw new NostrError(
|
||||||
`invalid id ${event.id} for event ${JSON.stringify(
|
`invalid id ${event.id} for event ${JSON.stringify(
|
||||||
event
|
event
|
||||||
)}, expected ${serializeEventId(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)}`)
|
throw new NostrError(`invalid signature for event ${JSON.stringify(event)}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -221,9 +221,7 @@ export function parseEvent(event: RawEvent): Event {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function serializeEventId(
|
function serializeEventId(event: UnsignedWithPubkey<RawEvent>): EventId {
|
||||||
event: UnsignedWithPubkey<RawEvent>
|
|
||||||
): EventId {
|
|
||||||
const serialized = JSON.stringify([
|
const serialized = JSON.stringify([
|
||||||
0,
|
0,
|
||||||
event.pubkey,
|
event.pubkey,
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
const fs = require("fs")
|
const fs = require("fs")
|
||||||
|
|
||||||
const isProduction = process.env.NODE_ENV == "production";
|
const isProduction = process.env.NODE_ENV == "production"
|
||||||
|
|
||||||
const entry = {
|
const entry = {
|
||||||
lib: "./src/index.ts",
|
lib: "./src/index.ts",
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Regex to match email address
|
* Regex to match email address
|
||||||
*/
|
*/
|
||||||
export const EmailRegex =
|
export const EmailRegex =
|
||||||
// eslint-disable-next-line no-useless-escape
|
// 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,}))$/;
|
/^(([^<>()\[\]\\.,;:\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,}))$/;
|
||||||
|
|
||||||
|
@ -1,14 +1,13 @@
|
|||||||
|
|
||||||
declare module "light-bolt11-decoder" {
|
declare module "light-bolt11-decoder" {
|
||||||
export function decode(pr?: string): ParsedInvoice;
|
export function decode(pr?: string): ParsedInvoice;
|
||||||
|
|
||||||
export interface ParsedInvoice {
|
export interface ParsedInvoice {
|
||||||
paymentRequest: string;
|
paymentRequest: string;
|
||||||
sections: Section[];
|
sections: Section[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Section {
|
export interface Section {
|
||||||
name: string;
|
name: string;
|
||||||
value: string | Uint8Array | number | undefined;
|
value: string | Uint8Array | number | undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -38,7 +38,7 @@ export abstract class FeedCache<TCached> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async preload() {
|
async preload() {
|
||||||
const keys = await this.table?.toCollection().primaryKeys() ?? [];
|
const keys = (await this.table?.toCollection().primaryKeys()) ?? [];
|
||||||
this.onTable = new Set<string>(keys.map(a => a as string));
|
this.onTable = new Set<string>(keys.map(a => a as string));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,4 +3,4 @@ export * from "./lnurl";
|
|||||||
export * from "./utils";
|
export * from "./utils";
|
||||||
export * from "./work-queue";
|
export * from "./work-queue";
|
||||||
export * from "./feed-cache";
|
export * from "./feed-cache";
|
||||||
export * from "./invoices";
|
export * from "./invoices";
|
||||||
|
@ -1,48 +1,55 @@
|
|||||||
|
|
||||||
import { bytesToHex } from "@noble/hashes/utils";
|
import { bytesToHex } from "@noble/hashes/utils";
|
||||||
import { decode as invoiceDecode } from "light-bolt11-decoder";
|
import { decode as invoiceDecode } from "light-bolt11-decoder";
|
||||||
|
|
||||||
export interface InvoiceDetails {
|
export interface InvoiceDetails {
|
||||||
amount?: number;
|
amount?: number;
|
||||||
expire?: number;
|
expire?: number;
|
||||||
timestamp?: number;
|
timestamp?: number;
|
||||||
description?: string;
|
description?: string;
|
||||||
descriptionHash?: string;
|
descriptionHash?: string;
|
||||||
paymentHash?: string;
|
paymentHash?: string;
|
||||||
expired: boolean;
|
expired: boolean;
|
||||||
pr: string;
|
pr: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function decodeInvoice(pr: string): InvoiceDetails | undefined {
|
export function decodeInvoice(pr: string): InvoiceDetails | undefined {
|
||||||
try {
|
try {
|
||||||
const parsed = invoiceDecode(pr);
|
const parsed = invoiceDecode(pr);
|
||||||
|
|
||||||
const amountSection = parsed.sections.find(a => a.name === "amount");
|
const amountSection = parsed.sections.find(a => a.name === "amount");
|
||||||
const amount = amountSection ? Number(amountSection.value as number | string) : undefined;
|
const amount = amountSection ? Number(amountSection.value as number | string) : undefined;
|
||||||
|
|
||||||
const timestampSection = parsed.sections.find(a => a.name === "timestamp");
|
const timestampSection = parsed.sections.find(a => a.name === "timestamp");
|
||||||
const timestamp = timestampSection ? Number(timestampSection.value as number | string) : undefined;
|
const timestamp = timestampSection ? Number(timestampSection.value as number | string) : undefined;
|
||||||
|
|
||||||
const expirySection = parsed.sections.find(a => a.name === "expiry");
|
const expirySection = parsed.sections.find(a => a.name === "expiry");
|
||||||
const expire = expirySection ? Number(expirySection.value as number | string) : undefined;
|
const expire = expirySection ? Number(expirySection.value as number | string) : undefined;
|
||||||
const descriptionSection = parsed.sections.find(a => a.name === "description")?.value;
|
const descriptionSection = parsed.sections.find(a => a.name === "description")?.value;
|
||||||
const descriptionHashSection = parsed.sections.find(a => a.name === "description_hash")?.value;
|
const descriptionHashSection = parsed.sections.find(a => a.name === "description_hash")?.value;
|
||||||
const paymentHashSection = parsed.sections.find(a => a.name === "payment_hash")?.value;
|
const paymentHashSection = parsed.sections.find(a => a.name === "payment_hash")?.value;
|
||||||
const ret = {
|
const ret = {
|
||||||
pr,
|
pr,
|
||||||
amount: amount,
|
amount: amount,
|
||||||
expire: timestamp && expire ? timestamp + expire : undefined,
|
expire: timestamp && expire ? timestamp + expire : undefined,
|
||||||
timestamp: timestamp,
|
timestamp: timestamp,
|
||||||
description: descriptionSection as string | undefined,
|
description: descriptionSection as string | undefined,
|
||||||
descriptionHash: descriptionHashSection ? (typeof descriptionHashSection === "string" ? descriptionHashSection as string : bytesToHex(descriptionHashSection as Uint8Array)) : undefined,
|
descriptionHash: descriptionHashSection
|
||||||
paymentHash: paymentHashSection ? (typeof paymentHashSection === "string" ? paymentHashSection as string : bytesToHex(paymentHashSection as Uint8Array)) : undefined,
|
? typeof descriptionHashSection === "string"
|
||||||
expired: false,
|
? (descriptionHashSection as string)
|
||||||
};
|
: bytesToHex(descriptionHashSection as Uint8Array)
|
||||||
if (ret.expire) {
|
: undefined,
|
||||||
ret.expired = ret.expire < new Date().getTime() / 1000;
|
paymentHash: paymentHashSection
|
||||||
}
|
? typeof paymentHashSection === "string"
|
||||||
return ret;
|
? (paymentHashSection as string)
|
||||||
} catch (e) {
|
: bytesToHex(paymentHashSection as Uint8Array)
|
||||||
console.error(e);
|
: undefined,
|
||||||
|
expired: false,
|
||||||
|
};
|
||||||
|
if (ret.expire) {
|
||||||
|
ret.expired = ret.expire < new Date().getTime() / 1000;
|
||||||
}
|
}
|
||||||
|
return ret;
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -205,26 +205,26 @@ export class LNURL {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface LNURLService {
|
export interface LNURLService {
|
||||||
tag: string
|
tag: string;
|
||||||
nostrPubkey?: string
|
nostrPubkey?: string;
|
||||||
minSendable?: number
|
minSendable?: number;
|
||||||
maxSendable?: number
|
maxSendable?: number;
|
||||||
metadata: string
|
metadata: string;
|
||||||
callback: string
|
callback: string;
|
||||||
commentAllowed?: number
|
commentAllowed?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LNURLStatus {
|
export interface LNURLStatus {
|
||||||
status: "SUCCESS" | "ERROR"
|
status: "SUCCESS" | "ERROR";
|
||||||
reason?: string
|
reason?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LNURLInvoice extends LNURLStatus {
|
export interface LNURLInvoice extends LNURLStatus {
|
||||||
pr?: string
|
pr?: string;
|
||||||
successAction?: LNURLSuccessAction
|
successAction?: LNURLSuccessAction;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LNURLSuccessAction {
|
export interface LNURLSuccessAction {
|
||||||
description?: string
|
description?: string;
|
||||||
url?: string
|
url?: string;
|
||||||
}
|
}
|
||||||
|
@ -71,7 +71,10 @@ export function countMembers(a: any) {
|
|||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function equalProp(a: string | number | Array<string | number> | undefined, b: string | number | Array<string | number> | undefined) {
|
export function equalProp(
|
||||||
|
a: string | number | Array<string | number> | undefined,
|
||||||
|
b: string | number | Array<string | number> | undefined
|
||||||
|
) {
|
||||||
if ((a !== undefined && b === undefined) || (a === undefined && b !== undefined)) {
|
if ((a !== undefined && b === undefined) || (a === undefined && b !== undefined)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -130,7 +133,7 @@ export function appendDedupe<T>(a?: Array<T>, b?: Array<T>) {
|
|||||||
|
|
||||||
export const sha256 = (str: string | Uint8Array): string => {
|
export const sha256 = (str: string | Uint8Array): string => {
|
||||||
return utils.bytesToHex(sha2(str));
|
return utils.bytesToHex(sha2(str));
|
||||||
}
|
};
|
||||||
|
|
||||||
export function getPublicKey(privKey: string) {
|
export function getPublicKey(privKey: string) {
|
||||||
return utils.bytesToHex(secp.schnorr.getPublicKey(privKey));
|
return utils.bytesToHex(secp.schnorr.getPublicKey(privKey));
|
||||||
|
@ -3,54 +3,49 @@
|
|||||||
React hooks for @snort/system
|
React hooks for @snort/system
|
||||||
|
|
||||||
Sample:
|
Sample:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
import { useMemo } from "react"
|
import { useMemo } from "react";
|
||||||
import { useRequestBuilder, useUserProfile } from "@snort/system-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
|
// singleton nostr system class
|
||||||
const System = new NostrSystem({});
|
const System = new NostrSystem({});
|
||||||
|
|
||||||
// some bootstrap relays
|
// 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 }) {
|
export function Note({ ev }: { ev: TaggedRawEvent }) {
|
||||||
// get profile from cache or request a profile from relays
|
// get profile from cache or request a profile from relays
|
||||||
const profile = useUserProfile(System, ev.pubkey);
|
const profile = useUserProfile(System, ev.pubkey);
|
||||||
|
|
||||||
return <div>
|
return (
|
||||||
Post by: {profile.name ?? profile.display_name}
|
<div>
|
||||||
<p>
|
Post by: {profile.name ?? profile.display_name}
|
||||||
{ev.content}
|
<p>{ev.content}</p>
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function UserPosts(props: { pubkey: string }) {
|
export function UserPosts(props: { pubkey: string }) {
|
||||||
const sub = useMemo(() => {
|
const sub = useMemo(() => {
|
||||||
const rb = new RequestBuilder("get-posts");
|
const rb = new RequestBuilder("get-posts");
|
||||||
rb.withFilter()
|
rb.withFilter().authors([props.pubkey]).kinds([1]).limit(10);
|
||||||
.authors([props.pubkey])
|
|
||||||
.kinds([1])
|
|
||||||
.limit(10);
|
|
||||||
|
|
||||||
return rb;
|
return rb;
|
||||||
}, [props.pubkey]);
|
}, [props.pubkey]);
|
||||||
|
|
||||||
const data = useRequestBuilder<FlatNoteStore>(System, FlatNoteStore, sub);
|
const data = useRequestBuilder < FlatNoteStore > (System, FlatNoteStore, sub);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{data.data.map(a => <Note ev={a} />)}
|
{data.data.map(a => (
|
||||||
</>
|
<Note ev={a} />
|
||||||
)
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function MyApp() {
|
export function MyApp() {
|
||||||
return (
|
return <UserPosts pubkey="63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed" />;
|
||||||
<UserPosts pubkey="63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed" />
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
@ -1,48 +1,42 @@
|
|||||||
import { useMemo } from "react"
|
import { useMemo } from "react";
|
||||||
import { useRequestBuilder, useUserProfile } from "../src";
|
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({});
|
const System = new NostrSystem({});
|
||||||
|
|
||||||
// some bootstrap relays
|
// 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 }) {
|
export function Note({ ev }: { ev: TaggedRawEvent }) {
|
||||||
const profile = useUserProfile(System, ev.pubkey);
|
const profile = useUserProfile(System, ev.pubkey);
|
||||||
|
|
||||||
return <div>
|
return (
|
||||||
Post by: {profile.name ?? profile.display_name}
|
<div>
|
||||||
<p>
|
Post by: {profile.name ?? profile.display_name}
|
||||||
{ev.content}
|
<p>{ev.content}</p>
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function UserPosts(props: { pubkey: string }) {
|
export function UserPosts(props: { pubkey: string }) {
|
||||||
const sub = useMemo(() => {
|
const sub = useMemo(() => {
|
||||||
const rb = new RequestBuilder("get-posts");
|
const rb = new RequestBuilder("get-posts");
|
||||||
rb.withFilter()
|
rb.withFilter().authors([props.pubkey]).kinds([1]).limit(10);
|
||||||
.authors([props.pubkey])
|
|
||||||
.kinds([1])
|
|
||||||
.limit(10);
|
|
||||||
|
|
||||||
return rb;
|
return rb;
|
||||||
}, [props.pubkey]);
|
}, [props.pubkey]);
|
||||||
|
|
||||||
const data = useRequestBuilder<FlatNoteStore>(System, FlatNoteStore, sub);
|
const data = useRequestBuilder<FlatNoteStore>(System, FlatNoteStore, sub);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{data.data.map(a => <Note ev={a} />)}
|
{data.data.map(a => (
|
||||||
</>
|
<Note ev={a} />
|
||||||
)
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function MyApp() {
|
export function MyApp() {
|
||||||
return (
|
return <UserPosts pubkey="63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed" />;
|
||||||
<UserPosts pubkey="63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed" />
|
}
|
||||||
)
|
|
||||||
}
|
|
||||||
|
@ -20,4 +20,4 @@
|
|||||||
"@snort/system": "^1.0.16",
|
"@snort/system": "^1.0.16",
|
||||||
"@snort/shared": "^1.0.4"
|
"@snort/shared": "^1.0.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
export * from "./useRequestBuilder";
|
export * from "./useRequestBuilder";
|
||||||
export * from "./useSystemState";
|
export * from "./useSystemState";
|
||||||
export * from "./useUserProfile";
|
export * from "./useUserProfile";
|
||||||
|
@ -7,7 +7,7 @@ import { unwrap } from "@snort/shared";
|
|||||||
*/
|
*/
|
||||||
const useRequestBuilder = <TStore extends NoteStore, TSnapshot = ReturnType<TStore["getSnapshotData"]>>(
|
const useRequestBuilder = <TStore extends NoteStore, TSnapshot = ReturnType<TStore["getSnapshotData"]>>(
|
||||||
system: SystemInterface,
|
system: SystemInterface,
|
||||||
type: { new(): TStore },
|
type: { new (): TStore },
|
||||||
rb: RequestBuilder | null
|
rb: RequestBuilder | null
|
||||||
) => {
|
) => {
|
||||||
const subscribe = (onChanged: () => void) => {
|
const subscribe = (onChanged: () => void) => {
|
||||||
@ -37,4 +37,4 @@ const useRequestBuilder = <TStore extends NoteStore, TSnapshot = ReturnType<TSto
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export { useRequestBuilder };
|
export { useRequestBuilder };
|
||||||
|
@ -16,7 +16,7 @@ export function useUserProfile(system: NostrSystem, pubKey?: HexKey): MetadataCa
|
|||||||
if (pubKey) {
|
if (pubKey) {
|
||||||
system.ProfileLoader.UntrackMetadata(pubKey);
|
system.ProfileLoader.UntrackMetadata(pubKey);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
},
|
},
|
||||||
() => system.ProfileLoader.Cache.getFromCache(pubKey)
|
() => system.ProfileLoader.Cache.getFromCache(pubKey)
|
||||||
);
|
);
|
||||||
|
@ -3,14 +3,15 @@
|
|||||||
A collection of caching and querying techniquies used by https://snort.social to serve all content from the nostr protocol.
|
A collection of caching and querying techniquies used by https://snort.social to serve all content from the nostr protocol.
|
||||||
|
|
||||||
Simple example:
|
Simple example:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
import {
|
import {
|
||||||
NostrSystem,
|
NostrSystem,
|
||||||
EventPublisher,
|
EventPublisher,
|
||||||
UserRelaysCache,
|
UserRelaysCache,
|
||||||
RequestBuilder,
|
RequestBuilder,
|
||||||
FlatNoteStore,
|
FlatNoteStore,
|
||||||
StoreSnapshot
|
StoreSnapshot
|
||||||
} from "@snort/system"
|
} from "@snort/system"
|
||||||
|
|
||||||
// Provided in-memory / indexedDb cache for relays
|
// 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
|
// these patterns will be managed in @snort/system-react to make it easier to use react or other UI frameworks
|
||||||
// release();
|
// release();
|
||||||
})();
|
})();
|
||||||
```
|
```
|
||||||
|
@ -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
|
// Provided in-memory / indexedDb cache for relays
|
||||||
// You can also implement your own with "RelayCache" interface
|
// You can also implement your own with "RelayCache" interface
|
||||||
@ -6,47 +6,47 @@ const RelaysCache = new UserRelaysCache();
|
|||||||
|
|
||||||
// example auth handler using NIP-07
|
// example auth handler using NIP-07
|
||||||
const AuthHandler = async (challenge: string, relay: string) => {
|
const AuthHandler = async (challenge: string, relay: string) => {
|
||||||
const pub = await EventPublisher.nip7();
|
const pub = await EventPublisher.nip7();
|
||||||
if (pub) {
|
if (pub) {
|
||||||
return await pub.nip42Auth(challenge, relay);
|
return await pub.nip42Auth(challenge, relay);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
// Singleton instance to store all connections and access query fetching system
|
// Singleton instance to store all connections and access query fetching system
|
||||||
const System = new NostrSystem({
|
const System = new NostrSystem({
|
||||||
relayCache: RelaysCache,
|
relayCache: RelaysCache,
|
||||||
authHandler: AuthHandler // can be left undefined if you dont care about NIP-42 Auth
|
authHandler: AuthHandler, // can be left undefined if you dont care about NIP-42 Auth
|
||||||
});
|
});
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
// connec to one "bootstrap" relay to pull profiles/relay lists from
|
// 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
|
// 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 });
|
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
|
// 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");
|
const rb = new RequestBuilder("get-posts");
|
||||||
rb.withFilter()
|
rb.withFilter()
|
||||||
.authors(["63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed"]) // Kieran pubkey
|
.authors(["63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed"]) // Kieran pubkey
|
||||||
.kinds([1])
|
.kinds([1])
|
||||||
.limit(10);
|
.limit(10);
|
||||||
|
|
||||||
const q = System.Query<FlatNoteStore>(FlatNoteStore, rb);
|
const q = System.Query<FlatNoteStore>(FlatNoteStore, rb);
|
||||||
// basic usage using "onEvent", fired for every event added to the store
|
// basic usage using "onEvent", fired for every event added to the store
|
||||||
q.onEvent = (sub, e) => {
|
q.onEvent = (sub, e) => {
|
||||||
console.debug(sub, e);
|
console.debug(sub, e);
|
||||||
}
|
};
|
||||||
|
|
||||||
// Hookable type using change notification, limited to every 500ms
|
// Hookable type using change notification, limited to every 500ms
|
||||||
const release = q.feed.hook(() => {
|
const release = q.feed.hook(() => {
|
||||||
// since we use the FlatNoteStore we expect NostrEvent[]
|
// since we use the FlatNoteStore we expect NostrEvent[]
|
||||||
// other stores provide different data, like a single event instead of an array (latest version)
|
// other stores provide different data, like a single event instead of an array (latest version)
|
||||||
const state = q.feed.snapshot as StoreSnapshot<ReturnType<FlatNoteStore["getSnapshotData"]>>;
|
const state = q.feed.snapshot as StoreSnapshot<ReturnType<FlatNoteStore["getSnapshotData"]>>;
|
||||||
|
|
||||||
// do something with snapshot of store
|
// do something with snapshot of store
|
||||||
console.log(`We have ${state.data.length} events now!`)
|
console.log(`We have ${state.data.length} events now!`);
|
||||||
});
|
});
|
||||||
|
|
||||||
// release the hook when its not needed anymore
|
// 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
|
// these patterns will be managed in @snort/system-react to make it easier to use react or other UI frameworks
|
||||||
// release();
|
// release();
|
||||||
})();
|
})();
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
"license": "GPL-3.0-or-later",
|
"license": "GPL-3.0-or-later",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "rm -rf dist && tsc",
|
"build": "rm -rf dist && tsc",
|
||||||
"test": "jest"
|
"test": "jest --runInBand"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"src",
|
"src",
|
||||||
@ -33,4 +33,4 @@
|
|||||||
"dexie": "^3.2.4",
|
"dexie": "^3.2.4",
|
||||||
"uuid": "^9.0.0"
|
"uuid": "^9.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
56
packages/system/src/cache/db.ts
vendored
56
packages/system/src/cache/db.ts
vendored
@ -6,37 +6,37 @@ const NAME = "snort-system";
|
|||||||
const VERSION = 2;
|
const VERSION = 2;
|
||||||
|
|
||||||
const STORES = {
|
const STORES = {
|
||||||
users: "++pubkey, name, display_name, picture, nip05, npub",
|
users: "++pubkey, name, display_name, picture, nip05, npub",
|
||||||
relayMetrics: "++addr",
|
relayMetrics: "++addr",
|
||||||
userRelays: "++pubkey",
|
userRelays: "++pubkey",
|
||||||
events: "++id, pubkey, created_at"
|
events: "++id, pubkey, created_at",
|
||||||
};
|
};
|
||||||
|
|
||||||
export class SnortSystemDb extends Dexie {
|
export class SnortSystemDb extends Dexie {
|
||||||
ready = false;
|
ready = false;
|
||||||
users!: Table<MetadataCache>;
|
users!: Table<MetadataCache>;
|
||||||
relayMetrics!: Table<RelayMetrics>;
|
relayMetrics!: Table<RelayMetrics>;
|
||||||
userRelays!: Table<UsersRelays>;
|
userRelays!: Table<UsersRelays>;
|
||||||
events!: Table<NostrEvent>;
|
events!: Table<NostrEvent>;
|
||||||
dms!: Table<NostrEvent>;
|
dms!: Table<NostrEvent>;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super(NAME);
|
super(NAME);
|
||||||
this.version(VERSION).stores(STORES);
|
this.version(VERSION).stores(STORES);
|
||||||
}
|
}
|
||||||
|
|
||||||
isAvailable() {
|
isAvailable() {
|
||||||
if ("indexedDB" in window) {
|
if ("indexedDB" in window) {
|
||||||
return new Promise<boolean>(resolve => {
|
return new Promise<boolean>(resolve => {
|
||||||
const req = window.indexedDB.open("dummy", 1);
|
const req = window.indexedDB.open("dummy", 1);
|
||||||
req.onsuccess = () => {
|
req.onsuccess = () => {
|
||||||
resolve(true);
|
resolve(true);
|
||||||
};
|
};
|
||||||
req.onerror = () => {
|
req.onerror = () => {
|
||||||
resolve(false);
|
resolve(false);
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
|
||||||
return Promise.resolve(false);
|
|
||||||
}
|
}
|
||||||
}
|
return Promise.resolve(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
2
packages/system/src/cache/index.ts
vendored
2
packages/system/src/cache/index.ts
vendored
@ -70,4 +70,4 @@ export function mapEventToProfile(ev: NostrEvent) {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to parse JSON", ev, e);
|
console.error("Failed to parse JSON", ev, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
30
packages/system/src/cache/relay-metric.ts
vendored
30
packages/system/src/cache/relay-metric.ts
vendored
@ -2,21 +2,21 @@ import { db, RelayMetrics } from ".";
|
|||||||
import { FeedCache } from "@snort/shared";
|
import { FeedCache } from "@snort/shared";
|
||||||
|
|
||||||
export class RelayMetricCache extends FeedCache<RelayMetrics> {
|
export class RelayMetricCache extends FeedCache<RelayMetrics> {
|
||||||
constructor() {
|
constructor() {
|
||||||
super("RelayMetrics", db.relayMetrics);
|
super("RelayMetrics", db.relayMetrics);
|
||||||
}
|
}
|
||||||
|
|
||||||
key(of: RelayMetrics): string {
|
key(of: RelayMetrics): string {
|
||||||
return of.addr;
|
return of.addr;
|
||||||
}
|
}
|
||||||
|
|
||||||
override async preload(): Promise<void> {
|
override async preload(): Promise<void> {
|
||||||
await super.preload();
|
await super.preload();
|
||||||
// load everything
|
// load everything
|
||||||
await this.buffer([...this.onTable]);
|
await this.buffer([...this.onTable]);
|
||||||
}
|
}
|
||||||
|
|
||||||
takeSnapshot(): Array<RelayMetrics> {
|
takeSnapshot(): Array<RelayMetrics> {
|
||||||
return [...this.cache.values()];
|
return [...this.cache.values()];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
2
packages/system/src/cache/user-metadata.ts
vendored
2
packages/system/src/cache/user-metadata.ts
vendored
@ -145,4 +145,4 @@ export class UserProfileCache extends FeedCache<MetadataCache> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
2
packages/system/src/cache/user-relays.ts
vendored
2
packages/system/src/cache/user-relays.ts
vendored
@ -26,4 +26,4 @@ export class UserRelaysCache extends FeedCache<UsersRelays> {
|
|||||||
takeSnapshot(): Array<UsersRelays> {
|
takeSnapshot(): Array<UsersRelays> {
|
||||||
return [...this.cache.values()];
|
return [...this.cache.values()];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -421,7 +421,12 @@ export class Connection extends ExternalStore<ConnectionStateSnapshot> {
|
|||||||
const lastActivity = unixNowMs() - this.#activity;
|
const lastActivity = unixNowMs() - this.#activity;
|
||||||
if (lastActivity > 30_000 && !this.IsClosed) {
|
if (lastActivity > 30_000 && !this.IsClosed) {
|
||||||
if (this.ActiveRequests.size > 0) {
|
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 {
|
} else {
|
||||||
this.Close();
|
this.Close();
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,6 @@ export const DefaultConnectTimeout = 2000;
|
|||||||
// eslint-disable-next-line no-useless-escape
|
// eslint-disable-next-line no-useless-escape
|
||||||
export const HashtagRegex = /(#[^\s!@#$%^&*()=+.\/,\[{\]};:'"?><]+)/g;
|
export const HashtagRegex = /(#[^\s!@#$%^&*()=+.\/,\[{\]};:'"?><]+)/g;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* How long profile cache should be considered valid for
|
* How long profile cache should be considered valid for
|
||||||
*/
|
*/
|
||||||
|
@ -6,17 +6,17 @@ import { EventKind, HexKey, NostrEvent } from ".";
|
|||||||
import { Nip4WebCryptoEncryptor } from "./impl/nip4";
|
import { Nip4WebCryptoEncryptor } from "./impl/nip4";
|
||||||
|
|
||||||
export interface Tag {
|
export interface Tag {
|
||||||
key: string
|
key: string;
|
||||||
value?: string
|
value?: string;
|
||||||
relay?: string
|
relay?: string;
|
||||||
marker?: string // NIP-10
|
marker?: string; // NIP-10
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Thread {
|
export interface Thread {
|
||||||
root?: Tag
|
root?: Tag;
|
||||||
replyTo?: Tag
|
replyTo?: Tag;
|
||||||
mentions: Array<Tag>
|
mentions: Array<Tag>;
|
||||||
pubKeys: Array<HexKey>
|
pubKeys: Array<HexKey>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export abstract class EventExt {
|
export abstract class EventExt {
|
||||||
@ -41,7 +41,7 @@ export abstract class EventExt {
|
|||||||
|
|
||||||
const sig = secp.schnorr.sign(e.id, key);
|
const sig = secp.schnorr.sign(e.id, key);
|
||||||
e.sig = utils.bytesToHex(sig);
|
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");
|
throw new Error("Signing failed");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -84,12 +84,12 @@ export abstract class EventExt {
|
|||||||
|
|
||||||
static parseTag(tag: Array<string>) {
|
static parseTag(tag: Array<string>) {
|
||||||
if (tag.length < 1) {
|
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 = {
|
const ret = {
|
||||||
key: tag[0],
|
key: tag[0],
|
||||||
value: tag[1]
|
value: tag[1],
|
||||||
} as Tag;
|
} as Tag;
|
||||||
switch (ret.key) {
|
switch (ret.key) {
|
||||||
case "e": {
|
case "e": {
|
||||||
|
@ -95,7 +95,7 @@ export class EventPublisher {
|
|||||||
* Create an EventPublisher for a private key
|
* Create an EventPublisher for a private key
|
||||||
*/
|
*/
|
||||||
static privateKey(privateKey: string) {
|
static privateKey(privateKey: string) {
|
||||||
const signer = new PrivateKeySigner(privateKey)
|
const signer = new PrivateKeySigner(privateKey);
|
||||||
return new EventPublisher(signer, signer.getPubKey());
|
return new EventPublisher(signer, signer.getPubKey());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { ReqFilter, UsersRelays } from ".";
|
import { ReqFilter, UsersRelays } from ".";
|
||||||
import { unwrap } from "@snort/shared";
|
import { dedupe, unwrap } from "@snort/shared";
|
||||||
import debug from "debug";
|
import debug from "debug";
|
||||||
|
import { FlatReqFilter } from "request-expander";
|
||||||
|
|
||||||
const PickNRelays = 2;
|
const PickNRelays = 2;
|
||||||
|
|
||||||
@ -9,6 +10,11 @@ export interface RelayTaggedFilter {
|
|||||||
filter: ReqFilter;
|
filter: ReqFilter;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface RelayTaggedFlatFilters {
|
||||||
|
relay: string;
|
||||||
|
filters: Array<FlatReqFilter>;
|
||||||
|
}
|
||||||
|
|
||||||
export interface RelayTaggedFilters {
|
export interface RelayTaggedFilters {
|
||||||
relay: string;
|
relay: string;
|
||||||
filters: Array<ReqFilter>;
|
filters: Array<ReqFilter>;
|
||||||
@ -43,11 +49,10 @@ export function splitAllByWriteRelays(cache: RelayCache, filters: Array<ReqFilte
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Split filters by authors
|
* Split filters by authors
|
||||||
* @param filter
|
|
||||||
* @returns
|
|
||||||
*/
|
*/
|
||||||
export function splitByWriteRelays(cache: RelayCache, filter: ReqFilter): Array<RelayTaggedFilter> {
|
export function splitByWriteRelays(cache: RelayCache, filter: ReqFilter): Array<RelayTaggedFilter> {
|
||||||
if ((filter.authors?.length ?? 0) === 0) {
|
const authors = filter.authors;
|
||||||
|
if ((authors?.length ?? 0) === 0) {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
relay: "",
|
relay: "",
|
||||||
@ -56,15 +61,87 @@ export function splitByWriteRelays(cache: RelayCache, filter: ReqFilter): Array<
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
const allRelays = unwrap(filter.authors).map(a => {
|
const topRelays = pickTopRelays(cache, unwrap(authors), PickNRelays);
|
||||||
|
const pickedRelays = dedupe(topRelays.flatMap(a => a.relays));
|
||||||
|
|
||||||
|
const picked = pickedRelays.map(a => {
|
||||||
|
const keysOnPickedRelay = dedupe(topRelays.filter(b => b.relays.includes(a)).map(b => b.key));
|
||||||
|
return {
|
||||||
|
relay: a,
|
||||||
|
filter: {
|
||||||
|
...filter,
|
||||||
|
authors: keysOnPickedRelay,
|
||||||
|
},
|
||||||
|
} as RelayTaggedFilter;
|
||||||
|
});
|
||||||
|
const noRelays = dedupe(topRelays.filter(a => a.relays.length === 0).map(a => a.key));
|
||||||
|
if (noRelays.length > 0) {
|
||||||
|
picked.push({
|
||||||
|
relay: "",
|
||||||
|
filter: {
|
||||||
|
...filter,
|
||||||
|
authors: noRelays,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
debug("GOSSIP")("Picked %o", picked);
|
||||||
|
return picked;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Split filters by author
|
||||||
|
*/
|
||||||
|
export function splitFlatByWriteRelays(cache: RelayCache, input: Array<FlatReqFilter>): Array<RelayTaggedFlatFilters> {
|
||||||
|
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 authorsOnRelay = new Set(topRelays.filter(v => v.relays.includes(a)).map(v => v.key));
|
||||||
|
return {
|
||||||
|
relay: a,
|
||||||
|
filters: input.filter(v => v.authors && authorsOnRelay.has(v.authors)),
|
||||||
|
} as RelayTaggedFlatFilters;
|
||||||
|
});
|
||||||
|
const noRelays = new Set(topRelays.filter(v => v.relays.length === 0).map(v => v.key));
|
||||||
|
if (noRelays.size > 0) {
|
||||||
|
picked.push({
|
||||||
|
relay: "",
|
||||||
|
filters: input.filter(v => !v.authors || noRelays.has(v.authors)),
|
||||||
|
} as RelayTaggedFlatFilters);
|
||||||
|
}
|
||||||
|
|
||||||
|
debug("GOSSIP")("Picked %o", picked);
|
||||||
|
return picked;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pick most popular relays for each authors
|
||||||
|
*/
|
||||||
|
function pickTopRelays(cache: RelayCache, authors: Array<string>, n: number) {
|
||||||
|
// map of pubkey -> [write relays]
|
||||||
|
const allRelays = authors.map(a => {
|
||||||
return {
|
return {
|
||||||
key: a,
|
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)),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const missing = allRelays.filter(a => a.relays === undefined || a.relays.length === 0);
|
const missing = allRelays.filter(a => a.relays === undefined || a.relays.length === 0);
|
||||||
const hasRelays = 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) => {
|
const relayUserMap = hasRelays.reduce((acc, v) => {
|
||||||
for (const r of unwrap(v.relays)) {
|
for (const r of unwrap(v.relays)) {
|
||||||
if (!acc.has(r.url)) {
|
if (!acc.has(r.url)) {
|
||||||
@ -83,36 +160,21 @@ export function splitByWriteRelays(cache: RelayCache, filter: ReqFilter): Array<
|
|||||||
// <key, relay[]> - pick n top relays
|
// <key, relay[]> - pick n top relays
|
||||||
// <relay, key[]> - map keys per relay (for subscription filter)
|
// <relay, key[]> - map keys per relay (for subscription filter)
|
||||||
|
|
||||||
const userPickedRelays = unwrap(filter.authors).map(k => {
|
return hasRelays
|
||||||
// pick top 3 relays for this key
|
.map(k => {
|
||||||
const relaysForKey = topRelays
|
// pick top N relays for this key
|
||||||
.filter(([, v]) => v.has(k))
|
const relaysForKey = topRelays
|
||||||
.slice(0, PickNRelays)
|
.filter(([, v]) => v.has(k.key))
|
||||||
.map(([k]) => k);
|
.slice(0, n)
|
||||||
return { k, relaysForKey };
|
.map(([k]) => k);
|
||||||
});
|
return { key: k.key, relays: relaysForKey };
|
||||||
|
})
|
||||||
const pickedRelays = new Set(userPickedRelays.map(a => a.relaysForKey).flat());
|
.concat(
|
||||||
|
missing.map(a => {
|
||||||
const picked = [...pickedRelays].map(a => {
|
return {
|
||||||
const keysOnPickedRelay = new Set(userPickedRelays.filter(b => b.relaysForKey.includes(a)).map(b => b.k));
|
key: a.key,
|
||||||
return {
|
relays: [],
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
@ -4,49 +4,49 @@ import { base64 } from "@scure/base";
|
|||||||
import { secp256k1 } from "@noble/curves/secp256k1";
|
import { secp256k1 } from "@noble/curves/secp256k1";
|
||||||
|
|
||||||
export class Nip4WebCryptoEncryptor implements MessageEncryptor {
|
export class Nip4WebCryptoEncryptor implements MessageEncryptor {
|
||||||
getSharedSecret(privateKey: string, publicKey: string) {
|
getSharedSecret(privateKey: string, publicKey: string) {
|
||||||
const sharedPoint = secp256k1.getSharedSecret(privateKey, "02" + publicKey);
|
const sharedPoint = secp256k1.getSharedSecret(privateKey, "02" + publicKey);
|
||||||
const sharedX = sharedPoint.slice(1, 33);
|
const sharedX = sharedPoint.slice(1, 33);
|
||||||
return sharedX;
|
return sharedX;
|
||||||
}
|
}
|
||||||
|
|
||||||
async encryptData(content: string, sharedSecet: Uint8Array) {
|
async encryptData(content: string, sharedSecet: Uint8Array) {
|
||||||
const key = await this.#importKey(sharedSecet);
|
const key = await this.#importKey(sharedSecet);
|
||||||
const iv = window.crypto.getRandomValues(new Uint8Array(16));
|
const iv = window.crypto.getRandomValues(new Uint8Array(16));
|
||||||
const data = new TextEncoder().encode(content);
|
const data = new TextEncoder().encode(content);
|
||||||
const result = await window.crypto.subtle.encrypt(
|
const result = await window.crypto.subtle.encrypt(
|
||||||
{
|
{
|
||||||
name: "AES-CBC",
|
name: "AES-CBC",
|
||||||
iv: iv,
|
iv: iv,
|
||||||
},
|
},
|
||||||
key,
|
key,
|
||||||
data
|
data
|
||||||
);
|
);
|
||||||
const uData = new Uint8Array(result);
|
const uData = new Uint8Array(result);
|
||||||
return `${base64.encode(uData)}?iv=${base64.encode(iv)}`;
|
return `${base64.encode(uData)}?iv=${base64.encode(iv)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Decrypt the content of the message
|
* Decrypt the content of the message
|
||||||
*/
|
*/
|
||||||
async decryptData(cyphertext: string, sharedSecet: Uint8Array) {
|
async decryptData(cyphertext: string, sharedSecet: Uint8Array) {
|
||||||
const key = await this.#importKey(sharedSecet);
|
const key = await this.#importKey(sharedSecet);
|
||||||
const cSplit = cyphertext.split("?iv=");
|
const cSplit = cyphertext.split("?iv=");
|
||||||
const data = base64.decode(cSplit[0]);
|
const data = base64.decode(cSplit[0]);
|
||||||
const iv = base64.decode(cSplit[1]);
|
const iv = base64.decode(cSplit[1]);
|
||||||
|
|
||||||
const result = await window.crypto.subtle.decrypt(
|
const result = await window.crypto.subtle.decrypt(
|
||||||
{
|
{
|
||||||
name: "AES-CBC",
|
name: "AES-CBC",
|
||||||
iv: iv,
|
iv: iv,
|
||||||
},
|
},
|
||||||
key,
|
key,
|
||||||
data
|
data
|
||||||
);
|
);
|
||||||
return new TextDecoder().decode(result);
|
return new TextDecoder().decode(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
async #importKey(sharedSecet: Uint8Array) {
|
async #importKey(sharedSecet: Uint8Array) {
|
||||||
return await window.crypto.subtle.importKey("raw", sharedSecet, { name: "AES-CBC" }, false, ["encrypt", "decrypt"]);
|
return await window.crypto.subtle.importKey("raw", sharedSecet, { name: "AES-CBC" }, false, ["encrypt", "decrypt"]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,40 +1,39 @@
|
|||||||
import { MessageEncryptor } from "index";
|
import { MessageEncryptor } from "index";
|
||||||
|
|
||||||
import { base64 } from "@scure/base";
|
import { base64 } from "@scure/base";
|
||||||
import { randomBytes } from '@noble/hashes/utils'
|
import { randomBytes } from "@noble/hashes/utils";
|
||||||
import { streamXOR as xchacha20 } from '@stablelib/xchacha20'
|
import { streamXOR as xchacha20 } from "@stablelib/xchacha20";
|
||||||
import { secp256k1 } from "@noble/curves/secp256k1";
|
import { secp256k1 } from "@noble/curves/secp256k1";
|
||||||
import { sha256 } from '@noble/hashes/sha256'
|
import { sha256 } from "@noble/hashes/sha256";
|
||||||
|
|
||||||
export enum Nip44Version {
|
export enum Nip44Version {
|
||||||
Reserved = 0x00,
|
Reserved = 0x00,
|
||||||
XChaCha20 = 0x01
|
XChaCha20 = 0x01,
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Nip44Encryptor implements MessageEncryptor {
|
export class Nip44Encryptor implements MessageEncryptor {
|
||||||
getSharedSecret(privateKey: string, publicKey: string) {
|
getSharedSecret(privateKey: string, publicKey: string) {
|
||||||
const key = secp256k1.getSharedSecret(privateKey, '02' + publicKey)
|
const key = secp256k1.getSharedSecret(privateKey, "02" + publicKey);
|
||||||
return sha256(key.slice(1, 33));
|
return sha256(key.slice(1, 33));
|
||||||
}
|
}
|
||||||
|
|
||||||
encryptData(content: string, sharedSecret: Uint8Array) {
|
encryptData(content: string, sharedSecret: Uint8Array) {
|
||||||
const nonce = randomBytes(24)
|
const nonce = randomBytes(24);
|
||||||
const plaintext = new TextEncoder().encode(content)
|
const plaintext = new TextEncoder().encode(content);
|
||||||
const ciphertext = xchacha20(sharedSecret, nonce, plaintext, plaintext);
|
const ciphertext = xchacha20(sharedSecret, nonce, plaintext, plaintext);
|
||||||
const ctb64 = base64.encode(Uint8Array.from(ciphertext))
|
const ctb64 = base64.encode(Uint8Array.from(ciphertext));
|
||||||
const nonceb64 = base64.encode(nonce)
|
const nonceb64 = base64.encode(nonce);
|
||||||
return JSON.stringify({ ciphertext: ctb64, nonce: nonceb64, v: Nip44Version.XChaCha20 })
|
return JSON.stringify({ ciphertext: ctb64, nonce: nonceb64, v: Nip44Version.XChaCha20 });
|
||||||
}
|
}
|
||||||
|
|
||||||
decryptData(cyphertext: string, sharedSecret: Uint8Array) {
|
decryptData(cyphertext: string, sharedSecret: Uint8Array) {
|
||||||
const dt = JSON.parse(cyphertext)
|
const dt = JSON.parse(cyphertext);
|
||||||
if (dt.v !== 1) throw new Error('NIP44: unknown encryption version')
|
if (dt.v !== 1) throw new Error("NIP44: unknown encryption version");
|
||||||
|
|
||||||
const ciphertext = base64.decode(dt.ciphertext)
|
const ciphertext = base64.decode(dt.ciphertext);
|
||||||
const nonce = base64.decode(dt.nonce)
|
const nonce = base64.decode(dt.nonce);
|
||||||
const plaintext = xchacha20(sharedSecret, nonce, ciphertext, ciphertext)
|
const plaintext = xchacha20(sharedSecret, nonce, ciphertext, ciphertext);
|
||||||
const text = new TextDecoder().decode(plaintext)
|
const text = new TextDecoder().decode(plaintext);
|
||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
|
||||||
|
@ -12,204 +12,213 @@ import EventKind from "../event-kind";
|
|||||||
const NIP46_KIND = 24_133;
|
const NIP46_KIND = 24_133;
|
||||||
|
|
||||||
interface Nip46Metadata {
|
interface Nip46Metadata {
|
||||||
name: string
|
name: string;
|
||||||
url?: string
|
url?: string;
|
||||||
description?: string
|
description?: string;
|
||||||
icons?: Array<string>
|
icons?: Array<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Nip46Request {
|
interface Nip46Request {
|
||||||
id: string
|
id: string;
|
||||||
method: string
|
method: string;
|
||||||
params: Array<any>
|
params: Array<any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Nip46Response {
|
interface Nip46Response {
|
||||||
id: string
|
id: string;
|
||||||
result: any
|
result: any;
|
||||||
error: string
|
error: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface QueueObj {
|
interface QueueObj {
|
||||||
resolve: (o: any) => void;
|
resolve: (o: any) => void;
|
||||||
reject: (e: Error) => void;
|
reject: (e: Error) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Nip46Signer implements EventSigner {
|
export class Nip46Signer implements EventSigner {
|
||||||
#conn?: Connection;
|
#conn?: Connection;
|
||||||
#relay: string;
|
#relay: string;
|
||||||
#localPubkey: string;
|
#localPubkey: string;
|
||||||
#remotePubkey?: string;
|
#remotePubkey?: string;
|
||||||
#token?: string;
|
#token?: string;
|
||||||
#insideSigner: EventSigner;
|
#insideSigner: EventSigner;
|
||||||
#commandQueue: Map<string, QueueObj> = new Map();
|
#commandQueue: Map<string, QueueObj> = new Map();
|
||||||
#log = debug("NIP-46");
|
#log = debug("NIP-46");
|
||||||
#proto: string;
|
#proto: string;
|
||||||
#didInit: boolean = false;
|
#didInit: boolean = false;
|
||||||
|
|
||||||
constructor(config: string, insideSigner?: EventSigner) {
|
constructor(config: string, insideSigner?: EventSigner) {
|
||||||
const u = new URL(config);
|
const u = new URL(config);
|
||||||
this.#proto = u.protocol;
|
this.#proto = u.protocol;
|
||||||
this.#localPubkey = u.pathname.substring(2);
|
this.#localPubkey = u.pathname.substring(2);
|
||||||
|
|
||||||
if (u.hash.length > 1) {
|
if (u.hash.length > 1) {
|
||||||
this.#token = u.hash.substring(1);
|
this.#token = u.hash.substring(1);
|
||||||
}
|
}
|
||||||
if (this.#localPubkey.startsWith("npub")) {
|
if (this.#localPubkey.startsWith("npub")) {
|
||||||
this.#localPubkey = bech32ToHex(this.#localPubkey);
|
this.#localPubkey = bech32ToHex(this.#localPubkey);
|
||||||
}
|
|
||||||
|
|
||||||
this.#relay = unwrap(u.searchParams.get("relay"));
|
|
||||||
this.#insideSigner = insideSigner ?? new PrivateKeySigner(secp256k1.utils.randomPrivateKey())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get relays() {
|
this.#relay = unwrap(u.searchParams.get("relay"));
|
||||||
return [this.#relay];
|
this.#insideSigner = insideSigner ?? new PrivateKeySigner(secp256k1.utils.randomPrivateKey());
|
||||||
}
|
}
|
||||||
|
|
||||||
get privateKey() {
|
get relays() {
|
||||||
if(this.#insideSigner instanceof PrivateKeySigner) {
|
return [this.#relay];
|
||||||
return this.#insideSigner.privateKey;
|
}
|
||||||
}
|
|
||||||
}
|
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<void>((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) {
|
if (isBunker) {
|
||||||
this.#remotePubkey = this.#localPubkey;
|
await this.#connect(unwrap(this.#remotePubkey));
|
||||||
this.#localPubkey = await this.#insideSigner.getPubKey();
|
resolve();
|
||||||
|
} else {
|
||||||
|
this.#commandQueue.set("connect", {
|
||||||
|
reject,
|
||||||
|
resolve,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return await new Promise<void>((resolve, reject) => {
|
};
|
||||||
this.#conn = new Connection(this.#relay, { read: true, write: true });
|
this.#conn.Connect();
|
||||||
this.#conn.OnEvent = async (sub, e) => {
|
this.#didInit = true;
|
||||||
await this.#onReply(e);
|
});
|
||||||
}
|
}
|
||||||
this.#conn.OnConnected = async () => {
|
|
||||||
this.#conn!.QueueReq(["REQ", "reply", {
|
|
||||||
kinds: [NIP46_KIND],
|
|
||||||
"#p": [this.#localPubkey]
|
|
||||||
}], () => { });
|
|
||||||
|
|
||||||
if (isBunker) {
|
async close() {
|
||||||
await this.#connect(unwrap(this.#remotePubkey));
|
if (this.#conn) {
|
||||||
resolve();
|
await this.#disconnect();
|
||||||
} else {
|
this.#conn.CloseReq("reply");
|
||||||
this.#commandQueue.set("connect", {
|
this.#conn.Close();
|
||||||
reject,
|
this.#conn = undefined;
|
||||||
resolve
|
this.#didInit = false;
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
this.#conn.Connect();
|
|
||||||
this.#didInit = true;
|
|
||||||
})
|
|
||||||
|
|
||||||
|
async describe() {
|
||||||
|
return await this.#rpc<Array<string>>("describe", []);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getPubKey() {
|
||||||
|
return await this.#rpc<string>("get_public_key", []);
|
||||||
|
}
|
||||||
|
|
||||||
|
async nip4Encrypt(content: string, otherKey: string) {
|
||||||
|
return await this.#rpc<string>("nip04_encrypt", [otherKey, content]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async nip4Decrypt(content: string, otherKey: string) {
|
||||||
|
return await this.#rpc<string>("nip04_decrypt", [otherKey, content]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async sign(ev: NostrEvent) {
|
||||||
|
const evStr = await this.#rpc<string>("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<string>("connect", connectParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
async #onReply(e: NostrEvent) {
|
||||||
|
if (e.kind !== NIP46_KIND) {
|
||||||
|
throw new Error("Unknown event kind");
|
||||||
}
|
}
|
||||||
|
|
||||||
async close() {
|
const decryptedContent = await this.#insideSigner.nip4Decrypt(e.content, e.pubkey);
|
||||||
if (this.#conn) {
|
const reply = JSON.parse(decryptedContent) as Nip46Request | Nip46Response;
|
||||||
await this.#disconnect();
|
|
||||||
this.#conn.CloseReq("reply");
|
let id = reply.id;
|
||||||
this.#conn.Close();
|
this.#log("Recv: %O", reply);
|
||||||
this.#conn = undefined;
|
if ("method" in reply && reply.method === "connect") {
|
||||||
this.#didInit = false;
|
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() {
|
pending.resolve(reply);
|
||||||
return await this.#rpc<Array<string>>("describe", []);
|
this.#commandQueue.delete(reply.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async #rpc<T>(method: string, params: Array<any>) {
|
||||||
|
if (!this.#didInit) {
|
||||||
|
await this.init();
|
||||||
}
|
}
|
||||||
|
if (!this.#conn) throw new Error("Connection error");
|
||||||
|
|
||||||
async getPubKey() {
|
const payload = {
|
||||||
return await this.#rpc<string>("get_public_key", []);
|
id: uuid(),
|
||||||
}
|
method,
|
||||||
|
params,
|
||||||
|
} as Nip46Request;
|
||||||
|
|
||||||
async nip4Encrypt(content: string, otherKey: string) {
|
this.#sendCommand(payload, unwrap(this.#remotePubkey));
|
||||||
return await this.#rpc<string>("nip04_encrypt", [otherKey, content]);
|
return await new Promise<T>((resolve, reject) => {
|
||||||
}
|
this.#commandQueue.set(payload.id, {
|
||||||
|
resolve: async (o: Nip46Response) => {
|
||||||
|
resolve(o.result as T);
|
||||||
|
},
|
||||||
|
reject,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async nip4Decrypt(content: string, otherKey: string) {
|
async #sendCommand(payload: Nip46Request | Nip46Response, target: string) {
|
||||||
return await this.#rpc<string>("nip04_decrypt", [otherKey, content]);
|
if (!this.#conn) return;
|
||||||
}
|
|
||||||
|
|
||||||
async sign(ev: NostrEvent) {
|
const eb = new EventBuilder();
|
||||||
const evStr = await this.#rpc<string>("sign_event", [JSON.stringify(ev)]);
|
eb.kind(NIP46_KIND as EventKind)
|
||||||
return JSON.parse(evStr);
|
.content(await this.#insideSigner.nip4Encrypt(JSON.stringify(payload), target))
|
||||||
}
|
.tag(["p", target]);
|
||||||
|
|
||||||
async #disconnect() {
|
this.#log("Send: %O", payload);
|
||||||
return await this.#rpc("disconnect", []);
|
const evCommand = await eb.buildAndSign(this.#insideSigner);
|
||||||
}
|
await this.#conn.SendAsync(evCommand);
|
||||||
|
}
|
||||||
async #connect(pk: string) {
|
|
||||||
const connectParams = [pk];
|
|
||||||
if (this.#token) {
|
|
||||||
connectParams.push(this.#token);
|
|
||||||
}
|
|
||||||
return await this.#rpc<string>("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<T>(method: string, params: Array<any>) {
|
|
||||||
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<T>((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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -6,56 +6,55 @@ const Nip7Queue: Array<WorkQueueItem> = [];
|
|||||||
processWorkQueue(Nip7Queue);
|
processWorkQueue(Nip7Queue);
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
nostr?: {
|
nostr?: {
|
||||||
getPublicKey: () => Promise<HexKey>;
|
getPublicKey: () => Promise<HexKey>;
|
||||||
signEvent: <T extends NostrEvent>(event: T) => Promise<T>;
|
signEvent: <T extends NostrEvent>(event: T) => Promise<T>;
|
||||||
|
|
||||||
getRelays?: () => Promise<Record<string, { read: boolean; write: boolean }>>;
|
getRelays?: () => Promise<Record<string, { read: boolean; write: boolean }>>;
|
||||||
|
|
||||||
nip04?: {
|
nip04?: {
|
||||||
encrypt?: (pubkey: HexKey, plaintext: string) => Promise<string>;
|
encrypt?: (pubkey: HexKey, plaintext: string) => Promise<string>;
|
||||||
decrypt?: (pubkey: HexKey, ciphertext: string) => Promise<string>;
|
decrypt?: (pubkey: HexKey, ciphertext: string) => Promise<string>;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Nip7Signer implements EventSigner {
|
export class Nip7Signer implements EventSigner {
|
||||||
init(): Promise<void> {
|
init(): Promise<void> {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
async getPubKey(): Promise<string> {
|
async getPubKey(): Promise<string> {
|
||||||
if (!window.nostr) {
|
if (!window.nostr) {
|
||||||
throw new Error("Cannot use NIP-07 signer, not found!");
|
throw new Error("Cannot use NIP-07 signer, not found!");
|
||||||
}
|
|
||||||
return await barrierQueue(Nip7Queue, () => unwrap(window.nostr).getPublicKey());
|
|
||||||
}
|
}
|
||||||
|
return await barrierQueue(Nip7Queue, () => unwrap(window.nostr).getPublicKey());
|
||||||
|
}
|
||||||
|
|
||||||
async nip4Encrypt(content: string, key: string): Promise<string> {
|
async nip4Encrypt(content: string, key: string): Promise<string> {
|
||||||
if (!window.nostr) {
|
if (!window.nostr) {
|
||||||
throw new Error("Cannot use NIP-07 signer, not found!");
|
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)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
return await barrierQueue(Nip7Queue, () =>
|
||||||
|
unwrap(window.nostr?.nip04?.encrypt).call(window.nostr?.nip04, key, content)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async nip4Decrypt(content: string, otherKey: string): Promise<string> {
|
async nip4Decrypt(content: string, otherKey: string): Promise<string> {
|
||||||
if (!window.nostr) {
|
if (!window.nostr) {
|
||||||
throw new Error("Cannot use NIP-07 signer, not found!");
|
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)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
return await barrierQueue(Nip7Queue, () =>
|
||||||
|
unwrap(window.nostr?.nip04?.decrypt).call(window.nostr?.nip04, otherKey, content)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async sign(ev: NostrEvent): Promise<NostrEvent> {
|
async sign(ev: NostrEvent): Promise<NostrEvent> {
|
||||||
if (!window.nostr) {
|
if (!window.nostr) {
|
||||||
throw new Error("Cannot use NIP-07 signer, not found!");
|
throw new Error("Cannot use NIP-07 signer, not found!");
|
||||||
}
|
|
||||||
return await barrierQueue(Nip7Queue, () => unwrap(window.nostr).signEvent(ev));
|
|
||||||
}
|
}
|
||||||
|
return await barrierQueue(Nip7Queue, () => unwrap(window.nostr).signEvent(ev));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
@ -37,7 +37,7 @@ export interface SystemInterface {
|
|||||||
HandleAuth?: AuthHandler;
|
HandleAuth?: AuthHandler;
|
||||||
get Sockets(): Array<ConnectionStateSnapshot>;
|
get Sockets(): Array<ConnectionStateSnapshot>;
|
||||||
GetQuery(id: string): Query | undefined;
|
GetQuery(id: string): Query | undefined;
|
||||||
Query<T extends NoteStore>(type: { new(): T }, req: RequestBuilder | null): Query;
|
Query<T extends NoteStore>(type: { new (): T }, req: RequestBuilder | null): Query;
|
||||||
ConnectToRelay(address: string, options: RelaySettings): Promise<void>;
|
ConnectToRelay(address: string, options: RelaySettings): Promise<void>;
|
||||||
DisconnectRelay(address: string): void;
|
DisconnectRelay(address: string): void;
|
||||||
BroadcastEvent(ev: NostrEvent): void;
|
BroadcastEvent(ev: NostrEvent): void;
|
||||||
@ -53,7 +53,7 @@ export interface SystemSnapshot {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface MessageEncryptor {
|
export interface MessageEncryptor {
|
||||||
getSharedSecret(privateKey: string, publicKey: string): Promise<Uint8Array> | Uint8Array
|
getSharedSecret(privateKey: string, publicKey: string): Promise<Uint8Array> | Uint8Array;
|
||||||
encryptData(plaintext: string, sharedSecet: Uint8Array): Promise<string> | string
|
encryptData(plaintext: string, sharedSecet: Uint8Array): Promise<string> | string;
|
||||||
decryptData(cyphertext: string, sharedSecet: Uint8Array): Promise<string> | string
|
decryptData(cyphertext: string, sharedSecet: Uint8Array): Promise<string> | string;
|
||||||
}
|
}
|
||||||
|
@ -2,109 +2,108 @@ import { bech32ToHex, hexToBech32 } from "@snort/shared";
|
|||||||
import { NostrPrefix, decodeTLV, TLVEntryType } from ".";
|
import { NostrPrefix, decodeTLV, TLVEntryType } from ".";
|
||||||
|
|
||||||
export interface NostrLink {
|
export interface NostrLink {
|
||||||
type: NostrPrefix;
|
type: NostrPrefix;
|
||||||
id: string;
|
id: string;
|
||||||
kind?: number;
|
kind?: number;
|
||||||
author?: string;
|
author?: string;
|
||||||
relays?: Array<string>;
|
relays?: Array<string>;
|
||||||
encode(): string;
|
encode(): string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function validateNostrLink(link: string): boolean {
|
export function validateNostrLink(link: string): boolean {
|
||||||
try {
|
try {
|
||||||
const parsedLink = parseNostrLink(link);
|
const parsedLink = parseNostrLink(link);
|
||||||
if (!parsedLink) {
|
if (!parsedLink) {
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (parsedLink.type === NostrPrefix.PublicKey || parsedLink.type === NostrPrefix.Note) {
|
|
||||||
return parsedLink.id.length === 64;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
if (parsedLink.type === NostrPrefix.PublicKey || parsedLink.type === NostrPrefix.Note) {
|
||||||
|
return parsedLink.id.length === 64;
|
||||||
export function tryParseNostrLink(link: string, prefixHint?: NostrPrefix): NostrLink | undefined {
|
|
||||||
try {
|
|
||||||
return parseNostrLink(link, prefixHint);
|
|
||||||
} catch {
|
|
||||||
return undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
export function tryParseNostrLink(link: string, prefixHint?: NostrPrefix): NostrLink | undefined {
|
||||||
|
try {
|
||||||
const isPrefix = (prefix: NostrPrefix) => {
|
return parseNostrLink(link, prefixHint);
|
||||||
return entity.startsWith(prefix);
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseNostrLink(link: string, prefixHint?: NostrPrefix): NostrLink {
|
||||||
|
const entity = link.startsWith("web+nostr:") || link.startsWith("nostr:") ? link.split(":")[1] : link;
|
||||||
|
|
||||||
|
const isPrefix = (prefix: NostrPrefix) => {
|
||||||
|
return entity.startsWith(prefix);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isPrefix(NostrPrefix.PublicKey)) {
|
||||||
|
const id = bech32ToHex(entity);
|
||||||
|
if (id.length !== 64) throw new Error("Invalid nostr link, must contain 32 byte id");
|
||||||
|
return {
|
||||||
|
type: NostrPrefix.PublicKey,
|
||||||
|
id: id,
|
||||||
|
encode: () => hexToBech32(NostrPrefix.PublicKey, id),
|
||||||
};
|
};
|
||||||
|
} else if (isPrefix(NostrPrefix.Note)) {
|
||||||
if (isPrefix(NostrPrefix.PublicKey)) {
|
const id = bech32ToHex(entity);
|
||||||
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");
|
if (id.length !== 64) throw new Error("Invalid nostr link, must contain 32 byte id");
|
||||||
return {
|
return {
|
||||||
type: NostrPrefix.PublicKey,
|
type: NostrPrefix.Profile,
|
||||||
id: id,
|
id,
|
||||||
encode: () => hexToBech32(NostrPrefix.PublicKey, id),
|
relays,
|
||||||
|
kind,
|
||||||
|
author,
|
||||||
|
encode,
|
||||||
};
|
};
|
||||||
} else if (isPrefix(NostrPrefix.Note)) {
|
} else if (isPrefix(NostrPrefix.Event)) {
|
||||||
const id = bech32ToHex(entity);
|
|
||||||
if (id.length !== 64) throw new Error("Invalid nostr link, must contain 32 byte id");
|
if (id.length !== 64) throw new Error("Invalid nostr link, must contain 32 byte id");
|
||||||
return {
|
return {
|
||||||
type: NostrPrefix.Note,
|
type: NostrPrefix.Event,
|
||||||
id: id,
|
id,
|
||||||
encode: () => hexToBech32(NostrPrefix.Note, id),
|
relays,
|
||||||
|
kind,
|
||||||
|
author,
|
||||||
|
encode,
|
||||||
};
|
};
|
||||||
} else if (isPrefix(NostrPrefix.Profile) || isPrefix(NostrPrefix.Event) || isPrefix(NostrPrefix.Address)) {
|
} else if (isPrefix(NostrPrefix.Address)) {
|
||||||
const decoded = decodeTLV(entity);
|
|
||||||
|
|
||||||
const id = decoded.find(a => a.type === TLVEntryType.Special)?.value as string;
|
|
||||||
const relays = decoded.filter(a => a.type === TLVEntryType.Relay).map(a => a.value as string);
|
|
||||||
const author = decoded.find(a => a.type === TLVEntryType.Author)?.value as string;
|
|
||||||
const kind = decoded.find(a => a.type === TLVEntryType.Kind)?.value as number;
|
|
||||||
|
|
||||||
const encode = () => {
|
|
||||||
return entity; // return original
|
|
||||||
};
|
|
||||||
if (isPrefix(NostrPrefix.Profile)) {
|
|
||||||
if (id.length !== 64) throw new Error("Invalid nostr link, must contain 32 byte id");
|
|
||||||
return {
|
|
||||||
type: NostrPrefix.Profile,
|
|
||||||
id,
|
|
||||||
relays,
|
|
||||||
kind,
|
|
||||||
author,
|
|
||||||
encode,
|
|
||||||
};
|
|
||||||
} else if (isPrefix(NostrPrefix.Event)) {
|
|
||||||
if (id.length !== 64) throw new Error("Invalid nostr link, must contain 32 byte id");
|
|
||||||
return {
|
|
||||||
type: NostrPrefix.Event,
|
|
||||||
id,
|
|
||||||
relays,
|
|
||||||
kind,
|
|
||||||
author,
|
|
||||||
encode,
|
|
||||||
};
|
|
||||||
} else if (isPrefix(NostrPrefix.Address)) {
|
|
||||||
return {
|
|
||||||
type: NostrPrefix.Address,
|
|
||||||
id,
|
|
||||||
relays,
|
|
||||||
kind,
|
|
||||||
author,
|
|
||||||
encode,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} else if (prefixHint) {
|
|
||||||
return {
|
return {
|
||||||
type: prefixHint,
|
type: NostrPrefix.Address,
|
||||||
id: link,
|
id,
|
||||||
encode: () => hexToBech32(prefixHint, link),
|
relays,
|
||||||
|
kind,
|
||||||
|
author,
|
||||||
|
encode,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
throw new Error("Invalid nostr link");
|
} else if (prefixHint) {
|
||||||
|
return {
|
||||||
|
type: prefixHint,
|
||||||
|
id: link,
|
||||||
|
encode: () => hexToBech32(prefixHint, link),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
throw new Error("Invalid nostr link");
|
||||||
|
}
|
||||||
|
@ -17,7 +17,7 @@ import {
|
|||||||
UserRelaysCache,
|
UserRelaysCache,
|
||||||
RelayMetricCache,
|
RelayMetricCache,
|
||||||
db,
|
db,
|
||||||
UsersRelays
|
UsersRelays,
|
||||||
} from ".";
|
} from ".";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -67,10 +67,10 @@ export class NostrSystem extends ExternalStore<SystemSnapshot> implements System
|
|||||||
#relayMetrics: RelayMetricHandler;
|
#relayMetrics: RelayMetricHandler;
|
||||||
|
|
||||||
constructor(props: {
|
constructor(props: {
|
||||||
authHandler?: AuthHandler,
|
authHandler?: AuthHandler;
|
||||||
relayCache?: FeedCache<UsersRelays>,
|
relayCache?: FeedCache<UsersRelays>;
|
||||||
profileCache?: FeedCache<MetadataCache>
|
profileCache?: FeedCache<MetadataCache>;
|
||||||
relayMetrics?: FeedCache<RelayMetrics>
|
relayMetrics?: FeedCache<RelayMetrics>;
|
||||||
}) {
|
}) {
|
||||||
super();
|
super();
|
||||||
this.#handleAuth = props.authHandler;
|
this.#handleAuth = props.authHandler;
|
||||||
@ -99,11 +99,7 @@ export class NostrSystem extends ExternalStore<SystemSnapshot> implements System
|
|||||||
*/
|
*/
|
||||||
async Init() {
|
async Init() {
|
||||||
db.ready = await db.isAvailable();
|
db.ready = await db.isAvailable();
|
||||||
const t = [
|
const t = [this.#relayCache.preload(), this.#profileCache.preload(), this.#relayMetricsCache.preload()];
|
||||||
this.#relayCache.preload(),
|
|
||||||
this.#profileCache.preload(),
|
|
||||||
this.#relayMetricsCache.preload()
|
|
||||||
];
|
|
||||||
await Promise.all(t);
|
await Promise.all(t);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -118,8 +114,8 @@ export class NostrSystem extends ExternalStore<SystemSnapshot> implements System
|
|||||||
this.#sockets.set(addr, c);
|
this.#sockets.set(addr, c);
|
||||||
c.OnEvent = (s, e) => this.OnEvent(s, e);
|
c.OnEvent = (s, e) => this.OnEvent(s, e);
|
||||||
c.OnEose = s => this.OnEndOfStoredEvents(c, s);
|
c.OnEose = s => this.OnEndOfStoredEvents(c, s);
|
||||||
c.OnDisconnect = (code) => this.OnRelayDisconnect(c, code);
|
c.OnDisconnect = code => this.OnRelayDisconnect(c, code);
|
||||||
c.OnConnected = (r) => this.OnRelayConnected(c, r);
|
c.OnConnected = r => this.OnRelayConnected(c, r);
|
||||||
await c.Connect();
|
await c.Connect();
|
||||||
} else {
|
} else {
|
||||||
// update settings if already connected
|
// update settings if already connected
|
||||||
@ -170,7 +166,7 @@ export class NostrSystem extends ExternalStore<SystemSnapshot> implements System
|
|||||||
c.OnEvent = (s, e) => this.OnEvent(s, e);
|
c.OnEvent = (s, e) => this.OnEvent(s, e);
|
||||||
c.OnEose = s => this.OnEndOfStoredEvents(c, s);
|
c.OnEose = s => this.OnEndOfStoredEvents(c, s);
|
||||||
c.OnDisconnect = code => this.OnRelayDisconnect(c, code);
|
c.OnDisconnect = code => this.OnRelayDisconnect(c, code);
|
||||||
c.OnConnected = (r) => this.OnRelayConnected(c, r);
|
c.OnConnected = r => this.OnRelayConnected(c, r);
|
||||||
await c.Connect();
|
await c.Connect();
|
||||||
return c;
|
return c;
|
||||||
}
|
}
|
||||||
@ -194,7 +190,7 @@ export class NostrSystem extends ExternalStore<SystemSnapshot> implements System
|
|||||||
return this.Queries.get(id);
|
return this.Queries.get(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
Query<T extends NoteStore>(type: { new(): T }, req: RequestBuilder): Query {
|
Query<T extends NoteStore>(type: { new (): T }, req: RequestBuilder): Query {
|
||||||
const existing = this.Queries.get(req.id);
|
const existing = this.Queries.get(req.id);
|
||||||
if (existing) {
|
if (existing) {
|
||||||
// if same instance, just return query
|
// if same instance, just return query
|
||||||
|
@ -38,21 +38,21 @@ export type ReqCommand = [cmd: "REQ", id: string, ...filters: Array<ReqFilter>];
|
|||||||
* Raw REQ filter object
|
* Raw REQ filter object
|
||||||
*/
|
*/
|
||||||
export interface ReqFilter {
|
export interface ReqFilter {
|
||||||
ids?: u256[]
|
ids?: u256[];
|
||||||
authors?: u256[]
|
authors?: u256[];
|
||||||
kinds?: number[]
|
kinds?: number[];
|
||||||
"#e"?: u256[]
|
"#e"?: u256[];
|
||||||
"#p"?: u256[]
|
"#p"?: u256[];
|
||||||
"#t"?: string[]
|
"#t"?: string[];
|
||||||
"#d"?: string[]
|
"#d"?: string[];
|
||||||
"#r"?: string[]
|
"#r"?: string[];
|
||||||
"#a"?: string[]
|
"#a"?: string[];
|
||||||
"#g"?: string[]
|
"#g"?: string[];
|
||||||
search?: string
|
search?: string;
|
||||||
since?: number
|
since?: number;
|
||||||
until?: number
|
until?: number;
|
||||||
limit?: number
|
limit?: number;
|
||||||
[key: string]: Array<string> | Array<number> | string | number | undefined
|
[key: string]: Array<string> | Array<number> | string | number | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -249,8 +249,8 @@ export class ReplaceableNoteStore extends HookedNoteStore<Readonly<TaggedRawEven
|
|||||||
*/
|
*/
|
||||||
export class NoteCollection extends KeyedReplaceableNoteStore {
|
export class NoteCollection extends KeyedReplaceableNoteStore {
|
||||||
constructor() {
|
constructor() {
|
||||||
super((e) => {
|
super(e => {
|
||||||
const legacyReplaceable = [0, 3, 41]
|
const legacyReplaceable = [0, 3, 41];
|
||||||
if (e.kind >= 30_000 && e.kind < 40_000) {
|
if (e.kind >= 30_000 && e.kind < 40_000) {
|
||||||
return `${e.kind}:${e.pubkey}:${findTag(e, "d")}`; // Parameterized replaceable
|
return `${e.kind}:${e.pubkey}:${findTag(e, "d")}`; // Parameterized replaceable
|
||||||
} else if (e.kind >= 10_000 && e.kind < 20_000) {
|
} else if (e.kind >= 10_000 && e.kind < 20_000) {
|
||||||
@ -263,6 +263,6 @@ export class NoteCollection extends KeyedReplaceableNoteStore {
|
|||||||
// unknown kind
|
// unknown kind
|
||||||
return e.id;
|
return e.id;
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,10 @@
|
|||||||
|
|
||||||
import debug from "debug";
|
import debug from "debug";
|
||||||
import { unixNowMs, FeedCache } from "@snort/shared";
|
import { unixNowMs, FeedCache } from "@snort/shared";
|
||||||
import { EventKind, HexKey, SystemInterface, TaggedRawEvent, NoteCollection, RequestBuilder } from ".";
|
import { EventKind, HexKey, SystemInterface, TaggedRawEvent, NoteCollection, RequestBuilder } from ".";
|
||||||
import { ProfileCacheExpire } from "./const";
|
import { ProfileCacheExpire } from "./const";
|
||||||
import { mapEventToProfile, MetadataCache } from "./cache";
|
import { mapEventToProfile, MetadataCache } from "./cache";
|
||||||
|
|
||||||
const MetadataRelays = [
|
const MetadataRelays = ["wss://purplepag.es"];
|
||||||
"wss://purplepag.es"
|
|
||||||
]
|
|
||||||
|
|
||||||
export class ProfileLoaderService {
|
export class ProfileLoaderService {
|
||||||
#system: SystemInterface;
|
#system: SystemInterface;
|
||||||
@ -88,7 +85,8 @@ export class ProfileLoaderService {
|
|||||||
.authors([...missing]);
|
.authors([...missing]);
|
||||||
|
|
||||||
if (this.#missingLastRun.size > 0) {
|
if (this.#missingLastRun.size > 0) {
|
||||||
const fMissing = sub.withFilter()
|
const fMissing = sub
|
||||||
|
.withFilter()
|
||||||
.kinds([EventKind.SetMetadata])
|
.kinds([EventKind.SetMetadata])
|
||||||
.authors([...this.#missingLastRun]);
|
.authors([...this.#missingLastRun]);
|
||||||
MetadataRelays.forEach(r => fMissing.relay(r));
|
MetadataRelays.forEach(r => fMissing.relay(r));
|
||||||
@ -128,7 +126,7 @@ export class ProfileLoaderService {
|
|||||||
const empty = couldNotFetch.map(a =>
|
const empty = couldNotFetch.map(a =>
|
||||||
this.#cache.update({
|
this.#cache.update({
|
||||||
pubkey: a,
|
pubkey: a,
|
||||||
loaded: unixNowMs() - ProfileCacheExpire + 5_000, // expire in 5s
|
loaded: unixNowMs() - ProfileCacheExpire + 30_000, // expire in 30s
|
||||||
created: 69,
|
created: 69,
|
||||||
} as MetadataCache)
|
} as MetadataCache)
|
||||||
);
|
);
|
||||||
|
@ -3,11 +3,10 @@ import debug from "debug";
|
|||||||
import { unixNowMs, unwrap } from "@snort/shared";
|
import { unixNowMs, unwrap } from "@snort/shared";
|
||||||
|
|
||||||
import { Connection, ReqFilter, Nips, TaggedRawEvent } from ".";
|
import { Connection, ReqFilter, Nips, TaggedRawEvent } from ".";
|
||||||
import { reqFilterEq } from "./utils";
|
|
||||||
import { NoteStore } from "./note-collection";
|
import { NoteStore } from "./note-collection";
|
||||||
import { flatMerge } from "./request-merger";
|
import { flatMerge } from "./request-merger";
|
||||||
import { BuiltRawReqFilter } from "./request-builder";
|
import { BuiltRawReqFilter } from "./request-builder";
|
||||||
import { expandFilter } from "./request-expander";
|
import { FlatReqFilter, expandFilter } from "./request-expander";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tracing for relay query status
|
* Tracing for relay query status
|
||||||
@ -19,6 +18,7 @@ class QueryTrace {
|
|||||||
eose?: number;
|
eose?: number;
|
||||||
close?: number;
|
close?: number;
|
||||||
#wasForceClosed = false;
|
#wasForceClosed = false;
|
||||||
|
readonly flatFilters: Array<FlatReqFilter>;
|
||||||
readonly #fnClose: (id: string) => void;
|
readonly #fnClose: (id: string) => void;
|
||||||
readonly #fnProgress: () => void;
|
readonly #fnProgress: () => void;
|
||||||
|
|
||||||
@ -33,6 +33,7 @@ class QueryTrace {
|
|||||||
this.start = unixNowMs();
|
this.start = unixNowMs();
|
||||||
this.#fnClose = fnClose;
|
this.#fnClose = fnClose;
|
||||||
this.#fnProgress = fnProgress;
|
this.#fnProgress = fnProgress;
|
||||||
|
this.flatFilters = filters.flatMap(expandFilter);
|
||||||
}
|
}
|
||||||
|
|
||||||
sentToRelay() {
|
sentToRelay() {
|
||||||
@ -168,13 +169,7 @@ export class Query implements QueryBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get flatFilters() {
|
get flatFilters() {
|
||||||
const f: Array<ReqFilter> = [];
|
return this.#tracing.flatMap(a => a.flatFilters);
|
||||||
for (const x of this.#tracing.flatMap(a => a.filters)) {
|
|
||||||
if (!f.some(a => reqFilterEq(a, x))) {
|
|
||||||
f.push(x);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return f.flatMap(expandFilter);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get feed() {
|
get feed() {
|
||||||
@ -255,7 +250,7 @@ export class Query implements QueryBase {
|
|||||||
#onProgress() {
|
#onProgress() {
|
||||||
const isFinished = this.progress === 1;
|
const isFinished = this.progress === 1;
|
||||||
if (this.feed.loading !== isFinished) {
|
if (this.feed.loading !== isFinished) {
|
||||||
this.#log("%s loading=%s, progress=%d", this.id, this.feed.loading, this.progress);
|
this.#log("%s loading=%s, progress=%d, traces=%O", this.id, this.feed.loading, this.progress, this.#tracing);
|
||||||
this.feed.loading = isFinished;
|
this.feed.loading = isFinished;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,13 +3,11 @@ import { Connection } from "connection";
|
|||||||
import { RelayMetrics } from "cache";
|
import { RelayMetrics } from "cache";
|
||||||
|
|
||||||
export class RelayMetricHandler {
|
export class RelayMetricHandler {
|
||||||
readonly #cache: FeedCache<RelayMetrics>;
|
readonly #cache: FeedCache<RelayMetrics>;
|
||||||
|
|
||||||
constructor(cache: FeedCache<RelayMetrics>) {
|
constructor(cache: FeedCache<RelayMetrics>) {
|
||||||
this.#cache = cache;
|
this.#cache = cache;
|
||||||
}
|
}
|
||||||
|
|
||||||
onDisconnect(c: Connection, code: number) {
|
onDisconnect(c: Connection, code: number) {}
|
||||||
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -4,8 +4,8 @@ import { appendDedupe, sanitizeRelayUrl, unixNowMs } from "@snort/shared";
|
|||||||
|
|
||||||
import { ReqFilter, u256, HexKey, EventKind } from ".";
|
import { ReqFilter, u256, HexKey, EventKind } from ".";
|
||||||
import { diffFilters } from "./request-splitter";
|
import { diffFilters } from "./request-splitter";
|
||||||
import { RelayCache, splitAllByWriteRelays, splitByWriteRelays } from "./gossip-model";
|
import { RelayCache, splitByWriteRelays, splitFlatByWriteRelays } from "./gossip-model";
|
||||||
import { mergeSimilar } from "./request-merger";
|
import { flatMerge, mergeSimilar } from "./request-merger";
|
||||||
import { FlatReqFilter, expandFilter } from "./request-expander";
|
import { FlatReqFilter, expandFilter } from "./request-expander";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -108,14 +108,13 @@ export class RequestBuilder {
|
|||||||
|
|
||||||
const next = this.#builders.flatMap(f => expandFilter(f.filter));
|
const next = this.#builders.flatMap(f => expandFilter(f.filter));
|
||||||
const diff = diffFilters(prev, next);
|
const diff = diffFilters(prev, next);
|
||||||
const ts = (unixNowMs() - start);
|
const ts = unixNowMs() - start;
|
||||||
this.#log("buildDiff %s %d ms", this.id, ts);
|
this.#log("buildDiff %s %d ms", this.id, ts);
|
||||||
if (diff.changed) {
|
if (diff.changed) {
|
||||||
this.#log(diff);
|
return splitFlatByWriteRelays(relays, diff.added).map(a => {
|
||||||
return splitAllByWriteRelays(relays, diff.added).map(a => {
|
|
||||||
return {
|
return {
|
||||||
strategy: RequestStrategy.AuthorsRelays,
|
strategy: RequestStrategy.AuthorsRelays,
|
||||||
filters: a.filters,
|
filters: flatMerge(a.filters),
|
||||||
relay: a.relay,
|
relay: a.relay,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
@ -1,19 +1,19 @@
|
|||||||
import { ReqFilter } from "./nostr";
|
import { ReqFilter } from "./nostr";
|
||||||
|
|
||||||
export interface FlatReqFilter {
|
export interface FlatReqFilter {
|
||||||
keys: number
|
keys: number;
|
||||||
ids?: string
|
ids?: string;
|
||||||
authors?: string
|
authors?: string;
|
||||||
kinds?: number
|
kinds?: number;
|
||||||
"#e"?: string
|
"#e"?: string;
|
||||||
"#p"?: string
|
"#p"?: string;
|
||||||
"#t"?: string
|
"#t"?: string;
|
||||||
"#d"?: string
|
"#d"?: string;
|
||||||
"#r"?: string
|
"#r"?: string;
|
||||||
search?: string
|
search?: string;
|
||||||
since?: number
|
since?: number;
|
||||||
until?: number
|
until?: number;
|
||||||
limit?: number
|
limit?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -114,7 +114,7 @@ export function flatMerge(all: Array<FlatReqFilter>): Array<ReqFilter> {
|
|||||||
acc[k].push(v);
|
acc[k].push(v);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
return acc;
|
return acc;
|
||||||
}, {} as any) as ReqFilter;
|
}, {} as any) as ReqFilter;
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,10 @@
|
|||||||
import { flatFilterEq } from "./utils";
|
import { flatFilterEq } from "./utils";
|
||||||
import { FlatReqFilter } from "./request-expander";
|
import { FlatReqFilter } from "./request-expander";
|
||||||
import { flatMerge } from "./request-merger";
|
|
||||||
|
|
||||||
export function diffFilters(prev: Array<FlatReqFilter>, next: Array<FlatReqFilter>, calcRemoved?: boolean) {
|
export function diffFilters(prev: Array<FlatReqFilter>, next: Array<FlatReqFilter>, calcRemoved?: boolean) {
|
||||||
const added = [];
|
const added = [];
|
||||||
const removed = [];
|
const removed = [];
|
||||||
|
|
||||||
prev = [...prev];
|
|
||||||
next = [...next];
|
|
||||||
for (const n of next) {
|
for (const n of next) {
|
||||||
const px = prev.findIndex(a => flatFilterEq(a, n));
|
const px = prev.findIndex(a => flatFilterEq(a, n));
|
||||||
if (px !== -1) {
|
if (px !== -1) {
|
||||||
@ -28,8 +25,8 @@ export function diffFilters(prev: Array<FlatReqFilter>, next: Array<FlatReqFilte
|
|||||||
}
|
}
|
||||||
const changed = added.length > 0 || removed.length > 0;
|
const changed = added.length > 0 || removed.length > 0;
|
||||||
return {
|
return {
|
||||||
added: changed ? flatMerge(added) : [],
|
added: changed ? added : [],
|
||||||
removed: changed ? flatMerge(removed) : [],
|
removed: changed ? removed : [],
|
||||||
changed,
|
changed,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -1,42 +1,45 @@
|
|||||||
|
|
||||||
import { equalProp } from "@snort/shared";
|
import { equalProp } from "@snort/shared";
|
||||||
import { FlatReqFilter } from "./request-expander";
|
import { FlatReqFilter } from "./request-expander";
|
||||||
import { NostrEvent, ReqFilter } from "./nostr";
|
import { NostrEvent, ReqFilter } from "./nostr";
|
||||||
|
|
||||||
export function findTag(e: NostrEvent, tag: string) {
|
export function findTag(e: NostrEvent, tag: string) {
|
||||||
const maybeTag = e.tags.find(evTag => {
|
const maybeTag = e.tags.find(evTag => {
|
||||||
return evTag[0] === tag;
|
return evTag[0] === tag;
|
||||||
});
|
});
|
||||||
return maybeTag && maybeTag[1];
|
return maybeTag && maybeTag[1];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function reqFilterEq(a: FlatReqFilter | ReqFilter, b: FlatReqFilter | ReqFilter): boolean {
|
export function reqFilterEq(a: FlatReqFilter | ReqFilter, b: FlatReqFilter | ReqFilter): boolean {
|
||||||
return equalProp(a.ids, b.ids)
|
return (
|
||||||
&& equalProp(a.kinds, b.kinds)
|
equalProp(a.ids, b.ids) &&
|
||||||
&& equalProp(a.authors, b.authors)
|
equalProp(a.kinds, b.kinds) &&
|
||||||
&& equalProp(a.limit, b.limit)
|
equalProp(a.authors, b.authors) &&
|
||||||
&& equalProp(a.since, b.since)
|
equalProp(a.limit, b.limit) &&
|
||||||
&& equalProp(a.until, b.until)
|
equalProp(a.since, b.since) &&
|
||||||
&& equalProp(a.search, b.search)
|
equalProp(a.until, b.until) &&
|
||||||
&& equalProp(a["#e"], b["#e"])
|
equalProp(a.search, b.search) &&
|
||||||
&& equalProp(a["#p"], b["#p"])
|
equalProp(a["#e"], b["#e"]) &&
|
||||||
&& equalProp(a["#t"], b["#t"])
|
equalProp(a["#p"], b["#p"]) &&
|
||||||
&& equalProp(a["#d"], b["#d"])
|
equalProp(a["#t"], b["#t"]) &&
|
||||||
&& equalProp(a["#r"], b["#r"]);
|
equalProp(a["#d"], b["#d"]) &&
|
||||||
|
equalProp(a["#r"], b["#r"])
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function flatFilterEq(a: FlatReqFilter, b: FlatReqFilter): boolean {
|
export function flatFilterEq(a: FlatReqFilter, b: FlatReqFilter): boolean {
|
||||||
return a.keys === b.keys
|
return (
|
||||||
&& a.since === b.since
|
a.keys === b.keys &&
|
||||||
&& a.until === b.until
|
a.since === b.since &&
|
||||||
&& a.limit === b.limit
|
a.until === b.until &&
|
||||||
&& a.search === b.search
|
a.limit === b.limit &&
|
||||||
&& a.ids === b.ids
|
a.search === b.search &&
|
||||||
&& a.kinds === b.kinds
|
a.ids === b.ids &&
|
||||||
&& a.authors === b.authors
|
a.kinds === b.kinds &&
|
||||||
&& a["#e"] === b["#e"]
|
a.authors === b.authors &&
|
||||||
&& a["#p"] === b["#p"]
|
a["#e"] === b["#e"] &&
|
||||||
&& a["#t"] === b["#t"]
|
a["#p"] === b["#p"] &&
|
||||||
&& a["#d"] === b["#d"]
|
a["#t"] === b["#t"] &&
|
||||||
&& a["#r"] === b["#r"];
|
a["#d"] === b["#d"] &&
|
||||||
}
|
a["#r"] === b["#r"]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@ -5,88 +5,88 @@ import { findTag } from "./utils";
|
|||||||
import { MetadataCache } from "./cache";
|
import { MetadataCache } from "./cache";
|
||||||
|
|
||||||
function getInvoice(zap: NostrEvent): InvoiceDetails | undefined {
|
function getInvoice(zap: NostrEvent): InvoiceDetails | undefined {
|
||||||
const bolt11 = findTag(zap, "bolt11");
|
const bolt11 = findTag(zap, "bolt11");
|
||||||
if (!bolt11) {
|
if (!bolt11) {
|
||||||
throw new Error("Invalid zap, missing bolt11 tag");
|
throw new Error("Invalid zap, missing bolt11 tag");
|
||||||
}
|
}
|
||||||
return decodeInvoice(bolt11);
|
return decodeInvoice(bolt11);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseZap(zapReceipt: NostrEvent, userCache: FeedCache<MetadataCache>, refNote?: NostrEvent): ParsedZap {
|
export function parseZap(zapReceipt: NostrEvent, userCache: FeedCache<MetadataCache>, refNote?: NostrEvent): ParsedZap {
|
||||||
let innerZapJson = findTag(zapReceipt, "description");
|
let innerZapJson = findTag(zapReceipt, "description");
|
||||||
if (innerZapJson) {
|
if (innerZapJson) {
|
||||||
try {
|
try {
|
||||||
const invoice = getInvoice(zapReceipt);
|
const invoice = getInvoice(zapReceipt);
|
||||||
if (innerZapJson.startsWith("%")) {
|
if (innerZapJson.startsWith("%")) {
|
||||||
innerZapJson = decodeURIComponent(innerZapJson);
|
innerZapJson = decodeURIComponent(innerZapJson);
|
||||||
}
|
}
|
||||||
const zapRequest: NostrEvent = JSON.parse(innerZapJson);
|
const zapRequest: NostrEvent = JSON.parse(innerZapJson);
|
||||||
if (Array.isArray(zapRequest)) {
|
if (Array.isArray(zapRequest)) {
|
||||||
// old format, ignored
|
// old format, ignored
|
||||||
throw new Error("deprecated zap format");
|
throw new Error("deprecated zap format");
|
||||||
}
|
}
|
||||||
const isForwardedZap = refNote?.tags.some(a => a[0] === "zap") ?? false;
|
const isForwardedZap = refNote?.tags.some(a => a[0] === "zap") ?? false;
|
||||||
const anonZap = zapRequest.tags.find(a => a[0] === "anon");
|
const anonZap = zapRequest.tags.find(a => a[0] === "anon");
|
||||||
const metaHash = sha256(innerZapJson);
|
const metaHash = sha256(innerZapJson);
|
||||||
const pollOpt = zapRequest.tags.find(a => a[0] === "poll_option")?.[1];
|
const pollOpt = zapRequest.tags.find(a => a[0] === "poll_option")?.[1];
|
||||||
const ret: ParsedZap = {
|
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 {
|
|
||||||
id: zapReceipt.id,
|
id: zapReceipt.id,
|
||||||
zapService: zapReceipt.pubkey,
|
zapService: zapReceipt.pubkey,
|
||||||
amount: 0,
|
amount: (invoice?.amount ?? 0) / 1000,
|
||||||
valid: false,
|
event: findTag(zapRequest, "e"),
|
||||||
anonZap: false,
|
sender: zapRequest.pubkey,
|
||||||
errors: ["invalid zap, parsing failed"],
|
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 {
|
export interface ParsedZap {
|
||||||
id: HexKey;
|
id: HexKey;
|
||||||
event?: HexKey;
|
event?: HexKey;
|
||||||
receiver?: HexKey;
|
receiver?: HexKey;
|
||||||
amount: number;
|
amount: number;
|
||||||
content?: string;
|
content?: string;
|
||||||
sender?: HexKey;
|
sender?: HexKey;
|
||||||
valid: boolean;
|
valid: boolean;
|
||||||
zapService: HexKey;
|
zapService: HexKey;
|
||||||
anonZap: boolean;
|
anonZap: boolean;
|
||||||
errors: Array<string>;
|
errors: Array<string>;
|
||||||
pollOption?: number;
|
pollOption?: number;
|
||||||
}
|
}
|
||||||
|
@ -1,62 +0,0 @@
|
|||||||
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..."],
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
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"],
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
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: []
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
@ -1,37 +0,0 @@
|
|||||||
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: []
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const a = [{
|
|
||||||
"until": 1686651693,
|
|
||||||
"limit": 200,
|
|
||||||
"kinds": [
|
|
||||||
1,
|
|
||||||
6,
|
|
||||||
6969
|
|
||||||
],
|
|
||||||
"authors": [
|
|
||||||
"3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d"
|
|
||||||
]
|
|
||||||
}];
|
|
||||||
|
|
||||||
const output = splitAllByWriteRelays(Relays, a);
|
|
||||||
expect(output).toEqual([
|
|
||||||
{
|
|
||||||
relay: "",
|
|
||||||
filters: a
|
|
||||||
}
|
|
||||||
])
|
|
||||||
})
|
|
||||||
})
|
|
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import { schnorr, secp256k1 } from "@noble/curves/secp256k1";
|
import { schnorr, secp256k1 } from "@noble/curves/secp256k1";
|
||||||
import { Nip4WebCryptoEncryptor } from "../src/impl/nip4";
|
import { Nip4WebCryptoEncryptor } from "../src/impl/nip4";
|
||||||
import { Nip44Encryptor } from "../src/impl/nip44";
|
import { Nip44Encryptor } from "../src/impl/nip44";
|
||||||
@ -10,36 +9,36 @@ const bKey = secp256k1.utils.randomPrivateKey();
|
|||||||
const bPubKey = schnorr.getPublicKey(bKey);
|
const bPubKey = schnorr.getPublicKey(bKey);
|
||||||
|
|
||||||
describe("NIP-04", () => {
|
describe("NIP-04", () => {
|
||||||
it("should encrypt/decrypt", async () => {
|
it("should encrypt/decrypt", async () => {
|
||||||
const msg = "test hello, 123";
|
const msg = "test hello, 123";
|
||||||
const enc = new Nip4WebCryptoEncryptor();
|
const enc = new Nip4WebCryptoEncryptor();
|
||||||
const sec = enc.getSharedSecret(bytesToHex(aKey), bytesToHex(bPubKey));
|
const sec = enc.getSharedSecret(bytesToHex(aKey), bytesToHex(bPubKey));
|
||||||
|
|
||||||
const ciphertext = await enc.encryptData(msg, sec);
|
const ciphertext = await enc.encryptData(msg, sec);
|
||||||
expect(ciphertext).toMatch(/^.*\?iv=.*$/i);
|
expect(ciphertext).toMatch(/^.*\?iv=.*$/i);
|
||||||
|
|
||||||
const dec = new Nip4WebCryptoEncryptor();
|
const dec = new Nip4WebCryptoEncryptor();
|
||||||
const sec2 = enc.getSharedSecret(bytesToHex(bKey), bytesToHex(aPubKey));
|
const sec2 = enc.getSharedSecret(bytesToHex(bKey), bytesToHex(aPubKey));
|
||||||
const plaintext = await dec.decryptData(ciphertext, sec2);
|
const plaintext = await dec.decryptData(ciphertext, sec2);
|
||||||
expect(plaintext).toEqual(msg);
|
expect(plaintext).toEqual(msg);
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
|
|
||||||
describe("NIP-44", () => {
|
describe("NIP-44", () => {
|
||||||
it("should encrypt/decrypt", () => {
|
it("should encrypt/decrypt", () => {
|
||||||
const msg = "test hello, 123";
|
const msg = "test hello, 123";
|
||||||
const enc = new Nip44Encryptor();
|
const enc = new Nip44Encryptor();
|
||||||
const sec = enc.getSharedSecret(bytesToHex(aKey), bytesToHex(bPubKey));
|
const sec = enc.getSharedSecret(bytesToHex(aKey), bytesToHex(bPubKey));
|
||||||
|
|
||||||
const ciphertext = enc.encryptData(msg, sec);
|
const ciphertext = enc.encryptData(msg, sec);
|
||||||
const jObj = JSON.parse(ciphertext);
|
const jObj = JSON.parse(ciphertext);
|
||||||
expect(jObj).toHaveProperty("ciphertext")
|
expect(jObj).toHaveProperty("ciphertext");
|
||||||
expect(jObj).toHaveProperty("nonce")
|
expect(jObj).toHaveProperty("nonce");
|
||||||
expect(jObj.v).toBe(1);
|
expect(jObj.v).toBe(1);
|
||||||
|
|
||||||
const dec = new Nip44Encryptor();
|
const dec = new Nip44Encryptor();
|
||||||
const sec2 = enc.getSharedSecret(bytesToHex(bKey), bytesToHex(aPubKey));
|
const sec2 = enc.getSharedSecret(bytesToHex(bKey), bytesToHex(aPubKey));
|
||||||
const plaintext = dec.decryptData(ciphertext, sec2);
|
const plaintext = dec.decryptData(ciphertext, sec2);
|
||||||
expect(plaintext).toEqual(msg);
|
expect(plaintext).toEqual(msg);
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import { Connection } from "../src";
|
import { Connection } from "../src";
|
||||||
import { describe, expect } from "@jest/globals";
|
import { describe, expect } from "@jest/globals";
|
||||||
import { Query } from "../src/Query";
|
import { Query } from "../src/query";
|
||||||
import { getRandomValues } from "crypto";
|
import { getRandomValues } from "crypto";
|
||||||
import { FlatNoteStore } from "../src/NoteCollection";
|
import { FlatNoteStore } from "../src/note-collection";
|
||||||
import { RequestStrategy } from "../src/RequestBuilder";
|
import { RequestStrategy } from "../src/request-builder";
|
||||||
|
|
||||||
window.crypto = {} as any;
|
window.crypto = {} as any;
|
||||||
window.crypto.getRandomValues = getRandomValues as any;
|
window.crypto.getRandomValues = getRandomValues as any;
|
||||||
|
62
packages/system/tests/event-ext.test.ts
Normal file
62
packages/system/tests/event-ext.test.ts
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import { EventExt } from "../src/event-ext";
|
||||||
|
|
||||||
|
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..."],
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
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"],
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
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: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
33
packages/system/tests/gossip-model.test.ts
Normal file
33
packages/system/tests/gossip-model.test.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import { splitAllByWriteRelays } from "../src/gossip-model";
|
||||||
|
|
||||||
|
describe("GossipModel", () => {
|
||||||
|
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 output = splitAllByWriteRelays(Relays, a);
|
||||||
|
expect(output).toEqual([
|
||||||
|
{
|
||||||
|
relay: "",
|
||||||
|
filters: a,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
@ -1,6 +1,6 @@
|
|||||||
import { TaggedRawEvent } from "../src/Nostr";
|
import { TaggedRawEvent } from "../src/nostr";
|
||||||
import { describe, expect } from "@jest/globals";
|
import { describe, expect } from "@jest/globals";
|
||||||
import { FlatNoteStore, ReplaceableNoteStore } from "../src/NoteCollection";
|
import { FlatNoteStore, ReplaceableNoteStore } from "../src/note-collection";
|
||||||
|
|
||||||
describe("NoteStore", () => {
|
describe("NoteStore", () => {
|
||||||
describe("flat", () => {
|
describe("flat", () => {
|
@ -1,7 +1,7 @@
|
|||||||
import { RelayCache } from "../src/GossipModel";
|
import { RelayCache } from "../src/gossip-model";
|
||||||
import { RequestBuilder, RequestStrategy } from "../src/RequestBuilder";
|
import { RequestBuilder, RequestStrategy } from "../src/request-builder";
|
||||||
import { describe, expect } from "@jest/globals";
|
import { describe, expect } from "@jest/globals";
|
||||||
import { expandFilter } from "../src/RequestExpander";
|
import { expandFilter } from "../src/request-expander";
|
||||||
import { bytesToHex } from "@noble/curves/abstract/utils";
|
import { bytesToHex } from "@noble/curves/abstract/utils";
|
||||||
import { unixNow, unixNowMs } from "@snort/shared";
|
import { unixNow, unixNowMs } from "@snort/shared";
|
||||||
|
|
||||||
@ -20,7 +20,7 @@ const DummyCache = {
|
|||||||
write: true,
|
write: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]
|
],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
} as RelayCache;
|
} as RelayCache;
|
||||||
@ -181,23 +181,24 @@ describe("build diff, large follow list", () => {
|
|||||||
|
|
||||||
const start = unixNowMs();
|
const start = unixNowMs();
|
||||||
const a = rb.build(DummyCache);
|
const a = rb.build(DummyCache);
|
||||||
expect(a).toEqual(f.map(a => {
|
expect(a).toEqual(
|
||||||
return {
|
f.map(a => {
|
||||||
strategy: RequestStrategy.AuthorsRelays,
|
return {
|
||||||
relay: `wss://${a}.com/`,
|
strategy: RequestStrategy.AuthorsRelays,
|
||||||
filters: [
|
relay: `wss://${a}.com/`,
|
||||||
{
|
filters: [
|
||||||
kinds: [1, 6, 10002, 3, 6969],
|
{
|
||||||
authors: [a],
|
kinds: [1, 6, 10002, 3, 6969],
|
||||||
}
|
authors: [a],
|
||||||
],
|
},
|
||||||
}
|
],
|
||||||
}));
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
expect(unixNowMs() - start).toBeLessThan(500);
|
expect(unixNowMs() - start).toBeLessThan(500);
|
||||||
|
|
||||||
const start2 = unixNowMs();
|
const start2 = unixNowMs();
|
||||||
const b = rb.buildDiff(DummyCache, rb.buildRaw().flatMap(expandFilter));
|
const b = rb.buildDiff(DummyCache, rb.buildRaw().flatMap(expandFilter));
|
||||||
expect(b).toEqual([]);
|
expect(b).toEqual([]);
|
||||||
expect(unixNowMs() - start2).toBeLessThan(100);
|
expect(unixNowMs() - start2).toBeLessThan(100);
|
||||||
|
});
|
||||||
})
|
|
@ -1,4 +1,4 @@
|
|||||||
import { expandFilter } from "../src/RequestExpander";
|
import { expandFilter } from "../src/request-expander";
|
||||||
|
|
||||||
describe("RequestExpander", () => {
|
describe("RequestExpander", () => {
|
||||||
test("expand filter", () => {
|
test("expand filter", () => {
|
@ -1,4 +1,4 @@
|
|||||||
import { eventMatchesFilter } from "../src/RequestMatcher";
|
import { eventMatchesFilter } from "../src/request-matcher";
|
||||||
|
|
||||||
describe("RequestMatcher", () => {
|
describe("RequestMatcher", () => {
|
||||||
it("should match simple filter", () => {
|
it("should match simple filter", () => {
|
@ -1,6 +1,6 @@
|
|||||||
import { ReqFilter } from "../src";
|
import { ReqFilter } from "../src";
|
||||||
import { canMergeFilters, filterIncludes, flatMerge, mergeSimilar, simpleMerge } from "../src/RequestMerger";
|
import { canMergeFilters, filterIncludes, flatMerge, mergeSimilar, simpleMerge } from "../src/request-merger";
|
||||||
import { FlatReqFilter, expandFilter } from "../src/RequestExpander";
|
import { FlatReqFilter, expandFilter } from "../src/request-expander";
|
||||||
|
|
||||||
describe("RequestMerger", () => {
|
describe("RequestMerger", () => {
|
||||||
it("should simple merge authors", () => {
|
it("should simple merge authors", () => {
|
||||||
@ -108,50 +108,50 @@ describe("flatMerge", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('canMerge', () => {
|
describe("canMerge", () => {
|
||||||
it("should have 0 distance", () => {
|
it("should have 0 distance", () => {
|
||||||
const a = {
|
const a = {
|
||||||
ids: "a",
|
ids: "a",
|
||||||
keys: 1
|
keys: 1,
|
||||||
};
|
};
|
||||||
const b = {
|
const b = {
|
||||||
ids: "a",
|
ids: "a",
|
||||||
keys: 1
|
keys: 1,
|
||||||
};
|
};
|
||||||
expect(canMergeFilters(a, b)).toEqual(true);
|
expect(canMergeFilters(a, b)).toEqual(true);
|
||||||
});
|
});
|
||||||
it("should have 1 distance", () => {
|
it("should have 1 distance", () => {
|
||||||
const a = {
|
const a = {
|
||||||
ids: "a",
|
ids: "a",
|
||||||
keys: 1
|
keys: 1,
|
||||||
};
|
};
|
||||||
const b = {
|
const b = {
|
||||||
ids: "b",
|
ids: "b",
|
||||||
keys: 1
|
keys: 1,
|
||||||
};
|
};
|
||||||
expect(canMergeFilters(a, b)).toEqual(true);
|
expect(canMergeFilters(a, b)).toEqual(true);
|
||||||
});
|
});
|
||||||
it("should have 10 distance", () => {
|
it("should have 10 distance", () => {
|
||||||
const a = {
|
const a = {
|
||||||
ids: "a",
|
ids: "a",
|
||||||
keys: 1
|
keys: 1,
|
||||||
};
|
};
|
||||||
const b = {
|
const b = {
|
||||||
ids: "a",
|
ids: "a",
|
||||||
kinds: 1,
|
kinds: 1,
|
||||||
keys: 2
|
keys: 2,
|
||||||
};
|
};
|
||||||
expect(canMergeFilters(a, b)).toEqual(false);
|
expect(canMergeFilters(a, b)).toEqual(false);
|
||||||
});
|
});
|
||||||
it("should have 11 distance", () => {
|
it("should have 11 distance", () => {
|
||||||
const a = {
|
const a = {
|
||||||
ids: "a",
|
ids: "a",
|
||||||
keys: 1
|
keys: 1,
|
||||||
};
|
};
|
||||||
const b = {
|
const b = {
|
||||||
ids: "b",
|
ids: "b",
|
||||||
kinds: 1,
|
kinds: 1,
|
||||||
keys: 2
|
keys: 2,
|
||||||
};
|
};
|
||||||
expect(canMergeFilters(a, b)).toEqual(false);
|
expect(canMergeFilters(a, b)).toEqual(false);
|
||||||
});
|
});
|
||||||
@ -160,13 +160,13 @@ describe('canMerge', () => {
|
|||||||
since: 1,
|
since: 1,
|
||||||
until: 100,
|
until: 100,
|
||||||
kinds: [1],
|
kinds: [1],
|
||||||
authors: ["kieran", "snort", "c", "d", "e"]
|
authors: ["kieran", "snort", "c", "d", "e"],
|
||||||
};
|
};
|
||||||
const b = {
|
const b = {
|
||||||
since: 1,
|
since: 1,
|
||||||
until: 100,
|
until: 100,
|
||||||
kinds: [6969],
|
kinds: [6969],
|
||||||
authors: ["kieran", "snort", "c", "d", "e"]
|
authors: ["kieran", "snort", "c", "d", "e"],
|
||||||
};
|
};
|
||||||
expect(canMergeFilters(a, b)).toEqual(true);
|
expect(canMergeFilters(a, b)).toEqual(true);
|
||||||
});
|
});
|
||||||
@ -175,14 +175,14 @@ describe('canMerge', () => {
|
|||||||
since: 1,
|
since: 1,
|
||||||
until: 100,
|
until: 100,
|
||||||
kinds: [1],
|
kinds: [1],
|
||||||
authors: ["f", "kieran", "snort", "c", "d"]
|
authors: ["f", "kieran", "snort", "c", "d"],
|
||||||
};
|
};
|
||||||
const b = {
|
const b = {
|
||||||
since: 1,
|
since: 1,
|
||||||
until: 100,
|
until: 100,
|
||||||
kinds: [1],
|
kinds: [1],
|
||||||
authors: ["kieran", "snort", "c", "d", "e"]
|
authors: ["kieran", "snort", "c", "d", "e"],
|
||||||
};
|
};
|
||||||
expect(canMergeFilters(a, b)).toEqual(true);
|
expect(canMergeFilters(a, b)).toEqual(true);
|
||||||
});
|
});
|
||||||
})
|
});
|
@ -1,15 +1,15 @@
|
|||||||
import { ReqFilter } from "../src";
|
import { ReqFilter } from "../src";
|
||||||
import { describe, expect } from "@jest/globals";
|
import { describe, expect } from "@jest/globals";
|
||||||
import { diffFilters } from "../src/RequestSplitter";
|
import { diffFilters } from "../src/request-splitter";
|
||||||
import { expandFilter } from "../src/RequestExpander";
|
import { expandFilter } from "../src/request-expander";
|
||||||
|
|
||||||
describe("RequestSplitter", () => {
|
describe("RequestSplitter", () => {
|
||||||
test("single filter add value", () => {
|
test("single filter add value", () => {
|
||||||
const a: Array<ReqFilter> = [{ kinds: [0], authors: ["a"] }];
|
const a: Array<ReqFilter> = [{ kinds: [0], authors: ["a"] }];
|
||||||
const b: Array<ReqFilter> = [{ kinds: [0], authors: ["a", "b"] }];
|
const b: Array<ReqFilter> = [{ kinds: [0], authors: ["a", "b"] }];
|
||||||
const diff = diffFilters(a.flatMap(expandFilter), b.flatMap(expandFilter), true);
|
const diff = diffFilters(a.flatMap(expandFilter), b.flatMap(expandFilter), true);
|
||||||
expect(diff).toEqual({
|
expect(diff).toMatchObject({
|
||||||
added: [{ kinds: [0], authors: ["b"] }],
|
added: [{ kinds: 0, authors: "b" }],
|
||||||
removed: [],
|
removed: [],
|
||||||
changed: true,
|
changed: true,
|
||||||
});
|
});
|
||||||
@ -18,9 +18,9 @@ describe("RequestSplitter", () => {
|
|||||||
const a: Array<ReqFilter> = [{ kinds: [0], authors: ["a"] }];
|
const a: Array<ReqFilter> = [{ kinds: [0], authors: ["a"] }];
|
||||||
const b: Array<ReqFilter> = [{ kinds: [0], authors: ["b"] }];
|
const b: Array<ReqFilter> = [{ kinds: [0], authors: ["b"] }];
|
||||||
const diff = diffFilters(a.flatMap(expandFilter), b.flatMap(expandFilter), true);
|
const diff = diffFilters(a.flatMap(expandFilter), b.flatMap(expandFilter), true);
|
||||||
expect(diff).toEqual({
|
expect(diff).toMatchObject({
|
||||||
added: [{ kinds: [0], authors: ["b"] }],
|
added: [{ kinds: 0, authors: "b" }],
|
||||||
removed: [{ kinds: [0], authors: ["a"] }],
|
removed: [{ kinds: 0, authors: "a" }],
|
||||||
changed: true,
|
changed: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -28,9 +28,12 @@ describe("RequestSplitter", () => {
|
|||||||
const a: Array<ReqFilter> = [{ kinds: [0], authors: ["a"], since: 100 }];
|
const a: Array<ReqFilter> = [{ kinds: [0], authors: ["a"], since: 100 }];
|
||||||
const b: Array<ReqFilter> = [{ kinds: [0], authors: ["a", "b"], since: 101 }];
|
const b: Array<ReqFilter> = [{ kinds: [0], authors: ["a", "b"], since: 101 }];
|
||||||
const diff = diffFilters(a.flatMap(expandFilter), b.flatMap(expandFilter), true);
|
const diff = diffFilters(a.flatMap(expandFilter), b.flatMap(expandFilter), true);
|
||||||
expect(diff).toEqual({
|
expect(diff).toMatchObject({
|
||||||
added: [{ kinds: [0], authors: ["a", "b"], since: 101 }],
|
added: [
|
||||||
removed: [{ kinds: [0], authors: ["a"], since: 100 }],
|
{ kinds: 0, authors: "a", since: 101 },
|
||||||
|
{ kinds: 0, authors: "b", since: 101 },
|
||||||
|
],
|
||||||
|
removed: [{ kinds: 0, authors: "a", since: 100 }],
|
||||||
changed: true,
|
changed: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -44,10 +47,10 @@ describe("RequestSplitter", () => {
|
|||||||
{ kinds: [69], authors: ["a", "c"] },
|
{ kinds: [69], authors: ["a", "c"] },
|
||||||
];
|
];
|
||||||
const diff = diffFilters(a.flatMap(expandFilter), b.flatMap(expandFilter), true);
|
const diff = diffFilters(a.flatMap(expandFilter), b.flatMap(expandFilter), true);
|
||||||
expect(diff).toEqual({
|
expect(diff).toMatchObject({
|
||||||
added: [
|
added: [
|
||||||
{ kinds: [0], authors: ["b"] },
|
{ kinds: 0, authors: "b" },
|
||||||
{ kinds: [69], authors: ["c"] },
|
{ kinds: 69, authors: "c" },
|
||||||
],
|
],
|
||||||
removed: [],
|
removed: [],
|
||||||
changed: true,
|
changed: true,
|
||||||
@ -63,12 +66,15 @@ describe("RequestSplitter", () => {
|
|||||||
{ kinds: [69], authors: ["c"] },
|
{ kinds: [69], authors: ["c"] },
|
||||||
];
|
];
|
||||||
const diff = diffFilters(a.flatMap(expandFilter), b.flatMap(expandFilter), true);
|
const diff = diffFilters(a.flatMap(expandFilter), b.flatMap(expandFilter), true);
|
||||||
expect(diff).toEqual({
|
expect(diff).toMatchObject({
|
||||||
added: [
|
added: [
|
||||||
{ kinds: [0], authors: ["b"] },
|
{ kinds: 0, authors: "b" },
|
||||||
{ kinds: [69], authors: ["c"] },
|
{ kinds: 69, authors: "c" },
|
||||||
|
],
|
||||||
|
removed: [
|
||||||
|
{ kinds: 0, authors: "a" },
|
||||||
|
{ kinds: 69, authors: "a" },
|
||||||
],
|
],
|
||||||
removed: [{ kinds: [0, 69], authors: ["a"] }],
|
|
||||||
changed: true,
|
changed: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -79,8 +85,8 @@ describe("RequestSplitter", () => {
|
|||||||
{ kinds: [69], authors: ["c"] },
|
{ kinds: [69], authors: ["c"] },
|
||||||
];
|
];
|
||||||
const diff = diffFilters(a.flatMap(expandFilter), b.flatMap(expandFilter), true);
|
const diff = diffFilters(a.flatMap(expandFilter), b.flatMap(expandFilter), true);
|
||||||
expect(diff).toEqual({
|
expect(diff).toMatchObject({
|
||||||
added: [{ kinds: [69], authors: ["c"] }],
|
added: [{ kinds: 69, authors: "c" }],
|
||||||
removed: [],
|
removed: [],
|
||||||
changed: true,
|
changed: true,
|
||||||
});
|
});
|
@ -2,4 +2,4 @@ import { TextEncoder, TextDecoder } from "util";
|
|||||||
import { Crypto } from "@peculiar/webcrypto";
|
import { Crypto } from "@peculiar/webcrypto";
|
||||||
|
|
||||||
Object.assign(global, { TextDecoder, TextEncoder });
|
Object.assign(global, { TextDecoder, TextEncoder });
|
||||||
Object.assign(globalThis.window.crypto, new Crypto());
|
Object.assign(globalThis.window.crypto, new Crypto());
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { NostrPrefix } from "../src/Links";
|
import { NostrPrefix } from "../src/links";
|
||||||
import { parseNostrLink, tryParseNostrLink } from "../src/NostrLink";
|
import { parseNostrLink, tryParseNostrLink } from "../src/nostr-link";
|
||||||
|
|
||||||
describe("tryParseNostrLink", () => {
|
describe("tryParseNostrLink", () => {
|
||||||
it("is a valid nostr link", () => {
|
it("is a valid nostr link", () => {
|
@ -22,10 +22,7 @@
|
|||||||
"depends": []
|
"depends": []
|
||||||
},
|
},
|
||||||
"externalBin": [],
|
"externalBin": [],
|
||||||
"icon": [
|
"icon": ["icons/128x128.png", "icons/128x128@2x.png"],
|
||||||
"icons/128x128.png",
|
|
||||||
"icons/128x128@2x.png"
|
|
||||||
],
|
|
||||||
"identifier": "social.snort.app",
|
"identifier": "social.snort.app",
|
||||||
"longDescription": "",
|
"longDescription": "",
|
||||||
"macOS": {
|
"macOS": {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user