3
0
Fork 0
mirror of https://github.com/ZeppelinBot/Zeppelin.git synced 2025-03-14 21:31:50 +00:00

Work on better error messages for mod actions, allow muting users that are not on the server; WIP

This commit is contained in:
Dragory 2019-04-18 08:45:51 +03:00
parent 3643c319d4
commit f3e6c05c67
9 changed files with 272 additions and 125 deletions

12
package-lock.json generated
View file

@ -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",

View file

@ -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",

View file

@ -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"
}

View file

@ -12,8 +12,8 @@ type UnknownAction<T extends string> = T extends KnownActions ? never : T;
type ActionFn<T> = (args: T) => any | Promise<any>;
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;

View file

@ -51,4 +51,6 @@ export enum LogType {
VOICE_CHANNEL_FORCE_MOVE,
CASE_UPDATE,
MEMBER_MUTE_REJOIN,
}

View file

@ -155,6 +155,24 @@ export class ModActionsPlugin extends ZeppelinPlugin<IModActionsPluginConfig> {
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<IModActionsPluginConfig> {
msg.channel.createMessage(successMessage(`Note added on **${userName}** (Case #${createdCase.case_number})`));
}
@d.command("warn", "<member:Member> <reason:string$>", {
@d.command("warn", "<userId:string> <reason:string$>", {
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<IModActionsPluginConfig> {
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<IModActionsPluginConfig> {
}
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<IModActionsPluginConfig> {
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<IModActionsPluginConfig> {
this.serverLogs.log(LogType.MEMBER_WARN, {
mod: stripObjectToScalars(mod.user),
member: stripObjectToScalars(args.member, ["user"]),
member: stripObjectToScalars(memberToWarn, ["user"]),
});
}
@d.command("mute", "<member:Member> <time:delay> <reason:string$>", {
overloads: ["<member:Member> <time:delay>", "<member:Member> [reason:string$]"],
@d.command("mute", "<userId:userId> <time:delay> <reason:string$>", {
overloads: ["<userId:userId> <time:delay>", "<userId:userId> [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<IModActionsPluginConfig> {
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<IModActionsPluginConfig> {
},
});
} 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<IModActionsPluginConfig> {
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<IModActionsPluginConfig> {
msg.channel.createMessage(successMessage(response));
}
@d.command("unmute", "<member:Member> <time:delay> <reason:string$>", {
overloads: ["<member:Member> <time:delay>", "<member:Member> [reason:string$]"],
@d.command("unmute", "<userId:userId> <time:delay> <reason:string$>", {
overloads: ["<userId:userId> <time:delay>", "<userId:userId> [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<IModActionsPluginConfig> {
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<IModActionsPluginConfig> {
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<IModActionsPluginConfig> {
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<IModActionsPluginConfig> {
}
}
@d.command("kick", "<member:Member> [reason:string$]", {
@d.command("kick", "<userId:userId> [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<IModActionsPluginConfig> {
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<IModActionsPluginConfig> {
}
// 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<IModActionsPluginConfig> {
});
// 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<IModActionsPluginConfig> {
// 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", "<member:Member> [reason:string$]", {
@d.command("ban", "<userId:userId> [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<IModActionsPluginConfig> {
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<IModActionsPluginConfig> {
}
// 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<IModActionsPluginConfig> {
});
// 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<IModActionsPluginConfig> {
// 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", "<member:Member> [reason:string$]", {
@d.command("softban", "<userId:userId> [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<IModActionsPluginConfig> {
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<IModActionsPluginConfig> {
// 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<IModActionsPluginConfig> {
// Log the action
this.serverLogs.log(LogType.MEMBER_SOFTBAN, {
mod: stripObjectToScalars(mod.user),
member: stripObjectToScalars(args.member, ["user"]),
member: stripObjectToScalars(memberToSoftban, ["user"]),
});
}

View file

@ -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<IMutesPluginConfig> {
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<IMutesPluginConfig> {
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<IMutesPluginConfig> {
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<IMutesPluginConfig> {
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<IMutesPluginConfig> {
}
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<IMutesPluginConfig> {
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<IMutesPluginConfig> {
};
}
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<IMutesPluginConfig> {
}
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<IMutesPluginConfig> {
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<IMutesPluginConfig> {
}
}
/**
* 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
*/

View file

@ -69,35 +69,36 @@ export class PersistPlugin extends ZeppelinPlugin<IPersistPluginConfig> {
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(", "),
});
}
}

View file

@ -245,7 +245,7 @@ export class SpamPlugin extends ZeppelinPlugin<ISpamPluginConfig> {
? 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<ISpamPluginConfig> {
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