WIP ModActions
This commit is contained in:
parent
a3d0ec03d9
commit
ebcb28261b
25 changed files with 1162 additions and 6 deletions
|
@ -8,6 +8,7 @@ import { decodeAndValidateStrict, StrictValidationError } from "./validatorUtils
|
|||
import { deepKeyIntersect, errorMessage, successMessage } from "./utils";
|
||||
import { ZeppelinPluginBlueprint } from "./plugins/ZeppelinPluginBlueprint";
|
||||
import { TZeppelinKnub } from "./types";
|
||||
import { ExtendedMatchParams } from "knub/dist/config/PluginConfigManager"; // TODO: Export from Knub index
|
||||
|
||||
const { getMemberLevel } = helpers;
|
||||
|
||||
|
@ -21,6 +22,11 @@ export function canActOn(pluginData: PluginData<any>, member1: Member, member2:
|
|||
return allowSameLevel ? ourLevel >= memberLevel : ourLevel > memberLevel;
|
||||
}
|
||||
|
||||
export function hasPermission(pluginData: PluginData<any>, permission: string, matchParams: ExtendedMatchParams) {
|
||||
const config = pluginData.config.getMatchingConfig(matchParams);
|
||||
return helpers.hasPermission(config, permission);
|
||||
}
|
||||
|
||||
export function getPluginConfigPreprocessor(blueprint: ZeppelinPluginBlueprint) {
|
||||
return (options: PluginOptions<any>) => {
|
||||
const decodedConfig = blueprint.configSchema
|
||||
|
|
|
@ -7,6 +7,8 @@ import { GuildCases } from "../../data/GuildCases";
|
|||
import { createCaseNote } from "./functions/createCaseNote";
|
||||
import { Case } from "../../data/entities/Case";
|
||||
import { postCaseToCaseLogChannel } from "./functions/postToCaseLogChannel";
|
||||
import { CaseTypes } from "../../data/CaseTypes";
|
||||
import { getCaseTypeAmountForUserId } from "./functions/getCaseTypeAmountForUserId";
|
||||
|
||||
const defaultOptions = {
|
||||
config: {
|
||||
|
@ -21,22 +23,28 @@ export const CasesPlugin = zeppelinPlugin<CasesPluginType>()("cases", {
|
|||
|
||||
public: {
|
||||
createCase(pluginData) {
|
||||
return async (args: CaseArgs) => {
|
||||
return (args: CaseArgs) => {
|
||||
return createCase(pluginData, args);
|
||||
};
|
||||
},
|
||||
|
||||
createCaseNote(pluginData) {
|
||||
return async (args: CaseNoteArgs) => {
|
||||
return (args: CaseNoteArgs) => {
|
||||
return createCaseNote(pluginData, args);
|
||||
};
|
||||
},
|
||||
|
||||
postCaseToCaseLogChannel(pluginData) {
|
||||
return async (caseOrCaseId: Case | number) => {
|
||||
return (caseOrCaseId: Case | number) => {
|
||||
return postCaseToCaseLogChannel(pluginData, caseOrCaseId);
|
||||
};
|
||||
},
|
||||
|
||||
getCaseTypeAmountForUserId(pluginData) {
|
||||
return (userID: string, type: CaseTypes) => {
|
||||
return getCaseTypeAmountForUserId(pluginData, userID, type);
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
onLoad(pluginData) {
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
import { PluginData } from "knub";
|
||||
import { CasesPluginType } from "../types";
|
||||
import { CaseTypes } from "../../../data/CaseTypes";
|
||||
|
||||
export async function getCaseTypeAmountForUserId(
|
||||
pluginData: PluginData<CasesPluginType>,
|
||||
userID: string,
|
||||
type: CaseTypes,
|
||||
): Promise<number> {
|
||||
const cases = (await pluginData.state.cases.getByUserId(userID)).filter(c => !c.is_hidden);
|
||||
let typeAmount = 0;
|
||||
|
||||
if (cases.length > 0) {
|
||||
cases.forEach(singleCase => {
|
||||
if (singleCase.type === type.valueOf()) {
|
||||
typeAmount++;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return typeAmount;
|
||||
}
|
77
backend/src/plugins/ModActions/ModActionsPlugin.ts
Normal file
77
backend/src/plugins/ModActions/ModActionsPlugin.ts
Normal file
|
@ -0,0 +1,77 @@
|
|||
import { zeppelinPlugin } from "../ZeppelinPluginBlueprint";
|
||||
import { CasesPlugin } from "../Cases/CasesPlugin";
|
||||
import { MutesPlugin } from "../Mutes/MutesPlugin";
|
||||
import { ConfigSchema, ModActionsPluginType } from "./types";
|
||||
import { CreateBanCaseOnManualBanEvt } from "./events/CreateBanCaseOnManualBanEvt";
|
||||
import { CreateUnbanCaseOnManualUnbanEvt } from "./events/CreateUnbanCaseOnManualUnbanEvt";
|
||||
import { CreateKickCaseOnManualKickEvt } from "./events/CreateKickCaseOnManualKickEvt";
|
||||
import { UpdateCmd } from "./commands/UpdateCmd";
|
||||
import { NoteCmd } from "./commands/NoteCmd";
|
||||
import { WarnCmd } from "./commands/WarnCmd";
|
||||
import { MuteCmd } from "./commands/MuteCmd";
|
||||
|
||||
const defaultOptions = {
|
||||
config: {
|
||||
dm_on_warn: true,
|
||||
dm_on_kick: false,
|
||||
dm_on_ban: false,
|
||||
message_on_warn: false,
|
||||
message_on_kick: false,
|
||||
message_on_ban: false,
|
||||
message_channel: null,
|
||||
warn_message: "You have received a warning on the {guildName} server: {reason}",
|
||||
kick_message: "You have been kicked from the {guildName} server. Reason given: {reason}",
|
||||
ban_message: "You have been banned from the {guildName} server. Reason given: {reason}",
|
||||
alert_on_rejoin: false,
|
||||
alert_channel: null,
|
||||
warn_notify_enabled: false,
|
||||
warn_notify_threshold: 5,
|
||||
warn_notify_message:
|
||||
"The user already has **{priorWarnings}** warnings!\n Please check their prior cases and assess whether or not to warn anyways.\n Proceed with the warning?",
|
||||
ban_delete_message_days: 1,
|
||||
|
||||
can_note: false,
|
||||
can_warn: false,
|
||||
can_mute: false,
|
||||
can_kick: false,
|
||||
can_ban: false,
|
||||
can_view: false,
|
||||
can_addcase: false,
|
||||
can_massban: false,
|
||||
can_hidecase: false,
|
||||
can_act_as_other: false,
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
level: ">=50",
|
||||
config: {
|
||||
can_note: true,
|
||||
can_warn: true,
|
||||
can_mute: true,
|
||||
can_kick: true,
|
||||
can_ban: true,
|
||||
can_view: true,
|
||||
can_addcase: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
level: ">=100",
|
||||
config: {
|
||||
can_massban: true,
|
||||
can_hidecase: true,
|
||||
can_act_as_other: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const ModActionsPlugin = zeppelinPlugin<ModActionsPluginType>()("mod_actions", {
|
||||
configSchema: ConfigSchema,
|
||||
defaultOptions,
|
||||
|
||||
dependencies: [CasesPlugin, MutesPlugin],
|
||||
|
||||
events: [CreateBanCaseOnManualBanEvt, CreateUnbanCaseOnManualUnbanEvt, CreateKickCaseOnManualKickEvt],
|
||||
|
||||
commands: [UpdateCmd, NoteCmd, WarnCmd, MuteCmd],
|
||||
});
|
78
backend/src/plugins/ModActions/commands/MuteCmd.ts
Normal file
78
backend/src/plugins/ModActions/commands/MuteCmd.ts
Normal file
|
@ -0,0 +1,78 @@
|
|||
import { modActionsCommand } from "../types";
|
||||
import { commandTypeHelpers as ct } from "../../../commandTypes";
|
||||
import { Case } from "../../../data/entities/Case";
|
||||
import { canActOn, hasPermission, sendErrorMessage, sendSuccessMessage } from "../../../pluginUtils";
|
||||
import { formatReasonWithAttachments } from "../functions/formatReasonWithAttachments";
|
||||
import { CasesPlugin } from "../../Cases/CasesPlugin";
|
||||
import { LogType } from "../../../data/LogType";
|
||||
import { CaseTypes } from "../../../data/CaseTypes";
|
||||
import { errorMessage, resolveMember, resolveUser, stripObjectToScalars } from "../../../utils";
|
||||
import { isBanned } from "../functions/isBanned";
|
||||
import { waitForReaction } from "knub/dist/helpers";
|
||||
import { readContactMethodsFromArgs } from "../functions/readContactMethodsFromArgs";
|
||||
import { warnMember } from "../functions/warnMember";
|
||||
import { TextChannel } from "eris";
|
||||
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 MuteCmd = modActionsCommand({
|
||||
trigger: "mute",
|
||||
permission: "can_mute",
|
||||
description: "Mute 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 memberToMute = await resolveMember(pluginData.client, pluginData.guild, user.id);
|
||||
|
||||
if (!memberToMute) {
|
||||
const _isBanned = await isBanned(pluginData, user.id);
|
||||
const prefix = pluginData.guildConfig.prefix;
|
||||
if (_isBanned) {
|
||||
sendErrorMessage(
|
||||
pluginData,
|
||||
msg.channel,
|
||||
`User is banned. Use \`${prefix}forcemute\` if you want to mute them anyway.`,
|
||||
);
|
||||
} else {
|
||||
sendErrorMessage(
|
||||
pluginData,
|
||||
msg.channel,
|
||||
`User is not on the server. Use \`${prefix}forcemute\` if you want to mute them anyway.`,
|
||||
);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Make sure we're allowed to mute this member
|
||||
if (memberToMute && !canActOn(pluginData, msg.member, memberToMute)) {
|
||||
sendErrorMessage(pluginData, msg.channel, "Cannot mute: insufficient permissions");
|
||||
return;
|
||||
}
|
||||
|
||||
actualMuteUserCmd(pluginData, user, msg, args);
|
||||
},
|
||||
});
|
44
backend/src/plugins/ModActions/commands/NoteCmd.ts
Normal file
44
backend/src/plugins/ModActions/commands/NoteCmd.ts
Normal file
|
@ -0,0 +1,44 @@
|
|||
import { modActionsCommand } from "../types";
|
||||
import { commandTypeHelpers as ct } from "../../../commandTypes";
|
||||
import { Case } from "../../../data/entities/Case";
|
||||
import { sendErrorMessage, sendSuccessMessage } from "../../../pluginUtils";
|
||||
import { formatReasonWithAttachments } from "../functions/formatReasonWithAttachments";
|
||||
import { CasesPlugin } from "../../Cases/CasesPlugin";
|
||||
import { LogType } from "../../../data/LogType";
|
||||
import { CaseTypes } from "../../../data/CaseTypes";
|
||||
import { resolveUser, stripObjectToScalars } from "../../../utils";
|
||||
|
||||
export const NoteCmd = modActionsCommand({
|
||||
trigger: "note",
|
||||
permission: "can_note",
|
||||
description: "Add a note to the specified user",
|
||||
|
||||
signature: {
|
||||
user: ct.string(),
|
||||
note: ct.string({ catchAll: true }),
|
||||
},
|
||||
|
||||
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 userName = `${user.username}#${user.discriminator}`;
|
||||
const reason = formatReasonWithAttachments(args.note, msg.attachments);
|
||||
|
||||
const casesPlugin = pluginData.getPlugin(CasesPlugin);
|
||||
const createdCase = await casesPlugin.createCase({
|
||||
userId: user.id,
|
||||
modId: msg.author.id,
|
||||
type: CaseTypes.Note,
|
||||
reason,
|
||||
});
|
||||
|
||||
pluginData.state.serverLogs.log(LogType.MEMBER_NOTE, {
|
||||
mod: stripObjectToScalars(msg.author),
|
||||
user: stripObjectToScalars(user, ["user", "roles"]),
|
||||
reason,
|
||||
});
|
||||
|
||||
sendSuccessMessage(pluginData, msg.channel, `Note added on **${userName}** (Case #${createdCase.case_number})`);
|
||||
},
|
||||
});
|
57
backend/src/plugins/ModActions/commands/UpdateCmd.ts
Normal file
57
backend/src/plugins/ModActions/commands/UpdateCmd.ts
Normal file
|
@ -0,0 +1,57 @@
|
|||
import { modActionsCommand } from "../types";
|
||||
import { commandTypeHelpers as ct } from "../../../commandTypes";
|
||||
import { Case } from "../../../data/entities/Case";
|
||||
import { sendErrorMessage, sendSuccessMessage } from "../../../pluginUtils";
|
||||
import { formatReasonWithAttachments } from "../functions/formatReasonWithAttachments";
|
||||
import { CasesPlugin } from "../../Cases/CasesPlugin";
|
||||
import { LogType } from "../../../data/LogType";
|
||||
import { CaseTypes } from "../../../data/CaseTypes";
|
||||
|
||||
export const UpdateCmd = modActionsCommand({
|
||||
trigger: "update",
|
||||
permission: "can_note",
|
||||
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 }),
|
||||
},
|
||||
|
||||
async run({ pluginData, message: msg, args }) {
|
||||
let theCase: Case;
|
||||
if (args.caseNumber != null) {
|
||||
theCase = await pluginData.state.cases.findByCaseNumber(args.caseNumber);
|
||||
} else {
|
||||
theCase = await pluginData.state.cases.findLatestByModId(msg.author.id);
|
||||
}
|
||||
|
||||
if (!theCase) {
|
||||
sendErrorMessage(pluginData, msg.channel, "Case not found");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!args.note && msg.attachments.length === 0) {
|
||||
sendErrorMessage(pluginData, msg.channel, "Text or attachment required");
|
||||
return;
|
||||
}
|
||||
|
||||
const note = formatReasonWithAttachments(args.note, msg.attachments);
|
||||
|
||||
const casesPlugin = pluginData.getPlugin(CasesPlugin);
|
||||
await casesPlugin.createCaseNote({
|
||||
caseId: theCase.id,
|
||||
modId: msg.author.id,
|
||||
body: note,
|
||||
});
|
||||
|
||||
pluginData.state.serverLogs.log(LogType.CASE_UPDATE, {
|
||||
mod: msg.author,
|
||||
caseNumber: theCase.case_number,
|
||||
caseType: CaseTypes[theCase.type],
|
||||
note,
|
||||
});
|
||||
|
||||
sendSuccessMessage(pluginData, msg.channel, `Case \`#${theCase.case_number}\` updated`);
|
||||
},
|
||||
});
|
113
backend/src/plugins/ModActions/commands/WarnCmd.ts
Normal file
113
backend/src/plugins/ModActions/commands/WarnCmd.ts
Normal file
|
@ -0,0 +1,113 @@
|
|||
import { modActionsCommand } from "../types";
|
||||
import { commandTypeHelpers as ct } from "../../../commandTypes";
|
||||
import { Case } from "../../../data/entities/Case";
|
||||
import { canActOn, hasPermission, sendErrorMessage, sendSuccessMessage } from "../../../pluginUtils";
|
||||
import { formatReasonWithAttachments } from "../functions/formatReasonWithAttachments";
|
||||
import { CasesPlugin } from "../../Cases/CasesPlugin";
|
||||
import { LogType } from "../../../data/LogType";
|
||||
import { CaseTypes } from "../../../data/CaseTypes";
|
||||
import { errorMessage, resolveMember, resolveUser, stripObjectToScalars } from "../../../utils";
|
||||
import { isBanned } from "../functions/isBanned";
|
||||
import { waitForReaction } from "knub/dist/helpers";
|
||||
import { readContactMethodsFromArgs } from "../functions/readContactMethodsFromArgs";
|
||||
import { warnMember } from "../functions/warnMember";
|
||||
import { TextChannel } from "eris";
|
||||
|
||||
export const WarnCmd = modActionsCommand({
|
||||
trigger: "warn",
|
||||
permission: "can_warn",
|
||||
description: "Send a warning to the specified user",
|
||||
|
||||
signature: {
|
||||
user: ct.string(),
|
||||
reason: ct.string({ catchAll: true }),
|
||||
|
||||
mod: ct.member({ option: true }),
|
||||
notify: ct.string({ option: true }),
|
||||
"notify-channel": ct.textChannel({ option: true }),
|
||||
},
|
||||
|
||||
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 memberToWarn = await resolveMember(pluginData.client, pluginData.guild, user.id);
|
||||
|
||||
if (!memberToWarn) {
|
||||
const _isBanned = await isBanned(pluginData, user.id);
|
||||
if (_isBanned) {
|
||||
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 warn this member
|
||||
if (!canActOn(pluginData, msg.member, memberToWarn)) {
|
||||
sendErrorMessage(pluginData, msg.channel, "Cannot warn: 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 })) {
|
||||
msg.channel.createMessage(errorMessage("No permission for -mod"));
|
||||
return;
|
||||
}
|
||||
|
||||
mod = args.mod;
|
||||
}
|
||||
|
||||
const config = pluginData.config.get();
|
||||
const reason = formatReasonWithAttachments(args.reason, msg.attachments);
|
||||
|
||||
const casesPlugin = pluginData.getPlugin(CasesPlugin);
|
||||
const priorWarnAmount = await casesPlugin.getCaseTypeAmountForUserId(memberToWarn.id, CaseTypes.Warn);
|
||||
if (config.warn_notify_enabled && priorWarnAmount >= config.warn_notify_threshold) {
|
||||
const tooManyWarningsMsg = await msg.channel.createMessage(
|
||||
config.warn_notify_message.replace("{priorWarnings}", `${priorWarnAmount}`),
|
||||
);
|
||||
|
||||
const reply = await waitForReaction(pluginData.client, tooManyWarningsMsg, ["✅", "❌"]);
|
||||
tooManyWarningsMsg.delete();
|
||||
if (!reply || reply.name === "❌") {
|
||||
msg.channel.createMessage(errorMessage("Warn cancelled by moderator"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let contactMethods;
|
||||
try {
|
||||
contactMethods = readContactMethodsFromArgs(args);
|
||||
} catch (e) {
|
||||
sendErrorMessage(pluginData, msg.channel, e.message);
|
||||
return;
|
||||
}
|
||||
|
||||
const warnResult = await warnMember(pluginData, memberToWarn, reason, {
|
||||
contactMethods,
|
||||
caseArgs: {
|
||||
modId: mod.id,
|
||||
ppId: mod.id !== msg.author.id ? msg.author.id : null,
|
||||
reason,
|
||||
},
|
||||
retryPromptChannel: msg.channel as TextChannel,
|
||||
});
|
||||
|
||||
if (warnResult.status === "failed") {
|
||||
sendErrorMessage(pluginData, msg.channel, "Failed to warn user");
|
||||
return;
|
||||
}
|
||||
|
||||
const messageResultText = warnResult.notifyResult.text ? ` (${warnResult.notifyResult.text})` : "";
|
||||
|
||||
sendSuccessMessage(
|
||||
pluginData,
|
||||
msg.channel,
|
||||
`Warned **${memberToWarn.user.username}#${memberToWarn.user.discriminator}** (Case #${warnResult.case.case_number})${messageResultText}`,
|
||||
);
|
||||
},
|
||||
});
|
|
@ -0,0 +1,49 @@
|
|||
import { eventListener } from "knub";
|
||||
import { IgnoredEventType, ModActionsPluginType } from "../types";
|
||||
import { isEventIgnored } from "../functions/isEventIgnored";
|
||||
import { clearIgnoredEvent } from "../functions/clearIgnoredEvents";
|
||||
import { Constants as ErisConstants } from "eris";
|
||||
import { safeFindRelevantAuditLogEntry } from "../functions/safeFindRelevantAuditLogEntry";
|
||||
import { CasesPlugin } from "../../Cases/CasesPlugin";
|
||||
import { CaseTypes } from "../../../data/CaseTypes";
|
||||
|
||||
/**
|
||||
* Create a BAN case automatically when a user is banned manually.
|
||||
* Attempts to find the ban's details in the audit log.
|
||||
*/
|
||||
export const CreateBanCaseOnManualBanEvt = eventListener<ModActionsPluginType>()(
|
||||
"guildBanAdd",
|
||||
async ({ pluginData, args: { guild, user } }) => {
|
||||
if (isEventIgnored(pluginData, IgnoredEventType.Ban, user.id)) {
|
||||
clearIgnoredEvent(pluginData, IgnoredEventType.Ban, user.id);
|
||||
return;
|
||||
}
|
||||
|
||||
const relevantAuditLogEntry = await safeFindRelevantAuditLogEntry(
|
||||
pluginData,
|
||||
ErisConstants.AuditLogActions.MEMBER_BAN_ADD,
|
||||
user.id,
|
||||
);
|
||||
|
||||
const casesPlugin = pluginData.getPlugin(CasesPlugin);
|
||||
if (relevantAuditLogEntry) {
|
||||
const modId = relevantAuditLogEntry.user.id;
|
||||
const auditLogId = relevantAuditLogEntry.id;
|
||||
|
||||
casesPlugin.createCase({
|
||||
userId: user.id,
|
||||
modId,
|
||||
type: CaseTypes.Ban,
|
||||
auditLogId,
|
||||
reason: relevantAuditLogEntry.reason,
|
||||
automatic: true,
|
||||
});
|
||||
} else {
|
||||
casesPlugin.createCase({
|
||||
userId: user.id,
|
||||
modId: null,
|
||||
type: CaseTypes.Ban,
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
|
@ -0,0 +1,55 @@
|
|||
import { eventListener } from "knub";
|
||||
import { IgnoredEventType, ModActionsPluginType } from "../types";
|
||||
import { isEventIgnored } from "../functions/isEventIgnored";
|
||||
import { clearIgnoredEvent } from "../functions/clearIgnoredEvents";
|
||||
import { Constants as ErisConstants } from "eris";
|
||||
import { safeFindRelevantAuditLogEntry } from "../functions/safeFindRelevantAuditLogEntry";
|
||||
import { CasesPlugin } from "../../Cases/CasesPlugin";
|
||||
import { CaseTypes } from "../../../data/CaseTypes";
|
||||
import { logger } from "../../../logger";
|
||||
import { LogType } from "../../../data/LogType";
|
||||
import { stripObjectToScalars } from "../../../utils";
|
||||
|
||||
/**
|
||||
* Create a KICK case automatically when a user is kicked manually.
|
||||
* Attempts to find the kick's details in the audit log.
|
||||
*/
|
||||
export const CreateKickCaseOnManualKickEvt = eventListener<ModActionsPluginType>()(
|
||||
"guildMemberRemove",
|
||||
async ({ pluginData, args: { member } }) => {
|
||||
if (isEventIgnored(pluginData, IgnoredEventType.Kick, member.id)) {
|
||||
clearIgnoredEvent(pluginData, IgnoredEventType.Kick, member.id);
|
||||
return;
|
||||
}
|
||||
|
||||
const kickAuditLogEntry = await safeFindRelevantAuditLogEntry(
|
||||
pluginData,
|
||||
ErisConstants.AuditLogActions.MEMBER_KICK,
|
||||
member.id,
|
||||
);
|
||||
|
||||
if (kickAuditLogEntry) {
|
||||
const existingCaseForThisEntry = await pluginData.state.cases.findByAuditLogId(kickAuditLogEntry.id);
|
||||
if (existingCaseForThisEntry) {
|
||||
logger.warn(
|
||||
`Tried to create duplicate case for audit log entry ${kickAuditLogEntry.id}, existing case id ${existingCaseForThisEntry.id}`,
|
||||
);
|
||||
} else {
|
||||
const casesPlugin = pluginData.getPlugin(CasesPlugin);
|
||||
casesPlugin.createCase({
|
||||
userId: member.id,
|
||||
modId: kickAuditLogEntry.user.id,
|
||||
type: CaseTypes.Kick,
|
||||
auditLogId: kickAuditLogEntry.id,
|
||||
reason: kickAuditLogEntry.reason,
|
||||
automatic: true,
|
||||
});
|
||||
}
|
||||
|
||||
pluginData.state.serverLogs.log(LogType.MEMBER_KICK, {
|
||||
user: stripObjectToScalars(member.user),
|
||||
mod: stripObjectToScalars(kickAuditLogEntry.user),
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
|
@ -0,0 +1,49 @@
|
|||
import { eventListener } from "knub";
|
||||
import { IgnoredEventType, ModActionsPluginType } from "../types";
|
||||
import { isEventIgnored } from "../functions/isEventIgnored";
|
||||
import { clearIgnoredEvent } from "../functions/clearIgnoredEvents";
|
||||
import { Constants as ErisConstants } from "eris";
|
||||
import { safeFindRelevantAuditLogEntry } from "../functions/safeFindRelevantAuditLogEntry";
|
||||
import { CasesPlugin } from "../../Cases/CasesPlugin";
|
||||
import { CaseTypes } from "../../../data/CaseTypes";
|
||||
|
||||
/**
|
||||
* Create an UNBAN case automatically when a user is unbanned manually.
|
||||
* Attempts to find the unban's details in the audit log.
|
||||
*/
|
||||
export const CreateUnbanCaseOnManualUnbanEvt = eventListener<ModActionsPluginType>()(
|
||||
"guildBanRemove",
|
||||
async ({ pluginData, args: { guild, user } }) => {
|
||||
if (isEventIgnored(pluginData, IgnoredEventType.Unban, user.id)) {
|
||||
clearIgnoredEvent(pluginData, IgnoredEventType.Unban, user.id);
|
||||
return;
|
||||
}
|
||||
|
||||
const relevantAuditLogEntry = await safeFindRelevantAuditLogEntry(
|
||||
pluginData,
|
||||
ErisConstants.AuditLogActions.MEMBER_BAN_REMOVE,
|
||||
user.id,
|
||||
);
|
||||
|
||||
const casesPlugin = pluginData.getPlugin(CasesPlugin);
|
||||
if (relevantAuditLogEntry) {
|
||||
const modId = relevantAuditLogEntry.user.id;
|
||||
const auditLogId = relevantAuditLogEntry.id;
|
||||
|
||||
casesPlugin.createCase({
|
||||
userId: user.id,
|
||||
modId,
|
||||
type: CaseTypes.Unban,
|
||||
auditLogId,
|
||||
automatic: true,
|
||||
});
|
||||
} else {
|
||||
casesPlugin.createCase({
|
||||
userId: user.id,
|
||||
modId: null,
|
||||
type: CaseTypes.Unban,
|
||||
automatic: true,
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
107
backend/src/plugins/ModActions/functions/actualMuteUserCmd.ts
Normal file
107
backend/src/plugins/ModActions/functions/actualMuteUserCmd.ts
Normal file
|
@ -0,0 +1,107 @@
|
|||
import { Member, Message, TextChannel, User } from "eris";
|
||||
import { asSingleLine, isDiscordRESTError, UnknownUser } from "../../../utils";
|
||||
import { hasPermission, sendErrorMessage, sendSuccessMessage } from "../../../pluginUtils";
|
||||
import { PluginData } from "knub";
|
||||
import { ModActionsPluginType } from "../types";
|
||||
import humanizeDuration from "humanize-duration";
|
||||
import { formatReasonWithAttachments } from "./formatReasonWithAttachments";
|
||||
import { MuteResult } from "../../Mutes/types";
|
||||
import { MutesPlugin } from "../../Mutes/MutesPlugin";
|
||||
import { readContactMethodsFromArgs } from "./readContactMethodsFromArgs";
|
||||
import { ERRORS, RecoverablePluginError } from "../../../RecoverablePluginError";
|
||||
import { logger } from "../../../logger";
|
||||
|
||||
/**
|
||||
* The actual function run by both !mute and !forcemute.
|
||||
* The only difference between the two commands is in target member validation.
|
||||
*/
|
||||
export async function actualMuteUserCmd(
|
||||
pluginData: PluginData<ModActionsPluginType>,
|
||||
user: User | UnknownUser,
|
||||
msg: Message,
|
||||
args: { time?: number; reason?: string; mod: Member; notify?: string; "notify-channel"?: TextChannel },
|
||||
) {
|
||||
// The moderator who did the action is the message author or, if used, the specified -mod
|
||||
let mod = msg.member;
|
||||
let pp = null;
|
||||
|
||||
if (args.mod) {
|
||||
if (!hasPermission(pluginData, "can_act_as_other", { message: msg })) {
|
||||
sendErrorMessage(pluginData, msg.channel, "No permission for -mod");
|
||||
return;
|
||||
}
|
||||
|
||||
mod = args.mod;
|
||||
pp = msg.author;
|
||||
}
|
||||
|
||||
const timeUntilUnmute = args.time && humanizeDuration(args.time);
|
||||
const reason = formatReasonWithAttachments(args.reason, msg.attachments);
|
||||
|
||||
let muteResult: MuteResult;
|
||||
const mutesPlugin = pluginData.getPlugin(MutesPlugin);
|
||||
|
||||
let contactMethods;
|
||||
try {
|
||||
contactMethods = readContactMethodsFromArgs(args);
|
||||
} catch (e) {
|
||||
sendErrorMessage(pluginData, msg.channel, e.message);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
muteResult = await mutesPlugin.muteUser(user.id, args.time, reason, {
|
||||
contactMethods,
|
||||
caseArgs: {
|
||||
modId: mod.id,
|
||||
ppId: pp && pp.id,
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
if (e instanceof RecoverablePluginError && e.code === ERRORS.NO_MUTE_ROLE_IN_CONFIG) {
|
||||
sendErrorMessage(pluginData, msg.channel, "Could not mute the user: no mute role set in config");
|
||||
} else if (isDiscordRESTError(e) && e.code === 10007) {
|
||||
sendErrorMessage(pluginData, msg.channel, "Could not mute the user: unknown member");
|
||||
} else {
|
||||
logger.error(`Failed to mute user ${user.id}: ${e.stack}`);
|
||||
if (user.id == null) {
|
||||
// tslint-disable-next-line:no-console
|
||||
console.trace("[DEBUG] Null user.id for mute");
|
||||
}
|
||||
sendErrorMessage(pluginData, msg.channel, "Could not mute the user");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Confirm the action to the moderator
|
||||
let response;
|
||||
if (args.time) {
|
||||
if (muteResult.updatedExistingMute) {
|
||||
response = asSingleLine(`
|
||||
Updated **${user.username}#${user.discriminator}**'s
|
||||
mute to ${timeUntilUnmute} (Case #${muteResult.case.case_number})
|
||||
`);
|
||||
} else {
|
||||
response = asSingleLine(`
|
||||
Muted **${user.username}#${user.discriminator}**
|
||||
for ${timeUntilUnmute} (Case #${muteResult.case.case_number})
|
||||
`);
|
||||
}
|
||||
} else {
|
||||
if (muteResult.updatedExistingMute) {
|
||||
response = asSingleLine(`
|
||||
Updated **${user.username}#${user.discriminator}**'s
|
||||
mute to indefinite (Case #${muteResult.case.case_number})
|
||||
`);
|
||||
} else {
|
||||
response = asSingleLine(`
|
||||
Muted **${user.username}#${user.discriminator}**
|
||||
indefinitely (Case #${muteResult.case.case_number})
|
||||
`);
|
||||
}
|
||||
}
|
||||
|
||||
if (muteResult.notifyResult.text) response += ` (${muteResult.notifyResult.text})`;
|
||||
sendSuccessMessage(pluginData, msg.channel, response);
|
||||
}
|
75
backend/src/plugins/ModActions/functions/banUserId.ts
Normal file
75
backend/src/plugins/ModActions/functions/banUserId.ts
Normal file
|
@ -0,0 +1,75 @@
|
|||
import { PluginData } from "knub";
|
||||
import { BanOptions, BanResult, IgnoredEventType, ModActionsPluginType } from "../types";
|
||||
import { notifyUser, resolveUser, stripObjectToScalars, ucfirst, UserNotificationResult } from "../../../utils";
|
||||
import { User } from "eris";
|
||||
import { renderTemplate } from "../../../templateFormatter";
|
||||
import { getDefaultContactMethods } from "./getDefaultContactMethods";
|
||||
import { LogType } from "../../../data/LogType";
|
||||
import { ignoreEvent } from "./ignoreEvent";
|
||||
import { CasesPlugin } from "../../Cases/CasesPlugin";
|
||||
import { CaseTypes } from "../../../data/CaseTypes";
|
||||
|
||||
/**
|
||||
* Ban the specified user id, whether or not they're actually on the server at the time. Generates a case.
|
||||
*/
|
||||
export async function banUserId(
|
||||
pluginData: PluginData<ModActionsPluginType>,
|
||||
userId: string,
|
||||
reason: string = null,
|
||||
banOptions: BanOptions = {},
|
||||
): Promise<BanResult> {
|
||||
const config = pluginData.config.get();
|
||||
const user = await resolveUser(pluginData.client, userId);
|
||||
|
||||
// Attempt to message the user *before* banning them, as doing it after may not be possible
|
||||
let notifyResult: UserNotificationResult = { method: null, success: true };
|
||||
if (reason && user instanceof User) {
|
||||
const banMessage = await renderTemplate(config.ban_message, {
|
||||
guildName: pluginData.guild.name,
|
||||
reason,
|
||||
});
|
||||
|
||||
const contactMethods = banOptions?.contactMethods
|
||||
? banOptions.contactMethods
|
||||
: getDefaultContactMethods(pluginData, "ban");
|
||||
notifyResult = await notifyUser(user, banMessage, contactMethods);
|
||||
}
|
||||
|
||||
// (Try to) ban the user
|
||||
pluginData.state.serverLogs.ignoreLog(LogType.MEMBER_BAN, userId);
|
||||
ignoreEvent(pluginData, IgnoredEventType.Ban, userId);
|
||||
try {
|
||||
const deleteMessageDays = Math.min(30, Math.max(0, banOptions.deleteMessageDays ?? 1));
|
||||
await pluginData.guild.banMember(userId, deleteMessageDays);
|
||||
} catch (e) {
|
||||
return {
|
||||
status: "failed",
|
||||
error: e.message,
|
||||
};
|
||||
}
|
||||
|
||||
// Create a case for this action
|
||||
const casesPlugin = pluginData.getPlugin(CasesPlugin);
|
||||
const createdCase = await casesPlugin.createCase({
|
||||
...(banOptions.caseArgs || {}),
|
||||
userId,
|
||||
modId: banOptions.caseArgs?.modId,
|
||||
type: CaseTypes.Ban,
|
||||
reason,
|
||||
noteDetails: notifyResult.text ? [ucfirst(notifyResult.text)] : [],
|
||||
});
|
||||
|
||||
// Log the action
|
||||
const mod = await resolveUser(pluginData.client, banOptions.caseArgs?.modId);
|
||||
pluginData.state.serverLogs.log(LogType.MEMBER_BAN, {
|
||||
mod: stripObjectToScalars(mod),
|
||||
user: stripObjectToScalars(user),
|
||||
reason,
|
||||
});
|
||||
|
||||
return {
|
||||
status: "success",
|
||||
case: createdCase,
|
||||
notifyResult,
|
||||
};
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
import { PluginData } from "knub";
|
||||
import { IgnoredEventType, ModActionsPluginType } from "../types";
|
||||
|
||||
export function clearIgnoredEvent(
|
||||
pluginData: PluginData<ModActionsPluginType>,
|
||||
type: IgnoredEventType,
|
||||
userId: string,
|
||||
) {
|
||||
pluginData.state.ignoredEvents.splice(
|
||||
pluginData.state.ignoredEvents.findIndex(info => type === info.type && userId === info.userId),
|
||||
1,
|
||||
);
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
import { Attachment } from "eris";
|
||||
|
||||
export function formatReasonWithAttachments(reason: string, attachments: Attachment[]) {
|
||||
const attachmentUrls = attachments.map(a => a.url);
|
||||
return ((reason || "") + " " + attachmentUrls.join(" ")).trim();
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
import { PluginData } from "knub";
|
||||
import { ModActionsPluginType } from "../types";
|
||||
import { UserNotificationMethod } from "../../../utils";
|
||||
import { TextChannel } from "eris";
|
||||
|
||||
export function getDefaultContactMethods(
|
||||
pluginData: PluginData<ModActionsPluginType>,
|
||||
type: "warn" | "kick" | "ban",
|
||||
): UserNotificationMethod[] {
|
||||
const methods: UserNotificationMethod[] = [];
|
||||
const config = pluginData.config.get();
|
||||
|
||||
if (config[`dm_on_${type}`]) {
|
||||
methods.push({ type: "dm" });
|
||||
}
|
||||
|
||||
if (config[`message_on_${type}`] && config.message_channel) {
|
||||
const channel = pluginData.guild.channels.get(config.message_channel);
|
||||
if (channel instanceof TextChannel) {
|
||||
methods.push({
|
||||
type: "channel",
|
||||
channel,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return methods;
|
||||
}
|
20
backend/src/plugins/ModActions/functions/ignoreEvent.ts
Normal file
20
backend/src/plugins/ModActions/functions/ignoreEvent.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
import { PluginData } from "knub";
|
||||
import { IgnoredEventType, ModActionsPluginType } from "../types";
|
||||
import { SECONDS } from "../../../utils";
|
||||
import { clearIgnoredEvent } from "./clearIgnoredEvents";
|
||||
|
||||
const DEFAULT_TIMEOUT = 15 * SECONDS;
|
||||
|
||||
export function ignoreEvent(
|
||||
pluginData: PluginData<ModActionsPluginType>,
|
||||
type: IgnoredEventType,
|
||||
userId: string,
|
||||
timeout = DEFAULT_TIMEOUT,
|
||||
) {
|
||||
pluginData.state.ignoredEvents.push({ type, userId });
|
||||
|
||||
// Clear after expiry (15sec by default)
|
||||
setTimeout(() => {
|
||||
clearIgnoredEvent(pluginData, type, userId);
|
||||
}, timeout);
|
||||
}
|
16
backend/src/plugins/ModActions/functions/isBanned.ts
Normal file
16
backend/src/plugins/ModActions/functions/isBanned.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
import { PluginData } from "knub";
|
||||
import { ModActionsPluginType } from "../types";
|
||||
import { isDiscordHTTPError } from "../../../utils";
|
||||
|
||||
export async function isBanned(pluginData: PluginData<ModActionsPluginType>, userId: string): Promise<boolean> {
|
||||
try {
|
||||
const bans = await pluginData.guild.getBans();
|
||||
return bans.some(b => b.user.id === userId);
|
||||
} catch (e) {
|
||||
if (isDiscordHTTPError(e) && e.code === 500) {
|
||||
return false;
|
||||
}
|
||||
|
||||
throw e;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
import { PluginData } from "knub";
|
||||
import { IgnoredEventType, ModActionsPluginType } from "../types";
|
||||
|
||||
export function isEventIgnored(pluginData: PluginData<ModActionsPluginType>, type: IgnoredEventType, userId: string) {
|
||||
return pluginData.state.ignoredEvents.some(info => type === info.type && userId === info.userId);
|
||||
}
|
73
backend/src/plugins/ModActions/functions/kickMember.ts
Normal file
73
backend/src/plugins/ModActions/functions/kickMember.ts
Normal file
|
@ -0,0 +1,73 @@
|
|||
import { PluginData } from "knub";
|
||||
import { IgnoredEventType, KickOptions, KickResult, ModActionsPluginType } from "../types";
|
||||
import { Member } from "eris";
|
||||
import { notifyUser, resolveUser, stripObjectToScalars, ucfirst, UserNotificationResult } from "../../../utils";
|
||||
import { renderTemplate } from "../../../templateFormatter";
|
||||
import { getDefaultContactMethods } from "./getDefaultContactMethods";
|
||||
import { LogType } from "../../../data/LogType";
|
||||
import { ignoreEvent } from "./ignoreEvent";
|
||||
import { CaseTypes } from "../../../data/CaseTypes";
|
||||
import { CasesPlugin } from "../../Cases/CasesPlugin";
|
||||
|
||||
/**
|
||||
* Kick the specified server member. Generates a case.
|
||||
*/
|
||||
export async function kickMember(
|
||||
pluginData: PluginData<ModActionsPluginType>,
|
||||
member: Member,
|
||||
reason: string = null,
|
||||
kickOptions: KickOptions = {},
|
||||
): Promise<KickResult> {
|
||||
const config = pluginData.config.get();
|
||||
|
||||
// Attempt to message the user *before* kicking them, as doing it after may not be possible
|
||||
let notifyResult: UserNotificationResult = { method: null, success: true };
|
||||
if (reason) {
|
||||
const kickMessage = await renderTemplate(config.kick_message, {
|
||||
guildName: pluginData.guild.name,
|
||||
reason,
|
||||
});
|
||||
|
||||
const contactMethods = kickOptions?.contactMethods
|
||||
? kickOptions.contactMethods
|
||||
: getDefaultContactMethods(pluginData, "kick");
|
||||
notifyResult = await notifyUser(member.user, kickMessage, contactMethods);
|
||||
}
|
||||
|
||||
// Kick the user
|
||||
pluginData.state.serverLogs.ignoreLog(LogType.MEMBER_KICK, member.id);
|
||||
ignoreEvent(pluginData, IgnoredEventType.Kick, member.id);
|
||||
try {
|
||||
await member.kick();
|
||||
} catch (e) {
|
||||
return {
|
||||
status: "failed",
|
||||
error: e.message,
|
||||
};
|
||||
}
|
||||
|
||||
// Create a case for this action
|
||||
const casesPlugin = pluginData.getPlugin(CasesPlugin);
|
||||
const createdCase = await casesPlugin.createCase({
|
||||
...(kickOptions.caseArgs || {}),
|
||||
userId: member.id,
|
||||
modId: kickOptions.caseArgs?.modId,
|
||||
type: CaseTypes.Kick,
|
||||
reason,
|
||||
noteDetails: notifyResult.text ? [ucfirst(notifyResult.text)] : [],
|
||||
});
|
||||
|
||||
// Log the action
|
||||
const mod = await resolveUser(pluginData.client, kickOptions.caseArgs?.modId);
|
||||
pluginData.state.serverLogs.log(LogType.MEMBER_KICK, {
|
||||
mod: stripObjectToScalars(mod),
|
||||
user: stripObjectToScalars(member.user),
|
||||
reason,
|
||||
});
|
||||
|
||||
return {
|
||||
status: "success",
|
||||
case: createdCase,
|
||||
notifyResult,
|
||||
};
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
import { TextChannel } from "eris";
|
||||
import { disableUserNotificationStrings, UserNotificationMethod } from "../../../utils";
|
||||
|
||||
export function readContactMethodsFromArgs(args: {
|
||||
notify?: string;
|
||||
"notify-channel"?: TextChannel;
|
||||
}): null | UserNotificationMethod[] {
|
||||
if (args.notify) {
|
||||
if (args.notify === "dm") {
|
||||
return [{ type: "dm" }];
|
||||
} else if (args.notify === "channel") {
|
||||
if (!args["notify-channel"]) {
|
||||
throw new Error("No `-notify-channel` specified");
|
||||
}
|
||||
|
||||
return [{ type: "channel", channel: args["notify-channel"] }];
|
||||
} else if (disableUserNotificationStrings.includes(args.notify)) {
|
||||
return [];
|
||||
} else {
|
||||
throw new Error("Unknown contact method");
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
import { findRelevantAuditLogEntry, isDiscordRESTError } from "../../../utils";
|
||||
import { PluginData } from "knub";
|
||||
import { ModActionsPluginType } from "../types";
|
||||
import { LogType } from "../../../data/LogType";
|
||||
|
||||
/**
|
||||
* Wrapper for findRelevantAuditLogEntry() that handles permission errors gracefully
|
||||
*/
|
||||
export async function safeFindRelevantAuditLogEntry(
|
||||
pluginData: PluginData<ModActionsPluginType>,
|
||||
actionType: number,
|
||||
userId: string,
|
||||
attempts?: number,
|
||||
attemptDelay?: number,
|
||||
) {
|
||||
try {
|
||||
return await findRelevantAuditLogEntry(pluginData.guild, actionType, userId, attempts, attemptDelay);
|
||||
} catch (e) {
|
||||
if (isDiscordRESTError(e) && e.code === 50013) {
|
||||
pluginData.state.serverLogs.log(LogType.BOT_ALERT, {
|
||||
body: "Missing permissions to read audit log",
|
||||
});
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
68
backend/src/plugins/ModActions/functions/warnMember.ts
Normal file
68
backend/src/plugins/ModActions/functions/warnMember.ts
Normal file
|
@ -0,0 +1,68 @@
|
|||
import { PluginData } from "knub";
|
||||
import { ModActionsPluginType, WarnOptions, WarnResult } from "../types";
|
||||
import { Member } from "eris";
|
||||
import { getDefaultContactMethods } from "./getDefaultContactMethods";
|
||||
import { notifyUser, resolveUser, stripObjectToScalars, ucfirst } from "../../../utils";
|
||||
import { waitForReaction } from "knub/dist/helpers";
|
||||
import { CasesPlugin } from "../../Cases/CasesPlugin";
|
||||
import { CaseTypes } from "../../../data/CaseTypes";
|
||||
import { LogType } from "../../../data/LogType";
|
||||
|
||||
export async function warnMember(
|
||||
pluginData: PluginData<ModActionsPluginType>,
|
||||
member: Member,
|
||||
reason: string,
|
||||
warnOptions: WarnOptions = {},
|
||||
): Promise<WarnResult | null> {
|
||||
const config = pluginData.config.get();
|
||||
|
||||
const warnMessage = config.warn_message.replace("{guildName}", pluginData.guild.name).replace("{reason}", reason);
|
||||
const contactMethods = warnOptions?.contactMethods
|
||||
? warnOptions.contactMethods
|
||||
: getDefaultContactMethods(pluginData, "warn");
|
||||
const notifyResult = await notifyUser(member.user, warnMessage, contactMethods);
|
||||
|
||||
if (!notifyResult.success) {
|
||||
if (warnOptions.retryPromptChannel && pluginData.guild.channels.has(warnOptions.retryPromptChannel.id)) {
|
||||
const failedMsg = await warnOptions.retryPromptChannel.createMessage(
|
||||
"Failed to message the user. Log the warning anyway?",
|
||||
);
|
||||
const reply = await waitForReaction(pluginData.client, failedMsg, ["✅", "❌"]);
|
||||
failedMsg.delete();
|
||||
if (!reply || reply.name === "❌") {
|
||||
return {
|
||||
status: "failed",
|
||||
error: "Failed to message user",
|
||||
};
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
status: "failed",
|
||||
error: "Failed to message user",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const casesPlugin = pluginData.getPlugin(CasesPlugin);
|
||||
const createdCase = await casesPlugin.createCase({
|
||||
...(warnOptions.caseArgs || {}),
|
||||
userId: member.id,
|
||||
modId: warnOptions.caseArgs?.modId,
|
||||
type: CaseTypes.Warn,
|
||||
reason,
|
||||
noteDetails: notifyResult.text ? [ucfirst(notifyResult.text)] : [],
|
||||
});
|
||||
|
||||
const mod = await resolveUser(pluginData.client, warnOptions.caseArgs?.modId);
|
||||
pluginData.state.serverLogs.log(LogType.MEMBER_WARN, {
|
||||
mod: stripObjectToScalars(mod),
|
||||
member: stripObjectToScalars(member, ["user", "roles"]),
|
||||
reason,
|
||||
});
|
||||
|
||||
return {
|
||||
status: "success",
|
||||
case: createdCase,
|
||||
notifyResult,
|
||||
};
|
||||
}
|
116
backend/src/plugins/ModActions/types.ts
Normal file
116
backend/src/plugins/ModActions/types.ts
Normal file
|
@ -0,0 +1,116 @@
|
|||
import * as t from "io-ts";
|
||||
import { tNullable, UserNotificationMethod, UserNotificationResult } from "../../utils";
|
||||
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";
|
||||
|
||||
export const ConfigSchema = t.type({
|
||||
dm_on_warn: t.boolean,
|
||||
dm_on_kick: t.boolean,
|
||||
dm_on_ban: t.boolean,
|
||||
message_on_warn: t.boolean,
|
||||
message_on_kick: t.boolean,
|
||||
message_on_ban: t.boolean,
|
||||
message_channel: tNullable(t.string),
|
||||
warn_message: tNullable(t.string),
|
||||
kick_message: tNullable(t.string),
|
||||
ban_message: tNullable(t.string),
|
||||
alert_on_rejoin: t.boolean,
|
||||
alert_channel: tNullable(t.string),
|
||||
warn_notify_enabled: t.boolean,
|
||||
warn_notify_threshold: t.number,
|
||||
warn_notify_message: t.string,
|
||||
ban_delete_message_days: t.number,
|
||||
can_note: t.boolean,
|
||||
can_warn: t.boolean,
|
||||
can_mute: t.boolean,
|
||||
can_kick: t.boolean,
|
||||
can_ban: t.boolean,
|
||||
can_view: t.boolean,
|
||||
can_addcase: t.boolean,
|
||||
can_massban: t.boolean,
|
||||
can_hidecase: t.boolean,
|
||||
can_act_as_other: t.boolean,
|
||||
});
|
||||
export type TConfigSchema = t.TypeOf<typeof ConfigSchema>;
|
||||
|
||||
export interface ModActionsPluginType extends BasePluginType {
|
||||
config: TConfigSchema;
|
||||
state: {
|
||||
mutes: GuildMutes;
|
||||
cases: GuildCases;
|
||||
serverLogs: GuildLogs;
|
||||
|
||||
ignoredEvents: IIgnoredEvent[];
|
||||
};
|
||||
}
|
||||
|
||||
export enum IgnoredEventType {
|
||||
Ban = 1,
|
||||
Unban,
|
||||
Kick,
|
||||
}
|
||||
|
||||
export interface IIgnoredEvent {
|
||||
type: IgnoredEventType;
|
||||
userId: string;
|
||||
}
|
||||
|
||||
export type WarnResult =
|
||||
| {
|
||||
status: "failed";
|
||||
error: string;
|
||||
}
|
||||
| {
|
||||
status: "success";
|
||||
case: Case;
|
||||
notifyResult: UserNotificationResult;
|
||||
};
|
||||
|
||||
export type KickResult =
|
||||
| {
|
||||
status: "failed";
|
||||
error: string;
|
||||
}
|
||||
| {
|
||||
status: "success";
|
||||
case: Case;
|
||||
notifyResult: UserNotificationResult;
|
||||
};
|
||||
|
||||
export type BanResult =
|
||||
| {
|
||||
status: "failed";
|
||||
error: string;
|
||||
}
|
||||
| {
|
||||
status: "success";
|
||||
case: Case;
|
||||
notifyResult: UserNotificationResult;
|
||||
};
|
||||
|
||||
export type WarnMemberNotifyRetryCallback = () => boolean | Promise<boolean>;
|
||||
|
||||
export interface WarnOptions {
|
||||
caseArgs?: Partial<CaseArgs>;
|
||||
contactMethods?: UserNotificationMethod[];
|
||||
retryPromptChannel?: TextChannel;
|
||||
}
|
||||
|
||||
export interface KickOptions {
|
||||
caseArgs?: Partial<CaseArgs>;
|
||||
contactMethods?: UserNotificationMethod[];
|
||||
}
|
||||
|
||||
export interface BanOptions {
|
||||
caseArgs?: Partial<CaseArgs>;
|
||||
contactMethods?: UserNotificationMethod[];
|
||||
deleteMessageDays?: number;
|
||||
}
|
||||
|
||||
export const modActionsCommand = command<ModActionsPluginType>();
|
|
@ -1,5 +1,5 @@
|
|||
import { zeppelinPlugin } from "../ZeppelinPluginBlueprint";
|
||||
import { ConfigSchema, MutesPluginType } from "./types";
|
||||
import { ConfigSchema, MuteOptions, MutesPluginType } from "./types";
|
||||
import { CasesPlugin } from "../Cases/CasesPlugin";
|
||||
import { GuildMutes } from "../../data/GuildMutes";
|
||||
import { GuildCases } from "../../data/GuildCases";
|
||||
|
@ -11,6 +11,7 @@ import { ClearBannedMutesCmd } from "./commands/ClearBannedMutesCmd";
|
|||
import { ClearActiveMuteOnRoleRemovalEvt } from "./events/ClearActiveMuteOnRoleRemovalEvt";
|
||||
import { ClearMutesWithoutRoleCmd } from "./commands/ClearMutesWithoutRoleCmd";
|
||||
import { ClearMutesCmd } from "./commands/ClearMutesCmd";
|
||||
import { muteUser } from "./functions/muteUser";
|
||||
|
||||
const defaultOptions = {
|
||||
config: {
|
||||
|
@ -55,9 +56,26 @@ export const MutesPlugin = zeppelinPlugin<MutesPluginType>()("mutes", {
|
|||
|
||||
dependencies: [CasesPlugin],
|
||||
|
||||
commands: [MutesCmd, ClearBannedMutesCmd, ClearMutesWithoutRoleCmd, ClearMutesCmd],
|
||||
// prettier-ignore
|
||||
commands: [
|
||||
MutesCmd,
|
||||
ClearBannedMutesCmd,
|
||||
ClearMutesWithoutRoleCmd,
|
||||
ClearMutesCmd,
|
||||
],
|
||||
|
||||
events: [ClearActiveMuteOnRoleRemovalEvt],
|
||||
// prettier-ignore
|
||||
events: [
|
||||
ClearActiveMuteOnRoleRemovalEvt,
|
||||
],
|
||||
|
||||
public: {
|
||||
muteUser(pluginData) {
|
||||
return (userId: string, muteTime: number = null, reason: string = null, muteOptions: MuteOptions = {}) => {
|
||||
return muteUser(pluginData, userId, muteTime, reason, muteOptions);
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
onLoad(pluginData) {
|
||||
pluginData.state.mutes = GuildMutes.getGuildInstance(pluginData.guild.id);
|
||||
|
|
Loading…
Add table
Reference in a new issue