This write up is still work in progress...
Touch Grass Bot
Touch Grass Bot is a discord bot that I have been commissioned to make for the Discord server, Plants VS Zombies R34, it obviously being a porn server. This bot was mostly a bot intended for moderation that was unique to the server where the moderators needed to see evidence that someone was 18+ before accessing the porn content.
Features
- Basic moderation tools such as banning and muting
- Delete messages based on an exact and a wildcard slurfilter
- Modify the roles of members
- Verify and porn ban members
- Record moderators stats
History
Attempting to write out the history of this bot is a bit tricky as most of the stuff that I've done to it was a while ago and it's not like my memory is perfect
What I do remember initally was that the bot was just a generic moderation bot as the other bots at the time were too restricting what you can do on its free plan and subscription fees are a bitch
The core feature on its inital version was that it could verify or pornban someone by changing their roles.
Over time more features were added such as just generally silly commands as well as adding a sticky roles feature since the current bots were also being bitches about it at the time. This requiring that I had to add database support. Although this also gave the excuse of adding other addtional features such as mod stats since the moderators were always dick measuring
Some time later I found that the underlying code wasn't well written, was prone to break easily and wasnt easy to iterate upon. So I just decided to scrap the whole thing and create a "template" that you can see today and is used repeated with my other bots.
Some time after that I then created the slur filter to the request of the owner of the server that I was making this for leading to the creation of the exact and the wildcard slur filter.
Thats pretty much the history that I can remember as well as just reading off the Github commits
Technical Details
Before properly diving into the bot, we should agknowledge what the file structure of this bot currently looks like:
/src/
/src/classes/
/src/commands/
/src/commands/interaction/
/src/commands/interaction/info/
/src/commands/interaction/moderation/
/src/data/
/src/events/
/src/handlers/
/src/modules/
/src/schemas/
/src/types/
/src/index.ts
Lets first look within the classes file, which contains pretty much the most important part of the bot
./client.ts
./command.ts
./event.ts
./handler.ts
client.ts is for creating the bot client and managing things such as commands, cooldowns, and handlers (normal or timed)
command.ts is the basic template of what all commands will contain having two classes called InteractionCommand and MessageCommand
event.ts is the basic template for all bot events and what code to run when an event is fired e.g. Events.MessageCreate
handler.ts is the basic template for just... handling things, in my case I use it to initalise interaction commands as well as bot events, but this could easily be expanded to handle things like message commands
Now lets look at the next important thing that gets executed and thats the /src/handlers folder which gets all of its files run pretty much at startup
./eventHandler.ts
./interactionCommandHandler.ts
The event handler gets every single event found in the events folder an initalises them
The interaction command handler also pretty much does the same thing as the event handler where it gets every single file in the commands/interaction folder and then adds the slash command data to the client for discord to later receive in events/ready.ts
Now for the next important thing after that and that is the events that the bot subscribes to
./guildMemberAdd.ts
./guildMemberRemove.ts
./interactionCreate.ts
./messageCreate.ts
./ready.ts
GuildMemberAdd and GuildMember exist solely to provide people with roles or to keep note of them within the database.
Ready exists to provide Discord's API with the commands that the bot listens to.
InteractionCreate is a lot more complicated as it has to deal with serving commands to the user without any fuck ups, it has to juggle a bunch of things at the same time so lets go through what it does bit by bit. There are 4 functions in this event, the default execute(), applicationCommand(), getUserCooldowns(), getRemainingCooldown()
async execute(interaction: Interaction): Promise
if (interaction.isCommand()) {
return this.applicationCommand(interaction);
}
}
execute() really is just nothing, its just a function that checks if its even a slash command at all. Realistically, if I wanted this bot to be fully featured, I'd've included functuionality for button commands and the like but I was just feeling lazy
Now this next section is going to seem a bit funky lol but trust me, i'm cooking...
private async applicationCommand(interaction: ChatInputCommandInteraction
const client: BotClient = interaction.client as BotClient;
const channel: TextChannel = client.channels.cache.get(interaction.channel?.id as string) as TextChannel;
const { cooldowns } = client;
const commandName: string = interaction.commandName;
const command: InteractionCommand = client.interactionCommands.get(commandName);
if (!command) {
console.error(`Could not find command "${commandName}"`);
return;
}
if (command.nsfw && !channel.nsfw) {
return interaction.reply({ content: 'This command can only be used in NSFW channels.', ephemeral: true });
}
const cooldownDuration = command.cooldown ?? 3;
const userCooldowns = this.getUserCooldowns(cooldowns, command.data.name);
const remainingCooldown = this.getRemainingCooldown(interaction, userCooldowns, cooldownDuration);
if (remainingCooldown > 0) {
const expiredTimestamp = Math.round((Date.now() + remainingCooldown) / 1000);
await interaction.reply({ content: `Please wait, you are on a cooldown for \`${command.data.name}\`. You can use it again
return;
}
if (interaction.guild) {
await getProfile(interaction.user.id, interaction.guild.id);
await getGuild(interaction.guild.id);
}
try {
await command.execute(interaction);
}
catch (error) {
console.error(error);
await interaction.reply({ content: 'There was an error while executing this command!', ephemeral: true });
}
userCooldowns.set(interaction.user.id, Date.now());
setTimeout(() => userCooldowns.delete(interaction.user.id), cooldownDuration * 1000);
}
This seems like a lot but lets go over the variables first.
client is gotten from the interaction.client parameter just so that I can use it for a bunch of other stuff that you'll see soon
channel is uh... honestly i'm, not sure what kind of crack I was smoking here, I can only assume it looks like this for type validation, but looking in the discord.js docs, its clear that wasnt necessary, this could easily be:
const channel: TextChannel = interaction.channel
cooldown this is just a collection of cooldowns gotten from the client... nothing special to it.
commandName this pretty much stores the name of the command as a variable, this is very important as the next variable, command gets the command from the client's collection of interaction commands that match the command by name
cooldownDuration this gets the cooldown listed within the command, if its null, defaults to 3s
userCooldowns gets ALL of the cooldowns for this command, probably not the best way to do this lol
remainingCooldown filters out which cooldown belongs to the user that created the interaction and gets the time remaining on that cooldown
Thats all of the variables, now lets see how this executes
Pretty much this starts out by checking the command exists at all, theoretically should never happen since all of the data of a command comes from the (file of the) command anyway.
Then we check if the command and the channel is nsfw at all, useful for bots that deal with nsfw stuff such as this one, although there isn't really an nsfw command in this version of the bot
Then we have the cooldown check, just simple maths is all
Now we get to the strangest part of my code...
if (interaction.guild) {
await getProfile(interaction.user.id, interaction.guild.id);
await getGuild(interaction.guild.id);
}
What?? Why would you need this, the only thing this code is for is to get the values of the profile and guild so why isnt it being assigned to anything??
Funny thing is, I made is so that in these functions, is that it tries to create a new profile and guild if it doesnt exist. Highly counterproductive and useless... but it exists anyway. Why? I CAN'T REMEMBER. I probably was taking a double dose of that crack I mentioned earlier. But I must've had a reason if I put it there
After that we have the code that just executes the command and then the code to set the cooldown.
Theoretically I could've used a database instead of a collection for my cooldowns but I was feeling lazy, and this solution works as is.
As for the other two functions... They're just getters and calculators
private getUserCooldowns(cooldowns: Collection
let userCooldowns = cooldowns.get(commandName);
if (!userCooldowns) {
userCooldowns = new Collection
cooldowns.set(commandName, userCooldowns);
}
return userCooldowns;
}
private getRemainingCooldown(interaction: Interaction, userCooldowns: Collection
const lastUsed = userCooldowns.get(interaction.user.id);
if (!lastUsed) {
return 0;
}
const now = Date.now();
const expirationTime = lastUsed + cooldownDuration * 1000;
return Math.max(expirationTime - now, 0);
}
Personal Notes
While this wasn't my first bot, this was the bot that really advanced my understanding of Node.js, npm, Javascript and typescript. In my eyes I see this as the "jumping point" after I made pogcoin
While this bot isn't being used anymore due to a lack of demand for it, this really gave me the opportunity to challenge myself and create a new bot that would be presented for everyone to see.