Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(MessageComponents): clickybois (MessageButton, MessageActionRow, associated Collectors) #5674

Merged
merged 55 commits into from Jun 4, 2021
Merged
Show file tree
Hide file tree
Changes from 54 commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
801c3a5
chore: new branch without broken rebasing
monbrey May 25, 2021
9d54257
refactor: make component interactions more generic
monbrey May 25, 2021
2a9e3a5
Apply suggestions from code review
monbrey May 26, 2021
f454435
fix: add constants to typings
monbrey May 26, 2021
fe9f3c6
fix: export new classes
monbrey May 26, 2021
25cf234
feat: improved jsdoc typedefs
monbrey May 26, 2021
adb2cbf
fix: rename component type
monbrey May 26, 2021
3aafa5c
feat: typings for missing methods and general improvements
monbrey May 27, 2021
c0c3a58
fix: changed the wrong constant
monbrey May 27, 2021
52f128a
refactor: move interaction response methods to interface
monbrey May 27, 2021
a45bb0e
refactor: move additional response methods
monbrey May 27, 2021
7045839
fix: make Component resolvables less generic
monbrey May 27, 2021
17431c4
fix: bug in API transform of component
monbrey May 27, 2021
46513e3
fix: allow components in webhook edits
monbrey May 27, 2021
77d9398
feat: typings for new response type methods
monbrey May 27, 2021
310ff7a
fix: ordering
monbrey May 27, 2021
2422442
feat: collector for components for channels
monbrey May 27, 2021
8d3417a
feat: typings for component collectors
monbrey May 28, 2021
b1be9c6
fix: missing ComponentInteraction#componentType prop, plus docs
monbrey May 28, 2021
e2d2cde
fix: Apply suggestions from code review
monbrey May 28, 2021
10d9f1d
fix: apply suggestions from code review
monbrey May 28, 2021
641feef
refactor: big rename, consolidate the two collectors
monbrey May 28, 2021
77e4419
refactor: toJSON methods for api transformation
monbrey May 28, 2021
2d0c215
fix: typo
monbrey May 28, 2021
a2f0e59
fix: missing toJSON typings
monbrey May 28, 2021
eccc148
feat: handle invalid component types
monbrey May 28, 2021
b5db7bd
fix: typo
monbrey May 28, 2021
7b609ac
fix: rmeove types from Options definitions
monbrey May 28, 2021
d45e612
fix: apply suggestions from code review
monbrey May 28, 2021
cc7ec7d
fix: apply suggestions from code review
monbrey May 28, 2021
81b3311
fix: use webhook.send directly
monbrey May 28, 2021
fdf8ea7
fix: addComponents array bug
monbrey May 29, 2021
aa84502
feat: use emoji parsing
monbrey May 29, 2021
4659c2d
fix: apply suggestion from code review
monbrey May 29, 2021
1c549a1
fix: simplify components mapping
monbrey May 29, 2021
be380f2
fix: stringify emoji input
monbrey May 29, 2021
d6270b1
feat: stricter typings for components
monbrey May 30, 2021
cd196a1
fix: revert change to followUp, doesnt support ephem
monbrey May 30, 2021
c1d9a4b
fix: apply suggestions from code review
monbrey May 31, 2021
0cb81a3
fix(MessageButton): allow IDs in setEmoji
monbrey May 31, 2021
6e691d4
fix: align docs with typings, remove setType, add spliceComponents
monbrey May 31, 2021
9876dec
fix: apply suggestions from code review
monbrey Jun 1, 2021
b14168e
fix: suggestions from code review
monbrey Jun 1, 2021
a31cea9
fix: remove trailing whitespace
SpaceEEC Jun 1, 2021
77d3616
fix: remove max mention from addComponents too
SpaceEEC Jun 1, 2021
784bffc
types: apply suggestions from code review
monbrey Jun 1, 2021
d4ceeda
fix: use const
monbrey Jun 1, 2021
de003f3
chore: remove unused props and constructor
monbrey Jun 1, 2021
234321d
feat: enforce strings
monbrey Jun 2, 2021
b152631
Update src/structures/Message.js
monbrey Jun 2, 2021
a281d79
docs: prop MessageButton#style is nullable
monbrey Jun 2, 2021
2d9fd1b
feat: flatten to infinity
monbrey Jun 2, 2021
961db5c
fix: apply suggestions from code review
monbrey Jun 3, 2021
1a6a6c2
fix: apply suggestions from code review
monbrey Jun 3, 2021
4c0f226
types: missing method
monbrey Jun 4, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
35 changes: 23 additions & 12 deletions src/client/websocket/handlers/INTERACTION_CREATE.js
Expand Up @@ -4,20 +4,31 @@ const { Events, InteractionTypes } = require('../../../util/Constants');
let Structures;

module.exports = (client, { d: data }) => {
if (data.type === InteractionTypes.APPLICATION_COMMAND) {
if (!Structures) Structures = require('../../../util/Structures');
const CommandInteraction = Structures.get('CommandInteraction');
let interaction;
switch (data.type) {
case InteractionTypes.APPLICATION_COMMAND: {
if (!Structures) Structures = require('../../../util/Structures');
const CommandInteraction = Structures.get('CommandInteraction');

const interaction = new CommandInteraction(client, data);
interaction = new CommandInteraction(client, data);
break;
}
case InteractionTypes.MESSAGE_COMPONENT: {
if (!Structures) Structures = require('../../../util/Structures');
const MessageComponentInteraction = Structures.get('MessageComponentInteraction');

/**
* Emitted when an interaction is created.
* @event Client#interaction
* @param {Interaction} interaction The interaction which was created
*/
client.emit(Events.INTERACTION_CREATE, interaction);
return;
interaction = new MessageComponentInteraction(client, data);
break;
}
default:
client.emit(Events.DEBUG, `[INTERACTION] Received interaction with unknown type: ${data.type}`);
return;
}

client.emit(Events.DEBUG, `[INTERACTION] Received interaction with unknown type: ${data.type}`);
/**
* Emitted when an interaction is created.
* @event Client#interaction
* @param {Interaction} interaction The interaction which was created
*/
client.emit(Events.INTERACTION_CREATE, interaction);
};
4 changes: 4 additions & 0 deletions src/errors/Messages.js
Expand Up @@ -44,6 +44,10 @@ const Messages = {
EMBED_DESCRIPTION: 'MessageEmbed description must be a string.',
EMBED_AUTHOR_NAME: 'MessageEmbed author name must be a string.',

BUTTON_LABEL: 'MessageButton label must be a string',
BUTTON_URL: 'MessageButton url must be a string',
BUTTON_CUSTOM_ID: 'MessageButton customID must be a string',

FILE_NOT_FOUND: file => `File could not be found: ${file}`,

USER_NO_DMCHANNEL: 'No DM Channel exists!',
Expand Down
5 changes: 5 additions & 0 deletions src/index.js
Expand Up @@ -68,6 +68,7 @@ module.exports = {
BaseGuild: require('./structures/BaseGuild'),
BaseGuildEmoji: require('./structures/BaseGuildEmoji'),
BaseGuildVoiceChannel: require('./structures/BaseGuildVoiceChannel'),
BaseMessageComponent: require('./structures/BaseMessageComponent'),
CategoryChannel: require('./structures/CategoryChannel'),
Channel: require('./structures/Channel'),
ClientApplication: require('./structures/ClientApplication'),
Expand All @@ -92,8 +93,12 @@ module.exports = {
Interaction: require('./structures/Interaction'),
Invite: require('./structures/Invite'),
Message: require('./structures/Message'),
MessageActionRow: require('./structures/MessageActionRow'),
MessageAttachment: require('./structures/MessageAttachment'),
MessageButton: require('./structures/MessageButton'),
MessageCollector: require('./structures/MessageCollector'),
MessageComponentInteraction: require('./structures/MessageComponentInteraction'),
MessageComponentInteractionCollector: require('./structures/MessageComponentInteractionCollector'),
MessageEmbed: require('./structures/MessageEmbed'),
MessageMentions: require('./structures/MessageMentions'),
MessageReaction: require('./structures/MessageReaction'),
Expand Down
4 changes: 4 additions & 0 deletions src/structures/APIMessage.js
@@ -1,5 +1,6 @@
'use strict';

const BaseMessageComponent = require('./BaseMessageComponent');
const MessageAttachment = require('./MessageAttachment');
const MessageEmbed = require('./MessageEmbed');
const { RangeError } = require('../errors');
Expand Down Expand Up @@ -151,6 +152,8 @@ class APIMessage {
}
const embeds = embedLikes.map(e => new MessageEmbed(e).toJSON());

const components = this.options.components?.map(c => BaseMessageComponent.create(c).toJSON());

let username;
let avatarURL;
if (isWebhook) {
Expand Down Expand Up @@ -196,6 +199,7 @@ class APIMessage {
nonce,
embed: !isWebhookLike ? (this.options.embed === null ? null : embeds[0]) : undefined,
embeds: isWebhookLike ? embeds : undefined,
components,
username,
avatar_url: avatarURL,
allowed_mentions:
Expand Down
94 changes: 94 additions & 0 deletions src/structures/BaseMessageComponent.js
@@ -0,0 +1,94 @@
'use strict';

const { TypeError } = require('../errors');
const { MessageComponentTypes, Events } = require('../util/Constants');

/**
* Represents an interactive component of a Message. It should not be necessary to construct this directly.
monbrey marked this conversation as resolved.
Show resolved Hide resolved
* See {@link MessageComponent}
*/
class BaseMessageComponent {
/**
* Options for a BaseMessageComponent
* @typedef {Object} BaseMessageComponentOptions
* @property {MessageComponentTypeResolvable} type The type of this component
*/

/**
* Data that can be resolved into options for a MessageComponent. This can be:
* * MessageActionRowOptions
* * MessageButtonOptions
* @typedef {MessageActionRowOptions|MessageButtonOptions} MessageComponentOptions
*/

/**
* Components that can be sent in a message
* @typedef {MessageActionRow|MessageButton} MessageComponent
*/

/**
* Data that can be resolved to a MessageComponentType. This can be:
* * {@link MessageComponentType}
* * string
* * number
* @typedef {string|number|MessageComponentType} MessageComponentTypeResolvable
*/

/**
* @param {BaseMessageComponent|BaseMessageComponentOptions} [data={}] The options for this component
*/
constructor(data) {
/**
* The type of this component
* @type {?MessageComponentType}
*/
this.type = 'type' in data ? BaseMessageComponent.resolveType(data.type) : null;
}

/**
* Constructs a MessageComponent based on the type of the incoming data
* @param {MessageComponentOptions} data Data for a MessageComponent
* @param {Client|WebhookClient} [client] Client constructing this component
* @param {boolean} [skipValidation=false] Whether or not to validate the component type
* @returns {?MessageComponent}
* @private
*/
static create(data, client, skipValidation = false) {
let component;
let type = data.type;

if (typeof type === 'string') type = MessageComponentTypes[type];

switch (type) {
case MessageComponentTypes.ACTION_ROW: {
const MessageActionRow = require('./MessageActionRow');
component = new MessageActionRow(data);
break;
}
case MessageComponentTypes.BUTTON: {
const MessageButton = require('./MessageButton');
component = new MessageButton(data);
break;
}
default:
if (client) {
client.emit(Events.DEBUG, `[BaseMessageComponent] Received component with unknown type: ${data.type}`);
} else if (!skipValidation) {
throw new TypeError('INVALID_TYPE', 'data.type', 'valid MessageComponentType');
}
}
return component;
}

/**
* Resolves the type of a MessageComponent
* @param {MessageComponentTypeResolvable} type The type to resolve
* @returns {MessageComponentType}
* @private
*/
static resolveType(type) {
return typeof type === 'string' ? type : MessageComponentTypes[type];
}
}

module.exports = BaseMessageComponent;
156 changes: 14 additions & 142 deletions src/structures/CommandInteraction.js
@@ -1,16 +1,15 @@
'use strict';

const APIMessage = require('./APIMessage');
const Interaction = require('./Interaction');
const InteractionResponses = require('./interfaces/InteractionResponses');
const WebhookClient = require('../client/WebhookClient');
const { Error } = require('../errors');
const Collection = require('../util/Collection');
const { ApplicationCommandOptionTypes, InteractionResponseTypes } = require('../util/Constants');
const MessageFlags = require('../util/MessageFlags');
const { ApplicationCommandOptionTypes } = require('../util/Constants');

/**
* Represents a command interaction.
monbrey marked this conversation as resolved.
Show resolved Hide resolved
* @extends {Interaction}
* @implements {InteractionResponses}
*/
class CommandInteraction extends Interaction {
constructor(client, data) {
Expand Down Expand Up @@ -69,126 +68,6 @@ class CommandInteraction extends Interaction {
return this.guild?.commands.cache.get(id) ?? this.client.application.commands.cache.get(id) ?? null;
}

/**
* Options for deferring the reply to a {@link CommandInteraction}.
* @typedef {Object} InteractionDeferOptions
* @property {boolean} [ephemeral] Whether the reply should be ephemeral
*/

/**
* Defers the reply to this interaction.
* @param {InteractionDeferOptions} [options] Options for deferring the reply to this interaction
* @returns {Promise<void>}
* @example
* // Defer the reply to this interaction
* interaction.defer()
* .then(console.log)
* .catch(console.error)
* @example
* // Defer to send an ephemeral reply later
* interaction.defer({ ephemeral: true })
* .then(console.log)
* .catch(console.error);
*/
async defer({ ephemeral } = {}) {
if (this.deferred || this.replied) throw new Error('INTERACTION_ALREADY_REPLIED');
await this.client.api.interactions(this.id, this.token).callback.post({
data: {
type: InteractionResponseTypes.DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE,
data: {
flags: ephemeral ? MessageFlags.FLAGS.EPHEMERAL : undefined,
},
},
});
this.deferred = true;
}

/**
* Options for a reply to an interaction.
* @typedef {BaseMessageOptions} InteractionReplyOptions
* @property {boolean} [ephemeral] Whether the reply should be ephemeral
* @property {MessageEmbed[]|Object[]} [embeds] An array of embeds for the message
*/

/**
* Creates a reply to this interaction.
* @param {string|APIMessage|MessageAdditions} content The content for the reply
* @param {InteractionReplyOptions} [options] Additional options for the reply
* @returns {Promise<void>}
* @example
* // Reply to the interaction with an embed
* const embed = new MessageEmbed().setDescription('Pong!');
*
* interaction.reply(embed)
* .then(console.log)
* .catch(console.error);
* @example
* // Create an ephemeral reply
* interaction.reply('Pong!', { ephemeral: true })
* .then(console.log)
* .catch(console.error);
*/
async reply(content, options) {
if (this.deferred || this.replied) throw new Error('INTERACTION_ALREADY_REPLIED');
const apiMessage = content instanceof APIMessage ? content : APIMessage.create(this, content, options);
const { data, files } = await apiMessage.resolveData().resolveFiles();

await this.client.api.interactions(this.id, this.token).callback.post({
data: {
type: InteractionResponseTypes.CHANNEL_MESSAGE_WITH_SOURCE,
data,
},
files,
});
this.replied = true;
}

/**
* Fetches the initial reply to this interaction.
* @see Webhook#fetchMessage
* @returns {Promise<Message|Object>}
* @example
* // Fetch the reply to this interaction
* interaction.fetchReply()
* .then(reply => console.log(`Replied with ${reply.content}`))
* .catch(console.error);
*/
async fetchReply() {
const raw = await this.webhook.fetchMessage('@original');
return this.channel?.messages.add(raw) ?? raw;
}

/**
* Edits the initial reply to this interaction.
* @see Webhook#editMessage
* @param {string|APIMessage|MessageAdditions} content The new content for the message
* @param {WebhookEditMessageOptions} [options] The options to provide
* @returns {Promise<Message|Object>}
* @example
* // Edit the reply to this interaction
* interaction.editReply('New content')
* .then(console.log)
* .catch(console.error);
*/
async editReply(content, options) {
const raw = await this.webhook.editMessage('@original', content, options);
return this.channel?.messages.add(raw) ?? raw;
}

/**
* Deletes the initial reply to this interaction.
* @see Webhook#deleteMessage
* @returns {Promise<void>}
* @example
* // Delete the reply to this interaction
* interaction.deleteReply()
* .then(console.log)
* .catch(console.error);
*/
async deleteReply() {
await this.webhook.deleteMessage('@original');
}

/**
* Represents an option of a received command interaction.
* @typedef {Object} CommandInteractionOption
Expand All @@ -203,24 +82,6 @@ class CommandInteraction extends Interaction {
* @property {Role|Object} [role] The resolved role
*/

/**
* Send a follow-up message to this interaction.
* @param {string|APIMessage|MessageAdditions} content The content for the reply
* @param {InteractionReplyOptions} [options] Additional options for the reply
* @returns {Promise<Message|Object>}
*/
async followUp(content, options) {
const apiMessage = content instanceof APIMessage ? content : APIMessage.create(this, content, options);
const { data, files } = await apiMessage.resolveData().resolveFiles();

const raw = await this.client.api.webhooks(this.applicationID, this.token).post({
data,
files,
});

return this.channel?.messages.add(raw) ?? raw;
}

/**
* Transforms an option received from the API.
* @param {Object} option The received option
Expand Down Expand Up @@ -267,6 +128,17 @@ class CommandInteraction extends Interaction {
}
return optionsCollection;
}

// These are here only for documentation purposes - they are implemented by InteractionResponses
/* eslint-disable no-empty-function */
defer() {}
reply() {}
fetchReply() {}
editReply() {}
deleteReply() {}
followUp() {}
}

InteractionResponses.applyToClass(CommandInteraction, ['deferUpdate', 'update']);

module.exports = CommandInteraction;
2 changes: 2 additions & 0 deletions src/structures/DMChannel.js
Expand Up @@ -91,6 +91,8 @@ class DMChannel extends Channel {
get typingCount() {}
createMessageCollector() {}
awaitMessages() {}
createMessageComponentInteractionCollector() {}
awaitMessageComponentInteractions() {}
// Doesn't work on DM channels; bulkDelete() {}
}

Expand Down