├── .gitignore ├── nodemon.json ├── .prettierrc ├── src ├── index.ts ├── lib │ ├── utils.ts │ └── prisma.ts ├── commands │ ├── util │ │ └── ping.ts │ ├── animal │ │ └── cat.ts │ └── economy │ │ ├── weekly.ts │ │ └── daily.ts ├── structures │ ├── Event.ts │ ├── Bot.ts │ └── Command.ts ├── handlers │ ├── EventHandler.ts │ └── InteractionHandler.ts └── events │ ├── client │ └── ready.ts │ └── interactions │ └── interactionCreate.ts ├── .github ├── dependabot.yml └── workflows │ ├── lint.yml │ └── test-tsc.yml ├── .swcrc ├── .eslintrc.json ├── README.md ├── .env.example ├── prisma └── schema.prisma ├── LICENSE ├── package.json └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .vscode 3 | .env 4 | dist -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": [ 3 | "**/*.ts" 4 | ], 5 | "exec": "ts-node --esm src/index.ts", 6 | "ext": "js ts" 7 | } -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "trailingComma": "all", 4 | "singleQuote": false, 5 | "printWidth": 100, 6 | "tabWidth": 2 7 | } -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import "dotenv/config"; 2 | import { Bot } from "./structures/Bot.js"; 3 | 4 | const bot = new Bot(); 5 | 6 | bot.login(process.env["BOT_TOKEN"]); 7 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "npm" # "npm" "yarn" 4 | directory: "." 5 | schedule: 6 | interval: "monthly" # "daily" "weekly" "monthly" 7 | -------------------------------------------------------------------------------- /.swcrc: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/swcrc", 3 | "module": { 4 | "type": "es6" 5 | }, 6 | "jsc": { 7 | "target": "es2020", 8 | "externalHelpers": true, 9 | "parser": { 10 | "syntax": "typescript", 11 | "dts": false 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | "node": true 6 | }, 7 | "extends": ["@casper124578/eslint-config"], 8 | "parser": "@typescript-eslint/parser", 9 | "parserOptions": { 10 | "ecmaVersion": 2022, 11 | "sourceType": "module", 12 | "project": "./tsconfig.json" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # discord-bot-template 2 | 3 | Discord.js bot template with ESM, Prisma, MongoDB TypeScript, ESLint and Prettier. 4 | 5 | ## Installation 6 | 7 | 1. Install the dependencies: `npm install` 8 | 2. Copy, paste and rename `.env.example` to `.env` (Linux: `cp .env.example .env`) 9 | 3. [Get your bot token](https://discord.com/developers/applications) and place it in the `.env` file 10 | 4. Develop the bot using `npm run dev` 11 | 5. Start the bot in production `npm run build && npm run start` 12 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from "node:path"; 2 | 3 | interface ImportFileOptions { 4 | filename: string; 5 | constructorOptions: unknown[]; 6 | } 7 | 8 | export async function importFileFromFilename({ 9 | filename, 10 | constructorOptions, 11 | }: ImportFileOptions): Promise { 12 | const filePath = resolve(process.cwd(), filename); 13 | const File = await (await import(filePath)).default; 14 | const constructor = new File(...constructorOptions); 15 | return constructor; 16 | } 17 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Check ESLint 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | ESLint: 7 | runs-on: ubuntu-latest 8 | 9 | strategy: 10 | matrix: 11 | node-version: [16.x] 12 | 13 | steps: 14 | - uses: actions/checkout@v3 15 | - name: Use Node.js ${{ matrix.node-version }} 16 | uses: actions/setup-node@v3 17 | with: 18 | node-version: ${{ matrix.node-version }} 19 | 20 | - name: Install dependencies 21 | run: npm install 22 | - name: Run lint 23 | run: npm run lint 24 | -------------------------------------------------------------------------------- /src/commands/util/ping.ts: -------------------------------------------------------------------------------- 1 | import { Command, type CommandContext } from "../../structures/Command.js"; 2 | import type { Bot } from "../../structures/Bot.js"; 3 | import ms from "pretty-ms"; 4 | 5 | export default class PingCommand extends Command { 6 | constructor(bot: Bot) { 7 | super(bot, { 8 | name: "ping", 9 | description: "Returns the bot ping!", 10 | }); 11 | } 12 | 13 | async execute({ interaction }: CommandContext) { 14 | const ping = this.bot.ws.ping; 15 | 16 | await interaction.reply(`The bot's ping is: ${ms(ping)}`); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.github/workflows/test-tsc.yml: -------------------------------------------------------------------------------- 1 | name: Check TypeScript build 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | ESLint: 7 | runs-on: ubuntu-latest 8 | 9 | strategy: 10 | matrix: 11 | node-version: [16.x] 12 | 13 | steps: 14 | - uses: actions/checkout@v3 15 | - name: Use Node.js ${{ matrix.node-version }} 16 | uses: actions/setup-node@v3 17 | with: 18 | node-version: ${{ matrix.node-version }} 19 | 20 | - name: Install dependencies 21 | run: npm install 22 | - name: Run test tsc 23 | run: "npm run test-tsc" 24 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | BOT_TOKEN="" 2 | DEV_GUILD_ID="" 3 | 4 | # This was inserted by `prisma init`: 5 | # Environment variables declared in this file are automatically made available to Prisma. 6 | # See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema 7 | 8 | # Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB (Preview). 9 | # See the documentation for all the connection string options: https://pris.ly/d/connection-strings 10 | 11 | DATABASE_URL="mongodb+srv://test:test@cluster0.ns1yp.mongodb.net/myFirstDatabase" -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | generator client { 5 | provider = "prisma-client-js" 6 | } 7 | 8 | datasource db { 9 | provider = "mongodb" 10 | url = env("DATABASE_URL") 11 | 12 | } 13 | 14 | model DiscordGuildMember { 15 | id String @id @default(auto()) @map("_id") @db.ObjectId 16 | discordId String 17 | guildId String 18 | cash Int @default(0) 19 | lastDailyUsed DateTime? 20 | lastWeeklyUsed DateTime? 21 | 22 | 23 | @@unique([discordId, guildId], name: "discordIdGuildId") 24 | } 25 | -------------------------------------------------------------------------------- /src/structures/Event.ts: -------------------------------------------------------------------------------- 1 | import type * as DJS from "discord.js"; 2 | import type { Bot } from "./Bot.js"; 3 | 4 | export type EventName = keyof DJS.ClientEvents; 5 | 6 | interface EventOptions { 7 | bot: Bot; 8 | name: EventName; 9 | } 10 | 11 | export abstract class Event { 12 | bot: Bot; 13 | name: EventName; 14 | 15 | constructor(options: EventOptions) { 16 | this.bot = options.bot; 17 | this.name = options.name; 18 | } 19 | 20 | /** 21 | * @param {Bot} bot The bot client 22 | * @param {string[]} args event args 23 | * @returns {DJS.Awaitable} 24 | */ 25 | abstract execute(bot: Bot, ...args: any[]): DJS.Awaitable; 26 | } 27 | -------------------------------------------------------------------------------- /src/lib/prisma.ts: -------------------------------------------------------------------------------- 1 | import type * as DJS from "discord.js"; 2 | import { PrismaClient } from "@prisma/client"; 3 | 4 | export const prisma = new PrismaClient(); 5 | 6 | interface Options { 7 | interaction: DJS.CommandInteraction<"cached">; 8 | } 9 | 10 | export class PrismaUtils { 11 | async upsertDiscordGuildMember({ interaction }: Options) { 12 | return prisma.discordGuildMember.upsert({ 13 | where: { 14 | discordIdGuildId: { discordId: interaction.user.id, guildId: interaction.guildId }, 15 | }, 16 | create: { discordId: interaction.user.id, guildId: interaction.guildId }, 17 | update: { discordId: interaction.user.id, guildId: interaction.guildId }, 18 | }); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/structures/Bot.ts: -------------------------------------------------------------------------------- 1 | import * as DJS from "discord.js"; 2 | import type { Command } from "./Command.js"; 3 | import { EventHandler } from "../handlers/EventHandler.js"; 4 | import { prisma, PrismaUtils } from "../lib/prisma.js"; 5 | 6 | export class Bot extends DJS.Client { 7 | commands: DJS.Collection = new DJS.Collection(); 8 | prisma: typeof prisma; 9 | prismaUtils: PrismaUtils; 10 | 11 | constructor() { 12 | super({ 13 | intents: [ 14 | DJS.IntentsBitField.Flags.Guilds, 15 | /* provide your intents here */ 16 | ], 17 | }); 18 | 19 | new EventHandler(this).loadEvents(); 20 | 21 | this.prisma = prisma; 22 | this.prismaUtils = new PrismaUtils(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/commands/animal/cat.ts: -------------------------------------------------------------------------------- 1 | import * as DJS from "discord.js"; 2 | import { request } from "undici"; 3 | import { Command, type CommandContext } from "../../structures/Command.js"; 4 | import type { Bot } from "../../structures/Bot.js"; 5 | 6 | export default class CatCommand extends Command { 7 | constructor(bot: Bot) { 8 | super(bot, { 9 | name: "cat", 10 | description: "Shows a picture of a cat", 11 | }); 12 | } 13 | 14 | async execute({ interaction }: CommandContext) { 15 | try { 16 | const data = (await (await request("https://nekos.life/api/v2/img/meow")).body.json()) as { 17 | url: string; 18 | }; 19 | 20 | const embed = new DJS.EmbedBuilder().setImage(data.url); 21 | 22 | await interaction.reply({ embeds: [embed] }); 23 | } catch (err) { 24 | console.error(err); 25 | interaction.reply({ content: "An unexpected error occurred", ephemeral: true }); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/structures/Command.ts: -------------------------------------------------------------------------------- 1 | import type * as DJS from "discord.js"; 2 | import type { Bot } from "./Bot.js"; 3 | 4 | export interface InteractionCommandOptions { 5 | name: string; 6 | description?: string; 7 | options?: DJS.ApplicationCommandOptionData[]; 8 | 9 | ownerOnly?: boolean; 10 | nsfwOnly?: boolean; 11 | } 12 | 13 | export interface CommandContext { 14 | /** the interaction from Discord's API */ 15 | interaction: DJS.CommandInteraction<"cached">; 16 | } 17 | 18 | export abstract class Command { 19 | bot: Bot; 20 | name: string; 21 | options: InteractionCommandOptions; 22 | 23 | constructor(bot: Bot, options: InteractionCommandOptions) { 24 | this.bot = bot; 25 | this.name = options.name; 26 | this.options = options; 27 | } 28 | 29 | /** 30 | * @param {CommandContext} context discord.js interaction 31 | * @returns {DJS.Awaitable} 32 | */ 33 | abstract execute(context: CommandContext): DJS.Awaitable; 34 | } 35 | -------------------------------------------------------------------------------- /src/handlers/EventHandler.ts: -------------------------------------------------------------------------------- 1 | import { globby } from "globby"; 2 | import { importFileFromFilename } from "../lib/utils.js"; 3 | import type { Bot } from "../structures/Bot.js"; 4 | import type { Event } from "../structures/Event.js"; 5 | 6 | export class EventHandler { 7 | bot: Bot; 8 | 9 | constructor(bot: Bot) { 10 | this.bot = bot; 11 | } 12 | 13 | async loadEvents() { 14 | try { 15 | const path = 16 | process.env["NODE_ENV"] === "production" ? "./dist/events/**/*.js" : "./src/events/**/*.ts"; 17 | 18 | const files = await globby(path); 19 | await Promise.all(files.map(async (filename) => this.loadEvent(filename))); 20 | } catch (e) { 21 | console.log("An error occurred when loading the events", { e }); 22 | } 23 | } 24 | 25 | private async loadEvent(filename: string) { 26 | const event = await importFileFromFilename({ 27 | filename, 28 | constructorOptions: [this.bot], 29 | }); 30 | 31 | this.bot.on(event.name, event.execute.bind(null, this.bot)); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/events/client/ready.ts: -------------------------------------------------------------------------------- 1 | import * as DJS from "discord.js"; 2 | import { InteractionHandler } from "../../handlers/InteractionHandler.js"; 3 | import type { Bot } from "../../structures/Bot.js"; 4 | import { Event } from "../../structures/Event.js"; 5 | 6 | export default class ReadyEvent extends Event { 7 | constructor(bot: Bot) { 8 | super({ bot, name: DJS.Events.ClientReady }); 9 | } 10 | 11 | async execute(bot: Bot) { 12 | const userCount = bot.guilds.cache.reduce((a, g) => a + g.memberCount, 0); 13 | const serverCount = bot.guilds.cache.size; 14 | 15 | console.info(`Bot is running ${userCount} users and ${serverCount} servers`); 16 | 17 | new InteractionHandler(bot).loadInteractions(); 18 | 19 | // change statuses every 60 seconds (Min is 15s) 20 | const statuses = [`${serverCount} servers.`, `${userCount} users`]; 21 | 22 | setInterval(() => { 23 | const status = statuses[Math.floor(Math.random() * statuses.length)]; 24 | bot.user?.setActivity(status!, { type: DJS.ActivityType.Watching }); 25 | }, 60000); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License Copyright (c) 2021 Dev-CasperTheGhost 2 | 3 | Permission is hereby granted, 4 | free of charge, to any person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, copy, modify, merge, 7 | publish, distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to the 9 | following conditions: 10 | 11 | The above copyright notice and this permission notice 12 | (including the next paragraph) shall be included in all copies or substantial 13 | portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO 18 | EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR 19 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "discord-bot-template", 3 | "version": "0.0.0", 4 | "type": "module", 5 | "license": "MIT", 6 | "scripts": { 7 | "dev": "nodemon", 8 | "build": "npx prisma generate && npx swc ./src -d dist", 9 | "start": "NODE_ENV=production node dist/index.js", 10 | "format": "prettier --write \"**/*.{js,jsx,ts,tsx,json}\" --ignore-path .gitignore", 11 | "lint": "eslint . --ext .js,.jsx,.ts,.tsx", 12 | "test-tsc": "tsc --noEmit", 13 | "postinstall": "npx prisma generate" 14 | }, 15 | "dependencies": { 16 | "@discordjs/builders": "^1.2.0", 17 | "@prisma/client": "^4.7.0", 18 | "discord.js": "^14.5.0", 19 | "globby": "^13.1.2", 20 | "pretty-ms": "^8.0.0", 21 | "undici": "^5.10.0" 22 | }, 23 | "devDependencies": { 24 | "@casper124578/eslint-config": "^5.0.1", 25 | "@swc/cli": "^0.1.57", 26 | "@swc/core": "^1.3.4", 27 | "@swc/helpers": "^0.4.14", 28 | "@types/glob": "^8.0.0", 29 | "@types/node": "^18.7.23", 30 | "dotenv": "^16.0.3", 31 | "eslint": "^8.28.0", 32 | "nodemon": "^2.0.20", 33 | "prettier": "^2.7.1", 34 | "prisma": "^4.7.1", 35 | "regenerator-runtime": "^0.13.11", 36 | "ts-node": "^10.9.1", 37 | "typescript": "^4.8.4" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/commands/economy/weekly.ts: -------------------------------------------------------------------------------- 1 | import { Command, type CommandContext } from "../../structures/Command.js"; 2 | import type { Bot } from "../../structures/Bot.js"; 3 | import { time } from "@discordjs/builders"; 4 | 5 | export default class WeeklyCommand extends Command { 6 | private SEVEN_DAYS_TIMEOUT_MS = 60 * 60 * 24 * 7 * 1000; 7 | private WEEKLY_CASH_AMOUNT = 1500; 8 | 9 | constructor(bot: Bot) { 10 | super(bot, { 11 | name: "weekly", 12 | description: "Collect your weekly reward (1500 cash)", 13 | }); 14 | } 15 | 16 | async execute({ interaction }: CommandContext) { 17 | const discordUser = await this.bot.prismaUtils.upsertDiscordGuildMember({ interaction }); 18 | 19 | const lastWeeklyUsed = discordUser.lastWeeklyUsed?.getTime() ?? 0; 20 | const hasWeeklyExpired = 21 | discordUser.lastWeeklyUsed !== null && 22 | this.SEVEN_DAYS_TIMEOUT_MS - (Date.now() - lastWeeklyUsed) > 0; 23 | 24 | if (!hasWeeklyExpired) { 25 | await this.bot.prisma.discordGuildMember.update({ 26 | where: { id: discordUser.id }, 27 | data: { lastWeeklyUsed: new Date(), cash: { increment: this.WEEKLY_CASH_AMOUNT } }, 28 | }); 29 | 30 | const timeLeft = new Date(Date.now() + this.SEVEN_DAYS_TIMEOUT_MS); 31 | interaction.reply({ 32 | content: `Successfully collected ${ 33 | this.WEEKLY_CASH_AMOUNT 34 | } cash. Next reward can be collected ${time(timeLeft, "R")}`, 35 | }); 36 | 37 | return; 38 | } 39 | 40 | const timeLeft = new Date( 41 | Date.now() + this.SEVEN_DAYS_TIMEOUT_MS - (Date.now() - lastWeeklyUsed), 42 | ); 43 | 44 | await interaction.reply({ 45 | content: `Please try again ${time(timeLeft, "R")}`, 46 | }); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/commands/economy/daily.ts: -------------------------------------------------------------------------------- 1 | import { Command, type CommandContext } from "../../structures/Command.js"; 2 | import type { Bot } from "../../structures/Bot.js"; 3 | import { time } from "@discordjs/builders"; 4 | 5 | export default class DailyCommand extends Command { 6 | private TWENTY_FOUR_HOUR_TIMEOUT_MS = 60 * 60 * 24 * 1000; 7 | private DAILY_CASH_AMOUNT = 500; 8 | 9 | constructor(bot: Bot) { 10 | super(bot, { 11 | name: "daily", 12 | description: "Collect your daily reward (500 cash)", 13 | }); 14 | } 15 | 16 | async execute({ interaction }: CommandContext) { 17 | const discordUser = await this.bot.prismaUtils.upsertDiscordGuildMember({ interaction }); 18 | 19 | const lastDailyUsed = discordUser.lastDailyUsed?.getTime() ?? 0; 20 | const hasDailyExpired = 21 | discordUser.lastDailyUsed !== null && 22 | this.TWENTY_FOUR_HOUR_TIMEOUT_MS - (Date.now() - lastDailyUsed) > 0; 23 | 24 | if (!hasDailyExpired) { 25 | await this.bot.prisma.discordGuildMember.update({ 26 | where: { id: discordUser.id }, 27 | data: { lastDailyUsed: new Date(), cash: { increment: this.DAILY_CASH_AMOUNT } }, 28 | }); 29 | 30 | const timeLeft = new Date(Date.now() + this.TWENTY_FOUR_HOUR_TIMEOUT_MS); 31 | interaction.reply({ 32 | content: `Successfully collected ${ 33 | this.DAILY_CASH_AMOUNT 34 | } cash. Next reward can be collected ${time(timeLeft, "R")}`, 35 | }); 36 | 37 | return; 38 | } 39 | 40 | const timeLeft = new Date( 41 | Date.now() + this.TWENTY_FOUR_HOUR_TIMEOUT_MS - (Date.now() - lastDailyUsed), 42 | ); 43 | 44 | await interaction.reply({ 45 | content: `Please try again ${time(timeLeft, "R")}`, 46 | }); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/events/interactions/interactionCreate.ts: -------------------------------------------------------------------------------- 1 | import * as DJS from "discord.js"; 2 | import type { Bot } from "../../structures/Bot.js"; 3 | import { Event } from "../../structures/Event.js"; 4 | 5 | export default class InteractionEvent extends Event { 6 | constructor(bot: Bot) { 7 | super({ bot, name: DJS.Events.InteractionCreate }); 8 | } 9 | 10 | private isNsfwChannel(interaction: DJS.CommandInteraction<"cached">) { 11 | return interaction.channel instanceof DJS.TextChannel && !interaction.channel.nsfw; 12 | } 13 | 14 | private isOwner(interaction: DJS.CommandInteraction<"cached">) { 15 | const owners = process.env["OWNERS"]; 16 | return owners?.includes(interaction.user.id); 17 | } 18 | 19 | async execute(bot: Bot, interaction: DJS.Interaction<"cached">) { 20 | if (!interaction.isCommand()) return; 21 | 22 | const command = bot.commands.get(interaction.commandName); 23 | if (!command) return; 24 | 25 | if (command.options.ownerOnly && !this.isOwner(interaction)) { 26 | await interaction.reply({ content: "This command is owner only", ephemeral: true }); 27 | 28 | return; 29 | } 30 | 31 | if (command.options.nsfwOnly && this.isNsfwChannel(interaction)) { 32 | await interaction.reply({ 33 | content: "Command can only be used in a NSFW channel!", 34 | ephemeral: true, 35 | }); 36 | 37 | return; 38 | } 39 | 40 | try { 41 | await command.execute({ interaction }); 42 | } catch (err) { 43 | console.error(err); 44 | if (interaction.replied) return; 45 | 46 | if (interaction.deferred) { 47 | interaction.editReply({ content: "An error occurred! Please try again later." }); 48 | } else { 49 | interaction.reply({ 50 | ephemeral: true, 51 | content: "An error occurred! Please try again later.", 52 | }); 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/handlers/InteractionHandler.ts: -------------------------------------------------------------------------------- 1 | import { globby } from "globby"; 2 | import type { Bot } from "../structures/Bot.js"; 3 | import type { Command } from "../structures/Command.js"; 4 | import type * as DJS from "discord.js"; 5 | import { importFileFromFilename } from "../lib/utils.js"; 6 | import ms from "pretty-ms"; 7 | 8 | // warning: This can only be initialized in the ready event! 9 | export class InteractionHandler { 10 | bot: Bot; 11 | 12 | constructor(bot: Bot) { 13 | this.bot = bot; 14 | } 15 | 16 | async loadInteractions() { 17 | try { 18 | const path = 19 | process.env["NODE_ENV"] === "production" 20 | ? "./dist/commands/**/*.js" 21 | : "./src/commands/**/*.ts"; 22 | 23 | const files = await globby(path); 24 | const loadInteractionsStart = Date.now(); 25 | 26 | await Promise.all(files.map(async (filename) => this.loadInteraction(filename))); 27 | 28 | const interactionLoadTime = Date.now() - loadInteractionsStart; 29 | console.log(`Interactions loaded: ${ms(interactionLoadTime)}`); 30 | } catch (e) { 31 | console.log(e); 32 | } 33 | } 34 | 35 | private async loadInteraction(filename: string) { 36 | const interaction = await importFileFromFilename({ 37 | filename, 38 | constructorOptions: [this.bot], 39 | }); 40 | 41 | this.bot.commands.set(interaction.name, interaction); 42 | 43 | const data: DJS.ApplicationCommandData = { 44 | name: interaction.name, 45 | description: interaction.options.description ?? "Empty description", 46 | options: interaction.options.options ?? [], 47 | }; 48 | 49 | if (process.env["DEV_GUILD_ID"]) { 50 | const guild = await this.bot.guilds.fetch(process.env["DEV_GUILD_ID"]); 51 | await guild.commands.create(data); 52 | } else { 53 | /** 54 | * note: commands might only show up after 30-60 minutes. 55 | */ 56 | await this.bot.application?.commands.create(data); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Basic Options */ 6 | "target": "esnext" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', 'ES2021', or 'ESNEXT'. */, 7 | "module": "esnext" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */, 8 | "removeComments": true /* Do not emit comments to output. */, 9 | "downlevelIteration": true /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */, 10 | "isolatedModules": true /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */, 11 | "importsNotUsedAsValues": "error", 12 | "importHelpers": true, 13 | "outDir": "dist", 14 | "sourceMap": true, 15 | "lib": ["esnext"], 16 | 17 | /* Strict Type-Checking Options */ 18 | "strict": true /* Enable all strict type-checking options. */, 19 | "strictNullChecks": true /* Enable strict null checks. */, 20 | "strictFunctionTypes": true /* Enable strict checking of function types. */, 21 | "strictBindCallApply": true /* Enable strict 'bind', 'call', and 'apply' methods on functions. */, 22 | "strictPropertyInitialization": true /* Enable strict checking of property initialization in classes. */, 23 | "noImplicitThis": true /* Raise error on 'this' expressions with an implied 'any' type. */, 24 | "alwaysStrict": true /* Parse in strict mode and emit "use strict" for each source file. */, 25 | 26 | /* Additional Checks */ 27 | "noUnusedLocals": true /* Report errors on unused locals. */, 28 | "noUnusedParameters": true /* Report errors on unused parameters. */, 29 | "noImplicitReturns": true /* Report error when not all code paths in function return a value. */, 30 | "noFallthroughCasesInSwitch": true /* Report errors for fallthrough cases in switch statement. */, 31 | "noUncheckedIndexedAccess": true /* Include 'undefined' in index signature results */, 32 | "noPropertyAccessFromIndexSignature": true /* Require undeclared properties from index signatures to use element accesses. */, 33 | 34 | /* Module Resolution Options */ 35 | "moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */, 36 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, 37 | 38 | /* Advanced Options */ 39 | "skipLibCheck": true /* Skip type checking of declaration files. */, 40 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 41 | }, 42 | "ts-node": { 43 | "swc": true 44 | } 45 | } 46 | --------------------------------------------------------------------------------