diff --git a/package-lock.json b/package-lock.json index fa5f07cc..0e5402b7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5373,9 +5373,9 @@ "dev": true }, "knub": { - "version": "19.3.0", - "resolved": "https://registry.npmjs.org/knub/-/knub-19.3.0.tgz", - "integrity": "sha512-/nOE0bE6a/3GtGLpozhiAulmF1j4MLwpK+s0/PbXBacl/MTbFAoTdND8Nv1GzgTj1PNf+jxVULDuKjFYwytrVA==", + "version": "19.3.2", + "resolved": "https://registry.npmjs.org/knub/-/knub-19.3.2.tgz", + "integrity": "sha512-JFN1XvSSkTYOsTyws+RSWE4pas5yLh1uff6k4fTm62P+KVssAVwBdX0Sw/YvYOo413u2ZSgNrnpIvmpPAWV9Rg==", "requires": { "escape-string-regexp": "^1.0.5", "lodash.at": "^4.6.0", @@ -7768,9 +7768,9 @@ "dev": true }, "ts-essentials": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/ts-essentials/-/ts-essentials-2.0.3.tgz", - "integrity": "sha512-cPF62miIiZf5Q+L1YTKBGk/xIKqDPVm6p+NmaYK5LFaR9Y6Y3gMOJoNXCbXnYJOqnmw5jfF9XJLkT9+vXlPe+g==" + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/ts-essentials/-/ts-essentials-2.0.4.tgz", + "integrity": "sha512-LVtnDkVXrFCPpGG5vShEYXwxO22Snawu7qF7DBKHW9fTQ/NibBg9zLyf4ipeQzNGR/MRuIUisXcdZQD7FGhaKw==" }, "ts-node": { "version": "3.3.0", diff --git a/package.json b/package.json index df9517f3..a43e9bf0 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "escape-string-regexp": "^1.0.5", "humanize-duration": "^3.15.0", "js-yaml": "^3.13.1", - "knub": "^19.3.0", + "knub": "^19.3.2", "lodash.at": "^4.6.0", "lodash.chunk": "^4.2.0", "lodash.difference": "^4.5.0", diff --git a/src/data/DefaultLogMessages.json b/src/data/DefaultLogMessages.json index 70ae4407..65a348c3 100644 --- a/src/data/DefaultLogMessages.json +++ b/src/data/DefaultLogMessages.json @@ -1,8 +1,8 @@ { "MEMBER_WARN": "⚠️ {userMention(member)} was warned by {userMention(mod)}", - "MEMBER_MUTE": "🔇 {userMention(member)} was muted indefinitely by {userMention(mod)}", - "MEMBER_TIMED_MUTE": "🔇 {userMention(member)} was muted for **{time}** by {userMention(mod)}", - "MEMBER_UNMUTE": "🔊 {userMention(member)} was unmuted by {userMention(mod)}", + "MEMBER_MUTE": "🔇 {userMention(user)} was muted indefinitely by {userMention(mod)}", + "MEMBER_TIMED_MUTE": "🔇 {userMention(user)} was muted for **{time}** by {userMention(mod)}", + "MEMBER_UNMUTE": "🔊 {userMention(user)} was unmuted by {userMention(mod)}", "MEMBER_TIMED_UNMUTE": "🔊 {userMention(member)} was scheduled to be unmuted in **{time}** by {userMention(mod)}", "MEMBER_MUTE_EXPIRED": "🔊 {userMention(member)}'s mute expired", "MEMBER_KICK": "👢 {userMention(user)} was kicked by {userMention(mod)}", @@ -17,7 +17,7 @@ "MEMBER_ROLE_CHANGES": "🔑 {userMention(member)}: roles changed: added **{addedRoles}**, removed **{removedRoles}** by {userMention(mod)}", "MEMBER_NICK_CHANGE": "✏ {userMention(member)}: nickname changed from **{oldNick}** to **{newNick}**", "MEMBER_USERNAME_CHANGE": "✏ {userMention(member)}: username changed from **{oldName}** to **{newName}**", - "MEMBER_RESTORE": "💿 {userMention(member)} was restored", + "MEMBER_RESTORE": "💿 Restored {restoredData} for {userMention(member)} on rejoin", "CHANNEL_CREATE": "🖊 Channel {channelMention(channel)} was created", "CHANNEL_DELETE": "🗑 Channel {channelMention(channel)} was deleted", @@ -50,5 +50,7 @@ "MEMBER_JOIN_WITH_PRIOR_RECORDS": "⚠ {userMention(member)} joined with prior records. Recent cases:\n{recentCaseSummary}", - "CASE_UPDATE": "✏ {userMention(mod)} updated case #{caseNumber} ({caseType}) with note:\n```{note}```" + "CASE_UPDATE": "✏ {userMention(mod)} updated case #{caseNumber} ({caseType}) with note:\n```{note}```", + + "MEMBER_MUTE_REJOIN": "⚠ Reapplied active mute for {userMention(member)} on rejoin" } diff --git a/src/data/GuildActions.ts b/src/data/GuildActions.ts index de5a2a4a..f41c91fb 100644 --- a/src/data/GuildActions.ts +++ b/src/data/GuildActions.ts @@ -12,8 +12,8 @@ type UnknownAction = T extends KnownActions ? never : T; type ActionFn = (args: T) => any | Promise; -type MuteActionArgs = { member: Member; muteTime?: number; reason?: string; caseDetails?: ICaseDetails }; -type UnmuteActionArgs = { member: Member; unmuteTime?: number; reason?: string; caseDetails?: ICaseDetails }; +type MuteActionArgs = { userId: string; muteTime?: number; reason?: string; caseDetails?: ICaseDetails }; +type UnmuteActionArgs = { userId: string; unmuteTime?: number; reason?: string; caseDetails?: ICaseDetails }; type CreateCaseActionArgs = ICaseDetails; type CreateCaseNoteActionArgs = { caseId: number; diff --git a/src/data/LogType.ts b/src/data/LogType.ts index 5d726dfe..2ff4db75 100644 --- a/src/data/LogType.ts +++ b/src/data/LogType.ts @@ -51,4 +51,6 @@ export enum LogType { VOICE_CHANNEL_FORCE_MOVE, CASE_UPDATE, + + MEMBER_MUTE_REJOIN, } diff --git a/src/plugins/ModActions.ts b/src/plugins/ModActions.ts index a45503ea..d6256d5f 100644 --- a/src/plugins/ModActions.ts +++ b/src/plugins/ModActions.ts @@ -155,6 +155,24 @@ export class ModActionsPlugin extends ZeppelinPlugin { return ((reason || "") + " " + attachmentUrls.join(" ")).trim(); } + async resolveMember(userId: string): Promise<{ member: Member; isBanned?: boolean }> { + let member = this.guild.members.get(userId); + + if (!member) { + try { + member = await this.bot.getRESTGuildMember(this.guildId, userId); + } catch (e) {} // tslint:disable-line + } + + if (!member) { + const bans = (await this.guild.getBans()) as any; + const isBanned = bans.some(b => b.user.id === userId); + return { member, isBanned }; + } + + return { member }; + } + /** * Add a BAN action automatically when a user is banned. * Attempts to find the ban's details in the audit log. @@ -345,13 +363,25 @@ export class ModActionsPlugin extends ZeppelinPlugin { msg.channel.createMessage(successMessage(`Note added on **${userName}** (Case #${createdCase.case_number})`)); } - @d.command("warn", " ", { + @d.command("warn", " ", { options: [{ name: "mod", type: "member" }], }) @d.permission("can_warn") - async warnCmd(msg: Message, args: any) { + async warnCmd(msg: Message, args: { userId: string; reason: string; mod?: Member }) { + const { member: memberToWarn, isBanned } = await this.resolveMember(args.userId); + + if (!memberToWarn) { + if (isBanned) { + this.sendErrorMessage(msg.channel, `User is banned`); + } else { + this.sendErrorMessage(msg.channel, `User not found on the server`); + } + + return; + } + // Make sure we're allowed to warn this member - if (!this.canActOn(msg.member, args.member)) { + if (!this.canActOn(msg.member, memberToWarn)) { msg.channel.createMessage(errorMessage("Cannot warn: insufficient permissions")); return; } @@ -372,7 +402,7 @@ export class ModActionsPlugin extends ZeppelinPlugin { const warnMessage = config.warn_message.replace("{guildName}", this.guild.name).replace("{reason}", reason); - const userMessageResult = await notifyUser(this.bot, this.guild, args.member.user, warnMessage, { + const userMessageResult = await notifyUser(this.bot, this.guild, memberToWarn.user, warnMessage, { useDM: config.dm_on_warn, useChannel: config.message_on_warn, }); @@ -387,7 +417,7 @@ export class ModActionsPlugin extends ZeppelinPlugin { } const createdCase: Case = await this.actions.fire("createCase", { - userId: args.member.id, + userId: memberToWarn.id, modId: mod.id, type: CaseTypes.Warn, reason, @@ -399,7 +429,7 @@ export class ModActionsPlugin extends ZeppelinPlugin { msg.channel.createMessage( successMessage( - `Warned **${args.member.user.username}#${args.member.user.discriminator}** (Case #${ + `Warned **${memberToWarn.user.username}#${memberToWarn.user.discriminator}** (Case #${ createdCase.case_number })${messageResultText}`, ), @@ -407,18 +437,33 @@ export class ModActionsPlugin extends ZeppelinPlugin { this.serverLogs.log(LogType.MEMBER_WARN, { mod: stripObjectToScalars(mod.user), - member: stripObjectToScalars(args.member, ["user"]), + member: stripObjectToScalars(memberToWarn, ["user"]), }); } - @d.command("mute", " ", { - overloads: [" ", " [reason:string$]"], + @d.command("mute", " ", { + overloads: [" ", " [reason:string$]"], options: [{ name: "mod", type: "member" }], }) @d.permission("can_mute") - async muteCmd(msg: Message, args: { member: Member; time?: number; reason?: string; mod: Member }) { + async muteCmd(msg: Message, args: { userId: string; time?: number; reason?: string; mod: Member }) { + const user = this.bot.users.get(args.userId) || { ...unknownUser, id: args.userId }; + const { member: memberToMute, isBanned } = await this.resolveMember(user.id); + + if (!memberToMute) { + const notOnServerMsg = isBanned + ? await msg.channel.createMessage("User is banned. Apply a mute to them anyway?") + : await msg.channel.createMessage("User is not on the server. Apply a mute to them anyway?"); + + const reply = await waitForReaction(this.bot, notOnServerMsg, ["✅", "❌"], msg.author.id); + notOnServerMsg.delete(); + if (!reply || reply.name === "❌") { + return; + } + } + // Make sure we're allowed to mute this member - if (!this.canActOn(msg.member, args.member)) { + if (memberToMute && !this.canActOn(msg.member, memberToMute)) { msg.channel.createMessage(errorMessage("Cannot mute: insufficient permissions")); return; } @@ -444,7 +489,7 @@ export class ModActionsPlugin extends ZeppelinPlugin { try { muteResult = await this.actions.fire("mute", { - member: args.member, + userId: user.id, muteTime: args.time, reason, caseDetails: { @@ -453,7 +498,7 @@ export class ModActionsPlugin extends ZeppelinPlugin { }, }); } catch (e) { - logger.error(`Failed to mute user ${args.member.id}: ${e.message}`); + logger.error(`Failed to mute user ${user.id}: ${e.stack}`); msg.channel.createMessage(errorMessage("Could not mute the user")); return; } @@ -463,24 +508,24 @@ export class ModActionsPlugin extends ZeppelinPlugin { if (args.time) { if (muteResult.updatedExistingMute) { response = asSingleLine(` - Updated **${args.member.user.username}#${args.member.user.discriminator}**'s + Updated **${user.username}#${user.discriminator}**'s mute to ${timeUntilUnmute} (Case #${muteResult.case.case_number}) `); } else { response = asSingleLine(` - Muted **${args.member.user.username}#${args.member.user.discriminator}** + Muted **${user.username}#${user.discriminator}** for ${timeUntilUnmute} (Case #${muteResult.case.case_number}) `); } } else { if (muteResult.updatedExistingMute) { response = asSingleLine(` - Updated **${args.member.user.username}#${args.member.user.discriminator}**'s + Updated **${user.username}#${user.discriminator}**'s mute to indefinite (Case #${muteResult.case.case_number}) `); } else { response = asSingleLine(` - Muted **${args.member.user.username}#${args.member.user.discriminator}** + Muted **${user.username}#${user.discriminator}** indefinitely (Case #${muteResult.case.case_number}) `); } @@ -490,14 +535,36 @@ export class ModActionsPlugin extends ZeppelinPlugin { msg.channel.createMessage(successMessage(response)); } - @d.command("unmute", " ", { - overloads: [" ", " [reason:string$]"], + @d.command("unmute", " ", { + overloads: [" ", " [reason:string$]"], options: [{ name: "mod", type: "member" }], }) @d.permission("can_mute") - async unmuteCmd(msg: Message, args: { member: Member; time?: number; reason?: string; mod?: Member }) { - // Make sure we're allowed to mute this member - if (!this.canActOn(msg.member, args.member)) { + async unmuteCmd(msg: Message, args: { userId: string; time?: number; reason?: string; mod?: Member }) { + // Check if they're muted in the first place + if (!(await this.mutes.isMuted(args.userId))) { + msg.channel.createMessage(errorMessage("Cannot unmute: member is not muted")); + return; + } + + // Find the server member to unmute + const user = this.bot.users.get(args.userId) || { ...unknownUser, id: args.userId }; + const { member: memberToUnmute, isBanned } = await this.resolveMember(user.id); + + if (!memberToUnmute) { + const notOnServerMsg = isBanned + ? await msg.channel.createMessage("User is banned. Unmute them anyway?") + : await msg.channel.createMessage("User is not on the server. Unmute them anyway?"); + + const reply = await waitForReaction(this.bot, notOnServerMsg, ["✅", "❌"], msg.author.id); + notOnServerMsg.delete(); + if (!reply || reply.name === "❌") { + return; + } + } + + // Make sure we're allowed to unmute this member + if (memberToUnmute && !this.canActOn(msg.member, memberToUnmute)) { msg.channel.createMessage(errorMessage("Cannot unmute: insufficient permissions")); return; } @@ -516,16 +583,10 @@ export class ModActionsPlugin extends ZeppelinPlugin { pp = msg.author; } - // Check if they're muted in the first place - if (!(await this.mutes.isMuted(args.member.id))) { - msg.channel.createMessage(errorMessage("Cannot unmute: member is not muted")); - return; - } - const reason = this.formatReasonWithAttachments(args.reason, msg.attachments); const result = await this.actions.fire("unmute", { - member: args.member, + userId: user.id, unmuteTime: args.time, caseDetails: { modId: mod.id, @@ -540,7 +601,7 @@ export class ModActionsPlugin extends ZeppelinPlugin { msg.channel.createMessage( successMessage( asSingleLine(` - Unmuting **${args.member.user.username}#${args.member.user.discriminator}** + Unmuting **${user.username}#${user.discriminator}** in ${timeUntilUnmute} (Case #${result.case.case_number}) `), ), @@ -549,7 +610,7 @@ export class ModActionsPlugin extends ZeppelinPlugin { msg.channel.createMessage( successMessage( asSingleLine(` - Unmuted **${args.member.user.username}#${args.member.user.discriminator}** + Unmuted **${user.username}#${user.discriminator}** (Case #${result.case.case_number}) `), ), @@ -557,13 +618,25 @@ export class ModActionsPlugin extends ZeppelinPlugin { } } - @d.command("kick", " [reason:string$]", { + @d.command("kick", " [reason:string$]", { options: [{ name: "mod", type: "member" }], }) @d.permission("can_kick") - async kickCmd(msg, args: { member: Member; reason: string; mod: Member }) { + async kickCmd(msg, args: { userId: string; reason: string; mod: Member }) { + const { member: memberToKick, isBanned } = await this.resolveMember(args.userId); + + if (!memberToKick) { + if (isBanned) { + this.sendErrorMessage(msg.channel, `User is banned`); + } else { + this.sendErrorMessage(msg.channel, `User not found on the server`); + } + + return; + } + // Make sure we're allowed to kick this member - if (!this.canActOn(msg.member, args.member)) { + if (!this.canActOn(msg.member, memberToKick)) { msg.channel.createMessage(errorMessage("Cannot kick: insufficient permissions")); return; } @@ -590,7 +663,7 @@ export class ModActionsPlugin extends ZeppelinPlugin { reason, }); - userMessageResult = await notifyUser(this.bot, this.guild, args.member.user, kickMessage, { + userMessageResult = await notifyUser(this.bot, this.guild, memberToKick.user, kickMessage, { useDM: config.dm_on_kick, useChannel: config.message_on_kick, channelId: config.message_channel, @@ -598,13 +671,13 @@ export class ModActionsPlugin extends ZeppelinPlugin { } // Kick the user - this.serverLogs.ignoreLog(LogType.MEMBER_KICK, args.member.id); - this.ignoreEvent(IgnoredEventType.Kick, args.member.id); - args.member.kick(reason); + this.serverLogs.ignoreLog(LogType.MEMBER_KICK, memberToKick.id); + this.ignoreEvent(IgnoredEventType.Kick, memberToKick.id); + memberToKick.kick(reason); // Create a case for this action const createdCase = await this.actions.fire("createCase", { - userId: args.member.id, + userId: memberToKick.id, modId: mod.id, type: CaseTypes.Kick, reason, @@ -613,7 +686,7 @@ export class ModActionsPlugin extends ZeppelinPlugin { }); // Confirm the action to the moderator - let response = `Kicked **${args.member.user.username}#${args.member.user.discriminator}** (Case #${ + let response = `Kicked **${memberToKick.user.username}#${memberToKick.user.discriminator}** (Case #${ createdCase.case_number })`; @@ -623,17 +696,29 @@ export class ModActionsPlugin extends ZeppelinPlugin { // Log the action this.serverLogs.log(LogType.MEMBER_KICK, { mod: stripObjectToScalars(mod.user), - user: stripObjectToScalars(args.member.user), + user: stripObjectToScalars(memberToKick.user), }); } - @d.command("ban", " [reason:string$]", { + @d.command("ban", " [reason:string$]", { options: [{ name: "mod", type: "member" }], }) @d.permission("can_ban") - async banCmd(msg, args: { member: Member; reason?: string; mod?: Member }) { + async banCmd(msg, args: { userId: string; reason?: string; mod?: Member }) { + const { member: memberToBan, isBanned } = await this.resolveMember(args.userId); + + if (!memberToBan) { + if (isBanned) { + this.sendErrorMessage(msg.channel, `User is already banned`); + } else { + this.sendErrorMessage(msg.channel, `User not found on the server`); + } + + return; + } + // Make sure we're allowed to ban this member - if (!this.canActOn(msg.member, args.member)) { + if (!this.canActOn(msg.member, memberToBan)) { msg.channel.createMessage(errorMessage("Cannot ban: insufficient permissions")); return; } @@ -660,7 +745,7 @@ export class ModActionsPlugin extends ZeppelinPlugin { reason, }); - userMessageResult = await notifyUser(this.bot, this.guild, args.member.user, banMessage, { + userMessageResult = await notifyUser(this.bot, this.guild, memberToBan.user, banMessage, { useDM: config.dm_on_ban, useChannel: config.message_on_ban, channelId: config.message_channel, @@ -668,13 +753,13 @@ export class ModActionsPlugin extends ZeppelinPlugin { } // Ban the user - this.serverLogs.ignoreLog(LogType.MEMBER_BAN, args.member.id); - this.ignoreEvent(IgnoredEventType.Ban, args.member.id); - args.member.ban(1, reason); + this.serverLogs.ignoreLog(LogType.MEMBER_BAN, memberToBan.id); + this.ignoreEvent(IgnoredEventType.Ban, memberToBan.id); + memberToBan.ban(1, reason); // Create a case for this action const createdCase = await this.actions.fire("createCase", { - userId: args.member.id, + userId: memberToBan.id, modId: mod.id, type: CaseTypes.Ban, reason, @@ -683,7 +768,7 @@ export class ModActionsPlugin extends ZeppelinPlugin { }); // Confirm the action to the moderator - let response = `Banned **${args.member.user.username}#${args.member.user.discriminator}** (Case #${ + let response = `Banned **${memberToBan.user.username}#${memberToBan.user.discriminator}** (Case #${ createdCase.case_number })`; @@ -693,17 +778,29 @@ export class ModActionsPlugin extends ZeppelinPlugin { // Log the action this.serverLogs.log(LogType.MEMBER_BAN, { mod: stripObjectToScalars(mod.user), - user: stripObjectToScalars(args.member.user), + user: stripObjectToScalars(memberToBan.user), }); } - @d.command("softban", " [reason:string$]", { + @d.command("softban", " [reason:string$]", { options: [{ name: "mod", type: "member" }], }) @d.permission("can_ban") - async softbanCmd(msg, args) { + async softbanCmd(msg, args: { userId: string; reason: string; mod?: Member }) { + const { member: memberToSoftban, isBanned } = await this.resolveMember(args.userId); + + if (!memberToSoftban) { + if (isBanned) { + this.sendErrorMessage(msg.channel, `User is already banned`); + } else { + this.sendErrorMessage(msg.channel, `User not found on the server`); + } + + return; + } + // Make sure we're allowed to ban this member - if (!this.canActOn(msg.member, args.member)) { + if (!this.canActOn(msg.member, memberToSoftban)) { msg.channel.createMessage(errorMessage("Cannot ban: insufficient permissions")); return; } @@ -722,17 +819,17 @@ export class ModActionsPlugin extends ZeppelinPlugin { const reason = this.formatReasonWithAttachments(args.reason, msg.attachments); // Softban the user = ban, and immediately unban - this.serverLogs.ignoreLog(LogType.MEMBER_BAN, args.member.id); - this.serverLogs.ignoreLog(LogType.MEMBER_UNBAN, args.member.id); - this.ignoreEvent(IgnoredEventType.Ban, args.member.id); - this.ignoreEvent(IgnoredEventType.Unban, args.member.id); + this.serverLogs.ignoreLog(LogType.MEMBER_BAN, memberToSoftban.id); + this.serverLogs.ignoreLog(LogType.MEMBER_UNBAN, memberToSoftban.id); + this.ignoreEvent(IgnoredEventType.Ban, memberToSoftban.id); + this.ignoreEvent(IgnoredEventType.Unban, memberToSoftban.id); - await args.member.ban(1, reason); - await this.guild.unbanMember(args.member.id); + await memberToSoftban.ban(1, reason); + await this.guild.unbanMember(memberToSoftban.id); // Create a case for this action const createdCase = await this.actions.fire("createCase", { - userId: args.member.id, + userId: memberToSoftban.id, modId: mod.id, type: CaseTypes.Softban, reason, @@ -742,7 +839,7 @@ export class ModActionsPlugin extends ZeppelinPlugin { // Confirm the action to the moderator msg.channel.createMessage( successMessage( - `Softbanned **${args.member.user.username}#${args.member.user.discriminator}** (Case #${ + `Softbanned **${memberToSoftban.user.username}#${memberToSoftban.user.discriminator}** (Case #${ createdCase.case_number })`, ), @@ -751,7 +848,7 @@ export class ModActionsPlugin extends ZeppelinPlugin { // Log the action this.serverLogs.log(LogType.MEMBER_SOFTBAN, { mod: stripObjectToScalars(mod.user), - member: stripObjectToScalars(args.member, ["user"]), + member: stripObjectToScalars(memberToSoftban, ["user"]), }); } diff --git a/src/plugins/Mutes.ts b/src/plugins/Mutes.ts index 397a5b27..d16bb4b6 100644 --- a/src/plugins/Mutes.ts +++ b/src/plugins/Mutes.ts @@ -14,6 +14,7 @@ import { stripObjectToScalars, successMessage, ucfirst, + unknownUser, } from "../utils"; import humanizeDuration from "humanize-duration"; import { LogType } from "../data/LogType"; @@ -90,10 +91,10 @@ export class MutesPlugin extends ZeppelinPlugin { this.serverLogs = new GuildLogs(this.guildId); this.actions.register("mute", args => { - return this.muteMember(args.member, args.muteTime, args.reason, args.caseDetails); + return this.muteUser(args.userId, args.muteTime, args.reason, args.caseDetails); }); this.actions.register("unmute", args => { - return this.unmuteMember(args.member, args.unmuteTime, args.caseDetails); + return this.unmuteUser(args.userId, args.unmuteTime, args.caseDetails); }); // Check for expired mutes every 5s @@ -108,8 +109,21 @@ export class MutesPlugin extends ZeppelinPlugin { clearInterval(this.muteClearIntervalId); } - public async muteMember( - member: Member, + async resolveMember(userId: string) { + if (this.guild.members.has(userId)) { + return this.guild.members.get(userId); + } + + try { + const member = await this.bot.getRESTGuildMember(this.guildId, userId); + return member; + } catch (e) { + return null; + } + } + + public async muteUser( + userId: string, muteTime: number = null, reason: string = null, caseDetails: ICaseDetails = {}, @@ -124,32 +138,37 @@ export class MutesPlugin extends ZeppelinPlugin { caseDetails.modId = this.bot.user.id; } - // Apply mute role if it's missing - if (!member.roles.includes(muteRole)) { - await member.addRole(muteRole); - } + const user = this.bot.users.get(userId) || { ...unknownUser, id: userId }; + const member = await this.resolveMember(userId); - // If enabled, move the user to the mute voice channel (e.g. afk - just to apply the voice perms from the mute role) - const moveToVoiceChannelId = this.getConfig().move_to_voice_channel; - if (moveToVoiceChannelId && member.voiceState.channelID) { - try { - await member.edit({ channelID: moveToVoiceChannelId }); - } catch (e) { - logger.warn(`Could not move user ${member.id} to voice channel ${moveToVoiceChannelId} when muting`); + if (member) { + // Apply mute role if it's missing + if (!member.roles.includes(muteRole)) { + await member.addRole(muteRole); + } + + // If enabled, move the user to the mute voice channel (e.g. afk - just to apply the voice perms from the mute role) + const moveToVoiceChannelId = this.getConfig().move_to_voice_channel; + if (moveToVoiceChannelId && member.voiceState.channelID) { + try { + await member.edit({ channelID: moveToVoiceChannelId }); + } catch (e) { + logger.warn(`Could not move user ${member.id} to voice channel ${moveToVoiceChannelId} when muting`); + } } } // If the user is already muted, update the duration of their existing mute - const existingMute = await this.mutes.findExistingMuteForUserId(member.id); + const existingMute = await this.mutes.findExistingMuteForUserId(user.id); let notifyResult: INotifyUserResult = { status: NotifyUserStatus.Ignored }; if (existingMute) { - await this.mutes.updateExpiryTime(member.id, muteTime); + await this.mutes.updateExpiryTime(user.id, muteTime); } else { - await this.mutes.addMute(member.id, muteTime); + await this.mutes.addMute(user.id, muteTime); // If it's a new mute, attempt to message the user - const config = this.getMatchingConfig({ member }); + const config = this.getMatchingConfig({ member, userId }); const template = muteTime ? config.timed_mute_message : config.mute_message; const muteMessage = @@ -160,12 +179,16 @@ export class MutesPlugin extends ZeppelinPlugin { time: timeUntilUnmute, })); - if (muteMessage) { - notifyResult = await notifyUser(this.bot, this.guild, member.user, muteMessage, { - useDM: config.dm_on_mute, - useChannel: config.message_on_mute, - channelId: config.message_channel, - }); + if (reason && muteMessage) { + if (user instanceof User) { + notifyResult = await notifyUser(this.bot, this.guild, user, muteMessage, { + useDM: config.dm_on_mute, + useChannel: config.message_on_mute, + channelId: config.message_channel, + }); + } else { + notifyResult = { status: NotifyUserStatus.Failed }; + } } } @@ -189,7 +212,7 @@ export class MutesPlugin extends ZeppelinPlugin { } theCase = await this.actions.fire("createCase", { - userId: member.id, + userId, modId: caseDetails.modId, type: CaseTypes.Mute, reason, @@ -197,20 +220,21 @@ export class MutesPlugin extends ZeppelinPlugin { noteDetails, extraNotes: caseDetails.extraNotes, }); - await this.mutes.setCaseId(member.id, theCase.id); + await this.mutes.setCaseId(user.id, theCase.id); } // Log the action + const mod = this.bot.users.get(caseDetails.modId) || { ...unknownUser, id: caseDetails.modId }; if (muteTime) { this.serverLogs.log(LogType.MEMBER_TIMED_MUTE, { - mod: stripObjectToScalars(caseDetails.modId), - member: stripObjectToScalars(member, ["user"]), + mod: stripObjectToScalars(mod), + user: stripObjectToScalars(user), time: timeUntilUnmute, }); } else { this.serverLogs.log(LogType.MEMBER_MUTE, { - mod: stripObjectToScalars(caseDetails.modId), - member: stripObjectToScalars(member, ["user"]), + mod: stripObjectToScalars(mod), + user: stripObjectToScalars(user), }); } @@ -221,21 +245,26 @@ export class MutesPlugin extends ZeppelinPlugin { }; } - public async unmuteMember(member: Member, unmuteTime: number = null, caseDetails: ICaseDetails = {}) { - const existingMute = await this.mutes.findExistingMuteForUserId(member.id); + public async unmuteUser(userId: string, unmuteTime: number = null, caseDetails: ICaseDetails = {}) { + const existingMute = await this.mutes.findExistingMuteForUserId(userId); if (!existingMute) return; + const user = this.bot.users.get(userId) || { ...unknownUser, id: userId }; + const member = await this.resolveMember(userId); + if (unmuteTime) { // Schedule timed unmute (= just set the mute's duration) - await this.mutes.updateExpiryTime(member.id, unmuteTime); + await this.mutes.updateExpiryTime(userId, unmuteTime); } else { // Unmute immediately - const muteRole = this.getConfig().mute_role; - if (member.roles.includes(muteRole)) { - await member.removeRole(muteRole); + if (member) { + const muteRole = this.getConfig().mute_role; + if (member.roles.includes(muteRole)) { + await member.removeRole(muteRole); + } } - await this.mutes.clear(member.id); + await this.mutes.clear(userId); } const timeUntilUnmute = unmuteTime && humanizeDuration(unmuteTime); @@ -249,7 +278,7 @@ export class MutesPlugin extends ZeppelinPlugin { } const createdCase = await this.actions.fire("createCase", { - userId: member.id, + userId, modId: caseDetails.modId, type: CaseTypes.Unmute, reason: caseDetails.reason, @@ -262,13 +291,13 @@ export class MutesPlugin extends ZeppelinPlugin { if (unmuteTime) { this.serverLogs.log(LogType.MEMBER_TIMED_UNMUTE, { mod: stripObjectToScalars(mod), - member: stripObjectToScalars(member, ["user"]), + user: stripObjectToScalars(user), time: timeUntilUnmute, }); } else { this.serverLogs.log(LogType.MEMBER_UNMUTE, { mod: stripObjectToScalars(mod), - member: stripObjectToScalars(member, ["user"]), + user: stripObjectToScalars(user), }); } @@ -416,6 +445,22 @@ export class MutesPlugin extends ZeppelinPlugin { } } + /** + * Reapply active mutes on join + */ + @d.event("guildMemberAdd") + async onGuildMemberAdd(_, member: Member) { + const mute = await this.mutes.findExistingMuteForUserId(member.id); + if (mute) { + const muteRole = this.getConfig().mute_role; + await member.addRole(muteRole); + + this.serverLogs.log(LogType.MEMBER_MUTE_REJOIN, { + member: stripObjectToScalars(member, ["user"]), + }); + } + } + /** * Clear active mute from the member if the member is banned */ diff --git a/src/plugins/Persist.ts b/src/plugins/Persist.ts index 7f0f8498..f7bb6d37 100644 --- a/src/plugins/Persist.ts +++ b/src/plugins/Persist.ts @@ -69,35 +69,36 @@ export class PersistPlugin extends ZeppelinPlugin { const persistedData = await this.persistedData.find(member.id); if (!persistedData) return; - let restore = false; const toRestore: MemberOptions = {}; const config = this.getConfig(); + const restoredData = []; const persistedRoles = config.persisted_roles; if (persistedRoles.length) { const rolesToRestore = intersection(persistedRoles, persistedData.roles); if (rolesToRestore.length) { - restore = true; + restoredData.push("roles"); toRestore.roles = rolesToRestore; } } if (config.persist_nicknames && persistedData.nickname) { - restore = true; + restoredData.push("nickname"); toRestore.nick = persistedData.nickname; } if (config.persist_voice_mutes && persistedData.is_voice_muted) { - restore = true; + restoredData.push("voice mute"); toRestore.mute = true; } - if (restore) { + if (restoredData.length) { await member.edit(toRestore, "Restored upon rejoin"); await this.persistedData.clear(member.id); this.logs.log(LogType.MEMBER_RESTORE, { member: stripObjectToScalars(member, ["user"]), + restoredData: restoredData.join(", "), }); } } diff --git a/src/plugins/Spam.ts b/src/plugins/Spam.ts index ec36560d..d28508e9 100644 --- a/src/plugins/Spam.ts +++ b/src/plugins/Spam.ts @@ -245,7 +245,7 @@ export class SpamPlugin extends ZeppelinPlugin { ? convertDelayStringToMS(spamConfig.mute_time.toString()) : 120 * 1000; muteResult = await this.actions.fire("mute", { - member, + userId: member.id, muteTime, reason: "Automatic spam detection", caseDetails: { @@ -366,7 +366,7 @@ export class SpamPlugin extends ZeppelinPlugin { if (spamConfig.mute && member) { const muteTime = spamConfig.mute_time ? spamConfig.mute_time * 60 * 1000 : 120 * 1000; this.logs.ignoreLog(LogType.MEMBER_ROLE_ADD, userId); - this.actions.fire("mute", { member, muteTime, reason: "Automatic spam detection" }); + this.actions.fire("mute", { userId: member.id, muteTime, reason: "Automatic spam detection" }); } // Clear recent cases