First good build

This commit is contained in:
Skylar Grant 2023-01-18 20:10:05 -05:00
parent 578a776f59
commit aa7f912f93
18 changed files with 380 additions and 317 deletions

View File

@ -2,9 +2,9 @@ name: Docker Image CI
on: on:
push: push:
branches: [ "main" ] branches: [ "master" ]
pull_request: pull_request:
branches: [ "main" ] branches: [ "master" ]
env: env:
DHUB_UNAME: ${{ secrets.DHUB_UNAME }} DHUB_UNAME: ${{ secrets.DHUB_UNAME }}

105
README.md
View File

@ -1,100 +1,13 @@
# About Nodbot #Grow A Tree Analyzer
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. 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 ##Commands
`docker build --tag=name/nodbot .`
## Push Docker Image * `/setup` - Attempts automatic detection of your server's tree and leaderboard messages.
`docker push name/nodbot` * `/setupinfo` - Displays your server's current configuration.
* `/reset` - Resets your server's configuration, run `/setup` again if needed.
# 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=<your bot's token from step 3>
isDev=<true/false>
dbHost=<mySQL host>
dbPort=<mySQL port (3306)>
dbUser=<mySQL username>
dbPass=<mySQL user password>
dbName=<mySQL table name>
tenorAPIKey=<Tenor API Key>
ownerId=<your Discord user ID>
statusChannelId=<Discord channel ID of channel used for status messages>
clientId=<Discord user ID of your bot>
```

View File

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

View File

@ -2,8 +2,7 @@
const dotenv = require('dotenv'); const dotenv = require('dotenv');
dotenv.config(); dotenv.config();
const { REST } = require('@discordjs/rest'); const { REST, Routes } = require('discord.js');
const { Routes } = require('discord-api-types/v9');
const { guildId } = require('./config.json'); const { guildId } = require('./config.json');
const clientId = process.env.clientId; const clientId = process.env.clientId;
const token = process.env.TOKEN; const token = process.env.TOKEN;
@ -21,7 +20,7 @@ for (const file of commandFiles) {
console.log(commands); console.log(commands);
const rest = new REST({ version: '9' }).setToken(token); const rest = new REST({ version: '10' }).setToken(token);
(async () => { (async () => {
try { try {

View File

@ -1,4 +1,9 @@
{ {
"guildId": "868542949737246730", "guildId": "868542949737246730",
"validCommands": [] "messageId": "",
"treeMessageId": "",
"treeHeight": 0,
"validCommands": [],
"rankMessageId": "",
"rankings": []
} }

BIN
dot-commands/.DS_Store vendored

Binary file not shown.

View File

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

View File

@ -3,19 +3,19 @@
const dotenv = require('dotenv'); const dotenv = require('dotenv');
dotenv.config(); dotenv.config();
const isDev = process.env.isDev; const isDev = process.env.isDev;
const ownerId = process.env.ownerId;
// filesystem // filesystem
const fs = require('fs'); const fs = require('fs');
// Discord.js // Discord.js
const Discord = require('discord.js'); const Discord = require('discord.js');
const { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } = Discord;
// Various imports from other files // Various imports from other files
const config = require('./config.json'); const config = require('./config.json');
let messageIds = require('./messageIds.json');
const strings = require('./strings.json'); const strings = require('./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 dotCommandFiles = fs.readdirSync('./dot-commands/').filter(file => file.endsWith('.js'));
const functions = { const functions = {
// Functions for managing and creating Collections // Functions for managing and creating Collections
@ -31,135 +31,212 @@ const functions = {
} }
} }
if (isDev) console.log('Slash Commands Collection Built'); 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: { builders: {
help(interaction) { refreshAction() {
// Construct the Help Embed // Create the button to go in the Action Row
const helpEmbed = new Discord.MessageEmbed() const refreshButton = new ButtonBuilder()
.setColor('BLUE') .setCustomId('refresh')
.setAuthor('Help Page') .setLabel('Refresh')
.setDescription(strings.help.description) .setStyle(ButtonStyle.Primary);
.setThumbnail(strings.urls.avatar); // Create the Action Row with the Button in it, to be sent with the Embed
const refreshActionRow = new ActionRowBuilder()
.addComponents(
refreshButton
);
return refreshActionRow;
},
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 ``'
}
// Construct the Slash Commands help let trimmedRank = breakdown[0].slice(breakdown[0].indexOf('#') + 1, breakdown[0].lastIndexOf('``'));
let slashCommandsFields = []; let trimmedName = breakdown[1].slice(breakdown[1].indexOf('``') + 2);
trimmedName = trimmedName.slice(0, trimmedName.indexOf('``'));
const slashCommandsMap = interaction.client.slashCommands.map(e => { let trimmedHeight = parseFloat(breakdown[2].slice(0, breakdown[2].indexOf('ft'))).toFixed(1);
return {
name: e.data.name,
description: e.data.description
};
})
for (const e of slashCommandsMap) { rankings.push({
slashCommandsFields.push({ rank: trimmedRank,
name: `- /${e.name}`, name: trimmedName,
value: e.description, height: trimmedHeight
inline: false, });
}); }
}
// Construct the Dot Commands Help messageIds[interaction.guildId].rankings = rankings;
let dotCommandsFields = []; fs.writeFileSync('./messageIds.json', JSON.stringify(messageIds));
messageIds = require('./messageIds.json');
const dotCommandsMap = interaction.client.dotCommands.map(e => { resolve(rankings);
return { });
name: e.name, });
description: e.description, } else {
usage: e.usage reject("The rankMessageId is undefined somehow");
}; return;
}
}); });
for (const e of dotCommandsMap) { },
dotCommandsFields.push({ compare(interaction) {
name: `- .${e.name}`, if (messageIds[interaction.guildId] == undefined) {
value: `${e.description}\nUsage: ${e.usage}`, return `Please reset the reference messages! (${interaction.guildId})`;
inline: false,
});
} }
let treeHeight = parseFloat(messageIds[interaction.guildId].treeHeight).toFixed(1);
helpEmbed.addField('Slash Commands', strings.help.slash); if ((messageIds[interaction.guildId].rankings.length > 0) && (treeHeight > 0)) {
helpEmbed.addFields(slashCommandsFields); let replyString = 'Current Tree Height: ' + treeHeight + 'ft\n\n';
helpEmbed.addField('Dot Commands', strings.help.dot); messageIds[interaction.guildId].rankings.forEach(e => {
helpEmbed.addFields(dotCommandsFields); let difference = parseFloat(e.height).toFixed(1) - treeHeight;
const absDifference = parseFloat(Math.abs(difference)).toFixed(1);
return { embeds: [ if (difference > 0) {
helpEmbed replyString += `${absDifference}ft shorter than rank #${e.rank}\n`;
], ephemeral: true }; } else if (difference < 0) {
}, replyString += `${absDifference}ft taller than rank #${e.rank}\n`;
text(commandData) { } else if (difference == 0) {
return { embeds: [new Discord.MessageEmbed() replyString += `Same height as rank #${e.rank}\n`;
.setAuthor(commandData.command) }
.setDescription(commandData.content) });
.setTimestamp() return 'Here\'s how your tree compares: \n' + replyString;
.setFooter(commandData.author)]}; } 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; module.exports = functions;

31
main.js
View File

@ -28,20 +28,18 @@ const isDev = process.env.isDev;
client.once('ready', () => { client.once('ready', () => {
fn.collections.slashCommands(client); fn.collections.slashCommands(client);
fn.collections.dotCommands(client);
fn.collections.setvalidCommands(client);
console.log('Ready!'); console.log('Ready!');
client.channels.fetch(statusChannelId).then(channel => { 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 // slash-commands
client.on('interactionCreate', async interaction => { client.on('interactionCreate', async interaction => {
if (interaction.isCommand()) { if (interaction.isCommand()) {
if (isDev) { // if (isDev) {
console.log(interaction); // console.log(interaction);
} // }
const { commandName } = interaction; const { commandName } = interaction;
if (client.slashCommands.has(commandName)) { 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); console.error('Slash command attempted to run but not found: ' + commandName);
} }
} }
});
// dot-commands if (interaction.isButton() && interaction.component.customId == 'refresh') {
client.on('messageCreate', message => { fn.refresh(interaction);
// 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.');
}
} }
return;
}); });
client.login(token); client.login(token);

1
messageIds.json Normal file
View File

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

View File

@ -1,33 +1,23 @@
{ {
"name": "nodbot", "name": "treeanalyzer",
"version": "3.0.8", "version": "1.0.0",
"description": "Nods and Nod Accessories.", "description": "Analyze Grow A Tree",
"main": "main.js", "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": { "scripts": {
"test": "echo \"Error: no test specified\" && exit 1" "test": "echo \"Error: no test specified\" && exit 1"
}, },
"repository": { "repository": {
"type": "git", "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", "license": "ISC",
"bugs": { "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"
}
} }

12
slash-commands/compare.js Normal file
View File

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

49
slash-commands/help.js Normal file
View File

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

12
slash-commands/reset.js Normal file
View File

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

47
slash-commands/setup.js Normal file
View File

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

View File

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

View File

@ -1,4 +1,4 @@
const { SlashCommandBuilder } = require('@discordjs/builders'); const { SlashCommandBuilder } = require('discord.js');
const fn = require('../functions.js'); const fn = require('../functions.js');
module.exports = { module.exports = {

View File

@ -1,8 +1,12 @@
{ {
"help": { "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.", "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.",
"slash": "Slash Commands always begin with a / and a menu will pop up to help complete the commands.", "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.",
"dot": "Dot Commands have the command at the end of the message, for example to search for a gif of 'nod', type 'nod.gif'" "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": { "emoji": {
"joint": "<:joint:862082955902976000>", "joint": "<:joint:862082955902976000>",