
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).
164 lines
4.7 KiB
TypeScript
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);
|
|
}
|
|
}
|
|
}
|
|
}
|