From 5e7a7bbf258bf6dba2c8e8803a82ab2fefe8bb85 Mon Sep 17 00:00:00 2001 From: pakkographic Date: Sun, 1 Sep 2024 22:55:36 +0200 Subject: [PATCH] Interaction Components - GUILDS intent has been fixed, allowing caching\n - Introducing Interaction Components, including the ComponentInteraction class, only availale to use by using Application Command Interactions\nCheck diff for detailed changelogs. --- lib/Constants.ts | 4 + lib/gateway/events/GuildHandler.ts | 45 ++++- lib/gateway/events/MessageHandler.ts | 126 +++++++----- lib/routes/Channels.ts | 1 + lib/structures/Client.ts | 4 +- lib/structures/CommandInteraction.ts | 24 +-- lib/structures/ComponentInteraction.ts | 255 +++++++++++++++++++++++++ lib/structures/Message.ts | 13 +- lib/structures/ReactionInfo.ts | 3 + lib/structures/TextChannel.ts | 23 ++- lib/types/channels.d.ts | 4 + lib/types/events.d.ts | 4 +- lib/types/interactions.d.ts | 26 ++- lib/types/json.d.ts | 24 ++- lib/util/Util.ts | 46 ++++- 15 files changed, 524 insertions(+), 78 deletions(-) create mode 100644 lib/structures/ComponentInteraction.ts diff --git a/lib/Constants.ts b/lib/Constants.ts index 3f78ac3..19661b0 100644 --- a/lib/Constants.ts +++ b/lib/Constants.ts @@ -53,4 +53,8 @@ export enum GatewayLayerIntent { GUILD_WEBHOOKS, } +export enum InteractionComponentType { + BUTTON, +} + export type SocialLinkType = APISocialLinkType; diff --git a/lib/gateway/events/GuildHandler.ts b/lib/gateway/events/GuildHandler.ts index 25513e7..4375961 100644 --- a/lib/gateway/events/GuildHandler.ts +++ b/lib/gateway/events/GuildHandler.ts @@ -7,7 +7,13 @@ import { GatewayEventHandler } from "./GatewayEventHandler"; -import { BannedMember, Guild, Role, Member } from "../../index"; +import { + BannedMember, + Guild, + Role, + Member, + GatewayLayerIntent +} from "../../index"; import type { GuildCreateInfo, GuildDeleteInfo } from "../../types"; import type { @@ -37,35 +43,44 @@ import { Category } from "../../structures/Category"; /** Internal component, emitting guild events. */ export class GuildHandler extends GatewayEventHandler { - guildBanAdd(data: GatewayEvent_ServerMemberBanned): void{ + get isGuildIntentEnabled(): boolean { + return this.client.util.isIntentEnabled([GatewayLayerIntent.GUILDS]); + } + guildBanAdd(data: GatewayEvent_ServerMemberBanned): void { + if (!this.isGuildIntentEnabled) return; const GuildMemberBanComponent = new BannedMember(data.serverId, data.serverMemberBan, this.client); this.client.emit("guildBanAdd", GuildMemberBanComponent); } - guildBanRemove(data: GatewayEvent_ServerMemberUnbanned): void{ + guildBanRemove(data: GatewayEvent_ServerMemberUnbanned): void { + if (!this.isGuildIntentEnabled) return; const GuildMemberBanComponent = new BannedMember(data.serverId, data.serverMemberBan, this.client); this.client.emit("guildBanRemove", GuildMemberBanComponent); } guildCategoryCreate(data: GatewayEvent_CategoryCreated): void { + if (!this.isGuildIntentEnabled) return; const category = new Category(data.category, this.client); this.client.emit("guildCategoryCreate", category); } guildCategoryDelete(data: GatewayEvent_CategoryCreated): void { + if (!this.isGuildIntentEnabled) return; const category = new Category(data.category, this.client); this.client.emit("guildCategoryDelete", category); } guildCategoryUpdate(data: GatewayEvent_CategoryCreated): void { + if (!this.isGuildIntentEnabled) return; const category = new Category(data.category, this.client); this.client.emit("guildCategoryUpdate", category); } - guildCreate(data: GatewayEvent_BotServerMembershipCreated): void{ + guildCreate(data: GatewayEvent_BotServerMembershipCreated): void { const GuildComponent = new Guild(data.server, this.client); this.client.guilds.add(GuildComponent); const output = { guild: GuildComponent, inviterID: data.createdBy }; + if (!this.isGuildIntentEnabled) return; this.client.emit("guildCreate", output as GuildCreateInfo); } guildDelete(data: GatewayEvent_BotServerMembershipDeleted): void { @@ -75,11 +90,13 @@ export class GuildHandler extends GatewayEventHandler { guild: GuildComponent, removerID: data.createdBy }; + if (!this.isGuildIntentEnabled) return; this.client.emit("guildDelete", output as GuildDeleteInfo); } guildGroupCreate(data: GatewayEvent_GroupCreated): void { const GuildGroupComponent = new Group(data.group, this.client); this.client.guilds.get(data.serverId)?.groups.add(GuildGroupComponent); + if (!this.isGuildIntentEnabled) return; this.client.emit("guildGroupCreate", GuildGroupComponent); } guildGroupDelete(data: GatewayEvent_GroupDeleted): void { @@ -87,6 +104,7 @@ export class GuildHandler extends GatewayEventHandler { const GuildGroupComponent = guild?.groups.update(new Group(data.group, this.client)) ?? new Group(data.group, this.client); + if (!this.isGuildIntentEnabled) return; this.client.emit("guildGroupDelete", GuildGroupComponent); } guildGroupUpdate(data: GatewayEvent_GroupUpdated): void { @@ -95,44 +113,52 @@ export class GuildHandler extends GatewayEventHandler { const GuildGroupComponent = guild?.groups.update(new Group(data.group, this.client)) ?? new Group(data.group, this.client); + if (!this.isGuildIntentEnabled) return; this.client.emit("guildGroupUpdate", GuildGroupComponent, CachedGroup); } - guildMemberAdd(data: GatewayEvent_ServerMemberJoined): void{ + guildMemberAdd(data: GatewayEvent_ServerMemberJoined): void { + if (!this.isGuildIntentEnabled) return; const MemberComponent = new Member(data.member, this.client, data.serverId); this.client.emit("guildMemberAdd", MemberComponent, data.serverMemberCount); } - guildMemberRemove(data: GatewayEvent_ServerMemberRemoved): void{ + guildMemberRemove(data: GatewayEvent_ServerMemberRemoved): void { + if (!this.isGuildIntentEnabled) return; const output = new MemberRemoveInfo(data, data.userId, this.client); this.client.emit("guildMemberRemove", output); } - guildMemberRoleUpdate(data: GatewayEvent_ServerRolesUpdated): void{ + guildMemberRoleUpdate(data: GatewayEvent_ServerRolesUpdated): void { + if (!this.isGuildIntentEnabled) return; const output = new MemberUpdateInfo(data, data.memberRoleIds[0].userId, this.client); this.client.emit("guildMemberUpdate", output); } guildMemberSocialLinkCreate(data: GatewayEvent_ServerMemberSocialLinkCreated): void { + if (!this.isGuildIntentEnabled) return; this.client.emit( "guildMemberUpdate", new MemberUpdateInfo(data, data.socialLink.userId, this.client) ); } guildMemberSocialLinkDelete(data: GatewayEvent_ServerMemberSocialLinkDeleted): void { + if (!this.isGuildIntentEnabled) return; this.client.emit( "guildMemberUpdate", new MemberUpdateInfo(data, data.socialLink.userId, this.client) ); } guildMemberSocialLinkUpdate(data: GatewayEvent_ServerMemberSocialLinkUpdated): void { + if (!this.isGuildIntentEnabled) return; this.client.emit( "guildMemberUpdate", new MemberUpdateInfo(data, data.socialLink.userId, this.client) ); } - guildMemberUpdate(data: GatewayEvent_ServerMemberUpdated): void{ + guildMemberUpdate(data: GatewayEvent_ServerMemberUpdated): void { + if (!this.isGuildIntentEnabled) return; const output = new MemberUpdateInfo(data, data.userInfo.id, this.client); this.client.emit("guildMemberUpdate", output); } @@ -141,6 +167,7 @@ export class GuildHandler extends GatewayEventHandler { const role = guild?.roles.add(new Role(data.role, this.client)) ?? new Role(data.role, this.client); + if (!this.isGuildIntentEnabled) return; this.client.emit("guildRoleCreate", role); } @@ -150,6 +177,7 @@ export class GuildHandler extends GatewayEventHandler { guild?.roles.update(new Role(data.role, this.client)) ?? new Role(data.role, this.client); guild?.roles.delete(data.role.id); + if (!this.isGuildIntentEnabled) return; this.client.emit("guildRoleDelete", role); } guildRoleUpdate(data: GatewayEvent_RoleUpdated): void { @@ -158,6 +186,7 @@ export class GuildHandler extends GatewayEventHandler { const role = guild?.roles.update(new Role(data.role, this.client)) ?? new Role(data.role, this.client); + if (!this.isGuildIntentEnabled) return; this.client.emit("guildRoleUpdate", role, cachedRole); } } diff --git a/lib/gateway/events/MessageHandler.ts b/lib/gateway/events/MessageHandler.ts index e7644dd..3731794 100644 --- a/lib/gateway/events/MessageHandler.ts +++ b/lib/gateway/events/MessageHandler.ts @@ -17,11 +17,13 @@ import { type GatewayEvent_ChatMessageCreated, type GatewayEvent_ChatMessageDeleted, type GatewayEvent_ChatMessageUpdated, - GatewayLayerIntent + GatewayLayerIntent, + InteractionComponentType } from "../../Constants"; import { type TextChannel } from "../../structures/TextChannel"; -import type { ChannelMessageReactionBulkRemove, PrivateApplicationCommand } from "../../types/"; +import type { AnyTextableChannel, ChannelMessageReactionBulkRemove, PrivateApplicationCommand } from "../../types/"; import { CommandInteraction } from "../../structures/CommandInteraction"; +import { ComponentInteraction } from "../../structures/ComponentInteraction"; /** Internal component, emitting message events. */ export class MessageHandler extends GatewayEventHandler { @@ -37,9 +39,6 @@ export class MessageHandler extends GatewayEventHandler { const guild = this.client.guilds.get(guildID); if (typeof channel !== "boolean") guild?.channels?.add(channel); } - get isGuildIntentEnabled(): boolean { - return this.client.util.isIntentEnabled([GatewayLayerIntent.GUILDS]); - } get isMessageIntentEnabled(): boolean { return this.client.util.isIntentEnabled([GatewayLayerIntent.GUILD_MESSAGES]); } @@ -47,11 +46,9 @@ export class MessageHandler extends GatewayEventHandler { return this.client.util.isIntentEnabled([GatewayLayerIntent.GUILD_MESSAGE_REACTIONS]); } async messageCreate(data: GatewayEvent_ChatMessageCreated): Promise { - if (this.isGuildIntentEnabled) { - if (this.client.params.waitForCaching) - await this.addGuildChannel(data.serverId, data.message.channelId); - else void this.addGuildChannel(data.serverId, data.message.channelId); - } + if (this.client.params.waitForCaching) + await this.addGuildChannel(data.serverId, data.message.channelId); + else void this.addGuildChannel(data.serverId, data.message.channelId); if ( this.client.application.enabled @@ -96,7 +93,18 @@ export class MessageHandler extends GatewayEventHandler { || executionType === "full" ) { const interaction = - new CommandInteraction( + this.client.getChannel(data.serverId, data.message.channelId) + ?.interactions.update( + { + guildID: data.serverId, + message: data.message, + name: currentCommandName!, + directReply: isReplyingApp, + executionType + }, + {} + ) + ?? new CommandInteraction( { guildID: data.serverId, message: data.message, @@ -152,31 +160,27 @@ export class MessageHandler extends GatewayEventHandler { } } - if (!this.isMessageIntentEnabled) return; - if (!this.client.util.isIntentEnabled([GatewayLayerIntent.MESSAGE_CONTENT])) data.message.content = ""; const channel = this.client.getChannel(data.serverId, data.message.channelId); const MessageComponent = - channel?.messages?.update(data.message) ?? new Message(data.message, this.client); + channel?.messages?.update(data.message, {}) ?? new Message(data.message, this.client); + if (!this.isMessageIntentEnabled) return; this.client.emit("messageCreate", MessageComponent); } async messageDelete(data: GatewayEvent_ChatMessageDeleted): Promise { - if (this.isGuildIntentEnabled) { - if (this.client.params.waitForCaching) - await this.addGuildChannel(data.serverId, data.message.channelId); - else void this.addGuildChannel(data.serverId, data.message.channelId); - } + if (this.client.params.waitForCaching) + await this.addGuildChannel(data.serverId, data.message.channelId); + else void this.addGuildChannel(data.serverId, data.message.channelId); - if (!this.isMessageIntentEnabled) return; const channel = this.client.getChannel(data.serverId, data.message.channelId); - const PU_Message = channel?.messages?.update(data.message) ?? { + const PU_Message = channel?.messages?.update(data.message, {}) ?? { id: data.message.id, guildID: data.serverId, channelID: data.message.channelId, @@ -189,16 +193,13 @@ export class MessageHandler extends GatewayEventHandler { && PU_Message["content" as keyof object] ) Object.assign(PU_Message, { content: "" }); + if (!this.isMessageIntentEnabled) return; this.client.emit("messageDelete", PU_Message); } async messagePin(data: GatewayEvent_ChannelMessagePinned): Promise { - if (this.isGuildIntentEnabled) { - if (this.client.params.waitForCaching) - await this.addGuildChannel(data.serverId, data.message.channelId); - else void this.addGuildChannel(data.serverId, data.message.channelId); - } - - if (!this.isMessageIntentEnabled) return; + if (this.client.params.waitForCaching) + await this.addGuildChannel(data.serverId, data.message.channelId); + else void this.addGuildChannel(data.serverId, data.message.channelId); if (!this.client.util.isIntentEnabled([GatewayLayerIntent.MESSAGE_CONTENT])) data.message.content = ""; @@ -206,21 +207,60 @@ export class MessageHandler extends GatewayEventHandler { const channel = this.client.getChannel(data.serverId, data.message.channelId); const MessageComponent = - channel?.messages?.update(data.message) ?? new Message(data.message, this.client); + channel?.messages?.update(data.message, {}) ?? new Message(data.message, this.client); + + if (!this.isMessageIntentEnabled) return; this.client.emit("messagePin", MessageComponent); } async messageReactionAdd(data: GatewayEvent_ChannelMessageReactionCreated): Promise { - if (this.isGuildIntentEnabled && data.serverId) { + if (data.serverId) { if (this.client.params.waitForCaching) await this.addGuildChannel(data.serverId, data.reaction.channelId); else void this.addGuildChannel(data.serverId, data.reaction.channelId); } - if (!this.isMessageReactionIntentEnabled) return; const ReactionInfo = new MessageReactionInfo(data, this.client); + + if (this.client.application.enabled && data.serverId) { + // from cache + const channel = + this.client.getChannel(data.serverId, data.reaction.channelId); + if (channel && channel.messages.has(data.reaction.messageId)) { + const interactionMessage = + channel.messages.get(data.reaction.messageId)!; + const originalInteraction = + channel.interactions.get(interactionMessage.originals.triggerID ?? "none"); + const hasComponents = interactionMessage.components.length !== 0; + const emoteComponent = + interactionMessage.components + .find(component => + component.type === InteractionComponentType.BUTTON && component.emoteID === data.reaction.emote.id + ); + if (originalInteraction + && hasComponents + && emoteComponent + && data.reaction.createdBy !== this.client.user?.id + ) { + this.client.emit( + "interactionCreate", + new ComponentInteraction( + { + customID: emoteComponent.customID, + emoteID: emoteComponent.emoteID, + userTriggerMessageID: originalInteraction.id, + reactionInfo: ReactionInfo + }, + this.client + ) + ); + } + } + } + + if (!this.isMessageReactionIntentEnabled) return; this.client.emit("reactionAdd", ReactionInfo); } async messageReactionBulkRemove(data: GatewayEvent_ChannelMessageReactionManyDeleted): Promise { - if (this.isGuildIntentEnabled && data.serverId) { + if (data.serverId) { if (this.client.params.waitForCaching) await this.addGuildChannel(data.serverId, data.channelId); else void this.addGuildChannel(data.serverId, data.channelId); @@ -237,7 +277,7 @@ export class MessageHandler extends GatewayEventHandler { this.client.emit("reactionBulkRemove", BulkRemoveInfo); } async messageReactionRemove(data: GatewayEvent_ChannelMessageReactionDeleted): Promise { - if (this.isGuildIntentEnabled && data.serverId) { + if (data.serverId) { if (this.client.params.waitForCaching) await this.addGuildChannel(data.serverId, data.reaction.channelId); else void this.addGuildChannel(data.serverId, data.reaction.channelId); @@ -247,26 +287,22 @@ export class MessageHandler extends GatewayEventHandler { this.client.emit("reactionRemove", ReactionInfo); } async messageUnpin(data: GatewayEvent_ChannelMessageUnpinned): Promise { - if (this.isGuildIntentEnabled) { - if (this.client.params.waitForCaching) - await this.addGuildChannel(data.serverId, data.message.channelId); - else void this.addGuildChannel(data.serverId, data.message.channelId); - } + if (this.client.params.waitForCaching) + await this.addGuildChannel(data.serverId, data.message.channelId); + else void this.addGuildChannel(data.serverId, data.message.channelId); if (!this.isMessageIntentEnabled) return; if (!this.client.util.isIntentEnabled([GatewayLayerIntent.MESSAGE_CONTENT])) data.message.content = ""; const channel = this.client.getChannel(data.serverId, data.message.channelId); const MessageComponent = - channel?.messages?.update(data.message) ?? new Message(data.message, this.client); + channel?.messages?.update(data.message, {}) ?? new Message(data.message, this.client); this.client.emit("messageUnpin", MessageComponent); } async messageUpdate(data: GatewayEvent_ChatMessageUpdated): Promise { - if (this.isGuildIntentEnabled) { - if (this.client.params.waitForCaching) - await this.addGuildChannel(data.serverId, data.message.channelId); - else void this.addGuildChannel(data.serverId, data.message.channelId); - } + if (this.client.params.waitForCaching) + await this.addGuildChannel(data.serverId, data.message.channelId); + else void this.addGuildChannel(data.serverId, data.message.channelId); if (!this.isMessageIntentEnabled) return; if (!this.client.util.isIntentEnabled([GatewayLayerIntent.MESSAGE_CONTENT])) data.message.content = ""; @@ -274,7 +310,7 @@ export class MessageHandler extends GatewayEventHandler { this.client.getChannel(data.serverId, data.message.channelId); const CachedMessage = channel?.messages?.get(data.message.id)?.toJSON() ?? null; const MessageComponent = - channel?.messages?.update(data.message) ?? new Message(data.message, this.client); + channel?.messages?.update(data.message, {}) ?? new Message(data.message, this.client); this.client.emit("messageUpdate", MessageComponent, CachedMessage); } } diff --git a/lib/routes/Channels.ts b/lib/routes/Channels.ts index e8023db..67280d0 100644 --- a/lib/routes/Channels.ts +++ b/lib/routes/Channels.ts @@ -375,6 +375,7 @@ export class Channels { json: { message: content, note } }).then(data => new ListItem(data.listItem, this.#manager.client)); } + /** Send a message in a specified channel. * @param channelID ID of the channel. * @param options Message options diff --git a/lib/structures/Client.ts b/lib/structures/Client.ts index 809b7c7..7510e7a 100644 --- a/lib/structures/Client.ts +++ b/lib/structures/Client.ts @@ -358,10 +358,10 @@ export class Client extends TypedEmitter { * @param channelID ID of the channel containing the message. * @param messageID ID of the message you'd like to get. */ - getMessage(guildID: string, channelID: string, messageID: string): Message | undefined { + getMessage(guildID: string, channelID: string, messageID: string): Message | undefined { const channel = this.getChannel(guildID, channelID); if (channel instanceof TextChannel) { - return channel?.messages.get(messageID); + return channel?.messages.get(messageID) as Message; } } diff --git a/lib/structures/CommandInteraction.ts b/lib/structures/CommandInteraction.ts index f0f27b3..62f3951 100644 --- a/lib/structures/CommandInteraction.ts +++ b/lib/structures/CommandInteraction.ts @@ -15,19 +15,19 @@ import { Member } from "./Member"; import type { TextChannel } from "./TextChannel"; import type { AnyTextableChannel, - InteractionData, + InteractionConstructorParams, CommandInteractionData, - CreateMessageOptions, - RawMentions, + CreateInteractionMessageOptions, EditMessageOptions, Embed, - CommandInteractionConstructorParams, - JSONCommandInteraction + InteractionData, + JSONCommandInteraction, + RawMentions } from "../types"; import { InteractionOptionWrapper } from "../util/InteractionOptionWrapper"; /** Represents a Command Interaction. */ -export class CommandInteraction extends Base { +export class CommandInteraction extends Base { private _cachedChannel!: T extends AnyTextableChannel ? T : undefined; private _cachedGuild?: T extends Guild ? Guild : Guild | null; /** ID of the last message created with this interaction. */ @@ -57,7 +57,7 @@ export class CommandInteraction extends Base extends Base> { + async createFollowup(options: CreateInteractionMessageOptions): Promise> { if (!this.acknowledged || !this.originalID) throw new Error( "Interaction has not been acknowledged, " + @@ -211,6 +211,7 @@ export class CommandInteraction extends Base(this.channelID, options.components ?? [], response); if (!(this.originalID)) this.originalID = response.id; return response; } @@ -219,7 +220,7 @@ export class CommandInteraction extends Base> { + async createMessage(options: CreateInteractionMessageOptions): Promise> { if (this.acknowledged) throw new Error( "Interaction has already been acknowledged, " + @@ -249,6 +250,7 @@ export class CommandInteraction extends Base(this.channelID, options.components ?? [], response); if (!(this.originalID)) this.originalID = response.id; return response; } @@ -273,7 +275,7 @@ export class CommandInteraction extends Base>{ @@ -285,7 +287,7 @@ export class CommandInteraction extends Base extends Base { + private _cachedChannel!: T extends AnyTextableChannel ? T : undefined; + private _cachedGuild?: T extends AnyTextableChannel ? Guild : Guild | null; + /** ID of the last message created with this interaction. */ + _lastMessageID: string | null; + /** Interaction acknowledgement. */ + acknowledged: boolean; + /** Channel on which the interaction was sent, if cached. */ + channel: T | null; + /** ID of the channel on which the interaction was sent. */ + channelID: string; + /** Component Interaction Data */ + data: ComponentInteractionData; + /** ID of the server on which the interaction was sent. */ + guildID: (T extends AnyTextableChannel ? string : string | null) | null; + /** ID of the interaction author. */ + memberID: string; + /** Interaction Message, triggering this interaction, if cached. */ + message: Message | null; + /** ID of the Interaction Message, triggering this interaction. */ + messageID: string; + /** ID of the original response of this interaction, if existant. */ + originalID: string | null; + constructor( + data: ComponentInteractionData, + client: Client, + params?: InteractionConstructorParams + ) { + super(client.util.generateNumericID(), client); + this._lastMessageID = null; + this.acknowledged = false; + this.channelID = data.reactionInfo.channelID; + this.channel = client.getChannel(data.reactionInfo.raw.serverId ?? "none", this.channelID) ?? null; + this.data = { + customID: data.customID, + emoteID: data.emoteID, + userTriggerMessageID: data.userTriggerMessageID, + reactionInfo: data.reactionInfo + }; + this.guildID = data.reactionInfo.guildID; + this.messageID = data.reactionInfo.messageID; + this.memberID = data.reactionInfo.reactorID; + this.message = client.getMessage(this.guildID ?? "none", this.channelID, this.messageID) ?? null; + this.originalID = params?.originalID ?? null; + this.acknowledged = params?.acknowledged ?? false; + this.update(data); + } + + /** Create a follow-up message that replies to the original response. + * (use ComponentInteraction#createMessage if the interaction has not been acknowledged). + * @param options Message options. + */ + async createFollowup(options: CreateInteractionMessageOptions): Promise> { + if (!this.acknowledged || !this.originalID) + throw new Error( + "Interaction has not been acknowledged, " + + "please acknowledge the message using the createMessage method." + ); + + console.log(this.data.userTriggerMessageID, this.originalID, this.messageID); + + if (!options.replyMessageIDs) { + options.replyMessageIDs = [this.messageID]; + } else if (!options.replyMessageIDs.includes(this.messageID)) { + options.replyMessageIDs.push(this.messageID); + } + + if (options.replyMessageIDs?.includes(this.messageID)) { + options.replyMessageIDs[options.replyMessageIDs.length - 1] = this.originalID; + } + + if (!options.isPrivate && this.message?.isPrivate) options.isPrivate = true; + + const response = + await this.client.rest.channels.createMessage( + this.channelID, + options, + { + originals: { + triggerID: this.messageID, + responseID: this.originalID + }, + acknowledged: true + } + ); + + this._lastMessageID = response.id as string; + await this.client.util.bulkAddComponents(this.channelID, options.components ?? [], response); + if (!(this.originalID)) this.originalID = response.id; + return response; + } + + /** This method is used to create a message following this interaction + * (use ComponentInteraction#createFollowup on already acknowledged interactions). + * @param options Message options. + */ + async createMessage(options: CreateInteractionMessageOptions): Promise> { + if (this.acknowledged) + throw new Error( + "Interaction has already been acknowledged, " + + "please use the createFollowup method." + ); + + const idToUse = this.messageID; + if (!options.replyMessageIDs) { + options.replyMessageIDs = [idToUse]; + } else if (!options.replyMessageIDs.includes(idToUse)) { + options.replyMessageIDs.push(idToUse); + } + + if (!options.isPrivate && this.message?.isPrivate) options.isPrivate = true; + + const response = + await this.client.rest.channels.createMessage( + this.channelID, + options, + { + originals: { + triggerID: this.messageID, + responseID: this.originalID + }, + acknowledged: true + } + ); + this._lastMessageID = response.id as string; + this.acknowledged = true; + await this.client.util.bulkAddComponents(this.channelID, options.components ?? [], response); + if (!(this.originalID)) this.originalID = response.id; + return response; + } + + /** Edit the last message sent with the interaction. + * @param newMessage New message's options. + */ + async editLast(newMessage: EditMessageOptions): Promise>{ + if (!this._lastMessageID) throw new TypeError("Cannot edit last message if it does not exist."); + return this.client.rest.channels.editMessage( + this.channelID, + this._lastMessageID, + newMessage + ); + } + + /** Edit the message's original response message. + * @param newMessage New message's options. + */ + async editOriginal( + newMessage: { + content?: string; + embeds?: Array; + } + ): Promise> { + if (!this.originalID) + throw new Error( + "Couldn't edit the original message from this Interaction, " + + "as it either does not exist or has not been cached." + ); + + return this.client.rest.channels.editMessage( + this.channelID, + this.originalID, + newMessage, + { + originals: { + triggerID: this.messageID, + responseID: this.originalID + } + } + ); + } + + /** + * Edit Parent Interaction. + * @param newMessage New message content. + */ + async editParent(newMessage: EditInteractionMessageOptions): Promise> { + if (this.acknowledged) throw new Error("Cannot edit parent interaction that has already been acknowledged."); + this.acknowledged = true; + return this.message!.edit(newMessage); + } + + /** + * Get the latest message sent with this interaction. + */ + async getLast(): Promise> { + if (!this._lastMessageID) + throw new TypeError("Cannot get last message if it does not exist."); + return this.client.rest.channels.getMessage( + this.channelID, + this._lastMessageID + ); + } + + /** + * Get the original message response. + */ + async getOriginal(): Promise> { + if (!this.originalID) + throw new Error( + "Couldn't get the original message from this interaction, " + + "as it either does not exist or has not been cached." + ); + + return this.client.rest.channels.getMessage( + this.channelID, + this.originalID, + { + originals: { + triggerID: this.messageID, + responseID: this.originalID + } + } + ); + } + + override toJSON(): JSONComponentInteraction { + return { + ...super.toJSON(), + _lastMessageID: this._lastMessageID, + acknowledged: this.acknowledged, + channelID: this.channelID, + data: this.data, + guildID: this.guildID, + memberID: this.memberID, + messageID: this.messageID, + originalID: this.originalID + }; + } +} diff --git a/lib/structures/Message.ts b/lib/structures/Message.ts index b7cc021..2d4537b 100644 --- a/lib/structures/Message.ts +++ b/lib/structures/Message.ts @@ -23,7 +23,8 @@ import type { Embed, RawMessage, RawEmbed, - RawMentions + RawMentions, + AnyInteractionComponent } from "../types"; /** Represents a guild message. */ @@ -36,6 +37,8 @@ export class Message extends Base { acknowledged: boolean; /** ID of the channel on which the message was sent. */ channelID: string; + /** Message components */ + components: Array; /** Content of the message. */ content: string | null; /** When the message was created. */ @@ -85,6 +88,7 @@ export class Message extends Base { this.guildID = data.serverId ?? null; this.channelID = data.channelId; this.content = data.content ?? ""; + this.components = []; this.hiddenLinkPreviewURLs = data.hiddenLinkPreviewUrls ?? []; this.embeds = data.embeds ?? []; this.replyMessageIDs = data.replyMessageIds ?? []; @@ -416,7 +420,12 @@ export class Message extends Base { /** This method is used to edit the current message. * @param newMessage New message's options */ - async edit(newMessage: EditMessageOptions): Promise>{ + async edit(newMessage: EditMessageOptions): Promise> { + if (newMessage["components" as keyof object]) this.components = newMessage["components" as keyof object]; + if (this.components) { + await this.client.rest.channels.bulkDeleteReactions(this.channelID, "ChannelMessage", this.id); + await this.client.util.bulkAddComponents(this.channelID, this.components, this, false); + } return this.client.rest.channels.editMessage( this.channelID, this.id as string, diff --git a/lib/structures/ReactionInfo.ts b/lib/structures/ReactionInfo.ts index 6b9ac00..2d780c6 100644 --- a/lib/structures/ReactionInfo.ts +++ b/lib/structures/ReactionInfo.ts @@ -34,6 +34,8 @@ export class ReactionInfo { client!: Client; /** Emote. */ emoji: RawEmote; + /** Guild ID */ + guildID: string | null; raw: GatewayEvent_ChannelMessageReactionCreated | GatewayEvent_ChannelMessageReactionDeleted | GatewayEvent_ForumTopicReactionCreated @@ -77,6 +79,7 @@ export class ReactionInfo { ) { this.raw = data; this.channelID = data.reaction.channelId; + this.guildID = data.serverId ?? null; this.reactorID = data.reaction.createdBy; this.emoji = { id: data.reaction.emote.id, diff --git a/lib/structures/TextChannel.ts b/lib/structures/TextChannel.ts index c2bdc7b..817b110 100644 --- a/lib/structures/TextChannel.ts +++ b/lib/structures/TextChannel.ts @@ -10,11 +10,15 @@ import type { Client } from "./Client"; import { Message } from "./Message"; import { GuildChannel } from "./GuildChannel"; import type { Permission } from "./Permission"; +import { CommandInteraction } from "./CommandInteraction"; import type { AnyTextableChannel, + InteractionConstructorParams, + CommandInteractionData, CreateMessageOptions, EditMessageOptions, JSONTextChannel, + MessageConstructorParams, RawChannel, RawMessage } from "../types"; @@ -23,14 +27,26 @@ import TypedCollection from "../util/TypedCollection"; /** Represents a guild channel where you can chat with others. */ export class TextChannel extends GuildChannel { + /** Cached interactions. */ + interactions: TypedCollection< + string, + CommandInteractionData, + CommandInteraction, + [params?: InteractionConstructorParams] + >; /** Cached messages. */ - messages: TypedCollection>; + messages: TypedCollection, [params?: MessageConstructorParams]>; /** * @param data raw data * @param client client */ constructor(data: RawChannel, client: Client){ super(data, client); + this.interactions = new TypedCollection( + CommandInteraction, + client, + client.params.collectionLimits?.messages + ); this.messages = new TypedCollection( Message, client, @@ -122,9 +138,8 @@ export class TextChannel extends GuildChannel { override toJSON(): JSONTextChannel { return { ...super.toJSON(), - messages: this.messages.map(message => message.toJSON()) + interactions: this.interactions.map(interaction => interaction.toJSON()), + messages: this.messages.map(message => message.toJSON()) }; } - - } diff --git a/lib/types/channels.d.ts b/lib/types/channels.d.ts index 9e01664..c71023c 100644 --- a/lib/types/channels.d.ts +++ b/lib/types/channels.d.ts @@ -5,6 +5,7 @@ // Copyright (c) 2024 DinographicPixels. All rights reserved. // +import type { AnyInteractionComponent } from "./interactions"; import type { Message } from "../structures/Message"; import type { GuildChannel } from "../structures/GuildChannel"; import type { TextChannel } from "../structures/TextChannel"; @@ -89,6 +90,9 @@ export interface MessageOriginals { triggerMessage: Message | null; } +export type CreateInteractionMessageOptions = CreateMessageOptions & { components?: Array; }; +export type EditInteractionMessageOptions = EditMessageOptions & { components?: Array; }; + export interface CreateMessageOptions { /** The content of the message (min length 1; max length 4000) */ content?: string; diff --git a/lib/types/events.d.ts b/lib/types/events.d.ts index 129d66a..d96f932 100644 --- a/lib/types/events.d.ts +++ b/lib/types/events.d.ts @@ -45,6 +45,7 @@ import type { PossiblyUncachedMessage } from "./channels"; import type { RawAppUser } from "./users"; +import type { AnyInteraction } from "./interactions"; import type { BannedMember } from "../structures/BannedMember"; import type { ForumThread } from "../structures/ForumThread"; import type { ForumThreadComment } from "../structures/ForumThreadComment"; @@ -70,7 +71,6 @@ import type { AnnouncementComment } from "../structures/AnnouncementComment"; import type { Group } from "../structures/Group"; import type { Role } from "../structures/Role"; import type { Category } from "../structures/Category"; -import type { CommandInteraction } from "../structures/CommandInteraction"; /** Every client events. */ export interface ClientEvents { @@ -217,7 +217,7 @@ export interface ClientEvents { /** @event Emitted when a guild role is updated. */ guildRoleUpdate: [role: Role, oldRole: JSONRole | null]; /** @event Emitted when an interaction is created. */ - interactionCreate: [interaction: CommandInteraction]; + interactionCreate: [interaction: AnyInteraction]; /** @event Emitted when a list item is completed. */ listItemComplete: [item: ListItem]; /** @event Emitted when a list item is created. */ diff --git a/lib/types/interactions.d.ts b/lib/types/interactions.d.ts index c77c9f4..3132c2e 100644 --- a/lib/types/interactions.d.ts +++ b/lib/types/interactions.d.ts @@ -7,6 +7,10 @@ import type { RawMentions, RawMessage } from "./channels"; import type { ApplicationCommand, PrivateApplicationCommand } from "./applications"; import type { InteractionOptionWrapper } from "../util/InteractionOptionWrapper"; +import type { InteractionComponentType } from "../Constants"; +import type { CommandInteraction } from "../structures/CommandInteraction"; +import type { ComponentInteraction } from "../structures/ComponentInteraction"; +import type { MessageReactionInfo } from "../structures/MessageReactionInfo"; export interface InteractionData { applicationCommand: ApplicationCommand | PrivateApplicationCommand; @@ -15,6 +19,13 @@ export interface InteractionData { // resolved?: InteractionResolved; } +export interface ComponentInteractionData { + customID: string; + emoteID: number; + reactionInfo: MessageReactionInfo; + userTriggerMessageID: string; +} + // export interface InteractionResolved { // users: TypedCollection; // } @@ -38,7 +49,20 @@ export interface CommandInteractionData { name: string; } -export interface CommandInteractionConstructorParams { +export interface InteractionConstructorParams { acknowledged?: boolean; originalID?: string; } + +export interface InteractionComponent { + type: InteractionComponentType; +} + +export interface InteractionButtonComponent extends InteractionComponent { + customID: string; + emoteID: number; + type: InteractionComponentType.BUTTON; +} + +export type AnyInteractionComponent = InteractionButtonComponent; +export type AnyInteraction = CommandInteraction | ComponentInteraction; diff --git a/lib/types/json.d.ts b/lib/types/json.d.ts index 6073dbb..6005d91 100644 --- a/lib/types/json.d.ts +++ b/lib/types/json.d.ts @@ -13,7 +13,7 @@ import type { RawMentions, CalendarRSVPStatus } from "./channels"; -import type { InteractionData } from "./interactions"; +import type { ComponentInteractionData, InteractionData } from "./interactions"; import type { Member } from "../structures/Member"; import type { User } from "../structures/User"; import type { Guild } from "../structures/Guild"; @@ -85,6 +85,25 @@ export interface JSONCommandInteraction extends JSONBase { replyMessageIDs: Array; } +export interface JSONComponentInteraction extends JSONBase { + /** ID of the last message created with this interaction. */ + _lastMessageID: string | null; + /** Interaction acknowledgement. */ + acknowledged: boolean; + /** ID of the channel on which the interaction was sent. */ + channelID: string; + /** Component Interaction Data */ + data: ComponentInteractionData; + /** ID of the server on which the interaction was sent. */ + guildID: string | null; + /** ID of the interaction author. */ + memberID: string; + /** ID of the Interaction Message, triggering this interaction. */ + messageID: string; + /** ID of the original response of this interaction, if existant. */ + originalID: string | null; +} + export interface JSONForumThreadComment extends JSONBase { /** ID of the forum channel containing this thread. */ channelID: string; @@ -164,6 +183,8 @@ export interface JSONGuildChannel extends JSONBase { } export interface JSONTextChannel extends JSONGuildChannel { + /** Cached interactions. */ + interactions: Array; /** Cached messages. */ messages: Array; } @@ -189,6 +210,7 @@ export interface JSONAnnouncementChannel extends JSONGuildChannel { } export type AnyJSONChannel = JSONTextChannel | JSONDocChannel | JSONForumChannel | JSONGuildChannel | JSONCalendarChannel; +export type AnyJSONInteraction = JSONCommandInteraction | JSONComponentInteraction; export interface JSONCalendarEvent extends JSONBase { /** Details about event cancellation (if canceled) */ diff --git a/lib/util/Util.ts b/lib/util/Util.ts index 44f57e6..51cc0b5 100644 --- a/lib/util/Util.ts +++ b/lib/util/Util.ts @@ -24,7 +24,8 @@ import type { RawCategory, RawMessage, RawEmbed, - MessageAttachment + MessageAttachment, + AnyInteractionComponent } from "../types"; import { Channel } from "../structures/Channel"; import { ForumThread } from "../structures/ForumThread"; @@ -36,7 +37,7 @@ import { Group } from "../structures/Group"; import { Subscription } from "../structures/Subscription"; import { Category } from "../structures/Category"; import { Message } from "../structures/Message"; -import { GatewayLayerIntent } from "../Constants"; +import { GatewayLayerIntent, InteractionComponentType } from "../Constants"; import type { APIURLSignature } from "guildedapi-types.ts/v1"; import { fetch } from "undici"; @@ -46,6 +47,40 @@ export class Util { this.#client = client; } + async bulkAddComponents( + channelID: string, + components: Array, + message: Message, + pushComponents = true + ): Promise> { + // TODO: Enhance errors/ add more of them making them easier to understand. + for (const component of components) { + if (component.type === InteractionComponentType.BUTTON) { + const regExpCheck = /^[\d_a-z-]{1,32}$/; + if (!regExpCheck.test(component.customID)) + throw new Error( + "Invalid component, customID property is considered invalid, " + + "requirements: \"1-32 characters containing no capital letters, spaces, or symbols other than - and _\"." + ); + await this.#client.rest.channels + .createReaction( + channelID, + "ChannelMessage", + message.id, + component.emoteID) + .catch((err: Error): void => { + this.#client.emit("error", err); + throw new Error( + "Invalid component error, please check formatting, " + + "emote availability or any other issue that could cause this error." + ); + }); + if (pushComponents) message.components.push(component); + } + } + return message; + } + embedsToParsed(embeds: Array): Array { return embeds.map(embed => ({ author: embed.author === undefined ? undefined : { @@ -99,6 +134,13 @@ export class Util { url: embed.url })); } + generateNumericID(length = 18): string { + let id = ""; + for (let i = 0; i < length; i++) { + id += Math.floor(Math.random() * 10).toString(); + } + return id; + } async getAttachments(attachmentURLs: Array): Promise> { const imageExtensions = new Set(["jpg", "jpeg", "png", "gif", "bmp", "webp", "svg"]); const MessageAttachments: Array = [];