This discord bot was built with custom classes, i18next for translations, Pino as logger and MongoDB as database.
Made with 💖 by Nikki.
All it takes is just 6-7 simple steps.
- Clone the repository.
git clone https://github.com/CuteNikki/discord-bot.git
- Navigate into the project directory.
cd discord-bot
- Install all the dependencies.
bun install
- Set up your config file.
# copy example.config.json and rename to config.json
# or use this command if you are on Linux.
cp example.config.json config.json
# fill in all values (more details in the config file).
- Deploy the slash commands.
bun run deploy
# you may also use the /register-commands slash command on discord,
# once the commands have been registered using the deploy command.
- Run the bot using a script.
# Run in development:
bun run dev
# or compile:
bun run build
# and run:
bun run start
# You may also use --debug for more detailed console logs!
- (optional) Configure more settings using the developer command.
# This is used for giving Supporter Badge on support server boost.
/developer-configuration support-guild-id set <guildId>
# This is used for the /support command.
/developer-configuration support-invite-url set <url>
# This is used for the /invite command.
/developer-configuration bot-invite-url set <url>
# This is used to manage badges of a user.
/developer-configuration badges add/remove/show <user> [badge]
# This is used to manage bans.
/developer-configuration bans add/remove/list [user]
This displays the use of i18next, the command class and all its properties including autocomplete.
import {
Colors, // All of default discord colors.
EmbedBuilder,
PermissionFlagsBits,
SlashCommandBuilder,
InteractionContextType,
ApplicationIntegrationType,
type ColorResolvable
} from 'discord.js';
import { t } from 'i18next';
import { Command, ModuleType } from 'classes/command';
import { logger } from 'utils/logger';
export default new Command({
// The module this command belongs to.
// It categorizes commands in the commands list.
// It disables this command if the module is disabled.
module: ModuleType.General,
// 1 second cooldown between command uses.
cooldown: 1_000,
// Only developers can use this command.
isDeveloperOnly: false,
// The bot needs to have this permission to be able to use this command.
botPermissions: ['SendMessages'],
// The slash command data.
data: new SlashCommandBuilder()
.setName('preview-color')
.setDescription('Sends an embed with a color to preview')
// Allowing the command to be used in guilds, DMs and private channels.
.setIntegrationTypes(ApplicationIntegrationType.GuildInstall, ApplicationIntegrationType.UserInstall)
// Allowing the command to be used in guilds, DMs and private channels.
.setContexts(InteractionContextType.BotDM, InteractionContextType.Guild, InteractionContextType.PrivateChannel)
// By default only users with the manage messages permission can see and use this command.
// UNLESS it was changed in the server settings under the integrations tab (per user or role).
.setDefaultMemberPermissions(PermissionFlagsBits.ManageMessages)
// Adding options
.addStringOption(
(option) =>
option
.setName('color')
.setDescription('The color to preview')
.setRequired(true) // Makes the option required.
.setAutocomplete(true) // Enable autocompletion.
),
// On input, the autocomplete function is called.
async autocomplete({ interaction, client }) {
// This gets us whatever the user has typed.
const input = interaction.options.getFocused();
const colors = [
{ name: 'white', value: Colors.White.toString(16) },
{ name: 'aqua', value: Colors.Aqua.toString(16) },
{ name: 'green', value: Colors.Green.toString(16) },
{ name: 'blue', value: Colors.Blue.toString(16) },
{ name: 'yellow', value: Colors.Yellow.toString(16) },
{ name: 'purple', value: Colors.Purple.toString(16) },
{ name: 'luminous-vivid-pink', value: Colors.LuminousVividPink.toString(16) },
{ name: 'fuchsia', value: Colors.Fuchsia.toString(16) },
{ name: 'gold', value: Colors.Gold.toString(16) },
{ name: 'orange', value: Colors.Orange.toString(16) },
{ name: 'red', value: Colors.Red.toString(16) },
{ name: 'grey', value: Colors.Grey.toString(16) },
{ name: 'navy', value: Colors.Navy.toString(16) },
{ name: 'dark-aqua', value: Colors.DarkAqua.toString(16) },
{ name: 'dark-green', value: Colors.DarkGreen.toString(16) },
{ name: 'dark-blue', value: Colors.DarkBlue.toString(16) },
{ name: 'dark-purple', value: Colors.DarkPurple.toString(16) },
{ name: 'dark-vivid-pink', value: Colors.DarkVividPink.toString(16) },
{ name: 'dark-gold', value: Colors.DarkGold.toString(16) },
{ name: 'dark-orange', value: Colors.DarkOrange.toString(16) },
{ name: 'dark-red', value: Colors.DarkRed.toString(16) },
{ name: 'dark-grey', value: Colors.DarkGrey.toString(16) },
{ name: 'darker-grey', value: Colors.DarkerGrey.toString(16) },
{ name: 'light-grey', value: Colors.LightGrey.toString(16) },
{ name: 'dark-navy', value: Colors.DarkNavy.toString(16) },
{ name: 'blurple', value: Colors.Blurple.toString(16) },
{ name: 'greyple', value: Colors.Greyple.toString(16) },
{ name: 'dark-but-not-black', value: Colors.DarkButNotBlack.toString(16) },
{ name: 'not-quite-black', value: Colors.NotQuiteBlack.toString(16) }
];
// If the input is empty, return all colors but limit to 25!
// Discord API only allows for a max of 25 choices.
if (!input.length) {
await interaction.respond(colors.slice(0, 25));
return;
}
await interaction.respond(
colors
// Filter the colors to only respond with the ones that match the input.
.filter((color) => color.name.toLowerCase().includes(input.toLowerCase()))
// Again, making sure we only ever return max of 25 results.
.slice(0, 25)
);
},
// On command, the execute function is called.
// order of interaction, client and lng does not matter.
async execute({ interaction, client, lng }) {
// get a guilds language:
// import { getGuildLanguage } from 'db/language';
// const guildLng = await getGuildLanguage(guildId);
// get a different users language:
// import { getUserLanguage } from 'db/language';
// const otherLng = await getUserLanguage(userId);
// get the color the user provided
const color = interaction.options.getString('color', true);
// Autocomplete allows you to give the user a list to choose from,
// but they will still be able to type in whatever they want!
// it's a must to check if they actually provided a valid color.
try {
// Send the embed with the color preview if the color is valid.
await interaction.reply({
embeds: [
new EmbedBuilder()
// We get a type error here without the casting.
.setColor(color as ColorResolvable)
.setDescription(t('preview-color.preview', { lng, color }))
]
});
} catch (err) {
// This block runs if an invalid color is provided.
logger.debug({ err }, 'Error while previewing color');
// In case something else went wrong and the reply was actually sent, edit it.
if (!interaction.replied) {
await interaction.reply({ content: t('preview-color.invalid', { lng }), ephemeral: true });
} else {
await interaction.editReply({ content: t('preview-color.invalid', { lng }) });
}
}
}
});
src/structure/locales/{lng}-messages.json
{
"preview-color": {
"preview": "Heres a preview of the color {{color}}!",
"invalid": "The color you provided is invalid!"
}
}
src/structure/locales/{lng}-commands.json
{
"preview-color": {
"name": "preview-color",
"description": "Sends an embed with a color to preview",
"options": [
{
"name": "color",
"description": "The color to preview"
}
]
}
}
import { Events } from 'discord.js';
import { Event } from 'classes/event';
import { logger } from 'utils/logger';
export default new Event({
// the name of the event
name: Events.ClientReady,
// only run this once, won't run again even if another ready event is emitted
once: true,
// it is important to always have client first
// and other events properties after that
execute(client, readyClient) {
logger.info(`Logged in as ${readyClient.user.username}#${readyClient.user.discriminator}`);
}
});
import { Button } from 'classes/button';
export default new Button({
// The module this button belongs to.
module: ModuleType.General,
// The custom identifier of the button.
customId: 'ping',
// If the button was received by a command, only the command sender can use this button.
isAuthorOnly: true,
// If true then a button with the custom id of "abc-ping-abc" would still
// trigger this button because the custom id includes "ping".
// This is useful to pass an id or other information to the button.
isCustomIdIncluded: false, // In this case, we don't need it.
// The permissions required by a member to use this button.
permissions: ['SendMessages'], // Ignored in DMs.
// Permissions the bot needs to execute this function.
botPermissions: ['SendMessages'], // Ignored in DMs.
// On button click, the execute function is called.
async execute({ interaction, client, lng }) {
await interaction.channel?.send({ content: 'pong!' }); // Send a response in the channel.
}
});
Contributions, issues and feature requests are welcome. Feel free to check issues page if you want to contribute.
Please ⭐️ this repository if this project helped you!
Copyright © 2024 CuteNikki. This project is MIT licensed.
- utility module (more features might be added)
- developer module (more features might be added)
- fun module (more features will be added)
- game command
- 2048 (maybe?)
- Lights Out (maybe?)
- game command
- config
- reaction roles (already implemented)
- fully customizable message
- word chain game (next word needs to start with the last letter of the previous word)
- economy
- config
- balance
- deposit
- withdraw
- pay
- inventory
- daily
- transfer
- marry
- divorce
- leaderboard
- rob
- work
- bet
- coinflip
- profile
- profile context
- shop
- confession
- config command
- confess
- suggestions
- config command
- suggest
- level config
- text and voice levelling is fully implemented with ignored & enabled channels, announcements, reward and multiplier roles
- levelup announcement
- fully customizable message
- reaction roles (already implemented)