diff --git a/.DS_Store b/.DS_Store old mode 100644 new mode 100755 index 3eeca27..5d17b2d Binary files a/.DS_Store and b/.DS_Store differ diff --git a/.dockerignore b/.dockerignore old mode 100644 new mode 100755 diff --git a/.eslintrc.json b/.eslintrc.json old mode 100644 new mode 100755 diff --git a/.github/workflows/docker-image-dev.yml b/.github/workflows/docker-image-dev.yml old mode 100644 new mode 100755 index d9a73c1..9cc4276 --- a/.github/workflows/docker-image-dev.yml +++ b/.github/workflows/docker-image-dev.yml @@ -1,8 +1,7 @@ name: Silvanus-Dev Dockerization on: - push: - branches: [ "*-dev" ] + workflow_dispatch: env: DHUB_UNAME: ${{ secrets.DHUB_UNAME }} @@ -16,9 +15,15 @@ jobs: steps: - uses: actions/checkout@v3 - - name: Build the Docker image + - name: Build the Docker image v0idf1sh/silvanus-dev run: docker build . --file Dockerfile --tag v0idf1sh/silvanus-dev - name: Log into Docker Hub run: docker login -u $DHUB_UNAME -p $DHUB_PWORD - - name: Push image to Docker Hub + - name: Push image to Docker Hub v0idf1sh/silvanus-dev run: docker push v0idf1sh/silvanus-dev + - name: Set up a skeleton .env file + run: echo "TOKEN=${{secrets.DEVTOKEN}}" > .env && echo "BOTID=${{ secrets.CLIENTID }}" >> .env + - name: Install modules + run: npm i + - name: Refresh commands with Discord + run: node modules/_deploy-global.js \ No newline at end of file diff --git a/.github/workflows/docker-image-prod.yml b/.github/workflows/docker-image-prod.yml new file mode 100755 index 0000000..c334ef3 --- /dev/null +++ b/.github/workflows/docker-image-prod.yml @@ -0,0 +1,29 @@ +name: Silvanus Production Dockerization + +on: + workflow_dispatch: + +env: + DHUB_UNAME: ${{ secrets.DHUB_UNAME }} + DHUB_PWORD: ${{ secrets.DHUB_PWORD }} + +jobs: + + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Build the Docker image v0idf1sh/silvanus + run: docker build . --file Dockerfile --tag v0idf1sh/silvanus + - name: Log into Docker Hub + run: docker login -u $DHUB_UNAME -p $DHUB_PWORD + - name: Push image to Docker Hub v0idf1sh/silvanus + run: docker push v0idf1sh/silvanus + - name: Set up a skeleton .env file + run: echo "TOKEN=${{secrets.PRODTOKEN}}" > .env && echo "clientId=${{ secrets.PRODCLIENTID }}" >> .env + - name: Install modules + run: npm i + - name: Refresh commands with Discord + run: node modules/_deploy-global.js \ No newline at end of file diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml deleted file mode 100644 index b400d25..0000000 --- a/.github/workflows/docker-image.yml +++ /dev/null @@ -1,24 +0,0 @@ -name: Silvanus Dockerization - -on: - pull_request: - branches: [ "main" ] - -env: - DHUB_UNAME: ${{ secrets.DHUB_UNAME }} - DHUB_PWORD: ${{ secrets.DHUB_PWORD }} - -jobs: - - build: - - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v3 - - name: Build the Docker image - run: docker build . --file Dockerfile --tag v0idf1sh/silvanus - - name: Log into Docker Hub - run: docker login -u $DHUB_UNAME -p $DHUB_PWORD - - name: Push image to Docker Hub - run: docker push v0idf1sh/silvanus diff --git a/.gitignore b/.gitignore old mode 100644 new mode 100755 index b9e44c7..1ddee34 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ env.prod .DS_Store data/guildInfo.json data/rawstring.txt +modules/input.txt # Custom folders # gifs/* diff --git a/Dockerfile b/Dockerfile old mode 100644 new mode 100755 index 0edf60a..da7938b --- a/Dockerfile +++ b/Dockerfile @@ -5,4 +5,4 @@ WORKDIR /usr/src/app COPY package.json ./ RUN npm install COPY . . -CMD [ "node", "main.js" ] \ No newline at end of file +CMD ["/bin/sh", "-c", "node main.js 2> /logs/error.log 1> /logs/silvanus.log"] \ No newline at end of file diff --git a/README.md b/README.md old mode 100644 new mode 100755 index 017087a..85e2e8a --- a/README.md +++ b/README.md @@ -20,18 +20,27 @@ Otherwise, run `/setup` to set the proper channels for the bot to look in for th Use `/commands` to view a description of all my commands. ## Permissions -Silvanus requires permissions to `Send Messages` and `Send Messages in Threads` if applicable. +Silvanus requires permissions to `Send Messages` and `Send Messages in Threads` if applicable. If you plan to use the Role Menu Silvanus will also need permission to `Manage Roles` ## Commands * `/setup` - You only need to run this command if your server has its `/tree` and `/top trees` messages in separate channels. -* `/setupinfo` - Displays links to the current Tree and Tallest Trees messages configured in your server. +* `/setupinfo` - Displays your server's configuration information. * `/compare` - Sends a refreshable embed that calculcates the height difference between your tree and the trees currently displayed on your Tallest Trees message. There is also an Active Growth Indicator (`[💧]`). -* `/setping` - Guild members with the `Manage Roles` permission can run this command to set up automatic reminders when your tree is ready to be watered. - * Type a reminder message (including any `@pings` desired) and select a channel to send the reminders in. - * Once this command has been run a new `Reset Ping` button will appear next time you refresh the `/compare` message. - * Click the `Reset Ping` button to be sent a reminder the next time the tree is ready to be watered. - * Use `/optout` to disable reminder messages for your server. +* `/notifications` - Guild members with the `Manage Roles` permission can run this command to set up automatic reminders when your tree is ready to be watered or when fruit is dropping. + * This feature relies on Grow A Tree's built-in Notification system. Refer to Grow A Tree for instructions on setting them up. + * `watchchannel`: Select the channel you've configured Grow A Tree to send notifications in. + * `watermessage`: This option sets the message to send when the tree is ready to be watered. This can include `@pings`, links, etc. + * `pingchannel`: Select the channel you want Silvanus to forward the notifications to. + * `fruitmessage`: Optional: This sets the message to send when the tree is dropping fruit. If not set, the `watermessage` will be used instead. +* `/rolemenu` - Creates a menu for users to give themselves Water and Fruit pingable roles. + * Requires `Manage Roles` permission to run. + * `waterrole`: Select the role to give users when they select the Water button + * `fruitrole`: Optional: Select the role to give users when they select the Fruit button + * If this option isn't set, no Fruit Role will be available for self-assignment. * `/watertime` - Calculates the wait time between waters for a tree of a given height. -* `/timetoheight` - Calculates how long it would take to go from `beginheight` to `endheight`. + * `height`: The height in feet to calculate for. +* `/timetoheight` - Calculates how long it would take to grow to a height + * `endheight`: The destination height, in feet. + * `beginheight`: Optional: The starting height, in feet. If this option isn't set, the current height of your tree will be used insead. * `/reset` - Removes your server's configuration from the database. * `/help` - Displays the bot's help page and links to each command. \ No newline at end of file diff --git a/TODO.md b/TODO.md old mode 100644 new mode 100755 diff --git a/data/config.json b/data/config.json old mode 100644 new mode 100755 diff --git a/data/strings.json b/data/strings.json old mode 100644 new mode 100755 index afb5fcc..184e013 --- a/data/strings.json +++ b/data/strings.json @@ -7,7 +7,7 @@ "info": "Silvanus is the ultimate Grow A Tree companion bot! Quickly compare your server's tree to others on the leaderboard with automatic calculation of tree height differences, active growth detection, watering time calculations, and more!\n\nImportant Note: Silvanus is only as up-to-date as your server's newest Tree and Tallest Trees messages. Make sure to refresh them before refreshing Silvanus' Compare message.", "setup": "If your ``/tree`` and ``/top trees`` messages are in the same channel, simple run in that channel and you're good to go!\n\nOtherwise, run to set the proper channels for the bot to look in for the ``/tree`` and ``/top trees`` messages.\n\nUse to view a description of all my commands.", "permissions": "At a minimum, Silvanus requires permissions to `Send Messages` and `Send Messages in Threads` if applicable. If Analyzer is given permission to `Manage Messages`, the bot will delete the `.settree` and `.setranks` messages to reduce spam.", - "allCommands": " - You only need to run this command if your server has its ``/tree`` and ``/top trees`` messages in separate channels.\n - Displays links to the current Tree and Tallest Trees messages configured in your server.\n - Sends a refreshable embed that calculcates the height difference between your tree and the trees currently displayed on your Tallest Trees message. There is also an Active Growth Indicator (``[💧]``).\n - Guild members with the ``Manage Roles`` permission can run this command to set up automatic reminders when your tree is ready to be watered.\n Type a reminder message (including any ``@pings`` desired) and select a channel to send the reminders in.\n Once this command has been run a new ``Reset Ping`` button will appear next time you refresh the message.\n Click the ``Reset Ping`` button to be sent a reminder the next time the tree is ready to be watered.\n Use to disable reminder messages for your server.\n - Calculates the wait time between waters for a tree of a given height.\n - Calculates how long it would take to go from ``beginheight`` to ``endheight``.\n - Removes your server's configuration from the database.\n - Displays the bot's help page and links to each command." + "allCommands": " - You only need to run this command if your server has its ``/tree`` and ``/top trees`` messages in separate channels.\n - Displays your server's configuration information.\n - Sends a refreshable embed that calculcates the height difference between your tree and the trees currently displayed on your Tallest Trees message. There is also an Active Growth Indicator (``[💧]``).\n - Guild members with the ``Manage Roles`` permission can run this command to set up automatic reminders when your tree is ready to be watered or when fruit is dropping.\n This feature relies on Grow A Tree's built-in Notification system. Refer to Grow A Tree for instructions on setting them up.\n ``watchchannel``: Select the channel you've configured Grow A Tree to send notifications in.\n ``watermessage``: This option sets the message to send when the tree is ready to be watered. This can include ``@pings``, links, etc.\n ``pingchannel``: Select the channel you want Silvanus to forward the notifications to.\n ``fruitmessage``: Optional: This sets the message to send when the tree is dropping fruit. If not set, the ``watermessage`` will be used instead.\n - Creates a menu for users to give themselves Water and Fruit pingable roles.\n Requires ``Manage Roles`` permission to run.\n ``waterrole``: Select the role to give users when they select the Water button\n ``fruitrole``: Optional: Select the role to give users when they select the Fruit button\n If this option isn't set, no Fruit Role will be available for self-assignment.\n - Calculates the wait time between waters for a tree of a given height.\n ``height``: The height in feet to calculate for.\n - Calculates how long it would take to grow to a height\n ``endheight``: The destination height, in feet.\n ``beginheight``: Optional: The starting height, in feet. If this option isn't set, the current height of your tree will be used insead.\n - Removes your server's configuration from the database.\n - Displays the bot's help page and links to each command." }, "commands": { "compare": "", @@ -19,21 +19,39 @@ }, "embeds": { "footer": "Silvanus is not affiliated with Grow A Tree or Limbo Labs", - "color": "0x55FF55" + "color": "0x55FF55", + "errorTitle": "Oops!", + "errorPrefix": "There seems to have been a problem.", + "waterColor": "0x5555FF", + "fruitColor": "0xCC5555", + "waterTitle": "Water Notification", + "fruitTitle": "Fruit Notification", + "roleMenuTitle": "Role Menu", + "treeRoleMenu": [ + "Use the buttons below to give yourself roles.\n\n", + "``💧`` - ", + ": Get notifications when the tree is ready to be watered.", + "\n``🍎`` - ", + ": Get notifications when fruit is falling from the tree." + ], + "roleMenuFooter": "Tip: Tap the button again to remove the role." }, "emoji": { "joint": "<:joint:862082955902976000>", "next": "⏭️", "previous": "⏮️", "confirm": "☑️", - "cancel": "❌" + "cancel": "❌", + "water": "💧", + "fruit": "🍎" }, "urls": { "avatar": "https://cdn.discordapp.com/avatars/513184762073055252/12227aa23a06d5178853e59b72c7487b.webp?size=128", "supportServer": "https://discord.gg/g5JRGn7PxU" }, "error": { - "noGuild": "Setup has not been completed yet. Try running or " + "noGuild": "Setup has not been completed yet. Try running or ", + "invalidSubcommand": "Invalid subcommand detected." }, "status": { "treeAndLeaderboard": "Tree and leaderboard messages were both found, setup is complete. Run to verify. Run to get started!", @@ -42,7 +60,15 @@ "missingLeaderboardMessage": "There was a problem finding the Tallest Trees message. Please make sure the ``/tree`` and ``/top trees`` messages are in this channel, or run to set the ``/tree`` and ``/top trees`` channels.", "missingLeaderboardChannel": "There was a problem finding the Tallest Trees channel, was it deleted? Please make sure the ``/tree`` and ``/top trees`` messages are in this channel, or run to set the ``/tree`` and ``/top trees`` channels.", "missingTreeMessage": "There was a problem finding the Tree message. Please make sure the ``/tree`` and ``/top trees`` messages are this channel, or run to set the ``/tree`` and ``/top trees`` channels.", - "missingTreeChannel": "There was a problem finding the Tree channel, was it deleted? Please make sure the ``/tree`` and ``/top trees`` messages are in this channel, or run to set the ``/tree`` and ``/top trees`` channels." + "missingTreeChannel": "There was a problem finding the Tree channel, was it deleted? Please make sure the ``/tree`` and ``/top trees`` messages are in this channel, or run to set the ``/tree`` and ``/top trees`` channels.", + "reset": "All guild configuration information has been removed from the database.", + "resetError": "There was a problem deleting your guild information, contact @voidf1sh#0420 for help.", + "noRoleMenu": "A role menu has not been created for this guild yet. Run to create a role menu.", + "optout": "Notification relay has been disabled, to re-enable the relay use with no options." + }, + "notifications": { + "water": "is ready to be watered again!", + "fruit": "Fruit is appearing!" }, "temp": {} } \ No newline at end of file diff --git a/main.js b/main.js old mode 100644 new mode 100755 index fef604a..2753916 --- a/main.js +++ b/main.js @@ -4,7 +4,7 @@ const dotenv = require('dotenv'); dotenv.config(); const token = process.env.TOKEN; -const statusChannelId = process.env.statusChannelId; +const statusChannelId = process.env.STATUSCHANNELID; // Discord.JS const { Client, GatewayIntentBits, Partials, ActivityType } = require('discord.js'); @@ -25,13 +25,14 @@ const client = new Client({ const fn = require('./modules/functions.js'); const strings = require('./data/strings.json'); const dbfn = require('./modules/dbfn.js'); -const isDev = process.env.isDev; +const isDev = process.env.DEBUG; -client.once('ready', () => { - fn.collections.slashCommands(client); +client.once('ready', async () => { + await fn.collectionBuilders.slashCommands(client); + await fn.collectionBuilders.guildInfos(client); + await fn.setupCollectors(client); console.log('Ready!'); client.user.setActivity({ name: strings.activity.name, type: ActivityType.Watching }); - fn.checkReady(client); if (isDev == 'false') { client.channels.fetch(statusChannelId).then(channel => { channel.send(`${new Date().toISOString()} -- \nStartup Sequence Complete <@481933290912350209>`); @@ -42,9 +43,9 @@ client.once('ready', () => { // slash-commands client.on('interactionCreate', async interaction => { if (interaction.isCommand()) { - // if (isDev) { - // console.log(interaction); - // } + if (isDev) { + console.log(interaction); + } const { commandName } = interaction; if (client.slashCommands.has(commandName)) { @@ -55,26 +56,31 @@ client.on('interactionCreate', async interaction => { } } - if (interaction.isButton() && interaction.component.customId == 'refresh') { - // console.log(JSON.stringify(interaction)); - await fn.refresh(interaction).catch(err => { - interaction.channel.send(fn.builders.errorEmbed(err)); - }); - } else if (interaction.isButton() && interaction.component.customId == 'resetping') { - await fn.resetPing(interaction); - await fn.refresh(interaction).catch(err => { - interaction.channel.send(fn.builders.errorEmbed(err)); - }); - } else if (interaction.isButton() && interaction.component.customId == 'deleteping') { - if (interaction.message.deletable) { - await dbfn.setRemindedStatus(interaction.guildId, 0); - await dbfn.getGuildInfo(interaction.guildId).then(async res => { - const guildInfo = res.data; - await fn.refreshComparisonMessage(interaction.client, guildInfo); - }); - await interaction.message.delete().catch(err => { - console.error(err); - }); + if (interaction.isButton()) { + switch (interaction.component.customId) { + case 'refresh': + // console.log(JSON.stringify(interaction)); + await fn.refresh(interaction).catch(err => { + interaction.channel.send(fn.builders.errorEmbed(err)); + }); + break; + case 'deleteping': + if (interaction.message.deletable) { + await interaction.message.delete().catch(err => { + console.error(err); + }); + } + break; + case 'waterpingrole': + const waterPingStatus = await fn.buttonHandlers.waterPing(interaction); + await interaction.reply(waterPingStatus).catch(e => console.error(e)); + break; + case 'fruitpingrole': + const fruitPingStatus = await fn.buttonHandlers.fruitPing(interaction); + await interaction.reply(fruitPingStatus).catch(e => console.error(e)); + break; + default: + break; } } }); diff --git a/modules/CustomClasses.js b/modules/CustomClasses.js new file mode 100755 index 0000000..c35730f --- /dev/null +++ b/modules/CustomClasses.js @@ -0,0 +1,179 @@ +const mysql = require('mysql'); +const db = mysql.createConnection({ + host: process.env.DBHOST, + user: process.env.DBUSER, + password: process.env.DBPASS, + database: process.env.DBNAME, + port: process.env.DBPORT +}); + +module.exports = { + GuildInfo: class { + constructor() { + this.guildId = ""; + this.treeName = ""; + this.treeHeight = 0; + this.treeMessageId = ""; + this.treeChannelId = ""; + this.leaderboardMessageId = ""; + this.leaderboardChannelId = ""; + this.waterMessage = ""; + this.waterRoleId = ""; + this.fruitMessage = ""; + this.fruitRoleId = ""; + this.reminderChannelId = ""; + this.watchChannelId = ""; + this.notificationsEnabled = false; + } + + setId(id) { + this.guildId = id; + return this; + } + setName(name) { + this.treeName = name; + return this; + } + setHeight(height) { + this.treeHeight = height; + return this; + } + setTreeMessage(messageId, channelId) { + this.treeMessageId = messageId; + this.treeChannelId = channelId; + return this; + } + setLeaderboardMessage(messageId, channelId) { + this.leaderboardMessageId = messageId; + this.leaderboardChannelId = channelId; + return this; + } + setReminders(waterMessage, fruitMessage, reminderChannelId, watchChannelId, enabled) { + if (waterMessage) this.waterMessage = waterMessage; + if (fruitMessage) this.fruitMessage = fruitMessage; + if (reminderChannelId) this.reminderChannelId = reminderChannelId; + if (watchChannelId) this.watchChannelId = watchChannelId; + if (enabled) this.notificationsEnabled = enabled; + return this; + } + setRoles(waterRoleId, fruitRoleId) { + this.waterRoleId = waterRoleId; + if (fruitRoleId) this.fruitRoleId = fruitRoleId; + return this; + } + queryBuilder(query) { + let queryParts = []; + switch (query) { + case "setAll": + queryParts = [ + `INSERT INTO guild_info `, + `(guild_id, `, + `tree_name, `, + `tree_height, `, + `tree_message_id, `, + `tree_channel_id, `, + `leaderboard_message_id, `, + `leaderboard_channel_id, `, + `water_message, `, + `fruit_message, `, + `reminder_channel_id, `, + `watch_channel_id) `, + `VALUES (${db.escape(this.guildId)}, `, + `${db.escape(this.treeName)}, `, + `${db.escape(this.treeHeight)}, `, + `${db.escape(this.treeMessageId)}, `, + `${db.escape(this.treeChannelId)}, `, + `${db.escape(this.leaderboardMessageId)}, `, + `${db.escape(this.leaderboardChannelId)}, `, + `${db.escape(this.waterMessage)}, `, + `${db.escape(this.fruitMessage)}, `, + `${db.escape(this.reminderChannelId)}, `, + `${db.escape(this.watchChannelId)}) `, + `ON DUPLICATE KEY UPDATE tree_name = ${db.escape(this.treeName)}, `, + `tree_height = ${db.escape(this.treeHeight)}, `, + `tree_message_id = ${db.escape(this.treeMessageId)}, `, + `tree_channel_id = ${db.escape(this.treeChannelId)}, `, + `leaderboard_message_id = ${db.escape(this.leaderboardMessageId)}, `, + `leaderboard_channel_id = ${db.escape(this.leaderboardChannelId)}, `, + `water_message = ${db.escape(this.waterMessage)}, `, + `fruit_message = ${db.escape(this.fruitMessage)}, `, + `reminder_channel_id = ${db.escape(this.reminderChannelId)}, `, + `watch_channel_id = ${db.escape(this.watchChannelId)})` + ]; + return queryParts.join(''); + break; + case "setReminders": + queryParts = [ + `INSERT INTO guild_info (guild_id, water_message, fruit_message, reminder_channel_id, watch_channel_id, notifications_enabled) VALUES (`, + `${db.escape(this.guildId)},`, + `${db.escape(this.waterMessage)},`, + `${db.escape(this.fruitMessage)},`, + `${db.escape(this.reminderChannelId)},`, + `${db.escape(this.watchChannelId)},`, + `${db.escape(this.notificationsEnabled)}`, + `) ON DUPLICATE KEY UPDATE water_message = ${db.escape(this.waterMessage)}, `, + `fruit_message = ${db.escape(this.fruitMessage)}, `, + `reminder_channel_id = ${db.escape(this.reminderChannelId)}, `, + `watch_channel_id = ${db.escape(this.watchChannelId)},`, + `notifications_enabled = ${db.escape(this.notificationsEnabled)}` + ]; + return queryParts.join(''); + break; + case "setTreeMessage": + queryParts = [ + `UPDATE guild_info SET tree_message_id = ${db.escape(this.treeMessageId)}, `, + `tree_channel_id = ${db.escape(this.treeChannelId)}, `, + `WHERE guild_id = ${db.escape(this.guildId)}` + ]; + return queryParts.join(''); + break; + case "setLeaderboardMessage": + queryParts = [ + `UPDATE guild_info SET leaderboard_message_id = ${db.escape(this.leaderboardMessageId)}, `, + `leaderboard_channel_id = ${db.escape(this.leaderboardChannelId)}, `, + `WHERE guild_id = ${db.escape(this.guildId)}` + ]; + return queryParts.join(''); + break; + case "setRoles": + if (this.fruitRoleId != "") { + queryParts = [ + `INSERT INTO guild_info (`, + `guild_id, water_role_id, fruit_role_id`, + `) VALUES (`, + `${db.escape(this.guildId)}, ${db.escape(this.waterRoleId)}, ${db.escape(this.fruitRoleId)}`, + `) ON DUPLICATE KEY UPDATE water_role_id = ${db.escape(this.waterRoleId)}, `, + `fruit_role_id = ${db.escape(this.fruitRoleId)}` + ]; + } else { + queryParts = [ + `UPDATE guild_info SET water_role_id = ${db.escape(this.waterRoleId)} `, + `WHERE guild_id = ${db.escape(this.guildId)}` + ]; + } + return queryParts.join(''); + break; + default: + break; + } + } + generateSetupInfo() { + let setupInfoParts = [ + `Here is your server's configuration:`, + `Tree Name: ${this.treeName}`, + `Tree Height: ${this.treeHeight}`, + `Tree Channel: <#${this.treeChannelId}>`, + `[Tree Link](https://discord.com/channels/${this.guildId}/${this.treeChannelId}/${this.treeMessageId})`, + `Leaderboard Channel: <#${this.leaderboardChannelId}>`, + `[Leaderboard Link](https://discord.com/channels/${this.guildId}/${this.leaderboardChannelId}/${this.leaderboardMessageId})`, + `Notification Watch Channel: <#${this.watchChannelId}>`, + `Notification Relay Channel: <#${this.reminderChannelId}>`, + `Water Message: ${this.waterMessage}`, + `Fruit Message: ${this.fruitMessage}`, + `Water Role: <@&${this.waterRoleId}>`, + `Fruit Role: <@&${this.fruitRoleId}>` + ] + return setupInfoParts.join('\n'); + } + } +} \ No newline at end of file diff --git a/modules/_clear-commands.js b/modules/_clear-commands.js old mode 100644 new mode 100755 diff --git a/modules/_deploy-commands.js b/modules/_deploy-commands.js old mode 100644 new mode 100755 diff --git a/modules/_deploy-global.js b/modules/_deploy-global.js old mode 100644 new mode 100755 index a225989..3d90514 --- a/modules/_deploy-global.js +++ b/modules/_deploy-global.js @@ -4,7 +4,7 @@ dotenv.config(); const { REST } = require('@discordjs/rest'); const { Routes } = require('discord-api-types/v9'); -const clientId = process.env.clientId; +const clientId = process.env.BOTID; const token = process.env.TOKEN; const fs = require('fs'); @@ -18,13 +18,28 @@ for (const file of commandFiles) { } } -console.log(commands); +console.log(`Token: ${token} Client ID: ${clientId}`); const rest = new REST({ version: '9' }).setToken(token); -(async () => { +async function deleteCommands() { try { - console.log('Started refreshing application (/) commands.'); + console.log('Started deleting application (/) commands.'); + + await rest.put( + Routes.applicationCommands(clientId), + { body: "" }, + ); + + console.log('Successfully deleted application (/) commands.'); + } catch (error) { + console.error(error); + } +} + +async function uploadCommands() { + try { + console.log('Started reloading application (/) commands.'); await rest.put( Routes.applicationCommands(clientId), @@ -36,4 +51,9 @@ const rest = new REST({ version: '9' }).setToken(token); } catch (error) { console.error(error); } +} + +(async () => { + await deleteCommands(); + await uploadCommands(); })(); \ No newline at end of file diff --git a/modules/_prepareStrings.js b/modules/_prepareStrings.js old mode 100644 new mode 100755 index 1c26722..cb2ae16 --- a/modules/_prepareStrings.js +++ b/modules/_prepareStrings.js @@ -14,20 +14,21 @@ const fs = require('fs'); const replaceAll = require('string.prototype.replaceall'); -const string = fs.readFileSync('./data/rawstring.txt').toString(); +const path = "./modules/input.txt"; +const string = fs.readFileSync(path).toString(); let newString = replaceAll(string, '\* ', ''); -newString = replaceAll(newString, '\n', '\\n'); +newString = replaceAll(newString, '\r\n', '\\n'); newString = replaceAll(newString, '\t', ' - '); newString = replaceAll(newString, '`/setup`', ''); newString = replaceAll(newString, '`/setupinfo`', ''); newString = replaceAll(newString, '`/compare`', ''); -newString = replaceAll(newString, '`/setping`', ''); -newString = replaceAll(newString, '`/optout`', ''); newString = replaceAll(newString, '`/watertime`', ''); newString = replaceAll(newString, '`/timetoheight`', ''); newString = replaceAll(newString, '`/reset`', ''); newString = replaceAll(newString, '`/help`', ''); newString = replaceAll(newString, '`/commands`', ''); +newString = replaceAll(newString, '`/notifications`', ''); +newString = replaceAll(newString, '`/rolemenu`', ''); newString = replaceAll(newString, '`', '``'); -fs.writeFileSync('./data/rawstring.txt', newString); +fs.writeFileSync(path, newString); return "Done"; \ No newline at end of file diff --git a/modules/dbfn.js b/modules/dbfn.js old mode 100644 new mode 100755 index 5eac3e4..2eb1854 --- a/modules/dbfn.js +++ b/modules/dbfn.js @@ -2,6 +2,7 @@ const dotenv = require('dotenv'); dotenv.config(); const debugMode = process.env.DEBUG || true; const mysql = require('mysql'); +const { GuildInfo } = require('./CustomClasses.js'); /* Table Structures guild_info @@ -33,42 +34,6 @@ leaderboard */ module.exports = { - createGuildTables(guildId) { - const db = mysql.createConnection({ - host: process.env.DBHOST, - user: process.env.DBUSER, - password: process.env.DBPASS, - database: process.env.DBNAME, - port: process.env.DBPORT - }); - db.connect((err) => { - if (err) throw `Error connecting to the database: ${err.message}`; - }); - // Create the guild-information and rank-information tables to be used. - const createGuildInfoTableQuery = "CREATE TABLE IF NOT EXISTS guild_info(guild_id VARCHAR(50) NOT NULL, tree_name VARCHAR(100) NOT NULL DEFAULT 'Run /setup where your tree is.', tree_height INT(10) NOT NULL DEFAULT 0, tree_message_id VARCHAR(50) NOT NULL DEFAULT 'Run /setup where your tree is.', tree_channel_id VARCHAR(50) NOT NULL DEFAULT 'Run /setup where your tree is.', leaderboard_message_id VARCHAR(50) NOT NULL DEFAULT 'Run /setup where your leaderboard is.', leaderboard_channel_id VARCHAR(50) NOT NULL DEFAULT 'Run /setup where your leaderboard is.', CONSTRAINT guild_pk PRIMARY KEY (guild_id))"; - const createLeaderboardTableQuery = "CREATE TABLE IF NOT EXISTS leaderboard(id INT(10) NOT NULL AUTO_INCREMENT,guild_id VARCHAR(50) NOT NULL,tree_name VARCHAR(100) NOT NULL,tree_rank INT(10) NOT NULL,tree_height INT(10) NOT NULL DEFAULT 1,has_pin TINYINT(1) NOT NULL DEFAULT 0,timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, CONSTRAINT id_pk PRIMARY KEY(id))"; - // TODO run the queries, then add a call to this function at the beginning of main.js or functions.js - return new Promise((resolve, reject) => { - db.query(createGuildInfoTableQuery, (err) => { - if (err) { - reject("Error creating the guild_info table: " + err.message); - console.error("Offending query: " + createGuildInfoTableQuery); - db.end(); - return; - } - db.query(createLeaderboardTableQuery, (err) => { - if (err) { - reject("Error creating the leaderboard table: " + err.message); - console.error("Offending query: " + createLeaderboardTableQuery); - db.end(); - return; - } - resolve({ "status": "Successfully checked both tables.", "data": null }); - db.end(); - }); - }); - }); - }, getGuildInfo(guildId) { const db = mysql.createConnection({ host: process.env.DBHOST, @@ -91,45 +56,26 @@ module.exports = { db.end(); return; } - /*const guildInfo = { "guildId": "123", - "treeName": "name", - "treeHeight": 123, - "treeMessageId": "123", - "treeChannelId": "123", - "leaderboardMessageId": "123", - "leaderboardChannelId": "123", - "reminderMessage": "Abc", - "reminderChannelId": "123", - "remindedStatus": 0, - "comparisonMessageId": "123" - };*/ if (res.length == 0) { reject("There is no database entry for your guild yet. Try running /setup"); db.end(); return; } row = res[0]; - const guildInfo = { - "guildId": guildId, - "treeName": row.tree_name, - "treeHeight": row.tree_height, - "treeMessageId": row.tree_message_id, - "treeChannelId": row.tree_channel_id, - "leaderboardMessageId": row.leaderboard_message_id, - "leaderboardChannelId": row.leaderboard_channel_id, - "reminderMessage": row.ping_role_id, - "reminderChannelId": row.ping_channel_id, - "remindedStatus": row.reminded_status, - "reminderOptIn": row.reminder_optin, - "comparisonMessageId": row.comparison_message_id, - "comparisonChannelId": row.comparison_channel_id - }; + const guildInfo = new GuildInfo() + .setId(row.guild_id) + .setName(row.tree_name) + .setHeight(row.tree_height) + .setTreeMessage(row.tree_message_id, row.tree_channel_id) + .setLeaderboardMessage(row.leaderboard_message_id, row.leaderboard_channel_id) + .setReminders(row.water_message, row.fruit_message, row.reminder_channel_id, row.watch_channel_id, row.notifications_enabled) + .setRoles(row.water_role_id, row.fruit_role_id); db.end(); - resolve({ "status": "Successfully fetched guild information", "data": guildInfo }); + resolve(guildInfo); }); }); }, - setGuildInfo(guildInfo) { + getAllGuildInfos() { const db = mysql.createConnection({ host: process.env.DBHOST, user: process.env.DBUSER, @@ -140,21 +86,62 @@ module.exports = { db.connect((err) => { if (err) throw `Error connecting to the database: ${err.message}`; }); - // Returns a Promise, resolve({ "status": "", "data": null }) - // guildInfo = { "guildId": "123", "treeName": "name", "treeHeight": 123, "treeMessageId": "123", "treeChannelId": "123", "leaderboardMessageId": "123", "leaderboardChannelId": "123"} - // Set a server's tree information in the database - const insertGuildInfoQuery = `INSERT INTO guild_info (guild_id, tree_name, tree_height, tree_message_id, tree_channel_id, leaderboard_message_id, leaderboard_channel_id) VALUES (${db.escape(guildInfo.guildId)}, ${db.escape(guildInfo.treeName)}, ${db.escape(guildInfo.treeHeight)},${db.escape(guildInfo.treeMessageId)}, ${db.escape(guildInfo.treeChannelId)}, ${db.escape(guildInfo.leaderboardMessageId)}, ${db.escape(guildInfo.leaderboardChannelId)}) ON DUPLICATE KEY UPDATE tree_name = ${db.escape(guildInfo.treeName)},tree_height = ${db.escape(guildInfo.treeHeight)},tree_message_id = ${db.escape(guildInfo.treeMessageId)},tree_channel_id = ${db.escape(guildInfo.treeChannelId)},leaderboard_message_id = ${db.escape(guildInfo.leaderboardMessageId)},leaderboard_channel_id = ${db.escape(guildInfo.leaderboardChannelId)}`; - // TODO run this query and return a promise, then resolve with { "status": , "data": null } + // Get a server's tree information from the database + const query = 'SELECT * FROM guild_info'; + // TODO run this query and return a promise then structure the output into a GuildInfo object. resolve with { "status": , "data": guildInfo } return new Promise((resolve, reject) => { - db.query(insertGuildInfoQuery, (err, res) => { + db.query(query, (err, res) => { if (err) { console.error(err); + reject("Error fetching all guild infos: " + err.message); + db.end(); + return; + } + if (res.length == 0) { + reject("There are no servers yet!"); + db.end(); + return; + } + let guildInfos = []; + for (let i = 0; i < res.length; i++) { + let row = res[i]; + guildInfos.push(new GuildInfo() + .setId(row.guild_id) + .setName(row.tree_name) + .setHeight(row.tree_height) + .setTreeMessage(row.tree_message_id, row.tree_channel_id) + .setLeaderboardMessage(row.leaderboard_message_id, row.leaderboard_channel_id) + .setReminders(row.water_message, row.fruit_message, row.reminder_channel_id, row.watch_channel_id, row.notifications_enabled) + .setRoles(row.water_role_id, row.fruit_role_id) + ); + } + + db.end(); + resolve(guildInfos); + }); + }); + }, + setGuildInfo(query) { + const db = mysql.createConnection({ + host: process.env.DBHOST, + user: process.env.DBUSER, + password: process.env.DBPASS, + database: process.env.DBNAME, + port: process.env.DBPORT + }); + db.connect((err) => { + if (err) throw `Error connecting to the database: ${err.message}`; + }); + return new Promise((resolve, reject) => { + db.query(query, (err, res) => { + if (err) { + console.error(err + "\n" + query); reject("Error setting the guild info: " + err.message); db.end(); return; } db.end(); - resolve({ "status": "Successfully set the guild information", "data": null }); + resolve(); }); }); }, @@ -352,177 +339,5 @@ module.exports = { resolve({ "status": "Successfully fetched historic 24hr tree.", "data": hist24hTree }); }); }); - }, - setReminderInfo(guildId, reminderMessage, reminderChannelId) { - const db = mysql.createConnection({ - host: process.env.DBHOST, - user: process.env.DBUSER, - password: process.env.DBPASS, - database: process.env.DBNAME, - port: process.env.DBPORT - }); - db.connect((err) => { - if (err) throw `Error connecting to the database: ${err.message}`; - }); - // Returns a Promise, resolve({ "status": "", "data": leaderboard }) - const insertReminderInfoQuery = `UPDATE guild_info SET ping_role_id = ${db.escape(reminderMessage)}, ping_channel_id = ${db.escape(reminderChannelId)} WHERE guild_id = ${db.escape(guildId)}`; - // TODO run the query and return a promise then process the results. resolve with { "status": , "data": leaderboard } - return new Promise((resolve, reject) => { - db.query(insertReminderInfoQuery, (err, res) => { - if (err) { - console.error(err); - db.end(); - reject("Error updating the reminder info: " + err.message); - return; - } - db.end(); - resolve({ "status": `Successfully set the reminder message to "${reminderMessage}" in <#${reminderChannelId}>`, "data": res }); - }); - }); - }, - setRemindedStatus(guildId, remindedStatus) { - const db = mysql.createConnection({ - host: process.env.DBHOST, - user: process.env.DBUSER, - password: process.env.DBPASS, - database: process.env.DBNAME, - port: process.env.DBPORT - }); - db.connect((err) => { - if (err) throw `Error connecting to the database: ${err.message}`; - }); - // Returns a Promise, resolve({ "status": "", "data": leaderboard }) - const setRemindedStatusQuery = `UPDATE guild_info SET reminded_status = ${db.escape(remindedStatus)} WHERE guild_id = ${db.escape(guildId)}`; - // TODO run the query and return a promise then process the results. resolve with { "status": , "data": leaderboard } - return new Promise((resolve, reject) => { - db.query(setRemindedStatusQuery, (err, res) => { - if (err) { - console.error(err); - db.end(); - reject("Error updating the reminded status: " + err.message); - return; - } - db.end(); - resolve({ "status": `Successfully set the reminded status to ${remindedStatus}`, "data": res }); - // console.log("Boop: " + remindedStatus); - }); - }); - }, - setReminderOptIn(guildId, optIn) { - const db = mysql.createConnection({ - host: process.env.DBHOST, - user: process.env.DBUSER, - password: process.env.DBPASS, - database: process.env.DBNAME, - port: process.env.DBPORT - }); - db.connect((err) => { - if (err) throw `Error connecting to the database: ${err.message}`; - }); - // Returns a Promise, resolve({ "status": "", "data": leaderboard }) - const setReminderOptInQuery = `UPDATE guild_info SET reminder_optin = ${db.escape(optIn)} WHERE guild_id = ${db.escape(guildId)}`; - // TODO run the query and return a promise then process the results. resolve with { "status": , "data": leaderboard } - return new Promise((resolve, reject) => { - db.query(setReminderOptInQuery, (err, res) => { - if (err) { - console.error(err); - db.end(); - reject("Error updating the reminder opt-in status: " + err.message); - return; - } - db.end(); - resolve({ "status": `Successfully set the reminder opt-in status to ${optIn}`, "data": res }); - }); - }); - }, - getOptedInGuilds() { - const db = mysql.createConnection({ - host: process.env.DBHOST, - user: process.env.DBUSER, - password: process.env.DBPASS, - database: process.env.DBNAME, - port: process.env.DBPORT - }); - db.connect((err) => { - if (err) throw `Error connecting to the database: ${err.message}`; - }); - // Get a server's tree information from the database - const getOptedInGuildsQuery = `SELECT * FROM guild_info WHERE reminder_optin = 1`; - // TODO run this query and return a promise then structure the output into a GuildInfo object. resolve with { "status": , "data": guildInfo } - return new Promise((resolve, reject) => { - db.query(getOptedInGuildsQuery, (err, res) => { - if (err) { - console.error(err); - reject("Error fetching guild information: " + err.message); - db.end(); - return; - } - /*const guildInfo = { "guildId": "123", - "treeName": "name", - "treeHeight": 123, - "treeMessageId": "123", - "treeChannelId": "123", - "leaderboardMessageId": "123", - "leaderboardChannelId": "123", - "reminderMessage": "Abc", - "reminderChannelId": "123", - "remindedStatus": 0, - "comparisonMessageId": "123" - };*/ - if (res.length == 0) { - resolve({ "status": "No servers have opted in yet" }); - db.end(); - return; - } - row = res[0]; - let guilds = []; - res.forEach(row => { - guilds.push({ - "guildId": row.guild_id, - "treeName": row.tree_name, - "treeHeight": row.tree_height, - "treeMessageId": row.tree_message_id, - "treeChannelId": row.tree_channel_id, - "leaderboardMessageId": row.leaderboard_message_id, - "leaderboardChannelId": row.leaderboard_channel_id, - "reminderMessage": row.ping_role_id, - "reminderChannelId": row.ping_channel_id, - "remindedStatus": row.reminded_status, - "comparisonMessageId": row.comparison_message_id, - "comparisonChannelId": row.comparison_channel_id - }); - }); - db.end(); - resolve({ "status": "Successfully fetched guild information", "data": guilds }); - }); - }); - }, - setComparisonMessage(comparisonMessage, guildId) { - const db = mysql.createConnection({ - host: process.env.DBHOST, - user: process.env.DBUSER, - password: process.env.DBPASS, - database: process.env.DBNAME, - port: process.env.DBPORT - }); - db.connect((err) => { - if (err) throw `Error connecting to the database: ${err.message}`; - }); - // Returns a Promise, resolve({ "status": "", "data": leaderboard }) - const setComparisonMessageQuery = `UPDATE guild_info SET comparison_message_id = ${db.escape(comparisonMessage.id)}, comparison_channel_id = ${db.escape(comparisonMessage.channel.id)} WHERE guild_id = ${db.escape(guildId)}`; - // console.log(JSON.stringify(comparisonMessage)); - // TODO run the query and return a promise then process the results. resolve with { "status": , "data": leaderboard } - return new Promise((resolve, reject) => { - db.query(setComparisonMessageQuery, (err, res) => { - if (err) { - console.error(err); - db.end(); - reject("Error updating the comparison message ID: " + err.message); - return; - } - db.end(); - resolve({ "status": `Successfully set the comparison message ID: ${comparisonMessage}`, "data": res }); - }); - }); } }; \ No newline at end of file diff --git a/modules/functions.js b/modules/functions.js old mode 100644 new mode 100755 index 90d2e97..fcd3268 --- a/modules/functions.js +++ b/modules/functions.js @@ -11,6 +11,7 @@ const fs = require('fs'); // Discord.js const Discord = require('discord.js'); const { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } = Discord; +const { GuildInfo } = require('./CustomClasses'); // Various imports from other files const config = require('../data/config.json'); @@ -19,15 +20,9 @@ const slashCommandFiles = fs.readdirSync('./slash-commands/').filter(file => fil const dbfn = require('./dbfn.js'); const { finished } = require('stream'); -dbfn.createGuildTables().then(res => { - console.log(res.status); -}).catch(err => { - console.error(err); -}); - const functions = { // Functions for managing and creating Collections - collections: { + collectionBuilders: { // Create the collection of slash commands slashCommands(client) { if (!client.slashCommands) client.slashCommands = new Discord.Collection(); @@ -39,6 +34,15 @@ const functions = { } } if (isDev) console.log('Slash Commands Collection Built'); + }, + async guildInfos(client) { + const guildInfos = await dbfn.getAllGuildInfos(); + if (!client.guildInfos) client.guildInfos = new Discord.Collection(); + client.guildInfos.clear(); + for (const guildInfo of guildInfos) { + client.guildInfos.set(guildInfo.guildId, guildInfo); + } + return 'guildInfos Collection Built'; } }, builders: { @@ -64,43 +68,51 @@ const functions = { .addComponents( refreshButton ); - if (guildInfo.reminderOptIn == 1 && guildInfo.remindedStatus == 1) { - const resetPingButton = new ButtonBuilder() - .setCustomId('resetping') - .setLabel('Reset Ping') - .setStyle(ButtonStyle.Secondary); - refreshActionRow.addComponents(resetPingButton); - } else if (guildInfo.reminderOptIn == 1 && guildInfo.remindedStatus == 0) { - const resetPingButton = new ButtonBuilder() - .setCustomId('resetping') - .setLabel('[Armed]') - .setStyle(ButtonStyle.Secondary); - refreshActionRow.addComponents(resetPingButton); - } return refreshActionRow; + }, + treeRoleMenu(fruit) { + let actionRow = new ActionRowBuilder().addComponents(this.buttons.waterPing()); + if (fruit) { + actionRow.addComponents(this.buttons.fruitPing()); + } + return actionRow; + }, + buttons: { + acceptRules() { + return new ButtonBuilder() + .setCustomId('acceptrules') + .setLabel(`${strings.emoji.confirm} Accept Rules`) + .setStyle(ButtonStyle.Primary); + }, + waterPing() { + return new ButtonBuilder() + .setCustomId('waterpingrole') + .setLabel(strings.emoji.water) + .setStyle(ButtonStyle.Primary); + }, + fruitPing() { + return new ButtonBuilder() + .setCustomId('fruitpingrole') + .setLabel(strings.emoji.fruit) + .setStyle(ButtonStyle.Primary); + } } }, - async refreshAction(guildId) { - // Create the button to go in the Action Row - const refreshButton = new ButtonBuilder() - .setCustomId('refresh') - .setLabel('Refresh') - .setStyle(ButtonStyle.Primary); - const resetPingButton = new ButtonBuilder() - .setCustomId('resetping') - .setLabel('Reset Ping') - .setStyle(ButtonStyle.Secondary); - // Create the Action Row with the Button in it, to be sent with the Embed - let refreshActionRow = new ActionRowBuilder() - .addComponents( - refreshButton - ); - const getGuildInfoResponse = await dbfn.getGuildInfo(guildId); - const guildInfo = getGuildInfoResponse.data; - if (guildInfo.reminderMessage != "" && guildInfo.reminderChannelId != "") { - refreshActionRow.addComponents(resetPingButton); + embeds: { + treeRoleMenu(guildInfo) { + const actionRow = functions.builders.actionRows.treeRoleMenu(guildInfo.fruitRoleId == "" ? false : true); + let tempStrings = strings.embeds.treeRoleMenu; + let description = tempStrings[0] + tempStrings[1] + `<@&${guildInfo.waterRoleId}>` + tempStrings[2]; + if (guildInfo.fruitRoleId != "") { + description += tempStrings[3] + `<@&${guildInfo.fruitRoleId}>` + tempStrings[4]; + } + const embed = new EmbedBuilder() + .setColor(strings.embeds.color) + .setTitle(strings.embeds.roleMenuTitle) + .setDescription(description) + .setFooter({ text: strings.embeds.roleMenuFooter }); + return { embeds: [embed], components: [actionRow] }; } - return refreshActionRow; }, comparisonEmbed(content, guildInfo) { // Create the embed using the content passed to this function @@ -112,11 +124,21 @@ const functions = { const messageContents = { embeds: [embed], components: [this.actionRows.comparisonActionRow(guildInfo)] }; return messageContents; }, - reminderEmbed(content, guildInfo) { + waterReminderEmbed(content, guildInfo) { // Create the embed using the content passed to this function const embed = new EmbedBuilder() - .setColor(strings.embeds.color) - .setTitle('Water Reminder') + .setColor(strings.embeds.waterColor) + .setTitle(strings.embeds.waterTitle) + .setDescription(`[Click here to go to your Tree](https://discord.com/channels/${guildInfo.guildId}/${guildInfo.treeChannelId}/${guildInfo.treeMessageId})`) + .setFooter({ text: `Click ♻️ to delete this message` }); + const messageContents = { content: content, embeds: [embed], components: [this.actionRows.reminderActionRow()] }; + return messageContents; + }, + fruitReminderEmbed(content, guildInfo) { + // Create the embed using the content passed to this function + const embed = new EmbedBuilder() + .setColor(strings.embeds.fruitColor) + .setTitle(strings.embeds.fruitTitle) .setDescription(`[Click here to go to your Tree](https://discord.com/channels/${guildInfo.guildId}/${guildInfo.treeChannelId}/${guildInfo.treeMessageId})`) .setFooter({ text: `Click ♻️ to delete this message` }); const messageContents = { content: content, embeds: [embed], components: [this.actionRows.reminderActionRow()] }; @@ -135,8 +157,8 @@ const functions = { errorEmbed(content) { const embed = new EmbedBuilder() .setColor(0xFF0000) - .setTitle('Error!') - .setDescription("Error: " + content) + .setTitle(strings.embeds.errorTitle) + .setDescription(`${strings.embeds.errorPrefix}\n${content}`) .setFooter({ text: `v${package.version} - ${strings.embeds.footer}` }); const messageContents = { embeds: [embed], ephemeral: true }; return messageContents; @@ -423,8 +445,9 @@ const functions = { // await dbfn.setGuildInfo(guildInfo); // Bundle guildInfo into the response - const getGuildInfoResponse = await dbfn.getGuildInfo(guildInfo.guildId); - response.data = getGuildInfoResponse.data; + // const getGuildInfoResponse = await dbfn.getGuildInfo(guildInfo.guildId); + await functions.collectionBuilders.guildInfos(interaction.client); + response.data = interaction.client.guildInfos.get(guildInfo.guildId); // Set the response status, this is only used as a response to /setup if (treeFound && leaderboardFound) { // we found both the tree and leaderboard @@ -458,9 +481,57 @@ const functions = { } } }, + buttonHandlers: { + async fruitPing(interaction) { + if (interaction.client.guildInfos.has(interaction.guildId)) { + let guildInfo = interaction.client.guildInfos.get(interaction.guildId); + const role = await functions.roles.fetchRole(interaction.guild, guildInfo.fruitRoleId); + let status = "No Changes Made"; + if (interaction.member.roles.cache.some(role => role.id == guildInfo.fruitRoleId)) { + await functions.roles.takeRole(interaction.member, role); + status = "Removed the fruit role."; + } else { + await functions.roles.giveRole(interaction.member, role); + status = "Added the fruit role."; + } + return functions.builders.embed(status); + } else { + throw "Guild doesn't exist in database!"; + } + }, + async waterPing(interaction) { + if (interaction.client.guildInfos.has(interaction.guildId)) { + let guildInfo = interaction.client.guildInfos.get(interaction.guildId); + let status = "No Changes Made"; + const role = await functions.roles.fetchRole(interaction.guild, guildInfo.waterRoleId); + if (interaction.member.roles.cache.some(role => role.id == guildInfo.waterRoleId)) { + await functions.roles.takeRole(interaction.member, role); + status = "Removed the water role."; + } else { + await functions.roles.giveRole(interaction.member, role); + status = "Added the water role."; + } + return functions.builders.embed(status); + } else { + throw "Guild doesn't exist in database!"; + } + } + }, + roles: { + async fetchRole(guild, roleId) { + return await guild.roles.fetch(roleId).catch(err => console.error("Error fetching the role: " + err + "\n" + roleId)); + }, + async giveRole(member, role) { + await member.roles.add(role).catch(err => console.error("Error giving the role: " + err + "\n" + JSON.stringify(role))); + }, + async takeRole(member, role) { + await member.roles.remove(role).catch(err => console.error("Error taking the role: " + err + "\n" + JSON.stringify(role))); + } + }, async refresh(interaction) { - const getGuildInfoResponse = await dbfn.getGuildInfo(interaction.guildId); - let guildInfo = getGuildInfoResponse.data; + // const getGuildInfoResponse = await dbfn.getGuildInfo(interaction.guildId); + // let guildInfo = getGuildInfoResponse.data; + let guildInfo = interaction.client.guildInfos.get(interaction.guild.id); const findMessagesResponse = await this.messages.find(interaction, guildInfo); if (findMessagesResponse.code == 1) { guildInfo = findMessagesResponse.data; @@ -472,17 +543,15 @@ const functions = { const comparedRankings = await this.rankings.compare(interaction, guildInfo); const embed = this.builders.comparisonEmbed(comparedRankings, guildInfo); - await interaction.update(embed).then(async interactionResponse => { - // console.log(interactionResponse.interaction.message); - await dbfn.setComparisonMessage(interactionResponse.interaction.message, interaction.guildId); - }); + await interaction.update(embed).catch(e => console.error(e)); } else { await interaction.update(this.builders.errorEmbed(findMessagesResponse.status)); } }, - reset(guildId) { + reset(interaction) { return new Promise((resolve, reject) => { - dbfn.deleteGuildInfo(guildId).then(res => { + dbfn.deleteGuildInfo(interaction.guildId).then(res => { + functions.collectionBuilders.guildInfos(interaction.client); resolve(res); }).catch(err => { console.error(err); @@ -491,24 +560,31 @@ const functions = { }); }); }, - getInfo(guildId) { - return new Promise((resolve, reject) => { - dbfn.getGuildInfo(guildId).then(res => { - let guildInfo = res.data; - let guildInfoString = ""; - guildInfoString += `Tree Message: https://discord.com/channels/${guildId}/${guildInfo.treeChannelId}/${guildInfo.treeMessageId}\n`; - guildInfoString += `Rank Message: https://discord.com/channels/${guildId}/${guildInfo.leaderboardChannelId}/${guildInfo.leaderboardMessageId}\n`; - resolve(`Here is your servers setup info:\n${guildInfoString}`); - }).catch(err => { - console.error(err); - reject(err); - return; - }) - }); - }, getWaterTime(size) { return Math.floor(Math.pow(size * 0.07 + 5, 1.1)); // Seconds }, + parseWaterTime(seconds) { + // 60 secs in min + // 3600 secs in hr + // 86400 sec in day + + let waterParts = { + value: seconds, + units: "secs" + }; + + if (60 < seconds && seconds <= 3600) { // Minutes + waterParts.value = parseFloat(seconds / 60).toFixed(1); + waterParts.units = "mins"; + } else if (3600 < seconds && seconds <= 86400) { + waterParts.value = parseFloat(seconds / 3600).toFixed(1); + waterParts.units = "hrs"; + } else if (86400 < seconds) { + waterParts.value = parseFloat(seconds / 86400).toFixed(1); + waterParts.units = "days"; + } + return `${waterParts.value} ${waterParts.units}`; + }, timeToHeight(beginHeight, destHeight) { return new Promise((resolve, reject) => { let time = 0; @@ -545,102 +621,51 @@ const functions = { }, ms); }); }, - async sendReminder(guildInfo, guild) { - const { guildId, reminderChannelId, reminderMessage } = guildInfo; - const reminderChannel = await guild.channels.fetch(reminderChannelId); - const reminderEmbed = functions.builders.reminderEmbed(reminderMessage, guildInfo); - await reminderChannel.send(reminderEmbed).then(async m => { - const setRemindedStatusReponse = await dbfn.setRemindedStatus(guildId, 1); - return 1; - }).catch(err => { + async sendWaterReminder(guildInfo, message, channelId, guild) { + const reminderChannel = await guild.channels.fetch(channelId); + const reminderEmbed = functions.builders.waterReminderEmbed(message, guildInfo); + await reminderChannel.send(reminderEmbed).catch(err => { console.error(err); }); }, - async setReminder(interaction, ms) { - setTimeout(this.sendReminder(interaction), ms); - }, - async checkReady(client) { // Check if the guilds trees are ready to water - // let time = new Date(Date.now()); - // console.log("Ready check " + time.getSeconds()); - try { - // Get the guildInfos for each guild that is opted in and waiting to send a reminder - const getOptedInGuildsResponse = await dbfn.getOptedInGuilds(); - // getOptedInGuilds will return this if it gets an empty set from the database - if (getOptedInGuildsResponse.status != "No servers have opted in yet") { - // Get the Array of Guilds from the response - const guilds = getOptedInGuildsResponse.data; - // Iterate over the Array - for (let i = 0; i < guilds.length; i++) { - // console.log(`iter: ${i}`); - // Save the 'old' guild info that came from getOptedInGuilds - const oldGuildInfo = guilds[i]; - // Get up-to-date guildInfo from the database, probably unnecessary and redundant - const getGuildInfoResponse = await dbfn.getGuildInfo(oldGuildInfo.guildId); - // Save the new guildInfo so we can reference its remindedStatus - const guildInfo = getGuildInfoResponse.data; - const { guildId, treeChannelId, treeMessageId, remindedStatus } = guildInfo; - // console.log(`${guildInfo.treeName}: ${remindedStatus}`); - // Double check the remindedStatus to prevent double pings - if (remindedStatus == 0) { - // Fetch the guild - const guild = await client.guilds.fetch(guildId); - // Fetch the tree channel - const treeChannel = await guild.channels.fetch(treeChannelId); - // Fetch the tree message - const treeMessage = await treeChannel.messages.fetch(treeMessageId); - // Get the description from the embed of the tree message - const description = treeMessage.embeds[0].description; - // Default to not being ready to water - let readyToWater = false; - // Obviously if the tree says it's Ready to be watered, it's ready - if (description.includes("Ready to be watered")) { - readyToWater = true; - // But sometimes the tree doesn't refresh the embed, in that case we'll do a secondary check using the - // timestamp included in the embed. - } else { - const beginWaterTimestamp = description.indexOf(""); - // Split the description starting at "" to get just the numerical timestamp - const waterTimestamp = parseInt(description.slice(beginWaterTimestamp, endWaterTimestamp)); - // The Discord timestamp is in seconds, not ms so we need to divide by 1000 - const nowTimestamp = (Date.now() / 1000); - readyToWater = (nowTimestamp > waterTimestamp); - } - - if (readyToWater) { - // Send the reminder message - await this.sendReminder(guildInfo, guild); - guildInfo.remindedStatus = 1; - await this.refreshComparisonMessage(client, guildInfo); - } - } - } - await this.sleep(5000); - this.checkReady(client); - } else { - // console.log(getOptedInGuildsResponse.status); - await this.sleep(5000); - this.checkReady(client); - } - } catch (err) { + async sendFruitReminder(guildInfo, message, channelId, guild) { + const reminderChannel = await guild.channels.fetch(channelId); + const reminderEmbed = functions.builders.fruitReminderEmbed(message, guildInfo); + await reminderChannel.send(reminderEmbed).catch(err => { console.error(err); - await this.sleep(30000); - this.checkReady(client); - } + }); }, - async refreshComparisonMessage(client, guildInfo) { - if (guildInfo.comparisonChannelId != "" && guildInfo.comparisonMessageId != "") { - const guild = await client.guilds.fetch(guildInfo.guildId); - const comparisonChannel = await guild.channels.fetch(guildInfo.comparisonChannelId); - const comparisonMessage = await comparisonChannel.messages.fetch(guildInfo.comparisonMessageId); - const embed = comparisonMessage.embeds[0]; - const actionRow = this.builders.actionRows.comparisonActionRow(guildInfo); - await comparisonMessage.edit({ components: [actionRow] }); - return; - } - }, - async resetPing(interaction) { - await dbfn.setRemindedStatus(interaction.guildId, 0); + async setupCollectors(client) { + let guildInfos = client.guildInfos; + guildInfos.set("collectors", []); + await guildInfos.forEach(async guildInfo => { + if ( guildInfo instanceof GuildInfo && guildInfo.watchChannelId != "" && guildInfo.notificationsEnabled) { + const guild = await client.guilds.fetch(guildInfo.guildId); + // console.log(guildInfo instanceof GuildInfo); + const channel = await guild.channels.fetch(guildInfo.watchChannelId); + const filter = message => { + return message.author.id != process.env.BOTID; + } + const collector = channel.createMessageCollector({ filter }); + collector.on('collect', message => { + if (message.content.toLowerCase().includes("water ping")) { + this.sendWaterReminder(guildInfo, guildInfo.waterMessage, guildInfo.reminderChannelId, guild); + return; + } else if (message.content.toLowerCase().includes("fruit ping")) { + this.sendFruitReminder(guildInfo, guildInfo.fruitMessage, guildInfo.reminderChannelId, guild); + return; + } + if (message.embeds == undefined) return; + if (message.embeds.length == 0) return; + guildInfo = client.guildInfos.get(guild.id); + if (message.embeds[0].data.description.includes(strings.notifications.water)) { + this.sendWaterReminder(guildInfo, guildInfo.waterMessage, guildInfo.reminderChannelId, guild); + } else if (message.embeds[0].data.description.includes(strings.notifications.fruit)) { + this.sendFruitReminder(guildInfo, guildInfo.fruitMessage, guildInfo.reminderChannelId, guild); + } + }); + } + }); } }; diff --git a/modules/testing.js b/modules/testing.js new file mode 100755 index 0000000..e69de29 diff --git a/package.json b/package.json old mode 100644 new mode 100755 index 9a6acfc..12e8912 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "silvanus", - "version": "1.1.4", + "version": "1.2.1", "description": "Grow A Tree Companion Bot", "main": "main.js", "scripts": { diff --git a/slash-commands/.DS_Store b/slash-commands/.DS_Store old mode 100644 new mode 100755 diff --git a/slash-commands/commands.js b/slash-commands/commands.js old mode 100644 new mode 100755 diff --git a/slash-commands/compare.js b/slash-commands/compare.js old mode 100644 new mode 100755 index e631f86..4a34ca2 --- a/slash-commands/compare.js +++ b/slash-commands/compare.js @@ -1,6 +1,7 @@ -const { SlashCommandBuilder } = require('discord.js'); +const { SlashCommandBuilder, Guild } = require('discord.js'); const dbfn = require('../modules/dbfn.js'); const fn = require('../modules/functions.js'); +const { GuildInfo } = require('../modules/CustomClasses.js'); module.exports = { data: new SlashCommandBuilder() @@ -10,61 +11,40 @@ module.exports = { try { await interaction.deferReply(); // Get the guildInfo from the database - dbfn.getGuildInfo(interaction.guildId).then(async getGuildInfoResponse => { - let guildInfo = getGuildInfoResponse.data; - // Find the most recent tree and leaderboard messages in their respective channels + + if (interaction.client.guildInfos.has(interaction.guildId)) { + let guildInfo = interaction.client.guildInfos.get(interaction.guildId); const findMessagesResponse = await fn.messages.find(interaction, guildInfo); if (findMessagesResponse.code == 1) { - guildInfo = findMessagesResponse.data; + let guildInfo = interaction.client.guildInfos.get(interaction.guildId); // Parse the leaderboard message await fn.rankings.parse(interaction, guildInfo); // Build the string that shows the comparison // TODO Move the string building section to fn.builders? const comparedRankings = await fn.rankings.compare(interaction, guildInfo); const embed = fn.builders.comparisonEmbed(comparedRankings, guildInfo); - await interaction.editReply(embed).then(async message => { - await dbfn.setComparisonMessage(message, interaction.guildId); - }); + await interaction.editReply(embed).catch(e => console.error(e)); } else { await interaction.editReply(fn.builders.errorEmbed(findMessagesResponse.status)); } - - }).catch(async err => { // If we fail to fetch the guild's info from the database - // If the error is because the guild hasn't been setup yet, set it up - if (err === "There is no database entry for your guild yet. Try running /setup") { - // Create a basic guildInfo with blank data - let guildInfo = { - guildId: `${interaction.guildId}`, - treeName: "", - treeHeight: 0, - treeMessageId: "", - treeChannelId: `${interaction.channelId}`, // Use this interaction channel for the initial channel IDs - leaderboardMessageId: "", - leaderboardChannelId: `${interaction.channelId}`, - reminderMessage: "", - reminderChannelId: "", - remindedStatus: 0, - reminderOptIn: 0, - } - // Using the above guildInfo, try to find the Grow A Tree messages - const findMessagesResponse = await fn.messages.find(interaction, guildInfo); - guildInfo = findMessagesResponse.data; - if (findMessagesResponse.code == 1) { - // Build the string that shows the comparison // TODO Move the string building section to fn.builders? - const comparedRankings = await fn.rankings.compare(interaction, guildInfo); - const embed = fn.builders.comparisonEmbed(comparedRankings, guildInfo); - await interaction.editReply(embed).then(async message => { - await dbfn.setComparisonMessage(message.id, interaction.guildId); - }); - } else { - await interaction.editReply(fn.builders.errorEmbed(findMessagesResponse.status)); - } - + } else { + // Create a basic guildInfo with blank data + let guildInfo = new GuildInfo() + .setId(interaction.guildId) + .setTreeMessage("", interaction.channelId) + .setLeaderboardMessage("", interaction.channelId) + // Using the above guildInfo, try to find the Grow A Tree messages + const findMessagesResponse = await fn.messages.find(interaction, guildInfo); + guildInfo = findMessagesResponse.data; + if (findMessagesResponse.code == 1) { + // Build the string that shows the comparison // TODO Move the string building section to fn.builders? + const comparedRankings = await fn.rankings.compare(interaction, guildInfo); + const embed = fn.builders.comparisonEmbed(comparedRankings, guildInfo); + await interaction.editReply(embed).catch(e => console.error(e)); } else { - await interaction.editReply(fn.builders.errorEmbed("An unknown error occurred while running the compare command.")); - console.error(err); + await interaction.editReply(fn.builders.errorEmbed(findMessagesResponse.status)); } - }); + } } catch (err) { interaction.editReply(fn.builders.errorEmbed(err)).catch(err => { console.error(err); diff --git a/slash-commands/help.js b/slash-commands/help.js old mode 100644 new mode 100755 diff --git a/slash-commands/notifications.js b/slash-commands/notifications.js new file mode 100755 index 0000000..313c35e --- /dev/null +++ b/slash-commands/notifications.js @@ -0,0 +1,152 @@ +const { SlashCommandBuilder, PermissionFlagsBits } = require('discord.js'); +const { GuildInfo } = require('../modules/CustomClasses.js'); +const dbfn = require('../modules/dbfn.js'); +const fn = require('../modules/functions.js'); +const strings = require('../data/strings.json'); + +module.exports = { + data: new SlashCommandBuilder() + .setName('notifications') + .setDescription('A notification relay for improved water and fruit notifications') + .addSubcommand(sc => + sc.setName('set') + .setDescription('Set up the notification relay for the first time') + .addChannelOption(o => + o.setName('watchchannel') + .setDescription('The channel Grow A Tree sends your notifications in') + .setRequired(true) + ) + .addStringOption(o => + o.setName('watermessage') + .setDescription('Message to send for water reminders') + .setRequired(true) + ) + .addChannelOption(o => + o.setName('pingchannel') + .setDescription('The channel to send the water reminder in') + .setRequired(true) + ) + .addStringOption(o => + o.setName('fruitmessage') + .setDescription("Message to send for fruit reminders") + .setRequired(false) + ) + ) + .addSubcommand(sc => + sc.setName('update') + .setDescription('Update an already setup notification relay') + .addChannelOption(o => + o.setName('watchchannel') + .setDescription('The channel Grow A Tree sends your notifications in') + .setRequired(false) + ) + .addStringOption(o => + o.setName('watermessage') + .setDescription('Message to send for water reminders') + .setRequired(false) + ) + .addChannelOption(o => + o.setName('pingchannel') + .setDescription('The channel to send the water reminder in') + .setRequired(false) + ) + .addStringOption(o => + o.setName('fruitmessage') + .setDescription("Message to send for fruit reminders") + .setRequired(false) + ) + ) + .addSubcommand(sc => + sc.setName('disable') + .setDescription('Disable the notification relay') + ) + .setDefaultMemberPermissions(PermissionFlagsBits.ManageRoles), + async execute(interaction) { + try { + await interaction.deferReply({ ephemeral: true }); + const subcommand = interaction.options.getSubcommand(); + // if (process.env.DEBUG) console.log(`${typeof subcommand}: ${subcommand}`); + switch (subcommand) { + case "set": + if (interaction.client.guildInfos.has(interaction.guildId)) { + const watchChannel = interaction.options.getChannel('watchchannel'); + const waterMessage = interaction.options.getString('watermessage'); + const fruitMessage = interaction.options.getString('fruitmessage') ? interaction.options.getString('fruitmessage') : interaction.options.getString('watermessage'); + const reminderChannel = interaction.options.getChannel('pingchannel'); + let guildInfo = interaction.client.guildInfos.get(interaction.guildId); + guildInfo.setReminders(waterMessage, fruitMessage, reminderChannel.id, watchChannel.id, true); + let query = guildInfo.queryBuilder("setReminders"); + await dbfn.setGuildInfo(query); + const replyParts = [ + `I'll watch <#${watchChannel.id}> for Grow A Tree Notifications and relay them to <#${reminderChannel.id}>.`, + `Water Message: ${waterMessage}` + ]; + if (fruitMessage != "") replyParts.push(`Fruit Message: ${fruitMessage}`); + await interaction.editReply(replyParts.join("\n")).catch(e => console.error(e)); + fn.collectionBuilders.guildInfos(interaction.client); + } else { + const watchChannel = interaction.options.getChannel('watchchannel'); + const waterMessage = interaction.options.getString('watermessage'); + const fruitMessage = interaction.options.getString('fruitmessage') ? interaction.options.getString('fruitmessage') : interaction.options.getString('watermessage'); + const reminderChannel = interaction.options.getChannel('pingchannel'); + let guildInfo = new GuildInfo() + .setId(interaction.guildId) + .setReminders(waterMessage, fruitMessage, reminderChannel.id, watchChannel.id, true); + let query = guildInfo.queryBuilder("setReminders"); + await dbfn.setGuildInfo(query); + const replyParts = [ + `I'll watch <#${watchChannel.id}> for Grow A Tree Notifications and relay them to <#${reminderChannel.id}>.`, + `Water Message: ${waterMessage}` + ]; + if (fruitMessage != "") replyParts.push(`Fruit Message: ${fruitMessage}`); + await interaction.editReply(replyParts.join("\n")).catch(e => console.error(e)); + fn.collectionBuilders.guildInfos(interaction.client); + } + break; + case "update": + if (interaction.client.guildInfos.has(interaction.guildId)) { + let guildInfo = interaction.client.guildInfos.get(interaction.guildId); + const inWatchChannel = interaction.options.getChannel('watchchannel'); + const inWaterMessage = interaction.options.getString('watermessage'); + const inFruitMessage = interaction.options.getString('fruitmessage'); + const inReminderChannel = interaction.options.getChannel('pingchannel'); + + const outWatchChannelId = inWatchChannel ? inWatchChannel.id : guildInfo.watchChannelId; + const outWaterMessage = inWaterMessage ? inWaterMessage : guildInfo.waterMessage; + const outFruitMessage = inFruitMessage ? inFruitMessage : guildInfo.fruitMessage; + const outReminderChannelId = inReminderChannel ? inReminderChannel.id : guildInfo.reminderChannelId; + + guildInfo.setReminders(outWaterMessage, outFruitMessage, outReminderChannelId, outWatchChannelId, true); + let query = guildInfo.queryBuilder("setReminders"); + await dbfn.setGuildInfo(query); + const replyParts = [ + `I'll watch <#${outWatchChannelId}> for Grow A Tree Notifications and relay them to <#${outReminderChannelId}>.`, + `Water Message: ${outWaterMessage}` + ]; + if (outFruitMessage != "") replyParts.push(`Fruit Message: ${outFruitMessage}`); + await interaction.editReply(replyParts.join("\n")).catch(e => console.error(e)); + fn.collectionBuilders.guildInfos(interaction.client); + } else { + await interaction.editReply(fn.builders.errorEmbed("There is no existing notification relay to update!")).catch(e => console.error(e)); + } + break; + case 'disable': + if (interaction.client.guildInfos.has(interaction.guildId)) { + let guildInfo = interaction.client.guildInfos.get(interaction.guildId); + guildInfo.setReminders(undefined, undefined, undefined, undefined, false); + await dbfn.setGuildInfo(guildInfo.queryBuilder("setReminders")).catch(e => console.error(e)); + await fn.collectionBuilders.guildInfos(interaction.client); + await interaction.editReply(fn.builders.embed(strings.status.optout)).catch(e => console.error(e)); + } else { + await interaction.editReply(fn.builders.errorEmbed("A notification relay has not been set up yet!")).catch(e => console.error(e)); + } + break; + default: + await interaction.editReply(fn.builders.errorEmbed("Invalid subcommand detected.")).catch(e => console.error(e)); + break; + } + } catch (err) { + console.error("Error occurred while setting up a notification relay: " + err); + } + }, +}; \ No newline at end of file diff --git a/slash-commands/optout.js b/slash-commands/optout.js deleted file mode 100644 index 1d1cf37..0000000 --- a/slash-commands/optout.js +++ /dev/null @@ -1,20 +0,0 @@ -const { SlashCommandBuilder, PermissionFlagsBits } = require('discord.js'); -const dbfn = require('../modules/dbfn.js'); -const fn = require('../modules/functions.js'); - -module.exports = { - data: new SlashCommandBuilder() - .setName('optout') - .setDescription('Opt-out of automatic water reminders') - .setDefaultMemberPermissions(PermissionFlagsBits.ManageRoles), - async execute(interaction) { - try { - await interaction.deferReply({ ephemeral: true }); - const setReminderOptInResponse = await dbfn.setReminderOptIn(interaction.guildId, 0); - interaction.editReply(setReminderOptInResponse.status); - } catch(err) { - console.error(err); - await interaction.editReply(fn.builders.errorEmbed(err)); - } - }, -}; \ No newline at end of file diff --git a/slash-commands/reset.js b/slash-commands/reset.js deleted file mode 100644 index 7cd7985..0000000 --- a/slash-commands/reset.js +++ /dev/null @@ -1,24 +0,0 @@ -const { SlashCommandBuilder } = require('discord.js'); -const fn = require('../modules/functions.js'); - -module.exports = { - data: new SlashCommandBuilder() - .setName('reset') - .setDescription('Reset all message assignments in your server'), - execute(interaction) { - interaction.deferReply({ ephemeral: true }).then(() => { - fn.reset(interaction.guildId).then(res => { - interaction.editReply(fn.builders.embed("Assignments Reset")).catch(err => { - console.error(err); - }); - }).catch(err => { - console.error(err); - interaction.editReply("There was a problem deleting your guild information, contact @voidf1sh#0420 for help.").catch(err => { - console.error(err); - }); - }); - }).catch(err => { - console.error(err); - }); - }, -}; \ No newline at end of file diff --git a/slash-commands/rolemenu.js b/slash-commands/rolemenu.js new file mode 100755 index 0000000..f2ee244 --- /dev/null +++ b/slash-commands/rolemenu.js @@ -0,0 +1,18 @@ +const { SlashCommandBuilder } = require('discord.js'); +const fn = require('../modules/functions.js'); +const strings = require('../data/strings.json'); + +module.exports = { + data: new SlashCommandBuilder() + .setName('rolemenu') + .setDescription('Send a self-assignable role selection menu'), + async execute(interaction) { + await interaction.deferReply().catch(e => console.error(e)); + if (interaction.client.guildInfos.has(interaction.guildId)) { + let guildInfo = interaction.client.guildInfos.get(interaction.guildId); + await interaction.editReply(fn.builders.embeds.treeRoleMenu(guildInfo)).catch(e => console.error(e)); + } else { + await interaction.editReply(fn.builders.errorEmbed(strings.status.noRoleMenu)).catch(e => console.error(e)); + } + }, +}; \ No newline at end of file diff --git a/slash-commands/setping.js b/slash-commands/setping.js deleted file mode 100644 index 8674e5f..0000000 --- a/slash-commands/setping.js +++ /dev/null @@ -1,31 +0,0 @@ -const { SlashCommandBuilder, PermissionFlagsBits } = require('discord.js'); -const dbfn = require('../modules/dbfn.js'); -const fn = require('../modules/functions.js'); - -module.exports = { - data: new SlashCommandBuilder() - .setName('setping') - .setDescription('Opt-in to automatic water reminders') - .addStringOption(o => - o.setName('pingmsg') - .setDescription('The message to send for a water reminder') - .setRequired(true)) - .addChannelOption(o => - o.setName('pingchannel') - .setDescription('The channel to send the water reminder in') - .setRequired(true)) - .setDefaultMemberPermissions(PermissionFlagsBits.ManageRoles), - async execute(interaction) { - try { - await interaction.deferReply({ ephemeral: true }); - const reminderMessage = interaction.options.getString('pingmsg'); - const reminderChannel = interaction.options.getChannel('pingchannel'); - const setPingRoleResponse = await dbfn.setReminderInfo(interaction.guildId, reminderMessage, reminderChannel.id); - await dbfn.setReminderOptIn(interaction.guildId, 1); - interaction.editReply(setPingRoleResponse.status); - } catch(err) { - console.error(err); - await interaction.editReply(fn.builders.errorEmbed(err)); - } - }, -}; \ No newline at end of file diff --git a/slash-commands/setping.js.bak b/slash-commands/setping.js.bak deleted file mode 100644 index 4d13791..0000000 --- a/slash-commands/setping.js.bak +++ /dev/null @@ -1,18 +0,0 @@ -const { SlashCommandBuilder } = require('discord.js'); -const dbfn = require('../modules/dbfn.js'); -const fn = require('../modules/functions.js'); - -module.exports = { - data: new SlashCommandBuilder() - .setName('setping') - .setDescription('Run this command when you water your tree to have a reminder sent.'), - async execute(interaction) { - await interaction.deferReply({ ephemeral: true }); - const getGuildInfoResponse = await dbfn.getGuildInfo(interaction.guildId); - const guildInfo = getGuildInfoResponse.data; - const reminderTimeS = fn.getWaterTime(guildInfo.treeHeight); - const reminderTimeMs = reminderTimeS * 1000; - fn.setReminder(interaction, reminderTimeMs, guildInfo.pingRoleId); - interaction.editReply("A reminder has been set."); - }, -}; \ No newline at end of file diff --git a/slash-commands/setup.js b/slash-commands/setup.js old mode 100644 new mode 100755 index cbd1350..5e5cbae --- a/slash-commands/setup.js +++ b/slash-commands/setup.js @@ -1,37 +1,125 @@ -const { SlashCommandBuilder } = require('discord.js'); +const { SlashCommandBuilder, PermissionFlagsBits } = require('discord.js'); const fn = require('../modules/functions.js'); const strings = require('../data/strings.json'); const dbfn = require('../modules/dbfn.js'); +const { GuildInfo } = require('../modules/CustomClasses.js'); module.exports = { data: new SlashCommandBuilder() .setName('setup') .setDescription('Attempt automatic configuration of the bot.') - .addChannelOption(o => - o.setName('treechannel') - .setDescription('What channel is your tree in?') - .setRequired(true)) - .addChannelOption(o => - o.setName('leaderboardchannel') - .setDescription('If your leaderboard isn\'t in the same channel, where is it?') - .setRequired(false)), + .addSubcommand(sc => + sc.setName('compare') + .setDescription('Set up the channels to be used with /compare') + .addChannelOption(o => + o.setName('treechannel') + .setDescription('What channel is your tree in?') + .setRequired(true) + ) + .addChannelOption(o => + o.setName('leaderboardchannel') + .setDescription('If your leaderboard isn\'t in the same channel, where is it?') + .setRequired(false) + ) + ) + .addSubcommand(sc => + sc.setName('rolemenu') + .setDescription('Setup the roles to be used with /rolemenu') + .addRoleOption(o => + o.setName('waterrole') + .setDescription('The role for water reminder pings') + .setRequired(true) + ) + .addRoleOption(o => + o.setName('fruitrole') + .setDescription('The role for fruit alert pings') + .setRequired(false) + ) + ) + .addSubcommand(sc => + sc.setName('view') + .setDescription('View your server\'s configuration')) + .addSubcommand(sc => + sc.setName('reset') + .setDescription('Remove all server configuration from the database') + .addBooleanOption(o => + o.setName('confirm') + .setDescription('WARNING THIS IS IRREVERSIBLE') + .setRequired(true) + ) + ) + .setDefaultMemberPermissions(PermissionFlagsBits.Administrator), async execute(interaction) { await interaction.deferReply({ ephemeral: true }); - /**/ - let guildInfo = { - "guildId": interaction.guildId, - "treeName": "", - "treeHeight": 0, - "treeMessageId": "", - "treeChannelId": `${interaction.options.getChannel('treechannel').id }`, - "leaderboardMessageId": "", - "leaderboardChannelId": `${interaction.options.getChannel('leaderboardchannel').id || interaction.options.getChannel('treechannel').id }`, - "reminderMessage": "", - "reminderChannelId": "", - "remindedStatus": 0, - "reminderOptIn": 0, - }; - const findMessagesResponse = await fn.messages.find(interaction, guildInfo); - interaction.editReply(findMessagesResponse.status); + const subcommand = interaction.options.getSubcommand(); + switch (subcommand) { + case "compare": + if (interaction.client.guildInfos.has(interaction.guildId)) { + let guildInfo = interaction.client.guildInfos.get(interaction.guildId); + const findMessagesResponse = await fn.messages.find(interaction, guildInfo); + await interaction.editReply(findMessagesResponse.status).catch(e => console.error(e)); + } else { + let guildInfo = new GuildInfo() + .setId(interaction.guildId); + const findMessagesResponse = await fn.messages.find(interaction, guildInfo); + await interaction.editReply(findMessagesResponse.status).catch(e => console.error(e)); + } + break; + case "rolemenu": + let waterRoleId = interaction.options.getRole('waterrole').id; + let fruitRoleId = interaction.options.getRole('fruitrole') ? interaction.options.getRole('fruitrole').id : undefined; + if (interaction.client.guildInfos.has(interaction.guildId)) { + let guildInfo = interaction.client.guildInfos.get(interaction.guildId); + guildInfo.setRoles(waterRoleId, fruitRoleId); + await dbfn.setGuildInfo(guildInfo.queryBuilder("setRoles")); + await fn.collectionBuilders.guildInfos(interaction.client); + await interaction.editReply(fn.builders.embeds.treeRoleMenu(guildInfo)).catch(e => console.error(e)); + } else { + let guildInfo = new GuildInfo() + .setId(interaction.guildId); + guildInfo.setRoles(waterRoleId, fruitRoleId); + await dbfn.setGuildInfo(guildInfo.queryBuilder("setRoles")); + await fn.collectionBuilders.guildInfos(interaction.client); + await interaction.editReply(fn.builders.embeds.treeRoleMenu(guildInfo)).catch(e => console.error(e)); + } + break; + case "view": + try { + if (interaction.client.guildInfos.has(interaction.guildId)) { + let guildInfo = interaction.client.guildInfos.get(interaction.guildId); + await interaction.editReply(fn.builders.embed(guildInfo.generateSetupInfo())); + } else { + await interaction.editReply(fn.builders.errorEmbed("Guild doesn't exist in database!")); + } + } catch (err) { + console.error(err); + await interaction.editReply(fn.builders.errorEmbed("There was an error running the command.")); + } + break; + case "reset": + if (interaction.client.guildInfos.has(interaction.guildId)) { + let guildInfo = interaction.client.guildInfos.get(interaction.guildId); + if (interaction.options.getBoolean('confirm')) { + fn.reset(interaction).then(res => { + interaction.editReply(fn.builders.embed(strings.status.reset)).catch(err => { + console.error(err); + }); + }).catch(err => { + console.error(err); + interaction.editReply(strings.status.resetError).catch(err => { + console.error(err); + }); + }); + } else { + await interaction.editReply(fn.builders.embed("You must select 'true' to confirm setup reset. No changes have been made.")).catch(e => console.error(e)); + } + } else { + throw "Guild doesn't exist in database!"; + } + break; + default: + await interaction.editReply(fn.builders.errorEmbed(strings.error.invalidSubcommand)).catch(e => console.error(e)); + break; + } }, }; \ No newline at end of file diff --git a/slash-commands/setupinfo.js b/slash-commands/setupinfo.js deleted file mode 100644 index 8597c94..0000000 --- a/slash-commands/setupinfo.js +++ /dev/null @@ -1,21 +0,0 @@ -const { SlashCommandBuilder } = require('discord.js'); -const fn = require('../modules/functions.js'); - -module.exports = { - data: new SlashCommandBuilder() - .setName('setupinfo') - .setDescription('View information about how the bot is set up in your server'), - execute(interaction) { - interaction.deferReply({ephemeral: true}).then(() => { - fn.getInfo(interaction.guildId).then(res => { - const embed = fn.builders.embed(res); - interaction.editReply(embed); - }).catch(err => { - interaction.editReply(err); - console.error(err); - }); - }).catch(err => { - console.error(err); - }); - }, -}; \ No newline at end of file diff --git a/slash-commands/template b/slash-commands/template old mode 100644 new mode 100755 diff --git a/slash-commands/timetoheight.js b/slash-commands/timetoheight.js old mode 100644 new mode 100755 index f065c86..bf23959 --- a/slash-commands/timetoheight.js +++ b/slash-commands/timetoheight.js @@ -6,18 +6,22 @@ module.exports = { data: new SlashCommandBuilder() .setName('timetoheight') .setDescription('Calculate how long it would take to reach a given height') - .addStringOption(o => - o.setName('beginheight') - .setDescription('Begining tree height in feet, numbers ONLY') - .setRequired(true)) - .addStringOption(o => + .addIntegerOption(o => o.setName('endheight') - .setDescription('Ending tree height in feet, numbers ONLY') - .setRequired(true)), + .setDescription('Ending tree height in feet') + .setRequired(true)) + .addIntegerOption(o => + o.setName('beginheight') + .setDescription('Beginning tree height in feet') + .setRequired(false)), async execute(interaction) { await interaction.deferReply({ ephemeral: true }); - const beginHeight = interaction.options.getString('beginheight'); - const endHeight = interaction.options.getString('endheight'); + let beginHeight = interaction.options.getInteger('beginheight'); + const endHeight = interaction.options.getInteger('endheight'); + if (!beginHeight) { + const guildInfo = interaction.client.guildInfos.get(interaction.guild.id); + beginHeight = guildInfo.treeHeight; + } fn.timeToHeight(beginHeight, endHeight).then(res => { interaction.editReply(`It will take a tree that is ${beginHeight}ft tall ${res} to reach ${endHeight}ft.`); }).catch(err => { diff --git a/slash-commands/watertime.js b/slash-commands/watertime.js old mode 100644 new mode 100755 index 4338176..e83ad37 --- a/slash-commands/watertime.js +++ b/slash-commands/watertime.js @@ -6,13 +6,21 @@ module.exports = { data: new SlashCommandBuilder() .setName('watertime') .setDescription('Calculate the watering time for a given tree height') - .addStringOption(o => + .addIntegerOption(o => o.setName('height') - .setDescription('Tree height in feet, numbers ONLY') - .setRequired(true)), + .setDescription('Tree height') + .setRequired(true)) + .addBooleanOption(o => + o.setName('private') + .setDescription('Should the response be private? Default: true') + .setRequired(false)), async execute(interaction) { - await interaction.deferReply(); - const treeHeight = interaction.options.getString('height'); - await interaction.editReply(`A tree that is ${treeHeight}ft tall will have a watering time of ${fn.getWaterTime(treeHeight)} minutes.`); + const treeHeight = interaction.options.getInteger('height'); + const privateOpt = interaction.options.getBoolean('private'); + const private = privateOpt != undefined ? privateOpt : true; + await interaction.deferReply( {ephemeral: private }); + const waterSeconds = fn.getWaterTime(treeHeight); + const waterTime = fn.parseWaterTime(waterSeconds); + await interaction.editReply(`A tree that is ${treeHeight}ft tall will have a watering time of ${waterTime}.`); }, }; \ No newline at end of file