diff --git a/backend/src/plugins/Automod/AutomodPlugin.ts b/backend/src/plugins/Automod/AutomodPlugin.ts index fb040b23..9cd380c2 100644 --- a/backend/src/plugins/Automod/AutomodPlugin.ts +++ b/backend/src/plugins/Automod/AutomodPlugin.ts @@ -31,6 +31,9 @@ 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"; +import { unregisterEventListenersFromMap } from "../../utils/unregisterEventListenersFromMap"; const defaultOptions = { config: { @@ -163,7 +166,13 @@ export const AutomodPlugin = zeppelinGuildPlugin()("automod", showInDocs: true, info: pluginInfo, - dependencies: [LogsPlugin, ModActionsPlugin, MutesPlugin, CountersPlugin], + // prettier-ignore + dependencies: [ + LogsPlugin, + ModActionsPlugin, + MutesPlugin, + CountersPlugin, + ], configSchema: ConfigSchema, defaultOptions, @@ -173,6 +182,7 @@ export const AutomodPlugin = zeppelinGuildPlugin()("automod", return criteria?.antiraid_level ? criteria.antiraid_level === pluginData.state.cachedAntiraidLevel : false; }, + // prettier-ignore events: [ RunAutomodOnJoinEvt, RunAutomodOnMemberUpdate, @@ -238,13 +248,45 @@ export const AutomodPlugin = zeppelinGuildPlugin()("automod", countersPlugin.onCounterEvent("trigger", pluginData.state.onCounterTrigger); countersPlugin.onCounterEvent("reverseTrigger", pluginData.state.onCounterReverseTrigger); + + const modActionsEvents = pluginData.getPlugin(ModActionsPlugin).getEventEmitter(); + pluginData.state.modActionsListeners = new Map(); + 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("kick", (userId: string) => + runAutomodOnModAction(pluginData, "kick", userId), + ); + pluginData.state.modActionsListeners.set("ban", (userId: string) => + runAutomodOnModAction(pluginData, "ban", userId), + ); + pluginData.state.modActionsListeners.set("unban", (userId: string) => + runAutomodOnModAction(pluginData, "unban", userId), + ); + registerEventListenersFromMap(modActionsEvents, pluginData.state.modActionsListeners); + + 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("unmute", (userId: string) => + runAutomodOnModAction(pluginData, "unmute", userId), + ); + registerEventListenersFromMap(mutesEvents, pluginData.state.mutesListeners); }, async onBeforeUnload(pluginData) { const countersPlugin = pluginData.getPlugin(CountersPlugin); - countersPlugin.offCounterEvent("trigger", pluginData.state.onCounterTrigger); countersPlugin.offCounterEvent("reverseTrigger", pluginData.state.onCounterReverseTrigger); + + const modActionsEvents = pluginData.getPlugin(ModActionsPlugin).getEventEmitter(); + unregisterEventListenersFromMap(modActionsEvents, pluginData.state.modActionsListeners); + + const mutesEvents = pluginData.getPlugin(MutesPlugin).getEventEmitter(); + unregisterEventListenersFromMap(mutesEvents, pluginData.state.mutesListeners); }, async onUnload(pluginData) { diff --git a/backend/src/plugins/Automod/events/runAutomodOnModAction.ts b/backend/src/plugins/Automod/events/runAutomodOnModAction.ts new file mode 100644 index 00000000..5831f1eb --- /dev/null +++ b/backend/src/plugins/Automod/events/runAutomodOnModAction.ts @@ -0,0 +1,27 @@ +import { GuildPluginData } from "knub"; +import { AutomodContext, AutomodPluginType } from "../types"; +import { runAutomod } from "../functions/runAutomod"; +import { resolveUser, UnknownUser } from "../../../utils"; +import { ModActionType } from "../../ModActions/types"; + +export async function runAutomodOnModAction( + pluginData: GuildPluginData, + modAction: ModActionType, + userId: string, + reason?: string, +) { + const user = await resolveUser(pluginData.client, userId); + + const context: AutomodContext = { + timestamp: Date.now(), + user: user instanceof UnknownUser ? undefined : user, + modAction: { + type: modAction, + reason, + }, + }; + + pluginData.state.queue.add(async () => { + await runAutomod(pluginData, context); + }); +} diff --git a/backend/src/plugins/Automod/triggers/availableTriggers.ts b/backend/src/plugins/Automod/triggers/availableTriggers.ts index 2c3dbd5a..175df6d7 100644 --- a/backend/src/plugins/Automod/triggers/availableTriggers.ts +++ b/backend/src/plugins/Automod/triggers/availableTriggers.ts @@ -18,6 +18,13 @@ import { RoleAddedTrigger } from "./roleAdded"; import { RoleRemovedTrigger } from "./roleRemoved"; import { StickerSpamTrigger } from "./stickerSpam"; import { CounterTrigger } from "./counter"; +import { NoteTrigger } from "./note"; +import { WarnTrigger } from "./warn"; +import { MuteTrigger } from "./mute"; +import { UnmuteTrigger } from "./unmute"; +import { KickTrigger } from "./kick"; +import { BanTrigger } from "./ban"; +import { UnbanTrigger } from "./unban"; export const availableTriggers: Record> = { match_words: MatchWordsTrigger, @@ -40,6 +47,14 @@ export const availableTriggers: Record sticker_spam: StickerSpamTrigger, counter: CounterTrigger, + + note: NoteTrigger, + warn: WarnTrigger, + mute: MuteTrigger, + unmute: UnmuteTrigger, + kick: KickTrigger, + ban: BanTrigger, + unban: UnbanTrigger, }; export const AvailableTriggers = t.type({ @@ -63,4 +78,12 @@ export const AvailableTriggers = t.type({ sticker_spam: StickerSpamTrigger.configType, counter: CounterTrigger.configType, + + note: NoteTrigger.configType, + warn: WarnTrigger.configType, + mute: MuteTrigger.configType, + unmute: UnmuteTrigger.configType, + kick: KickTrigger.configType, + ban: BanTrigger.configType, + unban: UnbanTrigger.configType, }); diff --git a/backend/src/plugins/Automod/triggers/ban.ts b/backend/src/plugins/Automod/triggers/ban.ts new file mode 100644 index 00000000..64559e74 --- /dev/null +++ b/backend/src/plugins/Automod/triggers/ban.ts @@ -0,0 +1,24 @@ +import * as t from "io-ts"; +import { automodTrigger } from "../helpers"; + +// tslint:disable-next-line:no-empty-interface +interface BanTriggerResultType {} + +export const BanTrigger = automodTrigger()({ + configType: t.type({}), + defaultConfig: {}, + + async match({ context }) { + if (context.modAction?.type !== "ban") { + return; + } + + return { + extra: {}, + }; + }, + + renderMatchInformation({ matchResult }) { + return `User was banned`; + }, +}); diff --git a/backend/src/plugins/Automod/triggers/kick.ts b/backend/src/plugins/Automod/triggers/kick.ts new file mode 100644 index 00000000..284a867d --- /dev/null +++ b/backend/src/plugins/Automod/triggers/kick.ts @@ -0,0 +1,24 @@ +import * as t from "io-ts"; +import { automodTrigger } from "../helpers"; + +// tslint:disable-next-line:no-empty-interface +interface KickTriggerResultType {} + +export const KickTrigger = automodTrigger()({ + configType: t.type({}), + defaultConfig: {}, + + async match({ context }) { + if (context.modAction?.type !== "kick") { + return; + } + + return { + extra: {}, + }; + }, + + renderMatchInformation({ matchResult }) { + return `User was kicked`; + }, +}); diff --git a/backend/src/plugins/Automod/triggers/mute.ts b/backend/src/plugins/Automod/triggers/mute.ts new file mode 100644 index 00000000..94d14437 --- /dev/null +++ b/backend/src/plugins/Automod/triggers/mute.ts @@ -0,0 +1,24 @@ +import * as t from "io-ts"; +import { automodTrigger } from "../helpers"; + +// tslint:disable-next-line:no-empty-interface +interface MuteTriggerResultType {} + +export const MuteTrigger = automodTrigger()({ + configType: t.type({}), + defaultConfig: {}, + + async match({ context }) { + if (context.modAction?.type !== "mute") { + return; + } + + return { + extra: {}, + }; + }, + + renderMatchInformation({ matchResult }) { + return `User was muted`; + }, +}); diff --git a/backend/src/plugins/Automod/triggers/note.ts b/backend/src/plugins/Automod/triggers/note.ts new file mode 100644 index 00000000..14ef0abc --- /dev/null +++ b/backend/src/plugins/Automod/triggers/note.ts @@ -0,0 +1,24 @@ +import * as t from "io-ts"; +import { automodTrigger } from "../helpers"; + +// tslint:disable-next-line:no-empty-interface +interface NoteTriggerResultType {} + +export const NoteTrigger = automodTrigger()({ + configType: t.type({}), + defaultConfig: {}, + + async match({ context }) { + if (context.modAction?.type !== "note") { + return; + } + + return { + extra: {}, + }; + }, + + renderMatchInformation({ matchResult }) { + return `Note was added on user`; + }, +}); diff --git a/backend/src/plugins/Automod/triggers/unban.ts b/backend/src/plugins/Automod/triggers/unban.ts new file mode 100644 index 00000000..38e86db9 --- /dev/null +++ b/backend/src/plugins/Automod/triggers/unban.ts @@ -0,0 +1,24 @@ +import * as t from "io-ts"; +import { automodTrigger } from "../helpers"; + +// tslint:disable-next-line:no-empty-interface +interface UnbanTriggerResultType {} + +export const UnbanTrigger = automodTrigger()({ + configType: t.type({}), + defaultConfig: {}, + + async match({ context }) { + if (context.modAction?.type !== "unban") { + return; + } + + return { + extra: {}, + }; + }, + + renderMatchInformation({ matchResult }) { + return `User was unbanned`; + }, +}); diff --git a/backend/src/plugins/Automod/triggers/unmute.ts b/backend/src/plugins/Automod/triggers/unmute.ts new file mode 100644 index 00000000..9ccb5031 --- /dev/null +++ b/backend/src/plugins/Automod/triggers/unmute.ts @@ -0,0 +1,24 @@ +import * as t from "io-ts"; +import { automodTrigger } from "../helpers"; + +// tslint:disable-next-line:no-empty-interface +interface UnmuteTriggerResultType {} + +export const UnmuteTrigger = automodTrigger()({ + configType: t.type({}), + defaultConfig: {}, + + async match({ context }) { + if (context.modAction?.type !== "unmute") { + return; + } + + return { + extra: {}, + }; + }, + + renderMatchInformation({ matchResult }) { + return `User was unmuted`; + }, +}); diff --git a/backend/src/plugins/Automod/triggers/warn.ts b/backend/src/plugins/Automod/triggers/warn.ts new file mode 100644 index 00000000..711f5cd7 --- /dev/null +++ b/backend/src/plugins/Automod/triggers/warn.ts @@ -0,0 +1,24 @@ +import * as t from "io-ts"; +import { automodTrigger } from "../helpers"; + +// tslint:disable-next-line:no-empty-interface +interface WarnTriggerResultType {} + +export const WarnTrigger = automodTrigger()({ + configType: t.type({}), + defaultConfig: {}, + + async match({ context }) { + if (context.modAction?.type !== "warn") { + return; + } + + return { + extra: {}, + }; + }, + + renderMatchInformation({ matchResult }) { + return `User was warned`; + }, +}); diff --git a/backend/src/plugins/Automod/types.ts b/backend/src/plugins/Automod/types.ts index 39be0f6a..f6e273cb 100644 --- a/backend/src/plugins/Automod/types.ts +++ b/backend/src/plugins/Automod/types.ts @@ -14,6 +14,8 @@ import { RecentActionType } from "./constants"; import Timeout = NodeJS.Timeout; import { RegExpRunner } from "../../RegExpRunner"; import { CounterEvents } from "../Counters/types"; +import { ModActionsEvents, ModActionType } from "../ModActions/types"; +import { MutesEvents } from "../Mutes/types"; export const Rule = t.type({ enabled: t.boolean, @@ -90,6 +92,9 @@ export interface AutomodPluginType extends BasePluginType { onCounterTrigger: CounterEvents["trigger"]; onCounterReverseTrigger: CounterEvents["reverseTrigger"]; + + modActionsListeners: Map; + mutesListeners: Map; }; } @@ -112,6 +117,10 @@ export interface AutomodContext { added?: string[]; removed?: string[]; }; + modAction?: { + type: ModActionType; + reason?: string; + }; } export interface RecentAction { diff --git a/backend/src/plugins/Counters/types.ts b/backend/src/plugins/Counters/types.ts index c4ed03f6..3102e463 100644 --- a/backend/src/plugins/Counters/types.ts +++ b/backend/src/plugins/Counters/types.ts @@ -36,7 +36,6 @@ export interface CounterEvents { export interface CounterEventEmitter extends EventEmitter { on(event: U, listener: CounterEvents[U]): this; - emit(event: U, ...args: Parameters): boolean; } diff --git a/backend/src/plugins/ModActions/ModActionsPlugin.ts b/backend/src/plugins/ModActions/ModActionsPlugin.ts index 993de951..672e4630 100644 --- a/backend/src/plugins/ModActions/ModActionsPlugin.ts +++ b/backend/src/plugins/ModActions/ModActionsPlugin.ts @@ -39,6 +39,10 @@ import { DeleteCaseCmd } from "./commands/DeleteCaseCmd"; import { TimeAndDatePlugin } from "../TimeAndDate/TimeAndDatePlugin"; import { GuildTempbans } from "../../data/GuildTempbans"; import { outdatedTempbansLoop } from "./functions/outdatedTempbansLoop"; +import { EventEmitter } from "events"; +import { mapToPublicFn } from "../../pluginUtils"; +import { onModActionsEvent } from "./functions/onModActionsEvent"; +import { offModActionsEvent } from "./functions/offModActionsEvent"; const defaultOptions = { config: { @@ -165,6 +169,12 @@ export const ModActionsPlugin = zeppelinGuildPlugin()("mod banUserId(pluginData, userId, reason, banOptions); }; }, + + on: mapToPublicFn(onModActionsEvent), + off: mapToPublicFn(offModActionsEvent), + getEventEmitter(pluginData) { + return () => pluginData.state.events; + }, }, onLoad(pluginData) { @@ -179,10 +189,13 @@ export const ModActionsPlugin = zeppelinGuildPlugin()("mod state.outdatedTempbansTimeout = null; state.ignoredEvents = []; + state.events = new EventEmitter(); + outdatedTempbansLoop(pluginData); }, onUnload(pluginData) { pluginData.state.unloaded = true; + pluginData.state.events.removeAllListeners(); }, }); diff --git a/backend/src/plugins/ModActions/commands/ForcebanCmd.ts b/backend/src/plugins/ModActions/commands/ForcebanCmd.ts index d586cbf8..907237ef 100644 --- a/backend/src/plugins/ModActions/commands/ForcebanCmd.ts +++ b/backend/src/plugins/ModActions/commands/ForcebanCmd.ts @@ -67,6 +67,7 @@ export const ForcebanCmd = modActionsCmd({ pluginData.state.serverLogs.ignoreLog(LogType.MEMBER_BAN, user.id); try { + // FIXME: Use banUserId()? await pluginData.guild.banMember(user.id, 1, reason != null ? encodeURIComponent(reason) : undefined); } catch (e) { sendErrorMessage(pluginData, msg.channel, "Failed to forceban member"); @@ -93,5 +94,7 @@ export const ForcebanCmd = modActionsCmd({ caseNumber: createdCase.case_number, reason, }); + + pluginData.state.events.emit("ban", user.id, reason); }, }); diff --git a/backend/src/plugins/ModActions/commands/MassBanCmd.ts b/backend/src/plugins/ModActions/commands/MassBanCmd.ts index c4c1860f..bd79f7f3 100644 --- a/backend/src/plugins/ModActions/commands/MassBanCmd.ts +++ b/backend/src/plugins/ModActions/commands/MassBanCmd.ts @@ -75,6 +75,8 @@ export const MassbanCmd = modActionsCmd({ reason: `Mass ban: ${banReason}`, postInCaseLogOverride: false, }); + + pluginData.state.events.emit("ban", userId, banReason); } catch (e) { failedBans.push(userId); } diff --git a/backend/src/plugins/ModActions/commands/NoteCmd.ts b/backend/src/plugins/ModActions/commands/NoteCmd.ts index b0dd89f7..af6a1298 100644 --- a/backend/src/plugins/ModActions/commands/NoteCmd.ts +++ b/backend/src/plugins/ModActions/commands/NoteCmd.ts @@ -49,5 +49,7 @@ export const NoteCmd = modActionsCmd({ }); sendSuccessMessage(pluginData, msg.channel, `Note added on **${userName}** (Case #${createdCase.case_number})`); + + pluginData.state.events.emit("note", user.id, reason); }, }); diff --git a/backend/src/plugins/ModActions/commands/WarnCmd.ts b/backend/src/plugins/ModActions/commands/WarnCmd.ts index 617b0f05..8ac19baa 100644 --- a/backend/src/plugins/ModActions/commands/WarnCmd.ts +++ b/backend/src/plugins/ModActions/commands/WarnCmd.ts @@ -112,5 +112,7 @@ 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); }, }); diff --git a/backend/src/plugins/ModActions/events/CreateBanCaseOnManualBanEvt.ts b/backend/src/plugins/ModActions/events/CreateBanCaseOnManualBanEvt.ts index 931b7985..ca3faed8 100644 --- a/backend/src/plugins/ModActions/events/CreateBanCaseOnManualBanEvt.ts +++ b/backend/src/plugins/ModActions/events/CreateBanCaseOnManualBanEvt.ts @@ -69,5 +69,7 @@ export const CreateBanCaseOnManualBanEvt = modActionsEvt( caseNumber: createdCase?.case_number ?? 0, reason, }); + + pluginData.state.events.emit("ban", user.id, reason); }, ); diff --git a/backend/src/plugins/ModActions/events/CreateKickCaseOnManualKickEvt.ts b/backend/src/plugins/ModActions/events/CreateKickCaseOnManualKickEvt.ts index a88ba02d..dd353ac9 100644 --- a/backend/src/plugins/ModActions/events/CreateKickCaseOnManualKickEvt.ts +++ b/backend/src/plugins/ModActions/events/CreateKickCaseOnManualKickEvt.ts @@ -62,6 +62,8 @@ export const CreateKickCaseOnManualKickEvt = modActionsEvt( mod: mod ? stripObjectToScalars(mod) : null, caseNumber: createdCase?.case_number ?? 0, }); + + pluginData.state.events.emit("kick", member.id, kickAuditLogEntry.reason || undefined); } }, ); diff --git a/backend/src/plugins/ModActions/events/CreateUnbanCaseOnManualUnbanEvt.ts b/backend/src/plugins/ModActions/events/CreateUnbanCaseOnManualUnbanEvt.ts index e41e87a5..d7bb1c88 100644 --- a/backend/src/plugins/ModActions/events/CreateUnbanCaseOnManualUnbanEvt.ts +++ b/backend/src/plugins/ModActions/events/CreateUnbanCaseOnManualUnbanEvt.ts @@ -66,5 +66,7 @@ export const CreateUnbanCaseOnManualUnbanEvt = modActionsEvt( userId: user.id, caseNumber: createdCase?.case_number ?? 0, }); + + pluginData.state.events.emit("unban", user.id); }, ); diff --git a/backend/src/plugins/ModActions/functions/banUserId.ts b/backend/src/plugins/ModActions/functions/banUserId.ts index ad7025e7..85d05de9 100644 --- a/backend/src/plugins/ModActions/functions/banUserId.ts +++ b/backend/src/plugins/ModActions/functions/banUserId.ts @@ -127,6 +127,8 @@ export async function banUserId( banTime: banTime ? humanizeDuration(banTime) : null, }); + pluginData.state.events.emit("ban", user.id, reason); + return { status: "success", case: createdCase, diff --git a/backend/src/plugins/ModActions/functions/kickMember.ts b/backend/src/plugins/ModActions/functions/kickMember.ts index 16ac2ecc..9e297af8 100644 --- a/backend/src/plugins/ModActions/functions/kickMember.ts +++ b/backend/src/plugins/ModActions/functions/kickMember.ts @@ -85,6 +85,8 @@ export async function kickMember( reason, }); + pluginData.state.events.emit("kick", member.id, reason); + return { status: "success", case: createdCase, diff --git a/backend/src/plugins/ModActions/functions/offModActionsEvent.ts b/backend/src/plugins/ModActions/functions/offModActionsEvent.ts new file mode 100644 index 00000000..798fa733 --- /dev/null +++ b/backend/src/plugins/ModActions/functions/offModActionsEvent.ts @@ -0,0 +1,10 @@ +import { GuildPluginData } from "knub"; +import { ModActionsEvents, ModActionsPluginType } from "../types"; + +export function offModActionsEvent( + pluginData: GuildPluginData, + event: TEvent, + listener: ModActionsEvents[TEvent], +) { + return pluginData.state.events.off(event, listener); +} diff --git a/backend/src/plugins/ModActions/functions/onModActionsEvent.ts b/backend/src/plugins/ModActions/functions/onModActionsEvent.ts new file mode 100644 index 00000000..e1219bad --- /dev/null +++ b/backend/src/plugins/ModActions/functions/onModActionsEvent.ts @@ -0,0 +1,10 @@ +import { GuildPluginData } from "knub"; +import { ModActionsEvents, ModActionsPluginType } from "../types"; + +export function onModActionsEvent( + pluginData: GuildPluginData, + event: TEvent, + listener: ModActionsEvents[TEvent], +) { + return pluginData.state.events.on(event, listener); +} diff --git a/backend/src/plugins/ModActions/types.ts b/backend/src/plugins/ModActions/types.ts index cf492480..af4fd8d6 100644 --- a/backend/src/plugins/ModActions/types.ts +++ b/backend/src/plugins/ModActions/types.ts @@ -9,6 +9,7 @@ import { CaseArgs } from "../Cases/types"; import { TextChannel } from "eris"; import { GuildTempbans } from "../../data/GuildTempbans"; import Timeout = NodeJS.Timeout; +import { EventEmitter } from "events"; export const ConfigSchema = t.type({ dm_on_warn: t.boolean, @@ -45,6 +46,20 @@ export const ConfigSchema = t.type({ }); export type TConfigSchema = t.TypeOf; +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; + unban: (userId: string, reason?: string) => void; + // mute/unmute are in the Mutes plugin +} + +export interface ModActionsEventEmitter extends EventEmitter { + on(event: U, listener: ModActionsEvents[U]): this; + emit(event: U, ...args: Parameters): boolean; +} + export interface ModActionsPluginType extends BasePluginType { config: TConfigSchema; state: { @@ -56,6 +71,8 @@ export interface ModActionsPluginType extends BasePluginType { unloaded: boolean; outdatedTempbansTimeout: Timeout | null; ignoredEvents: IIgnoredEvent[]; + + events: ModActionsEventEmitter; }; } @@ -122,5 +139,7 @@ export interface BanOptions { deleteMessageDays?: number; } +export type ModActionType = "note" | "warn" | "mute" | "unmute" | "kick" | "ban" | "unban"; + export const modActionsCmd = guildCommand(); export const modActionsEvt = guildEventListener(); diff --git a/backend/src/plugins/Mutes/MutesPlugin.ts b/backend/src/plugins/Mutes/MutesPlugin.ts index 2c2e2f74..ac55acc6 100644 --- a/backend/src/plugins/Mutes/MutesPlugin.ts +++ b/backend/src/plugins/Mutes/MutesPlugin.ts @@ -17,6 +17,9 @@ import { Member } from "eris"; import { ClearActiveMuteOnMemberBanEvt } from "./events/ClearActiveMuteOnMemberBanEvt"; import { ReapplyActiveMuteOnJoinEvt } from "./events/ReapplyActiveMuteOnJoinEvt"; import { mapToPublicFn } from "../../pluginUtils"; +import { EventEmitter } from "events"; +import { onMutesEvent } from "./functions/onMutesEvent"; +import { offMutesEvent } from "./functions/offMutesEvent"; const defaultOptions = { config: { @@ -92,6 +95,12 @@ export const MutesPlugin = zeppelinGuildPlugin()("mutes", { return muteRole ? member.roles.includes(muteRole) : false; }; }, + + on: mapToPublicFn(onMutesEvent), + off: mapToPublicFn(offMutesEvent), + getEventEmitter(pluginData) { + return () => pluginData.state.events; + }, }, onLoad(pluginData) { @@ -100,6 +109,8 @@ export const MutesPlugin = zeppelinGuildPlugin()("mutes", { pluginData.state.serverLogs = new GuildLogs(pluginData.guild.id); pluginData.state.archives = GuildArchives.getGuildInstance(pluginData.guild.id); + pluginData.state.events = new EventEmitter(); + // Check for expired mutes every 5s const firstCheckTime = Math.max(Date.now(), FIRST_CHECK_TIME) + FIRST_CHECK_INCREMENT; FIRST_CHECK_TIME = firstCheckTime; @@ -115,5 +126,6 @@ export const MutesPlugin = zeppelinGuildPlugin()("mutes", { onUnload(pluginData) { clearInterval(pluginData.state.muteClearIntervalId); + pluginData.state.events.removeAllListeners(); }, }); diff --git a/backend/src/plugins/Mutes/functions/clearExpiredMutes.ts b/backend/src/plugins/Mutes/functions/clearExpiredMutes.ts index 882afae2..e99f2b73 100644 --- a/backend/src/plugins/Mutes/functions/clearExpiredMutes.ts +++ b/backend/src/plugins/Mutes/functions/clearExpiredMutes.ts @@ -38,5 +38,7 @@ export async function clearExpiredMutes(pluginData: GuildPluginData( + pluginData: GuildPluginData, + event: TEvent, + listener: MutesEvents[TEvent], +) { + return pluginData.state.events.off(event, listener); +} diff --git a/backend/src/plugins/Mutes/functions/onMutesEvent.ts b/backend/src/plugins/Mutes/functions/onMutesEvent.ts new file mode 100644 index 00000000..71f13ba4 --- /dev/null +++ b/backend/src/plugins/Mutes/functions/onMutesEvent.ts @@ -0,0 +1,10 @@ +import { GuildPluginData } from "knub"; +import { MutesEvents, MutesPluginType } from "../types"; + +export function onMutesEvent( + pluginData: GuildPluginData, + event: TEvent, + listener: MutesEvents[TEvent], +) { + return pluginData.state.events.on(event, listener); +} diff --git a/backend/src/plugins/Mutes/functions/unmuteUser.ts b/backend/src/plugins/Mutes/functions/unmuteUser.ts index ddae6cfd..dce5cc06 100644 --- a/backend/src/plugins/Mutes/functions/unmuteUser.ts +++ b/backend/src/plugins/Mutes/functions/unmuteUser.ts @@ -45,6 +45,7 @@ export async function unmuteUser( member.edit(memberOptions); } } else { + // tslint:disable-next-line:no-console console.warn( `Member ${userId} not found in guild ${pluginData.guild.name} (${pluginData.guild.id}) when attempting to unmute`, ); @@ -95,6 +96,12 @@ export async function unmuteUser( }); } + if (!unmuteTime) { + // If the member was unmuted, not just scheduled to be unmuted, fire the unmute event as well + // Scheduled unmutes have their event fired in clearExpiredMutes() + pluginData.state.events.emit("unmute", user.id, caseArgs.reason); + } + return { case: createdCase, }; diff --git a/backend/src/plugins/Mutes/types.ts b/backend/src/plugins/Mutes/types.ts index a18f1246..72ea52c2 100644 --- a/backend/src/plugins/Mutes/types.ts +++ b/backend/src/plugins/Mutes/types.ts @@ -10,6 +10,7 @@ import { GuildArchives } from "../../data/GuildArchives"; import { GuildMutes } from "../../data/GuildMutes"; import { CaseArgs } from "../Cases/types"; import Timeout = NodeJS.Timeout; +import { EventEmitter } from "events"; export const ConfigSchema = t.type({ mute_role: tNullable(t.string), @@ -31,6 +32,16 @@ export const ConfigSchema = t.type({ }); export type TConfigSchema = t.TypeOf; +export interface MutesEvents { + mute: (userId: string, reason?: string) => void; + unmute: (userId: string, reason?: string) => void; +} + +export interface MutesEventEmitter extends EventEmitter { + on(event: U, listener: MutesEvents[U]): this; + emit(event: U, ...args: Parameters): boolean; +} + export interface MutesPluginType extends BasePluginType { config: TConfigSchema; state: { @@ -40,6 +51,8 @@ export interface MutesPluginType extends BasePluginType { archives: GuildArchives; muteClearIntervalId: Timeout; + + events: MutesEventEmitter; }; } diff --git a/backend/src/utils/registerEventListenersFromMap.ts b/backend/src/utils/registerEventListenersFromMap.ts new file mode 100644 index 00000000..d2a4cf8c --- /dev/null +++ b/backend/src/utils/registerEventListenersFromMap.ts @@ -0,0 +1,7 @@ +import { EventEmitter } from "events"; + +export function registerEventListenersFromMap(eventEmitter: EventEmitter, map: Map) { + for (const [event, listener] of map.entries()) { + eventEmitter.on(event, listener); + } +} diff --git a/backend/src/utils/unregisterEventListenersFromMap.ts b/backend/src/utils/unregisterEventListenersFromMap.ts new file mode 100644 index 00000000..3aacb165 --- /dev/null +++ b/backend/src/utils/unregisterEventListenersFromMap.ts @@ -0,0 +1,7 @@ +import { EventEmitter } from "events"; + +export function unregisterEventListenersFromMap(eventEmitter: EventEmitter, map: Map) { + for (const [event, listener] of map.entries()) { + eventEmitter.off(event, listener); + } +}