From e38d1057f4ee7c9625267090b968d8072c2e9dd8 Mon Sep 17 00:00:00 2001 From: Naomi Carrigan Date: Thu, 9 Oct 2025 19:56:00 -0700 Subject: [PATCH] feat: add discord analytics --- .gitea/workflows/sonar.yml | 34 ------------------------- package.json | 3 ++- pnpm-lock.yaml | 25 ++++++++++++++---- src/events/_handleEvents.ts | 9 ++++++- src/events/guild/onAuditLogEntry.ts | 2 ++ src/events/interaction/onInteraction.ts | 2 ++ src/events/member/onMemberAdd.ts | 2 ++ src/events/member/onMemberRemove.ts | 2 ++ src/events/member/onMemberUpdate.ts | 2 ++ src/events/message/onMessage.ts | 3 +++ src/events/message/onMessageDelete.ts | 2 ++ src/events/message/onMessageEdit.ts | 2 ++ src/events/thread/onThreadCreate.ts | 2 ++ src/events/thread/onThreadDelete.ts | 2 ++ src/events/thread/onThreadUpdate.ts | 2 ++ src/events/voice/onVoiceUpdate.ts | 2 ++ 16 files changed, 55 insertions(+), 41 deletions(-) delete mode 100644 .gitea/workflows/sonar.yml diff --git a/.gitea/workflows/sonar.yml b/.gitea/workflows/sonar.yml deleted file mode 100644 index 783bb19..0000000 --- a/.gitea/workflows/sonar.yml +++ /dev/null @@ -1,34 +0,0 @@ -name: Code Analysis -on: - push: - branches: - - main - -jobs: - sonar: - name: SonarQube - - steps: - - name: Checkout Source Files - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: SonarCube Scan - uses: SonarSource/sonarqube-scan-action@v4 - timeout-minutes: 10 - env: - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - SONAR_HOST_URL: "https://quality.nhcarrigan.com" - with: - args: > - -Dsonar.sources=. - -Dsonar.projectKey=mod-bot - - - name: SonarQube Quality Gate check - uses: sonarsource/sonarqube-quality-gate-action@v1 - with: - pollingTimeoutSec: 600 - env: - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - SONAR_HOST_URL: "https://quality.nhcarrigan.com" diff --git a/package.json b/package.json index b751858..801df8c 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,8 @@ "typescript": "5.4.5" }, "dependencies": { - "@nhcarrigan/logger": "1.0.0", + "@nhcarrigan/discord-analytics": "0.0.6", + "@nhcarrigan/logger": "1.1.1", "@octokit/rest": "20.1.1", "@prisma/client": "5.13.0", "diff": "8.0.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bbff01a..20b1832 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,9 +8,12 @@ importers: .: dependencies: + '@nhcarrigan/discord-analytics': + specifier: 0.0.6 + version: 0.0.6(@nhcarrigan/logger@1.1.1)(discord.js@14.15.2) '@nhcarrigan/logger': - specifier: 1.0.0 - version: 1.0.0 + specifier: 1.1.1 + version: 1.1.1 '@octokit/rest': specifier: 20.1.1 version: 20.1.1 @@ -167,14 +170,20 @@ packages: '@humanwhocodes/object-schema@2.0.3': resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} + '@nhcarrigan/discord-analytics@0.0.6': + resolution: {integrity: sha512-Mci/zSY2nE24BM2cZx5EiYqwpRTTCBznFfs2BphejzDAaWPt1P12V5ln7OSUbFLGhvTD/Qwi0za3yPv6shQLoA==} + peerDependencies: + '@nhcarrigan/logger': '>=1.1.0-hotfix' + discord.js: ^14.0.0 + '@nhcarrigan/eslint-config@3.2.0': resolution: {integrity: sha512-DlB0T5o0BcRlqJ9ktejs+jtufrxwyWnzWXsCM8GrWMph+19dCBOf8w3aDQpuIu9hM16d79mDTvSwwDx5ekOVvw==} engines: {node: '20', pnpm: '8'} peerDependencies: eslint: '>=8' - '@nhcarrigan/logger@1.0.0': - resolution: {integrity: sha512-2e19Bie+ZKb6yKPKjhawqsENkhHatYkvBAmFZx9eToOXdOca+CYi51tldRMtejg6e0+4hOOf2bo5zdBQKmH0dw==} + '@nhcarrigan/logger@1.1.1': + resolution: {integrity: sha512-P6OEQFHDtf6psybYGljuCxkSW6DLQCsx1aZZ3w4YKBXHBFjDbhuvpM9K1kPhVN48hakitx2WPLEoIFr6YZELYw==} '@nhcarrigan/prettier-config@3.2.0': resolution: {integrity: sha512-AZOzwDTZfRiEinjUmqRj4gqZLYpLANhN1iMIsESxeuln+/BjGI06pINQsePIZ/I2HoPd+HGjNqu0S+Os9nHzuw==} @@ -2393,6 +2402,12 @@ snapshots: '@humanwhocodes/object-schema@2.0.3': {} + '@nhcarrigan/discord-analytics@0.0.6(@nhcarrigan/logger@1.1.1)(discord.js@14.15.2)': + dependencies: + '@nhcarrigan/logger': 1.1.1 + discord.js: 14.15.2 + node-schedule: 2.1.1 + '@nhcarrigan/eslint-config@3.2.0(eslint@8.57.0)(prettier@3.2.5)(typescript@5.4.5)': dependencies: '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5) @@ -2411,7 +2426,7 @@ snapshots: - supports-color - typescript - '@nhcarrigan/logger@1.0.0': {} + '@nhcarrigan/logger@1.1.1': {} '@nhcarrigan/prettier-config@3.2.0(prettier@3.2.5)': dependencies: diff --git a/src/events/_handleEvents.ts b/src/events/_handleEvents.ts index c83a2ec..cd7226c 100644 --- a/src/events/_handleEvents.ts +++ b/src/events/_handleEvents.ts @@ -1,3 +1,4 @@ +import { DiscordAnalytics } from "@nhcarrigan/discord-analytics"; import { ExtendedClient } from "../interfaces/ExtendedClient"; import { checkEntitledGuild } from "../utils/checkEntitledGuild"; @@ -17,6 +18,7 @@ import { onThreadCreate } from "./thread/onThreadCreate"; import { onThreadDelete } from "./thread/onThreadDelete"; import { onThreadUpdate } from "./thread/onThreadUpdate"; import { onVoiceUpdate } from "./voice/onVoiceUpdate"; +import { logHandler } from "../utils/logHandler.js"; /** * Module to mount the Discord event listeners. @@ -24,8 +26,13 @@ import { onVoiceUpdate } from "./voice/onVoiceUpdate"; * @param {ExtendedClient} bot The bot's Discord instance. */ export const handleEvents = (bot: ExtendedClient) => { + const analytics = new DiscordAnalytics(bot, logHandler); + /* Client Events */ - bot.on("ready", async () => await onReady(bot)); + bot.once("ready", async () => { + await onReady(bot); + analytics.startCron(); + }); bot.on("disconnect", () => onDisconnect()); /* Message Events */ diff --git a/src/events/guild/onAuditLogEntry.ts b/src/events/guild/onAuditLogEntry.ts index d219490..0ababcb 100644 --- a/src/events/guild/onAuditLogEntry.ts +++ b/src/events/guild/onAuditLogEntry.ts @@ -5,6 +5,7 @@ import { getModActionFromAuditLog } from "../../modules/events/getModActionFromA import { addCase } from "../../utils/addCase"; import { errorHandler } from "../../utils/errorHandler"; import { sendLogMessage } from "../../utils/sendLogMessage"; +import { logHandler } from "../../utils/logHandler.js"; /** * Handles properly logging a manual mod action based on audit logs. @@ -72,6 +73,7 @@ export const onAuditLogEntry = async ( false, caseNum ); + await logHandler.metric("audit_log_action", 1, { modAction, guildId: guild.id, targetId: target.id }); } catch (err) { await errorHandler(bot, "on audit log entry", err); } diff --git a/src/events/interaction/onInteraction.ts b/src/events/interaction/onInteraction.ts index d5c1b1d..b75dbc2 100644 --- a/src/events/interaction/onInteraction.ts +++ b/src/events/interaction/onInteraction.ts @@ -9,6 +9,7 @@ import { handleMassBanModal } from "../../modules/modals/handleMassBanModal"; import { handleMessageReportModal } from "../../modules/modals/handleMessageReportModal"; import { checkEntitledGuild } from "../../utils/checkEntitledGuild"; import { errorHandler } from "../../utils/errorHandler"; +import { logHandler } from "../../utils/logHandler.js"; /** * Handles interactions. @@ -60,6 +61,7 @@ export const onInteraction = async ( await handleMessageReportModal(bot, interaction); } } + await logHandler.metric("interaction_create", 1, { userId: interaction.user.id, guildId: interaction.guild.id, command: interaction.isCommand() ? interaction.commandName : interaction.customId }); } catch (err) { const id = await errorHandler(bot, "on interaction", err); if (!interaction.isAutocomplete()) { diff --git a/src/events/member/onMemberAdd.ts b/src/events/member/onMemberAdd.ts index 258425a..11ca8bc 100644 --- a/src/events/member/onMemberAdd.ts +++ b/src/events/member/onMemberAdd.ts @@ -3,6 +3,7 @@ import { GuildMember } from "discord.js"; import { ExtendedClient } from "../../interfaces/ExtendedClient"; import { getConfig } from "../../modules/data/getConfig"; import { errorHandler } from "../../utils/errorHandler"; +import { logHandler } from "../../utils/logHandler.js"; /** * Sends a log message to the configured log channel when a member @@ -36,6 +37,7 @@ export const onMemberAdd = async (bot: ExtendedClient, member: GuildMember) => { await channel.send({ content: `${user.tag} (${user.id}) has joined the server. Total Members: ${guild.memberCount}` }); + await logHandler.metric("member_join", 1, { userId: user.id, guildId: guild.id }); } catch (err) { await errorHandler(bot, "on member add", err); } diff --git a/src/events/member/onMemberRemove.ts b/src/events/member/onMemberRemove.ts index b795cc0..d3bd2c8 100644 --- a/src/events/member/onMemberRemove.ts +++ b/src/events/member/onMemberRemove.ts @@ -3,6 +3,7 @@ import { GuildMember, PartialGuildMember } from "discord.js"; import { ExtendedClient } from "../../interfaces/ExtendedClient"; import { getConfig } from "../../modules/data/getConfig"; import { errorHandler } from "../../utils/errorHandler"; +import { logHandler } from "../../utils/logHandler.js"; /** * Sends a log message to the configured log channel when a member @@ -54,6 +55,7 @@ export const onMemberRemove = async ( await channel.send({ content: `${user.tag} (${user.id}) has left the server (joined at ${joinStamp}). Total Members: ${guild.memberCount}` }); + await logHandler.metric("member_leave", 1, { userId: user.id, guildId: guild.id }); } catch (err) { await errorHandler(bot, "on member remove", err); } diff --git a/src/events/member/onMemberUpdate.ts b/src/events/member/onMemberUpdate.ts index 8c87c22..be02e05 100644 --- a/src/events/member/onMemberUpdate.ts +++ b/src/events/member/onMemberUpdate.ts @@ -3,6 +3,7 @@ import { GuildMember, PartialGuildMember } from "discord.js"; import { ExtendedClient } from "../../interfaces/ExtendedClient"; import { getConfig } from "../../modules/data/getConfig"; import { errorHandler } from "../../utils/errorHandler"; +import { logHandler } from "../../utils/logHandler.js"; /** * Sends a log message to the configured log channel when a member's @@ -74,6 +75,7 @@ export const onMemberUpdate = async ( )}` }); } + await logHandler.metric("member_update", 1, { userId: user.id, guildId: guild.id }); } catch (err) { await errorHandler(bot, "on member update", err); } diff --git a/src/events/message/onMessage.ts b/src/events/message/onMessage.ts index 34c2c69..7d04252 100644 --- a/src/events/message/onMessage.ts +++ b/src/events/message/onMessage.ts @@ -9,6 +9,7 @@ import { errorHandler } from "../../utils/errorHandler"; import { sendLogMessage } from "../../utils/sendLogMessage"; import { sendModDm } from "../../utils/sendModDm"; import { triggerModRequest } from "../../utils/triggerModRequest"; +import { logHandler } from "../../utils/logHandler.js"; const linkRegex = /https?:\/\/([a-zA-Z0-9_.-]{2,256}\.\w{2,24}\b)/g; @@ -67,6 +68,7 @@ export const onMessage = async (bot: ExtendedClient, message: Message) => { duration: calculateMuteDuration(24, "hours"), pruneDays: 0 }); + await logHandler.metric("automod_trigger", 1, { userId: author.id, guildId: guild.id }); return; } } @@ -135,6 +137,7 @@ export const onMessage = async (bot: ExtendedClient, message: Message) => { for (const record of levelRoles) { await member.roles.add(record.roleId).catch(() => null); } + await logHandler.metric("message_create", 1, { userId: author.id, guildId: guild.id }); } catch (err) { await errorHandler(bot, "on message", err); } diff --git a/src/events/message/onMessageDelete.ts b/src/events/message/onMessageDelete.ts index c1277e6..bc3a86b 100644 --- a/src/events/message/onMessageDelete.ts +++ b/src/events/message/onMessageDelete.ts @@ -5,6 +5,7 @@ import { ExtendedClient } from "../../interfaces/ExtendedClient"; import { getConfig } from "../../modules/data/getConfig"; import { customSubstring } from "../../utils/customSubstring"; import { errorHandler } from "../../utils/errorHandler"; +import { logHandler } from "../../utils/logHandler.js"; /** * Handles a message delete event. @@ -67,6 +68,7 @@ export const onMessageDelete = async ( parse: [] } }); + await logHandler.metric("message_delete", 1, { userId: author?.id ?? "unknown", guildId: guild.id }); } catch (err) { await errorHandler(bot, "on message delete", err); } diff --git a/src/events/message/onMessageEdit.ts b/src/events/message/onMessageEdit.ts index 821f97d..b391ffc 100644 --- a/src/events/message/onMessageEdit.ts +++ b/src/events/message/onMessageEdit.ts @@ -5,6 +5,7 @@ import { getConfig } from "../../modules/data/getConfig"; import { customSubstring } from "../../utils/customSubstring"; import { errorHandler } from "../../utils/errorHandler"; import { generateDiff } from "../../modules/events/generateDiff"; +import { logHandler } from "../../utils/logHandler.js"; /** * Handles a message edit event. @@ -58,6 +59,7 @@ export const onMessageEdit = async ( }>:\`\`\`diff\n${customSubstring(diffContent, 4000)}\n\`\`\``, allowedMentions: { parse: [] } }); + await logHandler.metric("message_edit", 1, { userId: author?.id ?? "unknown", guildId: guild.id }); } catch (err) { await errorHandler(bot, "on message edit", err); } diff --git a/src/events/thread/onThreadCreate.ts b/src/events/thread/onThreadCreate.ts index 5676955..e0da83e 100644 --- a/src/events/thread/onThreadCreate.ts +++ b/src/events/thread/onThreadCreate.ts @@ -3,6 +3,7 @@ import { ThreadChannel } from "discord.js"; import { ExtendedClient } from "../../interfaces/ExtendedClient"; import { getConfig } from "../../modules/data/getConfig"; import { errorHandler } from "../../utils/errorHandler"; +import { logHandler } from "../../utils/logHandler.js"; /** * Handles the creation of a new thread. @@ -36,6 +37,7 @@ export const onThreadCreate = async ( await channel.send({ content: `${thread.name} has been created in <#${thread.parentId}>` }); + await logHandler.metric("thread_create", 1, { userId: thread.ownerId ?? "unknown", guildId: thread.guild.id }); } catch (err) { await errorHandler(bot, "on thread create", err); } diff --git a/src/events/thread/onThreadDelete.ts b/src/events/thread/onThreadDelete.ts index 9735ba5..454fbbb 100644 --- a/src/events/thread/onThreadDelete.ts +++ b/src/events/thread/onThreadDelete.ts @@ -3,6 +3,7 @@ import { ThreadChannel } from "discord.js"; import { ExtendedClient } from "../../interfaces/ExtendedClient"; import { getConfig } from "../../modules/data/getConfig"; import { errorHandler } from "../../utils/errorHandler"; +import { logHandler } from "../../utils/logHandler.js"; /** * Handles the deletion of a thread. @@ -32,6 +33,7 @@ export const onThreadDelete = async ( await channel.send({ content: `${thread.name} has been deleted from <#${thread.parentId}>` }); + await logHandler.metric("thread_delete", 1, { userId: thread.ownerId ?? "unknown", guildId: thread.guild.id }); } catch (err) { await errorHandler(bot, "on thread create", err); } diff --git a/src/events/thread/onThreadUpdate.ts b/src/events/thread/onThreadUpdate.ts index 56099fe..f89b347 100644 --- a/src/events/thread/onThreadUpdate.ts +++ b/src/events/thread/onThreadUpdate.ts @@ -3,6 +3,7 @@ import { ThreadChannel } from "discord.js"; import { ExtendedClient } from "../../interfaces/ExtendedClient"; import { getConfig } from "../../modules/data/getConfig"; import { errorHandler } from "../../utils/errorHandler"; +import { logHandler } from "../../utils/logHandler.js"; /** * Handles a thread update. @@ -48,6 +49,7 @@ export const onThreadUpdate = async ( content: `${oldThread.name} has been renamed to ${newThread.name} in <#${newThread.parentId}>` }); } + await logHandler.metric("thread_update", 1, { userId: newThread.ownerId ?? "unknown", guildId: newThread.guild.id }); } catch (err) { await errorHandler(bot, "on thread update", err); } diff --git a/src/events/voice/onVoiceUpdate.ts b/src/events/voice/onVoiceUpdate.ts index b1ec67d..5e349ba 100644 --- a/src/events/voice/onVoiceUpdate.ts +++ b/src/events/voice/onVoiceUpdate.ts @@ -3,6 +3,7 @@ import { VoiceState } from "discord.js"; import { ExtendedClient } from "../../interfaces/ExtendedClient"; import { getConfig } from "../../modules/data/getConfig"; import { errorHandler } from "../../utils/errorHandler"; +import { logHandler } from "../../utils/logHandler.js"; /** * Handles voice state updates. @@ -76,6 +77,7 @@ export const onVoiceUpdate = async ( content: `${newVoice.member.user.tag} (${newVoice.member.id}) has been deafened.` }); } + await logHandler.metric("voice_update", 1, { userId: newVoice.member?.id ?? "unknown", guildId: newVoice.guild.id }); } catch (err) { await errorHandler(bot, "on voice update", err); }