diff --git a/deno.json b/deno.json index 5d45588..0225510 100644 --- a/deno.json +++ b/deno.json @@ -20,7 +20,7 @@ "@db/sqlite": "jsr:@db/sqlite@^0.11.1", "@isaacs/ttlcache": "npm:@isaacs/ttlcache@^1.4.1", "@noble/secp256k1": "npm:@noble/secp256k1@^2.0.0", - "@nostrify/nostrify": "jsr:@nostrify/nostrify@^0.17.1", + "@nostrify/nostrify": "jsr:@nostrify/nostrify@^0.19.0", "@sentry/deno": "https://deno.land/x/sentry@7.112.2/index.mjs", "@soapbox/kysely-deno-sqlite": "jsr:@soapbox/kysely-deno-sqlite@^2.1.0", "@soapbox/stickynotes": "jsr:@soapbox/stickynotes@^0.4.0", diff --git a/src/RelayError.ts b/src/RelayError.ts new file mode 100644 index 0000000..1d275f6 --- /dev/null +++ b/src/RelayError.ts @@ -0,0 +1,24 @@ +import { NostrRelayOK } from '@nostrify/nostrify'; + +export type RelayErrorPrefix = 'duplicate' | 'pow' | 'blocked' | 'rate-limited' | 'invalid' | 'error'; + +/** NIP-01 command line result. */ +export class RelayError extends Error { + constructor(prefix: RelayErrorPrefix, message: string) { + super(`${prefix}: ${message}`); + } + + /** Construct a RelayError from the reason message. */ + static fromReason(reason: string): RelayError { + const [prefix, ...rest] = reason.split(': '); + return new RelayError(prefix as RelayErrorPrefix, rest.join(': ')); + } + + /** Throw a new RelayError if the OK message is false. */ + static assert(msg: NostrRelayOK): void { + const [_, _eventId, ok, reason] = msg; + if (!ok) { + throw RelayError.fromReason(reason); + } + } +} diff --git a/src/controllers/nostr/relay.ts b/src/controllers/nostr/relay.ts index 7d70ad9..c0fa026 100644 --- a/src/controllers/nostr/relay.ts +++ b/src/controllers/nostr/relay.ts @@ -10,6 +10,7 @@ import { } from '@nostrify/nostrify'; import { relayInfoController } from '@/controllers/nostr/relay-info.ts'; import * as pipeline from '@/pipeline.ts'; +import { RelayError } from '@/RelayError.ts'; import { Storages } from '@/storages.ts'; import type { AppController } from '@/app.ts'; @@ -95,7 +96,7 @@ function connectStream(socket: WebSocket) { await pipeline.handleEvent(event, AbortSignal.timeout(1000)); send(['OK', event.id, true, '']); } catch (e) { - if (e instanceof pipeline.RelayError) { + if (e instanceof RelayError) { send(['OK', event.id, false, e.message]); } else { send(['OK', event.id, false, 'error: something went wrong']); diff --git a/src/pipeline.ts b/src/pipeline.ts index 3eb8913..b3eea25 100644 --- a/src/pipeline.ts +++ b/src/pipeline.ts @@ -1,5 +1,6 @@ -import { NostrEvent, NSchema as n } from '@nostrify/nostrify'; +import { NostrEvent, NPolicy, NSchema as n } from '@nostrify/nostrify'; import { LNURL } from '@nostrify/nostrify/ln'; +import { PipePolicy } from '@nostrify/nostrify/policies'; import Debug from '@soapbox/stickynotes/debug'; import { sql } from 'kysely'; @@ -9,6 +10,7 @@ import { deleteAttachedMedia } from '@/db/unattached-media.ts'; import { DittoEvent } from '@/interfaces/DittoEvent.ts'; import { isEphemeralKind } from '@/kinds.ts'; import { DVM } from '@/pipeline/DVM.ts'; +import { RelayError } from '@/RelayError.ts'; import { updateStats } from '@/stats.ts'; import { hydrateEvents, purifyEvent } from '@/storages/hydrate.ts'; import { Storages } from '@/storages.ts'; @@ -21,18 +23,10 @@ import { AdminSigner } from '@/signers/AdminSigner.ts'; import { lnurlCache } from '@/utils/lnurl.ts'; import { nip05Cache } from '@/utils/nip05.ts'; +import { MuteListPolicy } from '@/policies/MuteListPolicy.ts'; + const debug = Debug('ditto:pipeline'); -let UserPolicy: any; - -try { - UserPolicy = (await import('../data/policy.ts')).default; - debug('policy loaded from data/policy.ts'); -} catch (_e) { - // do nothing - debug('policy not found'); -} - /** * Common pipeline function to process (and maybe store) events. * It is idempotent, so it can be called multiple times for the same event. @@ -41,18 +35,13 @@ async function handleEvent(event: DittoEvent, signal: AbortSignal): Promise ${event.id}`); - await hydrateEvent(event, signal); - if (UserPolicy) { - const result = await new UserPolicy().call(event, signal); - debug(JSON.stringify(result)); - const [_, _eventId, ok, reason] = result; - if (!ok) { - const [prefix, ...rest] = reason.split(': '); - throw new RelayError(prefix, rest.join(': ')); - } + if (event.kind !== 24133) { + await policyFilter(event); } + await hydrateEvent(event, signal); + await Promise.all([ storeEvent(event, signal), parseMetadata(event, signal), @@ -66,6 +55,25 @@ async function handleEvent(event: DittoEvent, signal: AbortSignal): Promise { + const policies: NPolicy[] = [ + new MuteListPolicy(Conf.pubkey, Storages.admin), + ]; + + try { + const CustomPolicy = (await import('../data/policy.ts')).default; + policies.push(new CustomPolicy()); + } catch (_e) { + debug('policy not found - https://docs.soapbox.pub/ditto/policies/'); + } + + const policy = new PipePolicy(policies.reverse()); + + const result = await policy.call(event); + debug(JSON.stringify(result)); + RelayError.assert(result); +} + /** Encounter the event, and return whether it has already been encountered. */ async function encounterEvent(event: NostrEvent, signal: AbortSignal): Promise { const [existing] = await Storages.cache.query([{ ids: [event.id], limit: 1 }]); @@ -270,11 +278,4 @@ async function streamOut(event: NostrEvent): Promise { } } -/** NIP-20 command line result. */ -class RelayError extends Error { - constructor(prefix: 'duplicate' | 'pow' | 'blocked' | 'rate-limited' | 'invalid' | 'error', message: string) { - super(`${prefix}: ${message}`); - } -} - -export { handleEvent, RelayError }; +export { handleEvent }; diff --git a/src/policies/MuteListPolicy.test.ts b/src/policies/MuteListPolicy.test.ts index 69561f8..2c3baa3 100644 --- a/src/policies/MuteListPolicy.test.ts +++ b/src/policies/MuteListPolicy.test.ts @@ -27,7 +27,7 @@ Deno.test('block event: muted user cannot post', async () => { const ok = await policy.call(event1authorUserMeCopy); - assertEquals(ok, ['OK', event1authorUserMeCopy.id, false, 'You are banned in this server.']); + assertEquals(ok, ['OK', event1authorUserMeCopy.id, false, 'blocked: Your account has been deactivated.']); }); Deno.test('allow event: user is NOT muted because there is no muted event', async () => { diff --git a/src/policies/MuteListPolicy.ts b/src/policies/MuteListPolicy.ts index 1db8556..cae08eb 100644 --- a/src/policies/MuteListPolicy.ts +++ b/src/policies/MuteListPolicy.ts @@ -10,7 +10,7 @@ export class MuteListPolicy implements NPolicy { const pubkeys = getTagSet(muteList?.tags ?? [], 'p'); if (pubkeys.has(event.pubkey)) { - return ['OK', event.id, false, 'You are banned in this server.']; + return ['OK', event.id, false, 'blocked: Your account has been deactivated.']; } return ['OK', event.id, true, '']; diff --git a/src/utils/api.ts b/src/utils/api.ts index 3dfdfa3..81b157c 100644 --- a/src/utils/api.ts +++ b/src/utils/api.ts @@ -9,6 +9,7 @@ import { z } from 'zod'; import { type AppContext } from '@/app.ts'; import { Conf } from '@/config.ts'; import * as pipeline from '@/pipeline.ts'; +import { RelayError } from '@/RelayError.ts'; import { AdminSigner } from '@/signers/AdminSigner.ts'; import { Storages } from '@/storages.ts'; import { nostrNow } from '@/utils.ts'; @@ -108,12 +109,10 @@ async function updateAdminEvent( async function publishEvent(event: NostrEvent, c: AppContext): Promise { debug('EVENT', event); try { - await Promise.all([ - pipeline.handleEvent(event, c.req.raw.signal), - Storages.client.event(event), - ]); + await pipeline.handleEvent(event, c.req.raw.signal); + await Storages.client.event(event); } catch (e) { - if (e instanceof pipeline.RelayError) { + if (e instanceof RelayError) { throw new HTTPException(422, { res: c.json({ error: e.message }, 422), });