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:
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

View File

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

View File

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

48
main.js
View File

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

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 { 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);

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

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