feat(create-discord-bot): bun/deno templates (#9795)

This commit is contained in:
Noel
2023-08-21 22:48:13 +02:00
committed by GitHub
parent 8eb978d32c
commit dd5e7453e8
46 changed files with 700 additions and 115 deletions

View File

@@ -0,0 +1,2 @@
DISCORD_TOKEN=
APPLICATION_ID=

View File

@@ -0,0 +1,8 @@
{
"printWidth": 120,
"useTabs": true,
"singleQuote": true,
"quoteProps": "as-needed",
"trailingComma": "all",
"endOfLine": "lf"
}

View File

@@ -0,0 +1,3 @@
{
"recommendations": ["denoland.vscode-deno", "tamasfe.even-better-toml", "codezombiech.gitignore"]
}

View File

@@ -0,0 +1,15 @@
{
"editor.defaultFormatter": "denoland.vscode-deno",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll": true,
"source.organizeImports": false
},
"editor.trimAutoWhitespace": false,
"files.insertFinalNewline": true,
"files.eol": "\n",
"npm.packageManager": "[REPLACE_ME]",
"deno.enable": "[REPLACE_BOOL]",
"deno.lint": "[REPLACE_BOOL]",
"deno.unstable": false
}

View File

@@ -0,0 +1,40 @@
{
"$schema": "https://raw.githubusercontent.com/denoland/deno/main/cli/schemas/config-file.v1.json",
"tasks": {
"lint": "deno lint",
"deploy": "deno run --allow-read --allow-env --allow-net src/util/deploy.ts",
"format": "deno fmt",
"start": "deno run --allow-read --allow-env --allow-net src/index.ts"
},
"lint": {
"include": ["src/"],
"rules": {
"tags": ["recommended"],
"exclude": ["require-await", "no-await-in-sync-fn"]
}
},
"fmt": {
"useTabs": true,
"lineWidth": 120,
"semiColons": true,
"singleQuote": true,
"proseWrap": "preserve",
"include": ["src/"]
},
"compilerOptions": {
"alwaysStrict": true,
"emitDecoratorMetadata": true,
"verbatimModuleSyntax": true,
"lib": ["deno.window"],
"noFallthroughCasesInSwitch": true,
"noImplicitReturns": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"removeComments": false,
"strict": true,
"allowUnreachableCode": false,
"allowUnusedLabels": false,
"exactOptionalPropertyTypes": false,
"noImplicitOverride": true
}
}

View File

@@ -0,0 +1,27 @@
import type { RESTPostAPIApplicationCommandsJSONBody, CommandInteraction } from 'npm:discord.js@^14.13.0';
import type { StructurePredicate } from '../util/loaders.ts';
/**
* Defines the structure of a command
*/
export type Command = {
/**
* The data for the command
*/
data: RESTPostAPIApplicationCommandsJSONBody;
/**
* The function to execute when the command is called
*
* @param interaction - The interaction of the command
*/
execute(interaction: CommandInteraction): Promise<void> | void;
};
// Defines the predicate to check if an object is a valid Command type
export const predicate: StructurePredicate<Command> = (structure): structure is Command =>
Boolean(structure) &&
typeof structure === 'object' &&
'data' in structure! &&
'execute' in structure &&
typeof structure.data === 'object' &&
typeof structure.execute === 'function';

View File

@@ -0,0 +1,11 @@
import type { Command } from './index.ts';
export default {
data: {
name: 'ping',
description: 'Ping!',
},
async execute(interaction) {
await interaction.reply('Pong!');
},
} satisfies Command;

View File

@@ -0,0 +1,33 @@
import type { ClientEvents } from 'npm:discord.js@^14.13.0';
import type { StructurePredicate } from '../util/loaders.ts';
/**
* Defines the structure of an event.
*/
export type Event<T extends keyof ClientEvents = keyof ClientEvents> = {
/**
* The function to execute when the event is emitted.
*
* @param parameters - The parameters of the event
*/
execute(...parameters: ClientEvents[T]): Promise<void> | void;
/**
* The name of the event to listen to
*/
name: T;
/**
* Whether or not the event should only be listened to once
*
* @defaultValue false
*/
once?: boolean;
};
// Defines the predicate to check if an object is a valid Event type.
export const predicate: StructurePredicate<Event> = (structure): structure is Event =>
Boolean(structure) &&
typeof structure === 'object' &&
'name' in structure! &&
'execute' in structure &&
typeof structure.name === 'string' &&
typeof structure.execute === 'function';

View File

@@ -0,0 +1,10 @@
import { Events } from 'npm:discord.js@^14.13.0';
import type { Event } from './index.ts';
export default {
name: Events.ClientReady,
once: true,
async execute(client) {
console.log(`Ready! Logged in as ${client.user.tag}`);
},
} satisfies Event<Events.ClientReady>;

View File

@@ -0,0 +1,18 @@
import 'https://deno.land/std@0.199.0/dotenv/load.ts';
import { URL } from 'node:url';
import { Client, GatewayIntentBits } from 'npm:discord.js@^14.13.0';
import { loadCommands, loadEvents } from './util/loaders.ts';
import { registerEvents } from './util/registerEvents.ts';
// Initialize the client
const client = new Client({ intents: [GatewayIntentBits.Guilds] });
// Load the events and commands
const events = await loadEvents(new URL('events/', import.meta.url));
const commands = await loadCommands(new URL('commands/', import.meta.url));
// Register the event handlers
registerEvents(commands, events, client);
// Login to the client
void client.login(Deno.env.get('DISCORD_TOKEN'));

View File

@@ -0,0 +1,15 @@
import 'https://deno.land/std@0.199.0/dotenv/load.ts';
import { URL } from 'node:url';
import { API } from 'npm:@discordjs/core@^1.0.1/http-only';
import { REST } from 'npm:discord.js@^14.13.0';
import { loadCommands } from './loaders.ts';
const commands = await loadCommands(new URL('../commands/', import.meta.url));
const commandData = [...commands.values()].map((command) => command.data);
const rest = new REST({ version: '10' }).setToken(Deno.env.get('DISCORD_TOKEN')!);
const api = new API(rest);
const result = await api.applicationCommands.bulkOverwriteGlobalCommands(Deno.env.get('APPLICATION_ID')!, commandData);
console.log(`Successfully registered ${result.length} commands.`);

View File

@@ -0,0 +1,76 @@
import type { PathLike } from 'node:fs';
import { readdir, stat } from 'node:fs/promises';
import { URL } from 'node:url';
import type { Command } from '../commands/index.ts';
import { predicate as commandPredicate } from '../commands/index.ts';
import type { Event } from '../events/index.ts';
import { predicate as eventPredicate } from '../events/index.ts';
/**
* A predicate to check if the structure is valid
*/
export type StructurePredicate<T> = (structure: unknown) => structure is T;
/**
* Loads all the structures in the provided directory
*
* @param dir - The directory to load the structures from
* @param predicate - The predicate to check if the structure is valid
* @param recursive - Whether to recursively load the structures in the directory
* @returns
*/
export async function loadStructures<T>(
dir: PathLike,
predicate: StructurePredicate<T>,
recursive = true,
): Promise<T[]> {
// Get the stats of the directory
const statDir = await stat(dir);
// If the provided directory path is not a directory, throw an error
if (!statDir.isDirectory()) {
throw new Error(`The directory '${dir}' is not a directory.`);
}
// Get all the files in the directory
const files = await readdir(dir);
// Create an empty array to store the structures
const structures: T[] = [];
// Loop through all the files in the directory
for (const file of files) {
// If the file is index.js or the file does not end with .js, skip the file
if (file === 'index.ts' || !file.endsWith('.ts')) {
continue;
}
// Get the stats of the file
const statFile = await stat(new URL(`${dir}/${file}`));
// If the file is a directory and recursive is true, recursively load the structures in the directory
if (statFile.isDirectory() && recursive) {
structures.push(...(await loadStructures(`${dir}/${file}`, predicate, recursive)));
continue;
}
// Import the structure dynamically from the file
const structure = (await import(`${dir}/${file}`)).default;
// If the structure is a valid structure, add it
if (predicate(structure)) structures.push(structure);
}
return structures;
}
export async function loadCommands(dir: PathLike, recursive = true): Promise<Map<string, Command>> {
return (await loadStructures(dir, commandPredicate, recursive)).reduce(
(acc, cur) => acc.set(cur.data.name, cur),
new Map<string, Command>(),
);
}
export async function loadEvents(dir: PathLike, recursive = true): Promise<Event[]> {
return loadStructures(dir, eventPredicate, recursive);
}

View File

@@ -0,0 +1,25 @@
import { Events, type Client } from 'npm:discord.js@^14.13.0';
import type { Command } from '../commands/index.ts';
import type { Event } from '../events/index.ts';
export function registerEvents(commands: Map<string, Command>, events: Event[], client: Client): void {
// Create an event to handle command interactions
const interactionCreateEvent: Event<Events.InteractionCreate> = {
name: Events.InteractionCreate,
async execute(interaction) {
if (interaction.isCommand()) {
const command = commands.get(interaction.commandName);
if (!command) {
throw new Error(`Command '${interaction.commandName}' not found.`);
}
await command.execute(interaction);
}
},
};
for (const event of [...events, interactionCreateEvent]) {
client[event.once ? 'once' : 'on'](event.name, async (...args) => event.execute(...args));
}
}