First good build
This commit is contained in:
parent
578a776f59
commit
aa7f912f93
4
.github/workflows/docker-image.yml
vendored
4
.github/workflows/docker-image.yml
vendored
@ -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
105
README.md
@ -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>
|
|
||||||
```
|
|
||||||
|
14
Roadmap.md
14
Roadmap.md
@ -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
|
|
@ -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 {
|
||||||
|
@ -1,4 +1,9 @@
|
|||||||
{
|
{
|
||||||
"guildId": "868542949737246730",
|
"guildId": "868542949737246730",
|
||||||
"validCommands": []
|
"messageId": "",
|
||||||
|
"treeMessageId": "",
|
||||||
|
"treeHeight": 0,
|
||||||
|
"validCommands": [],
|
||||||
|
"rankMessageId": "",
|
||||||
|
"rankings": []
|
||||||
}
|
}
|
BIN
dot-commands/.DS_Store
vendored
BIN
dot-commands/.DS_Store
vendored
Binary file not shown.
@ -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');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
329
functions.js
329
functions.js
@ -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()
|
||||||
// Construct the Slash Commands help
|
.addComponents(
|
||||||
|
refreshButton
|
||||||
let slashCommandsFields = [];
|
);
|
||||||
|
return refreshActionRow;
|
||||||
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 };
|
|
||||||
},
|
},
|
||||||
text(commandData) {
|
comparisonEmbed(content, refreshActionRow) {
|
||||||
return { embeds: [new Discord.MessageEmbed()
|
// Create the embed using the content passed to this function
|
||||||
.setAuthor(commandData.command)
|
const embed = new EmbedBuilder()
|
||||||
.setDescription(commandData.content)
|
.setColor(strings.embeds.color)
|
||||||
.setTimestamp()
|
.setTitle('Tree Growth Comparison')
|
||||||
.setFooter(commandData.author)]};
|
.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;
|
module.exports = functions;
|
31
main.js
31
main.js
@ -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
1
messageIds.json
Normal 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"}}
|
32
package.json
32
package.json
@ -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
12
slash-commands/compare.js
Normal 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
49
slash-commands/help.js
Normal 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
12
slash-commands/reset.js
Normal 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
47
slash-commands/setup.js
Normal 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."));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
12
slash-commands/setupinfo.js
Normal file
12
slash-commands/setupinfo.js
Normal 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);
|
||||||
|
},
|
||||||
|
};
|
@ -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 = {
|
||||||
|
10
strings.json
10
strings.json
@ -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>",
|
||||||
|
Loading…
Reference in New Issue
Block a user