Notify user for spam detection mutes. Add notification status ('user notified in DMs' etc.) to the case. Log case updates. Add 'unmuted immediately' to the case for unmutes without a time.
This commit is contained in:
parent
40cb74ee28
commit
fe88766f02
10 changed files with 455 additions and 332 deletions
|
@ -48,5 +48,7 @@
|
|||
|
||||
"MASSBAN": "⚒ {userMention(mod)} massbanned {count} users",
|
||||
|
||||
"MEMBER_JOIN_WITH_PRIOR_RECORDS": "⚠ {userMention(member)} joined with prior records. Recent cases:\n{recentCaseSummary}"
|
||||
"MEMBER_JOIN_WITH_PRIOR_RECORDS": "⚠ {userMention(member)} joined with prior records. Recent cases:\n{recentCaseSummary}",
|
||||
|
||||
"CASE_UPDATE": "✏ {userMention(mod)} updated case #{caseNumber} ({caseType}) with note:\n```{note}```"
|
||||
}
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
import { BaseRepository } from "./BaseRepository";
|
||||
import { Member, TextableChannel } from "eris";
|
||||
import { CaseTypes } from "./CaseTypes";
|
||||
import { ICaseDetails } from "./GuildCases";
|
||||
import { Case } from "./entities/Case";
|
||||
import { INotifyUserResult } from "../utils";
|
||||
|
||||
type KnownActions = "mute" | "unmute";
|
||||
|
||||
|
@ -9,30 +12,32 @@ type UnknownAction<T extends string> = T extends KnownActions ? never : T;
|
|||
|
||||
type ActionFn<T> = (args: T) => any | Promise<any>;
|
||||
|
||||
type MuteActionArgs = { member: Member; muteTime?: number; reason?: string };
|
||||
type UnmuteActionArgs = { member: Member; unmuteTime?: number; reason?: string };
|
||||
type CreateCaseActionArgs = {
|
||||
userId: string;
|
||||
modId: string;
|
||||
type: CaseTypes;
|
||||
auditLogId?: string;
|
||||
reason?: string;
|
||||
automatic?: boolean;
|
||||
postInCaseLog?: boolean;
|
||||
ppId?: string;
|
||||
};
|
||||
type MuteActionArgs = { member: Member; muteTime?: number; reason?: string; caseDetails?: ICaseDetails };
|
||||
type UnmuteActionArgs = { member: Member; unmuteTime?: number; reason?: string; caseDetails?: ICaseDetails };
|
||||
type CreateCaseActionArgs = ICaseDetails;
|
||||
type CreateCaseNoteActionArgs = {
|
||||
caseId: number;
|
||||
modId: string;
|
||||
note: string;
|
||||
automatic?: boolean;
|
||||
postInCaseLog?: boolean;
|
||||
noteDetails?: string[];
|
||||
};
|
||||
type PostCaseActionArgs = {
|
||||
caseId: number;
|
||||
channel: TextableChannel;
|
||||
};
|
||||
|
||||
export type MuteActionResult = {
|
||||
case: Case;
|
||||
notifyResult: INotifyUserResult;
|
||||
updatedExistingMute: boolean;
|
||||
};
|
||||
|
||||
export type UnmuteActionResult = {
|
||||
case: Case;
|
||||
};
|
||||
|
||||
export class GuildActions extends BaseRepository {
|
||||
private actions: Map<string, ActionFn<any>>;
|
||||
|
||||
|
@ -63,8 +68,8 @@ export class GuildActions extends BaseRepository {
|
|||
this.actions.delete(actionName);
|
||||
}
|
||||
|
||||
public fire(actionName: "mute", args: MuteActionArgs): Promise<any>;
|
||||
public fire(actionName: "unmute", args: UnmuteActionArgs): Promise<any>;
|
||||
public fire(actionName: "mute", args: MuteActionArgs): Promise<MuteActionResult>;
|
||||
public fire(actionName: "unmute", args: UnmuteActionArgs): Promise<UnmuteActionResult>;
|
||||
public fire(actionName: "createCase", args: CreateCaseActionArgs): Promise<any>;
|
||||
public fire(actionName: "createCaseNote", args: CreateCaseNoteActionArgs): Promise<any>;
|
||||
public fire(actionName: "postCase", args: PostCaseActionArgs): Promise<any>;
|
||||
|
|
|
@ -8,6 +8,22 @@ import moment = require("moment-timezone");
|
|||
|
||||
const CASE_SUMMARY_REASON_MAX_LENGTH = 300;
|
||||
|
||||
/**
|
||||
* Used as a config object for functions that create cases
|
||||
*/
|
||||
export interface ICaseDetails {
|
||||
userId?: string;
|
||||
modId?: string;
|
||||
ppId?: string;
|
||||
type?: CaseTypes;
|
||||
auditLogId?: string;
|
||||
reason?: string;
|
||||
automatic?: boolean;
|
||||
postInCaseLogOverride?: boolean;
|
||||
noteDetails?: string[];
|
||||
extraNotes?: string[];
|
||||
}
|
||||
|
||||
export class GuildCases extends BaseRepository {
|
||||
private cases: Repository<Case>;
|
||||
private caseNotes: Repository<CaseNote>;
|
||||
|
|
|
@ -24,11 +24,16 @@ export class GuildMutes extends BaseRepository {
|
|||
return this.mutes.findOne({
|
||||
where: {
|
||||
guild_id: this.guildId,
|
||||
user_id: userId
|
||||
}
|
||||
user_id: userId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async isMuted(userId: string): Promise<boolean> {
|
||||
const mute = await this.findExistingMuteForUserId(userId);
|
||||
return mute != null;
|
||||
}
|
||||
|
||||
async addMute(userId, expiryTime): Promise<Mute> {
|
||||
const expiresAt = expiryTime
|
||||
? moment()
|
||||
|
@ -39,7 +44,7 @@ export class GuildMutes extends BaseRepository {
|
|||
const result = await this.mutes.insert({
|
||||
guild_id: this.guildId,
|
||||
user_id: userId,
|
||||
expires_at: expiresAt
|
||||
expires_at: expiresAt,
|
||||
});
|
||||
|
||||
return this.mutes.findOne({ where: result.identifiers[0] });
|
||||
|
@ -55,25 +60,14 @@ export class GuildMutes extends BaseRepository {
|
|||
return this.mutes.update(
|
||||
{
|
||||
guild_id: this.guildId,
|
||||
user_id: userId
|
||||
user_id: userId,
|
||||
},
|
||||
{
|
||||
expires_at: expiresAt
|
||||
}
|
||||
expires_at: expiresAt,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async addOrUpdateMute(userId, expiryTime): Promise<Mute> {
|
||||
const existingMute = await this.findExistingMuteForUserId(userId);
|
||||
|
||||
if (existingMute) {
|
||||
await this.updateExpiryTime(userId, expiryTime);
|
||||
return this.findExistingMuteForUserId(userId);
|
||||
} else {
|
||||
return this.addMute(userId, expiryTime);
|
||||
}
|
||||
}
|
||||
|
||||
async getActiveMutes(): Promise<Mute[]> {
|
||||
return this.mutes
|
||||
.createQueryBuilder("mutes")
|
||||
|
@ -81,7 +75,7 @@ export class GuildMutes extends BaseRepository {
|
|||
.andWhere(
|
||||
new Brackets(qb => {
|
||||
qb.where("expires_at > NOW()").orWhere("expires_at IS NULL");
|
||||
})
|
||||
}),
|
||||
)
|
||||
.getMany();
|
||||
}
|
||||
|
@ -90,18 +84,18 @@ export class GuildMutes extends BaseRepository {
|
|||
await this.mutes.update(
|
||||
{
|
||||
guild_id: this.guildId,
|
||||
user_id: userId
|
||||
user_id: userId,
|
||||
},
|
||||
{
|
||||
case_id: caseId
|
||||
}
|
||||
case_id: caseId,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async clear(userId) {
|
||||
await this.mutes.delete({
|
||||
guild_id: this.guildId,
|
||||
user_id: userId
|
||||
user_id: userId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -49,4 +49,6 @@ export enum LogType {
|
|||
|
||||
MEMBER_ROLE_CHANGES,
|
||||
VOICE_CHANNEL_FORCE_MOVE,
|
||||
|
||||
CASE_UPDATE,
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { Message, MessageContent, MessageFile, TextableChannel, TextChannel } from "eris";
|
||||
import { GuildCases } from "../data/GuildCases";
|
||||
import { GuildCases, ICaseDetails } from "../data/GuildCases";
|
||||
import { CaseTypes } from "../data/CaseTypes";
|
||||
import { Case } from "../data/entities/Case";
|
||||
import moment from "moment-timezone";
|
||||
|
@ -36,20 +36,18 @@ export class CasesPlugin extends ZeppelinPlugin<ICasesPluginConfig> {
|
|||
this.archives = GuildArchives.getInstance(this.guildId);
|
||||
|
||||
this.actions.register("createCase", args => {
|
||||
return this.createCase(
|
||||
args.userId,
|
||||
args.modId,
|
||||
args.type,
|
||||
args.auditLogId,
|
||||
args.reason,
|
||||
args.automatic,
|
||||
args.postInCaseLog,
|
||||
args.ppId,
|
||||
);
|
||||
return this.createCase(args);
|
||||
});
|
||||
|
||||
this.actions.register("createCaseNote", args => {
|
||||
return this.createCaseNote(args.caseId, args.modId, args.note, args.automatic, args.postInCaseLog);
|
||||
return this.createCaseNote(
|
||||
args.caseId,
|
||||
args.modId,
|
||||
args.note,
|
||||
args.automatic,
|
||||
args.postInCaseLog,
|
||||
args.noteDetails,
|
||||
);
|
||||
});
|
||||
|
||||
this.actions.register("postCase", async args => {
|
||||
|
@ -72,46 +70,47 @@ export class CasesPlugin extends ZeppelinPlugin<ICasesPluginConfig> {
|
|||
* Creates a new case and, depending on config, posts it in the case log channel
|
||||
* @return {Number} The ID of the created case
|
||||
*/
|
||||
public async createCase(
|
||||
userId: string,
|
||||
modId: string,
|
||||
type: CaseTypes,
|
||||
auditLogId: string = null,
|
||||
reason: string = null,
|
||||
automatic = false,
|
||||
postInCaseLogOverride = null,
|
||||
ppId = null,
|
||||
): Promise<Case> {
|
||||
const user = this.bot.users.get(userId);
|
||||
public async createCase(opts: ICaseDetails): Promise<Case> {
|
||||
const user = this.bot.users.get(opts.userId);
|
||||
const userName = user ? `${user.username}#${user.discriminator}` : "Unknown#0000";
|
||||
|
||||
const mod = this.bot.users.get(modId);
|
||||
const mod = this.bot.users.get(opts.modId);
|
||||
const modName = mod ? `${mod.username}#${mod.discriminator}` : "Unknown#0000";
|
||||
|
||||
let ppName = null;
|
||||
if (ppId) {
|
||||
const pp = this.bot.users.get(ppId);
|
||||
if (opts.ppId) {
|
||||
const pp = this.bot.users.get(opts.ppId);
|
||||
ppName = pp ? `${pp.username}#${pp.discriminator}` : "Unknown#0000";
|
||||
}
|
||||
|
||||
const createdCase = await this.cases.create({
|
||||
type,
|
||||
user_id: userId,
|
||||
type: opts.type,
|
||||
user_id: opts.userId,
|
||||
user_name: userName,
|
||||
mod_id: modId,
|
||||
mod_id: opts.modId,
|
||||
mod_name: modName,
|
||||
audit_log_id: auditLogId,
|
||||
pp_id: ppId,
|
||||
audit_log_id: opts.auditLogId,
|
||||
pp_id: opts.ppId,
|
||||
pp_name: ppName,
|
||||
});
|
||||
|
||||
if (reason) {
|
||||
await this.createCaseNote(createdCase, modId, reason, automatic, false);
|
||||
if (opts.reason || opts.noteDetails.length) {
|
||||
await this.createCaseNote(createdCase, opts.modId, opts.reason || "", opts.automatic, false, opts.noteDetails);
|
||||
}
|
||||
|
||||
if (opts.extraNotes) {
|
||||
for (const extraNote of opts.extraNotes) {
|
||||
await this.createCaseNote(createdCase, opts.modId, extraNote, opts.automatic, false);
|
||||
}
|
||||
}
|
||||
|
||||
const config = this.getConfig();
|
||||
|
||||
if (config.case_log_channel && (!automatic || config.log_automatic_actions) && postInCaseLogOverride !== false) {
|
||||
if (
|
||||
config.case_log_channel &&
|
||||
(!opts.automatic || config.log_automatic_actions) &&
|
||||
opts.postInCaseLogOverride !== false
|
||||
) {
|
||||
try {
|
||||
await this.postCaseToCaseLogChannel(createdCase);
|
||||
} catch (e) {} // tslint:disable-line
|
||||
|
@ -129,6 +128,7 @@ export class CasesPlugin extends ZeppelinPlugin<ICasesPluginConfig> {
|
|||
body: string,
|
||||
automatic = false,
|
||||
postInCaseLogOverride = null,
|
||||
noteDetails: string[] = null,
|
||||
): Promise<void> {
|
||||
const mod = this.bot.users.get(modId);
|
||||
const modName = mod ? `${mod.username}#${mod.discriminator}` : "Unknown#0000";
|
||||
|
@ -138,6 +138,11 @@ export class CasesPlugin extends ZeppelinPlugin<ICasesPluginConfig> {
|
|||
this.throwPluginRuntimeError(`Unknown case ID: ${caseOrCaseId}`);
|
||||
}
|
||||
|
||||
// Add note details to the beginning of the note
|
||||
if (noteDetails && noteDetails.length) {
|
||||
body = noteDetails.map(d => `__[${d}]__`).join(" ") + " " + body;
|
||||
}
|
||||
|
||||
await this.cases.createNote(theCase.id, {
|
||||
mod_id: modId,
|
||||
mod_name: modName,
|
||||
|
|
|
@ -3,11 +3,13 @@ import { Attachment, Constants as ErisConstants, Guild, Member, Message, TextCha
|
|||
import humanizeDuration from "humanize-duration";
|
||||
import { GuildCases } from "../data/GuildCases";
|
||||
import {
|
||||
convertDelayStringToMS,
|
||||
asSingleLine,
|
||||
createChunkedMessage,
|
||||
errorMessage,
|
||||
findRelevantAuditLogEntry,
|
||||
asSingleLine,
|
||||
INotifyUserResult,
|
||||
notifyUser,
|
||||
NotifyUserStatus,
|
||||
stripObjectToScalars,
|
||||
successMessage,
|
||||
trimLines,
|
||||
|
@ -17,9 +19,8 @@ import { CaseTypes } from "../data/CaseTypes";
|
|||
import { GuildLogs } from "../data/GuildLogs";
|
||||
import { LogType } from "../data/LogType";
|
||||
import { ZeppelinPlugin } from "./ZeppelinPlugin";
|
||||
import { GuildActions } from "../data/GuildActions";
|
||||
import { GuildActions, MuteActionResult } from "../data/GuildActions";
|
||||
import { Case } from "../data/entities/Case";
|
||||
import { Mute } from "../data/entities/Mute";
|
||||
import { renderTemplate } from "../templateFormatter";
|
||||
|
||||
enum IgnoredEventType {
|
||||
|
@ -33,31 +34,15 @@ interface IIgnoredEvent {
|
|||
userId: string;
|
||||
}
|
||||
|
||||
enum MessageResultStatus {
|
||||
Ignored = 1,
|
||||
Failed,
|
||||
DirectMessaged,
|
||||
ChannelMessaged,
|
||||
}
|
||||
|
||||
interface IMessageResult {
|
||||
status: MessageResultStatus;
|
||||
text?: string;
|
||||
}
|
||||
|
||||
interface IModActionsPluginConfig {
|
||||
dm_on_warn: boolean;
|
||||
dm_on_mute: boolean;
|
||||
dm_on_kick: boolean;
|
||||
dm_on_ban: boolean;
|
||||
message_on_warn: boolean;
|
||||
message_on_mute: boolean;
|
||||
message_on_kick: boolean;
|
||||
message_on_ban: boolean;
|
||||
message_channel: string;
|
||||
warn_message: string;
|
||||
mute_message: string;
|
||||
timed_mute_message: string;
|
||||
kick_message: string;
|
||||
ban_message: string;
|
||||
alert_on_rejoin: boolean;
|
||||
|
@ -98,17 +83,13 @@ export class ModActionsPlugin extends ZeppelinPlugin<IModActionsPluginConfig> {
|
|||
return {
|
||||
config: {
|
||||
dm_on_warn: true,
|
||||
dm_on_mute: false,
|
||||
dm_on_kick: false,
|
||||
dm_on_ban: false,
|
||||
message_on_warn: false,
|
||||
message_on_mute: false,
|
||||
message_on_kick: false,
|
||||
message_on_ban: false,
|
||||
message_channel: null,
|
||||
warn_message: "You have received a warning on {guildName}: {reason}",
|
||||
mute_message: "You have been muted on {guildName}. Reason given: {reason}",
|
||||
timed_mute_message: "You have been muted on {guildName} for {time}. Reason given: {reason}",
|
||||
kick_message: "You have been kicked from {guildName}. Reason given: {reason}",
|
||||
ban_message: "You have been banned from {guildName}. Reason given: {reason}",
|
||||
alert_on_rejoin: false,
|
||||
|
@ -307,7 +288,7 @@ export class ModActionsPlugin extends ZeppelinPlugin<IModActionsPluginConfig> {
|
|||
overloads: ["<note:string$>"],
|
||||
})
|
||||
@d.permission("can_note")
|
||||
async updateSpecificCmd(msg: Message, args: { caseNumber?: number; note: string }) {
|
||||
async updateCmd(msg: Message, args: { caseNumber?: number; note: string }) {
|
||||
let theCase: Case;
|
||||
if (args.caseNumber != null) {
|
||||
theCase = await this.cases.findByCaseNumber(args.caseNumber);
|
||||
|
@ -326,6 +307,13 @@ export class ModActionsPlugin extends ZeppelinPlugin<IModActionsPluginConfig> {
|
|||
note: args.note,
|
||||
});
|
||||
|
||||
this.serverLogs.log(LogType.CASE_UPDATE, {
|
||||
mod: msg.author,
|
||||
caseNumber: theCase.case_number,
|
||||
caseType: CaseTypes[theCase.type],
|
||||
note: args.note,
|
||||
});
|
||||
|
||||
msg.channel.createMessage(successMessage(`Case \`#${theCase.case_number}\` updated`));
|
||||
}
|
||||
|
||||
|
@ -373,14 +361,12 @@ export class ModActionsPlugin extends ZeppelinPlugin<IModActionsPluginConfig> {
|
|||
|
||||
const warnMessage = config.warn_message.replace("{guildName}", this.guild.name).replace("{reason}", reason);
|
||||
|
||||
const userMessageResult = await this.tryToMessageUser(
|
||||
args.member.user,
|
||||
warnMessage,
|
||||
config.dm_on_warn,
|
||||
config.message_on_warn,
|
||||
);
|
||||
const userMessageResult = await notifyUser(this.bot, this.guild, args.member.user, warnMessage, {
|
||||
useDM: config.dm_on_warn,
|
||||
useChannel: config.message_on_warn,
|
||||
});
|
||||
|
||||
if (userMessageResult.status === MessageResultStatus.Failed) {
|
||||
if (userMessageResult.status === NotifyUserStatus.Failed) {
|
||||
const failedMsg = await msg.channel.createMessage("Failed to message the user. Log the warning anyway?");
|
||||
const reply = await waitForReaction(this.bot, failedMsg, ["✅", "❌"], msg.author.id);
|
||||
failedMsg.delete();
|
||||
|
@ -427,6 +413,8 @@ export class ModActionsPlugin extends ZeppelinPlugin<IModActionsPluginConfig> {
|
|||
|
||||
// The moderator who did the action is the message author or, if used, the specified --mod
|
||||
let mod = msg.member;
|
||||
let pp = null;
|
||||
|
||||
if (args.mod) {
|
||||
if (!this.hasPermission("can_act_as_other", { message: msg })) {
|
||||
msg.channel.createMessage(errorMessage("No permission for --mod"));
|
||||
|
@ -434,116 +422,60 @@ export class ModActionsPlugin extends ZeppelinPlugin<IModActionsPluginConfig> {
|
|||
}
|
||||
|
||||
mod = args.mod;
|
||||
pp = msg.author;
|
||||
}
|
||||
|
||||
let userMessageResult: IMessageResult = { status: MessageResultStatus.Ignored };
|
||||
|
||||
const timeUntilUnmute = args.time && humanizeDuration(args.time);
|
||||
const reason = this.formatReasonWithAttachments(args.reason, msg.attachments);
|
||||
|
||||
// Apply "muted" role
|
||||
this.serverLogs.ignoreLog(LogType.MEMBER_ROLE_ADD, args.member.id);
|
||||
const mute: Mute = await this.actions.fire("mute", {
|
||||
member: args.member,
|
||||
muteTime: args.time,
|
||||
});
|
||||
let muteResult: MuteActionResult;
|
||||
|
||||
if (!mute) {
|
||||
try {
|
||||
muteResult = await this.actions.fire("mute", {
|
||||
member: args.member,
|
||||
muteTime: args.time,
|
||||
reason,
|
||||
caseDetails: {
|
||||
modId: mod.id,
|
||||
ppId: pp && pp.id,
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
logger.error(`Failed to mute user ${args.member.id}: ${e.message}`);
|
||||
msg.channel.createMessage(errorMessage("Could not mute the user"));
|
||||
return;
|
||||
}
|
||||
|
||||
const hasOldCase = mute.case_id != null;
|
||||
|
||||
let theCase;
|
||||
|
||||
if (hasOldCase) {
|
||||
// Update old case
|
||||
theCase = await this.cases.find(mute.case_id);
|
||||
const caseNote = `__[Mute updated to ${args.time ? timeUntilUnmute : "indefinite"}]__ ${reason}`.trim();
|
||||
await this.actions.fire("createCaseNote", {
|
||||
caseId: mute.case_id,
|
||||
modId: mod.id,
|
||||
note: caseNote,
|
||||
});
|
||||
} else {
|
||||
// Create new case
|
||||
const caseNote = `__[Muted ${args.time ? `for ${timeUntilUnmute}` : "indefinitely"}]__ ${reason}`.trim();
|
||||
theCase = await this.actions.fire("createCase", {
|
||||
userId: args.member.id,
|
||||
modId: mod.id,
|
||||
type: CaseTypes.Mute,
|
||||
reason: caseNote,
|
||||
ppId: mod.id !== msg.author.id ? msg.author.id : null,
|
||||
});
|
||||
await this.mutes.setCaseId(args.member.id, theCase.id);
|
||||
}
|
||||
|
||||
const config = this.getConfig();
|
||||
|
||||
// Message the user informing them of the mute
|
||||
// Don't message them if we're updating an old mute
|
||||
if (reason && !hasOldCase) {
|
||||
const template = args.time ? config.timed_mute_message : config.mute_message;
|
||||
|
||||
const muteMessage = await renderTemplate(template, {
|
||||
guildName: this.guild.name,
|
||||
reason,
|
||||
time: timeUntilUnmute,
|
||||
});
|
||||
|
||||
userMessageResult = await this.tryToMessageUser(
|
||||
args.member.user,
|
||||
muteMessage,
|
||||
config.dm_on_mute,
|
||||
config.message_on_mute,
|
||||
);
|
||||
}
|
||||
|
||||
// Confirm the action to the moderator
|
||||
let response;
|
||||
if (args.time) {
|
||||
if (hasOldCase) {
|
||||
if (muteResult.updatedExistingMute) {
|
||||
response = asSingleLine(`
|
||||
Updated **${args.member.user.username}#${args.member.user.discriminator}**'s
|
||||
mute to ${timeUntilUnmute} (Case #${theCase.case_number})
|
||||
mute to ${timeUntilUnmute} (Case #${muteResult.case.case_number})
|
||||
`);
|
||||
} else {
|
||||
response = asSingleLine(`
|
||||
Muted **${args.member.user.username}#${args.member.user.discriminator}**
|
||||
for ${timeUntilUnmute} (Case #${theCase.case_number})
|
||||
for ${timeUntilUnmute} (Case #${muteResult.case.case_number})
|
||||
`);
|
||||
}
|
||||
} else {
|
||||
if (hasOldCase) {
|
||||
if (muteResult.updatedExistingMute) {
|
||||
response = asSingleLine(`
|
||||
Updated **${args.member.user.username}#${args.member.user.discriminator}**'s
|
||||
mute to indefinite (Case #${theCase.case_number})
|
||||
mute to indefinite (Case #${muteResult.case.case_number})
|
||||
`);
|
||||
} else {
|
||||
response = asSingleLine(`
|
||||
Muted **${args.member.user.username}#${args.member.user.discriminator}**
|
||||
indefinitely (Case #${theCase.case_number})
|
||||
indefinitely (Case #${muteResult.case.case_number})
|
||||
`);
|
||||
}
|
||||
}
|
||||
|
||||
if (userMessageResult.text) response += ` (${userMessageResult.text})`;
|
||||
if (muteResult.notifyResult.text) response += ` (${muteResult.notifyResult.text})`;
|
||||
msg.channel.createMessage(successMessage(response));
|
||||
|
||||
// Log the action
|
||||
if (args.time) {
|
||||
this.serverLogs.log(LogType.MEMBER_TIMED_MUTE, {
|
||||
mod: stripObjectToScalars(mod.user),
|
||||
member: stripObjectToScalars(args.member, ["user"]),
|
||||
time: timeUntilUnmute,
|
||||
});
|
||||
} else {
|
||||
this.serverLogs.log(LogType.MEMBER_MUTE, {
|
||||
mod: stripObjectToScalars(mod.user),
|
||||
member: stripObjectToScalars(args.member, ["user"]),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@d.command("unmute", "<member:Member> <time:delay> <reason:string$>", {
|
||||
|
@ -559,76 +491,57 @@ export class ModActionsPlugin extends ZeppelinPlugin<IModActionsPluginConfig> {
|
|||
}
|
||||
|
||||
// The moderator who did the action is the message author or, if used, the specified --mod
|
||||
let mod = msg.member;
|
||||
let mod = msg.author;
|
||||
let pp = null;
|
||||
|
||||
if (args.mod) {
|
||||
if (!this.hasPermission("can_act_as_other", { message: msg })) {
|
||||
msg.channel.createMessage(errorMessage("No permission for --mod"));
|
||||
return;
|
||||
}
|
||||
|
||||
mod = args.mod;
|
||||
mod = args.mod.user;
|
||||
pp = msg.author;
|
||||
}
|
||||
|
||||
// Check if they're muted in the first place
|
||||
const mute = await this.mutes.findExistingMuteForUserId(args.member.id);
|
||||
if (!mute) {
|
||||
if (!(await this.mutes.isMuted(args.member.id))) {
|
||||
msg.channel.createMessage(errorMessage("Cannot unmute: member is not muted"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Convert unmute time from e.g. "2h30m" to milliseconds
|
||||
const timeUntilUnmute = args.time && humanizeDuration(args.time);
|
||||
|
||||
const reason = this.formatReasonWithAttachments(args.reason, msg.attachments);
|
||||
const caseNote = args.time ? `__[Scheduled unmute in ${timeUntilUnmute}]__ ${reason}` : reason;
|
||||
|
||||
// Create a case
|
||||
const createdCase = await this.actions.fire("createCase", {
|
||||
userId: args.member.id,
|
||||
modId: mod.id,
|
||||
type: CaseTypes.Unmute,
|
||||
reason: caseNote,
|
||||
ppId: mod.id !== msg.author.id ? msg.author.id : null,
|
||||
const result = await this.actions.fire("unmute", {
|
||||
member: args.member,
|
||||
unmuteTime: args.time,
|
||||
caseDetails: {
|
||||
modId: mod.id,
|
||||
ppId: pp && pp.id,
|
||||
reason,
|
||||
},
|
||||
});
|
||||
|
||||
// Confirm the action to the moderator
|
||||
if (args.time) {
|
||||
// If we have an unmute time, just update the old mute to expire in that time
|
||||
await this.actions.fire("unmute", { member: args.member, unmuteTime: args.time });
|
||||
|
||||
// Confirm the action to the moderator
|
||||
const timeUntilUnmute = args.time && humanizeDuration(args.time);
|
||||
msg.channel.createMessage(
|
||||
successMessage(
|
||||
`Unmuting **${args.member.user.username}#${args.member.user.discriminator}** in ${timeUntilUnmute} (Case #${
|
||||
createdCase.case_number
|
||||
})`,
|
||||
asSingleLine(`
|
||||
Unmuting **${args.member.user.username}#${args.member.user.discriminator}**
|
||||
in ${timeUntilUnmute} (Case #${result.case.case_number})
|
||||
`),
|
||||
),
|
||||
);
|
||||
|
||||
// Log the action
|
||||
this.serverLogs.log(LogType.MEMBER_TIMED_UNMUTE, {
|
||||
mod: stripObjectToScalars(mod.user),
|
||||
member: stripObjectToScalars(args.member, ["user"]),
|
||||
time: timeUntilUnmute,
|
||||
});
|
||||
} else {
|
||||
// Otherwise remove "muted" role immediately
|
||||
this.serverLogs.ignoreLog(LogType.MEMBER_ROLE_REMOVE, args.member.id);
|
||||
await this.actions.fire("unmute", { member: args.member });
|
||||
|
||||
// Confirm the action to the moderator
|
||||
msg.channel.createMessage(
|
||||
successMessage(
|
||||
`Unmuted **${args.member.user.username}#${args.member.user.discriminator}** (Case #${
|
||||
createdCase.case_number
|
||||
})`,
|
||||
asSingleLine(`
|
||||
Unmuted **${args.member.user.username}#${args.member.user.discriminator}**
|
||||
(Case #${result.case.case_number})
|
||||
`),
|
||||
),
|
||||
);
|
||||
|
||||
// Log the action
|
||||
this.serverLogs.log(LogType.MEMBER_UNMUTE, {
|
||||
mod: stripObjectToScalars(msg.member.user),
|
||||
member: stripObjectToScalars(args.member, ["user"]),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -658,19 +571,18 @@ export class ModActionsPlugin extends ZeppelinPlugin<IModActionsPluginConfig> {
|
|||
const reason = this.formatReasonWithAttachments(args.reason, msg.attachments);
|
||||
|
||||
// Attempt to message the user *before* kicking them, as doing it after may not be possible
|
||||
let userMessageResult: IMessageResult = { status: MessageResultStatus.Ignored };
|
||||
let userMessageResult: INotifyUserResult = { status: NotifyUserStatus.Ignored };
|
||||
if (args.reason) {
|
||||
const kickMessage = await renderTemplate(config.kick_message, {
|
||||
guildName: this.guild.name,
|
||||
reason,
|
||||
});
|
||||
|
||||
userMessageResult = await this.tryToMessageUser(
|
||||
args.member.user,
|
||||
kickMessage,
|
||||
config.dm_on_kick,
|
||||
config.message_on_kick,
|
||||
);
|
||||
userMessageResult = await notifyUser(this.bot, this.guild, args.member.user, kickMessage, {
|
||||
useDM: config.dm_on_kick,
|
||||
useChannel: config.message_on_kick,
|
||||
channelId: config.message_channel,
|
||||
});
|
||||
}
|
||||
|
||||
// Kick the user
|
||||
|
@ -728,19 +640,18 @@ export class ModActionsPlugin extends ZeppelinPlugin<IModActionsPluginConfig> {
|
|||
const reason = this.formatReasonWithAttachments(args.reason, msg.attachments);
|
||||
|
||||
// Attempt to message the user *before* banning them, as doing it after may not be possible
|
||||
let userMessageResult: IMessageResult = { status: MessageResultStatus.Ignored };
|
||||
let userMessageResult: INotifyUserResult = { status: NotifyUserStatus.Ignored };
|
||||
if (reason) {
|
||||
const banMessage = await renderTemplate(config.ban_message, {
|
||||
guildName: this.guild.name,
|
||||
reason,
|
||||
});
|
||||
|
||||
userMessageResult = await this.tryToMessageUser(
|
||||
args.member.user,
|
||||
banMessage,
|
||||
config.dm_on_ban,
|
||||
config.message_on_ban,
|
||||
);
|
||||
userMessageResult = await notifyUser(this.bot, this.guild, args.member.user, banMessage, {
|
||||
useDM: config.dm_on_ban,
|
||||
useChannel: config.message_on_ban,
|
||||
channelId: config.message_channel,
|
||||
});
|
||||
}
|
||||
|
||||
// Ban the user
|
||||
|
@ -1233,49 +1144,4 @@ export class ModActionsPlugin extends ZeppelinPlugin<IModActionsPluginConfig> {
|
|||
await this.cases.setHidden(theCase.id, false);
|
||||
msg.channel.createMessage(successMessage(`Case #${theCase.case_number} is no longer hidden!`));
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to message the specified user through DMs and/or the message channel.
|
||||
* Returns a promise that resolves to a status constant indicating the result.
|
||||
*/
|
||||
protected async tryToMessageUser(
|
||||
user: User,
|
||||
str: string,
|
||||
useDM: boolean,
|
||||
useChannel: boolean,
|
||||
): Promise<IMessageResult> {
|
||||
if (!useDM && !useChannel) {
|
||||
return { status: MessageResultStatus.Ignored };
|
||||
}
|
||||
|
||||
if (useDM) {
|
||||
try {
|
||||
const dmChannel = await this.bot.getDMChannel(user.id);
|
||||
await dmChannel.createMessage(str);
|
||||
logger.info(`Sent DM to ${user.id}: ${str}`);
|
||||
return {
|
||||
status: MessageResultStatus.DirectMessaged,
|
||||
text: "user notified with a direct message",
|
||||
};
|
||||
} catch (e) {} // tslint:disable-line
|
||||
}
|
||||
|
||||
const messageChannel = this.getConfig().message_channel;
|
||||
|
||||
if (useChannel && messageChannel) {
|
||||
try {
|
||||
const channel = this.guild.channels.get(messageChannel) as TextChannel;
|
||||
await channel.createMessage(`<@!${user.id}> ${str}`);
|
||||
return {
|
||||
status: MessageResultStatus.ChannelMessaged,
|
||||
text: `user notified in <#${channel.id}>`,
|
||||
};
|
||||
} catch (e) {} // tslint:disable-line
|
||||
}
|
||||
|
||||
return {
|
||||
status: MessageResultStatus.Failed,
|
||||
text: "failed to message user",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,15 +1,27 @@
|
|||
import { Member, Message, User } from "eris";
|
||||
import { GuildCases } from "../data/GuildCases";
|
||||
import { GuildCases, ICaseDetails } from "../data/GuildCases";
|
||||
import moment from "moment-timezone";
|
||||
import { ZeppelinPlugin } from "./ZeppelinPlugin";
|
||||
import { GuildActions } from "../data/GuildActions";
|
||||
import { GuildMutes } from "../data/GuildMutes";
|
||||
import { DBDateFormat, chunkMessageLines, stripObjectToScalars, successMessage, errorMessage, sleep } from "../utils";
|
||||
import {
|
||||
chunkMessageLines,
|
||||
DBDateFormat,
|
||||
errorMessage,
|
||||
INotifyUserResult,
|
||||
notifyUser,
|
||||
NotifyUserStatus,
|
||||
stripObjectToScalars,
|
||||
successMessage,
|
||||
ucfirst,
|
||||
} from "../utils";
|
||||
import humanizeDuration from "humanize-duration";
|
||||
import { LogType } from "../data/LogType";
|
||||
import { GuildLogs } from "../data/GuildLogs";
|
||||
import { decorators as d, IPluginOptions, logger } from "knub";
|
||||
import { Mute } from "../data/entities/Mute";
|
||||
import { renderTemplate } from "../templateFormatter";
|
||||
import { CaseTypes } from "../data/CaseTypes";
|
||||
|
||||
interface IMuteWithDetails extends Mute {
|
||||
member?: Member;
|
||||
|
@ -20,6 +32,12 @@ interface IMutesPluginConfig {
|
|||
mute_role: string;
|
||||
move_to_voice_channel: string;
|
||||
|
||||
dm_on_mute: boolean;
|
||||
message_on_mute: boolean;
|
||||
message_channel: string;
|
||||
mute_message: string;
|
||||
timed_mute_message: string;
|
||||
|
||||
can_view_list: boolean;
|
||||
can_cleanup: boolean;
|
||||
}
|
||||
|
@ -39,6 +57,12 @@ export class MutesPlugin extends ZeppelinPlugin<IMutesPluginConfig> {
|
|||
mute_role: null,
|
||||
move_to_voice_channel: null,
|
||||
|
||||
dm_on_mute: false,
|
||||
message_on_mute: false,
|
||||
message_channel: null,
|
||||
mute_message: "You have been muted on {guildName}. Reason given: {reason}",
|
||||
timed_mute_message: "You have been muted on {guildName} for {time}. Reason given: {reason}",
|
||||
|
||||
can_view_list: false,
|
||||
can_cleanup: false,
|
||||
},
|
||||
|
@ -66,10 +90,10 @@ export class MutesPlugin extends ZeppelinPlugin<IMutesPluginConfig> {
|
|||
this.serverLogs = new GuildLogs(this.guildId);
|
||||
|
||||
this.actions.register("mute", args => {
|
||||
return this.muteMember(args.member, args.muteTime);
|
||||
return this.muteMember(args.member, args.muteTime, args.reason, args.caseDetails);
|
||||
});
|
||||
this.actions.register("unmute", args => {
|
||||
return this.unmuteMember(args.member, args.unmuteTime);
|
||||
return this.unmuteMember(args.member, args.unmuteTime, args.caseDetails);
|
||||
});
|
||||
|
||||
// Check for expired mutes every 5s
|
||||
|
@ -84,33 +108,128 @@ export class MutesPlugin extends ZeppelinPlugin<IMutesPluginConfig> {
|
|||
clearInterval(this.muteClearIntervalId);
|
||||
}
|
||||
|
||||
public async muteMember(member: Member, muteTime: number = null) {
|
||||
public async muteMember(
|
||||
member: Member,
|
||||
muteTime: number = null,
|
||||
reason: string = null,
|
||||
caseDetails: ICaseDetails = {},
|
||||
) {
|
||||
const muteRole = this.getConfig().mute_role;
|
||||
if (!muteRole) return;
|
||||
|
||||
// Add muted role
|
||||
await member.addRole(muteRole);
|
||||
const timeUntilUnmute = muteTime && humanizeDuration(muteTime);
|
||||
|
||||
// No mod specified -> mark Zeppelin as the mod
|
||||
if (!caseDetails.modId) {
|
||||
caseDetails.modId = this.bot.user.id;
|
||||
}
|
||||
|
||||
// Apply mute role if it's missing
|
||||
if (!member.roles.includes(muteRole)) {
|
||||
await member.addRole(muteRole);
|
||||
}
|
||||
|
||||
// If enabled, move the user to the mute voice channel (e.g. afk - just to apply the voice perms from the mute role)
|
||||
const moveToVoiceChannelId = this.getConfig().move_to_voice_channel;
|
||||
if (moveToVoiceChannelId && member.voiceState.channelID) {
|
||||
try {
|
||||
await member.edit({
|
||||
channelID: moveToVoiceChannelId,
|
||||
});
|
||||
await member.edit({ channelID: moveToVoiceChannelId });
|
||||
} catch (e) {
|
||||
logger.warn(`Could not move user ${member.id} to voice channel ${moveToVoiceChannelId} when muting`);
|
||||
}
|
||||
}
|
||||
|
||||
// Create & return mute record
|
||||
return this.mutes.addOrUpdateMute(member.id, muteTime);
|
||||
// If the user is already muted, update the duration of their existing mute
|
||||
const existingMute = await this.mutes.findExistingMuteForUserId(member.id);
|
||||
let notifyResult: INotifyUserResult = { status: NotifyUserStatus.Ignored };
|
||||
|
||||
if (existingMute) {
|
||||
await this.mutes.updateExpiryTime(member.id, muteTime);
|
||||
} else {
|
||||
await this.mutes.addMute(member.id, muteTime);
|
||||
|
||||
// If it's a new mute, attempt to message the user
|
||||
const config = this.getMatchingConfig({ member });
|
||||
const template = muteTime ? config.timed_mute_message : config.mute_message;
|
||||
|
||||
const muteMessage =
|
||||
template &&
|
||||
(await renderTemplate(template, {
|
||||
guildName: this.guild.name,
|
||||
reason,
|
||||
time: timeUntilUnmute,
|
||||
}));
|
||||
|
||||
if (muteMessage) {
|
||||
notifyResult = await notifyUser(this.bot, this.guild, member.user, muteMessage, {
|
||||
useDM: config.dm_on_mute,
|
||||
useChannel: config.message_on_mute,
|
||||
channelId: config.message_channel,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Create/update a case
|
||||
let theCase;
|
||||
if (existingMute && existingMute.case_id) {
|
||||
// Update old case
|
||||
theCase = await this.cases.find(existingMute.case_id);
|
||||
const noteDetails = [`Mute updated to ${muteTime ? timeUntilUnmute : "indefinite"}`];
|
||||
await this.actions.fire("createCaseNote", {
|
||||
caseId: existingMute.case_id,
|
||||
modId: caseDetails.modId,
|
||||
note: reason,
|
||||
noteDetails,
|
||||
});
|
||||
} else {
|
||||
// Create new case
|
||||
const noteDetails = [`Muted ${muteTime ? `for ${timeUntilUnmute}` : "indefinitely"}`];
|
||||
if (notifyResult.status !== NotifyUserStatus.Ignored) {
|
||||
noteDetails.push(ucfirst(notifyResult.text));
|
||||
}
|
||||
|
||||
theCase = await this.actions.fire("createCase", {
|
||||
userId: member.id,
|
||||
modId: caseDetails.modId,
|
||||
type: CaseTypes.Mute,
|
||||
reason,
|
||||
ppId: caseDetails.ppId,
|
||||
noteDetails,
|
||||
extraNotes: caseDetails.extraNotes,
|
||||
});
|
||||
await this.mutes.setCaseId(member.id, theCase.id);
|
||||
}
|
||||
|
||||
// Log the action
|
||||
if (muteTime) {
|
||||
this.serverLogs.log(LogType.MEMBER_TIMED_MUTE, {
|
||||
mod: stripObjectToScalars(caseDetails.modId),
|
||||
member: stripObjectToScalars(member, ["user"]),
|
||||
time: timeUntilUnmute,
|
||||
});
|
||||
} else {
|
||||
this.serverLogs.log(LogType.MEMBER_MUTE, {
|
||||
mod: stripObjectToScalars(caseDetails.modId),
|
||||
member: stripObjectToScalars(member, ["user"]),
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
case: theCase,
|
||||
notifyResult,
|
||||
updatedExistingMute: !!existingMute,
|
||||
};
|
||||
}
|
||||
|
||||
public async unmuteMember(member: Member, unmuteTime: number = null) {
|
||||
public async unmuteMember(member: Member, unmuteTime: number = null, caseDetails: ICaseDetails = {}) {
|
||||
const existingMute = await this.mutes.findExistingMuteForUserId(member.id);
|
||||
if (!existingMute) return;
|
||||
|
||||
if (unmuteTime) {
|
||||
await this.mutes.addOrUpdateMute(member.id, unmuteTime);
|
||||
// Schedule timed unmute (= just set the mute's duration)
|
||||
await this.mutes.updateExpiryTime(member.id, unmuteTime);
|
||||
} else {
|
||||
// Unmute immediately
|
||||
const muteRole = this.getConfig().mute_role;
|
||||
if (member.roles.includes(muteRole)) {
|
||||
await member.removeRole(muteRole);
|
||||
|
@ -118,6 +237,44 @@ export class MutesPlugin extends ZeppelinPlugin<IMutesPluginConfig> {
|
|||
|
||||
await this.mutes.clear(member.id);
|
||||
}
|
||||
|
||||
const timeUntilUnmute = unmuteTime && humanizeDuration(unmuteTime);
|
||||
|
||||
// Create a case
|
||||
const noteDetails = [];
|
||||
if (unmuteTime) {
|
||||
noteDetails.push(`Scheduled unmute in ${timeUntilUnmute}`);
|
||||
} else {
|
||||
noteDetails.push(`Unmuted immediately`);
|
||||
}
|
||||
|
||||
const createdCase = await this.actions.fire("createCase", {
|
||||
userId: member.id,
|
||||
modId: caseDetails.modId,
|
||||
type: CaseTypes.Unmute,
|
||||
reason: caseDetails.reason,
|
||||
ppId: caseDetails.ppId,
|
||||
noteDetails,
|
||||
});
|
||||
|
||||
// Log the action
|
||||
const mod = this.bot.users.get(caseDetails.modId);
|
||||
if (unmuteTime) {
|
||||
this.serverLogs.log(LogType.MEMBER_TIMED_UNMUTE, {
|
||||
mod: stripObjectToScalars(mod),
|
||||
member: stripObjectToScalars(member, ["user"]),
|
||||
time: timeUntilUnmute,
|
||||
});
|
||||
} else {
|
||||
this.serverLogs.log(LogType.MEMBER_UNMUTE, {
|
||||
mod: stripObjectToScalars(mod),
|
||||
member: stripObjectToScalars(member, ["user"]),
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
case: createdCase,
|
||||
};
|
||||
}
|
||||
|
||||
@d.command("mutes", [], {
|
||||
|
@ -361,7 +518,6 @@ export class MutesPlugin extends ZeppelinPlugin<IMutesPluginConfig> {
|
|||
if (!member) continue;
|
||||
|
||||
try {
|
||||
this.serverLogs.ignoreLog(LogType.MEMBER_ROLE_REMOVE, member.id);
|
||||
await member.removeRole(this.getConfig().mute_role);
|
||||
} catch (e) {} // tslint:disable-line
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { decorators as d, IPluginOptions } from "knub";
|
||||
import { decorators as d, IPluginOptions, logger } from "knub";
|
||||
import { Channel, Member } from "eris";
|
||||
import humanizeDuration from "humanize-duration";
|
||||
import {
|
||||
convertDelayStringToMS,
|
||||
getEmojiInString,
|
||||
getRoleMentions,
|
||||
getUrlsInString,
|
||||
|
@ -16,7 +16,7 @@ import { GuildArchives } from "../data/GuildArchives";
|
|||
import moment from "moment-timezone";
|
||||
import { SavedMessage } from "../data/entities/SavedMessage";
|
||||
import { GuildSavedMessages } from "../data/GuildSavedMessages";
|
||||
import { GuildActions } from "../data/GuildActions";
|
||||
import { GuildActions, MuteActionResult } from "../data/GuildActions";
|
||||
import { Case } from "../data/entities/Case";
|
||||
import { GuildMutes } from "../data/GuildMutes";
|
||||
import { ZeppelinPlugin } from "./ZeppelinPlugin";
|
||||
|
@ -191,7 +191,7 @@ export class SpamPlugin extends ZeppelinPlugin<ISpamPluginConfig> {
|
|||
this.recentActions = this.recentActions.filter(action => action.timestamp >= expiryTimestamp);
|
||||
}
|
||||
|
||||
async saveSpamArchives(savedMessages: SavedMessage[], channel: Channel) {
|
||||
async saveSpamArchives(savedMessages: SavedMessage[]) {
|
||||
const expiresAt = moment().add(SPAM_ARCHIVE_EXPIRY_DAYS, "days");
|
||||
const archiveId = await this.archives.createFromSavedMessages(savedMessages, this.guild, expiresAt);
|
||||
|
||||
|
@ -239,12 +239,17 @@ export class SpamPlugin extends ZeppelinPlugin<ISpamPluginConfig> {
|
|||
const recentActions = this.getRecentActions(type, savedMessage.user_id, savedMessage.channel_id, since);
|
||||
|
||||
// Start by muting them, if enabled
|
||||
let timeUntilUnmute;
|
||||
let muteResult: MuteActionResult;
|
||||
if (spamConfig.mute && member) {
|
||||
const muteTime = spamConfig.mute_time ? spamConfig.mute_time * 60 * 1000 : 120 * 1000;
|
||||
timeUntilUnmute = humanizeDuration(muteTime);
|
||||
this.logs.ignoreLog(LogType.MEMBER_ROLE_ADD, savedMessage.user_id);
|
||||
this.actions.fire("mute", { member, muteTime, reason: "Automatic spam detection" });
|
||||
const muteTime = spamConfig.mute_time ? convertDelayStringToMS(spamConfig.mute_time) : 120 * 1000;
|
||||
muteResult = await this.actions.fire("mute", {
|
||||
member,
|
||||
muteTime,
|
||||
reason: "Automatic spam detection",
|
||||
caseDetails: {
|
||||
modId: this.bot.user.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Get the offending message IDs
|
||||
|
@ -285,19 +290,39 @@ export class SpamPlugin extends ZeppelinPlugin<ISpamPluginConfig> {
|
|||
|
||||
// Generate a log from the detected messages
|
||||
const channel = this.guild.channels.get(savedMessage.channel_id);
|
||||
const archiveUrl = await this.saveSpamArchives(uniqueMessages, channel);
|
||||
const archiveUrl = await this.saveSpamArchives(uniqueMessages);
|
||||
|
||||
// Create a case and log the actions taken above
|
||||
const caseType = spamConfig.mute ? CaseTypes.Mute : CaseTypes.Note;
|
||||
let caseText = trimLines(`
|
||||
Automatic spam detection: ${description} (over ${spamConfig.count} in ${spamConfig.interval}s)
|
||||
${archiveUrl}
|
||||
`);
|
||||
// Create a case
|
||||
if (muteResult) {
|
||||
// If the user was muted, the mute already generated a case - in that case, just update the case with extra details
|
||||
const updateText = trimLines(`
|
||||
Details: ${description} (over ${spamConfig.count} in ${spamConfig.interval}s)
|
||||
${archiveUrl}
|
||||
`);
|
||||
this.actions.fire("createCaseNote", {
|
||||
caseId: muteResult.case.id,
|
||||
modId: muteResult.case.mod_id,
|
||||
note: updateText,
|
||||
automatic: true,
|
||||
postInCaseLogOverride: false,
|
||||
});
|
||||
} else {
|
||||
// If the user was not muted, create a note case of the detected spam instead
|
||||
const caseText = trimLines(`
|
||||
Automatic spam detection: ${description} (over ${spamConfig.count} in ${spamConfig.interval}s)
|
||||
${archiveUrl}
|
||||
`);
|
||||
|
||||
if (spamConfig.mute) {
|
||||
caseText = `__[Muted for ${timeUntilUnmute}]__ ${caseText}`;
|
||||
this.actions.fire("createCase", {
|
||||
userId: savedMessage.user_id,
|
||||
modId: this.bot.user.id,
|
||||
type: CaseTypes.Note,
|
||||
reason: caseText,
|
||||
automatic: true,
|
||||
});
|
||||
}
|
||||
|
||||
// Create a log entry
|
||||
this.logs.log(LogType.MESSAGE_SPAM_DETECTED, {
|
||||
member: stripObjectToScalars(member, ["user"]),
|
||||
channel: stripObjectToScalars(channel),
|
||||
|
@ -306,24 +331,10 @@ export class SpamPlugin extends ZeppelinPlugin<ISpamPluginConfig> {
|
|||
interval: spamConfig.interval,
|
||||
archiveUrl,
|
||||
});
|
||||
|
||||
const theCase: Case = await this.actions.fire("createCase", {
|
||||
userId: savedMessage.user_id,
|
||||
modId: this.bot.user.id,
|
||||
type: caseType,
|
||||
reason: caseText,
|
||||
automatic: true,
|
||||
});
|
||||
|
||||
// For mutes, also set the mute's case id (for !mutes)
|
||||
if (spamConfig.mute && member) {
|
||||
await this.mutes.setCaseId(savedMessage.user_id, theCase.id);
|
||||
}
|
||||
}
|
||||
},
|
||||
err => {
|
||||
console.error("Error while detecting spam:");
|
||||
console.error(err);
|
||||
logger.error(`Error while detecting spam:\n${err}`);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
@ -469,7 +480,7 @@ export class SpamPlugin extends ZeppelinPlugin<ISpamPluginConfig> {
|
|||
);
|
||||
}
|
||||
|
||||
// TODO: Max duplicates
|
||||
// TODO: Max duplicates check
|
||||
}
|
||||
|
||||
@d.event("voiceChannelJoin")
|
||||
|
|
68
src/utils.ts
68
src/utils.ts
|
@ -1,4 +1,4 @@
|
|||
import { Emoji, Guild, GuildAuditLogEntry, TextableChannel } from "eris";
|
||||
import { Client, Emoji, Guild, GuildAuditLogEntry, TextableChannel, TextChannel, User } from "eris";
|
||||
import url from "url";
|
||||
import tlds from "tlds";
|
||||
import emojiRegex from "emoji-regex";
|
||||
|
@ -8,6 +8,7 @@ const fsp = fs.promises;
|
|||
|
||||
import https from "https";
|
||||
import tmp from "tmp";
|
||||
import { logger } from "knub";
|
||||
|
||||
/**
|
||||
* Turns a "delay string" such as "1h30m" to milliseconds
|
||||
|
@ -428,3 +429,68 @@ export const DBDateFormat = "YYYY-MM-DD HH:mm:ss";
|
|||
export type CustomEmoji = {
|
||||
id: string;
|
||||
} & Emoji;
|
||||
|
||||
export interface INotifyUserConfig {
|
||||
useDM?: boolean;
|
||||
useChannel?: boolean;
|
||||
channelId?: string;
|
||||
}
|
||||
|
||||
export enum NotifyUserStatus {
|
||||
Ignored = 1,
|
||||
Failed,
|
||||
DirectMessaged,
|
||||
ChannelMessaged,
|
||||
}
|
||||
|
||||
export interface INotifyUserResult {
|
||||
status: NotifyUserStatus;
|
||||
text?: string;
|
||||
}
|
||||
|
||||
export async function notifyUser(
|
||||
bot: Client,
|
||||
guild: Guild,
|
||||
user: User,
|
||||
body: string,
|
||||
config: INotifyUserConfig,
|
||||
): Promise<INotifyUserResult> {
|
||||
if (!config.useDM && !config.useChannel) {
|
||||
return { status: NotifyUserStatus.Ignored };
|
||||
}
|
||||
|
||||
if (config.useDM) {
|
||||
try {
|
||||
const dmChannel = await bot.getDMChannel(user.id);
|
||||
await dmChannel.createMessage(body);
|
||||
logger.info(`Notified ${user.id} via DM: ${body}`);
|
||||
return {
|
||||
status: NotifyUserStatus.DirectMessaged,
|
||||
text: "user notified with a direct message",
|
||||
};
|
||||
} catch (e) {} // tslint:disable-line
|
||||
}
|
||||
|
||||
if (config.useChannel && config.channelId) {
|
||||
try {
|
||||
const channel = guild.channels.get(config.channelId);
|
||||
if (channel instanceof TextChannel) {
|
||||
await channel.createMessage(`<@!${user.id}> ${body}`);
|
||||
return {
|
||||
status: NotifyUserStatus.ChannelMessaged,
|
||||
text: `user notified in <#${channel.id}>`,
|
||||
};
|
||||
}
|
||||
} catch (e) {} // tslint:disable-line
|
||||
}
|
||||
|
||||
return {
|
||||
status: NotifyUserStatus.Failed,
|
||||
text: "failed to message user",
|
||||
};
|
||||
}
|
||||
|
||||
export function ucfirst(str) {
|
||||
if (typeof str !== "string" || str === "") return str;
|
||||
return str[0].toUpperCase() + str.slice(1);
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue