mirror of
https://github.com/ZeppelinBot/Zeppelin.git
synced 2025-03-15 05:41:51 +00:00
counters: move triggers to counters plugin; architectural tweaks
This commit is contained in:
parent
7f75d6d8d3
commit
ab8ea2e7e5
17 changed files with 357 additions and 200 deletions
|
@ -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<TriggerComparisonOp, TriggerComparisonOp> = {
|
||||
"=": "!=",
|
||||
"!=": "=",
|
||||
">": "<=",
|
||||
"<": ">=",
|
||||
">=": "<",
|
||||
"<=": ">",
|
||||
};
|
||||
|
||||
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<void> {
|
||||
if (idsToKeep.length === 0) {
|
||||
return;
|
||||
const criteria: FindConditions<Counter> = {
|
||||
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<void> {
|
||||
|
@ -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<void> {
|
||||
|
@ -253,34 +248,53 @@ export class GuildCounters extends BaseGuildRepository {
|
|||
|
||||
async initCounterTrigger(
|
||||
counterId: number,
|
||||
triggerName: string,
|
||||
comparisonOp: TriggerComparisonOp,
|
||||
comparisonValue: number,
|
||||
reverseComparisonOp: TriggerComparisonOp,
|
||||
reverseComparisonValue: number,
|
||||
): Promise<CounterTrigger> {
|
||||
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<Array<{ channelId: string; userId: string }>> {
|
||||
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",
|
||||
|
|
|
@ -4,6 +4,37 @@ export const TRIGGER_COMPARISON_OPS = ["=", "!=", ">", "<", ">=", "<="] as const
|
|||
|
||||
export type TriggerComparisonOp = typeof TRIGGER_COMPARISON_OPS[number];
|
||||
|
||||
const REVERSE_OPS: Record<TriggerComparisonOp, TriggerComparisonOp> = {
|
||||
"=": "!=",
|
||||
"!=": "=",
|
||||
">": "<=",
|
||||
"<": ">=",
|
||||
">=": "<",
|
||||
"<=": ">",
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -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<void> {
|
||||
// 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<void> {
|
||||
// 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");
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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";
|
||||
}
|
|
@ -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";
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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[]>;
|
||||
};
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue