BerryBot Documentation
Table of Contents#
- Features
- Project Structure
- Setup and Installation
- Core Components
- Development Guidelines
- Troubleshooting
- License
- Authors
Features#
Added Features#
Administration
- Self Roles
- Allow users to assign themselves roles in different categories.
- Self Roles
Planned Features#
Moderation
Member Screening
- Require users to fill out a form before being allowed access to the server.
Mute, Ban, and Kick Commands
- Punish users for breaking server rules and terms of service, either permanently or temporarily.
Support Tickets
- Allow users to create support tickets and speak directly to server staff about an issue.
Event Logging
- Log all bot moderation events to a specific channel, to easily detect permission abuse by moderators.
Administration
- Member Welcoming
- Send personlized welcome messages to new users.
- Member Welcoming
Project Structure#
src/
├── commands/ # Bot commands
│ ├── Public/ # Public commands
│ └── Private/ # Private/developer commands
├── components/ # Message components (buttons, select menus, etc.)
├── config/ # Configuration files
├── database/ # Database models and connection
├── events/ # Discord event handlers
├── handlers/ # Command and event loading handlers
├── interfaces/ # TypeScript interfaces and types
├── messages/ # Message templates
├── util/ # Utility functions
├── bot.ts # Bot initialization
└── index.ts # Entry point
Setup and Installation#
Prerequisites#
- Node.js (v16 or higher)
- MongoDB Atlas account
- Discord Bot Token
Environment Variables#
Create a .env
file in the root directory with the following variables:
DISCORD_TOKEN=your_discord_bot_token
MONGO_STRING=your_mongodb_connection_string
DATABASE_NAME=your_database_name
DEVELOPER_ID=your_discord_user_id
NODE_ENV=development | production
Installation#
- Clone the repository
- Install dependencies:
yarn install
- Build the project:
yarn build
- Start the bot:
// For development
yarn dev
// For production
yarn start
Core Components#
Commands#
Each command is a TypeScript module that exports a command object. The bot’s command handler will scan /dist/commands
for .js
files, and register them automatically, so it is unnecessary to import your commands manually.
When a ChatInputCommandInteraction
event is sent to the bot, it will search it’s registry for the command, and run it’s execute()
function.
Example command structure:
import { SlashCommandBuilder, ChatInputCommandInteraction } from 'discord.js';
import { Command } from '../../../interfaces';
import { logger } from '../../util':
const command: Command = {
data: new SlashCommandBuilder()
.setName('test')
.setDescription('Test command')
.setDefaultMemberPermissions(0), // Can only be used in a server
async execute(interaction: ChatInputCommandInteraction) {
return await interaction.reply('Test command executed!');
}
};
module.exports = command;
If you want to add subcommands to a command (ex /command subCommand
), you must replace data
with a SlashCommandSubCommandBuilder
. They also have the required property parent
.
Example subcommand structure:
import { ActionRowBuilder, ButtonBuilder, ChatInputCommandInteraction, SlashCommandSubcommandBuilder } from 'discord.js';
import { buttons } from '../../../components';
import { Command } from '../../../interfaces';
const command: Command = {
parent: 'test',
data: new SlashCommandSubcommandBuilder()
.setName('button')
.setDescription('Test button'),
async execute(interaction: ChatInputCommandInteraction, client) {
// Return if the interaction wasn't used in a guild.
if (!interaction.guild) {
return await interaction.reply({ content: 'This command can only be used in a server.', ephemeral: true });
}
const row = new ActionRowBuilder<ButtonBuilder>().addComponents(await buttons.TestButton.build(client));
return await interaction.reply({ content: 'Test Button', components: [row] });
}
};
module.exports = command;
Adding a New Command#
- Create a new file in
src/commands/Public/
orsrc/commands/Private/
- Follow the command structure template
- The command will be automatically loaded by the command handler
Message Builders#
Message builders are reusable templates for Discord messages that can include embeds, components, and other message options. They are useful for creating consistent message layouts across different commands.
- Create a new file in
src/messages/
with the following structure:
import { MessageBuilder } from '../../interfaces';
import { Client } from '../../interfaces/Client';
export const WelcomeMessage: MessageBuilder = {
embeds: [], // Array of embeds to include
components: [], // Array of action rows with components
// Build function that returns the final message options
async build(client: Client, username: string) {
return {
embeds: [
{
title: `Welcome ${username}!`,
description: 'Thanks for joining our server!',
color: 0x00ff00
}
],
components: [
// Add your components here
]
};
}
};
- Use the message builder in your commands:
import { WelcomeMessage } from '../../messages';
// In your command
const message = await WelcomeMessage.build(client, interaction.user.username);
await interaction.reply(message);
Message Components#
If you want to utilize message components for interactivity, it is unecessary to script each one from the command file. Rather there is a system for reusable components in /src/components/
. This allows for reuse of scripted components across multiple commands, and keeps your command files from getting too long.
Message components have following properties:
{
id: string; // Unique ID for this component. (EX: 'role-selector')
permissions?: bigint[]; // Permission Bits required to interact with this component
multi_select?: boolean; // [SELECT MENU ONLY] Toggle Multi Select
developer?: boolean; // Developer only
execute(
interaction: ComponentInteraction, // Interaction object
client: Client, // Bot client
data?: { [key: string]: any }, // Any data parsed from the components unique ID
guild?: discord.Guild, // Guild the component is in
response?: discord.Collection<string, discord.TextInputComponent>, // [MODAL ONLY] Values entered into Text Inputs
selected?: discord.APISelectMenuOption | discord.APISelectMenuOption[] // [SELECT MENU ONLY] Menu items selected
): void;
build(client: Client, ...args: any): Promise<discord.ComponentBuilder>; // Any code and arguments needed to build the component.
}
Example component structure:
import { ButtonBuilder, ButtonStyle, PermissionFlagsBits } from 'discord.js';
import { ButtonComponent } from '../../interfaces/MessageComponent';
// Example button component that requires certain permissions to use
export const MessageComponent: ButtonComponent = {
id: 'test-button',
type: ComponentTypes.Button,
// User needs ManageEvents and ManageRoles permissions
permissions: [PermissionFlagsBits.ManageEvents, PermissionFlagsBits.ManageRoles],
async build(client, boolean) {
return new ButtonBuilder()
.setCustomId(await client.getCustomID('test-button', testJSON))
.setLabel('Test Button')
.setStyle(ButtonStyle.Primary);
},
// Handles the button click
execute(
interaction,
_client
) {
interaction.reply({
content: `You pressed the test button!`,
ephemeral: true
});
}
};
export default MessageComponent;
Persistent Component Data#
In some cases it is useful to store extra data in a component. To do this, utilize the client.getCustomID(id, jsonObject)
. This function utilizes the LZ-String
library to compress the stringified JSON object into a compact, URI-safe string. This compressed string is then embedded within a Discord component’s custom ID, allowing the storage of additional data without exceeding Discord’s character limits for component identifiers. (100 haracter limit)
Note: As this data is stored on discord, it is recommended not to store sensitive data.
Example Json:
const modal_data = {
type: "modal",
id: "mod_history",
userId: "123456789012345678",
page: 2,
filters: {
sort: "recent",
category: "moderation",
tags: ["bans", "kicks", "mutes"],
priority: "high",
resolved: false
},
};
const button_id = await client.getCustomID('open-history', modal_data);
//console.log(button_id) => open-history[ᯡࠫ䅜Ā匰ᜨאỠጢణㄠ㑀আ䆅栯倢恠材⠤炯ࠢ巉你䦃ࠨ怩䀹䀥䀵䀭䀽䀣䀳䀡䁄≬獈డ暑爛瀣ᠲ挡浡ᑕ͐ጔ〣ಀǹ々壬㇁䧂⩔ຎѰね⧜;∠ɐ恚丐öɻʜ㤥䲊〥氁∈ș䠲ᔦ䊹䎁㋠ǘ悘竸瘠⿃⠠]
Tip: Avoid including “dynamic” data, like user provided text or anything that isn’t predictable.
Buttons#
Buttons are defined in src/components/buttons/
. They are the only component that does not have any unique properties or arguments.
[!NOTE] An example button is seen at the start of this section.
Select Menus#
Select menus are defined in src/components/selectMenus/
. The property multi_select
can be set to true
to toggle multi-select. The argument seleted
passed through execute
is a discord.APISelectMenuOption
. If multi select is enabled, it is passed as an array.
Example select menu structure:
import { StringSelectMenuBuilder } from 'discord.js';
import { SelectMenuComponent } from '../../interfaces';
export const TestSelect: SelectMenuComponent = {
customId: 'test-select',
multi_select: false,
build: async (client) => {
return new StringSelectMenuBuilder()
.setCustomId('test-select')
.setPlaceholder('Select an option')
.addOptions([
{ label: 'Option 1', value: '1' },
{ label: 'Option 2', value: '2' }
]);
},
execute: async (interaction, _client, selected) => {
// Reply to the interaction with the selected option's label
interaction.reply({ content: `You selected ${selected.label}`, ephemeral: true });
}
};
Modals#
Modals are defined in src/components/modals/
. They are used to collect one or more text inputs. execute()
passes the responses as the argument response
, a Collection<string, discord.TextInputComponent>
. (The key is the customId you gave the TextInputBuilder)
Example modal structure:
import { ActionRowBuilder, ModalBuilder, TextInputBuilder, TextInputStyle } from 'discord.js';
import { ModalComponent, ComponentTypes } from '../../interfaces/MessageComponent';
import { logger } from '../../util';
export const MessageComponent: ModalComponent = {
id: 'test-modal',
async build(_client) {
const row = new ActionRowBuilder<TextInputBuilder>().addComponents(
new TextInputBuilder().setCustomId('test-modal-input').setPlaceholder('Test Input').setStyle(TextInputStyle.Short).setLabel('Test Input')
);
return new ModalBuilder().setTitle('Test Modal').setCustomId('test-modal').setComponents([row]);
},
async execute(interaction, _client, fields) {
logger.info(
'\n' + new (require('ascii-table'))().setHeading('Field', 'Response').addRow('test-modal-input', fields.get('test-modal-input')?.value).toString()
);
// Reply to the interaction with the response.
interaction.reply({ content: `This is a test modal! You said: ${fields.get('test-modal-input')?.value}`, ephemeral: true });
}
};
export default MessageComponent;
Adding a New Component#
- Choose the appropriate component type (Button, Select Menu, or Modal)
- Create a new file in the corresponding directory:
src/components/buttons/
for buttonssrc/components/selectMenus/
for select menussrc/components/modals/
for modals
- Follow the component interface structure.
- Import and register the component in
src/components/index.ts
for easy access.
import { TestButton } from './buttons/TestButton';
import { TestSelect } from './selectMenus/TestSelect';
import { TestModal } from './modals/TestModal';
export const components = {
buttons: {
TestButton
},
selectMenus: {
TestSelect
},
modals: {
TestModal
}
};
Example Usage:
import buttons from '../components/';
const row = new ActionRowBuilder().addComponents(button);
const message = {
components: [row]
}
MongoDB Database#
This bot optionally uses MongoDB for data storage. To use this you’ll need to create a free MongoDB Atlas account.
Creating a new Schema#
- Create a new
.ts
file in src/database/schemas with the following structure:
import mongoose from 'mongoose';
// TypeScript interface for MongoDB documents - includes all fields and Mongoose methods
export interface ExampleModel extends mongoose.Document {
document_id: number;
string: string;
number: number;
boolean: boolean;
object: ExampleObject;
}
// Example interface for nested object structure.
export interface ExampleObject {
string: string;
number: number;
boolean: boolean;
}
// Mongoose schema defines document structure and validation rules. Note this does not use traditional typings, thus the need to be manually outlined.
const ExampleSchema = new mongoose.Schema({
document_id: Number,
string: String,
number: Number,
boolean: Boolean,
object: {
string: String,
number: Number,
boolean: Boolean
}
});
// Creates Mongoose model with TypeScript type safety
export const ExampleModel = mongoose.model<ExampleModel>('ExampleSchema', ExampleSchema);
- In
src/database/index.ts
, import your database model:
import mongoose from 'mongoose';
import { config } from '../config';
import { ExampleModel } from './schemas/Example'; // Imported Model
...
- Create a
Model
object with the following properties and functions:
export const example: Model = {
// Function used to retrieve documents, and create a new one if it is not found.
get: async (document_id: string) =>
(await ExampleModel.findOne({ document_id: document_id })) ||
(await ExampleModel.create({
document_id: document_id
// Add any default values here
})),
update: async(document_id: string, data: ExampleModel) {
await GuildSettings.updateOne({ document_id: document_id }, settings);
}
model: ExampleModel
};
- Add your model to the
database
object at the end of the file. This object is accessible from the botclient
object. (Ex:client.database.serverSettings.get(guildId))
)
export const database = {
modReports,
userProfiles,
example
// Add models here
};
### Events
Events are handled in the `events/` directory. Each event is a TypeScript module that exports an event object.
Example event structure:
```typescript
import { Event } from '../../interfaces';
export const event: Event = {
name: 'eventName',
once: true,
execute: async (client) => {
// Event handling logic
}
};
Development Guidelines#
Handlers#
The bot uses three main handlers to manage its components:
Command Handler#
Located in src/handlers/CommandHandler.ts
, this handler:
- Loads all commands from the
commands/
directory - Registers slash commands with Discord
- Handles command execution
- Supports both public and private commands
- Automatically loads subcommands
Event Handler#
Located in src/handlers/EventHandler.ts
, this handler:
- Loads all events from the
events/
directory - Registers event listeners with Discord.js
- Supports both once and regular event listeners
- Handles event execution with proper error catching
Message Component Handler#
Located in src/handlers/MessageComponentHandler.ts
, this handler:
- Loads all message components (buttons, select menus, modals) from the
components/
directory - Registers component interactions
- Handles component execution
- Manages component state and data
Logging#
The bot uses Pino for logging. Pino logs are saved to logs/
. It is recommended to use the logger utility for all logging:
import { logger } from '../util';
logger.info('Your log message');
logger.error('Error message');
logger.warn('Warning message');
logger.debug('Debug message');
Troubleshooting#
Common Issues#
MongoDB Connection Issues#
- Check your MongoDB Atlas connection string
- Verify your IP is whitelisted in MongoDB Atlas
- Ensure your database user has proper permissions
- Check if your MongoDB cluster is running
Command Registration Issues#
- Check the command handler logs
- Verify command structure follows the interface
- Ensure the bot has proper permissions
- SubcommandsL Ensure the proper parent ID is set
Component Interaction Issues#
- Check the component’s custom ID
- Ensure there are no errors in the component’s execute function
- Ensure any data stored in the component is not too large (you will be notified in the logs)
License#
This project is licensed under the MIT License - see the LICENSE file for details.