remove legacy reports

This commit is contained in:
hzrd149
2025-03-27 10:24:03 +00:00
parent 1a24f99b32
commit 4eefa1adc4
34 changed files with 2948 additions and 2191 deletions

View File

@@ -11,7 +11,7 @@ PORT=3000
# the address to the i2p SOCKS5 proxy to enable connections to .i2p addresses # the address to the i2p SOCKS5 proxy to enable connections to .i2p addresses
# I2P_PROXY="127.0.0.1:4447" # I2P_PROXY="127.0.0.1:4447"
# I@P proxy type, SOCKS5 or HTTP # I2P proxy type, SOCKS5 or HTTP
# I2P_PROXY_TYPE="SOCKS5" # I2P_PROXY_TYPE="SOCKS5"
# sets a hardcoded tor address # sets a hardcoded tor address

View File

@@ -1,3 +1,3 @@
# bakery # noStrudel Bakery
A relay backend for noStrudel A backend for [noStrudel](https://github.com/hzrd149/noStrudel)

View File

@@ -11,6 +11,7 @@
"start": "node .", "start": "node .",
"dev": "nodemon --loader @swc-node/register/esm src/index.ts", "dev": "nodemon --loader @swc-node/register/esm src/index.ts",
"build": "tsc", "build": "tsc",
"test": "vitest run",
"format": "prettier -w ." "format": "prettier -w ."
}, },
"files": [ "files": [
@@ -75,7 +76,8 @@
"@types/ws": "^8.5.14", "@types/ws": "^8.5.14",
"nodemon": "^3.1.9", "nodemon": "^3.1.9",
"prettier": "^3.5.1", "prettier": "^3.5.1",
"typescript": "^5.7.3" "typescript": "^5.7.3",
"vitest": "^3.0.9"
}, },
"nodemonConfig": { "nodemonConfig": {
"ignore": [ "ignore": [

3949
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -29,7 +29,6 @@ import ProfileBook from "../modules/profile-book.js";
import ContactBook from "../modules/contact-book.js"; import ContactBook from "../modules/contact-book.js";
import CautiousPool from "../modules/cautious-pool.js"; import CautiousPool from "../modules/cautious-pool.js";
import RemoteAuthActions from "../modules/control/remote-auth-actions.js"; import RemoteAuthActions from "../modules/control/remote-auth-actions.js";
import ReportActions from "../modules/control/report-actions.js";
import LogStore from "../modules/log-store/log-store.js"; import LogStore from "../modules/log-store/log-store.js";
import DecryptionCache from "../modules/decryption-cache/decryption-cache.js"; import DecryptionCache from "../modules/decryption-cache/decryption-cache.js";
import DecryptionCacheActions from "../modules/control/decryption-cache.js"; import DecryptionCacheActions from "../modules/control/decryption-cache.js";
@@ -82,7 +81,6 @@ export default class App extends EventEmitter<EventMap> {
receiver: Receiver; receiver: Receiver;
scrapper: Scrapper; scrapper: Scrapper;
control: ControlApi; control: ControlApi;
reports: ReportActions;
pool: CautiousPool; pool: CautiousPool;
addressBook: AddressBook; addressBook: AddressBook;
profileBook: ProfileBook; profileBook: ProfileBook;
@@ -212,10 +210,6 @@ export default class App extends EventEmitter<EventMap> {
this.control.registerHandler(new LogsActions(this)); this.control.registerHandler(new LogsActions(this));
// reports
this.reports = new ReportActions(this);
this.control.registerHandler(this.reports);
// connect control api to websocket server // connect control api to websocket server
this.control.attachToServer(this.wss); this.control.attachToServer(this.wss);
@@ -412,7 +406,6 @@ export default class App extends EventEmitter<EventMap> {
this.scrapper.stop(); this.scrapper.stop();
this.receiver.stop(); this.receiver.stop();
await this.state.saveAll(); await this.state.saveAll();
this.reports.cleanup();
this.relay.stop(); this.relay.stop();
this.database.destroy(); this.database.destroy();
this.receiver.destroy(); this.receiver.destroy();

View File

@@ -1,21 +1,21 @@
import { NostrEvent } from 'nostr-tools'; import { NostrEvent } from "nostr-tools";
import { getTagValue } from './event.js'; import { getTagValue } from "./event.js";
import { COMMUNITY_CHANNEL_KIND } from './kinds.js'; import { COMMUNITY_CHANNEL_KIND } from "./kinds.js";
export const CHANNEL_KIND = COMMUNITY_CHANNEL_KIND; export const CHANNEL_KIND = COMMUNITY_CHANNEL_KIND;
export function getChannelId(channel: NostrEvent) { export function getChannelId(channel: NostrEvent) {
const id = getTagValue(channel, 'd'); const id = getTagValue(channel, "d");
if (!id) throw new Error('Channel missing id'); if (!id) throw new Error("Channel missing id");
return id; return id;
} }
export function getChannelName(channel: NostrEvent) { export function getChannelName(channel: NostrEvent) {
return getTagValue(channel, 'name'); return getTagValue(channel, "name");
} }
export function getChannelAbout(channel: NostrEvent) { export function getChannelAbout(channel: NostrEvent) {
return getTagValue(channel, 'about'); return getTagValue(channel, "about");
} }
export function getChannelPicture(channel: NostrEvent) { export function getChannelPicture(channel: NostrEvent) {
return getTagValue(channel, 'picture'); return getTagValue(channel, "picture");
} }

View File

@@ -1,18 +1,18 @@
import { NostrEvent } from 'nostr-tools'; import { NostrEvent } from "nostr-tools";
const tagValue = (e: NostrEvent, k: string) => e.tags.find((t) => t[0] === k)?.[1]; const tagValue = (e: NostrEvent, k: string) => e.tags.find((t) => t[0] === k)?.[1];
export function getCommunityName(definition: NostrEvent) { export function getCommunityName(definition: NostrEvent) {
return tagValue(definition, 'name'); return tagValue(definition, "name");
} }
export function getCommunityBanner(definition: NostrEvent) { export function getCommunityBanner(definition: NostrEvent) {
return tagValue(definition, 'banner'); return tagValue(definition, "banner");
} }
export function getCommunityImage(definition: NostrEvent) { export function getCommunityImage(definition: NostrEvent) {
return tagValue(definition, 'image'); return tagValue(definition, "image");
} }
export function getCommunityRelay(definition: NostrEvent) { export function getCommunityRelay(definition: NostrEvent) {
return tagValue(definition, 'r'); return tagValue(definition, "r");
} }
export function getCommunityCDN(definition: NostrEvent) { export function getCommunityCDN(definition: NostrEvent) {
return tagValue(definition, 'cdn'); return tagValue(definition, "cdn");
} }

View File

@@ -1,20 +1,20 @@
import { NostrEvent } from 'nostr-tools'; import { NostrEvent } from "nostr-tools";
import { safeRelayUrl } from './relays.js'; import { safeRelayUrl } from "./relays.js";
export function getRelaysFromContactList(event: NostrEvent) { export function getRelaysFromContactList(event: NostrEvent) {
try { try {
const json = JSON.parse(event.content) as Record<string, { write?: boolean; read?: boolean }>; const json = JSON.parse(event.content) as Record<string, { write?: boolean; read?: boolean }>;
const relays: { url: string; write?: boolean; read?: boolean }[] = []; const relays: { url: string; write?: boolean; read?: boolean }[] = [];
for (const [url, value] of Object.entries(json)) { for (const [url, value] of Object.entries(json)) {
const safeUrl = safeRelayUrl(url); const safeUrl = safeRelayUrl(url);
if (safeUrl) { if (safeUrl) {
relays.push({ url: safeUrl, write: value.write, read: value.read }); relays.push({ url: safeUrl, write: value.write, read: value.read });
} }
} }
return relays; return relays;
} catch (error) { } catch (error) {
return null; return null;
} }
} }

View File

@@ -1,10 +1,10 @@
import { NostrEvent } from 'nostr-tools'; import { NostrEvent } from "nostr-tools";
export function getDMSender(event: NostrEvent) { export function getDMSender(event: NostrEvent) {
return event.pubkey; return event.pubkey;
} }
export function getDMRecipient(event: NostrEvent) { export function getDMRecipient(event: NostrEvent) {
const pubkey = event.tags.find((t) => t[0] === 'p')?.[1]; const pubkey = event.tags.find((t) => t[0] === "p")?.[1];
if (!pubkey) throw new Error('Missing recipient pubkey'); if (!pubkey) throw new Error("Missing recipient pubkey");
return pubkey; return pubkey;
} }

View File

@@ -1,32 +1,32 @@
import { NostrEvent, kinds, nip19 } from 'nostr-tools'; import { NostrEvent, kinds, nip19 } from "nostr-tools";
export function isReplaceable(kind: number) { export function isReplaceable(kind: number) {
return kinds.isReplaceableKind(kind) || kinds.isParameterizedReplaceableKind(kind); return kinds.isReplaceableKind(kind) || kinds.isParameterizedReplaceableKind(kind);
} }
export function sortByDate(a: NostrEvent, b: NostrEvent) { export function sortByDate(a: NostrEvent, b: NostrEvent) {
return b.created_at - a.created_at; return b.created_at - a.created_at;
} }
export function getEventCoordinate(event: NostrEvent) { export function getEventCoordinate(event: NostrEvent) {
const d = event.tags.find((t) => t[0] === 'd')?.[1]; const d = event.tags.find((t) => t[0] === "d")?.[1];
return d ? `${event.kind}:${event.pubkey}:${d}` : `${event.kind}:${event.pubkey}`; return d ? `${event.kind}:${event.pubkey}:${d}` : `${event.kind}:${event.pubkey}`;
} }
export function getTagValue(event: NostrEvent, tag: string) { export function getTagValue(event: NostrEvent, tag: string) {
return event.tags.find((t) => t[0] === tag)?.[1]; return event.tags.find((t) => t[0] === tag)?.[1];
} }
export function doesEventMatchCoordinate(event: NostrEvent, coordinate: string) { export function doesEventMatchCoordinate(event: NostrEvent, coordinate: string) {
const [kind, pubkey, d] = coordinate.split(':'); const [kind, pubkey, d] = coordinate.split(":");
if (!kind || !pubkey || !d) return false; if (!kind || !pubkey || !d) return false;
return ( return (
event.kind === parseInt(kind) && event.pubkey === event.pubkey && event.tags.find((t) => t[0] === 'd')?.[1] === d event.kind === parseInt(kind) && event.pubkey === event.pubkey && event.tags.find((t) => t[0] === "d")?.[1] === d
); );
} }
export type CustomAddressPointer = Omit<nip19.AddressPointer, 'identifier'> & { export type CustomAddressPointer = Omit<nip19.AddressPointer, "identifier"> & {
identifier?: string; identifier?: string;
}; };
export function parseCoordinate(a: string): CustomAddressPointer | null; export function parseCoordinate(a: string): CustomAddressPointer | null;
@@ -37,27 +37,27 @@ export function parseCoordinate(a: string, requireD: true, silent: false): nip19
export function parseCoordinate(a: string, requireD: true, silent: true): nip19.AddressPointer | null; export function parseCoordinate(a: string, requireD: true, silent: true): nip19.AddressPointer | null;
export function parseCoordinate(a: string, requireD: false, silent: true): CustomAddressPointer | null; export function parseCoordinate(a: string, requireD: false, silent: true): CustomAddressPointer | null;
export function parseCoordinate(a: string, requireD = false, silent = true): CustomAddressPointer | null { export function parseCoordinate(a: string, requireD = false, silent = true): CustomAddressPointer | null {
const parts = a.split(':') as (string | undefined)[]; const parts = a.split(":") as (string | undefined)[];
const kind = parts[0] && parseInt(parts[0]); const kind = parts[0] && parseInt(parts[0]);
const pubkey = parts[1]; const pubkey = parts[1];
const d = parts[2]; const d = parts[2];
if (!kind) { if (!kind) {
if (silent) return null; if (silent) return null;
else throw new Error('Missing kind'); else throw new Error("Missing kind");
} }
if (!pubkey) { if (!pubkey) {
if (silent) return null; if (silent) return null;
else throw new Error('Missing pubkey'); else throw new Error("Missing pubkey");
} }
if (requireD && d === undefined) { if (requireD && d === undefined) {
if (silent) return null; if (silent) return null;
else throw new Error('Missing identifier'); else throw new Error("Missing identifier");
} }
return { return {
kind, kind,
pubkey, pubkey,
identifier: d, identifier: d,
}; };
} }

View File

@@ -1,7 +1,7 @@
export * from './channel.js'; export * from "./channel.js";
export * from './communities.js'; export * from "./communities.js";
export * from './dms.js'; export * from "./dms.js";
export * from './event.js'; export * from "./event.js";
export * from './kinds.js'; export * from "./kinds.js";
export * from './profile.js'; export * from "./profile.js";
export * from './nip19.js'; export * from "./nip19.js";

View File

@@ -1,104 +1,104 @@
import { EventTemplate, nip19, NostrEvent } from 'nostr-tools'; import { EventTemplate, nip19, NostrEvent } from "nostr-tools";
import { parseCoordinate } from './event.js'; import { parseCoordinate } from "./event.js";
function unixNow() { function unixNow() {
return Math.round(Date.now() / 1000); return Math.round(Date.now() / 1000);
} }
export function getPubkeysFromList(event: NostrEvent | EventTemplate) { export function getPubkeysFromList(event: NostrEvent | EventTemplate) {
return event.tags.filter((t) => t[0] === 'p' && t[1]).map((t) => ({ pubkey: t[1], relay: t[2], petname: t[3] })); return event.tags.filter((t) => t[0] === "p" && t[1]).map((t) => ({ pubkey: t[1], relay: t[2], petname: t[3] }));
} }
export function getEventPointersFromList(event: NostrEvent | EventTemplate): nip19.EventPointer[] { export function getEventPointersFromList(event: NostrEvent | EventTemplate): nip19.EventPointer[] {
return event.tags return event.tags
.filter((t) => t[0] === 'e' && t[1]) .filter((t) => t[0] === "e" && t[1])
.map((t) => (t[2] ? { id: t[1], relays: [t[2]] } : { id: t[1] })); .map((t) => (t[2] ? { id: t[1], relays: [t[2]] } : { id: t[1] }));
} }
export function getCoordinatesFromList(event: NostrEvent | EventTemplate) { export function getCoordinatesFromList(event: NostrEvent | EventTemplate) {
return event.tags.filter((t) => t[0] === 'a' && t[1] && t[2]).map((t) => ({ coordinate: t[1], relay: t[2] })); return event.tags.filter((t) => t[0] === "a" && t[1] && t[2]).map((t) => ({ coordinate: t[1], relay: t[2] }));
} }
export function getAddressPointersFromList(event: NostrEvent | EventTemplate): nip19.AddressPointer[] { export function getAddressPointersFromList(event: NostrEvent | EventTemplate): nip19.AddressPointer[] {
const pointers: nip19.AddressPointer[] = []; const pointers: nip19.AddressPointer[] = [];
for (const tag of event.tags) { for (const tag of event.tags) {
if (!tag[1]) continue; if (!tag[1]) continue;
const relay = tag[2]; const relay = tag[2];
const parsed = parseCoordinate(tag[1]); const parsed = parseCoordinate(tag[1]);
if (!parsed?.identifier) continue; if (!parsed?.identifier) continue;
pointers.push({ ...parsed, identifier: parsed?.identifier, relays: relay ? [relay] : undefined }); pointers.push({ ...parsed, identifier: parsed?.identifier, relays: relay ? [relay] : undefined });
} }
return pointers; return pointers;
} }
export function isPubkeyInList(list?: NostrEvent, pubkey?: string) { export function isPubkeyInList(list?: NostrEvent, pubkey?: string) {
if (!pubkey || !list) return false; if (!pubkey || !list) return false;
return list.tags.some((t) => t[0] === 'p' && t[1] === pubkey); return list.tags.some((t) => t[0] === "p" && t[1] === pubkey);
} }
export function listAddPerson( export function listAddPerson(
list: NostrEvent | EventTemplate, list: NostrEvent | EventTemplate,
pubkey: string, pubkey: string,
relay?: string, relay?: string,
petname?: string, petname?: string,
): EventTemplate { ): EventTemplate {
if (list.tags.some((t) => t[0] === 'p' && t[1] === pubkey)) throw new Error('Person already in list'); if (list.tags.some((t) => t[0] === "p" && t[1] === pubkey)) throw new Error("Person already in list");
const pTag = ['p', pubkey, relay ?? '', petname ?? '']; const pTag = ["p", pubkey, relay ?? "", petname ?? ""];
while (pTag[pTag.length - 1] === '') pTag.pop(); while (pTag[pTag.length - 1] === "") pTag.pop();
return { return {
created_at: unixNow(), created_at: unixNow(),
kind: list.kind, kind: list.kind,
content: list.content, content: list.content,
tags: [...list.tags, pTag], tags: [...list.tags, pTag],
}; };
} }
export function listRemovePerson(list: NostrEvent | EventTemplate, pubkey: string): EventTemplate { export function listRemovePerson(list: NostrEvent | EventTemplate, pubkey: string): EventTemplate {
return { return {
created_at: unixNow(), created_at: unixNow(),
kind: list.kind, kind: list.kind,
content: list.content, content: list.content,
tags: list.tags.filter((t) => !(t[0] === 'p' && t[1] === pubkey)), tags: list.tags.filter((t) => !(t[0] === "p" && t[1] === pubkey)),
}; };
} }
export function listAddEvent(list: NostrEvent | EventTemplate, event: string, relay?: string): EventTemplate { export function listAddEvent(list: NostrEvent | EventTemplate, event: string, relay?: string): EventTemplate {
if (list.tags.some((t) => t[0] === 'e' && t[1] === event)) throw new Error('Event already in list'); if (list.tags.some((t) => t[0] === "e" && t[1] === event)) throw new Error("Event already in list");
return { return {
created_at: unixNow(), created_at: unixNow(),
kind: list.kind, kind: list.kind,
content: list.content, content: list.content,
tags: [...list.tags, relay ? ['e', event, relay] : ['e', event]], tags: [...list.tags, relay ? ["e", event, relay] : ["e", event]],
}; };
} }
export function listRemoveEvent(list: NostrEvent | EventTemplate, event: string): EventTemplate { export function listRemoveEvent(list: NostrEvent | EventTemplate, event: string): EventTemplate {
return { return {
created_at: unixNow(), created_at: unixNow(),
kind: list.kind, kind: list.kind,
content: list.content, content: list.content,
tags: list.tags.filter((t) => !(t[0] === 'e' && t[1] === event)), tags: list.tags.filter((t) => !(t[0] === "e" && t[1] === event)),
}; };
} }
export function listAddCoordinate(list: NostrEvent | EventTemplate, coordinate: string, relay?: string): EventTemplate { export function listAddCoordinate(list: NostrEvent | EventTemplate, coordinate: string, relay?: string): EventTemplate {
if (list.tags.some((t) => t[0] === 'a' && t[1] === coordinate)) throw new Error('Event already in list'); if (list.tags.some((t) => t[0] === "a" && t[1] === coordinate)) throw new Error("Event already in list");
return { return {
created_at: unixNow(), created_at: unixNow(),
kind: list.kind, kind: list.kind,
content: list.content, content: list.content,
tags: [...list.tags, relay ? ['a', coordinate, relay] : ['a', coordinate]], tags: [...list.tags, relay ? ["a", coordinate, relay] : ["a", coordinate]],
}; };
} }
export function listRemoveCoordinate(list: NostrEvent | EventTemplate, coordinate: string): EventTemplate { export function listRemoveCoordinate(list: NostrEvent | EventTemplate, coordinate: string): EventTemplate {
return { return {
created_at: unixNow(), created_at: unixNow(),
kind: list.kind, kind: list.kind,
content: list.content, content: list.content,
tags: list.tags.filter((t) => !(t[0] === 'a' && t[1] === coordinate)), tags: list.tags.filter((t) => !(t[0] === "a" && t[1] === coordinate)),
}; };
} }

View File

@@ -1,15 +1,15 @@
import { NostrEvent } from 'nostr-tools'; import { NostrEvent } from "nostr-tools";
export function getArticleTitle(event: NostrEvent) { export function getArticleTitle(event: NostrEvent) {
return event.tags.find((t) => t[0] === 'title')?.[1]; return event.tags.find((t) => t[0] === "title")?.[1];
} }
export function getArticleSummary(event: NostrEvent) { export function getArticleSummary(event: NostrEvent) {
return event.tags.find((t) => t[0] === 'summary')?.[1]; return event.tags.find((t) => t[0] === "summary")?.[1];
} }
export function getArticleImage(event: NostrEvent) { export function getArticleImage(event: NostrEvent) {
return event.tags.find((t) => t[0] === 'image')?.[1]; return event.tags.find((t) => t[0] === "image")?.[1];
} }
export function getArticlePublishDate(event: NostrEvent) { export function getArticlePublishDate(event: NostrEvent) {
const timestamp = event.tags.find((t) => t[0] === 'published_at')?.[1]; const timestamp = event.tags.find((t) => t[0] === "published_at")?.[1];
return timestamp ? parseInt(timestamp) : undefined; return timestamp ? parseInt(timestamp) : undefined;
} }

View File

@@ -1,17 +1,17 @@
import { NostrEvent } from 'nostr-tools'; import { NostrEvent } from "nostr-tools";
import { safeRelayUrls } from './relays.js'; import { safeRelayUrls } from "./relays.js";
// inbox relays can be ["r", <url>, "read"] or ["r", <url>] // inbox relays can be ["r", <url>, "read"] or ["r", <url>]
export function getInboxes(event?: NostrEvent | null, fallback?: string[]) { export function getInboxes(event?: NostrEvent | null, fallback?: string[]) {
const tags = event ? event.tags.filter((t) => (t[0] === 'r' && t[2] === 'read') || t[2] === undefined) : []; const tags = event ? event.tags.filter((t) => (t[0] === "r" && t[2] === "read") || t[2] === undefined) : [];
const urls = safeRelayUrls(tags.map((t) => t[1])); const urls = safeRelayUrls(tags.map((t) => t[1]));
if (fallback && urls.length === 0) return fallback; if (fallback && urls.length === 0) return fallback;
return urls; return urls;
} }
export function getOutboxes(event?: NostrEvent | null, fallback?: string[]) { export function getOutboxes(event?: NostrEvent | null, fallback?: string[]) {
const tags = event ? event.tags.filter((t) => (t[0] === 'r' && t[2] === 'write') || t[2] === undefined) : []; const tags = event ? event.tags.filter((t) => (t[0] === "r" && t[2] === "write") || t[2] === undefined) : [];
const urls = safeRelayUrls(tags.map((t) => t[1])); const urls = safeRelayUrls(tags.map((t) => t[1]));
if (fallback && urls.length === 0) return fallback; if (fallback && urls.length === 0) return fallback;
return urls; return urls;
} }

View File

@@ -1,22 +1,22 @@
import { nip19, NostrEvent } from 'nostr-tools'; import { nip19, NostrEvent } from "nostr-tools";
import { isReplaceable } from './event.js'; import { isReplaceable } from "./event.js";
export function getSharableEventAddress(event: NostrEvent, relays?: Iterable<string>) { export function getSharableEventAddress(event: NostrEvent, relays?: Iterable<string>) {
if (isReplaceable(event.kind)) { if (isReplaceable(event.kind)) {
const d = event.tags.find((t) => t[0] === 'd' && t[1])?.[1]; const d = event.tags.find((t) => t[0] === "d" && t[1])?.[1];
if (!d) return null; if (!d) return null;
return nip19.naddrEncode({ return nip19.naddrEncode({
kind: event.kind, kind: event.kind,
identifier: d, identifier: d,
pubkey: event.pubkey, pubkey: event.pubkey,
relays: relays && Array.from(relays), relays: relays && Array.from(relays),
}); });
} else { } else {
return nip19.neventEncode({ return nip19.neventEncode({
id: event.id, id: event.id,
kind: event.kind, kind: event.kind,
relays: relays && Array.from(relays), relays: relays && Array.from(relays),
author: event.pubkey, author: event.pubkey,
}); });
} }
} }

View File

@@ -1,51 +1,51 @@
import { NostrEvent, nip19 } from 'nostr-tools'; import { NostrEvent, nip19 } from "nostr-tools";
export type Kind0ParsedContent = { export type Kind0ParsedContent = {
pubkey?: string; pubkey?: string;
name?: string; name?: string;
display_name?: string; display_name?: string;
displayName?: string; displayName?: string;
about?: string; about?: string;
/** @deprecated */ /** @deprecated */
image?: string; image?: string;
picture?: string; picture?: string;
banner?: string; banner?: string;
website?: string; website?: string;
lud16?: string; lud16?: string;
lud06?: string; lud06?: string;
nip05?: string; nip05?: string;
}; };
export function parseKind0Event(event: NostrEvent): Kind0ParsedContent { export function parseKind0Event(event: NostrEvent): Kind0ParsedContent {
if (event.kind !== 0) throw new Error('expected a kind 0 event'); if (event.kind !== 0) throw new Error("expected a kind 0 event");
try { try {
const metadata = JSON.parse(event.content) as Kind0ParsedContent; const metadata = JSON.parse(event.content) as Kind0ParsedContent;
metadata.pubkey = event.pubkey; metadata.pubkey = event.pubkey;
// ensure nip05 is a string // ensure nip05 is a string
if (metadata.nip05 && typeof metadata.nip05 !== 'string') metadata.nip05 = String(metadata.nip05); if (metadata.nip05 && typeof metadata.nip05 !== "string") metadata.nip05 = String(metadata.nip05);
// fix user website // fix user website
if (metadata.website) metadata.website = fixWebsiteUrl(metadata.website); if (metadata.website) metadata.website = fixWebsiteUrl(metadata.website);
return metadata; return metadata;
} catch (e) {} } catch (e) {}
return {}; return {};
} }
export function getSearchNames(metadata: Kind0ParsedContent) { export function getSearchNames(metadata: Kind0ParsedContent) {
if (!metadata) return []; if (!metadata) return [];
return [metadata.displayName, metadata.display_name, metadata.name].filter(Boolean) as string[]; return [metadata.displayName, metadata.display_name, metadata.name].filter(Boolean) as string[];
} }
export function getUserDisplayName(metadata: Kind0ParsedContent | undefined, pubkey: string) { export function getUserDisplayName(metadata: Kind0ParsedContent | undefined, pubkey: string) {
return metadata?.displayName || metadata?.display_name || metadata?.name || nip19.npubEncode(pubkey).slice(0, 8); return metadata?.displayName || metadata?.display_name || metadata?.name || nip19.npubEncode(pubkey).slice(0, 8);
} }
export function fixWebsiteUrl(website: string) { export function fixWebsiteUrl(website: string) {
if (website.match(/^http?s:\/\//)) { if (website.match(/^http?s:\/\//)) {
return website; return website;
} }
return 'https://' + website; return "https://" + website;
} }

View File

@@ -1,24 +1,24 @@
export function validateRelayURL(relay: string | URL) { export function validateRelayURL(relay: string | URL) {
if (typeof relay === 'string' && relay.includes(',ws')) throw new Error('Can not have multiple relays in one string'); if (typeof relay === "string" && relay.includes(",ws")) throw new Error("Can not have multiple relays in one string");
const url = typeof relay === 'string' ? new URL(relay) : relay; const url = typeof relay === "string" ? new URL(relay) : relay;
if (url.protocol !== 'wss:' && url.protocol !== 'ws:') throw new Error('Incorrect protocol'); if (url.protocol !== "wss:" && url.protocol !== "ws:") throw new Error("Incorrect protocol");
return url; return url;
} }
export function isValidRelayURL(relay: string | URL) { export function isValidRelayURL(relay: string | URL) {
try { try {
validateRelayURL(relay); validateRelayURL(relay);
return true; return true;
} catch (e) { } catch (e) {
return false; return false;
} }
} }
export function safeRelayUrl(relayUrl: string | URL) { export function safeRelayUrl(relayUrl: string | URL) {
try { try {
return validateRelayURL(relayUrl).toString(); return validateRelayURL(relayUrl).toString();
} catch (e) { } catch (e) {
return null; return null;
} }
} }
export function safeRelayUrls(urls: Iterable<string>): string[] { export function safeRelayUrls(urls: Iterable<string>): string[] {
return Array.from(urls).map(safeRelayUrl).filter(Boolean) as string[]; return Array.from(urls).map(safeRelayUrl).filter(Boolean) as string[];
} }

View File

@@ -1,17 +1,17 @@
export default class SuperMap<Key, Value> extends Map<Key, Value> { export default class SuperMap<Key, Value> extends Map<Key, Value> {
newValue: (key: Key) => Value; newValue: (key: Key) => Value;
constructor(newValue: (key: Key) => Value) { constructor(newValue: (key: Key) => Value) {
super(); super();
this.newValue = newValue; this.newValue = newValue;
} }
get(key: Key) { get(key: Key) {
let value = super.get(key); let value = super.get(key);
if (value === undefined) { if (value === undefined) {
value = this.newValue(key); value = this.newValue(key);
this.set(key, value); this.set(key, value);
} }
return value; return value;
} }
} }

View File

@@ -1,4 +1,3 @@
import { tap } from "rxjs";
import { kinds } from "nostr-tools"; import { kinds } from "nostr-tools";
import { MailboxesQuery } from "applesauce-core/queries"; import { MailboxesQuery } from "applesauce-core/queries";
import { getObservableValue, simpleTimeout } from "applesauce-core/observable"; import { getObservableValue, simpleTimeout } from "applesauce-core/observable";
@@ -30,14 +29,12 @@ export default class AddressBook {
async loadMailboxes(pubkey: string, relays?: string[], force?: boolean) { async loadMailboxes(pubkey: string, relays?: string[], force?: boolean) {
relays = arrayFallback(relays, COMMON_CONTACT_RELAYS); relays = arrayFallback(relays, COMMON_CONTACT_RELAYS);
this.log(`Requesting mailboxes from ${relays.length} relays for ${pubkey}`);
replaceableLoader.next({ kind: kinds.RelayList, pubkey, relays, force }); replaceableLoader.next({ kind: kinds.RelayList, pubkey, relays, force });
return getObservableValue( return getObservableValue(
queryStore.createQuery(MailboxesQuery, pubkey).pipe( queryStore
simpleTimeout(DEFAULT_REQUEST_TIMEOUT, `Failed to load mailboxes for ${pubkey}`), .createQuery(MailboxesQuery, pubkey)
tap((m) => m && this.log(`Found mailboxes for ${pubkey}`, m)), .pipe(simpleTimeout(DEFAULT_REQUEST_TIMEOUT, `Failed to load mailboxes for ${pubkey}`)),
),
); );
} }
} }

View File

@@ -1,4 +1,3 @@
import { tap } from "rxjs";
import { kinds } from "nostr-tools"; import { kinds } from "nostr-tools";
import { ReplaceableQuery, UserContactsQuery } from "applesauce-core/queries"; import { ReplaceableQuery, UserContactsQuery } from "applesauce-core/queries";
import { getObservableValue, simpleTimeout } from "applesauce-core/observable"; import { getObservableValue, simpleTimeout } from "applesauce-core/observable";
@@ -36,7 +35,6 @@ export default class ContactBook {
async loadContacts(pubkey: string, relays?: string[], force?: boolean) { async loadContacts(pubkey: string, relays?: string[], force?: boolean) {
relays = arrayFallback(relays, COMMON_CONTACT_RELAYS); relays = arrayFallback(relays, COMMON_CONTACT_RELAYS);
this.log(`Requesting contacts from ${relays.length} relays for ${pubkey}`);
replaceableLoader.next({ kind: kinds.Contacts, pubkey, relays, force }); replaceableLoader.next({ kind: kinds.Contacts, pubkey, relays, force });
return getObservableValue( return getObservableValue(
@@ -49,14 +47,12 @@ export default class ContactBook {
/** @deprecated */ /** @deprecated */
async loadContactsEvent(pubkey: string, relays?: string[]) { async loadContactsEvent(pubkey: string, relays?: string[]) {
relays = arrayFallback(relays, COMMON_CONTACT_RELAYS); relays = arrayFallback(relays, COMMON_CONTACT_RELAYS);
this.log(`Requesting contacts from ${relays.length} relays for ${pubkey}`);
replaceableLoader.next({ kind: kinds.Contacts, pubkey, relays }); replaceableLoader.next({ kind: kinds.Contacts, pubkey, relays });
return getObservableValue( return getObservableValue(
queryStore.createQuery(ReplaceableQuery, kinds.Contacts, pubkey).pipe( queryStore
simpleTimeout(DEFAULT_REQUEST_TIMEOUT, `Failed to load contacts for ${pubkey}`), .createQuery(ReplaceableQuery, kinds.Contacts, pubkey)
tap((c) => c && this.log(`Found contacts for ${pubkey}`, c)), .pipe(simpleTimeout(DEFAULT_REQUEST_TIMEOUT, `Failed to load contacts for ${pubkey}`)),
),
); );
} }
} }

View File

@@ -1,93 +0,0 @@
import { WebSocket } from "ws";
import { ReportArguments } from "@satellite-earth/core/types";
import { ReportsMessage } from "@satellite-earth/core/types/control-api/reports.js";
import type App from "../../app/index.js";
import { type ControlMessageHandler } from "./control-api.js";
import Report from "../reports/report.js";
import { logger } from "../../logger.js";
import REPORT_CLASSES from "../reports/reports/index.js";
/** handles ['CONTROL', 'REPORT', ...] messages */
export default class ReportActions implements ControlMessageHandler {
app: App;
name = "REPORT";
log = logger.extend("ReportActions");
types: {
[k in keyof ReportArguments]?: typeof Report<k>;
} = REPORT_CLASSES;
private reports = new Map<WebSocket | NodeJS.Process, Map<string, Report<any>>>();
constructor(app: App) {
this.app = app;
}
private getReportsForSocket(socket: WebSocket | NodeJS.Process) {
let map = this.reports.get(socket);
if (map) return map;
map = new Map();
this.reports.set(socket, map);
return map;
}
handleDisconnect(ws: WebSocket): void {
// close all reports for socket on disconnect
const reports = this.reports.get(ws);
if (reports) {
for (const [id, report] of reports) report.close();
if (reports.size) this.log(`Closed ${reports.size} reports for disconnected socket`);
this.reports.delete(ws);
}
}
// TODO: maybe move some of this logic out to a manager class so the control action class can be simpler
async handleMessage(sock: WebSocket | NodeJS.Process, message: ReportsMessage) {
const method = message[2];
switch (method) {
case "SUBSCRIBE": {
const reports = this.getReportsForSocket(sock);
const id = message[3];
const type = message[4];
const args = message[5];
let report = reports.get(id) as Report<typeof type> | undefined;
if (!report) {
const ReportClass = this.types[type];
if (!ReportClass) throw new Error("Missing class for report type: " + type);
this.log(`Creating ${type} ${id} report with args`, JSON.stringify(args));
report = new ReportClass(id, this.app, sock);
reports.set(id, report);
}
await report.run(args);
return true;
}
case "CLOSE": {
const reports = this.getReportsForSocket(sock);
const id = message[3];
const report = reports.get(id);
if (report) {
await report.close();
reports.delete(id);
}
return true;
}
default:
return false;
}
}
cleanup() {
for (const [sock, reports] of this.reports) {
for (const [id, report] of reports) {
report.close();
}
}
}
}

View File

@@ -1,4 +1,3 @@
import { tap } from "rxjs";
import { kinds } from "nostr-tools"; import { kinds } from "nostr-tools";
import { getObservableValue, simpleTimeout } from "applesauce-core/observable"; import { getObservableValue, simpleTimeout } from "applesauce-core/observable";
import { ProfileQuery } from "applesauce-core/queries"; import { ProfileQuery } from "applesauce-core/queries";
@@ -21,14 +20,12 @@ export default class ProfileBook {
async loadProfile(pubkey: string, relays?: string[], force?: boolean) { async loadProfile(pubkey: string, relays?: string[], force?: boolean) {
relays = arrayFallback(relays, COMMON_CONTACT_RELAYS); relays = arrayFallback(relays, COMMON_CONTACT_RELAYS);
this.log(`Requesting profile from ${relays.length} relays for ${pubkey}`);
replaceableLoader.next({ kind: kinds.Metadata, pubkey, relays, force }); replaceableLoader.next({ kind: kinds.Metadata, pubkey, relays, force });
return getObservableValue( return getObservableValue(
queryStore.createQuery(ProfileQuery, pubkey).pipe( queryStore
simpleTimeout(DEFAULT_REQUEST_TIMEOUT, `Failed to load profile for ${pubkey}`), .createQuery(ProfileQuery, pubkey)
tap((p) => p && this.log(`Found profile for ${pubkey}`, p)), .pipe(simpleTimeout(DEFAULT_REQUEST_TIMEOUT, `Failed to load profile for ${pubkey}`)),
),
); );
} }
} }

View File

@@ -1,101 +0,0 @@
import { filter, Observable, shareReplay, Subscription } from "rxjs";
import hash_sum from "hash-sum";
import { Session } from "../../relay/session.js";
import SuperMap from "../../helpers/super-map.js";
export type Query<T extends unknown = unknown> = (args: T, socket: WebSocket) => Observable<any>;
// open query messages (id, type, args)
export type QueryOpen<Args extends unknown> = ["QRY", "OPEN", string, string, Args];
// close query message (id)
export type QueryClose = ["QRY", "CLOSE", string];
// error messages (id, message)
export type QueryError = ["QRY", "ERR", string, string];
// result message (id, data)
export type QueryData<Result extends unknown> = ["QRY", "DATA", string, Result];
// report type
export type Report<Args extends Record<string, any>, Output> = (
args: Args,
) => Promise<Observable<Output>> | Observable<Output>;
/** A report manager designed to be created for each websocket connection */
export default class ReportManager {
types = new Map<string, Report<any, any>>();
protected reports = new SuperMap<Report<any, any>, Map<string, Observable<any>>>(() => new Map());
constructor(public session: Session) {
this.session
.pipe(filter((v) => Array.isArray(v) && v[0] === "QRY" && v[1]))
.subscribe(this.handleMessage.bind(this));
}
registerType(name: string, report: Report<any, any>) {
if (this.types.has(name)) throw new Error("A report type with that name already exists");
this.types.set(name, report);
}
unregisterType(name: string) {
this.types.delete(name);
}
/** Create or run a report */
async execute<Args extends Record<string, any>, Output>(
report: string | Report<Args, Output>,
args: Args,
): Promise<Observable<Output>> {
let type = typeof report === "string" ? this.types.get(report) : report;
if (!type) throw new Error("Failed to find report type");
const reports = this.reports.get(type);
const key = hash_sum(args);
let observable: Observable<Output> | undefined = reports.get(key);
if (!observable) {
// create new report
observable = (await type(args)).pipe(shareReplay());
reports.set(key, observable);
}
return observable;
}
subscriptions = new Map<string, Subscription>();
handleMessage(message: QueryOpen<any> | QueryClose) {
try {
switch (message[1]) {
case "OPEN":
this.openSub(message[2], message[3], message[4]);
break;
case "CLOSE":
this.closeSub(message[2]);
break;
}
} catch (error) {
// failed to handle message, ignore
}
}
protected async openSub(id: string, type: string, args: any) {
const sub = (await this.execute(type, args)).subscribe({
next: (result) => this.session.send(["QRY", "DATA", id, result] satisfies QueryData<any>),
error: (err) => {
if (err instanceof Error) this.session.send(["QRY", "ERR", id, err.message] satisfies QueryError);
else this.session.send(["QRY", "ERR", id, "Something went wrong"] satisfies QueryError);
},
complete: () => this.session.send(["QRY", "CLOSE", id] satisfies QueryClose),
});
this.subscriptions.set(id, sub);
return sub;
}
protected closeSub(id: string) {
const sub = this.subscriptions.get(id);
if (sub) {
sub.unsubscribe();
this.subscriptions.delete(id);
}
}
}

View File

@@ -1,80 +0,0 @@
import { WebSocket } from "ws";
import { Observable } from "rxjs";
import { ReportErrorMessage, ReportResultMessage } from "@satellite-earth/core/types/control-api/reports.js";
import { ReportArguments, ReportResults } from "@satellite-earth/core/types";
import type App from "../../app/index.js";
import { logger } from "../../logger.js";
export type NewReport = (socket: WebSocket | NodeJS.Process) => Observable<any>;
type f = () => void;
export default class Report<T extends keyof ReportResults> {
id: string;
// @ts-expect-error
readonly type: T = "";
socket: WebSocket | NodeJS.Process;
app: App;
running = false;
log = logger.extend("Report");
args?: ReportArguments[T];
private setupTeardown?: void | f;
constructor(id: string, app: App, socket: WebSocket | NodeJS.Process) {
this.id = id;
this.socket = socket;
this.app = app;
this.log = logger.extend("Report:" + this.type);
}
private sendError(message: string) {
this.socket.send?.(JSON.stringify(["CONTROL", "REPORT", "ERROR", this.id, message] satisfies ReportErrorMessage));
}
// override when extending
/** This method is run only once when the report starts */
async setup(args: ReportArguments[T]): Promise<void | f> {}
/** this method is run every time the client sends new arguments */
async execute(args: ReportArguments[T]) {}
/** this method is run when the report is closed */
cleanup() {}
// private methods
protected send(result: ReportResults[T]) {
this.socket.send?.(
JSON.stringify(["CONTROL", "REPORT", "RESULT", this.id, result] satisfies ReportResultMessage<T>),
);
}
// public api
async run(args: ReportArguments[T]) {
try {
this.args = args;
if (this.running === false) {
// hack to make sure the .log is extended correctly
this.log = logger.extend("Report:" + this.type);
this.setupTeardown = await this.setup(args);
}
this.log(`Executing with args`, JSON.stringify(args));
await this.execute(args);
this.running = true;
} catch (error) {
if (error instanceof Error) this.sendError(error.message);
else this.sendError("Unknown server error");
if (error instanceof Error) this.log("Error: " + error.message);
throw error;
}
}
close() {
this.setupTeardown?.();
this.cleanup();
this.running = false;
}
}

View File

@@ -1,115 +0,0 @@
import { ReportArguments, ReportResults } from "@satellite-earth/core/types";
import { getTagValue } from "applesauce-core/helpers";
import { NostrEvent } from "nostr-tools";
import Report from "../report.js";
import SuperMap from "../../../helpers/super-map.js";
export default class ConversationsReport extends Report<"CONVERSATIONS"> {
readonly type = "CONVERSATIONS";
private async getConversationResult(self: string, other: string) {
const sent = this.app.database.db
.prepare<[string, string], { pubkey: string; count: number; lastMessage: number }>(
`
SELECT tags.v as pubkey, count(events.id) as count, max(events.created_at) as lastMessage FROM tags
INNER JOIN events ON events.id = tags.e
WHERE events.kind = 4 AND tags.t = 'p' AND events.pubkey = ? AND tags.v = ?`,
)
.get(self, other);
const received = this.app.database.db
.prepare<[string, string], { pubkey: string; count: number; lastMessage: number }>(
`
SELECT events.pubkey, count(events.id) as count, max(events.created_at) as lastMessage FROM events
INNER JOIN tags ON tags.e = events.id
WHERE events.kind = 4 AND tags.t = 'p' AND tags.v = ? AND events.pubkey = ?`,
)
.get(self, other);
const result: ReportResults["CONVERSATIONS"] = {
pubkey: other,
count: (received?.count ?? 0) + (sent?.count ?? 0),
sent: 0,
received: 0,
};
if (received) {
result.received = received.count;
result.lastReceived = received.lastMessage;
}
if (sent) {
result.sent = sent.count;
result.lastSent = sent.lastMessage;
}
return result;
}
private async getAllConversationResults(self: string) {
const sent = this.app.database.db
.prepare<[string], { pubkey: string; count: number; lastMessage: number }>(
`
SELECT tags.v as pubkey, count(tags.v) as count, max(events.created_at) as lastMessage FROM tags
INNER JOIN events ON events.id = tags.e
WHERE events.kind = 4 AND tags.t = 'p' AND events.pubkey = ?
GROUP BY tags.v`,
)
.all(self);
const received = this.app.database.db
.prepare<[string], { pubkey: string; count: number; lastMessage: number }>(
`
SELECT events.pubkey, count(events.pubkey) as count, max(events.created_at) as lastMessage FROM events
INNER JOIN tags ON tags.e = events.id
WHERE events.kind = 4 AND tags.t = 'p' AND tags.v = ?
GROUP BY events.pubkey`,
)
.all(self);
const results = new SuperMap<string, ReportResults["CONVERSATIONS"]>((pubkey) => ({
pubkey,
count: sent.length + received.length,
sent: 0,
received: 0,
}));
for (const { pubkey, count, lastMessage } of received) {
const result = results.get(pubkey);
result.received = count;
result.lastReceived = lastMessage;
}
for (const { pubkey, count, lastMessage } of sent) {
const result = results.get(pubkey);
result.sent = count;
result.lastSent = lastMessage;
}
return Array.from(results.values()).sort(
(a, b) => Math.max(b.lastReceived ?? 0, b.lastSent ?? 0) - Math.max(a.lastReceived ?? 0, a.lastSent ?? 0),
);
}
async setup(args: ReportArguments["CONVERSATIONS"]) {
const listener = (event: NostrEvent) => {
const from = event.pubkey;
const to = getTagValue(event, "p");
if (!to) return;
const self = args.pubkey;
// get the latest stats from the database
this.getConversationResult(self, self === from ? to : from).then((result) => this.send(result));
};
this.app.directMessageManager.on("message", listener);
return () => this.app.directMessageManager.off("message", listener);
}
async execute(args: ReportArguments["CONVERSATIONS"]) {
const results = await this.getAllConversationResults(args.pubkey);
for (const result of results) {
this.send(result);
}
}
}

View File

@@ -1,11 +0,0 @@
import { ReportArguments } from "@satellite-earth/core/types";
import Report from "../report.js";
export default class DMSearchReport extends Report<"DM_SEARCH"> {
readonly type = "DM_SEARCH";
async execute(args: ReportArguments["DM_SEARCH"]) {
const results = await this.app.decryptionCache.search(args.query, args);
for (const result of results) this.send(result);
}
}

View File

@@ -1,69 +0,0 @@
import { ReportArguments } from "@satellite-earth/core/types";
import Report from "../report.js";
import { EventRow, parseEventRow } from "../../../sqlite/event-store.js";
export default class EventsSummaryReport extends Report<"EVENTS_SUMMARY"> {
readonly type = "EVENTS_SUMMARY";
async execute(args: ReportArguments["EVENTS_SUMMARY"]): Promise<void> {
let sql = `
SELECT
events.*,
COUNT(l.id) AS reactions,
COUNT(s.id) AS shares,
COUNT(r.id) AS replies,
(events.kind || ':' || events.pubkey || ':' || events.d) as address
FROM events
LEFT JOIN tags ON ( tags.t = 'e' AND tags.v = events.id ) OR ( tags.t = 'a' AND tags.v = address )
LEFT JOIN events AS l ON l.id = tags.e AND l.kind = 7
LEFT JOIN events AS s ON s.id = tags.e AND (s.kind = 6 OR s.kind = 16)
LEFT JOIN events AS r ON r.id = tags.e AND r.kind = 1
`;
const params: any[] = [];
const conditions: string[] = [];
if (args.kind !== undefined) {
conditions.push(`events.kind = ?`);
params.push(args.kind);
}
if (args.pubkey !== undefined) {
conditions.push(`events.pubkey = ?`);
params.push(args.pubkey);
}
if (conditions.length > 0) {
sql += ` WHERE ${conditions.join(" AND ")}\n`;
}
sql += " GROUP BY events.id\n";
switch (args.order) {
case "created_at":
sql += ` ORDER BY events.created_at DESC\n`;
break;
default:
case "interactions":
sql += ` ORDER BY reactions + shares + replies DESC\n`;
break;
}
let limit = args.limit || 100;
sql += ` LIMIT ?`;
params.push(limit);
const rows = await this.app.database.db
.prepare<any[], EventRow & { reactions: number; shares: number; replies: number }>(sql)
.all(...params);
const results = rows.map((row) => {
const event = parseEventRow(row);
return { event, reactions: row.reactions, shares: row.shares, replies: row.replies };
});
for (const result of results) {
this.send(result);
}
}
}

View File

@@ -1,26 +0,0 @@
import { ReportArguments } from "@satellite-earth/core/types";
import Report from "../report.js";
import OverviewReport from "./overview.js";
import ConversationsReport from "./conversations.js";
import ServicesReport from "./services.js";
import DMSearchReport from "./dm-search.js";
import ScrapperStatusReport from "./scrapper-status.js";
import ReceiverStatusReport from "./receiver-status.js";
import NotificationChannelsReport from "./notification-channels.js";
import EventsSummaryReport from "./events-summary.js";
const REPORT_CLASSES: {
[k in keyof ReportArguments]?: typeof Report<k>;
} = {
OVERVIEW: OverviewReport,
CONVERSATIONS: ConversationsReport,
SERVICES: ServicesReport,
DM_SEARCH: DMSearchReport,
SCRAPPER_STATUS: ScrapperStatusReport,
RECEIVER_STATUS: ReceiverStatusReport,
NOTIFICATION_CHANNELS: NotificationChannelsReport,
EVENTS_SUMMARY: EventsSummaryReport,
};
export default REPORT_CLASSES;

View File

@@ -1,29 +0,0 @@
import { NotificationChannel } from "@satellite-earth/core/types/control-api/notifications.js";
import Report from "../report.js";
export default class NotificationChannelsReport extends Report<"NOTIFICATION_CHANNELS"> {
readonly type = "NOTIFICATION_CHANNELS";
async setup() {
const listener = this.send.bind(this);
const removeListener = (channel: NotificationChannel) => {
this.send(["removed", channel.id]);
};
this.app.notifications.on("addChannel", listener);
this.app.notifications.on("updateChannel", listener);
this.app.notifications.on("removeChannel", removeListener);
return () => {
this.app.notifications.off("addChannel", listener);
this.app.notifications.off("updateChannel", listener);
this.app.notifications.off("removeChannel", removeListener);
};
}
async execute(args: {}): Promise<void> {
for (const channel of this.app.notifications.channels) {
this.send(channel);
}
}
}

View File

@@ -1,40 +0,0 @@
import { NostrEvent } from "nostr-tools";
import { ReportArguments } from "@satellite-earth/core/types";
import Report from "../report.js";
export default class OverviewReport extends Report<"OVERVIEW"> {
readonly type = "OVERVIEW";
async setup() {
const listener = (event: NostrEvent) => {
// update summary for pubkey
const result = this.app.database.db
.prepare<
[string],
{ pubkey: string; events: number; active: number }
>(`SELECT pubkey, COUNT(events.id) as \`events\`, MAX(created_at) as \`active\` FROM events WHERE pubkey=?`)
.get(event.pubkey);
if (result) this.send(result);
};
this.app.eventStore.on("event:inserted", listener);
return () => {
this.app.eventStore.off("event:inserted", listener);
};
}
async execute(args: ReportArguments["OVERVIEW"]) {
const results = await this.app.database.db
.prepare<
[],
{ pubkey: string; events: number; active: number }
>(`SELECT pubkey, COUNT(events.id) as \`events\`, MAX(created_at) as \`active\` FROM events GROUP BY pubkey ORDER BY \`events\` DESC`)
.all();
for (const result of results) {
this.send(result);
}
}
}

View File

@@ -1,38 +0,0 @@
import Report from "../report.js";
export default class ReceiverStatusReport extends Report<"RECEIVER_STATUS"> {
readonly type = "RECEIVER_STATUS";
update() {
this.send({
status: this.app.receiver.status,
startError: this.app.receiver.startupError?.message,
subscriptions: Array.from(this.app.receiver.map).map(([relay, pubkeys]) => ({
relay,
pubkeys: Array.from(pubkeys),
active: !!this.app.receiver.subscriptions.get(relay),
closed: !!this.app.receiver.subscriptions.get(relay)?.closed,
})),
});
}
async setup() {
const listener = this.update.bind(this);
this.app.receiver.on("status", listener);
this.app.receiver.on("subscribed", listener);
this.app.receiver.on("closed", listener);
this.app.receiver.on("error", listener);
return () => {
this.app.receiver.off("status", listener);
this.app.receiver.off("subscribed", listener);
this.app.receiver.off("closed", listener);
this.app.receiver.off("error", listener);
};
}
async execute(args: {}): Promise<void> {
this.update();
}
}

View File

@@ -1,55 +0,0 @@
import { NostrEvent } from "nostr-tools";
import _throttle from "lodash.throttle";
import Report from "../report.js";
export default class ScrapperStatusReport extends Report<"SCRAPPER_STATUS"> {
readonly type = "SCRAPPER_STATUS";
eventsPerSecond: number[] = [0];
update() {
const averageEventsPerSecond = this.eventsPerSecond.reduce((m, v) => m + v, 0) / this.eventsPerSecond.length;
const pubkeys = this.app.scrapper.state.pubkeys;
let activeSubscriptions = 0;
for (const [pubkey, scrapper] of this.app.scrapper.scrappers) {
for (const [relay, relayScrapper] of scrapper.relayScrappers) {
if (relayScrapper.running) activeSubscriptions++;
}
}
this.send({
running: this.app.scrapper.running,
eventsPerSecond: averageEventsPerSecond,
activeSubscriptions,
pubkeys,
});
}
async setup() {
const onEvent = (event: NostrEvent) => {
this.eventsPerSecond[0]++;
};
this.app.scrapper.on("event", onEvent);
const tick = setInterval(() => {
// start a new second
this.eventsPerSecond.unshift(0);
// limit to 60 seconds
while (this.eventsPerSecond.length > 60) this.eventsPerSecond.pop();
this.update();
}, 1000);
return () => {
this.app.scrapper.off("event", onEvent);
clearInterval(tick);
};
}
async execute(args: {}): Promise<void> {
this.update();
}
}

View File

@@ -1,12 +0,0 @@
import Report from "../report.js";
export default class ServicesReport extends Report<"SERVICES"> {
readonly type = "SERVICES";
async execute() {
const services = this.app.database.db
.prepare<[], { id: string }>(`SELECT service as id FROM logs GROUP BY service`)
.all();
for (const service of services) this.send(service);
}
}

View File

@@ -1,5 +1,5 @@
import { handleDeleteEvent } from './handle-delete-event.js'; import { handleDeleteEvent } from "./handle-delete-event.js";
export const RelayActions = { export const RelayActions = {
handleDeleteEvent, handleDeleteEvent,
}; };