feat(bot): add modals

This commit is contained in:
xHyroM 2023-04-08 14:05:40 +02:00
parent 5cb18619ad
commit ae5e39d08d
No known key found for this signature in database
GPG key ID: BE0423F386C436AA
11 changed files with 216 additions and 33 deletions

View file

@ -25,6 +25,7 @@ Promise.all([
charset: "utf8", charset: "utf8",
minify: !dev, minify: !dev,
watch: watch, watch: watch,
external: ["node:events"],
}), }),
]) ])
.catch((err) => { .catch((err) => {

View file

@ -1,17 +1,37 @@
import { Command } from "../structs/Command"; import { Command } from "../structs/Command";
import { ChannelType } from "discord-api-types/v10"; import { MessageFlags, TextInputStyle } from "discord-api-types/v10";
import { ActionRowBuilder, ChannelSelectMenuBuilder } from "builders"; import { ActionRowBuilder, TextInputBuilder } from "builders";
new Command({ new Command({
name: "setup", name: "setup",
flags: MessageFlags.Ephemeral,
acknowledge: false,
run: async (ctx) => { run: async (ctx) => {
await ctx.editReply({ return ctx.returnModal({
content: "Setup", title: "Setup",
custom_id: "test",
components: [
new ActionRowBuilder<TextInputBuilder>()
.addComponents(
new TextInputBuilder()
.setLabel("ASDSAD")
.setCustomId("prefix")
.setPlaceholder("Prefix")
.setStyle(TextInputStyle.Paragraph)
.setRequired(true),
)
.toJSON(),
],
});
/**
* await ctx.editReply({
content: "Select the channel to which the panel will be sent.",
components: [ components: [
new ActionRowBuilder<ChannelSelectMenuBuilder>() new ActionRowBuilder<ChannelSelectMenuBuilder>()
.addComponents( .addComponents(
new ChannelSelectMenuBuilder() new ChannelSelectMenuBuilder()
.setCustomId("channel") .setCustomId("setup:part-channel")
.addChannelTypes( .addChannelTypes(
ChannelType.GuildAnnouncement, ChannelType.GuildAnnouncement,
ChannelType.GuildText, ChannelType.GuildText,
@ -20,5 +40,6 @@ new Command({
.toJSON(), .toJSON(),
], ],
}); });
*/
}, },
}); });

View file

@ -0,0 +1,35 @@
import { ActionRowBuilder, RoleSelectMenuBuilder } from "builders";
import { Component } from "../structs/Component";
import { MessageFlags } from "discord-api-types/v10";
new Component({
id: "setup:part-channel",
flags: MessageFlags.Ephemeral,
run: async (ctx) => {
await ctx.editReply({
content: "Select the roles that will be available in the menu.",
components: [
new ActionRowBuilder<RoleSelectMenuBuilder>()
.addComponents(
new RoleSelectMenuBuilder()
.setCustomId("setup:part-roles")
.setPlaceholder("Select roles")
.setMinValues(1)
.setMaxValues(25),
)
.toJSON(),
],
});
},
});
new Component({
id: "setup:part-roles",
flags: MessageFlags.Ephemeral,
run: async (ctx) => {
await ctx.editReply({
content: "done",
components: [],
});
},
});

View file

@ -1,18 +1,21 @@
import "./commands/setup"; import "./commands/setup";
import "./components/setup";
import "./modals/setup";
import { import {
APIApplicationCommandInteraction, APIApplicationCommandInteraction,
APIMessageComponentInteraction, APIMessageComponentInteraction,
APIModalSubmitInteraction,
APIPingInteraction, APIPingInteraction,
InteractionResponseType, InteractionResponseType,
InteractionType, InteractionType,
} from "discord-api-types/v10"; } from "discord-api-types/v10";
import { COMMANDS, COMPONENTS } from "./registers"; import { COMMANDS, COMPONENTS, MODALS } from "./registers";
import { verify } from "./utils/verify"; import { verify } from "./utils/verify";
import respond from "./utils/respond"; import respond from "./utils/respond";
import { CommandContext } from "./structs/contexts/CommandContext"; import { CommandContext } from "./structs/contexts/CommandContext";
import { ComponentContext } from "./structs/contexts/ComponentContext"; import { ComponentContext } from "./structs/contexts/ComponentContext";
import { ModalContext } from "./structs/contexts/ModalContext";
console.log(COMMANDS);
export default { export default {
fetch: async (request: Request, env: Env) => { fetch: async (request: Request, env: Env) => {
@ -27,6 +30,7 @@ export default {
const interaction = (await request.json()) as const interaction = (await request.json()) as
| APIPingInteraction | APIPingInteraction
| APIApplicationCommandInteraction | APIApplicationCommandInteraction
| APIModalSubmitInteraction
| APIMessageComponentInteraction; | APIMessageComponentInteraction;
if (interaction.type === InteractionType.Ping) if (interaction.type === InteractionType.Ping)
@ -34,34 +38,66 @@ export default {
type: InteractionResponseType.Pong, type: InteractionResponseType.Pong,
}); });
if (interaction.type === InteractionType.ApplicationCommand) { switch (interaction.type) {
const command = COMMANDS.find( case InteractionType.ApplicationCommand: {
(cmd) => cmd.name === interaction.data.name, const command = COMMANDS.find(
); (cmd) => cmd.name === interaction.data.name,
);
if (!command) return new Response("Unknown command", { status: 404 }); if (!command) return new Response("Unknown command", { status: 404 });
try { try {
return respond({ if (command.acknowledge)
type: InteractionResponseType.DeferredChannelMessageWithSource, return respond({
}); type: InteractionResponseType.DeferredChannelMessageWithSource,
} finally { data: {
command.run(new CommandContext(interaction, env)); flags: command.flags,
},
});
} finally {
if (command.acknowledge)
command.run(new CommandContext(interaction, env));
// rome-ignore lint/correctness/noUnsafeFinally: it works, must do better typings etc...
else return command.run(new CommandContext(interaction, env));
}
break;
} }
} case InteractionType.ModalSubmit: {
const modal = MODALS.find((md) => md.id === interaction.data.custom_id);
const component = COMPONENTS.find( if (!modal) return new Response("Unknown modal", { status: 404 });
(cmp) => cmp.id === interaction.data.custom_id,
);
if (!component) return new Response("Unknown component", { status: 404 }); try {
return respond({
type: InteractionResponseType.DeferredChannelMessageWithSource,
data: {
flags: modal.flags,
},
});
} finally {
modal.run(new ModalContext(interaction, env));
}
}
case InteractionType.MessageComponent: {
const component = COMPONENTS.find(
(cmp) => cmp.id === interaction.data.custom_id,
);
try { if (!component)
return respond({ return new Response("Unknown component", { status: 404 });
type: InteractionResponseType.DeferredChannelMessageWithSource,
}); try {
} finally { return respond({
component.run(new ComponentContext(interaction, env)); type: InteractionResponseType.DeferredChannelMessageWithSource,
data: {
flags: component.flags,
},
});
} finally {
component.run(new ComponentContext(interaction, env));
}
}
} }
}, },
}; };

View file

@ -0,0 +1,8 @@
import { Modal } from "../structs/Modal";
new Modal({
id: "test",
run: async (ctx) => {
// TODO: finish
},
});

View file

@ -1,8 +1,10 @@
import { Command } from "./structs/Command"; import { Command } from "./structs/Command";
import { Component } from "./structs/Component"; import { Component } from "./structs/Component";
import { Modal } from "./structs/Modal";
export const COMMANDS: Command[] = []; export const COMMANDS: Command[] = [];
export const COMPONENTS: Component[] = []; export const COMPONENTS: Component[] = [];
export const MODALS: Modal[] = [];
export const registerCommand = (command: Command) => { export const registerCommand = (command: Command) => {
COMMANDS.push(command); COMMANDS.push(command);
@ -11,3 +13,7 @@ export const registerCommand = (command: Command) => {
export const registerComponent = (component: Component) => { export const registerComponent = (component: Component) => {
COMPONENTS.push(component); COMPONENTS.push(component);
}; };
export const registerModal = (modal: Modal) => {
MODALS.push(modal);
};

View file

@ -1,17 +1,24 @@
import { MessageFlags } from "discord-api-types/v10";
import { registerCommand } from "../registers"; import { registerCommand } from "../registers";
import { CommandContext } from "./contexts/CommandContext"; import { CommandContext } from "./contexts/CommandContext";
interface CommandOptions { interface CommandOptions {
name: string; name: string;
acknowledge?: boolean;
flags?: MessageFlags;
run: (interaction: CommandContext) => void; run: (interaction: CommandContext) => void;
} }
export class Command { export class Command<A> {
public name: string; public name: string;
public run: (interaction: CommandContext) => void; public acknowledge: boolean;
public flags: MessageFlags | undefined;
public run: (interaction: CommandContext) => void | Response;
constructor(options: CommandOptions) { constructor(options: CommandOptions) {
this.name = options.name; this.name = options.name;
this.acknowledge = options.acknowledge ?? true;
this.flags = options.flags;
this.run = options.run; this.run = options.run;
registerCommand(this); registerCommand(this);

View file

@ -1,17 +1,21 @@
import { MessageFlags } from "discord-api-types/v10";
import { registerComponent } from "../registers"; import { registerComponent } from "../registers";
import { ComponentContext } from "./contexts/ComponentContext"; import { ComponentContext } from "./contexts/ComponentContext";
interface ComponentOptions { interface ComponentOptions {
id: string; id: string;
flags?: MessageFlags;
run: (interaction: ComponentContext) => void; run: (interaction: ComponentContext) => void;
} }
export class Component { export class Component {
public id: string; public id: string;
public flags: MessageFlags | undefined;
public run: (interaction: ComponentContext) => void; public run: (interaction: ComponentContext) => void;
constructor(options: ComponentOptions) { constructor(options: ComponentOptions) {
this.id = options.id; this.id = options.id;
this.flags = options.flags;
this.run = options.run; this.run = options.run;
registerComponent(this); registerComponent(this);

View file

@ -0,0 +1,23 @@
import { MessageFlags } from "discord-api-types/v10";
import { registerModal } from "../registers";
import { ModalContext } from "./contexts/ModalContext";
interface ModalOptions {
id: string;
flags?: MessageFlags;
run: (interaction: ModalContext) => void;
}
export class Modal {
public id: string;
public flags: MessageFlags | undefined;
public run: (interaction: ModalContext) => void;
constructor(options: ModalOptions) {
this.id = options.id;
this.flags = options.flags;
this.run = options.run;
registerModal(this);
}
}

View file

@ -1,9 +1,12 @@
import { import {
APIInteraction, APIInteraction,
APIInteractionResponseCallbackData, APIInteractionResponseCallbackData,
APIModalInteractionResponseCallbackData,
InteractionResponseType,
RouteBases, RouteBases,
Routes, Routes,
} from "discord-api-types/v10"; } from "discord-api-types/v10";
import respond from "../../utils/respond";
export class Context { export class Context {
public interaction: APIInteraction; public interaction: APIInteraction;
@ -17,7 +20,7 @@ export class Context {
public async editReply(content: APIInteractionResponseCallbackData) { public async editReply(content: APIInteractionResponseCallbackData) {
return await fetch( return await fetch(
`${RouteBases.api}${Routes.webhookMessage( `${RouteBases.api}${Routes.webhookMessage(
this.interaction.application_id, this.interaction.id,
this.interaction.token, this.interaction.token,
)}`, )}`,
{ {
@ -30,4 +33,31 @@ export class Context {
}, },
); );
} }
public async showModal(content: APIModalInteractionResponseCallbackData) {
return await fetch(
`${RouteBases.api}${Routes.interactionCallback(
this.interaction.id,
this.interaction.token,
)}`,
{
method: "POST",
body: JSON.stringify({
type: InteractionResponseType.Modal,
data: content,
}),
headers: {
"Content-Type": "application/json",
Authorization: `Bot ${this.env.token}`,
},
},
);
}
public async returnModal(content: APIModalInteractionResponseCallbackData) {
return respond({
type: InteractionResponseType.Modal,
data: content,
});
}
} }

View file

@ -0,0 +1,12 @@
import { APIModalSubmitInteraction } from "discord-api-types/v10";
import { Context } from "./Context";
export class ModalContext extends Context {
public interaction: APIModalSubmitInteraction;
constructor(interaction: APIModalSubmitInteraction, env: Env) {
super(interaction, env);
this.interaction = interaction;
}
}