From d7bc3bca6042cf5000e2b2bdeb882855ebb5d6b4 Mon Sep 17 00:00:00 2001 From: Naomi Carrigan Date: Tue, 7 Oct 2025 17:59:15 -0700 Subject: [PATCH] feat: add analytics and logging, log interactions --- .vscode/settings.json | 6 +++ package.json | 3 +- pnpm-lock.yaml | 59 +++++++++++++++++++++--- src/index.ts | 5 +++ src/modules/processMessage.ts | 66 --------------------------- src/server/processMessage.ts | 85 +++++++++++++++++++++++++++++++++++ 6 files changed, 152 insertions(+), 72 deletions(-) create mode 100644 .vscode/settings.json delete mode 100644 src/modules/processMessage.ts create mode 100644 src/server/processMessage.ts diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..2beb504 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "explicit" + }, + "eslint.validate": ["typescript"] +} diff --git a/package.json b/package.json index 40b91d7..5278893 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,8 @@ "typescript": "5.9.2" }, "dependencies": { - "@nhcarrigan/logger": "1.0.0", + "@nhcarrigan/discord-analytics": "0.0.3", + "@nhcarrigan/logger": "1.1.1", "alex": "11.0.1", "discord.js": "14.22.1", "extra-markdown-text": "0.1.6", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 140683f..0d67880 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,9 +8,12 @@ importers: .: dependencies: + '@nhcarrigan/discord-analytics': + specifier: 0.0.3 + version: 0.0.3(@nhcarrigan/logger@1.1.1)(discord.js@14.22.1) '@nhcarrigan/logger': - specifier: 1.0.0 - version: 1.0.0 + specifier: 1.1.1 + version: 1.1.1 alex: specifier: 11.0.1 version: 11.0.1 @@ -344,6 +347,12 @@ packages: '@jridgewell/sourcemap-codec@1.5.5': resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + '@nhcarrigan/discord-analytics@0.0.3': + resolution: {integrity: sha512-XUHcvS3ZbuwVpztHH7pC0T/C8nOJt/T5qz7pDKp3/4Kvlbg0UdeVSZio27QkLqRtB/CvNUS78xM3k9odwN1M7A==} + peerDependencies: + '@nhcarrigan/logger': '>=1.1.0-hotfix' + discord.js: ^14.0.0 + '@nhcarrigan/eslint-config@5.2.0': resolution: {integrity: sha512-YpTTqhviKMlRwKF+RC/GYiA5i2jTCmg8uftuiufldneNV5HMbGpTfBbV7tpa8++5mpYJc4+eZaf40QbDiz84dQ==} engines: {node: '>=22', pnpm: '>=9'} @@ -354,8 +363,8 @@ packages: typescript: '>=5' vitest: '>=2' - '@nhcarrigan/logger@1.0.0': - resolution: {integrity: sha512-2e19Bie+ZKb6yKPKjhawqsENkhHatYkvBAmFZx9eToOXdOca+CYi51tldRMtejg6e0+4hOOf2bo5zdBQKmH0dw==} + '@nhcarrigan/logger@1.1.1': + resolution: {integrity: sha512-P6OEQFHDtf6psybYGljuCxkSW6DLQCsx1aZZ3w4YKBXHBFjDbhuvpM9K1kPhVN48hakitx2WPLEoIFr6YZELYw==} '@nhcarrigan/typescript-config@4.0.0': resolution: {integrity: sha512-969HVha7A/Sg77fuMwOm6p14a+7C5iE6g55OD71srqwKIgksQl+Ex/hAI/pyzTQFDQ/FBJbpnHlR4Ov25QV/rw==} @@ -1054,6 +1063,10 @@ packages: core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + cron-parser@4.9.0: + resolution: {integrity: sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==} + engines: {node: '>=12.0.0'} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -2021,6 +2034,9 @@ packages: lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + long-timeout@0.1.1: + resolution: {integrity: sha512-BFRuQUqc7x2NWxfJBCyUrN8iYUYznzL9JROmRz1gZ6KlOIgmoD+njPVbb+VNn2nGMKggMsK79iUNErillsrx7w==} + longest-streak@3.1.0: resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} @@ -2046,6 +2062,10 @@ packages: resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} engines: {node: '>=12'} + luxon@3.7.2: + resolution: {integrity: sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==} + engines: {node: '>=12'} + magic-bytes.js@1.12.1: resolution: {integrity: sha512-ThQLOhN86ZkJ7qemtVRGYM+gRgR8GEXNli9H/PMvpnZsE44Xfh3wx9kGJaldg314v85m+bFW6WBMaVHJc/c3zA==} @@ -2311,6 +2331,10 @@ packages: node-releases@2.0.19: resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} + node-schedule@2.1.1: + resolution: {integrity: sha512-OXdegQq03OmXEjt2hZP33W2YPs/E5BcFQks46+G2gAxs4gHOIVD1u7EqlYLYSKsaIpyKCK9Gbk0ta1/gjRSMRQ==} + engines: {node: '>=6'} + nopt@7.2.1: resolution: {integrity: sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} @@ -2846,6 +2870,9 @@ packages: sonic-boom@4.2.0: resolution: {integrity: sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==} + sorted-array-functions@1.3.0: + resolution: {integrity: sha512-2sqgzeFlid6N4Z2fUQ1cvFmTOLRi/sEDzSQ0OKYchqgoPmQBVyM3959qYx3fpS6Esef80KjmpgPeEr028dP3OA==} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -3658,6 +3685,12 @@ snapshots: '@jridgewell/sourcemap-codec@1.5.5': {} + '@nhcarrigan/discord-analytics@0.0.3(@nhcarrigan/logger@1.1.1)(discord.js@14.22.1)': + dependencies: + '@nhcarrigan/logger': 1.1.1 + discord.js: 14.22.1 + node-schedule: 2.1.1 + '@nhcarrigan/eslint-config@5.2.0(@typescript-eslint/utils@8.40.0(eslint@9.33.0)(typescript@5.9.2))(eslint@9.33.0)(playwright@1.55.0)(react@19.1.1)(typescript@5.9.2)(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(yaml@2.8.1))': dependencies: '@eslint-community/eslint-plugin-eslint-comments': 4.4.1(eslint@9.33.0) @@ -3687,7 +3720,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - '@nhcarrigan/logger@1.0.0': {} + '@nhcarrigan/logger@1.1.1': {} '@nhcarrigan/typescript-config@4.0.0(typescript@5.9.2)': dependencies: @@ -4451,6 +4484,10 @@ snapshots: core-util-is@1.0.3: {} + cron-parser@4.9.0: + dependencies: + luxon: 3.7.2 + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -5627,6 +5664,8 @@ snapshots: lodash@4.17.21: {} + long-timeout@0.1.1: {} + longest-streak@3.1.0: {} loose-envify@1.4.0: @@ -5645,6 +5684,8 @@ snapshots: lru-cache@7.18.3: {} + luxon@3.7.2: {} + magic-bytes.js@1.12.1: {} magic-string@0.30.18: @@ -6175,6 +6216,12 @@ snapshots: node-releases@2.0.19: {} + node-schedule@2.1.1: + dependencies: + cron-parser: 4.9.0 + long-timeout: 0.1.1 + sorted-array-functions: 1.3.0 + nopt@7.2.1: dependencies: abbrev: 2.0.0 @@ -6843,6 +6890,8 @@ snapshots: dependencies: atomic-sleep: 1.0.0 + sorted-array-functions@1.3.0: {} + source-map-js@1.2.1: {} space-separated-tokens@2.0.2: {} diff --git a/src/index.ts b/src/index.ts index 105a21c..d339581 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,6 +4,7 @@ * @author Naomi Carrigan */ +import { DiscordAnalytics } from "@nhcarrigan/discord-analytics"; import { Client, GatewayIntentBits, @@ -22,8 +23,11 @@ const caelia = new Client({ ], }); +const analytics = new DiscordAnalytics(caelia, logger); + caelia.once(Events.ClientReady, () => { void logger.log("debug", `Logged in as ${caelia.user?.username ?? "unknown user"}.`); + analytics.startCron(); }); caelia.on(Events.MessageCreate, (message) => { @@ -31,6 +35,7 @@ caelia.on(Events.MessageCreate, (message) => { }); caelia.on(Events.InteractionCreate, (interaction) => { + void analytics.logGatewayEvent(Events.InteractionCreate, { ...interaction }); if (!interaction.isChatInputCommand()) { return; } diff --git a/src/modules/processMessage.ts b/src/modules/processMessage.ts deleted file mode 100644 index fdc8f91..0000000 --- a/src/modules/processMessage.ts +++ /dev/null @@ -1,66 +0,0 @@ -/** - * @copyright NHCarrigan - * @license Naomi's Public License - * @author Naomi Carrigan - */ - -import { markdown } from "alex"; -import { MessageFlags, type Message } from "discord.js"; -import { replaceCodeBlocks, replaceLinks } from "extra-markdown-text"; -import { alexConfig } from "../config/alex.js"; -import { logger } from "../utils/logger.js"; - -/** - * Checks if a message contains non-inclusive language. If so, - * provides a suggestion to improve vocabulary. - * @param message -- The message payload from Discord. - */ -export const processMessage = async(message: Message): Promise => { - try { - const { content, author, system } = message; - if (author.bot || system || content.length <= 0) { - return; - } - const noCodeBlocks = replaceCodeBlocks(content, () => { - return ""; - }); - const noLinks = replaceLinks(noCodeBlocks, () => { - return ""; - }); - const matches = markdown(noLinks, alexConfig).messages; - if (matches.length === 0) { - return; - } - const responses = matches.map((match) => { - return `- You used the word \`${match.actual ?? "unknown word"}\`. This may not be inclusive language, because: ${match.reason.length > 0 - ? match.reason - : "I said so."}${[ null, "", undefined, "undefined", "null" ].includes(match.note) - ? "" - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- Fuck off - : ` -- ${match.note!}`}`; - }); - - await message.reply({ components: [ - { content: `Hi darling~! It looks like you may have used some language that is not the most inclusive...\n\n${responses.join("\n")}`, - type: 10 }, - { - components: [ - { - label: "Is this inaccurate? Let us know!", - style: 5, - type: 2, - url: "https://chat.nhcarrigan.com", - }, - ], - type: 1, - }, - ], - flags: [ MessageFlags.IsComponentsV2 ] }).catch(() => { - return null; - }); - } catch (error) { - if (error instanceof Error) { - await logger.error("process message module", error); - } - } -}; diff --git a/src/server/processMessage.ts b/src/server/processMessage.ts new file mode 100644 index 0000000..49acba3 --- /dev/null +++ b/src/server/processMessage.ts @@ -0,0 +1,85 @@ +/** + * @copyright NHCarrigan + * @license Naomi's Public License + * @author Naomi Carrigan + */ + +import { markdown } from "alex"; +import { MessageFlags, type Message } from "discord.js"; +import { replaceCodeBlocks, replaceLinks } from "extra-markdown-text"; +import { alexConfig } from "../config/alex.js"; +import { logger } from "../utils/logger.js"; + +/** + * Checks if a message contains non-inclusive language. If so, + * provides a suggestion to improve vocabulary. + * @param message -- The message payload from Discord. + */ +// eslint-disable-next-line max-lines-per-function -- This function is long but it's not complex. +export const processMessage = async(message: Message): Promise => { + try { + const { content, author, system, guild } = message; + if (author.bot || system || content.length <= 0) { + return; + } + const noCodeBlocks = replaceCodeBlocks(content, () => { + return ""; + }); + const noLinks = replaceLinks(noCodeBlocks, () => { + return ""; + }); + const matches = markdown(noLinks, alexConfig).messages; + if (matches.length === 0) { + return; + } + await logger.metric("messages_flagged", 1, { + guild: guild?.id ?? "DM", + user: author.id, + }); + const responses = matches.map((match) => { + return `- You used the word \`${ + match.actual ?? "unknown word" + }\`. This may not be inclusive language, because: ${ + match.reason.length > 0 + ? match.reason + : "I said so." + }${ + [ null, "", undefined, "undefined", "null" ].includes(match.note) + ? "" + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- Fuck off + : ` -- ${match.note!}` + }`; + }); + + await message. + reply({ + components: [ + { + content: `Hi darling~! It looks like you may have used some language that is not the most inclusive...\n\n${responses.join( + "\n", + )}`, + type: 10, + }, + { + components: [ + { + label: "Is this inaccurate? Let us know!", + style: 5, + type: 2, + url: "https://chat.nhcarrigan.com", + }, + ], + type: 1, + }, + ], + flags: [ MessageFlags.IsComponentsV2 ], + }). + catch(() => { + return null; + }); + } catch (error) { + if (error instanceof Error) { + await logger.error("process message module", error); + } + } +};