3
0
Fork 0
mirror of https://github.com/ZeppelinBot/Zeppelin.git synced 2025-03-15 05:41:51 +00:00

Add warn and mute commands. General code clean-up.

This commit is contained in:
Dragory 2018-07-08 13:57:27 +03:00
parent 28bb8165bc
commit 15b7da82e8
5 changed files with 264 additions and 67 deletions

View file

@ -3,8 +3,8 @@ exports.up = async function(knex) {
await knex.schema.createTable('mutes', table => { await knex.schema.createTable('mutes', table => {
table.bigInteger('guild_id').unsigned().notNullable(); table.bigInteger('guild_id').unsigned().notNullable();
table.bigInteger('user_id').unsigned().notNullable(); table.bigInteger('user_id').unsigned().notNullable();
table.dateTime('created_at'); table.dateTime('created_at').defaultTo(knex.raw('NOW()'));
table.dateTime('expires_at'); table.dateTime('expires_at').nullable().defaultTo(null);
table.primary(['guild_id', 'user_id']); table.primary(['guild_id', 'user_id']);
table.index(['expires_at']); table.index(['expires_at']);

View file

@ -1,5 +1,5 @@
import knex from "../knex"; import knex from "../knex";
import moment from "moment-timezone"; import * as moment from "moment-timezone";
import Mute from "../models/Mute"; import Mute from "../models/Mute";
export class GuildMutes { export class GuildMutes {
@ -12,20 +12,20 @@ export class GuildMutes {
async getExpiredMutes(): Promise<Mute[]> { async getExpiredMutes(): Promise<Mute[]> {
const result = await knex("mutes") const result = await knex("mutes")
.where("guild_id", this.guildId) .where("guild_id", this.guildId)
.where("expires_at", "<=", "CURDATE()")
.whereNotNull("expires_at") .whereNotNull("expires_at")
.whereRaw("expires_at <= NOW()")
.select(); .select();
return result.map(r => new Mute(r)); return result.map(r => new Mute(r));
} }
async findExistingMuteForUserId(userId: string): Promise<Mute[]> { async findExistingMuteForUserId(userId: string): Promise<Mute> {
const result = await knex("mutes") const result = await knex("mutes")
.where("guild_id", this.guildId) .where("guild_id", this.guildId)
.where("user_id", userId) .where("user_id", userId)
.first(); .first();
return result.map(r => new Mute(r)); return result ? new Mute(result) : null;
} }
async addMute(userId, expiryTime) { async addMute(userId, expiryTime) {
@ -69,8 +69,8 @@ export class GuildMutes {
} }
} }
async unmute(userId) { async clear(userId) {
return knex return knex("mutes")
.where("guild_id", this.guildId) .where("guild_id", this.guildId)
.where("user_id", userId) .where("user_id", userId)
.delete(); .delete();

View file

@ -0,0 +1,8 @@
export enum ModActionType {
Ban = 1,
Unban,
Note,
Warn,
Kick,
Mute
}

View file

@ -9,14 +9,11 @@ import {
} from "eris"; } from "eris";
import * as moment from "moment-timezone"; import * as moment from "moment-timezone";
import { GuildModActions } from "../data/GuildModActions"; import { GuildModActions } from "../data/GuildModActions";
import { convertDelayStringToMS, errorMessage, successMessage } from "../utils";
enum ActionType { import { GuildMutes } from "../data/GuildMutes";
Ban = 1, import Timer = NodeJS.Timer;
Unban, import ModAction from "../models/ModAction";
Note, import { ModActionType } from "../data/ModActionType";
Warn,
Kick
}
const sleep = (ms: number): Promise<void> => { const sleep = (ms: number): Promise<void> => {
return new Promise(resolve => { return new Promise(resolve => {
@ -26,14 +23,29 @@ const sleep = (ms: number): Promise<void> => {
export class ModActionsPlugin extends Plugin { export class ModActionsPlugin extends Plugin {
protected modActions: GuildModActions; protected modActions: GuildModActions;
protected mutes: GuildMutes;
protected muteClearIntervalId: Timer;
async onLoad() { async onLoad() {
this.modActions = new GuildModActions(this.guildId); this.modActions = new GuildModActions(this.guildId);
this.mutes = new GuildMutes(this.guildId);
// 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: true, dm_on_mute: true,
dm_on_kick: false, dm_on_kick: false,
@ -44,9 +56,14 @@ export class ModActionsPlugin extends Plugin {
message_on_ban: false, message_on_ban: false,
message_channel: null, message_channel: null,
warn_message: "You have received a warning on {guildName}: {reason}", warn_message: "You have received a warning on {guildName}: {reason}",
mute_message: "You have been muted on {guildName} for {reason}", mute_message:
kick_message: "You have been kicked from {guildName} for {reason}", "You have been muted on {guildName}. Reason given: {reason}",
ban_message: "You have been banned from {guildName} for {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}",
ban_message:
"You have been banned from {guildName}. Reason given: {reason}",
log_automatic_actions: true, log_automatic_actions: true,
action_log_channel: null, action_log_channel: null,
alert_on_rejoin: false, alert_on_rejoin: false,
@ -88,31 +105,20 @@ export class ModActionsPlugin extends Plugin {
user.id user.id
); );
let modActionId;
if (relevantAuditLogEntry) { if (relevantAuditLogEntry) {
const modId = relevantAuditLogEntry.user.id; const modId = relevantAuditLogEntry.user.id;
const auditLogId = relevantAuditLogEntry.id; const auditLogId = relevantAuditLogEntry.id;
modActionId = await this.createModAction( await this.createModAction(
user.id, user.id,
modId, modId,
ActionType.Ban, ModActionType.Ban,
auditLogId auditLogId,
relevantAuditLogEntry.reason
); );
if (relevantAuditLogEntry.reason) {
await this.createModActionNote(
modActionId,
modId,
relevantAuditLogEntry.reason
);
}
} else { } else {
modActionId = await this.createModAction(user.id, null, ActionType.Ban); await this.createModAction(user.id, null, ModActionType.Ban);
} }
this.displayModAction(modActionId);
} }
/** /**
@ -126,23 +132,19 @@ export class ModActionsPlugin extends Plugin {
user.id user.id
); );
let modActionId;
if (relevantAuditLogEntry) { if (relevantAuditLogEntry) {
const modId = relevantAuditLogEntry.user.id; const modId = relevantAuditLogEntry.user.id;
const auditLogId = relevantAuditLogEntry.id; const auditLogId = relevantAuditLogEntry.id;
modActionId = await this.createModAction( await this.createModAction(
user.id, user.id,
modId, modId,
ActionType.Unban, ModActionType.Unban,
auditLogId auditLogId
); );
} else { } else {
modActionId = await this.createModAction(user.id, null, ActionType.Unban); await this.createModAction(user.id, null, ModActionType.Unban);
} }
this.displayModAction(modActionId);
} }
/** /**
@ -168,9 +170,9 @@ export class ModActionsPlugin extends Plugin {
} }
/** /**
* Update the specified case by adding more details to it * Update the specified case by adding more notes/details to it
*/ */
@d.command("update", "<caseNumber:number> <note:string$>") @d.command(/update|updatecase/, "<caseNumber:number> <note:string$>")
@d.permission("note") @d.permission("note")
async updateCmd(msg: Message, args: any) { async updateCmd(msg: Message, args: any) {
const action = await this.modActions.findByCaseNumber(args.caseNumber); const action = await this.modActions.findByCaseNumber(args.caseNumber);
@ -188,24 +190,128 @@ export class ModActionsPlugin extends Plugin {
} }
await this.createModActionNote(action.id, msg.author.id, args.note); await this.createModActionNote(action.id, msg.author.id, args.note);
this.postModActionToActionLog(action.id); // Post updated action to action log
this.displayModAction(action.id, msg.channel.id);
} }
/**
* Create a new NOTE type mod action and add the specified note to it
*/
@d.command("note", "<userId:string> <note:string$>") @d.command("note", "<userId:string> <note:string$>")
@d.permission("note") @d.permission("note")
async noteCmd(msg: Message, args: any) { async noteCmd(msg: Message, args: any) {
const actionId = await this.createModAction( await this.createModAction(
args.userId, args.userId,
msg.author.id, msg.author.id,
ActionType.Note ModActionType.Note,
null,
args.note
); );
await this.createModActionNote(actionId, msg.author.id, args.note); }
this.displayModAction(actionId, msg.channel.id); @d.command("warn", "<member:Member> <reason:string$>")
@d.permission("warn")
async warnCmd(msg: Message, args: any) {
const warnMessage = this.configValue("warn_message")
.replace("{guildName}", this.guild.name)
.replace("{reason}", args.reason);
if (this.configValue("dm_on_warn")) {
const dmChannel = await this.bot.getDMChannel(args.member.id);
await dmChannel.createMessage(warnMessage);
}
if (this.configValue("message_on_warn")) {
const channel = this.guild.channels.get(
this.configValue("message_channel")
) as TextChannel;
if (channel) {
await channel.createMessage(`<@!${args.member.id}> ${warnMessage}`);
}
}
await this.createModAction(
args.member.id,
msg.author.id,
ModActionType.Warn,
null,
args.reason
);
msg.channel.createMessage(successMessage("Member warned"));
}
@d.command("mute", "<member:Member> [time:string] [reason:string$]")
@d.permission("mute")
async muteCmd(msg: Message, args: any) {
if (!this.configValue("mute_role")) {
msg.channel.createMessage(
errorMessage("Cannot mute: no mute role specified")
);
return;
}
// Make sure we're allowed to mute this member
if (msg.member.id !== args.member.id) {
const ourLevel = this.getMemberLevel(msg.member);
const memberLevel = this.getMemberLevel(args.member);
if (ourLevel <= memberLevel) {
msg.channel.createMessage(
errorMessage("Cannot mute: insufficient permissions")
);
return;
}
}
// Convert mute time from e.g. "2h30m" to milliseconds
const muteTime = args.time ? convertDelayStringToMS(args.time) : null;
if (muteTime == null && args.time) {
// Invalid muteTime -> assume it's actually part of the reason
args.reason = `${args.time} ${args.reason ? args.reason : ""}`.trim();
}
// Apply "muted" role
await args.member.addRole(this.configValue("mute_role"));
await this.mutes.addOrUpdateMute(args.member.id, muteTime);
// Log the action
await this.createModAction(
args.member.id,
msg.author.id,
ModActionType.Mute,
null,
args.reason
);
// Message the user informing them of the mute
if (args.reason) {
const muteMessage = this.configValue("mute_message")
.replace("{guildName}", this.guild.name)
.replace("{reason}", args.reason);
if (this.configValue("dm_on_mute")) {
const dmChannel = await this.bot.getDMChannel(args.member.id);
await dmChannel.createMessage(muteMessage);
}
if (
this.configValue("message_on_mute") &&
this.configValue("message_channel")
) {
const channel = this.guild.channels.get(
this.configValue("message_channel")
) as TextChannel;
await channel.createMessage(`<@!${args.member.id}> ${muteMessage}`);
}
}
// Confirm the action to the moderator
if (muteTime) {
const unmuteTime = moment()
.add(muteTime, "ms")
.format("YYYY-MM-DD HH:mm:ss");
msg.channel.createMessage(
successMessage(`Member muted until ${unmuteTime}`)
);
} else {
msg.channel.createMessage(successMessage(`Member muted indefinitely`));
}
} }
/** /**
@ -248,8 +354,11 @@ export class ModActionsPlugin extends Plugin {
* Shows information about the specified action in a message embed. * Shows information about the specified action in a message embed.
* If no channelId is specified, uses the channel id from config. * If no channelId is specified, uses the channel id from config.
*/ */
protected async displayModAction(actionOrId: any, channelId: string = null) { protected async displayModAction(
let action; actionOrId: ModAction | number,
channelId: string
) {
let action: ModAction;
if (typeof actionOrId === "number") { if (typeof actionOrId === "number") {
action = await this.modActions.find(actionOrId); action = await this.modActions.find(actionOrId);
} else { } else {
@ -257,17 +366,12 @@ export class ModActionsPlugin extends Plugin {
} }
if (!action) return; if (!action) return;
if (!this.guild.channels.get(channelId)) return;
if (!channelId) {
channelId = this.configValue("action_log_channel");
}
if (!channelId) return;
const notes = await this.modActions.getActionNotes(action.id); const notes = await this.modActions.getActionNotes(action.id);
const createdAt = moment(action.created_at); const createdAt = moment(action.created_at);
const actionTypeStr = ActionType[action.action_type].toUpperCase(); const actionTypeStr = ModActionType[action.action_type].toUpperCase();
const embed: any = { const embed: any = {
title: `${actionTypeStr} - Case #${action.case_number}`, title: `${actionTypeStr} - Case #${action.case_number}`,
@ -307,7 +411,10 @@ export class ModActionsPlugin extends Plugin {
}); });
}); });
} else { } else {
embed.addField("!!! THIS CASE HAS NO NOTES !!!", "\u200B"); embed.fields.push({
name: "!!! THIS CASE HAS NO NOTES !!!",
value: "\u200B"
});
} }
(this.bot.guilds (this.bot.guilds
@ -317,6 +424,17 @@ export class ModActionsPlugin extends Plugin {
}); });
} }
/**
* Posts the specified mod action to the guild's action log channel
*/
protected async postModActionToActionLog(actionOrId: ModAction | number) {
const actionLogChannelId = this.configValue("action_log_channel");
if (!actionLogChannelId) return;
if (!this.guild.channels.get(actionLogChannelId)) return;
return this.displayModAction(actionOrId, actionLogChannelId);
}
/** /**
* Attempts to find a relevant audit log entry for the given user and action. Only accepts audit log entries from the past 10 minutes. * Attempts to find a relevant audit log entry for the given user and action. Only accepts audit log entries from the past 10 minutes.
*/ */
@ -348,8 +466,9 @@ export class ModActionsPlugin extends Plugin {
protected async createModAction( protected async createModAction(
userId: string, userId: string,
modId: string, modId: string,
actionType: ActionType, actionType: ModActionType,
auditLogId: string = null auditLogId: string = null,
reason: string = null
): Promise<number> { ): Promise<number> {
const user = this.bot.users.get(userId); const user = this.bot.users.get(userId);
const userName = user const userName = user
@ -361,7 +480,7 @@ export class ModActionsPlugin extends Plugin {
? `${mod.username}#${mod.discriminator}` ? `${mod.username}#${mod.discriminator}`
: "Unknown#0000"; : "Unknown#0000";
return this.modActions.create({ const createdId = await this.modActions.create({
user_id: userId, user_id: userId,
user_name: userName, user_name: userName,
mod_id: modId, mod_id: modId,
@ -369,6 +488,18 @@ export class ModActionsPlugin extends Plugin {
action_type: actionType, action_type: actionType,
audit_log_id: auditLogId audit_log_id: auditLogId
}); });
if (reason) {
await this.createModActionNote(createdId, modId, reason);
}
if (this.configValue("action_log_channel")) {
try {
await this.postModActionToActionLog(createdId);
} catch (e) {} // tslint:disable-line
}
return createdId;
} }
protected async createModActionNote( protected async createModActionNote(
@ -384,7 +515,21 @@ export class ModActionsPlugin extends Plugin {
return this.modActions.createNote(modActionId, { return this.modActions.createNote(modActionId, {
mod_id: modId, mod_id: modId,
mod_name: modName, mod_name: modName,
body 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 {
await member.removeRole(this.configValue("mute_role"));
} catch (e) {} // tslint:disable-line
await this.mutes.clear(member.id);
}
}
} }

44
src/utils.ts Normal file
View file

@ -0,0 +1,44 @@
import * as moment from "moment-timezone";
import { ModActionType } from "./data/ModActionType";
/**
* Turns a "delay string" such as "1h30m" to milliseconds
* @param {String} str
* @returns {Number}
*/
export function convertDelayStringToMS(str) {
const regex = /^([0-9]+)\s*([dhms])?[a-z]*\s*/;
let match;
let ms = 0;
str = str.trim();
// tslint:disable-next-line
while (str !== "" && (match = str.match(regex)) !== null) {
if (match[2] === "d") ms += match[1] * 1000 * 60 * 60 * 24;
else if (match[2] === "h") ms += match[1] * 1000 * 60 * 60;
else if (match[2] === "s") ms += match[1] * 1000;
else if (match[2] === "m" || !match[2]) ms += match[1] * 1000 * 60;
str = str.slice(match[0].length);
}
// Invalid delay string
if (str !== "") {
return null;
}
return ms;
}
export function successMessage(str) {
return `👌 ${str}`;
}
export function errorMessage(str) {
return `${str}`;
}
export function uclower(str) {
return str[0].toLowerCase() + str.slice(1);
}