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",
|
"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 { BaseRepository } from "./BaseRepository";
|
||||||
import { Member, TextableChannel } from "eris";
|
import { Member, TextableChannel } from "eris";
|
||||||
import { CaseTypes } from "./CaseTypes";
|
import { CaseTypes } from "./CaseTypes";
|
||||||
|
import { ICaseDetails } from "./GuildCases";
|
||||||
|
import { Case } from "./entities/Case";
|
||||||
|
import { INotifyUserResult } from "../utils";
|
||||||
|
|
||||||
type KnownActions = "mute" | "unmute";
|
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 ActionFn<T> = (args: T) => any | Promise<any>;
|
||||||
|
|
||||||
type MuteActionArgs = { member: Member; muteTime?: number; reason?: string };
|
type MuteActionArgs = { member: Member; muteTime?: number; reason?: string; caseDetails?: ICaseDetails };
|
||||||
type UnmuteActionArgs = { member: Member; unmuteTime?: number; reason?: string };
|
type UnmuteActionArgs = { member: Member; unmuteTime?: number; reason?: string; caseDetails?: ICaseDetails };
|
||||||
type CreateCaseActionArgs = {
|
type CreateCaseActionArgs = ICaseDetails;
|
||||||
userId: string;
|
|
||||||
modId: string;
|
|
||||||
type: CaseTypes;
|
|
||||||
auditLogId?: string;
|
|
||||||
reason?: string;
|
|
||||||
automatic?: boolean;
|
|
||||||
postInCaseLog?: boolean;
|
|
||||||
ppId?: string;
|
|
||||||
};
|
|
||||||
type CreateCaseNoteActionArgs = {
|
type CreateCaseNoteActionArgs = {
|
||||||
caseId: number;
|
caseId: number;
|
||||||
modId: string;
|
modId: string;
|
||||||
note: string;
|
note: string;
|
||||||
automatic?: boolean;
|
automatic?: boolean;
|
||||||
postInCaseLog?: boolean;
|
postInCaseLog?: boolean;
|
||||||
|
noteDetails?: string[];
|
||||||
};
|
};
|
||||||
type PostCaseActionArgs = {
|
type PostCaseActionArgs = {
|
||||||
caseId: number;
|
caseId: number;
|
||||||
channel: TextableChannel;
|
channel: TextableChannel;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type MuteActionResult = {
|
||||||
|
case: Case;
|
||||||
|
notifyResult: INotifyUserResult;
|
||||||
|
updatedExistingMute: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UnmuteActionResult = {
|
||||||
|
case: Case;
|
||||||
|
};
|
||||||
|
|
||||||
export class GuildActions extends BaseRepository {
|
export class GuildActions extends BaseRepository {
|
||||||
private actions: Map<string, ActionFn<any>>;
|
private actions: Map<string, ActionFn<any>>;
|
||||||
|
|
||||||
|
@ -63,8 +68,8 @@ export class GuildActions extends BaseRepository {
|
||||||
this.actions.delete(actionName);
|
this.actions.delete(actionName);
|
||||||
}
|
}
|
||||||
|
|
||||||
public fire(actionName: "mute", args: MuteActionArgs): Promise<any>;
|
public fire(actionName: "mute", args: MuteActionArgs): Promise<MuteActionResult>;
|
||||||
public fire(actionName: "unmute", args: UnmuteActionArgs): Promise<any>;
|
public fire(actionName: "unmute", args: UnmuteActionArgs): Promise<UnmuteActionResult>;
|
||||||
public fire(actionName: "createCase", args: CreateCaseActionArgs): Promise<any>;
|
public fire(actionName: "createCase", args: CreateCaseActionArgs): Promise<any>;
|
||||||
public fire(actionName: "createCaseNote", args: CreateCaseNoteActionArgs): Promise<any>;
|
public fire(actionName: "createCaseNote", args: CreateCaseNoteActionArgs): Promise<any>;
|
||||||
public fire(actionName: "postCase", args: PostCaseActionArgs): 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;
|
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 {
|
export class GuildCases extends BaseRepository {
|
||||||
private cases: Repository<Case>;
|
private cases: Repository<Case>;
|
||||||
private caseNotes: Repository<CaseNote>;
|
private caseNotes: Repository<CaseNote>;
|
||||||
|
|
|
@ -24,11 +24,16 @@ export class GuildMutes extends BaseRepository {
|
||||||
return this.mutes.findOne({
|
return this.mutes.findOne({
|
||||||
where: {
|
where: {
|
||||||
guild_id: this.guildId,
|
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> {
|
async addMute(userId, expiryTime): Promise<Mute> {
|
||||||
const expiresAt = expiryTime
|
const expiresAt = expiryTime
|
||||||
? moment()
|
? moment()
|
||||||
|
@ -39,7 +44,7 @@ export class GuildMutes extends BaseRepository {
|
||||||
const result = await this.mutes.insert({
|
const result = await this.mutes.insert({
|
||||||
guild_id: this.guildId,
|
guild_id: this.guildId,
|
||||||
user_id: userId,
|
user_id: userId,
|
||||||
expires_at: expiresAt
|
expires_at: expiresAt,
|
||||||
});
|
});
|
||||||
|
|
||||||
return this.mutes.findOne({ where: result.identifiers[0] });
|
return this.mutes.findOne({ where: result.identifiers[0] });
|
||||||
|
@ -55,25 +60,14 @@ export class GuildMutes extends BaseRepository {
|
||||||
return this.mutes.update(
|
return this.mutes.update(
|
||||||
{
|
{
|
||||||
guild_id: this.guildId,
|
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[]> {
|
async getActiveMutes(): Promise<Mute[]> {
|
||||||
return this.mutes
|
return this.mutes
|
||||||
.createQueryBuilder("mutes")
|
.createQueryBuilder("mutes")
|
||||||
|
@ -81,7 +75,7 @@ export class GuildMutes extends BaseRepository {
|
||||||
.andWhere(
|
.andWhere(
|
||||||
new Brackets(qb => {
|
new Brackets(qb => {
|
||||||
qb.where("expires_at > NOW()").orWhere("expires_at IS NULL");
|
qb.where("expires_at > NOW()").orWhere("expires_at IS NULL");
|
||||||
})
|
}),
|
||||||
)
|
)
|
||||||
.getMany();
|
.getMany();
|
||||||
}
|
}
|
||||||
|
@ -90,18 +84,18 @@ export class GuildMutes extends BaseRepository {
|
||||||
await this.mutes.update(
|
await this.mutes.update(
|
||||||
{
|
{
|
||||||
guild_id: this.guildId,
|
guild_id: this.guildId,
|
||||||
user_id: userId
|
user_id: userId,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
case_id: caseId
|
case_id: caseId,
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async clear(userId) {
|
async clear(userId) {
|
||||||
await this.mutes.delete({
|
await this.mutes.delete({
|
||||||
guild_id: this.guildId,
|
guild_id: this.guildId,
|
||||||
user_id: userId
|
user_id: userId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -49,4 +49,6 @@ export enum LogType {
|
||||||
|
|
||||||
MEMBER_ROLE_CHANGES,
|
MEMBER_ROLE_CHANGES,
|
||||||
VOICE_CHANNEL_FORCE_MOVE,
|
VOICE_CHANNEL_FORCE_MOVE,
|
||||||
|
|
||||||
|
CASE_UPDATE,
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { Message, MessageContent, MessageFile, TextableChannel, TextChannel } from "eris";
|
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 { CaseTypes } from "../data/CaseTypes";
|
||||||
import { Case } from "../data/entities/Case";
|
import { Case } from "../data/entities/Case";
|
||||||
import moment from "moment-timezone";
|
import moment from "moment-timezone";
|
||||||
|
@ -36,20 +36,18 @@ export class CasesPlugin extends ZeppelinPlugin<ICasesPluginConfig> {
|
||||||
this.archives = GuildArchives.getInstance(this.guildId);
|
this.archives = GuildArchives.getInstance(this.guildId);
|
||||||
|
|
||||||
this.actions.register("createCase", args => {
|
this.actions.register("createCase", args => {
|
||||||
return this.createCase(
|
return this.createCase(args);
|
||||||
args.userId,
|
|
||||||
args.modId,
|
|
||||||
args.type,
|
|
||||||
args.auditLogId,
|
|
||||||
args.reason,
|
|
||||||
args.automatic,
|
|
||||||
args.postInCaseLog,
|
|
||||||
args.ppId,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.actions.register("createCaseNote", 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 => {
|
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
|
* Creates a new case and, depending on config, posts it in the case log channel
|
||||||
* @return {Number} The ID of the created case
|
* @return {Number} The ID of the created case
|
||||||
*/
|
*/
|
||||||
public async createCase(
|
public async createCase(opts: ICaseDetails): Promise<Case> {
|
||||||
userId: string,
|
const user = this.bot.users.get(opts.userId);
|
||||||
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);
|
|
||||||
const userName = user ? `${user.username}#${user.discriminator}` : "Unknown#0000";
|
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";
|
const modName = mod ? `${mod.username}#${mod.discriminator}` : "Unknown#0000";
|
||||||
|
|
||||||
let ppName = null;
|
let ppName = null;
|
||||||
if (ppId) {
|
if (opts.ppId) {
|
||||||
const pp = this.bot.users.get(ppId);
|
const pp = this.bot.users.get(opts.ppId);
|
||||||
ppName = pp ? `${pp.username}#${pp.discriminator}` : "Unknown#0000";
|
ppName = pp ? `${pp.username}#${pp.discriminator}` : "Unknown#0000";
|
||||||
}
|
}
|
||||||
|
|
||||||
const createdCase = await this.cases.create({
|
const createdCase = await this.cases.create({
|
||||||
type,
|
type: opts.type,
|
||||||
user_id: userId,
|
user_id: opts.userId,
|
||||||
user_name: userName,
|
user_name: userName,
|
||||||
mod_id: modId,
|
mod_id: opts.modId,
|
||||||
mod_name: modName,
|
mod_name: modName,
|
||||||
audit_log_id: auditLogId,
|
audit_log_id: opts.auditLogId,
|
||||||
pp_id: ppId,
|
pp_id: opts.ppId,
|
||||||
pp_name: ppName,
|
pp_name: ppName,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (reason) {
|
if (opts.reason || opts.noteDetails.length) {
|
||||||
await this.createCaseNote(createdCase, modId, reason, automatic, false);
|
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();
|
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 {
|
try {
|
||||||
await this.postCaseToCaseLogChannel(createdCase);
|
await this.postCaseToCaseLogChannel(createdCase);
|
||||||
} catch (e) {} // tslint:disable-line
|
} catch (e) {} // tslint:disable-line
|
||||||
|
@ -129,6 +128,7 @@ export class CasesPlugin extends ZeppelinPlugin<ICasesPluginConfig> {
|
||||||
body: string,
|
body: string,
|
||||||
automatic = false,
|
automatic = false,
|
||||||
postInCaseLogOverride = null,
|
postInCaseLogOverride = null,
|
||||||
|
noteDetails: string[] = null,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const mod = this.bot.users.get(modId);
|
const mod = this.bot.users.get(modId);
|
||||||
const modName = mod ? `${mod.username}#${mod.discriminator}` : "Unknown#0000";
|
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}`);
|
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, {
|
await this.cases.createNote(theCase.id, {
|
||||||
mod_id: modId,
|
mod_id: modId,
|
||||||
mod_name: modName,
|
mod_name: modName,
|
||||||
|
|
|
@ -3,11 +3,13 @@ import { Attachment, Constants as ErisConstants, Guild, Member, Message, TextCha
|
||||||
import humanizeDuration from "humanize-duration";
|
import humanizeDuration from "humanize-duration";
|
||||||
import { GuildCases } from "../data/GuildCases";
|
import { GuildCases } from "../data/GuildCases";
|
||||||
import {
|
import {
|
||||||
convertDelayStringToMS,
|
asSingleLine,
|
||||||
createChunkedMessage,
|
createChunkedMessage,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
findRelevantAuditLogEntry,
|
findRelevantAuditLogEntry,
|
||||||
asSingleLine,
|
INotifyUserResult,
|
||||||
|
notifyUser,
|
||||||
|
NotifyUserStatus,
|
||||||
stripObjectToScalars,
|
stripObjectToScalars,
|
||||||
successMessage,
|
successMessage,
|
||||||
trimLines,
|
trimLines,
|
||||||
|
@ -17,9 +19,8 @@ import { CaseTypes } from "../data/CaseTypes";
|
||||||
import { GuildLogs } from "../data/GuildLogs";
|
import { GuildLogs } from "../data/GuildLogs";
|
||||||
import { LogType } from "../data/LogType";
|
import { LogType } from "../data/LogType";
|
||||||
import { ZeppelinPlugin } from "./ZeppelinPlugin";
|
import { ZeppelinPlugin } from "./ZeppelinPlugin";
|
||||||
import { GuildActions } from "../data/GuildActions";
|
import { GuildActions, MuteActionResult } from "../data/GuildActions";
|
||||||
import { Case } from "../data/entities/Case";
|
import { Case } from "../data/entities/Case";
|
||||||
import { Mute } from "../data/entities/Mute";
|
|
||||||
import { renderTemplate } from "../templateFormatter";
|
import { renderTemplate } from "../templateFormatter";
|
||||||
|
|
||||||
enum IgnoredEventType {
|
enum IgnoredEventType {
|
||||||
|
@ -33,31 +34,15 @@ interface IIgnoredEvent {
|
||||||
userId: string;
|
userId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
enum MessageResultStatus {
|
|
||||||
Ignored = 1,
|
|
||||||
Failed,
|
|
||||||
DirectMessaged,
|
|
||||||
ChannelMessaged,
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IMessageResult {
|
|
||||||
status: MessageResultStatus;
|
|
||||||
text?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IModActionsPluginConfig {
|
interface IModActionsPluginConfig {
|
||||||
dm_on_warn: boolean;
|
dm_on_warn: boolean;
|
||||||
dm_on_mute: boolean;
|
|
||||||
dm_on_kick: boolean;
|
dm_on_kick: boolean;
|
||||||
dm_on_ban: boolean;
|
dm_on_ban: boolean;
|
||||||
message_on_warn: boolean;
|
message_on_warn: boolean;
|
||||||
message_on_mute: boolean;
|
|
||||||
message_on_kick: boolean;
|
message_on_kick: boolean;
|
||||||
message_on_ban: boolean;
|
message_on_ban: boolean;
|
||||||
message_channel: string;
|
message_channel: string;
|
||||||
warn_message: string;
|
warn_message: string;
|
||||||
mute_message: string;
|
|
||||||
timed_mute_message: string;
|
|
||||||
kick_message: string;
|
kick_message: string;
|
||||||
ban_message: string;
|
ban_message: string;
|
||||||
alert_on_rejoin: boolean;
|
alert_on_rejoin: boolean;
|
||||||
|
@ -98,17 +83,13 @@ export class ModActionsPlugin extends ZeppelinPlugin<IModActionsPluginConfig> {
|
||||||
return {
|
return {
|
||||||
config: {
|
config: {
|
||||||
dm_on_warn: true,
|
dm_on_warn: true,
|
||||||
dm_on_mute: false,
|
|
||||||
dm_on_kick: false,
|
dm_on_kick: false,
|
||||||
dm_on_ban: false,
|
dm_on_ban: false,
|
||||||
message_on_warn: false,
|
message_on_warn: false,
|
||||||
message_on_mute: false,
|
|
||||||
message_on_kick: false,
|
message_on_kick: false,
|
||||||
message_on_ban: false,
|
message_on_ban: false,
|
||||||
message_channel: null,
|
message_channel: null,
|
||||||
warn_message: "You have received a warning on {guildName}: {reason}",
|
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}",
|
kick_message: "You have been kicked from {guildName}. Reason given: {reason}",
|
||||||
ban_message: "You have been banned from {guildName}. Reason given: {reason}",
|
ban_message: "You have been banned from {guildName}. Reason given: {reason}",
|
||||||
alert_on_rejoin: false,
|
alert_on_rejoin: false,
|
||||||
|
@ -307,7 +288,7 @@ export class ModActionsPlugin extends ZeppelinPlugin<IModActionsPluginConfig> {
|
||||||
overloads: ["<note:string$>"],
|
overloads: ["<note:string$>"],
|
||||||
})
|
})
|
||||||
@d.permission("can_note")
|
@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;
|
let theCase: Case;
|
||||||
if (args.caseNumber != null) {
|
if (args.caseNumber != null) {
|
||||||
theCase = await this.cases.findByCaseNumber(args.caseNumber);
|
theCase = await this.cases.findByCaseNumber(args.caseNumber);
|
||||||
|
@ -326,6 +307,13 @@ export class ModActionsPlugin extends ZeppelinPlugin<IModActionsPluginConfig> {
|
||||||
note: args.note,
|
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`));
|
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 warnMessage = config.warn_message.replace("{guildName}", this.guild.name).replace("{reason}", reason);
|
||||||
|
|
||||||
const userMessageResult = await this.tryToMessageUser(
|
const userMessageResult = await notifyUser(this.bot, this.guild, args.member.user, warnMessage, {
|
||||||
args.member.user,
|
useDM: config.dm_on_warn,
|
||||||
warnMessage,
|
useChannel: config.message_on_warn,
|
||||||
config.dm_on_warn,
|
});
|
||||||
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 failedMsg = await msg.channel.createMessage("Failed to message the user. Log the warning anyway?");
|
||||||
const reply = await waitForReaction(this.bot, failedMsg, ["✅", "❌"], msg.author.id);
|
const reply = await waitForReaction(this.bot, failedMsg, ["✅", "❌"], msg.author.id);
|
||||||
failedMsg.delete();
|
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
|
// 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;
|
||||||
|
|
||||||
if (args.mod) {
|
if (args.mod) {
|
||||||
if (!this.hasPermission("can_act_as_other", { message: msg })) {
|
if (!this.hasPermission("can_act_as_other", { message: msg })) {
|
||||||
msg.channel.createMessage(errorMessage("No permission for --mod"));
|
msg.channel.createMessage(errorMessage("No permission for --mod"));
|
||||||
|
@ -434,116 +422,60 @@ export class ModActionsPlugin extends ZeppelinPlugin<IModActionsPluginConfig> {
|
||||||
}
|
}
|
||||||
|
|
||||||
mod = args.mod;
|
mod = args.mod;
|
||||||
|
pp = msg.author;
|
||||||
}
|
}
|
||||||
|
|
||||||
let userMessageResult: IMessageResult = { status: MessageResultStatus.Ignored };
|
|
||||||
|
|
||||||
const timeUntilUnmute = args.time && humanizeDuration(args.time);
|
const timeUntilUnmute = args.time && humanizeDuration(args.time);
|
||||||
const reason = this.formatReasonWithAttachments(args.reason, msg.attachments);
|
const reason = this.formatReasonWithAttachments(args.reason, msg.attachments);
|
||||||
|
|
||||||
// Apply "muted" role
|
let muteResult: MuteActionResult;
|
||||||
this.serverLogs.ignoreLog(LogType.MEMBER_ROLE_ADD, args.member.id);
|
|
||||||
const mute: Mute = await this.actions.fire("mute", {
|
|
||||||
member: args.member,
|
|
||||||
muteTime: args.time,
|
|
||||||
});
|
|
||||||
|
|
||||||
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"));
|
msg.channel.createMessage(errorMessage("Could not mute the user"));
|
||||||
return;
|
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
|
// Confirm the action to the moderator
|
||||||
let response;
|
let response;
|
||||||
if (args.time) {
|
if (args.time) {
|
||||||
if (hasOldCase) {
|
if (muteResult.updatedExistingMute) {
|
||||||
response = asSingleLine(`
|
response = asSingleLine(`
|
||||||
Updated **${args.member.user.username}#${args.member.user.discriminator}**'s
|
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 {
|
} else {
|
||||||
response = asSingleLine(`
|
response = asSingleLine(`
|
||||||
Muted **${args.member.user.username}#${args.member.user.discriminator}**
|
Muted **${args.member.user.username}#${args.member.user.discriminator}**
|
||||||
for ${timeUntilUnmute} (Case #${theCase.case_number})
|
for ${timeUntilUnmute} (Case #${muteResult.case.case_number})
|
||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (hasOldCase) {
|
if (muteResult.updatedExistingMute) {
|
||||||
response = asSingleLine(`
|
response = asSingleLine(`
|
||||||
Updated **${args.member.user.username}#${args.member.user.discriminator}**'s
|
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 {
|
} else {
|
||||||
response = asSingleLine(`
|
response = asSingleLine(`
|
||||||
Muted **${args.member.user.username}#${args.member.user.discriminator}**
|
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));
|
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$>", {
|
@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
|
// 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 (args.mod) {
|
||||||
if (!this.hasPermission("can_act_as_other", { message: msg })) {
|
if (!this.hasPermission("can_act_as_other", { message: msg })) {
|
||||||
msg.channel.createMessage(errorMessage("No permission for --mod"));
|
msg.channel.createMessage(errorMessage("No permission for --mod"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
mod = args.mod;
|
mod = args.mod.user;
|
||||||
|
pp = msg.author;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if they're muted in the first place
|
// Check if they're muted in the first place
|
||||||
const mute = await this.mutes.findExistingMuteForUserId(args.member.id);
|
if (!(await this.mutes.isMuted(args.member.id))) {
|
||||||
if (!mute) {
|
|
||||||
msg.channel.createMessage(errorMessage("Cannot unmute: member is not muted"));
|
msg.channel.createMessage(errorMessage("Cannot unmute: member is not muted"));
|
||||||
return;
|
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 reason = this.formatReasonWithAttachments(args.reason, msg.attachments);
|
||||||
const caseNote = args.time ? `__[Scheduled unmute in ${timeUntilUnmute}]__ ${reason}` : reason;
|
|
||||||
|
|
||||||
// Create a case
|
const result = await this.actions.fire("unmute", {
|
||||||
const createdCase = await this.actions.fire("createCase", {
|
member: args.member,
|
||||||
userId: args.member.id,
|
unmuteTime: args.time,
|
||||||
modId: mod.id,
|
caseDetails: {
|
||||||
type: CaseTypes.Unmute,
|
modId: mod.id,
|
||||||
reason: caseNote,
|
ppId: pp && pp.id,
|
||||||
ppId: mod.id !== msg.author.id ? msg.author.id : null,
|
reason,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Confirm the action to the moderator
|
||||||
if (args.time) {
|
if (args.time) {
|
||||||
// If we have an unmute time, just update the old mute to expire in that time
|
const timeUntilUnmute = args.time && humanizeDuration(args.time);
|
||||||
await this.actions.fire("unmute", { member: args.member, unmuteTime: args.time });
|
|
||||||
|
|
||||||
// Confirm the action to the moderator
|
|
||||||
msg.channel.createMessage(
|
msg.channel.createMessage(
|
||||||
successMessage(
|
successMessage(
|
||||||
`Unmuting **${args.member.user.username}#${args.member.user.discriminator}** in ${timeUntilUnmute} (Case #${
|
asSingleLine(`
|
||||||
createdCase.case_number
|
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 {
|
} 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(
|
msg.channel.createMessage(
|
||||||
successMessage(
|
successMessage(
|
||||||
`Unmuted **${args.member.user.username}#${args.member.user.discriminator}** (Case #${
|
asSingleLine(`
|
||||||
createdCase.case_number
|
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);
|
const reason = this.formatReasonWithAttachments(args.reason, msg.attachments);
|
||||||
|
|
||||||
// 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 userMessageResult: IMessageResult = { status: MessageResultStatus.Ignored };
|
let userMessageResult: INotifyUserResult = { status: NotifyUserStatus.Ignored };
|
||||||
if (args.reason) {
|
if (args.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,
|
||||||
});
|
});
|
||||||
|
|
||||||
userMessageResult = await this.tryToMessageUser(
|
userMessageResult = await notifyUser(this.bot, this.guild, args.member.user, kickMessage, {
|
||||||
args.member.user,
|
useDM: config.dm_on_kick,
|
||||||
kickMessage,
|
useChannel: config.message_on_kick,
|
||||||
config.dm_on_kick,
|
channelId: config.message_channel,
|
||||||
config.message_on_kick,
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Kick the user
|
// Kick the user
|
||||||
|
@ -728,19 +640,18 @@ export class ModActionsPlugin extends ZeppelinPlugin<IModActionsPluginConfig> {
|
||||||
const reason = this.formatReasonWithAttachments(args.reason, msg.attachments);
|
const reason = this.formatReasonWithAttachments(args.reason, msg.attachments);
|
||||||
|
|
||||||
// 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 userMessageResult: IMessageResult = { status: MessageResultStatus.Ignored };
|
let userMessageResult: INotifyUserResult = { status: NotifyUserStatus.Ignored };
|
||||||
if (reason) {
|
if (reason) {
|
||||||
const banMessage = await renderTemplate(config.ban_message, {
|
const banMessage = await renderTemplate(config.ban_message, {
|
||||||
guildName: this.guild.name,
|
guildName: this.guild.name,
|
||||||
reason,
|
reason,
|
||||||
});
|
});
|
||||||
|
|
||||||
userMessageResult = await this.tryToMessageUser(
|
userMessageResult = await notifyUser(this.bot, this.guild, args.member.user, banMessage, {
|
||||||
args.member.user,
|
useDM: config.dm_on_ban,
|
||||||
banMessage,
|
useChannel: config.message_on_ban,
|
||||||
config.dm_on_ban,
|
channelId: config.message_channel,
|
||||||
config.message_on_ban,
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ban the user
|
// Ban the user
|
||||||
|
@ -1233,49 +1144,4 @@ export class ModActionsPlugin extends ZeppelinPlugin<IModActionsPluginConfig> {
|
||||||
await this.cases.setHidden(theCase.id, false);
|
await this.cases.setHidden(theCase.id, false);
|
||||||
msg.channel.createMessage(successMessage(`Case #${theCase.case_number} is no longer hidden!`));
|
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 { Member, Message, User } from "eris";
|
||||||
import { GuildCases } from "../data/GuildCases";
|
import { GuildCases, ICaseDetails } from "../data/GuildCases";
|
||||||
import moment from "moment-timezone";
|
import moment from "moment-timezone";
|
||||||
import { ZeppelinPlugin } from "./ZeppelinPlugin";
|
import { ZeppelinPlugin } from "./ZeppelinPlugin";
|
||||||
import { GuildActions } from "../data/GuildActions";
|
import { GuildActions } from "../data/GuildActions";
|
||||||
import { GuildMutes } from "../data/GuildMutes";
|
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 humanizeDuration from "humanize-duration";
|
||||||
import { LogType } from "../data/LogType";
|
import { LogType } from "../data/LogType";
|
||||||
import { GuildLogs } from "../data/GuildLogs";
|
import { GuildLogs } from "../data/GuildLogs";
|
||||||
import { decorators as d, IPluginOptions, logger } from "knub";
|
import { decorators as d, IPluginOptions, logger } from "knub";
|
||||||
import { Mute } from "../data/entities/Mute";
|
import { Mute } from "../data/entities/Mute";
|
||||||
|
import { renderTemplate } from "../templateFormatter";
|
||||||
|
import { CaseTypes } from "../data/CaseTypes";
|
||||||
|
|
||||||
interface IMuteWithDetails extends Mute {
|
interface IMuteWithDetails extends Mute {
|
||||||
member?: Member;
|
member?: Member;
|
||||||
|
@ -20,6 +32,12 @@ interface IMutesPluginConfig {
|
||||||
mute_role: string;
|
mute_role: string;
|
||||||
move_to_voice_channel: 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_view_list: boolean;
|
||||||
can_cleanup: boolean;
|
can_cleanup: boolean;
|
||||||
}
|
}
|
||||||
|
@ -39,6 +57,12 @@ export class MutesPlugin extends ZeppelinPlugin<IMutesPluginConfig> {
|
||||||
mute_role: null,
|
mute_role: null,
|
||||||
move_to_voice_channel: 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_view_list: false,
|
||||||
can_cleanup: false,
|
can_cleanup: false,
|
||||||
},
|
},
|
||||||
|
@ -66,10 +90,10 @@ export class MutesPlugin extends ZeppelinPlugin<IMutesPluginConfig> {
|
||||||
this.serverLogs = new GuildLogs(this.guildId);
|
this.serverLogs = new GuildLogs(this.guildId);
|
||||||
|
|
||||||
this.actions.register("mute", args => {
|
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 => {
|
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
|
// Check for expired mutes every 5s
|
||||||
|
@ -84,33 +108,128 @@ export class MutesPlugin extends ZeppelinPlugin<IMutesPluginConfig> {
|
||||||
clearInterval(this.muteClearIntervalId);
|
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;
|
const muteRole = this.getConfig().mute_role;
|
||||||
if (!muteRole) return;
|
if (!muteRole) return;
|
||||||
|
|
||||||
// Add muted role
|
const timeUntilUnmute = muteTime && humanizeDuration(muteTime);
|
||||||
await member.addRole(muteRole);
|
|
||||||
|
// 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)
|
// 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;
|
const moveToVoiceChannelId = this.getConfig().move_to_voice_channel;
|
||||||
if (moveToVoiceChannelId && member.voiceState.channelID) {
|
if (moveToVoiceChannelId && member.voiceState.channelID) {
|
||||||
try {
|
try {
|
||||||
await member.edit({
|
await member.edit({ channelID: moveToVoiceChannelId });
|
||||||
channelID: moveToVoiceChannelId,
|
|
||||||
});
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.warn(`Could not move user ${member.id} to voice channel ${moveToVoiceChannelId} when muting`);
|
logger.warn(`Could not move user ${member.id} to voice channel ${moveToVoiceChannelId} when muting`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create & return mute record
|
// If the user is already muted, update the duration of their existing mute
|
||||||
return this.mutes.addOrUpdateMute(member.id, muteTime);
|
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) {
|
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 {
|
} else {
|
||||||
|
// Unmute immediately
|
||||||
const muteRole = this.getConfig().mute_role;
|
const muteRole = this.getConfig().mute_role;
|
||||||
if (member.roles.includes(muteRole)) {
|
if (member.roles.includes(muteRole)) {
|
||||||
await member.removeRole(muteRole);
|
await member.removeRole(muteRole);
|
||||||
|
@ -118,6 +237,44 @@ export class MutesPlugin extends ZeppelinPlugin<IMutesPluginConfig> {
|
||||||
|
|
||||||
await this.mutes.clear(member.id);
|
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", [], {
|
@d.command("mutes", [], {
|
||||||
|
@ -361,7 +518,6 @@ export class MutesPlugin extends ZeppelinPlugin<IMutesPluginConfig> {
|
||||||
if (!member) continue;
|
if (!member) continue;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.serverLogs.ignoreLog(LogType.MEMBER_ROLE_REMOVE, member.id);
|
|
||||||
await member.removeRole(this.getConfig().mute_role);
|
await member.removeRole(this.getConfig().mute_role);
|
||||||
} catch (e) {} // tslint:disable-line
|
} 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 { Channel, Member } from "eris";
|
||||||
import humanizeDuration from "humanize-duration";
|
|
||||||
import {
|
import {
|
||||||
|
convertDelayStringToMS,
|
||||||
getEmojiInString,
|
getEmojiInString,
|
||||||
getRoleMentions,
|
getRoleMentions,
|
||||||
getUrlsInString,
|
getUrlsInString,
|
||||||
|
@ -16,7 +16,7 @@ import { GuildArchives } from "../data/GuildArchives";
|
||||||
import moment from "moment-timezone";
|
import moment from "moment-timezone";
|
||||||
import { SavedMessage } from "../data/entities/SavedMessage";
|
import { SavedMessage } from "../data/entities/SavedMessage";
|
||||||
import { GuildSavedMessages } from "../data/GuildSavedMessages";
|
import { GuildSavedMessages } from "../data/GuildSavedMessages";
|
||||||
import { GuildActions } from "../data/GuildActions";
|
import { GuildActions, MuteActionResult } from "../data/GuildActions";
|
||||||
import { Case } from "../data/entities/Case";
|
import { Case } from "../data/entities/Case";
|
||||||
import { GuildMutes } from "../data/GuildMutes";
|
import { GuildMutes } from "../data/GuildMutes";
|
||||||
import { ZeppelinPlugin } from "./ZeppelinPlugin";
|
import { ZeppelinPlugin } from "./ZeppelinPlugin";
|
||||||
|
@ -191,7 +191,7 @@ export class SpamPlugin extends ZeppelinPlugin<ISpamPluginConfig> {
|
||||||
this.recentActions = this.recentActions.filter(action => action.timestamp >= expiryTimestamp);
|
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 expiresAt = moment().add(SPAM_ARCHIVE_EXPIRY_DAYS, "days");
|
||||||
const archiveId = await this.archives.createFromSavedMessages(savedMessages, this.guild, expiresAt);
|
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);
|
const recentActions = this.getRecentActions(type, savedMessage.user_id, savedMessage.channel_id, since);
|
||||||
|
|
||||||
// Start by muting them, if enabled
|
// Start by muting them, if enabled
|
||||||
let timeUntilUnmute;
|
let muteResult: MuteActionResult;
|
||||||
if (spamConfig.mute && member) {
|
if (spamConfig.mute && member) {
|
||||||
const muteTime = spamConfig.mute_time ? spamConfig.mute_time * 60 * 1000 : 120 * 1000;
|
const muteTime = spamConfig.mute_time ? convertDelayStringToMS(spamConfig.mute_time) : 120 * 1000;
|
||||||
timeUntilUnmute = humanizeDuration(muteTime);
|
muteResult = await this.actions.fire("mute", {
|
||||||
this.logs.ignoreLog(LogType.MEMBER_ROLE_ADD, savedMessage.user_id);
|
member,
|
||||||
this.actions.fire("mute", { member, muteTime, reason: "Automatic spam detection" });
|
muteTime,
|
||||||
|
reason: "Automatic spam detection",
|
||||||
|
caseDetails: {
|
||||||
|
modId: this.bot.user.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the offending message IDs
|
// Get the offending message IDs
|
||||||
|
@ -285,19 +290,39 @@ export class SpamPlugin extends ZeppelinPlugin<ISpamPluginConfig> {
|
||||||
|
|
||||||
// Generate a log from the detected messages
|
// Generate a log from the detected messages
|
||||||
const channel = this.guild.channels.get(savedMessage.channel_id);
|
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
|
// Create a case
|
||||||
const caseType = spamConfig.mute ? CaseTypes.Mute : CaseTypes.Note;
|
if (muteResult) {
|
||||||
let caseText = trimLines(`
|
// If the user was muted, the mute already generated a case - in that case, just update the case with extra details
|
||||||
Automatic spam detection: ${description} (over ${spamConfig.count} in ${spamConfig.interval}s)
|
const updateText = trimLines(`
|
||||||
${archiveUrl}
|
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) {
|
this.actions.fire("createCase", {
|
||||||
caseText = `__[Muted for ${timeUntilUnmute}]__ ${caseText}`;
|
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, {
|
this.logs.log(LogType.MESSAGE_SPAM_DETECTED, {
|
||||||
member: stripObjectToScalars(member, ["user"]),
|
member: stripObjectToScalars(member, ["user"]),
|
||||||
channel: stripObjectToScalars(channel),
|
channel: stripObjectToScalars(channel),
|
||||||
|
@ -306,24 +331,10 @@ export class SpamPlugin extends ZeppelinPlugin<ISpamPluginConfig> {
|
||||||
interval: spamConfig.interval,
|
interval: spamConfig.interval,
|
||||||
archiveUrl,
|
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 => {
|
err => {
|
||||||
console.error("Error while detecting spam:");
|
logger.error(`Error while detecting spam:\n${err}`);
|
||||||
console.error(err);
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -469,7 +480,7 @@ export class SpamPlugin extends ZeppelinPlugin<ISpamPluginConfig> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Max duplicates
|
// TODO: Max duplicates check
|
||||||
}
|
}
|
||||||
|
|
||||||
@d.event("voiceChannelJoin")
|
@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 url from "url";
|
||||||
import tlds from "tlds";
|
import tlds from "tlds";
|
||||||
import emojiRegex from "emoji-regex";
|
import emojiRegex from "emoji-regex";
|
||||||
|
@ -8,6 +8,7 @@ const fsp = fs.promises;
|
||||||
|
|
||||||
import https from "https";
|
import https from "https";
|
||||||
import tmp from "tmp";
|
import tmp from "tmp";
|
||||||
|
import { logger } from "knub";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Turns a "delay string" such as "1h30m" to milliseconds
|
* 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 = {
|
export type CustomEmoji = {
|
||||||
id: string;
|
id: string;
|
||||||
} & Emoji;
|
} & 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