From b1462c7962915d5c52a90d24fc258eebddd023a7 Mon Sep 17 00:00:00 2001 From: SloneFallion <31500329+SloneFallion@users.noreply.github.com> Date: Mon, 10 Apr 2023 19:56:51 -0400 Subject: [PATCH] Initial publish. --- .env | 8 + .gitignore | 130 +++++ commands/about.js | 24 + commands/interaction.js | 989 ++++++++++++++++++++++++++++++++++++ commands/say.js | 31 ++ deploy.js | 39 ++ events/buttonListener.js | 15 + events/interactionCreate.js | 22 + events/ready.js | 12 + events/selectListener.js | 59 +++ exports/bossMessageID.js | 220 ++++++++ index.js | 51 ++ package.json | 15 + readme.md | 70 +++ 14 files changed, 1685 insertions(+) create mode 100644 .env create mode 100644 .gitignore create mode 100644 commands/about.js create mode 100644 commands/interaction.js create mode 100644 commands/say.js create mode 100644 deploy.js create mode 100644 events/buttonListener.js create mode 100644 events/interactionCreate.js create mode 100644 events/ready.js create mode 100644 events/selectListener.js create mode 100644 exports/bossMessageID.js create mode 100644 index.js create mode 100644 package.json create mode 100644 readme.md diff --git a/.env b/.env new file mode 100644 index 0000000..fc703ae --- /dev/null +++ b/.env @@ -0,0 +1,8 @@ +# Your Discord bot's token. +DISCORD_TOKEN= + +# Your Discord ID. +botmaintainer= + +#Your Discord bot's client ID. +client_id= \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a3b1f6f --- /dev/null +++ b/.gitignore @@ -0,0 +1,130 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +# .env is allowed as a blank template. +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp +.cache + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* diff --git a/commands/about.js b/commands/about.js new file mode 100644 index 0000000..48483cd --- /dev/null +++ b/commands/about.js @@ -0,0 +1,24 @@ +const { SlashCommandBuilder } = require('discord.js'); + +// Set up the command for deployment. +module.exports = { + // Define the command. + data: new SlashCommandBuilder() + .setName('about') + .setDescription('Provides bot origin and maintainer info.') + .setDefaultMemberPermissions('2147483648') // Member has "Use Slash Commands" permission. + .setDMPermission(false), + + // Execute the command. + async execute(interaction) { + try { + await interaction.reply({ + content: `This bot was created by Slone#0110 with some help from hazen#7037. +In-game: Arkondriel, commander of the Cat crew on Solaris. +On this server, the bot is maintained by <@${process.env.botmaintainer}>.`, ephemeral: true + }); + } catch (error) { + return console.log(error); + } + }, +}; \ No newline at end of file diff --git a/commands/interaction.js b/commands/interaction.js new file mode 100644 index 0000000..aa0b655 --- /dev/null +++ b/commands/interaction.js @@ -0,0 +1,989 @@ +const { ActionRowBuilder, ButtonBuilder, ButtonStyle, SlashCommandBuilder, StringSelectMenuBuilder } = require('discord.js'); + +// Set up the command for deployment. +module.exports = { + data: new SlashCommandBuilder() + .setName('interaction') + .setDescription('Create an interactable object.') + .setDefaultMemberPermissions('32') // Member has manage server permissions. + .setDMPermission(false) + .addStringOption(option => + option.setName('create') + .setDescription('The object you wish to create.') + .setRequired(true) + .addChoices( + { name: 'Boss Timer', value: 'bossTimerMenu' } + )) + .addStringOption(option => + option.setName('bossmenu') + .setDescription('The boss timer menu you wish to create.') + .addChoices( + { name: 'Apophis', value: 'apophisMenu' }, + { name: 'Barbarossa', value: 'barbarossaMenu' }, + { name: 'Culton', value: 'cultonMenu' }, + { name: 'Devourer', value: 'devourerMenu' }, + { name: 'Dragon', value: 'dragonMenu' }, + { name: 'Eva', value: 'evaMenu' }, + { name: 'Frost Bot', value: 'frostbotMenu' }, + { name: 'Haboela', value: 'haboelaMenu' }, + { name: 'Harrah', value: 'harrahMenu' }, + { name: 'Lucia', value: 'luciaMenu' }, + { name: 'Magma', value: 'magmaMenu' }, + { name: 'Robarg', value: 'robargMenu' }, + { name: 'Rudolph', value: 'rudolphMenu' }, + { name: 'Scylla', value: 'scyllaMenu' }, + { name: 'Sobek', value: 'sobekMenu' }, + )), + + // Execute the command. + async execute(interaction) { + try { + // Remove the command's reply. + await interaction.deferReply({ ephemeral: true }); + await interaction.deleteReply(); + // Find the user-specified values of the command. + const object = interaction.options.getString('create'); + const bossMenu = interaction.options.getString('bossmenu'); + if (object === 'bossTimerMenu') { + // Make a buton that replies with a countdown of the user's line change. + const lineCooldownButton = new ActionRowBuilder() + .addComponents( + new ButtonBuilder() + .setCustomId('lineCooldown') + .setLabel(`Line Cooldown Timer`) + .setStyle(ButtonStyle.Secondary), + ); + // Cache the channel the interaction came from. + const channel = client.channels.cache.get(interaction.channelId); + if (bossMenu === 'apophisMenu') { + // Build select menus. + const apophisSelector = new ActionRowBuilder() + .addComponents( + new StringSelectMenuBuilder() + .setCustomId('apophisTimestamp') + .setPlaceholder('Mark Defeated') + .addOptions([ + { + label: `Channel 1`, + value: `apophisChannel1`, + }, + { + label: `Channel 2`, + value: `apophisChannel2`, + }, + { + label: `Channel 3`, + value: `apophisChannel3`, + }, + { + label: `Channel 4`, + value: `apophisChannel4`, + }, + { + label: `Channel 5`, + value: `apophisChannel5`, + }, + { + label: `Channel 6`, + value: `apophisChannel6`, + }, + { + label: `Channel 7`, + value: `apophisChannel7`, + }, + { + label: `Channel 8`, + value: `apophisChannel8`, + }, + { + label: `Channel 9`, + value: `apophisChannel9`, + }, + { + label: `Channel 10`, + value: `apophisChannel10`, + }, + { + label: `Channel 11`, + value: `apophisChannel11`, + }, + { + label: `Channel 12`, + value: `apophisChannel12`, + }, + { + label: `Channel 13`, + value: `apophisChannel13`, + }, + { + label: `Channel 14`, + value: `apophisChannel14`, + }, + { + label: `Channel 15`, + value: `apophisChannel15`, + }, + { + label: `Channel 16`, + value: `apophisChannel16`, + } + ]) + ) + await channel.send({ components: [lineCooldownButton, apophisSelector] }); + } + if (bossMenu === 'barbarossaMenu') { + // Build select menus. + const barbarossaSelector = new ActionRowBuilder() + .addComponents( + new StringSelectMenuBuilder() + .setCustomId('barbarossaTimestamp') + .setPlaceholder('Mark Defeated') + .addOptions([ + { + label: `Channel 1`, + value: `barbarossaChannel1`, + }, + { + label: `Channel 2`, + value: `barbarossaChannel2`, + }, + { + label: `Channel 3`, + value: `barbarossaChannel3`, + }, + { + label: `Channel 4`, + value: `barbarossaChannel4`, + }, + { + label: `Channel 5`, + value: `barbarossaChannel5`, + }, + { + label: `Channel 6`, + value: `barbarossaChannel6`, + }, + { + label: `Channel 7`, + value: `barbarossaChannel7`, + }, + { + label: `Channel 8`, + value: `barbarossaChannel8`, + }, + { + label: `Channel 9`, + value: `barbarossaChannel9`, + }, + { + label: `Channel 10`, + value: `barbarossaChannel10`, + }, + { + label: `Channel 11`, + value: `barbarossaChannel11`, + }, + { + label: `Channel 12`, + value: `barbarossaChannel12`, + }, + { + label: `Channel 13`, + value: `barbarossaChannel13`, + }, + { + label: `Channel 14`, + value: `barbarossaChannel14`, + }, + { + label: `Channel 15`, + value: `barbarossaChannel15`, + }, + { + label: `Channel 16`, + value: `barbarossaChannel16`, + } + ]) + ) + await channel.send({ components: [lineCooldownButton, barbarossaSelector] }); + } + if (bossMenu === 'cultonMenu') { + // Build select menus. + const cultonSelector = new ActionRowBuilder() + .addComponents( + new StringSelectMenuBuilder() + .setCustomId('cultonTimestamp') + .setPlaceholder('Mark Defeated') + .addOptions([ + { + label: `Channel 1`, + value: `cultonChannel1`, + }, + { + label: `Channel 2`, + value: `cultonChannel2`, + }, + { + label: `Channel 3`, + value: `cultonChannel3`, + }, + { + label: `Channel 4`, + value: `cultonChannel4`, + }, + { + label: `Channel 5`, + value: `cultonChannel5`, + }, + { + label: `Channel 6`, + value: `cultonChannel6`, + }, + { + label: `Channel 7`, + value: `cultonChannel7`, + }, + { + label: `Channel 8`, + value: `cultonChannel8`, + } + ]) + ) + await channel.send({ components: [lineCooldownButton, cultonSelector] }); + } + if (bossMenu === 'devourerMenu') { + // Build select menus. + const devourerSelector = new ActionRowBuilder() + .addComponents( + new StringSelectMenuBuilder() + .setCustomId('devourerTimestamp') + .setPlaceholder('Mark Defeated') + .addOptions([ + { + label: `Channel 1`, + value: `devourerChannel1`, + }, + { + label: `Channel 2`, + value: `devourerChannel2`, + } + ]) + ) + await channel.send({ components: [lineCooldownButton, devourerSelector] }); + } + if (bossMenu === 'dragonMenu') { + // Build select menus. + const dragonSelector = new ActionRowBuilder() + .addComponents( + new StringSelectMenuBuilder() + .setCustomId('dragonTimestamp') + .setPlaceholder('Mark Defeated') + .addOptions([ + { + label: `Channel 1`, + value: `dragonChannel1`, + }, + { + label: `Channel 2`, + value: `dragonChannel2`, + } + ]) + ) + await channel.send({ components: [lineCooldownButton, dragonSelector] }); + } + if (bossMenu === 'evaMenu') { + // Build select menus. + const evaSelector = new ActionRowBuilder() + .addComponents( + new StringSelectMenuBuilder() + .setCustomId('evaTimestamp') + .setPlaceholder('Mark Defeated') + .addOptions([ + { + label: `Channel 1`, + value: `evaChannel1`, + }, + { + label: `Channel 2`, + value: `evaChannel2`, + }, + { + label: `Channel 3`, + value: `evaChannel3`, + }, + { + label: `Channel 4`, + value: `evaChannel4`, + }, + { + label: `Channel 5`, + value: `evaChannel5`, + }, + { + label: `Channel 6`, + value: `evaChannel6`, + }, + { + label: `Channel 7`, + value: `evaChannel7`, + }, + { + label: `Channel 8`, + value: `evaChannel8`, + }, + { + label: `Channel 9`, + value: `evaChannel9`, + }, + { + label: `Channel 10`, + value: `evaChannel10`, + } + ]) + ) + await channel.send({ components: [lineCooldownButton, evaSelector] }); + } + if (bossMenu === 'frostbotMenu') { + // Build select menus. + const frostbotSelector = new ActionRowBuilder() + .addComponents( + new StringSelectMenuBuilder() + .setCustomId('frostbotTimestamp') + .setPlaceholder('Mark Defeated') + .addOptions([ + { + label: `Channel 1`, + value: `frostbotChannel1`, + }, + { + label: `Channel 2`, + value: `frostbotChannel2`, + }, + { + label: `Channel 3`, + value: `frostbotChannel3`, + }, + { + label: `Channel 4`, + value: `frostbotChannel4`, + }, + { + label: `Channel 5`, + value: `frostbotChannel5`, + }, + { + label: `Channel 6`, + value: `frostbotChannel6`, + }, + { + label: `Channel 7`, + value: `frostbotChannel7`, + }, + { + label: `Channel 8`, + value: `frostbotChannel8`, + }, + { + label: `Channel 9`, + value: `frostbotChannel9`, + }, + { + label: `Channel 10`, + value: `frostbotChannel10`, + }, + { + label: `Channel 11`, + value: `frostbotChannel11`, + }, + { + label: `Channel 12`, + value: `frostbotChannel12`, + }, + { + label: `Channel 13`, + value: `frostbotChannel13`, + }, + { + label: `Channel 14`, + value: `frostbotChannel14`, + }, + { + label: `Channel 15`, + value: `frostbotChannel15`, + }, + { + label: `Channel 16`, + value: `frostbotChannel16`, + } + ]) + ) + await channel.send({ components: [lineCooldownButton, frostbotSelector] }); + } + if (bossMenu === 'haboelaMenu') { + // Build select menus. + const haboelaSelector = new ActionRowBuilder() + .addComponents( + new StringSelectMenuBuilder() + .setCustomId('haboelaTimestamp') + .setPlaceholder('Mark Defeated') + .addOptions([ + { + label: `Channel 1`, + value: `haboelaChannel1`, + }, + { + label: `Channel 2`, + value: `haboelaChannel2`, + }, + { + label: `Channel 3`, + value: `haboelaChannel3`, + }, + { + label: `Channel 4`, + value: `haboelaChannel4`, + }, + { + label: `Channel 5`, + value: `haboelaChannel5`, + }, + { + label: `Channel 6`, + value: `haboelaChannel6`, + }, + { + label: `Channel 7`, + value: `haboelaChannel7`, + }, + { + label: `Channel 8`, + value: `haboelaChannel8`, + }, + { + label: `Channel 9`, + value: `haboelaChannel9`, + }, + { + label: `Channel 10`, + value: `haboelaChannel10`, + }, + { + label: `Channel 11`, + value: `haboelaChannel11`, + }, + { + label: `Channel 12`, + value: `haboelaChannel12`, + }, + { + label: `Channel 13`, + value: `haboelaChannel13`, + }, + { + label: `Channel 14`, + value: `haboelaChannel14`, + }, + { + label: `Channel 15`, + value: `haboelaChannel15`, + }, + { + label: `Channel 16`, + value: `haboelaChannel16`, + }, + { + label: `Channel 17`, + value: `haboelaChannel17`, + }, + { + label: `Channel 18`, + value: `haboelaChannel18`, + }, + { + label: `Channel 19`, + value: `haboelaChannel19`, + }, + { + label: `Channel 20`, + value: `haboelaChannel20`, + } + ]) + ) + await channel.send({ components: [lineCooldownButton, haboelaSelector] }); + } + if (bossMenu === 'harrahMenu') { + // Build select menus. + const harrahSelector = new ActionRowBuilder() + .addComponents( + new StringSelectMenuBuilder() + .setCustomId('harrahTimestamp') + .setPlaceholder('Mark Defeated') + .addOptions([ + { + label: `Channel 1`, + value: `harrahChannel1`, + }, + { + label: `Channel 2`, + value: `harrahChannel2`, + }, + { + label: `Channel 3`, + value: `harrahChannel3`, + }, + { + label: `Channel 4`, + value: `harrahChannel4`, + }, + { + label: `Channel 5`, + value: `harrahChannel5`, + }, + { + label: `Channel 6`, + value: `harrahChannel6`, + }, + { + label: `Channel 7`, + value: `harrahChannel7`, + }, + { + label: `Channel 8`, + value: `harrahChannel8`, + } + ]) + ) + await channel.send({ components: [lineCooldownButton, harrahSelector] }); + } + if (bossMenu === 'luciaMenu') { + // Build select menus. + const luciaSelector = new ActionRowBuilder() + .addComponents( + new StringSelectMenuBuilder() + .setCustomId('luciaTimestamp') + .setPlaceholder('Mark Defeated') + .addOptions([ + { + label: `Channel 1`, + value: `luciaChannel1`, + }, + { + label: `Channel 2`, + value: `luciaChannel2`, + }, + { + label: `Channel 3`, + value: `luciaChannel3`, + }, + { + label: `Channel 4`, + value: `luciaChannel4`, + }, + { + label: `Channel 5`, + value: `luciaChannel5`, + }, + { + label: `Channel 6`, + value: `luciaChannel6`, + }, + { + label: `Channel 7`, + value: `luciaChannel7`, + }, + { + label: `Channel 8`, + value: `luciaChannel8`, + }, + { + label: `Channel 9`, + value: `luciaChannel9`, + }, + { + label: `Channel 10`, + value: `luciaChannel10`, + }, + { + label: `Channel 11`, + value: `luciaChannel11`, + }, + { + label: `Channel 12`, + value: `luciaChannel12`, + }, + { + label: `Channel 13`, + value: `luciaChannel13`, + }, + { + label: `Channel 14`, + value: `luciaChannel14`, + }, + { + label: `Channel 15`, + value: `luciaChannel15`, + }, + { + label: `Channel 16`, + value: `luciaChannel16`, + } + ]) + ) + await channel.send({ components: [lineCooldownButton, luciaSelector] }); + } + if (bossMenu === 'magmaMenu') { + // Build select menus. + const magmaSelector = new ActionRowBuilder() + .addComponents( + new StringSelectMenuBuilder() + .setCustomId('magmaTimestamp') + .setPlaceholder('Mark Defeated') + .addOptions([ + { + label: `Channel 1`, + value: `magmaChannel1`, + }, + { + label: `Channel 2`, + value: `magmaChannel2`, + }, + { + label: `Channel 3`, + value: `magmaChannel3`, + }, + { + label: `Channel 4`, + value: `magmaChannel4`, + }, + { + label: `Channel 5`, + value: `magmaChannel5`, + }, + { + label: `Channel 6`, + value: `magmaChannel6`, + }, + { + label: `Channel 7`, + value: `magmaChannel7`, + }, + { + label: `Channel 8`, + value: `magmaChannel8`, + }, + { + label: `Channel 9`, + value: `magmaChannel9`, + }, + { + label: `Channel 10`, + value: `magmaChannel10`, + } + ]) + ) + await channel.send({ components: [lineCooldownButton, magmaSelector] }); + } + if (bossMenu === 'robargMenu') { + // Build select menus. + const robargSelector = new ActionRowBuilder() + .addComponents( + new StringSelectMenuBuilder() + .setCustomId('robargTimestamp') + .setPlaceholder('Mark Defeated') + .addOptions([ + { + label: `Channel 1`, + value: `robargChannel1`, + }, + { + label: `Channel 2`, + value: `robargChannel2`, + }, + { + label: `Channel 3`, + value: `robargChannel3`, + }, + { + label: `Channel 4`, + value: `robargChannel4`, + }, + { + label: `Channel 5`, + value: `robargChannel5`, + }, + { + label: `Channel 6`, + value: `robargChannel6`, + }, + { + label: `Channel 7`, + value: `robargChannel7`, + }, + { + label: `Channel 8`, + value: `robargChannel8`, + }, + { + label: `Channel 9`, + value: `robargChannel9`, + }, + { + label: `Channel 10`, + value: `robargChannel10`, + }, + { + label: `Channel 11`, + value: `robargChannel11`, + }, + { + label: `Channel 12`, + value: `robargChannel12`, + }, + { + label: `Channel 13`, + value: `robargChannel13`, + }, + { + label: `Channel 14`, + value: `robargChannel14`, + }, + { + label: `Channel 15`, + value: `robargChannel15`, + }, + { + label: `Channel 16`, + value: `robargChannel16`, + } + ]) + ) + // Send the select menu. + await channel.send({ components: [lineCooldownButton, robargSelector] }); + } + if (bossMenu === 'rudolphMenu') { + // Build select menus. + const rudolphSelector = new ActionRowBuilder() + .addComponents( + new StringSelectMenuBuilder() + .setCustomId('rudolphTimestamp') + .setPlaceholder('Mark Defeated') + .addOptions([ + { + label: `Channel 1`, + value: `rudolphChannel1`, + }, + { + label: `Channel 2`, + value: `rudolphChannel2`, + }, + { + label: `Channel 3`, + value: `rudolphChannel3`, + }, + { + label: `Channel 4`, + value: `rudolphChannel4`, + }, + { + label: `Channel 5`, + value: `rudolphChannel5`, + }, + { + label: `Channel 6`, + value: `rudolphChannel6`, + }, + { + label: `Channel 7`, + value: `rudolphChannel7`, + }, + { + label: `Channel 8`, + value: `rudolphChannel8`, + }, + { + label: `Channel 9`, + value: `rudolphChannel9`, + }, + { + label: `Channel 10`, + value: `rudolphChannel10`, + } + ]) + ) + await channel.send({ components: [lineCooldownButton, rudolphSelector] }); + } + if (bossMenu === 'scyllaMenu') { + // Build select menus. + const scyllaSelector = new ActionRowBuilder() + .addComponents( + new StringSelectMenuBuilder() + .setCustomId('scyllaTimestamp') + .setPlaceholder('Mark Defeated') + .addOptions([ + { + label: `Channel 1`, + value: `scyllaChannel1`, + }, + { + label: `Channel 2`, + value: `scyllaChannel2`, + }, + { + label: `Channel 3`, + value: `scyllaChannel3`, + }, + { + label: `Channel 4`, + value: `scyllaChannel4`, + }, + { + label: `Channel 5`, + value: `scyllaChannel5`, + }, + { + label: `Channel 6`, + value: `scyllaChannel6`, + }, + { + label: `Channel 7`, + value: `scyllaChannel7`, + }, + { + label: `Channel 8`, + value: `scyllaChannel8`, + }, + { + label: `Channel 9`, + value: `scyllaChannel9`, + }, + { + label: `Channel 10`, + value: `scyllaChannel10`, + }, + { + label: `Channel 11`, + value: `scyllaChannel11`, + }, + { + label: `Channel 12`, + value: `scyllaChannel12`, + }, + { + label: `Channel 13`, + value: `scyllaChannel13`, + }, + { + label: `Channel 14`, + value: `scyllaChannel14`, + }, + { + label: `Channel 15`, + value: `scyllaChannel15`, + }, + { + label: `Channel 16`, + value: `scyllaChannel16`, + }, + { + label: `Channel 17`, + value: `scyllaChannel17`, + }, + { + label: `Channel 18`, + value: `scyllaChannel18`, + }, + { + label: `Channel 19`, + value: `scyllaChannel19`, + }, + { + label: `Channel 20`, + value: `scyllaChannel20`, + } + ]) + ) + await channel.send({ components: [lineCooldownButton, scyllaSelector] }); + } + if (bossMenu === 'sobekMenu') { + // Build select menus. + const sobekSelector = new ActionRowBuilder() + .addComponents( + new StringSelectMenuBuilder() + .setCustomId('sobekTimestamp') + .setPlaceholder('Mark Defeated') + .addOptions([ + { + label: `Channel 1`, + value: `sobekChannel1`, + }, + { + label: `Channel 2`, + value: `sobekChannel2`, + }, + { + label: `Channel 3`, + value: `sobekChannel3`, + }, + { + label: `Channel 4`, + value: `sobekChannel4`, + }, + { + label: `Channel 5`, + value: `sobekChannel5`, + }, + { + label: `Channel 6`, + value: `sobekChannel6`, + }, + { + label: `Channel 7`, + value: `sobekChannel7`, + }, + { + label: `Channel 8`, + value: `sobekChannel8`, + }, + { + label: `Channel 9`, + value: `sobekChannel9`, + }, + { + label: `Channel 10`, + value: `sobekChannel10`, + }, + { + label: `Channel 11`, + value: `sobekChannel11`, + }, + { + label: `Channel 12`, + value: `sobekChannel12`, + }, + { + label: `Channel 13`, + value: `sobekChannel13`, + }, + { + label: `Channel 14`, + value: `sobekChannel14`, + }, + { + label: `Channel 15`, + value: `sobekChannel15`, + }, + { + label: `Channel 16`, + value: `sobekChannel16`, + } + ]) + ) + await channel.send({ components: [lineCooldownButton, sobekSelector] }); + } + }; + + } catch (error) { + return console.log(error); + } + }, +}; \ No newline at end of file diff --git a/commands/say.js b/commands/say.js new file mode 100644 index 0000000..508e6ce --- /dev/null +++ b/commands/say.js @@ -0,0 +1,31 @@ +const { SlashCommandBuilder } = require('discord.js'); + +// Set up the command for deployment. +module.exports = { + data: new SlashCommandBuilder() + .setName('say') + .setDescription('Say a short message through the BOT..') + .setDefaultMemberPermissions('32') // Member has manage server permissions. + .setDMPermission(false) + .addStringOption(option => + option.setName('message') + .setDescription('What you want the BOT to say.') + .setRequired(true)), + + // Execute the command. + async execute(interaction) { + try { + // Cache and set the channel. + const channel = client.channels.cache.get(interaction.channelId); + // Remove the command's reply. + await interaction.deferReply({ ephemeral: true }); + await interaction.deleteReply(); + // Emulate typing. + await channel.sendTyping(); + // Send the message. + await channel.send(interaction.options.getString('message')); + } catch (error) { + return console.log(error); + } + }, +}; diff --git a/deploy.js b/deploy.js new file mode 100644 index 0000000..1fa71c0 --- /dev/null +++ b/deploy.js @@ -0,0 +1,39 @@ +const { REST, Routes } = require('discord.js'); + +// Require dotenv for environment variables. +require("dotenv").config(); + +// Declare and set a Discord token from the environment variables. +const token = process.env.DISCORD_TOKEN; +const clientId = process.env.client_id; +const fs = require('node:fs'); + +const commands = []; +// Grab all the command files from the commands directory you created earlier +const commandFiles = fs.readdirSync('./commands').filter(file => file.endsWith('.js')); + +// Grab the SlashCommandBuilder#toJSON() output of each command's data for deployment +for (const file of commandFiles) { + const command = require(`./commands/${file}`); + commands.push(command.data.toJSON()); +} +// Construct and prepare an instance of the REST module +const rest = new REST({ version: '10' }).setToken(token); + +// and deploy your commands! +(async () => { + try { + console.log(`Started refreshing ${commands.length} application (/) commands.`); + + // The put method is used to fully refresh all commands in the guild with the current set + const data = await rest.put( + Routes.applicationCommands(clientId), + { body: commands }, + ); + + console.log(`Successfully reloaded ${data.length} application (/) commands.`); + } catch (error) { + // And of course, make sure you catch and log any errors! + console.error(error); + } +})(); diff --git a/events/buttonListener.js b/events/buttonListener.js new file mode 100644 index 0000000..969a36d --- /dev/null +++ b/events/buttonListener.js @@ -0,0 +1,15 @@ +const { ActionRowBuilder, Events, ModalBuilder, TextInputBuilder, TextInputStyle, } = require('discord.js'); + +client.on(Events.InteractionCreate, async interaction => { + try { + if (!interaction.isButton()) return; + if (interaction.customId === 'lineCooldown') + { + const timestamp = Date.now(); + const relativeTimer = Math.floor((timestamp / 1000) + 1800); + interaction.reply({content: `Your channel cooldown ends at , in .`, ephemeral:true }); + } +} catch (error) { + return console.log(error); + } +}); \ No newline at end of file diff --git a/events/interactionCreate.js b/events/interactionCreate.js new file mode 100644 index 0000000..ce0adc6 --- /dev/null +++ b/events/interactionCreate.js @@ -0,0 +1,22 @@ +const { Events } = require('discord.js'); + +module.exports = { + name: Events.InteractionCreate, + async execute(interaction) { + if (!interaction.isChatInputCommand()) return; + + const command = interaction.client.commands.get(interaction.commandName); + + if (!command) { + console.error(`No command matching ${interaction.commandName} was found.`); + return; + } + + try { + await command.execute(interaction); + } catch (error) { + console.error(`Error executing ${interaction.commandName}`); + console.error(error); + } + }, +}; \ No newline at end of file diff --git a/events/ready.js b/events/ready.js new file mode 100644 index 0000000..20cdbbc --- /dev/null +++ b/events/ready.js @@ -0,0 +1,12 @@ +const { ActivityType, Events } = require('discord.js'); + +module.exports = { + name: Events.ClientReady, + once: true, + async execute(client) { + // Set activity + client.user.setActivity('Tower of Fantasy', { type: ActivityType.Playing }); + // Announce that the BOT is ready. + console.log(`Ready! Logged in as ${client.user.tag}`); + }, +}; diff --git a/events/selectListener.js b/events/selectListener.js new file mode 100644 index 0000000..af8f8d7 --- /dev/null +++ b/events/selectListener.js @@ -0,0 +1,59 @@ +const { EmbedBuilder, Events, GuildMemberManager, GuildMemberRoleManager } = require('discord.js'); + +client.on(Events.InteractionCreate, async interaction => { + try { + if (!interaction.isStringSelectMenu()) return; + // Acknowledge the menu interaction. + await interaction.deferUpdate(); + // Reset the menu. + await interaction.editReply({ content: '' }); + // Generate a timestamp based on when the user interacted with the menu. + const timestamp = Date.now(); + // Obtain message ID's for the boss timers. These should be set in advance. + const bossTimers = require('../exports/bossMessageID.js') + // Read the value of the menu interaction. + const selected = interaction.values[0]; + // Cache and set the channel that the interaction came from. + const channel = client.channels.cache.get(interaction.channelId); + // Set the + let channelNumber = selected.match(/\d+/g); + // Match the selected value from the menu interaction to a key in '../exports/bossMessageID.js'. + let bossTimerID = bossTimers.bossTimers.find(r => r.key === selected) || { key: "error", value: `Something went wrong or this item is not yet finished. The BOT author has been notified.` } + // Fetch the message by the ID matched from the previous operation. + await channel.messages.fetch(bossTimerID.messageID).then(message => { + // Edit the message with the channel selected, relative and absolute timestamp (timezone-agnostic), and the user that selected the interaction. + message.edit(`Channel ${channelNumber}: , (<@!${interaction.user.id}>).`); + }) + // Add a one second delay to be sure Discord has marked the message as edited before we fetch the message list. + setTimeout(async function () { + // Fetch the message list from the channel the interaction came from. + const messages = await channel.messages.fetch(); + // Find and store the second-to-last message in the channel. + const messageSortedByTime = messages.first(2)[1] + // Sort and store all of the messages received by last edited. + const sortedMessages = messages.sort((a, b) => { + return b.editedTimestamp - a.editedTimestamp + }).map(message => { + return { + content: message.content, + messageid: message.id + } + }) + // Print the content of each message to a single variable. Skip the second-to-last message stored earlier. Also skip any empty messages. This includes pictures and menu select embeds. + const reducedMessages = sortedMessages.filter(msg => msg.content !== '' && msg.messageid !== messageSortedByTime.id + ).reduce((acc, msg) => { + return { content: acc.content + "\n" + msg.content } + }) + // Post the sorted list in an edit to the second-to-last message in the channel. + messageSortedByTime.edit(`**Sorted by Last Killed:** \n${reducedMessages.content}`); + }, 1000 * 1) + // If there is a broken menu, DM the BOT maintainer. + if (bossTimerID.key === 'error') { + // Send a DM to the bot maintainer. + client.users.send(`${process.env.botmaintainer}`, `User **<@${interaction.user.id}>** *(${interaction.user.username}#${interaction.user.discriminator})* requested incomplete or broken menu item ${selected}.`); + console.log(`User **<@${interaction.user.id}>** *(${interaction.user.username}#${interaction.user.discriminator})* requested incomplete or broken menu item ${selected}.`); + } + } catch (error) { + return console.log(error); + } +}); \ No newline at end of file diff --git a/exports/bossMessageID.js b/exports/bossMessageID.js new file mode 100644 index 0000000..dfdae7e --- /dev/null +++ b/exports/bossMessageID.js @@ -0,0 +1,220 @@ +exports.bossTimers = [ + // Define message ID's of boss channels. + + // Apophis + { key: `apophisChannel1`, messageID: `` }, + { key: `apophisChannel2`, messageID: `` }, + { key: `apophisChannel3`, messageID: `` }, + { key: `apophisChannel4`, messageID: `` }, + { key: `apophisChannel5`, messageID: `` }, + { key: `apophisChannel6`, messageID: `` }, + { key: `apophisChannel7`, messageID: `` }, + { key: `apophisChannel8`, messageID: `` }, + { key: `apophisChannel9`, messageID: `` }, + { key: `apophisChannel10`, messageID: `` }, + { key: `apophisChannel11`, messageID: `` }, + { key: `apophisChannel12`, messageID: `` }, + { key: `apophisChannel13`, messageID: `` }, + { key: `apophisChannel14`, messageID: `` }, + { key: `apophisChannel15`, messageID: `` }, + { key: `apophisChannel16`, messageID: `` }, + + // Barbarossa + { key: `barbarossaChannel1`, messageID: `` }, + { key: `barbarossaChannel2`, messageID: `` }, + { key: `barbarossaChannel3`, messageID: `` }, + { key: `barbarossaChannel4`, messageID: `` }, + { key: `barbarossaChannel5`, messageID: `` }, + { key: `barbarossaChannel6`, messageID: `` }, + { key: `barbarossaChannel7`, messageID: `` }, + { key: `barbarossaChannel8`, messageID: `` }, + { key: `barbarossaChannel9`, messageID: `` }, + { key: `barbarossaChannel10`, messageID: `` }, + { key: `barbarossaChannel11`, messageID: `` }, + { key: `barbarossaChannel12`, messageID: `` }, + { key: `barbarossaChannel13`, messageID: `` }, + { key: `barbarossaChannel14`, messageID: `` }, + { key: `barbarossaChannel15`, messageID: `` }, + { key: `barbarossaChannel16`, messageID: `` }, + + // Culton + { key: `cultonChannel1`, messageID: `` }, + { key: `cultonChannel2`, messageID: `` }, + { key: `cultonChannel3`, messageID: `` }, + { key: `cultonChannel4`, messageID: `` }, + { key: `cultonChannel5`, messageID: `` }, + { key: `cultonChannel6`, messageID: `` }, + { key: `cultonChannel7`, messageID: `` }, + { key: `cultonChannel8`, messageID: `` }, + + // Devourer + { key: `devourerChannel1`, messageID: `` }, + { key: `devourerChannel2`, messageID: `` }, + + // Eva + { key: `evaChannel1`, messageID: `` }, + { key: `evaChannel2`, messageID: `` }, + { key: `evaChannel3`, messageID: `` }, + { key: `evaChannel4`, messageID: `` }, + { key: `evaChannel5`, messageID: `` }, + { key: `evaChannel6`, messageID: `` }, + { key: `evaChannel7`, messageID: `` }, + { key: `evaChannel8`, messageID: `` }, + { key: `evaChannel9`, messageID: `` }, + { key: `evaChannel10`, messageID: `` }, + + // Frost Bot + { key: `frostbotChannel1`, messageID: `` }, + { key: `frostbotChannel2`, messageID: `` }, + { key: `frostbotChannel3`, messageID: `` }, + { key: `frostbotChannel4`, messageID: `` }, + { key: `frostbotChannel5`, messageID: `` }, + { key: `frostbotChannel6`, messageID: `` }, + { key: `frostbotChannel7`, messageID: `` }, + { key: `frostbotChannel8`, messageID: `` }, + { key: `frostbotChannel9`, messageID: `` }, + { key: `frostbotChannel10`, messageID: `` }, + { key: `frostbotChannel11`, messageID: `` }, + { key: `frostbotChannel12`, messageID: `` }, + { key: `frostbotChannel13`, messageID: `` }, + { key: `frostbotChannel14`, messageID: `` }, + { key: `frostbotChannel15`, messageID: `` }, + { key: `frostbotChannel16`, messageID: `` }, + + // Haboela + { key: `haboelaChannel1`, messageID: `` }, + { key: `haboelaChannel2`, messageID: `` }, + { key: `haboelaChannel3`, messageID: `` }, + { key: `haboelaChannel4`, messageID: `` }, + { key: `haboelaChannel5`, messageID: `` }, + { key: `haboelaChannel6`, messageID: `` }, + { key: `haboelaChannel7`, messageID: `` }, + { key: `haboelaChannel8`, messageID: `` }, + { key: `haboelaChannel9`, messageID: `` }, + { key: `haboelaChannel10`, messageID: `` }, + { key: `haboelaChannel11`, messageID: `` }, + { key: `haboelaChannel12`, messageID: `` }, + { key: `haboelaChannel13`, messageID: `` }, + { key: `haboelaChannel14`, messageID: `` }, + { key: `haboelaChannel15`, messageID: `` }, + { key: `haboelaChannel16`, messageID: `` }, + { key: `haboelaChannel17`, messageID: `` }, + { key: `haboelaChannel18`, messageID: `` }, + { key: `haboelaChannel19`, messageID: `` }, + { key: `haboelaChannel20`, messageID: `` }, + + // Harrah + { key: `harrahChannel1`, messageID: `` }, + { key: `harrahChannel2`, messageID: `` }, + { key: `harrahChannel3`, messageID: `` }, + { key: `harrahChannel4`, messageID: `` }, + { key: `harrahChannel5`, messageID: `` }, + { key: `harrahChannel6`, messageID: `` }, + { key: `harrahChannel7`, messageID: `` }, + { key: `harrahChannel8`, messageID: `` }, + + // Interdimensional Frostfire Dragon + { key: `dragonChannel1`, messageID: `` }, + { key: `dragonChannel2`, messageID: `` }, + + // Lucia + { key: `luciaChannel1`, messageID: `` }, + { key: `luciaChannel2`, messageID: `` }, + { key: `luciaChannel3`, messageID: `` }, + { key: `luciaChannel4`, messageID: `` }, + { key: `luciaChannel5`, messageID: `` }, + { key: `luciaChannel6`, messageID: `` }, + { key: `luciaChannel7`, messageID: `` }, + { key: `luciaChannel8`, messageID: `` }, + { key: `luciaChannel9`, messageID: `` }, + { key: `luciaChannel10`, messageID: `` }, + { key: `luciaChannel11`, messageID: `` }, + { key: `luciaChannel12`, messageID: `` }, + { key: `luciaChannel13`, messageID: `` }, + { key: `luciaChannel14`, messageID: `` }, + { key: `luciaChannel15`, messageID: `` }, + { key: `luciaChannel16`, messageID: `` }, + + // Magma + { key: `magmaChannel1`, messageID: `` }, + { key: `magmaChannel2`, messageID: `` }, + { key: `magmaChannel3`, messageID: `` }, + { key: `magmaChannel4`, messageID: `` }, + { key: `magmaChannel5`, messageID: `` }, + { key: `magmaChannel6`, messageID: `` }, + { key: `magmaChannel7`, messageID: `` }, + { key: `magmaChannel8`, messageID: `` }, + { key: `magmaChannel9`, messageID: `` }, + { key: `magmaChannel10`, messageID: `` }, + + // Robarg + { key: `robargChannel1`, messageID: `` }, + { key: `robargChannel2`, messageID: `` }, + { key: `robargChannel3`, messageID: `` }, + { key: `robargChannel4`, messageID: `` }, + { key: `robargChannel5`, messageID: `` }, + { key: `robargChannel6`, messageID: `` }, + { key: `robargChannel7`, messageID: `` }, + { key: `robargChannel8`, messageID: `` }, + { key: `robargChannel9`, messageID: `` }, + { key: `robargChannel10`, messageID: `` }, + { key: `robargChannel11`, messageID: `` }, + { key: `robargChannel12`, messageID: `` }, + { key: `robargChannel13`, messageID: `` }, + { key: `robargChannel14`, messageID: `` }, + { key: `robargChannel15`, messageID: `` }, + { key: `robargChannel16`, messageID: `` }, + + // Rudolph + { key: `rudolphChannel1`, messageID: `` }, + { key: `rudolphChannel2`, messageID: `` }, + { key: `rudolphChannel3`, messageID: `` }, + { key: `rudolphChannel4`, messageID: `` }, + { key: `rudolphChannel5`, messageID: `` }, + { key: `rudolphChannel6`, messageID: `` }, + { key: `rudolphChannel7`, messageID: `` }, + { key: `rudolphChannel8`, messageID: `` }, + { key: `rudolphChannel9`, messageID: `` }, + { key: `rudolphChannel10`, messageID: `` }, + + // Scylla + { key: `scyllaChannel1`, messageID: `` }, + { key: `scyllaChannel2`, messageID: `` }, + { key: `scyllaChannel3`, messageID: `` }, + { key: `scyllaChannel4`, messageID: `` }, + { key: `scyllaChannel5`, messageID: `` }, + { key: `scyllaChannel6`, messageID: `` }, + { key: `scyllaChannel7`, messageID: `` }, + { key: `scyllaChannel8`, messageID: `` }, + { key: `scyllaChannel9`, messageID: `` }, + { key: `scyllaChannel10`, messageID: `` }, + { key: `scyllaChannel11`, messageID: `` }, + { key: `scyllaChannel12`, messageID: `` }, + { key: `scyllaChannel13`, messageID: `` }, + { key: `scyllaChannel14`, messageID: `` }, + { key: `scyllaChannel16`, messageID: `` }, + { key: `scyllaChannel17`, messageID: `` }, + { key: `scyllaChannel18`, messageID: `` }, + { key: `scyllaChannel19`, messageID: `` }, + { key: `scyllaChannel20`, messageID: `` }, + + + // Sobek + { key: `sobekChannel1`, messageID: `` }, + { key: `sobekChannel2`, messageID: `` }, + { key: `sobekChannel3`, messageID: `` }, + { key: `sobekChannel4`, messageID: `` }, + { key: `sobekChannel5`, messageID: `` }, + { key: `sobekChannel6`, messageID: `` }, + { key: `sobekChannel7`, messageID: `` }, + { key: `sobekChannel8`, messageID: `` }, + { key: `sobekChannel9`, messageID: `` }, + { key: `sobekChannel10`, messageID: `` }, + { key: `sobekChannel11`, messageID: `` }, + { key: `sobekChannel12`, messageID: `` }, + { key: `sobekChannel13`, messageID: `` }, + { key: `sobekChannel14`, messageID: `` }, + { key: `sobekChannel15`, messageID: `` }, + { key: `sobekChannel16`, messageID: `` } + +] \ No newline at end of file diff --git a/index.js b/index.js new file mode 100644 index 0000000..d6d6efa --- /dev/null +++ b/index.js @@ -0,0 +1,51 @@ +// Require the necessary discord.js classes. +const { ActivityType, Client, Collection, Events, GatewayIntentBits } = require('discord.js'); + +// Require dotenv for environment variables. +require("dotenv").config(); + +// Require native Node modules. +const fs = require('node:fs'); +const path = require('node:path'); + + +// Declare and set a Discord token from the environment variables. +const { token } = process.env.DISCORD_TOKEN; + +// Create a new client instance +const client = new Client({ intents: [GatewayIntentBits.Guilds] }); +global.client = client; + +// Log in to Discord with your client's token +client.login(process.env.token); + +// Command Handling +client.commands = new Collection(); + +const commandsPath = path.join(__dirname, 'commands'); +const commandFiles = fs.readdirSync(commandsPath).filter(file => file.endsWith('.js')); + +for (const file of commandFiles) { + const filePath = path.join(commandsPath, file); + const command = require(filePath); + // Set a new item in the Collection with the key as the command name and the value as the exported module + if ('data' in command && 'execute' in command) { + client.commands.set(command.data.name, command); + } else { + console.log(`[WARNING] The command at ${filePath} is missing a required "data" or "execute" property.`); + } +} + +// Event Handling +const eventsPath = path.join(__dirname, 'events'); +const eventFiles = fs.readdirSync(eventsPath).filter(file => file.endsWith('.js')); + +for (const file of eventFiles) { + const filePath = path.join(eventsPath, file); + const event = require(filePath); + if (event.once) { + client.once(event.name, (...args) => event.execute(...args)); + } else { + client.on(event.name, (...args) => event.execute(...args)); + } +} \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..2ec8cc4 --- /dev/null +++ b/package.json @@ -0,0 +1,15 @@ +{ + "name": "tof-boss-tracker", + "version": "1.0.0", + "description": "A BOT to track boss kills in Tower of Fantasy.", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC", + "dependencies": { + "discord.js": "^14.7.1", + "dotenv": "^16.0.3" + } +} diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..780158d --- /dev/null +++ b/readme.md @@ -0,0 +1,70 @@ +### Tower of Fantasy Discord Boss Tracker Bot +## About + +This bot tracks defeated bosses by boss and channel in an easy-to use way. Simply click the boss in a forum channel list, then select the channel the boss was defeated in to update the tracker for everyone. You can also display a personal channel change cooldown to keep track of who will change next when chaining bosses. Screenshots are below. + +## Pre-requisites +A Discord account with [developer mode enabled](https://support.discord.com/hc/en-us/articles/206346498-Where-can-I-find-my-User-Server-Message-ID-) and Node.JS. +If you're not familiar with Node.JS or working with developer applications and Bots on Discord, please see [this guide](https://discordjs.guide/preparations) and follow the sections below. +* [Installing Node.js](https://discordjs.guide/preparations/#installing-node-js) +* [Setting up a bot application](https://discordjs.guide/preparations/setting-up-a-bot-application.html) +* [Adding your bot to servers](https://discordjs.guide/preparations/adding-your-bot-to-servers.html) + +## Initial Bot Setup: +1. Clone or [download](https://github.com/SloneFallion/tof-boss-tracker/archive/refs/heads/main.zip) the repository. +2. Edit `.env` to supply the necessary values. +3. In a terminal, change to the directory. +4. Run `npm install discord.js dotenv`. +5. Run `node deploy.js` to deploy commands. +6. Run `node index.js` to start the bot (for automatic startup and recovery, I recommend [PM2](https://pm2.keymetrics.io)). + +## Discord Setup: +1. Create a new forum in your Discord server and give it the following permissions: +* Everyone: + * Deny ALL +* Bot + * Allow Send Messages in Posts + * Allow Embed Links + * Allow Read Post History + +2. For each boss: +Create a forum post with the title of the boss and your preferred picture of the boss. +Note: If you want the boss tracker forum to be sorted alphabetically, it is recommended that you set the forum sort order to Creation Time, then post the bosses alphabetically reversed (Z-A). + +3. For each boss: +Use the /say command to issue placeholder messages. You need a placeholder for each channel, then an extra placeholder for the sorted list. +`/say message:Time not set.` +Use the /interaction command after the placeholder messages have been created. +`/interaction create:Boss Timer bossmenu:` + +## Configuring the Bot +1. Shut down the bot. +2. Edit `exports/bossMessageID.js` and copy the placeholder message ID's of each boss to the corresponding to their order (first to last message). The last placeholder message ID before the interaction menu does not need to be obtained/configured. + +## Production +1. Start the bot. +2. Set the forum permissions as follows: +* Everyone: + * Deny ALL + * Allow View Channel + * Allow Read Post History + * Allow Create Invite at your discretion +* Bot + * No changes + +You're ready to go! + +## Future Changes +If and when channels are removed or added from servers (usually after a patch), the files `commands/interaction.js` and `exports/bossMessageID.js` will need to be updated to reflect the channel availability. + +Specifically for adding new channels, placeholder messages may need to be regenerated for formatting reasons, as it will not show contiguously if new messages are added. Step 3 of Discord Setup, as well as both steps in Configuring the BOT will need to be repeated. + +## Suggestion +Using an internal persistent database would save a lot of manual work on future updates regarding channel changes. Additionally, a single message embed could be used rather than many individual messages, which would allow for easy formatting changes. At this time, such a change is not being actively developed by me. + +## Screenshots +[![](https://i.imgur.com/DkLXWBt.png)](https://i.imgur.com/DkLXWBt.png) +[![](https://i.imgur.com/ralv6Ay.png)](https://i.imgur.com/ralv6Ay.png) + +## License +[MPL 2.0](https://choosealicense.com/licenses/mpl-2.0/) \ No newline at end of file