From 9f3c3e091844d22793cfd2880ffc2936ce41feab Mon Sep 17 00:00:00 2001 From: Advaith Date: Sun, 29 Nov 2020 16:08:54 -0800 Subject: [PATCH 01/17] docs(WebSocketManager): fix type of status (#5059) --- src/client/websocket/WebSocketManager.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/websocket/WebSocketManager.js b/src/client/websocket/WebSocketManager.js index f892985f..586c83d4 100644 --- a/src/client/websocket/WebSocketManager.js +++ b/src/client/websocket/WebSocketManager.js @@ -76,7 +76,7 @@ class WebSocketManager extends EventEmitter { /** * The current status of this WebSocketManager - * @type {number} + * @type {Status} */ this.status = Status.IDLE; From 90d458820b145a01d59399f7c99c7364f5f07702 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Rom=C3=A1n?= Date: Sun, 6 Dec 2020 17:58:08 +0100 Subject: [PATCH 02/17] chore(Engine): bump Node.js to v14.0.0 (#5067) Co-authored-by: SpaceEEC --- .github/workflows/deploy.yml | 8 ++++---- .github/workflows/test-cron.yml | 16 ++++++++-------- .github/workflows/test.yml | 16 ++++++++-------- README.md | 2 +- docs/general/faq.md | 2 +- docs/general/welcome.md | 2 +- package.json | 2 +- 7 files changed, 24 insertions(+), 24 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index c3c30e1c..66717370 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -15,10 +15,10 @@ jobs: - name: Checkout repository uses: actions/checkout@master - - name: Install Node v12 + - name: Install Node v14 uses: actions/setup-node@master with: - node-version: 12 + node-version: 14 - name: Install dependencies run: npm ci @@ -35,10 +35,10 @@ jobs: - name: Checkout repository uses: actions/checkout@master - - name: Install Node v12 + - name: Install Node v14 uses: actions/setup-node@master with: - node-version: 12 + node-version: 14 - name: Install dependencies run: npm ci diff --git a/.github/workflows/test-cron.yml b/.github/workflows/test-cron.yml index ec1fdb11..cbfd9eee 100644 --- a/.github/workflows/test-cron.yml +++ b/.github/workflows/test-cron.yml @@ -10,10 +10,10 @@ jobs: - name: Checkout repository uses: actions/checkout@v1 - - name: Install Node v12 + - name: Install Node v14 uses: actions/setup-node@v1 with: - node-version: 12 + node-version: 14 - name: Install dependencies run: npm ci @@ -28,10 +28,10 @@ jobs: - name: Checkout repository uses: actions/checkout@v1 - - name: Install Node v12 + - name: Install Node v14 uses: actions/setup-node@v1 with: - node-version: 12 + node-version: 14 - name: Install dependencies run: npm ci @@ -46,10 +46,10 @@ jobs: - name: Checkout repository uses: actions/checkout@v2 - - name: Install Node v12 + - name: Install Node v14 uses: actions/setup-node@v1 with: - node-version: 12 + node-version: 14 - name: Install dependencies run: npm ci @@ -67,10 +67,10 @@ jobs: - name: Checkout repository uses: actions/checkout@v1 - - name: Install Node v12 + - name: Install Node v14 uses: actions/setup-node@v1 with: - node-version: 12 + node-version: 14 - name: Install dependencies run: npm ci diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 43ab5c09..4a0f3b20 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,10 +8,10 @@ jobs: - name: Checkout repository uses: actions/checkout@v2 - - name: Install Node v12 + - name: Install Node v14 uses: actions/setup-node@v1 with: - node-version: 12 + node-version: 14 - name: Install dependencies run: npm ci @@ -26,10 +26,10 @@ jobs: - name: Checkout repository uses: actions/checkout@v2 - - name: Install Node v12 + - name: Install Node v14 uses: actions/setup-node@v1 with: - node-version: 12 + node-version: 14 - name: Install dependencies run: npm ci @@ -44,10 +44,10 @@ jobs: - name: Checkout repository uses: actions/checkout@v2 - - name: Install Node v12 + - name: Install Node v14 uses: actions/setup-node@v1 with: - node-version: 12 + node-version: 14 - name: Install dependencies run: npm ci @@ -65,10 +65,10 @@ jobs: - name: Checkout repository uses: actions/checkout@v2 - - name: Install Node v12 + - name: Install Node v14 uses: actions/setup-node@v1 with: - node-version: 12 + node-version: 14 - name: Install dependencies run: npm ci diff --git a/README.md b/README.md index cbc2bb0e..46535a50 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ discord.js is a powerful [Node.js](https://nodejs.org) module that allows you to ## Installation -**Node.js 12.0.0 or newer is required.** +**Node.js 14.0.0 or newer is required.** Ignore any warnings about unmet peer dependencies, as they're all optional. Without voice support: `npm install discord.js` diff --git a/docs/general/faq.md b/docs/general/faq.md index 6624a727..466cac1c 100644 --- a/docs/general/faq.md +++ b/docs/general/faq.md @@ -4,7 +4,7 @@ These questions are some of the most frequently asked. ## No matter what, I get `SyntaxError: Block-scoped declarations (let, const, function, class) not yet supported outside strict mode`‽ -Update to Node.js 12.0.0 or newer. +Update to Node.js 14.0.0 or newer. ## How do I get voice working? diff --git a/docs/general/welcome.md b/docs/general/welcome.md index 2d639c6a..efd7a7e7 100644 --- a/docs/general/welcome.md +++ b/docs/general/welcome.md @@ -33,7 +33,7 @@ discord.js is a powerful [Node.js](https://nodejs.org) module that allows you to ## Installation -**Node.js 12.0.0 or newer is required.** +**Node.js 14.0.0 or newer is required.** Ignore any warnings about unmet peer dependencies, as they're all optional. Without voice support: `npm install discord.js` diff --git a/package.json b/package.json index 83bf9031..9c0848ee 100644 --- a/package.json +++ b/package.json @@ -80,7 +80,7 @@ "webpack-cli": "^3.3.12" }, "engines": { - "node": ">=12.0.0" + "node": ">=14.0.0" }, "browser": { "@discordjs/opus": false, From e272fd6909a17941d2d3e4840e75436d98a41198 Mon Sep 17 00:00:00 2001 From: BannerBomb Date: Sun, 6 Dec 2020 11:59:12 -0500 Subject: [PATCH 03/17] fix(BaseGuildEmoji): typo in requiresColons (#5076) --- src/structures/BaseGuildEmoji.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/structures/BaseGuildEmoji.js b/src/structures/BaseGuildEmoji.js index a2007c67..09dd04f3 100644 --- a/src/structures/BaseGuildEmoji.js +++ b/src/structures/BaseGuildEmoji.js @@ -17,7 +17,7 @@ class BaseGuildEmoji extends Emoji { */ this.guild = guild; - this.requireColons = null; + this.requiresColons = null; this.managed = null; this.available = null; From 09d07553ab5f4fa8643862129c17550a21e1c081 Mon Sep 17 00:00:00 2001 From: Carter <45381083+Fyko@users.noreply.github.com> Date: Sun, 6 Dec 2020 10:03:39 -0700 Subject: [PATCH 04/17] docs(User): fix typos in jsdoc (#5060) --- src/structures/User.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/structures/User.js b/src/structures/User.js index d7ebe03f..2811e862 100644 --- a/src/structures/User.js +++ b/src/structures/User.js @@ -278,7 +278,7 @@ class User extends Base { /** * Fetches this user's flags. - * @param {boolean} [force=false] Whether to skip the cache check and request the AP + * @param {boolean} [force=false] Whether to skip the cache check and request the API * @returns {Promise} */ async fetchFlags(force = false) { @@ -290,7 +290,7 @@ class User extends Base { /** * Fetches this user. - * @param {boolean} [force=false] Whether to skip the cache check and request the AP + * @param {boolean} [force=false] Whether to skip the cache check and request the API * @returns {Promise} */ fetch(force = false) { From 7365f403006eeb28ab10f03cbf85416272678ef7 Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Tue, 8 Dec 2020 20:11:44 +0100 Subject: [PATCH 05/17] fix(Collector): throw an error if a non-function was provided as filter (#5034) --- src/structures/interfaces/Collector.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/structures/interfaces/Collector.js b/src/structures/interfaces/Collector.js index 969b3d2c..e8745385 100644 --- a/src/structures/interfaces/Collector.js +++ b/src/structures/interfaces/Collector.js @@ -1,6 +1,7 @@ 'use strict'; const EventEmitter = require('events'); +const { TypeError } = require('../../errors'); const Collection = require('../../util/Collection'); const Util = require('../../util/Util'); @@ -74,6 +75,10 @@ class Collector extends EventEmitter { */ this._idletimeout = null; + if (typeof filter !== 'function') { + throw new TypeError('INVALID_TYPE', 'filter', 'function'); + } + this.handleCollect = this.handleCollect.bind(this); this.handleDispose = this.handleDispose.bind(this); From 60e5a0e46f57cf297b66f1a940d24a20f46b5319 Mon Sep 17 00:00:00 2001 From: monbrey Date: Wed, 9 Dec 2020 07:08:26 +1100 Subject: [PATCH 06/17] feat(Message|TextChannel): Inline replies (#4874) * feat(Message): remove reply functionality * feat(InlineReplies): add INLINE_REPLY constant/typing * feat(InlineReplies): add Message#replyReference property * feat(InlineReplies): add typings for sending inline replies * feat(InlineReplies): provide support for inline-replying to messages * feat(Message): add referencedMessage getter * fix: check that Message#reference is defined in referencedMessage * refactor(InlineReplies): rename property, rework Message resolution * docs: update jsdoc for inline replies * feat(Message): inline reply method * fix(ApiMessage): finish renaming replyTo * fix: jsdocs for Message#referencedMessage Co-authored-by: Tristan Guichaoua <33934311+tguichaoua@users.noreply.github.com> * fix: restore reply typings * fix: dont pass channel_id to API when replying * chore: update jsdocs * chore: more jsdoc updates * feat(AllowedMentions): add typings for replied_user * fix: naming conventions * fix(Message): referenced_message is null, not undefined * fix(MessageMentionOptions): repliedUser should be optional * chore: get this back to the right state * fix(ApiMessage): pass allowed_mentions when replying without content * fix(ApiMessage): prevent mutation of client options Co-authored-by: almostSouji Co-authored-by: Tristan Guichaoua <33934311+tguichaoua@users.noreply.github.com> --- README.md | 2 +- docs/examples/avatars.js | 2 +- docs/examples/moderation.md | 16 +++---- docs/general/welcome.md | 2 +- docs/topics/voice.md | 2 +- src/structures/APIMessage.js | 48 ++++++++----------- src/structures/Emoji.js | 2 +- src/structures/Message.js | 38 ++++++++++----- src/structures/interfaces/TextBasedChannel.js | 3 +- src/util/Constants.js | 5 ++ test/disableMentions.js | 6 +-- test/random.js | 4 +- test/sendtest.js | 3 -- test/tester1000.js | 7 +-- test/voice.js | 11 ++--- test/webhooktest.js | 3 -- typings/index.d.ts | 7 ++- 17 files changed, 80 insertions(+), 81 deletions(-) diff --git a/README.md b/README.md index 46535a50..146ee1af 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,7 @@ client.on('ready', () => { client.on('message', msg => { if (msg.content === 'ping') { - msg.reply('pong'); + msg.channel.send('pong'); } }); diff --git a/docs/examples/avatars.js b/docs/examples/avatars.js index acb13d3a..6687033d 100644 --- a/docs/examples/avatars.js +++ b/docs/examples/avatars.js @@ -23,7 +23,7 @@ client.on('message', message => { // If the message is "what is my avatar" if (message.content === 'what is my avatar') { // Send the user's avatar URL - message.reply(message.author.displayAvatarURL()); + message.channel.send(message.author.displayAvatarURL()); } }); diff --git a/docs/examples/moderation.md b/docs/examples/moderation.md index 2eebf7f7..30bc23f1 100644 --- a/docs/examples/moderation.md +++ b/docs/examples/moderation.md @@ -45,23 +45,23 @@ client.on('message', message => { .kick('Optional reason that will display in the audit logs') .then(() => { // We let the message author know we were able to kick the person - message.reply(`Successfully kicked ${user.tag}`); + message.channel.send(`Successfully kicked ${user.tag}`); }) .catch(err => { // An error happened // This is generally due to the bot not being able to kick the member, // either due to missing permissions or role hierarchy - message.reply('I was unable to kick the member'); + message.channel.send('I was unable to kick the member'); // Log the error console.error(err); }); } else { // The mentioned user isn't in this guild - message.reply("That user isn't in this guild!"); + message.channel.send("That user isn't in this guild!"); } // Otherwise, if no user was mentioned } else { - message.reply("You didn't mention the user to kick!"); + message.channel.send("You didn't mention the user to kick!"); } } }); @@ -121,23 +121,23 @@ client.on('message', message => { }) .then(() => { // We let the message author know we were able to ban the person - message.reply(`Successfully banned ${user.tag}`); + message.channel.send(`Successfully banned ${user.tag}`); }) .catch(err => { // An error happened // This is generally due to the bot not being able to ban the member, // either due to missing permissions or role hierarchy - message.reply('I was unable to ban the member'); + message.channel.send('I was unable to ban the member'); // Log the error console.error(err); }); } else { // The mentioned user isn't in this guild - message.reply("That user isn't in this guild!"); + message.channel.send("That user isn't in this guild!"); } } else { // Otherwise, if no user was mentioned - message.reply("You didn't mention the user to ban!"); + message.channel.send("You didn't mention the user to ban!"); } } }); diff --git a/docs/general/welcome.md b/docs/general/welcome.md index efd7a7e7..0dde0486 100644 --- a/docs/general/welcome.md +++ b/docs/general/welcome.md @@ -68,7 +68,7 @@ client.on('ready', () => { client.on('message', msg => { if (msg.content === 'ping') { - msg.reply('pong'); + msg.channel.send('pong'); } }); diff --git a/docs/topics/voice.md b/docs/topics/voice.md index cb20dd5c..8da93676 100644 --- a/docs/topics/voice.md +++ b/docs/topics/voice.md @@ -37,7 +37,7 @@ client.on('message', async message => { if (message.member.voice.channel) { const connection = await message.member.voice.channel.join(); } else { - message.reply('You need to join a voice channel first!'); + message.channel.send('You need to join a voice channel first!'); } } }); diff --git a/src/structures/APIMessage.js b/src/structures/APIMessage.js index 5f6aea9c..bd7cc610 100644 --- a/src/structures/APIMessage.js +++ b/src/structures/APIMessage.js @@ -79,8 +79,6 @@ class APIMessage { * @returns {?(string|string[])} */ makeContent() { - const GuildMember = require('./GuildMember'); - let content; if (this.options.content === null) { content = ''; @@ -110,25 +108,14 @@ class APIMessage { const isCode = typeof this.options.code !== 'undefined' && this.options.code !== false; const splitOptions = isSplit ? { ...this.options.split } : undefined; - let mentionPart = ''; - if (this.options.reply && !this.isUser && this.target.type !== 'dm') { - const id = this.target.client.users.resolveID(this.options.reply); - mentionPart = `<@${this.options.reply instanceof GuildMember && this.options.reply.nickname ? '!' : ''}${id}>, `; - if (isSplit) { - splitOptions.prepend = `${mentionPart}${splitOptions.prepend || ''}`; - } - } - - if (content || mentionPart) { + if (content) { if (isCode) { const codeName = typeof this.options.code === 'string' ? this.options.code : ''; - content = `${mentionPart}\`\`\`${codeName}\n${Util.cleanCodeBlockContent(content)}\n\`\`\``; + content = `\`\`\`${codeName}\n${Util.cleanCodeBlockContent(content)}\n\`\`\``; if (isSplit) { splitOptions.prepend = `${splitOptions.prepend || ''}\`\`\`${codeName}\n`; splitOptions.append = `\n\`\`\`${splitOptions.append || ''}`; } - } else if (mentionPart) { - content = `${mentionPart}${content}`; } if (isSplit) { @@ -185,19 +172,20 @@ class APIMessage { typeof this.options.allowedMentions === 'undefined' ? this.target.client.options.allowedMentions : this.options.allowedMentions; - if (this.options.reply) { - const id = this.target.client.users.resolveID(this.options.reply); - if (allowedMentions) { - // Clone the object as not to alter the ClientOptions object - allowedMentions = Util.cloneObject(allowedMentions); - const parsed = allowedMentions.parse && allowedMentions.parse.includes('users'); - // Check if the mention won't be parsed, and isn't supplied in `users` - if (!parsed && !(allowedMentions.users && allowedMentions.users.includes(id))) { - if (!allowedMentions.users) allowedMentions.users = []; - allowedMentions.users.push(id); - } - } else { - allowedMentions = { users: [id] }; + + if (allowedMentions) { + allowedMentions = Util.cloneObject(allowedMentions); + allowedMentions.replied_user = allowedMentions.repliedUser; + delete allowedMentions.repliedUser; + } + + let message_reference; + if (typeof this.options.replyTo !== 'undefined') { + const message_id = this.isMessage + ? this.target.channel.messages.resolveID(this.options.replyTo) + : this.target.messages.resolveID(this.options.replyTo); + if (message_id) { + message_reference = { message_id }; } } @@ -209,8 +197,10 @@ class APIMessage { embeds, username, avatar_url: avatarURL, - allowed_mentions: typeof content === 'undefined' ? undefined : allowedMentions, + allowed_mentions: + typeof content === 'undefined' && typeof message_reference === 'undefined' ? undefined : allowedMentions, flags, + message_reference, }; return this; } diff --git a/src/structures/Emoji.js b/src/structures/Emoji.js index 0214ea80..86119d30 100644 --- a/src/structures/Emoji.js +++ b/src/structures/Emoji.js @@ -82,7 +82,7 @@ class Emoji extends Base { * @example * // Send a custom emoji from a guild: * const emoji = guild.emojis.cache.first(); - * msg.reply(`Hello! ${emoji}`); + * msg.channel.send(`Hello! ${emoji}`); * @example * // Send the emoji used in a reaction to the channel the reaction is part of * reaction.message.channel.send(`The emoji used was: ${reaction.emoji}`); diff --git a/src/structures/Message.js b/src/structures/Message.js index afdfa943..3f420e32 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -205,11 +205,11 @@ class Message extends Base { this.flags = new MessageFlags(data.flags).freeze(); /** - * Reference data sent in a crossposted message. + * Reference data sent in a crossposted message or inline reply. * @typedef {Object} MessageReference - * @property {string} channelID ID of the channel the message was crossposted from - * @property {?string} guildID ID of the guild the message was crossposted from - * @property {?string} messageID ID of the message that was crossposted + * @property {string} channelID ID of the channel the message was referenced + * @property {?string} guildID ID of the guild the message was referenced + * @property {?string} messageID ID of the message that was referenced */ /** @@ -223,6 +223,10 @@ class Message extends Base { messageID: data.message_reference.message_id, } : null; + + if (data.referenced_message) { + this.channel.messages.add(data.referenced_message); + } } /** @@ -425,6 +429,18 @@ class Message extends Base { ); } + /** + * The Message this crosspost/reply/pin-add references, if cached + * @type {?Message} + * @readonly + */ + get referencedMessage() { + if (!this.reference) return null; + const referenceChannel = this.client.channels.resolve(this.reference.channelID); + if (!referenceChannel) return null; + return referenceChannel.messages.resolve(this.reference.messageID); + } + /** * Whether the message is crosspostable by the client user * @type {boolean} @@ -588,21 +604,19 @@ class Message extends Base { } /** - * Replies to the message. + * Send an inline reply to this message. * @param {StringResolvable|APIMessage} [content=''] The content for the message - * @param {MessageOptions|MessageAdditions} [options={}] The options to provide + * @param {MessageOptions|MessageAdditions} [options] The additional options to provide + * @param {MessageResolvable} [options.replyTo=this] The message to reply to * @returns {Promise} - * @example - * // Reply to a message - * message.reply('Hey, I\'m a reply!') - * .then(() => console.log(`Sent a reply to ${message.author.username}`)) - * .catch(console.error); */ reply(content, options) { return this.channel.send( content instanceof APIMessage ? content - : APIMessage.transformOptions(content, options, { reply: this.member || this.author }), + : APIMessage.transformOptions(content, options, { + replyTo: this, + }), ); } diff --git a/src/structures/interfaces/TextBasedChannel.js b/src/structures/interfaces/TextBasedChannel.js index f5269e97..b47409e6 100644 --- a/src/structures/interfaces/TextBasedChannel.js +++ b/src/structures/interfaces/TextBasedChannel.js @@ -65,7 +65,7 @@ class TextBasedChannel { * @property {string|boolean} [code] Language for optional codeblock formatting to apply * @property {boolean|SplitOptions} [split=false] Whether or not the message should be split into multiple messages if * it exceeds the character limit. If an object is provided, these are the options for splitting the message - * @property {UserResolvable} [reply] User to reply to (prefixes the message with a mention, except in DMs) + * @property {MessageResolvable} [replyTo] The message to reply to (must be in the same channel) */ /** @@ -74,6 +74,7 @@ class TextBasedChannel { * @property {MessageMentionTypes[]} [parse] Types of mentions to be parsed * @property {Snowflake[]} [users] Snowflakes of Users to be parsed as mentions * @property {Snowflake[]} [roles] Snowflakes of Roles to be parsed as mentions + * @property {boolean} [repliedUser] Whether the author of the Message being replied to should be pinged */ /** diff --git a/src/util/Constants.js b/src/util/Constants.js index 82699922..1d84a51e 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -403,6 +403,7 @@ exports.WSEvents = keyMirror([ * * CHANNEL_FOLLOW_ADD * * GUILD_DISCOVERY_DISQUALIFIED * * GUILD_DISCOVERY_REQUALIFIED + * * REPLY * @typedef {string} MessageType */ exports.MessageTypes = [ @@ -422,6 +423,10 @@ exports.MessageTypes = [ null, 'GUILD_DISCOVERY_DISQUALIFIED', 'GUILD_DISCOVERY_REQUALIFIED', + null, + null, + null, + 'REPLY', ]; /** diff --git a/test/disableMentions.js b/test/disableMentions.js index 97077e20..0521d34c 100644 --- a/test/disableMentions.js +++ b/test/disableMentions.js @@ -36,9 +36,9 @@ client.on('message', message => { // Clean content and log each character console.log(Util.cleanContent(args.join(' '), message).split('')); - if (command === 'test1') message.reply(tests[0]); - else if (command === 'test2') message.reply(tests[1]); - else if (command === 'test3') message.reply(tests[2]); + if (command === 'test1') message.channel.send(tests[0]); + else if (command === 'test2') message.channel.send(tests[1]); + else if (command === 'test3') message.channel.send(tests[2]); }); client.login(token).catch(console.error); diff --git a/test/random.js b/test/random.js index d1521287..38e4c575 100644 --- a/test/random.js +++ b/test/random.js @@ -135,7 +135,7 @@ client.on('message', message => { } message.channel.send('last one...').then(m => { const diff = Date.now() - start; - m.reply(`Each message took ${diff / 21}ms to send`); + m.channel.send(`Each message took ${diff / 21}ms to send`); }); } @@ -206,7 +206,7 @@ client.on('message', msg => { .join() .then(conn => { con = conn; - msg.reply('done'); + msg.channel.send('done'); const s = ytdl(song, { filter: 'audioonly' }, { passes: 3 }); s.on('error', e => console.log(`e w stream 2 ${e}`)); disp = conn.playStream(s); diff --git a/test/sendtest.js b/test/sendtest.js index 186e1c23..691c4e24 100644 --- a/test/sendtest.js +++ b/test/sendtest.js @@ -32,7 +32,6 @@ const tests = [ m => m.channel.send(fill('x'), { split: true }), m => m.channel.send(fill('1'), { code: 'js', split: true }), - m => m.channel.send(fill('x'), { reply: m.author, code: 'js', split: true }), m => m.channel.send(fill('xyz '), { split: { char: ' ' } }), m => m.channel.send('x', { embed: { description: 'a' } }), @@ -99,7 +98,6 @@ const tests = [ async m => m.channel.send({ files: [await read(fileA)] }), async m => m.channel.send(fill('x'), { - reply: m.author, code: 'js', split: true, embed: embed().setImage('attachment://zero.png'), @@ -111,7 +109,6 @@ const tests = [ m => m.channel.send({ files: [{ attachment: readStream(fileA) }] }), async m => m.channel.send(fill('xyz '), { - reply: m.author, code: 'js', split: { char: ' ', prepend: 'hello! ', append: '!!!' }, embed: embed().setImage('attachment://zero.png'), diff --git a/test/tester1000.js b/test/tester1000.js index 471860b3..077b996b 100644 --- a/test/tester1000.js +++ b/test/tester1000.js @@ -31,16 +31,13 @@ const commands = { } message.channel.send(res, { code: 'js' }); }, - ping: message => message.reply('pong'), + ping: message => message.channel.send('pong'), }; client.on('message', message => { if (!message.content.startsWith(prefix) || message.author.bot) return; - message.content = message.content - .replace(prefix, '') - .trim() - .split(' '); + message.content = message.content.replace(prefix, '').trim().split(' '); const command = message.content.shift(); message.content = message.content.join(' '); diff --git a/test/voice.js b/test/voice.js index 37d2669a..1775f280 100644 --- a/test/voice.js +++ b/test/voice.js @@ -43,20 +43,15 @@ client.on('message', m => { conn.receiver.createStream(m.author, true).on('data', b => console.log(b.toString())); conn.player.on('error', (...e) => console.log('player', ...e)); if (!connections.has(m.guild.id)) connections.set(m.guild.id, { conn, queue: [] }); - m.reply('ok!'); + m.channel.send('ok!'); conn.play(ytdl('https://www.youtube.com/watch?v=_XXOSf0s2nk', { filter: 'audioonly' }, { passes: 3 })); }); } else { - m.reply('Specify a voice channel!'); + m.channel.send('Specify a voice channel!'); } } else if (m.content.startsWith('#eval') && m.author.id === '66564597481480192') { try { - const com = eval( - m.content - .split(' ') - .slice(1) - .join(' '), - ); + const com = eval(m.content.split(' ').slice(1).join(' ')); m.channel.send(com, { code: true }); } catch (e) { console.log(e); diff --git a/test/webhooktest.js b/test/webhooktest.js index f73c693f..3691dd8d 100644 --- a/test/webhooktest.js +++ b/test/webhooktest.js @@ -32,7 +32,6 @@ const tests = [ (m, hook) => hook.send(fill('x'), { split: true }), (m, hook) => hook.send(fill('1'), { code: 'js', split: true }), - (m, hook) => hook.send(fill('x'), { reply: m.author, code: 'js', split: true }), (m, hook) => hook.send(fill('xyz '), { split: { char: ' ' } }), (m, hook) => hook.send({ embeds: [{ description: 'a' }] }), @@ -96,7 +95,6 @@ const tests = [ async (m, hook) => hook.send({ files: [await read(fileA)] }), async (m, hook) => hook.send(fill('x'), { - reply: m.author, code: 'js', split: true, embeds: [embed().setImage('attachment://zero.png')], @@ -108,7 +106,6 @@ const tests = [ (m, hook) => hook.send({ files: [{ attachment: readStream(fileA) }] }), async (m, hook) => hook.send(fill('xyz '), { - reply: m.author, code: 'js', split: { char: ' ', prepend: 'hello! ', append: '!!!' }, embeds: [embed().setImage('attachment://zero.png')], diff --git a/typings/index.d.ts b/typings/index.d.ts index 38c575fb..1f0e39f5 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -987,6 +987,7 @@ declare module 'discord.js' { public webhookID: Snowflake | null; public flags: Readonly; public reference: MessageReference | null; + public readonly referencedMessage: Message | null; public awaitReactions( filter: CollectorFilter, options?: AwaitReactionsOptions, @@ -2826,6 +2827,7 @@ declare module 'discord.js' { parse?: MessageMentionTypes[]; roles?: Snowflake[]; users?: Snowflake[]; + repliedUser?: boolean; } type MessageMentionTypes = 'roles' | 'users' | 'everyone'; @@ -2840,7 +2842,7 @@ declare module 'discord.js' { files?: (FileOptions | BufferResolvable | Stream | MessageAttachment)[]; code?: string | boolean; split?: boolean | SplitOptions; - reply?: UserResolvable; + replyTo?: MessageResolvable; } type MessageReactionResolvable = MessageReaction | Snowflake; @@ -2870,7 +2872,8 @@ declare module 'discord.js' { | 'USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_3' | 'CHANNEL_FOLLOW_ADD' | 'GUILD_DISCOVERY_DISQUALIFIED' - | 'GUILD_DISCOVERY_REQUALIFIED'; + | 'GUILD_DISCOVERY_REQUALIFIED' + | 'REPLY'; interface OverwriteData { allow?: PermissionResolvable; From 2685b960d7bbf47d7884545ad06aec8160907342 Mon Sep 17 00:00:00 2001 From: Advaith Date: Tue, 8 Dec 2020 13:07:06 -0800 Subject: [PATCH 07/17] docs(Client): #emojis is a BaseGuildEmojiManager (#5048) --- src/client/Client.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/Client.js b/src/client/Client.js index a8ec7e49..1d88cfac 100644 --- a/src/client/Client.js +++ b/src/client/Client.js @@ -166,7 +166,7 @@ class Client extends BaseClient { /** * All custom emojis that the client has access to, mapped by their IDs - * @type {GuildEmojiManager} + * @type {BaseGuildEmojiManager} * @readonly */ get emojis() { From c6f87aa32d8bee67c7d5e2ecb6bef08f69b9c57d Mon Sep 17 00:00:00 2001 From: Gus Caplan Date: Tue, 10 Nov 2020 12:15:07 -0600 Subject: [PATCH 08/17] interactions wip --- .eslintrc.json | 1 + package.json | 4 +- src/client/Client.js | 19 +++ src/client/InteractionClient.js | 131 ++++++++++++++++++ src/client/actions/ActionsManager.js | 1 + src/client/actions/InteractionCreate.js | 15 ++ .../voice/dispatcher/StreamDispatcher.js | 2 +- src/client/voice/receiver/PacketHandler.js | 4 +- .../websocket/handlers/INTERACTION_CREATE.js | 5 + src/index.js | 1 + src/structures/APIMessage.js | 12 ++ src/structures/Interaction.js | 112 +++++++++++++++ src/util/Constants.js | 30 +++- src/util/MessageFlags.js | 2 + .../util/Secretbox.js => util/Sodium.js} | 3 + 15 files changed, 335 insertions(+), 7 deletions(-) create mode 100644 src/client/InteractionClient.js create mode 100644 src/client/actions/InteractionCreate.js create mode 100644 src/client/websocket/handlers/INTERACTION_CREATE.js create mode 100644 src/structures/Interaction.js rename src/{client/voice/util/Secretbox.js => util/Sodium.js} (84%) diff --git a/.eslintrc.json b/.eslintrc.json index d9f2f558..f785763f 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,4 +1,5 @@ { + "root": true, "extends": ["eslint:recommended", "plugin:prettier/recommended"], "plugins": ["import"], "parserOptions": { diff --git a/package.json b/package.json index 9c0848ee..33a0c78c 100644 --- a/package.json +++ b/package.json @@ -110,9 +110,9 @@ "src/client/voice/receiver/PacketHandler.js": false, "src/client/voice/receiver/Receiver.js": false, "src/client/voice/util/PlayInterface.js": false, - "src/client/voice/util/Secretbox.js": false, "src/client/voice/util/Silence.js": false, - "src/client/voice/util/VolumeInterface.js": false + "src/client/voice/util/VolumeInterface.js": false, + "src/util/Sodium.js": false }, "husky": { "hooks": { diff --git a/src/client/Client.js b/src/client/Client.js index 1d88cfac..01617b88 100644 --- a/src/client/Client.js +++ b/src/client/Client.js @@ -1,6 +1,7 @@ 'use strict'; const BaseClient = require('./BaseClient'); +const InteractionClient = require('./InteractionClient'); const ActionsManager = require('./actions/ActionsManager'); const ClientVoiceManager = require('./voice/ClientVoiceManager'); const WebSocketManager = require('./websocket/WebSocketManager'); @@ -103,6 +104,24 @@ class Client extends BaseClient { ? ShardClientUtil.singleton(this, process.env.SHARDING_MANAGER_MODE) : null; + /** + * The interaction client. + * @type {InteractionClient} + */ + this.interactionClient = new InteractionClient( + options, + interaction => { + /** + * Emitted when an interaction is created. + * @event Client#interactionCreate + * @param {Interaction} interaction The interaction which was created. + */ + this.client.emit(Events.INTERACTION_CREATE, interaction); + return null; + }, + this, + ); + /** * All of the {@link User} objects that have been cached at any point, mapped by their IDs * @type {UserManager} diff --git a/src/client/InteractionClient.js b/src/client/InteractionClient.js new file mode 100644 index 00000000..d7d13065 --- /dev/null +++ b/src/client/InteractionClient.js @@ -0,0 +1,131 @@ +'use strict'; + +const BaseClient = require('./BaseClient'); +const Interaction = require('../structures/Interaction'); +const { ApplicationCommandOptionType, InteractionType, InteractionResponseType } = require('../util/Constants'); + +let sodium; + +/** + * Interaction client is used for interactions. + * + * ```js + * const client = new InteractionClient({ + * token: ABC, + * publicKey: XYZ, + * }, async (interaction) => { + * if (will take a long time) { + * doSomethingLong.then((d) => { + * interaction.reply({ + * content: 'wow that took long', + * }); + * }); + * // return null to signal that we will be replying via `interaction.reply`. + * return null; + * } + * return { content: 'hi!' }; + * }); + * ``` + */ +class InteractionClient extends BaseClient { + /** + * @param {Options} options Options for the client. + * @param {Handler} handler Handler to handle things. + * @param {undefined} client For internal use. + */ + constructor(options, handler, client) { + super(options); + this.client = client || this; + this.handler = handler; + this.publicKey = options.publicKey ? Buffer.from(options.publicKey, 'hex') : undefined; + } + + getCommands(guildID) { + let path = this.client.api.applications('@me'); + if (guildID) { + path = path.guilds(guildID); + } + return path.commands.get(); + } + + createCommand(command, guildID) { + let path = this.client.api.applications('@me'); + if (guildID) { + path = path.guilds(guildID); + } + return path.commands.post({ + data: { + name: command.name, + description: command.description, + options: command.options.map(function m(o) { + return { + type: ApplicationCommandOptionType[o.type], + name: o.name, + description: o.description, + default: o.default, + required: o.required, + choices: o.choices, + options: o.options.map(m), + }; + }), + }, + }); + } + + deleteCommand(commandID, guildID) { + let path = this.client.api.applications('@me'); + if (guildID) { + path = path.guilds(guildID); + } + return path.commands(commandID).delete(); + } + + async handle(data) { + switch (data.type) { + case InteractionType.PING: + return { + type: InteractionResponseType.PONG, + }; + case InteractionType.APPLICATION_COMMAND: { + try { + const interaction = new Interaction(this.client, data); + const result = await this.handler(interaction); + if (result === null) { + return { + type: InteractionResponseType.ACKNOWLEDGE, + }; + } + // handle result as message resolvable here, probably DRY this branch with code in + // `Interaction#reply`, except `Interaction#reply` obviously does a POST and this just + // returns the data. + throw new Error('fucc'); + } catch (e) { + this.client.emit('error', e); + return { + type: InteractionResponseType.ACKNOWLEDGE, + }; + } + } + default: + throw new RangeError('Invalid interaction data'); + } + } + + async handleFromHTTP(body, signature) { + if (sodium === undefined) { + sodium = require('../util/Sodium'); + } + if (!sodium.methods.verify(Buffer.from(signature, 'hex'), Buffer.from(body), this.publicKey)) { + throw new Error('Invalid signature'); + } + const data = JSON.parse(body); + const result = await this.handle(data); + return JSON.stringify(result); + } + + async handleFromGateway(data) { + await this.handle(data); + } +} + +module.exports = InteractionClient; diff --git a/src/client/actions/ActionsManager.js b/src/client/actions/ActionsManager.js index 4055795a..31ba210c 100644 --- a/src/client/actions/ActionsManager.js +++ b/src/client/actions/ActionsManager.js @@ -37,6 +37,7 @@ class ActionsManager { this.register(require('./GuildIntegrationsUpdate')); this.register(require('./WebhooksUpdate')); this.register(require('./TypingStart')); + this.register(require('./InteractionCreate')); } register(Action) { diff --git a/src/client/actions/InteractionCreate.js b/src/client/actions/InteractionCreate.js new file mode 100644 index 00000000..c349a0eb --- /dev/null +++ b/src/client/actions/InteractionCreate.js @@ -0,0 +1,15 @@ +'use strict'; + +const Action = require('./Action'); + +class InteractionCreateAction extends Action { + handle(data) { + this.client.interactionClient.handleFromGateway(data).catch(e => { + this.client.emit('error', e); + }); + + return {}; + } +} + +module.exports = InteractionCreateAction; diff --git a/src/client/voice/dispatcher/StreamDispatcher.js b/src/client/voice/dispatcher/StreamDispatcher.js index bb1c7bba..bd41d4f4 100644 --- a/src/client/voice/dispatcher/StreamDispatcher.js +++ b/src/client/voice/dispatcher/StreamDispatcher.js @@ -1,7 +1,7 @@ 'use strict'; const { Writable } = require('stream'); -const secretbox = require('../util/Secretbox'); +const secretbox = require('../../../util/Sodium'); const Silence = require('../util/Silence'); const VolumeInterface = require('../util/VolumeInterface'); diff --git a/src/client/voice/receiver/PacketHandler.js b/src/client/voice/receiver/PacketHandler.js index bf1a220a..807593ff 100644 --- a/src/client/voice/receiver/PacketHandler.js +++ b/src/client/voice/receiver/PacketHandler.js @@ -1,8 +1,8 @@ 'use strict'; const EventEmitter = require('events'); +const sodium = require('../../../util/Sodium'); const Speaking = require('../../../util/Speaking'); -const secretbox = require('../util/Secretbox'); const { SILENCE_FRAME } = require('../util/Silence'); // The delay between packets when a user is considered to have stopped speaking @@ -58,7 +58,7 @@ class PacketHandler extends EventEmitter { } // Open packet - let packet = secretbox.methods.open(buffer.slice(12, end), this.nonce, secret_key); + let packet = sodium.methods.open(buffer.slice(12, end), this.nonce, secret_key); if (!packet) return new Error('Failed to decrypt voice packet'); packet = Buffer.from(packet); diff --git a/src/client/websocket/handlers/INTERACTION_CREATE.js b/src/client/websocket/handlers/INTERACTION_CREATE.js new file mode 100644 index 00000000..5bf30fcc --- /dev/null +++ b/src/client/websocket/handlers/INTERACTION_CREATE.js @@ -0,0 +1,5 @@ +'use strict'; + +module.exports = (client, packet) => { + client.actions.InteractionCreate.handle(packet.d); +}; diff --git a/src/index.js b/src/index.js index d83e1bfa..e68fdbec 100644 --- a/src/index.js +++ b/src/index.js @@ -6,6 +6,7 @@ module.exports = { // "Root" classes (starting points) BaseClient: require('./client/BaseClient'), Client: require('./client/Client'), + InteractionClient: require('./client/InteractionClient'), Shard: require('./sharding/Shard'), ShardClientUtil: require('./sharding/ShardClientUtil'), ShardingManager: require('./sharding/ShardingManager'), diff --git a/src/structures/APIMessage.js b/src/structures/APIMessage.js index bd7cc610..c24c87ea 100644 --- a/src/structures/APIMessage.js +++ b/src/structures/APIMessage.js @@ -74,6 +74,16 @@ class APIMessage { return this.target instanceof Message; } + /** + * Whether or not the target is an interaction + * @type {boolean} + * @readonly + */ + get isInteraction() { + const Interaction = require('./Interaction'); + return this.target instanceof Interaction; + } + /** * Makes the content of this message. * @returns {?(string|string[])} @@ -166,6 +176,8 @@ class APIMessage { if (this.isMessage) { // eslint-disable-next-line eqeqeq flags = this.options.flags != null ? new MessageFlags(this.options.flags).bitfield : this.target.flags.bitfield; + } else if (this.isInteraction) { + flags = this.options.ephemeral ? MessageFlags.EPHEMERAL : undefined; } let allowedMentions = diff --git a/src/structures/Interaction.js b/src/structures/Interaction.js new file mode 100644 index 00000000..4c811c43 --- /dev/null +++ b/src/structures/Interaction.js @@ -0,0 +1,112 @@ +'use strict'; + +const APIMessage = require('./APIMessage'); +const Base = require('./Base'); +const { InteractionResponseType } = require('../util/Constants'); +const Snowflake = require('../util/Snowflake'); + +/** + * Represents an interaction, see {@link InteractionClient}. + * @extends {Base} + */ +class Interaction extends Base { + constructor(client, data) { + super(client); + this._patch(data); + } + + _patch(data) { + /** + * The ID of this interaction. + * @type {Snowflake} + */ + this.id = data.id; + + /** + * The token of this interaction. + * @type {string} + */ + this.token = data.token; + + /** + * The ID of the invoked command. + * @type {Snowflake} + */ + this.commandID = data.data.id; + + /** + * The name of the invoked command. + * @type {string} + */ + this.commandName = data.data.name; + + /** + * The options passed to the command. + * @type {Object} + */ + this.options = data.data.options; + + /** + * The channel this interaction was sent in. + * @type {Channel} + */ + this.channel = this.client.channels.cache.get(data.channel_id); + + /** + * The guild this interaction was sent in, if any. + * @type {?Guild} + */ + this.guild = data.guild_id ? this.client.guilds.cache.get(data.guild_id) : null; + + /** + * If this interaction was sent in a guild, the member which sent it. + * @type {?Member} + */ + this.member = data.member ? this.guild.members.add(data.member, false) : null; + } + + /** + * The timestamp the emoji was created at, or null if unicode + * @type {?number} + * @readonly + */ + get createdTimestamp() { + if (!this.id) return null; + return Snowflake.deconstruct(this.id).timestamp; + } + + /** + * The time the emoji was created at, or null if unicode + * @type {?Date} + * @readonly + */ + get createdAt() { + if (!this.id) return null; + return new Date(this.createdTimestamp); + } + + async reply(content, options) { + let apiMessage; + + if (content instanceof APIMessage) { + apiMessage = content.resolveData(); + } else { + apiMessage = APIMessage.create(this, content, options).resolveData(); + if (Array.isArray(apiMessage.data.content)) { + throw new Error(); + } + } + + const { data, files } = await apiMessage.resolveFiles(); + + return this.client.api.interactions(this.id, this.token).callback.post({ + data: { + type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, + data, + }, + files, + }); + } +} + +module.exports = Interaction; diff --git a/src/util/Constants.js b/src/util/Constants.js index 1d84a51e..0af65b3a 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -77,14 +77,14 @@ exports.DefaultOptions = { /** * HTTP options * @typedef {Object} HTTPOptions - * @property {number} [version=7] API version to use + * @property {number} [version=8] API version to use * @property {string} [api='https://discord.com/api'] Base url of the API * @property {string} [cdn='https://cdn.discordapp.com'] Base url of the CDN * @property {string} [invite='https://discord.gg'] Base url of invites * @property {string} [template='https://discord.new'] Base url of templates */ http: { - version: 7, + version: 8, api: 'https://discord.com/api', cdn: 'https://cdn.discordapp.com', invite: 'https://discord.gg', @@ -282,6 +282,7 @@ exports.Events = { SHARD_READY: 'shardReady', SHARD_RESUME: 'shardResume', INVALIDATED: 'invalidated', + INTERACTION_CREATE: 'interactionCreate', RAW: 'raw', }; @@ -345,6 +346,7 @@ exports.PartialTypes = keyMirror(['USER', 'CHANNEL', 'GUILD_MEMBER', 'MESSAGE', * * VOICE_STATE_UPDATE * * VOICE_SERVER_UPDATE * * WEBHOOKS_UPDATE + * * INTERACTION_CREATE * @typedef {string} WSEventType */ exports.WSEvents = keyMirror([ @@ -384,6 +386,7 @@ exports.WSEvents = keyMirror([ 'VOICE_STATE_UPDATE', 'VOICE_SERVER_UPDATE', 'WEBHOOKS_UPDATE', + 'INTERACTION_CREATE', ]); /** @@ -676,6 +679,29 @@ exports.WebhookTypes = [ 'Channel Follower', ]; +exports.ApplicationCommandOptionType = { + SUB_COMMAND: 1, + SUB_COMMAND_GROUP: 2, + STRING: 3, + INTEGER: 4, + BOOLEAN: 5, + USER: 6, + CHANNEL: 7, + ROLE: 8, +}; + +exports.InteractionType = { + PING: 1, + APPLICATION_COMMAND: 2, +}; + +exports.InteractionResponseType = { + PONG: 1, + ACKNOWLEDGE: 2, + CHANNEL_MESSAGE: 3, + CHANNEL_MESSAGE_WITH_SOURCE: 4, +}; + function keyMirror(arr) { let tmp = Object.create(null); for (const value of arr) tmp[value] = value; diff --git a/src/util/MessageFlags.js b/src/util/MessageFlags.js index 536cbd88..e693158e 100644 --- a/src/util/MessageFlags.js +++ b/src/util/MessageFlags.js @@ -22,6 +22,7 @@ class MessageFlags extends BitField {} * * `SUPPRESS_EMBEDS` * * `SOURCE_MESSAGE_DELETED` * * `URGENT` + * * `EPHEMERAL` * @type {Object} * @see {@link https://discord.com/developers/docs/resources/channel#message-object-message-flags} */ @@ -31,6 +32,7 @@ MessageFlags.FLAGS = { SUPPRESS_EMBEDS: 1 << 2, SOURCE_MESSAGE_DELETED: 1 << 3, URGENT: 1 << 4, + EPHEMERAL: 1 << 6, }; module.exports = MessageFlags; diff --git a/src/client/voice/util/Secretbox.js b/src/util/Sodium.js similarity index 84% rename from src/client/voice/util/Secretbox.js rename to src/util/Sodium.js index c16a4353..e19b5b9a 100644 --- a/src/client/voice/util/Secretbox.js +++ b/src/util/Sodium.js @@ -5,16 +5,19 @@ const libs = { open: sodium.api.crypto_secretbox_open_easy, close: sodium.api.crypto_secretbox_easy, random: n => sodium.randombytes_buf(n), + verify: sodium.api.crypto_sign_verify_detached, }), 'libsodium-wrappers': sodium => ({ open: sodium.crypto_secretbox_open_easy, close: sodium.crypto_secretbox_easy, random: n => sodium.randombytes_buf(n), + verify: sodium.crypto_sign_verify_detached, }), tweetnacl: tweetnacl => ({ open: tweetnacl.secretbox.open, close: tweetnacl.secretbox, random: n => tweetnacl.randomBytes(n), + verify: (s, d, p) => tweetnacl.sign.detached.verify(d, s, p), }), }; From a626dc8c41b02ec724f81db036b952bbcc683f84 Mon Sep 17 00:00:00 2001 From: Gus Caplan Date: Fri, 11 Dec 2020 13:25:04 -0600 Subject: [PATCH 09/17] more --- src/client/InteractionClient.js | 70 +++++++++++++++++++++++---------- src/structures/Interaction.js | 14 +++---- src/util/Constants.js | 1 + 3 files changed, 57 insertions(+), 28 deletions(-) diff --git a/src/client/InteractionClient.js b/src/client/InteractionClient.js index d7d13065..ffb1faec 100644 --- a/src/client/InteractionClient.js +++ b/src/client/InteractionClient.js @@ -1,6 +1,7 @@ 'use strict'; const BaseClient = require('./BaseClient'); +const APIMessage = require('../structures/APIMessage'); const Interaction = require('../structures/Interaction'); const { ApplicationCommandOptionType, InteractionType, InteractionResponseType } = require('../util/Constants'); @@ -37,6 +38,7 @@ class InteractionClient extends BaseClient { super(options); this.client = client || this; this.handler = handler; + this.token = options.token; this.publicKey = options.publicKey ? Buffer.from(options.publicKey, 'hex') : undefined; } @@ -87,44 +89,72 @@ class InteractionClient extends BaseClient { type: InteractionResponseType.PONG, }; case InteractionType.APPLICATION_COMMAND: { - try { - const interaction = new Interaction(this.client, data); - const result = await this.handler(interaction); - if (result === null) { - return { - type: InteractionResponseType.ACKNOWLEDGE, - }; + const interaction = new Interaction(this.client, data); + + let done = false; + const r0 = new Promise(resolve => { + this.client.setTimeout(() => { + done = true; + resolve({ + type: InteractionResponseType.ACKNOWLEDGE_WITH_SOURCE, + }); + }, 500); + }); + const r1 = this.handler(interaction).then(async r => { + if (done) { + interaction.reply(r).catch(e => { + this.client.emit('error', e); + }); + return undefined; } - // handle result as message resolvable here, probably DRY this branch with code in - // `Interaction#reply`, except `Interaction#reply` obviously does a POST and this just - // returns the data. - throw new Error('fucc'); - } catch (e) { - this.client.emit('error', e); + + let apiMessage; + + if (r instanceof APIMessage) { + apiMessage = r.resolveData(); + } else { + apiMessage = APIMessage.create(interaction, r).resolveData(); + if (Array.isArray(apiMessage.data.content)) { + throw new Error(); + } + } + + const resolved = await apiMessage.resolveFiles(); return { - type: InteractionResponseType.ACKNOWLEDGE, + type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, + data: resolved.data, }; - } + }); + + const result = await Promise.race([r0, r1]); + + return result; } default: throw new RangeError('Invalid interaction data'); } } - async handleFromHTTP(body, signature) { + async handleFromHTTP(body, signature, timestamp) { if (sodium === undefined) { sodium = require('../util/Sodium'); } - if (!sodium.methods.verify(Buffer.from(signature, 'hex'), Buffer.from(body), this.publicKey)) { - throw new Error('Invalid signature'); + if (!sodium.methods.verify(Buffer.from(signature, 'hex'), Buffer.from(timestamp + body), this.publicKey)) { + return { status: 400, body: '' }; } const data = JSON.parse(body); + const result = await this.handle(data); - return JSON.stringify(result); + + return { + status: 200, + body: JSON.stringify(result), + }; } async handleFromGateway(data) { - await this.handle(data); + const interaction = new Interaction(this.client, data); + await this.handler(interaction); } } diff --git a/src/structures/Interaction.js b/src/structures/Interaction.js index 4c811c43..0d6ea0a1 100644 --- a/src/structures/Interaction.js +++ b/src/structures/Interaction.js @@ -50,19 +50,19 @@ class Interaction extends Base { * The channel this interaction was sent in. * @type {Channel} */ - this.channel = this.client.channels.cache.get(data.channel_id); + this.channel = this.client.channels?.cache.get(data.channel_id); /** * The guild this interaction was sent in, if any. * @type {?Guild} */ - this.guild = data.guild_id ? this.client.guilds.cache.get(data.guild_id) : null; + this.guild = data.guild_id ? this.client.guilds?.cache.get(data.guild_id) : null; /** * If this interaction was sent in a guild, the member which sent it. * @type {?Member} */ - this.member = data.member ? this.guild.members.add(data.member, false) : null; + this.member = data.member ? this.guild?.members.add(data.member, false) : null; } /** @@ -99,11 +99,9 @@ class Interaction extends Base { const { data, files } = await apiMessage.resolveFiles(); - return this.client.api.interactions(this.id, this.token).callback.post({ - data: { - type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, - data, - }, + return this.client.api.webhooks(this.id, this.token).post({ + auth: false, + data, files, }); } diff --git a/src/util/Constants.js b/src/util/Constants.js index 0af65b3a..9eeb4147 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -700,6 +700,7 @@ exports.InteractionResponseType = { ACKNOWLEDGE: 2, CHANNEL_MESSAGE: 3, CHANNEL_MESSAGE_WITH_SOURCE: 4, + ACKNOWLEDGE_WITH_SOURCE: 5, }; function keyMirror(arr) { From 21bfe3da7c348c359f9ef551aae76fea2f298f16 Mon Sep 17 00:00:00 2001 From: Gus Caplan Date: Fri, 11 Dec 2020 13:43:51 -0600 Subject: [PATCH 10/17] middleware model --- src/client/InteractionClient.js | 41 +++++++++++++++++++++++---------- 1 file changed, 29 insertions(+), 12 deletions(-) diff --git a/src/client/InteractionClient.js b/src/client/InteractionClient.js index ffb1faec..c87790f4 100644 --- a/src/client/InteractionClient.js +++ b/src/client/InteractionClient.js @@ -135,20 +135,37 @@ class InteractionClient extends BaseClient { } } - async handleFromHTTP(body, signature, timestamp) { - if (sodium === undefined) { - sodium = require('../util/Sodium'); - } - if (!sodium.methods.verify(Buffer.from(signature, 'hex'), Buffer.from(timestamp + body), this.publicKey)) { - return { status: 400, body: '' }; - } - const data = JSON.parse(body); + middleware() { + return async (req, res, next) => { + const timestamp = req.get('x-signature-timestamp'); + const signature = req.get('x-signature-ed25519'); - const result = await this.handle(data); + const chunks = []; + for await (const chunk of req) { + chunks.push(chunk); + } + const body = Buffer.concat(chunks); - return { - status: 200, - body: JSON.stringify(result), + if (sodium === undefined) { + sodium = require('../util/Sodium'); + } + if ( + !sodium.methods.verify( + Buffer.from(signature, 'hex'), + Buffer.concat([Buffer.from(timestamp), body]), + this.publicKey, + ) + ) { + res.status(403).end(); + return; + } + + const data = JSON.parse(body.toString()); + + const result = await this.handle(data); + res.status(200).end(JSON.stringify(result)); + + next(); }; } From db6d1b3ba952efbcc63f6bafdb09860f57654163 Mon Sep 17 00:00:00 2001 From: Gus Caplan Date: Fri, 11 Dec 2020 13:48:31 -0600 Subject: [PATCH 11/17] fix webhook --- src/client/InteractionClient.js | 7 ++++++- src/structures/Interaction.js | 5 ++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/client/InteractionClient.js b/src/client/InteractionClient.js index c87790f4..39af1b2c 100644 --- a/src/client/InteractionClient.js +++ b/src/client/InteractionClient.js @@ -36,10 +36,15 @@ class InteractionClient extends BaseClient { */ constructor(options, handler, client) { super(options); - this.client = client || this; + this.handler = handler; this.token = options.token; this.publicKey = options.publicKey ? Buffer.from(options.publicKey, 'hex') : undefined; + this.clientId = options.clientId; + + // Compat for direct usage + this.client = client || this; + this.interactionClient = this; } getCommands(guildID) { diff --git a/src/structures/Interaction.js b/src/structures/Interaction.js index 0d6ea0a1..44f1fedf 100644 --- a/src/structures/Interaction.js +++ b/src/structures/Interaction.js @@ -99,7 +99,10 @@ class Interaction extends Base { const { data, files } = await apiMessage.resolveFiles(); - return this.client.api.webhooks(this.id, this.token).post({ + const clientId = this.client.interactionClient.clientId + || (await this.client.api.oauth2.applications('@me').get()).id; + + return this.client.api.webhooks(clientId, this.token).post({ auth: false, data, files, From 5be32161b9782445ff268675cb9dc32f75f57776 Mon Sep 17 00:00:00 2001 From: Gus Caplan Date: Fri, 11 Dec 2020 14:18:26 -0600 Subject: [PATCH 12/17] basic working --- src/client/Client.js | 3 +- src/client/InteractionClient.js | 64 +++++++++++++++------------------ src/structures/Interaction.js | 21 ++++++----- 3 files changed, 41 insertions(+), 47 deletions(-) diff --git a/src/client/Client.js b/src/client/Client.js index 01617b88..28177b67 100644 --- a/src/client/Client.js +++ b/src/client/Client.js @@ -116,8 +116,7 @@ class Client extends BaseClient { * @event Client#interactionCreate * @param {Interaction} interaction The interaction which was created. */ - this.client.emit(Events.INTERACTION_CREATE, interaction); - return null; + this.emit(Events.INTERACTION_CREATE, interaction); }, this, ); diff --git a/src/client/InteractionClient.js b/src/client/InteractionClient.js index 39af1b2c..cb76ee39 100644 --- a/src/client/InteractionClient.js +++ b/src/client/InteractionClient.js @@ -1,7 +1,6 @@ 'use strict'; const BaseClient = require('./BaseClient'); -const APIMessage = require('../structures/APIMessage'); const Interaction = require('../structures/Interaction'); const { ApplicationCommandOptionType, InteractionType, InteractionResponseType } = require('../util/Constants'); @@ -15,16 +14,16 @@ let sodium; * token: ABC, * publicKey: XYZ, * }, async (interaction) => { + * // automatically handles long responses * if (will take a long time) { - * doSomethingLong.then((d) => { + * await doSomethingLong.then((d) => { * interaction.reply({ * content: 'wow that took long', * }); * }); - * // return null to signal that we will be replying via `interaction.reply`. - * return null; + * } else { + * await interaction.reply('hi!'); * } - * return { content: 'hi!' }; * }); * ``` */ @@ -40,7 +39,7 @@ class InteractionClient extends BaseClient { this.handler = handler; this.token = options.token; this.publicKey = options.publicKey ? Buffer.from(options.publicKey, 'hex') : undefined; - this.clientId = options.clientId; + this.clientID = options.clientID; // Compat for direct usage this.client = client || this; @@ -94,44 +93,34 @@ class InteractionClient extends BaseClient { type: InteractionResponseType.PONG, }; case InteractionType.APPLICATION_COMMAND: { - const interaction = new Interaction(this.client, data); - - let done = false; - const r0 = new Promise(resolve => { + let timedOut = false; + let resolve; + const p0 = new Promise(r => { + resolve = r; this.client.setTimeout(() => { - done = true; - resolve({ + timedOut = true; + r({ type: InteractionResponseType.ACKNOWLEDGE_WITH_SOURCE, }); }, 500); }); - const r1 = this.handler(interaction).then(async r => { - if (done) { - interaction.reply(r).catch(e => { - this.client.emit('error', e); - }); - return undefined; + + const interaction = new Interaction(this.client, data, resolved => { + if (timedOut) { + return false; } - - let apiMessage; - - if (r instanceof APIMessage) { - apiMessage = r.resolveData(); - } else { - apiMessage = APIMessage.create(interaction, r).resolveData(); - if (Array.isArray(apiMessage.data.content)) { - throw new Error(); - } - } - - const resolved = await apiMessage.resolveFiles(); - return { + resolve({ type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, data: resolved.data, - }; + }); + return true; }); - const result = await Promise.race([r0, r1]); + Promise.resolve(this.handler(interaction)).catch(e => { + this.client.emit('error', e); + }); + + const result = await p0; return result; } @@ -175,8 +164,11 @@ class InteractionClient extends BaseClient { } async handleFromGateway(data) { - const interaction = new Interaction(this.client, data); - await this.handler(interaction); + const result = await this.handle(data); + + await this.client.api.interactions(data.id, data.token).callback.post({ + data: result, + }); } } diff --git a/src/structures/Interaction.js b/src/structures/Interaction.js index 44f1fedf..7b2c7adf 100644 --- a/src/structures/Interaction.js +++ b/src/structures/Interaction.js @@ -10,8 +10,9 @@ const Snowflake = require('../util/Snowflake'); * @extends {Base} */ class Interaction extends Base { - constructor(client, data) { + constructor(client, data, handler) { super(client); + this.handler = handler; this._patch(data); } @@ -97,16 +98,18 @@ class Interaction extends Base { } } - const { data, files } = await apiMessage.resolveFiles(); + const resolved = await apiMessage.resolveFiles(); - const clientId = this.client.interactionClient.clientId - || (await this.client.api.oauth2.applications('@me').get()).id; + if (!this.handler(resolved)) { + const clientID = this.client.interactionClient.clientID + || (await this.client.api.oauth2.applications('@me').get()).id; - return this.client.api.webhooks(clientId, this.token).post({ - auth: false, - data, - files, - }); + await this.client.api.webhooks(clientID, this.token).post({ + auth: false, + data: resolved.data, + files: resolved.files, + }); + } } } From 790b6b3b5c65a01546478265f696c56adb892a53 Mon Sep 17 00:00:00 2001 From: Gus Caplan Date: Fri, 11 Dec 2020 14:44:58 -0600 Subject: [PATCH 13/17] better command api --- src/client/InteractionClient.js | 35 ++++++++----- src/index.js | 2 + src/structures/Guild.js | 17 ++++++ src/structures/Interaction.js | 10 ++-- src/structures/InteractionCommand.js | 77 ++++++++++++++++++++++++++++ src/util/Constants.js | 3 ++ 6 files changed, 126 insertions(+), 18 deletions(-) create mode 100644 src/structures/InteractionCommand.js diff --git a/src/client/InteractionClient.js b/src/client/InteractionClient.js index cb76ee39..08d27d6a 100644 --- a/src/client/InteractionClient.js +++ b/src/client/InteractionClient.js @@ -2,6 +2,7 @@ const BaseClient = require('./BaseClient'); const Interaction = require('../structures/Interaction'); +const ApplicationCommand = require('../structures/InteractionCommand'); const { ApplicationCommandOptionType, InteractionType, InteractionResponseType } = require('../util/Constants'); let sodium; @@ -46,20 +47,32 @@ class InteractionClient extends BaseClient { this.interactionClient = this; } - getCommands(guildID) { + /** + * Get registered slash commands. + * @param {Snowflake?} guildID Optional guild ID. + * @returns {[Command]} + */ + async getCommands(guildID) { let path = this.client.api.applications('@me'); if (guildID) { path = path.guilds(guildID); } - return path.commands.get(); + const commands = await path.commands.get(); + return commands.map(c => new ApplicationCommand(this, c, guildID)); } + /** + * Create a command. + * @param {Object} command The command description. + * @param {Snowflake?} guildID Optional guild ID. + * @returns {ApplicationCommand} The created command. + */ createCommand(command, guildID) { let path = this.client.api.applications('@me'); if (guildID) { path = path.guilds(guildID); } - return path.commands.post({ + const c = path.commands.post({ data: { name: command.name, description: command.description, @@ -71,19 +84,12 @@ class InteractionClient extends BaseClient { default: o.default, required: o.required, choices: o.choices, - options: o.options.map(m), + options: o.options ? o.options.map(m) : undefined, }; }), }, }); - } - - deleteCommand(commandID, guildID) { - let path = this.client.api.applications('@me'); - if (guildID) { - path = path.guilds(guildID); - } - return path.commands(commandID).delete(); + return new ApplicationCommand(this, c, guildID); } async handle(data) { @@ -129,6 +135,11 @@ class InteractionClient extends BaseClient { } } + /** + * An express-like middleware factory which can be used + * with webhook interactions. + * @returns {Function} The middleware function. + */ middleware() { return async (req, res, next) => { const timestamp = req.get('x-signature-timestamp'); diff --git a/src/index.js b/src/index.js index e68fdbec..fb1a09f6 100644 --- a/src/index.js +++ b/src/index.js @@ -59,6 +59,7 @@ module.exports = { // Structures Application: require('./structures/interfaces/Application'), + ApplicationCommand: require('./structures/ApplicationCommand'), Base: require('./structures/Base'), Activity: require('./structures/Presence').Activity, APIMessage: require('./structures/APIMessage'), @@ -81,6 +82,7 @@ module.exports = { GuildPreview: require('./structures/GuildPreview'), GuildTemplate: require('./structures/GuildTemplate'), Integration: require('./structures/Integration'), + Interaction: require('./structures/Interaction'), Invite: require('./structures/Invite'), Message: require('./structures/Message'), MessageAttachment: require('./structures/MessageAttachment'), diff --git a/src/structures/Guild.js b/src/structures/Guild.js index c9c4fe9f..524ea64c 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -1432,6 +1432,23 @@ class Guild extends Base { .then(() => this); } + /** + * Get the commands associated with this guild. + * @returns {ApplicationCommand[]} A list of commands. + */ + getCommands() { + return this.client.interactionClient.getCommands(this.id); + } + + /** + * Create a command. See {@link InteractionClient}. + * @param {Object} command The command description. + * @returns {ApplicationCommand} The created command. + */ + createCommand(command) { + return this.client.interactionClient.createCommand(command, this.id); + } + /** * Leaves the guild. * @returns {Promise} diff --git a/src/structures/Interaction.js b/src/structures/Interaction.js index 7b2c7adf..c3b931e2 100644 --- a/src/structures/Interaction.js +++ b/src/structures/Interaction.js @@ -67,22 +67,20 @@ class Interaction extends Base { } /** - * The timestamp the emoji was created at, or null if unicode - * @type {?number} + * The timestamp the interaction was created at. + * @type {number} * @readonly */ get createdTimestamp() { - if (!this.id) return null; return Snowflake.deconstruct(this.id).timestamp; } /** - * The time the emoji was created at, or null if unicode - * @type {?Date} + * The time the interaction was created at. + * @type {Date} * @readonly */ get createdAt() { - if (!this.id) return null; return new Date(this.createdTimestamp); } diff --git a/src/structures/InteractionCommand.js b/src/structures/InteractionCommand.js new file mode 100644 index 00000000..b4d23c7e --- /dev/null +++ b/src/structures/InteractionCommand.js @@ -0,0 +1,77 @@ +'use strict'; + +const Base = require('./Base'); +const { ApplicationCommandOptionType } = require('../util/Constants'); +const Snowflake = require('../util/Snowflake'); + +/** + * Represents an application command, see {@link InteractionClient}. + * @extends {Base} + */ +class ApplicationCommand extends Base { + constructor(client, data, guildID) { + super(client); + + /** + * The ID of the guild this command is part of, if any. + * @type {Snowflake?} + * @readonly + */ + this.guildID = guildID; + + this._patch(data); + } + + _patch(data) { + this.id = data.id; + + this.appplicationID = data.application_id; + + this.name = data.name; + + this.description = data.description; + + this.options = data.options.map(function m(o) { + return { + type: ApplicationCommandOptionType[o.type], + name: o.name, + description: o.description, + default: o.default, + required: o.required, + choices: o.choices, + options: o.options ? o.options.map(m) : undefined, + }; + }); + } + + /** + * The timestamp the command was created at. + * @type {number} + * @readonly + */ + get createdTimestamp() { + return Snowflake.deconstruct(this.id).timestamp; + } + + /** + * The time the command was created at. + * @type {Date} + * @readonly + */ + get createdAt() { + return new Date(this.createdTimestamp); + } + + /** + * Delete this command. + */ + async delete() { + let path = this.client.api.applications('@me'); + if (this.guildID) { + path = path.guilds(this.guildID); + } + await path.commands(this.id).delete(); + } +} + +module.exports = ApplicationCommand; diff --git a/src/util/Constants.js b/src/util/Constants.js index 9eeb4147..0429fa12 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -689,6 +689,9 @@ exports.ApplicationCommandOptionType = { CHANNEL: 7, ROLE: 8, }; +Object.entries(exports.ApplicationCommandOptionType).forEach(([k, v]) => { + exports.ApplicationCommandOptionType[v] = k; +}); exports.InteractionType = { PING: 1, From 4599ef954f9b179513272f07ad40e55d5297230a Mon Sep 17 00:00:00 2001 From: Gus Caplan Date: Fri, 11 Dec 2020 14:46:28 -0600 Subject: [PATCH 14/17] lint --- src/structures/Interaction.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/structures/Interaction.js b/src/structures/Interaction.js index c3b931e2..6141ff90 100644 --- a/src/structures/Interaction.js +++ b/src/structures/Interaction.js @@ -2,7 +2,6 @@ const APIMessage = require('./APIMessage'); const Base = require('./Base'); -const { InteractionResponseType } = require('../util/Constants'); const Snowflake = require('../util/Snowflake'); /** @@ -99,8 +98,8 @@ class Interaction extends Base { const resolved = await apiMessage.resolveFiles(); if (!this.handler(resolved)) { - const clientID = this.client.interactionClient.clientID - || (await this.client.api.oauth2.applications('@me').get()).id; + const clientID = + this.client.interactionClient.clientID || (await this.client.api.oauth2.applications('@me').get()).id; await this.client.api.webhooks(clientID, this.token).post({ auth: false, From 8c65961a071aba07df574e342c066326a0ef36c8 Mon Sep 17 00:00:00 2001 From: Gus Caplan Date: Fri, 11 Dec 2020 15:05:25 -0600 Subject: [PATCH 15/17] add ack api --- src/client/InteractionClient.js | 37 ++++++++++++++++++++------------- src/structures/Interaction.js | 20 ++++++++++++++---- 2 files changed, 39 insertions(+), 18 deletions(-) diff --git a/src/client/InteractionClient.js b/src/client/InteractionClient.js index 08d27d6a..3a709d08 100644 --- a/src/client/InteractionClient.js +++ b/src/client/InteractionClient.js @@ -108,19 +108,30 @@ class InteractionClient extends BaseClient { r({ type: InteractionResponseType.ACKNOWLEDGE_WITH_SOURCE, }); - }, 500); + }, 250); }); - const interaction = new Interaction(this.client, data, resolved => { - if (timedOut) { - return false; - } - resolve({ - type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, - data: resolved.data, - }); - return true; - }); + const syncHandle = { + acknowledge() { + if (!timedOut) { + resolve({ + type: InteractionResponseType.ACKNOWLEDGE_WITH_SOURCE, + }); + } + }, + reply(resolved) { + if (timedOut) { + return false; + } + resolve({ + type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, + data: resolved.data, + }); + return true; + }, + }; + + const interaction = new Interaction(this.client, data, syncHandle); Promise.resolve(this.handler(interaction)).catch(e => { this.client.emit('error', e); @@ -141,7 +152,7 @@ class InteractionClient extends BaseClient { * @returns {Function} The middleware function. */ middleware() { - return async (req, res, next) => { + return async (req, res) => { const timestamp = req.get('x-signature-timestamp'); const signature = req.get('x-signature-ed25519'); @@ -169,8 +180,6 @@ class InteractionClient extends BaseClient { const result = await this.handle(data); res.status(200).end(JSON.stringify(result)); - - next(); }; } diff --git a/src/structures/Interaction.js b/src/structures/Interaction.js index 6141ff90..b97bb07a 100644 --- a/src/structures/Interaction.js +++ b/src/structures/Interaction.js @@ -9,9 +9,9 @@ const Snowflake = require('../util/Snowflake'); * @extends {Base} */ class Interaction extends Base { - constructor(client, data, handler) { + constructor(client, data, syncHandle) { super(client); - this.handler = handler; + this.syncHandle = syncHandle; this._patch(data); } @@ -83,6 +83,18 @@ class Interaction extends Base { return new Date(this.createdTimestamp); } + /** + * Acknowledge this interaction without content. + */ + async acknowledge() { + await this.syncHandle.acknowledge(); + } + + /** + * Reply to this interaction. + * @param {(StringResolvable | APIMessage)?} content The content for the message. + * @param {(MessageOptions | MessageAdditions)?} options The options to provide. + */ async reply(content, options) { let apiMessage; @@ -91,13 +103,13 @@ class Interaction extends Base { } else { apiMessage = APIMessage.create(this, content, options).resolveData(); if (Array.isArray(apiMessage.data.content)) { - throw new Error(); + throw new Error('Message is too long'); } } const resolved = await apiMessage.resolveFiles(); - if (!this.handler(resolved)) { + if (!this.syncHandle.reply(resolved)) { const clientID = this.client.interactionClient.clientID || (await this.client.api.oauth2.applications('@me').get()).id; From 2b8a6294afb6bab758ba200af1d8c4381805ddcf Mon Sep 17 00:00:00 2001 From: Gus Caplan Date: Fri, 11 Dec 2020 15:10:12 -0600 Subject: [PATCH 16/17] fixes --- src/client/InteractionClient.js | 4 +-- ...actionCommand.js => ApplicationCommand.js} | 25 +++++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) rename src/structures/{InteractionCommand.js => ApplicationCommand.js} (77%) diff --git a/src/client/InteractionClient.js b/src/client/InteractionClient.js index 3a709d08..c7d05302 100644 --- a/src/client/InteractionClient.js +++ b/src/client/InteractionClient.js @@ -1,8 +1,8 @@ 'use strict'; const BaseClient = require('./BaseClient'); +const ApplicationCommand = require('../structures/ApplicationCommand'); const Interaction = require('../structures/Interaction'); -const ApplicationCommand = require('../structures/InteractionCommand'); const { ApplicationCommandOptionType, InteractionType, InteractionResponseType } = require('../util/Constants'); let sodium; @@ -50,7 +50,7 @@ class InteractionClient extends BaseClient { /** * Get registered slash commands. * @param {Snowflake?} guildID Optional guild ID. - * @returns {[Command]} + * @returns {Command[]} */ async getCommands(guildID) { let path = this.client.api.applications('@me'); diff --git a/src/structures/InteractionCommand.js b/src/structures/ApplicationCommand.js similarity index 77% rename from src/structures/InteractionCommand.js rename to src/structures/ApplicationCommand.js index b4d23c7e..c646ff7e 100644 --- a/src/structures/InteractionCommand.js +++ b/src/structures/ApplicationCommand.js @@ -23,14 +23,39 @@ class ApplicationCommand extends Base { } _patch(data) { + /** + * The ID of this command. + * @type {Snowflake} + * @readonly + */ this.id = data.id; + /** + * The ID of the application which owns this command. + * @type {Snowflake} + * @readonly + */ this.appplicationID = data.application_id; + /** + * The name of this command. + * @type {string} + * @readonly + */ this.name = data.name; + /** + * The description of this command. + * @type {string} + * @readonly + */ this.description = data.description; + /** + * The options of this command. + * @type {Object[]} + * @readonly + */ this.options = data.options.map(function m(o) { return { type: ApplicationCommandOptionType[o.type], From 53412f6b9f285e1e67f4872d382617d46c28cee4 Mon Sep 17 00:00:00 2001 From: Gus Caplan Date: Fri, 11 Dec 2020 17:14:00 -0600 Subject: [PATCH 17/17] various stuff --- src/client/Client.js | 13 +------ src/client/InteractionClient.js | 52 +++++++++++++++++----------- src/structures/ApplicationCommand.js | 2 +- src/structures/Interaction.js | 12 +++++-- 4 files changed, 44 insertions(+), 35 deletions(-) diff --git a/src/client/Client.js b/src/client/Client.js index 28177b67..8c20286b 100644 --- a/src/client/Client.js +++ b/src/client/Client.js @@ -108,18 +108,7 @@ class Client extends BaseClient { * The interaction client. * @type {InteractionClient} */ - this.interactionClient = new InteractionClient( - options, - interaction => { - /** - * Emitted when an interaction is created. - * @event Client#interactionCreate - * @param {Interaction} interaction The interaction which was created. - */ - this.emit(Events.INTERACTION_CREATE, interaction); - }, - this, - ); + this.interactionClient = new InteractionClient(options, this); /** * All of the {@link User} objects that have been cached at any point, mapped by their IDs diff --git a/src/client/InteractionClient.js b/src/client/InteractionClient.js index c7d05302..631c5895 100644 --- a/src/client/InteractionClient.js +++ b/src/client/InteractionClient.js @@ -3,27 +3,29 @@ const BaseClient = require('./BaseClient'); const ApplicationCommand = require('../structures/ApplicationCommand'); const Interaction = require('../structures/Interaction'); -const { ApplicationCommandOptionType, InteractionType, InteractionResponseType } = require('../util/Constants'); +const { Events, ApplicationCommandOptionType, InteractionType, InteractionResponseType } = require('../util/Constants'); let sodium; /** * Interaction client is used for interactions. * - * ```js + * @example * const client = new InteractionClient({ * token: ABC, * publicKey: XYZ, - * }, async (interaction) => { + * }); + * + * client.on('interactionCreate', () => { * // automatically handles long responses * if (will take a long time) { - * await doSomethingLong.then((d) => { + * doSomethingLong.then((d) => { * interaction.reply({ * content: 'wow that took long', * }); * }); * } else { - * await interaction.reply('hi!'); + * interaction.reply('hi!'); * } * }); * ``` @@ -31,16 +33,25 @@ let sodium; class InteractionClient extends BaseClient { /** * @param {Options} options Options for the client. - * @param {Handler} handler Handler to handle things. * @param {undefined} client For internal use. */ - constructor(options, handler, client) { + constructor(options, client) { super(options); - this.handler = handler; - this.token = options.token; - this.publicKey = options.publicKey ? Buffer.from(options.publicKey, 'hex') : undefined; - this.clientID = options.clientID; + Object.defineProperty(this, 'token', { + value: options.token, + writable: true, + }); + + Object.defineProperty(this, 'clientID', { + value: options.clientID, + writable: true, + }); + + Object.defineProperty(this, 'publicKey', { + value: options.publicKey ? Buffer.from(options.publicKey, 'hex') : undefined, + writable: true, + }); // Compat for direct usage this.client = client || this; @@ -49,7 +60,7 @@ class InteractionClient extends BaseClient { /** * Get registered slash commands. - * @param {Snowflake?} guildID Optional guild ID. + * @param {Snowflake} [guildID] Optional guild ID. * @returns {Command[]} */ async getCommands(guildID) { @@ -92,7 +103,7 @@ class InteractionClient extends BaseClient { return new ApplicationCommand(this, c, guildID); } - async handle(data) { + handle(data) { switch (data.type) { case InteractionType.PING: return { @@ -101,7 +112,7 @@ class InteractionClient extends BaseClient { case InteractionType.APPLICATION_COMMAND: { let timedOut = false; let resolve; - const p0 = new Promise(r => { + const directPromise = new Promise(r => { resolve = r; this.client.setTimeout(() => { timedOut = true; @@ -133,13 +144,14 @@ class InteractionClient extends BaseClient { const interaction = new Interaction(this.client, data, syncHandle); - Promise.resolve(this.handler(interaction)).catch(e => { - this.client.emit('error', e); - }); + /** + * Emitted when an interaction is created. + * @event Client#interactionCreate + * @param {Interaction} interaction The interaction which was created. + */ + this.client.emit(Events.INTERACTION_CREATE, interaction); - const result = await p0; - - return result; + return directPromise; } default: throw new RangeError('Invalid interaction data'); diff --git a/src/structures/ApplicationCommand.js b/src/structures/ApplicationCommand.js index c646ff7e..560b95a8 100644 --- a/src/structures/ApplicationCommand.js +++ b/src/structures/ApplicationCommand.js @@ -17,7 +17,7 @@ class ApplicationCommand extends Base { * @type {Snowflake?} * @readonly */ - this.guildID = guildID; + this.guildID = guildID || null; this._patch(data); } diff --git a/src/structures/Interaction.js b/src/structures/Interaction.js index b97bb07a..c7e47f06 100644 --- a/src/structures/Interaction.js +++ b/src/structures/Interaction.js @@ -19,48 +19,56 @@ class Interaction extends Base { /** * The ID of this interaction. * @type {Snowflake} + * @readonly */ this.id = data.id; /** * The token of this interaction. * @type {string} + * @readonly */ this.token = data.token; /** * The ID of the invoked command. * @type {Snowflake} + * @readonly */ this.commandID = data.data.id; /** * The name of the invoked command. * @type {string} + * @readonly */ this.commandName = data.data.name; /** * The options passed to the command. * @type {Object} + * @readonly */ this.options = data.data.options; /** * The channel this interaction was sent in. - * @type {Channel} + * @type {?Channel} + * @readonly */ - this.channel = this.client.channels?.cache.get(data.channel_id); + this.channel = this.client.channels?.cache.get(data.channel_id) || null; /** * The guild this interaction was sent in, if any. * @type {?Guild} + * @readonly */ this.guild = data.guild_id ? this.client.guilds?.cache.get(data.guild_id) : null; /** * If this interaction was sent in a guild, the member which sent it. * @type {?Member} + * @readonly */ this.member = data.member ? this.guild?.members.add(data.member, false) : null; }