2020-01-29 02:44:11 +02:00
|
|
|
import { trimPluginDescription, ZeppelinPlugin } from "../ZeppelinPlugin";
|
2019-08-18 16:40:15 +03:00
|
|
|
import * as t from "io-ts";
|
|
|
|
import {
|
|
|
|
convertDelayStringToMS,
|
2019-11-30 18:07:25 +02:00
|
|
|
disableInlineCode,
|
|
|
|
disableLinkPreviews,
|
2020-01-29 02:44:11 +02:00
|
|
|
disableUserNotificationStrings,
|
2019-08-18 16:40:15 +03:00
|
|
|
getEmojiInString,
|
|
|
|
getInviteCodesInString,
|
|
|
|
getRoleMentions,
|
|
|
|
getUrlsInString,
|
|
|
|
getUserMentions,
|
2019-10-11 01:59:56 +03:00
|
|
|
messageSummary,
|
2019-08-18 16:40:15 +03:00
|
|
|
MINUTES,
|
|
|
|
noop,
|
2019-10-11 01:59:56 +03:00
|
|
|
SECONDS,
|
|
|
|
stripObjectToScalars,
|
2019-11-28 18:34:48 +02:00
|
|
|
tDeepPartial,
|
2020-01-29 02:44:11 +02:00
|
|
|
UserNotificationMethod,
|
2019-10-11 22:56:34 +03:00
|
|
|
verboseChannelMention,
|
2020-01-26 19:54:32 +02:00
|
|
|
} from "../../utils";
|
2020-01-29 02:44:11 +02:00
|
|
|
import { configUtils, CooldownManager, decorators as d, IPluginOptions, logger } from "knub";
|
2020-01-26 19:54:32 +02:00
|
|
|
import { Member, Message, TextChannel, User } from "eris";
|
2019-08-18 16:40:15 +03:00
|
|
|
import escapeStringRegexp from "escape-string-regexp";
|
2020-01-26 19:54:32 +02:00
|
|
|
import { SimpleCache } from "../../SimpleCache";
|
|
|
|
import { Queue } from "../../Queue";
|
|
|
|
import { ModActionsPlugin } from "../ModActions";
|
|
|
|
import { MutesPlugin } from "../Mutes";
|
|
|
|
import { LogsPlugin } from "../Logs";
|
|
|
|
import { LogType } from "../../data/LogType";
|
|
|
|
import { GuildSavedMessages } from "../../data/GuildSavedMessages";
|
|
|
|
import { GuildArchives } from "../../data/GuildArchives";
|
|
|
|
import { GuildLogs } from "../../data/GuildLogs";
|
|
|
|
import { SavedMessage } from "../../data/entities/SavedMessage";
|
2019-10-11 01:59:56 +03:00
|
|
|
import moment from "moment-timezone";
|
2020-01-26 19:54:32 +02:00
|
|
|
import { renderTemplate } from "../../templateFormatter";
|
2019-11-30 22:04:28 +02:00
|
|
|
import { transliterate } from "transliteration";
|
2020-01-26 19:54:32 +02:00
|
|
|
import { IMatchParams } from "knub/dist/configUtils";
|
|
|
|
import { GuildAntiraidLevels } from "../../data/GuildAntiraidLevels";
|
|
|
|
import {
|
|
|
|
AnyTriggerMatchResult,
|
|
|
|
BaseTextSpamTrigger,
|
|
|
|
MessageInfo,
|
|
|
|
OtherRecentAction,
|
|
|
|
OtherSpamTriggerMatchResult,
|
|
|
|
OtherTriggerMatchResult,
|
|
|
|
RecentAction,
|
|
|
|
RecentActionType,
|
|
|
|
RecentSpam,
|
|
|
|
Rule,
|
|
|
|
TBaseSpamTrigger,
|
|
|
|
TBaseTextSpamTrigger,
|
|
|
|
TextRecentAction,
|
|
|
|
TextSpamTriggerMatchResult,
|
|
|
|
TextTriggerMatchResult,
|
|
|
|
TextTriggerWithMultipleMatchTypes,
|
|
|
|
TMatchInvitesTrigger,
|
|
|
|
TMatchLinksTrigger,
|
|
|
|
TMatchRegexTrigger,
|
|
|
|
TMatchWordsTrigger,
|
|
|
|
TMemberJoinTrigger,
|
|
|
|
TRule,
|
2020-03-20 18:04:37 +01:00
|
|
|
TMatchAttachmentTypeTrigger,
|
2020-01-26 19:54:32 +02:00
|
|
|
} from "./types";
|
|
|
|
import { pluginInfo } from "./info";
|
2020-01-29 02:44:11 +02:00
|
|
|
import { ERRORS, RecoverablePluginError } from "../../RecoverablePluginError";
|
|
|
|
import Timeout = NodeJS.Timeout;
|
2020-03-20 18:04:37 +01:00
|
|
|
import { StrictValidationError } from "src/validatorUtils";
|
2020-01-26 19:54:32 +02:00
|
|
|
|
|
|
|
const unactioned = (action: TextRecentAction | OtherRecentAction) => !action.actioned;
|
2019-08-18 16:40:15 +03:00
|
|
|
|
|
|
|
/**
|
2020-01-26 19:54:32 +02:00
|
|
|
* DEFAULTS
|
2019-08-18 16:40:15 +03:00
|
|
|
*/
|
|
|
|
|
2019-11-30 22:06:26 +02:00
|
|
|
const defaultMatchWordsTrigger: Partial<TMatchWordsTrigger> = {
|
2019-08-18 16:40:15 +03:00
|
|
|
case_sensitive: false,
|
|
|
|
only_full_words: true,
|
2019-11-30 22:04:28 +02:00
|
|
|
normalize: false,
|
|
|
|
loose_matching: false,
|
|
|
|
loose_matching_threshold: 4,
|
2019-08-18 16:40:15 +03:00
|
|
|
match_messages: true,
|
|
|
|
match_embeds: true,
|
2019-10-11 01:59:56 +03:00
|
|
|
match_visible_names: false,
|
2019-08-18 16:40:15 +03:00
|
|
|
match_usernames: false,
|
|
|
|
match_nicknames: false,
|
2019-10-26 02:59:00 +03:00
|
|
|
match_custom_status: false,
|
2019-08-18 16:40:15 +03:00
|
|
|
};
|
|
|
|
|
|
|
|
const defaultMatchRegexTrigger: Partial<TMatchRegexTrigger> = {
|
|
|
|
case_sensitive: false,
|
2019-11-30 22:04:28 +02:00
|
|
|
normalize: false,
|
2019-08-18 16:40:15 +03:00
|
|
|
match_messages: true,
|
|
|
|
match_embeds: true,
|
2019-10-11 01:59:56 +03:00
|
|
|
match_visible_names: false,
|
2019-08-18 16:40:15 +03:00
|
|
|
match_usernames: false,
|
|
|
|
match_nicknames: false,
|
2019-10-26 02:59:00 +03:00
|
|
|
match_custom_status: false,
|
2019-08-18 16:40:15 +03:00
|
|
|
};
|
|
|
|
|
|
|
|
const defaultMatchInvitesTrigger: Partial<TMatchInvitesTrigger> = {
|
|
|
|
allow_group_dm_invites: false,
|
|
|
|
match_messages: true,
|
|
|
|
match_embeds: true,
|
2019-10-11 01:59:56 +03:00
|
|
|
match_visible_names: false,
|
2019-08-18 16:40:15 +03:00
|
|
|
match_usernames: false,
|
|
|
|
match_nicknames: false,
|
2019-10-26 02:59:00 +03:00
|
|
|
match_custom_status: false,
|
2019-08-18 16:40:15 +03:00
|
|
|
};
|
|
|
|
|
|
|
|
const defaultMatchLinksTrigger: Partial<TMatchLinksTrigger> = {
|
|
|
|
include_subdomains: true,
|
|
|
|
match_messages: true,
|
|
|
|
match_embeds: true,
|
2019-10-11 01:59:56 +03:00
|
|
|
match_visible_names: false,
|
2019-08-18 16:40:15 +03:00
|
|
|
match_usernames: false,
|
|
|
|
match_nicknames: false,
|
2019-10-26 02:59:00 +03:00
|
|
|
match_custom_status: false,
|
2019-08-18 16:40:15 +03:00
|
|
|
};
|
|
|
|
|
2020-03-20 18:04:37 +01:00
|
|
|
const defaultMatchAttachmentTypeTrigger: Partial<TMatchAttachmentTypeTrigger> = {
|
|
|
|
filetype_blacklist: [],
|
|
|
|
blacklist_enabled: false,
|
|
|
|
filetype_whitelist: [],
|
|
|
|
whitelist_enabled: false,
|
|
|
|
match_messages: true,
|
|
|
|
match_embeds: true,
|
|
|
|
match_visible_names: false,
|
|
|
|
match_usernames: false,
|
|
|
|
match_nicknames: false,
|
|
|
|
match_custom_status: false,
|
|
|
|
};
|
|
|
|
|
2019-08-18 16:40:15 +03:00
|
|
|
const defaultTextSpamTrigger: Partial<t.TypeOf<typeof BaseTextSpamTrigger>> = {
|
|
|
|
per_channel: true,
|
|
|
|
};
|
|
|
|
|
2020-01-26 19:54:32 +02:00
|
|
|
const defaultMemberJoinTrigger: Partial<TMemberJoinTrigger> = {
|
|
|
|
only_new: false,
|
|
|
|
new_threshold: "1h",
|
|
|
|
};
|
2019-08-18 16:40:15 +03:00
|
|
|
|
|
|
|
const defaultTriggers = {
|
|
|
|
match_words: defaultMatchWordsTrigger,
|
2019-10-11 01:59:56 +03:00
|
|
|
match_regex: defaultMatchRegexTrigger,
|
|
|
|
match_invites: defaultMatchInvitesTrigger,
|
|
|
|
match_links: defaultMatchLinksTrigger,
|
2020-03-20 18:04:37 +01:00
|
|
|
match_attachment_type: defaultMatchAttachmentTypeTrigger,
|
2019-10-11 01:59:56 +03:00
|
|
|
message_spam: defaultTextSpamTrigger,
|
|
|
|
mention_spam: defaultTextSpamTrigger,
|
|
|
|
link_spam: defaultTextSpamTrigger,
|
|
|
|
attachment_spam: defaultTextSpamTrigger,
|
|
|
|
emoji_spam: defaultTextSpamTrigger,
|
|
|
|
line_spam: defaultTextSpamTrigger,
|
|
|
|
character_spam: defaultTextSpamTrigger,
|
2020-01-26 19:54:32 +02:00
|
|
|
member_join: defaultMemberJoinTrigger,
|
2019-08-18 16:40:15 +03:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
2020-01-26 19:54:32 +02:00
|
|
|
* CONFIG
|
2019-08-18 16:40:15 +03:00
|
|
|
*/
|
|
|
|
|
2020-01-26 19:54:32 +02:00
|
|
|
const ConfigSchema = t.type({
|
|
|
|
rules: t.record(t.string, Rule),
|
|
|
|
antiraid_levels: t.array(t.string),
|
|
|
|
can_set_antiraid: t.boolean,
|
|
|
|
can_view_antiraid: t.boolean,
|
|
|
|
});
|
|
|
|
type TConfigSchema = t.TypeOf<typeof ConfigSchema>;
|
2019-08-18 16:40:15 +03:00
|
|
|
|
2020-01-26 19:54:32 +02:00
|
|
|
const PartialConfigSchema = tDeepPartial(ConfigSchema);
|
2019-08-18 16:40:15 +03:00
|
|
|
|
2020-01-26 19:54:32 +02:00
|
|
|
interface ICustomOverrides {
|
|
|
|
antiraid_level: string;
|
|
|
|
}
|
2019-08-18 16:40:15 +03:00
|
|
|
|
2020-01-26 19:54:32 +02:00
|
|
|
/**
|
|
|
|
* MISC
|
|
|
|
*/
|
2019-08-18 16:40:15 +03:00
|
|
|
|
2020-01-26 19:54:32 +02:00
|
|
|
const RECENT_SPAM_EXPIRY_TIME = 30 * SECONDS;
|
2020-01-28 22:18:12 +02:00
|
|
|
const RECENT_ACTION_EXPIRY_TIME = 5 * MINUTES;
|
2019-10-11 01:59:56 +03:00
|
|
|
const RECENT_NICKNAME_CHANGE_EXPIRY_TIME = 5 * MINUTES;
|
2019-08-18 16:40:15 +03:00
|
|
|
|
|
|
|
const inviteCache = new SimpleCache(10 * MINUTES);
|
|
|
|
|
2020-01-26 19:54:32 +02:00
|
|
|
const RAID_SPAM_IDENTIFIER = "raid";
|
|
|
|
|
2019-11-30 18:07:25 +02:00
|
|
|
/**
|
|
|
|
* General plugin flow:
|
2020-01-26 19:54:32 +02:00
|
|
|
*
|
|
|
|
* - Message based triggers:
|
|
|
|
* 1. matchRuleToMessage()
|
|
|
|
* 2. if match -> applyActionsOnMatch()
|
|
|
|
* 3. if spam -> clearTextSpamRecentActions()
|
|
|
|
*
|
|
|
|
* - Non-message based non-spam triggers:
|
|
|
|
* 1. bespoke match function
|
|
|
|
* 2. if match -> applyActionsOnMatch()
|
|
|
|
*
|
|
|
|
* - Non-message based spam triggers:
|
|
|
|
* 1. matchOtherSpamInRule()
|
|
|
|
* 2. if match -> applyActionsOnMatch()
|
|
|
|
* 3. -> clearOtherSpamRecentActions()
|
|
|
|
*
|
|
|
|
* To log actions for spam detection, logRecentActionsForMessage() is called for each message, and several other events
|
|
|
|
* call addRecentAction() directly. These are then checked by matchRuleToMessage() and matchOtherSpamInRule() to detect
|
|
|
|
* spam.
|
2019-11-30 18:07:25 +02:00
|
|
|
*/
|
2020-01-26 19:54:32 +02:00
|
|
|
export class AutomodPlugin extends ZeppelinPlugin<TConfigSchema, ICustomOverrides> {
|
2019-08-18 16:40:15 +03:00
|
|
|
public static pluginName = "automod";
|
2019-08-22 02:58:32 +03:00
|
|
|
public static configSchema = ConfigSchema;
|
2019-09-30 00:47:19 +03:00
|
|
|
public static dependencies = ["mod_actions", "mutes", "logs"];
|
2019-08-18 16:40:15 +03:00
|
|
|
|
2020-01-26 19:54:32 +02:00
|
|
|
public static pluginInfo = pluginInfo;
|
2019-10-11 01:59:56 +03:00
|
|
|
|
2019-08-18 16:40:15 +03:00
|
|
|
protected unloaded = false;
|
|
|
|
|
|
|
|
// Handle automod checks/actions in a queue so we don't get overlap on the same user
|
|
|
|
protected automodQueue: Queue;
|
|
|
|
|
2019-10-11 01:59:56 +03:00
|
|
|
// Recent actions are used to detect spam triggers
|
2019-08-18 16:40:15 +03:00
|
|
|
protected recentActions: RecentAction[];
|
|
|
|
protected recentActionClearInterval: Timeout;
|
|
|
|
|
2020-01-26 19:54:32 +02:00
|
|
|
// After a spam trigger is tripped and the rule's action carried out, a unique identifier is placed here so further
|
|
|
|
// spam (either messages that were sent before the bot managed to mute the user or, with global spam, other users
|
|
|
|
// continuing to spam) is "included" in the same match and doesn't generate duplicate cases or logs.
|
|
|
|
// Key: rule_name-match_identifier
|
|
|
|
protected recentSpam: Map<string, RecentSpam>;
|
|
|
|
protected recentSpamClearInterval: Timeout;
|
2019-10-11 01:59:56 +03:00
|
|
|
|
|
|
|
protected recentNicknameChanges: Map<string, { expiresAt: number }>;
|
|
|
|
protected recentNicknameChangesClearInterval: Timeout;
|
|
|
|
|
2019-10-26 02:59:00 +03:00
|
|
|
protected cooldownManager: CooldownManager;
|
|
|
|
|
2019-10-11 01:59:56 +03:00
|
|
|
protected onMessageCreateFn;
|
|
|
|
|
|
|
|
protected savedMessages: GuildSavedMessages;
|
|
|
|
protected archives: GuildArchives;
|
|
|
|
protected guildLogs: GuildLogs;
|
2020-01-26 19:54:32 +02:00
|
|
|
protected antiraidLevels: GuildAntiraidLevels;
|
|
|
|
|
|
|
|
protected loadedAntiraidLevel: boolean;
|
|
|
|
protected cachedAntiraidLevel: string | null;
|
2019-10-11 01:59:56 +03:00
|
|
|
|
2019-11-28 18:34:48 +02:00
|
|
|
protected static preprocessStaticConfig(config: t.TypeOf<typeof PartialConfigSchema>) {
|
|
|
|
if (config.rules) {
|
2019-08-18 16:40:15 +03:00
|
|
|
// Loop through each rule
|
|
|
|
for (const [name, rule] of Object.entries(config.rules)) {
|
|
|
|
rule["name"] = name;
|
|
|
|
|
|
|
|
// If the rule doesn't have an explicitly set "enabled" property, set it to true
|
|
|
|
if (rule["enabled"] == null) {
|
|
|
|
rule["enabled"] = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Loop through the rule's triggers
|
2019-11-28 18:34:48 +02:00
|
|
|
if (rule["triggers"]) {
|
2019-08-18 16:40:15 +03:00
|
|
|
for (const trigger of rule["triggers"]) {
|
2019-10-11 01:59:56 +03:00
|
|
|
// Apply default config to the triggers used in this rule
|
2019-08-18 16:40:15 +03:00
|
|
|
for (const [defaultTriggerName, defaultTrigger] of Object.entries(defaultTriggers)) {
|
2019-11-28 18:34:48 +02:00
|
|
|
if (trigger[defaultTriggerName]) {
|
2019-11-02 22:11:26 +02:00
|
|
|
trigger[defaultTriggerName] = configUtils.mergeConfig({}, defaultTrigger, trigger[defaultTriggerName]);
|
2019-08-18 16:40:15 +03:00
|
|
|
}
|
|
|
|
}
|
2020-03-20 18:04:37 +01:00
|
|
|
|
|
|
|
if (trigger.match_attachment_type) {
|
|
|
|
const white = trigger.match_attachment_type.whitelist_enabled;
|
|
|
|
const black = trigger.match_attachment_type.blacklist_enabled;
|
|
|
|
|
|
|
|
if (white && black) {
|
|
|
|
throw new StrictValidationError([
|
|
|
|
`Cannot have both blacklist and whitelist enabled at rule <${rule.name}/match_attachment_type>`,
|
|
|
|
]);
|
|
|
|
} else if (!white && !black) {
|
|
|
|
throw new StrictValidationError([
|
|
|
|
`Must have either blacklist or whitelist enabled at rule <${rule.name}/match_attachment_type>`,
|
|
|
|
]);
|
|
|
|
}
|
|
|
|
}
|
2019-08-18 16:40:15 +03:00
|
|
|
}
|
|
|
|
}
|
2019-10-11 23:16:15 +03:00
|
|
|
|
|
|
|
// Enable logging of automod actions by default
|
2019-11-28 18:34:48 +02:00
|
|
|
if (rule["actions"]) {
|
2019-10-11 23:16:15 +03:00
|
|
|
if (rule["actions"]["log"] == null) {
|
|
|
|
rule["actions"]["log"] = true;
|
|
|
|
}
|
|
|
|
}
|
2019-08-18 16:40:15 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return config;
|
|
|
|
}
|
|
|
|
|
2020-01-26 19:54:32 +02:00
|
|
|
public static getStaticDefaultOptions(): IPluginOptions<TConfigSchema, ICustomOverrides> {
|
2019-08-18 16:40:15 +03:00
|
|
|
return {
|
2020-01-26 19:54:32 +02:00
|
|
|
config: {
|
|
|
|
rules: {},
|
|
|
|
antiraid_levels: ["low", "medium", "high"],
|
|
|
|
can_set_antiraid: false,
|
|
|
|
can_view_antiraid: false,
|
|
|
|
},
|
|
|
|
overrides: [
|
|
|
|
{
|
|
|
|
level: ">=50",
|
|
|
|
config: {
|
|
|
|
can_view_antiraid: true,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
level: ">=100",
|
|
|
|
config: {
|
|
|
|
can_set_antiraid: true,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
],
|
2019-08-18 16:40:15 +03:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2020-01-26 19:54:32 +02:00
|
|
|
protected matchCustomOverrideCriteria(criteria: ICustomOverrides, matchParams: IMatchParams) {
|
2020-01-28 22:15:32 +02:00
|
|
|
return criteria?.antiraid_level && criteria.antiraid_level === this.cachedAntiraidLevel;
|
2020-01-26 19:54:32 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
protected async onLoad() {
|
2019-08-18 16:40:15 +03:00
|
|
|
this.automodQueue = new Queue();
|
2019-10-11 01:59:56 +03:00
|
|
|
|
|
|
|
this.recentActions = [];
|
2019-10-13 22:03:06 +03:00
|
|
|
this.recentActionClearInterval = setInterval(() => this.clearOldRecentActions(), 1 * MINUTES);
|
|
|
|
|
2020-01-26 19:54:32 +02:00
|
|
|
this.recentSpam = new Map();
|
|
|
|
this.recentSpamClearInterval = setInterval(() => this.clearExpiredRecentSpam(), 1 * SECONDS);
|
2019-10-11 01:59:56 +03:00
|
|
|
|
|
|
|
this.recentNicknameChanges = new Map();
|
|
|
|
this.recentNicknameChangesClearInterval = setInterval(() => this.clearExpiredRecentNicknameChanges(), 30 * SECONDS);
|
|
|
|
|
2019-10-26 02:59:00 +03:00
|
|
|
this.cooldownManager = new CooldownManager();
|
|
|
|
|
2019-10-11 01:59:56 +03:00
|
|
|
this.savedMessages = GuildSavedMessages.getGuildInstance(this.guildId);
|
|
|
|
this.archives = GuildArchives.getGuildInstance(this.guildId);
|
|
|
|
this.guildLogs = new GuildLogs(this.guildId);
|
2020-01-26 19:54:32 +02:00
|
|
|
this.antiraidLevels = GuildAntiraidLevels.getGuildInstance(this.guildId);
|
|
|
|
|
|
|
|
this.cachedAntiraidLevel = await this.antiraidLevels.get();
|
2019-10-11 01:59:56 +03:00
|
|
|
|
|
|
|
this.onMessageCreateFn = msg => this.onMessageCreate(msg);
|
|
|
|
this.savedMessages.events.on("create", this.onMessageCreateFn);
|
|
|
|
}
|
|
|
|
|
|
|
|
protected getModActions(): ModActionsPlugin {
|
|
|
|
return this.getPlugin("mod_actions");
|
|
|
|
}
|
|
|
|
|
|
|
|
protected getLogs(): LogsPlugin {
|
|
|
|
return this.getPlugin("logs");
|
2019-08-18 16:40:15 +03:00
|
|
|
}
|
|
|
|
|
2020-01-12 17:17:01 +02:00
|
|
|
protected getMutes(): MutesPlugin {
|
|
|
|
return this.getPlugin("mutes");
|
|
|
|
}
|
|
|
|
|
2019-08-18 16:40:15 +03:00
|
|
|
protected onUnload() {
|
|
|
|
this.unloaded = true;
|
2019-10-11 01:59:56 +03:00
|
|
|
this.savedMessages.events.off("create", this.onMessageCreateFn);
|
2019-08-18 16:40:15 +03:00
|
|
|
clearInterval(this.recentActionClearInterval);
|
2020-01-26 19:54:32 +02:00
|
|
|
clearInterval(this.recentSpamClearInterval);
|
2019-10-13 22:03:06 +03:00
|
|
|
clearInterval(this.recentNicknameChangesClearInterval);
|
2019-08-18 16:40:15 +03:00
|
|
|
}
|
|
|
|
|
2019-11-30 18:07:25 +02:00
|
|
|
/**
|
|
|
|
* @return Matched word
|
|
|
|
*/
|
|
|
|
protected evaluateMatchWordsTrigger(trigger: TMatchWordsTrigger, str: string): null | string {
|
2019-11-30 22:04:28 +02:00
|
|
|
if (trigger.normalize) {
|
|
|
|
str = transliterate(str);
|
|
|
|
}
|
|
|
|
|
|
|
|
const looseMatchingThreshold = Math.min(Math.max(trigger.loose_matching_threshold, 1), 64);
|
|
|
|
|
2019-08-18 16:40:15 +03:00
|
|
|
for (const word of trigger.words) {
|
2019-11-30 22:04:28 +02:00
|
|
|
// When performing loose matching, allow any amount of whitespace or up to looseMatchingThreshold number of other
|
|
|
|
// characters between the matched characters. E.g. if we're matching banana, a loose match could also match b a n a n a
|
|
|
|
let pattern = trigger.loose_matching
|
|
|
|
? [...word].map(c => escapeStringRegexp(c)).join(`(?:\\s*|.{0,${looseMatchingThreshold})`)
|
|
|
|
: escapeStringRegexp(word);
|
|
|
|
|
|
|
|
if (trigger.only_full_words) {
|
|
|
|
pattern = `\\b${pattern}\\b`;
|
|
|
|
}
|
2019-08-18 16:40:15 +03:00
|
|
|
|
|
|
|
const regex = new RegExp(pattern, trigger.case_sensitive ? "" : "i");
|
2019-10-11 01:59:56 +03:00
|
|
|
const test = regex.test(str);
|
2019-11-30 18:07:25 +02:00
|
|
|
if (test) return word;
|
2019-08-18 16:40:15 +03:00
|
|
|
}
|
2019-10-11 01:59:56 +03:00
|
|
|
|
2019-11-30 18:07:25 +02:00
|
|
|
return null;
|
2019-08-18 16:40:15 +03:00
|
|
|
}
|
|
|
|
|
2019-11-30 18:07:25 +02:00
|
|
|
/**
|
|
|
|
* @return Matched regex pattern
|
|
|
|
*/
|
|
|
|
protected evaluateMatchRegexTrigger(trigger: TMatchRegexTrigger, str: string): null | string {
|
2019-11-30 22:04:28 +02:00
|
|
|
if (trigger.normalize) {
|
|
|
|
str = transliterate(str);
|
|
|
|
}
|
|
|
|
|
2019-08-18 16:40:15 +03:00
|
|
|
// TODO: Time limit regexes
|
|
|
|
for (const pattern of trigger.patterns) {
|
|
|
|
const regex = new RegExp(pattern, trigger.case_sensitive ? "" : "i");
|
2019-10-11 01:59:56 +03:00
|
|
|
const test = regex.test(str);
|
2019-11-30 18:07:25 +02:00
|
|
|
if (test) return regex.source;
|
2019-08-18 16:40:15 +03:00
|
|
|
}
|
2019-10-11 01:59:56 +03:00
|
|
|
|
2019-11-30 18:07:25 +02:00
|
|
|
return null;
|
2019-08-18 16:40:15 +03:00
|
|
|
}
|
|
|
|
|
2019-11-30 18:07:25 +02:00
|
|
|
/**
|
|
|
|
* @return Matched invite code
|
|
|
|
*/
|
|
|
|
protected async evaluateMatchInvitesTrigger(trigger: TMatchInvitesTrigger, str: string): Promise<null | string> {
|
2019-08-18 16:40:15 +03:00
|
|
|
const inviteCodes = getInviteCodesInString(str);
|
2019-11-30 18:07:25 +02:00
|
|
|
if (inviteCodes.length === 0) return null;
|
2019-08-18 16:40:15 +03:00
|
|
|
|
|
|
|
const uniqueInviteCodes = Array.from(new Set(inviteCodes));
|
|
|
|
|
|
|
|
for (const code of uniqueInviteCodes) {
|
|
|
|
if (trigger.include_invite_codes && trigger.include_invite_codes.includes(code)) {
|
2019-11-30 18:07:25 +02:00
|
|
|
return code;
|
2019-08-18 16:40:15 +03:00
|
|
|
}
|
|
|
|
if (trigger.exclude_invite_codes && !trigger.exclude_invite_codes.includes(code)) {
|
2019-11-30 18:07:25 +02:00
|
|
|
return code;
|
2019-08-18 16:40:15 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-11-30 18:07:25 +02:00
|
|
|
for (const inviteCode of uniqueInviteCodes) {
|
|
|
|
const invite = await this.resolveInvite(inviteCode);
|
|
|
|
if (!invite) return inviteCode;
|
2019-10-13 22:05:26 +03:00
|
|
|
|
2019-08-18 16:40:15 +03:00
|
|
|
if (trigger.include_guilds && trigger.include_guilds.includes(invite.guild.id)) {
|
2019-11-30 18:07:25 +02:00
|
|
|
return inviteCode;
|
2019-08-18 16:40:15 +03:00
|
|
|
}
|
|
|
|
if (trigger.exclude_guilds && !trigger.exclude_guilds.includes(invite.guild.id)) {
|
2019-11-30 18:07:25 +02:00
|
|
|
return inviteCode;
|
2019-08-18 16:40:15 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-11-30 18:07:25 +02:00
|
|
|
return null;
|
2019-08-18 16:40:15 +03:00
|
|
|
}
|
|
|
|
|
2019-11-30 18:07:25 +02:00
|
|
|
/**
|
|
|
|
* @return Matched link
|
|
|
|
*/
|
|
|
|
protected evaluateMatchLinksTrigger(trigger: TMatchLinksTrigger, str: string): null | string {
|
2019-08-18 16:40:15 +03:00
|
|
|
const links = getUrlsInString(str, true);
|
2019-11-27 20:41:45 +02:00
|
|
|
|
2019-08-18 16:40:15 +03:00
|
|
|
for (const link of links) {
|
|
|
|
const normalizedHostname = link.hostname.toLowerCase();
|
|
|
|
|
2020-04-11 16:56:55 +03:00
|
|
|
// Exclude > Include
|
|
|
|
// In order of specificity, regex > word > domain
|
|
|
|
|
|
|
|
if (trigger.exclude_regex) {
|
|
|
|
for (const pattern of trigger.exclude_regex) {
|
|
|
|
if (pattern.test(link.input)) {
|
|
|
|
return null;
|
2019-08-18 16:40:15 +03:00
|
|
|
}
|
2020-04-11 16:56:55 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (trigger.include_regex) {
|
|
|
|
for (const pattern of trigger.include_regex) {
|
|
|
|
if (pattern.test(link.input)) {
|
|
|
|
return link.input;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (trigger.exclude_words) {
|
|
|
|
for (const word of trigger.exclude_words) {
|
|
|
|
const regex = new RegExp(escapeStringRegexp(word), "i");
|
|
|
|
if (regex.test(link.input)) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (trigger.include_words) {
|
|
|
|
for (const word of trigger.include_words) {
|
|
|
|
const regex = new RegExp(escapeStringRegexp(word), "i");
|
|
|
|
if (regex.test(link.input)) {
|
|
|
|
return link.input;
|
2019-08-18 16:40:15 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (trigger.exclude_domains) {
|
|
|
|
for (const domain of trigger.exclude_domains) {
|
|
|
|
const normalizedDomain = domain.toLowerCase();
|
|
|
|
if (normalizedDomain === normalizedHostname) {
|
2019-11-30 18:07:25 +02:00
|
|
|
return null;
|
2019-08-18 16:40:15 +03:00
|
|
|
}
|
|
|
|
if (trigger.include_subdomains && normalizedHostname.endsWith(`.${domain}`)) {
|
2019-11-30 18:07:25 +02:00
|
|
|
return null;
|
2019-08-18 16:40:15 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-11-30 18:07:25 +02:00
|
|
|
return link.toString();
|
2019-08-18 16:40:15 +03:00
|
|
|
}
|
2020-04-11 16:56:55 +03:00
|
|
|
|
|
|
|
if (trigger.include_domains) {
|
|
|
|
for (const domain of trigger.include_domains) {
|
|
|
|
const normalizedDomain = domain.toLowerCase();
|
|
|
|
if (normalizedDomain === normalizedHostname) {
|
|
|
|
return domain;
|
|
|
|
}
|
|
|
|
if (trigger.include_subdomains && normalizedHostname.endsWith(`.${domain}`)) {
|
|
|
|
return domain;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2019-08-18 16:40:15 +03:00
|
|
|
}
|
|
|
|
|
2019-11-30 18:07:25 +02:00
|
|
|
return null;
|
2019-08-18 16:40:15 +03:00
|
|
|
}
|
|
|
|
|
2020-03-20 18:04:37 +01:00
|
|
|
protected evaluateMatchAttachmentTypeTrigger(trigger: TMatchAttachmentTypeTrigger, msg: SavedMessage): null | string {
|
|
|
|
if (!msg.data.attachments) return null;
|
|
|
|
const attachments: any[] = msg.data.attachments;
|
|
|
|
|
|
|
|
for (const attachment of attachments) {
|
|
|
|
const attachment_type = attachment.filename.split(`.`).pop();
|
|
|
|
if (trigger.blacklist_enabled && trigger.filetype_blacklist.includes(attachment_type)) {
|
|
|
|
return `${attachment_type} - blacklisted`;
|
|
|
|
}
|
|
|
|
if (trigger.whitelist_enabled && !trigger.filetype_whitelist.includes(attachment_type)) {
|
|
|
|
return `${attachment_type} - not whitelisted`;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2019-08-18 16:40:15 +03:00
|
|
|
protected matchTextSpamTrigger(
|
|
|
|
recentActionType: RecentActionType,
|
|
|
|
trigger: TBaseTextSpamTrigger,
|
2019-10-11 01:59:56 +03:00
|
|
|
msg: SavedMessage,
|
2020-01-26 19:54:32 +02:00
|
|
|
): Omit<TextSpamTriggerMatchResult, "trigger" | "rule"> {
|
2019-10-11 01:59:56 +03:00
|
|
|
const since = moment.utc(msg.posted_at).valueOf() - convertDelayStringToMS(trigger.within);
|
2020-01-26 19:54:32 +02:00
|
|
|
const identifier = trigger.per_channel ? `${msg.channel_id}-${msg.user_id}` : msg.user_id;
|
|
|
|
const recentActions = this.getMatchingRecentActions(recentActionType, identifier, since);
|
2019-10-11 01:59:56 +03:00
|
|
|
const totalCount = recentActions.reduce((total, action) => {
|
|
|
|
return total + action.count;
|
|
|
|
}, 0);
|
|
|
|
|
|
|
|
if (totalCount >= trigger.amount) {
|
2019-08-18 16:40:15 +03:00
|
|
|
return {
|
|
|
|
type: "textspam",
|
|
|
|
actionType: recentActionType,
|
2020-01-26 19:54:32 +02:00
|
|
|
recentActions: recentActions as TextRecentAction[],
|
|
|
|
identifier,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
protected matchOtherSpamTrigger(
|
|
|
|
recentActionType: RecentActionType,
|
|
|
|
trigger: TBaseSpamTrigger,
|
|
|
|
identifier: string | null,
|
|
|
|
): Omit<OtherSpamTriggerMatchResult, "trigger" | "rule"> {
|
|
|
|
const since = moment.utc().valueOf() - convertDelayStringToMS(trigger.within);
|
|
|
|
const recentActions = this.getMatchingRecentActions(recentActionType, identifier, since) as OtherRecentAction[];
|
|
|
|
const totalCount = recentActions.reduce((total, action) => {
|
|
|
|
return total + action.count;
|
|
|
|
}, 0);
|
|
|
|
|
|
|
|
if (totalCount >= trigger.amount) {
|
|
|
|
return {
|
|
|
|
type: "otherspam",
|
|
|
|
actionType: recentActionType,
|
|
|
|
recentActions,
|
|
|
|
identifier,
|
2019-08-18 16:40:15 +03:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2019-11-30 22:06:26 +02:00
|
|
|
protected async matchMultipleTextTypesOnMessage<T>(
|
2019-08-18 16:40:15 +03:00
|
|
|
trigger: TextTriggerWithMultipleMatchTypes,
|
2019-10-11 01:59:56 +03:00
|
|
|
msg: SavedMessage,
|
2019-11-30 22:06:26 +02:00
|
|
|
matchFn: (str: string) => T | Promise<T> | null,
|
|
|
|
): Promise<Partial<TextTriggerMatchResult<T>>> {
|
2020-01-26 19:54:32 +02:00
|
|
|
const messageInfo: MessageInfo = { channelId: msg.channel_id, messageId: msg.id, userId: msg.user_id };
|
2020-04-15 22:38:13 +03:00
|
|
|
const member = await this.getMember(msg.user_id);
|
2019-08-18 16:40:15 +03:00
|
|
|
|
2020-04-10 11:27:23 +03:00
|
|
|
if (trigger.match_messages && msg.data.content) {
|
2019-10-11 01:59:56 +03:00
|
|
|
const str = msg.data.content;
|
2019-11-30 22:06:26 +02:00
|
|
|
const matchResult = await matchFn(str);
|
|
|
|
if (matchResult) {
|
|
|
|
return { type: "message", str, userId: msg.user_id, messageInfo, matchedValue: matchResult };
|
|
|
|
}
|
2019-08-18 16:40:15 +03:00
|
|
|
}
|
|
|
|
|
2019-10-11 01:59:56 +03:00
|
|
|
if (trigger.match_embeds && msg.data.embeds && msg.data.embeds.length) {
|
|
|
|
const str = JSON.stringify(msg.data.embeds[0]);
|
2019-11-30 22:06:26 +02:00
|
|
|
const matchResult = await matchFn(str);
|
|
|
|
if (matchResult) {
|
|
|
|
return { type: "embed", str, userId: msg.user_id, messageInfo, matchedValue: matchResult };
|
|
|
|
}
|
2019-10-11 01:59:56 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
if (trigger.match_visible_names) {
|
|
|
|
const str = member.nick || msg.data.author.username;
|
2019-11-30 22:06:26 +02:00
|
|
|
const matchResult = await matchFn(str);
|
|
|
|
if (matchResult) {
|
|
|
|
return { type: "visiblename", str, userId: msg.user_id, matchedValue: matchResult };
|
|
|
|
}
|
2019-08-18 16:40:15 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
if (trigger.match_usernames) {
|
2019-10-11 01:59:56 +03:00
|
|
|
const str = `${msg.data.author.username}#${msg.data.author.discriminator}`;
|
2019-11-30 22:06:26 +02:00
|
|
|
const matchResult = await matchFn(str);
|
|
|
|
if (matchResult) {
|
|
|
|
return { type: "username", str, userId: msg.user_id, matchedValue: matchResult };
|
|
|
|
}
|
2019-08-18 16:40:15 +03:00
|
|
|
}
|
|
|
|
|
2019-10-11 01:59:56 +03:00
|
|
|
if (trigger.match_nicknames && member.nick) {
|
|
|
|
const str = member.nick;
|
2019-11-30 22:06:26 +02:00
|
|
|
const matchResult = await matchFn(str);
|
|
|
|
if (matchResult) {
|
|
|
|
return { type: "nickname", str, userId: msg.user_id, matchedValue: matchResult };
|
|
|
|
}
|
2019-08-18 16:40:15 +03:00
|
|
|
}
|
|
|
|
|
2019-10-26 02:59:00 +03:00
|
|
|
// type 4 = custom status
|
2020-04-15 22:38:13 +03:00
|
|
|
if (trigger.match_custom_status && member.game?.type === 4 && member.game?.state) {
|
2019-10-26 02:59:00 +03:00
|
|
|
const str = member.game.state;
|
2019-11-30 22:06:26 +02:00
|
|
|
const matchResult = await matchFn(str);
|
|
|
|
if (matchResult) {
|
|
|
|
return { type: "customstatus", str, userId: msg.user_id, matchedValue: matchResult };
|
|
|
|
}
|
2019-10-26 02:59:00 +03:00
|
|
|
}
|
|
|
|
|
2019-08-18 16:40:15 +03:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2019-11-30 22:06:26 +02:00
|
|
|
protected async matchMultipleTextTypesOnMember<T>(
|
2019-08-18 16:40:15 +03:00
|
|
|
trigger: TextTriggerWithMultipleMatchTypes,
|
|
|
|
member: Member,
|
2019-11-30 22:06:26 +02:00
|
|
|
matchFn: (str: string) => T | Promise<T> | null,
|
|
|
|
): Promise<Partial<TextTriggerMatchResult<T>>> {
|
2019-08-18 16:40:15 +03:00
|
|
|
if (trigger.match_usernames) {
|
|
|
|
const str = `${member.user.username}#${member.user.discriminator}`;
|
2019-11-30 22:06:26 +02:00
|
|
|
const matchResult = await matchFn(str);
|
|
|
|
if (matchResult) {
|
|
|
|
return { type: "username", str, userId: member.id, matchedValue: matchResult };
|
|
|
|
}
|
2019-08-18 16:40:15 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
if (trigger.match_nicknames && member.nick) {
|
|
|
|
const str = member.nick;
|
2019-11-30 22:06:26 +02:00
|
|
|
const matchResult = await matchFn(str);
|
|
|
|
if (matchResult) {
|
|
|
|
return { type: "nickname", str, userId: member.id, matchedValue: matchResult };
|
|
|
|
}
|
2019-08-18 16:40:15 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns whether the triggers in the rule match the given message
|
|
|
|
*/
|
|
|
|
protected async matchRuleToMessage(
|
|
|
|
rule: TRule,
|
2019-10-11 01:59:56 +03:00
|
|
|
msg: SavedMessage,
|
2019-08-18 16:40:15 +03:00
|
|
|
): Promise<TextTriggerMatchResult | TextSpamTriggerMatchResult> {
|
2019-10-11 01:59:56 +03:00
|
|
|
if (!rule.enabled) return;
|
|
|
|
|
2019-08-18 16:40:15 +03:00
|
|
|
for (const trigger of rule.triggers) {
|
|
|
|
if (trigger.match_words) {
|
|
|
|
const match = await this.matchMultipleTextTypesOnMessage(trigger.match_words, msg, str => {
|
|
|
|
return this.evaluateMatchWordsTrigger(trigger.match_words, str);
|
|
|
|
});
|
2019-11-30 22:06:26 +02:00
|
|
|
if (match) return { ...match, trigger: "match_words" } as TextTriggerMatchResult;
|
2019-08-18 16:40:15 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
if (trigger.match_regex) {
|
|
|
|
const match = await this.matchMultipleTextTypesOnMessage(trigger.match_regex, msg, str => {
|
|
|
|
return this.evaluateMatchRegexTrigger(trigger.match_regex, str);
|
|
|
|
});
|
2019-11-30 22:06:26 +02:00
|
|
|
if (match) return { ...match, trigger: "match_regex" } as TextTriggerMatchResult;
|
2019-08-18 16:40:15 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
if (trigger.match_invites) {
|
|
|
|
const match = await this.matchMultipleTextTypesOnMessage(trigger.match_invites, msg, str => {
|
|
|
|
return this.evaluateMatchInvitesTrigger(trigger.match_invites, str);
|
|
|
|
});
|
2019-11-30 22:06:26 +02:00
|
|
|
if (match) return { ...match, trigger: "match_invites" } as TextTriggerMatchResult;
|
2019-08-18 16:40:15 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
if (trigger.match_links) {
|
|
|
|
const match = await this.matchMultipleTextTypesOnMessage(trigger.match_links, msg, str => {
|
|
|
|
return this.evaluateMatchLinksTrigger(trigger.match_links, str);
|
|
|
|
});
|
2019-11-30 22:06:26 +02:00
|
|
|
if (match) return { ...match, trigger: "match_links" } as TextTriggerMatchResult;
|
2019-08-18 16:40:15 +03:00
|
|
|
}
|
|
|
|
|
2020-03-20 18:04:37 +01:00
|
|
|
if (trigger.match_attachment_type) {
|
|
|
|
const match = await this.matchMultipleTextTypesOnMessage(trigger.match_attachment_type, msg, str => {
|
|
|
|
return this.evaluateMatchAttachmentTypeTrigger(trigger.match_attachment_type, msg);
|
|
|
|
});
|
|
|
|
if (match) return { ...match, trigger: "match_attachment_type" } as TextTriggerMatchResult;
|
|
|
|
}
|
|
|
|
|
2019-10-11 01:59:56 +03:00
|
|
|
if (trigger.message_spam) {
|
|
|
|
const match = this.matchTextSpamTrigger(RecentActionType.Message, trigger.message_spam, msg);
|
2020-01-26 19:54:32 +02:00
|
|
|
if (match) return { ...match, rule, trigger: "message_spam" };
|
2019-08-18 16:40:15 +03:00
|
|
|
}
|
|
|
|
|
2019-10-11 01:59:56 +03:00
|
|
|
if (trigger.mention_spam) {
|
|
|
|
const match = this.matchTextSpamTrigger(RecentActionType.Mention, trigger.mention_spam, msg);
|
2020-01-26 19:54:32 +02:00
|
|
|
if (match) return { ...match, rule, trigger: "mention_spam" };
|
2019-08-18 16:40:15 +03:00
|
|
|
}
|
|
|
|
|
2019-10-11 01:59:56 +03:00
|
|
|
if (trigger.link_spam) {
|
|
|
|
const match = this.matchTextSpamTrigger(RecentActionType.Link, trigger.link_spam, msg);
|
2020-01-26 19:54:32 +02:00
|
|
|
if (match) return { ...match, rule, trigger: "link_spam" };
|
2019-08-18 16:40:15 +03:00
|
|
|
}
|
|
|
|
|
2019-10-11 01:59:56 +03:00
|
|
|
if (trigger.attachment_spam) {
|
|
|
|
const match = this.matchTextSpamTrigger(RecentActionType.Attachment, trigger.attachment_spam, msg);
|
2020-01-26 19:54:32 +02:00
|
|
|
if (match) return { ...match, rule, trigger: "attachment_spam" };
|
2019-08-18 16:40:15 +03:00
|
|
|
}
|
|
|
|
|
2019-10-11 01:59:56 +03:00
|
|
|
if (trigger.emoji_spam) {
|
|
|
|
const match = this.matchTextSpamTrigger(RecentActionType.Emoji, trigger.emoji_spam, msg);
|
2020-01-26 19:54:32 +02:00
|
|
|
if (match) return { ...match, rule, trigger: "emoji_spam" };
|
2019-08-18 16:40:15 +03:00
|
|
|
}
|
|
|
|
|
2019-10-11 01:59:56 +03:00
|
|
|
if (trigger.line_spam) {
|
|
|
|
const match = this.matchTextSpamTrigger(RecentActionType.Line, trigger.line_spam, msg);
|
2020-01-26 19:54:32 +02:00
|
|
|
if (match) return { ...match, rule, trigger: "line_spam" };
|
2019-08-18 16:40:15 +03:00
|
|
|
}
|
|
|
|
|
2019-10-11 01:59:56 +03:00
|
|
|
if (trigger.character_spam) {
|
|
|
|
const match = this.matchTextSpamTrigger(RecentActionType.Character, trigger.character_spam, msg);
|
2020-01-26 19:54:32 +02:00
|
|
|
if (match) return { ...match, rule, trigger: "character_spam" };
|
2019-08-18 16:40:15 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2020-01-26 19:54:32 +02:00
|
|
|
protected async matchOtherSpamInRule(rule: TRule, userId: string): Promise<OtherSpamTriggerMatchResult> {
|
|
|
|
if (!rule.enabled) return;
|
|
|
|
|
|
|
|
for (const trigger of rule.triggers) {
|
|
|
|
if (trigger.member_join_spam) {
|
|
|
|
const match = this.matchOtherSpamTrigger(RecentActionType.MemberJoin, trigger.member_join_spam, null);
|
|
|
|
if (match) return { ...match, rule, trigger: "member_join_spam" };
|
2019-10-11 01:59:56 +03:00
|
|
|
}
|
2020-01-26 19:54:32 +02:00
|
|
|
}
|
2019-10-11 01:59:56 +03:00
|
|
|
|
2020-01-26 19:54:32 +02:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
protected async matchMemberJoinTriggerInRule(rule: TRule, member: Member): Promise<OtherTriggerMatchResult> {
|
|
|
|
if (!rule.enabled) return;
|
|
|
|
|
|
|
|
const result: OtherTriggerMatchResult = { trigger: "member_join", type: "other", userId: member.id };
|
|
|
|
|
|
|
|
for (const trigger of rule.triggers) {
|
|
|
|
if (trigger.member_join) {
|
|
|
|
if (trigger.member_join.only_new) {
|
|
|
|
const threshold = Date.now() - convertDelayStringToMS(trigger.member_join.new_threshold);
|
|
|
|
if (member.createdAt >= threshold) {
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
}
|
2019-10-11 01:59:56 +03:00
|
|
|
}
|
|
|
|
|
2020-01-26 19:54:32 +02:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
protected async addRecentMessageAction(action: TextRecentAction) {
|
|
|
|
this.recentActions.push({
|
|
|
|
...action,
|
|
|
|
expiresAt: Date.now() + RECENT_ACTION_EXPIRY_TIME,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
protected async addRecentAction(action: OtherRecentAction) {
|
2019-10-11 01:59:56 +03:00
|
|
|
this.recentActions.push({
|
|
|
|
...action,
|
|
|
|
expiresAt: Date.now() + RECENT_ACTION_EXPIRY_TIME,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2019-08-18 16:40:15 +03:00
|
|
|
/**
|
|
|
|
* Logs recent actions for spam detection purposes
|
|
|
|
*/
|
2019-10-11 01:59:56 +03:00
|
|
|
protected async logRecentActionsForMessage(msg: SavedMessage) {
|
|
|
|
const timestamp = moment.utc(msg.posted_at).valueOf();
|
|
|
|
const globalIdentifier = msg.user_id;
|
|
|
|
const perChannelIdentifier = `${msg.channel_id}-${msg.user_id}`;
|
2020-01-26 19:54:32 +02:00
|
|
|
const messageInfo: MessageInfo = { channelId: msg.channel_id, messageId: msg.id, userId: msg.user_id };
|
2019-08-18 16:40:15 +03:00
|
|
|
|
2019-10-11 01:59:56 +03:00
|
|
|
this.addRecentMessageAction({
|
2019-08-18 16:40:15 +03:00
|
|
|
type: RecentActionType.Message,
|
|
|
|
identifier: globalIdentifier,
|
|
|
|
timestamp,
|
|
|
|
count: 1,
|
|
|
|
messageInfo,
|
|
|
|
});
|
2019-10-11 01:59:56 +03:00
|
|
|
this.addRecentMessageAction({
|
2019-08-18 16:40:15 +03:00
|
|
|
type: RecentActionType.Message,
|
|
|
|
identifier: perChannelIdentifier,
|
|
|
|
timestamp,
|
|
|
|
count: 1,
|
|
|
|
messageInfo,
|
|
|
|
});
|
|
|
|
|
2019-10-11 01:59:56 +03:00
|
|
|
const mentionCount =
|
|
|
|
getUserMentions(msg.data.content || "").length + getRoleMentions(msg.data.content || "").length;
|
2019-08-18 16:40:15 +03:00
|
|
|
if (mentionCount) {
|
2019-10-11 01:59:56 +03:00
|
|
|
this.addRecentMessageAction({
|
2019-08-18 16:40:15 +03:00
|
|
|
type: RecentActionType.Mention,
|
|
|
|
identifier: globalIdentifier,
|
|
|
|
timestamp,
|
|
|
|
count: mentionCount,
|
|
|
|
messageInfo,
|
|
|
|
});
|
2019-10-11 01:59:56 +03:00
|
|
|
this.addRecentMessageAction({
|
2019-08-18 16:40:15 +03:00
|
|
|
type: RecentActionType.Mention,
|
|
|
|
identifier: perChannelIdentifier,
|
|
|
|
timestamp,
|
|
|
|
count: mentionCount,
|
|
|
|
messageInfo,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2019-10-11 01:59:56 +03:00
|
|
|
const linkCount = getUrlsInString(msg.data.content || "").length;
|
2019-08-18 16:40:15 +03:00
|
|
|
if (linkCount) {
|
2019-10-11 01:59:56 +03:00
|
|
|
this.addRecentMessageAction({
|
2019-08-18 16:40:15 +03:00
|
|
|
type: RecentActionType.Link,
|
|
|
|
identifier: globalIdentifier,
|
|
|
|
timestamp,
|
|
|
|
count: linkCount,
|
|
|
|
messageInfo,
|
|
|
|
});
|
2019-10-11 01:59:56 +03:00
|
|
|
this.addRecentMessageAction({
|
2019-08-18 16:40:15 +03:00
|
|
|
type: RecentActionType.Link,
|
|
|
|
identifier: perChannelIdentifier,
|
|
|
|
timestamp,
|
|
|
|
count: linkCount,
|
|
|
|
messageInfo,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2019-10-11 01:59:56 +03:00
|
|
|
const attachmentCount = msg.data.attachments && msg.data.attachments.length;
|
2019-08-18 16:40:15 +03:00
|
|
|
if (attachmentCount) {
|
2019-10-11 01:59:56 +03:00
|
|
|
this.addRecentMessageAction({
|
2019-08-18 16:40:15 +03:00
|
|
|
type: RecentActionType.Attachment,
|
|
|
|
identifier: globalIdentifier,
|
|
|
|
timestamp,
|
|
|
|
count: attachmentCount,
|
|
|
|
messageInfo,
|
|
|
|
});
|
2019-10-11 01:59:56 +03:00
|
|
|
this.addRecentMessageAction({
|
2019-08-18 16:40:15 +03:00
|
|
|
type: RecentActionType.Attachment,
|
|
|
|
identifier: perChannelIdentifier,
|
|
|
|
timestamp,
|
|
|
|
count: attachmentCount,
|
|
|
|
messageInfo,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2019-10-11 01:59:56 +03:00
|
|
|
const emojiCount = getEmojiInString(msg.data.content || "").length;
|
2019-08-18 16:40:15 +03:00
|
|
|
if (emojiCount) {
|
2019-10-11 01:59:56 +03:00
|
|
|
this.addRecentMessageAction({
|
2019-08-18 16:40:15 +03:00
|
|
|
type: RecentActionType.Emoji,
|
|
|
|
identifier: globalIdentifier,
|
|
|
|
timestamp,
|
|
|
|
count: emojiCount,
|
|
|
|
messageInfo,
|
|
|
|
});
|
2019-10-11 01:59:56 +03:00
|
|
|
this.addRecentMessageAction({
|
2019-08-18 16:40:15 +03:00
|
|
|
type: RecentActionType.Emoji,
|
|
|
|
identifier: perChannelIdentifier,
|
|
|
|
timestamp,
|
|
|
|
count: emojiCount,
|
|
|
|
messageInfo,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
// + 1 is for the first line of the message (which doesn't have a line break)
|
2019-10-11 01:59:56 +03:00
|
|
|
const lineCount = msg.data.content ? (msg.data.content.match(/\n/g) || []).length + 1 : 0;
|
2019-08-18 16:40:15 +03:00
|
|
|
if (lineCount) {
|
2019-10-11 01:59:56 +03:00
|
|
|
this.addRecentMessageAction({
|
2019-08-18 16:40:15 +03:00
|
|
|
type: RecentActionType.Line,
|
|
|
|
identifier: globalIdentifier,
|
|
|
|
timestamp,
|
|
|
|
count: lineCount,
|
|
|
|
messageInfo,
|
|
|
|
});
|
2019-10-11 01:59:56 +03:00
|
|
|
this.addRecentMessageAction({
|
2019-08-18 16:40:15 +03:00
|
|
|
type: RecentActionType.Line,
|
|
|
|
identifier: perChannelIdentifier,
|
|
|
|
timestamp,
|
|
|
|
count: lineCount,
|
|
|
|
messageInfo,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2019-10-11 01:59:56 +03:00
|
|
|
const characterCount = [...(msg.data.content || "")].length;
|
2019-08-18 16:40:15 +03:00
|
|
|
if (characterCount) {
|
2019-10-11 01:59:56 +03:00
|
|
|
this.addRecentMessageAction({
|
2019-08-18 16:40:15 +03:00
|
|
|
type: RecentActionType.Character,
|
|
|
|
identifier: globalIdentifier,
|
|
|
|
timestamp,
|
|
|
|
count: characterCount,
|
|
|
|
messageInfo,
|
|
|
|
});
|
2019-10-11 01:59:56 +03:00
|
|
|
this.addRecentMessageAction({
|
2019-08-18 16:40:15 +03:00
|
|
|
type: RecentActionType.Character,
|
|
|
|
identifier: perChannelIdentifier,
|
|
|
|
timestamp,
|
|
|
|
count: characterCount,
|
|
|
|
messageInfo,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-01-26 19:54:32 +02:00
|
|
|
protected getMatchingRecentActions(type: RecentActionType, identifier: string | null, since: number) {
|
2019-08-18 16:40:15 +03:00
|
|
|
return this.recentActions.filter(action => {
|
2020-01-26 19:54:32 +02:00
|
|
|
return action.type === type && (!identifier || action.identifier === identifier) && action.timestamp >= since;
|
2019-08-18 16:40:15 +03:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2020-01-26 19:54:32 +02:00
|
|
|
protected async clearExpiredRecentSpam() {
|
|
|
|
for (const [key, info] of this.recentSpam.entries()) {
|
2019-10-11 01:59:56 +03:00
|
|
|
if (info.expiresAt <= Date.now()) {
|
2020-01-26 19:54:32 +02:00
|
|
|
this.recentSpam.delete(key);
|
2019-10-11 01:59:56 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
protected async clearOldRecentActions() {
|
|
|
|
this.recentActions = this.recentActions.filter(info => {
|
|
|
|
return info.expiresAt <= Date.now();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
protected async clearExpiredRecentNicknameChanges() {
|
|
|
|
for (const [key, info] of this.recentNicknameChanges.entries()) {
|
|
|
|
if (info.expiresAt <= Date.now()) {
|
|
|
|
this.recentNicknameChanges.delete(key);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-01-29 02:44:11 +02:00
|
|
|
protected readContactMethodsFromAction(action: {
|
|
|
|
notify?: string;
|
|
|
|
notifyChannel?: string;
|
|
|
|
}): UserNotificationMethod[] | null {
|
|
|
|
if (action.notify === "dm") {
|
|
|
|
return [{ type: "dm" }];
|
|
|
|
} else if (action.notify === "channel") {
|
|
|
|
if (!action.notifyChannel) {
|
|
|
|
throw new RecoverablePluginError(ERRORS.NO_USER_NOTIFICATION_CHANNEL);
|
|
|
|
}
|
|
|
|
|
|
|
|
const channel = this.guild.channels.get(action.notifyChannel);
|
|
|
|
if (!(channel instanceof TextChannel)) {
|
|
|
|
throw new RecoverablePluginError(ERRORS.INVALID_USER_NOTIFICATION_CHANNEL);
|
|
|
|
}
|
|
|
|
|
|
|
|
return [{ type: "channel", channel }];
|
|
|
|
} else if (action.notify && disableUserNotificationStrings.includes(action.notify)) {
|
|
|
|
return [];
|
|
|
|
}
|
|
|
|
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2019-11-30 22:06:26 +02:00
|
|
|
/**
|
|
|
|
* Apply the actions of the specified rule on the matched message/member
|
|
|
|
*/
|
2019-08-18 16:40:15 +03:00
|
|
|
protected async applyActionsOnMatch(rule: TRule, matchResult: AnyTriggerMatchResult) {
|
2019-11-30 18:07:25 +02:00
|
|
|
if (rule.cooldown && this.checkAndUpdateCooldown(rule, matchResult)) {
|
|
|
|
return;
|
2019-10-11 01:59:56 +03:00
|
|
|
}
|
|
|
|
|
2020-01-26 19:54:32 +02:00
|
|
|
const actionsTaken = [];
|
|
|
|
|
|
|
|
let recentSpamKey: string = null;
|
|
|
|
let recentSpam: RecentSpam = null;
|
|
|
|
let spamUserIdsToAction: string[] = [];
|
|
|
|
|
|
|
|
if (matchResult.type === "textspam" || matchResult.type === "otherspam") {
|
|
|
|
recentSpamKey = `${rule.name}-${matchResult.identifier}`;
|
|
|
|
recentSpam = this.recentSpam.get(recentSpamKey);
|
|
|
|
|
|
|
|
if (matchResult.type === "textspam") {
|
|
|
|
spamUserIdsToAction = matchResult.recentActions.map(action => action.messageInfo.userId);
|
|
|
|
} else if (matchResult.type === "otherspam") {
|
|
|
|
spamUserIdsToAction = matchResult.recentActions.map(action => action.userId);
|
|
|
|
}
|
|
|
|
|
|
|
|
spamUserIdsToAction = Array.from(new Set(spamUserIdsToAction)).filter(id => !recentSpam?.actionedUsers.has(id));
|
|
|
|
}
|
|
|
|
|
|
|
|
let archiveId = recentSpam?.archiveId;
|
|
|
|
if (matchResult.type === "textspam") {
|
|
|
|
const messageInfos = matchResult.recentActions.filter(unactioned).map(a => a.messageInfo);
|
|
|
|
if (messageInfos.length) {
|
|
|
|
const savedMessages = await this.savedMessages.getMultiple(messageInfos.map(info => info.messageId));
|
|
|
|
|
|
|
|
if (archiveId) {
|
|
|
|
await this.archives.addSavedMessagesToArchive(archiveId, savedMessages, this.guild);
|
|
|
|
} else {
|
|
|
|
archiveId = await this.archives.createFromSavedMessages(savedMessages, this.guild);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-01-28 22:16:19 +02:00
|
|
|
const matchSummary = await this.getMatchSummary(matchResult, archiveId);
|
2019-10-11 01:59:56 +03:00
|
|
|
|
2019-11-30 18:07:25 +02:00
|
|
|
let caseExtraNote = `Matched automod rule "${rule.name}"`;
|
2019-10-11 01:59:56 +03:00
|
|
|
if (matchSummary) {
|
|
|
|
caseExtraNote += `\n${matchSummary}`;
|
|
|
|
}
|
|
|
|
|
2019-08-18 16:40:15 +03:00
|
|
|
if (rule.actions.clean) {
|
2020-01-26 19:54:32 +02:00
|
|
|
const messagesToDelete: MessageInfo[] = [];
|
2019-10-11 01:59:56 +03:00
|
|
|
|
2019-08-18 16:40:15 +03:00
|
|
|
if (matchResult.type === "message" || matchResult.type === "embed") {
|
2019-10-11 01:59:56 +03:00
|
|
|
messagesToDelete.push(matchResult.messageInfo);
|
2020-01-26 19:54:32 +02:00
|
|
|
} else if (matchResult.type === "textspam") {
|
|
|
|
messagesToDelete.push(...matchResult.recentActions.filter(unactioned).map(a => a.messageInfo));
|
2019-10-11 01:59:56 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
for (const { channelId, messageId } of messagesToDelete) {
|
2020-01-26 19:54:32 +02:00
|
|
|
await this.guildLogs.ignoreLog(LogType.MESSAGE_DELETE, messageId);
|
2019-10-11 01:59:56 +03:00
|
|
|
await this.bot.deleteMessage(channelId, messageId).catch(noop);
|
2019-08-18 16:40:15 +03:00
|
|
|
}
|
2019-10-11 01:59:56 +03:00
|
|
|
|
|
|
|
actionsTaken.push("clean");
|
2019-08-18 16:40:15 +03:00
|
|
|
}
|
|
|
|
|
2019-08-22 01:23:19 +03:00
|
|
|
if (rule.actions.warn) {
|
2019-10-11 01:59:56 +03:00
|
|
|
const reason = rule.actions.warn.reason || "Warned automatically";
|
2020-01-29 02:44:11 +02:00
|
|
|
const contactMethods = this.readContactMethodsFromAction(rule.actions.warn);
|
2019-10-11 01:59:56 +03:00
|
|
|
|
2019-08-22 01:23:19 +03:00
|
|
|
const caseArgs = {
|
|
|
|
modId: this.bot.user.id,
|
2019-10-11 01:59:56 +03:00
|
|
|
extraNotes: [caseExtraNote],
|
2019-08-22 01:23:19 +03:00
|
|
|
};
|
|
|
|
|
2020-01-26 19:54:32 +02:00
|
|
|
let membersToWarn = [];
|
2020-04-08 21:05:46 +03:00
|
|
|
if (
|
|
|
|
matchResult.type === "message" ||
|
|
|
|
matchResult.type === "embed" ||
|
|
|
|
matchResult.type === "other" ||
|
|
|
|
matchResult.type === "username" ||
|
|
|
|
matchResult.type === "nickname" ||
|
|
|
|
matchResult.type === "customstatus"
|
|
|
|
) {
|
2020-01-26 19:54:32 +02:00
|
|
|
membersToWarn = [await this.getMember(matchResult.userId)];
|
|
|
|
} else if (matchResult.type === "textspam" || matchResult.type === "otherspam") {
|
|
|
|
for (const id of spamUserIdsToAction) {
|
|
|
|
membersToWarn.push(await this.getMember(id));
|
2019-08-22 01:23:19 +03:00
|
|
|
}
|
|
|
|
}
|
2019-10-11 01:59:56 +03:00
|
|
|
|
2020-01-26 19:54:32 +02:00
|
|
|
if (membersToWarn.length) {
|
|
|
|
for (const member of membersToWarn) {
|
2020-01-29 02:44:11 +02:00
|
|
|
await this.getModActions().warnMember(member, reason, { contactMethods, caseArgs });
|
2020-01-26 19:54:32 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
actionsTaken.push("warn");
|
|
|
|
}
|
2019-08-22 01:23:19 +03:00
|
|
|
}
|
|
|
|
|
2019-08-18 16:40:15 +03:00
|
|
|
if (rule.actions.mute) {
|
|
|
|
const duration = rule.actions.mute.duration ? convertDelayStringToMS(rule.actions.mute.duration) : null;
|
|
|
|
const reason = rule.actions.mute.reason || "Muted automatically";
|
|
|
|
const caseArgs = {
|
|
|
|
modId: this.bot.user.id,
|
2019-10-11 01:59:56 +03:00
|
|
|
extraNotes: [caseExtraNote],
|
2019-08-18 16:40:15 +03:00
|
|
|
};
|
2020-01-29 02:51:07 +02:00
|
|
|
const contactMethods = this.readContactMethodsFromAction(rule.actions.mute);
|
2019-08-18 16:40:15 +03:00
|
|
|
|
2020-01-26 19:54:32 +02:00
|
|
|
let userIdsToMute = [];
|
2020-04-08 21:05:46 +03:00
|
|
|
if (
|
|
|
|
matchResult.type === "message" ||
|
|
|
|
matchResult.type === "embed" ||
|
|
|
|
matchResult.type === "other" ||
|
|
|
|
matchResult.type === "username" ||
|
|
|
|
matchResult.type === "nickname" ||
|
|
|
|
matchResult.type === "customstatus"
|
|
|
|
) {
|
2020-01-26 19:54:32 +02:00
|
|
|
userIdsToMute = [matchResult.userId];
|
|
|
|
} else if (matchResult.type === "textspam" || matchResult.type === "otherspam") {
|
|
|
|
userIdsToMute.push(...spamUserIdsToAction);
|
2019-08-18 16:40:15 +03:00
|
|
|
}
|
2019-10-11 01:59:56 +03:00
|
|
|
|
2020-01-26 19:54:32 +02:00
|
|
|
if (userIdsToMute.length) {
|
2020-04-03 16:40:05 +03:00
|
|
|
for (const userId of userIdsToMute) {
|
|
|
|
await this.getMutes().muteUser(userId, duration, reason, { contactMethods, caseArgs });
|
2020-01-26 19:54:32 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
actionsTaken.push("mute");
|
|
|
|
}
|
2019-08-18 16:40:15 +03:00
|
|
|
}
|
|
|
|
|
2019-08-22 01:23:19 +03:00
|
|
|
if (rule.actions.kick) {
|
|
|
|
const reason = rule.actions.kick.reason || "Kicked automatically";
|
|
|
|
const caseArgs = {
|
|
|
|
modId: this.bot.user.id,
|
2019-10-11 01:59:56 +03:00
|
|
|
extraNotes: [caseExtraNote],
|
2019-08-22 01:23:19 +03:00
|
|
|
};
|
2020-01-29 02:51:07 +02:00
|
|
|
const contactMethods = this.readContactMethodsFromAction(rule.actions.kick);
|
2019-08-22 01:23:19 +03:00
|
|
|
|
2020-01-26 19:54:32 +02:00
|
|
|
let membersToKick = [];
|
2020-04-08 21:05:46 +03:00
|
|
|
if (
|
|
|
|
matchResult.type === "message" ||
|
|
|
|
matchResult.type === "embed" ||
|
|
|
|
matchResult.type === "other" ||
|
|
|
|
matchResult.type === "username" ||
|
|
|
|
matchResult.type === "nickname" ||
|
|
|
|
matchResult.type === "customstatus"
|
|
|
|
) {
|
2020-01-26 19:54:32 +02:00
|
|
|
membersToKick = [await this.getMember(matchResult.userId)];
|
|
|
|
} else if (matchResult.type === "textspam" || matchResult.type === "otherspam") {
|
|
|
|
for (const id of spamUserIdsToAction) {
|
|
|
|
membersToKick.push(await this.getMember(id));
|
2019-08-22 01:23:19 +03:00
|
|
|
}
|
|
|
|
}
|
2019-10-11 01:59:56 +03:00
|
|
|
|
2020-01-26 19:54:32 +02:00
|
|
|
if (membersToKick.length) {
|
|
|
|
for (const member of membersToKick) {
|
2020-01-29 02:44:11 +02:00
|
|
|
await this.getModActions().kickMember(member, reason, { contactMethods, caseArgs });
|
2020-01-26 19:54:32 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
actionsTaken.push("kick");
|
|
|
|
}
|
2019-08-22 01:23:19 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
if (rule.actions.ban) {
|
|
|
|
const reason = rule.actions.ban.reason || "Banned automatically";
|
|
|
|
const caseArgs = {
|
|
|
|
modId: this.bot.user.id,
|
2019-10-11 01:59:56 +03:00
|
|
|
extraNotes: [caseExtraNote],
|
2019-08-22 01:23:19 +03:00
|
|
|
};
|
2020-01-29 02:51:07 +02:00
|
|
|
const contactMethods = this.readContactMethodsFromAction(rule.actions.ban);
|
2020-04-11 16:27:58 +03:00
|
|
|
const deleteMessageDays = rule.actions.ban.deleteMessageDays;
|
2019-08-22 01:23:19 +03:00
|
|
|
|
2020-01-26 19:54:32 +02:00
|
|
|
let userIdsToBan = [];
|
2020-04-08 21:05:46 +03:00
|
|
|
if (
|
|
|
|
matchResult.type === "message" ||
|
|
|
|
matchResult.type === "embed" ||
|
|
|
|
matchResult.type === "other" ||
|
|
|
|
matchResult.type === "username" ||
|
|
|
|
matchResult.type === "nickname" ||
|
|
|
|
matchResult.type === "customstatus"
|
|
|
|
) {
|
2020-01-26 19:54:32 +02:00
|
|
|
userIdsToBan = [matchResult.userId];
|
|
|
|
} else if (matchResult.type === "textspam" || matchResult.type === "otherspam") {
|
|
|
|
userIdsToBan.push(...spamUserIdsToAction);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (userIdsToBan.length) {
|
|
|
|
for (const userId of userIdsToBan) {
|
2020-04-11 16:27:58 +03:00
|
|
|
await this.getModActions().banUserId(userId, reason, {
|
|
|
|
contactMethods,
|
|
|
|
caseArgs,
|
|
|
|
deleteMessageDays,
|
|
|
|
});
|
2019-08-22 01:23:19 +03:00
|
|
|
}
|
2019-10-11 01:59:56 +03:00
|
|
|
|
2020-01-26 19:54:32 +02:00
|
|
|
actionsTaken.push("ban");
|
|
|
|
}
|
2019-08-22 01:23:19 +03:00
|
|
|
}
|
|
|
|
|
2019-10-11 01:59:56 +03:00
|
|
|
if (rule.actions.change_nickname) {
|
2020-01-26 19:54:32 +02:00
|
|
|
const userIdsToChange =
|
|
|
|
matchResult.type === "textspam" || matchResult.type === "otherspam"
|
|
|
|
? [...spamUserIdsToAction]
|
|
|
|
: [matchResult.userId];
|
2019-10-11 01:59:56 +03:00
|
|
|
|
|
|
|
for (const userId of userIdsToChange) {
|
|
|
|
if (this.recentNicknameChanges.has(userId)) continue;
|
|
|
|
this.guild
|
|
|
|
.editMember(userId, {
|
|
|
|
nick: rule.actions.change_nickname.name,
|
|
|
|
})
|
|
|
|
.catch(() => {
|
|
|
|
this.getLogs().log(LogType.BOT_ALERT, {
|
|
|
|
body: `Failed to change the nickname of \`${userId}\``,
|
|
|
|
});
|
|
|
|
});
|
|
|
|
this.recentNicknameChanges.set(userId, { expiresAt: RECENT_NICKNAME_CHANGE_EXPIRY_TIME });
|
|
|
|
}
|
|
|
|
|
|
|
|
actionsTaken.push("nickname");
|
|
|
|
}
|
|
|
|
|
2019-11-30 16:18:29 +02:00
|
|
|
if (rule.actions.add_roles) {
|
2020-01-26 19:54:32 +02:00
|
|
|
const userIdsToChange =
|
|
|
|
matchResult.type === "textspam" || matchResult.type === "otherspam"
|
|
|
|
? [...spamUserIdsToAction]
|
|
|
|
: [matchResult.userId];
|
|
|
|
|
2019-11-30 16:18:29 +02:00
|
|
|
for (const userId of userIdsToChange) {
|
|
|
|
const member = await this.getMember(userId);
|
|
|
|
if (!member) continue;
|
|
|
|
|
|
|
|
const memberRoles = new Set(member.roles);
|
2019-11-30 18:07:25 +02:00
|
|
|
for (const roleId of rule.actions.add_roles) {
|
2019-11-30 16:18:29 +02:00
|
|
|
memberRoles.add(roleId);
|
|
|
|
}
|
|
|
|
|
2019-11-30 18:07:25 +02:00
|
|
|
if (memberRoles.size === member.roles.length) {
|
|
|
|
// No role changes
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2019-11-30 16:18:29 +02:00
|
|
|
const rolesArr = Array.from(memberRoles.values());
|
|
|
|
await member.edit({
|
|
|
|
roles: rolesArr,
|
|
|
|
});
|
|
|
|
member.roles = rolesArr; // Make sure we know of the new roles internally as well
|
|
|
|
}
|
2019-11-30 18:07:25 +02:00
|
|
|
|
|
|
|
actionsTaken.push("add roles");
|
2019-11-30 16:18:29 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if (rule.actions.remove_roles) {
|
2020-01-26 19:54:32 +02:00
|
|
|
const userIdsToChange =
|
|
|
|
matchResult.type === "textspam" || matchResult.type === "otherspam"
|
|
|
|
? [...spamUserIdsToAction]
|
|
|
|
: [matchResult.userId];
|
|
|
|
|
2019-11-30 16:18:29 +02:00
|
|
|
for (const userId of userIdsToChange) {
|
|
|
|
const member = await this.getMember(userId);
|
|
|
|
if (!member) continue;
|
|
|
|
|
|
|
|
const memberRoles = new Set(member.roles);
|
2019-11-30 18:07:25 +02:00
|
|
|
for (const roleId of rule.actions.remove_roles) {
|
2019-11-30 16:18:29 +02:00
|
|
|
memberRoles.delete(roleId);
|
|
|
|
}
|
|
|
|
|
2019-11-30 18:07:25 +02:00
|
|
|
if (memberRoles.size === member.roles.length) {
|
|
|
|
// No role changes
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2019-11-30 16:18:29 +02:00
|
|
|
const rolesArr = Array.from(memberRoles.values());
|
|
|
|
await member.edit({
|
|
|
|
roles: rolesArr,
|
|
|
|
});
|
|
|
|
member.roles = rolesArr; // Make sure we know of the new roles internally as well
|
|
|
|
}
|
2019-11-30 18:07:25 +02:00
|
|
|
|
|
|
|
actionsTaken.push("remove roles");
|
2019-11-30 16:18:29 +02:00
|
|
|
}
|
|
|
|
|
2020-01-26 19:54:32 +02:00
|
|
|
if (rule.actions.set_antiraid_level !== undefined) {
|
|
|
|
await this.setAntiraidLevel(rule.actions.set_antiraid_level);
|
|
|
|
actionsTaken.push("set antiraid level");
|
|
|
|
}
|
|
|
|
|
2020-04-08 17:15:13 +03:00
|
|
|
if (rule.actions.reply && matchResult.type === "message") {
|
|
|
|
const channelId = matchResult.messageInfo.channelId;
|
|
|
|
const channel = this.guild.channels.get(channelId);
|
|
|
|
if (channel && channel instanceof TextChannel) {
|
|
|
|
const user = await this.resolveUser(matchResult.userId);
|
|
|
|
const formatted = await renderTemplate(rule.actions.reply, {
|
|
|
|
user: stripObjectToScalars(user),
|
|
|
|
});
|
|
|
|
if (formatted) {
|
|
|
|
await channel.createMessage(formatted);
|
|
|
|
actionsTaken.push("reply");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-01-26 19:54:32 +02:00
|
|
|
if (matchResult.type === "textspam" || matchResult.type === "otherspam") {
|
|
|
|
for (const action of matchResult.recentActions) {
|
|
|
|
action.actioned = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (recentSpam) {
|
|
|
|
for (const id of spamUserIdsToAction) {
|
|
|
|
recentSpam.actionedUsers.add(id);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
const newRecentSpamEntry: RecentSpam = {
|
|
|
|
actionedUsers: new Set(spamUserIdsToAction),
|
|
|
|
expiresAt: Date.now() + RECENT_SPAM_EXPIRY_TIME,
|
|
|
|
archiveId,
|
|
|
|
};
|
|
|
|
this.recentSpam.set(recentSpamKey, newRecentSpamEntry);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-10-11 23:16:15 +03:00
|
|
|
// Don't wait for the rest before continuing to other automod items in the queue
|
|
|
|
(async () => {
|
2020-01-26 19:54:32 +02:00
|
|
|
let user;
|
|
|
|
let users;
|
|
|
|
let safeUser;
|
|
|
|
let safeUsers;
|
|
|
|
|
|
|
|
if (matchResult.type === "textspam" || matchResult.type === "otherspam") {
|
|
|
|
users = spamUserIdsToAction.map(id => this.getUser(id));
|
|
|
|
} else {
|
|
|
|
user = this.getUser(matchResult.userId);
|
|
|
|
users = [user];
|
|
|
|
}
|
|
|
|
|
|
|
|
safeUser = user ? stripObjectToScalars(user) : null;
|
|
|
|
safeUsers = users.map(u => stripObjectToScalars(u));
|
2019-10-11 01:59:56 +03:00
|
|
|
|
2019-11-30 18:07:25 +02:00
|
|
|
const logData = {
|
|
|
|
rule: rule.name,
|
|
|
|
user: safeUser,
|
|
|
|
users: safeUsers,
|
|
|
|
actionsTaken: actionsTaken.length ? actionsTaken.join(", ") : "<none>",
|
|
|
|
matchSummary,
|
|
|
|
};
|
2020-01-26 19:54:32 +02:00
|
|
|
|
|
|
|
if (recentSpam && !spamUserIdsToAction.length) {
|
|
|
|
// This action was part of a recent spam match and we didn't find any new users to action i.e. the only users
|
|
|
|
// who triggered this match had already been actioned. In that case, we don't need to post any new log messages.
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2019-11-30 18:07:25 +02:00
|
|
|
const logMessage = this.getLogs().getLogMessage(LogType.AUTOMOD_ACTION, logData);
|
|
|
|
|
2019-10-11 01:59:56 +03:00
|
|
|
if (rule.actions.alert) {
|
2019-10-11 23:16:15 +03:00
|
|
|
const channel = this.guild.channels.get(rule.actions.alert.channel);
|
|
|
|
if (channel && channel instanceof TextChannel) {
|
|
|
|
const text = rule.actions.alert.text;
|
|
|
|
const rendered = await renderTemplate(rule.actions.alert.text, {
|
|
|
|
rule: rule.name,
|
|
|
|
user: safeUser,
|
|
|
|
users: safeUsers,
|
|
|
|
text,
|
|
|
|
matchSummary,
|
2019-11-30 18:07:25 +02:00
|
|
|
logMessage,
|
2019-10-11 23:16:15 +03:00
|
|
|
});
|
|
|
|
channel.createMessage(rendered);
|
|
|
|
actionsTaken.push("alert");
|
|
|
|
} else {
|
|
|
|
this.getLogs().log(LogType.BOT_ALERT, {
|
2019-11-02 22:11:26 +02:00
|
|
|
body: `Invalid channel id \`${rule.actions.alert.channel}\` for alert action in automod rule **${rule.name}**`,
|
2019-10-11 23:16:15 +03:00
|
|
|
});
|
|
|
|
}
|
2019-10-11 01:59:56 +03:00
|
|
|
}
|
|
|
|
|
2019-10-11 23:16:15 +03:00
|
|
|
if (rule.actions.log) {
|
2019-11-30 18:07:25 +02:00
|
|
|
this.getLogs().log(LogType.AUTOMOD_ACTION, logData);
|
2019-10-11 01:59:56 +03:00
|
|
|
}
|
2019-10-11 23:16:15 +03:00
|
|
|
})();
|
2019-08-18 16:40:15 +03:00
|
|
|
}
|
|
|
|
|
2019-11-30 18:07:25 +02:00
|
|
|
/**
|
2019-11-30 22:06:26 +02:00
|
|
|
* Check if the rule's on cooldown and bump its usage count towards the cooldown up
|
2019-11-30 18:07:25 +02:00
|
|
|
* @return Whether the rule's on cooldown
|
|
|
|
*/
|
|
|
|
protected checkAndUpdateCooldown(rule: TRule, matchResult: AnyTriggerMatchResult): boolean {
|
|
|
|
let cooldownKey = rule.name + "-";
|
|
|
|
|
2020-01-26 19:54:32 +02:00
|
|
|
if (matchResult.type === "textspam" || matchResult.type === "otherspam") {
|
|
|
|
logger.warn("Spam cooldowns are WIP and not currently functional");
|
|
|
|
}
|
|
|
|
|
|
|
|
if (matchResult.type === "message" || matchResult.type === "embed") {
|
2019-11-30 18:07:25 +02:00
|
|
|
cooldownKey += matchResult.userId;
|
|
|
|
} else if (
|
|
|
|
matchResult.type === "username" ||
|
|
|
|
matchResult.type === "nickname" ||
|
|
|
|
matchResult.type === "visiblename" ||
|
|
|
|
matchResult.type === "customstatus"
|
|
|
|
) {
|
|
|
|
cooldownKey += matchResult.userId;
|
|
|
|
} else {
|
|
|
|
cooldownKey = null;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (cooldownKey) {
|
|
|
|
if (this.cooldownManager.isOnCooldown(cooldownKey)) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
const cooldownTime = convertDelayStringToMS(rule.cooldown, "s");
|
|
|
|
if (cooldownTime) {
|
|
|
|
this.cooldownManager.setCooldown(cooldownKey, cooldownTime);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2019-11-30 22:06:26 +02:00
|
|
|
/**
|
|
|
|
* Returns a text summary for the match result for use in logs/alerts
|
|
|
|
*/
|
2020-01-26 19:54:32 +02:00
|
|
|
protected async getMatchSummary(matchResult: AnyTriggerMatchResult, archiveId: string = null): Promise<string> {
|
2019-11-30 18:07:25 +02:00
|
|
|
if (matchResult.type === "message" || matchResult.type === "embed") {
|
|
|
|
const message = await this.savedMessages.find(matchResult.messageInfo.messageId);
|
|
|
|
const channel = this.guild.channels.get(matchResult.messageInfo.channelId);
|
|
|
|
const channelMention = channel ? verboseChannelMention(channel) : `\`#${message.channel_id}\``;
|
|
|
|
|
2020-04-03 16:54:57 +03:00
|
|
|
return (
|
|
|
|
trimPluginDescription(`
|
2019-11-30 22:06:26 +02:00
|
|
|
Matched ${this.getMatchedValueText(matchResult)} in message in ${channelMention}:
|
2020-04-03 16:54:57 +03:00
|
|
|
`) +
|
|
|
|
"\n" +
|
|
|
|
messageSummary(message)
|
|
|
|
);
|
2020-01-26 19:54:32 +02:00
|
|
|
} else if (matchResult.type === "textspam") {
|
2019-11-30 18:07:25 +02:00
|
|
|
const baseUrl = this.knub.getGlobalConfig().url;
|
|
|
|
const archiveUrl = this.archives.getUrl(baseUrl, archiveId);
|
|
|
|
|
|
|
|
return trimPluginDescription(`
|
|
|
|
Matched spam: ${disableLinkPreviews(archiveUrl)}
|
|
|
|
`);
|
|
|
|
} else if (matchResult.type === "username") {
|
2019-11-30 22:06:26 +02:00
|
|
|
return `Matched ${this.getMatchedValueText(matchResult)} in username: ${matchResult.str}`;
|
2019-11-30 18:07:25 +02:00
|
|
|
} else if (matchResult.type === "nickname") {
|
2019-11-30 22:06:26 +02:00
|
|
|
return `Matched ${this.getMatchedValueText(matchResult)} in nickname: ${matchResult.str}`;
|
2019-11-30 18:07:25 +02:00
|
|
|
} else if (matchResult.type === "visiblename") {
|
2019-11-30 22:06:26 +02:00
|
|
|
return `Matched ${this.getMatchedValueText(matchResult)} in visible name: ${matchResult.str}`;
|
2019-11-30 18:07:25 +02:00
|
|
|
} else if (matchResult.type === "customstatus") {
|
2019-11-30 22:06:26 +02:00
|
|
|
return `Matched ${this.getMatchedValueText(matchResult)} in custom status: ${matchResult.str}`;
|
2020-01-26 19:54:32 +02:00
|
|
|
} else if (matchResult.type === "otherspam") {
|
|
|
|
return `Matched other spam`;
|
2019-11-30 22:06:26 +02:00
|
|
|
}
|
2020-01-26 19:54:32 +02:00
|
|
|
|
|
|
|
return "";
|
2019-11-30 22:06:26 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns a formatted version of the matched value (word, regex pattern, link, etc.) for use in the match summary
|
|
|
|
*/
|
|
|
|
protected getMatchedValueText(matchResult: TextTriggerMatchResult): string | null {
|
|
|
|
if (matchResult.trigger === "match_words") {
|
|
|
|
return `word \`${disableInlineCode(matchResult.matchedValue)}\``;
|
|
|
|
} else if (matchResult.trigger === "match_regex") {
|
|
|
|
return `regex \`${disableInlineCode(matchResult.matchedValue)}\``;
|
|
|
|
} else if (matchResult.trigger === "match_invites") {
|
|
|
|
return `invite code \`${disableInlineCode(matchResult.matchedValue)}\``;
|
|
|
|
} else if (matchResult.trigger === "match_links") {
|
|
|
|
return `link \`${disableInlineCode(matchResult.matchedValue)}\``;
|
2020-03-20 18:04:37 +01:00
|
|
|
} else if (matchResult.trigger === "match_attachment_type") {
|
|
|
|
return `attachment type \`${disableInlineCode(matchResult.matchedValue)}\``;
|
2019-11-30 18:07:25 +02:00
|
|
|
}
|
2019-11-30 22:06:26 +02:00
|
|
|
|
|
|
|
return typeof matchResult.matchedValue === "string" ? `\`${disableInlineCode(matchResult.matchedValue)}\`` : null;
|
2019-11-30 18:07:25 +02:00
|
|
|
}
|
|
|
|
|
2019-11-30 22:06:26 +02:00
|
|
|
/**
|
|
|
|
* Run automod actions on new messages
|
|
|
|
*/
|
2019-10-11 01:59:56 +03:00
|
|
|
protected onMessageCreate(msg: SavedMessage) {
|
|
|
|
if (msg.is_bot) return;
|
|
|
|
|
2019-08-18 16:40:15 +03:00
|
|
|
this.automodQueue.add(async () => {
|
|
|
|
if (this.unloaded) return;
|
|
|
|
|
|
|
|
await this.logRecentActionsForMessage(msg);
|
|
|
|
|
2019-10-11 01:59:56 +03:00
|
|
|
const member = this.guild.members.get(msg.user_id);
|
|
|
|
const config = this.getMatchingConfig({
|
|
|
|
member,
|
|
|
|
userId: msg.user_id,
|
|
|
|
channelId: msg.channel_id,
|
|
|
|
});
|
2019-08-18 16:40:15 +03:00
|
|
|
for (const [name, rule] of Object.entries(config.rules)) {
|
|
|
|
const matchResult = await this.matchRuleToMessage(rule, msg);
|
|
|
|
if (matchResult) {
|
2020-04-10 21:27:31 +03:00
|
|
|
// Make sure the message still exists in our database when we try to apply actions on it.
|
|
|
|
// In high stress situations, such as raids, detection can be delayed and automod might try to act on messages
|
|
|
|
// we no longer have in our database, i.e. messages deleted over 5 minutes go. Since this is an edge case and
|
|
|
|
// undefined behaviour, don't apply actions on that message.
|
|
|
|
const savedMsg = await this.savedMessages.find(msg.id);
|
|
|
|
if (!savedMsg) return;
|
|
|
|
|
2019-08-18 16:40:15 +03:00
|
|
|
await this.applyActionsOnMatch(rule, matchResult);
|
2019-11-27 20:30:50 +02:00
|
|
|
break; // Don't apply multiple rules to the same message
|
2019-08-18 16:40:15 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
2020-01-26 19:54:32 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* When a new member joins, check for both join triggers and join spam triggers
|
|
|
|
*/
|
|
|
|
@d.event("guildMemberAdd")
|
|
|
|
protected onMemberJoin(_, member: Member) {
|
|
|
|
if (member.user.bot) return;
|
|
|
|
|
|
|
|
this.automodQueue.add(async () => {
|
|
|
|
if (this.unloaded) return;
|
|
|
|
|
|
|
|
await this.addRecentAction({
|
|
|
|
identifier: RAID_SPAM_IDENTIFIER,
|
|
|
|
type: RecentActionType.MemberJoin,
|
|
|
|
userId: member.id,
|
|
|
|
timestamp: Date.now(),
|
|
|
|
count: 1,
|
|
|
|
});
|
|
|
|
|
|
|
|
const config = this.getConfigForMember(member);
|
|
|
|
|
|
|
|
for (const [name, rule] of Object.entries(config.rules)) {
|
|
|
|
const spamMatch = await this.matchOtherSpamInRule(rule, member.id);
|
|
|
|
if (spamMatch) {
|
|
|
|
await this.applyActionsOnMatch(rule, spamMatch);
|
|
|
|
}
|
|
|
|
|
|
|
|
const joinMatch = await this.matchMemberJoinTriggerInRule(rule, member);
|
|
|
|
if (joinMatch) {
|
|
|
|
await this.applyActionsOnMatch(rule, joinMatch);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
protected async setAntiraidLevel(level: string | null, user?: User) {
|
2020-01-27 22:07:46 +02:00
|
|
|
this.cachedAntiraidLevel = level;
|
|
|
|
await this.antiraidLevels.set(level);
|
2020-01-26 19:54:32 +02:00
|
|
|
|
|
|
|
if (user) {
|
|
|
|
this.guildLogs.log(LogType.SET_ANTIRAID_USER, {
|
|
|
|
level: level ?? "off",
|
|
|
|
user: stripObjectToScalars(user),
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
this.guildLogs.log(LogType.SET_ANTIRAID_AUTO, {
|
|
|
|
level: level ?? "off",
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@d.command("antiraid clear", [], {
|
|
|
|
aliases: ["antiraid reset", "antiraid none", "antiraid off"],
|
|
|
|
})
|
|
|
|
@d.permission("can_set_antiraid")
|
|
|
|
public async clearAntiraidCmd(msg: Message) {
|
|
|
|
await this.setAntiraidLevel(null, msg.author);
|
2020-01-28 22:16:37 +02:00
|
|
|
this.sendSuccessMessage(msg.channel, "Anti-raid turned **off**");
|
2020-01-26 19:54:32 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
@d.command("antiraid", "<level:string>")
|
|
|
|
@d.permission("can_set_antiraid")
|
|
|
|
public async setAntiraidCmd(msg: Message, args: { level: string }) {
|
|
|
|
if (!this.getConfig().antiraid_levels.includes(args.level)) {
|
|
|
|
this.sendErrorMessage(msg.channel, "Unknown anti-raid level");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
await this.setAntiraidLevel(args.level, msg.author);
|
|
|
|
this.sendSuccessMessage(msg.channel, `Anti-raid set to **${args.level}**`);
|
|
|
|
}
|
|
|
|
|
|
|
|
@d.command("antiraid")
|
|
|
|
@d.permission("can_view_antiraid")
|
|
|
|
public async viewAntiraidCmd(msg: Message, args: { level: string }) {
|
|
|
|
if (this.cachedAntiraidLevel) {
|
|
|
|
msg.channel.createMessage(`Anti-raid is set to **${this.cachedAntiraidLevel}**`);
|
|
|
|
} else {
|
|
|
|
msg.channel.createMessage("Anti-raid is off!");
|
|
|
|
}
|
|
|
|
}
|
2019-08-18 16:40:15 +03:00
|
|
|
}
|