From 7bff6db516569274eee0037650ca1803317d3bd4 Mon Sep 17 00:00:00 2001 From: Skylar Grant Date: Thu, 4 Jul 2024 20:29:49 +0000 Subject: [PATCH] Initial commit --- .dockerignore | 5 ++ .eslintrc.json | 49 ++++++++++++ .github/workflows/docker-image.yml | 29 +++++++ .gitignore | 118 +++++++++++++++++++++++++++++ Dockerfile | 8 ++ README.md | 7 ++ data/config.json | 4 + data/strings.json | 24 ++++++ main.js | 48 ++++++++++++ modules/_clear-commands.js | 28 +++++++ modules/_deploy-global.js | 38 ++++++++++ modules/_sanitizeInput.js | 18 +++++ modules/functions.js | 80 +++++++++++++++++++ modules/input.txt | 0 package.json | 7 ++ slash-commands/commands.js | 37 +++++++++ slash-commands/help.js | 30 ++++++++ slash-commands/template | 31 ++++++++ 18 files changed, 561 insertions(+) create mode 100644 .dockerignore create mode 100644 .eslintrc.json create mode 100644 .github/workflows/docker-image.yml create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 data/config.json create mode 100644 data/strings.json create mode 100644 main.js create mode 100644 modules/_clear-commands.js create mode 100644 modules/_deploy-global.js create mode 100644 modules/_sanitizeInput.js create mode 100644 modules/functions.js create mode 100644 modules/input.txt create mode 100644 package.json create mode 100644 slash-commands/commands.js create mode 100644 slash-commands/help.js create mode 100644 slash-commands/template diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..163b5f6 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,5 @@ +node_modules +npm-debug.log +env.dev +env.prod +.env \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..ae431cc --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,49 @@ +{ + "extends": "eslint:recommended", + "env": { + "node": true, + "es6": true + }, + "parserOptions": { + "ecmaVersion": 2021 + }, + "rules": { + "arrow-spacing": ["warn", { "before": true, "after": true }], + "brace-style": ["error", "stroustrup", { "allowSingleLine": true }], + "comma-dangle": ["error", "always-multiline"], + "comma-spacing": "error", + "comma-style": "error", + "curly": ["error", "multi-line", "consistent"], + "dot-location": ["error", "property"], + "handle-callback-err": "off", + "indent": ["error", "tab"], + "keyword-spacing": "error", + "max-nested-callbacks": ["error", { "max": 4 }], + "max-statements-per-line": ["error", { "max": 2 }], + "no-console": "off", + "no-empty-function": "error", + "no-floating-decimal": "error", + "no-inline-comments": "error", + "no-lonely-if": "error", + "no-multi-spaces": "error", + "no-multiple-empty-lines": ["error", { "max": 2, "maxEOF": 1, "maxBOF": 0 }], + "no-shadow": ["error", { "allow": ["err", "resolve", "reject"] }], + "no-trailing-spaces": ["error"], + "no-var": "error", + "object-curly-spacing": ["error", "always"], + "prefer-const": "error", + "quotes": ["error", "single"], + "semi": ["error", "always"], + "space-before-blocks": "error", + "space-before-function-paren": ["error", { + "anonymous": "never", + "named": "never", + "asyncArrow": "always" + }], + "space-in-parens": "error", + "space-infix-ops": "error", + "space-unary-ops": "error", + "spaced-comment": "error", + "yoda": "error" + } +} \ No newline at end of file diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml new file mode 100644 index 0000000..acfae2a --- /dev/null +++ b/.github/workflows/docker-image.yml @@ -0,0 +1,29 @@ +name: Bot Dockerization + +on: + workflow_dispatch: + +env: + DHUB_UNAME: ${{ secrets.DHUB_UNAME }} + DHUB_PWORD: ${{ secrets.DHUB_PWORD }} + +jobs: + + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Build the Docker image + run: docker build . --file Dockerfile --tag v0idf1sh/tag + - name: Log into Docker Hub + run: docker login -u $DHUB_UNAME -p $DHUB_PWORD + - name: Push image to Docker Hub + run: docker push v0idf1sh/tag + - 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/.gitignore b/.gitignore new file mode 100644 index 0000000..923ffd7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,118 @@ +# IDE Config Files +.vscode +package-lock.json +.VSCodeCounter/ +env.dev +env.prod +.DS_Store + +# Custom folders +# gifs/* +# pastas/* + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-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/ + +# TypeScript v1 declaration files +typings/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# 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 variables file +.env +.env.test +.env.dev +.env.prod + +# parcel-bundler cache (https://parceljs.org/) +.cache + +# Next.js build output +.next + +# 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 + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..a0b785a --- /dev/null +++ b/Dockerfile @@ -0,0 +1,8 @@ +FROM node:16 +RUN mkdir -p /usr/src/app +WORKDIR /usr/src/app + +COPY package.json ./ +RUN npm install +COPY . . +CMD [ "node", "main.js" ] \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..4331276 --- /dev/null +++ b/README.md @@ -0,0 +1,7 @@ +# 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. + +# Dependencies +* `dotenv` +* `discord.js` +* `fs` (built in) \ No newline at end of file diff --git a/data/config.json b/data/config.json new file mode 100644 index 0000000..05eb669 --- /dev/null +++ b/data/config.json @@ -0,0 +1,4 @@ +{ + "guildId": "", + "validCommands": [] +} \ No newline at end of file diff --git a/data/strings.json b/data/strings.json new file mode 100644 index 0000000..05757af --- /dev/null +++ b/data/strings.json @@ -0,0 +1,24 @@ +{ + "help": { + "title": "Title", + "content": "Some help content", + "footer": "Witty Text Here" + }, + "embeds": { + "footer": "github/voidf1sh/discord-bot-template", + "color": "0x55FF55", + "errorTitle": "Error", + "errorColor": "0xFF0000", + "infoTitle": "Information", + "infoColor": "0x8888FF" + }, + "emoji": { + "next": "⏭️", + "previous": "⏮️", + "confirm": "☑️", + "cancel": "❌" + }, + "errors": { + "generalCommand": "Sorry, there was an error running that command." + } +} \ No newline at end of file diff --git a/main.js b/main.js new file mode 100644 index 0000000..929b536 --- /dev/null +++ b/main.js @@ -0,0 +1,48 @@ +/* eslint-disable no-case-declarations */ +/* eslint-disable indent */ +// dotenv for handling environment variables +const dotenv = require('dotenv'); +dotenv.config(); +const token = process.env.TOKEN;; + +// Discord.JS +const { Client, GatewayIntentBits } = require('discord.js'); +const client = new Client({ + intents: [ + GatewayIntentBits.Guilds + ] +}); + +// Various imports +const fn = require('./modules/functions.js'); +const strings = require('./data/strings.json'); +const debugMode = process.env.DEBUG; +const statusChannelId = process.env.STATUSCHANNELID + +client.once('ready', () => { + fn.collectionBuilders.slashCommands(client); + console.log('Ready!'); + // client.channels.fetch(statusChannelId).then(channel => { + // channel.send(`${new Date().toISOString()} -- Ready`).catch(e => console.error(e)); + // }); +}); + +// slash-commands +client.on('interactionCreate', async interaction => { + if (interaction.isCommand()) { + const { commandName } = interaction; + + if (client.slashCommands.has(commandName)) { + client.slashCommands.get(commandName).execute(interaction).catch(e => console.error(e)); + } else { + interaction.reply('Sorry, I don\'t have access to that command.').catch(e => console.error(e)); + console.error('Slash command attempted to run but not found: /' + commandName); + } + } +}); + +process.on('uncaughtException', err => { + console.error(err); +}); + +client.login(token); \ No newline at end of file diff --git a/modules/_clear-commands.js b/modules/_clear-commands.js new file mode 100644 index 0000000..34ee25d --- /dev/null +++ b/modules/_clear-commands.js @@ -0,0 +1,28 @@ +// dotenv for handling environment variables +const dotenv = require('dotenv'); +dotenv.config(); + +const { REST, Routes } = require('discord.js'); +const botId = process.env.BOTID; +const token = process.env.TOKEN; +const fs = require('fs'); + +console.log(`Token: ...${token.slice(-5)} | Bot ID: ${botId}`); + +const rest = new REST({ version: '10' }).setToken(token); + +(async () => { + try { + console.log('Started clearing global application (/) commands.'); + + await rest.put( + Routes.applicationCommands(botId), + { body: '' }, + ); + + console.log('Successfully cleared global application (/) commands.'); + process.exit(); + } catch (error) { + console.error(error); + } +})(); \ No newline at end of file diff --git a/modules/_deploy-global.js b/modules/_deploy-global.js new file mode 100644 index 0000000..f3c7c92 --- /dev/null +++ b/modules/_deploy-global.js @@ -0,0 +1,38 @@ +// dotenv for handling environment variables +const dotenv = require('dotenv'); +dotenv.config(); + +const { REST, Routes } = require('discord.js'); +const botId = process.env.BOTID; +const token = process.env.TOKEN; +const fs = require('fs'); + +const commands = []; +const commandFiles = fs.readdirSync('./slash-commands').filter(file => file.endsWith('.js')); + +for (const file of commandFiles) { + const command = require(`../slash-commands/${file}`); + if (command.data != undefined) { + commands.push(command.data.toJSON()); + } +} + +console.log(`Token: ...${token.slice(-5)} | Bot ID: ${botId}`); + +const rest = new REST({ version: '10' }).setToken(token); + +(async () => { + try { + console.log('Started refreshing global application (/) commands.'); + + await rest.put( + Routes.applicationCommands(botId), + { body: commands }, + ); + + console.log('Successfully reloaded global application (/) commands.'); + process.exit(); + } catch (error) { + console.error(error); + } +})(); \ No newline at end of file diff --git a/modules/_sanitizeInput.js b/modules/_sanitizeInput.js new file mode 100644 index 0000000..1a25698 --- /dev/null +++ b/modules/_sanitizeInput.js @@ -0,0 +1,18 @@ +const replaceAll = require('string.prototype.replaceall'); +const fs = require('fs'); +const path = "./input.txt"; +const input = fs.readFileSync(path); +let output = ""; + +console.log(input); + +if (input.includes("\r\n")) { + output = replaceAll(input, "\r\n", "\\n"); +} else { + output = replaceAll(input, "\n", "\\n"); +} + +output = replaceAll(output, "`", "``"); + +console.log(output); +fs.writeFileSync(path, output); \ No newline at end of file diff --git a/modules/functions.js b/modules/functions.js new file mode 100644 index 0000000..2e0ab4d --- /dev/null +++ b/modules/functions.js @@ -0,0 +1,80 @@ +// dotenv for importing environment variables +const dotenv = require('dotenv'); +const fs = require('fs'); +// Configure Environment Variables +dotenv.config(); + +// Discord.js +const Discord = require('discord.js'); +const { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } = Discord; + +// Various imports from other files +const debugMode = process.env.DEBUG; +const config = require('../data/config.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 + collectionBuilders: { + // Create the collection of slash commands + slashCommands(client) { + if (!client.slashCommands) client.slashCommands = new Discord.Collection(); + client.slashCommands.clear(); + for (const file of slashCommandFiles) { + const slashCommand = require(`../slash-commands/${file}`); + if (slashCommand.data != undefined) { + client.slashCommands.set(slashCommand.data.name, slashCommand); + } + } + if (debugMode) console.log('Slash Commands Collection Built'); + } + }, + builders: { + actionRows: { + example() { + // Create the button to go in the Action Row + const exampleButton = this.buttons.exampleButton(); + // Create the Action Row with the Button in it, to be sent with the Embed + return new ActionRowBuilder() + .addComponents(exampleButton); + }, + buttons: { + exampleButton() { + return new ButtonBuilder() + .setCustomId('id') + .setLabel('Label') + .setStyle(ButtonStyle.Primary); + } + } + }, + embeds: { + help(private) { + const embed = new EmbedBuilder() + .setColor(strings.embeds.color) + .setTitle(strings.help.title) + .setDescription(strings.help.content) + .setFooter({ text: strings.help.footer }); + return { embeds: [embed] }; + }, + error(content) { + const embed = new EmbedBuilder() + .setColor(strings.error.color) + .setTitle(strings.error.title) + .setDescription(content) + .setFooter({ text: strings.embeds.footer }); + return { embeds: [embed], ephemeral: true }; + }, + info(content) { + const embed = new EmbedBuilder() + .setColor(strings.embeds.infoColor) + .setTitle(strings.embeds.infoTitle) + .setDescription(content) + .setFooter({ text: strings.embeds.footer }); + return { embeds: [embed], ephemeral: true }; + } + } + } +}; + +module.exports = functions; \ No newline at end of file diff --git a/modules/input.txt b/modules/input.txt new file mode 100644 index 0000000..e69de29 diff --git a/package.json b/package.json new file mode 100644 index 0000000..1e6381b --- /dev/null +++ b/package.json @@ -0,0 +1,7 @@ +{ + "dependencies": { + "discord.js": "^14.7.1", + "dotenv": "^16.0.3", + "string.prototype.replaceall": "^1.0.7" + } +} diff --git a/slash-commands/commands.js b/slash-commands/commands.js new file mode 100644 index 0000000..cf5d9f1 --- /dev/null +++ b/slash-commands/commands.js @@ -0,0 +1,37 @@ +const { SlashCommandBuilder } = require('discord.js'); +const fn = require('../modules/functions.js'); +const strings = require('../data/strings.json'); + +module.exports = { + data: new SlashCommandBuilder() + .setName("commands") + .setDescription("View all of the bot's commands") + .addBooleanOption(o => + o.setName("private") + .setDescription("Should the reply be visible only to you?") + .setRequired(false) + ), + id: "", // The command ID, used to generate clickable commands + about: "[meta] View all of the bot's commands", // A description of the command to be used with /commands + async execute(interaction) { + let private = interaction.options.getBoolean('private'); + if (private == undefined) { + private = true; + } + // Defer the reply so we have time to do things + await interaction.deferReply({ ephemeral: private }).catch(e => console.error(e)); + try { + // Code here... + let commandsParts = ["These are all of the bot's commands, and some information about them:"]; + interaction.client.slashCommands.forEach(slashCommand => { + commandsParts.push(` - ${slashCommand.about}`); + }); + let commandsString = commandsParts.join("\n"); + await interaction.editReply(fn.builders.embeds.info(commandsString)).catch(e => console.error(e)); + } catch(err) { + // In case of error, log it and let the user know something went wrong + console.error(err); + await interaction.editReply(strings.errors.generalCommand).catch(e => console.error(e)); + } + }, +}; \ No newline at end of file diff --git a/slash-commands/help.js b/slash-commands/help.js new file mode 100644 index 0000000..9324c2e --- /dev/null +++ b/slash-commands/help.js @@ -0,0 +1,30 @@ +const { SlashCommandBuilder } = require('discord.js'); +const fn = require('../modules/functions.js'); +const strings = require('../data/strings.json'); + +module.exports = { + data: new SlashCommandBuilder() + .setName("help") + .setDescription("Get some help using the bot") + .addBooleanOption(o => + o.setName("private") + .setDescription("Should the reply be visible only to you?") + .setRequired(false) + ), + id: "", // The command ID, used to generate clickable commands + about: "Get some help using the bot", // A description of the command to be used with /commands + async execute(interaction) { + let private = interaction.options.getBoolean('private'); + if (private == undefined) { + private = true; + } + await interaction.deferReply({ ephemeral: private }).catch(e => console.error(e)); + try { + await interaction.editReply(fn.builders.embeds.help()).catch(e => console.error(e)); + } catch(err) { + // In case of error, log it and let the user know something went wrong + console.error(err); + await interaction.editReply(strings.errors.generalCommand).catch(e => console.error(e)); + } + }, +}; \ No newline at end of file diff --git a/slash-commands/template b/slash-commands/template new file mode 100644 index 0000000..ceb2de6 --- /dev/null +++ b/slash-commands/template @@ -0,0 +1,31 @@ +const { SlashCommandBuilder } = require('discord.js'); +const fn = require('../modules/functions.js'); +const strings = require('../data/strings.json'); + +module.exports = { + data: new SlashCommandBuilder() + .setName("") + .setDescription("") + .addBooleanOption(o => + o.setName("private") + .setDescription("Should the reply be visible only to you?") + .setRequired(false) + ), + id: "", // The command ID, used to generate clickable commands + about: "", // A description of the command to be used with /commands + async execute(interaction) { + let private = interaction.options.getBoolean('private'); + if (private == undefined) { + private = true; + } + // Defer the reply so we have time to do things + await interaction.deferReply({ ephemeral: private }).catch(e => console.error(e)); + try { + // Code here... + } catch(err) { + // In case of error, log it and let the user know something went wrong + console.error(err); + await interaction.editReply(strings.errors.generalCommand).catch(e => console.error(e)); + } + }, +}; \ No newline at end of file