3
0
Fork 0
mirror of https://github.com/ZeppelinBot/Zeppelin.git synced 2025-03-15 05:41:51 +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 "dev": true
}, },
"knub": { "knub": {
"version": "19.3.0", "version": "19.3.2",
"resolved": "https://registry.npmjs.org/knub/-/knub-19.3.0.tgz", "resolved": "https://registry.npmjs.org/knub/-/knub-19.3.2.tgz",
"integrity": "sha512-/nOE0bE6a/3GtGLpozhiAulmF1j4MLwpK+s0/PbXBacl/MTbFAoTdND8Nv1GzgTj1PNf+jxVULDuKjFYwytrVA==", "integrity": "sha512-JFN1XvSSkTYOsTyws+RSWE4pas5yLh1uff6k4fTm62P+KVssAVwBdX0Sw/YvYOo413u2ZSgNrnpIvmpPAWV9Rg==",
"requires": { "requires": {
"escape-string-regexp": "^1.0.5", "escape-string-regexp": "^1.0.5",
"lodash.at": "^4.6.0", "lodash.at": "^4.6.0",
@ -7768,9 +7768,9 @@
"dev": true "dev": true
}, },
"ts-essentials": { "ts-essentials": {
"version": "2.0.3", "version": "2.0.4",
"resolved": "https://registry.npmjs.org/ts-essentials/-/ts-essentials-2.0.3.tgz", "resolved": "https://registry.npmjs.org/ts-essentials/-/ts-essentials-2.0.4.tgz",
"integrity": "sha512-cPF62miIiZf5Q+L1YTKBGk/xIKqDPVm6p+NmaYK5LFaR9Y6Y3gMOJoNXCbXnYJOqnmw5jfF9XJLkT9+vXlPe+g==" "integrity": "sha512-LVtnDkVXrFCPpGG5vShEYXwxO22Snawu7qF7DBKHW9fTQ/NibBg9zLyf4ipeQzNGR/MRuIUisXcdZQD7FGhaKw=="
}, },
"ts-node": { "ts-node": {
"version": "3.3.0", "version": "3.3.0",

View file

@ -26,7 +26,7 @@
"escape-string-regexp": "^1.0.5", "escape-string-regexp": "^1.0.5",
"humanize-duration": "^3.15.0", "humanize-duration": "^3.15.0",
"js-yaml": "^3.13.1", "js-yaml": "^3.13.1",
"knub": "^19.3.0", "knub": "^19.3.2",
"lodash.at": "^4.6.0", "lodash.at": "^4.6.0",
"lodash.chunk": "^4.2.0", "lodash.chunk": "^4.2.0",
"lodash.difference": "^4.5.0", "lodash.difference": "^4.5.0",

View file

@ -1,8 +1,8 @@
{ {
"MEMBER_WARN": "⚠️ {userMention(member)} was warned by {userMention(mod)}", "MEMBER_WARN": "⚠️ {userMention(member)} was warned by {userMention(mod)}",
"MEMBER_MUTE": "🔇 {userMention(member)} was muted indefinitely by {userMention(mod)}", "MEMBER_MUTE": "🔇 {userMention(user)} was muted indefinitely by {userMention(mod)}",
"MEMBER_TIMED_MUTE": "🔇 {userMention(member)} was muted for **{time}** by {userMention(mod)}", "MEMBER_TIMED_MUTE": "🔇 {userMention(user)} was muted for **{time}** by {userMention(mod)}",
"MEMBER_UNMUTE": "🔊 {userMention(member)} was unmuted 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_TIMED_UNMUTE": "🔊 {userMention(member)} was scheduled to be unmuted in **{time}** by {userMention(mod)}",
"MEMBER_MUTE_EXPIRED": "🔊 {userMention(member)}'s mute expired", "MEMBER_MUTE_EXPIRED": "🔊 {userMention(member)}'s mute expired",
"MEMBER_KICK": "👢 {userMention(user)} was kicked by {userMention(mod)}", "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_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_NICK_CHANGE": "✏ {userMention(member)}: nickname changed from **{oldNick}** to **{newNick}**",
"MEMBER_USERNAME_CHANGE": "✏ {userMention(member)}: username changed from **{oldName}** to **{newName}**", "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_CREATE": "🖊 Channel {channelMention(channel)} was created",
"CHANNEL_DELETE": "🗑 Channel {channelMention(channel)} was deleted", "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}", "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 ActionFn<T> = (args: T) => any | Promise<any>;
type MuteActionArgs = { member: Member; muteTime?: number; reason?: string; caseDetails?: ICaseDetails }; type MuteActionArgs = { userId: string; muteTime?: number; reason?: string; caseDetails?: ICaseDetails };
type UnmuteActionArgs = { member: Member; unmuteTime?: number; reason?: string; caseDetails?: ICaseDetails }; type UnmuteActionArgs = { userId: string; unmuteTime?: number; reason?: string; caseDetails?: ICaseDetails };
type CreateCaseActionArgs = ICaseDetails; type CreateCaseActionArgs = ICaseDetails;
type CreateCaseNoteActionArgs = { type CreateCaseNoteActionArgs = {
caseId: number; caseId: number;

View file

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

View file

@ -155,6 +155,24 @@ export class ModActionsPlugin extends ZeppelinPlugin<IModActionsPluginConfig> {
return ((reason || "") + " " + attachmentUrls.join(" ")).trim(); 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. * Add a BAN action automatically when a user is banned.
* Attempts to find the ban's details in the audit log. * 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})`)); 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" }], options: [{ name: "mod", type: "member" }],
}) })
@d.permission("can_warn") @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 // 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")); msg.channel.createMessage(errorMessage("Cannot warn: insufficient permissions"));
return; 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 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, useDM: config.dm_on_warn,
useChannel: config.message_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", { const createdCase: Case = await this.actions.fire("createCase", {
userId: args.member.id, userId: memberToWarn.id,
modId: mod.id, modId: mod.id,
type: CaseTypes.Warn, type: CaseTypes.Warn,
reason, reason,
@ -399,7 +429,7 @@ export class ModActionsPlugin extends ZeppelinPlugin<IModActionsPluginConfig> {
msg.channel.createMessage( msg.channel.createMessage(
successMessage( successMessage(
`Warned **${args.member.user.username}#${args.member.user.discriminator}** (Case #${ `Warned **${memberToWarn.user.username}#${memberToWarn.user.discriminator}** (Case #${
createdCase.case_number createdCase.case_number
})${messageResultText}`, })${messageResultText}`,
), ),
@ -407,18 +437,33 @@ export class ModActionsPlugin extends ZeppelinPlugin<IModActionsPluginConfig> {
this.serverLogs.log(LogType.MEMBER_WARN, { this.serverLogs.log(LogType.MEMBER_WARN, {
mod: stripObjectToScalars(mod.user), mod: stripObjectToScalars(mod.user),
member: stripObjectToScalars(args.member, ["user"]), member: stripObjectToScalars(memberToWarn, ["user"]),
}); });
} }
@d.command("mute", "<member:Member> <time:delay> <reason:string$>", { @d.command("mute", "<userId:userId> <time:delay> <reason:string$>", {
overloads: ["<member:Member> <time:delay>", "<member:Member> [reason:string$]"], overloads: ["<userId:userId> <time:delay>", "<userId:userId> [reason:string$]"],
options: [{ name: "mod", type: "member" }], options: [{ name: "mod", type: "member" }],
}) })
@d.permission("can_mute") @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 // 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")); msg.channel.createMessage(errorMessage("Cannot mute: insufficient permissions"));
return; return;
} }
@ -444,7 +489,7 @@ export class ModActionsPlugin extends ZeppelinPlugin<IModActionsPluginConfig> {
try { try {
muteResult = await this.actions.fire("mute", { muteResult = await this.actions.fire("mute", {
member: args.member, userId: user.id,
muteTime: args.time, muteTime: args.time,
reason, reason,
caseDetails: { caseDetails: {
@ -453,7 +498,7 @@ export class ModActionsPlugin extends ZeppelinPlugin<IModActionsPluginConfig> {
}, },
}); });
} catch (e) { } 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")); msg.channel.createMessage(errorMessage("Could not mute the user"));
return; return;
} }
@ -463,24 +508,24 @@ export class ModActionsPlugin extends ZeppelinPlugin<IModActionsPluginConfig> {
if (args.time) { if (args.time) {
if (muteResult.updatedExistingMute) { if (muteResult.updatedExistingMute) {
response = asSingleLine(` 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}) mute to ${timeUntilUnmute} (Case #${muteResult.case.case_number})
`); `);
} else { } else {
response = asSingleLine(` response = asSingleLine(`
Muted **${args.member.user.username}#${args.member.user.discriminator}** Muted **${user.username}#${user.discriminator}**
for ${timeUntilUnmute} (Case #${muteResult.case.case_number}) for ${timeUntilUnmute} (Case #${muteResult.case.case_number})
`); `);
} }
} else { } else {
if (muteResult.updatedExistingMute) { if (muteResult.updatedExistingMute) {
response = asSingleLine(` 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}) mute to indefinite (Case #${muteResult.case.case_number})
`); `);
} else { } else {
response = asSingleLine(` response = asSingleLine(`
Muted **${args.member.user.username}#${args.member.user.discriminator}** Muted **${user.username}#${user.discriminator}**
indefinitely (Case #${muteResult.case.case_number}) indefinitely (Case #${muteResult.case.case_number})
`); `);
} }
@ -490,14 +535,36 @@ export class ModActionsPlugin extends ZeppelinPlugin<IModActionsPluginConfig> {
msg.channel.createMessage(successMessage(response)); msg.channel.createMessage(successMessage(response));
} }
@d.command("unmute", "<member:Member> <time:delay> <reason:string$>", { @d.command("unmute", "<userId:userId> <time:delay> <reason:string$>", {
overloads: ["<member:Member> <time:delay>", "<member:Member> [reason:string$]"], overloads: ["<userId:userId> <time:delay>", "<userId:userId> [reason:string$]"],
options: [{ name: "mod", type: "member" }], options: [{ name: "mod", type: "member" }],
}) })
@d.permission("can_mute") @d.permission("can_mute")
async unmuteCmd(msg: Message, args: { member: Member; time?: number; reason?: string; mod?: Member }) { async unmuteCmd(msg: Message, args: { userId: string; time?: number; reason?: string; mod?: Member }) {
// Make sure we're allowed to mute this member // Check if they're muted in the first place
if (!this.canActOn(msg.member, args.member)) { 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")); msg.channel.createMessage(errorMessage("Cannot unmute: insufficient permissions"));
return; return;
} }
@ -516,16 +583,10 @@ export class ModActionsPlugin extends ZeppelinPlugin<IModActionsPluginConfig> {
pp = msg.author; 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 reason = this.formatReasonWithAttachments(args.reason, msg.attachments);
const result = await this.actions.fire("unmute", { const result = await this.actions.fire("unmute", {
member: args.member, userId: user.id,
unmuteTime: args.time, unmuteTime: args.time,
caseDetails: { caseDetails: {
modId: mod.id, modId: mod.id,
@ -540,7 +601,7 @@ export class ModActionsPlugin extends ZeppelinPlugin<IModActionsPluginConfig> {
msg.channel.createMessage( msg.channel.createMessage(
successMessage( successMessage(
asSingleLine(` asSingleLine(`
Unmuting **${args.member.user.username}#${args.member.user.discriminator}** Unmuting **${user.username}#${user.discriminator}**
in ${timeUntilUnmute} (Case #${result.case.case_number}) in ${timeUntilUnmute} (Case #${result.case.case_number})
`), `),
), ),
@ -549,7 +610,7 @@ export class ModActionsPlugin extends ZeppelinPlugin<IModActionsPluginConfig> {
msg.channel.createMessage( msg.channel.createMessage(
successMessage( successMessage(
asSingleLine(` asSingleLine(`
Unmuted **${args.member.user.username}#${args.member.user.discriminator}** Unmuted **${user.username}#${user.discriminator}**
(Case #${result.case.case_number}) (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" }], options: [{ name: "mod", type: "member" }],
}) })
@d.permission("can_kick") @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 // 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")); msg.channel.createMessage(errorMessage("Cannot kick: insufficient permissions"));
return; return;
} }
@ -590,7 +663,7 @@ export class ModActionsPlugin extends ZeppelinPlugin<IModActionsPluginConfig> {
reason, 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, useDM: config.dm_on_kick,
useChannel: config.message_on_kick, useChannel: config.message_on_kick,
channelId: config.message_channel, channelId: config.message_channel,
@ -598,13 +671,13 @@ export class ModActionsPlugin extends ZeppelinPlugin<IModActionsPluginConfig> {
} }
// Kick the user // Kick the user
this.serverLogs.ignoreLog(LogType.MEMBER_KICK, args.member.id); this.serverLogs.ignoreLog(LogType.MEMBER_KICK, memberToKick.id);
this.ignoreEvent(IgnoredEventType.Kick, args.member.id); this.ignoreEvent(IgnoredEventType.Kick, memberToKick.id);
args.member.kick(reason); memberToKick.kick(reason);
// Create a case for this action // Create a case for this action
const createdCase = await this.actions.fire("createCase", { const createdCase = await this.actions.fire("createCase", {
userId: args.member.id, userId: memberToKick.id,
modId: mod.id, modId: mod.id,
type: CaseTypes.Kick, type: CaseTypes.Kick,
reason, reason,
@ -613,7 +686,7 @@ export class ModActionsPlugin extends ZeppelinPlugin<IModActionsPluginConfig> {
}); });
// Confirm the action to the moderator // 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 createdCase.case_number
})`; })`;
@ -623,17 +696,29 @@ export class ModActionsPlugin extends ZeppelinPlugin<IModActionsPluginConfig> {
// Log the action // Log the action
this.serverLogs.log(LogType.MEMBER_KICK, { this.serverLogs.log(LogType.MEMBER_KICK, {
mod: stripObjectToScalars(mod.user), 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" }], options: [{ name: "mod", type: "member" }],
}) })
@d.permission("can_ban") @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 // 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")); msg.channel.createMessage(errorMessage("Cannot ban: insufficient permissions"));
return; return;
} }
@ -660,7 +745,7 @@ export class ModActionsPlugin extends ZeppelinPlugin<IModActionsPluginConfig> {
reason, 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, useDM: config.dm_on_ban,
useChannel: config.message_on_ban, useChannel: config.message_on_ban,
channelId: config.message_channel, channelId: config.message_channel,
@ -668,13 +753,13 @@ export class ModActionsPlugin extends ZeppelinPlugin<IModActionsPluginConfig> {
} }
// Ban the user // Ban the user
this.serverLogs.ignoreLog(LogType.MEMBER_BAN, args.member.id); this.serverLogs.ignoreLog(LogType.MEMBER_BAN, memberToBan.id);
this.ignoreEvent(IgnoredEventType.Ban, args.member.id); this.ignoreEvent(IgnoredEventType.Ban, memberToBan.id);
args.member.ban(1, reason); memberToBan.ban(1, reason);
// Create a case for this action // Create a case for this action
const createdCase = await this.actions.fire("createCase", { const createdCase = await this.actions.fire("createCase", {
userId: args.member.id, userId: memberToBan.id,
modId: mod.id, modId: mod.id,
type: CaseTypes.Ban, type: CaseTypes.Ban,
reason, reason,
@ -683,7 +768,7 @@ export class ModActionsPlugin extends ZeppelinPlugin<IModActionsPluginConfig> {
}); });
// Confirm the action to the moderator // 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 createdCase.case_number
})`; })`;
@ -693,17 +778,29 @@ export class ModActionsPlugin extends ZeppelinPlugin<IModActionsPluginConfig> {
// Log the action // Log the action
this.serverLogs.log(LogType.MEMBER_BAN, { this.serverLogs.log(LogType.MEMBER_BAN, {
mod: stripObjectToScalars(mod.user), 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" }], options: [{ name: "mod", type: "member" }],
}) })
@d.permission("can_ban") @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 // 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")); msg.channel.createMessage(errorMessage("Cannot ban: insufficient permissions"));
return; return;
} }
@ -722,17 +819,17 @@ export class ModActionsPlugin extends ZeppelinPlugin<IModActionsPluginConfig> {
const reason = this.formatReasonWithAttachments(args.reason, msg.attachments); const reason = this.formatReasonWithAttachments(args.reason, msg.attachments);
// Softban the user = ban, and immediately unban // Softban the user = ban, and immediately unban
this.serverLogs.ignoreLog(LogType.MEMBER_BAN, args.member.id); this.serverLogs.ignoreLog(LogType.MEMBER_BAN, memberToSoftban.id);
this.serverLogs.ignoreLog(LogType.MEMBER_UNBAN, args.member.id); this.serverLogs.ignoreLog(LogType.MEMBER_UNBAN, memberToSoftban.id);
this.ignoreEvent(IgnoredEventType.Ban, args.member.id); this.ignoreEvent(IgnoredEventType.Ban, memberToSoftban.id);
this.ignoreEvent(IgnoredEventType.Unban, args.member.id); this.ignoreEvent(IgnoredEventType.Unban, memberToSoftban.id);
await args.member.ban(1, reason); await memberToSoftban.ban(1, reason);
await this.guild.unbanMember(args.member.id); await this.guild.unbanMember(memberToSoftban.id);
// Create a case for this action // Create a case for this action
const createdCase = await this.actions.fire("createCase", { const createdCase = await this.actions.fire("createCase", {
userId: args.member.id, userId: memberToSoftban.id,
modId: mod.id, modId: mod.id,
type: CaseTypes.Softban, type: CaseTypes.Softban,
reason, reason,
@ -742,7 +839,7 @@ export class ModActionsPlugin extends ZeppelinPlugin<IModActionsPluginConfig> {
// Confirm the action to the moderator // Confirm the action to the moderator
msg.channel.createMessage( msg.channel.createMessage(
successMessage( successMessage(
`Softbanned **${args.member.user.username}#${args.member.user.discriminator}** (Case #${ `Softbanned **${memberToSoftban.user.username}#${memberToSoftban.user.discriminator}** (Case #${
createdCase.case_number createdCase.case_number
})`, })`,
), ),
@ -751,7 +848,7 @@ export class ModActionsPlugin extends ZeppelinPlugin<IModActionsPluginConfig> {
// Log the action // Log the action
this.serverLogs.log(LogType.MEMBER_SOFTBAN, { this.serverLogs.log(LogType.MEMBER_SOFTBAN, {
mod: stripObjectToScalars(mod.user), mod: stripObjectToScalars(mod.user),
member: stripObjectToScalars(args.member, ["user"]), member: stripObjectToScalars(memberToSoftban, ["user"]),
}); });
} }

View file

@ -14,6 +14,7 @@ import {
stripObjectToScalars, stripObjectToScalars,
successMessage, successMessage,
ucfirst, ucfirst,
unknownUser,
} from "../utils"; } from "../utils";
import humanizeDuration from "humanize-duration"; import humanizeDuration from "humanize-duration";
import { LogType } from "../data/LogType"; import { LogType } from "../data/LogType";
@ -90,10 +91,10 @@ export class MutesPlugin extends ZeppelinPlugin<IMutesPluginConfig> {
this.serverLogs = new GuildLogs(this.guildId); this.serverLogs = new GuildLogs(this.guildId);
this.actions.register("mute", args => { 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 => { 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 // Check for expired mutes every 5s
@ -108,8 +109,21 @@ export class MutesPlugin extends ZeppelinPlugin<IMutesPluginConfig> {
clearInterval(this.muteClearIntervalId); clearInterval(this.muteClearIntervalId);
} }
public async muteMember( async resolveMember(userId: string) {
member: Member, 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, muteTime: number = null,
reason: string = null, reason: string = null,
caseDetails: ICaseDetails = {}, caseDetails: ICaseDetails = {},
@ -124,32 +138,37 @@ export class MutesPlugin extends ZeppelinPlugin<IMutesPluginConfig> {
caseDetails.modId = this.bot.user.id; caseDetails.modId = this.bot.user.id;
} }
// Apply mute role if it's missing const user = this.bot.users.get(userId) || { ...unknownUser, id: userId };
if (!member.roles.includes(muteRole)) { const member = await this.resolveMember(userId);
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) if (member) {
const moveToVoiceChannelId = this.getConfig().move_to_voice_channel; // Apply mute role if it's missing
if (moveToVoiceChannelId && member.voiceState.channelID) { if (!member.roles.includes(muteRole)) {
try { await member.addRole(muteRole);
await member.edit({ channelID: moveToVoiceChannelId }); }
} catch (e) {
logger.warn(`Could not move user ${member.id} to voice channel ${moveToVoiceChannelId} when muting`); // 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 // 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 }; let notifyResult: INotifyUserResult = { status: NotifyUserStatus.Ignored };
if (existingMute) { if (existingMute) {
await this.mutes.updateExpiryTime(member.id, muteTime); await this.mutes.updateExpiryTime(user.id, muteTime);
} else { } 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 // 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 template = muteTime ? config.timed_mute_message : config.mute_message;
const muteMessage = const muteMessage =
@ -160,12 +179,16 @@ export class MutesPlugin extends ZeppelinPlugin<IMutesPluginConfig> {
time: timeUntilUnmute, time: timeUntilUnmute,
})); }));
if (muteMessage) { if (reason && muteMessage) {
notifyResult = await notifyUser(this.bot, this.guild, member.user, muteMessage, { if (user instanceof User) {
useDM: config.dm_on_mute, notifyResult = await notifyUser(this.bot, this.guild, user, muteMessage, {
useChannel: config.message_on_mute, useDM: config.dm_on_mute,
channelId: config.message_channel, 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", { theCase = await this.actions.fire("createCase", {
userId: member.id, userId,
modId: caseDetails.modId, modId: caseDetails.modId,
type: CaseTypes.Mute, type: CaseTypes.Mute,
reason, reason,
@ -197,20 +220,21 @@ export class MutesPlugin extends ZeppelinPlugin<IMutesPluginConfig> {
noteDetails, noteDetails,
extraNotes: caseDetails.extraNotes, extraNotes: caseDetails.extraNotes,
}); });
await this.mutes.setCaseId(member.id, theCase.id); await this.mutes.setCaseId(user.id, theCase.id);
} }
// Log the action // Log the action
const mod = this.bot.users.get(caseDetails.modId) || { ...unknownUser, id: caseDetails.modId };
if (muteTime) { if (muteTime) {
this.serverLogs.log(LogType.MEMBER_TIMED_MUTE, { this.serverLogs.log(LogType.MEMBER_TIMED_MUTE, {
mod: stripObjectToScalars(caseDetails.modId), mod: stripObjectToScalars(mod),
member: stripObjectToScalars(member, ["user"]), user: stripObjectToScalars(user),
time: timeUntilUnmute, time: timeUntilUnmute,
}); });
} else { } else {
this.serverLogs.log(LogType.MEMBER_MUTE, { this.serverLogs.log(LogType.MEMBER_MUTE, {
mod: stripObjectToScalars(caseDetails.modId), mod: stripObjectToScalars(mod),
member: stripObjectToScalars(member, ["user"]), user: stripObjectToScalars(user),
}); });
} }
@ -221,21 +245,26 @@ export class MutesPlugin extends ZeppelinPlugin<IMutesPluginConfig> {
}; };
} }
public async unmuteMember(member: Member, unmuteTime: number = null, caseDetails: ICaseDetails = {}) { public async unmuteUser(userId: string, unmuteTime: number = null, caseDetails: ICaseDetails = {}) {
const existingMute = await this.mutes.findExistingMuteForUserId(member.id); const existingMute = await this.mutes.findExistingMuteForUserId(userId);
if (!existingMute) return; if (!existingMute) return;
const user = this.bot.users.get(userId) || { ...unknownUser, id: userId };
const member = await this.resolveMember(userId);
if (unmuteTime) { if (unmuteTime) {
// Schedule timed unmute (= just set the mute's duration) // Schedule timed unmute (= just set the mute's duration)
await this.mutes.updateExpiryTime(member.id, unmuteTime); await this.mutes.updateExpiryTime(userId, unmuteTime);
} else { } else {
// Unmute immediately // Unmute immediately
const muteRole = this.getConfig().mute_role; if (member) {
if (member.roles.includes(muteRole)) { const muteRole = this.getConfig().mute_role;
await member.removeRole(muteRole); 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); const timeUntilUnmute = unmuteTime && humanizeDuration(unmuteTime);
@ -249,7 +278,7 @@ export class MutesPlugin extends ZeppelinPlugin<IMutesPluginConfig> {
} }
const createdCase = await this.actions.fire("createCase", { const createdCase = await this.actions.fire("createCase", {
userId: member.id, userId,
modId: caseDetails.modId, modId: caseDetails.modId,
type: CaseTypes.Unmute, type: CaseTypes.Unmute,
reason: caseDetails.reason, reason: caseDetails.reason,
@ -262,13 +291,13 @@ export class MutesPlugin extends ZeppelinPlugin<IMutesPluginConfig> {
if (unmuteTime) { if (unmuteTime) {
this.serverLogs.log(LogType.MEMBER_TIMED_UNMUTE, { this.serverLogs.log(LogType.MEMBER_TIMED_UNMUTE, {
mod: stripObjectToScalars(mod), mod: stripObjectToScalars(mod),
member: stripObjectToScalars(member, ["user"]), user: stripObjectToScalars(user),
time: timeUntilUnmute, time: timeUntilUnmute,
}); });
} else { } else {
this.serverLogs.log(LogType.MEMBER_UNMUTE, { this.serverLogs.log(LogType.MEMBER_UNMUTE, {
mod: stripObjectToScalars(mod), 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 * 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); const persistedData = await this.persistedData.find(member.id);
if (!persistedData) return; if (!persistedData) return;
let restore = false;
const toRestore: MemberOptions = {}; const toRestore: MemberOptions = {};
const config = this.getConfig(); const config = this.getConfig();
const restoredData = [];
const persistedRoles = config.persisted_roles; const persistedRoles = config.persisted_roles;
if (persistedRoles.length) { if (persistedRoles.length) {
const rolesToRestore = intersection(persistedRoles, persistedData.roles); const rolesToRestore = intersection(persistedRoles, persistedData.roles);
if (rolesToRestore.length) { if (rolesToRestore.length) {
restore = true; restoredData.push("roles");
toRestore.roles = rolesToRestore; toRestore.roles = rolesToRestore;
} }
} }
if (config.persist_nicknames && persistedData.nickname) { if (config.persist_nicknames && persistedData.nickname) {
restore = true; restoredData.push("nickname");
toRestore.nick = persistedData.nickname; toRestore.nick = persistedData.nickname;
} }
if (config.persist_voice_mutes && persistedData.is_voice_muted) { if (config.persist_voice_mutes && persistedData.is_voice_muted) {
restore = true; restoredData.push("voice mute");
toRestore.mute = true; toRestore.mute = true;
} }
if (restore) { if (restoredData.length) {
await member.edit(toRestore, "Restored upon rejoin"); await member.edit(toRestore, "Restored upon rejoin");
await this.persistedData.clear(member.id); await this.persistedData.clear(member.id);
this.logs.log(LogType.MEMBER_RESTORE, { this.logs.log(LogType.MEMBER_RESTORE, {
member: stripObjectToScalars(member, ["user"]), member: stripObjectToScalars(member, ["user"]),
restoredData: restoredData.join(", "),
}); });
} }
} }

View file

@ -245,7 +245,7 @@ export class SpamPlugin extends ZeppelinPlugin<ISpamPluginConfig> {
? convertDelayStringToMS(spamConfig.mute_time.toString()) ? convertDelayStringToMS(spamConfig.mute_time.toString())
: 120 * 1000; : 120 * 1000;
muteResult = await this.actions.fire("mute", { muteResult = await this.actions.fire("mute", {
member, userId: member.id,
muteTime, muteTime,
reason: "Automatic spam detection", reason: "Automatic spam detection",
caseDetails: { caseDetails: {
@ -366,7 +366,7 @@ export class SpamPlugin extends ZeppelinPlugin<ISpamPluginConfig> {
if (spamConfig.mute && member) { if (spamConfig.mute && member) {
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, userId); 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 // Clear recent cases