+8
-8
deno.json
+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
+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
+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
+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
+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
+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
+5
-1
src/main.ts