This commit is contained in:
Siddharth Singh 2025-04-03 18:06:07 +05:30
parent e533ec22b6
commit 3ace715498
No known key found for this signature in database
4 changed files with 153 additions and 153 deletions

View file

@ -9,42 +9,42 @@ 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(),
antiDuplicationPolicyStore: {
get: (key: Deno.KvKey) => Promise.resolve({ key, value: null, versionstamp: null }),
set: () => Promise.resolve({ ok: true, versionstamp: '00000000000000000000' }),
},
store: new MockRelay(),
});
interface PolicySpecItem {
name: keyof typeof policyRegistry.available;
params?: Record<string, ParamValue | (number | string)[]>;
name: keyof typeof policyRegistry.available;
params?: Record<string, ParamValue | (number | string)[]>;
}
export interface PolicySpec {
policies: PolicySpecItem[];
policies: PolicySpecItem[];
}
export const normalizeNpub = (itm: string) => {
if (!itm.startsWith('npub1')) return itm;
return nip19.decode(itm as `npub1${string}`).data;
if (!itm.startsWith('npub1')) return itm;
return nip19.decode(itm as `npub1${string}`).data;
};
export const DEFAULT_POLICY_SPEC: PolicySpec = {
policies: [
{ 'name': 'AntiDuplicationPolicy' },
{ 'name': 'HellthreadPolicy' },
{ 'name': 'ReplyBotPolicy' },
{ 'name': 'SizePolicy' },
{ 'name': 'HashtagPolicy', 'params': { 'hashtags': ['NSFW', 'explicit', 'violence', 'cp', 'porn'] } },
],
policies: [
{ 'name': 'AntiDuplicationPolicy' },
{ 'name': 'HellthreadPolicy' },
{ 'name': 'ReplyBotPolicy' },
{ 'name': 'SizePolicy' },
{ 'name': 'HashtagPolicy', 'params': { 'hashtags': ['NSFW', 'explicit', 'violence', 'cp', 'porn'] } },
],
};
export const createPolicyEvent = async (conf: DittoConf, policies: PolicySpec) => {
return await conf.signer.signEvent({
kind: 11984,
content: JSON.stringify(policies),
created_at: nostrNow(),
tags: [],
});
return await conf.signer.signEvent({
kind: 11984,
content: JSON.stringify(policies),
created_at: nostrNow(),
tags: [],
});
};

View file

@ -3,103 +3,103 @@ 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 schema = z.object({
name: z.string(),
age: z.number(),
});
const result = zodSchemaToFields(schema);
assertEquals(result, {
name: { type: 'string' },
age: { type: '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 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' },
});
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 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' },
});
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 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' },
});
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 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 },
});
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 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 },
});
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 schema = z.object({
active: z.boolean(),
});
const result = zodSchemaToFields(schema);
assertEquals(result, {
active: { type: 'boolean' },
});
const result = zodSchemaToFields(schema);
assertEquals(result, {
active: { type: 'boolean' },
});
});
Deno.test('zodSchemaToFields - invalid schema', () => {
const schema = z.object({
invalid: z.any(),
});
const schema = z.object({
invalid: z.any(),
});
const result = zodSchemaToFields(schema);
assertEquals(result, {
invalid: { type: 'unknown' },
});
const result = zodSchemaToFields(schema);
assertEquals(result, {
invalid: { type: 'unknown' },
});
});

View file

@ -3,52 +3,52 @@ import { z } from 'zod';
type FieldType = 'string' | 'multi_string' | 'number' | 'multi_number' | 'boolean' | 'unknown';
export interface FieldItem {
type: FieldType;
description?: string;
optional?: boolean;
default?: any;
type: FieldType;
description?: string;
optional?: boolean;
default?: any;
}
interface UnwrappedZodType {
baseType: z.ZodTypeAny;
optional?: boolean;
defaultValue?: any;
description?: string;
baseType: z.ZodTypeAny;
optional?: boolean;
defaultValue?: any;
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: any = undefined;
let description: string | undefined = undefined;
let optional = false;
let defaultValue: any = undefined;
let description: string | undefined = undefined;
description = field.description;
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;
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;
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;
}
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;
}
if (description) result.description = description;
return result;
}
/**
@ -64,27 +64,27 @@ function unwrapZodType(field: z.ZodTypeAny): UnwrappedZodType {
* Special-cases NIP-01 filters as `multi_string`.
*/
export function zodSchemaToFields(schema: z.ZodObject<any>): Record<string, FieldItem> {
const result: 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;
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 (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;
}
if (baseType.description && !result[key].description) result[key].description = baseType.description;
}
return result;
return result;
}

View file

@ -17,21 +17,21 @@ export const FiltersPolicyOptsSchema = z.object({
filters: z.array(n.filter()),
});
interface FiltersPolicyOpts extends z.TypeOf<typeof FiltersPolicyOptsSchema> { }
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> { }
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> { }
export interface WhitelistPolicyOpts extends z.TypeOf<typeof WhitelistPolicyOptsSchema> {}
/** Options for `RegexPolicy`. */
export const RegexPolicyOptsSchema = z.object({
@ -48,7 +48,7 @@ export const KeywordPolicyOptsSchema = z.object({
keywords: z.array(z.string()).describe('Banned keywords (case-insensitive)'),
});
export interface KeywordPolicyOpts extends z.TypeOf<typeof KeywordPolicyOptsSchema> { }
export interface KeywordPolicyOpts extends z.TypeOf<typeof KeywordPolicyOptsSchema> {}
/** Options for `DomainPolicy`. */
export const DomainPolicyOptsSchema = z.object({
@ -66,7 +66,7 @@ export const PubkeyBanPolicyOptsSchema = z.object({
pubkeys: z.array(z.string()).describe('Banned pubkeys'),
});
export interface PubkeyBanPolicyOpts extends z.TypeOf<typeof PubkeyBanPolicyOptsSchema> { }
export interface PubkeyBanPolicyOpts extends z.TypeOf<typeof PubkeyBanPolicyOptsSchema> {}
/** Options for `OpenAIPolicy`. */
export const OpenAIPolicyOptsSchema = z.object({
@ -122,7 +122,7 @@ 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>> { }
export interface SizePolicyOpts extends Partial<z.TypeOf<typeof SizePolicyOptsSchema>> {}
/** Options for `AntiDuplicationPolicy`. */
export const AntiDuplicationPolicyOptsSchema = z.object({
@ -140,11 +140,11 @@ 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>> { }
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>> { }
export interface PowPolicyOpts extends Partial<z.TypeOf<typeof PowPolicyOptsSchema>> {}