zappyzep/backend/src/plugins/Stats.ts
Dragory 84135b201b
Add anti-raid levels to automod. Large refactor of spam detection. Add member_join and member_join_spam triggers.
Anti-raid levels don't by themselves do anything, but they can be
used in overrides to activate specific automod items.

Spam detection should now be more reliable and also combine further
spam messages after the initial detection into the archive.

Messages deleted by automod no longer create the normal deletion log
entry. Instead, the AUTOMOD_ACTION log entry contains the deleted
message or an archive if there are multiple (i.e. spam).
2020-01-26 19:54:32 +02:00

164 lines
4.7 KiB
TypeScript

import { ZeppelinPlugin } from "./ZeppelinPlugin";
import * as t from "io-ts";
import { convertDelayStringToMS, DAYS, HOURS, tAlphanumeric, tDateTime, tDeepPartial, tDelayString } from "../utils";
import { IPluginOptions } from "knub";
import moment from "moment-timezone";
import { GuildStats } from "../data/GuildStats";
import { Message } from "eris";
import escapeStringRegexp from "escape-string-regexp";
import { SavedMessage } from "../data/entities/SavedMessage";
import { GuildSavedMessages } from "../data/GuildSavedMessages";
//region TYPES
const tBaseSource = t.type({
name: tAlphanumeric,
track: t.boolean,
retention_period: tDelayString,
});
const tMemberMessagesSource = t.intersection([
tBaseSource,
t.type({
type: t.literal("member_messages"),
}),
]);
type TMemberMessagesSource = t.TypeOf<typeof tMemberMessagesSource>;
const tChannelMessagesSource = t.intersection([
tBaseSource,
t.type({
type: t.literal("channel_messages"),
}),
]);
type TChannelMessagesSource = t.TypeOf<typeof tChannelMessagesSource>;
const tKeywordsSource = t.intersection([
tBaseSource,
t.type({
type: t.literal("keywords"),
keywords: t.array(t.string),
}),
]);
type TKeywordsSource = t.TypeOf<typeof tKeywordsSource>;
const tSource = t.union([tMemberMessagesSource, tChannelMessagesSource, tKeywordsSource]);
type TSource = t.TypeOf<typeof tSource>;
const tConfigSchema = t.type({
sources: t.record(tAlphanumeric, tSource),
});
type TConfigSchema = t.TypeOf<typeof tConfigSchema>;
const tPartialConfigSchema = tDeepPartial(tConfigSchema);
//endregion
//region CONSTANTS
const DEFAULT_RETENTION_PERIOD = "4w";
//endregion
//region PLUGIN
export class StatsPlugin extends ZeppelinPlugin<TConfigSchema> {
public static pluginName = "stats";
public static configSchema = tConfigSchema;
public static showInDocs = false;
protected stats: GuildStats;
protected savedMessages: GuildSavedMessages;
private onMessageCreateFn;
private cleanStatsInterval;
public static getStaticDefaultOptions(): IPluginOptions<TConfigSchema> {
return {
config: {
sources: {},
},
};
}
protected static applyDefaultsToSource(source: Partial<TSource>) {
if (source.track == null) {
source.track = true;
}
if (source.retention_period == null) {
source.retention_period = DEFAULT_RETENTION_PERIOD;
}
}
protected static preprocessStaticConfig(config: t.TypeOf<typeof tPartialConfigSchema>) {
if (config.sources) {
for (const [key, source] of Object.entries(config.sources)) {
source.name = key;
this.applyDefaultsToSource(source);
}
}
return config;
}
protected onLoad() {
this.stats = GuildStats.getGuildInstance(this.guildId);
this.savedMessages = GuildSavedMessages.getGuildInstance(this.guildId);
this.onMessageCreateFn = this.savedMessages.events.on("create", msg => this.onMessageCreate(msg));
this.cleanOldStats();
this.cleanStatsInterval = setInterval(() => this.cleanOldStats(), 1 * DAYS);
}
protected onUnload() {
this.savedMessages.events.off("create", this.onMessageCreateFn);
clearInterval(this.cleanStatsInterval);
}
protected async cleanOldStats() {
const config = this.getConfig();
for (const source of Object.values(config.sources)) {
const cutoffMS = convertDelayStringToMS(source.retention_period);
const cutoff = moment()
.subtract(cutoffMS, "ms")
.format("YYYY-MM-DD HH:mm:ss");
await this.stats.deleteOldValues(source.name, cutoff);
}
}
protected saveMemberMessagesStats(source: TMemberMessagesSource, msg: SavedMessage) {
this.stats.saveValue(source.name, msg.user_id, 1);
}
protected saveChannelMessagesStats(source: TChannelMessagesSource, msg: SavedMessage) {
this.stats.saveValue(source.name, msg.channel_id, 1);
}
protected saveKeywordsStats(source: TKeywordsSource, msg: SavedMessage) {
const content = msg.data.content;
if (!content) return;
for (const keyword of source.keywords) {
const regex = new RegExp(`\\b${escapeStringRegexp(keyword)}\\b`, "i");
if (content.match(regex)) {
this.stats.saveValue(source.name, "keyword", 1);
break;
}
}
}
onMessageCreate(msg: SavedMessage) {
const config = this.getConfigForMemberIdAndChannelId(msg.user_id, msg.channel_id);
for (const source of Object.values(config.sources)) {
if (!source.track) continue;
if (source.type === "member_messages") {
this.saveMemberMessagesStats(source, msg);
} else if (source.type === "channel_messages") {
this.saveChannelMessagesStats(source, msg);
} else if (source.type === "keywords") {
this.saveKeywordsStats(source, msg);
}
}
}
}