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"; 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; const tChannelMessagesSource = t.intersection([ tBaseSource, t.type({ type: t.literal("channel_messages"), }), ]); type TChannelMessagesSource = t.TypeOf; const tKeywordsSource = t.intersection([ tBaseSource, t.type({ type: t.literal("keywords"), keywords: t.array(t.string), }), ]); type TKeywordsSource = t.TypeOf; const tSource = t.union([tMemberMessagesSource, tChannelMessagesSource, tKeywordsSource]); type TSource = t.TypeOf; const tConfigSchema = t.type({ sources: t.record(tAlphanumeric, tSource), }); type TConfigSchema = t.TypeOf; const tPartialConfigSchema = tDeepPartial(tConfigSchema); const DEFAULT_RETENTION_PERIOD = "4w"; export class StatsPlugin extends ZeppelinPlugin { 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 { return { config: { sources: {}, }, }; } protected static preprocessStaticConfig(config: t.TypeOf) { // TODO: Limit min period, min period start date if (config.sources) { for (const [key, source] of Object.entries(config.sources)) { source.name = key; if (source.track == null) { source.track = true; } if (source.retention_period == null) { source.retention_period = DEFAULT_RETENTION_PERIOD; } } } 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); } } } }