mirror of
https://github.com/ZeppelinBot/Zeppelin.git
synced 2025-05-20 16:25:03 +00:00
Merge branch 'master' into fr_cleanWithUpdate
This commit is contained in:
commit
6c273a1193
115 changed files with 1697 additions and 513 deletions
18
backend/package-lock.json
generated
18
backend/package-lock.json
generated
|
@ -1634,10 +1634,10 @@
|
|||
"dev": true
|
||||
},
|
||||
"eris": {
|
||||
"version": "https://github.com/Dragory/eris/archive/custom.tar.gz",
|
||||
"integrity": "sha512-6wb+mk7l/IDzqqki1IH0F8+U1dzGCbw7cHsg6dBVZ6emflHz+NnOND8XV3LPVnUQkw8ABIYzZhmYYXasURgmfg==",
|
||||
"version": "github:abalabahaha/eris#54fc78d3a1f9f8ebe8b072c9c87c674c8453d016",
|
||||
"from": "github:abalabahaha/eris#dev",
|
||||
"requires": {
|
||||
"opusscript": "^0.0.7",
|
||||
"opusscript": "^0.0.8",
|
||||
"tweetnacl": "^1.0.1",
|
||||
"ws": "^7.2.1"
|
||||
}
|
||||
|
@ -2883,9 +2883,9 @@
|
|||
}
|
||||
},
|
||||
"opusscript": {
|
||||
"version": "0.0.7",
|
||||
"resolved": "https://registry.npmjs.org/opusscript/-/opusscript-0.0.7.tgz",
|
||||
"integrity": "sha512-DcBadTdYTUuH9zQtepsLjQn4Ll6rs3dmeFvN+SD0ThPnxRBRm/WC1zXWPg+wgAJimB784gdZvUMA57gDP7FdVg==",
|
||||
"version": "0.0.8",
|
||||
"resolved": "https://registry.npmjs.org/opusscript/-/opusscript-0.0.8.tgz",
|
||||
"integrity": "sha512-VSTi1aWFuCkRCVq+tx/BQ5q9fMnQ9pVZ3JU4UHKqTkf0ED3fKEPdr+gKAAl3IA2hj9rrP6iyq3hlcJq3HELtNQ==",
|
||||
"optional": true
|
||||
},
|
||||
"ora": {
|
||||
|
@ -4696,9 +4696,9 @@
|
|||
}
|
||||
},
|
||||
"ws": {
|
||||
"version": "7.4.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.0.tgz",
|
||||
"integrity": "sha512-kyFwXuV/5ymf+IXhS6f0+eAFvydbaBW3zjpT6hUdAh/hbVjTIB5EHBGi0bPoCLSK2wcuz3BrEkB9LrYv1Nm4NQ=="
|
||||
"version": "7.4.4",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.4.tgz",
|
||||
"integrity": "sha512-Qm8k8ojNQIMx7S+Zp8u/uHOx7Qazv3Yv4q68MiWWWOJhiwG5W3x7iqmRtJo8xxrciZUY4vRxUTJCKuRnF28ZZw=="
|
||||
},
|
||||
"xdg-basedir": {
|
||||
"version": "4.0.0",
|
||||
|
|
|
@ -31,7 +31,7 @@
|
|||
"deep-diff": "^1.0.2",
|
||||
"dotenv": "^4.0.0",
|
||||
"emoji-regex": "^8.0.0",
|
||||
"eris": "https://github.com/Dragory/eris/archive/custom.tar.gz",
|
||||
"eris": "github:abalabahaha/eris#dev",
|
||||
"erlpack": "github:abalabahaha/erlpack",
|
||||
"escape-string-regexp": "^1.0.5",
|
||||
"express": "^4.17.0",
|
||||
|
|
|
@ -39,7 +39,11 @@ function simpleDiscordAPIRequest(bearerToken, path): Promise<any> {
|
|||
return;
|
||||
}
|
||||
|
||||
res.on("data", data => resolve(JSON.parse(data)));
|
||||
let rawData = "";
|
||||
res.on("data", data => (rawData += data));
|
||||
res.on("end", () => {
|
||||
resolve(JSON.parse(rawData));
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
|
|
|
@ -36,9 +36,9 @@
|
|||
"MESSAGE_DELETE_BARE": "🗑 Message (`{messageId}`) deleted in {channelMention(channel)} (no more info available)",
|
||||
"MESSAGE_DELETE_AUTO": "🗑 Auto-deleted message (`{message.id}`) from {userMention(user)} in {channelMention(channel)} (originally posted at **{messageDate}**):{messageSummary(message)}",
|
||||
|
||||
"VOICE_CHANNEL_JOIN": "🎙 🔵 {userMention(member)} joined **{channel.name}**",
|
||||
"VOICE_CHANNEL_MOVE": "🎙 ↔ {userMention(member)} moved from **{oldChannel.name}** to **{newChannel.name}**",
|
||||
"VOICE_CHANNEL_LEAVE": "🎙 🔴 {userMention(member)} left **{channel.name}**",
|
||||
"VOICE_CHANNEL_JOIN": "🎙 🔵 {userMention(member)} joined {channelMention(channel)}",
|
||||
"VOICE_CHANNEL_MOVE": "🎙 ↔ {userMention(member)} moved from {channelMention(oldChannel)} to {channelMention(newChannel)}",
|
||||
"VOICE_CHANNEL_LEAVE": "🎙 🔴 {userMention(member)} left {channelMention(channel)}",
|
||||
"VOICE_CHANNEL_FORCE_MOVE": "\uD83C\uDF99 ✍ {userMention(member)} was moved from **{oldChannel.name}** to **{newChannel.name}** by {userMention(mod)}",
|
||||
"VOICE_CHANNEL_FORCE_DISCONNECT": "\uD83C\uDF99 🚫 {userMention(member)} was forcefully disconnected from **{oldChannel.name}** by {userMention(mod)}",
|
||||
|
||||
|
|
|
@ -1,49 +1,26 @@
|
|||
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];
|
||||
}
|
||||
import { Queue } from "../Queue";
|
||||
|
||||
const DELETE_UNUSED_COUNTERS_AFTER = 1 * DAYS;
|
||||
const DELETE_UNUSED_COUNTER_TRIGGERS_AFTER = 1 * DAYS;
|
||||
|
||||
const MAX_COUNTER_VALUE = 2147483647; // 2^31-1, for MySQL INT
|
||||
|
||||
const decayQueue = new Queue();
|
||||
|
||||
async function deleteCountersMarkedToBeDeleted(): Promise<void> {
|
||||
await getRepository(Counter)
|
||||
.createQueryBuilder()
|
||||
|
@ -92,6 +69,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 +93,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> {
|
||||
|
@ -183,64 +161,88 @@ export class GuildCounters extends BaseGuildRepository {
|
|||
);
|
||||
}
|
||||
|
||||
async decay(id: number, decayPeriodMs: number, decayAmount: number) {
|
||||
const counter = (await this.counters.findOne({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
}))!;
|
||||
decay(id: number, decayPeriodMs: number, decayAmount: number) {
|
||||
return decayQueue.add(async () => {
|
||||
const counter = (await this.counters.findOne({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
}))!;
|
||||
|
||||
const diffFromLastDecayMs = moment.utc().diff(moment.utc(counter.last_decay_at!), "ms");
|
||||
if (diffFromLastDecayMs < decayPeriodMs) {
|
||||
return;
|
||||
}
|
||||
const diffFromLastDecayMs = moment.utc().diff(moment.utc(counter.last_decay_at!), "ms");
|
||||
if (diffFromLastDecayMs < decayPeriodMs) {
|
||||
return;
|
||||
}
|
||||
|
||||
const decayAmountToApply = Math.round((diffFromLastDecayMs / decayPeriodMs) * decayAmount);
|
||||
if (decayAmountToApply === 0) {
|
||||
return;
|
||||
}
|
||||
const decayAmountToApply = Math.round((diffFromLastDecayMs / decayPeriodMs) * decayAmount);
|
||||
if (decayAmountToApply === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate new last_decay_at based on the rounded decay amount we applied. This makes it so that over time, the decayed amount will stay accurate, even if we round some here.
|
||||
const newLastDecayDate = moment
|
||||
.utc(counter.last_decay_at)
|
||||
.add((decayAmountToApply / decayAmount) * decayPeriodMs, "ms")
|
||||
.format(DBDateFormat);
|
||||
// Calculate new last_decay_at based on the rounded decay amount we applied. This makes it so that over time, the decayed amount will stay accurate, even if we round some here.
|
||||
const newLastDecayDate = moment
|
||||
.utc(counter.last_decay_at)
|
||||
.add((decayAmountToApply / decayAmount) * decayPeriodMs, "ms")
|
||||
.format(DBDateFormat);
|
||||
|
||||
const rawUpdate =
|
||||
decayAmountToApply >= 0
|
||||
? `GREATEST(value - ${decayAmountToApply}, 0)`
|
||||
: `LEAST(value + ${Math.abs(decayAmountToApply)}, ${MAX_COUNTER_VALUE})`;
|
||||
const rawUpdate =
|
||||
decayAmountToApply >= 0
|
||||
? `GREATEST(value - ${decayAmountToApply}, 0)`
|
||||
: `LEAST(value + ${Math.abs(decayAmountToApply)}, ${MAX_COUNTER_VALUE})`;
|
||||
|
||||
await this.counterValues.update(
|
||||
{
|
||||
counter_id: id,
|
||||
},
|
||||
{
|
||||
value: () => rawUpdate,
|
||||
},
|
||||
);
|
||||
// Using an UPDATE with ORDER BY in an attempt to avoid deadlocks from simultaneous decays
|
||||
// Also see https://dev.mysql.com/doc/refman/8.0/en/innodb-deadlocks-handling.html
|
||||
await this.counterValues
|
||||
.createQueryBuilder("CounterValue")
|
||||
.where("counter_id = :id", { id })
|
||||
.orderBy("id")
|
||||
.update({
|
||||
value: () => rawUpdate,
|
||||
})
|
||||
.execute();
|
||||
|
||||
await this.counters.update(
|
||||
{
|
||||
id,
|
||||
},
|
||||
{
|
||||
last_decay_at: newLastDecayDate,
|
||||
},
|
||||
);
|
||||
await this.counters.update(
|
||||
{
|
||||
id,
|
||||
},
|
||||
{
|
||||
last_decay_at: newLastDecayDate,
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
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 +255,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 +396,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 +429,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 +437,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 +468,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 +481,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;
|
||||
}
|
||||
|
|
|
@ -156,6 +156,13 @@ connect().then(async () => {
|
|||
restMode: true,
|
||||
compress: false,
|
||||
guildCreateTimeout: 0,
|
||||
// Disable mentions by default
|
||||
allowedMentions: {
|
||||
everyone: false,
|
||||
users: false,
|
||||
roles: false,
|
||||
repliedUser: false,
|
||||
},
|
||||
intents: [
|
||||
// Privileged
|
||||
"guildMembers",
|
||||
|
|
|
@ -11,7 +11,7 @@ export class MigrateUsernamesToNewHistoryTable1556909512501 implements Migration
|
|||
|
||||
await new Promise(async resolve => {
|
||||
const stream = await queryRunner.stream("SELECT CONCAT(user_id, '-', username) AS `key` FROM username_history");
|
||||
stream.on("result", row => {
|
||||
stream.on("data", (row: any) => {
|
||||
migratedUsernames.add(row.key);
|
||||
});
|
||||
stream.on("end", resolve);
|
||||
|
@ -25,7 +25,7 @@ export class MigrateUsernamesToNewHistoryTable1556909512501 implements Migration
|
|||
const stream = await queryRunner.stream(
|
||||
`SELECT * FROM name_history WHERE type=1 ORDER BY timestamp ASC LIMIT ${BATCH_SIZE}`,
|
||||
);
|
||||
stream.on("result", row => {
|
||||
stream.on("data", (row: any) => {
|
||||
const key = `${row.user_id}-${row.value}`;
|
||||
|
||||
if (!migratedUsernames.has(key)) {
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
|
@ -7,6 +7,7 @@ import { onMessageCreate } from "./util/onMessageCreate";
|
|||
import { onMessageDelete } from "./util/onMessageDelete";
|
||||
import { onMessageDeleteBulk } from "./util/onMessageDeleteBulk";
|
||||
import { TimeAndDatePlugin } from "../TimeAndDate/TimeAndDatePlugin";
|
||||
import { LogsPlugin } from "../Logs/LogsPlugin";
|
||||
|
||||
const defaultOptions: PluginOptions<AutoDeletePluginType> = {
|
||||
config: {
|
||||
|
@ -23,7 +24,7 @@ export const AutoDeletePlugin = zeppelinGuildPlugin<AutoDeletePluginType>()("aut
|
|||
configurationGuide: "Maximum deletion delay is currently 5 minutes",
|
||||
},
|
||||
|
||||
dependencies: [TimeAndDatePlugin],
|
||||
dependencies: [TimeAndDatePlugin, LogsPlugin],
|
||||
configSchema: ConfigSchema,
|
||||
defaultOptions,
|
||||
|
||||
|
|
|
@ -2,24 +2,58 @@ import { GuildPluginData } from "knub";
|
|||
import { AutoDeletePluginType } from "../types";
|
||||
import moment from "moment-timezone";
|
||||
import { LogType } from "../../../data/LogType";
|
||||
import { stripObjectToScalars, resolveUser } from "../../../utils";
|
||||
import { resolveUser, stripObjectToScalars, verboseChannelMention } from "../../../utils";
|
||||
import { logger } from "../../../logger";
|
||||
import { scheduleNextDeletion } from "./scheduleNextDeletion";
|
||||
import { TimeAndDatePlugin } from "../../TimeAndDate/TimeAndDatePlugin";
|
||||
import { hasDiscordPermissions } from "../../../utils/hasDiscordPermissions";
|
||||
import { Constants } from "eris";
|
||||
import { LogsPlugin } from "../../Logs/LogsPlugin";
|
||||
|
||||
export async function deleteNextItem(pluginData: GuildPluginData<AutoDeletePluginType>) {
|
||||
const [itemToDelete] = pluginData.state.deletionQueue.splice(0, 1);
|
||||
if (!itemToDelete) return;
|
||||
|
||||
scheduleNextDeletion(pluginData);
|
||||
|
||||
const channel = pluginData.guild.channels.get(itemToDelete.message.channel_id);
|
||||
if (!channel) {
|
||||
// Channel was deleted, ignore
|
||||
return;
|
||||
}
|
||||
|
||||
const logs = pluginData.getPlugin(LogsPlugin);
|
||||
const perms = channel.permissionsOf(pluginData.client.user.id);
|
||||
|
||||
if (!hasDiscordPermissions(perms, Constants.Permissions.readMessages | Constants.Permissions.readMessageHistory)) {
|
||||
logs.log(LogType.BOT_ALERT, {
|
||||
body: `Missing permissions to read messages or message history in auto-delete channel ${verboseChannelMention(
|
||||
channel,
|
||||
)}`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!hasDiscordPermissions(perms, Constants.Permissions.manageMessages)) {
|
||||
logs.log(LogType.BOT_ALERT, {
|
||||
body: `Missing permissions to delete messages in auto-delete channel ${verboseChannelMention(channel)}`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const timeAndDate = pluginData.getPlugin(TimeAndDatePlugin);
|
||||
|
||||
pluginData.state.guildLogs.ignoreLog(LogType.MESSAGE_DELETE, itemToDelete.message.id);
|
||||
pluginData.client.deleteMessage(itemToDelete.message.channel_id, itemToDelete.message.id).catch(logger.warn);
|
||||
pluginData.client.deleteMessage(itemToDelete.message.channel_id, itemToDelete.message.id).catch(err => {
|
||||
if (err.code === 10008) {
|
||||
// "Unknown Message", probably already deleted by automod or another bot, ignore
|
||||
return;
|
||||
}
|
||||
|
||||
scheduleNextDeletion(pluginData);
|
||||
logger.warn(err);
|
||||
});
|
||||
|
||||
const user = await resolveUser(pluginData.client, itemToDelete.message.user_id);
|
||||
const channel = pluginData.guild.channels.get(itemToDelete.message.channel_id);
|
||||
const messageDate = timeAndDate
|
||||
.inGuildTz(moment.utc(itemToDelete.message.data.timestamp, "x"))
|
||||
.format(timeAndDate.getDateFormat("pretty_datetime"));
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
@ -254,14 +235,20 @@ export const AutomodPlugin = zeppelinGuildPlugin<AutomodPluginType>()("automod",
|
|||
pluginData.state.modActionsListeners.set("note", (userId: string) =>
|
||||
runAutomodOnModAction(pluginData, "note", userId),
|
||||
);
|
||||
pluginData.state.modActionsListeners.set("warn", (userId: string) =>
|
||||
runAutomodOnModAction(pluginData, "warn", userId),
|
||||
pluginData.state.modActionsListeners.set(
|
||||
"warn",
|
||||
(userId: string, reason: string | undefined, isAutomodAction: boolean) =>
|
||||
runAutomodOnModAction(pluginData, "warn", userId, reason, isAutomodAction),
|
||||
);
|
||||
pluginData.state.modActionsListeners.set("kick", (userId: string) =>
|
||||
runAutomodOnModAction(pluginData, "kick", userId),
|
||||
pluginData.state.modActionsListeners.set(
|
||||
"kick",
|
||||
(userId: string, reason: string | undefined, isAutomodAction: boolean) =>
|
||||
runAutomodOnModAction(pluginData, "kick", userId, reason, isAutomodAction),
|
||||
);
|
||||
pluginData.state.modActionsListeners.set("ban", (userId: string) =>
|
||||
runAutomodOnModAction(pluginData, "ban", userId),
|
||||
pluginData.state.modActionsListeners.set(
|
||||
"ban",
|
||||
(userId: string, reason: string | undefined, isAutomodAction: boolean) =>
|
||||
runAutomodOnModAction(pluginData, "ban", userId, reason, isAutomodAction),
|
||||
);
|
||||
pluginData.state.modActionsListeners.set("unban", (userId: string) =>
|
||||
runAutomodOnModAction(pluginData, "unban", userId),
|
||||
|
@ -270,7 +257,11 @@ export const AutomodPlugin = zeppelinGuildPlugin<AutomodPluginType>()("automod",
|
|||
|
||||
const mutesEvents = pluginData.getPlugin(MutesPlugin).getEventEmitter();
|
||||
pluginData.state.mutesListeners = new Map();
|
||||
pluginData.state.mutesListeners.set("mute", (userId: string) => runAutomodOnModAction(pluginData, "mute", userId));
|
||||
pluginData.state.mutesListeners.set(
|
||||
"mute",
|
||||
(userId: string, reason: string | undefined, isAutomodAction: boolean) =>
|
||||
runAutomodOnModAction(pluginData, "mute", userId, reason, isAutomodAction),
|
||||
);
|
||||
pluginData.state.mutesListeners.set("unmute", (userId: string) =>
|
||||
runAutomodOnModAction(pluginData, "unmute", userId),
|
||||
);
|
||||
|
|
|
@ -9,6 +9,7 @@ import { getMissingPermissions } from "../../../utils/getMissingPermissions";
|
|||
import { canAssignRole } from "../../../utils/canAssignRole";
|
||||
import { missingPermissionError } from "../../../utils/missingPermissionError";
|
||||
import { ignoreRoleChange } from "../functions/ignoredRoleChanges";
|
||||
import { memberRolesLock } from "../../../utils/lockNameHelpers";
|
||||
|
||||
const p = Constants.Permissions;
|
||||
|
||||
|
@ -64,7 +65,7 @@ export const AddRolesAction = automodAction({
|
|||
return;
|
||||
}
|
||||
|
||||
const memberRolesLock = await pluginData.locks.acquire(`member-roles-${member.id}`);
|
||||
const memberRoleLock = await pluginData.locks.acquire(memberRolesLock(member));
|
||||
|
||||
const rolesArr = Array.from(memberRoles.values());
|
||||
await member.edit({
|
||||
|
@ -72,7 +73,7 @@ export const AddRolesAction = automodAction({
|
|||
});
|
||||
member.roles = rolesArr; // Make sure we know of the new roles internally as well
|
||||
|
||||
memberRolesLock.unlock();
|
||||
memberRoleLock.unlock();
|
||||
}),
|
||||
);
|
||||
},
|
||||
|
|
30
backend/src/plugins/Automod/actions/addToCounter.ts
Normal file
30
backend/src/plugins/Automod/actions/addToCounter.ts
Normal file
|
@ -0,0 +1,30 @@
|
|||
import * as t from "io-ts";
|
||||
import { automodAction } from "../helpers";
|
||||
import { CountersPlugin } from "../../Counters/CountersPlugin";
|
||||
import { LogType } from "../../../data/LogType";
|
||||
|
||||
export const AddToCounterAction = automodAction({
|
||||
configType: t.type({
|
||||
counter: t.string,
|
||||
amount: t.number,
|
||||
}),
|
||||
|
||||
defaultConfig: {},
|
||||
|
||||
async apply({ pluginData, contexts, actionConfig, matchResult, ruleName }) {
|
||||
const countersPlugin = pluginData.getPlugin(CountersPlugin);
|
||||
if (!countersPlugin.counterExists(actionConfig.counter)) {
|
||||
pluginData.state.logs.log(LogType.BOT_ALERT, {
|
||||
body: `Unknown counter \`${actionConfig.counter}\` in \`add_to_counter\` action of Automod rule \`${ruleName}\``,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
countersPlugin.changeCounterValue(
|
||||
actionConfig.counter,
|
||||
contexts[0].message?.channel_id || null,
|
||||
contexts[0].user?.id || null,
|
||||
actionConfig.amount,
|
||||
);
|
||||
},
|
||||
});
|
|
@ -9,6 +9,7 @@ import {
|
|||
resolveMember,
|
||||
stripObjectToScalars,
|
||||
tNullable,
|
||||
verboseChannelMention,
|
||||
} from "../../../utils";
|
||||
import { resolveActionContactMethods } from "../functions/resolveActionContactMethods";
|
||||
import { ModActionsPlugin } from "../../ModActions/ModActionsPlugin";
|
||||
|
@ -68,7 +69,23 @@ export const AlertAction = automodAction({
|
|||
throw err;
|
||||
}
|
||||
|
||||
await createChunkedMessage(channel, rendered);
|
||||
try {
|
||||
await createChunkedMessage(channel, rendered);
|
||||
} catch (err) {
|
||||
if (err.code === 50001) {
|
||||
logs.log(LogType.BOT_ALERT, {
|
||||
body: `Missing access to send alert to channel ${verboseChannelMention(
|
||||
channel,
|
||||
)} in automod rule **${ruleName}**`,
|
||||
});
|
||||
} else {
|
||||
logs.log(LogType.BOT_ALERT, {
|
||||
body: `Error ${err.code || "UNKNOWN"} when sending alert to channel ${verboseChannelMention(
|
||||
channel,
|
||||
)} in automod rule **${ruleName}**`,
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logs.log(LogType.BOT_ALERT, {
|
||||
body: `Invalid channel id \`${actionConfig.channel}\` for alert action in automod rule **${ruleName}**`,
|
||||
|
|
|
@ -12,7 +12,8 @@ import { AddRolesAction } from "./addRoles";
|
|||
import { RemoveRolesAction } from "./removeRoles";
|
||||
import { SetAntiraidLevelAction } from "./setAntiraidLevel";
|
||||
import { ReplyAction } from "./reply";
|
||||
import { ChangeCounterAction } from "./changeCounter";
|
||||
import { AddToCounterAction } from "./addToCounter";
|
||||
import { SetCounterAction } from "./setCounter";
|
||||
|
||||
export const availableActions: Record<string, AutomodActionBlueprint<any>> = {
|
||||
clean: CleanAction,
|
||||
|
@ -27,7 +28,8 @@ export const availableActions: Record<string, AutomodActionBlueprint<any>> = {
|
|||
remove_roles: RemoveRolesAction,
|
||||
set_antiraid_level: SetAntiraidLevelAction,
|
||||
reply: ReplyAction,
|
||||
change_counter: ChangeCounterAction,
|
||||
add_to_counter: AddToCounterAction,
|
||||
set_counter: SetCounterAction,
|
||||
};
|
||||
|
||||
export const AvailableActions = t.type({
|
||||
|
@ -43,5 +45,6 @@ export const AvailableActions = t.type({
|
|||
remove_roles: RemoveRolesAction.configType,
|
||||
set_antiraid_level: SetAntiraidLevelAction.configType,
|
||||
reply: ReplyAction.configType,
|
||||
change_counter: ChangeCounterAction.configType,
|
||||
add_to_counter: AddToCounterAction.configType,
|
||||
set_counter: SetCounterAction.configType,
|
||||
});
|
||||
|
|
|
@ -4,6 +4,7 @@ import { LogType } from "../../../data/LogType";
|
|||
import { asyncMap, nonNullish, resolveMember, tNullable, unique } from "../../../utils";
|
||||
import { resolveActionContactMethods } from "../functions/resolveActionContactMethods";
|
||||
import { ModActionsPlugin } from "../../ModActions/ModActionsPlugin";
|
||||
import { CaseArgs } from "../../Cases/types";
|
||||
|
||||
export const BanAction = automodAction({
|
||||
configType: t.type({
|
||||
|
@ -22,16 +23,22 @@ export const BanAction = automodAction({
|
|||
const contactMethods = actionConfig.notify ? resolveActionContactMethods(pluginData, actionConfig) : undefined;
|
||||
const deleteMessageDays = actionConfig.deleteMessageDays || undefined;
|
||||
|
||||
const caseArgs = {
|
||||
const caseArgs: Partial<CaseArgs> = {
|
||||
modId: pluginData.client.user.id,
|
||||
extraNotes: matchResult.fullSummary ? [matchResult.fullSummary] : [],
|
||||
automatic: true,
|
||||
};
|
||||
|
||||
const userIdsToBan = unique(contexts.map(c => c.user?.id).filter(nonNullish));
|
||||
|
||||
const modActions = pluginData.getPlugin(ModActionsPlugin);
|
||||
for (const userId of userIdsToBan) {
|
||||
await modActions.banUserId(userId, reason, { contactMethods, caseArgs, deleteMessageDays });
|
||||
await modActions.banUserId(userId, reason, {
|
||||
contactMethods,
|
||||
caseArgs,
|
||||
deleteMessageDays,
|
||||
isAutomodAction: true,
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,27 +0,0 @@
|
|||
import * as t from "io-ts";
|
||||
import { automodAction } from "../helpers";
|
||||
import { CountersPlugin } from "../../Counters/CountersPlugin";
|
||||
|
||||
export const ChangeCounterAction = automodAction({
|
||||
configType: t.type({
|
||||
name: t.string,
|
||||
change: t.string,
|
||||
}),
|
||||
|
||||
defaultConfig: {},
|
||||
|
||||
async apply({ pluginData, contexts, actionConfig, matchResult }) {
|
||||
const change = parseInt(actionConfig.change, 10);
|
||||
if (Number.isNaN(change)) {
|
||||
throw new Error("Invalid change number");
|
||||
}
|
||||
|
||||
const countersPlugin = pluginData.getPlugin(CountersPlugin);
|
||||
countersPlugin.changeCounterValue(
|
||||
actionConfig.name,
|
||||
contexts[0].message?.channel_id || null,
|
||||
contexts[0].user?.id || null,
|
||||
change,
|
||||
);
|
||||
},
|
||||
});
|
|
@ -4,6 +4,7 @@ import { LogType } from "../../../data/LogType";
|
|||
import { asyncMap, nonNullish, resolveMember, tNullable, unique } from "../../../utils";
|
||||
import { resolveActionContactMethods } from "../functions/resolveActionContactMethods";
|
||||
import { ModActionsPlugin } from "../../ModActions/ModActionsPlugin";
|
||||
import { CaseArgs } from "../../Cases/types";
|
||||
|
||||
export const KickAction = automodAction({
|
||||
configType: t.type({
|
||||
|
@ -20,9 +21,10 @@ export const KickAction = automodAction({
|
|||
const reason = actionConfig.reason || "Kicked automatically";
|
||||
const contactMethods = actionConfig.notify ? resolveActionContactMethods(pluginData, actionConfig) : undefined;
|
||||
|
||||
const caseArgs = {
|
||||
const caseArgs: Partial<CaseArgs> = {
|
||||
modId: pluginData.client.user.id,
|
||||
extraNotes: matchResult.fullSummary ? [matchResult.fullSummary] : [],
|
||||
automatic: true,
|
||||
};
|
||||
|
||||
const userIdsToKick = unique(contexts.map(c => c.user?.id).filter(nonNullish));
|
||||
|
@ -31,7 +33,7 @@ export const KickAction = automodAction({
|
|||
const modActions = pluginData.getPlugin(ModActionsPlugin);
|
||||
for (const member of membersToKick) {
|
||||
if (!member) continue;
|
||||
await modActions.kickMember(member, reason, { contactMethods, caseArgs });
|
||||
await modActions.kickMember(member, reason, { contactMethods, caseArgs, isAutomodAction: true });
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -15,6 +15,7 @@ import { ModActionsPlugin } from "../../ModActions/ModActionsPlugin";
|
|||
import { MutesPlugin } from "../../Mutes/MutesPlugin";
|
||||
import { ERRORS, RecoverablePluginError } from "../../../RecoverablePluginError";
|
||||
import { LogsPlugin } from "../../Logs/LogsPlugin";
|
||||
import { CaseArgs } from "../../Cases/types";
|
||||
|
||||
export const MuteAction = automodAction({
|
||||
configType: t.type({
|
||||
|
@ -37,9 +38,10 @@ export const MuteAction = automodAction({
|
|||
const rolesToRemove = actionConfig.remove_roles_on_mute;
|
||||
const rolesToRestore = actionConfig.restore_roles_on_mute;
|
||||
|
||||
const caseArgs = {
|
||||
const caseArgs: Partial<CaseArgs> = {
|
||||
modId: pluginData.client.user.id,
|
||||
extraNotes: matchResult.fullSummary ? [matchResult.fullSummary] : [],
|
||||
automatic: true,
|
||||
};
|
||||
|
||||
const userIdsToMute = unique(contexts.map(c => c.user?.id).filter(nonNullish));
|
||||
|
@ -47,7 +49,14 @@ export const MuteAction = automodAction({
|
|||
const mutes = pluginData.getPlugin(MutesPlugin);
|
||||
for (const userId of userIdsToMute) {
|
||||
try {
|
||||
await mutes.muteUser(userId, duration, reason, { contactMethods, caseArgs }, rolesToRemove, rolesToRestore);
|
||||
await mutes.muteUser(
|
||||
userId,
|
||||
duration,
|
||||
reason,
|
||||
{ contactMethods, caseArgs, isAutomodAction: true },
|
||||
rolesToRemove,
|
||||
rolesToRestore,
|
||||
);
|
||||
} catch (e) {
|
||||
if (e instanceof RecoverablePluginError && e.code === ERRORS.NO_MUTE_ROLE_IN_CONFIG) {
|
||||
pluginData.getPlugin(LogsPlugin).log(LogType.BOT_ALERT, {
|
||||
|
|
|
@ -10,6 +10,7 @@ import { missingPermissionError } from "../../../utils/missingPermissionError";
|
|||
import { canAssignRole } from "../../../utils/canAssignRole";
|
||||
import { Constants } from "eris";
|
||||
import { ignoreRoleChange } from "../functions/ignoredRoleChanges";
|
||||
import { memberRolesLock } from "../../../utils/lockNameHelpers";
|
||||
|
||||
const p = Constants.Permissions;
|
||||
|
||||
|
@ -66,7 +67,7 @@ export const RemoveRolesAction = automodAction({
|
|||
return;
|
||||
}
|
||||
|
||||
const memberRolesLock = await pluginData.locks.acquire(`member-roles-${member.id}`);
|
||||
const memberRoleLock = await pluginData.locks.acquire(memberRolesLock(member));
|
||||
|
||||
const rolesArr = Array.from(memberRoles.values());
|
||||
await member.edit({
|
||||
|
@ -74,7 +75,7 @@ export const RemoveRolesAction = automodAction({
|
|||
});
|
||||
member.roles = rolesArr; // Make sure we know of the new roles internally as well
|
||||
|
||||
memberRolesLock.unlock();
|
||||
memberRoleLock.unlock();
|
||||
}),
|
||||
);
|
||||
},
|
||||
|
|
|
@ -9,10 +9,13 @@ import {
|
|||
tMessageContent,
|
||||
tNullable,
|
||||
unique,
|
||||
verboseChannelMention,
|
||||
} from "../../../utils";
|
||||
import { TextChannel } from "eris";
|
||||
import { AdvancedMessageContent, Constants, MessageContent, TextChannel, User } from "eris";
|
||||
import { AutomodContext } from "../types";
|
||||
import { renderTemplate } from "../../../templateFormatter";
|
||||
import { hasDiscordPermissions } from "../../../utils/hasDiscordPermissions";
|
||||
import { LogType } from "../../../data/LogType";
|
||||
|
||||
export const ReplyAction = automodAction({
|
||||
configType: t.union([
|
||||
|
@ -25,7 +28,7 @@ export const ReplyAction = automodAction({
|
|||
|
||||
defaultConfig: {},
|
||||
|
||||
async apply({ pluginData, contexts, actionConfig }) {
|
||||
async apply({ pluginData, contexts, actionConfig, ruleName }) {
|
||||
const contextsWithTextChannels = contexts
|
||||
.filter(c => c.message?.channel_id)
|
||||
.filter(c => pluginData.guild.channels.get(c.message!.channel_id) instanceof TextChannel);
|
||||
|
@ -40,7 +43,7 @@ export const ReplyAction = automodAction({
|
|||
}, new Map());
|
||||
|
||||
for (const [channelId, _contexts] of contextsByChannelId.entries()) {
|
||||
const users = unique(Array.from(new Set(_contexts.map(c => c.user).filter(Boolean))));
|
||||
const users = unique(Array.from(new Set(_contexts.map(c => c.user).filter(Boolean)))) as User[];
|
||||
const user = users[0];
|
||||
|
||||
const renderReplyText = async str =>
|
||||
|
@ -50,10 +53,37 @@ export const ReplyAction = automodAction({
|
|||
const formatted =
|
||||
typeof actionConfig === "string"
|
||||
? await renderReplyText(actionConfig)
|
||||
: await renderRecursively(actionConfig.text, renderReplyText);
|
||||
: ((await renderRecursively(actionConfig.text, renderReplyText)) as AdvancedMessageContent);
|
||||
|
||||
if (formatted) {
|
||||
const channel = pluginData.guild.channels.get(channelId) as TextChannel;
|
||||
|
||||
// Check for basic Send Messages and View Channel permissions
|
||||
if (
|
||||
!hasDiscordPermissions(
|
||||
channel.permissionsOf(pluginData.client.user.id),
|
||||
Constants.Permissions.sendMessages | Constants.Permissions.readMessages,
|
||||
)
|
||||
) {
|
||||
pluginData.state.logs.log(LogType.BOT_ALERT, {
|
||||
body: `Missing permissions to reply in ${verboseChannelMention(channel)} in Automod rule \`${ruleName}\``,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
// If the message is an embed, check for embed permissions
|
||||
if (
|
||||
typeof formatted !== "string" &&
|
||||
!hasDiscordPermissions(channel.permissionsOf(pluginData.client.user.id), Constants.Permissions.embedLinks)
|
||||
) {
|
||||
pluginData.state.logs.log(LogType.BOT_ALERT, {
|
||||
body: `Missing permissions to reply **with an embed** in ${verboseChannelMention(
|
||||
channel,
|
||||
)} in Automod rule \`${ruleName}\``,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const replyMsg = await channel.createMessage(formatted);
|
||||
|
||||
if (typeof actionConfig === "object" && actionConfig.auto_delete) {
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
import * as t from "io-ts";
|
||||
import { automodAction } from "../helpers";
|
||||
import { setAntiraidLevel } from "../functions/setAntiraidLevel";
|
||||
import { tNullable } from "../../../utils";
|
||||
|
||||
export const SetAntiraidLevelAction = automodAction({
|
||||
configType: t.string,
|
||||
configType: tNullable(t.string),
|
||||
defaultConfig: "",
|
||||
|
||||
async apply({ pluginData, contexts, actionConfig }) {
|
||||
setAntiraidLevel(pluginData, actionConfig);
|
||||
setAntiraidLevel(pluginData, actionConfig ?? null);
|
||||
},
|
||||
});
|
||||
|
|
30
backend/src/plugins/Automod/actions/setCounter.ts
Normal file
30
backend/src/plugins/Automod/actions/setCounter.ts
Normal file
|
@ -0,0 +1,30 @@
|
|||
import * as t from "io-ts";
|
||||
import { automodAction } from "../helpers";
|
||||
import { CountersPlugin } from "../../Counters/CountersPlugin";
|
||||
import { LogType } from "../../../data/LogType";
|
||||
|
||||
export const SetCounterAction = automodAction({
|
||||
configType: t.type({
|
||||
counter: t.string,
|
||||
value: t.number,
|
||||
}),
|
||||
|
||||
defaultConfig: {},
|
||||
|
||||
async apply({ pluginData, contexts, actionConfig, matchResult, ruleName }) {
|
||||
const countersPlugin = pluginData.getPlugin(CountersPlugin);
|
||||
if (!countersPlugin.counterExists(actionConfig.counter)) {
|
||||
pluginData.state.logs.log(LogType.BOT_ALERT, {
|
||||
body: `Unknown counter \`${actionConfig.counter}\` in \`add_to_counter\` action of Automod rule \`${ruleName}\``,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
countersPlugin.setCounterValue(
|
||||
actionConfig.counter,
|
||||
contexts[0].message?.channel_id || null,
|
||||
contexts[0].user?.id || null,
|
||||
actionConfig.value,
|
||||
);
|
||||
},
|
||||
});
|
|
@ -4,6 +4,7 @@ import { LogType } from "../../../data/LogType";
|
|||
import { asyncMap, nonNullish, resolveMember, tNullable, unique } from "../../../utils";
|
||||
import { resolveActionContactMethods } from "../functions/resolveActionContactMethods";
|
||||
import { ModActionsPlugin } from "../../ModActions/ModActionsPlugin";
|
||||
import { CaseArgs } from "../../Cases/types";
|
||||
|
||||
export const WarnAction = automodAction({
|
||||
configType: t.type({
|
||||
|
@ -20,9 +21,10 @@ export const WarnAction = automodAction({
|
|||
const reason = actionConfig.reason || "Warned automatically";
|
||||
const contactMethods = actionConfig.notify ? resolveActionContactMethods(pluginData, actionConfig) : undefined;
|
||||
|
||||
const caseArgs = {
|
||||
const caseArgs: Partial<CaseArgs> = {
|
||||
modId: pluginData.client.user.id,
|
||||
extraNotes: matchResult.fullSummary ? [matchResult.fullSummary] : [],
|
||||
automatic: true,
|
||||
};
|
||||
|
||||
const userIdsToWarn = unique(contexts.map(c => c.user?.id).filter(nonNullish));
|
||||
|
@ -31,7 +33,7 @@ export const WarnAction = automodAction({
|
|||
const modActions = pluginData.getPlugin(ModActionsPlugin);
|
||||
for (const member of membersToWarn) {
|
||||
if (!member) continue;
|
||||
await modActions.warnMember(member, reason, { contactMethods, caseArgs });
|
||||
await modActions.warnMember(member, reason, { contactMethods, caseArgs, isAutomodAction: true });
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
import { GuildPluginData } from "knub";
|
||||
import { AutomodContext, AutomodPluginType } from "../types";
|
||||
import { runAutomod } from "../functions/runAutomod";
|
||||
import { User } from "eris";
|
||||
|
||||
export async function runAutomodOnAntiraidLevel(
|
||||
pluginData: GuildPluginData<AutomodPluginType>,
|
||||
level: string | null,
|
||||
user?: User,
|
||||
) {
|
||||
const context: AutomodContext = {
|
||||
timestamp: Date.now(),
|
||||
antiraid: {
|
||||
level,
|
||||
},
|
||||
user,
|
||||
};
|
||||
|
||||
pluginData.state.queue.add(async () => {
|
||||
await runAutomod(pluginData, context);
|
||||
});
|
||||
}
|
|
@ -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,6 +9,7 @@ export async function runAutomodOnModAction(
|
|||
modAction: ModActionType,
|
||||
userId: string,
|
||||
reason?: string,
|
||||
isAutomodAction: boolean = false,
|
||||
) {
|
||||
const user = await resolveUser(pluginData.client, userId);
|
||||
|
||||
|
@ -18,6 +19,7 @@ export async function runAutomodOnModAction(
|
|||
modAction: {
|
||||
type: modAction,
|
||||
reason,
|
||||
isAutomodAction,
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ export function checkAndUpdateCooldown(
|
|||
rule: TRule,
|
||||
context: AutomodContext,
|
||||
) {
|
||||
const cooldownKey = context.user?.id;
|
||||
const cooldownKey = `${rule.name}-${context.user?.id}`;
|
||||
|
||||
if (cooldownKey) {
|
||||
if (pluginData.state.cooldownManager.isOnCooldown(cooldownKey)) {
|
||||
|
|
|
@ -24,10 +24,10 @@ export async function runAutomod(pluginData: GuildPluginData<AutomodPluginType>,
|
|||
|
||||
for (const [ruleName, rule] of Object.entries(config.rules)) {
|
||||
if (rule.enabled === false) continue;
|
||||
if (!rule.affects_bots && (!user || user.bot) && !context.counterTrigger) continue;
|
||||
if (!rule.affects_bots && (!user || user.bot) && !context.counterTrigger && !context.antiraid) continue;
|
||||
|
||||
if (rule.cooldown && checkAndUpdateCooldown(pluginData, rule, context)) {
|
||||
return;
|
||||
continue;
|
||||
}
|
||||
|
||||
let matchResult: AutomodTriggerMatchResult<any> | null | undefined;
|
||||
|
|
|
@ -4,6 +4,7 @@ import { AutomodPluginType } from "../types";
|
|||
import { LogsPlugin } from "../../Logs/LogsPlugin";
|
||||
import { LogType } from "../../../data/LogType";
|
||||
import { stripObjectToScalars } from "../../../utils";
|
||||
import { runAutomodOnAntiraidLevel } from "../events/runAutomodOnAntiraidLevel";
|
||||
|
||||
export async function setAntiraidLevel(
|
||||
pluginData: GuildPluginData<AutomodPluginType>,
|
||||
|
@ -13,6 +14,8 @@ export async function setAntiraidLevel(
|
|||
pluginData.state.cachedAntiraidLevel = newLevel;
|
||||
await pluginData.state.antiraidLevels.set(newLevel);
|
||||
|
||||
runAutomodOnAntiraidLevel(pluginData, newLevel, user);
|
||||
|
||||
const logs = pluginData.getPlugin(LogsPlugin);
|
||||
|
||||
if (user) {
|
||||
|
|
|
@ -64,9 +64,9 @@ export const pluginInfo: ZeppelinGuildPluginBlueprint["info"] = {
|
|||
reason: 'Auto-muted for spam'
|
||||
my_second_filter:
|
||||
triggers:
|
||||
- message_spam:
|
||||
amount: 5
|
||||
within: 10s
|
||||
- emoji_spam:
|
||||
amount: 2
|
||||
within: 5s
|
||||
actions:
|
||||
clean: true
|
||||
overrides:
|
||||
|
|
33
backend/src/plugins/Automod/triggers/antiraidLevel.ts
Normal file
33
backend/src/plugins/Automod/triggers/antiraidLevel.ts
Normal file
|
@ -0,0 +1,33 @@
|
|||
import * as t from "io-ts";
|
||||
import { automodTrigger } from "../helpers";
|
||||
import { tNullable } from "../../../utils";
|
||||
|
||||
// tslint:disable-next-line
|
||||
interface AntiraidLevelTriggerResult {}
|
||||
|
||||
export const AntiraidLevelTrigger = automodTrigger<AntiraidLevelTriggerResult>()({
|
||||
configType: t.type({
|
||||
level: tNullable(t.string),
|
||||
}),
|
||||
|
||||
defaultConfig: {},
|
||||
|
||||
async match({ triggerConfig, context, pluginData }) {
|
||||
if (!context.antiraid) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (context.antiraid.level !== triggerConfig.level) {
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
extra: {},
|
||||
};
|
||||
},
|
||||
|
||||
renderMatchInformation({ matchResult, pluginData, contexts, triggerConfig }) {
|
||||
const newLevel = contexts[0].antiraid!.level;
|
||||
return newLevel ? `Antiraid level was set to ${newLevel}` : `Antiraid was turned off`;
|
||||
},
|
||||
});
|
29
backend/src/plugins/Automod/triggers/anyMessage.ts
Normal file
29
backend/src/plugins/Automod/triggers/anyMessage.ts
Normal file
|
@ -0,0 +1,29 @@
|
|||
import * as t from "io-ts";
|
||||
import { automodTrigger } from "../helpers";
|
||||
import { verboseChannelMention } from "../../../utils";
|
||||
|
||||
// tslint:disable-next-line:no-empty-interface
|
||||
interface AnyMessageResultType {}
|
||||
|
||||
export const AnyMessageTrigger = automodTrigger<AnyMessageResultType>()({
|
||||
configType: t.type({}),
|
||||
|
||||
defaultConfig: {},
|
||||
|
||||
async match({ pluginData, context, triggerConfig: trigger }) {
|
||||
if (!context.message) {
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
extra: {},
|
||||
};
|
||||
},
|
||||
|
||||
renderMatchInformation({ pluginData, contexts, matchResult }) {
|
||||
const channel = pluginData.guild.channels.get(contexts[0].message!.channel_id);
|
||||
return `Matched message (\`${contexts[0].message!.id}\`) in ${
|
||||
channel ? verboseChannelMention(channel) : "Unknown Channel"
|
||||
}`;
|
||||
},
|
||||
});
|
|
@ -17,7 +17,7 @@ import { MemberJoinTrigger } from "./memberJoin";
|
|||
import { RoleAddedTrigger } from "./roleAdded";
|
||||
import { RoleRemovedTrigger } from "./roleRemoved";
|
||||
import { StickerSpamTrigger } from "./stickerSpam";
|
||||
import { CounterTrigger } from "./counter";
|
||||
import { CounterTrigger } from "./counterTrigger";
|
||||
import { NoteTrigger } from "./note";
|
||||
import { WarnTrigger } from "./warn";
|
||||
import { MuteTrigger } from "./mute";
|
||||
|
@ -25,8 +25,12 @@ import { UnmuteTrigger } from "./unmute";
|
|||
import { KickTrigger } from "./kick";
|
||||
import { BanTrigger } from "./ban";
|
||||
import { UnbanTrigger } from "./unban";
|
||||
import { AnyMessageTrigger } from "./anyMessage";
|
||||
import { AntiraidLevelTrigger } from "./antiraidLevel";
|
||||
|
||||
export const availableTriggers: Record<string, AutomodTriggerBlueprint<any, any>> = {
|
||||
any_message: AnyMessageTrigger,
|
||||
|
||||
match_words: MatchWordsTrigger,
|
||||
match_regex: MatchRegexTrigger,
|
||||
match_invites: MatchInvitesTrigger,
|
||||
|
@ -46,7 +50,7 @@ export const availableTriggers: Record<string, AutomodTriggerBlueprint<any, any>
|
|||
member_join_spam: MemberJoinSpamTrigger,
|
||||
sticker_spam: StickerSpamTrigger,
|
||||
|
||||
counter: CounterTrigger,
|
||||
counter_trigger: CounterTrigger,
|
||||
|
||||
note: NoteTrigger,
|
||||
warn: WarnTrigger,
|
||||
|
@ -55,9 +59,13 @@ export const availableTriggers: Record<string, AutomodTriggerBlueprint<any, any>
|
|||
kick: KickTrigger,
|
||||
ban: BanTrigger,
|
||||
unban: UnbanTrigger,
|
||||
|
||||
antiraid_level: AntiraidLevelTrigger,
|
||||
};
|
||||
|
||||
export const AvailableTriggers = t.type({
|
||||
any_message: AnyMessageTrigger.configType,
|
||||
|
||||
match_words: MatchWordsTrigger.configType,
|
||||
match_regex: MatchRegexTrigger.configType,
|
||||
match_invites: MatchInvitesTrigger.configType,
|
||||
|
@ -77,7 +85,7 @@ export const AvailableTriggers = t.type({
|
|||
member_join_spam: MemberJoinSpamTrigger.configType,
|
||||
sticker_spam: StickerSpamTrigger.configType,
|
||||
|
||||
counter: CounterTrigger.configType,
|
||||
counter_trigger: CounterTrigger.configType,
|
||||
|
||||
note: NoteTrigger.configType,
|
||||
warn: WarnTrigger.configType,
|
||||
|
@ -86,4 +94,6 @@ export const AvailableTriggers = t.type({
|
|||
kick: KickTrigger.configType,
|
||||
ban: BanTrigger.configType,
|
||||
unban: UnbanTrigger.configType,
|
||||
|
||||
antiraid_level: AntiraidLevelTrigger.configType,
|
||||
});
|
||||
|
|
|
@ -5,13 +5,25 @@ import { automodTrigger } from "../helpers";
|
|||
interface BanTriggerResultType {}
|
||||
|
||||
export const BanTrigger = automodTrigger<BanTriggerResultType>()({
|
||||
configType: t.type({}),
|
||||
defaultConfig: {},
|
||||
configType: t.type({
|
||||
manual: t.boolean,
|
||||
automatic: t.boolean,
|
||||
}),
|
||||
|
||||
async match({ context }) {
|
||||
defaultConfig: {
|
||||
manual: true,
|
||||
automatic: true,
|
||||
},
|
||||
|
||||
async match({ context, triggerConfig }) {
|
||||
if (context.modAction?.type !== "ban") {
|
||||
return;
|
||||
}
|
||||
console.log(context);
|
||||
// If automatic && automatic turned off -> return
|
||||
if (context.modAction.isAutomodAction && !triggerConfig.automatic) return;
|
||||
// If manual && manual turned off -> return
|
||||
if (!context.modAction.isAutomodAction && !triggerConfig.manual) return;
|
||||
|
||||
return {
|
||||
extra: {},
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
});
|
|
@ -5,13 +5,24 @@ import { automodTrigger } from "../helpers";
|
|||
interface KickTriggerResultType {}
|
||||
|
||||
export const KickTrigger = automodTrigger<KickTriggerResultType>()({
|
||||
configType: t.type({}),
|
||||
defaultConfig: {},
|
||||
configType: t.type({
|
||||
manual: t.boolean,
|
||||
automatic: t.boolean,
|
||||
}),
|
||||
|
||||
async match({ context }) {
|
||||
defaultConfig: {
|
||||
manual: true,
|
||||
automatic: true,
|
||||
},
|
||||
|
||||
async match({ context, triggerConfig }) {
|
||||
if (context.modAction?.type !== "kick") {
|
||||
return;
|
||||
}
|
||||
// If automatic && automatic turned off -> return
|
||||
if (context.modAction.isAutomodAction && !triggerConfig.automatic) return;
|
||||
// If manual && manual turned off -> return
|
||||
if (!context.modAction.isAutomodAction && !triggerConfig.manual) return;
|
||||
|
||||
return {
|
||||
extra: {},
|
||||
|
|
|
@ -5,13 +5,24 @@ import { automodTrigger } from "../helpers";
|
|||
interface MuteTriggerResultType {}
|
||||
|
||||
export const MuteTrigger = automodTrigger<MuteTriggerResultType>()({
|
||||
configType: t.type({}),
|
||||
defaultConfig: {},
|
||||
configType: t.type({
|
||||
manual: t.boolean,
|
||||
automatic: t.boolean,
|
||||
}),
|
||||
|
||||
async match({ context }) {
|
||||
defaultConfig: {
|
||||
manual: true,
|
||||
automatic: true,
|
||||
},
|
||||
|
||||
async match({ context, triggerConfig }) {
|
||||
if (context.modAction?.type !== "mute") {
|
||||
return;
|
||||
}
|
||||
// If automatic && automatic turned off -> return
|
||||
if (context.modAction.isAutomodAction && !triggerConfig.automatic) return;
|
||||
// If manual && manual turned off -> return
|
||||
if (!context.modAction.isAutomodAction && !triggerConfig.manual) return;
|
||||
|
||||
return {
|
||||
extra: {},
|
||||
|
|
|
@ -5,13 +5,24 @@ import { automodTrigger } from "../helpers";
|
|||
interface WarnTriggerResultType {}
|
||||
|
||||
export const WarnTrigger = automodTrigger<WarnTriggerResultType>()({
|
||||
configType: t.type({}),
|
||||
defaultConfig: {},
|
||||
configType: t.type({
|
||||
manual: t.boolean,
|
||||
automatic: t.boolean,
|
||||
}),
|
||||
|
||||
async match({ context }) {
|
||||
defaultConfig: {
|
||||
manual: true,
|
||||
automatic: true,
|
||||
},
|
||||
|
||||
async match({ context, triggerConfig }) {
|
||||
if (context.modAction?.type !== "warn") {
|
||||
return;
|
||||
}
|
||||
// If automatic && automatic turned off -> return
|
||||
if (context.modAction.isAutomodAction && !triggerConfig.automatic) return;
|
||||
// If manual && manual turned off -> return
|
||||
if (!context.modAction.isAutomodAction && !triggerConfig.manual) return;
|
||||
|
||||
return {
|
||||
extra: {},
|
||||
|
|
|
@ -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;
|
||||
|
@ -120,6 +122,10 @@ export interface AutomodContext {
|
|||
modAction?: {
|
||||
type: ModActionType;
|
||||
reason?: string;
|
||||
isAutomodAction: boolean;
|
||||
};
|
||||
antiraid?: {
|
||||
level: string | null;
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -17,10 +17,12 @@ import { Configs } from "../../data/Configs";
|
|||
import { ApiPermissionAssignments } from "../../data/ApiPermissionAssignments";
|
||||
import { ListDashboardUsersCmd } from "./commands/ListDashboardUsersCmd";
|
||||
import { ListDashboardPermsCmd } from "./commands/ListDashboardPermsCmd";
|
||||
import { EligibleCmd } from "./commands/EligibleCmd";
|
||||
|
||||
const defaultOptions = {
|
||||
config: {
|
||||
can_use: false,
|
||||
can_eligible: false,
|
||||
update_cmd: null,
|
||||
},
|
||||
};
|
||||
|
@ -41,6 +43,7 @@ export const BotControlPlugin = zeppelinGlobalPlugin<BotControlPluginType>()("bo
|
|||
RemoveDashboardUserCmd,
|
||||
ListDashboardUsersCmd,
|
||||
ListDashboardPermsCmd,
|
||||
EligibleCmd,
|
||||
],
|
||||
|
||||
onLoad(pluginData) {
|
||||
|
|
59
backend/src/plugins/BotControl/commands/EligibleCmd.ts
Normal file
59
backend/src/plugins/BotControl/commands/EligibleCmd.ts
Normal file
|
@ -0,0 +1,59 @@
|
|||
import { botControlCmd } from "../types";
|
||||
import { sendErrorMessage, sendSuccessMessage } from "../../../pluginUtils";
|
||||
import { commandTypeHelpers as ct } from "../../../commandTypes";
|
||||
import { resolveInvite, verboseUserMention } from "../../../utils";
|
||||
|
||||
const REQUIRED_MEMBER_COUNT = 5000;
|
||||
|
||||
export const EligibleCmd = botControlCmd({
|
||||
trigger: ["eligible", "is_eligible", "iseligible"],
|
||||
permission: "can_eligible",
|
||||
|
||||
signature: {
|
||||
user: ct.resolvedUser(),
|
||||
inviteCode: ct.string(),
|
||||
},
|
||||
|
||||
async run({ pluginData, message: msg, args }) {
|
||||
if ((await pluginData.state.apiPermissionAssignments.getByUserId(args.user.id)).length) {
|
||||
sendSuccessMessage(
|
||||
pluginData,
|
||||
msg.channel,
|
||||
`${verboseUserMention(args.user)} is an existing bot operator. They are eligible!`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const invite = await resolveInvite(pluginData.client, args.inviteCode, true);
|
||||
if (!invite || !invite.guild) {
|
||||
sendErrorMessage(pluginData, msg.channel, "Could not resolve server from invite");
|
||||
return;
|
||||
}
|
||||
|
||||
if (invite.guild.features.includes("PARTNERED")) {
|
||||
sendSuccessMessage(pluginData, msg.channel, `Server is partnered. It is eligible!`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (invite.guild.features.includes("VERIFIED")) {
|
||||
sendSuccessMessage(pluginData, msg.channel, `Server is verified. It is eligible!`);
|
||||
return;
|
||||
}
|
||||
|
||||
const memberCount = invite.memberCount || 0;
|
||||
if (memberCount >= REQUIRED_MEMBER_COUNT) {
|
||||
sendSuccessMessage(
|
||||
pluginData,
|
||||
msg.channel,
|
||||
`Server has ${memberCount} members, which is equal or higher than the required ${REQUIRED_MEMBER_COUNT}. It is eligible!`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
sendErrorMessage(
|
||||
pluginData,
|
||||
msg.channel,
|
||||
`Server **${invite.guild.name}** (\`${invite.guild.id}\`) is not eligible`,
|
||||
);
|
||||
},
|
||||
});
|
|
@ -8,6 +8,7 @@ import { Configs } from "../../data/Configs";
|
|||
|
||||
export const ConfigSchema = t.type({
|
||||
can_use: t.boolean,
|
||||
can_eligible: t.boolean,
|
||||
update_cmd: tNullable(t.string),
|
||||
});
|
||||
export type TConfigSchema = t.TypeOf<typeof ConfigSchema>;
|
||||
|
|
|
@ -2,10 +2,11 @@ import { GuildPluginData } from "knub";
|
|||
import { CensorPluginType } from "../types";
|
||||
import { SavedMessage } from "../../../data/entities/SavedMessage";
|
||||
import { applyFiltersToMsg } from "./applyFiltersToMsg";
|
||||
import { messageLock } from "../../../utils/lockNameHelpers";
|
||||
|
||||
export async function onMessageCreate(pluginData: GuildPluginData<CensorPluginType>, savedMessage: SavedMessage) {
|
||||
if (savedMessage.is_bot) return;
|
||||
const lock = await pluginData.locks.acquire(`message-${savedMessage.id}`);
|
||||
const lock = await pluginData.locks.acquire(messageLock(savedMessage));
|
||||
|
||||
const wasDeleted = await applyFiltersToMsg(pluginData, savedMessage);
|
||||
|
||||
|
|
|
@ -2,10 +2,11 @@ import { GuildPluginData } from "knub";
|
|||
import { CensorPluginType } from "../types";
|
||||
import { SavedMessage } from "../../../data/entities/SavedMessage";
|
||||
import { applyFiltersToMsg } from "./applyFiltersToMsg";
|
||||
import { messageLock } from "../../../utils/lockNameHelpers";
|
||||
|
||||
export async function onMessageUpdate(pluginData: GuildPluginData<CensorPluginType>, savedMessage: SavedMessage) {
|
||||
if (savedMessage.is_bot) return;
|
||||
const lock = await pluginData.locks.acquire(`message-${savedMessage.id}`);
|
||||
const lock = await pluginData.locks.acquire(messageLock(savedMessage));
|
||||
|
||||
const wasDeleted = await applyFiltersToMsg(pluginData, savedMessage);
|
||||
|
||||
|
|
|
@ -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,24 @@ 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";
|
||||
import { counterExists } from "./functions/counterExists";
|
||||
|
||||
const MAX_COUNTERS = 5;
|
||||
const MAX_TRIGGERS_PER_COUNTER = 5;
|
||||
const DECAY_APPLY_INTERVAL = 5 * MINUTES;
|
||||
|
||||
const defaultOptions: PluginOptions<CountersPluginType> = {
|
||||
|
@ -45,14 +53,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;
|
||||
|
@ -69,21 +103,29 @@ const configPreprocessor: ConfigPreprocessorFn<CountersPluginType> = options =>
|
|||
* After being triggered, a trigger is "reset" if the counter value no longer matches the trigger (e.g. drops to 100 or below in the above example). After this, that trigger can be triggered again.
|
||||
*/
|
||||
export const CountersPlugin = zeppelinGuildPlugin<CountersPluginType>()("counters", {
|
||||
showInDocs: true,
|
||||
info: {
|
||||
prettyName: "Counters",
|
||||
description:
|
||||
"Keep track of per-user, per-channel, or global numbers and trigger specific actions based on this number",
|
||||
configurationGuide: "See <a href='/docs/setup-guides/counters'>Counters setup guide</a>",
|
||||
},
|
||||
|
||||
configSchema: ConfigSchema,
|
||||
defaultOptions,
|
||||
configPreprocessor,
|
||||
|
||||
public: {
|
||||
counterExists: mapToPublicFn(counterExists),
|
||||
|
||||
// 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 +141,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 +198,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) {
|
||||
|
|
|
@ -67,7 +67,7 @@ export const SetCounterCmd = guildCommand<CountersPluginType>()({
|
|||
|
||||
let channel = args.channel;
|
||||
if (!channel && counter.per_channel) {
|
||||
message.channel.createMessage(`Which channel's counter value would you like to add to?`);
|
||||
message.channel.createMessage(`Which channel's counter value would you like to change?`);
|
||||
const reply = await waitForReply(pluginData.client, message.channel, message.author.id);
|
||||
if (!reply || !reply.content) {
|
||||
sendErrorMessage(pluginData, message.channel, "Cancelling");
|
||||
|
@ -85,7 +85,7 @@ export const SetCounterCmd = guildCommand<CountersPluginType>()({
|
|||
|
||||
let user = args.user;
|
||||
if (!user && counter.per_user) {
|
||||
message.channel.createMessage(`Which user's counter value would you like to add to?`);
|
||||
message.channel.createMessage(`Which user's counter value would you like to change?`);
|
||||
const reply = await waitForReply(pluginData.client, message.channel, message.author.id);
|
||||
if (!reply || !reply.content) {
|
||||
sendErrorMessage(pluginData, message.channel, "Cancelling");
|
||||
|
@ -103,7 +103,7 @@ export const SetCounterCmd = guildCommand<CountersPluginType>()({
|
|||
|
||||
let value = args.value;
|
||||
if (!value) {
|
||||
message.channel.createMessage("How much would you like to add to the counter's value?");
|
||||
message.channel.createMessage("What would you like to set the counter's value to?");
|
||||
const reply = await waitForReply(pluginData.client, message.channel, message.author.id);
|
||||
if (!reply || !reply.content) {
|
||||
sendErrorMessage(pluginData, message.channel, "Cancelling");
|
||||
|
@ -111,7 +111,7 @@ export const SetCounterCmd = guildCommand<CountersPluginType>()({
|
|||
}
|
||||
|
||||
const potentialValue = parseInt(reply.content, 10);
|
||||
if (!potentialValue) {
|
||||
if (Number.isNaN(potentialValue)) {
|
||||
sendErrorMessage(pluginData, message.channel, "Not a number, cancelling");
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { GuildPluginData } from "knub";
|
||||
import { counterIdLock } from "../../../utils/lockNameHelpers";
|
||||
import { CountersPluginType } from "../types";
|
||||
import { checkCounterTrigger } from "./checkCounterTrigger";
|
||||
import { checkReverseCounterTrigger } from "./checkReverseCounterTrigger";
|
||||
|
@ -28,7 +29,7 @@ export async function changeCounterValue(
|
|||
userId = counter.per_user ? userId : null;
|
||||
|
||||
const counterId = pluginData.state.counterIds[counterName];
|
||||
const lock = await pluginData.locks.acquire(counterId.toString());
|
||||
const lock = await pluginData.locks.acquire(counterIdLock(counterId));
|
||||
|
||||
await pluginData.state.counters.changeCounterValue(counterId, channelId, userId, change);
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
7
backend/src/plugins/Counters/functions/counterExists.ts
Normal file
7
backend/src/plugins/Counters/functions/counterExists.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
import { GuildPluginData } from "knub";
|
||||
import { CountersPluginType } from "../types";
|
||||
|
||||
export function counterExists(pluginData: GuildPluginData<CountersPluginType>, counterName: string) {
|
||||
const config = pluginData.config.get();
|
||||
return config.counters[counterName] != null;
|
||||
}
|
|
@ -2,6 +2,7 @@ import { GuildPluginData } from "knub";
|
|||
import { CountersPluginType } from "../types";
|
||||
import { checkAllValuesForTrigger } from "./checkAllValuesForTrigger";
|
||||
import { checkAllValuesForReverseTrigger } from "./checkAllValuesForReverseTrigger";
|
||||
import { counterIdLock } from "../../../utils/lockNameHelpers";
|
||||
|
||||
export async function decayCounter(
|
||||
pluginData: GuildPluginData<CountersPluginType>,
|
||||
|
@ -16,7 +17,7 @@ export async function decayCounter(
|
|||
}
|
||||
|
||||
const counterId = pluginData.state.counterIds[counterName];
|
||||
const lock = await pluginData.locks.acquire(counterId.toString());
|
||||
const lock = await pluginData.locks.acquire(counterIdLock(counterId));
|
||||
|
||||
await pluginData.state.counters.decay(counterId, decayPeriodMS, decayAmount);
|
||||
|
||||
|
|
|
@ -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,4 +1,5 @@
|
|||
import { GuildPluginData } from "knub";
|
||||
import { counterIdLock } from "../../../utils/lockNameHelpers";
|
||||
import { CountersPluginType } from "../types";
|
||||
import { checkCounterTrigger } from "./checkCounterTrigger";
|
||||
import { checkReverseCounterTrigger } from "./checkReverseCounterTrigger";
|
||||
|
@ -25,7 +26,7 @@ export async function setCounterValue(
|
|||
}
|
||||
|
||||
const counterId = pluginData.state.counterIds[counterName];
|
||||
const lock = await pluginData.locks.acquire(counterId.toString());
|
||||
const lock = await pluginData.locks.acquire(counterIdLock(counterId));
|
||||
|
||||
await pluginData.state.counters.setCounterValue(counterId, channelId, userId, value);
|
||||
|
||||
|
|
|
@ -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[]>;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -45,7 +45,7 @@ export const AddCaseCmd = modActionsCmd({
|
|||
let mod = msg.member;
|
||||
if (args.mod) {
|
||||
if (!hasPermission(pluginData, "can_act_as_other", { message: msg })) {
|
||||
sendErrorMessage(pluginData, msg.channel, "No permission for -mod");
|
||||
sendErrorMessage(pluginData, msg.channel, "You don't have permission to use -mod");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -8,9 +8,10 @@ import { formatReasonWithAttachments } from "../functions/formatReasonWithAttach
|
|||
import { banUserId } from "../functions/banUserId";
|
||||
import { getMemberLevel, waitForReaction } from "knub/dist/helpers";
|
||||
import humanizeDuration from "humanize-duration";
|
||||
import { CasesPlugin } from "src/plugins/Cases/CasesPlugin";
|
||||
import { CaseTypes } from "src/data/CaseTypes";
|
||||
import { LogType } from "src/data/LogType";
|
||||
import { CasesPlugin } from "../../../plugins/Cases/CasesPlugin";
|
||||
import { CaseTypes } from "../../../data/CaseTypes";
|
||||
import { LogType } from "../../../data/LogType";
|
||||
import { banLock } from "../../../utils/lockNameHelpers";
|
||||
|
||||
const opts = {
|
||||
mod: ct.member({ option: true }),
|
||||
|
@ -54,7 +55,7 @@ export const BanCmd = modActionsCmd({
|
|||
let mod = msg.member;
|
||||
if (args.mod) {
|
||||
if (!hasPermission(pluginData, "can_act_as_other", { message: msg, channelId: msg.channel.id })) {
|
||||
sendErrorMessage(pluginData, msg.channel, "No permission for -mod");
|
||||
sendErrorMessage(pluginData, msg.channel, "You don't have permission to use -mod");
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -62,11 +63,11 @@ export const BanCmd = modActionsCmd({
|
|||
}
|
||||
|
||||
// acquire a lock because of the needed user-inputs below (if banned/not on server)
|
||||
const lock = await pluginData.locks.acquire(`ban-${user.id}`);
|
||||
const lock = await pluginData.locks.acquire(banLock(user));
|
||||
let forceban = false;
|
||||
const existingTempban = await pluginData.state.tempbans.findExistingTempbanForUserId(user.id);
|
||||
const banned = await isBanned(pluginData, user.id);
|
||||
if (!memberToBan) {
|
||||
const banned = await isBanned(pluginData, user.id);
|
||||
if (banned) {
|
||||
// Abort if trying to ban user indefinitely if they are already banned indefinitely
|
||||
if (!existingTempban && !time) {
|
||||
|
|
|
@ -17,7 +17,7 @@ const opts = {
|
|||
const casesPerPage = 5;
|
||||
|
||||
export const CasesModCmd = modActionsCmd({
|
||||
trigger: ["cases", "modlogs"],
|
||||
trigger: ["cases", "modlogs", "infractions"],
|
||||
permission: "can_view",
|
||||
description: "Show the most recent 5 cases by the specified -mod",
|
||||
|
||||
|
|
|
@ -54,7 +54,7 @@ export const ForcebanCmd = modActionsCmd({
|
|||
let mod = msg.member;
|
||||
if (args.mod) {
|
||||
if (!hasPermission(pluginData, "can_act_as_other", { message: msg })) {
|
||||
sendErrorMessage(pluginData, msg.channel, "No permission for -mod");
|
||||
sendErrorMessage(pluginData, msg.channel, "You don't have permission to use -mod");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ import { formatReasonWithAttachments } from "../functions/formatReasonWithAttach
|
|||
import { CasesPlugin } from "../../Cases/CasesPlugin";
|
||||
import { LogType } from "../../../data/LogType";
|
||||
import { CaseTypes } from "../../../data/CaseTypes";
|
||||
import { errorMessage, resolveMember, resolveUser, stripObjectToScalars } from "../../../utils";
|
||||
import { errorMessage, noop, resolveMember, resolveUser, stripObjectToScalars } from "../../../utils";
|
||||
import { isBanned } from "../functions/isBanned";
|
||||
import { waitForReaction } from "knub/dist/helpers";
|
||||
import { readContactMethodsFromArgs } from "../functions/readContactMethodsFromArgs";
|
||||
|
@ -59,15 +59,18 @@ export const MuteCmd = modActionsCmd({
|
|||
msg.channel,
|
||||
`User is banned. Use \`${prefix}forcemute\` if you want to mute them anyway.`,
|
||||
);
|
||||
return;
|
||||
} else {
|
||||
sendErrorMessage(
|
||||
pluginData,
|
||||
msg.channel,
|
||||
`User is not on the server. Use \`${prefix}forcemute\` if you want to mute them anyway.`,
|
||||
);
|
||||
}
|
||||
// Ask the mod if we should upgrade to a forcemute as the user is not on the server
|
||||
const notOnServerMsg = await msg.channel.createMessage("User not found on the server, forcemute instead?");
|
||||
const reply = await waitForReaction(pluginData.client, notOnServerMsg, ["✅", "❌"], msg.author.id);
|
||||
|
||||
return;
|
||||
notOnServerMsg.delete().catch(noop);
|
||||
if (!reply || reply.name === "❌") {
|
||||
sendErrorMessage(pluginData, msg.channel, "User not on server, mute cancelled by moderator");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure we're allowed to mute this member
|
||||
|
|
|
@ -37,7 +37,7 @@ export const UnbanCmd = modActionsCmd({
|
|||
let mod = msg.member;
|
||||
if (args.mod) {
|
||||
if (!hasPermission(pluginData, "can_act_as_other", { message: msg, channelId: msg.channel.id })) {
|
||||
sendErrorMessage(pluginData, msg.channel, "No permission for -mod");
|
||||
sendErrorMessage(pluginData, msg.channel, "You don't have permission to use -mod");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import { modActionsCmd } from "../types";
|
||||
import { commandTypeHelpers as ct } from "../../../commandTypes";
|
||||
import { canActOn, sendErrorMessage } from "../../../pluginUtils";
|
||||
import { resolveUser, resolveMember } from "../../../utils";
|
||||
import { resolveUser, resolveMember, noop } from "../../../utils";
|
||||
import { MutesPlugin } from "../../../plugins/Mutes/MutesPlugin";
|
||||
import { actualUnmuteCmd } from "../functions/actualUnmuteUserCmd";
|
||||
import { isBanned } from "../functions/isBanned";
|
||||
import { waitForReaction } from "knub/dist/helpers";
|
||||
|
||||
const opts = {
|
||||
mod: ct.member({ option: true }),
|
||||
|
@ -57,15 +58,18 @@ export const UnmuteCmd = modActionsCmd({
|
|||
msg.channel,
|
||||
`User is banned. Use \`${prefix}forceunmute\` to unmute them anyway.`,
|
||||
);
|
||||
return;
|
||||
} else {
|
||||
sendErrorMessage(
|
||||
pluginData,
|
||||
msg.channel,
|
||||
`User is not on the server. Use \`${prefix}forceunmute\` to unmute them anyway.`,
|
||||
);
|
||||
}
|
||||
// Ask the mod if we should upgrade to a forceunmute as the user is not on the server
|
||||
const notOnServerMsg = await msg.channel.createMessage("User not found on the server, forceunmute instead?");
|
||||
const reply = await waitForReaction(pluginData.client, notOnServerMsg, ["✅", "❌"], msg.author.id);
|
||||
|
||||
return;
|
||||
notOnServerMsg.delete().catch(noop);
|
||||
if (!reply || reply.name === "❌") {
|
||||
sendErrorMessage(pluginData, msg.channel, "User not on server, unmute cancelled by moderator");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure we're allowed to unmute this member
|
||||
|
|
|
@ -3,7 +3,7 @@ import { commandTypeHelpers as ct } from "../../../commandTypes";
|
|||
import { updateCase } from "../functions/updateCase";
|
||||
|
||||
export const UpdateCmd = modActionsCmd({
|
||||
trigger: "update",
|
||||
trigger: ["update", "reason"],
|
||||
permission: "can_note",
|
||||
description:
|
||||
"Update the specified case (or, if case number is omitted, your latest case) by adding more notes/details to it",
|
||||
|
|
|
@ -57,7 +57,7 @@ export const WarnCmd = modActionsCmd({
|
|||
let mod = msg.member;
|
||||
if (args.mod) {
|
||||
if (!hasPermission(pluginData, "can_act_as_other", { message: msg })) {
|
||||
msg.channel.createMessage(errorMessage("No permission for -mod"));
|
||||
msg.channel.createMessage(errorMessage("You don't have permission to use -mod"));
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -112,7 +112,5 @@ export const WarnCmd = modActionsCmd({
|
|||
msg.channel,
|
||||
`Warned **${memberToWarn.user.username}#${memberToWarn.user.discriminator}** (Case #${warnResult.case.case_number})${messageResultText}`,
|
||||
);
|
||||
|
||||
pluginData.state.events.emit("warn", user.id, reason);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -61,6 +61,7 @@ export const CreateKickCaseOnManualKickEvt = modActionsEvt(
|
|||
user: stripObjectToScalars(member.user),
|
||||
mod: mod ? stripObjectToScalars(mod) : null,
|
||||
caseNumber: createdCase?.case_number ?? 0,
|
||||
reason: kickAuditLogEntry.reason || "",
|
||||
});
|
||||
|
||||
pluginData.state.events.emit("kick", member.id, kickAuditLogEntry.reason || undefined);
|
||||
|
|
|
@ -52,7 +52,7 @@ export async function actualKickMemberCmd(
|
|||
let mod = msg.member;
|
||||
if (args.mod) {
|
||||
if (!hasPermission(pluginData.config.getForMessage(msg), "can_act_as_other")) {
|
||||
sendErrorMessage(pluginData, msg.channel, "No permission for -mod");
|
||||
sendErrorMessage(pluginData, msg.channel, "You don't have permission to use -mod");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -28,7 +28,7 @@ export async function actualMuteUserCmd(
|
|||
|
||||
if (args.mod) {
|
||||
if (!hasPermission(pluginData, "can_act_as_other", { message: msg })) {
|
||||
sendErrorMessage(pluginData, msg.channel, "No permission for -mod");
|
||||
sendErrorMessage(pluginData, msg.channel, "You don't have permission to use -mod");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@ export async function actualUnmuteCmd(
|
|||
|
||||
if (args.mod) {
|
||||
if (!hasPermission(pluginData, "can_act_as_other", { message: msg, channelId: msg.channel.id })) {
|
||||
sendErrorMessage(pluginData, msg.channel, "No permission for -mod");
|
||||
sendErrorMessage(pluginData, msg.channel, "You don't have permission to use -mod");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -127,7 +127,7 @@ export async function banUserId(
|
|||
banTime: banTime ? humanizeDuration(banTime) : null,
|
||||
});
|
||||
|
||||
pluginData.state.events.emit("ban", user.id, reason);
|
||||
pluginData.state.events.emit("ban", user.id, reason, banOptions.isAutomodAction);
|
||||
|
||||
return {
|
||||
status: "success",
|
||||
|
|
|
@ -1,16 +1,44 @@
|
|||
import { GuildPluginData } from "knub";
|
||||
import { ModActionsPluginType } from "../types";
|
||||
import { isDiscordHTTPError } from "../../../utils";
|
||||
import { isDiscordHTTPError, isDiscordRESTError, SECONDS, sleep } from "../../../utils";
|
||||
import { LogsPlugin } from "../../Logs/LogsPlugin";
|
||||
import { LogType } from "../../../data/LogType";
|
||||
import { hasDiscordPermissions } from "../../../utils/hasDiscordPermissions";
|
||||
import { Constants } from "eris";
|
||||
|
||||
export async function isBanned(
|
||||
pluginData: GuildPluginData<ModActionsPluginType>,
|
||||
userId: string,
|
||||
timeout: number = 5 * SECONDS,
|
||||
): Promise<boolean> {
|
||||
const botMember = pluginData.guild.members.get(pluginData.client.user.id);
|
||||
if (botMember && !hasDiscordPermissions(botMember.permissions, Constants.Permissions.banMembers)) {
|
||||
pluginData.getPlugin(LogsPlugin).log(LogType.BOT_ALERT, {
|
||||
body: `Missing "Ban Members" permission to check for existing bans`,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
export async function isBanned(pluginData: GuildPluginData<ModActionsPluginType>, userId: string): Promise<boolean> {
|
||||
try {
|
||||
const bans = await pluginData.guild.getBans();
|
||||
return bans.some(b => b.user.id === userId);
|
||||
const potentialBan = await Promise.race([pluginData.guild.getBan(userId), sleep(timeout)]);
|
||||
return potentialBan != null;
|
||||
} catch (e) {
|
||||
if (isDiscordHTTPError(e) && e.code === 500) {
|
||||
if (isDiscordRESTError(e) && e.code === 10026) {
|
||||
// [10026]: Unknown Ban
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isDiscordHTTPError(e) && e.code === 500) {
|
||||
// Internal server error, ignore
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isDiscordRESTError(e) && e.code === 50013) {
|
||||
pluginData.getPlugin(LogsPlugin).log(LogType.BOT_ALERT, {
|
||||
body: `Missing "Ban Members" permission to check for existing bans`,
|
||||
});
|
||||
}
|
||||
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -85,7 +85,7 @@ export async function kickMember(
|
|||
reason,
|
||||
});
|
||||
|
||||
pluginData.state.events.emit("kick", member.id, reason);
|
||||
pluginData.state.events.emit("kick", member.id, reason, kickOptions.isAutomodAction);
|
||||
|
||||
return {
|
||||
status: "success",
|
||||
|
|
|
@ -82,6 +82,8 @@ export async function warnMember(
|
|||
reason,
|
||||
});
|
||||
|
||||
pluginData.state.events.emit("warn", member.id, reason, warnOptions.isAutomodAction);
|
||||
|
||||
return {
|
||||
status: "success",
|
||||
case: createdCase,
|
||||
|
|
|
@ -48,9 +48,9 @@ export type TConfigSchema = t.TypeOf<typeof ConfigSchema>;
|
|||
|
||||
export interface ModActionsEvents {
|
||||
note: (userId: string, reason?: string) => void;
|
||||
warn: (userId: string, reason?: string) => void;
|
||||
kick: (userId: string, reason?: string) => void;
|
||||
ban: (userId: string, reason?: string) => void;
|
||||
warn: (userId: string, reason?: string, isAutomodAction?: boolean) => void;
|
||||
kick: (userId: string, reason?: string, isAutomodAction?: boolean) => void;
|
||||
ban: (userId: string, reason?: string, isAutomodAction?: boolean) => void;
|
||||
unban: (userId: string, reason?: string) => void;
|
||||
// mute/unmute are in the Mutes plugin
|
||||
}
|
||||
|
@ -126,17 +126,20 @@ export interface WarnOptions {
|
|||
caseArgs?: Partial<CaseArgs> | null;
|
||||
contactMethods?: UserNotificationMethod[] | null;
|
||||
retryPromptChannel?: TextChannel | null;
|
||||
isAutomodAction?: boolean;
|
||||
}
|
||||
|
||||
export interface KickOptions {
|
||||
caseArgs?: Partial<CaseArgs>;
|
||||
contactMethods?: UserNotificationMethod[];
|
||||
isAutomodAction?: boolean;
|
||||
}
|
||||
|
||||
export interface BanOptions {
|
||||
caseArgs?: Partial<CaseArgs>;
|
||||
contactMethods?: UserNotificationMethod[];
|
||||
deleteMessageDays?: number;
|
||||
isAutomodAction?: boolean;
|
||||
}
|
||||
|
||||
export type ModActionType = "note" | "warn" | "mute" | "unmute" | "kick" | "ban" | "unban";
|
||||
|
|
|
@ -25,6 +25,7 @@ const defaultOptions = {
|
|||
config: {
|
||||
mute_role: null,
|
||||
move_to_voice_channel: null,
|
||||
kick_from_voice_channel: false,
|
||||
|
||||
dm_on_mute: false,
|
||||
dm_on_update: false,
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { mutesEvt } from "../types";
|
||||
import { LogType } from "../../../data/LogType";
|
||||
import { stripObjectToScalars } from "../../../utils";
|
||||
import { memberRolesLock } from "../../../utils/lockNameHelpers";
|
||||
|
||||
/**
|
||||
* Reapply active mutes on join
|
||||
|
@ -11,9 +12,9 @@ export const ReapplyActiveMuteOnJoinEvt = mutesEvt("guildMemberAdd", async ({ pl
|
|||
const muteRole = pluginData.config.get().mute_role;
|
||||
|
||||
if (muteRole) {
|
||||
const memberRolesLock = await pluginData.locks.acquire(`member-roles-${member.id}`);
|
||||
const memberRoleLock = await pluginData.locks.acquire(memberRolesLock(member));
|
||||
await member.addRole(muteRole);
|
||||
memberRolesLock.unlock();
|
||||
memberRoleLock.unlock();
|
||||
}
|
||||
|
||||
pluginData.state.serverLogs.log(LogType.MEMBER_MUTE_REJOIN, {
|
||||
|
|
|
@ -17,8 +17,8 @@ import { CasesPlugin } from "../../Cases/CasesPlugin";
|
|||
import { CaseTypes } from "../../../data/CaseTypes";
|
||||
import { LogType } from "../../../data/LogType";
|
||||
import { Case } from "../../../data/entities/Case";
|
||||
import { sendErrorMessage } from "src/pluginUtils";
|
||||
import { LogsPlugin } from "src/plugins/Logs/LogsPlugin";
|
||||
import { LogsPlugin } from "../../../plugins/Logs/LogsPlugin";
|
||||
import { muteLock } from "../../../utils/lockNameHelpers";
|
||||
|
||||
export async function muteUser(
|
||||
pluginData: GuildPluginData<MutesPluginType>,
|
||||
|
@ -29,7 +29,7 @@ export async function muteUser(
|
|||
removeRolesOnMuteOverride: boolean | string[] | null = null,
|
||||
restoreRolesOnMuteOverride: boolean | string[] | null = null,
|
||||
) {
|
||||
const lock = await pluginData.locks.acquire(`mute-${userId}`);
|
||||
const lock = await pluginData.locks.acquire(muteLock({ id: userId }));
|
||||
|
||||
const muteRole = pluginData.config.get().mute_role;
|
||||
if (!muteRole) {
|
||||
|
@ -120,11 +120,12 @@ export async function muteUser(
|
|||
}
|
||||
|
||||
// If enabled, move the user to the mute voice channel (e.g. afk - just to apply the voice perms from the mute role)
|
||||
const moveToVoiceChannelId = pluginData.config.get().move_to_voice_channel;
|
||||
if (moveToVoiceChannelId) {
|
||||
const cfg = pluginData.config.get();
|
||||
const moveToVoiceChannel = cfg.kick_from_voice_channel ? null : cfg.move_to_voice_channel;
|
||||
if (moveToVoiceChannel || cfg.kick_from_voice_channel) {
|
||||
// TODO: Add back the voiceState check once we figure out how to get voice state for guild members that are loaded on-demand
|
||||
try {
|
||||
await member.edit({ channelID: moveToVoiceChannelId });
|
||||
await member.edit({ channelID: moveToVoiceChannel });
|
||||
} catch (e) {} // tslint:disable-line
|
||||
}
|
||||
}
|
||||
|
@ -246,7 +247,7 @@ export async function muteUser(
|
|||
|
||||
lock.unlock();
|
||||
|
||||
pluginData.state.events.emit("mute", user.id, reason);
|
||||
pluginData.state.events.emit("mute", user.id, reason, muteOptions.isAutomodAction);
|
||||
|
||||
return {
|
||||
case: theCase,
|
||||
|
|
|
@ -15,6 +15,7 @@ import { EventEmitter } from "events";
|
|||
export const ConfigSchema = t.type({
|
||||
mute_role: tNullable(t.string),
|
||||
move_to_voice_channel: tNullable(t.string),
|
||||
kick_from_voice_channel: t.boolean,
|
||||
|
||||
dm_on_mute: t.boolean,
|
||||
dm_on_update: t.boolean,
|
||||
|
@ -33,7 +34,7 @@ export const ConfigSchema = t.type({
|
|||
export type TConfigSchema = t.TypeOf<typeof ConfigSchema>;
|
||||
|
||||
export interface MutesEvents {
|
||||
mute: (userId: string, reason?: string) => void;
|
||||
mute: (userId: string, reason?: string, isAutomodAction?: boolean) => void;
|
||||
unmute: (userId: string, reason?: string) => void;
|
||||
}
|
||||
|
||||
|
@ -74,6 +75,7 @@ export type UnmuteResult = {
|
|||
export interface MuteOptions {
|
||||
caseArgs?: Partial<CaseArgs>;
|
||||
contactMethods?: UserNotificationMethod[];
|
||||
isAutomodAction?: boolean;
|
||||
}
|
||||
|
||||
export const mutesCmd = guildCommand<MutesPluginType>();
|
||||
|
|
|
@ -7,6 +7,7 @@ import { getMissingPermissions } from "../../../utils/getMissingPermissions";
|
|||
import { LogsPlugin } from "../../Logs/LogsPlugin";
|
||||
import { missingPermissionError } from "../../../utils/missingPermissionError";
|
||||
import { canAssignRole } from "../../../utils/canAssignRole";
|
||||
import { memberRolesLock } from "../../../utils/lockNameHelpers";
|
||||
|
||||
const p = Constants.Permissions;
|
||||
|
||||
|
@ -17,11 +18,11 @@ export const LoadDataEvt = persistEvt({
|
|||
const member = meta.args.member;
|
||||
const pluginData = meta.pluginData;
|
||||
|
||||
const memberRolesLock = await pluginData.locks.acquire(`member-roles-${member.id}`);
|
||||
const memberRoleLock = await pluginData.locks.acquire(memberRolesLock(member));
|
||||
|
||||
const persistedData = await pluginData.state.persistedData.find(member.id);
|
||||
if (!persistedData) {
|
||||
memberRolesLock.unlock();
|
||||
memberRoleLock.unlock();
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -79,6 +80,6 @@ export const LoadDataEvt = persistEvt({
|
|||
});
|
||||
}
|
||||
|
||||
memberRolesLock.unlock();
|
||||
memberRoleLock.unlock();
|
||||
},
|
||||
});
|
||||
|
|
|
@ -2,6 +2,7 @@ import { GuildPluginData } from "knub";
|
|||
import { ReactionRolesPluginType, RoleChangeMode, PendingMemberRoleChanges } from "../types";
|
||||
import { resolveMember } from "../../../utils";
|
||||
import { logger } from "../../../logger";
|
||||
import { memberRolesLock } from "../../../utils/lockNameHelpers";
|
||||
|
||||
const ROLE_CHANGE_BATCH_DEBOUNCE_TIME = 1500;
|
||||
|
||||
|
@ -18,7 +19,7 @@ export async function addMemberPendingRoleChange(
|
|||
applyFn: async () => {
|
||||
pluginData.state.pendingRoleChanges.delete(memberId);
|
||||
|
||||
const lock = await pluginData.locks.acquire(`member-roles-${memberId}`);
|
||||
const lock = await pluginData.locks.acquire(memberRolesLock({ id: memberId }));
|
||||
|
||||
const member = await resolveMember(pluginData.client, pluginData.guild, memberId);
|
||||
if (member) {
|
||||
|
|
|
@ -7,7 +7,7 @@ import { remindersCmd } from "../types";
|
|||
import { TimeAndDatePlugin } from "../../TimeAndDate/TimeAndDatePlugin";
|
||||
|
||||
export const RemindCmd = remindersCmd({
|
||||
trigger: ["remind", "remindme"],
|
||||
trigger: ["remind", "remindme", "reminder"],
|
||||
usage: "!remind 3h Remind me of this in 3 hours please",
|
||||
permission: "can_use",
|
||||
|
||||
|
|
|
@ -20,13 +20,21 @@ export async function postDueRemindersLoop(pluginData: GuildPluginData<Reminders
|
|||
const target = moment.utc();
|
||||
const diff = target.diff(moment.utc(reminder.created_at, "YYYY-MM-DD HH:mm:ss"));
|
||||
const result = humanizeDuration(diff, { largest: 2, round: true });
|
||||
await channel.createMessage(
|
||||
disableLinkPreviews(
|
||||
await channel.createMessage({
|
||||
content: disableLinkPreviews(
|
||||
`Reminder for <@!${reminder.user_id}>: ${reminder.body} \n\`Set at ${reminder.created_at} (${result} ago)\``,
|
||||
),
|
||||
);
|
||||
allowedMentions: {
|
||||
users: [reminder.user_id],
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await channel.createMessage(disableLinkPreviews(`Reminder for <@!${reminder.user_id}>: ${reminder.body}`));
|
||||
await channel.createMessage({
|
||||
content: disableLinkPreviews(`Reminder for <@!${reminder.user_id}>: ${reminder.body}`),
|
||||
allowedMentions: {
|
||||
users: [reminder.user_id],
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
// Probably random Discord internal server error or missing permissions or somesuch
|
||||
|
|
|
@ -6,6 +6,7 @@ import { splitRoleNames } from "../util/splitRoleNames";
|
|||
import { normalizeRoleNames } from "../util/normalizeRoleNames";
|
||||
import { findMatchingRoles } from "../util/findMatchingRoles";
|
||||
import { Role } from "eris";
|
||||
import { memberRolesLock } from "../../../utils/lockNameHelpers";
|
||||
|
||||
export const RoleAddCmd = selfGrantableRolesCmd({
|
||||
trigger: ["role", "role add"],
|
||||
|
@ -16,7 +17,7 @@ export const RoleAddCmd = selfGrantableRolesCmd({
|
|||
},
|
||||
|
||||
async run({ message: msg, args, pluginData }) {
|
||||
const lock = await pluginData.locks.acquire(`grantableRoles:${msg.author.id}`);
|
||||
const lock = await pluginData.locks.acquire(memberRolesLock(msg.author));
|
||||
|
||||
const applyingEntries = getApplyingEntries(pluginData, msg);
|
||||
if (applyingEntries.length === 0) {
|
||||
|
|
|
@ -5,6 +5,7 @@ import { sendErrorMessage, sendSuccessMessage } from "../../../pluginUtils";
|
|||
import { splitRoleNames } from "../util/splitRoleNames";
|
||||
import { normalizeRoleNames } from "../util/normalizeRoleNames";
|
||||
import { findMatchingRoles } from "../util/findMatchingRoles";
|
||||
import { memberRolesLock } from "../../../utils/lockNameHelpers";
|
||||
|
||||
export const RoleRemoveCmd = selfGrantableRolesCmd({
|
||||
trigger: "role remove",
|
||||
|
@ -15,7 +16,7 @@ export const RoleRemoveCmd = selfGrantableRolesCmd({
|
|||
},
|
||||
|
||||
async run({ message: msg, args, pluginData }) {
|
||||
const lock = await pluginData.locks.acquire(`grantableRoles:${msg.author.id}`);
|
||||
const lock = await pluginData.locks.acquire(memberRolesLock(msg.author));
|
||||
|
||||
const applyingEntries = getApplyingEntries(pluginData, msg);
|
||||
if (applyingEntries.length === 0) {
|
||||
|
|
|
@ -10,6 +10,7 @@ import { BOT_SLOWMODE_PERMISSIONS } from "../requiredPermissions";
|
|||
import { LogsPlugin } from "../../Logs/LogsPlugin";
|
||||
import { LogType } from "../../../data/LogType";
|
||||
import { missingPermissionError } from "../../../utils/missingPermissionError";
|
||||
import { messageLock } from "../../../utils/lockNameHelpers";
|
||||
|
||||
export async function onMessageCreate(pluginData: GuildPluginData<SlowmodePluginType>, msg: SavedMessage) {
|
||||
if (msg.is_bot) return;
|
||||
|
@ -18,7 +19,7 @@ export async function onMessageCreate(pluginData: GuildPluginData<SlowmodePlugin
|
|||
if (!channel) return;
|
||||
|
||||
// Don't apply slowmode if the lock was interrupted earlier (e.g. the message was caught by word filters)
|
||||
const thisMsgLock = await pluginData.locks.acquire(`message-${msg.id}`);
|
||||
const thisMsgLock = await pluginData.locks.acquire(messageLock(msg));
|
||||
if (thisMsgLock.interrupted) return;
|
||||
|
||||
// Check if this channel even *has* a bot-maintained slowmode
|
||||
|
|
|
@ -57,6 +57,19 @@ export const StarboardPlugin = zeppelinGuildPlugin<StarboardPluginType>()("starb
|
|||
stars_required: 5
|
||||
~~~
|
||||
|
||||
### Basic starboard with custom color
|
||||
Any message on the server that gets 5 star reactions will be posted into the starboard channel (604342689038729226), with the given color (0x87CEEB).
|
||||
|
||||
~~~yml
|
||||
starboard:
|
||||
config:
|
||||
boards:
|
||||
basic:
|
||||
channel_id: "604342689038729226"
|
||||
stars_required: 5
|
||||
color: 0x87CEEB
|
||||
~~~
|
||||
|
||||
### Custom star emoji
|
||||
This is identical to the basic starboard above, but accepts two emoji: the regular star and a custom :mrvnSmile: emoji
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@ import { Message, TextChannel } from "eris";
|
|||
import { UnknownUser, resolveMember, noop, resolveUser } from "../../../utils";
|
||||
import { saveMessageToStarboard } from "../util/saveMessageToStarboard";
|
||||
import { updateStarboardMessageStarCount } from "../util/updateStarboardMessageStarCount";
|
||||
import { allStarboardsLock } from "../../../utils/lockNameHelpers";
|
||||
|
||||
export const StarboardReactionAddEvt = starboardEvt({
|
||||
event: "messageReactionAdd",
|
||||
|
@ -36,7 +37,7 @@ export const StarboardReactionAddEvt = starboardEvt({
|
|||
categoryId: (msg.channel as TextChannel).parentID,
|
||||
});
|
||||
|
||||
const boardLock = await pluginData.locks.acquire(`starboards`);
|
||||
const boardLock = await pluginData.locks.acquire(allStarboardsLock());
|
||||
|
||||
const applicableStarboards = Object.values(config.boards)
|
||||
.filter(board => board.enabled)
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import { allStarboardsLock } from "../../../utils/lockNameHelpers";
|
||||
import { starboardEvt } from "../types";
|
||||
|
||||
export const StarboardReactionRemoveEvt = starboardEvt({
|
||||
event: "messageReactionRemove",
|
||||
|
||||
async listener(meta) {
|
||||
const boardLock = await meta.pluginData.locks.acquire(`starboards`);
|
||||
const boardLock = await meta.pluginData.locks.acquire(allStarboardsLock());
|
||||
await meta.pluginData.state.starboardReactions.deleteStarboardReaction(meta.args.message.id, meta.args.member.id);
|
||||
boardLock.unlock();
|
||||
},
|
||||
|
@ -14,7 +15,7 @@ export const StarboardReactionRemoveAllEvt = starboardEvt({
|
|||
event: "messageReactionRemoveAll",
|
||||
|
||||
async listener(meta) {
|
||||
const boardLock = await meta.pluginData.locks.acquire(`starboards`);
|
||||
const boardLock = await meta.pluginData.locks.acquire(allStarboardsLock());
|
||||
await meta.pluginData.state.starboardReactions.deleteAllStarboardReactionsForMessageId(meta.args.message.id);
|
||||
boardLock.unlock();
|
||||
},
|
||||
|
|
|
@ -12,6 +12,7 @@ const StarboardOpts = t.type({
|
|||
copy_full_embed: tNullable(t.boolean),
|
||||
enabled: tNullable(t.boolean),
|
||||
show_star_count: t.boolean,
|
||||
color: tNullable(t.number),
|
||||
});
|
||||
export type TStarboardOpts = t.TypeOf<typeof StarboardOpts>;
|
||||
|
||||
|
@ -27,6 +28,7 @@ export const defaultStarboardOpts: Partial<TStarboardOpts> = {
|
|||
star_emoji: ["⭐"],
|
||||
enabled: true,
|
||||
show_star_count: true,
|
||||
color: null,
|
||||
};
|
||||
|
||||
export interface StarboardPluginType extends BasePluginType {
|
||||
|
|
|
@ -8,7 +8,11 @@ const videoAttachmentExtensions = ["mp4", "mkv", "mov"];
|
|||
|
||||
type StarboardEmbed = EmbedWith<"footer" | "author" | "fields" | "timestamp">;
|
||||
|
||||
export function createStarboardEmbedFromMessage(msg: Message, copyFullEmbed: boolean): StarboardEmbed {
|
||||
export function createStarboardEmbedFromMessage(
|
||||
msg: Message,
|
||||
copyFullEmbed: boolean,
|
||||
color?: number | null,
|
||||
): StarboardEmbed {
|
||||
const embed: StarboardEmbed = {
|
||||
footer: {
|
||||
text: `#${(msg.channel as GuildChannel).name}`,
|
||||
|
@ -20,6 +24,10 @@ export function createStarboardEmbedFromMessage(msg: Message, copyFullEmbed: boo
|
|||
timestamp: new Date(msg.timestamp).toISOString(),
|
||||
};
|
||||
|
||||
if (color != null) {
|
||||
embed.color = color;
|
||||
}
|
||||
|
||||
if (msg.author.avatarURL) {
|
||||
embed.author.icon_url = msg.author.avatarURL;
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ export async function saveMessageToStarboard(
|
|||
if (!channel) return;
|
||||
|
||||
const starCount = (await pluginData.state.starboardReactions.getAllReactionsForMessageId(msg.id)).length;
|
||||
const embed = createStarboardEmbedFromMessage(msg, Boolean(starboard.copy_full_embed));
|
||||
const embed = createStarboardEmbedFromMessage(msg, Boolean(starboard.copy_full_embed), starboard.color);
|
||||
embed.fields!.push(createStarboardPseudoFooterForMessage(starboard, msg, starboard.star_emoji![0], starCount));
|
||||
|
||||
const starboardMessage = await (channel as TextChannel).createMessage({ embed });
|
||||
|
|
|
@ -29,6 +29,7 @@ const defaultOptions: PluginOptions<TagsPluginType> = {
|
|||
user_tag_cooldown: null,
|
||||
global_tag_cooldown: null,
|
||||
user_cooldown: null,
|
||||
allow_mentions: false,
|
||||
global_cooldown: null,
|
||||
auto_delete_command: false,
|
||||
|
||||
|
|
|
@ -15,6 +15,7 @@ export const TagCategory = t.type({
|
|||
user_tag_cooldown: tNullable(t.union([t.string, t.number])), // Per user, per tag
|
||||
user_category_cooldown: tNullable(t.union([t.string, t.number])), // Per user, per tag category
|
||||
global_tag_cooldown: tNullable(t.union([t.string, t.number])), // Any user, per tag
|
||||
allow_mentions: tNullable(t.boolean), // Per user, per category
|
||||
global_category_cooldown: tNullable(t.union([t.string, t.number])), // Any user, per category
|
||||
auto_delete_command: tNullable(t.boolean), // Any tag, per tag category
|
||||
|
||||
|
@ -31,6 +32,7 @@ export const ConfigSchema = t.type({
|
|||
user_tag_cooldown: tNullable(t.union([t.string, t.number])), // Per user, per tag
|
||||
global_tag_cooldown: tNullable(t.union([t.string, t.number])), // Any user, per tag
|
||||
user_cooldown: tNullable(t.union([t.string, t.number])), // Per user
|
||||
allow_mentions: t.boolean, // Per user
|
||||
global_cooldown: tNullable(t.union([t.string, t.number])), // Any tag use
|
||||
auto_delete_command: t.boolean, // Any tag
|
||||
|
||||
|
|
|
@ -99,7 +99,11 @@ export async function onMessageCreate(pluginData: GuildPluginData<TagsPluginType
|
|||
return;
|
||||
}
|
||||
|
||||
const responseMsg = await channel.createMessage(tagResult.renderedContent);
|
||||
const allowMentions = tagResult.category?.allow_mentions ?? config.allow_mentions;
|
||||
const responseMsg = await channel.createMessage({
|
||||
...tagResult.renderedContent,
|
||||
allowedMentions: { roles: allowMentions, users: allowMentions },
|
||||
});
|
||||
|
||||
// Save the command-response message pair once the message is in our database
|
||||
const deleteWithCommand = tagResult.category?.delete_with_command ?? config.delete_with_command;
|
||||
|
|
|
@ -36,6 +36,7 @@ import { discardRegExpRunner, getRegExpRunner } from "../../regExpRunners";
|
|||
import { TimeAndDatePlugin } from "../TimeAndDate/TimeAndDatePlugin";
|
||||
import { VcdisconnectCmd } from "./commands/VcdisconnectCmd";
|
||||
import { ModActionsPlugin } from "../ModActions/ModActionsPlugin";
|
||||
import { refreshMembersIfNeeded } from "./refreshMembers";
|
||||
|
||||
const defaultOptions: PluginOptions<UtilityPluginType> = {
|
||||
config: {
|
||||
|
@ -156,6 +157,21 @@ export const UtilityPlugin = zeppelinGuildPlugin<UtilityPluginType>()("utility",
|
|||
sendSuccessMessage(pluginData, activeReloads.get(guild.id)!, "Reloaded!");
|
||||
activeReloads.delete(guild.id);
|
||||
}
|
||||
|
||||
// FIXME: Temp fix for role change detection for specific servers, load all guild members in the background on bot start
|
||||
const roleChangeDetectionFixServers = [
|
||||
"786212572285763605",
|
||||
"653681924384096287",
|
||||
"493351982887862283",
|
||||
"513338222810497041",
|
||||
"523043978178723840",
|
||||
"718076393295970376",
|
||||
"803251072877199400",
|
||||
"750492934343753798",
|
||||
];
|
||||
if (roleChangeDetectionFixServers.includes(pluginData.guild.id)) {
|
||||
refreshMembersIfNeeded(pluginData.guild);
|
||||
}
|
||||
},
|
||||
|
||||
onUnload(pluginData) {
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue