forked from syntaxbullet/AuroraBot-discord
Merge branch 'main' of https://git.ayau.me/HotPlate/discord-rpg-concept
This commit is contained in:
29
src/commands/economy/balance.ts
Normal file
29
src/commands/economy/balance.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { createCommand } from "@lib/utils";
|
||||
import { getUserBalance } from "@/modules/economy/economy.service";
|
||||
import { createUser, getUserById } from "@/modules/users/users.service";
|
||||
import { SlashCommandBuilder, EmbedBuilder, PermissionFlagsBits } from "discord.js";
|
||||
|
||||
export const balance = createCommand({
|
||||
data: new SlashCommandBuilder()
|
||||
.setName("balance")
|
||||
.setDescription("Check your balance")
|
||||
|
||||
|
||||
, execute: async (interaction) => {
|
||||
const user = interaction.user;
|
||||
// Ensure user exists in DB
|
||||
let dbUser = await getUserById(user.id);
|
||||
if (!dbUser) {
|
||||
await createUser(user.id);
|
||||
}
|
||||
|
||||
const balance = await getUserBalance(user.id);
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle(`${user.username}'s Balance`)
|
||||
.setDescription(`💰 **${balance} coins**`)
|
||||
.setColor("Green");
|
||||
|
||||
await interaction.reply({ embeds: [embed] });
|
||||
}
|
||||
});
|
||||
34
src/commands/system/reload.ts
Normal file
34
src/commands/system/reload.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { createCommand } from "@lib/utils";
|
||||
import { KyokoClient } from "@lib/KyokoClient";
|
||||
import { SlashCommandBuilder, EmbedBuilder, PermissionFlagsBits } from "discord.js";
|
||||
|
||||
export const reload = createCommand({
|
||||
data: new SlashCommandBuilder()
|
||||
.setName("reload")
|
||||
.setDescription("Reloads all commands")
|
||||
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator),
|
||||
execute: async (interaction) => {
|
||||
await interaction.deferReply({ ephemeral: true });
|
||||
|
||||
try {
|
||||
await KyokoClient.loadCommands(true);
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle("✅ System Reloaded")
|
||||
.setDescription(`Successfully reloaded ${KyokoClient.commands.size} commands.`)
|
||||
.setColor("Green");
|
||||
|
||||
// Deploy commands
|
||||
await KyokoClient.deployCommands();
|
||||
|
||||
await interaction.editReply({ embeds: [embed] });
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle("❌ Reload Failed")
|
||||
.setDescription("An error occurred while reloading commands. Check console for details.")
|
||||
.setColor("Red");
|
||||
|
||||
await interaction.editReply({ embeds: [embed] });
|
||||
}
|
||||
}
|
||||
});
|
||||
19
src/db/schema.ts
Normal file
19
src/db/schema.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { pgTable, integer, text, timestamp, serial } from "drizzle-orm/pg-core";
|
||||
|
||||
export const users = pgTable("users", {
|
||||
userId: text("user_id").primaryKey().notNull(),
|
||||
balance: integer("balance").notNull().default(0),
|
||||
lastDaily: timestamp("last_daily"),
|
||||
dailyStreak: integer("daily_streak").notNull().default(0),
|
||||
createdAt: timestamp("created_at").defaultNow(),
|
||||
});
|
||||
|
||||
export const transactions = pgTable("transactions", {
|
||||
transactionId: serial("transaction_id").primaryKey().notNull(),
|
||||
fromUserId: text("from_user_id").references(() => users.userId),
|
||||
toUserId: text("to_user_id").references(() => users.userId),
|
||||
amount: integer("amount").notNull(),
|
||||
occuredAt: timestamp("occured_at").defaultNow(),
|
||||
type: text("type").notNull(),
|
||||
description: text("description"),
|
||||
});
|
||||
38
src/index.ts
Normal file
38
src/index.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
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}`);
|
||||
});
|
||||
|
||||
KyokoClient.on(Events.InteractionCreate, async interaction => {
|
||||
if (!interaction.isChatInputCommand()) return;
|
||||
|
||||
const command = KyokoClient.commands.get(interaction.commandName);
|
||||
|
||||
if (!command) {
|
||||
console.error(`No command matching ${interaction.commandName} was found.`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await command.execute(interaction);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
if (interaction.replied || interaction.deferred) {
|
||||
await interaction.followUp({ content: 'There was an error while executing this command!', ephemeral: true });
|
||||
} else {
|
||||
await interaction.reply({ content: 'There was an error while executing this command!', ephemeral: true });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// login with the token from .env
|
||||
if (!env.DISCORD_BOT_TOKEN) {
|
||||
throw new Error("❌ DISCORD_BOT_TOKEN is not set in environment variables.");
|
||||
}
|
||||
KyokoClient.login(env.DISCORD_BOT_TOKEN);
|
||||
9
src/lib/DrizzleClient.ts
Normal file
9
src/lib/DrizzleClient.ts
Normal file
@@ -0,0 +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 = env.DATABASE_URL;
|
||||
const postgres = new SQL(connectionString);
|
||||
|
||||
export const DrizzleClient = drizzle(postgres, { schema });
|
||||
120
src/lib/KyokoClient.ts
Normal file
120
src/lib/KyokoClient.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
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 {
|
||||
|
||||
commands: Collection<string, Command>;
|
||||
|
||||
constructor({ intents }: { intents: number[] }) {
|
||||
super({ intents });
|
||||
this.commands = new Collection<string, Command>();
|
||||
}
|
||||
|
||||
async loadCommands(reload: boolean = false) {
|
||||
if (reload) {
|
||||
this.commands.clear();
|
||||
console.log("♻️ Reloading commands...");
|
||||
}
|
||||
|
||||
const commandsPath = join(import.meta.dir, '../commands');
|
||||
await this.readCommandsRecursively(commandsPath, reload);
|
||||
}
|
||||
|
||||
private async readCommandsRecursively(dir: string, reload: boolean = false) {
|
||||
try {
|
||||
const files = await readdir(dir, { withFileTypes: true });
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = join(dir, file.name);
|
||||
|
||||
if (file.isDirectory()) {
|
||||
await this.readCommandsRecursively(filePath, reload);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!file.name.endsWith('.ts') && !file.name.endsWith('.js')) continue;
|
||||
|
||||
try {
|
||||
const importPath = reload ? `${filePath}?t=${Date.now()}` : filePath;
|
||||
const commandModule = await import(importPath);
|
||||
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}`);
|
||||
} else {
|
||||
console.warn(`⚠️ Skipping invalid command in ${file.name}`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`❌ Failed to load command from ${filePath}:`, error);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error reading directory ${dir}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
private isValidCommand(command: any): command is Command {
|
||||
return command && typeof command === 'object' && 'data' in command && 'execute' in command;
|
||||
}
|
||||
|
||||
async deployCommands() {
|
||||
// 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 = 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.`);
|
||||
|
||||
let data;
|
||||
if (guildId) {
|
||||
console.log(`Registering commands to guild: ${guildId}`);
|
||||
data = await rest.put(
|
||||
Routes.applicationGuildCommands(clientId, guildId),
|
||||
{ body: commandsData },
|
||||
);
|
||||
// Clear global commands to avoid duplicates
|
||||
await rest.put(Routes.applicationCommands(clientId), { body: [] });
|
||||
} else {
|
||||
console.log('Registering commands globally');
|
||||
data = await rest.put(
|
||||
Routes.applicationCommands(clientId),
|
||||
{ body: commandsData },
|
||||
);
|
||||
}
|
||||
|
||||
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] });
|
||||
17
src/lib/env.ts
Normal file
17
src/lib/env.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { z } from "zod";
|
||||
|
||||
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"),
|
||||
});
|
||||
|
||||
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;
|
||||
6
src/lib/types.ts
Normal file
6
src/lib/types.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import type { ChatInputCommandInteraction, SlashCommandBuilder, SlashCommandOptionsOnlyBuilder, SlashCommandSubcommandsOnlyBuilder } from "discord.js";
|
||||
|
||||
export interface Command {
|
||||
data: SlashCommandBuilder | SlashCommandOptionsOnlyBuilder | SlashCommandSubcommandsOnlyBuilder;
|
||||
execute: (interaction: ChatInputCommandInteraction) => Promise<void> | void;
|
||||
}
|
||||
11
src/lib/utils.ts
Normal file
11
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;
|
||||
}
|
||||
12
src/modules/economy/economy.service.ts
Normal file
12
src/modules/economy/economy.service.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { DrizzleClient } from "@lib/DrizzleClient";
|
||||
import { users } from "@/db/schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
|
||||
export async function getUserBalance(userId: string) {
|
||||
const user = await DrizzleClient.query.users.findFirst({ where: eq(users.userId, userId) });
|
||||
return user?.balance ?? 0;
|
||||
}
|
||||
|
||||
export async function setUserBalance(userId: string, balance: number) {
|
||||
await DrizzleClient.update(users).set({ balance }).where(eq(users.userId, userId));
|
||||
}
|
||||
11
src/modules/users/users.service.ts
Normal file
11
src/modules/users/users.service.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { DrizzleClient } from "@lib/DrizzleClient";
|
||||
import { users } from "@/db/schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
|
||||
export async function getUserById(userId: string) {
|
||||
return await DrizzleClient.query.users.findFirst({ where: eq(users.userId, userId) });
|
||||
}
|
||||
|
||||
export async function createUser(userId: string) {
|
||||
return (await DrizzleClient.insert(users).values({ userId }).returning())[0]!;
|
||||
}
|
||||
Reference in New Issue
Block a user