diff --git a/src/policies/anti-duplication-policy.ts b/src/policies/anti-duplication-policy.ts index a9f15bf..7db9701 100755 --- a/src/policies/anti-duplication-policy.ts +++ b/src/policies/anti-duplication-policy.ts @@ -2,23 +2,33 @@ import { Keydb } from '../deps.ts'; import type { Policy } from '../types.ts'; -const ANTI_DUPLICATION_TTL = Number(Deno.env.get('ANTI_DUPLICATION_TTL') || 60000); -const ANTI_DUPLICATION_MIN_LENGTH = Number(Deno.env.get('ANTI_DUPLICATION_MIN_LENGTH') || 50); +interface AntiDuplication { + /** Time in ms until a message with this content may be posted again. Default: `60000` (1 minute). */ + ttl?: number; + /** Note text under this limit will be skipped by the policy. Default: `50`. */ + minLength?: number; + /** Database connection string. Default: `sqlite:///tmp/strfry-anti-duplication-policy.sqlite3` */ + databaseUrl?: string; +} /** * Prevent messages with the exact same content from being submitted repeatedly. * It stores a hashcode for each content in an SQLite database and rate-limits them. * Only messages that meet the minimum length criteria are selected. */ -const antiDuplicationPolicy: Policy = async (msg) => { +const antiDuplicationPolicy: Policy = async (msg, opts) => { + const ttl = opts?.ttl ?? 60000; + const minLength = opts?.minLength ?? 50; + const databaseUrl = opts?.databaseUrl || 'sqlite:///tmp/strfry-anti-duplication-policy.sqlite3'; + const { kind, content } = msg.event; - if (kind === 1 && content.length >= ANTI_DUPLICATION_MIN_LENGTH) { - const db = new Keydb('sqlite:///tmp/strfry-anti-duplication-policy.sqlite3'); + if (kind === 1 && content.length >= minLength) { + const db = new Keydb(databaseUrl); const hash = String(hashCode(content)); if (await db.get(hash)) { - await db.set(hash, 1, ANTI_DUPLICATION_TTL); + await db.set(hash, 1, ttl); return { id: msg.event.id, action: 'shadowReject', @@ -26,7 +36,7 @@ const antiDuplicationPolicy: Policy = async (msg) => { }; } - await db.set(hash, 1, ANTI_DUPLICATION_TTL); + await db.set(hash, 1, ttl); } return { @@ -52,3 +62,5 @@ function hashCode(str: string): number { } export default antiDuplicationPolicy; + +export type { AntiDuplication }; diff --git a/src/policies/hellthread-policy.ts b/src/policies/hellthread-policy.ts index bb980ee..9f05171 100755 --- a/src/policies/hellthread-policy.ts +++ b/src/policies/hellthread-policy.ts @@ -1,12 +1,13 @@ import type { Policy } from '../types.ts'; interface Hellthread { - limit: number; + /** Total number of "p" tags a kind 1 note may have before it's rejected. Default: `100` */ + limit?: number; } /** Reject messages that tag too many participants. */ const hellthreadPolicy: Policy = (msg, opts) => { - const limit = opts?.limit || 100; + const limit = opts?.limit ?? 100; if (msg.event.kind === 1) { const p = msg.event.tags.filter((tag) => tag[0] === 'p'); @@ -28,3 +29,5 @@ const hellthreadPolicy: Policy = (msg, opts) => { }; export default hellthreadPolicy; + +export type { Hellthread }; diff --git a/src/policies/noop-policy.ts b/src/policies/noop-policy.ts index a930ef9..0d5e86d 100755 --- a/src/policies/noop-policy.ts +++ b/src/policies/noop-policy.ts @@ -4,7 +4,7 @@ import type { Policy } from '../types.ts'; * Minimal sample policy for demonstration purposes. * Allows all events through. */ -const noopPolicy: Policy = (msg) => ({ +const noopPolicy: Policy = (msg) => ({ id: msg.event.id, action: 'accept', msg: '', diff --git a/src/policies/rate-limit-policy.ts b/src/policies/rate-limit-policy.ts index a6810bb..f368bd6 100755 --- a/src/policies/rate-limit-policy.ts +++ b/src/policies/rate-limit-policy.ts @@ -2,23 +2,34 @@ import { Keydb } from '../deps.ts'; import type { Policy } from '../types.ts'; -const IP_WHITELIST = (Deno.env.get('IP_WHITELIST') || '').split(','); - -const RATE_LIMIT_INTERVAL = Number(Deno.env.get('RATE_LIMIT_INTERVAL') || 60000); -const RATE_LIMIT_MAX = Number(Deno.env.get('RATE_LIMIT_MAX') || 10); +interface RateLimit { + /** How often (ms) to check whether `max` has been exceeded. Default: `60000` (1 minute). */ + interval?: number; + /** Max number of requests within the `interval` until the IP is rate-limited. Default: `10`. */ + max?: number; + /** List of IP addresses to skip this policy. */ + whitelist?: string[]; + /** Database connection string. Default: `sqlite:///tmp/strfry-rate-limit-policy.sqlite3` */ + databaseUrl?: string; +} /** * Rate-limits users by their IP address. * IPs are stored in an SQLite database. If you are running internal services, * it's a good idea to at least whitelist `127.0.0.1` etc. */ -const rateLimitPolicy: Policy = async (msg) => { - if ((msg.sourceType === 'IP4' || msg.sourceType === 'IP6') && !IP_WHITELIST.includes(msg.sourceInfo)) { - const db = new Keydb('sqlite:///tmp/strfry-rate-limit-policy.sqlite3'); - const count = await db.get(msg.sourceInfo) || 0; - await db.set(msg.sourceInfo, count + 1, RATE_LIMIT_INTERVAL); +const rateLimitPolicy: Policy = async (msg, opts) => { + const interval = opts?.interval ?? 60000; + const max = opts?.max ?? 10; + const whitelist = opts?.whitelist || []; + const databaseUrl = opts?.databaseUrl || 'sqlite:///tmp/strfry-rate-limit-policy.sqlite3'; - if (count >= RATE_LIMIT_MAX) { + if ((msg.sourceType === 'IP4' || msg.sourceType === 'IP6') && !whitelist.includes(msg.sourceInfo)) { + const db = new Keydb(databaseUrl); + const count = await db.get(msg.sourceInfo) || 0; + await db.set(msg.sourceInfo, count + 1, interval); + + if (count >= max) { return { id: msg.event.id, action: 'reject', @@ -35,3 +46,5 @@ const rateLimitPolicy: Policy = async (msg) => { }; export default rateLimitPolicy; + +export type { RateLimit }; diff --git a/src/policies/read-only-policy.ts b/src/policies/read-only-policy.ts index 996c27b..34fb709 100755 --- a/src/policies/read-only-policy.ts +++ b/src/policies/read-only-policy.ts @@ -1,7 +1,7 @@ import type { Policy } from '../types.ts'; /** This policy rejects all messages. */ -const readOnlyPolicy: Policy = (msg) => ({ +const readOnlyPolicy: Policy = (msg) => ({ id: msg.event.id, action: 'reject', msg: 'The relay is read-only.',