A powerful and extendable Discord bot, with it's own module system :3 thevoid.cafe/projects/voidy

✨ Upload latest state as of 2025-08-01

Jo 08a04c23 8f95a629

Changed files
+115 -22
src
+8 -8
deno.json
··· 1 1 { 2 - "tasks": { 3 - "dev": "deno run --watch --env-file --allow-env --allow-read --allow-net --unstable-cron src/main.ts" 4 - }, 5 - "imports": { 6 - "@std/fs": "jsr:@std/fs", 7 - "@std/path": "jsr:@std/path", 8 - "discord.js": "npm:discord.js" 9 - } 2 + "tasks": { 3 + "dev": "deno run --watch --env-file --allow-env --allow-read --allow-net --unstable-cron src/main.ts" 4 + }, 5 + "imports": { 6 + "@std/fs": "jsr:@std/fs", 7 + "@std/path": "jsr:@std/path", 8 + "discord.js": "npm:discord.js" 9 + } 10 10 }
+3 -5
src/core/client.ts
··· 1 - import { Client, GatewayIntentBits } from "discord.js"; 1 + import { Client, ClientOptions } from "discord.js"; 2 2 import { FeatureRegistry } from "./registry.ts"; 3 3 4 4 export class VoidyClient extends Client { 5 5 public registry: FeatureRegistry; 6 6 7 - constructor() { 8 - super({ 9 - intents: [GatewayIntentBits.Guilds], 10 - }); 7 + constructor(options: ClientOptions) { 8 + super(options); 11 9 12 10 this.registry = new FeatureRegistry(this); 13 11 }
+3 -3
src/core/registry.ts
··· 31 31 if (interaction.isButton()) { 32 32 const [featureId, buttonId] = interaction.customId.split(":"); 33 33 const feature = this.features.get(featureId); 34 - const handler = feature?.buttonHandlers?.get(buttonId); 34 + const buttonHandler = feature?.buttonHandlers?.get(buttonId); 35 35 36 - if (feature && handler) { 36 + if (feature && buttonHandler) { 37 37 const context = { 38 38 client: this.client, 39 39 createCustomId: (id: string) => `${feature.id}:${id}`, 40 40 }; 41 41 42 - return await handler(interaction, context); 42 + return await buttonHandler(interaction, context); 43 43 } 44 44 } 45 45 });
+4 -3
src/core/types.ts
··· 1 1 import { 2 2 ButtonInteraction, 3 - CommandInteraction, 3 + ChatInputCommandInteraction, 4 4 SlashCommandBuilder, 5 + SlashCommandOptionsOnlyBuilder, 5 6 } from "discord.js"; 6 7 import { VoidyClient } from "./client.ts"; 7 8 8 9 export interface Command { 9 - data: SlashCommandBuilder; 10 + data: SlashCommandBuilder | SlashCommandOptionsOnlyBuilder; 10 11 execute: ( 11 - interaction: CommandInteraction, 12 + interaction: ChatInputCommandInteraction, 12 13 context: FeatureContext, 13 14 ) => Promise<void>; 14 15 }
+90
src/features/utility/commands.ts
··· 1 1 import { 2 2 ButtonBuilder, 3 3 ButtonStyle, 4 + ChatInputCommandInteraction, 4 5 CommandInteraction, 5 6 ContainerBuilder, 6 7 MessageFlags, ··· 37 38 }); 38 39 }, 39 40 }; 41 + 42 + export const uploadCommand: Command = { 43 + data: new SlashCommandBuilder() 44 + .setName("upload") 45 + .setDescription("Uploads an image to the contest API") 46 + .addAttachmentOption((option) => 47 + option 48 + .setName("image") 49 + .setDescription("The image to upload") 50 + .setRequired(true) 51 + ) 52 + .addIntegerOption((option) => 53 + option 54 + .setName("contest_id") 55 + .setDescription("The contest ID") 56 + .setRequired(true) 57 + ) 58 + .addStringOption((option) => 59 + option 60 + .setName("participant_name") 61 + .setDescription("The name of the participant") 62 + .setRequired(true) 63 + ) 64 + .addStringOption((option) => 65 + option 66 + .setName("participant_email") 67 + .setDescription("The email of the participant") 68 + .setRequired(true) 69 + ), 70 + 71 + execute: async ( 72 + interaction: ChatInputCommandInteraction, 73 + _context: FeatureContext, 74 + ) => { 75 + const attachment = interaction.options.getAttachment("image", true); 76 + const contestId = interaction.options.getInteger("contest_id", true); 77 + const participantName = interaction.options.getString( 78 + "participant_name", 79 + true, 80 + ); 81 + const participantEmail = interaction.options.getString( 82 + "participant_email", 83 + true, 84 + ); 85 + 86 + await interaction.deferReply({ ephemeral: true }); 87 + 88 + try { 89 + const fileResponse = await fetch(attachment.url); 90 + const fileBuffer = await fileResponse.arrayBuffer(); 91 + 92 + const form = new FormData(); 93 + form.append("image", new Blob([fileBuffer])); 94 + form.append("contest_id", contestId.toString()); 95 + form.append("participant_name", participantName); 96 + form.append("participant_email", participantEmail); 97 + 98 + const res = await fetch("http://localhost:8000/api/upload-drawing", { 99 + method: "POST", 100 + body: form, 101 + }); 102 + 103 + if (!res.ok) { 104 + const error = await res.text(); 105 + console.log(error); 106 + 107 + await interaction.editReply({ 108 + content: `❌ Upload failed, check console for details.`, 109 + }); 110 + 111 + return; 112 + } 113 + 114 + const result = await res.json(); 115 + await interaction.editReply({ 116 + content: `✅ Image uploaded successfully! 117 + Participant ID: ${result.participant_id ?? "(not returned)"} 118 + Submission ID: ${result.submission_id ?? "(not returned)"}`, 119 + }); 120 + } catch (err) { 121 + console.error(err); 122 + await interaction.editReply({ 123 + content: `❌ An error occurred during upload.`, 124 + }); 125 + 126 + return; 127 + } 128 + }, 129 + };
+2 -2
src/features/utility/index.ts
··· 1 1 import type { Feature } from "../../core/types.ts"; 2 - import { pingCommand } from "./commands.ts"; 2 + import { pingCommand, uploadCommand } from "./commands.ts"; 3 3 import { refreshButton } from "./interactions.ts"; 4 4 5 5 const UtilityFeature: Feature = { 6 6 id: "utility", 7 7 name: "Utility Commands", 8 8 9 - commands: [pingCommand], 9 + commands: [pingCommand, uploadCommand], 10 10 buttonHandlers: new Map([ 11 11 ["refresh", refreshButton], 12 12 ]),
+5 -1
src/main.ts
··· 1 + import { GatewayIntentBits } from "discord.js"; 1 2 import { VoidyClient } from "./core/client.ts"; 2 3 3 - const client = new VoidyClient(); 4 + const client = new VoidyClient({ 5 + intents: [GatewayIntentBits.Guilds], 6 + }); 7 + 4 8 await client.start(Deno.env.get("BOT_TOKEN")!);