mirror of
https://github.com/aljazceru/bakery.git
synced 2025-12-17 04:35:13 +01:00
remove legacy reports
This commit is contained in:
@@ -11,7 +11,7 @@ PORT=3000
|
||||
|
||||
# the address to the i2p SOCKS5 proxy to enable connections to .i2p addresses
|
||||
# I2P_PROXY="127.0.0.1:4447"
|
||||
# I@P proxy type, SOCKS5 or HTTP
|
||||
# I2P proxy type, SOCKS5 or HTTP
|
||||
# I2P_PROXY_TYPE="SOCKS5"
|
||||
|
||||
# sets a hardcoded tor address
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
# bakery
|
||||
# noStrudel Bakery
|
||||
|
||||
A relay backend for noStrudel
|
||||
A backend for [noStrudel](https://github.com/hzrd149/noStrudel)
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"start": "node .",
|
||||
"dev": "nodemon --loader @swc-node/register/esm src/index.ts",
|
||||
"build": "tsc",
|
||||
"test": "vitest run",
|
||||
"format": "prettier -w ."
|
||||
},
|
||||
"files": [
|
||||
@@ -75,7 +76,8 @@
|
||||
"@types/ws": "^8.5.14",
|
||||
"nodemon": "^3.1.9",
|
||||
"prettier": "^3.5.1",
|
||||
"typescript": "^5.7.3"
|
||||
"typescript": "^5.7.3",
|
||||
"vitest": "^3.0.9"
|
||||
},
|
||||
"nodemonConfig": {
|
||||
"ignore": [
|
||||
|
||||
3949
pnpm-lock.yaml
generated
3949
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -29,7 +29,6 @@ import ProfileBook from "../modules/profile-book.js";
|
||||
import ContactBook from "../modules/contact-book.js";
|
||||
import CautiousPool from "../modules/cautious-pool.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 DecryptionCache from "../modules/decryption-cache/decryption-cache.js";
|
||||
import DecryptionCacheActions from "../modules/control/decryption-cache.js";
|
||||
@@ -82,7 +81,6 @@ export default class App extends EventEmitter<EventMap> {
|
||||
receiver: Receiver;
|
||||
scrapper: Scrapper;
|
||||
control: ControlApi;
|
||||
reports: ReportActions;
|
||||
pool: CautiousPool;
|
||||
addressBook: AddressBook;
|
||||
profileBook: ProfileBook;
|
||||
@@ -212,10 +210,6 @@ export default class App extends EventEmitter<EventMap> {
|
||||
|
||||
this.control.registerHandler(new LogsActions(this));
|
||||
|
||||
// reports
|
||||
this.reports = new ReportActions(this);
|
||||
this.control.registerHandler(this.reports);
|
||||
|
||||
// connect control api to websocket server
|
||||
this.control.attachToServer(this.wss);
|
||||
|
||||
@@ -412,7 +406,6 @@ export default class App extends EventEmitter<EventMap> {
|
||||
this.scrapper.stop();
|
||||
this.receiver.stop();
|
||||
await this.state.saveAll();
|
||||
this.reports.cleanup();
|
||||
this.relay.stop();
|
||||
this.database.destroy();
|
||||
this.receiver.destroy();
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
import { NostrEvent } from 'nostr-tools';
|
||||
import { NostrEvent } from "nostr-tools";
|
||||
|
||||
import { getTagValue } from './event.js';
|
||||
import { COMMUNITY_CHANNEL_KIND } from './kinds.js';
|
||||
import { getTagValue } from "./event.js";
|
||||
import { COMMUNITY_CHANNEL_KIND } from "./kinds.js";
|
||||
|
||||
export const CHANNEL_KIND = COMMUNITY_CHANNEL_KIND;
|
||||
|
||||
export function getChannelId(channel: NostrEvent) {
|
||||
const id = getTagValue(channel, 'd');
|
||||
if (!id) throw new Error('Channel missing id');
|
||||
const id = getTagValue(channel, "d");
|
||||
if (!id) throw new Error("Channel missing id");
|
||||
return id;
|
||||
}
|
||||
export function getChannelName(channel: NostrEvent) {
|
||||
return getTagValue(channel, 'name');
|
||||
return getTagValue(channel, "name");
|
||||
}
|
||||
export function getChannelAbout(channel: NostrEvent) {
|
||||
return getTagValue(channel, 'about');
|
||||
return getTagValue(channel, "about");
|
||||
}
|
||||
export function getChannelPicture(channel: NostrEvent) {
|
||||
return getTagValue(channel, 'picture');
|
||||
return getTagValue(channel, "picture");
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
export function getCommunityName(definition: NostrEvent) {
|
||||
return tagValue(definition, 'name');
|
||||
return tagValue(definition, "name");
|
||||
}
|
||||
export function getCommunityBanner(definition: NostrEvent) {
|
||||
return tagValue(definition, 'banner');
|
||||
return tagValue(definition, "banner");
|
||||
}
|
||||
export function getCommunityImage(definition: NostrEvent) {
|
||||
return tagValue(definition, 'image');
|
||||
return tagValue(definition, "image");
|
||||
}
|
||||
export function getCommunityRelay(definition: NostrEvent) {
|
||||
return tagValue(definition, 'r');
|
||||
return tagValue(definition, "r");
|
||||
}
|
||||
export function getCommunityCDN(definition: NostrEvent) {
|
||||
return tagValue(definition, 'cdn');
|
||||
return tagValue(definition, "cdn");
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { NostrEvent } from 'nostr-tools';
|
||||
import { safeRelayUrl } from './relays.js';
|
||||
import { NostrEvent } from "nostr-tools";
|
||||
import { safeRelayUrl } from "./relays.js";
|
||||
|
||||
export function getRelaysFromContactList(event: NostrEvent) {
|
||||
try {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { NostrEvent } from 'nostr-tools';
|
||||
import { NostrEvent } from "nostr-tools";
|
||||
|
||||
export function getDMSender(event: NostrEvent) {
|
||||
return event.pubkey;
|
||||
}
|
||||
export function getDMRecipient(event: NostrEvent) {
|
||||
const pubkey = event.tags.find((t) => t[0] === 'p')?.[1];
|
||||
if (!pubkey) throw new Error('Missing recipient pubkey');
|
||||
const pubkey = event.tags.find((t) => t[0] === "p")?.[1];
|
||||
if (!pubkey) throw new Error("Missing recipient pubkey");
|
||||
return pubkey;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { NostrEvent, kinds, nip19 } from 'nostr-tools';
|
||||
import { NostrEvent, kinds, nip19 } from "nostr-tools";
|
||||
|
||||
export function isReplaceable(kind: number) {
|
||||
return kinds.isReplaceableKind(kind) || kinds.isParameterizedReplaceableKind(kind);
|
||||
@@ -9,7 +9,7 @@ export function sortByDate(a: NostrEvent, b: 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}`;
|
||||
}
|
||||
|
||||
@@ -18,14 +18,14 @@ export function getTagValue(event: NostrEvent, tag: 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;
|
||||
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;
|
||||
};
|
||||
|
||||
@@ -37,22 +37,22 @@ 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: 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 pubkey = parts[1];
|
||||
const d = parts[2];
|
||||
|
||||
if (!kind) {
|
||||
if (silent) return null;
|
||||
else throw new Error('Missing kind');
|
||||
else throw new Error("Missing kind");
|
||||
}
|
||||
if (!pubkey) {
|
||||
if (silent) return null;
|
||||
else throw new Error('Missing pubkey');
|
||||
else throw new Error("Missing pubkey");
|
||||
}
|
||||
if (requireD && d === undefined) {
|
||||
if (silent) return null;
|
||||
else throw new Error('Missing identifier');
|
||||
else throw new Error("Missing identifier");
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export * from './channel.js';
|
||||
export * from './communities.js';
|
||||
export * from './dms.js';
|
||||
export * from './event.js';
|
||||
export * from './kinds.js';
|
||||
export * from './profile.js';
|
||||
export * from './nip19.js';
|
||||
export * from "./channel.js";
|
||||
export * from "./communities.js";
|
||||
export * from "./dms.js";
|
||||
export * from "./event.js";
|
||||
export * from "./kinds.js";
|
||||
export * from "./profile.js";
|
||||
export * from "./nip19.js";
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import { EventTemplate, nip19, NostrEvent } from 'nostr-tools';
|
||||
import { parseCoordinate } from './event.js';
|
||||
import { EventTemplate, nip19, NostrEvent } from "nostr-tools";
|
||||
import { parseCoordinate } from "./event.js";
|
||||
|
||||
function unixNow() {
|
||||
return Math.round(Date.now() / 1000);
|
||||
}
|
||||
|
||||
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[] {
|
||||
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] }));
|
||||
}
|
||||
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[] {
|
||||
const pointers: nip19.AddressPointer[] = [];
|
||||
@@ -33,7 +33,7 @@ export function getAddressPointersFromList(event: NostrEvent | EventTemplate): n
|
||||
|
||||
export function isPubkeyInList(list?: NostrEvent, pubkey?: string) {
|
||||
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(
|
||||
@@ -42,9 +42,9 @@ export function listAddPerson(
|
||||
relay?: string,
|
||||
petname?: string,
|
||||
): EventTemplate {
|
||||
if (list.tags.some((t) => t[0] === 'p' && t[1] === pubkey)) throw new Error('Person already in list');
|
||||
const pTag = ['p', pubkey, relay ?? '', petname ?? ''];
|
||||
while (pTag[pTag.length - 1] === '') pTag.pop();
|
||||
if (list.tags.some((t) => t[0] === "p" && t[1] === pubkey)) throw new Error("Person already in list");
|
||||
const pTag = ["p", pubkey, relay ?? "", petname ?? ""];
|
||||
while (pTag[pTag.length - 1] === "") pTag.pop();
|
||||
|
||||
return {
|
||||
created_at: unixNow(),
|
||||
@@ -59,18 +59,18 @@ export function listRemovePerson(list: NostrEvent | EventTemplate, pubkey: strin
|
||||
created_at: unixNow(),
|
||||
kind: list.kind,
|
||||
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 {
|
||||
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 {
|
||||
created_at: unixNow(),
|
||||
kind: list.kind,
|
||||
content: list.content,
|
||||
tags: [...list.tags, relay ? ['e', event, relay] : ['e', event]],
|
||||
tags: [...list.tags, relay ? ["e", event, relay] : ["e", event]],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -79,18 +79,18 @@ export function listRemoveEvent(list: NostrEvent | EventTemplate, event: string)
|
||||
created_at: unixNow(),
|
||||
kind: list.kind,
|
||||
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 {
|
||||
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 {
|
||||
created_at: unixNow(),
|
||||
kind: list.kind,
|
||||
content: list.content,
|
||||
tags: [...list.tags, relay ? ['a', coordinate, relay] : ['a', coordinate]],
|
||||
tags: [...list.tags, relay ? ["a", coordinate, relay] : ["a", coordinate]],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -99,6 +99,6 @@ export function listRemoveCoordinate(list: NostrEvent | EventTemplate, coordinat
|
||||
created_at: unixNow(),
|
||||
kind: list.kind,
|
||||
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)),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { NostrEvent } from 'nostr-tools';
|
||||
import { NostrEvent } from "nostr-tools";
|
||||
|
||||
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) {
|
||||
return event.tags.find((t) => t[0] === 'summary')?.[1];
|
||||
return event.tags.find((t) => t[0] === "summary")?.[1];
|
||||
}
|
||||
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) {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { NostrEvent } from 'nostr-tools';
|
||||
import { safeRelayUrls } from './relays.js';
|
||||
import { NostrEvent } from "nostr-tools";
|
||||
import { safeRelayUrls } from "./relays.js";
|
||||
|
||||
// inbox relays can be ["r", <url>, "read"] or ["r", <url>]
|
||||
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]));
|
||||
if (fallback && urls.length === 0) return fallback;
|
||||
return urls;
|
||||
}
|
||||
|
||||
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]));
|
||||
if (fallback && urls.length === 0) return fallback;
|
||||
return urls;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { nip19, NostrEvent } from 'nostr-tools';
|
||||
import { isReplaceable } from './event.js';
|
||||
import { nip19, NostrEvent } from "nostr-tools";
|
||||
import { isReplaceable } from "./event.js";
|
||||
|
||||
export function getSharableEventAddress(event: NostrEvent, relays?: Iterable<string>) {
|
||||
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;
|
||||
return nip19.naddrEncode({
|
||||
kind: event.kind,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { NostrEvent, nip19 } from 'nostr-tools';
|
||||
import { NostrEvent, nip19 } from "nostr-tools";
|
||||
|
||||
export type Kind0ParsedContent = {
|
||||
pubkey?: string;
|
||||
@@ -17,13 +17,13 @@ export type 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 {
|
||||
const metadata = JSON.parse(event.content) as Kind0ParsedContent;
|
||||
metadata.pubkey = event.pubkey;
|
||||
|
||||
// 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
|
||||
if (metadata.website) metadata.website = fixWebsiteUrl(metadata.website);
|
||||
@@ -47,5 +47,5 @@ export function fixWebsiteUrl(website: string) {
|
||||
if (website.match(/^http?s:\/\//)) {
|
||||
return website;
|
||||
}
|
||||
return 'https://' + website;
|
||||
return "https://" + website;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export function validateRelayURL(relay: string | URL) {
|
||||
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;
|
||||
if (url.protocol !== 'wss:' && url.protocol !== 'ws:') throw new Error('Incorrect protocol');
|
||||
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;
|
||||
if (url.protocol !== "wss:" && url.protocol !== "ws:") throw new Error("Incorrect protocol");
|
||||
return url;
|
||||
}
|
||||
export function isValidRelayURL(relay: string | URL) {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { tap } from "rxjs";
|
||||
import { kinds } from "nostr-tools";
|
||||
import { MailboxesQuery } from "applesauce-core/queries";
|
||||
import { getObservableValue, simpleTimeout } from "applesauce-core/observable";
|
||||
@@ -30,14 +29,12 @@ export default class AddressBook {
|
||||
|
||||
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 });
|
||||
|
||||
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)),
|
||||
),
|
||||
queryStore
|
||||
.createQuery(MailboxesQuery, pubkey)
|
||||
.pipe(simpleTimeout(DEFAULT_REQUEST_TIMEOUT, `Failed to load mailboxes for ${pubkey}`)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { tap } from "rxjs";
|
||||
import { kinds } from "nostr-tools";
|
||||
import { ReplaceableQuery, UserContactsQuery } from "applesauce-core/queries";
|
||||
import { getObservableValue, simpleTimeout } from "applesauce-core/observable";
|
||||
@@ -36,7 +35,6 @@ export default class ContactBook {
|
||||
|
||||
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(
|
||||
@@ -49,14 +47,12 @@ export default class ContactBook {
|
||||
/** @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)),
|
||||
),
|
||||
queryStore
|
||||
.createQuery(ReplaceableQuery, kinds.Contacts, pubkey)
|
||||
.pipe(simpleTimeout(DEFAULT_REQUEST_TIMEOUT, `Failed to load contacts for ${pubkey}`)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
import { tap } from "rxjs";
|
||||
import { kinds } from "nostr-tools";
|
||||
import { getObservableValue, simpleTimeout } from "applesauce-core/observable";
|
||||
import { ProfileQuery } from "applesauce-core/queries";
|
||||
@@ -21,14 +20,12 @@ export default class ProfileBook {
|
||||
|
||||
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 });
|
||||
|
||||
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)),
|
||||
),
|
||||
queryStore
|
||||
.createQuery(ProfileQuery, pubkey)
|
||||
.pipe(simpleTimeout(DEFAULT_REQUEST_TIMEOUT, `Failed to load profile for ${pubkey}`)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { handleDeleteEvent } from './handle-delete-event.js';
|
||||
import { handleDeleteEvent } from "./handle-delete-event.js";
|
||||
|
||||
export const RelayActions = {
|
||||
handleDeleteEvent,
|
||||
|
||||
Reference in New Issue
Block a user