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
|
# build and test outputs
|
||||||
dist/
|
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