Skip to content

An advanced and feature-rich TypeScript Discord bot built using custom classes

License

Notifications You must be signed in to change notification settings

CuteNikki/discord-bot

Repository files navigation

- A WORK IN PROGRESS -

DiscordJS V14 Bot written in TypeScript

This Discord bot was built with custom classes, i18next for translations, Pino as logger, and MongoDB as database.

pino mongodb i18next discord.js
Repository Stars Repository Issues Repository Forks License

Made with 💖 by Nikki.

Table of Contents

Run it locally

All it takes is just 6-7 simple steps.

  1. Clone the repository.

    git clone https://github.com/CuteNikki/discord-bot.git
  2. Navigate into the project directory.

    cd discord-bot
  3. Install all the dependencies.

    bun install
  4. 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).
  5. 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.
  6. 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!
  7. (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]

How to create new commands, events, buttons and more

Creating a slash command

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

Creating an event

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}`);
  }
});

Creating a button

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

Adding a new language

  1. Add the new language to supported lanuguages in src/structure/utilities/language.ts.
  2. Create a new folder in locales (src/structure/locales/{lng}).
  3. Create namespace files for commands and messages.
  4. Translate all values to the new language.
Currently supported languages
  • en Englisch (100%)
  • de German (100%)

Translating commands

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

Translating messages

src/structure/locales/{lng}/messages.json

{
  "preview-color": {
    "preview": "Here's a preview of the color {{color}}!",
    "invalid": "The color you provided is invalid!"
  }
}

Contributing

Contributions, issues, and feature requests are welcome. Feel free to check the issues page if you want to contribute.

Show your support

Please ⭐️ this repository if this project helped you!

License

Copyright © 2024 CuteNikki. This project is MIT licensed.

TO-DO

  • 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?)
  • 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
      • buy
      • sell
    • 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