initial commit
This commit is contained in:
commit
29c6cb9c4a
28 changed files with 2277 additions and 0 deletions
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
node_modules
|
||||
# Keep environment variables out of version control
|
||||
.env
|
10
README.md
Normal file
10
README.md
Normal 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
BIN
bun.lockb
Executable file
Binary file not shown.
37
commands/misc/ping.ts
Normal file
37
commands/misc/ping.ts
Normal 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
1
config.example.yml
Normal file
|
@ -0,0 +1 @@
|
|||
token: <BOT_TOKEN>
|
14
discordjs.d.ts
vendored
Normal file
14
discordjs.d.ts
vendored
Normal 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
25
dist/commands/misc/ping.js
vendored
Normal 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
23
dist/events/interactionCreate.js
vendored
Normal 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
18
dist/events/ready.js
vendored
Normal 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
42
dist/handlers/loaders/command.js
vendored
Normal 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
31
dist/handlers/loaders/event.js
vendored
Normal 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
31
dist/handlers/loaders/interactions.js
vendored
Normal 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
85
dist/index.js
vendored
Normal 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
28
dist/types.js
vendored
Normal 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
8
dist/utils/context.js
vendored
Normal 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
106
eslint.config.mjs
Normal 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',
|
||||
},
|
||||
}];
|
33
events/interactionCreate.ts
Normal file
33
events/interactionCreate.ts
Normal 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
27
events/ready.ts
Normal 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}`);
|
||||
|
||||
}
|
||||
|
||||
}
|
70
handlers/loaders/command.ts
Normal file
70
handlers/loaders/command.ts
Normal 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
46
handlers/loaders/event.ts
Normal 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}`);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
53
handlers/loaders/interactions.ts
Normal file
53
handlers/loaders/interactions.ts
Normal 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
124
index.ts
Normal 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
30
package.json
Normal 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
1308
pnpm-lock.yaml
Normal file
File diff suppressed because it is too large
Load diff
14
prisma/schema.prisma
Normal file
14
prisma/schema.prisma
Normal 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
37
tsconfig.json
Normal 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
59
types.ts
Normal 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
14
utils/context.ts
Normal 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);
|
||||
|
||||
}
|
Loading…
Reference in a new issue