Merge branch 'main' into zap-notification-streaming

This commit is contained in:
P. Reis 2024-09-17 17:06:45 -03:00
commit 373e9ca6d8
10 changed files with 35 additions and 14 deletions

View file

@ -77,6 +77,10 @@ class Conf {
static get testDatabaseUrl(): string { static get testDatabaseUrl(): string {
return Deno.env.get('TEST_DATABASE_URL') ?? 'memory://'; return Deno.env.get('TEST_DATABASE_URL') ?? 'memory://';
} }
/** PGlite debug level. 0 disables logging. */
static get pgliteDebug(): 0 | 1 | 2 | 3 | 4 | 5 {
return Number(Deno.env.get('PGLITE_DEBUG') || 0) as 0 | 1 | 2 | 3 | 4 | 5;
}
static db = { static db = {
/** Database query timeout configurations. */ /** Database query timeout configurations. */
timeouts: { timeouts: {

View file

@ -381,7 +381,7 @@ const followersController: AppController = (c) => {
const followingController: AppController = async (c) => { const followingController: AppController = async (c) => {
const pubkey = c.req.param('pubkey'); const pubkey = c.req.param('pubkey');
const pubkeys = await getFollowedPubkeys(pubkey); const pubkeys = await getFollowedPubkeys(pubkey);
return renderAccounts(c, pubkeys); return renderAccounts(c, [...pubkeys]);
}; };
/** https://docs.joinmastodon.org/methods/accounts/#block */ /** https://docs.joinmastodon.org/methods/accounts/#block */
@ -460,7 +460,7 @@ const familiarFollowersController: AppController = async (c) => {
const follows = await getFollowedPubkeys(pubkey); const follows = await getFollowedPubkeys(pubkey);
const results = await Promise.all(ids.map(async (id) => { const results = await Promise.all(ids.map(async (id) => {
const followLists = await store.query([{ kinds: [3], authors: follows, '#p': [id] }]) const followLists = await store.query([{ kinds: [3], authors: [...follows], '#p': [id] }])
.then((events) => hydrateEvents({ events, store })); .then((events) => hydrateEvents({ events, store }));
const accounts = await Promise.all( const accounts = await Promise.all(

View file

@ -215,7 +215,7 @@ async function topicToFilter(
// HACK: this puts the user's entire contacts list into RAM, // HACK: this puts the user's entire contacts list into RAM,
// and then calls `matchFilters` over it. Refreshing the page // and then calls `matchFilters` over it. Refreshing the page
// is required after following a new user. // is required after following a new user.
return pubkey ? { kinds: [1, 6, 9735], authors: await getFeedPubkeys(pubkey) } : undefined; return pubkey ? { kinds: [1, 6], authors: [...await getFeedPubkeys(pubkey)] } : undefined;
} }
} }

View file

@ -13,7 +13,7 @@ import { renderReblog, renderStatus } from '@/views/mastodon/statuses.ts';
const homeTimelineController: AppController = async (c) => { const homeTimelineController: AppController = async (c) => {
const params = c.get('pagination'); const params = c.get('pagination');
const pubkey = await c.get('signer')?.getPublicKey()!; const pubkey = await c.get('signer')?.getPublicKey()!;
const authors = await getFeedPubkeys(pubkey); const authors = [...await getFeedPubkeys(pubkey)];
return renderStatuses(c, [{ authors, kinds: [1, 6], ...params }]); return renderStatuses(c, [{ authors, kinds: [1, 6], ...params }]);
}; };

View file

@ -16,7 +16,7 @@ export class DittoDB {
switch (protocol) { switch (protocol) {
case 'file:': case 'file:':
case 'memory:': case 'memory:':
return DittoPglite.create(databaseUrl); return DittoPglite.create(databaseUrl, opts);
case 'postgres:': case 'postgres:':
case 'postgresql:': case 'postgresql:':
return DittoPostgres.create(databaseUrl, opts); return DittoPostgres.create(databaseUrl, opts);

View file

@ -10,4 +10,5 @@ export interface DittoDatabase {
export interface DittoDatabaseOpts { export interface DittoDatabaseOpts {
poolSize?: number; poolSize?: number;
debug?: 0 | 1 | 2 | 3 | 4 | 5;
} }

View file

@ -3,15 +3,18 @@ import { pg_trgm } from '@electric-sql/pglite/contrib/pg_trgm';
import { PgliteDialect } from '@soapbox/kysely-pglite'; import { PgliteDialect } from '@soapbox/kysely-pglite';
import { Kysely } from 'kysely'; import { Kysely } from 'kysely';
import { DittoDatabase } from '@/db/DittoDatabase.ts'; import { DittoDatabase, DittoDatabaseOpts } from '@/db/DittoDatabase.ts';
import { DittoTables } from '@/db/DittoTables.ts'; import { DittoTables } from '@/db/DittoTables.ts';
import { KyselyLogger } from '@/db/KyselyLogger.ts'; import { KyselyLogger } from '@/db/KyselyLogger.ts';
export class DittoPglite { export class DittoPglite {
static create(databaseUrl: string): DittoDatabase { static create(databaseUrl: string, opts?: DittoDatabaseOpts): DittoDatabase {
const kysely = new Kysely<DittoTables>({ const kysely = new Kysely<DittoTables>({
dialect: new PgliteDialect({ dialect: new PgliteDialect({
database: new PGlite(databaseUrl, { extensions: { pg_trgm } }), database: new PGlite(databaseUrl, {
extensions: { pg_trgm },
debug: opts?.debug,
}),
}), }),
log: KyselyLogger, log: KyselyLogger,
}); });

View file

@ -40,9 +40,14 @@ async function handleEvent(event: DittoEvent, signal: AbortSignal): Promise<void
if (!(await verifyEventWorker(event))) return; if (!(await verifyEventWorker(event))) return;
if (encounterEvent(event)) return; if (encounterEvent(event)) return;
if (await existsInDB(event)) return; if (await existsInDB(event)) return;
debug(`NostrEvent<${event.kind}> ${event.id}`); debug(`NostrEvent<${event.kind}> ${event.id}`);
pipelineEventsCounter.inc({ kind: event.kind }); pipelineEventsCounter.inc({ kind: event.kind });
if (isProtectedEvent(event)) {
throw new RelayError('invalid', 'protected event');
}
if (event.kind !== 24133) { if (event.kind !== 24133) {
await policyFilter(event); await policyFilter(event);
} }
@ -103,6 +108,11 @@ async function existsInDB(event: DittoEvent): Promise<boolean> {
return events.length > 0; return events.length > 0;
} }
/** Check whether the event has a NIP-70 `-` tag. */
function isProtectedEvent(event: NostrEvent): boolean {
return event.tags.some(([name]) => name === '-');
}
/** Hydrate the event with the user, if applicable. */ /** Hydrate the event with the user, if applicable. */
async function hydrateEvent(event: DittoEvent, signal: AbortSignal): Promise<void> { async function hydrateEvent(event: DittoEvent, signal: AbortSignal): Promise<void> {
await hydrateEvents({ events: [event], store: await Storages.db(), signal }); await hydrateEvents({ events: [event], store: await Storages.db(), signal });

View file

@ -56,16 +56,16 @@ const getFollows = async (pubkey: string, signal?: AbortSignal): Promise<NostrEv
}; };
/** Get pubkeys the user follows. */ /** Get pubkeys the user follows. */
async function getFollowedPubkeys(pubkey: string, signal?: AbortSignal): Promise<string[]> { async function getFollowedPubkeys(pubkey: string, signal?: AbortSignal): Promise<Set<string>> {
const event = await getFollows(pubkey, signal); const event = await getFollows(pubkey, signal);
if (!event) return []; if (!event) return new Set();
return [...getTagSet(event.tags, 'p')]; return getTagSet(event.tags, 'p');
} }
/** Get pubkeys the user follows, including the user's own pubkey. */ /** Get pubkeys the user follows, including the user's own pubkey. */
async function getFeedPubkeys(pubkey: string): Promise<string[]> { async function getFeedPubkeys(pubkey: string): Promise<Set<string>> {
const authors = await getFollowedPubkeys(pubkey); const authors = await getFollowedPubkeys(pubkey);
return [...authors, pubkey]; return authors.add(pubkey);
} }
async function getAncestors(store: NStore, event: NostrEvent, result: NostrEvent[] = []): Promise<NostrEvent[]> { async function getAncestors(store: NStore, event: NostrEvent, result: NostrEvent[] = []): Promise<NostrEvent[]> {

View file

@ -21,7 +21,10 @@ export class Storages {
public static async database(): Promise<DittoDatabase> { public static async database(): Promise<DittoDatabase> {
if (!this._database) { if (!this._database) {
this._database = (async () => { this._database = (async () => {
const db = DittoDB.create(Conf.databaseUrl, { poolSize: Conf.pg.poolSize }); const db = DittoDB.create(Conf.databaseUrl, {
poolSize: Conf.pg.poolSize,
debug: Conf.pgliteDebug,
});
await DittoDB.migrate(db.kysely); await DittoDB.migrate(db.kysely);
return db; return db;
})(); })();