Fix merge conflict
This commit is contained in:
commit
318f80a26d
12 changed files with 440 additions and 216 deletions
|
@ -1,21 +0,0 @@
|
|||
import util from "util";
|
||||
|
||||
export class PluginRuntimeError {
|
||||
public message: string;
|
||||
public pluginName: string;
|
||||
public guildId: string;
|
||||
|
||||
constructor(message: string, pluginName: string, guildId: string) {
|
||||
this.message = message;
|
||||
this.pluginName = pluginName;
|
||||
this.guildId = guildId;
|
||||
}
|
||||
|
||||
[util.inspect.custom](depth?, options?) {
|
||||
return `PRE [${this.pluginName}] [${this.guildId}] ${this.message}`;
|
||||
}
|
||||
|
||||
toString() {
|
||||
return this[util.inspect.custom]();
|
||||
}
|
||||
}
|
28
backend/src/RecoverablePluginError.ts
Normal file
28
backend/src/RecoverablePluginError.ts
Normal file
|
@ -0,0 +1,28 @@
|
|||
import { Guild } from "eris";
|
||||
|
||||
export enum ERRORS {
|
||||
NO_MUTE_ROLE_IN_CONFIG = 1,
|
||||
UNKNOWN_NOTE_CASE,
|
||||
INVALID_EMOJI,
|
||||
NO_USER_NOTIFICATION_CHANNEL,
|
||||
INVALID_USER_NOTIFICATION_CHANNEL,
|
||||
}
|
||||
|
||||
export const RECOVERABLE_PLUGIN_ERROR_MESSAGES = {
|
||||
[ERRORS.NO_MUTE_ROLE_IN_CONFIG]: "No mute role specified in config",
|
||||
[ERRORS.UNKNOWN_NOTE_CASE]: "Tried to add a note to an unknown case",
|
||||
[ERRORS.INVALID_EMOJI]: "Invalid emoji",
|
||||
[ERRORS.NO_USER_NOTIFICATION_CHANNEL]: "No user notify channel specified",
|
||||
[ERRORS.INVALID_USER_NOTIFICATION_CHANNEL]: "Invalid user notify channel specified",
|
||||
};
|
||||
|
||||
export class RecoverablePluginError extends Error {
|
||||
public readonly code: ERRORS;
|
||||
public readonly guild?: Guild;
|
||||
|
||||
constructor(code: ERRORS, guild?: Guild) {
|
||||
super(RECOVERABLE_PLUGIN_ERROR_MESSAGES[code]);
|
||||
this.guild = guild;
|
||||
this.code = code;
|
||||
}
|
||||
}
|
|
@ -26,6 +26,19 @@ setInterval(() => (recentDiscordErrors = Math.max(0, recentDiscordErrors - 1)),
|
|||
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
const errorHandler = err => {
|
||||
if (err instanceof RecoverablePluginError) {
|
||||
// Recoverable plugin errors can be, well, recovered from.
|
||||
// Log it in the console as a warning and post a warning to the guild's log.
|
||||
|
||||
// tslint:disable:no-console
|
||||
console.warn(`${err.guild.name}: [${err.code}] ${err.message}`);
|
||||
|
||||
const logs = new GuildLogs(err.guild.id);
|
||||
logs.log(LogType.BOT_ALERT, { body: `\`[${err.code}]\` ${err.message}` });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// tslint:disable:no-console
|
||||
console.error(err);
|
||||
|
||||
|
@ -76,6 +89,9 @@ import { errorMessage, successMessage } from "./utils";
|
|||
import { startUptimeCounter } from "./uptime";
|
||||
import { AllowedGuilds } from "./data/AllowedGuilds";
|
||||
import { IZeppelinGuildConfig, IZeppelinGlobalConfig } from "./types";
|
||||
import { RecoverablePluginError } from "./RecoverablePluginError";
|
||||
import { GuildLogs } from "./data/GuildLogs";
|
||||
import { LogType } from "./data/LogType";
|
||||
|
||||
logger.info("Connecting to database");
|
||||
connect().then(async conn => {
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import { PluginInfo, trimPluginDescription, ZeppelinPlugin } from "../ZeppelinPlugin";
|
||||
import { trimPluginDescription, ZeppelinPlugin } from "../ZeppelinPlugin";
|
||||
import * as t from "io-ts";
|
||||
import {
|
||||
convertDelayStringToMS,
|
||||
disableInlineCode,
|
||||
disableLinkPreviews,
|
||||
disableUserNotificationStrings,
|
||||
getEmojiInString,
|
||||
getInviteCodesInString,
|
||||
getRoleMentions,
|
||||
|
@ -15,12 +16,10 @@ import {
|
|||
SECONDS,
|
||||
stripObjectToScalars,
|
||||
tDeepPartial,
|
||||
tDelayString,
|
||||
tNullable,
|
||||
UnknownUser,
|
||||
UserNotificationMethod,
|
||||
verboseChannelMention,
|
||||
} from "../../utils";
|
||||
import { configUtils, CooldownManager, IPluginOptions, decorators as d, logger } from "knub";
|
||||
import { configUtils, CooldownManager, decorators as d, IPluginOptions, logger } from "knub";
|
||||
import { Member, Message, TextChannel, User } from "eris";
|
||||
import escapeStringRegexp from "escape-string-regexp";
|
||||
import { SimpleCache } from "../../SimpleCache";
|
||||
|
@ -29,7 +28,6 @@ import { ModActionsPlugin } from "../ModActions";
|
|||
import { MutesPlugin } from "../Mutes";
|
||||
import { LogsPlugin } from "../Logs";
|
||||
import { LogType } from "../../data/LogType";
|
||||
import { TSafeRegex } from "../../validatorUtils";
|
||||
import { GuildSavedMessages } from "../../data/GuildSavedMessages";
|
||||
import { GuildArchives } from "../../data/GuildArchives";
|
||||
import { GuildLogs } from "../../data/GuildLogs";
|
||||
|
@ -37,11 +35,9 @@ import { SavedMessage } from "../../data/entities/SavedMessage";
|
|||
import moment from "moment-timezone";
|
||||
import { renderTemplate } from "../../templateFormatter";
|
||||
import { transliterate } from "transliteration";
|
||||
import Timeout = NodeJS.Timeout;
|
||||
import { IMatchParams } from "knub/dist/configUtils";
|
||||
import { GuildAntiraidLevels } from "../../data/GuildAntiraidLevels";
|
||||
import {
|
||||
AnySpamTriggerMatchResult,
|
||||
AnyTriggerMatchResult,
|
||||
BaseTextSpamTrigger,
|
||||
MessageInfo,
|
||||
|
@ -66,6 +62,8 @@ import {
|
|||
TRule,
|
||||
} from "./types";
|
||||
import { pluginInfo } from "./info";
|
||||
import { ERRORS, RecoverablePluginError } from "../../RecoverablePluginError";
|
||||
import Timeout = NodeJS.Timeout;
|
||||
|
||||
const unactioned = (action: TextRecentAction | OtherRecentAction) => !action.actioned;
|
||||
|
||||
|
@ -878,6 +876,30 @@ export class AutomodPlugin extends ZeppelinPlugin<TConfigSchema, ICustomOverride
|
|||
}
|
||||
}
|
||||
|
||||
protected readContactMethodsFromAction(action: {
|
||||
notify?: string;
|
||||
notifyChannel?: string;
|
||||
}): UserNotificationMethod[] | null {
|
||||
if (action.notify === "dm") {
|
||||
return [{ type: "dm" }];
|
||||
} else if (action.notify === "channel") {
|
||||
if (!action.notifyChannel) {
|
||||
throw new RecoverablePluginError(ERRORS.NO_USER_NOTIFICATION_CHANNEL);
|
||||
}
|
||||
|
||||
const channel = this.guild.channels.get(action.notifyChannel);
|
||||
if (!(channel instanceof TextChannel)) {
|
||||
throw new RecoverablePluginError(ERRORS.INVALID_USER_NOTIFICATION_CHANNEL);
|
||||
}
|
||||
|
||||
return [{ type: "channel", channel }];
|
||||
} else if (action.notify && disableUserNotificationStrings.includes(action.notify)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the actions of the specified rule on the matched message/member
|
||||
*/
|
||||
|
@ -945,6 +967,7 @@ export class AutomodPlugin extends ZeppelinPlugin<TConfigSchema, ICustomOverride
|
|||
|
||||
if (rule.actions.warn) {
|
||||
const reason = rule.actions.warn.reason || "Warned automatically";
|
||||
const contactMethods = this.readContactMethodsFromAction(rule.actions.warn);
|
||||
|
||||
const caseArgs = {
|
||||
modId: this.bot.user.id,
|
||||
|
@ -962,7 +985,7 @@ export class AutomodPlugin extends ZeppelinPlugin<TConfigSchema, ICustomOverride
|
|||
|
||||
if (membersToWarn.length) {
|
||||
for (const member of membersToWarn) {
|
||||
await this.getModActions().warnMember(member, reason, caseArgs);
|
||||
await this.getModActions().warnMember(member, reason, { contactMethods, caseArgs });
|
||||
}
|
||||
|
||||
actionsTaken.push("warn");
|
||||
|
@ -976,6 +999,7 @@ export class AutomodPlugin extends ZeppelinPlugin<TConfigSchema, ICustomOverride
|
|||
modId: this.bot.user.id,
|
||||
extraNotes: [caseExtraNote],
|
||||
};
|
||||
const contactMethods = this.readContactMethodsFromAction(rule.actions.mute);
|
||||
|
||||
let userIdsToMute = [];
|
||||
if (matchResult.type === "message" || matchResult.type === "embed" || matchResult.type === "other") {
|
||||
|
@ -986,7 +1010,7 @@ export class AutomodPlugin extends ZeppelinPlugin<TConfigSchema, ICustomOverride
|
|||
|
||||
if (userIdsToMute.length) {
|
||||
for (const member of userIdsToMute) {
|
||||
await this.getMutes().muteUser(member.id, duration, reason, caseArgs);
|
||||
await this.getMutes().muteUser(member.id, duration, reason, { contactMethods, caseArgs });
|
||||
}
|
||||
|
||||
actionsTaken.push("mute");
|
||||
|
@ -999,6 +1023,7 @@ export class AutomodPlugin extends ZeppelinPlugin<TConfigSchema, ICustomOverride
|
|||
modId: this.bot.user.id,
|
||||
extraNotes: [caseExtraNote],
|
||||
};
|
||||
const contactMethods = this.readContactMethodsFromAction(rule.actions.kick);
|
||||
|
||||
let membersToKick = [];
|
||||
if (matchResult.type === "message" || matchResult.type === "embed" || matchResult.type === "other") {
|
||||
|
@ -1011,7 +1036,7 @@ export class AutomodPlugin extends ZeppelinPlugin<TConfigSchema, ICustomOverride
|
|||
|
||||
if (membersToKick.length) {
|
||||
for (const member of membersToKick) {
|
||||
await this.getModActions().kickMember(member, reason, caseArgs);
|
||||
await this.getModActions().kickMember(member, reason, { contactMethods, caseArgs });
|
||||
}
|
||||
|
||||
actionsTaken.push("kick");
|
||||
|
@ -1024,6 +1049,7 @@ export class AutomodPlugin extends ZeppelinPlugin<TConfigSchema, ICustomOverride
|
|||
modId: this.bot.user.id,
|
||||
extraNotes: [caseExtraNote],
|
||||
};
|
||||
const contactMethods = this.readContactMethodsFromAction(rule.actions.ban);
|
||||
|
||||
let userIdsToBan = [];
|
||||
if (matchResult.type === "message" || matchResult.type === "embed" || matchResult.type === "other") {
|
||||
|
@ -1034,7 +1060,7 @@ export class AutomodPlugin extends ZeppelinPlugin<TConfigSchema, ICustomOverride
|
|||
|
||||
if (userIdsToBan.length) {
|
||||
for (const userId of userIdsToBan) {
|
||||
await this.getModActions().banUserId(userId, reason, caseArgs);
|
||||
await this.getModActions().banUserId(userId, reason, { contactMethods, caseArgs });
|
||||
}
|
||||
|
||||
actionsTaken.push("ban");
|
||||
|
|
|
@ -226,20 +226,28 @@ export type TMemberJoinSpamTrigger = t.TypeOf<typeof MemberJoinTrigger>;
|
|||
export const CleanAction = t.boolean;
|
||||
|
||||
export const WarnAction = t.type({
|
||||
reason: t.string,
|
||||
reason: tNullable(t.string),
|
||||
notify: tNullable(t.string),
|
||||
notifyChannel: tNullable(t.string),
|
||||
});
|
||||
|
||||
export const MuteAction = t.type({
|
||||
duration: t.string,
|
||||
reason: tNullable(t.string),
|
||||
duration: tNullable(tDelayString),
|
||||
notify: tNullable(t.string),
|
||||
notifyChannel: tNullable(t.string),
|
||||
});
|
||||
|
||||
export const KickAction = t.type({
|
||||
reason: tNullable(t.string),
|
||||
notify: tNullable(t.string),
|
||||
notifyChannel: tNullable(t.string),
|
||||
});
|
||||
|
||||
export const BanAction = t.type({
|
||||
reason: tNullable(t.string),
|
||||
notify: tNullable(t.string),
|
||||
notifyChannel: tNullable(t.string),
|
||||
});
|
||||
|
||||
export const AlertAction = t.type({
|
||||
|
|
|
@ -11,6 +11,7 @@ import { GuildLogs } from "../data/GuildLogs";
|
|||
import { LogType } from "../data/LogType";
|
||||
import * as t from "io-ts";
|
||||
import { tNullable } from "../utils";
|
||||
import { ERRORS } from "../RecoverablePluginError";
|
||||
|
||||
const ConfigSchema = t.type({
|
||||
log_automatic_actions: t.boolean,
|
||||
|
@ -146,7 +147,7 @@ export class CasesPlugin extends ZeppelinPlugin<TConfigSchema> {
|
|||
public async createCaseNote(args: CaseNoteArgs): Promise<void> {
|
||||
const theCase = await this.cases.find(this.resolveCaseId(args.caseId));
|
||||
if (!theCase) {
|
||||
this.throwPluginRuntimeError(`Unknown case ID: ${args.caseId}`);
|
||||
this.throwRecoverablePluginError(ERRORS.UNKNOWN_NOTE_CASE);
|
||||
}
|
||||
|
||||
const mod = await this.resolveUser(args.modId);
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { GlobalPlugin, IBasePluginConfig, IPluginOptions, logger, configUtils } from "knub";
|
||||
import { PluginRuntimeError } from "../PluginRuntimeError";
|
||||
import * as t from "io-ts";
|
||||
import { pipe } from "fp-ts/lib/pipeable";
|
||||
import { fold } from "fp-ts/lib/Either";
|
||||
|
|
|
@ -7,20 +7,18 @@ import { GuildCases } from "../data/GuildCases";
|
|||
import {
|
||||
asSingleLine,
|
||||
createChunkedMessage,
|
||||
disableUserNotificationStrings,
|
||||
errorMessage,
|
||||
findRelevantAuditLogEntry,
|
||||
INotifyUserResult,
|
||||
multiSorter,
|
||||
notifyUser,
|
||||
NotifyUserStatus,
|
||||
stripObjectToScalars,
|
||||
successMessage,
|
||||
tNullable,
|
||||
trimEmptyStartEndLines,
|
||||
trimIndents,
|
||||
trimLines,
|
||||
ucfirst,
|
||||
UnknownUser,
|
||||
UserNotificationMethod,
|
||||
UserNotificationResult,
|
||||
} from "../utils";
|
||||
import { GuildMutes } from "../data/GuildMutes";
|
||||
import { CaseTypes } from "../data/CaseTypes";
|
||||
|
@ -32,6 +30,7 @@ import { renderTemplate } from "../templateFormatter";
|
|||
import { CaseArgs, CasesPlugin } from "./Cases";
|
||||
import { MuteResult, MutesPlugin } from "./Mutes";
|
||||
import * as t from "io-ts";
|
||||
import { ERRORS, RecoverablePluginError } from "../RecoverablePluginError";
|
||||
|
||||
const ConfigSchema = t.type({
|
||||
dm_on_warn: t.boolean,
|
||||
|
@ -80,7 +79,7 @@ export type WarnResult =
|
|||
| {
|
||||
status: "success";
|
||||
case: Case;
|
||||
notifyResult: INotifyUserResult;
|
||||
notifyResult: UserNotificationResult;
|
||||
};
|
||||
|
||||
export type KickResult =
|
||||
|
@ -91,7 +90,7 @@ export type KickResult =
|
|||
| {
|
||||
status: "success";
|
||||
case: Case;
|
||||
notifyResult: INotifyUserResult;
|
||||
notifyResult: UserNotificationResult;
|
||||
};
|
||||
|
||||
export type BanResult =
|
||||
|
@ -102,11 +101,27 @@ export type BanResult =
|
|||
| {
|
||||
status: "success";
|
||||
case: Case;
|
||||
notifyResult: INotifyUserResult;
|
||||
notifyResult: UserNotificationResult;
|
||||
};
|
||||
|
||||
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[];
|
||||
}
|
||||
|
||||
export class ModActionsPlugin extends ZeppelinPlugin<TConfigSchema> {
|
||||
public static pluginName = "mod_actions";
|
||||
public static dependencies = ["cases", "mutes"];
|
||||
|
@ -148,7 +163,7 @@ export class ModActionsPlugin extends ZeppelinPlugin<TConfigSchema> {
|
|||
ban_message: "You have been banned from the {guildName} server. Reason given: {reason}",
|
||||
alert_on_rejoin: false,
|
||||
alert_channel: null,
|
||||
warn_notify_threshold: 1,
|
||||
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?",
|
||||
|
||||
|
@ -202,7 +217,10 @@ export class ModActionsPlugin extends ZeppelinPlugin<TConfigSchema> {
|
|||
}
|
||||
|
||||
clearIgnoredEvent(type: IgnoredEventType, userId: any) {
|
||||
this.ignoredEvents.splice(this.ignoredEvents.findIndex(info => type === info.type && userId === info.userId), 1);
|
||||
this.ignoredEvents.splice(
|
||||
this.ignoredEvents.findIndex(info => type === info.type && userId === info.userId),
|
||||
1,
|
||||
);
|
||||
}
|
||||
|
||||
formatReasonWithAttachments(reason: string, attachments: Attachment[]) {
|
||||
|
@ -210,6 +228,50 @@ export class ModActionsPlugin extends ZeppelinPlugin<TConfigSchema> {
|
|||
return ((reason || "") + " " + attachmentUrls.join(" ")).trim();
|
||||
}
|
||||
|
||||
getDefaultContactMethods(type: "warn" | "kick" | "ban"): UserNotificationMethod[] {
|
||||
const methods: UserNotificationMethod[] = [];
|
||||
const config = this.getConfig();
|
||||
|
||||
if (config[`dm_on_${type}`]) {
|
||||
methods.push({ type: "dm" });
|
||||
}
|
||||
|
||||
if (config[`message_on_${type}`] && config.message_channel) {
|
||||
const channel = this.guild.channels.get(config.message_channel);
|
||||
if (channel instanceof TextChannel) {
|
||||
methods.push({
|
||||
type: "channel",
|
||||
channel,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return methods;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
async isBanned(userId): Promise<boolean> {
|
||||
try {
|
||||
const bans = (await this.guild.getBans()) as any;
|
||||
|
@ -330,9 +392,7 @@ export class ModActionsPlugin extends ZeppelinPlugin<TConfigSchema> {
|
|||
if (actions.length) {
|
||||
const alertChannel: any = this.guild.channels.get(alertChannelId);
|
||||
alertChannel.send(
|
||||
`<@!${member.id}> (${member.user.username}#${member.user.discriminator} \`${member.id}\`) joined with ${
|
||||
actions.length
|
||||
} prior record(s)`,
|
||||
`<@!${member.id}> (${member.user.username}#${member.user.discriminator} \`${member.id}\`) joined with ${actions.length} prior record(s)`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -353,9 +413,7 @@ export class ModActionsPlugin extends ZeppelinPlugin<TConfigSchema> {
|
|||
const existingCaseForThisEntry = await this.cases.findByAuditLogId(kickAuditLogEntry.id);
|
||||
if (existingCaseForThisEntry) {
|
||||
logger.warn(
|
||||
`Tried to create duplicate case for audit log entry ${kickAuditLogEntry.id}, existing case id ${
|
||||
existingCaseForThisEntry.id
|
||||
}`,
|
||||
`Tried to create duplicate case for audit log entry ${kickAuditLogEntry.id}, existing case id ${existingCaseForThisEntry.id}`,
|
||||
);
|
||||
} else {
|
||||
const casesPlugin = this.getPlugin<CasesPlugin>("cases");
|
||||
|
@ -379,22 +437,21 @@ export class ModActionsPlugin extends ZeppelinPlugin<TConfigSchema> {
|
|||
/**
|
||||
* Kick the specified server member. Generates a case.
|
||||
*/
|
||||
async kickMember(member: Member, reason: string = null, caseArgs: Partial<CaseArgs> = {}): Promise<KickResult> {
|
||||
async kickMember(member: Member, reason: string = null, kickOptions: KickOptions = {}): Promise<KickResult> {
|
||||
const config = this.getConfig();
|
||||
|
||||
// Attempt to message the user *before* kicking them, as doing it after may not be possible
|
||||
let notifyResult: INotifyUserResult = { status: NotifyUserStatus.Ignored };
|
||||
let notifyResult: UserNotificationResult = { method: null, success: true };
|
||||
if (reason) {
|
||||
const kickMessage = await renderTemplate(config.kick_message, {
|
||||
guildName: this.guild.name,
|
||||
reason,
|
||||
});
|
||||
|
||||
notifyResult = await notifyUser(this.bot, this.guild, member.user, kickMessage, {
|
||||
useDM: config.dm_on_kick,
|
||||
useChannel: config.message_on_kick,
|
||||
channelId: config.message_channel,
|
||||
});
|
||||
const contactMethods = kickOptions?.contactMethods
|
||||
? kickOptions.contactMethods
|
||||
: this.getDefaultContactMethods("kick");
|
||||
notifyResult = await notifyUser(member.user, kickMessage, contactMethods);
|
||||
}
|
||||
|
||||
// Kick the user
|
||||
|
@ -412,16 +469,16 @@ export class ModActionsPlugin extends ZeppelinPlugin<TConfigSchema> {
|
|||
// Create a case for this action
|
||||
const casesPlugin = this.getPlugin<CasesPlugin>("cases");
|
||||
const createdCase = await casesPlugin.createCase({
|
||||
...caseArgs,
|
||||
...(kickOptions.caseArgs || {}),
|
||||
userId: member.id,
|
||||
modId: caseArgs.modId,
|
||||
modId: kickOptions.caseArgs?.modId,
|
||||
type: CaseTypes.Kick,
|
||||
reason,
|
||||
noteDetails: notifyResult.status !== NotifyUserStatus.Ignored ? [ucfirst(notifyResult.text)] : [],
|
||||
noteDetails: notifyResult.text ? [ucfirst(notifyResult.text)] : [],
|
||||
});
|
||||
|
||||
// Log the action
|
||||
const mod = await this.resolveUser(caseArgs.modId);
|
||||
const mod = await this.resolveUser(kickOptions.caseArgs?.modId);
|
||||
this.serverLogs.log(LogType.MEMBER_KICK, {
|
||||
mod: stripObjectToScalars(mod),
|
||||
user: stripObjectToScalars(member.user),
|
||||
|
@ -437,22 +494,22 @@ export class ModActionsPlugin extends ZeppelinPlugin<TConfigSchema> {
|
|||
/**
|
||||
* Ban the specified user id, whether or not they're actually on the server at the time. Generates a case.
|
||||
*/
|
||||
async banUserId(userId: string, reason: string = null, caseArgs: Partial<CaseArgs> = {}): Promise<BanResult> {
|
||||
async banUserId(userId: string, reason: string = null, banOptions: BanOptions = {}): Promise<BanResult> {
|
||||
const config = this.getConfig();
|
||||
const user = await this.resolveUser(userId);
|
||||
|
||||
// Attempt to message the user *before* banning them, as doing it after may not be possible
|
||||
let notifyResult: INotifyUserResult = { status: NotifyUserStatus.Ignored };
|
||||
let notifyResult: UserNotificationResult = { method: null, success: true };
|
||||
if (reason && user instanceof User) {
|
||||
const banMessage = await renderTemplate(config.ban_message, {
|
||||
guildName: this.guild.name,
|
||||
reason,
|
||||
});
|
||||
notifyResult = await notifyUser(this.bot, this.guild, user, banMessage, {
|
||||
useDM: config.dm_on_ban,
|
||||
useChannel: config.message_on_ban,
|
||||
channelId: config.message_channel,
|
||||
});
|
||||
|
||||
const contactMethods = banOptions?.contactMethods
|
||||
? banOptions.contactMethods
|
||||
: this.getDefaultContactMethods("ban");
|
||||
notifyResult = await notifyUser(user, banMessage, contactMethods);
|
||||
}
|
||||
|
||||
// (Try to) ban the user
|
||||
|
@ -470,16 +527,16 @@ export class ModActionsPlugin extends ZeppelinPlugin<TConfigSchema> {
|
|||
// Create a case for this action
|
||||
const casesPlugin = this.getPlugin<CasesPlugin>("cases");
|
||||
const createdCase = await casesPlugin.createCase({
|
||||
...caseArgs,
|
||||
...(banOptions.caseArgs || {}),
|
||||
userId,
|
||||
modId: caseArgs.modId,
|
||||
modId: banOptions.caseArgs?.modId,
|
||||
type: CaseTypes.Ban,
|
||||
reason,
|
||||
noteDetails: notifyResult.status !== NotifyUserStatus.Ignored ? [ucfirst(notifyResult.text)] : [],
|
||||
noteDetails: notifyResult.text ? [ucfirst(notifyResult.text)] : [],
|
||||
});
|
||||
|
||||
// Log the action
|
||||
const mod = await this.resolveUser(caseArgs.modId);
|
||||
const mod = await this.resolveUser(banOptions.caseArgs?.modId);
|
||||
this.serverLogs.log(LogType.MEMBER_BAN, {
|
||||
mod: stripObjectToScalars(mod),
|
||||
user: stripObjectToScalars(user),
|
||||
|
@ -566,7 +623,11 @@ export class ModActionsPlugin extends ZeppelinPlugin<TConfigSchema> {
|
|||
}
|
||||
|
||||
@d.command("warn", "<user:string> <reason:string$>", {
|
||||
options: [{ name: "mod", type: "member" }],
|
||||
options: [
|
||||
{ name: "mod", type: "member" },
|
||||
{ name: "notify", type: "string" },
|
||||
{ name: "notify-channel", type: "channel" },
|
||||
],
|
||||
extra: {
|
||||
info: {
|
||||
description: "Send a warning to the specified user",
|
||||
|
@ -574,7 +635,10 @@ export class ModActionsPlugin extends ZeppelinPlugin<TConfigSchema> {
|
|||
},
|
||||
})
|
||||
@d.permission("can_warn")
|
||||
async warnCmd(msg: Message, args: { user: string; reason: string; mod?: Member }) {
|
||||
async warnCmd(
|
||||
msg: Message,
|
||||
args: { user: string; reason: string; mod?: Member; notify?: string; "notify-channel"?: TextChannel },
|
||||
) {
|
||||
const user = await this.resolveUser(args.user);
|
||||
if (!user) return this.sendErrorMessage(msg.channel, `User not found`);
|
||||
|
||||
|
@ -626,20 +690,26 @@ export class ModActionsPlugin extends ZeppelinPlugin<TConfigSchema> {
|
|||
}
|
||||
}
|
||||
|
||||
const warnMessage = config.warn_message.replace("{guildName}", this.guild.name).replace("{reason}", reason);
|
||||
const warnResult = await this.warnMember(
|
||||
memberToWarn,
|
||||
warnMessage,
|
||||
{
|
||||
let contactMethods;
|
||||
try {
|
||||
contactMethods = this.readContactMethodsFromArgs(args);
|
||||
} catch (e) {
|
||||
this.sendErrorMessage(msg.channel, e.message);
|
||||
return;
|
||||
}
|
||||
|
||||
const warnResult = await this.warnMember(memberToWarn, reason, {
|
||||
contactMethods,
|
||||
caseArgs: {
|
||||
modId: mod.id,
|
||||
ppId: mod.id !== msg.author.id ? msg.author.id : null,
|
||||
reason,
|
||||
},
|
||||
msg.channel as TextChannel,
|
||||
);
|
||||
retryPromptChannel: msg.channel as TextChannel,
|
||||
});
|
||||
|
||||
if (warnResult.status === "failed") {
|
||||
msg.channel.createMessage(errorMessage("Failed to warn user"));
|
||||
this.sendErrorMessage(msg.channel, "Failed to warn user");
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -647,28 +717,24 @@ export class ModActionsPlugin extends ZeppelinPlugin<TConfigSchema> {
|
|||
|
||||
this.sendSuccessMessage(
|
||||
msg.channel,
|
||||
`Warned **${memberToWarn.user.username}#${memberToWarn.user.discriminator}** (Case #${
|
||||
warnResult.case.case_number
|
||||
})${messageResultText}`,
|
||||
`Warned **${memberToWarn.user.username}#${memberToWarn.user.discriminator}** (Case #${warnResult.case.case_number})${messageResultText}`,
|
||||
);
|
||||
}
|
||||
|
||||
async warnMember(
|
||||
member: Member,
|
||||
warnMessage: string,
|
||||
caseArgs: Partial<CaseArgs> = {},
|
||||
retryPromptChannel: TextChannel = null,
|
||||
): Promise<WarnResult | null> {
|
||||
async warnMember(member: Member, reason: string, warnOptions: WarnOptions = {}): Promise<WarnResult | null> {
|
||||
const config = this.getConfig();
|
||||
|
||||
const notifyResult = await notifyUser(this.bot, this.guild, member.user, warnMessage, {
|
||||
useDM: config.dm_on_warn,
|
||||
useChannel: config.message_on_warn,
|
||||
});
|
||||
const warnMessage = config.warn_message.replace("{guildName}", this.guild.name).replace("{reason}", reason);
|
||||
const contactMethods = warnOptions?.contactMethods
|
||||
? warnOptions.contactMethods
|
||||
: this.getDefaultContactMethods("warn");
|
||||
const notifyResult = await notifyUser(member.user, warnMessage, contactMethods);
|
||||
|
||||
if (notifyResult.status === NotifyUserStatus.Failed) {
|
||||
if (retryPromptChannel && this.guild.channels.has(retryPromptChannel.id)) {
|
||||
const failedMsg = await retryPromptChannel.createMessage("Failed to message the user. Log the warning anyway?");
|
||||
if (!notifyResult.success) {
|
||||
if (warnOptions.retryPromptChannel && this.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(this.bot, failedMsg, ["✅", "❌"]);
|
||||
failedMsg.delete();
|
||||
if (!reply || reply.name === "❌") {
|
||||
|
@ -687,15 +753,15 @@ export class ModActionsPlugin extends ZeppelinPlugin<TConfigSchema> {
|
|||
|
||||
const casesPlugin = this.getPlugin<CasesPlugin>("cases");
|
||||
const createdCase = await casesPlugin.createCase({
|
||||
...caseArgs,
|
||||
...(warnOptions.caseArgs || {}),
|
||||
userId: member.id,
|
||||
modId: caseArgs.modId,
|
||||
modId: warnOptions.caseArgs?.modId,
|
||||
type: CaseTypes.Warn,
|
||||
reason: caseArgs.reason || warnMessage,
|
||||
noteDetails: notifyResult.status !== NotifyUserStatus.Ignored ? [ucfirst(notifyResult.text)] : [],
|
||||
reason,
|
||||
noteDetails: notifyResult.text ? [ucfirst(notifyResult.text)] : [],
|
||||
});
|
||||
|
||||
const mod = await this.resolveUser(caseArgs.modId);
|
||||
const mod = await this.resolveUser(warnOptions.caseArgs?.modId);
|
||||
this.serverLogs.log(LogType.MEMBER_WARN, {
|
||||
mod: stripObjectToScalars(mod),
|
||||
member: stripObjectToScalars(member, ["user", "roles"]),
|
||||
|
@ -712,7 +778,11 @@ export class ModActionsPlugin extends ZeppelinPlugin<TConfigSchema> {
|
|||
* The actual function run by both !mute and !forcemute.
|
||||
* The only difference between the two commands is in target member validation.
|
||||
*/
|
||||
async actualMuteCmd(user: User | UnknownUser, msg: Message, args: { time?: number; reason?: string; mod: Member }) {
|
||||
async actualMuteCmd(
|
||||
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;
|
||||
|
@ -733,17 +803,30 @@ export class ModActionsPlugin extends ZeppelinPlugin<TConfigSchema> {
|
|||
let muteResult: MuteResult;
|
||||
const mutesPlugin = this.getPlugin<MutesPlugin>("mutes");
|
||||
|
||||
let contactMethods;
|
||||
try {
|
||||
contactMethods = this.readContactMethodsFromArgs(args);
|
||||
} catch (e) {
|
||||
this.sendErrorMessage(msg.channel, e.message);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
muteResult = await mutesPlugin.muteUser(user.id, args.time, reason, {
|
||||
modId: mod.id,
|
||||
ppId: pp && pp.id,
|
||||
contactMethods,
|
||||
caseArgs: {
|
||||
modId: mod.id,
|
||||
ppId: pp && pp.id,
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
if (e instanceof DiscordRESTError && e.code === 10007) {
|
||||
msg.channel.createMessage(errorMessage("Could not mute the user: unknown member"));
|
||||
if (e instanceof RecoverablePluginError && e.code === ERRORS.NO_MUTE_ROLE_IN_CONFIG) {
|
||||
this.sendErrorMessage(msg.channel, "Could not mute the user: no mute role set in config");
|
||||
} else if (e instanceof DiscordRESTError && e.code === 10007) {
|
||||
this.sendErrorMessage(msg.channel, "Could not mute the user: unknown member");
|
||||
} else {
|
||||
logger.error(`Failed to mute user ${user.id}: ${e.stack}`);
|
||||
msg.channel.createMessage(errorMessage("Could not mute the user"));
|
||||
this.sendErrorMessage(msg.channel, "Could not mute the user");
|
||||
}
|
||||
|
||||
return;
|
||||
|
@ -783,7 +866,11 @@ export class ModActionsPlugin extends ZeppelinPlugin<TConfigSchema> {
|
|||
|
||||
@d.command("mute", "<user:string> <time:delay> <reason:string$>", {
|
||||
overloads: ["<user:string> <time:delay>", "<user:string> [reason:string$]"],
|
||||
options: [{ name: "mod", type: "member" }],
|
||||
options: [
|
||||
{ name: "mod", type: "member" },
|
||||
{ name: "notify", type: "string" },
|
||||
{ name: "notify-channel", type: "channel" },
|
||||
],
|
||||
extra: {
|
||||
info: {
|
||||
description: "Mute the specified member",
|
||||
|
@ -791,7 +878,17 @@ export class ModActionsPlugin extends ZeppelinPlugin<TConfigSchema> {
|
|||
},
|
||||
})
|
||||
@d.permission("can_mute")
|
||||
async muteCmd(msg: Message, args: { user: string; time?: number; reason?: string; mod: Member }) {
|
||||
async muteCmd(
|
||||
msg: Message,
|
||||
args: {
|
||||
user: string;
|
||||
time?: number;
|
||||
reason?: string;
|
||||
mod: Member;
|
||||
notify?: string;
|
||||
"notify-channel"?: TextChannel;
|
||||
},
|
||||
) {
|
||||
const user = await this.resolveUser(args.user);
|
||||
if (!user) return this.sendErrorMessage(msg.channel, `User not found`);
|
||||
|
||||
|
@ -826,7 +923,11 @@ export class ModActionsPlugin extends ZeppelinPlugin<TConfigSchema> {
|
|||
|
||||
@d.command("forcemute", "<user:string> <time:delay> <reason:string$>", {
|
||||
overloads: ["<user:string> <time:delay>", "<user:string> [reason:string$]"],
|
||||
options: [{ name: "mod", type: "member" }],
|
||||
options: [
|
||||
{ name: "mod", type: "member" },
|
||||
{ name: "notify", type: "string" },
|
||||
{ name: "notify-channel", type: "channel" },
|
||||
],
|
||||
extra: {
|
||||
info: {
|
||||
description: "Force-mute the specified user, even if they're not on the server",
|
||||
|
@ -834,7 +935,17 @@ export class ModActionsPlugin extends ZeppelinPlugin<TConfigSchema> {
|
|||
},
|
||||
})
|
||||
@d.permission("can_mute")
|
||||
async forcemuteCmd(msg: Message, args: { user: string; time?: number; reason?: string; mod: Member }) {
|
||||
async forcemuteCmd(
|
||||
msg: Message,
|
||||
args: {
|
||||
user: string;
|
||||
time?: number;
|
||||
reason?: string;
|
||||
mod: Member;
|
||||
notify?: string;
|
||||
"notify-channel"?: TextChannel;
|
||||
},
|
||||
) {
|
||||
const user = await this.resolveUser(args.user);
|
||||
if (!user) return this.sendErrorMessage(msg.channel, `User not found`);
|
||||
|
||||
|
@ -982,7 +1093,11 @@ export class ModActionsPlugin extends ZeppelinPlugin<TConfigSchema> {
|
|||
}
|
||||
|
||||
@d.command("kick", "<user:string> [reason:string$]", {
|
||||
options: [{ name: "mod", type: "member" }],
|
||||
options: [
|
||||
{ name: "mod", type: "member" },
|
||||
{ name: "notify", type: "string" },
|
||||
{ name: "notify-channel", type: "channel" },
|
||||
],
|
||||
extra: {
|
||||
info: {
|
||||
description: "Kick the specified member",
|
||||
|
@ -990,7 +1105,10 @@ export class ModActionsPlugin extends ZeppelinPlugin<TConfigSchema> {
|
|||
},
|
||||
})
|
||||
@d.permission("can_kick")
|
||||
async kickCmd(msg, args: { user: string; reason: string; mod: Member }) {
|
||||
async kickCmd(
|
||||
msg,
|
||||
args: { user: string; reason: string; mod: Member; notify?: string; "notify-channel"?: TextChannel },
|
||||
) {
|
||||
const user = await this.resolveUser(args.user);
|
||||
if (!user) return this.sendErrorMessage(msg.channel, `User not found`);
|
||||
|
||||
|
@ -1024,10 +1142,21 @@ export class ModActionsPlugin extends ZeppelinPlugin<TConfigSchema> {
|
|||
mod = args.mod;
|
||||
}
|
||||
|
||||
let contactMethods;
|
||||
try {
|
||||
contactMethods = this.readContactMethodsFromArgs(args);
|
||||
} catch (e) {
|
||||
this.sendErrorMessage(msg.channel, e.message);
|
||||
return;
|
||||
}
|
||||
|
||||
const reason = this.formatReasonWithAttachments(args.reason, msg.attachments);
|
||||
const kickResult = await this.kickMember(memberToKick, reason, {
|
||||
modId: mod.id,
|
||||
ppId: mod.id !== msg.author.id ? msg.author.id : null,
|
||||
contactMethods,
|
||||
caseArgs: {
|
||||
modId: mod.id,
|
||||
ppId: mod.id !== msg.author.id ? msg.author.id : null,
|
||||
},
|
||||
});
|
||||
|
||||
if (kickResult.status === "failed") {
|
||||
|
@ -1036,16 +1165,18 @@ export class ModActionsPlugin extends ZeppelinPlugin<TConfigSchema> {
|
|||
}
|
||||
|
||||
// Confirm the action to the moderator
|
||||
let response = `Kicked **${memberToKick.user.username}#${memberToKick.user.discriminator}** (Case #${
|
||||
kickResult.case.case_number
|
||||
})`;
|
||||
let response = `Kicked **${memberToKick.user.username}#${memberToKick.user.discriminator}** (Case #${kickResult.case.case_number})`;
|
||||
|
||||
if (kickResult.notifyResult.text) response += ` (${kickResult.notifyResult.text})`;
|
||||
this.sendSuccessMessage(msg.channel, response);
|
||||
}
|
||||
|
||||
@d.command("ban", "<user:string> [reason:string$]", {
|
||||
options: [{ name: "mod", type: "member" }],
|
||||
options: [
|
||||
{ name: "mod", type: "member" },
|
||||
{ name: "notify", type: "string" },
|
||||
{ name: "notify-channel", type: "channel" },
|
||||
],
|
||||
extra: {
|
||||
info: {
|
||||
description: "Ban the specified member",
|
||||
|
@ -1053,7 +1184,10 @@ export class ModActionsPlugin extends ZeppelinPlugin<TConfigSchema> {
|
|||
},
|
||||
})
|
||||
@d.permission("can_ban")
|
||||
async banCmd(msg, args: { user: string; reason?: string; mod?: Member }) {
|
||||
async banCmd(
|
||||
msg,
|
||||
args: { user: string; reason?: string; mod?: Member; notify?: string; "notify-channel"?: TextChannel },
|
||||
) {
|
||||
const user = await this.resolveUser(args.user);
|
||||
if (!user) return this.sendErrorMessage(msg.channel, `User not found`);
|
||||
|
||||
|
@ -1087,10 +1221,21 @@ export class ModActionsPlugin extends ZeppelinPlugin<TConfigSchema> {
|
|||
mod = args.mod;
|
||||
}
|
||||
|
||||
let contactMethods;
|
||||
try {
|
||||
contactMethods = this.readContactMethodsFromArgs(args);
|
||||
} catch (e) {
|
||||
this.sendErrorMessage(msg.channel, e.message);
|
||||
return;
|
||||
}
|
||||
|
||||
const reason = this.formatReasonWithAttachments(args.reason, msg.attachments);
|
||||
const banResult = await this.banUserId(memberToBan.id, reason, {
|
||||
modId: mod.id,
|
||||
ppId: mod.id !== msg.author.id ? msg.author.id : null,
|
||||
contactMethods,
|
||||
caseArgs: {
|
||||
modId: mod.id,
|
||||
ppId: mod.id !== msg.author.id ? msg.author.id : null,
|
||||
},
|
||||
});
|
||||
|
||||
if (banResult.status === "failed") {
|
||||
|
@ -1099,9 +1244,7 @@ export class ModActionsPlugin extends ZeppelinPlugin<TConfigSchema> {
|
|||
}
|
||||
|
||||
// Confirm the action to the moderator
|
||||
let response = `Banned **${memberToBan.user.username}#${memberToBan.user.discriminator}** (Case #${
|
||||
banResult.case.case_number
|
||||
})`;
|
||||
let response = `Banned **${memberToBan.user.username}#${memberToBan.user.discriminator}** (Case #${banResult.case.case_number})`;
|
||||
|
||||
if (banResult.notifyResult.text) response += ` (${banResult.notifyResult.text})`;
|
||||
this.sendSuccessMessage(msg.channel, response);
|
||||
|
@ -1186,9 +1329,7 @@ export class ModActionsPlugin extends ZeppelinPlugin<TConfigSchema> {
|
|||
// Confirm the action to the moderator
|
||||
this.sendSuccessMessage(
|
||||
msg.channel,
|
||||
`Softbanned **${memberToSoftban.user.username}#${memberToSoftban.user.discriminator}** (Case #${
|
||||
createdCase.case_number
|
||||
})`,
|
||||
`Softbanned **${memberToSoftban.user.username}#${memberToSoftban.user.discriminator}** (Case #${createdCase.case_number})`,
|
||||
);
|
||||
|
||||
// Log the action
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Member, Message, User } from "eris";
|
||||
import { Member, Message, TextChannel, User } from "eris";
|
||||
import { GuildCases } from "../data/GuildCases";
|
||||
import moment from "moment-timezone";
|
||||
import { ZeppelinPlugin } from "./ZeppelinPlugin";
|
||||
|
@ -7,15 +7,15 @@ import {
|
|||
chunkMessageLines,
|
||||
DBDateFormat,
|
||||
errorMessage,
|
||||
INotifyUserResult,
|
||||
UserNotificationResult,
|
||||
noop,
|
||||
notifyUser,
|
||||
NotifyUserStatus,
|
||||
stripObjectToScalars,
|
||||
successMessage,
|
||||
tNullable,
|
||||
ucfirst,
|
||||
UnknownUser,
|
||||
UserNotificationMethod,
|
||||
} from "../utils";
|
||||
import humanizeDuration from "humanize-duration";
|
||||
import { LogType } from "../data/LogType";
|
||||
|
@ -27,6 +27,7 @@ import { CaseTypes } from "../data/CaseTypes";
|
|||
import { CaseArgs, CasesPlugin } from "./Cases";
|
||||
import { Case } from "../data/entities/Case";
|
||||
import * as t from "io-ts";
|
||||
import { ERRORS, RecoverablePluginError } from "../RecoverablePluginError";
|
||||
|
||||
const ConfigSchema = t.type({
|
||||
mute_role: tNullable(t.string),
|
||||
|
@ -53,7 +54,7 @@ interface IMuteWithDetails extends Mute {
|
|||
|
||||
export type MuteResult = {
|
||||
case: Case;
|
||||
notifyResult: INotifyUserResult;
|
||||
notifyResult: UserNotificationResult;
|
||||
updatedExistingMute: boolean;
|
||||
};
|
||||
|
||||
|
@ -61,6 +62,11 @@ export type UnmuteResult = {
|
|||
case: Case;
|
||||
};
|
||||
|
||||
export interface MuteOptions {
|
||||
caseArgs?: Partial<CaseArgs>;
|
||||
contactMethods?: UserNotificationMethod[];
|
||||
}
|
||||
|
||||
const EXPIRED_MUTE_CHECK_INTERVAL = 60 * 1000;
|
||||
let FIRST_CHECK_TIME = Date.now();
|
||||
const FIRST_CHECK_INCREMENT = 5 * 1000;
|
||||
|
@ -136,16 +142,19 @@ export class MutesPlugin extends ZeppelinPlugin<TConfigSchema> {
|
|||
userId: string,
|
||||
muteTime: number = null,
|
||||
reason: string = null,
|
||||
caseArgs: Partial<CaseArgs> = {},
|
||||
muteOptions: MuteOptions = {},
|
||||
): Promise<MuteResult> {
|
||||
const muteRole = this.getConfig().mute_role;
|
||||
if (!muteRole) return;
|
||||
if (!muteRole) {
|
||||
this.throwRecoverablePluginError(ERRORS.NO_MUTE_ROLE_IN_CONFIG);
|
||||
}
|
||||
|
||||
const timeUntilUnmute = muteTime ? humanizeDuration(muteTime) : "indefinite";
|
||||
|
||||
// No mod specified -> mark Zeppelin as the mod
|
||||
if (!caseArgs.modId) {
|
||||
caseArgs.modId = this.bot.user.id;
|
||||
if (!muteOptions.caseArgs?.modId) {
|
||||
muteOptions.caseArgs = muteOptions.caseArgs ?? {};
|
||||
muteOptions.caseArgs.modId = this.bot.user.id;
|
||||
}
|
||||
|
||||
const user = await this.resolveUser(userId);
|
||||
|
@ -170,7 +179,7 @@ export class MutesPlugin extends ZeppelinPlugin<TConfigSchema> {
|
|||
|
||||
// If the user is already muted, update the duration of their existing mute
|
||||
const existingMute = await this.mutes.findExistingMuteForUserId(user.id);
|
||||
let notifyResult: INotifyUserResult = { status: NotifyUserStatus.Ignored };
|
||||
let notifyResult: UserNotificationResult = { method: null, success: true };
|
||||
|
||||
if (existingMute) {
|
||||
await this.mutes.updateExpiryTime(user.id, muteTime);
|
||||
|
@ -192,19 +201,27 @@ export class MutesPlugin extends ZeppelinPlugin<TConfigSchema> {
|
|||
time: timeUntilUnmute,
|
||||
}));
|
||||
|
||||
if (muteMessage) {
|
||||
const useDm = existingMute ? config.dm_on_update : config.dm_on_mute;
|
||||
const useChannel = existingMute ? config.message_on_update : config.message_on_mute;
|
||||
if (user instanceof User) {
|
||||
notifyResult = await notifyUser(this.bot, this.guild, user, muteMessage, {
|
||||
useDM: useDm,
|
||||
useChannel,
|
||||
channelId: config.message_channel,
|
||||
});
|
||||
if (muteMessage && user instanceof User) {
|
||||
let contactMethods = [];
|
||||
|
||||
if (muteOptions?.contactMethods) {
|
||||
contactMethods = muteOptions.contactMethods;
|
||||
} else {
|
||||
notifyResult = { status: NotifyUserStatus.Failed };
|
||||
const useDm = existingMute ? config.dm_on_update : config.dm_on_mute;
|
||||
if (useDm) {
|
||||
contactMethods.push({ type: "dm" });
|
||||
}
|
||||
|
||||
const useChannel = existingMute ? config.message_on_update : config.message_on_mute;
|
||||
const channel = config.message_channel && this.guild.channels.get(config.message_channel);
|
||||
if (useChannel && channel instanceof TextChannel) {
|
||||
contactMethods.push({ type: "channel", channel });
|
||||
}
|
||||
}
|
||||
|
||||
notifyResult = await notifyUser(user, muteMessage, contactMethods);
|
||||
}
|
||||
|
||||
// Create/update a case
|
||||
const casesPlugin = this.getPlugin<CasesPlugin>("cases");
|
||||
let theCase;
|
||||
|
@ -215,31 +232,31 @@ export class MutesPlugin extends ZeppelinPlugin<TConfigSchema> {
|
|||
// but instead we'll post the entire case afterwards
|
||||
theCase = await this.cases.find(existingMute.case_id);
|
||||
const noteDetails = [`Mute updated to ${muteTime ? timeUntilUnmute : "indefinite"}`];
|
||||
const reasons = [reason, ...(caseArgs.extraNotes || [])];
|
||||
const reasons = [reason, ...(muteOptions.caseArgs?.extraNotes || [])];
|
||||
for (const noteReason of reasons) {
|
||||
await casesPlugin.createCaseNote({
|
||||
caseId: existingMute.case_id,
|
||||
modId: caseArgs.modId,
|
||||
modId: muteOptions.caseArgs?.modId,
|
||||
body: noteReason,
|
||||
noteDetails,
|
||||
postInCaseLogOverride: false,
|
||||
});
|
||||
}
|
||||
|
||||
if (caseArgs.postInCaseLogOverride !== false) {
|
||||
if (muteOptions.caseArgs?.postInCaseLogOverride !== false) {
|
||||
casesPlugin.postCaseToCaseLogChannel(existingMute.case_id);
|
||||
}
|
||||
} else {
|
||||
// Create new case
|
||||
const noteDetails = [`Muted ${muteTime ? `for ${timeUntilUnmute}` : "indefinitely"}`];
|
||||
if (notifyResult.status !== NotifyUserStatus.Ignored) {
|
||||
if (notifyResult.text) {
|
||||
noteDetails.push(ucfirst(notifyResult.text));
|
||||
}
|
||||
|
||||
theCase = await casesPlugin.createCase({
|
||||
...caseArgs,
|
||||
...(muteOptions.caseArgs || {}),
|
||||
userId,
|
||||
modId: caseArgs.modId,
|
||||
modId: muteOptions.caseArgs?.modId,
|
||||
type: CaseTypes.Mute,
|
||||
reason,
|
||||
noteDetails,
|
||||
|
@ -248,7 +265,7 @@ export class MutesPlugin extends ZeppelinPlugin<TConfigSchema> {
|
|||
}
|
||||
|
||||
// Log the action
|
||||
const mod = await this.resolveUser(caseArgs.modId);
|
||||
const mod = await this.resolveUser(muteOptions.caseArgs?.modId);
|
||||
if (muteTime) {
|
||||
this.serverLogs.log(LogType.MEMBER_TIMED_MUTE, {
|
||||
mod: stripObjectToScalars(mod),
|
||||
|
|
|
@ -254,8 +254,10 @@ export class SpamPlugin extends ZeppelinPlugin<TConfigSchema> {
|
|||
? convertDelayStringToMS(spamConfig.mute_time.toString())
|
||||
: 120 * 1000;
|
||||
muteResult = await mutesPlugin.muteUser(member.id, muteTime, "Automatic spam detection", {
|
||||
modId: this.bot.user.id,
|
||||
postInCaseLogOverride: false,
|
||||
caseArgs: {
|
||||
modId: this.bot.user.id,
|
||||
postInCaseLogOverride: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -374,8 +376,10 @@ export class SpamPlugin extends ZeppelinPlugin<TConfigSchema> {
|
|||
const mutesPlugin = this.getPlugin<MutesPlugin>("mutes");
|
||||
const muteTime = spamConfig.mute_time ? convertDelayStringToMS(spamConfig.mute_time.toString()) : 120 * 1000;
|
||||
await mutesPlugin.muteUser(member.id, muteTime, "Automatic spam detection", {
|
||||
modId: this.bot.user.id,
|
||||
extraNotes: [`Details: ${details}`],
|
||||
caseArgs: {
|
||||
modId: this.bot.user.id,
|
||||
extraNotes: [`Details: ${details}`],
|
||||
},
|
||||
});
|
||||
} else {
|
||||
// If we're not muting the user, just add a note on them
|
||||
|
|
|
@ -1,30 +1,27 @@
|
|||
import { IBasePluginConfig, IPluginOptions, logger, Plugin, configUtils } from "knub";
|
||||
import { PluginRuntimeError } from "../PluginRuntimeError";
|
||||
import { configUtils, IBasePluginConfig, IPluginOptions, logger, Plugin } from "knub";
|
||||
import * as t from "io-ts";
|
||||
import { pipe } from "fp-ts/lib/pipeable";
|
||||
import { fold } from "fp-ts/lib/Either";
|
||||
import { PathReporter } from "io-ts/lib/PathReporter";
|
||||
import {
|
||||
deepKeyIntersect,
|
||||
isSnowflake,
|
||||
isUnicodeEmoji,
|
||||
MINUTES,
|
||||
Not,
|
||||
resolveMember,
|
||||
resolveRoleId,
|
||||
resolveUser,
|
||||
resolveUserId,
|
||||
tDeepPartial,
|
||||
trimEmptyStartEndLines,
|
||||
trimIndents,
|
||||
UnknownUser,
|
||||
resolveRoleId,
|
||||
} from "../utils";
|
||||
import { Invite, Member, User } from "eris";
|
||||
import DiscordRESTError from "eris/lib/errors/DiscordRESTError"; // tslint:disable-line
|
||||
import { performance } from "perf_hooks";
|
||||
import { decodeAndValidateStrict, StrictValidationError, validate } from "../validatorUtils";
|
||||
import { SimpleCache } from "../SimpleCache";
|
||||
import { Knub } from "knub/dist/Knub";
|
||||
import { TZeppelinKnub } from "../types";
|
||||
import { ERRORS, RecoverablePluginError } from "../RecoverablePluginError";
|
||||
|
||||
const SLOW_RESOLVE_THRESHOLD = 1500;
|
||||
|
||||
|
@ -74,8 +71,8 @@ export class ZeppelinPlugin<
|
|||
|
||||
protected readonly knub: TZeppelinKnub;
|
||||
|
||||
protected throwPluginRuntimeError(message: string) {
|
||||
throw new PluginRuntimeError(message, this.runtimePluginName, this.guildId);
|
||||
protected throwRecoverablePluginError(code: ERRORS) {
|
||||
throw new RecoverablePluginError(code, this.guild);
|
||||
}
|
||||
|
||||
protected canActOn(member1: Member, member2: Member, allowSameLevel = false) {
|
||||
|
@ -217,7 +214,7 @@ export class ZeppelinPlugin<
|
|||
}
|
||||
}
|
||||
} else {
|
||||
throw new PluginRuntimeError(`Invalid emoji: ${snowflake}`, this.runtimePluginName, this.guildId);
|
||||
this.throwRecoverablePluginError(ERRORS.INVALID_EMOJI);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -237,7 +234,9 @@ export class ZeppelinPlugin<
|
|||
* Resolves a user from the passed string. The passed string can be a user id, a user mention, a full username (with discrim), etc.
|
||||
* If the user is not found in the cache, it's fetched from the API.
|
||||
*/
|
||||
async resolveUser(userResolvable: string): Promise<User | UnknownUser> {
|
||||
async resolveUser(userResolvable: string): Promise<User | UnknownUser>;
|
||||
async resolveUser<T>(userResolvable: Not<T, string>): Promise<UnknownUser>;
|
||||
async resolveUser(userResolvable) {
|
||||
const start = performance.now();
|
||||
const user = await resolveUser(this.bot, userResolvable);
|
||||
const time = performance.now() - start;
|
||||
|
|
|
@ -118,6 +118,9 @@ function tDeepPartialProp(prop: any) {
|
|||
}
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/a/49262929/316944
|
||||
export type Not<T, E> = T & Exclude<T, E>;
|
||||
|
||||
/**
|
||||
* Mirrors EmbedOptions from Eris
|
||||
*/
|
||||
|
@ -743,63 +746,64 @@ export type CustomEmoji = {
|
|||
id: string;
|
||||
} & Emoji;
|
||||
|
||||
export interface INotifyUserConfig {
|
||||
useDM?: boolean;
|
||||
useChannel?: boolean;
|
||||
channelId?: string;
|
||||
}
|
||||
export type UserNotificationMethod = { type: "dm" } | { type: "channel"; channel: TextChannel };
|
||||
|
||||
export enum NotifyUserStatus {
|
||||
Ignored = 1,
|
||||
Failed,
|
||||
DirectMessaged,
|
||||
ChannelMessaged,
|
||||
}
|
||||
export const disableUserNotificationStrings = ["no", "none", "off"];
|
||||
|
||||
export interface INotifyUserResult {
|
||||
status: NotifyUserStatus;
|
||||
export interface UserNotificationResult {
|
||||
method: UserNotificationMethod | null;
|
||||
success: boolean;
|
||||
text?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to notify the user using one of the specified methods. Only the first one that succeeds will be used.
|
||||
* @param methods List of methods to try, in priority order
|
||||
*/
|
||||
export async function notifyUser(
|
||||
bot: Client,
|
||||
guild: Guild,
|
||||
user: User,
|
||||
body: string,
|
||||
config: INotifyUserConfig,
|
||||
): Promise<INotifyUserResult> {
|
||||
if (!config.useDM && !config.useChannel) {
|
||||
return { status: NotifyUserStatus.Ignored };
|
||||
methods: UserNotificationMethod[],
|
||||
): Promise<UserNotificationResult> {
|
||||
if (methods.length === 0) {
|
||||
return { method: null, success: true };
|
||||
}
|
||||
|
||||
if (config.useDM) {
|
||||
try {
|
||||
const dmChannel = await bot.getDMChannel(user.id);
|
||||
await dmChannel.createMessage(body);
|
||||
logger.info(`Notified ${user.id} via DM: ${body}`);
|
||||
return {
|
||||
status: NotifyUserStatus.DirectMessaged,
|
||||
text: "user notified with a direct message",
|
||||
};
|
||||
} catch (e) {} // tslint:disable-line
|
||||
}
|
||||
let lastError: Error = null;
|
||||
|
||||
if (config.useChannel && config.channelId) {
|
||||
try {
|
||||
const channel = guild.channels.get(config.channelId);
|
||||
if (channel instanceof TextChannel) {
|
||||
await channel.createMessage(`<@!${user.id}> ${body}`);
|
||||
for (const method of methods) {
|
||||
if (method.type === "dm") {
|
||||
try {
|
||||
const dmChannel = await user.getDMChannel();
|
||||
await dmChannel.createMessage(body);
|
||||
return {
|
||||
status: NotifyUserStatus.ChannelMessaged,
|
||||
text: `user notified in <#${channel.id}>`,
|
||||
method,
|
||||
success: true,
|
||||
text: "user notified with a direct message",
|
||||
};
|
||||
} catch (e) {
|
||||
lastError = e;
|
||||
}
|
||||
} catch (e) {} // tslint:disable-line
|
||||
} else if (method.type === "channel") {
|
||||
try {
|
||||
await method.channel.createMessage(`<@!${user.id}> ${body}`);
|
||||
return {
|
||||
method,
|
||||
success: true,
|
||||
text: `user notified in <#${method.channel.id}>`,
|
||||
};
|
||||
} catch (e) {
|
||||
lastError = e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const errorText = lastError ? `failed to message user: ${lastError.message}` : `failed to message user`;
|
||||
|
||||
return {
|
||||
status: NotifyUserStatus.Failed,
|
||||
text: "failed to message user",
|
||||
method: null,
|
||||
success: false,
|
||||
text: errorText,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -893,8 +897,10 @@ export function resolveUserId(bot: Client, value: string) {
|
|||
return null;
|
||||
}
|
||||
|
||||
export async function resolveUser(bot: Client, value: string): Promise<User | UnknownUser> {
|
||||
if (value == null || typeof value !== "string") {
|
||||
export async function resolveUser(bot: Client, value: string): Promise<User | UnknownUser>;
|
||||
export async function resolveUser<T>(bot: Client, value: Not<T, string>): Promise<UnknownUser>;
|
||||
export async function resolveUser<T>(bot, value) {
|
||||
if (typeof value !== "string") {
|
||||
return new UnknownUser();
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue