From 7a372533ec96374e4bf229a90e503d9a0acf26ef Mon Sep 17 00:00:00 2001 From: Dragory Date: Sun, 5 Aug 2018 00:18:50 +0300 Subject: [PATCH] Add case_id to mutes. Add !mutes command to list mutes. --- .../20180801235500_add_case_id_to_mutes.js | 12 +++ package-lock.json | 5 ++ package.json | 1 + src/data/GuildCases.ts | 8 ++ src/data/GuildMutes.ts | 16 ++++ src/models/Mute.ts | 1 + src/plugins/ModActions.ts | 85 ++++++++++++++++++- src/plugins/Spam.ts | 4 +- src/utils.ts | 2 + 9 files changed, 131 insertions(+), 3 deletions(-) create mode 100644 migrations/20180801235500_add_case_id_to_mutes.js diff --git a/migrations/20180801235500_add_case_id_to_mutes.js b/migrations/20180801235500_add_case_id_to_mutes.js new file mode 100644 index 00000000..e89e2952 --- /dev/null +++ b/migrations/20180801235500_add_case_id_to_mutes.js @@ -0,0 +1,12 @@ +exports.up = async function(knex, Promise) { + await knex.schema.table('mutes', table => { + table.integer('case_id').unsigned().nullable().defaultTo(null).after('user_id').references('id').inTable('cases').onDelete('SET NULL'); + }); +}; + +exports.down = async function(knex, Promise) { + await knex.schema.table('mutes', table => { + table.dropForeign('case_id'); + table.dropColumn('case_id'); + }); +}; diff --git a/package-lock.json b/package-lock.json index 63896586..3ca097f9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2824,6 +2824,11 @@ "resolved": "https://registry.npmjs.org/lodash.at/-/lodash.at-4.6.0.tgz", "integrity": "sha1-k83OZk8KGZTqM9181A4jr9EbD/g=" }, + "lodash.chunk": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.chunk/-/lodash.chunk-4.2.0.tgz", + "integrity": "sha1-ZuXOH3btJ7QwPYxlEujRIW6BBrw=" + }, "lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", diff --git a/package.json b/package.json index 06a26a7b..21cd9d8f 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "knex": "0.12.6", "knub": "^9.6.4", "lodash.at": "^4.6.0", + "lodash.chunk": "^4.2.0", "lodash.difference": "^4.5.0", "lodash.intersection": "^4.4.0", "lodash.isequal": "^4.5.0", diff --git a/src/data/GuildCases.ts b/src/data/GuildCases.ts index b0fbbc25..e9075874 100644 --- a/src/data/GuildCases.ts +++ b/src/data/GuildCases.ts @@ -9,6 +9,14 @@ export class GuildCases { this.guildId = guildId; } + async get(ids: number[]): Promise { + const result = await knex("cases") + .whereIn("id", ids) + .select(); + + return result.map(r => new Case(r)); + } + async find(id: number): Promise { const result = await knex("cases") .where("guild_id", this.guildId) diff --git a/src/data/GuildMutes.ts b/src/data/GuildMutes.ts index 50c49800..59e35d01 100644 --- a/src/data/GuildMutes.ts +++ b/src/data/GuildMutes.ts @@ -69,6 +69,22 @@ export class GuildMutes { } } + async getActiveMutes(): Promise { + const result = await knex("mutes") + .where("guild_id", this.guildId) + .where(q => q.whereRaw("expires_at > NOW()").orWhereNull("expires_at")) + .select(); + + return result.map(r => new Mute(r)); + } + + async setCaseId(userId, caseId) { + await knex("mutes") + .where("guild_id", this.guildId) + .where("user_id", userId) + .update({ case_id: caseId }); + } + async clear(userId) { return knex("mutes") .where("guild_id", this.guildId) diff --git a/src/models/Mute.ts b/src/models/Mute.ts index 385bf6f9..2e58bc4b 100644 --- a/src/models/Mute.ts +++ b/src/models/Mute.ts @@ -3,6 +3,7 @@ import Model from "./Model"; export default class Mute extends Model { public guild_id: string; public user_id: string; + public case_id: number; public created_at: string; public expires_at: string; } diff --git a/src/plugins/ModActions.ts b/src/plugins/ModActions.ts index 00e2d414..bb9a7914 100644 --- a/src/plugins/ModActions.ts +++ b/src/plugins/ModActions.ts @@ -2,9 +2,11 @@ import { decorators as d, Plugin, waitForReaction } from "knub"; import { Constants as ErisConstants, Guild, Member, Message, TextChannel, User } from "eris"; import moment from "moment-timezone"; import humanizeDuration from "humanize-duration"; +import chunk from "lodash.chunk"; import { GuildCases } from "../data/GuildCases"; import { convertDelayStringToMS, + DBDateFormat, disableLinkPreviews, errorMessage, findRelevantAuditLogEntry, @@ -36,8 +38,8 @@ interface IIgnoredEvent { const CASE_LIST_REASON_MAX_LENGTH = 80; export class ModActionsPlugin extends Plugin { + public mutes: GuildMutes; protected cases: GuildCases; - protected mutes: GuildMutes; protected serverLogs: GuildLogs; protected muteClearIntervalId: Timer; @@ -355,7 +357,14 @@ export class ModActionsPlugin extends Plugin { this.muteMember(args.member, muteTime, args.reason); // Create a case - await this.createCase(args.member.id, msg.author.id, CaseType.Mute, null, args.reason); + const caseId = await this.createCase( + args.member.id, + msg.author.id, + CaseType.Mute, + null, + args.reason + ); + await this.mutes.setCaseId(args.member.id, caseId); // Message the user informing them of the mute let messageSent = true; @@ -441,6 +450,78 @@ export class ModActionsPlugin extends Plugin { }); } + @d.command("mutes") + @d.permission("view") + async mutesCmd(msg: Message) { + 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 cases = caseIds ? 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", " [reason:string$]") @d.permission("kick") async kickCmd(msg, args) { diff --git a/src/plugins/Spam.ts b/src/plugins/Spam.ts index 34e005ed..3b436663 100644 --- a/src/plugins/Spam.ts +++ b/src/plugins/Spam.ts @@ -192,7 +192,7 @@ export class SpamPlugin extends Plugin { spamConfig.mute_time ? spamConfig.mute_time * 60 * 1000 : 120 * 1000, "Automatic spam detection" ); - await modActionsPlugin.createCase( + const caseId = await modActionsPlugin.createCase( msg.member.id, this.bot.user.id, CaseType.Mute, @@ -205,6 +205,8 @@ export class SpamPlugin extends Plugin { `), true ); + await modActionsPlugin.mutes.setCaseId(msg.member.id, caseId); + this.logs.log(LogType.MEMBER_MUTE_SPAM, { member: stripObjectToScalars(msg.member, ["user"]), channel: stripObjectToScalars(msg.channel), diff --git a/src/utils.ts b/src/utils.ts index d04a5b5f..a6b4649f 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -274,3 +274,5 @@ export function getRoleMentions(str: string) { export function disableLinkPreviews(str: string): string { return str.replace(/(?"); } + +export const DBDateFormat = "YYYY-MM-DD HH:mm:ss";