diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index 5f27b9d..ba2680c 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -1,10 +1,8 @@ -name: Docker Image CI +name: Voidbot Dockerization on: - push: - branches: [ "master" ] pull_request: - branches: [ "master" ] + branches: [ "main" ] env: DHUB_UNAME: ${{ secrets.DHUB_UNAME }} @@ -19,8 +17,14 @@ jobs: steps: - uses: actions/checkout@v3 - name: Build the Docker image - run: docker build . --file Dockerfile --tag v0idf1sh/treeanalyzer + run: docker build . --file Dockerfile --tag v0idf1sh/voidbot - name: Log into Docker Hub run: docker login -u $DHUB_UNAME -p $DHUB_PWORD - name: Push image to Docker Hub - run: docker push v0idf1sh/treeanalyzer + run: docker push v0idf1sh/voidbot + - name: Set up a skeleton .env file + run: echo "TOKEN=${{secrets.TOKEN}}" > .env && echo "BOTID=${{ secrets.BOTID }}" >> .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/README.md b/README.md index e49c919..a59ad06 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,4 @@ -# Discord Bot Template -This is a very basic Discord.js v14 bot template. This is meant to be an easy jumping-off point for quick bot creation without having to set up the basics every time. \ No newline at end of file +# VoidBot +This is a super simple bot developed specifically for use in my development support Discord server. + +VoidBot will handle automatically giving roles to members of the server, allow users to self-assign some roles, and will send custom embeds like About and Help messages. \ No newline at end of file diff --git a/data/strings.json b/data/strings.json index 6f3f0e9..ee29d78 100644 --- a/data/strings.json +++ b/data/strings.json @@ -5,17 +5,26 @@ "permissions": "" }, "embeds": { - "footer": "", - "color": "0x55FF55" + "footer": "Have a great day!", + "color": "0x5555FF", + "rulesTitle": "voidf1sh Development Server Rules", + "rules": "1. Show respect\n2. No politics\n3. No spam or self-promotion (server invites, advertisements, etc) without permission from a staff member. This includes DMing fellow members.\n4. No age-restricted or obscene content. This includes text, images, or links featuring nudity, sex, hard violence, or other graphically disturbing content.\n5. If you see something against the rules or something that makes you feel unsafe, let staff know. We want this server to be a welcoming space!", + "rulesFooter": "Use the Accept Rules button to gain access to the rest of the server.", + "roleMenuTitle": "Role Menu", + "treeRoleMenu": "Use the buttons below to give yourself roles.\n\n``💧`` - <@&1069416740389404763>: Get notifications when the tree is ready to be watered.\n``🍎`` - <@&1073698485376921602>: Get notifications when fruit is falling from the tree.", + "roleMenuFooter": "Tip: Tap the button again to remove the role." }, "emoji": { "next": "⏭️", "previous": "⏮️", "confirm": "☑️", - "cancel": "❌" + "cancel": "❌", + "water": "💧", + "fruit": "🍎" }, - "urls": { - "avatar": "" - }, - "temp": {} + "roleIds": { + "member": "1048328885118435368", + "waterPings": "1069416740389404763", + "fruitPings": "1073698485376921602" + } } \ No newline at end of file diff --git a/main.js b/main.js index dc55716..78127b2 100644 --- a/main.js +++ b/main.js @@ -4,33 +4,29 @@ const dotenv = require('dotenv'); dotenv.config(); const token = process.env.TOKEN; -const statusChannelId = process.env.statusChannelId; // Discord.JS -const { Client, GatewayIntentBits, Partials } = require('discord.js'); +const { Client, GatewayIntentBits } = require('discord.js'); const client = new Client({ intents: [ - GatewayIntentBits.Guilds, - GatewayIntentBits.GuildMessages, - GatewayIntentBits.GuildMessageReactions, - GatewayIntentBits.MessageContent - ], - partials: [ - Partials.Channel, - Partials.Message - ], + GatewayIntentBits.Guilds + ] }); // Various imports const fn = require('./modules/functions.js'); const strings = require('./data/strings.json'); -const isDev = process.env.isDev; +const isDev = process.env.DEBUG; +const statusChannelId = process.env.STATUSCHANNELID; client.once('ready', () => { - fn.collections.slashCommands(client); + // Build a collection of slash commands for the bot to use + fn.collectionBuilders.slashCommands(client); console.log('Ready!'); client.channels.fetch(statusChannelId).then(channel => { channel.send(`${new Date().toISOString()} -- Ready`); + }).catch(err => { + console.error("Error sending status message: " + err); }); }); @@ -42,13 +38,29 @@ client.on('interactionCreate', async interaction => { if (client.slashCommands.has(commandName)) { client.slashCommands.get(commandName).execute(interaction); } else { - interaction.reply('Sorry, I don\'t have access to that command.'); + interaction.reply('Sorry, I don\'t have access to that command.').catch(err => console.error(err)); console.error('Slash command attempted to run but not found: /' + commandName); } - } - - if (interaction.isButton() && interaction.component.customId == 'refresh') { - fn.refresh(interaction); + } else if (interaction.isButton()) { + switch (interaction.component.customId) { + case 'acceptrules': + await fn.buttonHandlers.acceptRules(interaction).catch(err => { + console.error("Error handling rule acceptance: " + err); + }); + break; + case 'waterpingrole': + await fn.buttonHandlers.waterPing(interaction).catch(err => { + console.error("Error handling water ping button: " + err); + }); + break; + case 'fruitpingrole': + await fn.buttonHandlers.fruitPing(interaction).catch(err => { + console.error("Error handling fruit ping button: " + err); + }); + break; + default: + break; + } } }); diff --git a/modules/_cleanInput.js b/modules/_cleanInput.js new file mode 100644 index 0000000..f112c53 --- /dev/null +++ b/modules/_cleanInput.js @@ -0,0 +1,8 @@ +const path = './modules/input.txt'; +const fs = require('fs'); +const replaceAll = require('string.prototype.replaceall'); +const string = fs.readFileSync(path).toString(); +console.log(JSON.stringify(string)); +let newString = replaceAll(string, '\r\n', '\\n'); +fs.writeFileSync(path, newString); +return "Done"; \ No newline at end of file diff --git a/modules/_deploy-global.js b/modules/_deploy-global.js index cd7b8ce..431ddf1 100644 --- 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'); @@ -12,13 +12,13 @@ const commands = []; const commandFiles = fs.readdirSync('./slash-commands').filter(file => file.endsWith('.js')); for (const file of commandFiles) { - const command = require(`./slash-commands/${file}`); + const command = require(`../slash-commands/${file}`); if (command.data != undefined) { commands.push(command.data.toJSON()); } } -console.log(commands); +// console.log(commands); const rest = new REST({ version: '9' }).setToken(token); diff --git a/modules/buttons.js b/modules/buttons.js new file mode 100644 index 0000000..2540125 --- /dev/null +++ b/modules/buttons.js @@ -0,0 +1,5 @@ +const strings = require('../data/strings.json'); + +module.exports = { + +} \ No newline at end of file diff --git a/modules/functions.js b/modules/functions.js index f3edb7f..58c287c 100644 --- a/modules/functions.js +++ b/modules/functions.js @@ -13,13 +13,12 @@ const { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } = Discord; // Various imports from other files const config = require('../data/config.json'); -let guildInfo = require('../data/guildInfo.json'); const strings = require('../data/strings.json'); const slashCommandFiles = fs.readdirSync('./slash-commands/').filter(file => file.endsWith('.js')); 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(); @@ -34,59 +33,126 @@ const functions = { } }, builders: { - refreshAction() { - // Create the button to go in the Action Row - const refreshButton = new ButtonBuilder() - .setCustomId('refresh') - .setLabel('Refresh') - .setStyle(ButtonStyle.Primary); - // Create the Action Row with the Button in it, to be sent with the Embed - const refreshActionRow = new ActionRowBuilder() - .addComponents( - refreshButton - ); - return refreshActionRow; + actionRows: { + acceptRules() { + // Create the Action Row with the Button in it, to be sent with the Embed + return new ActionRowBuilder() + .addComponents( + this.buttons.acceptRules() + ); + }, + treeRoleMenu() { + return new ActionRowBuilder() + .addComponents( + this.buttons.waterPing(), + this.buttons.fruitPing() + ); + }, + 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); + } + } }, - helpEmbed(content, private) { - const embed = new EmbedBuilder() - .setColor(strings.embeds.color) - .setTitle('Grow A Tree Analyzer Help') - .setDescription(content) - .setFooter({ text: strings.embeds.footer }); - const privateBool = private == 'true'; - const messageContents = { embeds: [embed], ephemeral: privateBool }; - return messageContents; - }, - errorEmbed(content) { - const embed = new EmbedBuilder() - .setColor(0xFF0000) - .setTitle('Error!') - .setDescription(content) - .setFooter({ text: strings.embeds.footer }); - const messageContents = { embeds: [embed], ephemeral: true }; - return messageContents; - }, - embed(content) { - const embed = new EmbedBuilder() - .setColor(0x8888FF) - .setTitle('Information') - .setDescription(content) - .setFooter({ text: strings.embeds.footer }); - const messageContents = { embeds: [embed], ephemeral: true }; - return messageContents; + embeds: { + helpEmbed(content, private) { + const embed = new EmbedBuilder() + .setColor(strings.embeds.color) + .setTitle('Grow A Tree Analyzer Help') + .setDescription(content) + .setFooter({ text: strings.embeds.footer }); + const privateBool = private == 'true'; + const messageContents = { embeds: [embed], ephemeral: privateBool }; + return messageContents; + }, + errorEmbed(content) { + const embed = new EmbedBuilder() + .setColor(0xFF0000) + .setTitle('Error!') + .setDescription(content) + .setFooter({ text: strings.embeds.footer }); + const messageContents = { embeds: [embed], ephemeral: true }; + return messageContents; + }, + info(content) { + const embed = new EmbedBuilder() + .setColor(0x8888FF) + .setTitle('Information') + .setDescription(content) + .setFooter({ text: strings.embeds.footer }); + const messageContents = { embeds: [embed], ephemeral: true }; + return messageContents; + }, + rules() { + const actionRow = functions.builders.actionRows.acceptRules(); + const embed = new EmbedBuilder() + .setColor(strings.embeds.color) + .setTitle(strings.embeds.rulesTitle) + .setDescription(strings.embeds.rules) + .setFooter({ text: strings.embeds.rulesFooter }); + return { embeds: [embed], components: [actionRow] }; + }, + treeRoleMenu() { + const actionRow = functions.builders.actionRows.treeRoleMenu(); + const embed = new EmbedBuilder() + .setColor(strings.embeds.color) + .setTitle(strings.embeds.roleMenuTitle) + .setDescription(strings.embeds.treeRoleMenu) + .setFooter({ text: strings.embeds.roleMenuFooter }); + return { embeds: [embed], components: [actionRow] }; + } } }, - refresh(interaction) { - functions.rankings.parse(interaction).then(r1 => { - functions.tree.parse(interaction).then(r2 => { - const embed = functions.builders.comparisonEmbed(functions.rankings.compare(interaction), functions.builders.refreshAction()) - interaction.update(embed); - }).catch(e => { - interaction.reply(functions.builders.errorEmbed(e)); - }); - }).catch(e => { - interaction.reply(functions.builders.errorEmbed(e)); - }); + roles: { + async fetchRole(guild, roleId) { + return await guild.roles.fetch(roleId).catch(err => console.error("Error fetching the role: " + err)); + }, + async giveRole(member, role) { + await member.roles.add(role).catch(err => console.error("Error giving the role: " + err)); + }, + async takeRole(member, role) { + await member.roles.remove(role).catch(err => console.error("Error taking the role: " + err)); + } + }, + buttonHandlers: { + async fruitPing(interaction) { + const role = await functions.roles.fetchRole(interaction.guild, strings.roleIds.fruitPings); + if (interaction.member.roles.cache.some(role => role.id == strings.roleIds.fruitPings)) { + functions.roles.takeRole(interaction.member, role); + } else { + functions.roles.giveRole(interaction.member, role); + } + await interaction.reply(functions.builders.embeds.info("Roles updated!")).catch(err => console.error(err)); + }, + async waterPing(interaction) { + const role = await functions.roles.fetchRole(interaction.guild, strings.roleIds.waterPings); + if (interaction.member.roles.cache.some(role => role.id == strings.roleIds.waterPings)) { + functions.roles.takeRole(interaction.member, role); + } else { + functions.roles.giveRole(interaction.member, role); + } + await interaction.reply(functions.builders.embeds.info("Roles updated!")).catch(err => console.error(err)); + }, + async acceptRules(interaction) { + const role = await functions.roles.fetchRole(interaction.guild, strings.roleIds.member); + functions.roles.giveRole(interaction.member, role).catch(err => console.error(err)); + await interaction.reply(functions.builders.embeds.info("Roles updated!")).catch(err => console.error(err)); + } } }; diff --git a/modules/input.txt b/modules/input.txt new file mode 100644 index 0000000..3f31249 --- /dev/null +++ b/modules/input.txt @@ -0,0 +1 @@ +Use the buttons below to give yourself roles.\n\n``💧`` - <@1073794977886392410>: Get notifications when the tree is ready to be watered.\n``🍎`` - <@1073795088183996496>: Get notifications when fruit is falling from the tree. \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..09011d9 --- /dev/null +++ b/package.json @@ -0,0 +1,24 @@ +{ + "name": "voidbot", + "version": "1.0.0", + "description": "voidf1sh Development Server Support Bot", + "main": "main.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/voidf1sh/voidbot.git" + }, + "author": "Skylar Grant", + "license": "ISC", + "bugs": { + "url": "https://github.com/voidf1sh/voidbot/issues" + }, + "homepage": "https://github.com/voidf1sh/voidbot#readme", + "dependencies": { + "discord.js": "^14.7.1", + "dotenv": "^16.0.3", + "string.prototype.replaceall": "^1.0.7" + } +} diff --git a/slash-commands/rolemenu.js b/slash-commands/rolemenu.js new file mode 100644 index 0000000..b891225 --- /dev/null +++ b/slash-commands/rolemenu.js @@ -0,0 +1,12 @@ +const { SlashCommandBuilder } = require('discord.js'); +const fn = require('../modules/functions.js'); + +module.exports = { + data: new SlashCommandBuilder() + .setName('rolemenu') + .setDescription('Send the role selection menu in the current channel'), + async execute(interaction) { + await interaction.deferReply().catch(err => console.error(err)); + await interaction.editReply(fn.builders.embeds.treeRoleMenu()).catch(err => console.error(err)); + }, +}; \ No newline at end of file diff --git a/slash-commands/rules.js b/slash-commands/rules.js new file mode 100644 index 0000000..f9040a3 --- /dev/null +++ b/slash-commands/rules.js @@ -0,0 +1,17 @@ +const { SlashCommandBuilder } = require('discord.js'); +const fn = require('../modules/functions.js'); + +module.exports = { + data: new SlashCommandBuilder() + .setName('rules') + .setDescription('Send the rules in the current channel'), + async execute(interaction) { + try { + await interaction.deferReply().catch(err => console.error(err)); + await interaction.editReply(fn.builders.embeds.rules()); + } catch(err) { + console.error(err); + } + + }, +}; \ No newline at end of file