Use actions/events for plugin interoperability. Move base case and mute functionality to their own plugins.

This commit is contained in:
Dragory 2018-11-25 17:04:26 +02:00
parent 22c515be38
commit 2e30a3b9e7
14 changed files with 674 additions and 332 deletions

17
src/PluginRuntimeError.ts Normal file
View file

@ -0,0 +1,17 @@
import util from "util";
export class PluginRuntimeError {
public message: string;
public pluginName: string;
public guildId: string;
constructor(message: string, pluginName: string, guildId: string) {
this.message = message;
this.pluginName = pluginName;
this.guildId = guildId;
}
[util.inspect.custom](depth, options) {
return `PRE [${this.pluginName}] [${this.guildId}] ${this.message}`;
}
}

View file

@ -28,11 +28,14 @@ export class QueuedEventEmitter {
listeners.splice(listeners.indexOf(listener), 1); listeners.splice(listeners.indexOf(listener), 1);
} }
emit(eventName: string, args: any[] = []) { emit(eventName: string, args: any[] = []): Promise<void> {
const listeners = [...(this.listeners.get(eventName) || []), ...(this.listeners.get("*") || [])]; const listeners = [...(this.listeners.get(eventName) || []), ...(this.listeners.get("*") || [])];
let promise: Promise<any> = Promise.resolve();
listeners.forEach(listener => { listeners.forEach(listener => {
this.queue.add(listener.bind(null, ...args)); promise = this.queue.add(listener.bind(null, ...args));
}); });
return promise;
} }
} }

24
src/data/GuildActions.ts Normal file
View file

@ -0,0 +1,24 @@
import { BaseRepository } from "./BaseRepository";
type ActionFn = (...args: any[]) => any | Promise<any>;
export class GuildActions extends BaseRepository {
private actions: Map<string, ActionFn>;
constructor(guildId) {
super(guildId);
this.actions = new Map();
}
public register(actionName: string, actionFn: ActionFn) {
this.actions.set(actionName, actionFn);
}
public unregister(actionName: string) {
this.actions.delete(actionName);
}
public fire(actionName: string, ...args: any[]): Promise<any> {
return this.actions.has(actionName) ? this.actions.get(actionName)(...args) : null;
}
}

View file

@ -53,26 +53,24 @@ export class GuildCases extends BaseRepository {
}); });
} }
async create(data): Promise<number> { async create(data): Promise<Case> {
const result = await this.cases.insert({ const result = await this.cases.insert({
...data, ...data,
guild_id: this.guildId, guild_id: this.guildId,
case_number: () => `(SELECT IFNULL(MAX(case_number)+1, 1) FROM cases AS ma2 WHERE guild_id = ${this.guildId})` case_number: () => `(SELECT IFNULL(MAX(case_number)+1, 1) FROM cases AS ma2 WHERE guild_id = ${this.guildId})`
}); });
return result.identifiers[0].id; return this.find(result.identifiers[0].id);
} }
update(id, data) { update(id, data) {
return this.cases.update(id, data); return this.cases.update(id, data);
} }
async createNote(caseId: number, data: any): Promise<number> { async createNote(caseId: number, data: any): Promise<void> {
const result = await this.caseNotes.insert({ await this.caseNotes.insert({
...data, ...data,
case_id: caseId case_id: caseId
}); });
return result.identifiers[0].id;
} }
} }

42
src/data/GuildEvents.ts Normal file
View file

@ -0,0 +1,42 @@
import { BaseRepository } from "./BaseRepository";
import { QueuedEventEmitter } from "../QueuedEventEmitter";
export class GuildEvents extends BaseRepository {
private queuedEventEmitter: QueuedEventEmitter;
private pluginListeners: Map<string, Map<string, any[]>>;
constructor(guildId) {
super(guildId);
this.queuedEventEmitter = new QueuedEventEmitter();
}
public on(pluginName: string, eventName: string, fn) {
this.queuedEventEmitter.on(eventName, fn);
if (!this.pluginListeners.has(pluginName)) {
this.pluginListeners.set(pluginName, new Map());
}
const pluginListeners = this.pluginListeners.get(pluginName);
if (!pluginListeners.has(eventName)) {
pluginListeners.set(eventName, []);
}
const pluginEventListeners = pluginListeners.get(eventName);
pluginEventListeners.push(fn);
}
public offPlugin(pluginName: string) {
const pluginListeners = this.pluginListeners.get(pluginName) || new Map();
for (const [eventName, listeners] of Array.from(pluginListeners.entries())) {
for (const listener of listeners) {
this.queuedEventEmitter.off(eventName, listener);
}
}
this.pluginListeners.delete(pluginName);
}
public emit(eventName: string, args: any[] = []) {
return this.queuedEventEmitter.emit(eventName, args);
}
}

View file

@ -29,18 +29,20 @@ export class GuildMutes extends BaseRepository {
}); });
} }
async addMute(userId, expiryTime) { async addMute(userId, expiryTime): Promise<Mute> {
const expiresAt = expiryTime const expiresAt = expiryTime
? moment() ? moment()
.add(expiryTime, "ms") .add(expiryTime, "ms")
.format("YYYY-MM-DD HH:mm:ss") .format("YYYY-MM-DD HH:mm:ss")
: null; : null;
return this.mutes.insert({ const result = await this.mutes.insert({
guild_id: this.guildId, guild_id: this.guildId,
user_id: userId, user_id: userId,
expires_at: expiresAt expires_at: expiresAt
}); });
return this.mutes.findOne(result.identifiers[0].id);
} }
async updateExpiryTime(userId, newExpiryTime) { async updateExpiryTime(userId, newExpiryTime) {
@ -61,11 +63,12 @@ export class GuildMutes extends BaseRepository {
); );
} }
async addOrUpdateMute(userId, expiryTime) { async addOrUpdateMute(userId, expiryTime): Promise<Mute> {
const existingMute = await this.findExistingMuteForUserId(userId); const existingMute = await this.findExistingMuteForUserId(userId);
if (existingMute) { if (existingMute) {
return this.updateExpiryTime(userId, expiryTime); await this.updateExpiryTime(userId, expiryTime);
return this.findExistingMuteForUserId(userId);
} else { } else {
return this.addMute(userId, expiryTime); return this.addMute(userId, expiryTime);
} }
@ -83,7 +86,7 @@ export class GuildMutes extends BaseRepository {
.getMany(); .getMany();
} }
async setCaseId(userId, caseId) { async setCaseId(userId: string, caseId: number) {
await this.mutes.update( await this.mutes.update(
{ {
guild_id: this.guildId, guild_id: this.guildId,

View file

@ -45,6 +45,8 @@ import { PersistPlugin } from "./plugins/Persist";
import { SpamPlugin } from "./plugins/Spam"; import { SpamPlugin } from "./plugins/Spam";
import { TagsPlugin } from "./plugins/Tags"; import { TagsPlugin } from "./plugins/Tags";
import { MessageSaverPlugin } from "./plugins/MessageSaver"; import { MessageSaverPlugin } from "./plugins/MessageSaver";
import { CasesPlugin } from "./plugins/Cases";
import { MutesPlugin } from "./plugins/Mutes";
// Run latest database migrations // Run latest database migrations
logger.info("Running database migrations"); logger.info("Running database migrations");
@ -56,10 +58,16 @@ connect().then(async conn => {
}); });
client.setMaxListeners(100); client.setMaxListeners(100);
const basePlugins = ["message_saver", "cases", "mutes"];
const bot = new Knub(client, { const bot = new Knub(client, {
plugins: { plugins: {
messageSaver: MessageSaverPlugin, // Base plugins (always enabled)
message_saver: MessageSaverPlugin,
cases: CasesPlugin,
mutes: MutesPlugin,
// Regular plugins
utility: UtilityPlugin, utility: UtilityPlugin,
mod_actions: ModActionsPlugin, mod_actions: ModActionsPlugin,
logs: LogsPlugin, logs: LogsPlugin,
@ -80,7 +88,7 @@ connect().then(async conn => {
const plugins = guildConfig.plugins || {}; const plugins = guildConfig.plugins || {};
const keys: string[] = Array.from(this.plugins.keys()); const keys: string[] = Array.from(this.plugins.keys());
return keys.filter(pluginName => { return keys.filter(pluginName => {
return (plugins[pluginName] && plugins[pluginName].enabled !== false) || pluginName === "messageSaver"; return basePlugins.includes(pluginName) || (plugins[pluginName] && plugins[pluginName].enabled !== false);
}); });
}, },

217
src/plugins/Cases.ts Normal file
View file

@ -0,0 +1,217 @@
import { Message, MessageContent, MessageFile, TextableChannel, TextChannel } from "eris";
import { GuildCases } from "../data/GuildCases";
import { CaseTypes } from "../data/CaseTypes";
import { Case } from "../data/entities/Case";
import moment from "moment-timezone";
import { CaseTypeColors } from "../data/CaseTypeColors";
import { ZeppelinPlugin } from "./ZeppelinPlugin";
import { GuildActions } from "../data/GuildActions";
export class CasesPlugin extends ZeppelinPlugin {
protected actions: GuildActions;
protected cases: GuildCases;
getDefaultOptions() {
return {
config: {
log_automatic_actions: true,
case_log_channel: null
}
};
}
onLoad() {
this.actions = GuildActions.getInstance(this.guildId);
this.cases = GuildCases.getInstance(this.guildId);
this.actions.register("createCase", args => {
return this.createCase(
args.userId,
args.modId,
args.type,
args.auditLogId,
args.reason,
args.automatic,
args.postInCaseLog
);
});
this.actions.register("createCaseNote", args => {
return this.createCaseNote(args.case || args.caseId, args.modId, args.note, args.automatic, args.postInCaseLog);
});
this.actions.register("postCase", async args => {
const embed = await this.getCaseEmbed(args.case || args.caseId);
return (args.channel as TextableChannel).createMessage(embed);
});
}
onUnload() {
this.actions.unregister("createCase");
this.actions.unregister("createCaseNote");
this.actions.unregister("postCase");
}
protected async resolveCase(caseOrCaseId: Case | number): Promise<Case> {
return caseOrCaseId instanceof Case ? caseOrCaseId : this.cases.with("notes").find(caseOrCaseId);
}
/**
* Creates a new case and, depending on config, posts it in the case log channel
* @return {Number} The ID of the created case
*/
public async createCase(
userId: string,
modId: string,
type: CaseTypes,
auditLogId: string = null,
reason: string = null,
automatic = false,
postInCaseLogOverride = null
): Promise<Case> {
const user = this.bot.users.get(userId);
const userName = user ? `${user.username}#${user.discriminator}` : "Unknown#0000";
const mod = this.bot.users.get(modId);
const modName = mod ? `${mod.username}#${mod.discriminator}` : "Unknown#0000";
const createdCase = await this.cases.create({
type,
user_id: userId,
user_name: userName,
mod_id: modId,
mod_name: modName,
audit_log_id: auditLogId
});
if (reason) {
await this.createCaseNote(createdCase, modId, reason);
}
if (
this.configValue("case_log_channel") &&
(!automatic || this.configValue("log_automatic_actions")) &&
postInCaseLogOverride !== false
) {
try {
await this.postCaseToCaseLogChannel(createdCase);
} catch (e) {} // tslint:disable-line
}
return createdCase;
}
/**
* Adds a case note to an existing case and, depending on config, posts the updated case in the case log channel
*/
public async createCaseNote(
caseOrCaseId: Case | number,
modId: string,
body: string,
automatic = false,
postInCaseLogOverride = null
): Promise<void> {
const mod = this.bot.users.get(modId);
const modName = mod ? `${mod.username}#${mod.discriminator}` : "Unknown#0000";
const theCase = await this.resolveCase(caseOrCaseId);
if (!theCase) {
this.throwPluginRuntimeError(`Unknown case ID: ${caseOrCaseId}`);
}
await this.cases.createNote(theCase.id, {
mod_id: modId,
mod_name: modName,
body: body || ""
});
if (theCase.mod_id == null) {
// If the case has no moderator information, assume the first one to add a note to it did the action
await this.cases.update(theCase.id, {
mod_id: modId,
mod_name: modName
});
}
if ((!automatic || this.configValue("log_automatic_actions")) && postInCaseLogOverride !== false) {
try {
await this.postCaseToCaseLogChannel(theCase.id);
} catch (e) {} // tslint:disable-line
}
}
/**
* Returns a Discord embed for the specified case
*/
public async getCaseEmbed(caseOrCaseId: Case | number): Promise<MessageContent> {
const theCase = await this.resolveCase(caseOrCaseId);
if (!theCase) return null;
const createdAt = moment(theCase.created_at);
const actionTypeStr = CaseTypes[theCase.type].toUpperCase();
const embed: any = {
title: `${actionTypeStr} - Case #${theCase.case_number}`,
footer: {
text: `Case created at ${createdAt.format("YYYY-MM-DD [at] HH:mm")}`
},
fields: [
{
name: "User",
value: `${theCase.user_name}\n<@!${theCase.user_id}>`,
inline: true
},
{
name: "Moderator",
value: `${theCase.mod_name}\n<@!${theCase.mod_id}>`,
inline: true
}
]
};
if (CaseTypeColors[theCase.type]) {
embed.color = CaseTypeColors[theCase.type];
}
if (theCase.notes.length) {
theCase.notes.forEach((note: any) => {
const noteDate = moment(note.created_at);
embed.fields.push({
name: `${note.mod_name} at ${noteDate.format("YYYY-MM-DD [at] HH:mm")}:`,
value: note.body
});
});
} else {
embed.fields.push({
name: "!!! THIS CASE HAS NO NOTES !!!",
value: "\u200B"
});
}
return { embed };
}
/**
* A helper for posting to the case log channel.
* Returns silently if the case log channel isn't specified or is invalid.
*/
public postToCaseLogChannel(content: MessageContent, file: MessageFile = null): Promise<Message> {
const caseLogChannelId = this.configValue("case_log_channel");
if (!caseLogChannelId) return;
const caseLogChannel = this.guild.channels.get(caseLogChannelId);
if (!caseLogChannel || !(caseLogChannel instanceof TextChannel)) return;
return caseLogChannel.createMessage(content, file);
}
/**
* A helper to post a case embed to the case log channel
*/
public async postCaseToCaseLogChannel(caseOrCaseId: Case | number): Promise<Message> {
const caseEmbed = await this.getCaseEmbed(caseOrCaseId);
if (!caseEmbed) return;
return this.postToCaseLogChannel(caseEmbed);
}
}

View file

@ -3,7 +3,13 @@ import { Invite, Message } from "eris";
import escapeStringRegexp from "escape-string-regexp"; import escapeStringRegexp from "escape-string-regexp";
import { GuildLogs } from "../data/GuildLogs"; import { GuildLogs } from "../data/GuildLogs";
import { LogType } from "../data/LogType"; import { LogType } from "../data/LogType";
import { getInviteCodesInString, getUrlsInString, stripObjectToScalars } from "../utils"; import {
deactivateMentions,
disableCodeBlocks,
getInviteCodesInString,
getUrlsInString,
stripObjectToScalars
} from "../utils";
import { ZalgoRegex } from "../data/Zalgo"; import { ZalgoRegex } from "../data/Zalgo";
import { GuildSavedMessages } from "../data/GuildSavedMessages"; import { GuildSavedMessages } from "../data/GuildSavedMessages";
import { SavedMessage } from "../data/entities/SavedMessage"; import { SavedMessage } from "../data/entities/SavedMessage";
@ -81,7 +87,7 @@ export class CensorPlugin extends Plugin {
member: stripObjectToScalars(member, ["user"]), member: stripObjectToScalars(member, ["user"]),
channel: stripObjectToScalars(channel), channel: stripObjectToScalars(channel),
reason, reason,
messageText: savedMessage.data.content messageText: disableCodeBlocks(deactivateMentions(savedMessage.data.content))
}); });
} }

View file

@ -1,14 +1,10 @@
import { decorators as d, Plugin, waitForReaction, waitForReply } from "knub"; import { decorators as d, waitForReaction, waitForReply } from "knub";
import { Constants as ErisConstants, Guild, Member, Message, TextChannel, User } from "eris"; import { Constants as ErisConstants, Guild, Member, Message, TextChannel, User } from "eris";
import moment from "moment-timezone";
import humanizeDuration from "humanize-duration"; import humanizeDuration from "humanize-duration";
import chunk from "lodash.chunk";
import { GuildCases } from "../data/GuildCases"; import { GuildCases } from "../data/GuildCases";
import { import {
chunkLines,
chunkMessageLines, chunkMessageLines,
convertDelayStringToMS, convertDelayStringToMS,
DBDateFormat,
disableLinkPreviews, disableLinkPreviews,
errorMessage, errorMessage,
findRelevantAuditLogEntry, findRelevantAuditLogEntry,
@ -18,12 +14,14 @@ import {
trimLines trimLines
} from "../utils"; } from "../utils";
import { GuildMutes } from "../data/GuildMutes"; import { GuildMutes } from "../data/GuildMutes";
import { Case } from "../data/entities/Case";
import { CaseTypes } from "../data/CaseTypes"; import { CaseTypes } from "../data/CaseTypes";
import { GuildLogs } from "../data/GuildLogs"; import { GuildLogs } from "../data/GuildLogs";
import { LogType } from "../data/LogType"; import { LogType } from "../data/LogType";
import Timer = NodeJS.Timer; import Timer = NodeJS.Timer;
import { CaseTypeColors } from "../data/CaseTypeColors"; import { ZeppelinPlugin } from "./ZeppelinPlugin";
import { GuildActions } from "../data/GuildActions";
import { Case } from "../data/entities/Case";
import { Mute } from "../data/entities/Mute";
enum IgnoredEventType { enum IgnoredEventType {
Ban = 1, Ban = 1,
@ -38,35 +36,26 @@ interface IIgnoredEvent {
const CASE_LIST_REASON_MAX_LENGTH = 80; const CASE_LIST_REASON_MAX_LENGTH = 80;
export class ModActionsPlugin extends Plugin { export class ModActionsPlugin extends ZeppelinPlugin {
public mutes: GuildMutes; protected actions: GuildActions;
protected mutes: GuildMutes;
protected cases: GuildCases; protected cases: GuildCases;
protected serverLogs: GuildLogs; protected serverLogs: GuildLogs;
protected muteClearIntervalId: Timer;
protected ignoredEvents: IIgnoredEvent[]; protected ignoredEvents: IIgnoredEvent[];
async onLoad() { async onLoad() {
this.cases = GuildCases.getInstance(this.guildId); this.actions = GuildActions.getInstance(this.guildId);
this.mutes = GuildMutes.getInstance(this.guildId); this.mutes = GuildMutes.getInstance(this.guildId);
this.cases = GuildCases.getInstance(this.guildId);
this.serverLogs = new GuildLogs(this.guildId); this.serverLogs = new GuildLogs(this.guildId);
this.ignoredEvents = []; this.ignoredEvents = [];
// Check for expired mutes every 5s
this.clearExpiredMutes();
this.muteClearIntervalId = setInterval(() => this.clearExpiredMutes(), 5000);
}
async onUnload() {
clearInterval(this.muteClearIntervalId);
} }
getDefaultOptions() { getDefaultOptions() {
return { return {
config: { config: {
mute_role: null,
dm_on_warn: true, dm_on_warn: true,
dm_on_mute: false, dm_on_mute: false,
dm_on_kick: false, dm_on_kick: false,
@ -81,8 +70,6 @@ export class ModActionsPlugin extends Plugin {
timed_mute_message: "You have been muted on {guildName} for {time}. Reason given: {reason}", timed_mute_message: "You have been muted on {guildName} for {time}. Reason given: {reason}",
kick_message: "You have been kicked from {guildName}. Reason given: {reason}", kick_message: "You have been kicked from {guildName}. Reason given: {reason}",
ban_message: "You have been banned from {guildName}. Reason given: {reason}", ban_message: "You have been banned from {guildName}. Reason given: {reason}",
log_automatic_actions: true,
case_log_channel: null,
alert_on_rejoin: false, alert_on_rejoin: false,
alert_channel: null alert_channel: null
}, },
@ -157,9 +144,19 @@ export class ModActionsPlugin extends Plugin {
const modId = relevantAuditLogEntry.user.id; const modId = relevantAuditLogEntry.user.id;
const auditLogId = relevantAuditLogEntry.id; const auditLogId = relevantAuditLogEntry.id;
await this.createCase(user.id, modId, CaseTypes.Ban, auditLogId, relevantAuditLogEntry.reason, true); this.actions.fire("createCase", {
userId: user.id,
modId,
type: CaseTypes.Ban,
auditLogId,
reason: relevantAuditLogEntry.reason,
automatic: true
});
} else { } else {
await this.createCase(user.id, null, CaseTypes.Ban); this.actions.fire("createCase", {
userId: user.id,
type: CaseTypes.Ban
});
} }
} }
@ -184,9 +181,19 @@ export class ModActionsPlugin extends Plugin {
const modId = relevantAuditLogEntry.user.id; const modId = relevantAuditLogEntry.user.id;
const auditLogId = relevantAuditLogEntry.id; const auditLogId = relevantAuditLogEntry.id;
await this.createCase(user.id, modId, CaseTypes.Unban, auditLogId, null, true); this.actions.fire("createCase", {
userId: user.id,
modId,
type: CaseTypes.Unban,
auditLogId,
automatic: true
});
} else { } else {
await this.createCase(user.id, null, CaseTypes.Unban); this.actions.fire("createCase", {
userId: user.id,
type: CaseTypes.Unban,
automatic: true
});
} }
} }
@ -226,14 +233,15 @@ export class ModActionsPlugin extends Plugin {
); );
if (kickAuditLogEntry) { if (kickAuditLogEntry) {
this.createCase( this.actions.fire("createCase", {
member.id, userId: member.id,
kickAuditLogEntry.user.id, modId: kickAuditLogEntry.user.id,
CaseTypes.Kick, type: CaseTypes.Kick,
kickAuditLogEntry.id, auditLogId: kickAuditLogEntry.id,
kickAuditLogEntry.reason, reason: kickAuditLogEntry.reason,
true automatic: true
); });
this.serverLogs.log(LogType.MEMBER_KICK, { this.serverLogs.log(LogType.MEMBER_KICK, {
user: stripObjectToScalars(member.user), user: stripObjectToScalars(member.user),
mod: stripObjectToScalars(kickAuditLogEntry.user) mod: stripObjectToScalars(kickAuditLogEntry.user)
@ -249,24 +257,16 @@ export class ModActionsPlugin extends Plugin {
async updateCmd(msg: Message, args: any) { async updateCmd(msg: Message, args: any) {
const theCase = await this.cases.findByCaseNumber(args.caseNumber); const theCase = await this.cases.findByCaseNumber(args.caseNumber);
if (!theCase) { if (!theCase) {
msg.channel.createMessage("Case not found!"); msg.channel.createMessage(errorMessage("Case not found"));
return; return;
} }
if (theCase.mod_id === null) { await this.actions.fire("createCaseNote", theCase, {
// If the action has no moderator information, assume the first one to update it did the action modId: msg.author.id,
await this.cases.update(theCase.id, { note: args.note
mod_id: msg.author.id, });
mod_name: `${msg.author.username}#${msg.author.discriminator}`
});
}
await this.createCaseNote(theCase.id, msg.author.id, args.note); msg.channel.createMessage(successMessage(`Case \`#${theCase.case_number}\` updated`));
this.postCaseToCaseLog(theCase.id); // Post updated case to case log
if (msg.channel.id !== this.configValue("case_log_channel")) {
msg.channel.createMessage(successMessage(`Case \`#${theCase.case_number}\` updated`));
}
} }
@d.command("note", "<userId:userId> <note:string$>") @d.command("note", "<userId:userId> <note:string$>")
@ -275,7 +275,13 @@ export class ModActionsPlugin extends Plugin {
const user = await this.bot.users.get(args.userId); const user = await this.bot.users.get(args.userId);
const userName = user ? `${user.username}#${user.discriminator}` : "member"; const userName = user ? `${user.username}#${user.discriminator}` : "member";
await this.createCase(args.userId, msg.author.id, CaseTypes.Note, null, args.note); await this.actions.fire("createCase", {
userId: args.userId,
modId: msg.author.id,
type: CaseTypes.Note,
reason: args.note
});
msg.channel.createMessage(successMessage(`Note added on ${userName}`)); msg.channel.createMessage(successMessage(`Note added on ${userName}`));
} }
@ -309,7 +315,12 @@ export class ModActionsPlugin extends Plugin {
} }
} }
await this.createCase(args.member.id, msg.author.id, CaseTypes.Warn, null, args.reason); await this.actions.fire("createCase", {
userId: args.member.id,
modId: msg.author.id,
type: CaseTypes.Warn,
reason: args.reason
});
msg.channel.createMessage( msg.channel.createMessage(
successMessage(`Warned **${args.member.user.username}#${args.member.user.discriminator}**`) successMessage(`Warned **${args.member.user.username}#${args.member.user.discriminator}**`)
@ -321,11 +332,6 @@ export class ModActionsPlugin extends Plugin {
}); });
} }
public async muteMember(member: Member, muteTime: number = null, reason: string = null) {
await member.addRole(this.configValue("mute_role"));
await this.mutes.addOrUpdateMute(member.id, muteTime);
}
@d.command("mute", "<member:Member> [time:string] [reason:string$]") @d.command("mute", "<member:Member> [time:string] [reason:string$]")
@d.permission("mute") @d.permission("mute")
async muteCmd(msg: Message, args: any) { async muteCmd(msg: Message, args: any) {
@ -353,20 +359,35 @@ export class ModActionsPlugin extends Plugin {
// Apply "muted" role // Apply "muted" role
this.serverLogs.ignoreLog(LogType.MEMBER_ROLE_ADD, args.member.id); this.serverLogs.ignoreLog(LogType.MEMBER_ROLE_ADD, args.member.id);
await this.muteMember(args.member, muteTime, args.reason); const mute: Mute = await this.actions.fire("mute", {
member: args.member,
muteTime
});
const mute = await this.mutes.findExistingMuteForUserId(args.member.id); if (!mute) {
const hasOldCase = mute && mute.case_id != null; msg.channel.createMessage(errorMessage("Could not mute the user"));
return;
}
const hasOldCase = mute.case_id != null;
if (hasOldCase) { if (hasOldCase) {
if (args.reason) { if (args.reason) {
await this.createCaseNote(mute.case_id, msg.author.id, args.reason); // Update old case
this.postCaseToCaseLog(mute.case_id); await this.actions.fire("createCaseNote", mute.case_id, {
modId: msg.author.id,
note: args.reason
});
} }
} else { } else {
// Create a case // Create new case
const caseId = await this.createCase(args.member.id, msg.author.id, CaseTypes.Mute, null, args.reason); const theCase: Case = await this.actions.fire("createCase", {
await this.mutes.setCaseId(args.member.id, caseId); userId: args.member.id,
modId: msg.author.id,
type: CaseTypes.Mute,
reason: args.reason
});
await this.mutes.setCaseId(args.member.id, theCase.id);
} }
// Message the user informing them of the mute // Message the user informing them of the mute
@ -438,7 +459,7 @@ export class ModActionsPlugin extends Plugin {
if (unmuteTime) { if (unmuteTime) {
// If we have an unmute time, just update the old mute to expire in that time // If we have an unmute time, just update the old mute to expire in that time
const timeUntilUnmute = unmuteTime && humanizeDuration(unmuteTime); const timeUntilUnmute = unmuteTime && humanizeDuration(unmuteTime);
this.mutes.addOrUpdateMute(args.member.id, unmuteTime); await this.actions.fire("unmute", { member: args.member, unmuteTime });
args.reason = args.reason ? `Timed unmute: ${args.reason}` : "Timed unmute"; args.reason = args.reason ? `Timed unmute: ${args.reason}` : "Timed unmute";
// Confirm the action to the moderator // Confirm the action to the moderator
@ -450,8 +471,7 @@ export class ModActionsPlugin extends Plugin {
} else { } else {
// Otherwise remove "muted" role immediately // Otherwise remove "muted" role immediately
this.serverLogs.ignoreLog(LogType.MEMBER_ROLE_REMOVE, args.member.id); this.serverLogs.ignoreLog(LogType.MEMBER_ROLE_REMOVE, args.member.id);
await args.member.removeRole(this.configValue("mute_role")); await this.actions.fire("unmute", { member: args.member });
await this.mutes.clear(args.member.id);
// Confirm the action to the moderator // Confirm the action to the moderator
msg.channel.createMessage( msg.channel.createMessage(
@ -460,7 +480,12 @@ export class ModActionsPlugin extends Plugin {
} }
// Create a case // Create a case
await this.createCase(args.member.id, msg.author.id, CaseTypes.Unmute, null, args.reason); await this.actions.fire("createCase", {
userId: args.member.id,
modId: msg.author.id,
type: CaseTypes.Unmute,
reason: args.reason
});
// Log the action // Log the action
this.serverLogs.log(LogType.MEMBER_UNMUTE, { this.serverLogs.log(LogType.MEMBER_UNMUTE, {
@ -472,76 +497,12 @@ export class ModActionsPlugin extends Plugin {
@d.command("mutes") @d.command("mutes")
@d.permission("view") @d.permission("view")
async mutesCmd(msg: Message) { async mutesCmd(msg: Message) {
const lines = []; this.actions.fire("postMuteList", msg.channel);
// Active, logged mutes
const activeMutes = await this.mutes.getActiveMutes();
activeMutes.sort((a, b) => {
if (a.expires_at == null && b.expires_at != null) return 1;
if (b.expires_at == null && a.expires_at != null) return -1;
if (a.expires_at == null && b.expires_at == null) {
return a.created_at > b.created_at ? -1 : 1;
}
return a.expires_at > b.expires_at ? 1 : -1;
});
const caseIds = activeMutes.map(m => m.case_id).filter(v => !!v);
const cases = caseIds.length ? await this.cases.get(caseIds) : [];
const casesById = cases.reduce((map, c) => map.set(c.id, c), new Map());
lines.push(
...activeMutes.map(mute => {
const user = this.bot.users.get(mute.user_id);
const username = user ? `${user.username}#${user.discriminator}` : "Unknown#0000";
const theCase = casesById[mute.case_id] || null;
const caseName = theCase ? `Case #${theCase.case_number}` : "No case";
let line = `\`${caseName}\` **${username}** (\`${mute.user_id}\`)`;
if (mute.expires_at) {
const timeUntilExpiry = moment().diff(moment(mute.expires_at, DBDateFormat));
const humanizedTime = humanizeDuration(timeUntilExpiry, { largest: 2, round: true });
line += ` (expires in ${humanizedTime})`;
} else {
line += ` (doesn't expire)`;
}
const mutedAt = moment(mute.created_at, DBDateFormat);
line += ` (muted at ${mutedAt.format("YYYY-MM-DD HH:mm:ss")})`;
return line;
})
);
// Manually added mute roles
const muteUserIds = activeMutes.reduce((set, m) => set.add(m.user_id), new Set());
const manuallyMutedMembers = [];
const muteRole = this.configValue("mute_role");
if (muteRole) {
this.guild.members.forEach(member => {
if (muteUserIds.has(member.id)) return;
if (member.roles.includes(muteRole)) manuallyMutedMembers.push(member);
});
}
lines.push(
...manuallyMutedMembers.map(member => {
return `\`Manual mute\` **${member.user.username}#${member.user.discriminator}** (\`${member.id}\`)`;
})
);
const chunks = chunk(lines, 15);
for (const [i, chunkLines] of chunks.entries()) {
let body = chunkLines.join("\n");
if (i === 0) body = `Active mutes:\n\n${body}`;
msg.channel.createMessage(body);
}
} }
@d.command("kick", "<member:Member> [reason:string$]") @d.command("kick", "<member:Member> [reason:string$]")
@d.permission("kick") @d.permission("kick")
async kickCmd(msg, args) { async kickCmd(msg, args: { member: Member; reason: string }) {
// Make sure we're allowed to kick this member // Make sure we're allowed to kick this member
if (!this.canActOn(msg.member, args.member)) { if (!this.canActOn(msg.member, args.member)) {
msg.channel.createMessage(errorMessage("Cannot kick: insufficient permissions")); msg.channel.createMessage(errorMessage("Cannot kick: insufficient permissions"));
@ -570,7 +531,12 @@ export class ModActionsPlugin extends Plugin {
args.member.kick(args.reason); args.member.kick(args.reason);
// Create a case for this action // Create a case for this action
await this.createCase(args.member.id, msg.author.id, CaseTypes.Kick, null, args.reason); await this.actions.fire("createCase", {
userId: args.member.id,
modId: msg.author.id,
type: CaseTypes.Kick,
reason: args.reason
});
// Confirm the action to the moderator // Confirm the action to the moderator
let response = `Kicked **${args.member.user.username}#${args.member.user.discriminator}**`; let response = `Kicked **${args.member.user.username}#${args.member.user.discriminator}**`;
@ -615,7 +581,12 @@ export class ModActionsPlugin extends Plugin {
args.member.ban(1, args.reason); args.member.ban(1, args.reason);
// Create a case for this action // Create a case for this action
await this.createCase(args.member.id, msg.author.id, CaseTypes.Ban, null, args.reason); await this.actions.fire("createCase", {
userId: args.member.id,
modId: msg.author.id,
type: CaseTypes.Ban,
reason: args.reason
});
// Confirm the action to the moderator // Confirm the action to the moderator
let response = `Banned **${args.member.user.username}#${args.member.user.discriminator}**`; let response = `Banned **${args.member.user.username}#${args.member.user.discriminator}**`;
@ -648,7 +619,12 @@ export class ModActionsPlugin extends Plugin {
await this.guild.unbanMember(args.member.id); await this.guild.unbanMember(args.member.id);
// Create a case for this action // Create a case for this action
await this.createCase(args.member.id, msg.author.id, CaseTypes.Softban, null, args.reason); await this.actions.fire("createCase", {
userId: args.member.id,
modId: msg.author.id,
type: CaseTypes.Softban,
reason: args.reason
});
// Confirm the action to the moderator // Confirm the action to the moderator
msg.channel.createMessage( msg.channel.createMessage(
@ -679,7 +655,12 @@ export class ModActionsPlugin extends Plugin {
msg.channel.createMessage(successMessage("Member unbanned!")); msg.channel.createMessage(successMessage("Member unbanned!"));
// Create a case // Create a case
this.createCase(args.userId, msg.author.id, CaseTypes.Unban, null, args.reason); await this.actions.fire("createCase", {
userId: args.member.id,
modId: msg.author.id,
type: CaseTypes.Unban,
reason: args.reason
});
// Log the action // Log the action
this.serverLogs.log(LogType.MEMBER_UNBAN, { this.serverLogs.log(LogType.MEMBER_UNBAN, {
@ -712,7 +693,12 @@ export class ModActionsPlugin extends Plugin {
msg.channel.createMessage(successMessage("Member forcebanned!")); msg.channel.createMessage(successMessage("Member forcebanned!"));
// Create a case // Create a case
this.createCase(args.userId, msg.author.id, CaseTypes.Ban, null, args.reason); await this.actions.fire("createCase", {
userId: args.userId,
modId: msg.author.id,
type: CaseTypes.Ban,
reason: args.reason
});
// Log the action // Log the action
this.serverLogs.log(LogType.MEMBER_FORCEBAN, { this.serverLogs.log(LogType.MEMBER_FORCEBAN, {
@ -766,7 +752,14 @@ export class ModActionsPlugin extends Plugin {
for (const userId of args.userIds) { for (const userId of args.userIds) {
try { try {
await this.guild.banMember(userId); await this.guild.banMember(userId);
await this.createCase(userId, msg.author.id, CaseTypes.Ban, null, `Mass ban: ${banReason}`, false, false);
await this.actions.fire("createCase", {
userId,
modId: msg.author.id,
type: CaseTypes.Ban,
reason: `Mass ban: ${banReason}`,
postInCaseLog: false
});
} catch (e) { } catch (e) {
failedBans.push(userId); failedBans.push(userId);
} }
@ -820,11 +813,16 @@ export class ModActionsPlugin extends Plugin {
} }
// Create the case // Create the case
const caseId = await this.createCase(args.target, msg.author.id, CaseTypes[type], null, args.reason); const theCase: Case = await this.actions.fire("createCase", {
const theCase = await this.cases.find(caseId); userId: args.target,
modId: msg.author.id,
type: CaseTypes[type],
reason: args.reason
});
msg.channel.createMessage(successMessage("Case created!"));
// Log the action // Log the action
msg.channel.createMessage(successMessage("Case created!"));
this.serverLogs.log(LogType.CASE_CREATE, { this.serverLogs.log(LogType.CASE_CREATE, {
mod: stripObjectToScalars(msg.member.user), mod: stripObjectToScalars(msg.member.user),
userId: args.userId, userId: args.userId,
@ -845,11 +843,14 @@ export class ModActionsPlugin extends Plugin {
const theCase = await this.cases.findByCaseNumber(args.caseNumber); const theCase = await this.cases.findByCaseNumber(args.caseNumber);
if (!theCase) { if (!theCase) {
msg.channel.createMessage("Case not found!"); msg.channel.createMessage(errorMessage("Case not found"));
return; return;
} }
this.displayCase(theCase.id, msg.channel.id); await this.actions.fire("postCase", {
caseId: theCase.id,
channel: msg.channel
});
} }
@d.command(/cases|usercases/, "<userId:userId> [expanded:string]") @d.command(/cases|usercases/, "<userId:userId> [expanded:string]")
@ -871,7 +872,10 @@ export class ModActionsPlugin extends Plugin {
// Expanded view (= individual case embeds) // Expanded view (= individual case embeds)
for (const theCase of cases) { for (const theCase of cases) {
await this.displayCase(theCase.id, msg.channel.id); await this.actions.fire("postCase", {
caseId: theCase.id,
channel: msg.channel
});
} }
} else { } else {
// Compact view (= regular message with a preview of each case) // Compact view (= regular message with a preview of each case)
@ -949,146 +953,4 @@ export class ModActionsPlugin extends Plugin {
return messageSent; return messageSent;
} }
/**
* Shows information about the specified action in a message embed.
* If no channelId is specified, uses the channel id from config.
*/
protected async displayCase(caseOrCaseId: Case | number, channelId: string) {
let theCase: Case;
if (typeof caseOrCaseId === "number") {
theCase = await this.cases.with("notes").find(caseOrCaseId);
} else {
theCase = caseOrCaseId;
}
if (!theCase) return;
if (!this.guild.channels.get(channelId)) return;
const createdAt = moment(theCase.created_at);
const actionTypeStr = CaseTypes[theCase.type].toUpperCase();
const embed: any = {
title: `${actionTypeStr} - Case #${theCase.case_number}`,
footer: {
text: `Case created at ${createdAt.format("YYYY-MM-DD [at] HH:mm")}`
},
fields: [
{
name: "User",
value: `${theCase.user_name}\n<@!${theCase.user_id}>`,
inline: true
},
{
name: "Moderator",
value: `${theCase.mod_name}\n<@!${theCase.mod_id}>`,
inline: true
}
]
};
if (CaseTypeColors[theCase.type]) {
embed.color = CaseTypeColors[theCase.type];
}
if (theCase.notes.length) {
theCase.notes.forEach((note: any) => {
const noteDate = moment(note.created_at);
embed.fields.push({
name: `${note.mod_name} at ${noteDate.format("YYYY-MM-DD [at] HH:mm")}:`,
value: note.body
});
});
} else {
embed.fields.push({
name: "!!! THIS CASE HAS NO NOTES !!!",
value: "\u200B"
});
}
const channel = this.guild.channels.get(channelId) as TextChannel;
await channel.createMessage({ embed });
}
/**
* Posts the specified mod action to the guild's action log channel
*/
protected async postCaseToCaseLog(caseOrCaseId: Case | number) {
const caseLogChannelId = this.configValue("case_log_channel");
if (!caseLogChannelId) return;
if (!this.guild.channels.get(caseLogChannelId)) return;
return this.displayCase(caseOrCaseId, caseLogChannelId);
}
public async createCase(
userId: string,
modId: string,
caseType: CaseTypes,
auditLogId: string = null,
reason: string = null,
automatic = false,
postInCaseLogOverride = null
): Promise<number> {
const user = this.bot.users.get(userId);
const userName = user ? `${user.username}#${user.discriminator}` : "Unknown#0000";
const mod = this.bot.users.get(modId);
const modName = mod ? `${mod.username}#${mod.discriminator}` : "Unknown#0000";
const createdId = await this.cases.create({
type: caseType,
user_id: userId,
user_name: userName,
mod_id: modId,
mod_name: modName,
audit_log_id: auditLogId
});
if (reason) {
await this.createCaseNote(createdId, modId, reason);
}
if (
this.configValue("case_log_channel") &&
(!automatic || this.configValue("log_automatic_actions")) &&
postInCaseLogOverride !== false
) {
try {
await this.postCaseToCaseLog(createdId);
} catch (e) {} // tslint:disable-line
}
return createdId;
}
protected async createCaseNote(caseId: number, modId: string, body: string) {
const mod = this.bot.users.get(modId);
const modName = mod ? `${mod.username}#${mod.discriminator}` : "Unknown#0000";
return this.cases.createNote(caseId, {
mod_id: modId,
mod_name: modName,
body: body || ""
});
}
protected async clearExpiredMutes() {
const expiredMutes = await this.mutes.getExpiredMutes();
for (const mute of expiredMutes) {
const member = this.guild.members.get(mute.user_id);
if (!member) continue;
try {
this.serverLogs.ignoreLog(LogType.MEMBER_ROLE_REMOVE, member.id);
await member.removeRole(this.configValue("mute_role"));
} catch (e) {} // tslint:disable-line
await this.mutes.clear(member.id);
this.serverLogs.log(LogType.MEMBER_MUTE_EXPIRED, {
member: stripObjectToScalars(member, ["user"])
});
}
}
} }

158
src/plugins/Mutes.ts Normal file
View file

@ -0,0 +1,158 @@
import { Member, TextableChannel } from "eris";
import { GuildCases } from "../data/GuildCases";
import moment from "moment-timezone";
import { ZeppelinPlugin } from "./ZeppelinPlugin";
import { GuildActions } from "../data/GuildActions";
import { GuildMutes } from "../data/GuildMutes";
import { DBDateFormat, chunkMessageLines, stripObjectToScalars } from "../utils";
import humanizeDuration from "humanize-duration";
import { LogType } from "../data/LogType";
import { GuildLogs } from "../data/GuildLogs";
export class MutesPlugin extends ZeppelinPlugin {
protected actions: GuildActions;
protected mutes: GuildMutes;
protected cases: GuildCases;
protected serverLogs: GuildLogs;
private muteClearIntervalId: NodeJS.Timer;
getDefaultOptions() {
return {
config: {
mute_role: null
}
};
}
onLoad() {
this.actions = GuildActions.getInstance(this.guildId);
this.mutes = GuildMutes.getInstance(this.guildId);
this.cases = GuildCases.getInstance(this.guildId);
this.serverLogs = new GuildLogs(this.guildId);
this.actions.register("mute", args => {
return this.muteMember(args.member, args.muteTime);
});
this.actions.register("unmute", args => {
return this.unmuteMember(args.member, args.unmuteTime);
});
this.actions.register("postMuteList", args => {
return this.postMuteList(args.channel);
});
// Check for expired mutes every 5s
this.clearExpiredMutes();
this.muteClearIntervalId = setInterval(() => this.clearExpiredMutes(), 5000);
}
onUnload() {
this.actions.unregister("mute");
this.actions.unregister("unmute");
this.actions.unregister("postMuteList");
clearInterval(this.muteClearIntervalId);
}
public async muteMember(member: Member, muteTime: number = null) {
const muteRole = this.configValue("mute_role");
if (!muteRole) return;
await member.addRole(muteRole);
return this.mutes.addOrUpdateMute(member.id, muteTime);
}
public async unmuteMember(member: Member, unmuteTime: number = null) {
if (unmuteTime) {
await this.mutes.addOrUpdateMute(member.id, unmuteTime);
} else {
await member.removeRole(this.configValue("mute_role"));
await this.mutes.clear(member.id);
}
}
public async postMuteList(channel: TextableChannel) {
const lines = [];
// Active, logged mutes
const activeMutes = await this.mutes.getActiveMutes();
activeMutes.sort((a, b) => {
if (a.expires_at == null && b.expires_at != null) return 1;
if (b.expires_at == null && a.expires_at != null) return -1;
if (a.expires_at == null && b.expires_at == null) {
return a.created_at > b.created_at ? -1 : 1;
}
return a.expires_at > b.expires_at ? 1 : -1;
});
const caseIds = activeMutes.map(m => m.case_id).filter(v => !!v);
const muteCases = caseIds.length ? await this.cases.get(caseIds) : [];
const muteCasesById = muteCases.reduce((map, c) => map.set(c.id, c), new Map());
lines.push(
...activeMutes.map(mute => {
const user = this.bot.users.get(mute.user_id);
const username = user ? `${user.username}#${user.discriminator}` : "Unknown#0000";
const theCase = muteCasesById[mute.case_id] || null;
const caseName = theCase ? `Case #${theCase.case_number}` : "No case";
let line = `\`${caseName}\` **${username}** (\`${mute.user_id}\`)`;
if (mute.expires_at) {
const timeUntilExpiry = moment().diff(moment(mute.expires_at, DBDateFormat));
const humanizedTime = humanizeDuration(timeUntilExpiry, { largest: 2, round: true });
line += ` (expires in ${humanizedTime})`;
} else {
line += ` (doesn't expire)`;
}
const mutedAt = moment(mute.created_at, DBDateFormat);
line += ` (muted at ${mutedAt.format("YYYY-MM-DD HH:mm:ss")})`;
return line;
})
);
// Manually added mute roles
const muteUserIds = activeMutes.reduce((set, m) => set.add(m.user_id), new Set());
const manuallyMutedMembers = [];
const muteRole = this.configValue("mute_role");
if (muteRole) {
this.guild.members.forEach(member => {
if (muteUserIds.has(member.id)) return;
if (member.roles.includes(muteRole)) manuallyMutedMembers.push(member);
});
}
lines.push(
...manuallyMutedMembers.map(member => {
return `\`Manual mute\` **${member.user.username}#${member.user.discriminator}** (\`${member.id}\`)`;
})
);
const message = `Active mutes:\n\n${lines.join("\n")}`;
const chunks = chunkMessageLines(message);
for (const chunk of chunks) {
channel.createMessage(chunk);
}
}
protected async clearExpiredMutes() {
const expiredMutes = await this.mutes.getExpiredMutes();
for (const mute of expiredMutes) {
const member = this.guild.members.get(mute.user_id);
if (!member) continue;
try {
this.serverLogs.ignoreLog(LogType.MEMBER_ROLE_REMOVE, member.id);
await member.removeRole(this.configValue("mute_role"));
} catch (e) {} // tslint:disable-line
await this.mutes.clear(member.id);
this.serverLogs.log(LogType.MEMBER_MUTE_EXPIRED, {
member: stripObjectToScalars(member, ["user"])
});
}
}
}

View file

@ -63,6 +63,6 @@ export class PostPlugin extends Plugin {
return; return;
} }
const edited = await this.bot.editMessage(savedMessage.channel_id, savedMessage.id, args.content); await this.bot.editMessage(savedMessage.channel_id, savedMessage.id, args.content);
} }
} }

View file

@ -1,7 +1,6 @@
import { decorators as d, Plugin } from "knub"; import { Plugin } from "knub";
import { Channel, Message, User } from "eris"; import { Channel, User } from "eris";
import { import {
formatTemplateString,
getEmojiInString, getEmojiInString,
getRoleMentions, getRoleMentions,
getUrlsInString, getUrlsInString,
@ -11,12 +10,14 @@ import {
} from "../utils"; } from "../utils";
import { LogType } from "../data/LogType"; import { LogType } from "../data/LogType";
import { GuildLogs } from "../data/GuildLogs"; import { GuildLogs } from "../data/GuildLogs";
import { ModActionsPlugin } from "./ModActions";
import { CaseTypes } from "../data/CaseTypes"; import { CaseTypes } from "../data/CaseTypes";
import { GuildArchives } from "../data/GuildArchives"; import { GuildArchives } from "../data/GuildArchives";
import moment from "moment-timezone"; import moment from "moment-timezone";
import { SavedMessage } from "../data/entities/SavedMessage"; import { SavedMessage } from "../data/entities/SavedMessage";
import { GuildSavedMessages } from "../data/GuildSavedMessages"; import { GuildSavedMessages } from "../data/GuildSavedMessages";
import { GuildActions } from "../data/GuildActions";
import { Case } from "../data/entities/Case";
import { GuildMutes } from "../data/GuildMutes";
enum RecentActionType { enum RecentActionType {
Message = 1, Message = 1,
@ -42,9 +43,11 @@ const MAX_INTERVAL = 300;
const SPAM_ARCHIVE_EXPIRY_DAYS = 90; const SPAM_ARCHIVE_EXPIRY_DAYS = 90;
export class SpamPlugin extends Plugin { export class SpamPlugin extends Plugin {
protected actions: GuildActions;
protected logs: GuildLogs; protected logs: GuildLogs;
protected archives: GuildArchives; protected archives: GuildArchives;
protected savedMessages: GuildSavedMessages; protected savedMessages: GuildSavedMessages;
protected mutes: GuildMutes;
private onMessageCreateFn; private onMessageCreateFn;
@ -90,9 +93,11 @@ export class SpamPlugin extends Plugin {
} }
onLoad() { onLoad() {
this.actions = GuildActions.getInstance(this.guildId);
this.logs = new GuildLogs(this.guildId); this.logs = new GuildLogs(this.guildId);
this.archives = GuildArchives.getInstance(this.guildId); this.archives = GuildArchives.getInstance(this.guildId);
this.savedMessages = GuildSavedMessages.getInstance(this.guildId); this.savedMessages = GuildSavedMessages.getInstance(this.guildId);
this.mutes = GuildMutes.getInstance(this.guildId);
this.recentActions = []; this.recentActions = [];
this.expiryInterval = setInterval(() => this.clearOldRecentActions(), 1000 * 60); this.expiryInterval = setInterval(() => this.clearOldRecentActions(), 1000 * 60);
@ -198,22 +203,12 @@ export class SpamPlugin extends Plugin {
// If the user tripped the spam filter... // If the user tripped the spam filter...
if (recentActionsCount > spamConfig.count) { if (recentActionsCount > spamConfig.count) {
const recentActions = this.getRecentActions(type, savedMessage.user_id, savedMessage.channel_id, since); const recentActions = this.getRecentActions(type, savedMessage.user_id, savedMessage.channel_id, since);
let modActionsPlugin;
// Start by muting them, if enabled // Start by muting them, if enabled
if (spamConfig.mute) { if (spamConfig.mute && member) {
// We use the ModActions plugin for muting the user
// This means that spam mute functionality requires the ModActions plugin to be loaded
const guildData = this.knub.getGuildData(this.guildId);
modActionsPlugin = guildData.loadedPlugins.get("mod_actions") as ModActionsPlugin;
if (!modActionsPlugin) return;
const muteTime = spamConfig.mute_time ? spamConfig.mute_time * 60 * 1000 : 120 * 1000; const muteTime = spamConfig.mute_time ? spamConfig.mute_time * 60 * 1000 : 120 * 1000;
this.logs.ignoreLog(LogType.MEMBER_ROLE_ADD, savedMessage.user_id);
if (member) { this.actions.fire("mute", { member, muteTime, reason: "Automatic spam detection" });
this.logs.ignoreLog(LogType.MEMBER_ROLE_ADD, savedMessage.user_id);
modActionsPlugin.muteMember(member, muteTime, "Automatic spam detection");
}
} }
// Get the offending message IDs // Get the offending message IDs
@ -273,18 +268,17 @@ export class SpamPlugin extends Plugin {
archiveUrl archiveUrl
}); });
const caseId = await modActionsPlugin.createCase( const theCase: Case = await this.actions.fire("createCase", {
savedMessage.user_id, userId: savedMessage.user_id,
this.bot.user.id, modId: this.bot.user.id,
caseType, type: caseType,
null, reason: caseText,
caseText, automatic: true
true });
);
// For mutes, also set the mute's case id (for !mutes) // For mutes, also set the mute's case id (for !mutes)
if (spamConfig.mute && member) { if (spamConfig.mute && member) {
await modActionsPlugin.mutes.setCaseId(savedMessage.user_id, caseId); await this.mutes.setCaseId(savedMessage.user_id, theCase.id);
} }
} }
}, },

View file

@ -0,0 +1,10 @@
import { Plugin } from "knub";
import { PluginRuntimeError } from "../PluginRuntimeError";
import { TextableChannel } from "eris";
import { errorMessage, successMessage } from "../utils";
export class ZeppelinPlugin extends Plugin {
protected throwPluginRuntimeError(message: string) {
throw new PluginRuntimeError(message, this.pluginName, this.guildId);
}
}