mirror of
https://gitlab.com/soapbox-pub/ditto.git
synced 2025-12-06 03:19:46 +00:00
Merge branch 'admin-dashboard-updates' into 'main'
Admin dashboard updates See merge request soapbox-pub/ditto!731
This commit is contained in:
commit
b29cc27858
15 changed files with 1272 additions and 1080 deletions
|
|
@ -18,7 +18,7 @@
|
|||
],
|
||||
"tasks": {
|
||||
"start": "deno run -A --env-file --deny-read=.env packages/ditto/server.ts",
|
||||
"dev": "deno run -A --env-file --deny-read=.env --watch packages/ditto/server.ts",
|
||||
"dev": "deno run -A --env-file --watch packages/ditto/server.ts",
|
||||
"hook": "deno run --allow-read --allow-run --allow-write https://deno.land/x/deno_hooks@0.1.1/mod.ts",
|
||||
"db:export": "deno run -A --env-file --deny-read=.env scripts/db-export.ts",
|
||||
"db:import": "deno run -A --env-file --deny-read=.env scripts/db-import.ts",
|
||||
|
|
@ -82,6 +82,7 @@
|
|||
"@std/fs": "jsr:@std/fs@^0.229.3",
|
||||
"@std/json": "jsr:@std/json@^0.223.0",
|
||||
"@std/media-types": "jsr:@std/media-types@^0.224.1",
|
||||
"@std/path": "jsr:@std/path@^1.0.8",
|
||||
"@std/streams": "jsr:@std/streams@^0.223.0",
|
||||
"@std/testing": "jsr:@std/testing@^1.0.9",
|
||||
"blurhash": "npm:blurhash@2.0.5",
|
||||
|
|
|
|||
9
fixtures/policy.ts
Normal file
9
fixtures/policy.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import { NostrEvent, NostrRelayInfo, NostrRelayOK, NPolicy } from '@nostrify/types';
|
||||
import { HashtagPolicy } from '@nostrify/policies';
|
||||
|
||||
export default class TestPolicy implements NPolicy {
|
||||
call(event: NostrEvent): Promise<NostrRelayOK> {
|
||||
return new HashtagPolicy(['other-blocked-tag']).call(event);
|
||||
}
|
||||
info?: NostrRelayInfo | undefined;
|
||||
}
|
||||
|
|
@ -152,6 +152,12 @@ import dittoNamesRoute from '@/routes/dittoNamesRoute.ts';
|
|||
import pleromaAdminPermissionGroupsRoute from '@/routes/pleromaAdminPermissionGroupsRoute.ts';
|
||||
import pleromaStatusesRoute from '@/routes/pleromaStatusesRoute.ts';
|
||||
import { DittoRelayStore } from '@/storages/DittoRelayStore.ts';
|
||||
import {
|
||||
adminCurrentPolicyController,
|
||||
adminListPoliciesController,
|
||||
adminUpdatePolicyController,
|
||||
} from '@/controllers/api/policies.ts';
|
||||
import { createPolicyEvent, DEFAULT_POLICY_SPEC } from '@/utils/policies/mod.ts';
|
||||
|
||||
export interface AppEnv extends DittoEnv {
|
||||
Variables: DittoEnv['Variables'] & {
|
||||
|
|
@ -198,6 +204,11 @@ const pgstore = new DittoPgStore({
|
|||
|
||||
const pool = new DittoPool({ conf, relay: pgstore });
|
||||
const relay = new DittoRelayStore({ db, conf, pool, relay: pgstore });
|
||||
const havePolicy = await relay.count([{ kinds: [11984], authors: [await conf.signer.getPublicKey()] }]);
|
||||
|
||||
if (!havePolicy.count) {
|
||||
await relay.event(await createPolicyEvent(conf, DEFAULT_POLICY_SPEC));
|
||||
}
|
||||
|
||||
await createNip89({ conf, relay });
|
||||
await seedZapSplits({ conf, relay });
|
||||
|
|
@ -498,6 +509,10 @@ app.delete('/api/v1/pleroma/admin/users/tag', userMiddleware({ role: 'admin' }),
|
|||
app.patch('/api/v1/pleroma/admin/users/suggest', userMiddleware({ role: 'admin' }), pleromaAdminSuggestController);
|
||||
app.patch('/api/v1/pleroma/admin/users/unsuggest', userMiddleware({ role: 'admin' }), pleromaAdminUnsuggestController);
|
||||
|
||||
app.get('/api/v1/admin/ditto/policies', userMiddleware({ role: 'admin' }), adminListPoliciesController);
|
||||
app.get('/api/v1/admin/ditto/policies/current', userMiddleware({ role: 'admin' }), adminCurrentPolicyController);
|
||||
app.put('/api/v1/admin/ditto/policies/current', userMiddleware({ role: 'admin' }), adminUpdatePolicyController);
|
||||
|
||||
app.route('/api/v1/custom_emojis', customEmojisRoute);
|
||||
|
||||
// Not (yet) implemented.
|
||||
|
|
@ -560,5 +575,4 @@ app.get('*', publicFiles, staticFiles, ratelimit, frontendController);
|
|||
app.onError(errorHandler);
|
||||
|
||||
export default app;
|
||||
|
||||
export type { AppContext, AppController, AppMiddleware };
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import denoJson from 'deno.json' with { type: 'json' };
|
|||
|
||||
import { AppController } from '@/app.ts';
|
||||
import { getInstanceMetadata } from '@/utils/instance.ts';
|
||||
import { logi } from '@soapbox/logi';
|
||||
|
||||
const version = `3.0.0 (compatible; Ditto ${denoJson.version})`;
|
||||
|
||||
|
|
@ -15,13 +16,60 @@ const features = [
|
|||
'v2_suggestions',
|
||||
];
|
||||
|
||||
const cache = (f: () => Promise<number | undefined>, interval: number) => {
|
||||
let lastCheck = 0;
|
||||
let value: number | undefined;
|
||||
|
||||
return async () => {
|
||||
const now = Date.now();
|
||||
if (value === undefined || now - lastCheck > interval) {
|
||||
lastCheck = now;
|
||||
try {
|
||||
value = await f();
|
||||
} catch (error) {
|
||||
logi({
|
||||
level: 'error',
|
||||
ns: 'ditto.routes.instanceV1',
|
||||
message: `Error fetching cached value: ${error}`,
|
||||
});
|
||||
value = undefined; // Ensure we retry next time
|
||||
}
|
||||
}
|
||||
return value ?? 0; // Prevent returning undefined
|
||||
};
|
||||
};
|
||||
|
||||
const instanceV1Controller: AppController = async (c) => {
|
||||
const { conf } = c.var;
|
||||
const { conf, db } = c.var;
|
||||
const { host, protocol } = conf.url;
|
||||
const meta = await getInstanceMetadata(c.var);
|
||||
|
||||
/** Protocol to use for WebSocket URLs, depending on the protocol of the `LOCAL_DOMAIN`. */
|
||||
const wsProtocol = protocol === 'http:' ? 'ws:' : 'wss:';
|
||||
const MINS_10 = 10 * 60 * 1000;
|
||||
|
||||
const userCount = cache(async () => {
|
||||
return await db.kysely
|
||||
.selectFrom('author_stats')
|
||||
.where('nip05_domain', '=', host)
|
||||
.select(({ fn }) => fn.count<number>('pubkey').distinct().as('users'))
|
||||
.executeTakeFirst().then((obj) => obj?.users || 0);
|
||||
}, MINS_10);
|
||||
|
||||
const domainCount = cache(async () => {
|
||||
return await db.kysely
|
||||
.selectFrom('author_stats')
|
||||
.select(({ fn }) => fn.count<number>('nip05_domain').distinct().as('domains'))
|
||||
.executeTakeFirst().then((obj) => obj?.domains || 0);
|
||||
}, MINS_10);
|
||||
|
||||
const statusCount = cache(async () => {
|
||||
return await db.kysely
|
||||
.selectFrom('nostr_events')
|
||||
.where('kind', '=', 1)
|
||||
.select(({ fn }) => fn.countAll<number>().as('events'))
|
||||
.executeTakeFirst().then((obj) => obj?.events || 0);
|
||||
}, MINS_10);
|
||||
|
||||
return c.json({
|
||||
uri: host,
|
||||
|
|
@ -58,9 +106,9 @@ const instanceV1Controller: AppController = async (c) => {
|
|||
},
|
||||
languages: ['en'],
|
||||
stats: {
|
||||
domain_count: 0,
|
||||
status_count: 0,
|
||||
user_count: 0,
|
||||
domain_count: Number(await domainCount()),
|
||||
status_count: Number(await statusCount()),
|
||||
user_count: Number(await userCount()),
|
||||
},
|
||||
urls: {
|
||||
streaming_api: `${wsProtocol}//${host}`,
|
||||
|
|
|
|||
85
packages/ditto/controllers/api/policies.ts
Normal file
85
packages/ditto/controllers/api/policies.ts
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
import { type AppController } from '@/app.ts';
|
||||
import { createPolicyEvent } from '@/utils/policies/mod.ts';
|
||||
import { DEFAULT_POLICY_SPEC, policyRegistry } from '@/utils/policies/mod.ts';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const adminListPoliciesController: AppController = (c) => {
|
||||
return c.json(
|
||||
Object.entries(policyRegistry.available)
|
||||
.map(([internalName, item]) => {
|
||||
return {
|
||||
internalName,
|
||||
...item,
|
||||
instantiate: undefined,
|
||||
schema: undefined,
|
||||
};
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
export const adminCurrentPolicyController: AppController = async (c) => {
|
||||
const { relay, conf } = c.var;
|
||||
const pubkey = await conf.signer.getPublicKey();
|
||||
|
||||
const current = await relay.query([{
|
||||
authors: [pubkey],
|
||||
kinds: [11984],
|
||||
}]).then((events) => events[0]);
|
||||
|
||||
if (current) return c.json({ spec: JSON.parse(current.content) });
|
||||
|
||||
await relay.event(await createPolicyEvent(conf, DEFAULT_POLICY_SPEC));
|
||||
return c.json({ spec: DEFAULT_POLICY_SPEC });
|
||||
};
|
||||
|
||||
const PolicySpecSchema = z.object({
|
||||
policies: z.array(z.object({
|
||||
name: z.string(),
|
||||
params: z.record(z.any()),
|
||||
})),
|
||||
});
|
||||
|
||||
export const adminUpdatePolicyController: AppController = async (c) => {
|
||||
const { relay, conf } = c.var;
|
||||
try {
|
||||
const req = await c.req.json();
|
||||
const parsed = PolicySpecSchema.parse(req);
|
||||
|
||||
// Validate each policy against its specific schema
|
||||
const invalidPolicies = parsed.policies.filter((policy) => {
|
||||
const policyItem = policyRegistry.available[policy.name];
|
||||
// If policy not found in registry, it's invalid
|
||||
if (!policyItem) {
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
// Try to parse the policy params against the specific schema
|
||||
policyItem.schema.parse(policy.params);
|
||||
return false; // Not invalid
|
||||
} catch (_) {
|
||||
return true; // Invalid policy
|
||||
}
|
||||
});
|
||||
|
||||
// If any policies are invalid, return an error
|
||||
if (invalidPolicies.length > 0) {
|
||||
return c.json({
|
||||
error: `Invalid policy specification for: ${invalidPolicies.map((p) => p.name).join(', ')}`,
|
||||
}, 400);
|
||||
}
|
||||
|
||||
await relay.event(await createPolicyEvent(conf, parsed));
|
||||
return c.json({
|
||||
message: 'Settings saved successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof SyntaxError) {
|
||||
return c.json({ error: 'Invalid JSON in request body' }, 400);
|
||||
}
|
||||
if (error instanceof z.ZodError) {
|
||||
return c.json({ error: 'Invalid policy specification', details: error.errors }, 400);
|
||||
}
|
||||
return c.json({ error: 'Failed to update policy' }, 500);
|
||||
}
|
||||
};
|
||||
|
|
@ -23,7 +23,7 @@ interface ParseNoteContentOpts {
|
|||
export function contentToHtml(content: string, mentions: MastodonMention[], opts: ParseNoteContentOpts): string {
|
||||
const { conf } = opts;
|
||||
|
||||
return linkifyStr(content, {
|
||||
const htmlString = linkifyStr(content, {
|
||||
render: {
|
||||
hashtag: ({ content }) => {
|
||||
const tag = content.replace(/^#/, '');
|
||||
|
|
@ -67,6 +67,9 @@ export function contentToHtml(content: string, mentions: MastodonMention[], opts
|
|||
},
|
||||
},
|
||||
}).replace(/\n+$/, '');
|
||||
|
||||
// Replace apostrophes with '
|
||||
return htmlString.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
/** Remove the tokens from the _end_ of the content. */
|
||||
|
|
|
|||
53
packages/ditto/utils/policies/mod.ts
Normal file
53
packages/ditto/utils/policies/mod.ts
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
import { nostrNow } from '@/utils.ts';
|
||||
import type { DittoConf } from '@ditto/conf';
|
||||
import { PolicyRegistry } from './registry.ts';
|
||||
import { MockRelay } from '@nostrify/nostrify/test';
|
||||
import { nip19 } from 'nostr-tools';
|
||||
|
||||
type ParamValue = string | number | boolean;
|
||||
|
||||
export { PolicyRegistry };
|
||||
|
||||
export const policyRegistry = new PolicyRegistry({
|
||||
antiDuplicationPolicyStore: {
|
||||
get: (key: Deno.KvKey) => Promise.resolve({ key, value: null, versionstamp: null }),
|
||||
set: () => Promise.resolve({ ok: true, versionstamp: '00000000000000000000' }),
|
||||
},
|
||||
store: new MockRelay(),
|
||||
});
|
||||
|
||||
export type PolicyParam = ParamValue | (string | number)[];
|
||||
export type PolicyParams = Record<string, PolicyParam>;
|
||||
|
||||
interface PolicySpecItem {
|
||||
name: keyof typeof policyRegistry.available;
|
||||
params?: PolicyParams;
|
||||
}
|
||||
|
||||
export interface PolicySpec {
|
||||
policies: PolicySpecItem[];
|
||||
}
|
||||
|
||||
export const normalizeNpub = (itm: string) => {
|
||||
if (!itm.startsWith('npub1')) return itm;
|
||||
return nip19.decode(itm as `npub1${string}`).data;
|
||||
};
|
||||
|
||||
export const DEFAULT_POLICY_SPEC: PolicySpec = {
|
||||
policies: [
|
||||
{ 'name': 'SizePolicy' },
|
||||
{ 'name': 'HellthreadPolicy' },
|
||||
{ 'name': 'HashtagPolicy', 'params': { 'hashtags': ['NSFW', 'explicit', 'violence', 'cp', 'porn'] } },
|
||||
{ 'name': 'ReplyBotPolicy' },
|
||||
{ 'name': 'AntiDuplicationPolicy' },
|
||||
],
|
||||
};
|
||||
|
||||
export const createPolicyEvent = async (conf: DittoConf, policies: PolicySpec) => {
|
||||
return await conf.signer.signEvent({
|
||||
kind: 11984,
|
||||
content: JSON.stringify(policies),
|
||||
created_at: nostrNow(),
|
||||
tags: [],
|
||||
});
|
||||
};
|
||||
105
packages/ditto/utils/policies/parameters.test.ts
Normal file
105
packages/ditto/utils/policies/parameters.test.ts
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
import { assertEquals } from '@std/assert';
|
||||
import { z } from 'zod';
|
||||
import { zodSchemaToFields } from './parameters.ts';
|
||||
|
||||
Deno.test('zodSchemaToFields - basic types', () => {
|
||||
const schema = z.object({
|
||||
name: z.string(),
|
||||
age: z.number(),
|
||||
});
|
||||
|
||||
const result = zodSchemaToFields(schema);
|
||||
assertEquals(result, {
|
||||
name: { type: 'string' },
|
||||
age: { type: 'number' },
|
||||
});
|
||||
});
|
||||
|
||||
Deno.test('zodSchemaToFields - array types', () => {
|
||||
const schema = z.object({
|
||||
tags: z.array(z.string()),
|
||||
scores: z.array(z.number()),
|
||||
});
|
||||
|
||||
const result = zodSchemaToFields(schema);
|
||||
assertEquals(result, {
|
||||
tags: { type: 'multi_string' },
|
||||
scores: { type: 'multi_number' },
|
||||
});
|
||||
});
|
||||
|
||||
Deno.test('zodSchemaToFields - special-case NIP-01 filters', () => {
|
||||
const schema = z.object({
|
||||
filters: z.array(z.string()),
|
||||
keywords: z.array(z.string()),
|
||||
});
|
||||
|
||||
const result = zodSchemaToFields(schema);
|
||||
assertEquals(result, {
|
||||
filters: { type: 'multi_string' },
|
||||
keywords: { type: 'multi_string' },
|
||||
});
|
||||
});
|
||||
|
||||
Deno.test('zodSchemaToFields - mixed types', () => {
|
||||
const schema = z.object({
|
||||
id: z.string(),
|
||||
values: z.array(z.number()),
|
||||
flags: z.array(z.string()).describe('Test description'),
|
||||
});
|
||||
|
||||
const result = zodSchemaToFields(schema);
|
||||
assertEquals(result, {
|
||||
id: { type: 'string' },
|
||||
values: { type: 'multi_number' },
|
||||
flags: { type: 'multi_string', description: 'Test description' },
|
||||
});
|
||||
});
|
||||
|
||||
Deno.test('zodSchemaToFields - optional fields', () => {
|
||||
const schema = z.object({
|
||||
name: z.string().optional(),
|
||||
age: z.number().optional(),
|
||||
});
|
||||
|
||||
const result = zodSchemaToFields(schema);
|
||||
assertEquals(result, {
|
||||
name: { type: 'string', optional: true },
|
||||
age: { type: 'number', optional: true },
|
||||
});
|
||||
});
|
||||
|
||||
Deno.test('zodSchemaToFields - default values', () => {
|
||||
const schema = z.object({
|
||||
name: z.string().default('John Doe'),
|
||||
age: z.number().default(30),
|
||||
});
|
||||
|
||||
const result = zodSchemaToFields(schema);
|
||||
assertEquals(result, {
|
||||
name: { type: 'string', default: 'John Doe' },
|
||||
age: { type: 'number', default: 30 },
|
||||
});
|
||||
});
|
||||
|
||||
Deno.test('zodSchemaToFields - boolean fields', () => {
|
||||
const schema = z.object({
|
||||
active: z.boolean(),
|
||||
});
|
||||
|
||||
const result = zodSchemaToFields(schema);
|
||||
assertEquals(result, {
|
||||
active: { type: 'boolean' },
|
||||
});
|
||||
});
|
||||
|
||||
Deno.test('zodSchemaToFields - invalid schema', () => {
|
||||
const schema = z.object({
|
||||
invalid: z.any(),
|
||||
});
|
||||
|
||||
const result = zodSchemaToFields(schema);
|
||||
assertEquals(result, {
|
||||
invalid: { type: 'unknown' },
|
||||
});
|
||||
});
|
||||
93
packages/ditto/utils/policies/parameters.ts
Normal file
93
packages/ditto/utils/policies/parameters.ts
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
import { z } from 'zod';
|
||||
import { PolicyParam } from '@/utils/policies/mod.ts';
|
||||
|
||||
type FieldType = 'string' | 'multi_string' | 'number' | 'multi_number' | 'boolean' | 'unknown';
|
||||
|
||||
export interface FieldItem {
|
||||
type: FieldType;
|
||||
description?: string;
|
||||
optional?: boolean;
|
||||
default?: PolicyParam;
|
||||
}
|
||||
|
||||
interface UnwrappedZodType {
|
||||
baseType: z.ZodTypeAny;
|
||||
optional?: boolean;
|
||||
defaultValue?: PolicyParam;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the base type from wrapped Zod types like ZodOptional and ZodDefault.
|
||||
*/
|
||||
function unwrapZodType(field: z.ZodTypeAny): UnwrappedZodType {
|
||||
let optional = false;
|
||||
let defaultValue: PolicyParam | undefined = undefined;
|
||||
let description: string | undefined = undefined;
|
||||
|
||||
description = field.description;
|
||||
|
||||
while (field instanceof z.ZodOptional || field instanceof z.ZodDefault) {
|
||||
if (field instanceof z.ZodOptional) optional = true;
|
||||
if (field instanceof z.ZodDefault) defaultValue = field._def.defaultValue();
|
||||
if (!description) description = field.description;
|
||||
|
||||
field = field._def.innerType;
|
||||
}
|
||||
|
||||
const result: UnwrappedZodType = { baseType: field };
|
||||
|
||||
if (optional) result.optional = true;
|
||||
if (typeof defaultValue !== 'undefined') {
|
||||
if (
|
||||
typeof defaultValue === 'string' && defaultValue.length > 0 ||
|
||||
(typeof defaultValue === 'number') ||
|
||||
(typeof defaultValue === 'object' && Object.keys(defaultValue).length > 0) ||
|
||||
(Array.isArray(defaultValue) && defaultValue.length > 0)
|
||||
) {
|
||||
result.defaultValue = defaultValue;
|
||||
}
|
||||
}
|
||||
if (description) result.description = description;
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes a Zod schema into a record of field types. NOT meant to be used
|
||||
* as a general-purpose serializer - this is specifically for generating policy
|
||||
* parameter descriptions! This function also parses internal Zod properties,
|
||||
* but there is precedent for this:
|
||||
* https://github.com/colinhacks/zod/discussions/1953
|
||||
*
|
||||
* With version pinning and the Zod maintainers' knowledge of this kind of
|
||||
* usage, it should be fine for it to be this way.
|
||||
*
|
||||
* Special-cases NIP-01 filters as `multi_string`.
|
||||
*/
|
||||
|
||||
// deno-lint-ignore no-explicit-any
|
||||
export function zodSchemaToFields(schema: z.ZodObject<any>): Record<string, FieldItem> {
|
||||
const result: Record<string, FieldItem> = {};
|
||||
|
||||
for (const [key, field] of Object.entries(schema.shape) as [string, z.ZodTypeAny][]) {
|
||||
const { baseType, optional, defaultValue, description } = unwrapZodType(field);
|
||||
result[key] = { type: 'unknown' };
|
||||
if (optional) result[key].optional = optional;
|
||||
if (defaultValue) result[key].default = defaultValue;
|
||||
if (description) result[key].description = description;
|
||||
|
||||
if (key === 'filters') {
|
||||
result[key].type = 'multi_string';
|
||||
} else if (baseType instanceof z.ZodArray) {
|
||||
const elementType = unwrapZodType(baseType._def.type).baseType._def.typeName;
|
||||
if (elementType === 'ZodNumber') result[key].type = 'multi_number';
|
||||
else result[key].type = 'multi_string';
|
||||
} else if (baseType instanceof z.ZodNumber) result[key].type = 'number';
|
||||
else if (baseType instanceof z.ZodBoolean) result[key].type = 'boolean';
|
||||
else if (baseType instanceof z.ZodString) result[key].type = 'string';
|
||||
|
||||
if (baseType.description && !result[key].description) result[key].description = baseType.description;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
236
packages/ditto/utils/policies/registry.ts
Normal file
236
packages/ditto/utils/policies/registry.ts
Normal file
|
|
@ -0,0 +1,236 @@
|
|||
import {
|
||||
AntiDuplicationPolicy,
|
||||
AuthorPolicy,
|
||||
DomainPolicy,
|
||||
FiltersPolicy,
|
||||
HashtagPolicy,
|
||||
HellthreadPolicy,
|
||||
KeywordPolicy,
|
||||
OpenAIPolicy,
|
||||
PowPolicy,
|
||||
PubkeyBanPolicy,
|
||||
RegexPolicy,
|
||||
ReplyBotPolicy,
|
||||
SizePolicy,
|
||||
WhitelistPolicy,
|
||||
WoTPolicy,
|
||||
} from '@nostrify/policies';
|
||||
import { FieldItem, zodSchemaToFields } from './parameters.ts';
|
||||
import { NPolicy, NStore } from '@nostrify/types';
|
||||
import { z } from 'zod';
|
||||
import {
|
||||
AntiDuplicationPolicyOpts,
|
||||
AntiDuplicationPolicyOptsSchema,
|
||||
DomainPolicyOptsSchema,
|
||||
FiltersPolicyOptsSchema,
|
||||
HashtagPolicyOptsSchema,
|
||||
HellthreadPolicyOptsSchema,
|
||||
KeywordPolicyOptsSchema,
|
||||
OpenAIPolicyOptsSchema,
|
||||
PowPolicyOptsSchema,
|
||||
PubkeyBanPolicyOptsSchema,
|
||||
RegexPolicyOptsSchema,
|
||||
ReplyBotPolicyOptsSchema,
|
||||
SizePolicyOptsSchema,
|
||||
WhitelistPolicyOptsSchema,
|
||||
WoTPolicyOptsSchema,
|
||||
} from '@/utils/policies/schemas.ts';
|
||||
import { normalizeNpub, PolicyParams } from '@/utils/policies/mod.ts';
|
||||
|
||||
export interface PolicyItem {
|
||||
instantiate: (params: PolicyParams) => NPolicy;
|
||||
name: string;
|
||||
parameters: Record<string, FieldItem>;
|
||||
description: string;
|
||||
schema: z.ZodSchema;
|
||||
}
|
||||
|
||||
export interface PolicyRegistryOpts {
|
||||
antiDuplicationPolicyStore?: AntiDuplicationPolicyOpts['kv'];
|
||||
store: NStore;
|
||||
}
|
||||
|
||||
export class PolicyRegistry {
|
||||
constructor(private opts: PolicyRegistryOpts) {}
|
||||
|
||||
available: Record<string, PolicyItem> = {
|
||||
'AntiDuplicationPolicy': {
|
||||
instantiate: (params) => {
|
||||
const parsed = AntiDuplicationPolicyOptsSchema.parse(params);
|
||||
if (!this.opts.antiDuplicationPolicyStore) {
|
||||
throw new Error('AntiDuplicationPolicy: tried to instantiate store but no store supplied!');
|
||||
}
|
||||
return new AntiDuplicationPolicy({
|
||||
kv: this.opts.antiDuplicationPolicyStore,
|
||||
...parsed,
|
||||
});
|
||||
},
|
||||
description: 'Prevent messages with the exact same content from being submitted repeatedly.',
|
||||
name: 'Deduplicate messages',
|
||||
parameters: zodSchemaToFields(AntiDuplicationPolicyOptsSchema),
|
||||
schema: AntiDuplicationPolicyOptsSchema,
|
||||
},
|
||||
'AuthorPolicy': {
|
||||
instantiate: () => {
|
||||
return new AuthorPolicy(this.opts.store);
|
||||
},
|
||||
description: 'Rejects events by authors without a kind 0 event associated with their pubkey.',
|
||||
name: 'Block events without associated profiles',
|
||||
parameters: {},
|
||||
schema: z.object({}), // Empty schema since no params are used
|
||||
},
|
||||
'DomainPolicy': {
|
||||
instantiate: (params) => {
|
||||
const parsed = DomainPolicyOptsSchema.parse(params);
|
||||
return new DomainPolicy(this.opts.store, parsed);
|
||||
},
|
||||
description: 'Ban events by pubkeys without a valid NIP-05 domain. Domains can also be whitelisted/blacklisted',
|
||||
name: 'Filter by NIP-05',
|
||||
parameters: zodSchemaToFields(DomainPolicyOptsSchema),
|
||||
schema: DomainPolicyOptsSchema,
|
||||
},
|
||||
'FiltersPolicy': {
|
||||
instantiate: (params) => {
|
||||
if (!params.filters || !Array.isArray(params.filters)) throw new Error('Invalid params to FiltersPolicy');
|
||||
const filters = params.filters.map((item) => {
|
||||
if (typeof item === 'number') return;
|
||||
try {
|
||||
return JSON.parse(item);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
}).filter(Boolean);
|
||||
const parsed = FiltersPolicyOptsSchema.parse({ filters });
|
||||
return new FiltersPolicy(parsed.filters);
|
||||
},
|
||||
description: 'Only allow events matching a given Nostr filter',
|
||||
name: 'Filter by Nostr filter',
|
||||
parameters: zodSchemaToFields(FiltersPolicyOptsSchema),
|
||||
schema: FiltersPolicyOptsSchema,
|
||||
},
|
||||
'HashtagPolicy': {
|
||||
instantiate: (params) => {
|
||||
const parsed = HashtagPolicyOptsSchema.parse(params);
|
||||
return new HashtagPolicy(parsed.hashtags);
|
||||
},
|
||||
description: 'Ban events containing the specified hashtags',
|
||||
name: 'Ban hashtags',
|
||||
parameters: zodSchemaToFields(HashtagPolicyOptsSchema),
|
||||
schema: HashtagPolicyOptsSchema,
|
||||
},
|
||||
'HellthreadPolicy': {
|
||||
instantiate: (params) => {
|
||||
const parsed = HellthreadPolicyOptsSchema.parse(params);
|
||||
return new HellthreadPolicy({
|
||||
...parsed,
|
||||
});
|
||||
},
|
||||
description: "Prevent 'hellthreads' - notes that tag hundreds of people to cause a nuisance and server load.",
|
||||
name: 'Limit events with excessive mentions',
|
||||
parameters: zodSchemaToFields(HellthreadPolicyOptsSchema),
|
||||
schema: HellthreadPolicyOptsSchema,
|
||||
},
|
||||
'KeywordPolicy': {
|
||||
instantiate: (params) => {
|
||||
const parsed = KeywordPolicyOptsSchema.parse(params);
|
||||
return new KeywordPolicy(parsed.keywords);
|
||||
},
|
||||
description: 'Ban events that contain specified keywords.',
|
||||
name: 'Block certain words',
|
||||
parameters: zodSchemaToFields(KeywordPolicyOptsSchema),
|
||||
schema: KeywordPolicyOptsSchema,
|
||||
},
|
||||
'OpenAIPolicy': {
|
||||
instantiate: (params) => {
|
||||
const parsed = OpenAIPolicyOptsSchema.parse(params);
|
||||
return new OpenAIPolicy({
|
||||
...parsed,
|
||||
});
|
||||
},
|
||||
description: "Use OpenAI moderation integration to block posts that don't meet your community guidelines.",
|
||||
name: 'Use OpenAI moderation',
|
||||
parameters: zodSchemaToFields(OpenAIPolicyOptsSchema),
|
||||
schema: OpenAIPolicyOptsSchema,
|
||||
},
|
||||
'PowPolicy': {
|
||||
instantiate: (params) => {
|
||||
const parsed = PowPolicyOptsSchema.parse(params);
|
||||
return new PowPolicy({
|
||||
...parsed,
|
||||
});
|
||||
},
|
||||
description: 'Use proof-of-work to limit events from spammers.',
|
||||
name: 'Require proof-of-work for events',
|
||||
parameters: zodSchemaToFields(PowPolicyOptsSchema),
|
||||
schema: PowPolicyOptsSchema,
|
||||
},
|
||||
'PubkeyBanPolicy': {
|
||||
instantiate: (params) => {
|
||||
const parsed = PubkeyBanPolicyOptsSchema.parse(params);
|
||||
return new PubkeyBanPolicy(parsed.pubkeys.map(normalizeNpub));
|
||||
},
|
||||
description: 'Ban events from certain pubkeys',
|
||||
name: 'Ban certain pubkeys',
|
||||
parameters: zodSchemaToFields(PubkeyBanPolicyOptsSchema),
|
||||
schema: PubkeyBanPolicyOptsSchema,
|
||||
},
|
||||
'RegexPolicy': {
|
||||
instantiate: (params) => {
|
||||
const parsed = RegexPolicyOptsSchema.parse(params);
|
||||
return new RegexPolicy(new RegExp(parsed.expr, parsed.flags));
|
||||
},
|
||||
description: 'Ban events that match a certain regular expression.',
|
||||
name: 'Filter by regex',
|
||||
parameters: zodSchemaToFields(RegexPolicyOptsSchema),
|
||||
schema: RegexPolicyOptsSchema,
|
||||
},
|
||||
'ReplyBotPolicy': {
|
||||
instantiate: (params) => {
|
||||
const parsed = ReplyBotPolicyOptsSchema.parse(params);
|
||||
return new ReplyBotPolicy({
|
||||
store: this.opts.store,
|
||||
...parsed,
|
||||
});
|
||||
},
|
||||
description: 'Block events that reply too quickly to other events.',
|
||||
name: 'Block reply spambots',
|
||||
parameters: zodSchemaToFields(ReplyBotPolicyOptsSchema),
|
||||
schema: ReplyBotPolicyOptsSchema,
|
||||
},
|
||||
'SizePolicy': {
|
||||
instantiate: (params) => {
|
||||
const parsed = SizePolicyOptsSchema.parse(params);
|
||||
return new SizePolicy({
|
||||
...parsed,
|
||||
});
|
||||
},
|
||||
description: 'Restrict events that are too big in size.',
|
||||
name: 'Block events by size',
|
||||
parameters: zodSchemaToFields(SizePolicyOptsSchema),
|
||||
schema: SizePolicyOptsSchema,
|
||||
},
|
||||
'WhitelistPolicy': {
|
||||
instantiate: (params) => {
|
||||
const parsed = WhitelistPolicyOptsSchema.parse(params);
|
||||
return new WhitelistPolicy(parsed.pubkeys.map(normalizeNpub));
|
||||
},
|
||||
description: 'Allow only whitelisted pubkeys to post. All other events are rejected.',
|
||||
name: 'Whitelist people',
|
||||
parameters: zodSchemaToFields(WhitelistPolicyOptsSchema),
|
||||
schema: WhitelistPolicyOptsSchema,
|
||||
},
|
||||
'WoTPolicy': {
|
||||
instantiate: (params) => {
|
||||
const parsed = WoTPolicyOptsSchema.parse(params);
|
||||
return new WoTPolicy({
|
||||
store: this.opts.store,
|
||||
...parsed,
|
||||
});
|
||||
},
|
||||
description: 'Use a web-of-trust to only allow users a certain distance from trusted users to publish posts.',
|
||||
name: 'Build a web-of-trust',
|
||||
parameters: zodSchemaToFields(WoTPolicyOptsSchema),
|
||||
schema: WoTPolicyOptsSchema,
|
||||
},
|
||||
};
|
||||
}
|
||||
150
packages/ditto/utils/policies/schemas.ts
Normal file
150
packages/ditto/utils/policies/schemas.ts
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
import { NostrEvent, NProfilePointer, NStore } from '@nostrify/types';
|
||||
import { z } from 'zod';
|
||||
import { NSchema as n } from '@nostrify/nostrify';
|
||||
|
||||
/** Options for the `WoTPolicy`. */
|
||||
export const WoTPolicyOptsSchema = z.object({
|
||||
pubkeys: z.array(z.string()),
|
||||
depth: z.number().default(2),
|
||||
});
|
||||
|
||||
export interface WoTPolicyOpts extends z.TypeOf<typeof WoTPolicyOptsSchema> {
|
||||
store: NStore;
|
||||
}
|
||||
|
||||
/** Options for `FiltersPolicy`. */
|
||||
export const FiltersPolicyOptsSchema = z.object({
|
||||
filters: z.array(n.filter()),
|
||||
});
|
||||
|
||||
interface FiltersPolicyOpts extends z.TypeOf<typeof FiltersPolicyOptsSchema> {}
|
||||
|
||||
/** Options for `HashtagPolicy`. */
|
||||
export const HashtagPolicyOptsSchema = z.object({
|
||||
hashtags: z.array(z.string()).describe('Banned hashtags (case-insensitive)'),
|
||||
});
|
||||
|
||||
export interface HashtagPolicyOpts extends z.TypeOf<typeof HashtagPolicyOptsSchema> {}
|
||||
|
||||
/** Options for `WhitelistPolicy`. */
|
||||
export const WhitelistPolicyOptsSchema = z.object({
|
||||
pubkeys: z.array(z.string()).describe('Allowed pubkeys'),
|
||||
});
|
||||
|
||||
export interface WhitelistPolicyOpts extends z.TypeOf<typeof WhitelistPolicyOptsSchema> {}
|
||||
|
||||
/** Options for `RegexPolicy`. */
|
||||
export const RegexPolicyOptsSchema = z.object({
|
||||
expr: z.string().describe('Expression'),
|
||||
flags: z.string().optional().describe('Additional RegExp flags. Leave blank if unsure.'),
|
||||
});
|
||||
|
||||
export interface RegexPolicyOpts extends Partial<z.TypeOf<typeof RegexPolicyOptsSchema>> {
|
||||
expr: string;
|
||||
}
|
||||
|
||||
/** Options for `KeywordPolicy`. */
|
||||
export const KeywordPolicyOptsSchema = z.object({
|
||||
keywords: z.array(z.string()).describe('Banned keywords (case-insensitive)'),
|
||||
});
|
||||
|
||||
export interface KeywordPolicyOpts extends z.TypeOf<typeof KeywordPolicyOptsSchema> {}
|
||||
|
||||
/** Options for `DomainPolicy`. */
|
||||
export const DomainPolicyOptsSchema = z.object({
|
||||
blacklist: z.array(z.string()).default([]).describe('Domains to blacklist'),
|
||||
whitelist: z.array(z.string()).optional().describe('Domains to whitelist'),
|
||||
});
|
||||
|
||||
export interface DomainPolicyOpts extends Partial<z.TypeOf<typeof DomainPolicyOptsSchema>> {
|
||||
lookup?(nip05: string, signal?: AbortSignal): Promise<NProfilePointer>;
|
||||
store: NStore;
|
||||
}
|
||||
|
||||
/** Options for `PubkeyBanPolicy`. */
|
||||
export const PubkeyBanPolicyOptsSchema = z.object({
|
||||
pubkeys: z.array(z.string()).describe('Banned pubkeys'),
|
||||
});
|
||||
|
||||
export interface PubkeyBanPolicyOpts extends z.TypeOf<typeof PubkeyBanPolicyOptsSchema> {}
|
||||
|
||||
/** Options for `OpenAIPolicy`. */
|
||||
export const OpenAIPolicyOptsSchema = z.object({
|
||||
endpoint: z.string().default('https://api.openai.com/v1/moderations').describe('OpenAI API endpoint'),
|
||||
kinds: z.array(z.number()).default([1, 30023]).describe('Event kinds to filter through the policy'),
|
||||
apiKey: z.string().describe('OpenAI API key'),
|
||||
});
|
||||
|
||||
interface OpenAIModerationResult {
|
||||
id: string;
|
||||
model: string;
|
||||
results: {
|
||||
categories: {
|
||||
'hate': boolean;
|
||||
'hate/threatening': boolean;
|
||||
'self-harm': boolean;
|
||||
'sexual': boolean;
|
||||
'sexual/minors': boolean;
|
||||
'violence': boolean;
|
||||
'violence/graphic': boolean;
|
||||
};
|
||||
category_scores: {
|
||||
'hate': number;
|
||||
'hate/threatening': number;
|
||||
'self-harm': number;
|
||||
'sexual': number;
|
||||
'sexual/minors': number;
|
||||
'violence': number;
|
||||
'violence/graphic': number;
|
||||
};
|
||||
flagged: boolean;
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface OpenAIPolicyOpts extends Partial<z.TypeOf<typeof OpenAIPolicyOptsSchema>> {
|
||||
apiKey: string;
|
||||
handler?(event: NostrEvent, result: OpenAIModerationResult): boolean;
|
||||
fetch?: typeof fetch;
|
||||
}
|
||||
|
||||
/** Options for `ReplyBotPolicy`. */
|
||||
export const ReplyBotPolicyOptsSchema = z.object({
|
||||
threshold: z.number().default(1).describe('Minimum time in seconds between two posts.'),
|
||||
kinds: z.array(z.number()).default([1]).describe('Event kinds to filter'),
|
||||
});
|
||||
|
||||
export interface ReplyBotPolicyOpts extends Partial<z.TypeOf<typeof ReplyBotPolicyOptsSchema>> {
|
||||
store: NStore;
|
||||
}
|
||||
|
||||
/** Options for `SizePolicy`. */
|
||||
export const SizePolicyOptsSchema = z.object({
|
||||
maxBytes: z.number().default(8 * 1024).describe('Max allowed message size in bytes'),
|
||||
});
|
||||
|
||||
export interface SizePolicyOpts extends Partial<z.TypeOf<typeof SizePolicyOptsSchema>> {}
|
||||
|
||||
/** Options for `AntiDuplicationPolicy`. */
|
||||
export const AntiDuplicationPolicyOptsSchema = z.object({
|
||||
expireIn: z.number().default(60000).describe('How long should we wait before an identical message can be reposted?'),
|
||||
minLength: z.number().default(50).describe('Minimum length for filtered messages'),
|
||||
});
|
||||
|
||||
export interface AntiDuplicationPolicyOpts extends Partial<z.TypeOf<typeof AntiDuplicationPolicyOptsSchema>> {
|
||||
kv: Pick<Deno.Kv, 'get' | 'set'>;
|
||||
deobfuscate?(event: NostrEvent): string;
|
||||
}
|
||||
|
||||
/** Options for `HellthreadPolicy`. */
|
||||
export const HellthreadPolicyOptsSchema = z.object({
|
||||
limit: z.number().default(100).describe('Maximum number of mentions to allow per post'),
|
||||
});
|
||||
|
||||
export interface HellthreadPolicyOpts extends Partial<z.TypeOf<typeof HellthreadPolicyOptsSchema>> {}
|
||||
|
||||
/** Options for `PowPolicy`. */
|
||||
export const PowPolicyOptsSchema = z.object({
|
||||
difficulty: z.number().default(1).describe('Number of bits of proof-of-work to require'),
|
||||
});
|
||||
|
||||
export interface PowPolicyOpts extends Partial<z.TypeOf<typeof PowPolicyOptsSchema>> {}
|
||||
|
|
@ -1,14 +1,45 @@
|
|||
import { DittoConf } from '@ditto/conf';
|
||||
import { generateSecretKey, nip19 } from 'nostr-tools';
|
||||
|
||||
import { PolicyWorker } from './policy.ts';
|
||||
import { PolicyWorker } from '@/workers/policy.ts';
|
||||
import { assertEquals } from '@std/assert/assert-equals';
|
||||
import { join } from '@std/path';
|
||||
|
||||
Deno.test('PolicyWorker', () => {
|
||||
const blocked = {
|
||||
id: '19afd70437944671e7f5a02b29221ad444ef7cf60113a5731667e272e59a3979',
|
||||
pubkey: '79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798',
|
||||
kind: 1,
|
||||
tags: [['t', 'porn'], ['t', 'other-blocked-tag']],
|
||||
content: 'this is a test of the policy system',
|
||||
sig:
|
||||
'1d73a7480cfd737b89dc1e0e7175dff67119915f31d24a279a45d56622f4b991b01e431d07b693ee6cd652f3f27274d9e203ee43ae44af7e70ce8647e5326196',
|
||||
created_at: 1743685015,
|
||||
};
|
||||
|
||||
Deno.test('PolicyWorker with script policy', async () => {
|
||||
const conf = new DittoConf(
|
||||
new Map([
|
||||
['DITTO_NSEC', nip19.nsecEncode(generateSecretKey())],
|
||||
['DATABASE_URL', Deno.env.get('DATABASE_URL')],
|
||||
['DITTO_POLICY', join(Deno.cwd(), 'fixtures', 'policy.ts')],
|
||||
]),
|
||||
);
|
||||
|
||||
new PolicyWorker(conf);
|
||||
const worker = new PolicyWorker(conf);
|
||||
const [, , ok] = await worker.call(blocked);
|
||||
assertEquals(ok, false);
|
||||
});
|
||||
|
||||
Deno.test('PolicyWorker with event policy', async () => {
|
||||
const conf = new DittoConf(
|
||||
new Map([
|
||||
['DITTO_NSEC', nip19.nsecEncode(generateSecretKey())],
|
||||
['DATABASE_URL', Deno.env.get('DATABASE_URL')],
|
||||
]),
|
||||
);
|
||||
|
||||
const worker = new PolicyWorker(conf);
|
||||
const [, , ok] = await worker.call(blocked);
|
||||
|
||||
assertEquals(ok, false);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -54,27 +54,14 @@ export class PolicyWorker implements NPolicy {
|
|||
databaseUrl: conf.databaseUrl,
|
||||
pubkey: await conf.signer.getPublicKey(),
|
||||
});
|
||||
|
||||
logi({
|
||||
level: 'info',
|
||||
ns: 'ditto.system.policy',
|
||||
msg: 'Using custom policy',
|
||||
msg: `Initialising custom policy`,
|
||||
path: conf.policy,
|
||||
enabled: true,
|
||||
});
|
||||
} catch (e) {
|
||||
if (e instanceof Error && e.message.includes('Module not found')) {
|
||||
logi({
|
||||
level: 'info',
|
||||
ns: 'ditto.system.policy',
|
||||
msg: 'Custom policy not found <https://docs.soapbox.pub/ditto/policies/>',
|
||||
path: null,
|
||||
enabled: false,
|
||||
});
|
||||
this.enabled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (e instanceof Error && e.message.includes('PGlite is not supported in worker threads')) {
|
||||
logi({
|
||||
level: 'warn',
|
||||
|
|
|
|||
|
|
@ -2,23 +2,23 @@ import { DittoConf } from '@ditto/conf';
|
|||
import { DittoPolyPg } from '@ditto/db';
|
||||
import '@soapbox/safe-fetch/load';
|
||||
import { NostrEvent, NostrRelayOK, NPolicy } from '@nostrify/nostrify';
|
||||
import { ReadOnlyPolicy } from '@nostrify/policies';
|
||||
import { PipePolicy, ReadOnlyPolicy } from '@nostrify/policies';
|
||||
import * as Comlink from 'comlink';
|
||||
|
||||
import { ReadOnlySigner } from '@/signers/ReadOnlySigner.ts';
|
||||
import { DittoPgStore } from '@/storages/DittoPgStore.ts';
|
||||
import { DEFAULT_POLICY_SPEC, PolicyRegistry, PolicySpec } from '@/utils/policies/mod.ts';
|
||||
import { logi } from '@soapbox/logi';
|
||||
|
||||
// @ts-ignore Don't try to access the env from this worker.
|
||||
Deno.env = new Map<string, string>();
|
||||
|
||||
/** Serializable object the worker can use to set up the state. */
|
||||
interface PolicyInit {
|
||||
/** Path to the policy module (https, jsr, file, etc) */
|
||||
path: string;
|
||||
/** Database URL to connect to. */
|
||||
databaseUrl: string;
|
||||
/** Admin pubkey to use for DittoPgStore checks. */
|
||||
pubkey: string;
|
||||
path: string;
|
||||
}
|
||||
|
||||
export class CustomPolicy implements NPolicy {
|
||||
|
|
@ -29,8 +29,8 @@ export class CustomPolicy implements NPolicy {
|
|||
return this.policy.call(event, signal);
|
||||
}
|
||||
|
||||
async init({ path, databaseUrl, pubkey }: PolicyInit): Promise<void> {
|
||||
const Policy = (await import(path)).default;
|
||||
async init(opts: PolicyInit): Promise<void> {
|
||||
const { databaseUrl, pubkey } = opts;
|
||||
|
||||
const db = new DittoPolyPg(databaseUrl, { poolSize: 1 });
|
||||
|
||||
|
|
@ -43,13 +43,47 @@ export class CustomPolicy implements NPolicy {
|
|||
},
|
||||
});
|
||||
|
||||
const policies: NPolicy[] = [];
|
||||
const store = new DittoPgStore({
|
||||
db,
|
||||
conf,
|
||||
timeout: 5_000,
|
||||
});
|
||||
try {
|
||||
const Policy = (await import(opts.path)).default;
|
||||
policies.push(new Policy({ db, store, pubkey }));
|
||||
} catch (e) {
|
||||
if (e instanceof Error && e.message.includes('Module not found')) {
|
||||
logi({
|
||||
level: 'info',
|
||||
ns: 'ditto.system.policy',
|
||||
msg: 'Custom policy not found <https://docs.soapbox.pub/ditto/policies/>',
|
||||
path: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
const registry = new PolicyRegistry({ store, antiDuplicationPolicyStore: await Deno.openKv() });
|
||||
const event = await store
|
||||
.query([{ kinds: [11984], authors: [await conf.signer.getPublicKey()] }])
|
||||
.then((results) => results[0]);
|
||||
|
||||
this.policy = new Policy({ db, store, pubkey });
|
||||
const spec: PolicySpec = event ? JSON.parse(event.content) : DEFAULT_POLICY_SPEC;
|
||||
|
||||
for (const item of spec.policies) {
|
||||
const policy = registry.available[item.name];
|
||||
if (!policy) continue;
|
||||
try {
|
||||
policies.push(policy.instantiate(item.params || {}));
|
||||
} catch (e) {
|
||||
logi({
|
||||
level: 'error',
|
||||
ns: 'ditto.system.policy.worker',
|
||||
msg: `Error instantiating policy ${item.name} with params \`${JSON.stringify(item.params)}\`: ${e}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.policy = new PipePolicy(policies);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue