mirror of
https://github.com/aljazceru/bakery.git
synced 2025-12-17 12:45:20 +01:00
start using applesauce
This commit is contained in:
22
.vscode/launch.json
vendored
Normal file
22
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Launch",
|
||||
"skipFiles": ["<node_internals>/**", "data/**"],
|
||||
"args": ["--loader", "@swc-node/register/esm", "src/index.ts"],
|
||||
"outFiles": ["${workspaceFolder}/**/*.js"],
|
||||
"env": {
|
||||
"NODE_ENV": "development",
|
||||
"DEBUG": "bakery,bakery:*,applesauce,applesauce:*",
|
||||
"DEBUG_HIDE_DATE": "true",
|
||||
"DEBUG_COLORS": "true"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -28,8 +28,8 @@
|
||||
"@noble/hashes": "^1.7.1",
|
||||
"@satellite-earth/core": "^0.5.0",
|
||||
"applesauce-core": "next",
|
||||
"applesauce-factory": "0.0.0-next-20250206174509",
|
||||
"applesauce-loaders": "^0.10.0",
|
||||
"applesauce-factory": "next",
|
||||
"applesauce-loaders": "next",
|
||||
"applesauce-signers": "next",
|
||||
"better-sqlite3": "^11.8.1",
|
||||
"blossom-client-sdk": "^2.1.1",
|
||||
|
||||
77
pnpm-lock.yaml
generated
77
pnpm-lock.yaml
generated
@@ -19,16 +19,16 @@ importers:
|
||||
version: 0.5.0(typescript@5.7.3)
|
||||
applesauce-core:
|
||||
specifier: next
|
||||
version: 0.0.0-next-20250206174509(typescript@5.7.3)
|
||||
version: 0.0.0-next-20250207175032(typescript@5.7.3)
|
||||
applesauce-factory:
|
||||
specifier: 0.0.0-next-20250206174509
|
||||
version: 0.0.0-next-20250206174509(typescript@5.7.3)
|
||||
specifier: next
|
||||
version: 0.0.0-next-20250207175032(typescript@5.7.3)
|
||||
applesauce-loaders:
|
||||
specifier: ^0.10.0
|
||||
version: 0.10.0(typescript@5.7.3)
|
||||
specifier: next
|
||||
version: 0.0.0-next-20250207175032(typescript@5.7.3)
|
||||
applesauce-signers:
|
||||
specifier: next
|
||||
version: 0.0.0-next-20250206174509(typescript@5.7.3)
|
||||
version: 0.0.0-next-20250207175032(typescript@5.7.3)
|
||||
better-sqlite3:
|
||||
specifier: ^11.8.1
|
||||
version: 11.8.1
|
||||
@@ -560,23 +560,20 @@ packages:
|
||||
resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
|
||||
engines: {node: '>= 8'}
|
||||
|
||||
applesauce-content@0.0.0-next-20250206174509:
|
||||
resolution: {integrity: sha512-+LXzDZdANYwZGKnpc0CFoEgF0KgKuOcYTEUrf0X79ezKejCEGwvn1UvNce2c8MIWfwhaHBTJq8kQO2gKaODQ9A==}
|
||||
applesauce-content@0.0.0-next-20250207175032:
|
||||
resolution: {integrity: sha512-n6fA+OGWipHXEQI8rt42/GRAYUTHvUScpXiBei9KvCs2rhIIA1wJ4dlHEyOTL12slcVMi3q6PfCxdv0uWNuHxw==}
|
||||
|
||||
applesauce-core@0.0.0-next-20250206174509:
|
||||
resolution: {integrity: sha512-+5G8y4WwjJVvL2dfn56w0mYhm4GjL0hoMk4ZJBM7FUBcpuq40UoBsslheIFaLYuzzZHvd28EtJycbyhBKM86pQ==}
|
||||
applesauce-core@0.0.0-next-20250207175032:
|
||||
resolution: {integrity: sha512-jTM62hmA7BRjO3EGZePMTrl77pA1uXaWOWMbdwyDKFcil9RZUGMKV2cL73SXtuxFQJRrFmgrTWSLzOEWEfVdVg==}
|
||||
|
||||
applesauce-core@0.10.0:
|
||||
resolution: {integrity: sha512-QMhUh4FIARcqY5soCB4Z8DIu+py0rYb28IgWT4gP9DLBGpDrY8lStXk7W1/46TLjEH97y0hbiXFK7kMCZ31oOQ==}
|
||||
applesauce-factory@0.0.0-next-20250207175032:
|
||||
resolution: {integrity: sha512-NMGrHpD/BxFNvm0AzK2fYzXGBjzqvoA/o6ZWZUvo8Y52qvETjz1qS7asMndMXBWszDHwo8e0buDMHjlAA5/prA==}
|
||||
|
||||
applesauce-factory@0.0.0-next-20250206174509:
|
||||
resolution: {integrity: sha512-E5OTJLYSh5vejQmuHZOqxLajJwPiQOFnlkhKQSBMeA2RyxClOuuDIm8Qj3PXlC1T9kAqIqL/CsieUcJ1jhUWdQ==}
|
||||
applesauce-loaders@0.0.0-next-20250207175032:
|
||||
resolution: {integrity: sha512-2gQF3O4jS7hFjFkI2lI0gMJVn1IB4iWUSR9FjWYICQfQXZ0ewpOhhi2KzX6oXdlMeZx/y2bh8aNUO6unvagGFA==}
|
||||
|
||||
applesauce-loaders@0.10.0:
|
||||
resolution: {integrity: sha512-cEPV8nSJKfmvVVRaSDc/3TRaDE8s0CSFJGzEvSYBTjokZBvQz2QZrlQXbwTvYD90XrWFH1LJ8dLDDnEUehaOow==}
|
||||
|
||||
applesauce-signers@0.0.0-next-20250206174509:
|
||||
resolution: {integrity: sha512-SfV1nG9YhF9aKS1c8uUg2ciNmetBbUgmV58l8/nnulHatGDDNtX+d34XGKxXXGRHUHk7bYtFKeeYSBNw4Wm94A==}
|
||||
applesauce-signers@0.0.0-next-20250207175032:
|
||||
resolution: {integrity: sha512-rJQvFpeIk9s0eHdNjl/DvMzD8pXTtx+Hr0Rcadsq0w2t+vAA0Z0B7G4xtG2Y+eCnIdSeJ/TdkebEcRLDV5eZHw==}
|
||||
|
||||
argparse@1.0.10:
|
||||
resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==}
|
||||
@@ -1606,8 +1603,8 @@ packages:
|
||||
resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
possible-typed-array-names@1.0.0:
|
||||
resolution: {integrity: sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==}
|
||||
possible-typed-array-names@1.1.0:
|
||||
resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
prebuild-install@7.1.3:
|
||||
@@ -2605,13 +2602,13 @@ snapshots:
|
||||
normalize-path: 3.0.0
|
||||
picomatch: 2.3.1
|
||||
|
||||
applesauce-content@0.0.0-next-20250206174509(typescript@5.7.3):
|
||||
applesauce-content@0.0.0-next-20250207175032(typescript@5.7.3):
|
||||
dependencies:
|
||||
'@cashu/cashu-ts': 2.0.0-rc1
|
||||
'@types/hast': 3.0.4
|
||||
'@types/mdast': 4.0.4
|
||||
'@types/unist': 3.0.3
|
||||
applesauce-core: 0.0.0-next-20250206174509(typescript@5.7.3)
|
||||
applesauce-core: 0.0.0-next-20250207175032(typescript@5.7.3)
|
||||
mdast-util-find-and-replace: 3.0.2
|
||||
nostr-tools: 2.10.4(typescript@5.7.3)
|
||||
remark: 15.0.1
|
||||
@@ -2622,7 +2619,7 @@ snapshots:
|
||||
- supports-color
|
||||
- typescript
|
||||
|
||||
applesauce-core@0.0.0-next-20250206174509(typescript@5.7.3):
|
||||
applesauce-core@0.0.0-next-20250207175032(typescript@5.7.3):
|
||||
dependencies:
|
||||
'@scure/base': 1.2.4
|
||||
debug: 4.4.0(supports-color@5.5.0)
|
||||
@@ -2636,33 +2633,19 @@ snapshots:
|
||||
- supports-color
|
||||
- typescript
|
||||
|
||||
applesauce-core@0.10.0(typescript@5.7.3):
|
||||
applesauce-factory@0.0.0-next-20250207175032(typescript@5.7.3):
|
||||
dependencies:
|
||||
'@scure/base': 1.2.4
|
||||
debug: 4.4.0(supports-color@5.5.0)
|
||||
fast-deep-equal: 3.1.3
|
||||
hash-sum: 2.0.0
|
||||
light-bolt11-decoder: 3.2.0
|
||||
nanoid: 5.0.9
|
||||
nostr-tools: 2.10.4(typescript@5.7.3)
|
||||
rxjs: 7.8.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
- typescript
|
||||
|
||||
applesauce-factory@0.0.0-next-20250206174509(typescript@5.7.3):
|
||||
dependencies:
|
||||
applesauce-content: 0.0.0-next-20250206174509(typescript@5.7.3)
|
||||
applesauce-core: 0.0.0-next-20250206174509(typescript@5.7.3)
|
||||
applesauce-content: 0.0.0-next-20250207175032(typescript@5.7.3)
|
||||
applesauce-core: 0.0.0-next-20250207175032(typescript@5.7.3)
|
||||
nanoid: 5.0.9
|
||||
nostr-tools: 2.10.4(typescript@5.7.3)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
- typescript
|
||||
|
||||
applesauce-loaders@0.10.0(typescript@5.7.3):
|
||||
applesauce-loaders@0.0.0-next-20250207175032(typescript@5.7.3):
|
||||
dependencies:
|
||||
applesauce-core: 0.10.0(typescript@5.7.3)
|
||||
applesauce-core: 0.0.0-next-20250207175032(typescript@5.7.3)
|
||||
nanoid: 5.0.9
|
||||
nostr-tools: 2.10.4(typescript@5.7.3)
|
||||
rx-nostr: 3.5.0
|
||||
@@ -2671,12 +2654,12 @@ snapshots:
|
||||
- supports-color
|
||||
- typescript
|
||||
|
||||
applesauce-signers@0.0.0-next-20250206174509(typescript@5.7.3):
|
||||
applesauce-signers@0.0.0-next-20250207175032(typescript@5.7.3):
|
||||
dependencies:
|
||||
'@noble/hashes': 1.7.1
|
||||
'@noble/secp256k1': 1.7.1
|
||||
'@scure/base': 1.2.4
|
||||
applesauce-core: 0.0.0-next-20250206174509(typescript@5.7.3)
|
||||
applesauce-core: 0.0.0-next-20250207175032(typescript@5.7.3)
|
||||
debug: 4.4.0(supports-color@5.5.0)
|
||||
nanoid: 5.0.9
|
||||
nostr-tools: 2.10.4(typescript@5.7.3)
|
||||
@@ -2707,7 +2690,7 @@ snapshots:
|
||||
|
||||
available-typed-arrays@1.0.7:
|
||||
dependencies:
|
||||
possible-typed-array-names: 1.0.0
|
||||
possible-typed-array-names: 1.1.0
|
||||
|
||||
b4a@1.6.7: {}
|
||||
|
||||
@@ -3918,7 +3901,7 @@ snapshots:
|
||||
|
||||
pirates@4.0.6: {}
|
||||
|
||||
possible-typed-array-names@1.0.0: {}
|
||||
possible-typed-array-names@1.1.0: {}
|
||||
|
||||
prebuild-install@7.1.3:
|
||||
dependencies:
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import EventEmitter from "events";
|
||||
import Database, { type Database as SQLDatabase } from "better-sqlite3";
|
||||
import { fileURLToPath } from "url";
|
||||
import path from "path";
|
||||
import fs from "fs";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
export type LocalDatabaseConfig = {
|
||||
directory: string;
|
||||
name: string;
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import path from "path";
|
||||
import { WebSocketServer } from "ws";
|
||||
import { createServer, Server } from "http";
|
||||
import { SimpleSigner } from "applesauce-signers/signers/simple-signer";
|
||||
import { IEventStore, NostrRelay, SQLiteEventStore } from "@satellite-earth/core";
|
||||
import { getDMRecipient } from "@satellite-earth/core/helpers/nostr";
|
||||
import { kinds } from "nostr-tools";
|
||||
import { AbstractRelay } from "nostr-tools/abstract-relay";
|
||||
import express, { Express } from "express";
|
||||
import { EventEmitter } from "events";
|
||||
import { SimpleSigner } from "applesauce-signers/signers/simple-signer";
|
||||
import cors from "cors";
|
||||
|
||||
import { logger } from "../logger.js";
|
||||
@@ -44,6 +43,12 @@ import SecretsManager from "../modules/secrets-manager.js";
|
||||
import outboundNetwork, { OutboundNetworkManager } from "../modules/network/outbound/index.js";
|
||||
import Switchboard from "../modules/switchboard/switchboard.js";
|
||||
import Gossip from "../modules/gossip.js";
|
||||
import database from "../services/database.js";
|
||||
import secrets from "../services/secrets.js";
|
||||
import config from "../services/config.js";
|
||||
import logStore from "../services/log-store.js";
|
||||
import stateManager from "../services/state.js";
|
||||
import eventCache from "../services/event-cache.js";
|
||||
|
||||
type EventMap = {
|
||||
listening: [];
|
||||
@@ -81,14 +86,12 @@ export default class App extends EventEmitter<EventMap> {
|
||||
switchboard: Switchboard;
|
||||
gossip: Gossip;
|
||||
|
||||
constructor(dataPath: string) {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.config = new ConfigManager(path.join(dataPath, "node.json"));
|
||||
this.config.read();
|
||||
this.config = config;
|
||||
|
||||
this.secrets = new SecretsManager(path.join(dataPath, "secrets.json"));
|
||||
this.secrets.read();
|
||||
this.secrets = secrets;
|
||||
|
||||
this.signer = new SimpleSigner(this.secrets.get("nostrKey"));
|
||||
|
||||
@@ -131,15 +134,13 @@ export default class App extends EventEmitter<EventMap> {
|
||||
headers.push("Access-Control-Allow-Origin: *");
|
||||
});
|
||||
|
||||
// Init embedded sqlite database
|
||||
this.database = new Database({ directory: dataPath });
|
||||
// Init sqlite database
|
||||
this.database = database;
|
||||
|
||||
// create log managers
|
||||
this.logStore = new LogStore(this.database.db);
|
||||
this.logStore.setup();
|
||||
this.logStore = logStore;
|
||||
|
||||
this.state = new ApplicationStateManager(this.database.db);
|
||||
this.state.setup();
|
||||
this.state = stateManager;
|
||||
|
||||
// Recognize local relay by matching auth string
|
||||
this.pool = new CautiousPool((relay: AbstractRelay, challenge: string) => {
|
||||
@@ -150,17 +151,16 @@ export default class App extends EventEmitter<EventMap> {
|
||||
});
|
||||
|
||||
// Initialize the event store
|
||||
this.eventStore = new SQLiteEventStore(this.database.db);
|
||||
this.eventStore.setup();
|
||||
this.eventStore = eventCache;
|
||||
|
||||
// setup decryption cache
|
||||
this.decryptionCache = new DecryptionCache(this.database.db);
|
||||
this.decryptionCache.setup();
|
||||
|
||||
// Setup managers user contacts and profiles
|
||||
this.addressBook = new AddressBook(this);
|
||||
this.profileBook = new ProfileBook(this);
|
||||
this.contactBook = new ContactBook(this);
|
||||
this.addressBook = new AddressBook();
|
||||
this.profileBook = new ProfileBook();
|
||||
this.contactBook = new ContactBook();
|
||||
|
||||
// Setup the notifications manager
|
||||
this.notifications = new NotificationsManager(this /*this.eventStore, this.state*/);
|
||||
@@ -291,8 +291,8 @@ export default class App extends EventEmitter<EventMap> {
|
||||
// send direct message
|
||||
const results = await this.directMessageManager.forwardMessage(ctx.event);
|
||||
|
||||
if (!results || !results.some((p) => p.status === "fulfilled")) throw new Error("Failed to forward message");
|
||||
return `Forwarded message to ${results.filter((p) => p.status === "fulfilled").length}/${results.length} relays`;
|
||||
if (!results || !results.some((p) => p.ok)) throw new Error("Failed to forward message");
|
||||
return `Forwarded message to ${results.filter((p) => p.ok).length}/${results.length} relays`;
|
||||
} else return next();
|
||||
});
|
||||
|
||||
@@ -320,8 +320,8 @@ export default class App extends EventEmitter<EventMap> {
|
||||
const profile = this.profileBook.getProfile(pubkey);
|
||||
if (!profile) {
|
||||
this.profileBook.loadProfile(pubkey, this.addressBook.getOutboxes(pubkey));
|
||||
this.addressBook.loadOutboxes(pubkey).then((outboxes) => {
|
||||
this.profileBook.loadProfile(pubkey, outboxes ?? undefined);
|
||||
this.addressBook.loadMailboxes(pubkey).then((mailboxes) => {
|
||||
this.profileBook.loadProfile(pubkey, mailboxes?.outboxes);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
23
src/core.ts
23
src/core.ts
@@ -1,23 +0,0 @@
|
||||
import { createRxNostr, noopVerifier } from "rx-nostr";
|
||||
import { verifyEvent } from "nostr-tools/wasm";
|
||||
import { EventStore, QueryStore } from "applesauce-core";
|
||||
import { logger } from "./logger.js";
|
||||
|
||||
const log = logger.extend("rx-nostr");
|
||||
|
||||
export const rxNostr = createRxNostr({
|
||||
verifier: async (event) => {
|
||||
try {
|
||||
return verifyEvent(event);
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
rxNostr.createConnectionStateObservable().subscribe((packet) => {
|
||||
log(`${packet.state} ${packet.from}`);
|
||||
});
|
||||
|
||||
export const eventStore = new EventStore();
|
||||
export const queryStore = new QueryStore(eventStore);
|
||||
@@ -1,4 +1,5 @@
|
||||
import "dotenv/config.js";
|
||||
import "dotenv/config";
|
||||
import { mkdirp } from "mkdirp";
|
||||
import { OUTBOUND_PROXY_TYPES } from "./const.js";
|
||||
|
||||
import { safeRelayUrls } from "applesauce-core/helpers";
|
||||
@@ -7,6 +8,8 @@ import { normalizeToHexPubkey } from "./helpers/nip19.js";
|
||||
export const OWNER_PUBKEY = process.env.OWNER_PUBKEY ? normalizeToHexPubkey(process.env.OWNER_PUBKEY) : undefined;
|
||||
export const PUBLIC_ADDRESS = process.env.PUBLIC_ADDRESS;
|
||||
export const DATA_PATH = process.env.DATA_PATH || "./data";
|
||||
await mkdirp(DATA_PATH);
|
||||
|
||||
export const PORT = parseInt(process.env.PORT ?? "") || 3000;
|
||||
|
||||
// I2P config
|
||||
@@ -30,4 +33,6 @@ export const BOOTSTRAP_RELAYS = process.env.BOOTSTRAP_RELAYS
|
||||
|
||||
export const COMMON_CONTACT_RELAYS = process.env.COMMON_CONTACT_RELAYS
|
||||
? safeRelayUrls(process.env.COMMON_CONTACT_RELAYS.split(","))
|
||||
: safeRelayUrls(["wss://purplepag.es", "wss://user.kindpag.es", "wss://relay.nos.social"]);
|
||||
: safeRelayUrls(["wss://purplepag.es", "wss://user.kindpag.es"]);
|
||||
|
||||
export const IS_DEV = process.env.NODE_ENV === "development";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export function arrayFallback<T>(arr: T[], fallback: T[]): T[] {
|
||||
if (arr.length === 0) return fallback;
|
||||
export function arrayFallback<T>(arr: T[] | undefined, fallback: T[]): T[] {
|
||||
if (arr === undefined || arr.length === 0) return fallback;
|
||||
else return arr;
|
||||
}
|
||||
|
||||
16
src/helpers/rxjs.ts
Normal file
16
src/helpers/rxjs.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { bufferTime, MonoTypeOperatorFunction, Subject, tap } from "rxjs";
|
||||
|
||||
export function bufferAudit<T>(buffer = 10_000, audit: (messages: T[]) => void): MonoTypeOperatorFunction<T> {
|
||||
return (source) => {
|
||||
const logBuffer = new Subject<T>();
|
||||
|
||||
logBuffer
|
||||
.pipe(
|
||||
bufferTime(buffer),
|
||||
tap((values) => audit(values)),
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
return source.pipe(tap((value) => logBuffer.next(value)));
|
||||
};
|
||||
}
|
||||
@@ -4,13 +4,12 @@ import process from "node:process";
|
||||
import path from "node:path";
|
||||
|
||||
import express, { Request } from "express";
|
||||
import { mkdirp } from "mkdirp";
|
||||
import dayjs from "dayjs";
|
||||
import duration from "dayjs/plugin/duration.js";
|
||||
import localizedFormat from "dayjs/plugin/localizedFormat.js";
|
||||
|
||||
import App from "./app/index.js";
|
||||
import { DATA_PATH, PUBLIC_ADDRESS } from "./env.js";
|
||||
import { PUBLIC_ADDRESS } from "./env.js";
|
||||
import { addListener, logger } from "./logger.js";
|
||||
import { pathExists } from "./helpers/fs.js";
|
||||
|
||||
@@ -19,8 +18,7 @@ dayjs.extend(duration);
|
||||
dayjs.extend(localizedFormat);
|
||||
|
||||
// create app
|
||||
await mkdirp(DATA_PATH);
|
||||
const app = new App(DATA_PATH);
|
||||
const app = new App();
|
||||
|
||||
// connect logger to app LogStore
|
||||
addListener(({ namespace }, ...args) => {
|
||||
|
||||
@@ -20,6 +20,4 @@ debug.log = function (this: Debugger, ...args: any[]) {
|
||||
console.log.apply(this, args);
|
||||
};
|
||||
|
||||
const logger = debug("bakery");
|
||||
|
||||
export { logger };
|
||||
export const logger = debug("bakery");
|
||||
|
||||
@@ -1,66 +1,44 @@
|
||||
import { NostrEvent, kinds } from "nostr-tools";
|
||||
import _throttle from "lodash.throttle";
|
||||
import { tap } from "rxjs";
|
||||
import { kinds } from "nostr-tools";
|
||||
import { MailboxesQuery } from "applesauce-core/queries";
|
||||
import { getObservableValue } from "applesauce-core/observable";
|
||||
import { getInboxes, getOutboxes } from "applesauce-core/helpers";
|
||||
|
||||
import { getInboxes, getOutboxes } from "@satellite-earth/core/helpers/nostr/mailboxes.js";
|
||||
import { logger } from "../logger.js";
|
||||
import App from "../app/index.js";
|
||||
import PubkeyBatchLoader from "./pubkey-batch-loader.js";
|
||||
import { COMMON_CONTACT_RELAYS } from "../env.js";
|
||||
import { replaceableLoader } from "../services/loaders.js";
|
||||
import { eventStore, queryStore } from "../services/stores.js";
|
||||
import { simpleTimeout } from "../operators/simple-timeout.js";
|
||||
import { arrayFallback } from "../helpers/array.js";
|
||||
|
||||
const DEFAULT_REQUEST_TIMEOUT = 10_000;
|
||||
|
||||
/** Loads 10002 events for pubkeys */
|
||||
export default class AddressBook {
|
||||
log = logger.extend("AddressBook");
|
||||
app: App;
|
||||
loader: PubkeyBatchLoader;
|
||||
|
||||
get extraRelays() {
|
||||
return this.loader.extraRelays;
|
||||
}
|
||||
set extraRelays(v: string[]) {
|
||||
this.loader.extraRelays = v;
|
||||
}
|
||||
|
||||
constructor(app: App) {
|
||||
this.app = app;
|
||||
|
||||
this.loader = new PubkeyBatchLoader(kinds.RelayList, this.app.pool, (pubkey) => {
|
||||
return this.app.eventStore.getEventsForFilters([{ kinds: [kinds.RelayList], authors: [pubkey] }])?.[0];
|
||||
});
|
||||
|
||||
this.loader.on("event", (event) => this.app.eventStore.addEvent(event));
|
||||
this.loader.on("batch", (found, failed) => {
|
||||
this.log(`Found ${found}, failed ${failed}, pending ${this.loader.queue}`);
|
||||
});
|
||||
}
|
||||
|
||||
getMailboxes(pubkey: string) {
|
||||
return this.loader.getEvent(pubkey);
|
||||
return eventStore.getReplaceable(kinds.RelayList, pubkey);
|
||||
}
|
||||
|
||||
getOutboxes(pubkey: string) {
|
||||
const mailboxes = this.getMailboxes(pubkey);
|
||||
return mailboxes && getOutboxes(mailboxes);
|
||||
}
|
||||
|
||||
getInboxes(pubkey: string) {
|
||||
const mailboxes = this.getMailboxes(pubkey);
|
||||
return mailboxes && getInboxes(mailboxes);
|
||||
}
|
||||
|
||||
handleEvent(event: NostrEvent) {
|
||||
this.loader.handleEvent(event);
|
||||
}
|
||||
async loadMailboxes(pubkey: string, relays?: string[], force?: boolean) {
|
||||
relays = arrayFallback(relays, COMMON_CONTACT_RELAYS);
|
||||
this.log(`Requesting mailboxes from ${relays.length} relays for ${pubkey}`);
|
||||
replaceableLoader.next({ kind: kinds.RelayList, pubkey, relays, force });
|
||||
|
||||
async loadMailboxes(pubkey: string, relays?: string[]) {
|
||||
return this.loader.getOrLoadEvent(pubkey, relays);
|
||||
}
|
||||
|
||||
async loadOutboxes(pubkey: string, relays?: string[]) {
|
||||
const mailboxes = await this.loadMailboxes(pubkey, relays);
|
||||
return mailboxes && getOutboxes(mailboxes);
|
||||
}
|
||||
|
||||
async loadInboxes(pubkey: string, relays?: string[]) {
|
||||
const mailboxes = await this.loadMailboxes(pubkey, relays);
|
||||
return mailboxes && getInboxes(mailboxes);
|
||||
return getObservableValue(
|
||||
queryStore.createQuery(MailboxesQuery, pubkey).pipe(
|
||||
simpleTimeout(DEFAULT_REQUEST_TIMEOUT, `Failed to load mailboxes for ${pubkey}`),
|
||||
tap((m) => m && this.log(`Found mailboxes for ${pubkey}`, m)),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,33 +1,24 @@
|
||||
import { NostrEvent, kinds } from "nostr-tools";
|
||||
import _throttle from "lodash.throttle";
|
||||
import { tap } from "rxjs";
|
||||
import { kinds } from "nostr-tools";
|
||||
import { ReplaceableQuery, UserContactsQuery } from "applesauce-core/queries";
|
||||
import { getObservableValue } from "applesauce-core/observable";
|
||||
|
||||
import { COMMON_CONTACT_RELAYS } from "../env.js";
|
||||
import { logger } from "../logger.js";
|
||||
import App from "../app/index.js";
|
||||
import PubkeyBatchLoader from "./pubkey-batch-loader.js";
|
||||
import { COMMON_CONTACT_RELAYS } from "../env.js";
|
||||
import { replaceableLoader } from "../services/loaders.js";
|
||||
import { eventStore, queryStore } from "../services/stores.js";
|
||||
import { simpleTimeout } from "../operators/simple-timeout.js";
|
||||
import { arrayFallback } from "../helpers/array.js";
|
||||
|
||||
const DEFAULT_REQUEST_TIMEOUT = 10_000;
|
||||
|
||||
/** Loads 3 contact lists for pubkeys */
|
||||
export default class ContactBook {
|
||||
log = logger.extend("ContactsBook");
|
||||
app: App;
|
||||
loader: PubkeyBatchLoader;
|
||||
extraRelays = COMMON_CONTACT_RELAYS;
|
||||
|
||||
constructor(app: App) {
|
||||
this.app = app;
|
||||
|
||||
this.loader = new PubkeyBatchLoader(kinds.Contacts, this.app.pool, (pubkey) => {
|
||||
return this.app.eventStore.getEventsForFilters([{ kinds: [kinds.Contacts], authors: [pubkey] }])?.[0];
|
||||
});
|
||||
|
||||
this.loader.on("event", (event) => this.app.eventStore.addEvent(event));
|
||||
this.loader.on("batch", (found, failed) => {
|
||||
this.log(`Found ${found}, failed ${failed}, pending ${this.loader.queue}`);
|
||||
});
|
||||
}
|
||||
|
||||
/** @deprecated use loadContacts instead */
|
||||
getContacts(pubkey: string) {
|
||||
return this.loader.getEvent(pubkey);
|
||||
return eventStore.getReplaceable(kinds.Contacts, pubkey);
|
||||
}
|
||||
|
||||
getFollowedPubkeys(pubkey: string): string[] {
|
||||
@@ -44,11 +35,29 @@ export default class ContactBook {
|
||||
return [];
|
||||
}
|
||||
|
||||
handleEvent(event: NostrEvent) {
|
||||
this.loader.handleEvent(event);
|
||||
async loadContacts(pubkey: string, relays?: string[], force?: boolean) {
|
||||
relays = arrayFallback(relays, COMMON_CONTACT_RELAYS);
|
||||
this.log(`Requesting contacts from ${relays.length} relays for ${pubkey}`);
|
||||
replaceableLoader.next({ kind: kinds.Contacts, pubkey, relays, force });
|
||||
|
||||
return getObservableValue(
|
||||
queryStore
|
||||
.createQuery(UserContactsQuery, pubkey)
|
||||
.pipe(simpleTimeout(DEFAULT_REQUEST_TIMEOUT, `Failed to load contacts for ${pubkey}`)),
|
||||
);
|
||||
}
|
||||
|
||||
async loadContacts(pubkey: string, relays: string[] = []) {
|
||||
return this.loader.getOrLoadEvent(pubkey, relays);
|
||||
/** @deprecated */
|
||||
async loadContactsEvent(pubkey: string, relays?: string[]) {
|
||||
relays = arrayFallback(relays, COMMON_CONTACT_RELAYS);
|
||||
this.log(`Requesting contacts from ${relays.length} relays for ${pubkey}`);
|
||||
replaceableLoader.next({ kind: kinds.Contacts, pubkey, relays });
|
||||
|
||||
return getObservableValue(
|
||||
queryStore.createQuery(ReplaceableQuery, kinds.Contacts, pubkey).pipe(
|
||||
simpleTimeout(DEFAULT_REQUEST_TIMEOUT, `Failed to load contacts for ${pubkey}`),
|
||||
tap((c) => c && this.log(`Found contacts for ${pubkey}`, c)),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
import { filter, lastValueFrom, mergeMap, Subscription, tap, toArray } from "rxjs";
|
||||
import { NostrEvent, kinds } from "nostr-tools";
|
||||
import { SubCloser } from "nostr-tools/abstract-pool";
|
||||
import { Subscription } from "nostr-tools/abstract-relay";
|
||||
import { getInboxes } from "applesauce-core/helpers";
|
||||
import { createRxForwardReq } from "rx-nostr";
|
||||
import { getRelaysFromContactList } from "@satellite-earth/core/helpers/nostr/contacts.js";
|
||||
import { MailboxesQuery } from "applesauce-core/queries";
|
||||
import { EventEmitter } from "events";
|
||||
|
||||
import { getRelaysFromContactList } from "@satellite-earth/core/helpers/nostr/contacts.js";
|
||||
import { logger } from "../logger.js";
|
||||
import type App from "../app/index.js";
|
||||
import { arrayFallback } from "../helpers/array.js";
|
||||
import { rxNostr } from "../services/rx-nostr.js";
|
||||
import { eventStore, queryStore } from "../services/stores.js";
|
||||
import { COMMON_CONTACT_RELAYS } from "../env.js";
|
||||
import { bufferAudit } from "../helpers/rxjs.js";
|
||||
|
||||
type EventMap = {
|
||||
open: [string, string];
|
||||
@@ -47,11 +51,11 @@ export default class DirectMessageManager extends EventEmitter<EventMap> {
|
||||
if (!addressedTo) return;
|
||||
|
||||
// get users inboxes
|
||||
let relays = await this.app.addressBook.loadInboxes(addressedTo);
|
||||
let relays = (await this.app.addressBook.loadMailboxes(addressedTo))?.inboxes;
|
||||
|
||||
if (!relays || relays.length === 0) {
|
||||
// try to send the DM to the users legacy app relays
|
||||
const contacts = await this.app.contactBook.loadContacts(addressedTo);
|
||||
const contacts = await this.app.contactBook.loadContactsEvent(addressedTo);
|
||||
if (contacts) {
|
||||
const appRelays = getRelaysFromContactList(contacts);
|
||||
|
||||
@@ -65,7 +69,7 @@ export default class DirectMessageManager extends EventEmitter<EventMap> {
|
||||
}
|
||||
|
||||
this.log(`Forwarding message to ${relays.length} relays`);
|
||||
const results = await Promise.allSettled(this.app.pool.publish(relays, event));
|
||||
const results = await lastValueFrom(rxNostr.send(event, { on: { relays } }).pipe(toArray()));
|
||||
|
||||
return results;
|
||||
}
|
||||
@@ -75,95 +79,93 @@ export default class DirectMessageManager extends EventEmitter<EventMap> {
|
||||
else return b + ":" + a;
|
||||
}
|
||||
|
||||
watching = new Map<string, Map<string, Subscription>>();
|
||||
watching = new Map<string, Subscription>();
|
||||
async watchInbox(pubkey: string) {
|
||||
if (this.watching.has(pubkey)) return;
|
||||
|
||||
this.app.addressBook.loadMailboxes(pubkey, COMMON_CONTACT_RELAYS, true);
|
||||
|
||||
this.log(`Watching ${pubkey} inboxes for mail`);
|
||||
const mailboxes = await this.app.addressBook.loadMailboxes(pubkey);
|
||||
if (!mailboxes) {
|
||||
this.log(`Failed to get ${pubkey} mailboxes`);
|
||||
return;
|
||||
}
|
||||
const subscription = queryStore
|
||||
.createQuery(MailboxesQuery, pubkey)
|
||||
.pipe(
|
||||
// ignore undefined
|
||||
filter((m) => !!m),
|
||||
// start a new subscription for each update
|
||||
mergeMap((mailboxes) => {
|
||||
const relays = arrayFallback(mailboxes.inboxes, this.explicitRelays);
|
||||
this.log(`Subscribing to ${relays.length} relays for ${pubkey}`);
|
||||
|
||||
const relays = arrayFallback(getInboxes(mailboxes), this.explicitRelays);
|
||||
const subscriptions = new Map<string, Subscription>();
|
||||
const req = createRxForwardReq();
|
||||
const sub = rxNostr.use(req, { on: { relays } }).pipe(
|
||||
filter((packet) => this.app.eventStore.addEvent(packet.event)),
|
||||
// also pass to event store
|
||||
tap((packet) => eventStore.add(packet.event, packet.from)),
|
||||
// log how many events where found every 10s
|
||||
bufferAudit(10_000, (events) => {
|
||||
if (events.length > 0) this.log(`Found ${events.length} new events for ${pubkey}`);
|
||||
}),
|
||||
);
|
||||
|
||||
for (const url of relays) {
|
||||
const subscribe = async () => {
|
||||
const relay = await this.app.pool.ensureRelay(url);
|
||||
const sub = relay.subscribe([{ kinds: [kinds.EncryptedDirectMessage], "#p": [pubkey] }], {
|
||||
onevent: (event) => {
|
||||
this.app.eventStore.addEvent(event);
|
||||
},
|
||||
onclose: () => {
|
||||
// reconnect if we are still watching this pubkey
|
||||
if (this.watching.has(pubkey)) {
|
||||
this.log(`Reconnecting to ${relay.url} for ${pubkey} inbox DMs`);
|
||||
setTimeout(() => subscribe(), 30_000);
|
||||
}
|
||||
},
|
||||
});
|
||||
req.emit({ kinds: [kinds.EncryptedDirectMessage], "#p": [pubkey] });
|
||||
|
||||
subscriptions.set(relay.url, sub);
|
||||
};
|
||||
return sub;
|
||||
}),
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
subscribe();
|
||||
}
|
||||
this.watching.set(pubkey, subscriptions);
|
||||
this.watching.set(pubkey, subscription);
|
||||
}
|
||||
stopWatchInbox(pubkey: string) {
|
||||
const subs = this.watching.get(pubkey);
|
||||
if (subs) {
|
||||
const sub = this.watching.get(pubkey);
|
||||
if (sub) {
|
||||
sub.unsubscribe();
|
||||
this.watching.delete(pubkey);
|
||||
for (const [_, sub] of subs) {
|
||||
sub.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
subscriptions = new Map<string, SubCloser>();
|
||||
openConversations = new Map<string, Subscription>();
|
||||
async openConversation(a: string, b: string) {
|
||||
const key = this.getConversationKey(a, b);
|
||||
|
||||
if (this.subscriptions.has(key)) return;
|
||||
if (this.openConversations.has(key)) return;
|
||||
|
||||
const aMailboxes = await this.app.addressBook.loadMailboxes(a);
|
||||
const bMailboxes = await this.app.addressBook.loadMailboxes(b);
|
||||
|
||||
// If inboxes for either user cannot be determined, either because nip65
|
||||
// was not found, or nip65 had no listed read relays, fall back to explicit
|
||||
const aInboxes = aMailboxes ? arrayFallback(getInboxes(aMailboxes), this.explicitRelays) : this.explicitRelays;
|
||||
const bInboxes = bMailboxes ? arrayFallback(getInboxes(bMailboxes), this.explicitRelays) : this.explicitRelays;
|
||||
// was not found, or nip65 had no listed read relays, fallback to explicit relays
|
||||
const aInboxes = aMailboxes ? arrayFallback(aMailboxes.inboxes, this.explicitRelays) : this.explicitRelays;
|
||||
const bInboxes = bMailboxes ? arrayFallback(bMailboxes.inboxes, this.explicitRelays) : this.explicitRelays;
|
||||
|
||||
const relays = new Set([...aInboxes, ...bInboxes]);
|
||||
|
||||
let events = 0;
|
||||
const sub = this.app.pool.subscribeMany(
|
||||
Array.from(relays),
|
||||
[{ kinds: [kinds.EncryptedDirectMessage], authors: [a, b], "#p": [a, b] }],
|
||||
{
|
||||
onevent: (event) => {
|
||||
events += +this.app.eventStore.addEvent(event);
|
||||
},
|
||||
oneose: () => {
|
||||
if (events) this.log(`Found ${events} new messages`);
|
||||
},
|
||||
},
|
||||
);
|
||||
const req = createRxForwardReq();
|
||||
const sub = rxNostr
|
||||
.use(req)
|
||||
.pipe(filter((packet) => this.app.eventStore.addEvent(packet.event)))
|
||||
.subscribe();
|
||||
|
||||
req.emit([{ kinds: [kinds.EncryptedDirectMessage], authors: [a, b], "#p": [a, b] }]);
|
||||
|
||||
this.log(`Opened conversation ${key} on ${relays.size} relays`);
|
||||
this.subscriptions.set(key, sub);
|
||||
this.openConversations.set(key, sub);
|
||||
this.emit("open", a, b);
|
||||
}
|
||||
|
||||
closeConversation(a: string, b: string) {
|
||||
const key = this.getConversationKey(a, b);
|
||||
|
||||
const sub = this.subscriptions.get(key);
|
||||
const sub = this.openConversations.get(key);
|
||||
if (sub) {
|
||||
sub.close();
|
||||
this.subscriptions.delete(key);
|
||||
sub.unsubscribe();
|
||||
this.openConversations.delete(key);
|
||||
this.emit("close", a, b);
|
||||
}
|
||||
}
|
||||
|
||||
[Symbol.dispose]() {
|
||||
for (const [_, sub] of this.watching) sub.unsubscribe();
|
||||
for (const [_, sub] of this.openConversations) sub.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { NotificationChannel, WebPushNotification } from "@satellite-earth/core/types/control-api/notifications.js";
|
||||
import { getDMRecipient, getDMSender, getUserDisplayName, parseKind0Event } from "@satellite-earth/core/helpers/nostr";
|
||||
import { getDMRecipient, getDMSender, getUserDisplayName } from "@satellite-earth/core/helpers/nostr";
|
||||
import { NostrEvent, kinds } from "nostr-tools";
|
||||
import { npubEncode } from "nostr-tools/nip19";
|
||||
import { getDisplayName, unixNow } from "applesauce-core/helpers";
|
||||
import EventEmitter from "events";
|
||||
import webPush from "web-push";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
import { logger } from "../../logger.js";
|
||||
import App from "../../app/index.js";
|
||||
import stateManager from "../../services/state.js";
|
||||
import config from "../../services/config.js";
|
||||
|
||||
export type NotificationsManagerState = {
|
||||
channels: NotificationChannel[];
|
||||
@@ -22,7 +24,7 @@ type EventMap = {
|
||||
export default class NotificationsManager extends EventEmitter<EventMap> {
|
||||
log = logger.extend("Notifications");
|
||||
app: App;
|
||||
lastRead: number = dayjs().unix();
|
||||
lastRead: number = unixNow();
|
||||
|
||||
webPushKeys: webPush.VapidKeys = webPush.generateVAPIDKeys();
|
||||
|
||||
@@ -39,7 +41,7 @@ export default class NotificationsManager extends EventEmitter<EventMap> {
|
||||
|
||||
async setup() {
|
||||
this.state = (
|
||||
await this.app.state.getMutableState<NotificationsManagerState>("notification-manager", { channels: [] })
|
||||
await stateManager.getMutableState<NotificationsManagerState>("notification-manager", { channels: [] })
|
||||
).proxy;
|
||||
}
|
||||
|
||||
@@ -71,7 +73,7 @@ export default class NotificationsManager extends EventEmitter<EventMap> {
|
||||
/** Whether a notification should be sent */
|
||||
shouldNotify(event: NostrEvent) {
|
||||
if (event.kind !== kinds.EncryptedDirectMessage) return;
|
||||
if (getDMRecipient(event) !== this.app.config.data.owner) return;
|
||||
if (getDMRecipient(event) !== config.data.owner) return;
|
||||
|
||||
if (event.created_at > this.lastRead) return true;
|
||||
}
|
||||
@@ -82,9 +84,8 @@ export default class NotificationsManager extends EventEmitter<EventMap> {
|
||||
switch (event.kind) {
|
||||
case kinds.EncryptedDirectMessage:
|
||||
const sender = getDMSender(event);
|
||||
const senderProfileEvent = await this.app.profileBook.loadProfile(sender);
|
||||
const senderProfile = senderProfileEvent ? parseKind0Event(senderProfileEvent) : undefined;
|
||||
const senderName = getUserDisplayName(senderProfile, sender);
|
||||
const senderProfile = await this.app.profileBook.loadProfile(sender);
|
||||
const senderName = senderProfile ? (getDisplayName(senderProfile) ?? "Unknown") : "Unknown";
|
||||
|
||||
return {
|
||||
kind: event.kind,
|
||||
|
||||
@@ -1,40 +1,35 @@
|
||||
import { NostrEvent, kinds } from "nostr-tools";
|
||||
import _throttle from "lodash.throttle";
|
||||
import { tap } from "rxjs";
|
||||
import { kinds } from "nostr-tools";
|
||||
import { getObservableValue } from "applesauce-core/observable";
|
||||
import { ProfileQuery } from "applesauce-core/queries";
|
||||
|
||||
import { COMMON_CONTACT_RELAYS } from "../env.js";
|
||||
import { logger } from "../logger.js";
|
||||
import App from "../app/index.js";
|
||||
import PubkeyBatchLoader from "./pubkey-batch-loader.js";
|
||||
import { replaceableLoader } from "../services/loaders.js";
|
||||
import { eventStore, queryStore } from "../services/stores.js";
|
||||
import { simpleTimeout } from "../operators/simple-timeout.js";
|
||||
import { arrayFallback } from "../helpers/array.js";
|
||||
|
||||
const DEFAULT_REQUEST_TIMEOUT = 10_000;
|
||||
|
||||
/** loads kind 0 metadata for pubkeys */
|
||||
export default class ProfileBook {
|
||||
log = logger.extend("ProfileBook");
|
||||
app: App;
|
||||
loader: PubkeyBatchLoader;
|
||||
extraRelays = COMMON_CONTACT_RELAYS;
|
||||
|
||||
constructor(app: App) {
|
||||
this.app = app;
|
||||
|
||||
this.loader = new PubkeyBatchLoader(kinds.Metadata, this.app.pool, (pubkey) => {
|
||||
return this.app.eventStore.getEventsForFilters([{ kinds: [kinds.Metadata], authors: [pubkey] }])?.[0];
|
||||
});
|
||||
|
||||
this.loader.on("event", (event) => this.app.eventStore.addEvent(event));
|
||||
this.loader.on("batch", (found, failed) => {
|
||||
this.log(`Found ${found}, failed ${failed}, pending ${this.loader.queue}`);
|
||||
});
|
||||
}
|
||||
|
||||
getProfile(pubkey: string) {
|
||||
return this.loader.getEvent(pubkey);
|
||||
return eventStore.getReplaceable(kinds.Metadata, pubkey);
|
||||
}
|
||||
|
||||
handleEvent(event: NostrEvent) {
|
||||
this.loader.handleEvent(event);
|
||||
}
|
||||
async loadProfile(pubkey: string, relays?: string[], force?: boolean) {
|
||||
relays = arrayFallback(relays, COMMON_CONTACT_RELAYS);
|
||||
this.log(`Requesting profile from ${relays.length} relays for ${pubkey}`);
|
||||
replaceableLoader.next({ kind: kinds.Metadata, pubkey, relays, force });
|
||||
|
||||
async loadProfile(pubkey: string, relays: string[] = []) {
|
||||
return this.loader.getOrLoadEvent(pubkey, relays);
|
||||
return getObservableValue(
|
||||
queryStore.createQuery(ProfileQuery, pubkey).pipe(
|
||||
simpleTimeout(DEFAULT_REQUEST_TIMEOUT, `Failed to load profile for ${pubkey}`),
|
||||
tap((p) => p && this.log(`Found profile for ${pubkey}`, p)),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,36 +1,13 @@
|
||||
import EventEmitter from "events";
|
||||
import { NostrEvent, SimplePool, Filter } from "nostr-tools";
|
||||
import { NostrEvent, SimplePool } from "nostr-tools";
|
||||
import SuperMap from "@satellite-earth/core/helpers/super-map.js";
|
||||
import { AbstractRelay, Subscription, SubscriptionParams } from "nostr-tools/abstract-relay";
|
||||
import { getPubkeysFromList } from "@satellite-earth/core/helpers/nostr/lists.js";
|
||||
import { getInboxes, getOutboxes } from "@satellite-earth/core/helpers/nostr/mailboxes.js";
|
||||
import { getRelaysFromContactList } from "@satellite-earth/core/helpers/nostr/contacts.js";
|
||||
import { Subscription } from "nostr-tools/abstract-relay";
|
||||
import { getRelaysFromContactsEvent } from "applesauce-core/helpers";
|
||||
|
||||
import { BOOTSTRAP_RELAYS } from "../../env.js";
|
||||
import { BOOTSTRAP_RELAYS, COMMON_CONTACT_RELAYS } from "../../env.js";
|
||||
import { logger } from "../../logger.js";
|
||||
import App from "../../app/index.js";
|
||||
|
||||
/** creates a new subscription and waits for it to get an event or close */
|
||||
function asyncSubscription(relay: AbstractRelay, filters: Filter[], opts: SubscriptionParams) {
|
||||
let resolved = false;
|
||||
|
||||
return new Promise<Subscription>((res, rej) => {
|
||||
const sub = relay.subscribe(filters, {
|
||||
onevent: (event) => {
|
||||
if (!resolved) res(sub);
|
||||
opts.onevent?.(event);
|
||||
},
|
||||
oneose: () => {
|
||||
if (!resolved) res(sub);
|
||||
opts.oneose?.();
|
||||
},
|
||||
onclose: (reason) => {
|
||||
if (!resolved) rej(new Error(reason));
|
||||
opts.onclose?.(reason);
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
import { arrayFallback } from "../../helpers/array.js";
|
||||
|
||||
type EventMap = {
|
||||
started: [Receiver];
|
||||
@@ -83,24 +60,26 @@ export default class Receiver extends EventEmitter<EventMap> {
|
||||
const owner = this.app.config.data.owner;
|
||||
if (!owner) throw new Error("Missing owner");
|
||||
|
||||
const ownerMailboxes = await this.app.addressBook.loadMailboxes(owner);
|
||||
const ownerInboxes = getInboxes(ownerMailboxes);
|
||||
const ownerOutboxes = getOutboxes(ownerMailboxes);
|
||||
const commonMailboxesRelays = [...BOOTSTRAP_RELAYS, ...COMMON_CONTACT_RELAYS];
|
||||
|
||||
const ownerMailboxes = await this.app.addressBook.loadMailboxes(owner, commonMailboxesRelays, true);
|
||||
|
||||
this.log("Searching for owner kind:3 contacts");
|
||||
const contacts = await this.app.contactBook.loadContacts(owner);
|
||||
const contacts = await this.app.contactBook.loadContacts(
|
||||
owner,
|
||||
arrayFallback(ownerMailboxes?.outboxes, BOOTSTRAP_RELAYS),
|
||||
true,
|
||||
);
|
||||
if (!contacts) throw new Error("Cant find contacts");
|
||||
|
||||
this.pubkeyRelays.clear();
|
||||
this.relayPubkeys.clear();
|
||||
|
||||
// add the owners details
|
||||
this.pubkeyRelays.set(owner, new Set(ownerOutboxes));
|
||||
for (const url of ownerOutboxes) this.relayPubkeys.get(url).add(owner);
|
||||
this.pubkeyRelays.set(owner, new Set(ownerMailboxes?.outboxes));
|
||||
if (ownerMailboxes?.outboxes) for (const url of ownerMailboxes?.outboxes) this.relayPubkeys.get(url).add(owner);
|
||||
|
||||
const people = getPubkeysFromList(contacts);
|
||||
|
||||
this.log(`Found ${people.length} contacts`);
|
||||
this.log(`Found ${contacts.length} contacts`);
|
||||
|
||||
let usersWithMailboxes = 0;
|
||||
let usersWithContactRelays = 0;
|
||||
@@ -108,20 +87,28 @@ export default class Receiver extends EventEmitter<EventMap> {
|
||||
|
||||
// fetch all addresses in parallel
|
||||
await Promise.all(
|
||||
people.map(async (person) => {
|
||||
const mailboxes = await this.app.addressBook.loadMailboxes(person.pubkey, ownerInboxes ?? BOOTSTRAP_RELAYS);
|
||||
contacts.map(async (person) => {
|
||||
const mailboxes = await this.app.addressBook.loadMailboxes(
|
||||
person.pubkey,
|
||||
arrayFallback(ownerMailboxes?.inboxes, commonMailboxesRelays),
|
||||
);
|
||||
|
||||
let relays = getOutboxes(mailboxes);
|
||||
let relays = mailboxes?.outboxes ?? [];
|
||||
|
||||
// if the user does not have any mailboxes try to get the relays stored in the contact list
|
||||
if (relays.length === 0) {
|
||||
this.log(`Failed to find mailboxes for ${person.pubkey}`);
|
||||
const contacts = await this.app.contactBook.loadContacts(person.pubkey, ownerInboxes ?? BOOTSTRAP_RELAYS);
|
||||
const contacts = await this.app.contactBook.loadContactsEvent(
|
||||
person.pubkey,
|
||||
arrayFallback(ownerMailboxes?.inboxes, BOOTSTRAP_RELAYS),
|
||||
);
|
||||
|
||||
if (contacts && contacts.content.startsWith("{")) {
|
||||
const parsed = getRelaysFromContactList(contacts);
|
||||
const parsed = getRelaysFromContactsEvent(contacts);
|
||||
if (parsed) {
|
||||
relays = parsed.filter((r) => r.write).map((r) => r.url);
|
||||
relays = Array.from(parsed.entries())
|
||||
.filter(([r, mode]) => mode === "all" || mode === "outbox")
|
||||
.map(([r]) => r);
|
||||
usersWithContactRelays++;
|
||||
} else {
|
||||
relays = BOOTSTRAP_RELAYS;
|
||||
|
||||
@@ -5,7 +5,6 @@ import { Deferred, createDefer } from "applesauce-core/promise";
|
||||
|
||||
import App from "../../app/index.js";
|
||||
import { logger } from "../../logger.js";
|
||||
import { getPubkeysFromList } from "@satellite-earth/core/helpers/nostr/lists.js";
|
||||
import PubkeyScrapper from "./pubkey-scrapper.js";
|
||||
|
||||
const MAX_TASKS = 10;
|
||||
@@ -49,7 +48,7 @@ export default class Scrapper extends EventEmitter<EventMap> {
|
||||
|
||||
if (!contacts) throw new Error("Missing contact list");
|
||||
|
||||
return { contacts: getPubkeysFromList(contacts), mailboxes };
|
||||
return { contacts, mailboxes };
|
||||
}
|
||||
|
||||
private async scrapeOwner() {
|
||||
@@ -104,7 +103,7 @@ export default class Scrapper extends EventEmitter<EventMap> {
|
||||
// check running again since this is resuming
|
||||
if (!this.running) return;
|
||||
|
||||
const promise = this.scrapeForPubkey(person.pubkey, person.relay);
|
||||
const promise = this.scrapeForPubkey(person.pubkey, person.relays?.[0]);
|
||||
|
||||
// add it to the tasks array
|
||||
this.tasks.add(promise);
|
||||
|
||||
@@ -3,7 +3,6 @@ import { NostrEvent } from "nostr-tools";
|
||||
import { EventEmitter } from "events";
|
||||
import { Debugger } from "debug";
|
||||
|
||||
import { getOutboxes } from "@satellite-earth/core/helpers/nostr/mailboxes.js";
|
||||
import PubkeyRelayScrapper, { PubkeyRelayScrapperState } from "./pubkey-relay-scrapper.js";
|
||||
import { logger } from "../../logger.js";
|
||||
|
||||
@@ -39,9 +38,7 @@ export default class PubkeyScrapper extends EventEmitter<EventMap> {
|
||||
async loadNext() {
|
||||
const { mailboxes } = await this.ensureData();
|
||||
|
||||
const outboxes = getOutboxes(mailboxes);
|
||||
|
||||
const relays = [...outboxes, ...this.additionalRelays];
|
||||
const relays = [...(mailboxes?.outboxes ?? []), ...this.additionalRelays];
|
||||
const scrappers: PubkeyRelayScrapper[] = [];
|
||||
for (const url of relays) {
|
||||
if (this.failed.has(url)) continue;
|
||||
|
||||
5
src/operators/simple-timeout.ts
Normal file
5
src/operators/simple-timeout.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { MonoTypeOperatorFunction, throwError, timeout } from "rxjs";
|
||||
|
||||
export function simpleTimeout<T extends unknown>(first: number, message: string): MonoTypeOperatorFunction<T> {
|
||||
return timeout({ first, with: () => throwError(() => new Error(message)) });
|
||||
}
|
||||
10
src/services/config.ts
Normal file
10
src/services/config.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import path from "node:path";
|
||||
|
||||
import ConfigManager from "../modules/config-manager.js";
|
||||
import { DATA_PATH } from "../env.js";
|
||||
|
||||
const config = new ConfigManager(path.join(DATA_PATH, "node.json"));
|
||||
|
||||
config.read();
|
||||
|
||||
export default config;
|
||||
7
src/services/database.ts
Normal file
7
src/services/database.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import LocalDatabase from "../app/database.js";
|
||||
import { DATA_PATH } from "../env.js";
|
||||
|
||||
// setup database
|
||||
const database = new LocalDatabase({ directory: DATA_PATH });
|
||||
|
||||
export default database;
|
||||
7
src/services/event-cache.ts
Normal file
7
src/services/event-cache.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { SQLiteEventStore } from "@satellite-earth/core";
|
||||
import database from "./database.js";
|
||||
|
||||
const eventCache = new SQLiteEventStore(database.db);
|
||||
await eventCache.setup();
|
||||
|
||||
export default eventCache;
|
||||
23
src/services/loaders.ts
Normal file
23
src/services/loaders.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { from, tap } from "rxjs";
|
||||
import { Filter } from "nostr-tools";
|
||||
import { isFromCache, markFromCache } from "applesauce-core/helpers";
|
||||
import { ReplaceableLoader } from "applesauce-loaders/loaders";
|
||||
|
||||
import { rxNostr } from "./rx-nostr.js";
|
||||
import eventCache from "./event-cache.js";
|
||||
import { eventStore } from "./stores.js";
|
||||
|
||||
function cacheRequest(filters: Filter[]) {
|
||||
const events = eventCache.getEventsForFilters(filters);
|
||||
return from(events).pipe(tap(markFromCache));
|
||||
}
|
||||
|
||||
export const replaceableLoader = new ReplaceableLoader(rxNostr, { cacheRequest });
|
||||
|
||||
replaceableLoader.subscribe((packet) => {
|
||||
// add it to event store
|
||||
const event = eventStore.add(packet.event, packet.from);
|
||||
|
||||
// save it to the cache if its new
|
||||
if (!isFromCache(event)) eventCache.addEvent(event);
|
||||
});
|
||||
7
src/services/log-store.ts
Normal file
7
src/services/log-store.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import LogStore from "../modules/log-store/log-store.js";
|
||||
import database from "./database.js";
|
||||
|
||||
const logStore = new LogStore(database.db);
|
||||
await logStore.setup();
|
||||
|
||||
export default logStore;
|
||||
36
src/services/rx-nostr.ts
Normal file
36
src/services/rx-nostr.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { ConnectionState, createRxNostr } from "rx-nostr";
|
||||
import { unixNow } from "applesauce-core/helpers";
|
||||
import { verifyEvent } from "nostr-tools/wasm";
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
import { nanoid } from "nanoid";
|
||||
|
||||
// import { logger } from "../logger.js";
|
||||
// const log = logger.extend("rx-nostr");
|
||||
|
||||
export const rxNostr = createRxNostr({
|
||||
verifier: async (event) => {
|
||||
try {
|
||||
return verifyEvent(event);
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// keep track of all relay connection states
|
||||
export const connections$ = new BehaviorSubject<Record<string, ConnectionState>>({});
|
||||
rxNostr.createConnectionStateObservable().subscribe((packet) => {
|
||||
const url = new URL(packet.from).toString();
|
||||
connections$.next({ ...connections$.value, [url]: packet.state });
|
||||
});
|
||||
|
||||
// capture all notices sent from relays
|
||||
export const notices$ = new BehaviorSubject<{ id: string; from: string; message: string; timestamp: number }[]>([]);
|
||||
rxNostr.createAllMessageObservable().subscribe((packet) => {
|
||||
if (packet.type === "NOTICE") {
|
||||
const from = new URL(packet.from).toString();
|
||||
|
||||
const notice = { id: nanoid(), from, message: packet.notice, timestamp: unixNow() };
|
||||
notices$.next([...notices$.value, notice]);
|
||||
}
|
||||
});
|
||||
9
src/services/secrets.ts
Normal file
9
src/services/secrets.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import path from "node:path";
|
||||
|
||||
import SecretsManager from "../modules/secrets-manager.js";
|
||||
import { DATA_PATH } from "../env.js";
|
||||
|
||||
const secrets = new SecretsManager(path.join(DATA_PATH, "secrets.json"));
|
||||
secrets.read();
|
||||
|
||||
export default secrets;
|
||||
7
src/services/state.ts
Normal file
7
src/services/state.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import ApplicationStateManager from "../modules/state/application-state-manager.js";
|
||||
import database from "./database.js";
|
||||
|
||||
const stateManager = new ApplicationStateManager(database.db);
|
||||
await stateManager.setup();
|
||||
|
||||
export default stateManager;
|
||||
20
src/services/stores.ts
Normal file
20
src/services/stores.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { EventStore, QueryStore } from "applesauce-core";
|
||||
import { IS_DEV } from "../env.js";
|
||||
import { rxNostr } from "./rx-nostr.js";
|
||||
|
||||
export const eventStore = new EventStore();
|
||||
export const queryStore = new QueryStore(eventStore);
|
||||
|
||||
// add all new events to event store
|
||||
rxNostr.createAllEventObservable().subscribe((packet) => {
|
||||
eventStore.add(packet.event, packet.from);
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
eventStore.database.prune();
|
||||
}, 10 * 60_000);
|
||||
|
||||
if (IS_DEV) {
|
||||
// @ts-ignore
|
||||
global.eventStore = eventStore;
|
||||
}
|
||||
Reference in New Issue
Block a user