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

counters: move triggers to counters plugin; architectural tweaks

This commit is contained in:
Dragory 2021-04-02 16:36:45 +03:00
parent 7f75d6d8d3
commit ab8ea2e7e5
No known key found for this signature in database
GPG key ID: 5F387BA66DF8AAC1
17 changed files with 357 additions and 200 deletions

View file

@ -29,7 +29,6 @@ import { logger } from "../../logger";
import { discardRegExpRunner, getRegExpRunner } from "../../regExpRunners";
import { RunAutomodOnMemberUpdate } from "./events/RunAutomodOnMemberUpdate";
import { CountersPlugin } from "../Counters/CountersPlugin";
import { parseCondition } from "../../data/GuildCounters";
import { runAutomodOnCounterTrigger } from "./events/runAutomodOnCounterTrigger";
import { runAutomodOnModAction } from "./events/runAutomodOnModAction";
import { registerEventListenersFromMap } from "../../utils/registerEventListenersFromMap";
@ -114,15 +113,6 @@ const configPreprocessor: ConfigPreprocessorFn<AutomodPluginType> = options => {
]);
}
}
if (triggerName === "counter") {
const parsedCondition = parseCondition(triggerObj[triggerName]!.condition);
if (parsedCondition == null) {
throw new StrictValidationError([
`Invalid counter condition '${triggerObj[triggerName]!.condition}' in rule <${rule.name}>`,
]);
}
}
}
}
}
@ -229,23 +219,14 @@ export const AutomodPlugin = zeppelinGuildPlugin<AutomodPluginType>()("automod",
async onAfterLoad(pluginData) {
const countersPlugin = pluginData.getPlugin(CountersPlugin);
pluginData.state.onCounterTrigger = (name, condition, channelId, userId) => {
runAutomodOnCounterTrigger(pluginData, name, condition, channelId, userId, false);
pluginData.state.onCounterTrigger = (name, triggerName, channelId, userId) => {
runAutomodOnCounterTrigger(pluginData, name, triggerName, channelId, userId, false);
};
pluginData.state.onCounterReverseTrigger = (name, condition, channelId, userId) => {
runAutomodOnCounterTrigger(pluginData, name, condition, channelId, userId, true);
pluginData.state.onCounterReverseTrigger = (name, triggerName, channelId, userId) => {
runAutomodOnCounterTrigger(pluginData, name, triggerName, channelId, userId, true);
};
const config = pluginData.config.get();
for (const rule of Object.values(config.rules)) {
for (const trigger of rule.triggers) {
if (trigger.counter) {
await countersPlugin.initCounterTrigger(trigger.counter.name, trigger.counter.condition);
}
}
}
countersPlugin.onCounterEvent("trigger", pluginData.state.onCounterTrigger);
countersPlugin.onCounterEvent("reverseTrigger", pluginData.state.onCounterReverseTrigger);

View file

@ -2,30 +2,38 @@ import { GuildPluginData } from "knub";
import { AutomodContext, AutomodPluginType } from "../types";
import { runAutomod } from "../functions/runAutomod";
import { resolveMember, resolveUser, UnknownUser } from "../../../utils";
import { CountersPlugin } from "../../Counters/CountersPlugin";
export async function runAutomodOnCounterTrigger(
pluginData: GuildPluginData<AutomodPluginType>,
counterName: string,
condition: string,
triggerName: string,
channelId: string | null,
userId: string | null,
reverse: boolean,
) {
const user = userId ? await resolveUser(pluginData.client, userId) : undefined;
const member = (userId && (await resolveMember(pluginData.client, pluginData.guild, userId))) || undefined;
const prettyCounterName = pluginData.getPlugin(CountersPlugin).getPrettyNameForCounter(counterName);
const prettyTriggerName = pluginData
.getPlugin(CountersPlugin)
.getPrettyNameForCounterTrigger(counterName, triggerName);
const context: AutomodContext = {
timestamp: Date.now(),
counterTrigger: {
name: counterName,
condition,
counter: counterName,
trigger: triggerName,
prettyCounter: prettyCounterName,
prettyTrigger: prettyTriggerName,
channelId,
userId,
reverse,
},
user: user instanceof UnknownUser ? undefined : user,
member,
// TODO: Channel
};
pluginData.state.queue.add(async () => {

View file

@ -9,8 +9,8 @@ interface CounterTriggerResult {}
export const CounterTrigger = automodTrigger<CounterTriggerResult>()({
configType: t.type({
name: t.string,
condition: t.string,
counter: t.string,
trigger: t.string,
reverse: tNullable(t.boolean),
}),
@ -21,11 +21,11 @@ export const CounterTrigger = automodTrigger<CounterTriggerResult>()({
return;
}
if (context.counterTrigger.name !== triggerConfig.name) {
if (context.counterTrigger.counter !== triggerConfig.counter) {
return;
}
if (context.counterTrigger.condition !== triggerConfig.condition) {
if (context.counterTrigger.trigger !== triggerConfig.trigger) {
return;
}
@ -40,7 +40,13 @@ export const CounterTrigger = automodTrigger<CounterTriggerResult>()({
},
renderMatchInformation({ matchResult, pluginData, contexts, triggerConfig }) {
// TODO: Show user, channel, reverse
return `Matched counter \`${triggerConfig.name} ${triggerConfig.condition}\``;
let str = `Matched counter trigger \`${contexts[0].counterTrigger!.prettyCounter} / ${
contexts[0].counterTrigger!.prettyTrigger
}\``;
if (contexts[0].counterTrigger!.reverse) {
str += " (reverse)";
}
return str;
},
});

View file

@ -103,8 +103,10 @@ export interface AutomodContext {
actioned?: boolean;
counterTrigger?: {
name: string;
condition: string;
counter: string;
trigger: string;
prettyCounter: string;
prettyTrigger: string;
channelId: string | null;
userId: string | null;
reverse: boolean;

View file

@ -1,5 +1,5 @@
import { zeppelinGuildPlugin } from "../ZeppelinPluginBlueprint";
import { ConfigSchema, CountersPluginType } from "./types";
import { ConfigSchema, CountersPluginType, TTrigger } from "./types";
import { GuildCounters } from "../../data/GuildCounters";
import { mapToPublicFn } from "../../pluginUtils";
import { changeCounterValue } from "./functions/changeCounterValue";
@ -10,16 +10,23 @@ import { onCounterEvent } from "./functions/onCounterEvent";
import { offCounterEvent } from "./functions/offCounterEvent";
import { emitCounterEvent } from "./functions/emitCounterEvent";
import { ConfigPreprocessorFn } from "knub/dist/config/configTypes";
import { initCounterTrigger } from "./functions/initCounterTrigger";
import { decayCounter } from "./functions/decayCounter";
import { validateCondition } from "./functions/validateCondition";
import { StrictValidationError } from "../../validatorUtils";
import { PluginOptions } from "knub";
import { ViewCounterCmd } from "./commands/ViewCounterCmd";
import { AddCounterCmd } from "./commands/AddCounterCmd";
import { SetCounterCmd } from "./commands/SetCounterCmd";
import {
buildCounterConditionString,
CounterTrigger,
getReverseCounterComparisonOp,
parseCounterConditionString,
} from "../../data/entities/CounterTrigger";
import { getPrettyNameForCounter } from "./functions/getPrettyNameForCounter";
import { getPrettyNameForCounterTrigger } from "./functions/getPrettyNameForCounterTrigger";
const MAX_COUNTERS = 5;
const MAX_TRIGGERS_PER_COUNTER = 5;
const DECAY_APPLY_INTERVAL = 5 * MINUTES;
const defaultOptions: PluginOptions<CountersPluginType> = {
@ -45,14 +52,40 @@ const defaultOptions: PluginOptions<CountersPluginType> = {
};
const configPreprocessor: ConfigPreprocessorFn<CountersPluginType> = options => {
for (const counter of Object.values(options.config?.counters || {})) {
for (const [counterName, counter] of Object.entries(options.config?.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: Partial<TTrigger> = typeof trigger === "string" ? { condition: trigger } : trigger;
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(options.config?.counters || {}).length > MAX_COUNTERS) {
throw new StrictValidationError([`You can only have at most ${MAX_COUNTERS} active counters`]);
throw new StrictValidationError([`You can only have at most ${MAX_COUNTERS} counters`]);
}
return options;
@ -76,14 +109,12 @@ export const CountersPlugin = zeppelinGuildPlugin<CountersPluginType>()("counter
public: {
// Change a counter's value by a relative amount, e.g. +5
changeCounterValue: mapToPublicFn(changeCounterValue),
// Set a counter's value to an absolute value
setCounterValue: mapToPublicFn(setCounterValue),
// Initialize a trigger. Once initialized, events will be fired when this trigger is triggered.
initCounterTrigger: mapToPublicFn(initCounterTrigger),
// Validate a trigger's condition string
validateCondition: mapToPublicFn(validateCondition),
getPrettyNameForCounter: mapToPublicFn(getPrettyNameForCounter),
getPrettyNameForCounterTrigger: mapToPublicFn(getPrettyNameForCounterTrigger),
onCounterEvent: mapToPublicFn(onCounterEvent),
offCounterEvent: mapToPublicFn(offCounterEvent),
@ -99,22 +130,48 @@ export const CountersPlugin = zeppelinGuildPlugin<CountersPluginType>()("counter
async onLoad(pluginData) {
pluginData.state.counters = new GuildCounters(pluginData.guild.id);
pluginData.state.events = new EventEmitter();
pluginData.state.counterTriggersByCounterId = new Map();
const activeTriggerIds: number[] = [];
// Initialize and store the IDs of each of the counters internally
pluginData.state.counterIds = {};
const config = pluginData.config.get();
for (const [counterName, counter] of Object.entries(config.counters)) {
for (const counter of Object.values(config.counters)) {
const dbCounter = await pluginData.state.counters.findOrCreateCounter(
counterName,
counter.name,
counter.per_channel,
counter.per_user,
);
pluginData.state.counterIds[counterName] = dbCounter.id;
pluginData.state.counterIds[counter.name] = dbCounter.id;
const thisCounterTriggers: CounterTrigger[] = [];
pluginData.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)!;
const counterTrigger = await pluginData.state.counters.initCounterTrigger(
dbCounter.id,
theTrigger.name,
parsedCondition[0],
parsedCondition[1],
parsedReverseCondition[0],
parsedReverseCondition[1],
);
activeTriggerIds.push(counterTrigger.id);
thisCounterTriggers.push(counterTrigger);
}
}
// Mark old/unused counters to be deleted later
await pluginData.state.counters.markUnusedCountersToBeDeleted([...Object.values(pluginData.state.counterIds)]);
// Mark old/unused triggers to be deleted later
await pluginData.state.counters.markUnusedTriggersToBeDeleted(activeTriggerIds);
// Start decay timers
pluginData.state.decayTimers = [];
for (const [counterName, counter] of Object.entries(config.counters)) {
@ -130,13 +187,6 @@ export const CountersPlugin = zeppelinGuildPlugin<CountersPluginType>()("counter
}, DECAY_APPLY_INTERVAL),
);
}
// Initially set the counter trigger map to just an empty map
// The actual triggers are added by other plugins via initCounterTrigger()
pluginData.state.counterTriggersByCounterId = new Map();
// Mark all triggers to be deleted later. This is cancelled/reset when a plugin adds the trigger again via initCounterTrigger().
await pluginData.state.counters.markAllTriggersTobeDeleted();
},
onUnload(pluginData) {

View file

@ -1,6 +1,5 @@
import { GuildPluginData } from "knub";
import { CountersPluginType } from "../types";
import { buildConditionString } from "../../../data/GuildCounters";
import { CounterTrigger } from "../../../data/entities/CounterTrigger";
import { emitCounterEvent } from "./emitCounterEvent";
@ -11,13 +10,6 @@ export async function checkAllValuesForReverseTrigger(
) {
const triggeredContexts = await pluginData.state.counters.checkAllValuesForReverseTrigger(counterTrigger);
for (const context of triggeredContexts) {
emitCounterEvent(
pluginData,
"reverseTrigger",
counterName,
buildConditionString(counterTrigger.comparison_op, counterTrigger.comparison_value),
context.channelId,
context.userId,
);
emitCounterEvent(pluginData, "reverseTrigger", counterName, counterTrigger.name, context.channelId, context.userId);
}
}

View file

@ -1,6 +1,5 @@
import { GuildPluginData } from "knub";
import { CountersPluginType } from "../types";
import { buildConditionString } from "../../../data/GuildCounters";
import { CounterTrigger } from "../../../data/entities/CounterTrigger";
import { emitCounterEvent } from "./emitCounterEvent";
@ -11,13 +10,6 @@ export async function checkAllValuesForTrigger(
) {
const triggeredContexts = await pluginData.state.counters.checkAllValuesForTrigger(counterTrigger);
for (const context of triggeredContexts) {
emitCounterEvent(
pluginData,
"trigger",
counterName,
buildConditionString(counterTrigger.comparison_op, counterTrigger.comparison_value),
context.channelId,
context.userId,
);
emitCounterEvent(pluginData, "trigger", counterName, counterTrigger.name, context.channelId, context.userId);
}
}

View file

@ -1,6 +1,5 @@
import { GuildPluginData } from "knub";
import { CountersPluginType } from "../types";
import { buildConditionString } from "../../../data/GuildCounters";
import { CounterTrigger } from "../../../data/entities/CounterTrigger";
import { emitCounterEvent } from "./emitCounterEvent";
@ -13,13 +12,6 @@ export async function checkCounterTrigger(
) {
const triggered = await pluginData.state.counters.checkForTrigger(counterTrigger, channelId, userId);
if (triggered) {
await emitCounterEvent(
pluginData,
"trigger",
counterName,
buildConditionString(counterTrigger.comparison_op, counterTrigger.comparison_value),
channelId,
userId,
);
await emitCounterEvent(pluginData, "trigger", counterName, counterTrigger.name, channelId, userId);
}
}

View file

@ -1,6 +1,5 @@
import { GuildPluginData } from "knub";
import { CountersPluginType } from "../types";
import { buildConditionString } from "../../../data/GuildCounters";
import { CounterTrigger } from "../../../data/entities/CounterTrigger";
import { emitCounterEvent } from "./emitCounterEvent";
@ -13,13 +12,6 @@ export async function checkReverseCounterTrigger(
) {
const triggered = await pluginData.state.counters.checkForReverseTrigger(counterTrigger, channelId, userId);
if (triggered) {
await emitCounterEvent(
pluginData,
"reverseTrigger",
counterName,
buildConditionString(counterTrigger.comparison_op, counterTrigger.comparison_value),
channelId,
userId,
);
await emitCounterEvent(pluginData, "reverseTrigger", counterName, counterTrigger.name, channelId, userId);
}
}

View file

@ -0,0 +1,8 @@
import { CountersPluginType } from "../types";
import { GuildPluginData } from "knub";
export function getPrettyNameForCounter(pluginData: GuildPluginData<CountersPluginType>, counterName: string) {
const config = pluginData.config.get();
const counter = config.counters[counterName];
return counter ? counter.pretty_name || counter.name : "Unknown Counter";
}

View file

@ -0,0 +1,17 @@
import { CountersPluginType, TTrigger } from "../types";
import { GuildPluginData } from "knub";
export function getPrettyNameForCounterTrigger(
pluginData: GuildPluginData<CountersPluginType>,
counterName: string,
triggerName: string,
) {
const config = pluginData.config.get();
const counter = config.counters[counterName];
if (!counter) {
return "Unknown Counter Trigger";
}
const trigger = counter.triggers[triggerName] as TTrigger | undefined;
return trigger ? trigger.pretty_name || trigger.name : "Unknown Counter Trigger";
}

View file

@ -1,31 +0,0 @@
import { GuildPluginData } from "knub";
import { CountersPluginType } from "../types";
import { parseCondition } from "../../../data/GuildCounters";
/**
* Initialize a counter trigger.
* After a counter trigger has been initialized, it will be checked against whenever the counter's values change.
* If the trigger is triggered, an event is emitted.
*/
export async function initCounterTrigger(
pluginData: GuildPluginData<CountersPluginType>,
counterName: string,
condition: string,
) {
const counterId = pluginData.state.counterIds[counterName];
if (!counterId) {
throw new Error(`Unknown counter: ${counterName}`);
}
const parsedComparison = parseCondition(condition);
if (!parsedComparison) {
throw new Error(`Invalid comparison string: ${condition}`);
}
const [comparisonOp, comparisonValue] = parsedComparison;
const counterTrigger = await pluginData.state.counters.initCounterTrigger(counterId, comparisonOp, comparisonValue);
if (!pluginData.state.counterTriggersByCounterId.has(counterId)) {
pluginData.state.counterTriggersByCounterId.set(counterId, new Map());
}
pluginData.state.counterTriggersByCounterId.get(counterId)!.set(counterTrigger.id, counterTrigger);
}

View file

@ -1,8 +0,0 @@
import { GuildPluginData } from "knub";
import { CountersPluginType } from "../types";
import { parseCondition } from "../../../data/GuildCounters";
export function validateCondition(pluginData: GuildPluginData<CountersPluginType>, condition: string) {
const parsed = parseCondition(condition);
return parsed != null;
}

View file

@ -6,11 +6,21 @@ import { EventEmitter } from "events";
import { CounterTrigger } from "../../data/entities/CounterTrigger";
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>;
export const Counter = t.type({
name: tNullable(t.string),
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,
@ -30,8 +40,8 @@ export const ConfigSchema = t.type({
export type TConfigSchema = t.TypeOf<typeof ConfigSchema>;
export interface CounterEvents {
trigger: (name: string, condition: string, channelId: string | null, userId: string | null) => void;
reverseTrigger: (name: string, condition: string, channelId: string | null, userId: string | null) => void;
trigger: (counterName: string, triggerName: string, channelId: string | null, userId: string | null) => void;
reverseTrigger: (counterName: string, triggerName: string, channelId: string | null, userId: string | null) => void;
}
export interface CounterEventEmitter extends EventEmitter {
@ -46,6 +56,6 @@ export interface CountersPluginType extends BasePluginType {
counterIds: Record<string, number>;
decayTimers: Timeout[];
events: CounterEventEmitter;
counterTriggersByCounterId: Map<number, Map<number, CounterTrigger>>;
counterTriggersByCounterId: Map<number, CounterTrigger[]>;
};
}