init: initial commit

This commit is contained in:
syntaxbullet
2025-12-02 20:53:49 +01:00
commit 25c7ba46f0
17 changed files with 578 additions and 0 deletions

19
app/src/db/schema.ts Normal file
View 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().unique(),
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().unique(),
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"),
});

34
app/src/index.ts Normal file
View File

@@ -0,0 +1,34 @@
import { Events } from "discord.js";
import { KyokoClient } from "@lib/KyokoClient";
// 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
KyokoClient.login(process.env.DISCORD_BOT_TOKEN);

View File

@@ -0,0 +1,8 @@
import { drizzle } from "drizzle-orm/bun-sql";
import { SQL } from "bun";
import * as schema from "@db/schema";
const connectionString = process.env.DATABASE_URL || "postgres://kyoko:kyoko@localhost:5432/kyoko";
const postgres = new SQL(connectionString);
export const DrizzleClient = drizzle(postgres, { schema });

View File

@@ -0,0 +1,89 @@
import { Client as DiscordClient, Collection, GatewayIntentBits, REST, Routes, SlashCommandBuilder } from "discord.js";
import { readdir } from "node:fs/promises";
import { join } from "node:path";
import type { Command } from "@lib/types";
class Client extends DiscordClient {
commands: Collection<string, Command>;
constructor({ intents }: { intents: number[] }) {
super({ intents });
this.commands = new Collection<string, Command>();
}
async loadCommands() {
const commandsPath = join(import.meta.dir, '../commands');
await this.readCommandsRecursively(commandsPath);
}
private async readCommandsRecursively(dir: string) {
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);
continue;
}
if (!file.name.endsWith('.ts') && !file.name.endsWith('.js')) continue;
try {
const commandModule = await import(filePath);
const commands = Object.values(commandModule);
for (const command of commands) {
if (this.isValidCommand(command)) {
this.commands.set(command.data.name, command);
console.log(`Loaded command: ${command.data.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() {
const rest = new REST().setToken(this.token!);
const commandsData = this.commands.map(c => c.data.toJSON());
const guildId = process.env.DISCORD_GUILD_ID;
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(this.user!.id, guildId),
{ body: commandsData },
);
// Clear global commands to avoid duplicates
await rest.put(Routes.applicationCommands(this.user!.id), { body: [] });
} else {
console.log('Registering commands globally');
data = await rest.put(
Routes.applicationCommands(this.user!.id),
{ body: commandsData },
);
}
console.log(`Successfully reloaded ${(data as any).length} application (/) commands.`);
} catch (error) {
console.error(error);
}
}
}
export const KyokoClient = new Client({ intents: [GatewayIntentBits.Guilds, GatewayIntentBits.MessageContent, GatewayIntentBits.GuildMessages] });

6
app/src/lib/types.ts Normal file
View 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;
}

View 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]!;
}

10
app/src/scripts/deploy.ts Normal file
View File

@@ -0,0 +1,10 @@
import { KyokoClient } from "@lib/KyokoClient";
console.log("Loading commands...");
await KyokoClient.loadCommands();
console.log("Deploying commands...");
await KyokoClient.deployCommands();
console.log("Done!");
process.exit(0);