From 89f6ec2ef8f15d3c57c49bee91544fbdd2a04409 Mon Sep 17 00:00:00 2001 From: Fuwn Date: Mon, 6 Oct 2025 23:09:47 -0700 Subject: feat(gateway:listeners): Add message statistics module --- .../gateway/src/listeners/clientReady/index.ts | 2 + .../gateway/src/listeners/messageCreate/index.ts | 2 + .../gateway/src/listeners/messageStatistics.ts | 258 +++++++++++++++++++++ 3 files changed, 262 insertions(+) create mode 100644 packages/gateway/src/listeners/messageStatistics.ts (limited to 'packages') diff --git a/packages/gateway/src/listeners/clientReady/index.ts b/packages/gateway/src/listeners/clientReady/index.ts index 8e7f81a..4e8fea7 100644 --- a/packages/gateway/src/listeners/clientReady/index.ts +++ b/packages/gateway/src/listeners/clientReady/index.ts @@ -2,11 +2,13 @@ import { Client, Events } from "discord.js"; import { handleVoiceConnection } from "./voiceConnection"; import { handleActivity } from "./activity"; import { handleUmagramCatchup } from "./umagramCatchup"; +import { initializeMessageStatistics } from "../messageStatistics"; export const handleClientReady = (client: Client) => { client.once(Events.ClientReady, async (readyClient) => { handleVoiceConnection(readyClient); handleActivity(readyClient); + initializeMessageStatistics(); await handleUmagramCatchup(readyClient); }); }; diff --git a/packages/gateway/src/listeners/messageCreate/index.ts b/packages/gateway/src/listeners/messageCreate/index.ts index d736844..197d7c1 100644 --- a/packages/gateway/src/listeners/messageCreate/index.ts +++ b/packages/gateway/src/listeners/messageCreate/index.ts @@ -8,9 +8,11 @@ import { handleRoleMentionCooldown } from "./roleMentionCooldown"; import { handleAICommand } from "./aiCommandHandler"; import { handleRandomEyesReaction } from "./randomEyesReaction"; import { handleRoleplayThumbsUpReaction } from "./roleplayThumbsUpReaction"; +import { recordMessageForStatistics } from "../messageStatistics"; export const handleMessageCreate = (client: Client) => { client.on(Events.MessageCreate, async (message: Message) => { + recordMessageForStatistics(message); await handleRoleplayUmagram(message); await handleRandomEyesReaction(message); await handleRoleplayThumbsUpReaction(message); diff --git a/packages/gateway/src/listeners/messageStatistics.ts b/packages/gateway/src/listeners/messageStatistics.ts new file mode 100644 index 0000000..27e4278 --- /dev/null +++ b/packages/gateway/src/listeners/messageStatistics.ts @@ -0,0 +1,258 @@ +import { Message } from "discord.js"; +import { CENTRAL_GUILD_ID, ROLEPLAY_GUILD_ID } from "../constants"; + +interface MessageStats { + totalMessages: number; + totalLength: number; + hourlyMessages: Map; + dailyMessages: Map; + hourlyChannels: Map>; + dailyChannels: Map>; + startTime: number; +} + +interface GuildStats { + [guildId: string]: MessageStats; +} + +class MessageStatisticsTracker { + private stats: GuildStats = { + [CENTRAL_GUILD_ID]: { + totalMessages: 0, + totalLength: 0, + hourlyMessages: new Map(), + dailyMessages: new Map(), + hourlyChannels: new Map(), + dailyChannels: new Map(), + startTime: Date.now(), + }, + [ROLEPLAY_GUILD_ID]: { + totalMessages: 0, + totalLength: 0, + hourlyMessages: new Map(), + dailyMessages: new Map(), + hourlyChannels: new Map(), + dailyChannels: new Map(), + startTime: Date.now(), + }, + }; + + private logInterval: ReturnType | null = null; + + constructor() { + this.initializeLogging(); + } + + public recordMessage(message: Message): void { + if (!message.content || message.author.bot || !message.guildId) return; + + if (!this.stats[message.guildId]) return; + + const now = new Date(); + const hour = now.getHours(); + const date = now.toISOString().split("T")[0]; + const channelId = message.channelId; + const guildStats = this.stats[message.guildId]; + + guildStats.totalMessages += 1; + guildStats.totalLength += message.content.length; + + const currentHourCount = guildStats.hourlyMessages.get(hour) || 0; + + guildStats.hourlyMessages.set(hour, currentHourCount + 1); + + const currentDayCount = guildStats.dailyMessages.get(date) || 0; + + guildStats.dailyMessages.set(date, currentDayCount + 1); + + if (!guildStats.hourlyChannels.has(hour)) + guildStats.hourlyChannels.set(hour, new Map()); + + const hourlyChannelMap = guildStats.hourlyChannels.get(hour)!; + const currentHourlyChannelCount = hourlyChannelMap.get(channelId) || 0; + + hourlyChannelMap.set(channelId, currentHourlyChannelCount + 1); + + if (!guildStats.dailyChannels.has(date)) + guildStats.dailyChannels.set(date, new Map()); + + const dailyChannelMap = guildStats.dailyChannels.get(date)!; + const currentDailyChannelCount = dailyChannelMap.get(channelId) || 0; + + dailyChannelMap.set(channelId, currentDailyChannelCount + 1); + } + + private calculateAverages(guildId: string): { + averageMessagesPerHour: number; + averageMessagesPerDay: number; + averageMessageLength: number; + } { + const guildStats = this.stats[guildId]; + const uptimeHours = (Date.now() - guildStats.startTime) / (1000 * 60 * 60); + const uptimeDays = uptimeHours / 24; + const averageMessagesPerHour = + guildStats.totalMessages / Math.max(uptimeHours, 1); + const averageMessagesPerDay = + guildStats.totalMessages / Math.max(uptimeDays, 1); + const averageMessageLength = + guildStats.totalMessages > 0 + ? guildStats.totalLength / guildStats.totalMessages + : 0; + + return { + averageMessagesPerHour, + averageMessagesPerDay, + averageMessageLength, + }; + } + + private getTopChannels(guildId: string): { + topHourlyChannels: Array<{ channelId: string; count: number }>; + topDailyChannels: Array<{ channelId: string; count: number }>; + } { + const guildStats = this.stats[guildId]; + const now = new Date(); + const currentHour = now.getHours(); + const currentDate = now.toISOString().split("T")[0]; + const currentHourChannels = + guildStats.hourlyChannels.get(currentHour) || new Map(); + const topHourlyChannels = Array.from(currentHourChannels.entries()) + .map(([channelId, count]) => ({ channelId, count })) + .sort((a, b) => b.count - a.count) + .slice(0, 5); + const currentDayChannels = + guildStats.dailyChannels.get(currentDate) || new Map(); + const topDailyChannels = Array.from(currentDayChannels.entries()) + .map(([channelId, count]) => ({ channelId, count })) + .sort((a, b) => b.count - a.count) + .slice(0, 5); + + return { + topHourlyChannels, + topDailyChannels, + }; + } + + private logStatistics(): void { + const date = new Date(); + + console.log(`Message Statistics Report (${date.toString()}):\n`); + + const centralAverages = this.calculateAverages(CENTRAL_GUILD_ID); + const centralStats = this.stats[CENTRAL_GUILD_ID]; + const centralTopChannels = this.getTopChannels(CENTRAL_GUILD_ID); + + console.log(`Central Guild:`); + console.log(` Total Messages: ${centralStats.totalMessages}`); + console.log( + ` Average Messages per Hour: ${centralAverages.averageMessagesPerHour.toFixed(2)}`, + ); + console.log( + ` Average Messages per Day: ${centralAverages.averageMessagesPerDay.toFixed(2)}`, + ); + console.log( + ` Average Message Length: ${centralAverages.averageMessageLength.toFixed(2)} characters`, + ); + console.log( + ` Uptime: ${((Date.now() - centralStats.startTime) / (1000 * 60 * 60)).toFixed(2)} hours`, + ); + + if (centralTopChannels.topHourlyChannels.length > 0) { + console.log(` Top 5 Channels (This Hour):`); + centralTopChannels.topHourlyChannels.forEach((channel, index) => { + console.log( + ` ${index + 1}. <#${channel.channelId}>: ${channel.count} messages`, + ); + }); + } + + if (centralTopChannels.topDailyChannels.length > 0) { + console.log(` Top 5 Channels (Today):`); + centralTopChannels.topDailyChannels.forEach((channel, index) => { + console.log( + ` ${index + 1}. <#${channel.channelId}>: ${channel.count} messages`, + ); + }); + } + + console.log(); + + const roleplayAverages = this.calculateAverages(ROLEPLAY_GUILD_ID); + const roleplayStats = this.stats[ROLEPLAY_GUILD_ID]; + const roleplayTopChannels = this.getTopChannels(ROLEPLAY_GUILD_ID); + + console.log(`Roleplay Guild:`); + console.log(` Total Messages: ${roleplayStats.totalMessages}`); + console.log( + ` Average Messages per Hour: ${roleplayAverages.averageMessagesPerHour.toFixed(2)}`, + ); + console.log( + ` Average Messages per Day: ${roleplayAverages.averageMessagesPerDay.toFixed(2)}`, + ); + console.log( + ` Average Message Length: ${roleplayAverages.averageMessageLength.toFixed(2)} characters`, + ); + console.log( + ` Uptime: ${((Date.now() - roleplayStats.startTime) / (1000 * 60 * 60)).toFixed(2)} hours`, + ); + + if (roleplayTopChannels.topHourlyChannels.length > 0) { + console.log(` Top 5 Channels (This Hour):`); + roleplayTopChannels.topHourlyChannels.forEach((channel, index) => { + console.log( + ` ${index + 1}. <#${channel.channelId}>: ${channel.count} messages`, + ); + }); + } + + if (roleplayTopChannels.topDailyChannels.length > 0) { + console.log(` Top 5 Channels (Today):`); + roleplayTopChannels.topDailyChannels.forEach((channel, index) => { + console.log( + ` ${index + 1}. <#${channel.channelId}>: ${channel.count} messages`, + ); + }); + } + + console.log(); + } + + private initializeLogging(): void { + this.logStatistics(); + + this.logInterval = setInterval( + () => { + this.logStatistics(); + }, + 12 * 60 * 60 * 1000, + ); + } + + public destroy(): void { + if (this.logInterval) { + clearInterval(this.logInterval); + + this.logInterval = null; + } + } +} + +let tracker: MessageStatisticsTracker | null = null; + +export const initializeMessageStatistics = (): void => { + if (tracker) tracker.destroy(); + + tracker = new MessageStatisticsTracker(); +}; + +export const recordMessageForStatistics = (message: Message): void => { + if (tracker) tracker.recordMessage(message); +}; + +export const destroyMessageStatistics = (): void => { + if (tracker) { + tracker.destroy(); + + tracker = null; + } +}; -- cgit v1.2.3