mirror of
https://github.com/tiramisulabs/seyfert.git
synced 2025-07-03 05:26:07 +00:00
wip: interactions
This commit is contained in:
parent
948cd2f717
commit
971f541f83
36
:x
Normal file
36
:x
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
|
||||||
|
import type { Model } from "../Base.ts";
|
||||||
|
import type { Snowflake } from "../../util/Snowflake.ts";
|
||||||
|
import type { Session } from "../../session/Session.ts";
|
||||||
|
import type { ApplicationCommandTypes, DiscordInteraction } from "../../vendor/external.ts";
|
||||||
|
import { InteractionResponseTypes } from "../../vendor/external.ts";
|
||||||
|
import BaseInteraction from "./BaseInteraction.ts";
|
||||||
|
import * as Routes from "../../util/Routes.ts";
|
||||||
|
|
||||||
|
export class PingInteraction extends BaseInteraction implements Model {
|
||||||
|
constructor(session: Session, data: DiscordInteraction) {
|
||||||
|
super(session, data);
|
||||||
|
this.commandId = data.data!.id;
|
||||||
|
this.commandName = data.data!.name;
|
||||||
|
this.commandType = data.data!.type;
|
||||||
|
this.commandGuildId = data.data!.guild_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
commandId: Snowflake;
|
||||||
|
commandName: string;
|
||||||
|
commandType: ApplicationCommandTypes;
|
||||||
|
commandGuildId?: Snowflake;
|
||||||
|
|
||||||
|
async pong() {
|
||||||
|
await this.session.rest.runMethod<undefined>(
|
||||||
|
this.session.rest,
|
||||||
|
"POST",
|
||||||
|
Routes.INTERACTION_ID_TOKEN(this.id, this.token),
|
||||||
|
{
|
||||||
|
type: InteractionResponseTypes.Pong,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PingInteraction;
|
@ -38,7 +38,7 @@ import Member from "../structures/Member.ts";
|
|||||||
import Message from "../structures/Message.ts";
|
import Message from "../structures/Message.ts";
|
||||||
import User from "../structures/User.ts";
|
import User from "../structures/User.ts";
|
||||||
import Guild from "../structures/guilds/Guild.ts";
|
import Guild from "../structures/guilds/Guild.ts";
|
||||||
import Interaction from "../structures/interactions/Interaction.ts";
|
import InteractionFactory from "../structures/interactions/interactions/InteractionFactory.ts";
|
||||||
|
|
||||||
export type RawHandler<T> = (...args: [Session, number, T]) => void;
|
export type RawHandler<T> = (...args: [Session, number, T]) => void;
|
||||||
export type Handler<T extends unknown[]> = (...args: T) => unknown;
|
export type Handler<T extends unknown[]> = (...args: T) => unknown;
|
||||||
@ -106,12 +106,7 @@ export const GUILD_ROLE_DELETE: RawHandler<DiscordGuildRoleDelete> = (session, _
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const INTERACTION_CREATE: RawHandler<DiscordInteraction> = (session, _shardId, interaction) => {
|
export const INTERACTION_CREATE: RawHandler<DiscordInteraction> = (session, _shardId, interaction) => {
|
||||||
session.unrepliedInteractions.add(BigInt(interaction.id));
|
session.emit("interactionCreate", InteractionFactory.from(session, interaction));
|
||||||
|
|
||||||
// could be improved
|
|
||||||
setTimeout(() => session.unrepliedInteractions.delete(BigInt(interaction.id)), 15 * 60 * 1000);
|
|
||||||
|
|
||||||
session.emit("interactionCreate", new Interaction(session, interaction));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CHANNEL_CREATE: RawHandler<DiscordChannel> = (session, _shardId, channel) => {
|
export const CHANNEL_CREATE: RawHandler<DiscordChannel> = (session, _shardId, channel) => {
|
||||||
|
9
mod.ts
9
mod.ts
@ -37,7 +37,14 @@ export * from "./structures/guilds/InviteGuild.ts";
|
|||||||
|
|
||||||
export * from "./structures/builders/EmbedBuilder.ts";
|
export * from "./structures/builders/EmbedBuilder.ts";
|
||||||
|
|
||||||
export * from "./structures/interactions/Interaction.ts";
|
export * from "./structures/interactions/AutoCompleteInteraction.ts";
|
||||||
|
export * from "./structures/interactions/BaseInteraction.ts";
|
||||||
|
export * from "./structures/interactions/CommandInteraction.ts";
|
||||||
|
export * from "./structures/interactions/CommandInteractionOptionResolver.ts";
|
||||||
|
export * from "./structures/interactions/ComponentInteraction.ts";
|
||||||
|
export * from "./structures/interactions/InteractionFactory.ts";
|
||||||
|
export * from "./structures/interactions/ModalSubmitInteraction.ts";
|
||||||
|
export * from "./structures/interactions/PingInteraction.ts";
|
||||||
|
|
||||||
export * from "./session/Session.ts";
|
export * from "./session/Session.ts";
|
||||||
|
|
||||||
|
@ -39,8 +39,6 @@ export class Session extends EventEmitter {
|
|||||||
rest: ReturnType<typeof createRestManager>;
|
rest: ReturnType<typeof createRestManager>;
|
||||||
gateway: ReturnType<typeof createGatewayManager>;
|
gateway: ReturnType<typeof createGatewayManager>;
|
||||||
|
|
||||||
unrepliedInteractions: Set<bigint> = new Set();
|
|
||||||
|
|
||||||
#botId: Snowflake;
|
#botId: Snowflake;
|
||||||
#applicationId?: Snowflake;
|
#applicationId?: Snowflake;
|
||||||
|
|
||||||
|
40
structures/interactions/AutoCompleteInteraction.ts
Normal file
40
structures/interactions/AutoCompleteInteraction.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
|
||||||
|
import type { Model } from "../Base.ts";
|
||||||
|
import type { Snowflake } from "../../util/Snowflake.ts";
|
||||||
|
import type { Session } from "../../session/Session.ts";
|
||||||
|
import type { ApplicationCommandTypes, DiscordInteraction, InteractionTypes } from "../../vendor/external.ts";
|
||||||
|
import type { ApplicationCommandOptionChoice } from "./BaseInteraction.ts";
|
||||||
|
import { InteractionResponseTypes } from "../../vendor/external.ts";
|
||||||
|
import BaseInteraction from "./BaseInteraction.ts";
|
||||||
|
import * as Routes from "../../util/Routes.ts";
|
||||||
|
|
||||||
|
export class AutoCompleteInteraction extends BaseInteraction implements Model {
|
||||||
|
constructor(session: Session, data: DiscordInteraction) {
|
||||||
|
super(session, data);
|
||||||
|
this.type = data.type as number;
|
||||||
|
this.commandId = data.data!.id;
|
||||||
|
this.commandName = data.data!.name;
|
||||||
|
this.commandType = data.data!.type;
|
||||||
|
this.commandGuildId = data.data!.guild_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
override type: InteractionTypes.ApplicationCommandAutocomplete;
|
||||||
|
commandId: Snowflake;
|
||||||
|
commandName: string;
|
||||||
|
commandType: ApplicationCommandTypes;
|
||||||
|
commandGuildId?: Snowflake;
|
||||||
|
|
||||||
|
async respond(choices: ApplicationCommandOptionChoice[]) {
|
||||||
|
await this.session.rest.runMethod<undefined>(
|
||||||
|
this.session.rest,
|
||||||
|
"POST",
|
||||||
|
Routes.INTERACTION_ID_TOKEN(this.id, this.token),
|
||||||
|
{
|
||||||
|
data: { choices },
|
||||||
|
type: InteractionResponseTypes.ApplicationCommandAutocompleteResult,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AutoCompleteInteraction;
|
84
structures/interactions/BaseInteraction.ts
Normal file
84
structures/interactions/BaseInteraction.ts
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import type { Model } from "../Base.ts";
|
||||||
|
import type { Session } from "../../session/Session.ts";
|
||||||
|
import type { DiscordInteraction } from "../../vendor/external.ts";
|
||||||
|
import type CommandInteraction from "./CommandInteraction.ts";
|
||||||
|
import type PingInteraction from "./PingInteraction.ts";
|
||||||
|
import { InteractionTypes } from "../../vendor/external.ts";
|
||||||
|
import { Snowflake } from "../../util/Snowflake.ts";
|
||||||
|
import User from "../User.ts";
|
||||||
|
import Member from "../Member.ts";
|
||||||
|
import Permsisions from "../Permissions.ts";
|
||||||
|
|
||||||
|
export abstract class BaseInteraction implements Model {
|
||||||
|
constructor(session: Session, data: DiscordInteraction) {
|
||||||
|
this.session = session;
|
||||||
|
this.id = data.id;
|
||||||
|
this.token = data.token;
|
||||||
|
this.type = data.type;
|
||||||
|
this.guildId = data.guild_id;
|
||||||
|
this.channelId = data.channel_id;
|
||||||
|
this.applicationId = data.application_id;
|
||||||
|
this.version = data.version;
|
||||||
|
|
||||||
|
// @ts-expect-error: vendor error
|
||||||
|
const perms = data.app_permissions as string;
|
||||||
|
|
||||||
|
if (perms) {
|
||||||
|
this.appPermissions = new Permsisions(BigInt(perms));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data.guild_id) {
|
||||||
|
this.user = new User(session, data.user!);
|
||||||
|
} else {
|
||||||
|
this.member = new Member(session, data.member!, data.guild_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly session: Session;
|
||||||
|
readonly id: Snowflake;
|
||||||
|
readonly token: string;
|
||||||
|
|
||||||
|
type: InteractionTypes;
|
||||||
|
guildId?: Snowflake;
|
||||||
|
channelId?: Snowflake;
|
||||||
|
applicationId?: Snowflake;
|
||||||
|
user?: User;
|
||||||
|
member?: Member;
|
||||||
|
appPermissions?: Permsisions;
|
||||||
|
|
||||||
|
readonly version: 1;
|
||||||
|
|
||||||
|
get createdTimestamp() {
|
||||||
|
return Snowflake.snowflakeToTimestamp(this.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
get createdAt() {
|
||||||
|
return new Date(this.createdTimestamp);
|
||||||
|
}
|
||||||
|
|
||||||
|
isCommand(): this is CommandInteraction {
|
||||||
|
return this.type === InteractionTypes.ApplicationCommand;
|
||||||
|
}
|
||||||
|
|
||||||
|
isAutoComplete() {
|
||||||
|
return this.type === InteractionTypes.ApplicationCommandAutocomplete;
|
||||||
|
}
|
||||||
|
|
||||||
|
isComponent() {
|
||||||
|
return this.type === InteractionTypes.MessageComponent;
|
||||||
|
}
|
||||||
|
|
||||||
|
isPing(): this is PingInteraction {
|
||||||
|
return this.type === InteractionTypes.Ping;
|
||||||
|
}
|
||||||
|
|
||||||
|
isModalSubmit() {
|
||||||
|
return this.type === InteractionTypes.ModalSubmit;
|
||||||
|
}
|
||||||
|
|
||||||
|
inGuild() {
|
||||||
|
return !!this.guildId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BaseInteraction;
|
108
structures/interactions/CommandInteraction.ts
Normal file
108
structures/interactions/CommandInteraction.ts
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
import type { Model } from "../Base.ts";
|
||||||
|
import type { Snowflake } from "../../util/Snowflake.ts";
|
||||||
|
import type { Session } from "../../session/Session.ts";
|
||||||
|
import type { ApplicationCommandTypes, DiscordMemberWithUser, DiscordInteraction, InteractionTypes } from "../../vendor/external.ts";
|
||||||
|
import type { CreateMessage } from "../Message.ts";
|
||||||
|
import { InteractionResponseTypes } from "../../vendor/external.ts";
|
||||||
|
import BaseInteraction from "./BaseInteraction.ts";
|
||||||
|
import CommandInteractionOptionResolver from "./CommandInteractionOptionResolver.ts";
|
||||||
|
import Attachment from "../Attachment.ts";
|
||||||
|
import User from "../User.ts";
|
||||||
|
import Member from "../Member.ts";
|
||||||
|
import Message from "../Message.ts";
|
||||||
|
import Role from "../Role.ts";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @link https://discord.com/developers/docs/interactions/slash-commands#interaction-response
|
||||||
|
* */
|
||||||
|
export interface InteractionResponse {
|
||||||
|
type: InteractionResponseTypes;
|
||||||
|
data?: InteractionApplicationCommandCallbackData;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @link https://discord.com/developers/docs/interactions/slash-commands#interaction-response-interactionapplicationcommandcallbackdata
|
||||||
|
* */
|
||||||
|
export interface InteractionApplicationCommandCallbackData extends Omit<CreateMessage, "messageReference"> {
|
||||||
|
customId?: string;
|
||||||
|
title?: string;
|
||||||
|
// TODO: use builder
|
||||||
|
// components?: MessageComponents;
|
||||||
|
flags?: number;
|
||||||
|
choices?: ApplicationCommandOptionChoice[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @link https://discord.com/developers/docs/interactions/slash-commands#applicationcommandoptionchoice
|
||||||
|
* */
|
||||||
|
export interface ApplicationCommandOptionChoice {
|
||||||
|
name: string;
|
||||||
|
value: string | number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CommandInteraction extends BaseInteraction implements Model {
|
||||||
|
constructor(session: Session, data: DiscordInteraction) {
|
||||||
|
super(session, data);
|
||||||
|
this.type = data.type as number;
|
||||||
|
this.commandId = data.data!.id;
|
||||||
|
this.commandName = data.data!.name;
|
||||||
|
this.commandType = data.data!.type;
|
||||||
|
this.commandGuildId = data.data!.guild_id;
|
||||||
|
this.options = new CommandInteractionOptionResolver(data.data!.options ?? []);
|
||||||
|
|
||||||
|
this.resolved = {
|
||||||
|
users: new Map(),
|
||||||
|
members: new Map(),
|
||||||
|
roles: new Map(),
|
||||||
|
attachments: new Map(),
|
||||||
|
messages: new Map(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (data.data!.resolved?.users) {
|
||||||
|
for (const [id, u] of Object.entries(data.data!.resolved.users)) {
|
||||||
|
this.resolved.users.set(id, new User(session, u));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.data!.resolved?.members && !!super.guildId) {
|
||||||
|
for (const [id, m] of Object.entries(data.data!.resolved.members)) {
|
||||||
|
this.resolved.members.set(id, new Member(session, m as DiscordMemberWithUser, super.guildId!));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.data!.resolved?.roles && !!super.guildId) {
|
||||||
|
for (const [id, r] of Object.entries(data.data!.resolved.roles)) {
|
||||||
|
this.resolved.roles.set(id, new Role(session, r, super.guildId!));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.data!.resolved?.attachments) {
|
||||||
|
for (const [id, a] of Object.entries(data.data!.resolved.attachments)) {
|
||||||
|
this.resolved.attachments.set(id, new Attachment(session, a));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.data!.resolved?.messages) {
|
||||||
|
for (const [id, m] of Object.entries(data.data!.resolved.messages)) {
|
||||||
|
this.resolved.messages.set(id, new Message(session, m));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override type: InteractionTypes.ApplicationCommand;
|
||||||
|
commandId: Snowflake;
|
||||||
|
commandName: string;
|
||||||
|
commandType: ApplicationCommandTypes;
|
||||||
|
commandGuildId?: Snowflake;
|
||||||
|
resolved: {
|
||||||
|
users: Map<Snowflake, User>;
|
||||||
|
members: Map<Snowflake, Member>;
|
||||||
|
roles: Map<Snowflake, Role>;
|
||||||
|
attachments: Map<Snowflake, Attachment>;
|
||||||
|
messages: Map<Snowflake, Message>;
|
||||||
|
};
|
||||||
|
options: CommandInteractionOptionResolver;
|
||||||
|
responded = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CommandInteraction;
|
233
structures/interactions/CommandInteractionOptionResolver.ts
Normal file
233
structures/interactions/CommandInteractionOptionResolver.ts
Normal file
@ -0,0 +1,233 @@
|
|||||||
|
import type { DiscordInteractionDataOption, DiscordInteractionDataResolved } from '../../vendor/external.ts';
|
||||||
|
import { ApplicationCommandOptionTypes } from "../../vendor/external.ts";
|
||||||
|
|
||||||
|
export function transformOasisInteractionDataOption(o: DiscordInteractionDataOption): CommandInteractionOption {
|
||||||
|
const output: CommandInteractionOption = { ...o, Otherwise: o.value as string | boolean | number | undefined };
|
||||||
|
|
||||||
|
switch (o.type) {
|
||||||
|
case ApplicationCommandOptionTypes.String:
|
||||||
|
output.String = o.value as string;
|
||||||
|
break;
|
||||||
|
case ApplicationCommandOptionTypes.Number:
|
||||||
|
output.Number = o.value as number;
|
||||||
|
break;
|
||||||
|
case ApplicationCommandOptionTypes.Integer:
|
||||||
|
output.Integer = o.value as number;
|
||||||
|
break;
|
||||||
|
case ApplicationCommandOptionTypes.Boolean:
|
||||||
|
output.Boolean = o.value as boolean;
|
||||||
|
break;
|
||||||
|
case ApplicationCommandOptionTypes.Role:
|
||||||
|
output.Role = BigInt(o.value as string);
|
||||||
|
break;
|
||||||
|
case ApplicationCommandOptionTypes.User:
|
||||||
|
output.User = BigInt(o.value as string);
|
||||||
|
break;
|
||||||
|
case ApplicationCommandOptionTypes.Channel:
|
||||||
|
output.Channel = BigInt(o.value as string);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ApplicationCommandOptionTypes.Mentionable:
|
||||||
|
case ApplicationCommandOptionTypes.SubCommand:
|
||||||
|
case ApplicationCommandOptionTypes.SubCommandGroup:
|
||||||
|
default:
|
||||||
|
output.Otherwise = o.value as string | boolean | number | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CommandInteractionOption extends Omit<DiscordInteractionDataOption, 'value'> {
|
||||||
|
Attachment?: string;
|
||||||
|
Boolean?: boolean;
|
||||||
|
User?: bigint;
|
||||||
|
Role?: bigint;
|
||||||
|
Number?: number;
|
||||||
|
Integer?: number;
|
||||||
|
Channel?: bigint;
|
||||||
|
String?: string;
|
||||||
|
Mentionable?: string;
|
||||||
|
Otherwise: string | number | boolean | bigint | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility class to get the resolved options for a command
|
||||||
|
* It is really typesafe
|
||||||
|
* @example const option = ctx.options.getStringOption("name");
|
||||||
|
*/
|
||||||
|
export class CommandInteractionOptionResolver {
|
||||||
|
#subcommand?: string;
|
||||||
|
#group?: string;
|
||||||
|
|
||||||
|
hoistedOptions: CommandInteractionOption[];
|
||||||
|
resolved?: DiscordInteractionDataResolved;
|
||||||
|
|
||||||
|
constructor(options?: DiscordInteractionDataOption[], resolved?: DiscordInteractionDataResolved) {
|
||||||
|
this.hoistedOptions = options?.map(transformOasisInteractionDataOption) ?? [];
|
||||||
|
|
||||||
|
// warning: black magic do not edit and thank djs authors
|
||||||
|
|
||||||
|
if (this.hoistedOptions[0]?.type === ApplicationCommandOptionTypes.SubCommandGroup) {
|
||||||
|
this.#group = this.hoistedOptions[0].name;
|
||||||
|
this.hoistedOptions = (this.hoistedOptions[0].options ?? []).map(transformOasisInteractionDataOption);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.hoistedOptions[0]?.type === ApplicationCommandOptionTypes.SubCommand) {
|
||||||
|
this.#subcommand = this.hoistedOptions[0].name;
|
||||||
|
this.hoistedOptions = (this.hoistedOptions[0].options ?? []).map(transformOasisInteractionDataOption);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.resolved = resolved;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getTypedOption(
|
||||||
|
name: string | number,
|
||||||
|
type: ApplicationCommandOptionTypes,
|
||||||
|
properties: Array<keyof CommandInteractionOption>,
|
||||||
|
required: boolean,
|
||||||
|
) {
|
||||||
|
const option = this.get(name, required);
|
||||||
|
|
||||||
|
if (!option) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (option.type !== type) {
|
||||||
|
// pass
|
||||||
|
}
|
||||||
|
|
||||||
|
if (required === true && properties.every((prop) => typeof option[prop] === "undefined")) {
|
||||||
|
throw new TypeError(`Properties ${properties.join(', ')} are missing in option ${name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return option;
|
||||||
|
}
|
||||||
|
|
||||||
|
get(name: string | number, required: true): CommandInteractionOption;
|
||||||
|
get(name: string | number, required: boolean): CommandInteractionOption | undefined;
|
||||||
|
get(name: string | number, required?: boolean) {
|
||||||
|
const option = this.hoistedOptions.find((o) =>
|
||||||
|
typeof name === 'number' ? o.name === name.toString() : o.name === name
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!option) {
|
||||||
|
if (required && name in this.hoistedOptions.map((o) => o.name)) {
|
||||||
|
throw new TypeError('Option marked as required was undefined');
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return option;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** searches for a string option */
|
||||||
|
getString(name: string | number, required: true): string;
|
||||||
|
getString(name: string | number, required?: boolean): string | undefined;
|
||||||
|
getString(name: string | number, required = false) {
|
||||||
|
const option = this.getTypedOption(name, ApplicationCommandOptionTypes.String, ['Otherwise'], required);
|
||||||
|
|
||||||
|
return option?.Otherwise ?? undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** searches for a number option */
|
||||||
|
getNumber(name: string | number, required: true): number;
|
||||||
|
getNumber(name: string | number, required?: boolean): number | undefined;
|
||||||
|
getNumber(name: string | number, required = false) {
|
||||||
|
const option = this.getTypedOption(name, ApplicationCommandOptionTypes.Number, ['Otherwise'], required);
|
||||||
|
|
||||||
|
return option?.Otherwise ?? undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** searhces for an integer option */
|
||||||
|
getInteger(name: string | number, required: true): number;
|
||||||
|
getInteger(name: string | number, required?: boolean): number | undefined;
|
||||||
|
getInteger(name: string | number, required = false) {
|
||||||
|
const option = this.getTypedOption(name, ApplicationCommandOptionTypes.Integer, ['Otherwise'], required);
|
||||||
|
|
||||||
|
return option?.Otherwise ?? undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** searches for a boolean option */
|
||||||
|
getBoolean(name: string | number, required: true): boolean;
|
||||||
|
getBoolean(name: string | number, required?: boolean): boolean | undefined;
|
||||||
|
getBoolean(name: string | number, required = false) {
|
||||||
|
const option = this.getTypedOption(name, ApplicationCommandOptionTypes.Boolean, ['Otherwise'], required);
|
||||||
|
|
||||||
|
return option?.Otherwise ?? undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** searches for a user option */
|
||||||
|
getUser(name: string | number, required: true): bigint;
|
||||||
|
getUser(name: string | number, required?: boolean): bigint | undefined;
|
||||||
|
getUser(name: string | number, required = false) {
|
||||||
|
const option = this.getTypedOption(name, ApplicationCommandOptionTypes.User, ['Otherwise'], required);
|
||||||
|
|
||||||
|
return option?.Otherwise ?? undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** searches for a channel option */
|
||||||
|
getChannel(name: string | number, required: true): bigint;
|
||||||
|
getChannel(name: string | number, required?: boolean): bigint | undefined;
|
||||||
|
getChannel(name: string | number, required = false) {
|
||||||
|
const option = this.getTypedOption(name, ApplicationCommandOptionTypes.Channel, ['Otherwise'], required);
|
||||||
|
|
||||||
|
return option?.Otherwise ?? undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** searches for a mentionable-based option */
|
||||||
|
getMentionable(name: string | number, required: true): string;
|
||||||
|
getMentionable(name: string | number, required?: boolean): string | undefined;
|
||||||
|
getMentionable(name: string | number, required = false) {
|
||||||
|
const option = this.getTypedOption(name, ApplicationCommandOptionTypes.Mentionable, ['Otherwise'], required);
|
||||||
|
|
||||||
|
return option?.Otherwise ?? undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** searches for a mentionable-based option */
|
||||||
|
getRole(name: string | number, required: true): bigint;
|
||||||
|
getRole(name: string | number, required?: boolean): bigint | undefined;
|
||||||
|
getRole(name: string | number, required = false) {
|
||||||
|
const option = this.getTypedOption(name, ApplicationCommandOptionTypes.Role, ['Otherwise'], required);
|
||||||
|
|
||||||
|
return option?.Otherwise ?? undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** searches for an attachment option */
|
||||||
|
getAttachment(name: string | number, required: true): string;
|
||||||
|
getAttachment(name: string | number, required?: boolean): string | undefined;
|
||||||
|
getAttachment(name: string | number, required = false) {
|
||||||
|
const option = this.getTypedOption(name, ApplicationCommandOptionTypes.Attachment, ['Otherwise'], required);
|
||||||
|
|
||||||
|
return option?.Otherwise ?? undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** searches for the focused option */
|
||||||
|
getFocused(full = false) {
|
||||||
|
const focusedOption = this.hoistedOptions.find((option) => option.focused);
|
||||||
|
|
||||||
|
if (!focusedOption) {
|
||||||
|
throw new TypeError('No option found');
|
||||||
|
}
|
||||||
|
|
||||||
|
return full ? focusedOption : focusedOption.Otherwise;
|
||||||
|
}
|
||||||
|
|
||||||
|
getSubCommand(required = true) {
|
||||||
|
if (required && !this.#subcommand) {
|
||||||
|
throw new TypeError('Option marked as required was undefined');
|
||||||
|
}
|
||||||
|
|
||||||
|
return [this.#subcommand, this.hoistedOptions];
|
||||||
|
}
|
||||||
|
|
||||||
|
getSubCommandGroup(required = false) {
|
||||||
|
if (required && !this.#group) {
|
||||||
|
throw new TypeError('Option marked as required was undefined');
|
||||||
|
}
|
||||||
|
|
||||||
|
return [this.#group, this.hoistedOptions];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CommandInteractionOptionResolver;
|
45
structures/interactions/ComponentInteraction.ts
Normal file
45
structures/interactions/ComponentInteraction.ts
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import type { Model } from "../Base.ts";
|
||||||
|
import type { Snowflake } from "../../util/Snowflake.ts";
|
||||||
|
import type { Session } from "../../session/Session.ts";
|
||||||
|
import type { DiscordInteraction, InteractionTypes } from "../../vendor/external.ts";
|
||||||
|
import { MessageComponentTypes } from "../../vendor/external.ts";
|
||||||
|
import BaseInteraction from "./BaseInteraction.ts";
|
||||||
|
import Message from "../Message.ts";
|
||||||
|
|
||||||
|
export class ComponentInteraction extends BaseInteraction implements Model {
|
||||||
|
constructor(session: Session, data: DiscordInteraction) {
|
||||||
|
super(session, data);
|
||||||
|
this.type = data.type as number;
|
||||||
|
this.componentType = data.data!.component_type!;
|
||||||
|
this.customId = data.data!.custom_id;
|
||||||
|
this.targetId = data.data!.target_id;
|
||||||
|
this.values = data.data!.values;
|
||||||
|
this.message = new Message(session, data.message!);
|
||||||
|
}
|
||||||
|
|
||||||
|
override type: InteractionTypes.MessageComponent;
|
||||||
|
componentType: MessageComponentTypes;
|
||||||
|
customId?: string;
|
||||||
|
targetId?: Snowflake;
|
||||||
|
values?: string[];
|
||||||
|
message: Message;
|
||||||
|
responded = false;
|
||||||
|
|
||||||
|
isButton() {
|
||||||
|
return this.componentType === MessageComponentTypes.Button;
|
||||||
|
}
|
||||||
|
|
||||||
|
isActionRow() {
|
||||||
|
return this.componentType === MessageComponentTypes.ActionRow;
|
||||||
|
}
|
||||||
|
|
||||||
|
isTextInput() {
|
||||||
|
return this.componentType === MessageComponentTypes.InputText;
|
||||||
|
}
|
||||||
|
|
||||||
|
isSelectMenu() {
|
||||||
|
return this.componentType === MessageComponentTypes.SelectMenu;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ComponentInteraction;
|
@ -1,145 +0,0 @@
|
|||||||
import type { Model } from "../Base.ts";
|
|
||||||
import type { Snowflake } from "../../util/Snowflake.ts";
|
|
||||||
import type { Session } from "../../session/Session.ts";
|
|
||||||
import type {
|
|
||||||
DiscordInteraction,
|
|
||||||
DiscordMessage,
|
|
||||||
FileContent,
|
|
||||||
InteractionResponseTypes,
|
|
||||||
InteractionTypes,
|
|
||||||
} from "../../vendor/external.ts";
|
|
||||||
import type { MessageFlags } from "../../util/shared/flags.ts";
|
|
||||||
import type { AllowedMentions } from "../Message.ts";
|
|
||||||
import User from "../User.ts";
|
|
||||||
import Message from "../Message.ts";
|
|
||||||
import Member from "../Member.ts";
|
|
||||||
import * as Routes from "../../util/Routes.ts";
|
|
||||||
|
|
||||||
export interface InteractionResponse {
|
|
||||||
type: InteractionResponseTypes;
|
|
||||||
data?: InteractionApplicationCommandCallbackData;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface InteractionApplicationCommandCallbackData {
|
|
||||||
content?: string;
|
|
||||||
tts?: boolean;
|
|
||||||
allowedMentions?: AllowedMentions;
|
|
||||||
files?: FileContent[];
|
|
||||||
customId?: string;
|
|
||||||
title?: string;
|
|
||||||
// components?: Component[];
|
|
||||||
flags?: MessageFlags;
|
|
||||||
choices?: ApplicationCommandOptionChoice[];
|
|
||||||
}
|
|
||||||
|
|
||||||
/** https://discord.com/developers/docs/interactions/slash-commands#applicationcommandoptionchoice */
|
|
||||||
export interface ApplicationCommandOptionChoice {
|
|
||||||
name: string;
|
|
||||||
value: string | number;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: abstract Interaction, CommandInteraction, ComponentInteraction, PingInteraction, etc
|
|
||||||
|
|
||||||
export class Interaction implements Model {
|
|
||||||
constructor(session: Session, data: DiscordInteraction) {
|
|
||||||
this.session = session;
|
|
||||||
this.id = data.id;
|
|
||||||
this.token = data.token;
|
|
||||||
this.type = data.type;
|
|
||||||
this.guildId = data.guild_id;
|
|
||||||
this.channelId = data.channel_id;
|
|
||||||
this.applicationId = data.application_id;
|
|
||||||
this.locale = data.locale;
|
|
||||||
this.data = data.data;
|
|
||||||
|
|
||||||
if (!data.guild_id) {
|
|
||||||
this.user = new User(session, data.user!);
|
|
||||||
} else {
|
|
||||||
this.member = new Member(session, data.member!, data.guild_id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
readonly session: Session;
|
|
||||||
readonly id: Snowflake;
|
|
||||||
readonly token: string;
|
|
||||||
|
|
||||||
type: InteractionTypes;
|
|
||||||
guildId?: Snowflake;
|
|
||||||
channelId?: Snowflake;
|
|
||||||
applicationId?: Snowflake;
|
|
||||||
locale?: string;
|
|
||||||
// deno-lint-ignore no-explicit-any
|
|
||||||
data: any;
|
|
||||||
user?: User;
|
|
||||||
member?: Member;
|
|
||||||
|
|
||||||
async respond({ type, data }: InteractionResponse) {
|
|
||||||
const toSend = {
|
|
||||||
tts: data?.tts,
|
|
||||||
title: data?.title,
|
|
||||||
flags: data?.flags,
|
|
||||||
content: data?.content,
|
|
||||||
choices: data?.choices,
|
|
||||||
custom_id: data?.customId,
|
|
||||||
allowed_mentions: data?.allowedMentions
|
|
||||||
? {
|
|
||||||
users: data.allowedMentions.users,
|
|
||||||
roles: data.allowedMentions.roles,
|
|
||||||
parse: data.allowedMentions.parse,
|
|
||||||
replied_user: data.allowedMentions.repliedUser,
|
|
||||||
}
|
|
||||||
: { parse: [] },
|
|
||||||
};
|
|
||||||
|
|
||||||
if (this.session.unrepliedInteractions.delete(BigInt(this.id))) {
|
|
||||||
await this.session.rest.sendRequest<undefined>(
|
|
||||||
this.session.rest,
|
|
||||||
{
|
|
||||||
url: Routes.INTERACTION_ID_TOKEN(this.id, this.token),
|
|
||||||
method: "POST",
|
|
||||||
payload: this.session.rest.createRequestBody(this.session.rest, {
|
|
||||||
method: "POST",
|
|
||||||
body: {
|
|
||||||
type: type,
|
|
||||||
data: toSend,
|
|
||||||
file: data?.files,
|
|
||||||
},
|
|
||||||
headers: {
|
|
||||||
// remove authorization header
|
|
||||||
Authorization: "",
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await this.session.rest.sendRequest<DiscordMessage>(
|
|
||||||
this.session.rest,
|
|
||||||
{
|
|
||||||
url: Routes.WEBHOOK(this.session.applicationId ?? this.session.botId, this.token),
|
|
||||||
method: "POST",
|
|
||||||
payload: this.session.rest.createRequestBody(this.session.rest, {
|
|
||||||
method: "POST",
|
|
||||||
body: {
|
|
||||||
...toSend,
|
|
||||||
file: data?.files,
|
|
||||||
},
|
|
||||||
headers: {
|
|
||||||
// remove authorization header
|
|
||||||
Authorization: "",
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
return new Message(this.session, result);
|
|
||||||
}
|
|
||||||
|
|
||||||
inGuild(): this is Interaction & { user: undefined; guildId: Snowflake; member: Member } {
|
|
||||||
return !!this.guildId;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Interaction;
|
|
32
structures/interactions/InteractionFactory.ts
Normal file
32
structures/interactions/InteractionFactory.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import type { Session } from "../../session/Session.ts";
|
||||||
|
import type { DiscordInteraction } from "../../vendor/external.ts";
|
||||||
|
import { InteractionTypes } from "../../vendor/external.ts";
|
||||||
|
import CommandInteraction from "./CommandInteraction.ts";
|
||||||
|
import ComponentInteraction from "./ComponentInteraction.ts";
|
||||||
|
import PingInteraction from "./PingInteraction.ts";
|
||||||
|
import AutoCompleteInteraction from "./AutoCompleteInteraction.ts";
|
||||||
|
import ModalSubmitInteraction from "./ModalSubmitInteraction.ts";
|
||||||
|
|
||||||
|
export type Interaction =
|
||||||
|
| CommandInteraction
|
||||||
|
| ComponentInteraction
|
||||||
|
| PingInteraction
|
||||||
|
| AutoCompleteInteraction
|
||||||
|
| ModalSubmitInteraction;
|
||||||
|
|
||||||
|
export class InteractionFactory {
|
||||||
|
static from(session: Session, interaction: DiscordInteraction): Interaction {
|
||||||
|
switch (interaction.type) {
|
||||||
|
case InteractionTypes.Ping:
|
||||||
|
return new PingInteraction(session, interaction);
|
||||||
|
case InteractionTypes.ApplicationCommand:
|
||||||
|
return new CommandInteraction(session, interaction);
|
||||||
|
case InteractionTypes.MessageComponent:
|
||||||
|
return new ComponentInteraction(session, interaction);
|
||||||
|
case InteractionTypes.ApplicationCommandAutocomplete:
|
||||||
|
return new AutoCompleteInteraction(session, interaction);
|
||||||
|
case InteractionTypes.ModalSubmit:
|
||||||
|
return new ModalSubmitInteraction(session, interaction);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
50
structures/interactions/ModalSubmitInteraction.ts
Normal file
50
structures/interactions/ModalSubmitInteraction.ts
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
|
||||||
|
import type { Model } from "../Base.ts";
|
||||||
|
import type { Snowflake } from "../../util/Snowflake.ts";
|
||||||
|
import type { Session } from "../../session/Session.ts";
|
||||||
|
import type { DiscordInteraction, InteractionTypes, MessageComponentTypes, DiscordMessageComponents } from "../../vendor/external.ts";
|
||||||
|
import BaseInteraction from "./BaseInteraction.ts";
|
||||||
|
import Message from "../Message.ts";
|
||||||
|
|
||||||
|
export class ModalSubmitInteraction extends BaseInteraction implements Model {
|
||||||
|
constructor(session: Session, data: DiscordInteraction) {
|
||||||
|
super(session, data);
|
||||||
|
this.type = data.type as number;
|
||||||
|
this.componentType = data.data!.component_type!;
|
||||||
|
this.customId = data.data!.custom_id;
|
||||||
|
this.targetId = data.data!.target_id;
|
||||||
|
this.values = data.data!.values;
|
||||||
|
|
||||||
|
this.components = data.data?.components?.map(ModalSubmitInteraction.transformComponent);
|
||||||
|
|
||||||
|
if (data.message) {
|
||||||
|
this.message = new Message(session, data.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override type: InteractionTypes.MessageComponent;
|
||||||
|
componentType: MessageComponentTypes;
|
||||||
|
customId?: string;
|
||||||
|
targetId?: Snowflake;
|
||||||
|
values?: string[];
|
||||||
|
message?: Message;
|
||||||
|
components;
|
||||||
|
|
||||||
|
static transformComponent(component: DiscordMessageComponents[number]) {
|
||||||
|
return {
|
||||||
|
type: component.type,
|
||||||
|
components: component.components.map((component) => {
|
||||||
|
return {
|
||||||
|
customId: component.custom_id,
|
||||||
|
value: (component as typeof component & { value: string }).value,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
inMessage(): this is ModalSubmitInteraction & { message: Message } {
|
||||||
|
return !!this.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ModalSubmitInteraction;
|
38
structures/interactions/PingInteraction.ts
Normal file
38
structures/interactions/PingInteraction.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
|
||||||
|
import type { Model } from "../Base.ts";
|
||||||
|
import type { Snowflake } from "../../util/Snowflake.ts";
|
||||||
|
import type { Session } from "../../session/Session.ts";
|
||||||
|
import type { ApplicationCommandTypes, DiscordInteraction, InteractionTypes } from "../../vendor/external.ts";
|
||||||
|
import { InteractionResponseTypes } from "../../vendor/external.ts";
|
||||||
|
import BaseInteraction from "./BaseInteraction.ts";
|
||||||
|
import * as Routes from "../../util/Routes.ts";
|
||||||
|
|
||||||
|
export class PingInteraction extends BaseInteraction implements Model {
|
||||||
|
constructor(session: Session, data: DiscordInteraction) {
|
||||||
|
super(session, data);
|
||||||
|
this.type = data.type as number;
|
||||||
|
this.commandId = data.data!.id;
|
||||||
|
this.commandName = data.data!.name;
|
||||||
|
this.commandType = data.data!.type;
|
||||||
|
this.commandGuildId = data.data!.guild_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
override type: InteractionTypes.Ping;
|
||||||
|
commandId: Snowflake;
|
||||||
|
commandName: string;
|
||||||
|
commandType: ApplicationCommandTypes;
|
||||||
|
commandGuildId?: Snowflake;
|
||||||
|
|
||||||
|
async pong() {
|
||||||
|
await this.session.rest.runMethod<undefined>(
|
||||||
|
this.session.rest,
|
||||||
|
"POST",
|
||||||
|
Routes.INTERACTION_ID_TOKEN(this.id, this.token),
|
||||||
|
{
|
||||||
|
type: InteractionResponseTypes.Pong,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PingInteraction;
|
Loading…
x
Reference in New Issue
Block a user