3
0
Fork 0
mirror of https://github.com/ZeppelinBot/Zeppelin.git synced 2025-05-10 12:25:02 +00:00

refactor: replace io-ts with zod

This commit is contained in:
Dragory 2024-01-14 14:25:42 +00:00
parent fafaefa1fb
commit 28692962bc
No known key found for this signature in database
161 changed files with 1450 additions and 2105 deletions

View file

@ -1,15 +1,12 @@
import { EventEmitter } from "events";
import { PluginOptions } from "knub";
import {
buildCounterConditionString,
CounterTrigger,
getReverseCounterComparisonOp,
parseCounterConditionString,
} from "../../data/entities/CounterTrigger";
import { GuildCounters } from "../../data/GuildCounters";
import {
CounterTrigger,
parseCounterConditionString
} from "../../data/entities/CounterTrigger";
import { mapToPublicFn } from "../../pluginUtils";
import { convertDelayStringToMS, MINUTES } from "../../utils";
import { parseIoTsSchema, StrictValidationError } from "../../validatorUtils";
import { MINUTES, convertDelayStringToMS, values } from "../../utils";
import { zeppelinGuildPlugin } from "../ZeppelinPluginBlueprint";
import { AddCounterCmd } from "./commands/AddCounterCmd";
import { CountersListCmd } from "./commands/CountersListCmd";
@ -25,10 +22,8 @@ import { getPrettyNameForCounterTrigger } from "./functions/getPrettyNameForCoun
import { offCounterEvent } from "./functions/offCounterEvent";
import { onCounterEvent } from "./functions/onCounterEvent";
import { setCounterValue } from "./functions/setCounterValue";
import { ConfigSchema, CountersPluginType, TTrigger } from "./types";
import { CountersPluginType, zCountersConfig } from "./types";
const MAX_COUNTERS = 5;
const MAX_TRIGGERS_PER_COUNTER = 5;
const DECAY_APPLY_INTERVAL = 5 * MINUTES;
const defaultOptions: PluginOptions<CountersPluginType> = {
@ -72,50 +67,12 @@ export const CountersPlugin = zeppelinGuildPlugin<CountersPluginType>()({
description:
"Keep track of per-user, per-channel, or global numbers and trigger specific actions based on this number",
configurationGuide: "See <a href='/docs/setup-guides/counters'>Counters setup guide</a>",
configSchema: ConfigSchema,
configSchema: zCountersConfig,
},
defaultOptions,
// TODO: Separate input and output types
configParser: (input) => {
for (const [counterName, counter] of Object.entries<any>((input as any).counters || {})) {
counter.name = counterName;
counter.per_user = counter.per_user ?? false;
counter.per_channel = counter.per_channel ?? false;
counter.initial_value = counter.initial_value ?? 0;
counter.triggers = counter.triggers || {};
if (Object.values(counter.triggers).length > MAX_TRIGGERS_PER_COUNTER) {
throw new StrictValidationError([`You can only have at most ${MAX_TRIGGERS_PER_COUNTER} triggers per counter`]);
}
// Normalize triggers
for (const [triggerName, trigger] of Object.entries(counter.triggers)) {
const triggerObj = (typeof trigger === "string" ? { condition: trigger } : trigger) as Partial<TTrigger>;
triggerObj.name = triggerName;
const parsedCondition = parseCounterConditionString(triggerObj.condition || "");
if (!parsedCondition) {
throw new StrictValidationError([
`Invalid comparison in counter trigger ${counterName}/${triggerName}: "${triggerObj.condition}"`,
]);
}
triggerObj.condition = buildCounterConditionString(parsedCondition[0], parsedCondition[1]);
triggerObj.reverse_condition =
triggerObj.reverse_condition ||
buildCounterConditionString(getReverseCounterComparisonOp(parsedCondition[0]), parsedCondition[1]);
counter.triggers[triggerName] = triggerObj as TTrigger;
}
}
if (Object.values((input as any).counters || {}).length > MAX_COUNTERS) {
throw new StrictValidationError([`You can only have at most ${MAX_COUNTERS} counters`]);
}
return parseIoTsSchema(ConfigSchema, input);
},
configParser: (input) => zCountersConfig.parse(input),
public: {
counterExists: mapToPublicFn(counterExists),
@ -163,13 +120,12 @@ export const CountersPlugin = zeppelinGuildPlugin<CountersPluginType>()({
state.counterTriggersByCounterId.set(dbCounter.id, thisCounterTriggers);
// Initialize triggers
for (const trigger of Object.values(counter.triggers)) {
const theTrigger = trigger as TTrigger;
const parsedCondition = parseCounterConditionString(theTrigger.condition)!;
const parsedReverseCondition = parseCounterConditionString(theTrigger.reverse_condition)!;
for (const trigger of values(counter.triggers)) {
const parsedCondition = parseCounterConditionString(trigger.condition)!;
const parsedReverseCondition = parseCounterConditionString(trigger.reverse_condition)!;
const counterTrigger = await state.counters.initCounterTrigger(
dbCounter.id,
theTrigger.name,
trigger.name,
parsedCondition[0],
parsedCondition[1],
parsedReverseCondition[0],

View file

@ -1,5 +1,5 @@
import { GuildPluginData } from "knub";
import { CountersPluginType, TTrigger } from "../types";
import { CountersPluginType } from "../types";
export function getPrettyNameForCounterTrigger(
pluginData: GuildPluginData<CountersPluginType>,
@ -12,6 +12,6 @@ export function getPrettyNameForCounterTrigger(
return "Unknown Counter Trigger";
}
const trigger = counter.triggers[triggerName] as TTrigger | undefined;
const trigger = counter.triggers[triggerName];
return trigger ? trigger.pretty_name || trigger.name : "Unknown Counter Trigger";
}

View file

@ -1,45 +1,98 @@
import { EventEmitter } from "events";
import * as t from "io-ts";
import { BasePluginType } from "knub";
import z from "zod";
import { GuildCounters } from "../../data/GuildCounters";
import { CounterTrigger } from "../../data/entities/CounterTrigger";
import { tDelayString, tNullable } from "../../utils";
import { CounterTrigger, buildCounterConditionString, getReverseCounterComparisonOp, parseCounterConditionString } from "../../data/entities/CounterTrigger";
import { zBoundedCharacters, zBoundedRecord, zDelayString } from "../../utils";
import Timeout = NodeJS.Timeout;
export const Trigger = t.type({
name: t.string,
pretty_name: tNullable(t.string),
condition: t.string,
reverse_condition: t.string,
});
export type TTrigger = t.TypeOf<typeof Trigger>;
const MAX_COUNTERS = 5;
const MAX_TRIGGERS_PER_COUNTER = 5;
export const Counter = t.type({
name: t.string,
pretty_name: tNullable(t.string),
per_channel: t.boolean,
per_user: t.boolean,
initial_value: t.number,
triggers: t.record(t.string, t.union([t.string, Trigger])),
decay: tNullable(
t.type({
amount: t.number,
every: tDelayString,
}),
export const zTrigger = z.strictObject({
// Dummy type because name gets replaced by the property key in zTriggerInput
name: z.never().optional().transform(() => ""),
pretty_name: zBoundedCharacters(0, 100).nullable().default(null),
condition: zBoundedCharacters(1, 64).refine(
(str) => parseCounterConditionString(str) !== null,
{ message: "Invalid counter trigger condition" },
),
reverse_condition: zBoundedCharacters(1, 64).refine(
(str) => parseCounterConditionString(str) !== null,
{ message: "Invalid counter trigger reverse condition" },
),
can_view: tNullable(t.boolean),
can_edit: tNullable(t.boolean),
can_reset_all: tNullable(t.boolean),
});
export type TCounter = t.TypeOf<typeof Counter>;
export const ConfigSchema = t.type({
counters: t.record(t.string, Counter),
can_view: t.boolean,
can_edit: t.boolean,
can_reset_all: t.boolean,
const zTriggerInput = z.union([zBoundedCharacters(0, 100), zTrigger])
.transform((val, ctx) => {
const ruleName = String(ctx.path[ctx.path.length - 2]).trim();
if (typeof val === "string") {
const parsedCondition = parseCounterConditionString(val);
if (!parsedCondition) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Invalid counter trigger condition",
});
return z.NEVER;
}
return {
name: ruleName,
pretty_name: null,
condition: buildCounterConditionString(parsedCondition[0], parsedCondition[1]),
reverse_condition: buildCounterConditionString(getReverseCounterComparisonOp(parsedCondition[0]), parsedCondition[1]),
};
}
return {
...val,
name: ruleName,
};
});
export const zCounter = z.strictObject({
// Typed as "never" because you are not expected to supply this directly.
// The transform instead picks it up from the property key and the output type is a string.
name: z.never().optional().transform((_, ctx) => {
const ruleName = String(ctx.path[ctx.path.length - 2]).trim();
if (! ruleName) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Counters must have names",
});
return z.NEVER;
}
return ruleName;
}),
pretty_name: zBoundedCharacters(0, 100).nullable().default(null),
per_channel: z.boolean().default(false),
per_user: z.boolean().default(false),
initial_value: z.number().default(0),
triggers: zBoundedRecord(
z.record(
zBoundedCharacters(0, 100),
zTriggerInput,
),
1,
MAX_TRIGGERS_PER_COUNTER,
),
decay: z.strictObject({
amount: z.number(),
every: zDelayString,
}).nullable().default(null),
can_view: z.boolean(),
can_edit: z.boolean(),
can_reset_all: z.boolean(),
});
export const zCountersConfig = z.strictObject({
counters: zBoundedRecord(
z.record(zBoundedCharacters(0, 100), zCounter),
0,
MAX_COUNTERS,
),
can_view: z.boolean(),
can_edit: z.boolean(),
can_reset_all: z.boolean(),
});
export type TConfigSchema = t.TypeOf<typeof ConfigSchema>;
export interface CounterEvents {
trigger: (counterName: string, triggerName: string, channelId: string | null, userId: string | null) => void;
@ -52,7 +105,7 @@ export interface CounterEventEmitter extends EventEmitter {
}
export interface CountersPluginType extends BasePluginType {
config: TConfigSchema;
config: z.infer<typeof zCountersConfig>;
state: {
counters: GuildCounters;
counterIds: Record<string, number>;