mirror of
https://github.com/ZeppelinBot/Zeppelin.git
synced 2025-05-23 17:45:03 +00:00
initial - tempbans & mutes
This commit is contained in:
parent
c84d1a0be1
commit
744b9273bb
14 changed files with 322 additions and 109 deletions
|
@ -11,6 +11,14 @@ export class GuildMutes extends BaseGuildRepository {
|
||||||
this.mutes = getRepository(Mute);
|
this.mutes = getRepository(Mute);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getAllTemporaryMutes(): Promise<Mute[]> {
|
||||||
|
return this.mutes
|
||||||
|
.createQueryBuilder("mutes")
|
||||||
|
.where("guild_id = :guild_id", { guild_id: this.guildId })
|
||||||
|
.andWhere("expires_at IS NOT NULL")
|
||||||
|
.getMany();
|
||||||
|
}
|
||||||
|
|
||||||
async getExpiredMutes(): Promise<Mute[]> {
|
async getExpiredMutes(): Promise<Mute[]> {
|
||||||
return this.mutes
|
return this.mutes
|
||||||
.createQueryBuilder("mutes")
|
.createQueryBuilder("mutes")
|
||||||
|
|
|
@ -11,9 +11,17 @@ export class GuildTempbans extends BaseGuildRepository {
|
||||||
this.tempbans = getRepository(Tempban);
|
this.tempbans = getRepository(Tempban);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getAllTempbans(): Promise<Tempban[]> {
|
||||||
|
return this.tempbans
|
||||||
|
.createQueryBuilder("tempbans")
|
||||||
|
.where("guild_id = :guild_id", { guild_id: this.guildId })
|
||||||
|
.andWhere("expires_at IS NOT NULL")
|
||||||
|
.getMany();
|
||||||
|
}
|
||||||
|
|
||||||
async getExpiredTempbans(): Promise<Tempban[]> {
|
async getExpiredTempbans(): Promise<Tempban[]> {
|
||||||
return this.tempbans
|
return this.tempbans
|
||||||
.createQueryBuilder("mutes")
|
.createQueryBuilder("tempbans")
|
||||||
.where("guild_id = :guild_id", { guild_id: this.guildId })
|
.where("guild_id = :guild_id", { guild_id: this.guildId })
|
||||||
.andWhere("expires_at IS NOT NULL")
|
.andWhere("expires_at IS NOT NULL")
|
||||||
.andWhere("expires_at <= NOW()")
|
.andWhere("expires_at <= NOW()")
|
||||||
|
|
|
@ -42,7 +42,7 @@ import { hasMutePermission } from "./functions/hasMutePerm";
|
||||||
import { kickMember } from "./functions/kickMember";
|
import { kickMember } from "./functions/kickMember";
|
||||||
import { offModActionsEvent } from "./functions/offModActionsEvent";
|
import { offModActionsEvent } from "./functions/offModActionsEvent";
|
||||||
import { onModActionsEvent } from "./functions/onModActionsEvent";
|
import { onModActionsEvent } from "./functions/onModActionsEvent";
|
||||||
import { outdatedTempbansLoop } from "./functions/outdatedTempbansLoop";
|
import { loadExpiringTimers } from "./functions/outdatedTempbansLoop";
|
||||||
import { updateCase } from "./functions/updateCase";
|
import { updateCase } from "./functions/updateCase";
|
||||||
import { warnMember } from "./functions/warnMember";
|
import { warnMember } from "./functions/warnMember";
|
||||||
import { BanOptions, ConfigSchema, KickOptions, ModActionsPluginType, WarnOptions } from "./types";
|
import { BanOptions, ConfigSchema, KickOptions, ModActionsPluginType, WarnOptions } from "./types";
|
||||||
|
@ -112,6 +112,8 @@ const defaultOptions = {
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const EXPIRED_BANS_CHECK_INTERVAL = 30 * MINUTES;
|
||||||
|
|
||||||
export const ModActionsPlugin = zeppelinGuildPlugin<ModActionsPluginType>()({
|
export const ModActionsPlugin = zeppelinGuildPlugin<ModActionsPluginType>()({
|
||||||
name: "mod_actions",
|
name: "mod_actions",
|
||||||
showInDocs: true,
|
showInDocs: true,
|
||||||
|
@ -200,8 +202,8 @@ export const ModActionsPlugin = zeppelinGuildPlugin<ModActionsPluginType>()({
|
||||||
state.serverLogs = new GuildLogs(guild.id);
|
state.serverLogs = new GuildLogs(guild.id);
|
||||||
|
|
||||||
state.unloaded = false;
|
state.unloaded = false;
|
||||||
state.outdatedTempbansTimeout = null;
|
|
||||||
state.ignoredEvents = [];
|
state.ignoredEvents = [];
|
||||||
|
pluginData.state.timers = [];
|
||||||
// Massbans can take a while depending on rate limits,
|
// Massbans can take a while depending on rate limits,
|
||||||
// so we're giving each massban 15 minutes to complete before launching the next massban
|
// so we're giving each massban 15 minutes to complete before launching the next massban
|
||||||
state.massbanQueue = new Queue(15 * MINUTES);
|
state.massbanQueue = new Queue(15 * MINUTES);
|
||||||
|
@ -210,11 +212,17 @@ export const ModActionsPlugin = zeppelinGuildPlugin<ModActionsPluginType>()({
|
||||||
},
|
},
|
||||||
|
|
||||||
afterLoad(pluginData) {
|
afterLoad(pluginData) {
|
||||||
outdatedTempbansLoop(pluginData);
|
loadExpiringTimers(pluginData);
|
||||||
|
pluginData.state.banClearIntervalId = setInterval(
|
||||||
|
() => loadExpiringTimers(pluginData),
|
||||||
|
EXPIRED_BANS_CHECK_INTERVAL,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
beforeUnload(pluginData) {
|
beforeUnload(pluginData) {
|
||||||
|
clearInterval(pluginData.state.banClearIntervalId);
|
||||||
pluginData.state.unloaded = true;
|
pluginData.state.unloaded = true;
|
||||||
pluginData.state.events.removeAllListeners();
|
pluginData.state.events.removeAllListeners();
|
||||||
|
pluginData.state.timers = [];
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -15,6 +15,8 @@ import { isBanned } from "../functions/isBanned";
|
||||||
import { readContactMethodsFromArgs } from "../functions/readContactMethodsFromArgs";
|
import { readContactMethodsFromArgs } from "../functions/readContactMethodsFromArgs";
|
||||||
import { modActionsCmd } from "../types";
|
import { modActionsCmd } from "../types";
|
||||||
import { LogsPlugin } from "../../Logs/LogsPlugin";
|
import { LogsPlugin } from "../../Logs/LogsPlugin";
|
||||||
|
import moment from "moment";
|
||||||
|
import { addTimer, removeTimer, removeTimerByUserId } from "../functions/outdatedTempbansLoop";
|
||||||
|
|
||||||
const opts = {
|
const opts = {
|
||||||
mod: ct.member({ option: true }),
|
mod: ct.member({ option: true }),
|
||||||
|
@ -93,11 +95,18 @@ export const BanCmd = modActionsCmd({
|
||||||
if (time && time > 0) {
|
if (time && time > 0) {
|
||||||
if (existingTempban) {
|
if (existingTempban) {
|
||||||
pluginData.state.tempbans.updateExpiryTime(user.id, time, mod.id);
|
pluginData.state.tempbans.updateExpiryTime(user.id, time, mod.id);
|
||||||
|
removeTimer(pluginData, existingTempban);
|
||||||
|
addTimer(pluginData, {
|
||||||
|
...existingTempban,
|
||||||
|
expires_at: moment().utc().add(time, "ms").format("YYYY-MM-DD HH:mm:ss"),
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
pluginData.state.tempbans.addTempban(user.id, time, mod.id);
|
const tempban = await pluginData.state.tempbans.addTempban(user.id, time, mod.id);
|
||||||
|
addTimer(pluginData, tempban);
|
||||||
}
|
}
|
||||||
} else if (existingTempban) {
|
} else if (existingTempban) {
|
||||||
pluginData.state.tempbans.clear(user.id);
|
pluginData.state.tempbans.clear(user.id);
|
||||||
|
removeTimerByUserId(pluginData, user.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a new case for the updated ban since we never stored the old case id and log the action
|
// Create a new case for the updated ban since we never stored the old case id and log the action
|
||||||
|
|
|
@ -10,6 +10,7 @@ import { formatReasonWithAttachments } from "../functions/formatReasonWithAttach
|
||||||
import { ignoreEvent } from "../functions/ignoreEvent";
|
import { ignoreEvent } from "../functions/ignoreEvent";
|
||||||
import { IgnoredEventType, modActionsCmd } from "../types";
|
import { IgnoredEventType, modActionsCmd } from "../types";
|
||||||
import { LogsPlugin } from "../../Logs/LogsPlugin";
|
import { LogsPlugin } from "../../Logs/LogsPlugin";
|
||||||
|
import { removeTimerByUserId } from "../functions/outdatedTempbansLoop";
|
||||||
|
|
||||||
const opts = {
|
const opts = {
|
||||||
mod: ct.member({ option: true }),
|
mod: ct.member({ option: true }),
|
||||||
|
@ -69,7 +70,7 @@ export const UnbanCmd = modActionsCmd({
|
||||||
});
|
});
|
||||||
// Delete the tempban, if one exists
|
// Delete the tempban, if one exists
|
||||||
pluginData.state.tempbans.clear(user.id);
|
pluginData.state.tempbans.clear(user.id);
|
||||||
|
removeTimerByUserId(pluginData, user.id);
|
||||||
// Confirm the action
|
// Confirm the action
|
||||||
sendSuccessMessage(pluginData, msg.channel, `Member unbanned (Case #${createdCase.case_number})`);
|
sendSuccessMessage(pluginData, msg.channel, `Member unbanned (Case #${createdCase.case_number})`);
|
||||||
|
|
||||||
|
|
|
@ -19,6 +19,8 @@ import { BanOptions, BanResult, IgnoredEventType, ModActionsPluginType } from ".
|
||||||
import { getDefaultContactMethods } from "./getDefaultContactMethods";
|
import { getDefaultContactMethods } from "./getDefaultContactMethods";
|
||||||
import { ignoreEvent } from "./ignoreEvent";
|
import { ignoreEvent } from "./ignoreEvent";
|
||||||
import { LogsPlugin } from "../../Logs/LogsPlugin";
|
import { LogsPlugin } from "../../Logs/LogsPlugin";
|
||||||
|
import { addTimer, removeTimer } from "./outdatedTempbansLoop";
|
||||||
|
import moment from "moment";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ban the specified user id, whether or not they're actually on the server at the time. Generates a case.
|
* Ban the specified user id, whether or not they're actually on the server at the time. Generates a case.
|
||||||
|
@ -109,8 +111,14 @@ export async function banUserId(
|
||||||
const selfId = pluginData.client.user!.id;
|
const selfId = pluginData.client.user!.id;
|
||||||
if (existingTempban) {
|
if (existingTempban) {
|
||||||
pluginData.state.tempbans.updateExpiryTime(user.id, banTime, banOptions.modId ?? selfId);
|
pluginData.state.tempbans.updateExpiryTime(user.id, banTime, banOptions.modId ?? selfId);
|
||||||
|
removeTimer(pluginData, existingTempban);
|
||||||
|
addTimer(pluginData, {
|
||||||
|
...existingTempban,
|
||||||
|
expires_at: moment().utc().add(banTime, "ms").format("YYYY-MM-DD HH:mm:ss"),
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
pluginData.state.tempbans.addTempban(user.id, banTime, banOptions.modId ?? selfId);
|
const tempban = await pluginData.state.tempbans.addTempban(user.id, banTime, banOptions.modId ?? selfId);
|
||||||
|
addTimer(pluginData, tempban);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,67 +4,123 @@ import { GuildPluginData } from "knub";
|
||||||
import moment from "moment-timezone";
|
import moment from "moment-timezone";
|
||||||
import { LogType } from "src/data/LogType";
|
import { LogType } from "src/data/LogType";
|
||||||
import { logger } from "src/logger";
|
import { logger } from "src/logger";
|
||||||
import { userToTemplateSafeUser } from "../../../utils/templateSafeObjects";
|
|
||||||
import { CaseTypes } from "../../../data/CaseTypes";
|
import { CaseTypes } from "../../../data/CaseTypes";
|
||||||
import { resolveUser, SECONDS } from "../../../utils";
|
import { MINUTES, resolveUser } from "../../../utils";
|
||||||
import { CasesPlugin } from "../../Cases/CasesPlugin";
|
import { CasesPlugin } from "../../Cases/CasesPlugin";
|
||||||
import { IgnoredEventType, ModActionsPluginType } from "../types";
|
import { IgnoredEventType, ModActionsPluginType } from "../types";
|
||||||
import { formatReasonWithAttachments } from "./formatReasonWithAttachments";
|
import { formatReasonWithAttachments } from "./formatReasonWithAttachments";
|
||||||
import { ignoreEvent } from "./ignoreEvent";
|
import { ignoreEvent } from "./ignoreEvent";
|
||||||
import { isBanned } from "./isBanned";
|
import { isBanned } from "./isBanned";
|
||||||
import { LogsPlugin } from "../../Logs/LogsPlugin";
|
import { LogsPlugin } from "../../Logs/LogsPlugin";
|
||||||
|
import { Tempban } from "src/data/entities/Tempban";
|
||||||
|
import { ExpiringTimer } from "src/utils/timers";
|
||||||
|
|
||||||
const TEMPBAN_LOOP_TIME = 60 * SECONDS;
|
const LOAD_LESS_THAN_MIN_COUNT = 60 * MINUTES;
|
||||||
|
|
||||||
export async function outdatedTempbansLoop(pluginData: GuildPluginData<ModActionsPluginType>) {
|
export function addTimer(pluginData: GuildPluginData<ModActionsPluginType>, tempban: Tempban) {
|
||||||
const outdatedTempbans = await pluginData.state.tempbans.getExpiredTempbans();
|
const existingMute = pluginData.state.timers.find(
|
||||||
|
(tm) => tm.options.key === tempban.user_id && tm.options.guildId === tempban.guild_id && !tm.done,
|
||||||
for (const tempban of outdatedTempbans) {
|
); // for future-proof when you do global events
|
||||||
if (!(await isBanned(pluginData, tempban.user_id))) {
|
if (!existingMute && tempban.expires_at) {
|
||||||
pluginData.state.tempbans.clear(tempban.user_id);
|
const exp = moment(tempban.expires_at!).toDate().getTime() - moment.utc().toDate().getTime();
|
||||||
continue;
|
const newTimer = new ExpiringTimer({
|
||||||
}
|
key: tempban.user_id,
|
||||||
|
guildId: tempban.guild_id,
|
||||||
pluginData.state.serverLogs.ignoreLog(LogType.MEMBER_UNBAN, tempban.user_id);
|
plugin: "tempban",
|
||||||
const reason = formatReasonWithAttachments(
|
expiry: exp,
|
||||||
`Tempban timed out.
|
callback: async () => {
|
||||||
Tempbanned at: \`${tempban.created_at} UTC\``,
|
await clearTempBan(pluginData, tempban);
|
||||||
[],
|
},
|
||||||
);
|
|
||||||
try {
|
|
||||||
ignoreEvent(pluginData, IgnoredEventType.Unban, tempban.user_id);
|
|
||||||
await pluginData.guild.bans.remove(tempban.user_id as Snowflake, reason ?? undefined);
|
|
||||||
} catch (e) {
|
|
||||||
pluginData.getPlugin(LogsPlugin).logBotAlert({
|
|
||||||
body: `Encountered an error trying to automatically unban ${tempban.user_id} after tempban timeout`,
|
|
||||||
});
|
|
||||||
logger.warn(`Error automatically unbanning ${tempban.user_id} (tempban timeout): ${e}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create case and delete tempban
|
|
||||||
const casesPlugin = pluginData.getPlugin(CasesPlugin);
|
|
||||||
const createdCase = await casesPlugin.createCase({
|
|
||||||
userId: tempban.user_id,
|
|
||||||
modId: tempban.mod_id,
|
|
||||||
type: CaseTypes.Unban,
|
|
||||||
reason,
|
|
||||||
ppId: undefined,
|
|
||||||
});
|
});
|
||||||
pluginData.state.tempbans.clear(tempban.user_id);
|
pluginData.state.timers.push(newTimer);
|
||||||
|
|
||||||
// Log the unban
|
|
||||||
const banTime = moment(tempban.created_at).diff(moment(tempban.expires_at));
|
|
||||||
pluginData.getPlugin(LogsPlugin).logMemberTimedUnban({
|
|
||||||
mod: await resolveUser(pluginData.client, tempban.mod_id),
|
|
||||||
userId: tempban.user_id,
|
|
||||||
caseNumber: createdCase.case_number,
|
|
||||||
reason,
|
|
||||||
banTime: humanizeDuration(banTime),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!pluginData.state.unloaded) {
|
|
||||||
pluginData.state.outdatedTempbansTimeout = setTimeout(() => outdatedTempbansLoop(pluginData), TEMPBAN_LOOP_TIME);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function removeTimer(pluginData: GuildPluginData<ModActionsPluginType>, tempban: Tempban) {
|
||||||
|
const existingMute = pluginData.state.timers.findIndex(
|
||||||
|
(tm) => tm.options.key === tempban.user_id && tm.options.guildId === tempban.guild_id && !tm.done,
|
||||||
|
);
|
||||||
|
if (existingMute) {
|
||||||
|
const tm = pluginData.state.timers[existingMute];
|
||||||
|
tm.clear();
|
||||||
|
tm.done = true;
|
||||||
|
pluginData.state.timers.splice(existingMute, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeTimerByUserId(pluginData: GuildPluginData<ModActionsPluginType>, user_id: Snowflake) {
|
||||||
|
const existingMute = pluginData.state.timers.findIndex((tm) => tm.options.key === user_id && !tm.done);
|
||||||
|
if (existingMute) {
|
||||||
|
const tm = pluginData.state.timers[existingMute];
|
||||||
|
tm.clear();
|
||||||
|
tm.done = true;
|
||||||
|
pluginData.state.timers.splice(existingMute, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadExpiringTimers(pluginData: GuildPluginData<ModActionsPluginType>) {
|
||||||
|
const now = moment.utc().toDate().getTime();
|
||||||
|
pluginData.state.timers = pluginData.state.timers.filter((tm) => !tm.done || !tm.timeout);
|
||||||
|
const tempbans = (await pluginData.state.tempbans.getAllTempbans()).filter((m) => m.expires_at);
|
||||||
|
const expiredBans = tempbans.filter((m) => now >= moment(m.expires_at!).toDate().getTime());
|
||||||
|
const expiringBans = tempbans.filter(
|
||||||
|
(m) => !expiredBans.find((exp) => exp.user_id === m.user_id && exp.guild_id === m.guild_id),
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const tempban of expiringBans) {
|
||||||
|
const expires = moment(tempban.expires_at!).toDate().getTime();
|
||||||
|
if (expires <= now) continue; // exclude expired mutes, just in case
|
||||||
|
if (expires > now + LOAD_LESS_THAN_MIN_COUNT) continue; // exclude timers that are expiring in over 180 mins
|
||||||
|
|
||||||
|
addTimer(pluginData, tempban);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const tempban of expiredBans) {
|
||||||
|
await clearTempBan(pluginData, tempban);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function clearTempBan(pluginData: GuildPluginData<ModActionsPluginType>, tempban: Tempban) {
|
||||||
|
if (!(await isBanned(pluginData, tempban.user_id))) {
|
||||||
|
pluginData.state.tempbans.clear(tempban.user_id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
pluginData.state.serverLogs.ignoreLog(LogType.MEMBER_UNBAN, tempban.user_id);
|
||||||
|
const reason = formatReasonWithAttachments(
|
||||||
|
`Tempban timed out.
|
||||||
|
Tempbanned at: \`${tempban.created_at} UTC\``,
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
ignoreEvent(pluginData, IgnoredEventType.Unban, tempban.user_id);
|
||||||
|
await pluginData.guild.bans.remove(tempban.user_id as Snowflake, reason ?? undefined);
|
||||||
|
} catch (e) {
|
||||||
|
pluginData.getPlugin(LogsPlugin).logBotAlert({
|
||||||
|
body: `Encountered an error trying to automatically unban ${tempban.user_id} after tempban timeout`,
|
||||||
|
});
|
||||||
|
logger.warn(`Error automatically unbanning ${tempban.user_id} (tempban timeout): ${e}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create case and delete tempban
|
||||||
|
const casesPlugin = pluginData.getPlugin(CasesPlugin);
|
||||||
|
const createdCase = await casesPlugin.createCase({
|
||||||
|
userId: tempban.user_id,
|
||||||
|
modId: tempban.mod_id,
|
||||||
|
type: CaseTypes.Unban,
|
||||||
|
reason,
|
||||||
|
ppId: undefined,
|
||||||
|
});
|
||||||
|
pluginData.state.tempbans.clear(tempban.user_id);
|
||||||
|
|
||||||
|
// Log the unban
|
||||||
|
const banTime = moment(tempban.created_at).diff(moment(tempban.expires_at));
|
||||||
|
pluginData.getPlugin(LogsPlugin).logMemberTimedUnban({
|
||||||
|
mod: await resolveUser(pluginData.client, tempban.mod_id),
|
||||||
|
userId: tempban.user_id,
|
||||||
|
caseNumber: createdCase.case_number,
|
||||||
|
reason,
|
||||||
|
banTime: humanizeDuration(banTime),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { TextChannel } from "discord.js";
|
||||||
import { EventEmitter } from "events";
|
import { EventEmitter } from "events";
|
||||||
import * as t from "io-ts";
|
import * as t from "io-ts";
|
||||||
import { BasePluginType, typedGuildCommand, typedGuildEventListener } from "knub";
|
import { BasePluginType, typedGuildCommand, typedGuildEventListener } from "knub";
|
||||||
|
import { ExpiringTimer } from "src/utils/timers";
|
||||||
import { Case } from "../../data/entities/Case";
|
import { Case } from "../../data/entities/Case";
|
||||||
import { GuildCases } from "../../data/GuildCases";
|
import { GuildCases } from "../../data/GuildCases";
|
||||||
import { GuildLogs } from "../../data/GuildLogs";
|
import { GuildLogs } from "../../data/GuildLogs";
|
||||||
|
@ -70,9 +71,9 @@ export interface ModActionsPluginType extends BasePluginType {
|
||||||
cases: GuildCases;
|
cases: GuildCases;
|
||||||
tempbans: GuildTempbans;
|
tempbans: GuildTempbans;
|
||||||
serverLogs: GuildLogs;
|
serverLogs: GuildLogs;
|
||||||
|
banClearIntervalId: Timeout;
|
||||||
|
timers: ExpiringTimer[];
|
||||||
unloaded: boolean;
|
unloaded: boolean;
|
||||||
outdatedTempbansTimeout: Timeout | null;
|
|
||||||
ignoredEvents: IIgnoredEvent[];
|
ignoredEvents: IIgnoredEvent[];
|
||||||
massbanQueue: Queue;
|
massbanQueue: Queue;
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { GuildMember, Snowflake } from "discord.js";
|
import { GuildMember, Snowflake } from "discord.js";
|
||||||
import { EventEmitter } from "events";
|
import { EventEmitter } from "events";
|
||||||
|
import { MINUTES } from "src/utils";
|
||||||
import { GuildArchives } from "../../data/GuildArchives";
|
import { GuildArchives } from "../../data/GuildArchives";
|
||||||
import { GuildCases } from "../../data/GuildCases";
|
import { GuildCases } from "../../data/GuildCases";
|
||||||
import { GuildLogs } from "../../data/GuildLogs";
|
import { GuildLogs } from "../../data/GuildLogs";
|
||||||
|
@ -15,7 +16,7 @@ import { MutesCmd } from "./commands/MutesCmd";
|
||||||
import { ClearActiveMuteOnMemberBanEvt } from "./events/ClearActiveMuteOnMemberBanEvt";
|
import { ClearActiveMuteOnMemberBanEvt } from "./events/ClearActiveMuteOnMemberBanEvt";
|
||||||
import { ClearActiveMuteOnRoleRemovalEvt } from "./events/ClearActiveMuteOnRoleRemovalEvt";
|
import { ClearActiveMuteOnRoleRemovalEvt } from "./events/ClearActiveMuteOnRoleRemovalEvt";
|
||||||
import { ReapplyActiveMuteOnJoinEvt } from "./events/ReapplyActiveMuteOnJoinEvt";
|
import { ReapplyActiveMuteOnJoinEvt } from "./events/ReapplyActiveMuteOnJoinEvt";
|
||||||
import { clearExpiredMutes } from "./functions/clearExpiredMutes";
|
import { loadExpiringTimers } from "./functions/clearExpiredMutes";
|
||||||
import { muteUser } from "./functions/muteUser";
|
import { muteUser } from "./functions/muteUser";
|
||||||
import { offMutesEvent } from "./functions/offMutesEvent";
|
import { offMutesEvent } from "./functions/offMutesEvent";
|
||||||
import { onMutesEvent } from "./functions/onMutesEvent";
|
import { onMutesEvent } from "./functions/onMutesEvent";
|
||||||
|
@ -58,7 +59,7 @@ const defaultOptions = {
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
const EXPIRED_MUTE_CHECK_INTERVAL = 60 * 1000;
|
const EXPIRED_MUTE_CHECK_INTERVAL = 30 * MINUTES;
|
||||||
|
|
||||||
export const MutesPlugin = zeppelinGuildPlugin<MutesPluginType>()({
|
export const MutesPlugin = zeppelinGuildPlugin<MutesPluginType>()({
|
||||||
name: "mutes",
|
name: "mutes",
|
||||||
|
@ -108,14 +109,14 @@ export const MutesPlugin = zeppelinGuildPlugin<MutesPluginType>()({
|
||||||
pluginData.state.cases = GuildCases.getGuildInstance(pluginData.guild.id);
|
pluginData.state.cases = GuildCases.getGuildInstance(pluginData.guild.id);
|
||||||
pluginData.state.serverLogs = new GuildLogs(pluginData.guild.id);
|
pluginData.state.serverLogs = new GuildLogs(pluginData.guild.id);
|
||||||
pluginData.state.archives = GuildArchives.getGuildInstance(pluginData.guild.id);
|
pluginData.state.archives = GuildArchives.getGuildInstance(pluginData.guild.id);
|
||||||
|
pluginData.state.timers = [];
|
||||||
pluginData.state.events = new EventEmitter();
|
pluginData.state.events = new EventEmitter();
|
||||||
},
|
},
|
||||||
|
|
||||||
afterLoad(pluginData) {
|
afterLoad(pluginData) {
|
||||||
clearExpiredMutes(pluginData);
|
loadExpiringTimers(pluginData);
|
||||||
pluginData.state.muteClearIntervalId = setInterval(
|
pluginData.state.muteClearIntervalId = setInterval(
|
||||||
() => clearExpiredMutes(pluginData),
|
() => loadExpiringTimers(pluginData),
|
||||||
EXPIRED_MUTE_CHECK_INTERVAL,
|
EXPIRED_MUTE_CHECK_INTERVAL,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -123,5 +124,6 @@ export const MutesPlugin = zeppelinGuildPlugin<MutesPluginType>()({
|
||||||
beforeUnload(pluginData) {
|
beforeUnload(pluginData) {
|
||||||
clearInterval(pluginData.state.muteClearIntervalId);
|
clearInterval(pluginData.state.muteClearIntervalId);
|
||||||
pluginData.state.events.removeAllListeners();
|
pluginData.state.events.removeAllListeners();
|
||||||
|
pluginData.state.timers = [];
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,50 +1,102 @@
|
||||||
import { Snowflake } from "discord.js";
|
|
||||||
import { GuildPluginData } from "knub";
|
import { GuildPluginData } from "knub";
|
||||||
import { memberToTemplateSafeMember } from "../../../utils/templateSafeObjects";
|
import { MINUTES, resolveMember, UnknownUser, verboseUserMention } from "../../../utils";
|
||||||
import { LogType } from "../../../data/LogType";
|
|
||||||
import { resolveMember, UnknownUser, verboseUserMention } from "../../../utils";
|
|
||||||
import { memberRolesLock } from "../../../utils/lockNameHelpers";
|
import { memberRolesLock } from "../../../utils/lockNameHelpers";
|
||||||
import { MutesPluginType } from "../types";
|
import { MutesPluginType } from "../types";
|
||||||
import { LogsPlugin } from "../../Logs/LogsPlugin";
|
import { LogsPlugin } from "../../Logs/LogsPlugin";
|
||||||
|
import { ExpiringTimer } from "src/utils/timers";
|
||||||
|
import { Mute } from "src/data/entities/Mute";
|
||||||
|
import moment from "moment";
|
||||||
|
|
||||||
export async function clearExpiredMutes(pluginData: GuildPluginData<MutesPluginType>) {
|
const LOAD_LESS_THAN_MIN_COUNT = 60 * MINUTES;
|
||||||
const expiredMutes = await pluginData.state.mutes.getExpiredMutes();
|
|
||||||
for (const mute of expiredMutes) {
|
|
||||||
const member = await resolveMember(pluginData.client, pluginData.guild, mute.user_id, true);
|
|
||||||
|
|
||||||
if (member) {
|
export function addTimer(pluginData: GuildPluginData<MutesPluginType>, mute: Mute) {
|
||||||
try {
|
const existingMute = pluginData.state.timers.find(
|
||||||
const lock = await pluginData.locks.acquire(memberRolesLock(member));
|
(tm) => tm.options.key === mute.user_id && tm.options.guildId === mute.guild_id && !tm.done,
|
||||||
|
); // for future-proof when you do global events
|
||||||
const muteRole = pluginData.config.get().mute_role;
|
if (!existingMute && mute.expires_at) {
|
||||||
if (muteRole) {
|
const exp = moment(mute.expires_at!).toDate().getTime() - moment.utc().toDate().getTime();
|
||||||
await member.roles.remove(muteRole);
|
const newTimer = new ExpiringTimer({
|
||||||
}
|
key: mute.user_id,
|
||||||
if (mute.roles_to_restore) {
|
guildId: mute.guild_id,
|
||||||
const guildRoles = pluginData.guild.roles.cache;
|
plugin: "mutes",
|
||||||
const newRoles = [...member.roles.cache.keys()].filter((roleId) => roleId !== muteRole);
|
expiry: exp,
|
||||||
for (const toRestore of mute.roles_to_restore) {
|
callback: async () => {
|
||||||
if (guildRoles.has(toRestore) && toRestore !== muteRole && !newRoles.includes(toRestore)) {
|
await clearExpiredMute(pluginData, mute);
|
||||||
newRoles.push(toRestore);
|
},
|
||||||
}
|
|
||||||
}
|
|
||||||
await member.roles.set(newRoles);
|
|
||||||
}
|
|
||||||
|
|
||||||
lock.unlock();
|
|
||||||
} catch {
|
|
||||||
pluginData.getPlugin(LogsPlugin).logBotAlert({
|
|
||||||
body: `Failed to remove mute role from ${verboseUserMention(member.user)}`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await pluginData.state.mutes.clear(mute.user_id);
|
|
||||||
|
|
||||||
pluginData.getPlugin(LogsPlugin).logMemberMuteExpired({
|
|
||||||
member: member || new UnknownUser({ id: mute.user_id }),
|
|
||||||
});
|
});
|
||||||
|
pluginData.state.timers.push(newTimer);
|
||||||
pluginData.state.events.emit("unmute", mute.user_id);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function removeTimer(pluginData: GuildPluginData<MutesPluginType>, mute: Mute) {
|
||||||
|
const existingMute = pluginData.state.timers.findIndex(
|
||||||
|
(tm) => tm.options.key === mute.user_id && tm.options.guildId === mute.guild_id && !tm.done,
|
||||||
|
);
|
||||||
|
if (existingMute) {
|
||||||
|
const tm = pluginData.state.timers[existingMute];
|
||||||
|
tm.clear();
|
||||||
|
tm.done = true;
|
||||||
|
pluginData.state.timers.splice(existingMute, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadExpiringTimers(pluginData: GuildPluginData<MutesPluginType>) {
|
||||||
|
const now = moment.utc().toDate().getTime();
|
||||||
|
pluginData.state.timers = pluginData.state.timers.filter((tm) => !tm.done || !tm.timeout);
|
||||||
|
const mutes = (await pluginData.state.mutes.getAllTemporaryMutes()).filter((m) => m.expires_at);
|
||||||
|
const expiredMutes = mutes.filter((m) => now >= moment(m.expires_at!).toDate().getTime());
|
||||||
|
const expiringMutes = mutes.filter(
|
||||||
|
(m) => !expiredMutes.find((exp) => exp.user_id === m.user_id && exp.guild_id === m.guild_id),
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const mute of expiringMutes) {
|
||||||
|
const expires = moment(mute.expires_at!).toDate().getTime();
|
||||||
|
if (expires <= now) continue; // exclude expired mutes, just in case
|
||||||
|
if (expires > now + LOAD_LESS_THAN_MIN_COUNT) continue; // exclude timers that are expiring in over 180 mins
|
||||||
|
|
||||||
|
addTimer(pluginData, mute);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const mute of expiredMutes) {
|
||||||
|
await clearExpiredMute(pluginData, mute);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function clearExpiredMute(pluginData: GuildPluginData<MutesPluginType>, mute: Mute) {
|
||||||
|
const member = await resolveMember(pluginData.client, pluginData.guild, mute.user_id, true);
|
||||||
|
|
||||||
|
if (member) {
|
||||||
|
try {
|
||||||
|
const lock = await pluginData.locks.acquire(memberRolesLock(member));
|
||||||
|
|
||||||
|
const muteRole = pluginData.config.get().mute_role;
|
||||||
|
if (muteRole) {
|
||||||
|
await member.roles.remove(muteRole);
|
||||||
|
}
|
||||||
|
if (mute.roles_to_restore) {
|
||||||
|
const guildRoles = pluginData.guild.roles.cache;
|
||||||
|
const newRoles = [...member.roles.cache.keys()].filter((roleId) => roleId !== muteRole);
|
||||||
|
for (const toRestore of mute.roles_to_restore) {
|
||||||
|
if (guildRoles.has(toRestore) && toRestore !== muteRole && !newRoles.includes(toRestore)) {
|
||||||
|
newRoles.push(toRestore);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await member.roles.set(newRoles);
|
||||||
|
}
|
||||||
|
|
||||||
|
lock.unlock();
|
||||||
|
} catch {
|
||||||
|
pluginData.getPlugin(LogsPlugin).logBotAlert({
|
||||||
|
body: `Failed to remove mute role from ${verboseUserMention(member.user)}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await pluginData.state.mutes.clear(mute.user_id);
|
||||||
|
|
||||||
|
pluginData.getPlugin(LogsPlugin).logMemberMuteExpired({
|
||||||
|
member: member || new UnknownUser({ id: mute.user_id }),
|
||||||
|
});
|
||||||
|
|
||||||
|
pluginData.state.events.emit("unmute", mute.user_id);
|
||||||
|
}
|
||||||
|
|
|
@ -19,6 +19,8 @@ import {
|
||||||
import { muteLock } from "../../../utils/lockNameHelpers";
|
import { muteLock } from "../../../utils/lockNameHelpers";
|
||||||
import { CasesPlugin } from "../../Cases/CasesPlugin";
|
import { CasesPlugin } from "../../Cases/CasesPlugin";
|
||||||
import { MuteOptions, MutesPluginType } from "../types";
|
import { MuteOptions, MutesPluginType } from "../types";
|
||||||
|
import { addTimer, removeTimer } from "./clearExpiredMutes";
|
||||||
|
import moment from "moment";
|
||||||
|
|
||||||
export async function muteUser(
|
export async function muteUser(
|
||||||
pluginData: GuildPluginData<MutesPluginType>,
|
pluginData: GuildPluginData<MutesPluginType>,
|
||||||
|
@ -139,8 +141,14 @@ export async function muteUser(
|
||||||
rolesToRestore = Array.from(new Set([...existingMute.roles_to_restore, ...rolesToRestore]));
|
rolesToRestore = Array.from(new Set([...existingMute.roles_to_restore, ...rolesToRestore]));
|
||||||
}
|
}
|
||||||
await pluginData.state.mutes.updateExpiryTime(user.id, muteTime, rolesToRestore);
|
await pluginData.state.mutes.updateExpiryTime(user.id, muteTime, rolesToRestore);
|
||||||
|
removeTimer(pluginData, existingMute);
|
||||||
|
addTimer(pluginData, {
|
||||||
|
...existingMute,
|
||||||
|
expires_at: moment().utc().add(muteTime, "ms").format("YYYY-MM-DD HH:mm:ss"),
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
await pluginData.state.mutes.addMute(user.id, muteTime, rolesToRestore);
|
const mute = await pluginData.state.mutes.addMute(user.id, muteTime, rolesToRestore);
|
||||||
|
addTimer(pluginData, mute);
|
||||||
}
|
}
|
||||||
|
|
||||||
const template = existingMute
|
const template = existingMute
|
||||||
|
|
|
@ -11,6 +11,8 @@ import { CaseArgs } from "../../Cases/types";
|
||||||
import { MutesPluginType, UnmuteResult } from "../types";
|
import { MutesPluginType, UnmuteResult } from "../types";
|
||||||
import { memberHasMutedRole } from "./memberHasMutedRole";
|
import { memberHasMutedRole } from "./memberHasMutedRole";
|
||||||
import { LogsPlugin } from "../../Logs/LogsPlugin";
|
import { LogsPlugin } from "../../Logs/LogsPlugin";
|
||||||
|
import { addTimer, removeTimer } from "./clearExpiredMutes";
|
||||||
|
import moment from "moment";
|
||||||
|
|
||||||
export async function unmuteUser(
|
export async function unmuteUser(
|
||||||
pluginData: GuildPluginData<MutesPluginType>,
|
pluginData: GuildPluginData<MutesPluginType>,
|
||||||
|
@ -28,9 +30,15 @@ export async function unmuteUser(
|
||||||
if (unmuteTime) {
|
if (unmuteTime) {
|
||||||
// Schedule timed unmute (= just set the mute's duration)
|
// Schedule timed unmute (= just set the mute's duration)
|
||||||
if (!existingMute) {
|
if (!existingMute) {
|
||||||
await pluginData.state.mutes.addMute(userId, unmuteTime);
|
const mute = await pluginData.state.mutes.addMute(userId, unmuteTime);
|
||||||
|
addTimer(pluginData, mute);
|
||||||
} else {
|
} else {
|
||||||
await pluginData.state.mutes.updateExpiryTime(userId, unmuteTime);
|
await pluginData.state.mutes.updateExpiryTime(userId, unmuteTime);
|
||||||
|
removeTimer(pluginData, existingMute);
|
||||||
|
addTimer(pluginData, {
|
||||||
|
...existingMute,
|
||||||
|
expires_at: moment().utc().add(unmuteTime, "ms").format("YYYY-MM-DD HH:mm:ss"),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Unmute immediately
|
// Unmute immediately
|
||||||
|
@ -60,6 +68,7 @@ export async function unmuteUser(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (existingMute) {
|
if (existingMute) {
|
||||||
|
removeTimer(pluginData, existingMute);
|
||||||
await pluginData.state.mutes.clear(userId);
|
await pluginData.state.mutes.clear(userId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { GuildMember } from "discord.js";
|
||||||
import { EventEmitter } from "events";
|
import { EventEmitter } from "events";
|
||||||
import * as t from "io-ts";
|
import * as t from "io-ts";
|
||||||
import { BasePluginType, typedGuildCommand, typedGuildEventListener } from "knub";
|
import { BasePluginType, typedGuildCommand, typedGuildEventListener } from "knub";
|
||||||
|
import { ExpiringTimer } from "src/utils/timers";
|
||||||
import { Case } from "../../data/entities/Case";
|
import { Case } from "../../data/entities/Case";
|
||||||
import { Mute } from "../../data/entities/Mute";
|
import { Mute } from "../../data/entities/Mute";
|
||||||
import { GuildArchives } from "../../data/GuildArchives";
|
import { GuildArchives } from "../../data/GuildArchives";
|
||||||
|
@ -54,6 +55,8 @@ export interface MutesPluginType extends BasePluginType {
|
||||||
|
|
||||||
muteClearIntervalId: Timeout;
|
muteClearIntervalId: Timeout;
|
||||||
|
|
||||||
|
timers: ExpiringTimer[];
|
||||||
|
|
||||||
events: MutesEventEmitter;
|
events: MutesEventEmitter;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
40
backend/src/utils/timers.ts
Normal file
40
backend/src/utils/timers.ts
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
import { Snowflake } from "discord-api-types";
|
||||||
|
|
||||||
|
type TimerCallback = (key: string, expiry: number) => void;
|
||||||
|
|
||||||
|
type TimerOptions = {
|
||||||
|
key: Snowflake;
|
||||||
|
guildId?: Snowflake;
|
||||||
|
expiry: number;
|
||||||
|
plugin?: string;
|
||||||
|
callback: TimerCallback;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class ExpiringTimer {
|
||||||
|
done: boolean = false;
|
||||||
|
options: TimerOptions;
|
||||||
|
timeout?: NodeJS.Timeout;
|
||||||
|
data?: any; // idk how to make this take generic <T> typings data
|
||||||
|
isValid() {
|
||||||
|
return !this.done;
|
||||||
|
}
|
||||||
|
private execute() {
|
||||||
|
if (!this.isValid()) return;
|
||||||
|
this.options.callback(this.options.key, this.options.expiry);
|
||||||
|
this.done = true;
|
||||||
|
}
|
||||||
|
init() {
|
||||||
|
if (this.timeout) this.clear();
|
||||||
|
this.timeout = setTimeout(() => this.execute(), this.options.expiry);
|
||||||
|
}
|
||||||
|
clear() {
|
||||||
|
if (this.timeout) {
|
||||||
|
clearTimeout(this.timeout);
|
||||||
|
this.timeout = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
constructor(options: TimerOptions) {
|
||||||
|
this.options = options;
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue