mirror of
https://github.com/tiramisulabs/seyfert.git
synced 2025-07-02 21:16:09 +00:00
resolving conflicts and wip interactions closes #19
This commit is contained in:
commit
91db746acb
@ -1,8 +1,5 @@
|
||||
{
|
||||
"fmt": {
|
||||
"files": {
|
||||
"exclude": ["vendor"]
|
||||
},
|
||||
"options": {
|
||||
"indentWidth": 4,
|
||||
"lineWidth": 120
|
||||
|
@ -33,6 +33,7 @@ import type {
|
||||
import type { Snowflake } from "../util/Snowflake.ts";
|
||||
import type { Session } from "../session/Session.ts";
|
||||
import type { Channel } from "../structures/channels/ChannelFactory.ts";
|
||||
import type { Interaction } from "../structures/interactions/InteractionFactory.ts";
|
||||
import ChannelFactory from "../structures/channels/ChannelFactory.ts";
|
||||
import GuildChannel from "../structures/channels/GuildChannel.ts";
|
||||
import ThreadChannel from "../structures/channels/ThreadChannel.ts";
|
||||
@ -40,9 +41,9 @@ import ThreadMember from "../structures/ThreadMember.ts";
|
||||
import Member from "../structures/Member.ts";
|
||||
import Message from "../structures/Message.ts";
|
||||
import User from "../structures/User.ts";
|
||||
import Guild from "../structures/guilds/Guild.ts";
|
||||
import Interaction from "../structures/interactions/Interaction.ts";
|
||||
import { Integration } from "../structures/Integration.ts"
|
||||
import Integration from "../structures/Integration.ts"
|
||||
import Guild from "../structures/guilds/Guild.ts";
|
||||
import InteractionFactory from "../structures/interactions/InteractionFactory.ts";
|
||||
|
||||
export type RawHandler<T> = (...args: [Session, number, T]) => void;
|
||||
export type Handler<T extends unknown[]> = (...args: T) => unknown;
|
||||
@ -110,12 +111,7 @@ export const GUILD_ROLE_DELETE: RawHandler<DiscordGuildRoleDelete> = (session, _
|
||||
};
|
||||
|
||||
export const INTERACTION_CREATE: RawHandler<DiscordInteraction> = (session, _shardId, interaction) => {
|
||||
session.unrepliedInteractions.add(BigInt(interaction.id));
|
||||
|
||||
// could be improved
|
||||
setTimeout(() => session.unrepliedInteractions.delete(BigInt(interaction.id)), 15 * 60 * 1000);
|
||||
|
||||
session.emit("interactionCreate", new Interaction(session, interaction));
|
||||
session.emit("interactionCreate", InteractionFactory.from(session, interaction));
|
||||
};
|
||||
|
||||
export const CHANNEL_CREATE: RawHandler<DiscordChannel> = (session, _shardId, channel) => {
|
||||
|
9
mod.ts
9
mod.ts
@ -42,7 +42,14 @@ export * from "./structures/builders/MessageButton.ts";
|
||||
export * from "./structures/builders/MessageSelectMenu.ts";
|
||||
export * from "./structures/builders/SelectMenuOptionBuilder.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";
|
||||
|
||||
|
@ -39,8 +39,6 @@ export class Session extends EventEmitter {
|
||||
rest: ReturnType<typeof createRestManager>;
|
||||
gateway: ReturnType<typeof createGatewayManager>;
|
||||
|
||||
unrepliedInteractions: Set<bigint> = new Set();
|
||||
|
||||
#botId: Snowflake;
|
||||
#applicationId?: Snowflake;
|
||||
|
||||
|
@ -75,4 +75,6 @@ export class Integration implements Model {
|
||||
user?: User;
|
||||
account: IntegrationAccount;
|
||||
application?: IntegrationApplication;
|
||||
}
|
||||
}
|
||||
|
||||
export default Integration;
|
||||
|
@ -48,6 +48,7 @@ export interface CreateMessage {
|
||||
allowedMentions?: AllowedMentions;
|
||||
files?: FileContent[];
|
||||
messageReference?: CreateMessageReference;
|
||||
tts?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -270,6 +271,7 @@ export class Message implements Model {
|
||||
}
|
||||
: undefined,
|
||||
embeds: options.embeds,
|
||||
tts: options.tts,
|
||||
},
|
||||
);
|
||||
|
||||
|
@ -1,9 +1,13 @@
|
||||
import type { Model } from "./Base.ts";
|
||||
import type { Session } from "../session/Session.ts";
|
||||
import type { Snowflake } from "../util/Snowflake.ts";
|
||||
import type { DiscordWebhook, WebhookTypes } from "../vendor/external.ts";
|
||||
import type { DiscordMessage, DiscordWebhook, WebhookTypes } from "../vendor/external.ts";
|
||||
import type { WebhookOptions } from "../util/Routes.ts";
|
||||
import type { CreateMessage } from "./Message.ts";
|
||||
import { iconHashToBigInt } from "../util/hash.ts";
|
||||
import User from "./User.ts";
|
||||
import Message from "./Message.ts";
|
||||
import * as Routes from "../util/Routes.ts";
|
||||
|
||||
export class Webhook implements Model {
|
||||
constructor(session: Session, data: DiscordWebhook) {
|
||||
@ -42,6 +46,62 @@ export class Webhook implements Model {
|
||||
channelId?: Snowflake;
|
||||
guildId?: Snowflake;
|
||||
user?: User;
|
||||
|
||||
async execute(options?: WebhookOptions & CreateMessage & { avatarUrl?: string, username?: string }) {
|
||||
if (!this.token) {
|
||||
return;
|
||||
}
|
||||
|
||||
const data = {
|
||||
content: options?.content,
|
||||
embeds: options?.embeds,
|
||||
tts: options?.tts,
|
||||
allowed_mentions: options?.allowedMentions,
|
||||
// @ts-ignore: TODO: component builder or something
|
||||
components: options?.components,
|
||||
file: options?.files,
|
||||
};
|
||||
|
||||
const message = await this.session.rest.sendRequest<DiscordMessage>(this.session.rest, {
|
||||
url: Routes.WEBHOOK(this.id, this.token!, {
|
||||
wait: options?.wait,
|
||||
threadId: options?.threadId,
|
||||
}),
|
||||
method: "POST",
|
||||
payload: this.session.rest.createRequestBody(this.session.rest, {
|
||||
method: "POST",
|
||||
body: {
|
||||
...data,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
return (options?.wait ?? true) ? new Message(this.session, message) : undefined;
|
||||
}
|
||||
|
||||
async fetch() {
|
||||
const message = await this.session.rest.runMethod<DiscordWebhook>(
|
||||
this.session.rest,
|
||||
"GET",
|
||||
Routes.WEBHOOK_TOKEN(this.id, this.token),
|
||||
);
|
||||
|
||||
return new Webhook(this.session, message);
|
||||
}
|
||||
|
||||
async fetchMessage(messageId: Snowflake) {
|
||||
if (!this.token) {
|
||||
return;
|
||||
}
|
||||
|
||||
const message = await this.session.rest.runMethod<DiscordMessage>(
|
||||
this.session.rest,
|
||||
"GET",
|
||||
Routes.WEBHOOK_MESSAGE(this.id, this.token, messageId),
|
||||
);
|
||||
|
||||
return new Message(this.session, message);
|
||||
}
|
||||
}
|
||||
|
||||
export default Webhook;
|
||||
|
@ -7,6 +7,7 @@ import type VoiceChannel from "./VoiceChannel.ts";
|
||||
import type DMChannel from "./DMChannel.ts";
|
||||
import type NewsChannel from "./NewsChannel.ts";
|
||||
import type ThreadChannel from "./ThreadChannel.ts";
|
||||
import type StageChannel from "./StageChannel.ts";
|
||||
import { ChannelTypes } from "../../vendor/external.ts";
|
||||
import { textBasedChannels } from "./TextChannel.ts";
|
||||
|
||||
@ -43,6 +44,10 @@ export abstract class BaseChannel implements Model {
|
||||
return this.type === ChannelTypes.GuildPublicThread || this.type === ChannelTypes.GuildPrivateThread;
|
||||
}
|
||||
|
||||
isStage(): this is StageChannel {
|
||||
return this.type === ChannelTypes.GuildStageVoice;
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return `<#${this.id}>`;
|
||||
}
|
||||
|
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 "./CommandInteraction.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;
|
150
structures/interactions/CommandInteraction.ts
Normal file
150
structures/interactions/CommandInteraction.ts
Normal file
@ -0,0 +1,150 @@
|
||||
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 type { MessageFlags } from "../../util/shared/flags.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";
|
||||
import Webhook from "../Webhook.ts";
|
||||
import * as Routes from "../../util/Routes.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 Pick<CreateMessage, "allowedMentions" | "content" | "embeds" | "files"> {
|
||||
customId?: string;
|
||||
title?: string;
|
||||
// components?: MessageComponents;
|
||||
flags?: MessageFlags;
|
||||
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;
|
||||
|
||||
async sendFollowUp(options: InteractionApplicationCommandCallbackData): Promise<Message> {
|
||||
const message = await Webhook.prototype.execute.call({
|
||||
id: this.applicationId!,
|
||||
token: this.token,
|
||||
session: this.session,
|
||||
}, options);
|
||||
|
||||
return message!;
|
||||
}
|
||||
|
||||
async respond({ type, data: options }: InteractionResponse): Promise<Message | undefined> {
|
||||
const data = {
|
||||
content: options?.content,
|
||||
custom_id: options?.customId,
|
||||
file: options?.files,
|
||||
allowed_mentions: options?.allowedMentions,
|
||||
flags: options?.flags,
|
||||
chocies: options?.choices,
|
||||
embeds: options?.embeds,
|
||||
title: options?.title,
|
||||
};
|
||||
|
||||
if (!this.respond) {
|
||||
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, data, file: options?.files },
|
||||
headers: { "Authorization": "" },
|
||||
}),
|
||||
});
|
||||
|
||||
this.responded = true;
|
||||
return;
|
||||
}
|
||||
|
||||
return this.sendFollowUp(data);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
34
structures/interactions/InteractionFactory.ts
Normal file
34
structures/interactions/InteractionFactory.ts
Normal file
@ -0,0 +1,34 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default InteractionFactory;
|
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;
|
@ -144,11 +144,26 @@ export function INTERACTION_ID_TOKEN(interactionId: Snowflake, token: string) {
|
||||
return `/interactions/${interactionId}/${token}/callback`;
|
||||
}
|
||||
|
||||
export function WEBHOOK(webhookId: Snowflake, token: string, options?: { wait?: boolean; threadId?: Snowflake }) {
|
||||
let url = `/webhooks/${webhookId}/${token}?`;
|
||||
export function WEBHOOK_MESSAGE(webhookId: Snowflake, token: string, messageId: Snowflake) {
|
||||
return `/webhooks/${webhookId}/${token}/messages/${messageId}`;
|
||||
}
|
||||
|
||||
if (options?.wait !== undefined) url += `wait=${options.wait}`;
|
||||
if (options?.threadId) url += `threadId=${options.threadId}`;
|
||||
export function WEBHOOK_TOKEN(webhookId: Snowflake, token?: string) {
|
||||
if (!token) return `/webhooks/${webhookId}`;
|
||||
return `/webhooks/${webhookId}/${token}`;
|
||||
}
|
||||
|
||||
export interface WebhookOptions {
|
||||
wait?: boolean;
|
||||
threadId?: Snowflake;
|
||||
}
|
||||
|
||||
export function WEBHOOK(webhookId: Snowflake, token: string, options?: WebhookOptions) {
|
||||
let url = `/webhooks/${webhookId}/${token}`;
|
||||
|
||||
if (options?.wait) url += `?wait=${options.wait}`;
|
||||
if (options?.threadId) url += `?threadId=${options.threadId}`;
|
||||
if (options?.wait && options.threadId) url += `?wait=${options.wait}&threadId=${options.threadId}`;
|
||||
|
||||
return url;
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user