diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index 76eb4f4..7a7f050 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -2,9 +2,9 @@ name: Docker Image CI on: push: - branches: [ "main" ] + branches: [ "master" ] pull_request: - branches: [ "main" ] + branches: [ "master" ] env: DHUB_UNAME: ${{ secrets.DHUB_UNAME }} diff --git a/README.md b/README.md index 34bc147..6bd4303 100644 --- a/README.md +++ b/README.md @@ -1,100 +1,13 @@ -# About Nodbot -Nodbot is a content saving and serving Discord bot. Nodbot is able to search Tenor for GIFs, save custom copypastas, and look up marijuana strain information. Nodbot is in semi-active development by voidf1sh. It's buggy as hell and very shoddily built. Don't use it. +#Grow A Tree Analyzer +This bot works with Grow A Tree Bot by Limbo Labs. This project is not affiliated with Limbo Labs in any way. -# Nodbot Help +This bot allows easy comparison between a server's tree and other trees displayed on the leaderboard. -Use the `/help` command to see the bot's help message. +##Usage +Add the bot to your server and make sure it has the proper permissions (`Send Messages` and `Send Messages in Threads` if applicable), then run `/setup` in the channel(s) that contain your server's tree and leaderboard messages. -## Create Docker Image -`docker build --tag=name/nodbot .` +##Commands -## Push Docker Image -`docker push name/nodbot` - -# Immediate To-Do - -1. ~~Sanitize inputs for SQL queries.~~ Done. -2. ~~Move environment variables so they don't get included in the image.~~ -3. Implement error handling on all actions. -4. Ephemeral responses to some/most slash commands. -5. Comment the code! Document! -6. Check for and create database tables if necessary. Handle errors. - -# Deploy NodBot Yourself - -1. Create an application at the [Discord Developer Portal](https://discord.com/developers/applications) -2. Convert the application into a Bot -3. Note down the token provided and keep this safe. You cannot view this token again, only regenerate a new one. -4. Create a Tenor account and obtain an API key. -5. Install and configure MySQL or MariaDB with a user for the bot and a datbase -* Create the table structure as outlined below (* Nodbot will soon create its own table structure) -6. Configure your environment variables as outlined below. -7. Fire it up with `node main.js` - -## Table Structure - -``` -Table: gifs -+-----------+---------------+------+-----+---------+----------------+ -| Field | Type | Null | Key | Default | Extra | -+-----------+---------------+------+-----+---------+----------------+ -| id | int(11) | NO | MUL | NULL | auto_increment | -| name | varchar(100) | NO | | NULL | | -| embed_url | varchar(1000) | NO | | NULL | | -+-----------+---------------+------+-----+---------+----------------+ - -Table: joints -+---------+---------------+------+-----+---------+----------------+ -| Field | Type | Null | Key | Default | Extra | -+---------+---------------+------+-----+---------+----------------+ -| id | int(11) | NO | MUL | NULL | auto_increment | -| content | varchar(1000) | NO | | NULL | | -+---------+---------------+------+-----+---------+----------------+ - -Table: pastas -+---------+---------------+------+-----+---------+----------------+ -| Field | Type | Null | Key | Default | Extra | -+---------+---------------+------+-----+---------+----------------+ -| id | int(11) | NO | MUL | NULL | auto_increment | -| name | varchar(100) | NO | | NULL | | -| content | varchar(1900) | NO | | NULL | | -| iconurl | varchar(200) | NO | | (url) | | -+---------+---------------+------+-----+---------+----------------+ - -Table: requests -+---------+---------------+------+-----+---------+----------------+ -| Field | Type | Null | Key | Default | Extra | -+---------+---------------+------+-----+---------+----------------+ -| id | int(11) | NO | MUL | NULL | auto_increment | -| author | varchar(100) | NO | | NULL | | -| request | varchar(1000) | NO | | NULL | | -| status | varchar(10) | YES | | Active | | -+---------+---------------+------+-----+---------+----------------+ - -Table: strains -+---------+-------------+------+-----+---------+-------+ -| Field | Type | Null | Key | Default | Extra | -+---------+-------------+------+-----+---------+-------+ -| id | smallint(6) | NO | | NULL | | -| name | varchar(60) | YES | | NULL | | -| type | varchar(10) | YES | | NULL | | -| effects | varchar(80) | YES | | NULL | | -| ailment | varchar(70) | YES | | NULL | | -| flavor | varchar(30) | YES | | NULL | | -+---------+-------------+------+-----+---------+-------+ -``` - -## Environment Variables -``` -TOKEN= -isDev= -dbHost= -dbPort= -dbUser= -dbPass= -dbName= -tenorAPIKey= -ownerId= -statusChannelId= -clientId= -``` \ No newline at end of file +* `/setup` - Attempts automatic detection of your server's tree and leaderboard messages. +* `/setupinfo` - Displays your server's current configuration. +* `/reset` - Resets your server's configuration, run `/setup` again if needed. diff --git a/Roadmap.md b/Roadmap.md deleted file mode 100644 index 80bce7b..0000000 --- a/Roadmap.md +++ /dev/null @@ -1,14 +0,0 @@ -# v3.1.0 - -* Name checking for saving content -* .jpg, .wav -* Audio/Video attachments for saved content. -* Pass The Joint -* Voting system for Super Adventure Club - -# v4.0.0 -* Scalability: modify the code to allow the bot to be used in multiple servers - * including saved content, saved commands, preferences, etc. - -# v3.?.? -= Joke generator for Hallihan \ No newline at end of file diff --git a/_deploy-commands.js b/_deploy-commands.js index 5ba21ce..4621f7b 100644 --- a/_deploy-commands.js +++ b/_deploy-commands.js @@ -2,8 +2,7 @@ const dotenv = require('dotenv'); dotenv.config(); -const { REST } = require('@discordjs/rest'); -const { Routes } = require('discord-api-types/v9'); +const { REST, Routes } = require('discord.js'); const { guildId } = require('./config.json'); const clientId = process.env.clientId; const token = process.env.TOKEN; @@ -21,7 +20,7 @@ for (const file of commandFiles) { console.log(commands); -const rest = new REST({ version: '9' }).setToken(token); +const rest = new REST({ version: '10' }).setToken(token); (async () => { try { diff --git a/config.json b/config.json index b9087cf..479d43f 100644 --- a/config.json +++ b/config.json @@ -1,4 +1,9 @@ { "guildId": "868542949737246730", - "validCommands": [] + "messageId": "", + "treeMessageId": "", + "treeHeight": 0, + "validCommands": [], + "rankMessageId": "", + "rankings": [] } \ No newline at end of file diff --git a/dot-commands/.DS_Store b/dot-commands/.DS_Store deleted file mode 100644 index 5008ddf..0000000 Binary files a/dot-commands/.DS_Store and /dev/null differ diff --git a/dot-commands/setmessage.js b/dot-commands/setmessage.js deleted file mode 100644 index 25a074e..0000000 --- a/dot-commands/setmessage.js +++ /dev/null @@ -1,25 +0,0 @@ -const fn = require('../functions.js'); -const config = require('../config.json'); - -module.exports = { - name: 'setmessage', - alias: 'sm', - description: 'Set the message to be used for rank analysis', - usage: 'Reply to the Tree Ranking message with .setmessage', - execute(message, commandData) { - if (message.reference != undefined) { - const repliedMessageId = message.reference.messageId; - message.channel.messages.fetch(repliedMessageId) - .then(repliedMessage => { - console.log(repliedMessage.embeds[0].data.description); - message.reply('ID: ' + repliedMessage.id); - }) - .catch(err => { - console.error(err); - message.reply('There was a problem fetching the message.'); - }); - } else { - message.reply('You must reply to the message'); - } - } -} \ No newline at end of file diff --git a/functions.js b/functions.js index 92cb3e6..ef537b2 100644 --- a/functions.js +++ b/functions.js @@ -3,19 +3,19 @@ const dotenv = require('dotenv'); dotenv.config(); const isDev = process.env.isDev; -const ownerId = process.env.ownerId; // filesystem const fs = require('fs'); // Discord.js const Discord = require('discord.js'); +const { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } = Discord; // Various imports from other files const config = require('./config.json'); +let messageIds = require('./messageIds.json'); const strings = require('./strings.json'); const slashCommandFiles = fs.readdirSync('./slash-commands/').filter(file => file.endsWith('.js')); -const dotCommandFiles = fs.readdirSync('./dot-commands/').filter(file => file.endsWith('.js')); const functions = { // Functions for managing and creating Collections @@ -31,135 +31,212 @@ const functions = { } } if (isDev) console.log('Slash Commands Collection Built'); - }, - setvalidCommands(client) { - for (const entry of client.dotCommands.map(command => command)) { - config.validCommands.push(entry.name); - if (Array.isArray(entry.alias)) { - entry.alias.forEach(element => { - config.validCommands.push(element); - }); - } else if (entry.alias != undefined) { - config.validCommands.push(entry.alias); - } - } - if (isDev) console.log(`Valid Commands Added to Config\n${config.validCommands}`); - }, - dotCommands(client) { - if (!client.dotCommands) client.dotCommands = new Discord.Collection(); - client.dotCommands.clear(); - for (const file of dotCommandFiles) { - const dotCommand = require(`./dot-commands/${file}`); - client.dotCommands.set(dotCommand.name, dotCommand); - if (Array.isArray(dotCommand.alias)) { - dotCommand.alias.forEach(element => { - client.dotCommands.set(element, dotCommand); - }); - } else if (dotCommand.alias != undefined) { - client.dotCommands.set(dotCommand.alias, dotCommand); - } - } - if (isDev) console.log('Dot Commands Collection Built'); - }, - }, - dot: { - getCommandData(message) { - const commandData = {}; - // Split the message content at the final instance of a period - const finalPeriod = message.content.lastIndexOf('.'); - if(isDev) console.log(message.content); - // If the final period is the last character, or doesn't exist - if (finalPeriod < 0) { - if (isDev) console.log(finalPeriod); - commandData.isCommand = false; - return commandData; - } - commandData.isCommand = true; - // Get the first part of the message, everything leading up to the final period - commandData.args = message.content.slice(0,finalPeriod).toLowerCase(); - // Get the last part of the message, everything after the final period - commandData.command = message.content.slice(finalPeriod).replace('.','').toLowerCase(); - commandData.author = `${message.author.username}#${message.author.discriminator}`; - return this.checkCommand(commandData); - }, - checkCommand(commandData) { - if (commandData.isCommand) { - const validCommands = require('./config.json').validCommands; - commandData.isValid = validCommands.includes(commandData.command); - // Add exceptions for messages that contain only a link - if (commandData.args.startsWith('http')) commandData.isValid = false; - } - else { - commandData.isValid = false; - console.error('Somehow a non-command made it to checkCommands()'); - } - return commandData; } }, - embeds: { - help(interaction) { - // Construct the Help Embed - const helpEmbed = new Discord.MessageEmbed() - .setColor('BLUE') - .setAuthor('Help Page') - .setDescription(strings.help.description) - .setThumbnail(strings.urls.avatar); - - // Construct the Slash Commands help - - let slashCommandsFields = []; - - const slashCommandsMap = interaction.client.slashCommands.map(e => { - return { - name: e.data.name, - description: e.data.description - }; - }) - - for (const e of slashCommandsMap) { - slashCommandsFields.push({ - name: `- /${e.name}`, - value: e.description, - inline: false, - }); - } - - // Construct the Dot Commands Help - let dotCommandsFields = []; - - const dotCommandsMap = interaction.client.dotCommands.map(e => { - return { - name: e.name, - description: e.description, - usage: e.usage - }; - }); - - for (const e of dotCommandsMap) { - dotCommandsFields.push({ - name: `- .${e.name}`, - value: `${e.description}\nUsage: ${e.usage}`, - inline: false, - }); - } - - helpEmbed.addField('Slash Commands', strings.help.slash); - helpEmbed.addFields(slashCommandsFields); - helpEmbed.addField('Dot Commands', strings.help.dot); - helpEmbed.addFields(dotCommandsFields); - - return { embeds: [ - helpEmbed - ], ephemeral: true }; + 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; }, - text(commandData) { - return { embeds: [new Discord.MessageEmbed() - .setAuthor(commandData.command) - .setDescription(commandData.content) - .setTimestamp() - .setFooter(commandData.author)]}; + comparisonEmbed(content, refreshActionRow) { + // Create the embed using the content passed to this function + const embed = new EmbedBuilder() + .setColor(strings.embeds.color) + .setTitle('Tree Growth Comparison') + .setDescription(content) + .setFooter({text: strings.embeds.footer}); + const messageContents = { embeds: [embed], components: [refreshActionRow] }; + return messageContents; }, + 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; + } }, + rankings: { + parse(interaction) { + return new Promise ((resolve, reject) => { + if (messageIds[interaction.guildId] == undefined) { + reject("The guild entry hasn't been created yet."); + return; + } + if (messageIds[interaction.guildId].rankMessageId != undefined) { + interaction.guild.channels.fetch(messageIds[interaction.guildId].rankChannelId).then(c => { + c.messages.fetch(messageIds[interaction.guildId].rankMessageId).then(rankMessage => { + if ((rankMessage.embeds.length == 0) || (rankMessage.embeds[0].data.title != 'Tallest Trees' )) { + reject("This doesn't appear to be a valid ``/top trees`` message."); + return; + } + let lines = rankMessage.embeds[0].data.description.split('\n'); + let rankings = []; + for (let i = 0; i < 10; i++) { + let breakdown = lines[i].split(' - '); + if (breakdown[0].includes('🥇')) { + breakdown[0] = '``#1 ``' + } else if (breakdown[0].includes('🥈')) { + breakdown[0] = '``#2 ``' + } else if (breakdown[0].includes('🥉')) { + breakdown[0] = '``#3 ``' + } + + let trimmedRank = breakdown[0].slice(breakdown[0].indexOf('#') + 1, breakdown[0].lastIndexOf('``')); + + let trimmedName = breakdown[1].slice(breakdown[1].indexOf('``') + 2); + trimmedName = trimmedName.slice(0, trimmedName.indexOf('``')); + + let trimmedHeight = parseFloat(breakdown[2].slice(0, breakdown[2].indexOf('ft'))).toFixed(1); + + rankings.push({ + rank: trimmedRank, + name: trimmedName, + height: trimmedHeight + }); + } + + messageIds[interaction.guildId].rankings = rankings; + fs.writeFileSync('./messageIds.json', JSON.stringify(messageIds)); + messageIds = require('./messageIds.json'); + resolve(rankings); + }); + }); + } else { + reject("The rankMessageId is undefined somehow"); + return; + } + }); + + }, + compare(interaction) { + if (messageIds[interaction.guildId] == undefined) { + return `Please reset the reference messages! (${interaction.guildId})`; + } + let treeHeight = parseFloat(messageIds[interaction.guildId].treeHeight).toFixed(1); + if ((messageIds[interaction.guildId].rankings.length > 0) && (treeHeight > 0)) { + let replyString = 'Current Tree Height: ' + treeHeight + 'ft\n\n'; + messageIds[interaction.guildId].rankings.forEach(e => { + let difference = parseFloat(e.height).toFixed(1) - treeHeight; + const absDifference = parseFloat(Math.abs(difference)).toFixed(1); + if (difference > 0) { + replyString += `${absDifference}ft shorter than rank #${e.rank}\n`; + } else if (difference < 0) { + replyString += `${absDifference}ft taller than rank #${e.rank}\n`; + } else if (difference == 0) { + replyString += `Same height as rank #${e.rank}\n`; + } + }); + return 'Here\'s how your tree compares: \n' + replyString; + } else { + console.error('Not configured correctly\n' + 'Guild ID: ' + interaction.guildId + '\nGuild Info: ' + JSON.stringify(messageIds[interaction.guildId])); + return 'Not configured correctly'; + } + } + }, + tree: { + parse(interaction) { + let input; + return new Promise((resolve, reject) => { + if (messageIds[interaction.guildId] == undefined) { + reject(`The guild entry hasn't been created yet. [${interaction.guildId || interaction.commandGuildId}]`); + return; + } + if (messageIds[interaction.guildId].treeMessageId != "") { + interaction.guild.channels.fetch(messageIds[interaction.guildId].treeChannelId).then(c => { + c.messages.fetch(messageIds[interaction.guildId].treeMessageId).then(m => { + if ( (m.embeds.length == 0) || !(m.embeds[0].data.description.includes('Your tree is')) ) { + reject("This doesn't appear to be a valid ``/tree`` message."); + return; + } + input = m.embeds[0].data.description; + let lines = input.split('\n'); + messageIds[interaction.guildId].treeHeight = parseFloat(lines[0].slice(lines[0].indexOf('is') + 3, lines[0].indexOf('ft'))).toFixed(1); + fs.writeFileSync('./messageIds.json', JSON.stringify(messageIds)); + messageIds = require('./messageIds.json'); + resolve("The reference tree message has been saved/updated."); + }); + }) + } else { + console.error('treeMessageId undefined'); + reject("There was an unknown error while setting the tree message."); + return; + } + }); + } + }, + 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)); + }); + }, + reset(guildId) { + delete messageIds[guildId]; + fs.writeFileSync('./messageIds.json', JSON.stringify(messageIds)); + messageIds = require('./messageIds.json'); + return; + }, + getInfo(guildId) { + const guildInfo = messageIds[guildId]; + if (guildInfo != undefined) { + let guildInfoString = ""; + if (guildInfo.treeMessageId != "") { + guildInfoString += `Tree Message ID: ${guildInfo.treeMessageId}\n`; + } + if (guildInfo.treeChannelId != "") { + guildInfoString += `Tree Channel ID: ${guildInfo.treeChannelId}\n`; + } + if (guildInfo.rankMessageId != "") { + guildInfoString += `Rank Message ID: ${guildInfo.rankMessageId}\n`; + } + if (guildInfo.rankChannelId != "") { + guildInfoString += `Rank Channel ID: ${guildInfo.rankChannelId}\n`; + } + if (guildInfo.treeHeight != "") { + guildInfoString += `Tree Height: ${guildInfo.treeHeight}\n`; + } + return `Here if your servers setup info:\n${guildInfoString}`; + } else { + return "Your guild hasn't been set up yet."; + } + } }; module.exports = functions; \ No newline at end of file diff --git a/main.js b/main.js index 8feacaf..13a41b1 100644 --- a/main.js +++ b/main.js @@ -28,20 +28,18 @@ const isDev = process.env.isDev; client.once('ready', () => { fn.collections.slashCommands(client); - fn.collections.dotCommands(client); - fn.collections.setvalidCommands(client); console.log('Ready!'); client.channels.fetch(statusChannelId).then(channel => { - channel.send(`${new Date().toISOString()} -- <@${process.env.ownerId}>\nStartup Sequence Complete`); + channel.send(`${new Date().toISOString()} -- \nStartup Sequence Complete`); }); }); // slash-commands client.on('interactionCreate', async interaction => { if (interaction.isCommand()) { - if (isDev) { - console.log(interaction); - } + // if (isDev) { + // console.log(interaction); + // } const { commandName } = interaction; if (client.slashCommands.has(commandName)) { @@ -51,27 +49,10 @@ client.on('interactionCreate', async interaction => { console.error('Slash command attempted to run but not found: ' + commandName); } } -}); -// dot-commands -client.on('messageCreate', message => { - // Some basic checking to prevent running unnecessary code - if (message.author.bot) return; - - // Break the message down into its components and analyze it - const commandData = fn.dot.getCommandData(message); - console.log(commandData); - - if (commandData.isValid && commandData.isCommand) { - try { - client.dotCommands.get(commandData.command).execute(message, commandData); - } - catch (error) { - console.error(error); - message.reply('There was an error trying to execute that command.'); - } + if (interaction.isButton() && interaction.component.customId == 'refresh') { + fn.refresh(interaction); } - return; }); client.login(token); \ No newline at end of file diff --git a/messageIds.json b/messageIds.json new file mode 100644 index 0000000..e26f355 --- /dev/null +++ b/messageIds.json @@ -0,0 +1 @@ +{"269250657809072139":{"treeMessageId":"1042987280954032158","treeChannelId":"1042987243670872154","rankMessageId":"1065390277767987210","rankChannelId":"1042987923928256553","rankings":[{"rank":"111","name":"Taboo Tree","height":"3030.0"},{"rank":"112","name":"booster tree","height":"3029.0"},{"rank":"113","name":"Pod Birch","height":"3017.0"},{"rank":"114","name":"Wise Mystical Tree","height":"3013.0"},{"rank":"115","name":"Shimmer Doge DAO Tree","height":"2978.9"},{"rank":"116","name":"Simon","height":"2969.1"},{"rank":"117","name":"Cloud Tree","height":"2959.6"},{"rank":"118","name":"The Monke Tree","height":"2947.3"},{"rank":"119","name":"Woody","height":"2925.6"},{"rank":"120","name":"Azure Woodku","height":"2908.0"}],"treeHeight":"179.1"},"803049831839956992":{"treeMessageId":"1052343193980645376","treeChannelId":"1022324601864331335","rankMessageId":"1065121016604524544","rankChannelId":"1022324665374474341","rankings":[{"rank":"1 ","name":"WLR's Ultimate Sudowoodo","height":14203.5},{"rank":"2 ","name":"Our Bonfire Tree","height":13572.9},{"rank":"3 ","name":"The Gremlin Tree","height":12968},{"rank":"4 ","name":"World Tree","height":10600.6},{"rank":"5 ","name":"Emory's Baby","height":10163.9},{"rank":"6 ","name":"Charles the Tree","height":9920},{"rank":"7 ","name":"SMMO-Babel","height":9397},{"rank":"8 ","name":"Spaghetti","height":9341},{"rank":"9 ","name":"the fortnite tree","height":8883},{"rank":"10","name":"TreeVana","height":8002.6}],"treeHeight":261},"1006579138909458513":{"treeMessageId":"1065399196682821713","treeChannelId":"1065398553528250369","rankMessageId":"1065399316979662928","rankChannelId":"1065398553528250369","rankings":[{"rank":"1 ","name":"WLR's Ultimate Sudowoodo","height":"14232.9"},{"rank":"2 ","name":"Our Bonfire Tree","height":"13612.1"},{"rank":"3 ","name":"The Gremlin Tree","height":"12994.8"},{"rank":"4 ","name":"World Tree","height":"10623.0"},{"rank":"5 ","name":"Emory's Baby","height":"10187.8"},{"rank":"6 ","name":"Charles the Tree","height":"9951.0"},{"rank":"7 ","name":"SMMO-Babel","height":"9438.0"},{"rank":"8 ","name":"Spaghetti","height":"9365.0"},{"rank":"9 ","name":"the fortnite tree","height":"8895.0"},{"rank":"10","name":"TreeVana","height":"8019.2"}],"treeHeight":"7941.9"},"760701839427108874":{"treeMessageId":"1065405516400042114","treeChannelId":"1050272057839067178","rankMessageId":"1051889574365904907","rankChannelId":"1050272057839067178","rankings":[{"rank":"251","name":"Bom","height":"1621.0"},{"rank":"252","name":"Tree Of Happiness","height":"1621.0"},{"rank":"253","name":"Treehouse","height":"1604.0"},{"rank":"254","name":"Viktoria II","height":"1595.0"},{"rank":"255","name":"tree of virginity","height":"1588.0"},{"rank":"256","name":"nick's pride","height":"1588.0"},{"rank":"257","name":"Vitraya Ramunong","height":"1583.0"},{"rank":"258","name":"oven tree","height":"1538.7"},{"rank":"259","name":"Tr3e","height":"1527.1"},{"rank":"260","name":"Dicecraft","height":"1523.0"}],"treeHeight":"1527.1"},"868542949737246730":{"treeMessageId":"1065417587502092328","treeChannelId":"1065417555445043300","rankMessageId":"1065417630581801100","rankChannelId":"1065417587502092328","rankings":[{"rank":"1 ","name":"WLR's Ultimate Sudowoodo","height":"14233.4"},{"rank":"2 ","name":"Our Bonfire Tree","height":"13612.7"},{"rank":"3 ","name":"The Gremlin Tree","height":"12995.3"},{"rank":"4 ","name":"World Tree","height":"10623.8"},{"rank":"5 ","name":"Emory's Baby","height":"10188.1"},{"rank":"6 ","name":"Charles the Tree","height":"9951.0"},{"rank":"7 ","name":"SMMO-Babel","height":"9438.4"},{"rank":"8 ","name":"Spaghetti","height":"9365.5"},{"rank":"9 ","name":"the fortnite tree","height":"8895.0"},{"rank":"10","name":"TreeVana","height":"8020.0"}],"treeHeight":"1.0"}} \ No newline at end of file diff --git a/package.json b/package.json index c3b8d21..03adfd0 100644 --- a/package.json +++ b/package.json @@ -1,33 +1,23 @@ { - "name": "nodbot", - "version": "3.0.8", - "description": "Nods and Nod Accessories.", + "name": "treeanalyzer", + "version": "1.0.0", + "description": "Analyze Grow A Tree", "main": "main.js", - "dependencies": { - "axios": "^0.21.4", - "discord.js": "^14.7.1", - "dotenv": "^10.0.0", - "fuzzy-search": "^3.2.1", - "mysql": "^2.18.1", - "tenorjs": "^1.0.10" - }, - "engines": { - "node": "18.x" - }, - "devDependencies": { - "eslint": "^7.32.0" - }, "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "repository": { "type": "git", - "url": "git+https://github.com/voidf1sh/nodbot.git" + "url": "git+https://github.com/voidf1sh/treeanalyzer.git" }, - "author": "voidf1sh#0420", + "author": "Skylar Grant", "license": "ISC", "bugs": { - "url": "https://github.com/voidf1sh/nodbot/issues" + "url": "https://github.com/voidf1sh/treeanalyzer/issues" }, - "homepage": "https://github.com/voidf1sh/nodbot#readme" + "homepage": "https://github.com/voidf1sh/treeanalyzer#readme", + "dependencies": { + "discord.js": "^14.7.1", + "dotenv": "^16.0.3" + } } diff --git a/slash-commands/compare.js b/slash-commands/compare.js new file mode 100644 index 0000000..b85c7fb --- /dev/null +++ b/slash-commands/compare.js @@ -0,0 +1,12 @@ +const { SlashCommandBuilder } = require('discord.js'); +const fn = require('../functions.js'); + +module.exports = { + data: new SlashCommandBuilder() + .setName('compare') + .setDescription('See how your tree compares to other trees!'), + async execute(interaction) { + const embed = fn.builders.comparisonEmbed(fn.rankings.compare(interaction), fn.builders.refreshAction()); + interaction.reply(embed); + }, +}; \ No newline at end of file diff --git a/slash-commands/help.js b/slash-commands/help.js new file mode 100644 index 0000000..4f4388f --- /dev/null +++ b/slash-commands/help.js @@ -0,0 +1,49 @@ +const { SlashCommandBuilder, messageLink } = require('discord.js'); +const fn = require('../functions.js'); +const strings = require('../strings.json'); + +module.exports = { + data: new SlashCommandBuilder() + .setName('help') + .setDescription('Get help using the bot') + .addSubcommand(subcommand => + subcommand + .setName('info') + .setDescription('Learn about the bot') + .addStringOption(o => + o.setName('private') + .setDescription('Should the response be sent privately?') + .setRequired(true) + .addChoices( + { name: "True", value: "true" }, + { name: "False", value: "false" } + ))) + .addSubcommand(subcommand => + subcommand + .setName('setup') + .setDescription('Learn how to setup the bot') + .addStringOption(o => + o.setName('private') + .setDescription('Should the response be sent privately?') + .setRequired(true) + .addChoices( + { name: "True", value: "true" }, + { name: "False", value: "false" } + ))) + .addSubcommand(subcommand => + subcommand + .setName('permissions') + .setDescription('Learn about the bot\'s permissions') + .addStringOption(o => + o.setName('private') + .setDescription('Should the response be sent privately?') + .setRequired(true) + .addChoices( + { name: "True", value: "true" }, + { name: "False", value: "false" } + ))), + execute(interaction) { + const helpEmbed = fn.builders.helpEmbed(strings.help[interaction.options.getSubcommand()], interaction.options.getString('private')); + interaction.reply(helpEmbed); + }, +}; \ No newline at end of file diff --git a/slash-commands/reset.js b/slash-commands/reset.js new file mode 100644 index 0000000..569e36c --- /dev/null +++ b/slash-commands/reset.js @@ -0,0 +1,12 @@ +const { SlashCommandBuilder } = require('discord.js'); +const fn = require('../functions.js'); + +module.exports = { + data: new SlashCommandBuilder() + .setName('reset') + .setDescription('Reset all message assignments in your server'), + execute(interaction) { + fn.reset(interaction.guildId); + interaction.reply(fn.builders.embed("Assignments Reset")); + }, +}; \ No newline at end of file diff --git a/slash-commands/setup.js b/slash-commands/setup.js new file mode 100644 index 0000000..8281e26 --- /dev/null +++ b/slash-commands/setup.js @@ -0,0 +1,47 @@ +const { SlashCommandBuilder } = require('discord.js'); +const fn = require('../functions.js'); +const messageIds = require('../messageIds.json'); + +module.exports = { + data: new SlashCommandBuilder() + .setName('setup') + .setDescription('Attempt automatic configuration of the bot.'), + execute(interaction) { + if (messageIds[interaction.guildId] == undefined) { + messageIds[interaction.guildId] = { + "treeMessageId": "", + "treeChannelId": "", + "rankMessageId": "", + "rankChannelId": "", + "rankings": [], + "treeHeight": 0 + }; + } + interaction.channel.messages.fetch({ limit: 20 }).then(msgs => { + let treeFound = false; + let rankFound = false; + msgs.forEach(msg => { + if (msg.embeds.length > 0) { + if (msg.embeds[0].data.description.includes("Your tree is")) { + treeFound = true; + messageIds[interaction.guildId].treeChannelId = msg.channelId; + messageIds[interaction.guildId].treeMessageId = msg.id; + fn.tree.parse(msg); + } else if (msg.embeds[0].data.title == "Tallest Trees") { + rankFound = true; + messageIds[interaction.guildId].rankChannelId = msg.channelId; + messageIds[interaction.guildId].rankMessageId = msg.id; + fn.rankings.parse(msg); + } + } + }); + if (treeFound && !(rankFound)) { + interaction.reply(fn.builders.embed("A tree message was found, but a leaderboard message was not. Please run this command again in the channel containing the leaderboard if you haven't done so already. Run ``/setupinfo`` to see if the message is set.")); + } else if (!(treeFound) && rankFound) { + interaction.reply(fn.builders.embed("A leaderboard message was found, but a tree message was not. Please run this command again in the channel containing the tree if you haven't done so already. Run ``/setupinfo`` to see if the message is set.")); + } else if (treeFound && rankFound) { + interaction.reply(fn.builders.embed("Tree and leaderboard messages were both found, setup is complete. Run ``/setupinfo`` to verify.")); + } + }); + }, +}; \ No newline at end of file diff --git a/slash-commands/setupinfo.js b/slash-commands/setupinfo.js new file mode 100644 index 0000000..8eef847 --- /dev/null +++ b/slash-commands/setupinfo.js @@ -0,0 +1,12 @@ +const { SlashCommandBuilder } = require('discord.js'); +const fn = require('../functions.js'); + +module.exports = { + data: new SlashCommandBuilder() + .setName('setupinfo') + .setDescription('View information about how the bot is set up in your server'), + execute(interaction) { + const embed = fn.builders.embed(fn.getInfo(interaction.guildId)); + interaction.reply(embed); + }, +}; \ No newline at end of file diff --git a/slash-commands/template b/slash-commands/template index c214fae..383beac 100644 --- a/slash-commands/template +++ b/slash-commands/template @@ -1,4 +1,4 @@ -const { SlashCommandBuilder } = require('@discordjs/builders'); +const { SlashCommandBuilder } = require('discord.js'); const fn = require('../functions.js'); module.exports = { diff --git a/strings.json b/strings.json index ccc342e..fa6f6db 100644 --- a/strings.json +++ b/strings.json @@ -1,8 +1,12 @@ { "help": { - "description": "Hi there! Thanks for checking out NodBot. NodBot is used in two distinct ways: with 'Slash Commands' (/help), and with 'Dot Commands' (nod.gif). The two types will be outlined below, along with usage examples.", - "slash": "Slash Commands always begin with a / and a menu will pop up to help complete the commands.", - "dot": "Dot Commands have the command at the end of the message, for example to search for a gif of 'nod', type 'nod.gif'" + "info": "This bot will analyze your tree (from the Grow A Tree Bot by Limbo Labs) and compare its growth to other trees displayed on the leaderboard.\n\nThis bot assumes that there is a single `/tree` message and a single `/top trees` message that everyone uses. If everyone creates their own `/tree` and `/top trees`, the reference messages will have to be updated every time, or old data will be displayed.\n\nGrow A Tree Analyzer is not affiliated with Grow A Tree Bot or Limbo Labs in any way.", + "setup": "To begin analyzing your Tree, first you must set up the reference messages.\n\n1. Reply to your server's `/tree` message with `.settree`.\n • Analyzer will DM you to confirm that the `/tree` message has been set correctly.\n2. Now reply to your server's `/top trees` message with `.setranks`.\n • Analyzer will send you a DM to confirm that the `/top trees` message has been set correctly.\n3. Now simply run `/compare` where you want your analysis to be visible.", + "permissions": "At a minimum, Grow A Tree Analyzer requires permissions to `Send Messages` and `Send Messages in Threads` if applicable. If Analyzer is given permission to `Manage Messages`, the bot will delete the `.settree` and `.setranks` messages to reduce spam." + }, + "embeds": { + "footer": "Grow A Tree Analyzer is not affiliated with Grow A Tree or Limbo Labs", + "color": "0x55FF55" }, "emoji": { "joint": "<:joint:862082955902976000>",