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

Counters v0.9

Includes automod trigger/action. No user-facing commands yet.
This commit is contained in:
Dragory 2021-02-13 17:29:10 +02:00
parent ec37cf27a2
commit c3407e2d5d
No known key found for this signature in database
GPG key ID: 5F387BA66DF8AAC1
29 changed files with 1387 additions and 3 deletions

View file

@ -0,0 +1,48 @@
import { GuildPluginData } from "knub";
import { CountersPluginType } from "../types";
import { checkCounterTrigger } from "./checkCounterTrigger";
import { checkReverseCounterTrigger } from "./checkReverseCounterTrigger";
export async function changeCounterValue(
pluginData: GuildPluginData<CountersPluginType>,
counterName: string,
channelId: string | null,
userId: string | null,
change: number,
) {
const config = pluginData.config.get();
const counter = config.counters[counterName];
if (!counter) {
throw new Error(`Unknown counter: ${counterName}`);
}
if (counter.per_channel && !channelId) {
throw new Error(`Counter is per channel but no channel ID was supplied`);
}
if (counter.per_user && !userId) {
throw new Error(`Counter is per user but no user ID was supplied`);
}
channelId = counter.per_channel ? channelId : null;
userId = counter.per_user ? userId : null;
const counterId = pluginData.state.counterIds[counterName];
const lock = await pluginData.locks.acquire(counterId.toString());
await pluginData.state.counters.changeCounterValue(counterId, channelId, userId, change);
// Check for trigger matches, if any, when the counter value changes
const triggers = pluginData.state.counterTriggersByCounterId.get(counterId);
if (triggers) {
const triggersArr = Array.from(triggers.values());
await Promise.all(
triggersArr.map(trigger => checkCounterTrigger(pluginData, counterName, trigger, channelId, userId)),
);
await Promise.all(
triggersArr.map(trigger => checkReverseCounterTrigger(pluginData, counterName, trigger, channelId, userId)),
);
}
lock.unlock();
}

View file

@ -0,0 +1,23 @@
import { GuildPluginData } from "knub";
import { CountersPluginType } from "../types";
import { buildConditionString } from "../../../data/GuildCounters";
import { CounterTrigger } from "../../../data/entities/CounterTrigger";
import { emitCounterEvent } from "./emitCounterEvent";
export async function checkAllValuesForReverseTrigger(
pluginData: GuildPluginData<CountersPluginType>,
counterName: string,
counterTrigger: CounterTrigger,
) {
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,
);
}
}

View file

@ -0,0 +1,23 @@
import { GuildPluginData } from "knub";
import { CountersPluginType } from "../types";
import { buildConditionString } from "../../../data/GuildCounters";
import { CounterTrigger } from "../../../data/entities/CounterTrigger";
import { emitCounterEvent } from "./emitCounterEvent";
export async function checkAllValuesForTrigger(
pluginData: GuildPluginData<CountersPluginType>,
counterName: string,
counterTrigger: CounterTrigger,
) {
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,
);
}
}

View file

@ -0,0 +1,25 @@
import { GuildPluginData } from "knub";
import { CountersPluginType } from "../types";
import { buildConditionString } from "../../../data/GuildCounters";
import { CounterTrigger } from "../../../data/entities/CounterTrigger";
import { emitCounterEvent } from "./emitCounterEvent";
export async function checkCounterTrigger(
pluginData: GuildPluginData<CountersPluginType>,
counterName: string,
counterTrigger: CounterTrigger,
channelId: string | null,
userId: string | null,
) {
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,
);
}
}

View file

@ -0,0 +1,25 @@
import { GuildPluginData } from "knub";
import { CountersPluginType } from "../types";
import { buildConditionString } from "../../../data/GuildCounters";
import { CounterTrigger } from "../../../data/entities/CounterTrigger";
import { emitCounterEvent } from "./emitCounterEvent";
export async function checkReverseCounterTrigger(
pluginData: GuildPluginData<CountersPluginType>,
counterName: string,
counterTrigger: CounterTrigger,
channelId: string | null,
userId: string | null,
) {
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,
);
}
}

View file

@ -0,0 +1,32 @@
import { GuildPluginData } from "knub";
import { CountersPluginType } from "../types";
import { checkAllValuesForTrigger } from "./checkAllValuesForTrigger";
import { checkAllValuesForReverseTrigger } from "./checkAllValuesForReverseTrigger";
export async function decayCounter(
pluginData: GuildPluginData<CountersPluginType>,
counterName: string,
decayPeriodMS: number,
decayAmount: number,
) {
const config = pluginData.config.get();
const counter = config.counters[counterName];
if (!counter) {
throw new Error(`Unknown counter: ${counterName}`);
}
const counterId = pluginData.state.counterIds[counterName];
const lock = await pluginData.locks.acquire(counterId.toString());
await pluginData.state.counters.decay(counterId, decayPeriodMS, decayAmount);
// Check for trigger matches, if any, when the counter value changes
const triggers = pluginData.state.counterTriggersByCounterId.get(counterId);
if (triggers) {
const triggersArr = Array.from(triggers.values());
await Promise.all(triggersArr.map(trigger => checkAllValuesForTrigger(pluginData, counterName, trigger)));
await Promise.all(triggersArr.map(trigger => checkAllValuesForReverseTrigger(pluginData, counterName, trigger)));
}
lock.unlock();
}

View file

@ -0,0 +1,10 @@
import { CounterEvents, CountersPluginType } from "../types";
import { GuildPluginData } from "knub";
export function emitCounterEvent<TEvent extends keyof CounterEvents>(
pluginData: GuildPluginData<CountersPluginType>,
event: TEvent,
...rest: Parameters<CounterEvents[TEvent]>
) {
return pluginData.state.events.emit(event, ...rest);
}

View file

@ -0,0 +1,31 @@
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

@ -0,0 +1,9 @@
import { CounterEventEmitter, CountersPluginType } from "../types";
import { GuildPluginData } from "knub";
export function offCounterEvent(
pluginData: GuildPluginData<CountersPluginType>,
...rest: Parameters<CounterEventEmitter["off"]>
) {
return pluginData.state.events.off(...rest);
}

View file

@ -0,0 +1,10 @@
import { CounterEvents, CountersPluginType } from "../types";
import { GuildPluginData } from "knub";
export function onCounterEvent<TEvent extends keyof CounterEvents>(
pluginData: GuildPluginData<CountersPluginType>,
event: TEvent,
listener: CounterEvents[TEvent],
) {
return pluginData.state.events.on(event, listener);
}

View file

@ -0,0 +1,45 @@
import { GuildPluginData } from "knub";
import { CountersPluginType } from "../types";
import { checkCounterTrigger } from "./checkCounterTrigger";
import { checkReverseCounterTrigger } from "./checkReverseCounterTrigger";
export async function setCounterValue(
pluginData: GuildPluginData<CountersPluginType>,
counterName: string,
channelId: string | null,
userId: string | null,
value: number,
) {
const config = pluginData.config.get();
const counter = config.counters[counterName];
if (!counter) {
throw new Error(`Unknown counter: ${counterName}`);
}
if (counter.per_channel && !channelId) {
throw new Error(`Counter is per channel but no channel ID was supplied`);
}
if (counter.per_user && !userId) {
throw new Error(`Counter is per user but no user ID was supplied`);
}
const counterId = pluginData.state.counterIds[counterName];
const lock = await pluginData.locks.acquire(counterId.toString());
await pluginData.state.counters.setCounterValue(counterId, channelId, userId, value);
// Check for trigger matches, if any, when the counter value changes
const triggers = pluginData.state.counterTriggersByCounterId.get(counterId);
if (triggers) {
const triggersArr = Array.from(triggers.values());
await Promise.all(
triggersArr.map(trigger => checkCounterTrigger(pluginData, counterName, trigger, channelId, userId)),
);
await Promise.all(
triggersArr.map(trigger => checkReverseCounterTrigger(pluginData, counterName, trigger, channelId, userId)),
);
}
lock.unlock();
}

View file

@ -0,0 +1,8 @@
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;
}