feat: initialize Discord bot project with basic command structure
- Add package.json with dependencies and scripts for building, testing, and running the bot. - Implement bot startup logic in src/bot.ts, handling interactions and commands. - Create command structure in src/commands/index.ts, including a ping command in src/commands/ping.ts. - Add configuration loading from environment variables in src/config.ts. - Implement command registration in Discord API in src/deploy-commands.ts. - Bootstrap the bot in src/index.ts. - Configure TypeScript settings in tsconfig.json.
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
DISCORD_TOKEN=
|
||||
DISCORD_CLIENT_ID=
|
||||
# Optional. If set, command registration targets this guild for instant updates.
|
||||
DISCORD_GUILD_ID=
|
||||
+5
-1
@@ -17,4 +17,8 @@ node_modules/
|
||||
|
||||
# build and test outputs
|
||||
dist/
|
||||
coverage/
|
||||
coverage/
|
||||
|
||||
# doc template
|
||||
arc42_adoc/
|
||||
download_docs_template.py
|
||||
@@ -0,0 +1,26 @@
|
||||
const tsEslint = require("@typescript-eslint/eslint-plugin");
|
||||
const tsParser = require("@typescript-eslint/parser");
|
||||
|
||||
/** @type {import("eslint").Linter.FlatConfig[]} */
|
||||
module.exports = [
|
||||
{
|
||||
ignores: ["dist/**", "node_modules/**"],
|
||||
},
|
||||
{
|
||||
files: ["**/*.ts"],
|
||||
languageOptions: {
|
||||
parser: tsParser,
|
||||
parserOptions: {
|
||||
project: "./tsconfig.json",
|
||||
sourceType: "module",
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
"@typescript-eslint": tsEslint,
|
||||
},
|
||||
rules: {
|
||||
"no-console": "off",
|
||||
"@typescript-eslint/no-explicit-any": "warn",
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,8 @@
|
||||
/** @type {import('jest').Config} */
|
||||
module.exports = {
|
||||
preset: "ts-jest",
|
||||
testEnvironment: "node",
|
||||
testMatch: ["**/*.test.ts"],
|
||||
moduleFileExtensions: ["ts", "js", "json"],
|
||||
clearMocks: true,
|
||||
};
|
||||
Generated
+6055
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "omo-bot",
|
||||
"version": "1.0.0",
|
||||
"description": "A custom Discord bot and web integration layer designed to bridge the [openmicodyssey.com](https://openmicodyssey.com) experience with our community server. This application gamifies community engagement, automates content syndication, and provides a suite of event management tools centered around the themes of stand-up comedy, indie filmmaking, and a cross-country road trip.",
|
||||
"main": "dist/index.js",
|
||||
"directories": {
|
||||
"doc": "docs"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc -p tsconfig.json",
|
||||
"dev": "ts-node src/index.ts",
|
||||
"start": "node dist/index.js",
|
||||
"lint": "eslint . --ext .ts",
|
||||
"register:commands": "ts-node src/deploy-commands.ts",
|
||||
"test": "jest --passWithNoTests"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"type": "commonjs",
|
||||
"dependencies": {
|
||||
"discord.js": "^14.26.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/node": "^25.8.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.59.3",
|
||||
"@typescript-eslint/parser": "^8.59.3",
|
||||
"eslint": "^10.4.0",
|
||||
"jest": "^30.4.2",
|
||||
"ts-jest": "^29.4.9",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^6.0.3"
|
||||
}
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
import {
|
||||
Client,
|
||||
Events,
|
||||
GatewayIntentBits,
|
||||
Interaction,
|
||||
Partials,
|
||||
} from "discord.js";
|
||||
import { commandMap } from "./commands";
|
||||
import { BotConfig } from "./config";
|
||||
|
||||
export async function startBot(config: BotConfig): Promise<Client> {
|
||||
const client = new Client({
|
||||
intents: [GatewayIntentBits.Guilds],
|
||||
partials: [Partials.Channel],
|
||||
});
|
||||
|
||||
client.once(Events.ClientReady, (readyClient) => {
|
||||
console.log(`Bot ready as ${readyClient.user.tag}`);
|
||||
});
|
||||
|
||||
client.on(Events.InteractionCreate, async (interaction: Interaction) => {
|
||||
if (!interaction.isChatInputCommand()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const command = commandMap.get(interaction.commandName);
|
||||
if (!command) {
|
||||
await interaction.reply({
|
||||
content: `Unknown command: ${interaction.commandName}`,
|
||||
ephemeral: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await command.execute(interaction);
|
||||
} catch (error: unknown) {
|
||||
console.error(`Command failed: ${interaction.commandName}`, error);
|
||||
|
||||
if (interaction.replied || interaction.deferred) {
|
||||
await interaction.followUp({
|
||||
content: "Command failed. Try again.",
|
||||
ephemeral: true,
|
||||
});
|
||||
} else {
|
||||
await interaction.reply({
|
||||
content: "Command failed. Try again.",
|
||||
ephemeral: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await client.login(config.discordToken);
|
||||
return client;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
|
||||
import { pingCommand } from "./ping";
|
||||
|
||||
export interface ChatCommand {
|
||||
data: SlashCommandBuilder;
|
||||
execute(interaction: ChatInputCommandInteraction): Promise<void>;
|
||||
}
|
||||
|
||||
export const commands: ChatCommand[] = [pingCommand];
|
||||
|
||||
export const commandMap = new Map<string, ChatCommand>(
|
||||
commands.map((command) => [command.data.name, command]),
|
||||
);
|
||||
@@ -0,0 +1,10 @@
|
||||
import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
|
||||
|
||||
export const pingCommand = {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName("ping")
|
||||
.setDescription("Check whether bot is alive."),
|
||||
async execute(interaction: ChatInputCommandInteraction): Promise<void> {
|
||||
await interaction.reply("Pong!");
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
export interface BotConfig {
|
||||
discordToken: string;
|
||||
discordClientId: string;
|
||||
discordGuildId?: string;
|
||||
}
|
||||
|
||||
function readRequiredEnv(name: string): string {
|
||||
const value = process.env[name];
|
||||
if (!value) {
|
||||
throw new Error(`Missing required environment variable: ${name}`);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
export function loadConfig(): BotConfig {
|
||||
const discordGuildId = process.env.DISCORD_GUILD_ID;
|
||||
|
||||
return {
|
||||
discordToken: readRequiredEnv("DISCORD_TOKEN"),
|
||||
discordClientId: readRequiredEnv("DISCORD_CLIENT_ID"),
|
||||
...(discordGuildId ? { discordGuildId } : {}),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { REST, Routes } from "discord.js";
|
||||
import { commands } from "./commands";
|
||||
import { loadConfig } from "./config";
|
||||
|
||||
async function registerCommands(): Promise<void> {
|
||||
const config = loadConfig();
|
||||
const rest = new REST({ version: "10" }).setToken(config.discordToken);
|
||||
const body = commands.map((command) => command.data.toJSON());
|
||||
|
||||
if (config.discordGuildId) {
|
||||
await rest.put(
|
||||
Routes.applicationGuildCommands(
|
||||
config.discordClientId,
|
||||
config.discordGuildId,
|
||||
),
|
||||
{ body },
|
||||
);
|
||||
console.log(
|
||||
`Registered ${body.length} guild command(s) for guild ${config.discordGuildId}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await rest.put(Routes.applicationCommands(config.discordClientId), { body });
|
||||
console.log(`Registered ${body.length} global command(s)`);
|
||||
}
|
||||
|
||||
void registerCommands().catch((error: unknown) => {
|
||||
console.error("Failed to register commands:", error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
import { startBot } from "./bot";
|
||||
import { loadConfig } from "./config";
|
||||
|
||||
export async function bootstrap(): Promise<void> {
|
||||
const config = loadConfig();
|
||||
await startBot(config);
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
void bootstrap().catch((error: unknown) => {
|
||||
console.error("Failed to start bot:", error);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
{
|
||||
// Visit https://aka.ms/tsconfig to read more about this file
|
||||
"compilerOptions": {
|
||||
// File Layout
|
||||
"rootDir": "src",
|
||||
"outDir": "dist",
|
||||
|
||||
// Environment Settings
|
||||
// See also https://aka.ms/tsconfig/module
|
||||
"module": "node16",
|
||||
"target": "es2022",
|
||||
"types": ["node", "jest"],
|
||||
// For nodejs:
|
||||
// "lib": ["esnext"],
|
||||
// "types": ["node"],
|
||||
// and npm install -D @types/node
|
||||
|
||||
// Other Outputs
|
||||
"sourceMap": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
|
||||
// Stricter Typechecking Options
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"exactOptionalPropertyTypes": true,
|
||||
|
||||
// Style Options
|
||||
// "noImplicitReturns": true,
|
||||
// "noImplicitOverride": true,
|
||||
// "noUnusedLocals": true,
|
||||
// "noUnusedParameters": true,
|
||||
// "noFallthroughCasesInSwitch": true,
|
||||
// "noPropertyAccessFromIndexSignature": true,
|
||||
|
||||
// Recommended Options
|
||||
"strict": true,
|
||||
"verbatimModuleSyntax": false,
|
||||
"isolatedModules": true,
|
||||
"noUncheckedSideEffectImports": true,
|
||||
"moduleResolution": "node16",
|
||||
"moduleDetection": "auto",
|
||||
"skipLibCheck": true,
|
||||
|
||||
"esModuleInterop": true
|
||||
},
|
||||
"include": ["src/**/*.ts", "tests/**/*.ts"],
|
||||
"exclude": ["dist", "node_modules"]
|
||||
}
|
||||
Reference in New Issue
Block a user