tangled
alpha
login
or
join now
thevoid.cafe
/
voidy
0
fork
atom
A powerful and extendable Discord bot, with it's own module system :3
thevoid.cafe/projects/voidy
0
fork
atom
overview
issues
pulls
pipelines
✨ Upload latest state as of 2025-08-01
thevoid.cafe
7 months ago
08a04c23
8f95a629
+115
-22
7 changed files
expand all
collapse all
unified
split
deno.json
src
core
client.ts
registry.ts
types.ts
features
utility
commands.ts
index.ts
main.ts
+8
-8
deno.json
···
1
1
{
2
2
-
"tasks": {
3
3
-
"dev": "deno run --watch --env-file --allow-env --allow-read --allow-net --unstable-cron src/main.ts"
4
4
-
},
5
5
-
"imports": {
6
6
-
"@std/fs": "jsr:@std/fs",
7
7
-
"@std/path": "jsr:@std/path",
8
8
-
"discord.js": "npm:discord.js"
9
9
-
}
2
2
+
"tasks": {
3
3
+
"dev": "deno run --watch --env-file --allow-env --allow-read --allow-net --unstable-cron src/main.ts"
4
4
+
},
5
5
+
"imports": {
6
6
+
"@std/fs": "jsr:@std/fs",
7
7
+
"@std/path": "jsr:@std/path",
8
8
+
"discord.js": "npm:discord.js"
9
9
+
}
10
10
}
+3
-5
src/core/client.ts
···
1
1
-
import { Client, GatewayIntentBits } from "discord.js";
1
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
7
-
constructor() {
8
8
-
super({
9
9
-
intents: [GatewayIntentBits.Guilds],
10
10
-
});
7
7
+
constructor(options: ClientOptions) {
8
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
34
-
const handler = feature?.buttonHandlers?.get(buttonId);
34
34
+
const buttonHandler = feature?.buttonHandlers?.get(buttonId);
35
35
36
36
-
if (feature && handler) {
36
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
42
-
return await handler(interaction, context);
42
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
3
-
CommandInteraction,
3
3
+
ChatInputCommandInteraction,
4
4
SlashCommandBuilder,
5
5
+
SlashCommandOptionsOnlyBuilder,
5
6
} from "discord.js";
6
7
import { VoidyClient } from "./client.ts";
7
8
8
9
export interface Command {
9
9
-
data: SlashCommandBuilder;
10
10
+
data: SlashCommandBuilder | SlashCommandOptionsOnlyBuilder;
10
11
execute: (
11
11
-
interaction: CommandInteraction,
12
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
4
+
ChatInputCommandInteraction,
4
5
CommandInteraction,
5
6
ContainerBuilder,
6
7
MessageFlags,
···
37
38
});
38
39
},
39
40
};
41
41
+
42
42
+
export const uploadCommand: Command = {
43
43
+
data: new SlashCommandBuilder()
44
44
+
.setName("upload")
45
45
+
.setDescription("Uploads an image to the contest API")
46
46
+
.addAttachmentOption((option) =>
47
47
+
option
48
48
+
.setName("image")
49
49
+
.setDescription("The image to upload")
50
50
+
.setRequired(true)
51
51
+
)
52
52
+
.addIntegerOption((option) =>
53
53
+
option
54
54
+
.setName("contest_id")
55
55
+
.setDescription("The contest ID")
56
56
+
.setRequired(true)
57
57
+
)
58
58
+
.addStringOption((option) =>
59
59
+
option
60
60
+
.setName("participant_name")
61
61
+
.setDescription("The name of the participant")
62
62
+
.setRequired(true)
63
63
+
)
64
64
+
.addStringOption((option) =>
65
65
+
option
66
66
+
.setName("participant_email")
67
67
+
.setDescription("The email of the participant")
68
68
+
.setRequired(true)
69
69
+
),
70
70
+
71
71
+
execute: async (
72
72
+
interaction: ChatInputCommandInteraction,
73
73
+
_context: FeatureContext,
74
74
+
) => {
75
75
+
const attachment = interaction.options.getAttachment("image", true);
76
76
+
const contestId = interaction.options.getInteger("contest_id", true);
77
77
+
const participantName = interaction.options.getString(
78
78
+
"participant_name",
79
79
+
true,
80
80
+
);
81
81
+
const participantEmail = interaction.options.getString(
82
82
+
"participant_email",
83
83
+
true,
84
84
+
);
85
85
+
86
86
+
await interaction.deferReply({ ephemeral: true });
87
87
+
88
88
+
try {
89
89
+
const fileResponse = await fetch(attachment.url);
90
90
+
const fileBuffer = await fileResponse.arrayBuffer();
91
91
+
92
92
+
const form = new FormData();
93
93
+
form.append("image", new Blob([fileBuffer]));
94
94
+
form.append("contest_id", contestId.toString());
95
95
+
form.append("participant_name", participantName);
96
96
+
form.append("participant_email", participantEmail);
97
97
+
98
98
+
const res = await fetch("http://localhost:8000/api/upload-drawing", {
99
99
+
method: "POST",
100
100
+
body: form,
101
101
+
});
102
102
+
103
103
+
if (!res.ok) {
104
104
+
const error = await res.text();
105
105
+
console.log(error);
106
106
+
107
107
+
await interaction.editReply({
108
108
+
content: `❌ Upload failed, check console for details.`,
109
109
+
});
110
110
+
111
111
+
return;
112
112
+
}
113
113
+
114
114
+
const result = await res.json();
115
115
+
await interaction.editReply({
116
116
+
content: `✅ Image uploaded successfully!
117
117
+
Participant ID: ${result.participant_id ?? "(not returned)"}
118
118
+
Submission ID: ${result.submission_id ?? "(not returned)"}`,
119
119
+
});
120
120
+
} catch (err) {
121
121
+
console.error(err);
122
122
+
await interaction.editReply({
123
123
+
content: `❌ An error occurred during upload.`,
124
124
+
});
125
125
+
126
126
+
return;
127
127
+
}
128
128
+
},
129
129
+
};
+2
-2
src/features/utility/index.ts
···
1
1
import type { Feature } from "../../core/types.ts";
2
2
-
import { pingCommand } from "./commands.ts";
2
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
9
-
commands: [pingCommand],
9
9
+
commands: [pingCommand, uploadCommand],
10
10
buttonHandlers: new Map([
11
11
["refresh", refreshButton],
12
12
]),
+5
-1
src/main.ts
···
1
1
+
import { GatewayIntentBits } from "discord.js";
1
2
import { VoidyClient } from "./core/client.ts";
2
3
3
3
-
const client = new VoidyClient();
4
4
+
const client = new VoidyClient({
5
5
+
intents: [GatewayIntentBits.Guilds],
6
6
+
});
7
7
+
4
8
await client.start(Deno.env.get("BOT_TOKEN")!);