Merge branch 'block-globally-muted-user-to-post' into 'main'
Do not allow deactivated accounts to post Closes #118 See merge request soapbox-pub/ditto!245
This commit is contained in:
commit
f12bb4b643
|
@ -20,7 +20,7 @@
|
||||||
"@db/sqlite": "jsr:@db/sqlite@^0.11.1",
|
"@db/sqlite": "jsr:@db/sqlite@^0.11.1",
|
||||||
"@isaacs/ttlcache": "npm:@isaacs/ttlcache@^1.4.1",
|
"@isaacs/ttlcache": "npm:@isaacs/ttlcache@^1.4.1",
|
||||||
"@noble/secp256k1": "npm:@noble/secp256k1@^2.0.0",
|
"@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",
|
"@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/kysely-deno-sqlite": "jsr:@soapbox/kysely-deno-sqlite@^2.1.0",
|
||||||
"@soapbox/stickynotes": "jsr:@soapbox/stickynotes@^0.4.0",
|
"@soapbox/stickynotes": "jsr:@soapbox/stickynotes@^0.4.0",
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,6 +10,7 @@ import {
|
||||||
} from '@nostrify/nostrify';
|
} from '@nostrify/nostrify';
|
||||||
import { relayInfoController } from '@/controllers/nostr/relay-info.ts';
|
import { relayInfoController } from '@/controllers/nostr/relay-info.ts';
|
||||||
import * as pipeline from '@/pipeline.ts';
|
import * as pipeline from '@/pipeline.ts';
|
||||||
|
import { RelayError } from '@/RelayError.ts';
|
||||||
import { Storages } from '@/storages.ts';
|
import { Storages } from '@/storages.ts';
|
||||||
|
|
||||||
import type { AppController } from '@/app.ts';
|
import type { AppController } from '@/app.ts';
|
||||||
|
@ -95,7 +96,7 @@ function connectStream(socket: WebSocket) {
|
||||||
await pipeline.handleEvent(event, AbortSignal.timeout(1000));
|
await pipeline.handleEvent(event, AbortSignal.timeout(1000));
|
||||||
send(['OK', event.id, true, '']);
|
send(['OK', event.id, true, '']);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof pipeline.RelayError) {
|
if (e instanceof RelayError) {
|
||||||
send(['OK', event.id, false, e.message]);
|
send(['OK', event.id, false, e.message]);
|
||||||
} else {
|
} else {
|
||||||
send(['OK', event.id, false, 'error: something went wrong']);
|
send(['OK', event.id, false, 'error: something went wrong']);
|
||||||
|
|
|
@ -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 { LNURL } from '@nostrify/nostrify/ln';
|
||||||
|
import { PipePolicy } from '@nostrify/nostrify/policies';
|
||||||
import Debug from '@soapbox/stickynotes/debug';
|
import Debug from '@soapbox/stickynotes/debug';
|
||||||
import { sql } from 'kysely';
|
import { sql } from 'kysely';
|
||||||
|
|
||||||
|
@ -9,6 +10,7 @@ import { deleteAttachedMedia } from '@/db/unattached-media.ts';
|
||||||
import { DittoEvent } from '@/interfaces/DittoEvent.ts';
|
import { DittoEvent } from '@/interfaces/DittoEvent.ts';
|
||||||
import { isEphemeralKind } from '@/kinds.ts';
|
import { isEphemeralKind } from '@/kinds.ts';
|
||||||
import { DVM } from '@/pipeline/DVM.ts';
|
import { DVM } from '@/pipeline/DVM.ts';
|
||||||
|
import { RelayError } from '@/RelayError.ts';
|
||||||
import { updateStats } from '@/stats.ts';
|
import { updateStats } from '@/stats.ts';
|
||||||
import { hydrateEvents, purifyEvent } from '@/storages/hydrate.ts';
|
import { hydrateEvents, purifyEvent } from '@/storages/hydrate.ts';
|
||||||
import { Storages } from '@/storages.ts';
|
import { Storages } from '@/storages.ts';
|
||||||
|
@ -21,18 +23,10 @@ import { AdminSigner } from '@/signers/AdminSigner.ts';
|
||||||
import { lnurlCache } from '@/utils/lnurl.ts';
|
import { lnurlCache } from '@/utils/lnurl.ts';
|
||||||
import { nip05Cache } from '@/utils/nip05.ts';
|
import { nip05Cache } from '@/utils/nip05.ts';
|
||||||
|
|
||||||
|
import { MuteListPolicy } from '@/policies/MuteListPolicy.ts';
|
||||||
|
|
||||||
const debug = Debug('ditto:pipeline');
|
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.
|
* Common pipeline function to process (and maybe store) events.
|
||||||
* It is idempotent, so it can be called multiple times for the same event.
|
* 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<void
|
||||||
if (!(await verifyEventWorker(event))) return;
|
if (!(await verifyEventWorker(event))) return;
|
||||||
if (await encounterEvent(event, signal)) return;
|
if (await encounterEvent(event, signal)) return;
|
||||||
debug(`NostrEvent<${event.kind}> ${event.id}`);
|
debug(`NostrEvent<${event.kind}> ${event.id}`);
|
||||||
await hydrateEvent(event, signal);
|
|
||||||
|
|
||||||
if (UserPolicy) {
|
if (event.kind !== 24133) {
|
||||||
const result = await new UserPolicy().call(event, signal);
|
await policyFilter(event);
|
||||||
debug(JSON.stringify(result));
|
|
||||||
const [_, _eventId, ok, reason] = result;
|
|
||||||
if (!ok) {
|
|
||||||
const [prefix, ...rest] = reason.split(': ');
|
|
||||||
throw new RelayError(prefix, rest.join(': '));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await hydrateEvent(event, signal);
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
storeEvent(event, signal),
|
storeEvent(event, signal),
|
||||||
parseMetadata(event, signal),
|
parseMetadata(event, signal),
|
||||||
|
@ -66,6 +55,25 @@ async function handleEvent(event: DittoEvent, signal: AbortSignal): Promise<void
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function policyFilter(event: NostrEvent): Promise<void> {
|
||||||
|
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. */
|
/** Encounter the event, and return whether it has already been encountered. */
|
||||||
async function encounterEvent(event: NostrEvent, signal: AbortSignal): Promise<boolean> {
|
async function encounterEvent(event: NostrEvent, signal: AbortSignal): Promise<boolean> {
|
||||||
const [existing] = await Storages.cache.query([{ ids: [event.id], limit: 1 }]);
|
const [existing] = await Storages.cache.query([{ ids: [event.id], limit: 1 }]);
|
||||||
|
@ -270,11 +278,4 @@ async function streamOut(event: NostrEvent): Promise<void> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** NIP-20 command line result. */
|
export { handleEvent };
|
||||||
class RelayError extends Error {
|
|
||||||
constructor(prefix: 'duplicate' | 'pow' | 'blocked' | 'rate-limited' | 'invalid' | 'error', message: string) {
|
|
||||||
super(`${prefix}: ${message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export { handleEvent, RelayError };
|
|
||||||
|
|
|
@ -27,7 +27,7 @@ Deno.test('block event: muted user cannot post', async () => {
|
||||||
|
|
||||||
const ok = await policy.call(event1authorUserMeCopy);
|
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 () => {
|
Deno.test('allow event: user is NOT muted because there is no muted event', async () => {
|
||||||
|
|
|
@ -10,7 +10,7 @@ export class MuteListPolicy implements NPolicy {
|
||||||
const pubkeys = getTagSet(muteList?.tags ?? [], 'p');
|
const pubkeys = getTagSet(muteList?.tags ?? [], 'p');
|
||||||
|
|
||||||
if (pubkeys.has(event.pubkey)) {
|
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, ''];
|
return ['OK', event.id, true, ''];
|
||||||
|
|
|
@ -9,6 +9,7 @@ import { z } from 'zod';
|
||||||
import { type AppContext } from '@/app.ts';
|
import { type AppContext } from '@/app.ts';
|
||||||
import { Conf } from '@/config.ts';
|
import { Conf } from '@/config.ts';
|
||||||
import * as pipeline from '@/pipeline.ts';
|
import * as pipeline from '@/pipeline.ts';
|
||||||
|
import { RelayError } from '@/RelayError.ts';
|
||||||
import { AdminSigner } from '@/signers/AdminSigner.ts';
|
import { AdminSigner } from '@/signers/AdminSigner.ts';
|
||||||
import { APISigner } from '@/signers/APISigner.ts';
|
import { APISigner } from '@/signers/APISigner.ts';
|
||||||
import { Storages } from '@/storages.ts';
|
import { Storages } from '@/storages.ts';
|
||||||
|
@ -103,12 +104,10 @@ async function updateAdminEvent<E extends EventStub>(
|
||||||
async function publishEvent(event: NostrEvent, c: AppContext): Promise<NostrEvent> {
|
async function publishEvent(event: NostrEvent, c: AppContext): Promise<NostrEvent> {
|
||||||
debug('EVENT', event);
|
debug('EVENT', event);
|
||||||
try {
|
try {
|
||||||
await Promise.all([
|
await pipeline.handleEvent(event, c.req.raw.signal);
|
||||||
pipeline.handleEvent(event, c.req.raw.signal),
|
await Storages.client.event(event);
|
||||||
Storages.client.event(event),
|
|
||||||
]);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof pipeline.RelayError) {
|
if (e instanceof RelayError) {
|
||||||
throw new HTTPException(422, {
|
throw new HTTPException(422, {
|
||||||
res: c.json({ error: e.message }, 422),
|
res: c.json({ error: e.message }, 422),
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue