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

automod: better formatting for matched content in log messages

This commit is contained in:
Dragory 2019-11-30 22:06:26 +02:00
parent 48adb1df90
commit 42df230e71

View file

@ -2,7 +2,6 @@ import { PluginInfo, trimPluginDescription, ZeppelinPlugin } from "./ZeppelinPlu
import * as t from "io-ts"; import * as t from "io-ts";
import { import {
convertDelayStringToMS, convertDelayStringToMS,
disableCodeBlocks,
disableInlineCode, disableInlineCode,
disableLinkPreviews, disableLinkPreviews,
getEmojiInString, getEmojiInString,
@ -21,7 +20,7 @@ import {
verboseChannelMention, verboseChannelMention,
} from "../utils"; } from "../utils";
import { configUtils, CooldownManager } from "knub"; import { configUtils, CooldownManager } from "knub";
import { Invite, Member, TextChannel } from "eris"; import { Member, TextChannel } from "eris";
import escapeStringRegexp from "escape-string-regexp"; import escapeStringRegexp from "escape-string-regexp";
import { SimpleCache } from "../SimpleCache"; import { SimpleCache } from "../SimpleCache";
import { Queue } from "../Queue"; import { Queue } from "../Queue";
@ -51,25 +50,26 @@ type TextTriggerWithMultipleMatchTypes = {
}; };
interface TriggerMatchResult { interface TriggerMatchResult {
trigger: string;
type: string; type: string;
} }
interface MessageTextTriggerMatchResult extends TriggerMatchResult { interface MessageTextTriggerMatchResult<T = any> extends TriggerMatchResult {
type: "message" | "embed"; type: "message" | "embed";
str: string; str: string;
userId: string; userId: string;
messageInfo: MessageInfo; messageInfo: MessageInfo;
matchedContent?: string; matchedValue: T;
} }
interface OtherTextTriggerMatchResult extends TriggerMatchResult { interface OtherTextTriggerMatchResult<T = any> extends TriggerMatchResult {
type: "username" | "nickname" | "visiblename" | "customstatus"; type: "username" | "nickname" | "visiblename" | "customstatus";
str: string; str: string;
userId: string; userId: string;
matchedContent?: string; matchedValue: T;
} }
type TextTriggerMatchResult = MessageTextTriggerMatchResult | OtherTextTriggerMatchResult; type TextTriggerMatchResult<T = any> = MessageTextTriggerMatchResult<T> | OtherTextTriggerMatchResult<T>;
interface TextSpamTriggerMatchResult extends TriggerMatchResult { interface TextSpamTriggerMatchResult extends TriggerMatchResult {
type: "textspam"; type: "textspam";
@ -118,8 +118,7 @@ const MatchWordsTrigger = t.type({
match_custom_status: t.boolean, match_custom_status: t.boolean,
}); });
type TMatchWordsTrigger = t.TypeOf<typeof MatchWordsTrigger>; type TMatchWordsTrigger = t.TypeOf<typeof MatchWordsTrigger>;
const defaultMatchWordsTrigger: TMatchWordsTrigger = { const defaultMatchWordsTrigger: Partial<TMatchWordsTrigger> = {
words: [],
case_sensitive: false, case_sensitive: false,
only_full_words: true, only_full_words: true,
normalize: false, normalize: false,
@ -732,7 +731,7 @@ export class AutomodPlugin extends ZeppelinPlugin<TConfigSchema> {
recentActionType: RecentActionType, recentActionType: RecentActionType,
trigger: TBaseTextSpamTrigger, trigger: TBaseTextSpamTrigger,
msg: SavedMessage, msg: SavedMessage,
): TextSpamTriggerMatchResult { ): Partial<TextSpamTriggerMatchResult> {
const since = moment.utc(msg.posted_at).valueOf() - convertDelayStringToMS(trigger.within); const since = moment.utc(msg.posted_at).valueOf() - convertDelayStringToMS(trigger.within);
const recentActions = trigger.per_channel const recentActions = trigger.per_channel
? this.getMatchingRecentActions(recentActionType, `${msg.channel_id}-${msg.user_id}`, since) ? this.getMatchingRecentActions(recentActionType, `${msg.channel_id}-${msg.user_id}`, since)
@ -754,69 +753,85 @@ export class AutomodPlugin extends ZeppelinPlugin<TConfigSchema> {
return null; return null;
} }
protected async matchMultipleTextTypesOnMessage( protected async matchMultipleTextTypesOnMessage<T>(
trigger: TextTriggerWithMultipleMatchTypes, trigger: TextTriggerWithMultipleMatchTypes,
msg: SavedMessage, msg: SavedMessage,
cb, matchFn: (str: string) => T | Promise<T> | null,
): Promise<TextTriggerMatchResult> { ): Promise<Partial<TextTriggerMatchResult<T>>> {
const messageInfo: MessageInfo = { channelId: msg.channel_id, messageId: msg.id }; const messageInfo: MessageInfo = { channelId: msg.channel_id, messageId: msg.id };
const member = this.guild.members.get(msg.user_id); const member = this.guild.members.get(msg.user_id);
if (trigger.match_messages) { if (trigger.match_messages) {
const str = msg.data.content; const str = msg.data.content;
const match = await cb(str); const matchResult = await matchFn(str);
if (match) return { type: "message", str, userId: msg.user_id, messageInfo, matchedContent: match }; if (matchResult) {
return { type: "message", str, userId: msg.user_id, messageInfo, matchedValue: matchResult };
}
} }
if (trigger.match_embeds && msg.data.embeds && msg.data.embeds.length) { if (trigger.match_embeds && msg.data.embeds && msg.data.embeds.length) {
const str = JSON.stringify(msg.data.embeds[0]); const str = JSON.stringify(msg.data.embeds[0]);
const match = await cb(str); const matchResult = await matchFn(str);
if (match) return { type: "embed", str, userId: msg.user_id, messageInfo, matchedContent: match }; if (matchResult) {
return { type: "embed", str, userId: msg.user_id, messageInfo, matchedValue: matchResult };
}
} }
if (trigger.match_visible_names) { if (trigger.match_visible_names) {
const str = member.nick || msg.data.author.username; const str = member.nick || msg.data.author.username;
const match = await cb(str); const matchResult = await matchFn(str);
if (match) return { type: "visiblename", str, userId: msg.user_id, matchedContent: match }; if (matchResult) {
return { type: "visiblename", str, userId: msg.user_id, matchedValue: matchResult };
}
} }
if (trigger.match_usernames) { if (trigger.match_usernames) {
const str = `${msg.data.author.username}#${msg.data.author.discriminator}`; const str = `${msg.data.author.username}#${msg.data.author.discriminator}`;
const match = await cb(str); const matchResult = await matchFn(str);
if (match) return { type: "username", str, userId: msg.user_id, matchedContent: match }; if (matchResult) {
return { type: "username", str, userId: msg.user_id, matchedValue: matchResult };
}
} }
if (trigger.match_nicknames && member.nick) { if (trigger.match_nicknames && member.nick) {
const str = member.nick; const str = member.nick;
const match = await cb(str); const matchResult = await matchFn(str);
if (match) return { type: "nickname", str, userId: msg.user_id, matchedContent: match }; if (matchResult) {
return { type: "nickname", str, userId: msg.user_id, matchedValue: matchResult };
}
} }
// type 4 = custom status // type 4 = custom status
if (trigger.match_custom_status && member.game && member.game.type === 4) { if (trigger.match_custom_status && member.game && member.game.type === 4) {
const str = member.game.state; const str = member.game.state;
const match = await cb(str); const matchResult = await matchFn(str);
if (match) return { type: "customstatus", str, userId: msg.user_id, matchedContent: match }; if (matchResult) {
return { type: "customstatus", str, userId: msg.user_id, matchedValue: matchResult };
}
} }
return null; return null;
} }
protected async matchMultipleTextTypesOnMember( protected async matchMultipleTextTypesOnMember<T>(
trigger: TextTriggerWithMultipleMatchTypes, trigger: TextTriggerWithMultipleMatchTypes,
member: Member, member: Member,
cb, matchFn: (str: string) => T | Promise<T> | null,
): Promise<TextTriggerMatchResult> { ): Promise<Partial<TextTriggerMatchResult<T>>> {
if (trigger.match_usernames) { if (trigger.match_usernames) {
const str = `${member.user.username}#${member.user.discriminator}`; const str = `${member.user.username}#${member.user.discriminator}`;
const match = await cb(str); const matchResult = await matchFn(str);
if (match) return { type: "username", str, userId: member.id }; if (matchResult) {
return { type: "username", str, userId: member.id, matchedValue: matchResult };
}
} }
if (trigger.match_nicknames && member.nick) { if (trigger.match_nicknames && member.nick) {
const str = member.nick; const str = member.nick;
const match = await cb(str); const matchResult = await matchFn(str);
if (match) return { type: "nickname", str, userId: member.id }; if (matchResult) {
return { type: "nickname", str, userId: member.id, matchedValue: matchResult };
}
} }
return null; return null;
@ -836,63 +851,63 @@ export class AutomodPlugin extends ZeppelinPlugin<TConfigSchema> {
const match = await this.matchMultipleTextTypesOnMessage(trigger.match_words, msg, str => { const match = await this.matchMultipleTextTypesOnMessage(trigger.match_words, msg, str => {
return this.evaluateMatchWordsTrigger(trigger.match_words, str); return this.evaluateMatchWordsTrigger(trigger.match_words, str);
}); });
if (match) return match; if (match) return { ...match, trigger: "match_words" } as TextTriggerMatchResult;
} }
if (trigger.match_regex) { if (trigger.match_regex) {
const match = await this.matchMultipleTextTypesOnMessage(trigger.match_regex, msg, str => { const match = await this.matchMultipleTextTypesOnMessage(trigger.match_regex, msg, str => {
return this.evaluateMatchRegexTrigger(trigger.match_regex, str); return this.evaluateMatchRegexTrigger(trigger.match_regex, str);
}); });
if (match) return match; if (match) return { ...match, trigger: "match_regex" } as TextTriggerMatchResult;
} }
if (trigger.match_invites) { if (trigger.match_invites) {
const match = await this.matchMultipleTextTypesOnMessage(trigger.match_invites, msg, str => { const match = await this.matchMultipleTextTypesOnMessage(trigger.match_invites, msg, str => {
return this.evaluateMatchInvitesTrigger(trigger.match_invites, str); return this.evaluateMatchInvitesTrigger(trigger.match_invites, str);
}); });
if (match) return match; if (match) return { ...match, trigger: "match_invites" } as TextTriggerMatchResult;
} }
if (trigger.match_links) { if (trigger.match_links) {
const match = await this.matchMultipleTextTypesOnMessage(trigger.match_links, msg, str => { const match = await this.matchMultipleTextTypesOnMessage(trigger.match_links, msg, str => {
return this.evaluateMatchLinksTrigger(trigger.match_links, str); return this.evaluateMatchLinksTrigger(trigger.match_links, str);
}); });
if (match) return match; if (match) return { ...match, trigger: "match_links" } as TextTriggerMatchResult;
} }
if (trigger.message_spam) { if (trigger.message_spam) {
const match = this.matchTextSpamTrigger(RecentActionType.Message, trigger.message_spam, msg); const match = this.matchTextSpamTrigger(RecentActionType.Message, trigger.message_spam, msg);
if (match) return match; if (match) return { ...match, trigger: "message_spam" } as TextSpamTriggerMatchResult;
} }
if (trigger.mention_spam) { if (trigger.mention_spam) {
const match = this.matchTextSpamTrigger(RecentActionType.Mention, trigger.mention_spam, msg); const match = this.matchTextSpamTrigger(RecentActionType.Mention, trigger.mention_spam, msg);
if (match) return match; if (match) return { ...match, trigger: "mention_spam" } as TextSpamTriggerMatchResult;
} }
if (trigger.link_spam) { if (trigger.link_spam) {
const match = this.matchTextSpamTrigger(RecentActionType.Link, trigger.link_spam, msg); const match = this.matchTextSpamTrigger(RecentActionType.Link, trigger.link_spam, msg);
if (match) return match; if (match) return { ...match, trigger: "link_spam" } as TextSpamTriggerMatchResult;
} }
if (trigger.attachment_spam) { if (trigger.attachment_spam) {
const match = this.matchTextSpamTrigger(RecentActionType.Attachment, trigger.attachment_spam, msg); const match = this.matchTextSpamTrigger(RecentActionType.Attachment, trigger.attachment_spam, msg);
if (match) return match; if (match) return { ...match, trigger: "attachment_spam" } as TextSpamTriggerMatchResult;
} }
if (trigger.emoji_spam) { if (trigger.emoji_spam) {
const match = this.matchTextSpamTrigger(RecentActionType.Emoji, trigger.emoji_spam, msg); const match = this.matchTextSpamTrigger(RecentActionType.Emoji, trigger.emoji_spam, msg);
if (match) return match; if (match) return { ...match, trigger: "emoji_spam" } as TextSpamTriggerMatchResult;
} }
if (trigger.line_spam) { if (trigger.line_spam) {
const match = this.matchTextSpamTrigger(RecentActionType.Line, trigger.line_spam, msg); const match = this.matchTextSpamTrigger(RecentActionType.Line, trigger.line_spam, msg);
if (match) return match; if (match) return { ...match, trigger: "line_spam" } as TextSpamTriggerMatchResult;
} }
if (trigger.character_spam) { if (trigger.character_spam) {
const match = this.matchTextSpamTrigger(RecentActionType.Character, trigger.character_spam, msg); const match = this.matchTextSpamTrigger(RecentActionType.Character, trigger.character_spam, msg);
if (match) return match; if (match) return { ...match, trigger: "character_spam" } as TextSpamTriggerMatchResult;
} }
} }
@ -1102,6 +1117,9 @@ export class AutomodPlugin extends ZeppelinPlugin<TConfigSchema> {
}); });
} }
/**
* Apply the actions of the specified rule on the matched message/member
*/
protected async applyActionsOnMatch(rule: TRule, matchResult: AnyTriggerMatchResult) { protected async applyActionsOnMatch(rule: TRule, matchResult: AnyTriggerMatchResult) {
if (rule.cooldown && this.checkAndUpdateCooldown(rule, matchResult)) { if (rule.cooldown && this.checkAndUpdateCooldown(rule, matchResult)) {
return; return;
@ -1335,6 +1353,7 @@ export class AutomodPlugin extends ZeppelinPlugin<TConfigSchema> {
} }
/** /**
* Check if the rule's on cooldown and bump its usage count towards the cooldown up
* @return Whether the rule's on cooldown * @return Whether the rule's on cooldown
*/ */
protected checkAndUpdateCooldown(rule: TRule, matchResult: AnyTriggerMatchResult): boolean { protected checkAndUpdateCooldown(rule: TRule, matchResult: AnyTriggerMatchResult): boolean {
@ -1371,15 +1390,17 @@ export class AutomodPlugin extends ZeppelinPlugin<TConfigSchema> {
return false; return false;
} }
/**
* Returns a text summary for the match result for use in logs/alerts
*/
protected async getMatchSummary(matchResult: AnyTriggerMatchResult): Promise<string> { protected async getMatchSummary(matchResult: AnyTriggerMatchResult): Promise<string> {
if (matchResult.type === "message" || matchResult.type === "embed") { if (matchResult.type === "message" || matchResult.type === "embed") {
const message = await this.savedMessages.find(matchResult.messageInfo.messageId); const message = await this.savedMessages.find(matchResult.messageInfo.messageId);
const channel = this.guild.channels.get(matchResult.messageInfo.channelId); const channel = this.guild.channels.get(matchResult.messageInfo.channelId);
const channelMention = channel ? verboseChannelMention(channel) : `\`#${message.channel_id}\``; const channelMention = channel ? verboseChannelMention(channel) : `\`#${message.channel_id}\``;
const matchedContent = disableInlineCode(matchResult.matchedContent);
return trimPluginDescription(` return trimPluginDescription(`
Matched \`${matchedContent}\` in message in ${channelMention}: Matched ${this.getMatchedValueText(matchResult)} in message in ${channelMention}:
${messageSummary(message)} ${messageSummary(message)}
`); `);
} else if (matchResult.type === "textspam" || matchResult.type === "raidspam") { } else if (matchResult.type === "textspam" || matchResult.type === "raidspam") {
@ -1392,20 +1413,36 @@ export class AutomodPlugin extends ZeppelinPlugin<TConfigSchema> {
Matched spam: ${disableLinkPreviews(archiveUrl)} Matched spam: ${disableLinkPreviews(archiveUrl)}
`); `);
} else if (matchResult.type === "username") { } else if (matchResult.type === "username") {
const matchedContent = disableInlineCode(matchResult.matchedContent); return `Matched ${this.getMatchedValueText(matchResult)} in username: ${matchResult.str}`;
return `Matched \`${matchedContent}\` in username: ${matchResult.str}`;
} else if (matchResult.type === "nickname") { } else if (matchResult.type === "nickname") {
const matchedContent = disableInlineCode(matchResult.matchedContent); return `Matched ${this.getMatchedValueText(matchResult)} in nickname: ${matchResult.str}`;
return `Matched \`${matchedContent}\` in nickname: ${matchResult.str}`;
} else if (matchResult.type === "visiblename") { } else if (matchResult.type === "visiblename") {
const matchedContent = disableInlineCode(matchResult.matchedContent); return `Matched ${this.getMatchedValueText(matchResult)} in visible name: ${matchResult.str}`;
return `Matched \`${matchedContent}\` in visible name: ${matchResult.str}`;
} else if (matchResult.type === "customstatus") { } else if (matchResult.type === "customstatus") {
const matchedContent = disableInlineCode(matchResult.matchedContent); return `Matched ${this.getMatchedValueText(matchResult)} in custom status: ${matchResult.str}`;
return `Matched \`${matchedContent}\` in custom status: ${matchResult.str}`;
} }
} }
/**
* 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)}\``;
}
return typeof matchResult.matchedValue === "string" ? `\`${disableInlineCode(matchResult.matchedValue)}\`` : null;
}
/**
* Run automod actions on new messages
*/
protected onMessageCreate(msg: SavedMessage) { protected onMessageCreate(msg: SavedMessage) {
if (msg.is_bot) return; if (msg.is_bot) return;