diff --git a/package-lock.json b/package-lock.json index 8201f1a..4cf8487 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "discord.js": "^14.16.3", "dotenv": "^16.4.5", - "firebase": "^11.0.2", + "firebase": "^11.5.0", "openai": "^4.68.1", "tsc-alias": "^1.8.10", "typescript": "^5.6.3" @@ -714,9 +714,9 @@ "license": "Apache-2.0" }, "node_modules/@firebase/app": { - "version": "0.11.2", - "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.11.2.tgz", - "integrity": "sha512-bFee0hPJZBzNtiizRxdgsu8C9DW3mn1y0OJJ4zHQsccjDYzGOfvN0G3CMGyBIiwNctsFpQa8orbp2IKywoUeqA==", + "version": "0.11.3", + "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.11.3.tgz", + "integrity": "sha512-QlTZl/RcqPSonYxB87n8KgAUW2L6ZZz0W4D91PVmQ1tJPsKsKPrWAFHL0ii2cQW6FxTxfNjbZ7kucuIcKXk3tw==", "license": "Apache-2.0", "dependencies": { "@firebase/component": "0.6.13", @@ -730,9 +730,9 @@ } }, "node_modules/@firebase/app-check": { - "version": "0.8.12", - "resolved": "https://registry.npmjs.org/@firebase/app-check/-/app-check-0.8.12.tgz", - "integrity": "sha512-LxjcoIFOU4sgK07ZWb8XDHxuVB+UKs41vPK+Sg9PeZMvEoz84fndFAx8Nz2nipiya2EmyxBgVhff8Hi6GBt+XA==", + "version": "0.8.13", + "resolved": "https://registry.npmjs.org/@firebase/app-check/-/app-check-0.8.13.tgz", + "integrity": "sha512-ONsgml8/dplUOAP42JQO6hhiWDEwR9+RUTLenxAN9S8N6gel/sDQ9Ci721Py1oASMGdDU8v9R7xAZxzvOX5lPg==", "license": "Apache-2.0", "dependencies": { "@firebase/component": "0.6.13", @@ -748,12 +748,12 @@ } }, "node_modules/@firebase/app-check-compat": { - "version": "0.3.19", - "resolved": "https://registry.npmjs.org/@firebase/app-check-compat/-/app-check-compat-0.3.19.tgz", - "integrity": "sha512-G8FMiqhrKc4gEEujrBDBBrbRav8MGqoLObWj1hy/riCSg4XlRYhpnq3ev8E9HTirqU1tAGH6oJl7vr+jfM7YNA==", + "version": "0.3.20", + "resolved": "https://registry.npmjs.org/@firebase/app-check-compat/-/app-check-compat-0.3.20.tgz", + "integrity": "sha512-/twgmlnNAaZ/wbz3kcQrL/26b+X+zUX+lBmu5LwwEcWcpnb+mrVEAKhD7/ttm52dxYiSWtLDeuXy3FXBhqBC5A==", "license": "Apache-2.0", "dependencies": { - "@firebase/app-check": "0.8.12", + "@firebase/app-check": "0.8.13", "@firebase/app-check-types": "0.5.3", "@firebase/component": "0.6.13", "@firebase/logger": "0.4.4", @@ -780,12 +780,12 @@ "license": "Apache-2.0" }, "node_modules/@firebase/app-compat": { - "version": "0.2.51", - "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.2.51.tgz", - "integrity": "sha512-pxF1+coABt+ugqNI0YXDlmkKv4kh3pjI5BqIJJ1VXBo42OZbKMsQbFeos14YBrWwiqqSjUvQ70FBNsv5E2wuxg==", + "version": "0.2.52", + "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.2.52.tgz", + "integrity": "sha512-0p/l1KiwhwwYTcPWoleFQHftOnYzeXvyVf3WNZyKFBAoQMpCVW6bVm/uO1bXF91AwU1JN0og888Y6Sc8avqZ+A==", "license": "Apache-2.0", "dependencies": { - "@firebase/app": "0.11.2", + "@firebase/app": "0.11.3", "@firebase/component": "0.6.13", "@firebase/logger": "0.4.4", "@firebase/util": "1.11.0", @@ -874,9 +874,9 @@ } }, "node_modules/@firebase/data-connect": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@firebase/data-connect/-/data-connect-0.3.1.tgz", - "integrity": "sha512-PNlfAJ2mcbyRlWfm41nfk8EksTuvMFTFIX+puNzeUa6OTIDtyp1IX1NJVc7n6WpfbErN7tNqcOEMe6BMtpcjVA==", + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@firebase/data-connect/-/data-connect-0.3.2.tgz", + "integrity": "sha512-PYG55JRTmvYrUuXXmYBsZexwKVP9aR3mIRRHxB9V2bQeRDZky6JtRZnH3GLhf4ZsxZy5Ewd8ul/jTOYR4gpD9w==", "license": "Apache-2.0", "dependencies": { "@firebase/auth-interop-types": "0.2.4", @@ -890,9 +890,9 @@ } }, "node_modules/@firebase/database": { - "version": "1.0.13", - "resolved": "https://registry.npmjs.org/@firebase/database/-/database-1.0.13.tgz", - "integrity": "sha512-cdc+LuseKdJXzlrCx8ePMXyctSWtYS9SsP3y7EeA85GzNh/IL0b7HOq0eShridL935iQ0KScZCj5qJtKkGE53g==", + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-1.0.14.tgz", + "integrity": "sha512-9nxYtkHAG02/Nh2Ssms1T4BbWPPjiwohCvkHDUl4hNxnki1kPgsLo5xe9kXNzbacOStmVys+RUXvwzynQSKmUQ==", "license": "Apache-2.0", "dependencies": { "@firebase/app-check-interop-types": "0.3.3", @@ -908,14 +908,14 @@ } }, "node_modules/@firebase/database-compat": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-2.0.4.tgz", - "integrity": "sha512-4qsptwZ3DTGNBje56ETItZQyA/HMalOelnLmkC3eR0M6+zkzOHjNHyWUWodW2mqxRKAM0sGkn+aIwYHKZFJXug==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-2.0.5.tgz", + "integrity": "sha512-CNf1UbvWh6qIaSf4sn6sx2DTDz/em/D7QxULH1LTxxDQHr9+CeYGvlAqrKnk4ZH0P0eIHyQFQU7RwkUJI0B9gQ==", "license": "Apache-2.0", "dependencies": { "@firebase/component": "0.6.13", - "@firebase/database": "1.0.13", - "@firebase/database-types": "1.0.9", + "@firebase/database": "1.0.14", + "@firebase/database-types": "1.0.10", "@firebase/logger": "0.4.4", "@firebase/util": "1.11.0", "tslib": "^2.1.0" @@ -925,9 +925,9 @@ } }, "node_modules/@firebase/database-types": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-1.0.9.tgz", - "integrity": "sha512-uCntrxPbJHhZsNRpMhxNCm7GzhYWX+7J2e57wq1ZZ4NJrQw5DORgkAzJMByYZcVAjgADnCxxhK/GkoypH+XpvQ==", + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-1.0.10.tgz", + "integrity": "sha512-mH6RC1E9/Pv8jf1/p+M8YFTX+iu+iHDN89hecvyO7wHrI4R1V0TXjxOHvX3nLJN1sfh0CWG6CHZ0VlrSmK/cwg==", "license": "Apache-2.0", "dependencies": { "@firebase/app-types": "0.9.3", @@ -935,9 +935,9 @@ } }, "node_modules/@firebase/firestore": { - "version": "4.7.9", - "resolved": "https://registry.npmjs.org/@firebase/firestore/-/firestore-4.7.9.tgz", - "integrity": "sha512-uq/bUtHDqJ5ZqPHAJIlNzHpXUtcVYcASz2V6y7UmP1WLlRKEt1yf1OcQW5u8pY2yq7162OnCl5J5mkOdMTMLZw==", + "version": "4.7.10", + "resolved": "https://registry.npmjs.org/@firebase/firestore/-/firestore-4.7.10.tgz", + "integrity": "sha512-6nKsyo2U+jYSCcSE5sjMdDNA23DMUvYPUvsYGg09CNvcTO8GGKsPs7SpOhspsB91mbacq+u627CDAx3FUhPSSQ==", "license": "Apache-2.0", "dependencies": { "@firebase/component": "0.6.13", @@ -956,13 +956,13 @@ } }, "node_modules/@firebase/firestore-compat": { - "version": "0.3.44", - "resolved": "https://registry.npmjs.org/@firebase/firestore-compat/-/firestore-compat-0.3.44.tgz", - "integrity": "sha512-4Lv2TyHEW+FugXPgmQ0ZylSbh9uFuKDP0lCL1hX9cbxXaafhC/Nww+DWokUQ2zZcynjc8fxFunw6Xbd3QHAlgA==", + "version": "0.3.45", + "resolved": "https://registry.npmjs.org/@firebase/firestore-compat/-/firestore-compat-0.3.45.tgz", + "integrity": "sha512-uRvi7AYPmsDl7UZwPyV7jgDGYusEZ2+U2g7MndbQHKIA8fNHpYC6QrzMs58+/IjX+kF/lkUn67Vrr0AkVjlY+Q==", "license": "Apache-2.0", "dependencies": { "@firebase/component": "0.6.13", - "@firebase/firestore": "4.7.9", + "@firebase/firestore": "4.7.10", "@firebase/firestore-types": "3.0.3", "@firebase/util": "1.11.0", "tslib": "^2.1.0" @@ -1120,9 +1120,9 @@ "license": "Apache-2.0" }, "node_modules/@firebase/performance": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/@firebase/performance/-/performance-0.7.1.tgz", - "integrity": "sha512-SkEUurawojCjav2V2AXo6BQLDtv02NxgXPLCiAvrkn95IAKI4W/UbLKYQvMbEez/nqvmnucLyklcMlB0Q5a1iw==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@firebase/performance/-/performance-0.7.2.tgz", + "integrity": "sha512-DXLLp0R0jdxH/yTmv+WTkOzsLl8YYecXh4lGZE0dzqC0IV8k+AxpLSSWvOTCkAETze8yEU/iF+PtgYVlGjfMMQ==", "license": "Apache-2.0", "dependencies": { "@firebase/component": "0.6.13", @@ -1137,14 +1137,14 @@ } }, "node_modules/@firebase/performance-compat": { - "version": "0.2.14", - "resolved": "https://registry.npmjs.org/@firebase/performance-compat/-/performance-compat-0.2.14.tgz", - "integrity": "sha512-/crPg0fDqHIx+FjFoEqWxNp+lJSF40ZG7x43AAJGRaUaWLJDncQm3UJB5/mABaRZb7obs1CQAcRtd4phZFkmZg==", + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/@firebase/performance-compat/-/performance-compat-0.2.15.tgz", + "integrity": "sha512-wUxsw7hGBEMN6XfvYQqwPIQp5LcJXawWM5tmYp6L7ClCoTQuEiCKHWWVurJgN8Q1YHzoHVgjNfPQAOVu29iMVg==", "license": "Apache-2.0", "dependencies": { "@firebase/component": "0.6.13", "@firebase/logger": "0.4.4", - "@firebase/performance": "0.7.1", + "@firebase/performance": "0.7.2", "@firebase/performance-types": "0.2.3", "@firebase/util": "1.11.0", "tslib": "^2.1.0" @@ -1258,9 +1258,9 @@ } }, "node_modules/@firebase/vertexai": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@firebase/vertexai/-/vertexai-1.1.0.tgz", - "integrity": "sha512-K8CgIFKJrfrf5lYhKnDXOu08FEmIzVExK+ApUZx4Bw2GAmLEA3wDVrsjuupuvpXZSp8QlzvEiXwqshqqc4v0pA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@firebase/vertexai/-/vertexai-1.2.0.tgz", + "integrity": "sha512-WUYIzFpOipjFXT2i0hT26wivJoIximizQptVs3KAxFAqbVlO8sjKPsMkgz0bh+tdKlqP4SUDda71fMUZXUKHgA==", "license": "Apache-2.0", "dependencies": { "@firebase/app-check-interop-types": "0.3.3", @@ -3109,39 +3109,39 @@ } }, "node_modules/firebase": { - "version": "11.4.0", - "resolved": "https://registry.npmjs.org/firebase/-/firebase-11.4.0.tgz", - "integrity": "sha512-Z6kwhWIPDgIm0+NUEQxwjH14hMP7t42WSFnf/78R0Vh59VovLYTOCTM3MIdY3jlSZ9uKz56FhXrvsNXNhAn/Xg==", + "version": "11.5.0", + "resolved": "https://registry.npmjs.org/firebase/-/firebase-11.5.0.tgz", + "integrity": "sha512-ZTpO/zD5nYqY02bGpXCg1dRNLggTXPQZdLQeSeur3jYH270p1QkAZZJsm/lrKZ2W4ZjBlafTxxs4OwN38Vyocw==", "license": "Apache-2.0", "dependencies": { "@firebase/analytics": "0.10.12", "@firebase/analytics-compat": "0.2.18", - "@firebase/app": "0.11.2", - "@firebase/app-check": "0.8.12", - "@firebase/app-check-compat": "0.3.19", - "@firebase/app-compat": "0.2.51", + "@firebase/app": "0.11.3", + "@firebase/app-check": "0.8.13", + "@firebase/app-check-compat": "0.3.20", + "@firebase/app-compat": "0.2.52", "@firebase/app-types": "0.9.3", "@firebase/auth": "1.9.1", "@firebase/auth-compat": "0.5.19", - "@firebase/data-connect": "0.3.1", - "@firebase/database": "1.0.13", - "@firebase/database-compat": "2.0.4", - "@firebase/firestore": "4.7.9", - "@firebase/firestore-compat": "0.3.44", + "@firebase/data-connect": "0.3.2", + "@firebase/database": "1.0.14", + "@firebase/database-compat": "2.0.5", + "@firebase/firestore": "4.7.10", + "@firebase/firestore-compat": "0.3.45", "@firebase/functions": "0.12.3", "@firebase/functions-compat": "0.3.20", "@firebase/installations": "0.6.13", "@firebase/installations-compat": "0.2.13", "@firebase/messaging": "0.12.17", "@firebase/messaging-compat": "0.2.17", - "@firebase/performance": "0.7.1", - "@firebase/performance-compat": "0.2.14", + "@firebase/performance": "0.7.2", + "@firebase/performance-compat": "0.2.15", "@firebase/remote-config": "0.6.0", "@firebase/remote-config-compat": "0.2.13", "@firebase/storage": "0.13.7", "@firebase/storage-compat": "0.3.17", "@firebase/util": "1.11.0", - "@firebase/vertexai": "1.1.0" + "@firebase/vertexai": "1.2.0" } }, "node_modules/form-data": { @@ -3185,20 +3185,6 @@ "dev": true, "license": "ISC" }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", diff --git a/package.json b/package.json index 123f3be..5d96a61 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "dependencies": { "discord.js": "^14.16.3", "dotenv": "^16.4.5", - "firebase": "^11.0.2", + "firebase": "^11.5.0", "openai": "^4.68.1", "tsc-alias": "^1.8.10", "typescript": "^5.6.3" diff --git a/res/server-configs.json b/res/server-configs.json new file mode 100644 index 0000000..2ca65c8 --- /dev/null +++ b/res/server-configs.json @@ -0,0 +1,7 @@ +{ + "1294893558024638475": { + "isEnabled": true, + "lastToggled": "2024-12-11T17:16:01.394Z", + "toggledBy": "186318354422824960" + } +} \ No newline at end of file diff --git a/res/test-server-configs.json b/res/test-server-configs.json new file mode 100644 index 0000000..2c21e24 --- /dev/null +++ b/res/test-server-configs.json @@ -0,0 +1,7 @@ +{ + "123456789": { + "isEnabled": false, + "lastToggled": "2025-03-23T18:25:56.621Z", + "toggledBy": "987654321" + } +} \ No newline at end of file diff --git a/src/discordApp.ts b/src/discordApp.ts index 7e44711..1e35563 100644 --- a/src/discordApp.ts +++ b/src/discordApp.ts @@ -1,6 +1,13 @@ import "dotenv/config"; -import { Client, GatewayIntentBits, Events } from "discord.js"; -import { clarify, embed, ping, tone, requestAnonymousClarification, mood, inDepthClarification, postemptiveToneAdd, getTones, action } from "./interactions" +import { + Client, + GatewayIntentBits, + Events, + Message, +} from "discord.js"; +import { clarify, embed, ping, tone, requestAnonymousClarification, mood, toggleBot, inDepthClarification, postemptiveToneAdd, getTones, action } from "./interactions" +import { get, ref } from "firebase/database"; +import database from "./firebase"; import { cleanupMoods } from "./helpers"; export async function launchBot(): Promise { @@ -20,10 +27,6 @@ export async function launchBot(): Promise { ], }); - // client.on takes an event and a function, and calls that function once the evnt is called. - // eventually, it may be wise to make each callback its own function for readibility. - - // called when bot is live client.on(Events.ClientReady, () => { if (client.user) { console.log(`client "ready": Logged in as ${client.user.tag}!`); @@ -39,12 +42,7 @@ export async function launchBot(): Promise { } }); - // called when a message is sent - client.on(Events.MessageCreate, async (message) => { - // message.content shows the body of the messages, with a few quirks. For one, - // any emojis will be in the discord format, and pings will show up as some - // arbitrary snowflake (ask me or look it up) e.g. `<@1295481669603688499>` - + client.on(Events.MessageCreate, async (message: Message) => { console.log(message.content); // TODO: add tone analysis here as well @@ -53,26 +51,57 @@ export async function launchBot(): Promise { // called when an interaction (e.g. slash command) is called. there are a bunch of different // interaction types, but we'll see which we need as time goes on. // TODO: find references for this + // Called when an interaction (e.g., slash command) is triggered client.on(Events.InteractionCreate, async (interaction) => { - if (interaction.isChatInputCommand()) { // slash command - if (interaction.commandName === "ping") await ping(interaction); - if (interaction.commandName === "embed") await embed(interaction); - if (interaction.commandName === "action") await action(interaction); - if (interaction.commandName === "list-tones") await getTones(interaction); - if (interaction.commandName === "mood") await mood(interaction); - } else if (interaction.isMessageContextMenuCommand()) { // command from the "apps" menu when clicking on a message - if (interaction.commandName === "Tone") await tone(interaction); - if (interaction.commandName === "Add Tone") await postemptiveToneAdd(interaction); - if (interaction.commandName === "Clarify") await clarify(interaction); - if (interaction.commandName === "In-Depth Clarification") await inDepthClarification(interaction); - if (interaction.commandName === "Request Anonymous Clarification") await requestAnonymousClarification(interaction); - } else { - console.log(interaction); - } + const guildId = interaction.guildId!; + const dbRef = ref(database, `servers/${guildId}/botStatus`); + + try { + // Fetch the bot status from the database + const snapshot = await get(dbRef); + const botStatus = snapshot.exists() ? snapshot.val() : "active"; // Default to 'active' if not set + + // If the bot is inactive, ignore the interaction and reply with a message + if (botStatus === "inactive") { + + if(interaction.isCommand() && interaction.commandName != "togglebot"){ + return interaction.reply({ + content: "Sorry, the bot is currently disabled for this server.", + flags: 64, // Make it ephemeral + }); + } + } + + // If the bot is active, process the interaction + if (interaction.isChatInputCommand()) { // slash command + if (interaction.commandName === "ping") await ping(interaction); + if (interaction.commandName === "embed") await embed(interaction); + if (interaction.commandName === "action") await action(interaction); + if (interaction.commandName === "list-tones") await getTones(interaction); + if (interaction.commandName === "mood") await mood(interaction); + if (interaction.commandName === "togglebot") await toggleBot(interaction); + } else if (interaction.isMessageContextMenuCommand()) { // command from the "apps" menu when clicking on a message + if (interaction.commandName === "Tone") await tone(interaction); + if (interaction.commandName === "Add Tone") await postemptiveToneAdd(interaction); + if (interaction.commandName === "Clarify") await clarify(interaction); + if (interaction.commandName === "In-Depth Clarification") await inDepthClarification(interaction); + if (interaction.commandName === "Request Anonymous Clarification") await requestAnonymousClarification(interaction); + } else { + console.log(interaction); + } + } catch (error) { + console.error("Error fetching bot status:", error); + if(interaction.isCommand()){ + interaction.reply({ + content: "There was an error while processing your request. Please try again later.", + flags: 64, + }); + }} }); // attempt to connect - client.login(process.env.DISCORD_TOKEN); - + await client.login(process.env.DISCORD_TOKEN); return client; } + +export default launchBot; diff --git a/src/interactions.ts b/src/interactions.ts index 24b9e31..1633653 100644 --- a/src/interactions.ts +++ b/src/interactions.ts @@ -8,6 +8,7 @@ import { EmbedBuilder, MessageContextMenuCommandInteraction, MessageFlags, + PermissionsBitField, Role, Snowflake, StringSelectMenuBuilder, @@ -15,8 +16,8 @@ import { } from "discord.js"; import { analyzeTone, analyzeMoodColor, emojiRepresentation, explanationOfTone } from "./gptRequests"; import db from './firebase'; // Import from your firebase.ts file -import { ref, set, get, child, query, DataSnapshot } from "firebase/database"; -import { addRoleToDatabase, MINIMUM_MOOD_LIFESPAN, removeRoleFromDatabase, removeRoleIfUnused} from "./helpers" +import { ref, set, get } from "firebase/database"; +import { addRoleToDatabase, MINIMUM_MOOD_LIFESPAN, removeRoleIfUnused } from "./helpers"; //getTones and Clarify rely on toneJSON. Implementing it in firebase would be better //import tonesData from "./tones.json" assert { type: "json"}; @@ -515,4 +516,43 @@ export async function requestAnonymousClarification(interaction: MessageContextM content: "There was an error handling the clarification request", }); } -} \ No newline at end of file +} +export async function toggleBot(interaction: ChatInputCommandInteraction): Promise { + await interaction.deferReply({ flags: MessageFlags.Ephemeral }); + + if (!(interaction.member?.permissions as PermissionsBitField).has(PermissionsBitField.Flags.ManageGuild)) { + interaction.editReply('You need "Manage Server" permission to toggle the bot!'); + return; + } + + const guildId = interaction.guildId!; // Get the guild ID + const dbRef = ref(db, `servers/${guildId}/botStatus`); + + try { + // Get the current bot status from the Realtime Database + const snapshot = await get(dbRef); + let newStatus = "active"; // Default to 'active' or lose your mind for a bit + + if (snapshot.exists() && snapshot.val() === "active") { + newStatus = snapshot.val() === "active" ? "inactive": "active"; // If active, set to inactive, and the opposite + } else { + await set(dbRef, "active"); + } + + // Log the new status for debugging or go insane + console.log(`Toggling bot status to: ${newStatus}`); + + // Update the bot status in the Realtime Database + await set(dbRef, newStatus); + + // Responds to the user with confirmation + interaction.editReply({ + content: `The bot has been turned ${newStatus === "active" ? "on" : "off"} for this server.`, + }); + } catch (error) { + console.error("Error toggling bot status:", error); + interaction.editReply({ + content: "There was an error while toggling the bot's status. Please try again later.", + }); + } +} diff --git a/src/registerCommands.ts b/src/registerCommands.ts index ff6de83..9949cf3 100644 --- a/src/registerCommands.ts +++ b/src/registerCommands.ts @@ -1,5 +1,5 @@ import "dotenv/config"; -import { REST, Routes, ApplicationCommandOptionType } from "discord.js"; +import { REST, Routes, ApplicationCommandOptionType, SlashCommandBuilder } from "discord.js"; // define a ts type for discord commands declare type command = { @@ -92,5 +92,10 @@ updateCommands([ { name:"Request Anonymous Clarification", type: 3, + }, + { + name: "togglebot", + description: "Enable or Disable VibeCheque for this server", + type: 1, } ]); \ No newline at end of file diff --git a/src/serverConfigManager.test.ts b/src/serverConfigManager.test.ts new file mode 100644 index 0000000..ec3238b --- /dev/null +++ b/src/serverConfigManager.test.ts @@ -0,0 +1,70 @@ +import { describe, it, beforeEach, expect } from '@jest/globals'; +import fs from 'fs'; +import path from 'path'; +import { ServerConfigManager } from './serverConfigManager'; + +describe('ServerConfigManager', () => { + const TEST_CONFIG_PATH = './res/test-server-configs.json'; + let serverConfigManager: ServerConfigManager; + + beforeEach(() => { + // Clean up test file before each test + if (fs.existsSync(TEST_CONFIG_PATH)) { + fs.unlinkSync(TEST_CONFIG_PATH); + } + + // Stop console.log output from flooding tests + jest.spyOn(console, "log").mockImplementation(() => {}); + + serverConfigManager = new ServerConfigManager(TEST_CONFIG_PATH); + }); + + it('should successfully toggle server status from enabled to disabled', () => { + const guildId = '123456789'; + const userId = '987654321'; + + // Verify initial state is enabled + expect(serverConfigManager.isServerEnabled(guildId)).toBe(true); + + // Toggle to disabled + const config = serverConfigManager.toggleServerStatus(guildId, userId); + + // Verify the toggle + expect(config.isEnabled).toBe(false); + expect(config.toggledBy).toBe(userId); + expect(config.lastToggled).not.toBeNull(); + + // Verify the state persisted + expect(serverConfigManager.isServerEnabled(guildId)).toBe(false); + }); + + it('should toggle server status multiple times successfully', () => { + const guildId = '123456789'; + const userId = '987654321'; + + // First toggle (true -> false) + let config = serverConfigManager.toggleServerStatus(guildId, userId); + expect(config.isEnabled).toBe(false); + + // Second toggle (false -> true) + config = serverConfigManager.toggleServerStatus(guildId, userId); + expect(config.isEnabled).toBe(true); + + // Third toggle (true -> false) + config = serverConfigManager.toggleServerStatus(guildId, userId); + expect(config.isEnabled).toBe(false); + }); + + it('should persist server status between instance recreations', () => { + const guildId = '123456789'; + const userId = '987654321'; + + // Toggle with first instance + serverConfigManager.toggleServerStatus(guildId, userId); + expect(serverConfigManager.isServerEnabled(guildId)).toBe(false); + + // Create new instance and verify state persisted + const newServerConfigManager = new ServerConfigManager(TEST_CONFIG_PATH); + expect(newServerConfigManager.isServerEnabled(guildId)).toBe(false); + }); +}); \ No newline at end of file diff --git a/src/serverConfigManager.ts b/src/serverConfigManager.ts new file mode 100644 index 0000000..691be11 --- /dev/null +++ b/src/serverConfigManager.ts @@ -0,0 +1,99 @@ + +import fs from 'fs'; +import path from 'path'; + +interface ServerConfig { + isEnabled: boolean; + lastToggled: string | null; + toggledBy: string | null; +} + +class ServerConfigManager { + private configPath: string; + private serverStates: Map; + + constructor(configPath: string = './res/server-configs.json') { + this.configPath = path.resolve(configPath); + this.serverStates = new Map(); + + // Load initial states from file if it exists + try { + if (fs.existsSync(this.configPath)) { + const savedConfigs = JSON.parse(fs.readFileSync(this.configPath, 'utf8')); + for (const [guildId, config] of Object.entries(savedConfigs)) { + this.serverStates.set(guildId, config as ServerConfig); + } + console.log('[ServerConfigManager] Loaded existing configurations'); + } + } catch (error) { + console.error('[ServerConfigManager] Error loading initial config:', error); + } + } + + private saveToFile(): void { + try { + const configObject = Object.fromEntries(this.serverStates); + fs.writeFileSync(this.configPath, JSON.stringify(configObject, null, 2)); + console.log('[ServerConfigManager] Saved configurations to file'); + } catch (error) { + console.error('[ServerConfigManager] Error saving to file:', error); + } + } + + public toggleServerStatus(guildId: string, userId: string): ServerConfig { + console.log(`[ServerConfigManager] Attempting to toggle server ${guildId}`); + + // Get current state or create default + let currentConfig = this.serverStates.get(guildId) || { + isEnabled: true, + lastToggled: null, + toggledBy: null + }; + + // Log before state + console.log('[ServerConfigManager] Before toggle:', currentConfig); + + // Create new config object with toggled state + const newConfig: ServerConfig = { + isEnabled: !currentConfig.isEnabled, + lastToggled: new Date().toISOString(), + toggledBy: userId + }; + + // Update in-memory state + this.serverStates.set(guildId, newConfig); + + // Log after state + console.log('[ServerConfigManager] After toggle:', newConfig); + + // Save to file as backup + this.saveToFile(); + + return newConfig; + } + + public isServerEnabled(guildId: string): boolean { + // If no state is stored, default to enabled + const state = this.serverStates.get(guildId); + if (!state) { + return true; + } + return state.isEnabled; + } + + public getServerConfig(guildId: string): ServerConfig { + const config = this.serverStates.get(guildId); + if (!config) { + const defaultConfig: ServerConfig = { + isEnabled: true, + lastToggled: null, + toggledBy: null + }; + this.serverStates.set(guildId, defaultConfig); + return defaultConfig; + } + return config; + } +} +export { ServerConfigManager }; +export default new ServerConfigManager(); \ No newline at end of file diff --git a/src/serverSetting.ts b/src/serverSetting.ts new file mode 100644 index 0000000..3aaeebe --- /dev/null +++ b/src/serverSetting.ts @@ -0,0 +1,32 @@ +import { get, ref, set } from "firebase/database"; +import database from "./firebase"; // Import the Realtime Database instance + +// Get the current setting for the server (whether the bot is enabled or disabled) +export async function getServerSetting(guildId: string): Promise { + const dbRef = ref(database, `servers/${guildId}/botStatus`); + + try { + const snapshot = await get(dbRef); + if (snapshot.exists()) { + return snapshot.val(); // Check if the bot is "active" + } else { + // Default to "active" if no setting is found + await set(dbRef, "active"); + return "active"; + } + } catch (error) { + console.error("Error getting server setting:", error); + throw error; + } +} + +// Toggle the bot's enabled/disabled setting for the server +export async function toggleServerSetting(guildId: string): Promise { + const currentSetting = await getServerSetting(guildId); + const newSetting = currentSetting ? "inactive" : "active"; // Switch between "active" and "inactive" + + const dbRef = ref(database, `servers/${guildId}/botStatus`); + await set(dbRef, newSetting); + + return newSetting; +} \ No newline at end of file diff --git a/src/testing/mocks/mockDiscord.ts b/src/testing/mocks/mockDiscord.ts index 0d21ef0..ae7b89b 100644 --- a/src/testing/mocks/mockDiscord.ts +++ b/src/testing/mocks/mockDiscord.ts @@ -24,6 +24,7 @@ import { InteractionReplyOptions, MessageCollector, UserFlags, + MessageMentions, GuildCreateOptions } from "discord.js"; import { timestampToSnowflake } from "../../helpers"; @@ -260,12 +261,19 @@ export class MockDiscord { return { client: this.client, author: this.user, + mentions: this.createMockMentions([]), content: "MESSAGE CONTENT", createMessageComponentCollector: jest.fn((filter: Function, componentType: ComponentType, time: number | undefined) => {return this.createMockCollector(filter, componentType, time)}), ...options } as unknown as Message; } + public createMockMentions(users: User[]): MessageMentions { + return { + has: jest.fn((data)=>users.includes(data)) + } as unknown as MessageMentions; + } + public createMockCollector(filter: Function, componentType: ComponentType, time: number | undefined): InteractionCollector { return { filter: filter,