From a07d554b416e5f092aa9da6b6ce7acded13a7aa9 Mon Sep 17 00:00:00 2001 From: HerozDotExe Date: Wed, 18 Jun 2025 19:23:32 +0200 Subject: [PATCH] chore: init --- .env.example | 3 + .gitignore | 1 + README.md | 3 + commands.ts | 27 +++++++ commands/template.ts | 10 +++ deno.json | 6 ++ deno.lock | 146 ++++++++++++++++++++++++++++++++++++++ main.ts | 64 +++++++++++++++++ scripts/deployCommands.ts | 42 +++++++++++ types.ts | 8 +++ 10 files changed, 310 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 README.md create mode 100644 commands.ts create mode 100644 commands/template.ts create mode 100644 deno.json create mode 100644 deno.lock create mode 100644 main.ts create mode 100644 scripts/deployCommands.ts create mode 100644 types.ts diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..891f15c --- /dev/null +++ b/.env.example @@ -0,0 +1,3 @@ +token= +clientId= +guildId= \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2eea525 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.env \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..9369178 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# CharCord + +Talk to AIs from character.ai directly into a discord voice channel. \ No newline at end of file diff --git a/commands.ts b/commands.ts new file mode 100644 index 0000000..9cbefc4 --- /dev/null +++ b/commands.ts @@ -0,0 +1,27 @@ +import * as path from "jsr:@std/path"; + +export async function gatherCommandsPaths(): Promise { + const commandsPath: string[] = []; + const foldersPath = path.join(import.meta.dirname!, "commands"); + const commandsRoot = Deno.readDir(foldersPath); + + for await (const entry of commandsRoot) { + if (entry.isDirectory) { + const commandsFolderPath = path.join(foldersPath, entry.name); + const commandsFiles = ( + await Array.fromAsync(Deno.readDir(commandsFolderPath)) + ).filter((file) => file.name.endsWith(".js")); + for (const file of commandsFiles) { + const commandPath = path.join(commandsFolderPath, file.name); + + commandsPath.push(commandPath); + } + } else if (entry.name !== "template.ts") { + const commandPath = path.join(foldersPath, entry.name); + + commandsPath.push(commandPath); + } + } + + return commandsPath; +} diff --git a/commands/template.ts b/commands/template.ts new file mode 100644 index 0000000..4db9d9a --- /dev/null +++ b/commands/template.ts @@ -0,0 +1,10 @@ +import { CacheType, ChatInputCommandInteraction, SlashCommandBuilder } from "npm:discord.js"; + +export default { + data: new SlashCommandBuilder() + .setName("template") + .setDescription("Template command"), + async execute(interaction: ChatInputCommandInteraction) { + await interaction.reply("Pong!"); + }, +}; diff --git a/deno.json b/deno.json new file mode 100644 index 0000000..12efbc8 --- /dev/null +++ b/deno.json @@ -0,0 +1,6 @@ +{ + "tasks": { + "dev": "deno run --watch --env-file --allow-all main.ts", + "deployCommands": "deno run --env-file --allow-all scripts/deployCommands.ts" + } +} diff --git a/deno.lock b/deno.lock new file mode 100644 index 0000000..1eaaa74 --- /dev/null +++ b/deno.lock @@ -0,0 +1,146 @@ +{ + "version": "4", + "specifiers": { + "jsr:@std/path@*": "1.1.0", + "npm:@types/node@*": "22.12.0", + "npm:discord.js@*": "14.20.0" + }, + "jsr": { + "@std/path@1.1.0": { + "integrity": "ddc94f8e3c275627281cbc23341df6b8bcc874d70374f75fec2533521e3d6886" + } + }, + "npm": { + "@discordjs/builders@1.11.2": { + "integrity": "sha512-F1WTABdd8/R9D1icJzajC4IuLyyS8f3rTOz66JsSI3pKvpCAtsMBweu8cyNYsIyvcrKAVn9EPK+Psoymq+XC0A==", + "dependencies": [ + "@discordjs/formatters", + "@discordjs/util", + "@sapphire/shapeshift", + "discord-api-types", + "fast-deep-equal", + "ts-mixer", + "tslib" + ] + }, + "@discordjs/collection@1.5.3": { + "integrity": "sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ==" + }, + "@discordjs/collection@2.1.1": { + "integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==" + }, + "@discordjs/formatters@0.6.1": { + "integrity": "sha512-5cnX+tASiPCqCWtFcFslxBVUaCetB0thvM/JyavhbXInP1HJIEU+Qv/zMrnuwSsX3yWH2lVXNJZeDK3EiP4HHg==", + "dependencies": [ + "discord-api-types" + ] + }, + "@discordjs/rest@2.5.1": { + "integrity": "sha512-Tg9840IneBcbrAjcGaQzHUJWFNq1MMWZjTdjJ0WS/89IffaNKc++iOvffucPxQTF/gviO9+9r8kEPea1X5J2Dw==", + "dependencies": [ + "@discordjs/collection@2.1.1", + "@discordjs/util", + "@sapphire/async-queue", + "@sapphire/snowflake", + "@vladfrangu/async_event_emitter", + "discord-api-types", + "magic-bytes.js", + "tslib", + "undici" + ] + }, + "@discordjs/util@1.1.1": { + "integrity": "sha512-eddz6UnOBEB1oITPinyrB2Pttej49M9FZQY8NxgEvc3tq6ZICZ19m70RsmzRdDHk80O9NoYN/25AqJl8vPVf/g==" + }, + "@discordjs/ws@1.2.3": { + "integrity": "sha512-wPlQDxEmlDg5IxhJPuxXr3Vy9AjYq5xCvFWGJyD7w7Np8ZGu+Mc+97LCoEc/+AYCo2IDpKioiH0/c/mj5ZR9Uw==", + "dependencies": [ + "@discordjs/collection@2.1.1", + "@discordjs/rest", + "@discordjs/util", + "@sapphire/async-queue", + "@types/ws", + "@vladfrangu/async_event_emitter", + "discord-api-types", + "tslib", + "ws" + ] + }, + "@sapphire/async-queue@1.5.5": { + "integrity": "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg==" + }, + "@sapphire/shapeshift@4.0.0": { + "integrity": "sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg==", + "dependencies": [ + "fast-deep-equal", + "lodash" + ] + }, + "@sapphire/snowflake@3.5.3": { + "integrity": "sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ==" + }, + "@types/node@22.12.0": { + "integrity": "sha512-Fll2FZ1riMjNmlmJOdAyY5pUbkftXslB5DgEzlIuNaiWhXd00FhWxVC/r4yV/4wBb9JfImTu+jiSvXTkJ7F/gA==", + "dependencies": [ + "undici-types" + ] + }, + "@types/ws@8.18.1": { + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "dependencies": [ + "@types/node" + ] + }, + "@vladfrangu/async_event_emitter@2.4.6": { + "integrity": "sha512-RaI5qZo6D2CVS6sTHFKg1v5Ohq/+Bo2LZ5gzUEwZ/WkHhwtGTCB/sVLw8ijOkAUxasZ+WshN/Rzj4ywsABJ5ZA==" + }, + "discord-api-types@0.38.12": { + "integrity": "sha512-vqkRM50N5Zc6OVckAqtSslbUEoXmpN4bd9xq2jkoK9fgO3KNRIOyMMQ7ipqjwjKuAgzWvU6G8bRIcYWaUe1sCA==" + }, + "discord.js@14.20.0": { + "integrity": "sha512-5fRTptK2vpuz+bTuAEUQLSo/3AgCSLHl6Mm9+/ofb+8cbbnjWllhtaqRBq7XcpzlBnfNEugKv8HvCwcOtIHpCg==", + "dependencies": [ + "@discordjs/builders", + "@discordjs/collection@1.5.3", + "@discordjs/formatters", + "@discordjs/rest", + "@discordjs/util", + "@discordjs/ws", + "@sapphire/snowflake", + "discord-api-types", + "fast-deep-equal", + "lodash.snakecase", + "magic-bytes.js", + "tslib", + "undici" + ] + }, + "fast-deep-equal@3.1.3": { + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "lodash.snakecase@4.1.1": { + "integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==" + }, + "lodash@4.17.21": { + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "magic-bytes.js@1.12.1": { + "integrity": "sha512-ThQLOhN86ZkJ7qemtVRGYM+gRgR8GEXNli9H/PMvpnZsE44Xfh3wx9kGJaldg314v85m+bFW6WBMaVHJc/c3zA==" + }, + "ts-mixer@6.0.4": { + "integrity": "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA==" + }, + "tslib@2.8.1": { + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, + "undici-types@6.20.0": { + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==" + }, + "undici@6.21.3": { + "integrity": "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==" + }, + "ws@8.18.2": { + "integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==" + } + } +} diff --git a/main.ts b/main.ts new file mode 100644 index 0000000..6ace994 --- /dev/null +++ b/main.ts @@ -0,0 +1,64 @@ +import { + Client as _client, + ClientOptions, + Collection, + Events, + GatewayIntentBits, + MessageFlags, +} from "npm:discord.js"; +const token = Deno.env.get("token"); +import { Command } from "./types.ts"; +import { gatherCommandsPaths } from "./commands.ts"; + + +class Client extends _client { + commands: Collection; + constructor(options: ClientOptions) { + super(options); + this.commands = new Collection(); + } +} + +const client = new Client({ intents: [GatewayIntentBits.Guilds] }); + +async function register(commandPath: string) { + const { default: command }: { default: Command } = await import(commandPath); + if ("data" in command && "execute" in command) { + client.commands.set(command.data.name, command); + console.log(`Registered ${command.data.name}.`); + } else { + console.log( + `[WARNING] The command at ${commandPath} is missing a required "data" or "execute" property.` + ); + } +} + +(await gatherCommandsPaths()).forEach(register) + +client.once(Events.ClientReady, (readyClient) => { + console.log(`Ready! Logged in as ${readyClient.user.tag}`); +}); + +client.on(Events.InteractionCreate, async interaction => { + if (!interaction.isChatInputCommand()) return; + + const command = client.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!', flags: MessageFlags.Ephemeral }); + } else { + await interaction.reply({ content: 'There was an error while executing this command!', flags: MessageFlags.Ephemeral }); + } + } +}); + +client.login(token); diff --git a/scripts/deployCommands.ts b/scripts/deployCommands.ts new file mode 100644 index 0000000..0df4752 --- /dev/null +++ b/scripts/deployCommands.ts @@ -0,0 +1,42 @@ +import { REST, RESTPostAPIChatInputApplicationCommandsJSONBody, Routes } from "npm:discord.js"; +const { clientId, guildId, token } = Deno.env.toObject(); +import { Command } from "../types.ts"; +import { gatherCommandsPaths } from "../commands.ts"; + +const commands: RESTPostAPIChatInputApplicationCommandsJSONBody[] = []; +async function register(commandPath: string) { + const { default: command }: { default: Command } = await import(commandPath); + if ("data" in command && "execute" in command) { + commands.push(command.data.toJSON()); + console.log(`Deploying ${command.data.name}...`); + } else { + console.log( + `[WARNING] The command at ${commandPath} is missing a required "data" or "execute" property.` + ); + } +} + +for (const commandPath of await gatherCommandsPaths()) { + await register(commandPath) +} + +const rest = new REST().setToken(token); + +(async () => { + try { + console.log( + `Started refreshing ${commands.length} application (/) commands.` + ); + + await rest.put( + Routes.applicationGuildCommands(clientId, guildId), + { body: commands } + ); + + console.log( + `Successfully reloaded ${commands.length} application (/) commands.` + ); + } catch (error) { + console.error(error); + } +})(); diff --git a/types.ts b/types.ts new file mode 100644 index 0000000..fbb5342 --- /dev/null +++ b/types.ts @@ -0,0 +1,8 @@ +import { CacheType, ChatInputCommandInteraction, SlashCommandBuilder } from "npm:discord.js"; + +export type Command = { + data: SlashCommandBuilder; + execute: ( + interaction: ChatInputCommandInteraction + ) => Promise; +};