initial commit

This commit is contained in:
Shane C 2024-07-02 10:20:16 -04:00
commit 29c6cb9c4a
Signed by: shanec
GPG key ID: E46B5FEA35B22FF9
28 changed files with 2277 additions and 0 deletions

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
node_modules
# Keep environment variables out of version control
.env

10
README.md Normal file
View file

@ -0,0 +1,10 @@
# Discord.JS v14 Typescript Template
This is a Discord.JS v14 bot template using TypeScript, this version includes the Prisma ORM.
## Features
- Command Handler
- Event Handler
- Interaction Handler
- Contexts
- Bun supported

BIN
bun.lockb Executable file

Binary file not shown.

37
commands/misc/ping.ts Normal file
View file

@ -0,0 +1,37 @@
import { ChatInputCommandInteraction, EmbedBuilder, SlashCommandBuilder } from 'discord.js';
import { Command } from '../../types.js';
export default class PingCommand extends Command {
constructor() {
super(
new SlashCommandBuilder()
.setName('ping')
.setDescription('Current latency of bot.')
.setDMPermission(false),
);
}
async execute(interaction: ChatInputCommandInteraction) {
const pingEmbed = new EmbedBuilder()
.addFields(
{
name: 'REST Latency:',
value: `${Math.round(Date.now() - interaction.createdTimestamp)}ms`,
inline: true,
},
{
name: 'WS Latency:',
value: `${Math.round(this.client.ws.ping)}ms`,
inline: true,
},
)
.setTimestamp()
.setColor('Green');
await interaction.reply({ embeds: [ pingEmbed ] });
}
}

1
config.example.yml Normal file
View file

@ -0,0 +1 @@
token: <BOT_TOKEN>

14
discordjs.d.ts vendored Normal file
View file

@ -0,0 +1,14 @@
import type { Command, Interaction as InteractionHandler } from './types.ts';
import type { PrismaClient } from '@prisma/client';
declare module 'discord.js' {
interface Client {
commands: Map<string, Command>;
interactionHandlers: Map<string, InteractionHandler>;
commandDir: string;
db: PrismaClient;
contexts: Map<string, Map>;
getContext<K, T>(contextName: string): Map<K, T>;
}
}

25
dist/commands/misc/ping.js vendored Normal file
View file

@ -0,0 +1,25 @@
import { EmbedBuilder, SlashCommandBuilder } from 'discord.js';
import { Command } from '../../types.js';
export default class PingCommand extends Command {
constructor() {
super(new SlashCommandBuilder()
.setName('ping')
.setDescription('Current latency of bot.')
.setDMPermission(false));
}
async execute(interaction) {
const pingEmbed = new EmbedBuilder()
.addFields({
name: 'REST Latency:',
value: `${Math.round(Date.now() - interaction.createdTimestamp)}ms`,
inline: true,
}, {
name: 'WS Latency:',
value: `${Math.round(this.client.ws.ping)}ms`,
inline: true,
})
.setTimestamp()
.setColor('Green');
await interaction.reply({ embeds: [pingEmbed] });
}
}

23
dist/events/interactionCreate.js vendored Normal file
View file

@ -0,0 +1,23 @@
import { Event } from '../types.js';
import { Events } from 'discord.js';
export default class InteractionCreateEvent extends Event {
constructor() {
super(Events.InteractionCreate, false);
}
async execute(interaction) {
if (interaction.isMessageComponent() || interaction.isModalSubmit()) {
const interactionHandler = this.client.interactionHandlers.get(interaction.customId);
if (!interactionHandler)
return;
interactionHandler.client = this.client;
await interactionHandler.execute(interaction);
}
if (!interaction.isChatInputCommand())
return;
const command = this.client.commands?.get(interaction.commandName);
if (!command)
return;
command.client = this.client;
await command.execute(interaction);
}
}

18
dist/events/ready.js vendored Normal file
View file

@ -0,0 +1,18 @@
import { ActivityType, Events } from 'discord.js';
import { Event } from '../types.js';
import { loadCommands } from '../handlers/loaders/command.js';
export default class ReadyEvent extends Event {
constructor() {
super(Events.ClientReady, true);
}
async execute(_client) {
this.client.user?.setActivity({
type: ActivityType.Custom,
state: 'Creating bugs...',
name: 'hello',
});
this.client.user?.setStatus('dnd');
this.client.commands = await loadCommands(this.client);
console.log(`Logged in as ${this.client.user?.tag}`);
}
}

42
dist/handlers/loaders/command.js vendored Normal file
View file

@ -0,0 +1,42 @@
import { readdirSync, statSync } from 'node:fs';
import path from 'node:path';
import { Routes } from 'discord.js';
export async function loadCommands(client) {
const currentAppCommands = await client.rest.get(Routes.applicationCommands(client.application.id));
const commandMap = new Map();
const baseCmdDir = readdirSync(client.commandDir);
for (const baseCmdDirFile of baseCmdDir) {
const combinedDirs = path.join(client.commandDir, baseCmdDirFile);
const fileInfo = statSync(combinedDirs);
if (fileInfo.isDirectory()) {
const categoryDir = readdirSync(combinedDirs).filter(file => process.versions.bun ? file.endsWith('.ts') : file.endsWith('.js'));
for (const cmdFile of categoryDir) {
const combinedCmdDir = path.join(combinedDirs, cmdFile);
try {
const commandClass = await import(combinedCmdDir);
const command = new commandClass.default();
command.filePath = combinedCmdDir;
const commandDataJSON = command.commandData.toJSON();
for (const currentAppCommand of currentAppCommands) {
if (currentAppCommand.name === commandDataJSON.name) {
await client.rest.patch(Routes.applicationCommand(client.application.id, currentAppCommand.id), { body: commandDataJSON });
break;
}
}
commandMap.set(command.commandData.name, command);
await client.rest.post(Routes.applicationCommands(client.application.id), { body: commandDataJSON });
}
catch (e) {
console.log('Invalid command');
console.error(`error ${e}`);
}
}
}
}
for (const currentAppCommand of currentAppCommands) {
if (!commandMap.has(currentAppCommand.name)) {
await client.rest.delete(Routes.applicationCommand(client.application.id, currentAppCommand.id));
}
}
return commandMap;
}

31
dist/handlers/loaders/event.js vendored Normal file
View file

@ -0,0 +1,31 @@
import { readdirSync } from 'node:fs';
import path from 'node:path';
export async function loadEvents(client, eventDir) {
const eventMap = new Map();
const baseEvtDir = readdirSync(eventDir);
for (const evtFile of baseEvtDir) {
const combinedEvtDir = path.join(eventDir, evtFile);
try {
const eventClass = await import(combinedEvtDir);
const event = new eventClass.default();
event.filePath = combinedEvtDir;
eventMap.set(event.eventName, event);
if (event.once) {
client.once(event.eventName, (...e) => {
event.client = client;
event.execute(...e);
});
}
else {
client.on(event.eventName, (...e) => {
event.client = client;
event.execute(...e);
});
}
}
catch (e) {
console.log('Invalid event');
console.error(`error ${e}`);
}
}
}

31
dist/handlers/loaders/interactions.js vendored Normal file
View file

@ -0,0 +1,31 @@
import { readdirSync } from 'fs';
import path from 'node:path';
import { statSync } from 'node:fs';
export async function loadInteractions(client, interDir) {
const interMap = new Map();
const baseInterDir = readdirSync(interDir);
for (const file of baseInterDir) {
const combinedDirs = path.join(interDir, file);
const fileInfo = statSync(combinedDirs);
if (fileInfo.isDirectory()) {
const categoryDir = readdirSync(combinedDirs).filter(catFile => process.versions.bun ? catFile.endsWith('.ts') : catFile.endsWith('.js'));
for (const interFile of categoryDir) {
const combinedInterDir = path.join(combinedDirs, interFile);
try {
const interClass = await import(combinedInterDir);
const interaction = new interClass.default();
interaction.filePath = combinedInterDir;
interaction.client = client;
for (const id of interaction.id) {
interMap.set(id, interaction);
}
}
catch (e) {
console.log('Invalid interaction');
console.error(`error ${e}`);
}
}
}
}
return interMap;
}

85
dist/index.js vendored Normal file
View file

@ -0,0 +1,85 @@
/* eslint-disable no-inline-comments */
import { Client, GatewayIntentBits } from 'discord.js';
import { program } from 'commander';
import { parse } from 'yaml';
import path from 'node:path';
import { PrismaClient } from '@prisma/client';
import { loadEvents } from './handlers/loaders/event.js';
import { loadInteractions } from './handlers/loaders/interactions.js';
import { getContext } from './utils/context.js';
program
.option('-c, --config <string>', 'Path to config file (e.g. /opt/config.yml)');
program.parse(process.argv);
let configPath = program.opts().config;
if (!configPath) {
configPath = 'config.yml';
}
let config;
if (process.versions.bun) { // Try to use bun native things if possible.
const configFile = Bun.file(configPath);
config = parse(await configFile.text());
}
else {
const { readFileSync } = await import('node:fs');
const configFile = readFileSync(configPath, 'utf8');
config = parse(configFile);
}
if (!config.token) {
throw Error('Please provide a token!'); // throw seems to be the only way to exit in this situation, possibly a function would be better.
}
const client = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.AutoModerationConfiguration,
GatewayIntentBits.AutoModerationExecution,
GatewayIntentBits.DirectMessageReactions,
GatewayIntentBits.DirectMessageTyping,
GatewayIntentBits.DirectMessages,
GatewayIntentBits.GuildEmojisAndStickers,
GatewayIntentBits.GuildIntegrations,
GatewayIntentBits.GuildInvites,
GatewayIntentBits.GuildMembers,
GatewayIntentBits.GuildMessageReactions,
GatewayIntentBits.GuildMessageTyping,
GatewayIntentBits.GuildModeration,
GatewayIntentBits.GuildPresences, // PRIVILEGED
GatewayIntentBits.GuildWebhooks,
GatewayIntentBits.MessageContent, // PRIVILEGED
],
});
client.contexts = new Map();
client.getContext = getContext;
client.db = new PrismaClient();
await client.db.$connect();
process.on('exit', async () => {
await client.db.$disconnect();
await client.destroy();
process.reallyExit(0);
});
let eventDir;
if (process.versions.bun) {
eventDir = path.join(path.dirname(Bun.main), 'events');
}
else {
const { fileURLToPath } = await import('node:url');
eventDir = path.join(path.dirname(fileURLToPath(import.meta.url)), 'events');
}
if (process.versions.bun) {
client.commandDir = path.join(path.dirname(Bun.main), 'commands');
}
else {
const { fileURLToPath } = await import('node:url');
client.commandDir = path.join(path.dirname(fileURLToPath(import.meta.url)), 'commands');
}
let interDir;
if (process.versions.bun) {
interDir = path.join(path.dirname(Bun.main), 'interactions');
}
else {
const { fileURLToPath } = await import('node:url');
interDir = path.join(path.dirname(fileURLToPath(import.meta.url)), 'interactions');
}
client.interactionHandlers = await loadInteractions(client, interDir);
await loadEvents(client, eventDir);
await client.login(config.token);

28
dist/types.js vendored Normal file
View file

@ -0,0 +1,28 @@
// Unpopular opinion, I like using classes for commands.
export class Command {
id;
client;
commandData;
filePath;
constructor(appCommandData) {
this.commandData = appCommandData;
}
}
export class Event {
eventName;
filePath;
client;
once;
constructor(eventName, once = false) {
this.eventName = eventName;
this.once = once;
}
}
export class Interaction {
id;
filePath;
client;
constructor(...interactionID) {
this.id = interactionID;
}
}

8
dist/utils/context.js vendored Normal file
View file

@ -0,0 +1,8 @@
export function getContext(contextName) {
// @ts-expect-error Some TypeScript issues.
const client = this;
if (!client.contexts.has(contextName)) {
client.contexts.set(contextName, new Map());
}
return client.contexts.get(contextName);
}

106
eslint.config.mjs Normal file
View file

@ -0,0 +1,106 @@
import typescriptEslint from '@typescript-eslint/eslint-plugin';
import globals from 'globals';
import tsParser from '@typescript-eslint/parser';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import js from '@eslint/js';
import { FlatCompat } from '@eslint/eslintrc';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended,
allConfig: js.configs.all,
});
export default [{
ignores: ['**/dist/'],
}, ...compat.extends('eslint:recommended', 'plugin:@typescript-eslint/recommended'), {
plugins: {
'@typescript-eslint': typescriptEslint,
},
languageOptions: {
globals: {
...globals.node,
},
parser: tsParser,
ecmaVersion: 2022,
sourceType: 'module',
},
rules: {
'arrow-spacing': ['warn', {
before: true,
after: true,
}],
'brace-style': ['error', 'stroustrup', {
allowSingleLine: true,
}],
'comma-dangle': ['error', 'always-multiline'],
'comma-spacing': 'error',
'comma-style': 'error',
curly: ['error', 'multi-line', 'consistent'],
'dot-location': ['error', 'property'],
'handle-callback-err': 'off',
indent: ['error', 'tab'],
'keyword-spacing': 'error',
'max-nested-callbacks': ['error', {
max: 4,
}],
'max-statements-per-line': ['error', {
max: 2,
}],
'no-console': 'off',
'no-empty-function': 'error',
'no-floating-decimal': 'error',
'no-inline-comments': 'error',
'no-lonely-if': 'error',
'no-multi-spaces': 'error',
'no-multiple-empty-lines': ['error', {
max: 2,
maxEOF: 1,
maxBOF: 0,
}],
'no-shadow': ['error', {
allow: ['err', 'resolve', 'reject'],
}],
'no-trailing-spaces': ['error'],
'no-var': 'error',
'@typescript-eslint/no-unused-vars': ['error', {
argsIgnorePattern: '^_',
destructuredArrayIgnorePattern: '^_',
varsIgnorePattern: '^_',
}],
'no-unused-vars': 'off',
'object-curly-spacing': ['error', 'always'],
'prefer-const': 'error',
quotes: ['error', 'single'],
semi: ['error', 'always'],
'space-before-blocks': 'error',
'space-before-function-paren': ['error', {
anonymous: 'never',
named: 'never',
asyncArrow: 'always',
}],
'space-in-parens': 'error',
'space-infix-ops': 'error',
'space-unary-ops': 'error',
'spaced-comment': 'error',
yoda: 'error',
},
}];

View file

@ -0,0 +1,33 @@
import { Event } from '../types.js';
import { Events, Interaction } from 'discord.js';
export default class InteractionCreateEvent extends Event {
constructor() {
super(Events.InteractionCreate, false);
}
async execute(interaction: Interaction) {
if (interaction.isMessageComponent() || interaction.isModalSubmit()) {
const interactionHandler = this.client.interactionHandlers.get(interaction.customId);
if (!interactionHandler) return;
interactionHandler.client = this.client;
await interactionHandler.execute(interaction);
}
if (!interaction.isChatInputCommand()) return;
const command = this.client.commands?.get(interaction.commandName);
if (!command) return;
command.client = this.client;
await command.execute(interaction);
}
}

27
events/ready.ts Normal file
View file

@ -0,0 +1,27 @@
import { ActivityType, Client, Events } from 'discord.js';
import { Event } from '../types.js';
import { loadCommands } from '../handlers/loaders/command.js';
export default class ReadyEvent extends Event {
constructor() {
super(Events.ClientReady, true);
}
async execute(_client: Client) {
this.client.user?.setActivity({
type: ActivityType.Custom,
state: 'Creating bugs...',
name: 'hello',
});
this.client.user?.setStatus('dnd');
this.client.commands = await loadCommands(this.client);
console.log(`Logged in as ${this.client.user?.tag}`);
}
}

View file

@ -0,0 +1,70 @@
import { Command } from '../../types.js';
import { readdirSync, statSync } from 'node:fs';
import path from 'node:path';
import { Client, RESTGetAPIApplicationCommandsResult, Routes } from 'discord.js';
export async function loadCommands(client: Client): Promise<Map<string, Command>> {
const currentAppCommands : RESTGetAPIApplicationCommandsResult = await client.rest.get(Routes.applicationCommands(client.application!.id)) as RESTGetAPIApplicationCommandsResult;
const commandMap: Map<string, Command> = new Map();
const baseCmdDir = readdirSync(client.commandDir);
for (const baseCmdDirFile of baseCmdDir) {
const combinedDirs = path.join(client.commandDir, baseCmdDirFile);
const fileInfo = statSync(combinedDirs);
if (fileInfo.isDirectory()) {
const categoryDir = readdirSync(combinedDirs).filter(file => process.versions.bun ? file.endsWith('.ts') : file.endsWith('.js'));
for (const cmdFile of categoryDir) {
const combinedCmdDir = path.join(combinedDirs, cmdFile);
try {
const commandClass = await import(combinedCmdDir);
const command : Command = new commandClass.default();
command.filePath = combinedCmdDir;
const commandDataJSON = command.commandData.toJSON();
for (const currentAppCommand of currentAppCommands) {
if (currentAppCommand.name === commandDataJSON.name) {
await client.rest.patch(Routes.applicationCommand(client.application!.id, currentAppCommand.id), { body: commandDataJSON });
break;
}
}
commandMap.set(command.commandData.name, command);
await client.rest.post(Routes.applicationCommands(client.application!.id), { body: commandDataJSON });
}
catch (e) {
console.log('Invalid command');
console.error(`error ${e}`);
}
}
}
}
for (const currentAppCommand of currentAppCommands) {
if (!commandMap.has(currentAppCommand.name)) {
await client.rest.delete(Routes.applicationCommand(client.application!.id, currentAppCommand.id));
}
}
return commandMap;
}

46
handlers/loaders/event.ts Normal file
View file

@ -0,0 +1,46 @@
import { Client } from 'discord.js';
import { Event } from '../../types.js';
import { readdirSync } from 'node:fs';
import path from 'node:path';
export async function loadEvents(client: Client, eventDir: string): Promise<void> {
const eventMap: Map<string, Event> = new Map();
const baseEvtDir = readdirSync(eventDir);
for (const evtFile of baseEvtDir) {
const combinedEvtDir = path.join(eventDir, evtFile);
try {
const eventClass = await import(combinedEvtDir);
const event : Event = new eventClass.default();
event.filePath = combinedEvtDir;
eventMap.set(event.eventName, event);
if (event.once) {
client.once(event.eventName, (...e) => {
event.client = client;
event.execute(...e);
});
}
else {
client.on(event.eventName, (...e) => {
event.client = client;
event.execute(...e);
});
}
}
catch (e) {
console.log('Invalid event');
console.error(`error ${e}`);
}
}
}

View file

@ -0,0 +1,53 @@
import { Interaction } from '../../types.js';
import { readdirSync } from 'fs';
import path from 'node:path';
import { statSync } from 'node:fs';
import { Client } from 'discord.js';
export async function loadInteractions(client: Client, interDir: string): Promise<Map<string, Interaction>> {
const interMap: Map<string, Interaction> = new Map();
const baseInterDir = readdirSync(interDir);
for (const file of baseInterDir) {
const combinedDirs = path.join(interDir, file);
const fileInfo = statSync(combinedDirs);
if (fileInfo.isDirectory()) {
const categoryDir = readdirSync(combinedDirs).filter(catFile => process.versions.bun ? catFile.endsWith('.ts') : catFile.endsWith('.js'));
for (const interFile of categoryDir) {
const combinedInterDir = path.join(combinedDirs, interFile);
try {
const interClass = await import(combinedInterDir);
const interaction: Interaction = new interClass.default();
interaction.filePath = combinedInterDir;
interaction.client = client;
for (const id of interaction.id) {
interMap.set(id, interaction);
}
}
catch (e) {
console.log('Invalid interaction');
console.error(`error ${e}`);
}
}
}
}
return interMap;
}

124
index.ts Normal file
View file

@ -0,0 +1,124 @@
/* eslint-disable no-inline-comments */
import { Client, GatewayIntentBits } from 'discord.js';
import { Config } from './types.js';
import { program } from 'commander';
import { parse } from 'yaml';
import path from 'node:path';
import { PrismaClient } from '@prisma/client';
import { loadEvents } from './handlers/loaders/event.js';
import { loadInteractions } from './handlers/loaders/interactions.js';
import { getContext } from './utils/context.js';
program
.option('-c, --config <string>', 'Path to config file (e.g. /opt/config.yml)');
program.parse(process.argv);
let configPath : string | undefined = program.opts().config;
if (!configPath) {
configPath = 'config.yml';
}
let config : Config;
if (process.versions.bun) { // Try to use bun native things if possible.
const configFile = Bun.file(configPath);
config = parse(await configFile.text());
}
else {
const { readFileSync } = await import('node:fs');
const configFile = readFileSync(configPath, 'utf8');
config = parse(configFile);
}
if (!config.token) {
throw Error('Please provide a token!'); // throw seems to be the only way to exit in this situation, possibly a function would be better.
}
const client = new Client({ // We probably don't need all of these intents. But for now this is okay.
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.AutoModerationConfiguration,
GatewayIntentBits.AutoModerationExecution,
GatewayIntentBits.DirectMessageReactions,
GatewayIntentBits.DirectMessageTyping,
GatewayIntentBits.DirectMessages,
GatewayIntentBits.GuildEmojisAndStickers,
GatewayIntentBits.GuildIntegrations,
GatewayIntentBits.GuildInvites,
GatewayIntentBits.GuildMembers,
GatewayIntentBits.GuildMessageReactions,
GatewayIntentBits.GuildMessageTyping,
GatewayIntentBits.GuildModeration,
GatewayIntentBits.GuildPresences, // PRIVILEGED
GatewayIntentBits.GuildWebhooks,
GatewayIntentBits.MessageContent, // PRIVILEGED
],
});
client.contexts = new Map();
client.getContext = getContext;
client.db = new PrismaClient();
await client.db.$connect();
process.on('exit', async () => {
await client.db.$disconnect();
await client.destroy();
process.reallyExit(0);
});
let eventDir : string;
if (process.versions.bun) {
eventDir = path.join(path.dirname(Bun.main), 'events');
}
else {
const { fileURLToPath } = await import('node:url');
eventDir = path.join(path.dirname(fileURLToPath(import.meta.url)), 'events');
}
if (process.versions.bun) {
client.commandDir = path.join(path.dirname(Bun.main), 'commands');
}
else {
const { fileURLToPath } = await import('node:url');
client.commandDir = path.join(path.dirname(fileURLToPath(import.meta.url)), 'commands');
}
let interDir : string;
if (process.versions.bun) {
interDir = path.join(path.dirname(Bun.main), 'interactions');
}
else {
const { fileURLToPath } = await import('node:url');
interDir = path.join(path.dirname(fileURLToPath(import.meta.url)), 'interactions');
}
client.interactionHandlers = await loadInteractions(client, interDir);
await loadEvents(client, eventDir);
await client.login(config.token);

30
package.json Normal file
View file

@ -0,0 +1,30 @@
{
"name": "ts-core",
"version": "1.2.4",
"main": "dist/index.ts",
"type": "module",
"scripts": {
"start": "tsc && node dist/index.js",
"lint": "eslint .",
"lint:fix": "eslint . --fix"
},
"dependencies": {
"@prisma/client": "5.16.1",
"commander": "^12.1.0",
"discord.js": "^14.15.3",
"prisma": "^5.16.1",
"yaml": "^2.4.5"
},
"devDependencies": {
"@eslint/eslintrc": "^3.1.0",
"@eslint/js": "^9.6.0",
"@types/node": "^20.14.9",
"@typescript-eslint/eslint-plugin": "^7.15.0",
"@typescript-eslint/parser": "^7.15.0",
"bun-types": "^1.1.17",
"eslint": "^9.6.0",
"globals": "^15.8.0",
"typescript": "^5.5.3"
},
"packageManager": "pnpm@9.4.0+sha512.f549b8a52c9d2b8536762f99c0722205efc5af913e77835dbccc3b0b0b2ca9e7dc8022b78062c17291c48e88749c70ce88eb5a74f1fa8c4bf5e18bb46c8bd83a"
}

1308
pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load diff

14
prisma/schema.prisma Normal file
View file

@ -0,0 +1,14 @@
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?
// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}

37
tsconfig.json Normal file
View file

@ -0,0 +1,37 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "NodeNext",
"types": [
"bun-types"
],
"outDir": "./dist",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictBindCallApply": true,
"strictPropertyInitialization": true,
"noImplicitThis": true,
"useUnknownInCatchVariables": true,
"alwaysStrict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"skipLibCheck": true
},
"include": [
"./**/*.ts",
"discordjs.d.ts"
],
"exclude": [
"node_modules/**/*"
]
}

59
types.ts Normal file
View file

@ -0,0 +1,59 @@
import {
ChatInputCommandInteraction,
Client,
ClientEvents,
MessageComponentInteraction, ModalSubmitInteraction,
SlashCommandBuilder,
Interaction as DiscordInteraction,
} from 'discord.js';
export interface Config {
token: string,
}
// Unpopular opinion, I like using classes for commands.
export abstract class Command {
public id!: string;
public client!: Client;
public commandData: SlashCommandBuilder | Omit<SlashCommandBuilder, 'addSubcommand' | 'addSubcommandGroup'>;
public filePath: string | undefined;
protected constructor(appCommandData: SlashCommandBuilder | Omit<SlashCommandBuilder, 'addSubcommand' | 'addSubcommandGroup'>) {
this.commandData = appCommandData;
}
abstract execute(interaction: ChatInputCommandInteraction): Promise<void>;
}
export abstract class Event {
public eventName: keyof ClientEvents;
public filePath: string | undefined;
public client!: Client;
public once: boolean;
protected constructor(eventName: keyof ClientEvents, once: boolean = false) {
this.eventName = eventName;
this.once = once;
}
abstract execute(...args: unknown[]): Promise<void>;
}
export abstract class Interaction {
public id: string[];
public filePath: string | undefined;
public client!: Client;
protected constructor(...interactionID: string[]) {
this.id = interactionID;
}
abstract beforeExecute?(interaction: DiscordInteraction | MessageComponentInteraction): Promise<void>;
abstract execute(interaction: MessageComponentInteraction | ModalSubmitInteraction): Promise<void>;
}

14
utils/context.ts Normal file
View file

@ -0,0 +1,14 @@
import { Client } from 'discord.js';
export function getContext<K, T>(contextName: string): Map<K, T> {
// @ts-expect-error Some TypeScript issues.
const client = this as Client;
if (!client.contexts.has(contextName)) {
client.contexts.set(contextName, new Map<K, T>());
}
return client.contexts.get(contextName);
}