diff --git a/backend/src/data/GuildCounters.ts b/backend/src/data/GuildCounters.ts index 3f05972e..234c481f 100644 --- a/backend/src/data/GuildCounters.ts +++ b/backend/src/data/GuildCounters.ts @@ -1,44 +1,18 @@ import { BaseGuildRepository } from "./BaseGuildRepository"; -import { getRepository, In, IsNull, LessThan, Not, Repository } from "typeorm"; +import { FindConditions, getRepository, In, IsNull, LessThan, Not, Repository } from "typeorm"; import { Counter } from "./entities/Counter"; import { CounterValue } from "./entities/CounterValue"; -import { CounterTrigger, TRIGGER_COMPARISON_OPS, TriggerComparisonOp } from "./entities/CounterTrigger"; +import { + CounterTrigger, + isValidCounterComparisonOp, + TRIGGER_COMPARISON_OPS, + TriggerComparisonOp, +} from "./entities/CounterTrigger"; import { CounterTriggerState } from "./entities/CounterTriggerState"; import moment from "moment-timezone"; import { DAYS, DBDateFormat, HOURS, MINUTES } from "../utils"; import { connection } from "./db"; -const comparisonStringRegex = new RegExp(`^(${TRIGGER_COMPARISON_OPS.join("|")})([1-9]\\d*)$`); - -/** - * @return Parsed comparison op and value, or null if the comparison string was invalid - */ -export function parseCondition(str: string): [TriggerComparisonOp, number] | null { - const matches = str.match(comparisonStringRegex); - return matches ? [matches[1] as TriggerComparisonOp, parseInt(matches[2], 10)] : null; -} - -export function buildConditionString(comparisonOp: TriggerComparisonOp, comparisonValue: number): string { - return `${comparisonOp}${comparisonValue}`; -} - -function isValidComparisonOp(op: string): boolean { - return TRIGGER_COMPARISON_OPS.includes(op as any); -} - -const REVERSE_OPS: Record = { - "=": "!=", - "!=": "=", - ">": "<=", - "<": ">=", - ">=": "<", - "<=": ">", -}; - -function getReverseComparisonOp(op: TriggerComparisonOp): TriggerComparisonOp { - return REVERSE_OPS[op]; -} - const DELETE_UNUSED_COUNTERS_AFTER = 1 * DAYS; const DELETE_UNUSED_COUNTER_TRIGGERS_AFTER = 1 * DAYS; @@ -92,6 +66,8 @@ export class GuildCounters extends BaseGuildRepository { // If the existing counter's properties match the ones we're looking for, return it. // Otherwise, delete the existing counter and re-create it with the proper properties. if (existing.per_channel === perChannel && existing.per_user === perUser) { + await this.counters.update({ id: existing.id }, { delete_at: null }); + return existing; } @@ -114,24 +90,23 @@ export class GuildCounters extends BaseGuildRepository { } async markUnusedCountersToBeDeleted(idsToKeep: number[]): Promise { - if (idsToKeep.length === 0) { - return; + const criteria: FindConditions = { + guild_id: this.guildId, + delete_at: IsNull(), + }; + + if (idsToKeep.length) { + criteria.id = Not(In(idsToKeep)); } const deleteAt = moment .utc() .add(DELETE_UNUSED_COUNTERS_AFTER, "ms") .format(DBDateFormat); - await this.counters.update( - { - guild_id: this.guildId, - id: Not(In(idsToKeep)), - delete_at: IsNull(), - }, - { - delete_at: deleteAt, - }, - ); + + await this.counters.update(criteria, { + delete_at: deleteAt, + }); } async deleteCountersMarkedToBeDeleted(): Promise { @@ -230,17 +205,37 @@ export class GuildCounters extends BaseGuildRepository { ); } - async markAllTriggersTobeDeleted() { - const deleteAt = moment - .utc() - .add(DELETE_UNUSED_COUNTER_TRIGGERS_AFTER, "ms") - .format(DBDateFormat); - await this.counterTriggers.update( - {}, - { - delete_at: deleteAt, - }, - ); + async markUnusedTriggersToBeDeleted(triggerIdsToKeep: number[]) { + let triggersToMarkQuery = this.counterTriggers + .createQueryBuilder("counterTriggers") + .innerJoin(Counter, "counters", "counters.id = counterTriggers.counter_id") + .where("counters.guild_id = :guildId", { guildId: this.guildId }); + + // If there are no active triggers, we just mark all triggers from the guild to be deleted. + // Otherwise, we mark all but the active triggers in the guild. + if (triggerIdsToKeep.length) { + triggersToMarkQuery = triggersToMarkQuery.andWhere("counterTriggers.id NOT IN (:...triggerIds)", { + triggerIds: triggerIdsToKeep, + }); + } + + const triggersToMark = await triggersToMarkQuery.getMany(); + + if (triggersToMark.length) { + const deleteAt = moment + .utc() + .add(DELETE_UNUSED_COUNTER_TRIGGERS_AFTER, "ms") + .format(DBDateFormat); + + await this.counterTriggers.update( + { + id: In(triggersToMark.map(t => t.id)), + }, + { + delete_at: deleteAt, + }, + ); + } } async deleteTriggersMarkedToBeDeleted(): Promise { @@ -253,34 +248,53 @@ export class GuildCounters extends BaseGuildRepository { async initCounterTrigger( counterId: number, + triggerName: string, comparisonOp: TriggerComparisonOp, comparisonValue: number, + reverseComparisonOp: TriggerComparisonOp, + reverseComparisonValue: number, ): Promise { - if (!isValidComparisonOp(comparisonOp)) { + if (!isValidCounterComparisonOp(comparisonOp)) { throw new Error(`Invalid comparison op: ${comparisonOp}`); } + if (!isValidCounterComparisonOp(reverseComparisonOp)) { + throw new Error(`Invalid comparison op: ${reverseComparisonOp}`); + } + if (typeof comparisonValue !== "number") { throw new Error(`Invalid comparison value: ${comparisonValue}`); } + if (typeof reverseComparisonValue !== "number") { + throw new Error(`Invalid comparison value: ${reverseComparisonValue}`); + } + return connection.transaction(async entityManager => { const existing = await entityManager.findOne(CounterTrigger, { counter_id: counterId, - comparison_op: comparisonOp, - comparison_value: comparisonValue, + name: triggerName, }); if (existing) { // Since all existing triggers are marked as to-be-deleted before they are re-initialized, this needs to be reset - await entityManager.update(CounterTrigger, existing.id, { delete_at: null }); + await entityManager.update(CounterTrigger, existing.id, { + comparison_op: comparisonOp, + comparison_value: comparisonValue, + reverse_comparison_op: reverseComparisonOp, + reverse_comparison_value: reverseComparisonValue, + delete_at: null, + }); return existing; } const insertResult = await entityManager.insert(CounterTrigger, { counter_id: counterId, + name: triggerName, comparison_op: comparisonOp, comparison_value: comparisonValue, + reverse_comparison_op: reverseComparisonOp, + reverse_comparison_value: reverseComparisonValue, }); return (await entityManager.findOne(CounterTrigger, insertResult.identifiers[0].id))!; @@ -375,8 +389,8 @@ export class GuildCounters extends BaseGuildRepository { CounterTriggerState, matchingValues.map(row => ({ trigger_id: counterTrigger.id, - channelId: row.channel_id, - userId: row.user_id, + channel_id: row.channel_id, + user_id: row.user_id, })), ); } @@ -408,7 +422,6 @@ export class GuildCounters extends BaseGuildRepository { userId = userId || "0"; return connection.transaction(async entityManager => { - const reverseOp = getReverseComparisonOp(counterTrigger.comparison_op); const matchingValue = await entityManager .createQueryBuilder(CounterValue, "cv") .innerJoin( @@ -417,7 +430,9 @@ export class GuildCounters extends BaseGuildRepository { "triggerStates.trigger_id = :triggerId AND triggerStates.user_id = cv.user_id AND triggerStates.channel_id = cv.channel_id", { triggerId: counterTrigger.id }, ) - .where(`cv.value ${reverseOp} :value`, { value: counterTrigger.comparison_value }) + .where(`cv.value ${counterTrigger.reverse_comparison_op} :value`, { + value: counterTrigger.reverse_comparison_value, + }) .andWhere(`cv.counter_id = :counterId`, { counterId: counterTrigger.counter_id }) .andWhere(`cv.channel_id = :channelId AND cv.user_id = :userId`, { channelId, userId }) .getOne(); @@ -446,7 +461,6 @@ export class GuildCounters extends BaseGuildRepository { counterTrigger: CounterTrigger, ): Promise> { return connection.transaction(async entityManager => { - const reverseOp = getReverseComparisonOp(counterTrigger.comparison_op); const matchingValues: Array<{ id: string; triggerStateId: string; @@ -460,7 +474,9 @@ export class GuildCounters extends BaseGuildRepository { "triggerStates.trigger_id = :triggerId AND triggerStates.user_id = cv.user_id AND triggerStates.channel_id = cv.channel_id", { triggerId: counterTrigger.id }, ) - .where(`cv.value ${reverseOp} :value`, { value: counterTrigger.comparison_value }) + .where(`cv.value ${counterTrigger.reverse_comparison_op} :value`, { + value: counterTrigger.reverse_comparison_value, + }) .andWhere(`cv.counter_id = :counterId`, { counterId: counterTrigger.counter_id }) .select([ "cv.id AS id", diff --git a/backend/src/data/entities/CounterTrigger.ts b/backend/src/data/entities/CounterTrigger.ts index 7cf20700..71b6bf52 100644 --- a/backend/src/data/entities/CounterTrigger.ts +++ b/backend/src/data/entities/CounterTrigger.ts @@ -4,6 +4,37 @@ export const TRIGGER_COMPARISON_OPS = ["=", "!=", ">", "<", ">=", "<="] as const export type TriggerComparisonOp = typeof TRIGGER_COMPARISON_OPS[number]; +const REVERSE_OPS: Record = { + "=": "!=", + "!=": "=", + ">": "<=", + "<": ">=", + ">=": "<", + "<=": ">", +}; + +export function getReverseCounterComparisonOp(op: TriggerComparisonOp): TriggerComparisonOp { + return REVERSE_OPS[op]; +} + +const comparisonStringRegex = new RegExp(`^(${TRIGGER_COMPARISON_OPS.join("|")})([1-9]\\d*)$`); + +/** + * @return Parsed comparison op and value, or null if the comparison string was invalid + */ +export function parseCounterConditionString(str: string): [TriggerComparisonOp, number] | null { + const matches = str.match(comparisonStringRegex); + return matches ? [matches[1] as TriggerComparisonOp, parseInt(matches[2], 10)] : null; +} + +export function buildCounterConditionString(comparisonOp: TriggerComparisonOp, comparisonValue: number): string { + return `${comparisonOp}${comparisonValue}`; +} + +export function isValidCounterComparisonOp(op: string): boolean { + return TRIGGER_COMPARISON_OPS.includes(op as any); +} + @Entity("counter_triggers") export class CounterTrigger { @PrimaryGeneratedColumn() @@ -12,12 +43,21 @@ export class CounterTrigger { @Column() counter_id: number; + @Column() + name: string; + @Column({ type: "varchar" }) comparison_op: TriggerComparisonOp; @Column() comparison_value: number; + @Column({ type: "varchar" }) + reverse_comparison_op: TriggerComparisonOp; + + @Column() + reverse_comparison_value: number; + @Column({ type: "datetime", nullable: true }) delete_at: string | null; } diff --git a/backend/src/migrations/1617363975046-UpdateCounterTriggers.ts b/backend/src/migrations/1617363975046-UpdateCounterTriggers.ts new file mode 100644 index 00000000..2e1e8664 --- /dev/null +++ b/backend/src/migrations/1617363975046-UpdateCounterTriggers.ts @@ -0,0 +1,90 @@ +import { MigrationInterface, QueryRunner, TableColumn, TableIndex } from "typeorm"; +import { TableForeignKey } from "typeorm/index"; + +export class UpdateCounterTriggers1617363975046 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + // Since we're adding a non-nullable unique name column and existing triggers won't have that, clear the table first + await queryRunner.query("DELETE FROM counter_triggers"); + + await queryRunner.addColumns("counter_triggers", [ + new TableColumn({ + name: "name", + type: "varchar", + length: "255", + }), + + new TableColumn({ + name: "reverse_comparison_op", + type: "varchar", + length: "16", + }), + + new TableColumn({ + name: "reverse_comparison_value", + type: "int", + }), + ]); + + // Drop foreign key for counter_id -- needed to be able to drop the following unique index + await queryRunner.dropForeignKey("counter_triggers", "FK_6bb47849ec95c87e58c5d3e6ae1"); + + // Index for ["counter_id", "comparison_op", "comparison_value"] + await queryRunner.dropIndex("counter_triggers", "IDX_ddc8a6701f1234b926d35aebf3"); + + await queryRunner.createIndex( + "counter_triggers", + new TableIndex({ + columnNames: ["counter_id", "name"], + isUnique: true, + }), + ); + + // Recreate foreign key for counter_id + await queryRunner.createForeignKey( + "counter_triggers", + new TableForeignKey({ + columnNames: ["counter_id"], + referencedTableName: "counters", + referencedColumnNames: ["id"], + onDelete: "CASCADE", + onUpdate: "CASCADE", + }), + ); + } + + public async down(queryRunner: QueryRunner): Promise { + // Since we're going back to unique comparison op and comparison value in this reverse-migration, + // clear table contents first so we don't run into any conflicts with triggers with different names but identical comparison op and comparison value + await queryRunner.query("DELETE FROM counter_triggers"); + + // Drop foreign key for counter_id -- needed to be able to drop the following unique index + await queryRunner.dropForeignKey("counter_triggers", "FK_6bb47849ec95c87e58c5d3e6ae1"); + + // Index for ["counter_id", "name"] + await queryRunner.dropIndex("counter_triggers", "IDX_2ec128e1d74bedd0288b60cdd1"); + + await queryRunner.createIndex( + "counter_triggers", + new TableIndex({ + columnNames: ["counter_id", "comparison_op", "comparison_value"], + isUnique: true, + }), + ); + + // Recreate foreign key for counter_id + await queryRunner.createForeignKey( + "counter_triggers", + new TableForeignKey({ + columnNames: ["counter_id"], + referencedTableName: "counters", + referencedColumnNames: ["id"], + onDelete: "CASCADE", + onUpdate: "CASCADE", + }), + ); + + await queryRunner.dropColumn("counter_triggers", "reverse_comparison_value"); + await queryRunner.dropColumn("counter_triggers", "reverse_comparison_op"); + await queryRunner.dropColumn("counter_triggers", "name"); + } +} diff --git a/backend/src/plugins/Automod/AutomodPlugin.ts b/backend/src/plugins/Automod/AutomodPlugin.ts index 9cd380c2..7bebd606 100644 --- a/backend/src/plugins/Automod/AutomodPlugin.ts +++ b/backend/src/plugins/Automod/AutomodPlugin.ts @@ -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 = 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()("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); diff --git a/backend/src/plugins/Automod/events/runAutomodOnCounterTrigger.ts b/backend/src/plugins/Automod/events/runAutomodOnCounterTrigger.ts index 50939d80..2b1b974d 100644 --- a/backend/src/plugins/Automod/events/runAutomodOnCounterTrigger.ts +++ b/backend/src/plugins/Automod/events/runAutomodOnCounterTrigger.ts @@ -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, 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 () => { diff --git a/backend/src/plugins/Automod/triggers/counter.ts b/backend/src/plugins/Automod/triggers/counter.ts index 6b3c3288..ddfda013 100644 --- a/backend/src/plugins/Automod/triggers/counter.ts +++ b/backend/src/plugins/Automod/triggers/counter.ts @@ -9,8 +9,8 @@ interface CounterTriggerResult {} export const CounterTrigger = automodTrigger()({ 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()({ 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()({ }, 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; }, }); diff --git a/backend/src/plugins/Automod/types.ts b/backend/src/plugins/Automod/types.ts index f6e273cb..37202143 100644 --- a/backend/src/plugins/Automod/types.ts +++ b/backend/src/plugins/Automod/types.ts @@ -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; diff --git a/backend/src/plugins/Counters/CountersPlugin.ts b/backend/src/plugins/Counters/CountersPlugin.ts index 56af0f96..b6789e81 100644 --- a/backend/src/plugins/Counters/CountersPlugin.ts +++ b/backend/src/plugins/Counters/CountersPlugin.ts @@ -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 = { @@ -45,14 +52,40 @@ const defaultOptions: PluginOptions = { }; const configPreprocessor: ConfigPreprocessorFn = 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 = 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()("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()("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()("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) { diff --git a/backend/src/plugins/Counters/functions/checkAllValuesForReverseTrigger.ts b/backend/src/plugins/Counters/functions/checkAllValuesForReverseTrigger.ts index 9336906e..19998225 100644 --- a/backend/src/plugins/Counters/functions/checkAllValuesForReverseTrigger.ts +++ b/backend/src/plugins/Counters/functions/checkAllValuesForReverseTrigger.ts @@ -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); } } diff --git a/backend/src/plugins/Counters/functions/checkAllValuesForTrigger.ts b/backend/src/plugins/Counters/functions/checkAllValuesForTrigger.ts index 277cff15..02b02e5f 100644 --- a/backend/src/plugins/Counters/functions/checkAllValuesForTrigger.ts +++ b/backend/src/plugins/Counters/functions/checkAllValuesForTrigger.ts @@ -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); } } diff --git a/backend/src/plugins/Counters/functions/checkCounterTrigger.ts b/backend/src/plugins/Counters/functions/checkCounterTrigger.ts index e45cf28d..5bdd1b05 100644 --- a/backend/src/plugins/Counters/functions/checkCounterTrigger.ts +++ b/backend/src/plugins/Counters/functions/checkCounterTrigger.ts @@ -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); } } diff --git a/backend/src/plugins/Counters/functions/checkReverseCounterTrigger.ts b/backend/src/plugins/Counters/functions/checkReverseCounterTrigger.ts index 5ed9b3d7..e601dd9d 100644 --- a/backend/src/plugins/Counters/functions/checkReverseCounterTrigger.ts +++ b/backend/src/plugins/Counters/functions/checkReverseCounterTrigger.ts @@ -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); } } diff --git a/backend/src/plugins/Counters/functions/getPrettyNameForCounter.ts b/backend/src/plugins/Counters/functions/getPrettyNameForCounter.ts new file mode 100644 index 00000000..766b4ef3 --- /dev/null +++ b/backend/src/plugins/Counters/functions/getPrettyNameForCounter.ts @@ -0,0 +1,8 @@ +import { CountersPluginType } from "../types"; +import { GuildPluginData } from "knub"; + +export function getPrettyNameForCounter(pluginData: GuildPluginData, counterName: string) { + const config = pluginData.config.get(); + const counter = config.counters[counterName]; + return counter ? counter.pretty_name || counter.name : "Unknown Counter"; +} diff --git a/backend/src/plugins/Counters/functions/getPrettyNameForCounterTrigger.ts b/backend/src/plugins/Counters/functions/getPrettyNameForCounterTrigger.ts new file mode 100644 index 00000000..d7a2b923 --- /dev/null +++ b/backend/src/plugins/Counters/functions/getPrettyNameForCounterTrigger.ts @@ -0,0 +1,17 @@ +import { CountersPluginType, TTrigger } from "../types"; +import { GuildPluginData } from "knub"; + +export function getPrettyNameForCounterTrigger( + pluginData: GuildPluginData, + 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"; +} diff --git a/backend/src/plugins/Counters/functions/initCounterTrigger.ts b/backend/src/plugins/Counters/functions/initCounterTrigger.ts deleted file mode 100644 index afc5e9c9..00000000 --- a/backend/src/plugins/Counters/functions/initCounterTrigger.ts +++ /dev/null @@ -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, - 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); -} diff --git a/backend/src/plugins/Counters/functions/validateCondition.ts b/backend/src/plugins/Counters/functions/validateCondition.ts deleted file mode 100644 index ca5e2e88..00000000 --- a/backend/src/plugins/Counters/functions/validateCondition.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { GuildPluginData } from "knub"; -import { CountersPluginType } from "../types"; -import { parseCondition } from "../../../data/GuildCounters"; - -export function validateCondition(pluginData: GuildPluginData, condition: string) { - const parsed = parseCondition(condition); - return parsed != null; -} diff --git a/backend/src/plugins/Counters/types.ts b/backend/src/plugins/Counters/types.ts index 3102e463..ec534452 100644 --- a/backend/src/plugins/Counters/types.ts +++ b/backend/src/plugins/Counters/types.ts @@ -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; + 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; 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; decayTimers: Timeout[]; events: CounterEventEmitter; - counterTriggersByCounterId: Map>; + counterTriggersByCounterId: Map; }; }