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