Ready for basic use, rules and tree role menu

This commit is contained in:
Skylar Grant 2023-02-10 22:41:47 -05:00
parent 0f79d61491
commit 23b17bc0ec
12 changed files with 248 additions and 88 deletions

View File

@ -1,10 +1,8 @@
name: Docker Image CI name: Voidbot Dockerization
on: on:
push:
branches: [ "master" ]
pull_request: pull_request:
branches: [ "master" ] branches: [ "main" ]
env: env:
DHUB_UNAME: ${{ secrets.DHUB_UNAME }} DHUB_UNAME: ${{ secrets.DHUB_UNAME }}
@ -19,8 +17,14 @@ jobs:
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- name: Build the Docker image - 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 - name: Log into Docker Hub
run: docker login -u $DHUB_UNAME -p $DHUB_PWORD run: docker login -u $DHUB_UNAME -p $DHUB_PWORD
- name: Push image to Docker Hub - 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

View File

@ -1,2 +1,4 @@
# Discord Bot Template # VoidBot
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. 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.

View File

@ -5,17 +5,26 @@
"permissions": "" "permissions": ""
}, },
"embeds": { "embeds": {
"footer": "", "footer": "Have a great day!",
"color": "0x55FF55" "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": { "emoji": {
"next": "⏭️", "next": "⏭️",
"previous": "⏮️", "previous": "⏮️",
"confirm": "☑️", "confirm": "☑️",
"cancel": "❌" "cancel": "❌",
"water": "💧",
"fruit": "🍎"
}, },
"urls": { "roleIds": {
"avatar": "" "member": "1048328885118435368",
}, "waterPings": "1069416740389404763",
"temp": {} "fruitPings": "1073698485376921602"
}
} }

48
main.js
View File

@ -4,33 +4,29 @@
const dotenv = require('dotenv'); const dotenv = require('dotenv');
dotenv.config(); dotenv.config();
const token = process.env.TOKEN; const token = process.env.TOKEN;
const statusChannelId = process.env.statusChannelId;
// Discord.JS // Discord.JS
const { Client, GatewayIntentBits, Partials } = require('discord.js'); const { Client, GatewayIntentBits } = require('discord.js');
const client = new Client({ const client = new Client({
intents: [ intents: [
GatewayIntentBits.Guilds, GatewayIntentBits.Guilds
GatewayIntentBits.GuildMessages, ]
GatewayIntentBits.GuildMessageReactions,
GatewayIntentBits.MessageContent
],
partials: [
Partials.Channel,
Partials.Message
],
}); });
// Various imports // Various imports
const fn = require('./modules/functions.js'); const fn = require('./modules/functions.js');
const strings = require('./data/strings.json'); const strings = require('./data/strings.json');
const isDev = process.env.isDev; const isDev = process.env.DEBUG;
const statusChannelId = process.env.STATUSCHANNELID;
client.once('ready', () => { 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!'); console.log('Ready!');
client.channels.fetch(statusChannelId).then(channel => { client.channels.fetch(statusChannelId).then(channel => {
channel.send(`${new Date().toISOString()} -- Ready`); 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)) { if (client.slashCommands.has(commandName)) {
client.slashCommands.get(commandName).execute(interaction); client.slashCommands.get(commandName).execute(interaction);
} else { } 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); console.error('Slash command attempted to run but not found: /' + commandName);
} }
} } else if (interaction.isButton()) {
switch (interaction.component.customId) {
if (interaction.isButton() && interaction.component.customId == 'refresh') { case 'acceptrules':
fn.refresh(interaction); 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;
}
} }
}); });

8
modules/_cleanInput.js Normal file
View File

@ -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";

View File

@ -4,7 +4,7 @@ dotenv.config();
const { REST } = require('@discordjs/rest'); const { REST } = require('@discordjs/rest');
const { Routes } = require('discord-api-types/v9'); const { Routes } = require('discord-api-types/v9');
const clientId = process.env.clientId; const clientId = process.env.BOTID;
const token = process.env.TOKEN; const token = process.env.TOKEN;
const fs = require('fs'); const fs = require('fs');
@ -12,13 +12,13 @@ const commands = [];
const commandFiles = fs.readdirSync('./slash-commands').filter(file => file.endsWith('.js')); const commandFiles = fs.readdirSync('./slash-commands').filter(file => file.endsWith('.js'));
for (const file of commandFiles) { for (const file of commandFiles) {
const command = require(`./slash-commands/${file}`); const command = require(`../slash-commands/${file}`);
if (command.data != undefined) { if (command.data != undefined) {
commands.push(command.data.toJSON()); commands.push(command.data.toJSON());
} }
} }
console.log(commands); // console.log(commands);
const rest = new REST({ version: '9' }).setToken(token); const rest = new REST({ version: '9' }).setToken(token);

5
modules/buttons.js Normal file
View File

@ -0,0 +1,5 @@
const strings = require('../data/strings.json');
module.exports = {
}

View File

@ -13,13 +13,12 @@ const { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } = Discord;
// Various imports from other files // Various imports from other files
const config = require('../data/config.json'); const config = require('../data/config.json');
let guildInfo = require('../data/guildInfo.json');
const strings = require('../data/strings.json'); const strings = require('../data/strings.json');
const slashCommandFiles = fs.readdirSync('./slash-commands/').filter(file => file.endsWith('.js')); const slashCommandFiles = fs.readdirSync('./slash-commands/').filter(file => file.endsWith('.js'));
const functions = { const functions = {
// Functions for managing and creating Collections // Functions for managing and creating Collections
collections: { collectionBuilders: {
// Create the collection of slash commands // Create the collection of slash commands
slashCommands(client) { slashCommands(client) {
if (!client.slashCommands) client.slashCommands = new Discord.Collection(); if (!client.slashCommands) client.slashCommands = new Discord.Collection();
@ -34,59 +33,126 @@ const functions = {
} }
}, },
builders: { builders: {
refreshAction() { actionRows: {
// Create the button to go in the Action Row acceptRules() {
const refreshButton = new ButtonBuilder() // Create the Action Row with the Button in it, to be sent with the Embed
.setCustomId('refresh') return new ActionRowBuilder()
.setLabel('Refresh') .addComponents(
.setStyle(ButtonStyle.Primary); this.buttons.acceptRules()
// Create the Action Row with the Button in it, to be sent with the Embed );
const refreshActionRow = new ActionRowBuilder() },
.addComponents( treeRoleMenu() {
refreshButton return new ActionRowBuilder()
); .addComponents(
return refreshActionRow; 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) { embeds: {
const embed = new EmbedBuilder() helpEmbed(content, private) {
.setColor(strings.embeds.color) const embed = new EmbedBuilder()
.setTitle('Grow A Tree Analyzer Help') .setColor(strings.embeds.color)
.setDescription(content) .setTitle('Grow A Tree Analyzer Help')
.setFooter({ text: strings.embeds.footer }); .setDescription(content)
const privateBool = private == 'true'; .setFooter({ text: strings.embeds.footer });
const messageContents = { embeds: [embed], ephemeral: privateBool }; const privateBool = private == 'true';
return messageContents; const messageContents = { embeds: [embed], ephemeral: privateBool };
}, return messageContents;
errorEmbed(content) { },
const embed = new EmbedBuilder() errorEmbed(content) {
.setColor(0xFF0000) const embed = new EmbedBuilder()
.setTitle('Error!') .setColor(0xFF0000)
.setDescription(content) .setTitle('Error!')
.setFooter({ text: strings.embeds.footer }); .setDescription(content)
const messageContents = { embeds: [embed], ephemeral: true }; .setFooter({ text: strings.embeds.footer });
return messageContents; const messageContents = { embeds: [embed], ephemeral: true };
}, return messageContents;
embed(content) { },
const embed = new EmbedBuilder() info(content) {
.setColor(0x8888FF) const embed = new EmbedBuilder()
.setTitle('Information') .setColor(0x8888FF)
.setDescription(content) .setTitle('Information')
.setFooter({ text: strings.embeds.footer }); .setDescription(content)
const messageContents = { embeds: [embed], ephemeral: true }; .setFooter({ text: strings.embeds.footer });
return messageContents; 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) { roles: {
functions.rankings.parse(interaction).then(r1 => { async fetchRole(guild, roleId) {
functions.tree.parse(interaction).then(r2 => { return await guild.roles.fetch(roleId).catch(err => console.error("Error fetching the role: " + err));
const embed = functions.builders.comparisonEmbed(functions.rankings.compare(interaction), functions.builders.refreshAction()) },
interaction.update(embed); async giveRole(member, role) {
}).catch(e => { await member.roles.add(role).catch(err => console.error("Error giving the role: " + err));
interaction.reply(functions.builders.errorEmbed(e)); },
}); async takeRole(member, role) {
}).catch(e => { await member.roles.remove(role).catch(err => console.error("Error taking the role: " + err));
interaction.reply(functions.builders.errorEmbed(e)); }
}); },
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));
}
} }
}; };

1
modules/input.txt Normal file
View File

@ -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.

24
package.json Normal file
View File

@ -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"
}
}

View File

@ -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));
},
};

17
slash-commands/rules.js Normal file
View File

@ -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);
}
},
};