forked from syntaxbullet/AuroraBot-discord
Refactor architecture: improve env loading and command workflow
This commit is contained in:
@@ -4,6 +4,8 @@ DB_NAME=kyoko
|
||||
DB_PORT=5432
|
||||
DB_HOST=db
|
||||
DISCORD_BOT_TOKEN=your-discord-bot-token
|
||||
DISCORD_CLIENT_ID=your-discord-client-id
|
||||
DISCORD_GUILD_ID=your-discord-guild-id
|
||||
DB_DATA_DIR=./db-data
|
||||
DB_LOG_DIR=./db-logs
|
||||
DATABASE_URL=postgres://kyoko:kyoko@db:5432/kyoko
|
||||
@@ -1,15 +1,65 @@
|
||||
# app
|
||||
# Kyoko - Discord Rpg
|
||||
|
||||
To install dependencies:
|
||||
A Discord bot built with [Bun](https://bun.sh), [Discord.js](https://discord.js.org/), and [Drizzle ORM](https://orm.drizzle.team/).
|
||||
|
||||
## Architecture
|
||||
|
||||
This project uses a modular architecture:
|
||||
|
||||
- **`src/index.ts`**: Entry point. initializes the client.
|
||||
- **`src/lib/KyokoClient.ts`**: Custom Discord Client wrapper handling command loading and events.
|
||||
- **`src/lib/env.ts`**: **Centralized Environment Configuration**. Validates environment variables using `zod` at startup.
|
||||
- **`src/lib/DrizzleClient.ts`**: Database client instance.
|
||||
- **`src/commands/`**: Command files.
|
||||
- **`src/db/`**: Database schema and migrations.
|
||||
|
||||
## Setup
|
||||
|
||||
1. **Install Dependencies**:
|
||||
```bash
|
||||
bun install
|
||||
```
|
||||
|
||||
To run:
|
||||
2. **Environment Variables**:
|
||||
Copy `.env.example` to `.env` (create one if it doesn't exist) and fill in the required values:
|
||||
```env
|
||||
DISCORD_BOT_TOKEN=your_token_here
|
||||
DISCORD_CLIENT_ID=your_client_id
|
||||
DISCORD_GUILD_ID=your_guild_id_optional
|
||||
DATABASE_URL=postgres://user:pass@localhost:5432/db_name
|
||||
```
|
||||
*Note: The app will fail to start if `DISCORD_BOT_TOKEN` or `DATABASE_URL` are missing or invalid.*
|
||||
|
||||
3. **Run Development**:
|
||||
```bash
|
||||
bun run index.ts
|
||||
bun run dev
|
||||
```
|
||||
|
||||
This project was created using `bun init` in bun v1.3.2. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime.
|
||||
4. **Database Migrations**:
|
||||
```bash
|
||||
bun run db:push # Apply schema changes
|
||||
bun run generate # Generate migrations
|
||||
```
|
||||
|
||||
## Deployment
|
||||
|
||||
### Manual Command Registration
|
||||
Since command registration is decoupled from startup, you must run this manually when you add or change commands.
|
||||
|
||||
**Option 1: Using Docker (Recommended)**
|
||||
Uses the credentials configured in `docker-compose.yml`.
|
||||
```bash
|
||||
docker compose run --rm app bun run deploy
|
||||
```
|
||||
|
||||
**Option 2: Running Locally**
|
||||
Requires valid `.env` file with `DISCORD_CLIENT_ID`.
|
||||
```bash
|
||||
bun run deploy
|
||||
```
|
||||
|
||||
## Development Features
|
||||
|
||||
- **Type Safety**: Full TypeScript support.
|
||||
- **Env Validation**: `zod` ensures all required env vars are present.
|
||||
- **Hot Reloading**: `bun --watch` for fast development.
|
||||
|
||||
@@ -6,7 +6,9 @@
|
||||
"name": "app",
|
||||
"dependencies": {
|
||||
"discord.js": "^14.25.1",
|
||||
"dotenv": "^17.2.3",
|
||||
"drizzle-orm": "^0.44.7",
|
||||
"zod": "^4.1.13",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest",
|
||||
@@ -113,6 +115,8 @@
|
||||
|
||||
"discord.js": ["discord.js@14.25.1", "", { "dependencies": { "@discordjs/builders": "^1.13.0", "@discordjs/collection": "1.5.3", "@discordjs/formatters": "^0.6.2", "@discordjs/rest": "^2.6.0", "@discordjs/util": "^1.2.0", "@discordjs/ws": "^1.2.3", "@sapphire/snowflake": "3.5.3", "discord-api-types": "^0.38.33", "fast-deep-equal": "3.1.3", "lodash.snakecase": "4.1.1", "magic-bytes.js": "^1.10.0", "tslib": "^2.6.3", "undici": "6.21.3" } }, "sha512-2l0gsPOLPs5t6GFZfQZKnL1OJNYFcuC/ETWsW4VtKVD/tg4ICa9x+jb9bkPffkMdRpRpuUaO/fKkHCBeiCKh8g=="],
|
||||
|
||||
"dotenv": ["dotenv@17.2.3", "", {}, "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w=="],
|
||||
|
||||
"drizzle-kit": ["drizzle-kit@0.31.7", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.25.4", "esbuild-register": "^3.5.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-hOzRGSdyKIU4FcTSFYGKdXEjFsncVwHZ43gY3WU5Bz9j5Iadp6Rh6hxLSQ1IWXpKLBKt/d5y1cpSPcV+FcoQ1A=="],
|
||||
|
||||
"drizzle-orm": ["drizzle-orm@0.44.7", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-quIpnYznjU9lHshEOAYLoZ9s3jweleHlZIAWR/jX9gAWNg/JhQ1wj0KGRf7/Zm+obRrYd9GjPVJg790QY9N5AQ=="],
|
||||
@@ -153,6 +157,8 @@
|
||||
|
||||
"ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="],
|
||||
|
||||
"zod": ["zod@4.1.13", "", {}, "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig=="],
|
||||
|
||||
"@discordjs/rest/@discordjs/collection": ["@discordjs/collection@2.1.1", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="],
|
||||
|
||||
"@discordjs/ws/@discordjs/collection": ["@discordjs/collection@2.1.1", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="],
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { defineConfig } from "drizzle-kit";
|
||||
import { env } from "./src/lib/env";
|
||||
|
||||
export default defineConfig({
|
||||
schema: "./schema.ts",
|
||||
schema: "./src/db/schema.ts",
|
||||
out: "./drizzle",
|
||||
dialect: "postgresql",
|
||||
dbCredentials: {
|
||||
url: process.env.DATABASE_URL || "postgres://kyoko:kyoko@localhost:5432/kyoko",
|
||||
url: env.DATABASE_URL,
|
||||
},
|
||||
});
|
||||
|
||||
22
app/drizzle/0000_big_obadiah_stane.sql
Normal file
22
app/drizzle/0000_big_obadiah_stane.sql
Normal file
@@ -0,0 +1,22 @@
|
||||
CREATE TABLE "transactions" (
|
||||
"transaction_id" serial PRIMARY KEY NOT NULL,
|
||||
"from_user_id" text,
|
||||
"to_user_id" text,
|
||||
"amount" integer NOT NULL,
|
||||
"occured_at" timestamp DEFAULT now(),
|
||||
"type" text NOT NULL,
|
||||
"description" text,
|
||||
CONSTRAINT "transactions_transaction_id_unique" UNIQUE("transaction_id")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "users" (
|
||||
"user_id" text PRIMARY KEY NOT NULL,
|
||||
"balance" integer DEFAULT 0 NOT NULL,
|
||||
"last_daily" timestamp,
|
||||
"daily_streak" integer DEFAULT 0 NOT NULL,
|
||||
"created_at" timestamp DEFAULT now(),
|
||||
CONSTRAINT "users_user_id_unique" UNIQUE("user_id")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "transactions" ADD CONSTRAINT "transactions_from_user_id_users_user_id_fk" FOREIGN KEY ("from_user_id") REFERENCES "public"."users"("user_id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "transactions" ADD CONSTRAINT "transactions_to_user_id_users_user_id_fk" FOREIGN KEY ("to_user_id") REFERENCES "public"."users"("user_id") ON DELETE no action ON UPDATE no action;
|
||||
164
app/drizzle/meta/0000_snapshot.json
Normal file
164
app/drizzle/meta/0000_snapshot.json
Normal file
@@ -0,0 +1,164 @@
|
||||
{
|
||||
"id": "e5884b86-8257-466d-86f8-5fef47cfcd50",
|
||||
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"tables": {
|
||||
"public.transactions": {
|
||||
"name": "transactions",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"transaction_id": {
|
||||
"name": "transaction_id",
|
||||
"type": "serial",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"from_user_id": {
|
||||
"name": "from_user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"to_user_id": {
|
||||
"name": "to_user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"amount": {
|
||||
"name": "amount",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"occured_at": {
|
||||
"name": "occured_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": "now()"
|
||||
},
|
||||
"type": {
|
||||
"name": "type",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"description": {
|
||||
"name": "description",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"transactions_from_user_id_users_user_id_fk": {
|
||||
"name": "transactions_from_user_id_users_user_id_fk",
|
||||
"tableFrom": "transactions",
|
||||
"tableTo": "users",
|
||||
"columnsFrom": [
|
||||
"from_user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"user_id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"transactions_to_user_id_users_user_id_fk": {
|
||||
"name": "transactions_to_user_id_users_user_id_fk",
|
||||
"tableFrom": "transactions",
|
||||
"tableTo": "users",
|
||||
"columnsFrom": [
|
||||
"to_user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"user_id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"transactions_transaction_id_unique": {
|
||||
"name": "transactions_transaction_id_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"transaction_id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.users": {
|
||||
"name": "users",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"balance": {
|
||||
"name": "balance",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": 0
|
||||
},
|
||||
"last_daily": {
|
||||
"name": "last_daily",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"daily_streak": {
|
||||
"name": "daily_streak",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": 0
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"users_user_id_unique": {
|
||||
"name": "users_user_id_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"user_id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
}
|
||||
},
|
||||
"enums": {},
|
||||
"schemas": {},
|
||||
"sequences": {},
|
||||
"roles": {},
|
||||
"policies": {},
|
||||
"views": {},
|
||||
"_meta": {
|
||||
"columns": {},
|
||||
"schemas": {},
|
||||
"tables": {}
|
||||
}
|
||||
}
|
||||
13
app/drizzle/meta/_journal.json
Normal file
13
app/drizzle/meta/_journal.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"entries": [
|
||||
{
|
||||
"idx": 0,
|
||||
"version": "7",
|
||||
"when": 1764930369542,
|
||||
"tag": "0000_big_obadiah_stane",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -15,11 +15,13 @@
|
||||
"generate": "drizzle-kit generate",
|
||||
"migrate": "drizzle-kit migrate",
|
||||
"db:push": "drizzle-kit push",
|
||||
"deploy": "bun src/scripts/deploy.ts",
|
||||
"deploy": "docker compose run --rm app bun src/scripts/deploy.ts",
|
||||
"dev": "bun --watch src/index.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"discord.js": "^14.25.1",
|
||||
"drizzle-orm": "^0.44.7"
|
||||
"dotenv": "^17.2.3",
|
||||
"drizzle-orm": "^0.44.7",
|
||||
"zod": "^4.1.13"
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
import type { Command } from "@lib/types";
|
||||
import { createCommand } from "@lib/utils";
|
||||
import { getUserBalance } from "@/modules/economy/economy.service";
|
||||
import { SlashCommandBuilder, EmbedBuilder } from "discord.js";
|
||||
|
||||
export const balance: Command = {
|
||||
export const balance = createCommand({
|
||||
data: new SlashCommandBuilder().setName("balance").setDescription("Check your balance"),
|
||||
execute: async (interaction) => {
|
||||
const balance = await getUserBalance(interaction.user.id) || 0;
|
||||
const embed = new EmbedBuilder().setDescription(`Your balance is ${balance}`);
|
||||
await interaction.reply({ embeds: [embed] });
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import { Events } from "discord.js";
|
||||
import { KyokoClient } from "@lib/KyokoClient";
|
||||
import { env } from "@lib/env";
|
||||
|
||||
// Load commands
|
||||
await KyokoClient.loadCommands();
|
||||
|
||||
KyokoClient.once(Events.ClientReady, async c => {
|
||||
console.log(`Ready! Logged in as ${c.user.tag}`);
|
||||
console.log("Deploying commands...");
|
||||
KyokoClient.deployCommands();
|
||||
});
|
||||
|
||||
KyokoClient.on(Events.InteractionCreate, async interaction => {
|
||||
@@ -33,4 +32,7 @@ KyokoClient.on(Events.InteractionCreate, async interaction => {
|
||||
});
|
||||
|
||||
// login with the token from .env
|
||||
KyokoClient.login(process.env.DISCORD_BOT_TOKEN);
|
||||
if (!env.DISCORD_BOT_TOKEN) {
|
||||
throw new Error("❌ DISCORD_BOT_TOKEN is not set in environment variables.");
|
||||
}
|
||||
KyokoClient.login(env.DISCORD_BOT_TOKEN);
|
||||
@@ -1,8 +1,9 @@
|
||||
import { drizzle } from "drizzle-orm/bun-sql";
|
||||
import { SQL } from "bun";
|
||||
import * as schema from "@db/schema";
|
||||
import { env } from "@lib/env";
|
||||
|
||||
const connectionString = process.env.DATABASE_URL || "postgres://kyoko:kyoko@localhost:5432/kyoko";
|
||||
const connectionString = env.DATABASE_URL;
|
||||
const postgres = new SQL(connectionString);
|
||||
|
||||
export const DrizzleClient = drizzle(postgres, { schema });
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Client as DiscordClient, Collection, GatewayIntentBits, REST, Routes, SlashCommandBuilder } from "discord.js";
|
||||
import { Client as DiscordClient, Collection, GatewayIntentBits, REST, Routes } from "discord.js";
|
||||
import { readdir } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import type { Command } from "@lib/types";
|
||||
import { env } from "@lib/env";
|
||||
|
||||
class Client extends DiscordClient {
|
||||
|
||||
@@ -34,15 +35,21 @@ class Client extends DiscordClient {
|
||||
try {
|
||||
const commandModule = await import(filePath);
|
||||
const commands = Object.values(commandModule);
|
||||
if (commands.length === 0) {
|
||||
console.warn(`⚠️ No commands found in ${file.name}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const command of commands) {
|
||||
if (this.isValidCommand(command)) {
|
||||
this.commands.set(command.data.name, command);
|
||||
console.log(`Loaded command: ${command.data.name}`);
|
||||
console.log(`✅ Loaded command: ${command.data.name}`);
|
||||
} else {
|
||||
console.warn(`⚠️ Skipping invalid command in ${file.name}`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to load command from ${filePath}:`, error);
|
||||
console.error(`❌ Failed to load command from ${filePath}:`, error);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -55,9 +62,22 @@ class Client extends DiscordClient {
|
||||
}
|
||||
|
||||
async deployCommands() {
|
||||
const rest = new REST().setToken(this.token!);
|
||||
// We use env.DISCORD_BOT_TOKEN directly so this can run without client.login()
|
||||
const token = env.DISCORD_BOT_TOKEN;
|
||||
if (!token) {
|
||||
console.error("❌ DISCORD_BOT_TOKEN is not set.");
|
||||
return;
|
||||
}
|
||||
|
||||
const rest = new REST().setToken(token);
|
||||
const commandsData = this.commands.map(c => c.data.toJSON());
|
||||
const guildId = process.env.DISCORD_GUILD_ID;
|
||||
const guildId = env.DISCORD_GUILD_ID;
|
||||
const clientId = env.DISCORD_CLIENT_ID;
|
||||
|
||||
if (!clientId) {
|
||||
console.error("❌ DISCORD_CLIENT_ID is not set.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log(`Started refreshing ${commandsData.length} application (/) commands.`);
|
||||
@@ -66,24 +86,29 @@ class Client extends DiscordClient {
|
||||
if (guildId) {
|
||||
console.log(`Registering commands to guild: ${guildId}`);
|
||||
data = await rest.put(
|
||||
Routes.applicationGuildCommands(this.user!.id, guildId),
|
||||
Routes.applicationGuildCommands(clientId, guildId),
|
||||
{ body: commandsData },
|
||||
);
|
||||
// Clear global commands to avoid duplicates
|
||||
await rest.put(Routes.applicationCommands(this.user!.id), { body: [] });
|
||||
await rest.put(Routes.applicationCommands(clientId), { body: [] });
|
||||
} else {
|
||||
console.log('Registering commands globally');
|
||||
data = await rest.put(
|
||||
Routes.applicationCommands(this.user!.id),
|
||||
Routes.applicationCommands(clientId),
|
||||
{ body: commandsData },
|
||||
);
|
||||
}
|
||||
|
||||
console.log(`Successfully reloaded ${(data as any).length} application (/) commands.`);
|
||||
} catch (error) {
|
||||
console.log(`✅ Successfully reloaded ${(data as any).length} application (/) commands.`);
|
||||
} catch (error: any) {
|
||||
if (error.code === 50001) {
|
||||
console.warn("⚠️ Missing Access: The bot is not in the guild or lacks 'applications.commands' scope.");
|
||||
console.warn(" If you are testing locally, make sure you invited the bot with scope 'bot applications.commands'.");
|
||||
} else {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const KyokoClient = new Client({ intents: [GatewayIntentBits.Guilds, GatewayIntentBits.MessageContent, GatewayIntentBits.GuildMessages] });
|
||||
18
app/src/lib/env.ts
Normal file
18
app/src/lib/env.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { z } from "zod";
|
||||
import "dotenv/config";
|
||||
|
||||
const envSchema = z.object({
|
||||
DISCORD_BOT_TOKEN: z.string().optional(),
|
||||
DISCORD_CLIENT_ID: z.string().optional(),
|
||||
DISCORD_GUILD_ID: z.string().optional(),
|
||||
DATABASE_URL: z.string().min(1, "Database URL is required").default("postgres://kyoko:kyoko@localhost:5432/kyoko"),
|
||||
});
|
||||
|
||||
const parsedEnv = envSchema.safeParse(process.env);
|
||||
|
||||
if (!parsedEnv.success) {
|
||||
console.error("❌ Invalid environment variables:", parsedEnv.error.flatten().fieldErrors);
|
||||
throw new Error("Invalid environment variables");
|
||||
}
|
||||
|
||||
export const env = parsedEnv.data;
|
||||
11
app/src/lib/utils.ts
Normal file
11
app/src/lib/utils.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import type { Command } from "./types";
|
||||
|
||||
/**
|
||||
* Type-safe helper to create a command definition.
|
||||
*
|
||||
* @param command The command definition
|
||||
* @returns The command object
|
||||
*/
|
||||
export function createCommand(command: Command): Command {
|
||||
return command;
|
||||
}
|
||||
14
app/src/scripts/deploy.ts
Normal file
14
app/src/scripts/deploy.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { KyokoClient } from "@lib/KyokoClient";
|
||||
|
||||
console.log("🚀 Starting deployment script...");
|
||||
|
||||
// Load all commands first
|
||||
await KyokoClient.loadCommands();
|
||||
|
||||
console.log(`📦 Loaded ${KyokoClient.commands.size} commands.`);
|
||||
|
||||
// Deploy
|
||||
await KyokoClient.deployCommands();
|
||||
|
||||
console.log("👋 Deployment script finished.");
|
||||
process.exit(0);
|
||||
@@ -32,6 +32,7 @@ services:
|
||||
- DB_HOST=db
|
||||
- DISCORD_BOT_TOKEN=${DISCORD_BOT_TOKEN}
|
||||
- DISCORD_GUILD_ID=${DISCORD_GUILD_ID}
|
||||
- DISCORD_CLIENT_ID=${DISCORD_CLIENT_ID}
|
||||
- DATABASE_URL=${DATABASE_URL}
|
||||
depends_on:
|
||||
- db
|
||||
|
||||
Reference in New Issue
Block a user