3
0
Fork 0
mirror of https://github.com/ZeppelinBot/Zeppelin.git synced 2025-03-16 14:11:50 +00:00

Fix merge conflict

This commit is contained in:
Dark 2020-02-16 17:30:17 +01:00
commit 318f80a26d
12 changed files with 440 additions and 216 deletions

View file

@ -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]();
}
}

View 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;
}
}

View file

@ -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 => {

View file

@ -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");

View file

@ -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({

View file

@ -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);

View file

@ -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";

View file

@ -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

View file

@ -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),

View file

@ -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

View file

@ -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;

View file

@ -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();
} }