Use actions/events for plugin interoperability. Move base case and mute functionality to their own plugins.
This commit is contained in:
parent
22c515be38
commit
2e30a3b9e7
14 changed files with 674 additions and 332 deletions
17
src/PluginRuntimeError.ts
Normal file
17
src/PluginRuntimeError.ts
Normal 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}`;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
24
src/data/GuildActions.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
42
src/data/GuildEvents.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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,
|
||||||
|
|
12
src/index.ts
12
src/index.ts
|
@ -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
217
src/plugins/Cases.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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))
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
158
src/plugins/Mutes.ts
Normal 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"])
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
10
src/plugins/ZeppelinPlugin.ts
Normal file
10
src/plugins/ZeppelinPlugin.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue