Merge pull request #89 from DarkView/k30_modActions

[K30] Migrated ModActions | Mutes dont work, issue in MutesPlugin->public
This commit is contained in:
Miikka 2020-07-25 12:13:20 +03:00 committed by GitHub
commit 6338ee9e8a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 1213 additions and 16 deletions

View file

@ -9,6 +9,7 @@ import { Case } from "../../data/entities/Case";
import { postCaseToCaseLogChannel } from "./functions/postToCaseLogChannel";
import { CaseTypes } from "../../data/CaseTypes";
import { getCaseTypeAmountForUserId } from "./functions/getCaseTypeAmountForUserId";
import { getCaseEmbed } from "./functions/getCaseEmbed";
const defaultOptions = {
config: {
@ -45,6 +46,12 @@ export const CasesPlugin = zeppelinPlugin<CasesPluginType>()("cases", {
return getCaseTypeAmountForUserId(pluginData, userID, type);
};
},
getCaseEmbed(pluginData) {
return (caseOrCaseId: Case | number) => {
return getCaseEmbed(pluginData, caseOrCaseId);
};
},
},
onLoad(pluginData) {

View file

@ -9,6 +9,25 @@ import { UpdateCmd } from "./commands/UpdateCmd";
import { NoteCmd } from "./commands/NoteCmd";
import { WarnCmd } from "./commands/WarnCmd";
import { MuteCmd } from "./commands/MuteCmd";
import { PostAlertOnMemberJoinEvt } from "./events/PostAlertOnMemberJoinEvt";
import { ForcemuteCmd } from "./commands/ForcemuteCmd";
import { UnmuteCmd } from "./commands/UnmuteCmd";
import { KickCmd } from "./commands/KickCmd";
import { SoftbanCmd } from "./commands/SoftbanCommand";
import { BanCmd } from "./commands/BanCmd";
import { UnbanCmd } from "./commands/UnbanCmd";
import { ForcebanCmd } from "./commands/ForcebanCmd";
import { MassbanCmd } from "./commands/MassBanCmd";
import { AddCaseCmd } from "./commands/AddCaseCmd";
import { CaseCmd } from "./commands/CaseCmd";
import { CasesUserCmd } from "./commands/CasesUserCmd";
import { CasesModCmd } from "./commands/CasesModCmd";
import { HideCaseCmd } from "./commands/HideCaseCmd";
import { UnhideCaseCmd } from "./commands/UnhideCaseCmd";
import { GuildMutes } from "src/data/GuildMutes";
import { GuildCases } from "src/data/GuildCases";
import { GuildLogs } from "src/data/GuildLogs";
import { ForceUnmuteCmd } from "./commands/ForceunmuteCmd";
const defaultOptions = {
config: {
@ -71,7 +90,42 @@ export const ModActionsPlugin = zeppelinPlugin<ModActionsPluginType>()("mod_acti
dependencies: [CasesPlugin, MutesPlugin],
events: [CreateBanCaseOnManualBanEvt, CreateUnbanCaseOnManualUnbanEvt, CreateKickCaseOnManualKickEvt],
events: [
CreateBanCaseOnManualBanEvt,
CreateUnbanCaseOnManualUnbanEvt,
CreateKickCaseOnManualKickEvt,
PostAlertOnMemberJoinEvt,
],
commands: [UpdateCmd, NoteCmd, WarnCmd, MuteCmd],
commands: [
UpdateCmd,
NoteCmd,
WarnCmd,
MuteCmd,
ForcemuteCmd,
UnmuteCmd,
ForceUnmuteCmd,
KickCmd,
SoftbanCmd,
BanCmd,
UnbanCmd,
ForcebanCmd,
MassbanCmd,
AddCaseCmd,
CaseCmd,
CasesUserCmd,
CasesModCmd,
HideCaseCmd,
UnhideCaseCmd,
],
onLoad(pluginData) {
const { state, guild } = pluginData;
state.mutes = GuildMutes.getGuildInstance(guild.id);
state.cases = GuildCases.getGuildInstance(guild.id);
state.serverLogs = new GuildLogs(guild.id);
state.ignoredEvents = [];
},
});

View file

@ -0,0 +1,90 @@
import { modActionsCommand } from "../types";
import { commandTypeHelpers as ct } from "../../../commandTypes";
import { canActOn, sendErrorMessage, hasPermission, sendSuccessMessage } from "../../../pluginUtils";
import { resolveUser, resolveMember, stripObjectToScalars } from "../../../utils";
import { formatReasonWithAttachments } from "../functions/formatReasonWithAttachments";
import { CaseTypes } from "src/data/CaseTypes";
import { CasesPlugin } from "src/plugins/Cases/CasesPlugin";
import { Case } from "src/data/entities/Case";
import { LogType } from "src/data/LogType";
const opts = {
mod: ct.member({ option: true }),
};
export const AddCaseCmd = modActionsCommand({
trigger: "addcase",
permission: "can_addcase",
description: "Add an arbitrary case to the specified user without taking any action",
signature: [
{
type: ct.string(),
user: ct.string(),
reason: ct.string({ required: false, catchAll: true }),
...opts,
},
],
async run({ pluginData, message: msg, args }) {
const user = await resolveUser(pluginData.client, args.user);
if (!user) return sendErrorMessage(pluginData, msg.channel, `User not found`);
// If the user exists as a guild member, make sure we can act on them first
const member = await resolveMember(pluginData.client, pluginData.guild, user.id);
if (member && !canActOn(pluginData, msg.member, member)) {
sendErrorMessage(pluginData, msg.channel, "Cannot add case on this user: insufficient permissions");
return;
}
// The moderator who did the action is the message author or, if used, the specified -mod
let mod = msg.member;
if (args.mod) {
if (!hasPermission(pluginData, "can_act_as_other", { message: msg })) {
sendErrorMessage(pluginData, msg.channel, "No permission for -mod");
return;
}
mod = args.mod;
}
// Verify the case type is valid
const type: string = args.type[0].toUpperCase() + args.type.slice(1).toLowerCase();
if (!CaseTypes[type]) {
sendErrorMessage(pluginData, msg.channel, "Cannot add case: invalid case type");
return;
}
const reason = formatReasonWithAttachments(args.reason, msg.attachments);
// Create the case
const casesPlugin = pluginData.getPlugin(CasesPlugin);
const theCase: Case = await casesPlugin.createCase({
userId: user.id,
modId: mod.id,
type: CaseTypes[type],
reason,
ppId: mod.id !== msg.author.id ? msg.author.id : null,
});
if (user) {
sendSuccessMessage(
pluginData,
msg.channel,
`Case #${theCase.case_number} created for **${user.username}#${user.discriminator}**`,
);
} else {
sendSuccessMessage(pluginData, msg.channel, `Case #${theCase.case_number} created`);
}
// Log the action
pluginData.state.serverLogs.log(LogType.CASE_CREATE, {
mod: stripObjectToScalars(mod.user),
userId: user.id,
caseNum: theCase.case_number,
caseType: type.toUpperCase(),
reason,
});
},
});

View file

@ -0,0 +1,97 @@
import { modActionsCommand, IgnoredEventType } from "../types";
import { commandTypeHelpers as ct } from "../../../commandTypes";
import { canActOn, sendErrorMessage, hasPermission, sendSuccessMessage } from "../../../pluginUtils";
import { resolveUser, resolveMember } from "../../../utils";
import { isBanned } from "../functions/isBanned";
import { readContactMethodsFromArgs } from "../functions/readContactMethodsFromArgs";
import { formatReasonWithAttachments } from "../functions/formatReasonWithAttachments";
import { banUserId } from "../functions/banUserId";
import { ignoreEvent } from "../functions/ignoreEvent";
import { LogType } from "src/data/LogType";
const opts = {
mod: ct.member({ option: true }),
notify: ct.string({ option: true }),
"notify-channel": ct.textChannel({ option: true }),
"delete-deays": ct.number({ option: true, shortcut: "d" }),
};
export const BanCmd = modActionsCommand({
trigger: "ban",
permission: "can_ban",
description: "Ban the specified member",
signature: [
{
user: ct.string(),
reason: ct.string({ required: false, catchAll: true }),
...opts,
},
],
async run({ pluginData, message: msg, args }) {
const user = await resolveUser(pluginData.client, args.user);
if (!user) return sendErrorMessage(pluginData, msg.channel, `User not found`);
const memberToBan = await resolveMember(pluginData.client, pluginData.guild, user.id);
if (!memberToBan) {
const banned = await isBanned(pluginData, user.id);
if (banned) {
sendErrorMessage(pluginData, msg.channel, `User is already banned`);
} else {
sendErrorMessage(pluginData, msg.channel, `User not found on the server`);
}
return;
}
// Make sure we're allowed to ban this member
if (!canActOn(pluginData, msg.member, memberToBan)) {
sendErrorMessage(pluginData, msg.channel, "Cannot ban: insufficient permissions");
return;
}
// The moderator who did the action is the message author or, if used, the specified -mod
let mod = msg.member;
if (args.mod) {
if (!hasPermission(pluginData, "can_act_as_other", { message: msg, channelId: msg.channel.id })) {
sendErrorMessage(pluginData, msg.channel, "No permission for -mod");
return;
}
mod = args.mod;
}
let contactMethods;
try {
contactMethods = readContactMethodsFromArgs(args);
} catch (e) {
sendErrorMessage(pluginData, msg.channel, e.message);
return;
}
const deleteMessageDays = args["delete-days"] ?? pluginData.config.getForMessage(msg).ban_delete_message_days;
const reason = formatReasonWithAttachments(args.reason, msg.attachments);
const banResult = await banUserId(pluginData, memberToBan.id, reason, {
contactMethods,
caseArgs: {
modId: mod.id,
ppId: mod.id !== msg.author.id ? msg.author.id : null,
},
deleteMessageDays,
});
if (banResult.status === "failed") {
sendErrorMessage(pluginData, msg.channel, `Failed to ban member`);
return;
}
// Confirm the action to the moderator
let response = `Banned **${memberToBan.user.username}#${memberToBan.user.discriminator}** (Case #${banResult.case.case_number})`;
if (banResult.notifyResult.text) response += ` (${banResult.notifyResult.text})`;
sendSuccessMessage(pluginData, msg.channel, response);
},
});

View file

@ -0,0 +1,29 @@
import { modActionsCommand } from "../types";
import { commandTypeHelpers as ct } from "../../../commandTypes";
import { sendErrorMessage } from "../../../pluginUtils";
import { CasesPlugin } from "src/plugins/Cases/CasesPlugin";
export const CaseCmd = modActionsCommand({
trigger: "case",
permission: "can_view",
description: "Show information about a specific case",
signature: [
{
caseNumber: ct.number(),
},
],
async run({ pluginData, message: msg, args }) {
const theCase = await pluginData.state.cases.findByCaseNumber(args.caseNumber);
if (!theCase) {
sendErrorMessage(pluginData, msg.channel, "Case not found");
return;
}
const casesPlugin = pluginData.getPlugin(CasesPlugin);
const embed = await casesPlugin.getCaseEmbed(theCase.id);
msg.channel.createMessage(embed);
},
});

View file

@ -0,0 +1,43 @@
import { modActionsCommand } from "../types";
import { commandTypeHelpers as ct } from "../../../commandTypes";
import { sendErrorMessage } from "../../../pluginUtils";
import { trimLines, createChunkedMessage } from "src/utils";
const opts = {
mod: ct.member({ option: true }),
};
export const CasesModCmd = modActionsCommand({
trigger: "cases",
permission: "can_view",
description: "Show the most recent 5 cases by the specified -mod",
signature: [
{
...opts,
},
],
async run({ pluginData, message: msg, args }) {
const modId = args.mod ? args.mod.id : msg.author.id;
const recentCases = await pluginData.state.cases.with("notes").getRecentByModId(modId, 5);
const mod = pluginData.client.users.get(modId);
const modName = mod ? `${mod.username}#${mod.discriminator}` : modId;
if (recentCases.length === 0) {
sendErrorMessage(pluginData, msg.channel, `No cases by **${modName}**`);
} else {
const lines = recentCases.map(c => pluginData.state.cases.getSummaryText(c));
const finalMessage = trimLines(`
Most recent 5 cases by **${modName}**:
${lines.join("\n")}
Use the \`case <num>\` command to see more info about individual cases
Use the \`cases <user>\` command to see a specific user's cases
`);
createChunkedMessage(msg.channel, finalMessage);
}
},
});

View file

@ -0,0 +1,84 @@
import { modActionsCommand } from "../types";
import { commandTypeHelpers as ct } from "../../../commandTypes";
import { sendErrorMessage } from "../../../pluginUtils";
import { CasesPlugin } from "src/plugins/Cases/CasesPlugin";
import { UnknownUser, multiSorter, trimLines, createChunkedMessage, resolveUser } from "src/utils";
const opts = {
expand: ct.bool({ option: true, isSwitch: true, shortcut: "e" }),
hidden: ct.bool({ option: true, isSwitch: true, shortcut: "h" }),
};
export const CasesUserCmd = modActionsCommand({
trigger: "cases",
permission: "can_view",
description: "Show a list of cases the specified user has",
signature: [
{
user: ct.string(),
...opts,
},
],
async run({ pluginData, message: msg, args }) {
const user = await resolveUser(pluginData.client, args.user);
if (!user) return sendErrorMessage(pluginData, msg.channel, `User not found`);
const cases = await pluginData.state.cases.with("notes").getByUserId(user.id);
const normalCases = cases.filter(c => !c.is_hidden);
const hiddenCases = cases.filter(c => c.is_hidden);
const userName =
user instanceof UnknownUser && cases.length
? cases[cases.length - 1].user_name
: `${user.username}#${user.discriminator}`;
if (cases.length === 0) {
msg.channel.createMessage(`No cases found for **${userName}**`);
} else {
const casesToDisplay = args.hidden ? cases : normalCases;
if (args.expand) {
if (casesToDisplay.length > 8) {
msg.channel.createMessage("Too many cases for expanded view. Please use compact view instead.");
return;
}
// Expanded view (= individual case embeds)
const casesPlugin = pluginData.getPlugin(CasesPlugin);
for (const theCase of casesToDisplay) {
const embed = await casesPlugin.getCaseEmbed(theCase.id);
msg.channel.createMessage(embed);
}
} else {
// Compact view (= regular message with a preview of each case)
const lines = [];
for (const theCase of casesToDisplay) {
theCase.notes.sort(multiSorter(["created_at", "id"]));
const caseSummary = pluginData.state.cases.getSummaryText(theCase);
lines.push(caseSummary);
}
if (!args.hidden && hiddenCases.length) {
if (hiddenCases.length === 1) {
lines.push(`*+${hiddenCases.length} hidden case, use "-hidden" to show it*`);
} else {
lines.push(`*+${hiddenCases.length} hidden cases, use "-hidden" to show them*`);
}
}
const finalMessage = trimLines(`
Cases for **${userName}**:
${lines.join("\n")}
Use the \`case <num>\` command to see more info about individual cases
`);
createChunkedMessage(msg.channel, finalMessage);
}
}
},
});

View file

@ -0,0 +1,93 @@
import { modActionsCommand, IgnoredEventType } from "../types";
import { commandTypeHelpers as ct } from "../../../commandTypes";
import { canActOn, sendErrorMessage, hasPermission, sendSuccessMessage } from "../../../pluginUtils";
import { resolveUser, resolveMember, stripObjectToScalars } from "../../../utils";
import { isBanned } from "../functions/isBanned";
import { readContactMethodsFromArgs } from "../functions/readContactMethodsFromArgs";
import { formatReasonWithAttachments } from "../functions/formatReasonWithAttachments";
import { banUserId } from "../functions/banUserId";
import { ignoreEvent } from "../functions/ignoreEvent";
import { LogType } from "src/data/LogType";
import { CaseTypes } from "src/data/CaseTypes";
import { CasesPlugin } from "src/plugins/Cases/CasesPlugin";
const opts = {
mod: ct.member({ option: true }),
};
export const ForcebanCmd = modActionsCommand({
trigger: "forceban",
permission: "can_ban",
description: "Force-ban the specified user, even if they aren't on the server",
signature: [
{
user: ct.string(),
reason: ct.string({ required: false, catchAll: true }),
...opts,
},
],
async run({ pluginData, message: msg, args }) {
const user = await resolveUser(pluginData.client, args.user);
if (!user) return sendErrorMessage(pluginData, msg.channel, `User not found`);
// If the user exists as a guild member, make sure we can act on them first
const member = await resolveMember(pluginData.client, pluginData.guild, user.id);
if (member && !canActOn(pluginData, msg.member, member)) {
sendErrorMessage(pluginData, msg.channel, "Cannot forceban this user: insufficient permissions");
return;
}
// Make sure the user isn't already banned
const banned = await isBanned(pluginData, user.id);
if (banned) {
sendErrorMessage(pluginData, msg.channel, `User is already banned`);
return;
}
// The moderator who did the action is the message author or, if used, the specified -mod
let mod = msg.member;
if (args.mod) {
if (!hasPermission(pluginData, "can_act_as_other", { message: msg })) {
sendErrorMessage(pluginData, msg.channel, "No permission for -mod");
return;
}
mod = args.mod;
}
const reason = formatReasonWithAttachments(args.reason, msg.attachments);
ignoreEvent(pluginData, IgnoredEventType.Ban, user.id);
pluginData.state.serverLogs.ignoreLog(LogType.MEMBER_BAN, user.id);
try {
await pluginData.guild.banMember(user.id, 1);
} catch (e) {
sendErrorMessage(pluginData, msg.channel, "Failed to forceban member");
return;
}
// Create a case
const casesPlugin = pluginData.getPlugin(CasesPlugin);
const createdCase = await casesPlugin.createCase({
userId: user.id,
modId: mod.id,
type: CaseTypes.Ban,
reason,
ppId: mod.id !== msg.author.id ? msg.author.id : null,
});
// Confirm the action
sendSuccessMessage(pluginData, msg.channel, `Member forcebanned (Case #${createdCase.case_number})`);
// Log the action
pluginData.state.serverLogs.log(LogType.MEMBER_FORCEBAN, {
mod: stripObjectToScalars(mod.user),
userId: user.id,
reason,
});
},
});

View file

@ -0,0 +1,48 @@
import { modActionsCommand } from "../types";
import { commandTypeHelpers as ct } from "../../../commandTypes";
import { canActOn, sendErrorMessage } from "../../../pluginUtils";
import { resolveMember, resolveUser } from "../../../utils";
import { actualMuteUserCmd } from "../functions/actualMuteUserCmd";
const opts = {
mod: ct.member({ option: true }),
notify: ct.string({ option: true }),
"notify-channel": ct.textChannel({ option: true }),
};
export const ForcemuteCmd = modActionsCommand({
trigger: "forcemute",
permission: "can_mute",
description: "Force-mute the specified user, even if they're not on the server",
signature: [
{
user: ct.string(),
time: ct.delay(),
reason: ct.string({ required: false, catchAll: true }),
...opts,
},
{
user: ct.string(),
reason: ct.string({ required: false, catchAll: true }),
...opts,
},
],
async run({ pluginData, message: msg, args }) {
const user = await resolveUser(pluginData.client, args.user);
if (!user) return sendErrorMessage(pluginData, msg.channel, `User not found`);
const memberToMute = await resolveMember(pluginData.client, pluginData.guild, user.id);
// Make sure we're allowed to mute this user
if (memberToMute && !canActOn(pluginData, msg.member, memberToMute)) {
sendErrorMessage(pluginData, msg.channel, "Cannot mute: insufficient permissions");
return;
}
actualMuteUserCmd(pluginData, user, msg, args);
},
});

View file

@ -0,0 +1,53 @@
import { modActionsCommand } from "../types";
import { commandTypeHelpers as ct } from "../../../commandTypes";
import { canActOn, sendErrorMessage } from "../../../pluginUtils";
import { resolveUser, resolveMember } from "../../../utils";
import { actualUnmuteCmd } from "../functions/actualUnmuteUserCmd";
const opts = {
mod: ct.member({ option: true }),
};
export const ForceUnmuteCmd = modActionsCommand({
trigger: "forceunmute",
permission: "can_mute",
description: "Force-unmute the specified user, even if they're not on the server",
signature: [
{
user: ct.string(),
time: ct.delay(),
reason: ct.string({ required: false, catchAll: true }),
...opts,
},
{
user: ct.string(),
reason: ct.string({ required: false, catchAll: true }),
...opts,
},
],
async run({ pluginData, message: msg, args }) {
const user = await resolveUser(pluginData.client, args.user);
if (!user) return sendErrorMessage(pluginData, msg.channel, `User not found`);
// Check if they're muted in the first place
if (!(await pluginData.state.mutes.isMuted(user.id))) {
sendErrorMessage(pluginData, msg.channel, "Cannot unmute: member is not muted");
return;
}
// Find the server member to unmute
const memberToUnmute = await resolveMember(pluginData.client, pluginData.guild, user.id);
// Make sure we're allowed to unmute this member
if (memberToUnmute && !canActOn(pluginData, msg.member, memberToUnmute)) {
sendErrorMessage(pluginData, msg.channel, "Cannot unmute: insufficient permissions");
return;
}
actualUnmuteCmd(pluginData, user, msg, args);
},
});

View file

@ -0,0 +1,30 @@
import { modActionsCommand } from "../types";
import { commandTypeHelpers as ct } from "../../../commandTypes";
import { sendErrorMessage, sendSuccessMessage } from "../../../pluginUtils";
export const HideCaseCmd = modActionsCommand({
trigger: ["hide", "hidecase", "hide_case"],
permission: "can_hidecase",
description: "Hide the specified case so it doesn't appear in !cases or !info",
signature: [
{
caseNum: ct.number(),
},
],
async run({ pluginData, message: msg, args }) {
const theCase = await pluginData.state.cases.findByCaseNumber(args.caseNum);
if (!theCase) {
sendErrorMessage(pluginData, msg.channel, "Case not found!");
return;
}
await pluginData.state.cases.setHidden(theCase.id, true);
sendSuccessMessage(
pluginData,
msg.channel,
`Case #${theCase.case_number} is now hidden! Use \`unhidecase\` to unhide it.`,
);
},
});

View file

@ -0,0 +1,35 @@
import { modActionsCommand } from "../types";
import { commandTypeHelpers as ct } from "../../../commandTypes";
import { canActOn, sendErrorMessage } from "../../../pluginUtils";
import { resolveUser, resolveMember } from "../../../utils";
import { MutesPlugin } from "src/plugins/Mutes/MutesPlugin";
import { actualUnmuteCmd } from "../functions/actualUnmuteUserCmd";
import { isBanned } from "../functions/isBanned";
import { plugin } from "knub";
import { actualKickMemberCmd } from "../functions/actualKickMemberCmd";
const opts = {
mod: ct.member({ option: true }),
notify: ct.string({ option: true }),
"notify-channel": ct.textChannel({ option: true }),
clean: ct.bool({ option: true, isSwitch: true }),
};
export const KickCmd = modActionsCommand({
trigger: "kick",
permission: "can_kick",
description: "Kick the specified member",
signature: [
{
user: ct.string(),
reason: ct.string({ required: false, catchAll: true }),
...opts,
},
],
async run({ pluginData, message: msg, args }) {
actualKickMemberCmd(pluginData, msg, args);
},
});

View file

@ -0,0 +1,109 @@
import { modActionsCommand, IgnoredEventType } from "../types";
import { commandTypeHelpers as ct } from "../../../commandTypes";
import { canActOn, sendErrorMessage, hasPermission, sendSuccessMessage } from "../../../pluginUtils";
import { resolveUser, resolveMember, stripObjectToScalars } from "../../../utils";
import { isBanned } from "../functions/isBanned";
import { readContactMethodsFromArgs } from "../functions/readContactMethodsFromArgs";
import { formatReasonWithAttachments } from "../functions/formatReasonWithAttachments";
import { banUserId } from "../functions/banUserId";
import { CaseTypes } from "src/data/CaseTypes";
import { TextChannel } from "eris";
import { waitForReply } from "knub/dist/helpers";
import { ignoreEvent } from "../functions/ignoreEvent";
import { CasesPlugin } from "src/plugins/Cases/CasesPlugin";
import { LogType } from "src/data/LogType";
export const MassbanCmd = modActionsCommand({
trigger: "massban",
permission: "can_massban",
description: "Mass-ban a list of user IDs",
signature: [
{
userIds: ct.string({ rest: true }),
},
],
async run({ pluginData, message: msg, args }) {
// Limit to 100 users at once (arbitrary?)
if (args.userIds.length > 100) {
sendErrorMessage(pluginData, msg.channel, `Can only massban max 100 users at once`);
return;
}
// Ask for ban reason (cleaner this way instead of trying to cram it into the args)
msg.channel.createMessage("Ban reason? `cancel` to cancel");
const banReasonReply = await waitForReply(pluginData.client, msg.channel as TextChannel, msg.author.id);
if (!banReasonReply || !banReasonReply.content || banReasonReply.content.toLowerCase().trim() === "cancel") {
sendErrorMessage(pluginData, msg.channel, "Cancelled");
return;
}
const banReason = formatReasonWithAttachments(banReasonReply.content, msg.attachments);
// Verify we can act on each of the users specified
for (const userId of args.userIds) {
const member = pluginData.guild.members.get(userId); // TODO: Get members on demand?
if (member && !canActOn(pluginData, msg.member, member)) {
sendErrorMessage(pluginData, msg.channel, "Cannot massban one or more users: insufficient permissions");
return;
}
}
// Ignore automatic ban cases and logs for these users
// We'll create our own cases below and post a single "mass banned" log instead
args.userIds.forEach(userId => {
// Use longer timeouts since this can take a while
ignoreEvent(pluginData, IgnoredEventType.Ban, userId, 120 * 1000);
pluginData.state.serverLogs.ignoreLog(LogType.MEMBER_BAN, userId, 120 * 1000);
});
// Show a loading indicator since this can take a while
const loadingMsg = await msg.channel.createMessage("Banning...");
// Ban each user and count failed bans (if any)
const failedBans = [];
const casesPlugin = pluginData.getPlugin(CasesPlugin);
for (const userId of args.userIds) {
try {
await pluginData.guild.banMember(userId, 1);
await casesPlugin.createCase({
userId,
modId: msg.author.id,
type: CaseTypes.Ban,
reason: `Mass ban: ${banReason}`,
postInCaseLogOverride: false,
});
} catch (e) {
failedBans.push(userId);
}
}
// Clear loading indicator
loadingMsg.delete();
const successfulBanCount = args.userIds.length - failedBans.length;
if (successfulBanCount === 0) {
// All bans failed - don't create a log entry and notify the user
sendErrorMessage(pluginData, msg.channel, "All bans failed. Make sure the IDs are valid.");
} else {
// Some or all bans were successful. Create a log entry for the mass ban and notify the user.
pluginData.state.serverLogs.log(LogType.MASSBAN, {
mod: stripObjectToScalars(msg.author),
count: successfulBanCount,
reason: banReason,
});
if (failedBans.length) {
sendSuccessMessage(
pluginData,
msg.channel,
`Banned ${successfulBanCount} users, ${failedBans.length} failed: ${failedBans.join(" ")}`,
);
} else {
sendSuccessMessage(pluginData, msg.channel, `Banned ${successfulBanCount} users successfully`);
}
}
},
});

View file

@ -0,0 +1,35 @@
import { modActionsCommand } from "../types";
import { commandTypeHelpers as ct } from "../../../commandTypes";
import { trimPluginDescription } from "../../../utils";
import { actualKickMemberCmd } from "../functions/actualKickMemberCmd";
const opts = {
mod: ct.member({ option: true }),
notify: ct.string({ option: true }),
"notify-channel": ct.textChannel({ option: true }),
};
export const SoftbanCmd = modActionsCommand({
trigger: "softban",
permission: "can_kick",
description: trimPluginDescription(`
"Softban" the specified user by banning and immediately unbanning them. Effectively a kick with message deletions.
This command will be removed in the future, please use kick with the \`- clean\` argument instead
`),
signature: [
{
user: ct.string(),
reason: ct.string({ required: false, catchAll: true }),
...opts,
},
],
async run({ pluginData, message: msg, args }) {
await actualKickMemberCmd(pluginData, msg, { clean: true, ...args });
await msg.channel.createMessage(
"Softban will be removed in the future - please use the kick command with the `-clean` argument instead!",
);
},
});

View file

@ -0,0 +1,76 @@
import { modActionsCommand, IgnoredEventType } from "../types";
import { commandTypeHelpers as ct } from "../../../commandTypes";
import { sendErrorMessage, hasPermission, sendSuccessMessage } from "../../../pluginUtils";
import { resolveUser, stripObjectToScalars } from "../../../utils";
import { formatReasonWithAttachments } from "../functions/formatReasonWithAttachments";
import { LogType } from "src/data/LogType";
import { ignoreEvent } from "../functions/ignoreEvent";
import { CaseTypes } from "src/data/CaseTypes";
import { CasesPlugin } from "src/plugins/Cases/CasesPlugin";
const opts = {
mod: ct.member({ option: true }),
};
export const UnbanCmd = modActionsCommand({
trigger: "unban",
permission: "can_ban",
description: "Unban the specified member",
signature: [
{
user: ct.string(),
reason: ct.string({ required: false, catchAll: true }),
...opts,
},
],
async run({ pluginData, message: msg, args }) {
const user = await resolveUser(pluginData.client, args.user);
if (!user) return sendErrorMessage(pluginData, msg.channel, `User not found`);
// The moderator who did the action is the message author or, if used, the specified -mod
let mod = msg.member;
if (args.mod) {
if (!hasPermission(pluginData, "can_act_as_other", { message: msg, channelId: msg.channel.id })) {
sendErrorMessage(pluginData, msg.channel, "No permission for -mod");
return;
}
mod = args.mod;
}
pluginData.state.serverLogs.ignoreLog(LogType.MEMBER_UNBAN, user.id);
try {
ignoreEvent(pluginData, IgnoredEventType.Unban, user.id);
await pluginData.guild.unbanMember(user.id);
} catch (e) {
sendErrorMessage(pluginData, msg.channel, "Failed to unban member; are you sure they're banned?");
return;
}
const reason = formatReasonWithAttachments(args.reason, msg.attachments);
// Create a case
const casesPlugin = pluginData.getPlugin(CasesPlugin);
const createdCase = await casesPlugin.createCase({
userId: user.id,
modId: mod.id,
type: CaseTypes.Unban,
reason,
ppId: mod.id !== msg.author.id ? msg.author.id : null,
});
// Confirm the action
sendSuccessMessage(pluginData, msg.channel, `Member unbanned (Case #${createdCase.case_number})`);
// Log the action
pluginData.state.serverLogs.log(LogType.MEMBER_UNBAN, {
mod: stripObjectToScalars(mod.user),
userId: user.id,
reason,
});
},
});

View file

@ -0,0 +1,26 @@
import { modActionsCommand } from "../types";
import { commandTypeHelpers as ct } from "../../../commandTypes";
import { sendErrorMessage, sendSuccessMessage } from "../../../pluginUtils";
export const UnhideCaseCmd = modActionsCommand({
trigger: ["unhide", "unhidecase", "unhide_case"],
permission: "can_hidecase",
description: "Un-hide the specified case, making it appear in !cases and !info again",
signature: [
{
caseNum: ct.number(),
},
],
async run({ pluginData, message: msg, args }) {
const theCase = await pluginData.state.cases.findByCaseNumber(args.caseNum);
if (!theCase) {
sendErrorMessage(pluginData, msg.channel, "Case not found!");
return;
}
await pluginData.state.cases.setHidden(theCase.id, false);
sendSuccessMessage(pluginData, msg.channel, `Case #${theCase.case_number} is no longer hidden!`);
},
});

View file

@ -0,0 +1,76 @@
import { modActionsCommand } from "../types";
import { commandTypeHelpers as ct } from "../../../commandTypes";
import { canActOn, sendErrorMessage } from "../../../pluginUtils";
import { resolveUser, resolveMember } from "../../../utils";
import { MutesPlugin } from "src/plugins/Mutes/MutesPlugin";
import { actualUnmuteCmd } from "../functions/actualUnmuteUserCmd";
import { isBanned } from "../functions/isBanned";
import { plugin } from "knub";
const opts = {
mod: ct.member({ option: true }),
};
export const UnmuteCmd = modActionsCommand({
trigger: "unmute",
permission: "can_mute",
description: "Unmute the specified member",
signature: [
{
user: ct.string(),
time: ct.delay(),
reason: ct.string({ required: false, catchAll: true }),
...opts,
},
{
user: ct.string(),
reason: ct.string({ required: false, catchAll: true }),
...opts,
},
],
async run({ pluginData, message: msg, args }) {
const user = await resolveUser(pluginData.client, args.user);
if (!user) return sendErrorMessage(pluginData, msg.channel, `User not found`);
const memberToUnmute = await resolveMember(pluginData.client, pluginData.guild, user.id);
const mutesPlugin = pluginData.getPlugin(MutesPlugin);
const hasMuteRole = memberToUnmute && mutesPlugin.hasMutedRole(memberToUnmute);
// Check if they're muted in the first place
if (!(await pluginData.state.mutes.isMuted(args.user)) && !hasMuteRole) {
sendErrorMessage(pluginData, msg.channel, "Cannot unmute: member is not muted");
return;
}
if (!memberToUnmute) {
const banned = await isBanned(pluginData, memberToUnmute.id);
const prefix = pluginData.guildConfig.prefix;
if (banned) {
sendErrorMessage(
pluginData,
msg.channel,
`User is banned. Use \`${prefix}forceunmute\` to unmute them anyway.`,
);
} else {
sendErrorMessage(
pluginData,
msg.channel,
`User is not on the server. Use \`${prefix}forceunmute\` to unmute them anyway.`,
);
}
return;
}
// Make sure we're allowed to unmute this member
if (memberToUnmute && !canActOn(pluginData, msg.member, memberToUnmute)) {
sendErrorMessage(pluginData, msg.channel, "Cannot unmute: insufficient permissions");
return;
}
actualUnmuteCmd(pluginData, user, msg, args);
},
});

View file

@ -13,10 +13,15 @@ export const UpdateCmd = modActionsCommand({
description:
"Update the specified case (or, if case number is omitted, your latest case) by adding more notes/details to it",
signature: {
caseNumber: ct.number(),
note: ct.string({ required: false, catchAll: true }),
},
signature: [
{
caseNumber: ct.number(),
note: ct.string({ required: false, catchAll: true }),
},
{
note: ct.string({ catchAll: true }),
},
],
async run({ pluginData, message: msg, args }) {
let theCase: Case;

View file

@ -1,7 +1,7 @@
import { eventListener } from "knub";
import { IgnoredEventType, ModActionsPluginType } from "../types";
import { isEventIgnored } from "../functions/isEventIgnored";
import { clearIgnoredEvent } from "../functions/clearIgnoredEvents";
import { clearIgnoredEvents } from "../functions/clearIgnoredEvents";
import { Constants as ErisConstants } from "eris";
import { safeFindRelevantAuditLogEntry } from "../functions/safeFindRelevantAuditLogEntry";
import { CasesPlugin } from "../../Cases/CasesPlugin";
@ -15,7 +15,7 @@ export const CreateBanCaseOnManualBanEvt = eventListener<ModActionsPluginType>()
"guildBanAdd",
async ({ pluginData, args: { guild, user } }) => {
if (isEventIgnored(pluginData, IgnoredEventType.Ban, user.id)) {
clearIgnoredEvent(pluginData, IgnoredEventType.Ban, user.id);
clearIgnoredEvents(pluginData, IgnoredEventType.Ban, user.id);
return;
}

View file

@ -1,7 +1,7 @@
import { eventListener } from "knub";
import { IgnoredEventType, ModActionsPluginType } from "../types";
import { isEventIgnored } from "../functions/isEventIgnored";
import { clearIgnoredEvent } from "../functions/clearIgnoredEvents";
import { clearIgnoredEvents } from "../functions/clearIgnoredEvents";
import { Constants as ErisConstants } from "eris";
import { safeFindRelevantAuditLogEntry } from "../functions/safeFindRelevantAuditLogEntry";
import { CasesPlugin } from "../../Cases/CasesPlugin";
@ -18,7 +18,7 @@ export const CreateKickCaseOnManualKickEvt = eventListener<ModActionsPluginType>
"guildMemberRemove",
async ({ pluginData, args: { member } }) => {
if (isEventIgnored(pluginData, IgnoredEventType.Kick, member.id)) {
clearIgnoredEvent(pluginData, IgnoredEventType.Kick, member.id);
clearIgnoredEvents(pluginData, IgnoredEventType.Kick, member.id);
return;
}

View file

@ -1,7 +1,7 @@
import { eventListener } from "knub";
import { IgnoredEventType, ModActionsPluginType } from "../types";
import { isEventIgnored } from "../functions/isEventIgnored";
import { clearIgnoredEvent } from "../functions/clearIgnoredEvents";
import { clearIgnoredEvents } from "../functions/clearIgnoredEvents";
import { Constants as ErisConstants } from "eris";
import { safeFindRelevantAuditLogEntry } from "../functions/safeFindRelevantAuditLogEntry";
import { CasesPlugin } from "../../Cases/CasesPlugin";
@ -15,7 +15,7 @@ export const CreateUnbanCaseOnManualUnbanEvt = eventListener<ModActionsPluginTyp
"guildBanRemove",
async ({ pluginData, args: { guild, user } }) => {
if (isEventIgnored(pluginData, IgnoredEventType.Unban, user.id)) {
clearIgnoredEvent(pluginData, IgnoredEventType.Unban, user.id);
clearIgnoredEvents(pluginData, IgnoredEventType.Unban, user.id);
return;
}

View file

@ -0,0 +1,26 @@
import { eventListener } from "knub";
import { ModActionsPluginType } from "../types";
/**
* Show an alert if a member with prior notes joins the server
*/
export const PostAlertOnMemberJoinEvt = eventListener<ModActionsPluginType>()(
"guildMemberAdd",
async ({ pluginData, args: { guild, member } }) => {
const config = pluginData.config.get();
if (!config.alert_on_rejoin) return;
const alertChannelId = config.alert_channel;
if (!alertChannelId) return;
const actions = await pluginData.state.cases.getByUserId(member.id);
if (actions.length) {
const alertChannel: any = pluginData.guild.channels.get(alertChannelId);
alertChannel.send(
`<@!${member.id}> (${member.user.username}#${member.user.discriminator} \`${member.id}\`) joined with ${actions.length} prior record(s)`,
);
}
},
);

View file

@ -0,0 +1,107 @@
import { Member, TextChannel } from "eris";
import { LogType } from "src/data/LogType";
import { IgnoredEventType, ModActionsPluginType } from "../types";
import { errorMessage, resolveUser, resolveMember } from "src/utils";
import { PluginData } from "knub";
import { sendErrorMessage, canActOn, sendSuccessMessage } from "src/pluginUtils";
import { hasPermission } from "knub/dist/helpers";
import { readContactMethodsFromArgs } from "./readContactMethodsFromArgs";
import { formatReasonWithAttachments } from "./formatReasonWithAttachments";
import { kickMember } from "./kickMember";
import { ignoreEvent } from "./ignoreEvent";
import { isBanned } from "./isBanned";
export async function actualKickMemberCmd(
pluginData: PluginData<ModActionsPluginType>,
msg,
args: {
user: string;
reason: string;
mod: Member;
notify?: string;
"notify-channel"?: TextChannel;
clean?: boolean;
},
) {
const user = await resolveUser(pluginData.client, args.user);
if (!user) return sendErrorMessage(pluginData, msg.channel, `User not found`);
const memberToKick = await resolveMember(pluginData.client, pluginData.guild, user.id);
if (!memberToKick) {
const banned = await isBanned(pluginData, user.id);
if (banned) {
sendErrorMessage(pluginData, msg.channel, `User is banned`);
} else {
sendErrorMessage(pluginData, msg.channel, `User not found on the server`);
}
return;
}
// Make sure we're allowed to kick this member
if (!canActOn(pluginData, msg.member, memberToKick)) {
sendErrorMessage(pluginData, msg.channel, "Cannot kick: insufficient permissions");
return;
}
// The moderator who did the action is the message author or, if used, the specified -mod
let mod = msg.member;
if (args.mod) {
if (!hasPermission(pluginData.config.getForMessage(msg), "can_act_as_other")) {
sendErrorMessage(pluginData, msg.channel, "No permission for -mod");
return;
}
mod = args.mod;
}
let contactMethods;
try {
contactMethods = readContactMethodsFromArgs(args);
} catch (e) {
sendErrorMessage(pluginData, msg.channel, e.message);
return;
}
const reason = formatReasonWithAttachments(args.reason, msg.attachments);
const kickResult = await kickMember(pluginData, memberToKick, reason, {
contactMethods,
caseArgs: {
modId: mod.id,
ppId: mod.id !== msg.author.id ? msg.author.id : null,
},
});
if (args.clean) {
pluginData.state.serverLogs.ignoreLog(LogType.MEMBER_BAN, memberToKick.id);
ignoreEvent(pluginData, IgnoredEventType.Ban, memberToKick.id);
try {
await memberToKick.ban(1);
} catch (e) {
sendErrorMessage(pluginData, msg.channel, "Failed to ban the user to clean messages (-clean)");
}
pluginData.state.serverLogs.ignoreLog(LogType.MEMBER_UNBAN, memberToKick.id);
ignoreEvent(pluginData, IgnoredEventType.Unban, memberToKick.id);
try {
await pluginData.guild.unbanMember(memberToKick.id);
} catch (e) {
sendErrorMessage(pluginData, msg.channel, "Failed to unban the user after banning them (-clean)");
}
}
if (kickResult.status === "failed") {
msg.channel.createMessage(errorMessage(`Failed to kick user`));
return;
}
// Confirm the action to the moderator
let response = `Kicked **${memberToKick.user.username}#${memberToKick.user.discriminator}** (Case #${kickResult.case.case_number})`;
if (kickResult.notifyResult.text) response += ` (${kickResult.notifyResult.text})`;
sendSuccessMessage(pluginData, msg.channel, response);
}

View file

@ -0,0 +1,60 @@
import { PluginData } from "knub";
import { ModActionsPluginType } from "../types";
import { User, Message, Member } from "eris";
import { UnknownUser, asSingleLine } from "src/utils";
import { sendErrorMessage, sendSuccessMessage, hasPermission } from "src/pluginUtils";
import { formatReasonWithAttachments } from "./formatReasonWithAttachments";
import { MutesPlugin } from "src/plugins/Mutes/MutesPlugin";
import humanizeDuration from "humanize-duration";
export async function actualUnmuteCmd(
pluginData: PluginData<ModActionsPluginType>,
user: User | UnknownUser,
msg: Message,
args: { time?: number; reason?: string; mod?: Member },
) {
// The moderator who did the action is the message author or, if used, the specified -mod
let mod = msg.author;
let pp = null;
if (args.mod) {
if (!hasPermission(pluginData, "can_act_as_other", { message: msg, channelId: msg.channel.id })) {
sendErrorMessage(pluginData, msg.channel, "No permission for -mod");
return;
}
mod = args.mod.user;
pp = msg.author;
}
const reason = formatReasonWithAttachments(args.reason, msg.attachments);
const mutesPlugin = pluginData.getPlugin(MutesPlugin);
const result = await mutesPlugin.unmuteUser(user.id, args.time, {
modId: mod.id,
ppId: pp && pp.id,
reason,
});
// Confirm the action to the moderator
if (args.time) {
const timeUntilUnmute = args.time && humanizeDuration(args.time);
sendSuccessMessage(
pluginData,
msg.channel,
asSingleLine(`
Unmuting **${user.username}#${user.discriminator}**
in ${timeUntilUnmute} (Case #${result.case.case_number})
`),
);
} else {
sendSuccessMessage(
pluginData,
msg.channel,
asSingleLine(`
Unmuted **${user.username}#${user.discriminator}**
(Case #${result.case.case_number})
`),
);
}
}

View file

@ -1,7 +1,7 @@
import { PluginData } from "knub";
import { IgnoredEventType, ModActionsPluginType } from "../types";
export function clearIgnoredEvent(
export function clearIgnoredEvents(
pluginData: PluginData<ModActionsPluginType>,
type: IgnoredEventType,
userId: string,

View file

@ -1,7 +1,7 @@
import { PluginData } from "knub";
import { IgnoredEventType, ModActionsPluginType } from "../types";
import { SECONDS } from "../../../utils";
import { clearIgnoredEvent } from "./clearIgnoredEvents";
import { clearIgnoredEvents } from "./clearIgnoredEvents";
const DEFAULT_TIMEOUT = 15 * SECONDS;
@ -15,6 +15,6 @@ export function ignoreEvent(
// Clear after expiry (15sec by default)
setTimeout(() => {
clearIgnoredEvent(pluginData, type, userId);
clearIgnoredEvents(pluginData, type, userId);
}, timeout);
}

View file

@ -4,7 +4,6 @@ import { BasePluginType, command } from "knub";
import { GuildMutes } from "../../data/GuildMutes";
import { GuildCases } from "../../data/GuildCases";
import { GuildLogs } from "../../data/GuildLogs";
import { GuildArchives } from "../../data/GuildArchives";
import { Case } from "../../data/entities/Case";
import { CaseArgs } from "../Cases/types";
import { TextChannel } from "eris";

View file

@ -12,6 +12,9 @@ import { ClearActiveMuteOnRoleRemovalEvt } from "./events/ClearActiveMuteOnRoleR
import { ClearMutesWithoutRoleCmd } from "./commands/ClearMutesWithoutRoleCmd";
import { ClearMutesCmd } from "./commands/ClearMutesCmd";
import { muteUser } from "./functions/muteUser";
import { unmuteUser } from "./functions/unmuteUser";
import { CaseArgs } from "../Cases/types";
import { Member } from "eris";
const defaultOptions = {
config: {
@ -75,6 +78,16 @@ export const MutesPlugin = zeppelinPlugin<MutesPluginType>()("mutes", {
return muteUser(pluginData, userId, muteTime, reason, muteOptions);
};
},
unmuteUser(pluginData) {
return (userId: string, unmuteTime: number = null, args: Partial<CaseArgs>) => {
return unmuteUser(pluginData, userId, unmuteTime, args);
};
},
hasMutedRole(pluginData) {
return (member: Member) => {
return member.roles.includes(pluginData.config.get().mute_role);
};
},
},
onLoad(pluginData) {

View file

@ -13,6 +13,7 @@ import { GuildConfigReloaderPlugin } from "./GuildConfigReloader/GuildConfigRelo
import { CasesPlugin } from "./Cases/CasesPlugin";
import { MutesPlugin } from "./Mutes/MutesPlugin";
import { TagsPlugin } from "./Tags/TagsPlugin";
import { ModActionsPlugin } from "./ModActions/ModActionsPlugin";
import { PostPlugin } from "./Post/PostPlugin";
import { AutoDeletePlugin } from "./AutoDelete/AutoDeletePlugin";
import { GuildInfoSaverPlugin } from "./GuildInfoSaver/GuildInfoSaverPlugin";
@ -34,6 +35,7 @@ export const guildPlugins: Array<ZeppelinPluginBlueprint<any>> = [
PingableRolesPlugin,
PostPlugin,
MessageSaverPlugin,
ModActionsPlugin,
NameHistoryPlugin,
RemindersPlugin,
RolesPlugin,