Compare commits

...

56 commits

Author SHA1 Message Date
Daniel Bulant
2eaafaf1d3
Fix options 2020-12-16 18:00:47 +01:00
Daniel Bulant
c513df444e
Attempt to fix the create command function 2020-12-16 17:54:46 +01:00
Daniel Bulant
4f5cdf2814
Update README.md 2020-12-16 16:51:40 +01:00
Daniel Bulant
0ca50889ae
Merge pull request #2 from devsnek/interactions
Interactions
2020-12-16 15:15:31 +01:00
Gus Caplan
53412f6b9f
various stuff 2020-12-11 17:14:32 -06:00
Gus Caplan
2b8a6294af
fixes 2020-12-11 15:10:12 -06:00
Gus Caplan
8c65961a07
add ack api 2020-12-11 15:05:25 -06:00
Gus Caplan
4599ef954f
lint 2020-12-11 14:46:28 -06:00
Gus Caplan
790b6b3b5c
better command api 2020-12-11 14:44:58 -06:00
Gus Caplan
5be32161b9
basic working 2020-12-11 14:22:49 -06:00
Gus Caplan
db6d1b3ba9
fix webhook 2020-12-11 14:22:48 -06:00
Gus Caplan
21bfe3da7c
middleware model 2020-12-11 14:22:48 -06:00
Gus Caplan
a626dc8c41
more 2020-12-11 14:22:48 -06:00
Gus Caplan
c6f87aa32d
interactions wip 2020-12-11 14:22:46 -06:00
Advaith
2685b960d7
docs(Client): #emojis is a BaseGuildEmojiManager (#5048) 2020-12-08 22:07:06 +01:00
monbrey
60e5a0e46f
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 <timoqueezle@gmail.com>
Co-authored-by: Tristan Guichaoua <33934311+tguichaoua@users.noreply.github.com>
2020-12-08 21:08:26 +01:00
SpaceEEC
7365f40300
fix(Collector): throw an error if a non-function was provided as filter (#5034) 2020-12-08 20:11:44 +01:00
Carter
09d07553ab
docs(User): fix typos in jsdoc (#5060) 2020-12-06 18:03:39 +01:00
BannerBomb
e272fd6909
fix(BaseGuildEmoji): typo in requiresColons (#5076) 2020-12-06 17:59:12 +01:00
Antonio Román
90d458820b
chore(Engine): bump Node.js to v14.0.0 (#5067)
Co-authored-by: SpaceEEC <spaceeec@yahoo.com>
2020-12-06 17:58:08 +01:00
Advaith
9f3c3e0918
docs(WebSocketManager): fix type of status (#5059) 2020-11-30 01:08:54 +01:00
Daniel Bulant
ec560b8107
Merge pull request #1 from monbrey/inline-replies
Inline replies
2020-11-29 19:32:58 +01:00
monbrey
fbb1c93454 fix(ApiMessage): prevent mutation of client options 2020-11-27 12:43:26 +11:00
monbrey
6b322f47a0
fix(MessageReaction): set MessageReaction#me in patch method (#5047) 2020-11-25 23:55:29 +01:00
Amish Shah
4fcb9ebf30
fix(Voice*): filter out silent audio from video users (#5035) 2020-11-25 23:51:16 +01:00
izexi
53529bd05d
fix(GuildTemplate): 'guild' getter (#5040) 2020-11-25 23:50:28 +01:00
monbrey
88625a5b7d fix(ApiMessage): pass allowed_mentions when replying without content 2020-11-24 09:01:24 +11:00
monbrey
e43ad1eea9 chore: get this back to the right state 2020-11-24 08:29:40 +11:00
Jan
8d650a7250
feat: BaseGuildEmojiManager (#4934)
Co-authored-by: Vlad Frangu <kingdgrizzle@gmail.com>
2020-11-22 19:48:30 +01:00
Jan
12a096b5f1
fix(RoleManager): fix ID return value, change return type to collection (#4935)
Co-authored-by: Ishmaam Khan <ishmaamk@gmail.com>
2020-11-22 19:39:19 +01:00
Advaith
6f3076325e
remove User#locale (#4932) 2020-11-22 19:39:06 +01:00
anandre
8c8883ef26
Remove Guild#member (#4890) 2020-11-22 19:21:01 +01:00
Junseo Park
4b555fdf4c
feat(Message): added string type for message nonce (#4782)
Co-authored-by: Vlad Frangu <kingdgrizzle@gmail.com>
Co-authored-by: Sugden <28943913+NotSugden@users.noreply.github.com>
Co-authored-by: SpaceEEC <spaceeec@yahoo.com>
2020-11-22 19:13:38 +01:00
monbrey
863734aba4
feat(GuildMemberManager): throw TypeError on incorrect GuildMemberManager#ban params (#4816)
Co-authored-by: Sugden <28943913+NotSugden@users.noreply.github.com>
2020-11-22 19:13:07 +01:00
monbrey
eadeeed72e fix(MessageMentionOptions): repliedUser should be optional 2020-11-19 18:56:09 +11:00
monbrey
939c495ebb fix(Message): referenced_message is null, not undefined 2020-11-18 08:16:20 +11:00
monbrey
065e89337e fix: naming conventions 2020-11-17 13:33:59 +11:00
monbrey
1028183c23 Merge remote-tracking branch 'upstream/master' into inline-replies 2020-11-17 10:40:31 +11:00
monbrey
b61a367392 feat(AllowedMentions): add typings for replied_user 2020-11-17 10:39:37 +11:00
monbrey
ff2dbfa52d chore: more jsdoc updates 2020-11-17 08:30:10 +11:00
monbrey
3463acafca chore: update jsdocs 2020-11-17 08:29:50 +11:00
monbrey
274ae9935e fix: dont pass channel_id to API when replying 2020-11-17 08:29:18 +11:00
monbrey
2eafeeca55 fix: restore reply typings 2020-11-17 08:24:16 +11:00
monbrey
08286459cb
fix: jsdocs for Message#referencedMessage
Co-authored-by: Tristan Guichaoua <33934311+tguichaoua@users.noreply.github.com>
2020-10-08 08:15:08 +11:00
monbrey
2e96b9a606 fix(ApiMessage): finish renaming replyTo 2020-10-07 21:39:01 +11:00
monbrey
ef1856bb1f Merge remote-tracking branch 'souji/maj/remove-msg-reply' into inline-replies 2020-10-07 21:09:05 +11:00
monbrey
4f7c207c99 feat(Message): inline reply method 2020-10-07 20:58:50 +11:00
monbrey
975b6cbd94 docs: update jsdoc for inline replies 2020-10-07 14:42:20 +11:00
monbrey
67c2e56647 refactor(InlineReplies): rename property, rework Message resolution 2020-10-07 14:18:17 +11:00
monbrey
ab0d6fc5c9 fix: check that Message#reference is defined in referencedMessage 2020-10-05 18:19:39 +11:00
monbrey
b8f50a09d2 feat(Message): add referencedMessage getter 2020-10-05 17:40:05 +11:00
monbrey
9f3108052c feat(InlineReplies): provide support for inline-replying to messages 2020-10-02 16:10:36 +10:00
monbrey
a5ce6cfa9a feat(InlineReplies): add typings for sending inline replies 2020-10-02 16:10:36 +10:00
monbrey
ab5ee838a3 feat(InlineReplies): add Message#replyReference property 2020-10-02 16:10:27 +10:00
monbrey
0ed281888d feat(InlineReplies): add INLINE_REPLY constant/typing 2020-10-02 14:57:40 +10:00
almostSouji
bc4dc22c1f
feat(Message): remove reply functionality 2020-10-01 14:10:36 +02:00
53 changed files with 843 additions and 259 deletions

View file

@ -1,4 +1,5 @@
{ {
"root": true,
"extends": ["eslint:recommended", "plugin:prettier/recommended"], "extends": ["eslint:recommended", "plugin:prettier/recommended"],
"plugins": ["import"], "plugins": ["import"],
"parserOptions": { "parserOptions": {

View file

@ -15,10 +15,10 @@ jobs:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@master uses: actions/checkout@master
- name: Install Node v12 - name: Install Node v14
uses: actions/setup-node@master uses: actions/setup-node@master
with: with:
node-version: 12 node-version: 14
- name: Install dependencies - name: Install dependencies
run: npm ci run: npm ci
@ -35,10 +35,10 @@ jobs:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@master uses: actions/checkout@master
- name: Install Node v12 - name: Install Node v14
uses: actions/setup-node@master uses: actions/setup-node@master
with: with:
node-version: 12 node-version: 14
- name: Install dependencies - name: Install dependencies
run: npm ci run: npm ci

View file

@ -10,10 +10,10 @@ jobs:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v1 uses: actions/checkout@v1
- name: Install Node v12 - name: Install Node v14
uses: actions/setup-node@v1 uses: actions/setup-node@v1
with: with:
node-version: 12 node-version: 14
- name: Install dependencies - name: Install dependencies
run: npm ci run: npm ci
@ -28,10 +28,10 @@ jobs:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v1 uses: actions/checkout@v1
- name: Install Node v12 - name: Install Node v14
uses: actions/setup-node@v1 uses: actions/setup-node@v1
with: with:
node-version: 12 node-version: 14
- name: Install dependencies - name: Install dependencies
run: npm ci run: npm ci
@ -46,10 +46,10 @@ jobs:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Install Node v12 - name: Install Node v14
uses: actions/setup-node@v1 uses: actions/setup-node@v1
with: with:
node-version: 12 node-version: 14
- name: Install dependencies - name: Install dependencies
run: npm ci run: npm ci
@ -67,10 +67,10 @@ jobs:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v1 uses: actions/checkout@v1
- name: Install Node v12 - name: Install Node v14
uses: actions/setup-node@v1 uses: actions/setup-node@v1
with: with:
node-version: 12 node-version: 14
- name: Install dependencies - name: Install dependencies
run: npm ci run: npm ci

View file

@ -8,10 +8,10 @@ jobs:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Install Node v12 - name: Install Node v14
uses: actions/setup-node@v1 uses: actions/setup-node@v1
with: with:
node-version: 12 node-version: 14
- name: Install dependencies - name: Install dependencies
run: npm ci run: npm ci
@ -26,10 +26,10 @@ jobs:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Install Node v12 - name: Install Node v14
uses: actions/setup-node@v1 uses: actions/setup-node@v1
with: with:
node-version: 12 node-version: 14
- name: Install dependencies - name: Install dependencies
run: npm ci run: npm ci
@ -44,10 +44,10 @@ jobs:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Install Node v12 - name: Install Node v14
uses: actions/setup-node@v1 uses: actions/setup-node@v1
with: with:
node-version: 12 node-version: 14
- name: Install dependencies - name: Install dependencies
run: npm ci run: npm ci
@ -65,10 +65,10 @@ jobs:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Install Node v12 - name: Install Node v14
uses: actions/setup-node@v1 uses: actions/setup-node@v1
with: with:
node-version: 12 node-version: 14
- name: Install dependencies - name: Install dependencies
run: npm ci run: npm ci

View file

@ -19,6 +19,7 @@
## Table of contents ## Table of contents
- [Changes](#changes)
- [About](#about) - [About](#about)
- [Installation](#installation) - [Installation](#installation)
- [Audio engines](#audio-engines) - [Audio engines](#audio-engines)
@ -29,6 +30,10 @@
- [Contributing](#contributing) - [Contributing](#contributing)
- [Help](#help) - [Help](#help)
## Changes
This fork has the inline replies and slash commands for **testing** purposes.
## About ## About
discord.js is a powerful [Node.js](https://nodejs.org) module that allows you to easily interact with the discord.js is a powerful [Node.js](https://nodejs.org) module that allows you to easily interact with the
@ -41,7 +46,7 @@ discord.js is a powerful [Node.js](https://nodejs.org) module that allows you to
## Installation ## 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. Ignore any warnings about unmet peer dependencies, as they're all optional.
Without voice support: `npm install discord.js` Without voice support: `npm install discord.js`
@ -76,7 +81,7 @@ client.on('ready', () => {
client.on('message', msg => { client.on('message', msg => {
if (msg.content === 'ping') { if (msg.content === 'ping') {
msg.reply('pong'); msg.channel.send('pong');
} }
}); });

View file

@ -23,7 +23,7 @@ client.on('message', message => {
// If the message is "what is my avatar" // If the message is "what is my avatar"
if (message.content === 'what is my avatar') { if (message.content === 'what is my avatar') {
// Send the user's avatar URL // Send the user's avatar URL
message.reply(message.author.displayAvatarURL()); message.channel.send(message.author.displayAvatarURL());
} }
}); });

View file

@ -33,7 +33,7 @@ client.on('message', message => {
// If we have a user mentioned // If we have a user mentioned
if (user) { if (user) {
// Now we get the member from the user // Now we get the member from the user
const member = message.guild.member(user); const member = message.guild.members.resolve(user);
// If the member is in the guild // If the member is in the guild
if (member) { if (member) {
/** /**
@ -45,23 +45,23 @@ client.on('message', message => {
.kick('Optional reason that will display in the audit logs') .kick('Optional reason that will display in the audit logs')
.then(() => { .then(() => {
// We let the message author know we were able to kick the person // 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 => { .catch(err => {
// An error happened // An error happened
// This is generally due to the bot not being able to kick the member, // This is generally due to the bot not being able to kick the member,
// either due to missing permissions or role hierarchy // 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 // Log the error
console.error(err); console.error(err);
}); });
} else { } else {
// The mentioned user isn't in this guild // 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 // Otherwise, if no user was mentioned
} else { } else {
message.reply("You didn't mention the user to kick!"); message.channel.send("You didn't mention the user to kick!");
} }
} }
}); });
@ -105,7 +105,7 @@ client.on('message', message => {
// If we have a user mentioned // If we have a user mentioned
if (user) { if (user) {
// Now we get the member from the user // Now we get the member from the user
const member = message.guild.member(user); const member = message.guild.members.resolve(user);
// If the member is in the guild // If the member is in the guild
if (member) { if (member) {
/** /**
@ -121,23 +121,23 @@ client.on('message', message => {
}) })
.then(() => { .then(() => {
// We let the message author know we were able to ban the person // 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 => { .catch(err => {
// An error happened // An error happened
// This is generally due to the bot not being able to ban the member, // This is generally due to the bot not being able to ban the member,
// either due to missing permissions or role hierarchy // 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 // Log the error
console.error(err); console.error(err);
}); });
} else { } else {
// The mentioned user isn't in this guild // 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 { } else {
// Otherwise, if no user was mentioned // 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!");
} }
} }
}); });

View file

@ -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` ## 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? ## How do I get voice working?

View file

@ -33,7 +33,7 @@ discord.js is a powerful [Node.js](https://nodejs.org) module that allows you to
## Installation ## 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. Ignore any warnings about unmet peer dependencies, as they're all optional.
Without voice support: `npm install discord.js` Without voice support: `npm install discord.js`
@ -68,7 +68,7 @@ client.on('ready', () => {
client.on('message', msg => { client.on('message', msg => {
if (msg.content === 'ping') { if (msg.content === 'ping') {
msg.reply('pong'); msg.channel.send('pong');
} }
}); });

View file

@ -37,7 +37,7 @@ client.on('message', async message => {
if (message.member.voice.channel) { if (message.member.voice.channel) {
const connection = await message.member.voice.channel.join(); const connection = await message.member.voice.channel.join();
} else { } else {
message.reply('You need to join a voice channel first!'); message.channel.send('You need to join a voice channel first!');
} }
} }
}); });

View file

@ -28,6 +28,7 @@ export const {
UserFlags, UserFlags,
Util, Util,
version, version,
BaseGuildEmojiManager,
ChannelManager, ChannelManager,
GuildChannelManager, GuildChannelManager,
GuildEmojiManager, GuildEmojiManager,

View file

@ -80,7 +80,7 @@
"webpack-cli": "^3.3.12" "webpack-cli": "^3.3.12"
}, },
"engines": { "engines": {
"node": ">=12.0.0" "node": ">=14.0.0"
}, },
"browser": { "browser": {
"@discordjs/opus": false, "@discordjs/opus": false,
@ -110,9 +110,9 @@
"src/client/voice/receiver/PacketHandler.js": false, "src/client/voice/receiver/PacketHandler.js": false,
"src/client/voice/receiver/Receiver.js": false, "src/client/voice/receiver/Receiver.js": false,
"src/client/voice/util/PlayInterface.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/Silence.js": false,
"src/client/voice/util/VolumeInterface.js": false "src/client/voice/util/VolumeInterface.js": false,
"src/util/Sodium.js": false
}, },
"husky": { "husky": {
"hooks": { "hooks": {

View file

@ -1,12 +1,13 @@
'use strict'; 'use strict';
const BaseClient = require('./BaseClient'); const BaseClient = require('./BaseClient');
const InteractionClient = require('./InteractionClient');
const ActionsManager = require('./actions/ActionsManager'); const ActionsManager = require('./actions/ActionsManager');
const ClientVoiceManager = require('./voice/ClientVoiceManager'); const ClientVoiceManager = require('./voice/ClientVoiceManager');
const WebSocketManager = require('./websocket/WebSocketManager'); const WebSocketManager = require('./websocket/WebSocketManager');
const { Error, TypeError, RangeError } = require('../errors'); const { Error, TypeError, RangeError } = require('../errors');
const BaseGuildEmojiManager = require('../managers/BaseGuildEmojiManager');
const ChannelManager = require('../managers/ChannelManager'); const ChannelManager = require('../managers/ChannelManager');
const GuildEmojiManager = require('../managers/GuildEmojiManager');
const GuildManager = require('../managers/GuildManager'); const GuildManager = require('../managers/GuildManager');
const UserManager = require('../managers/UserManager'); const UserManager = require('../managers/UserManager');
const ShardClientUtil = require('../sharding/ShardClientUtil'); const ShardClientUtil = require('../sharding/ShardClientUtil');
@ -103,6 +104,12 @@ class Client extends BaseClient {
? ShardClientUtil.singleton(this, process.env.SHARDING_MANAGER_MODE) ? ShardClientUtil.singleton(this, process.env.SHARDING_MANAGER_MODE)
: null; : null;
/**
* The interaction client.
* @type {InteractionClient}
*/
this.interactionClient = new InteractionClient(options, this);
/** /**
* All of the {@link User} objects that have been cached at any point, mapped by their IDs * All of the {@link User} objects that have been cached at any point, mapped by their IDs
* @type {UserManager} * @type {UserManager}
@ -166,11 +173,11 @@ class Client extends BaseClient {
/** /**
* All custom emojis that the client has access to, mapped by their IDs * All custom emojis that the client has access to, mapped by their IDs
* @type {GuildEmojiManager} * @type {BaseGuildEmojiManager}
* @readonly * @readonly
*/ */
get emojis() { get emojis() {
const emojis = new GuildEmojiManager({ client: this }); const emojis = new BaseGuildEmojiManager(this);
for (const guild of this.guilds.cache.values()) { for (const guild of this.guilds.cache.values()) {
if (guild.available) for (const emoji of guild.emojis.cache.values()) emojis.cache.set(emoji.id, emoji); if (guild.available) for (const emoji of guild.emojis.cache.values()) emojis.cache.set(emoji.id, emoji);
} }

View file

@ -0,0 +1,207 @@
'use strict';
const BaseClient = require('./BaseClient');
const ApplicationCommand = require('../structures/ApplicationCommand');
const Interaction = require('../structures/Interaction');
const { Events, ApplicationCommandOptionType, InteractionType, InteractionResponseType } = require('../util/Constants');
let sodium;
/**
* Interaction client is used for interactions.
*
* @example
* const client = new InteractionClient({
* token: ABC,
* publicKey: XYZ,
* });
*
* client.on('interactionCreate', () => {
* // automatically handles long responses
* if (will take a long time) {
* doSomethingLong.then((d) => {
* interaction.reply({
* content: 'wow that took long',
* });
* });
* } else {
* interaction.reply('hi!');
* }
* });
* ```
*/
class InteractionClient extends BaseClient {
/**
* @param {Options} options Options for the client.
* @param {undefined} client For internal use.
*/
constructor(options, client) {
super(options);
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;
this.interactionClient = this;
}
/**
* 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);
}
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 {Promise<ApplicationCommand>} The created command.
*/
async createCommand(command, guildID) {
let path = this.client.api.applications(this.client.user.id);
if (guildID) {
path = path.guilds(guildID);
}
const c = await 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 ? o.options.map(m) : undefined,
};
}),
},
});
return new ApplicationCommand(this, c, guildID);
}
handle(data) {
switch (data.type) {
case InteractionType.PING:
return {
type: InteractionResponseType.PONG,
};
case InteractionType.APPLICATION_COMMAND: {
let timedOut = false;
let resolve;
const directPromise = new Promise(r => {
resolve = r;
this.client.setTimeout(() => {
timedOut = true;
r({
type: InteractionResponseType.ACKNOWLEDGE_WITH_SOURCE,
});
}, 250);
});
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);
/**
* 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 directPromise;
}
default:
throw new RangeError('Invalid interaction data');
}
}
/**
* An express-like middleware factory which can be used
* with webhook interactions.
* @returns {Function} The middleware function.
*/
middleware() {
return async (req, res) => {
const timestamp = req.get('x-signature-timestamp');
const signature = req.get('x-signature-ed25519');
const chunks = [];
for await (const chunk of req) {
chunks.push(chunk);
}
const body = Buffer.concat(chunks);
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));
};
}
async handleFromGateway(data) {
const result = await this.handle(data);
await this.client.api.interactions(data.id, data.token).callback.post({
data: result,
});
}
}
module.exports = InteractionClient;

View file

@ -37,6 +37,7 @@ class ActionsManager {
this.register(require('./GuildIntegrationsUpdate')); this.register(require('./GuildIntegrationsUpdate'));
this.register(require('./WebhooksUpdate')); this.register(require('./WebhooksUpdate'));
this.register(require('./TypingStart')); this.register(require('./TypingStart'));
this.register(require('./InteractionCreate'));
} }
register(Action) { register(Action) {

View file

@ -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;

View file

@ -473,7 +473,11 @@ class VoiceConnection extends EventEmitter {
} }
onStartSpeaking({ user_id, ssrc, speaking }) { onStartSpeaking({ user_id, ssrc, speaking }) {
this.ssrcMap.set(+ssrc, { userID: user_id, speaking: speaking }); this.ssrcMap.set(+ssrc, {
...(this.ssrcMap.get(+ssrc) || {}),
userID: user_id,
speaking: speaking,
});
} }
/** /**
@ -501,7 +505,7 @@ class VoiceConnection extends EventEmitter {
} }
if (guild && user && !speaking.equals(old)) { if (guild && user && !speaking.equals(old)) {
const member = guild.member(user); const member = guild.members.resolve(user);
if (member) { if (member) {
/** /**
* Emitted once a guild member changes speaking state. * Emitted once a guild member changes speaking state.

View file

@ -1,7 +1,7 @@
'use strict'; 'use strict';
const { Writable } = require('stream'); const { Writable } = require('stream');
const secretbox = require('../util/Secretbox'); const secretbox = require('../../../util/Sodium');
const Silence = require('../util/Silence'); const Silence = require('../util/Silence');
const VolumeInterface = require('../util/VolumeInterface'); const VolumeInterface = require('../util/VolumeInterface');

View file

@ -189,7 +189,11 @@ class VoiceWebSocket extends EventEmitter {
this.emit('sessionDescription', packet.d); this.emit('sessionDescription', packet.d);
break; break;
case VoiceOPCodes.CLIENT_CONNECT: case VoiceOPCodes.CLIENT_CONNECT:
this.connection.ssrcMap.set(+packet.d.audio_ssrc, { userID: packet.d.user_id, speaking: 0 }); this.connection.ssrcMap.set(+packet.d.audio_ssrc, {
userID: packet.d.user_id,
speaking: 0,
hasVideo: Boolean(packet.d.video_ssrc),
});
break; break;
case VoiceOPCodes.CLIENT_DISCONNECT: case VoiceOPCodes.CLIENT_DISCONNECT:
const streamInfo = this.connection.receiver && this.connection.receiver.packets.streams.get(packet.d.user_id); const streamInfo = this.connection.receiver && this.connection.receiver.packets.streams.get(packet.d.user_id);

View file

@ -1,7 +1,9 @@
'use strict'; 'use strict';
const EventEmitter = require('events'); const EventEmitter = require('events');
const secretbox = require('../util/Secretbox'); const sodium = require('../../../util/Sodium');
const Speaking = require('../../../util/Speaking');
const { SILENCE_FRAME } = require('../util/Silence');
// The delay between packets when a user is considered to have stopped speaking // The delay between packets when a user is considered to have stopped speaking
// https://github.com/discordjs/discord.js/issues/3524#issuecomment-540373200 // https://github.com/discordjs/discord.js/issues/3524#issuecomment-540373200
@ -56,7 +58,7 @@ class PacketHandler extends EventEmitter {
} }
// Open packet // 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'); if (!packet) return new Error('Failed to decrypt voice packet');
packet = Buffer.from(packet); packet = Buffer.from(packet);
@ -84,12 +86,30 @@ class PacketHandler extends EventEmitter {
const userStat = this.connection.ssrcMap.get(ssrc); const userStat = this.connection.ssrcMap.get(ssrc);
if (!userStat) return; if (!userStat) return;
let opusPacket;
const streamInfo = this.streams.get(userStat.userID);
// If the user is in video, we need to check if the packet is just silence
if (userStat.hasVideo) {
opusPacket = this.parseBuffer(buffer);
if (opusPacket instanceof Error) {
// Only emit an error if we were actively receiving packets from this user
if (streamInfo) {
this.emit('error', opusPacket);
return;
}
}
if (SILENCE_FRAME.equals(opusPacket)) {
// If this is a silence frame, pretend we never received it
return;
}
}
let speakingTimeout = this.speakingTimeouts.get(ssrc); let speakingTimeout = this.speakingTimeouts.get(ssrc);
if (typeof speakingTimeout === 'undefined') { if (typeof speakingTimeout === 'undefined') {
// Ensure at least the speaking bit is set. // Ensure at least the speaking bit is set.
// As the object is by reference, it's only needed once per client re-connect. // As the object is by reference, it's only needed once per client re-connect.
if (userStat.speaking === 0) { if (userStat.speaking === 0) {
userStat.speaking = 1; userStat.speaking = Speaking.FLAGS.SPEAKING;
} }
this.connection.onSpeaking({ user_id: userStat.userID, ssrc: ssrc, speaking: userStat.speaking }); this.connection.onSpeaking({ user_id: userStat.userID, ssrc: ssrc, speaking: userStat.speaking });
speakingTimeout = this.receiver.connection.client.setTimeout(() => { speakingTimeout = this.receiver.connection.client.setTimeout(() => {
@ -106,16 +126,18 @@ class PacketHandler extends EventEmitter {
speakingTimeout.refresh(); speakingTimeout.refresh();
} }
let stream = this.streams.get(userStat.userID); if (streamInfo) {
if (!stream) return; const { stream } = streamInfo;
stream = stream.stream; if (!opusPacket) {
const opusPacket = this.parseBuffer(buffer); opusPacket = this.parseBuffer(buffer);
if (opusPacket instanceof Error) { if (opusPacket instanceof Error) {
this.emit('error', opusPacket); this.emit('error', opusPacket);
return; return;
} }
}
stream.push(opusPacket); stream.push(opusPacket);
} }
}
} }
module.exports = PacketHandler; module.exports = PacketHandler;

View file

@ -10,4 +10,6 @@ class Silence extends Readable {
} }
} }
Silence.SILENCE_FRAME = SILENCE_FRAME;
module.exports = Silence; module.exports = Silence;

View file

@ -76,7 +76,7 @@ class WebSocketManager extends EventEmitter {
/** /**
* The current status of this WebSocketManager * The current status of this WebSocketManager
* @type {number} * @type {Status}
*/ */
this.status = Status.IDLE; this.status = Status.IDLE;

View file

@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet) => {
client.actions.InteractionCreate.handle(packet.d);
};

View file

@ -71,7 +71,7 @@ const Messages = {
IMAGE_SIZE: size => `Invalid image size: ${size}`, IMAGE_SIZE: size => `Invalid image size: ${size}`,
MESSAGE_BULK_DELETE_TYPE: 'The messages must be an Array, Collection, or number.', MESSAGE_BULK_DELETE_TYPE: 'The messages must be an Array, Collection, or number.',
MESSAGE_NONCE_TYPE: 'Message nonce must fit in an unsigned 64-bit integer.', MESSAGE_NONCE_TYPE: 'Message nonce must be an integer or a string.',
TYPING_COUNT: 'Count must be at least 1', TYPING_COUNT: 'Count must be at least 1',

View file

@ -6,6 +6,7 @@ module.exports = {
// "Root" classes (starting points) // "Root" classes (starting points)
BaseClient: require('./client/BaseClient'), BaseClient: require('./client/BaseClient'),
Client: require('./client/Client'), Client: require('./client/Client'),
InteractionClient: require('./client/InteractionClient'),
Shard: require('./sharding/Shard'), Shard: require('./sharding/Shard'),
ShardClientUtil: require('./sharding/ShardClientUtil'), ShardClientUtil: require('./sharding/ShardClientUtil'),
ShardingManager: require('./sharding/ShardingManager'), ShardingManager: require('./sharding/ShardingManager'),
@ -33,6 +34,7 @@ module.exports = {
version: require('../package.json').version, version: require('../package.json').version,
// Managers // Managers
BaseGuildEmojiManager: require('./managers/BaseGuildEmojiManager'),
ChannelManager: require('./managers/ChannelManager'), ChannelManager: require('./managers/ChannelManager'),
GuildChannelManager: require('./managers/GuildChannelManager'), GuildChannelManager: require('./managers/GuildChannelManager'),
GuildEmojiManager: require('./managers/GuildEmojiManager'), GuildEmojiManager: require('./managers/GuildEmojiManager'),
@ -57,6 +59,7 @@ module.exports = {
// Structures // Structures
Application: require('./structures/interfaces/Application'), Application: require('./structures/interfaces/Application'),
ApplicationCommand: require('./structures/ApplicationCommand'),
Base: require('./structures/Base'), Base: require('./structures/Base'),
Activity: require('./structures/Presence').Activity, Activity: require('./structures/Presence').Activity,
APIMessage: require('./structures/APIMessage'), APIMessage: require('./structures/APIMessage'),
@ -79,6 +82,7 @@ module.exports = {
GuildPreview: require('./structures/GuildPreview'), GuildPreview: require('./structures/GuildPreview'),
GuildTemplate: require('./structures/GuildTemplate'), GuildTemplate: require('./structures/GuildTemplate'),
Integration: require('./structures/Integration'), Integration: require('./structures/Integration'),
Interaction: require('./structures/Interaction'),
Invite: require('./structures/Invite'), Invite: require('./structures/Invite'),
Message: require('./structures/Message'), Message: require('./structures/Message'),
MessageAttachment: require('./structures/MessageAttachment'), MessageAttachment: require('./structures/MessageAttachment'),

View file

@ -0,0 +1,80 @@
'use strict';
const BaseManager = require('./BaseManager');
const GuildEmoji = require('../structures/GuildEmoji');
const ReactionEmoji = require('../structures/ReactionEmoji');
const { parseEmoji } = require('../util/Util');
/**
* Holds methods to resolve GuildEmojis and stores their cache.
* @extends {BaseManager}
*/
class BaseGuildEmojiManager extends BaseManager {
constructor(client, iterable) {
super(client, iterable, GuildEmoji);
}
/**
* The cache of GuildEmojis
* @type {Collection<Snowflake, GuildEmoji>}
* @name BaseGuildEmojiManager#cache
*/
/**
* Data that can be resolved into a GuildEmoji object. This can be:
* * A custom emoji ID
* * A GuildEmoji object
* * A ReactionEmoji object
* @typedef {Snowflake|GuildEmoji|ReactionEmoji} EmojiResolvable
*/
/**
* Resolves an EmojiResolvable to an Emoji object.
* @param {EmojiResolvable} emoji The Emoji resolvable to identify
* @returns {?GuildEmoji}
*/
resolve(emoji) {
if (emoji instanceof ReactionEmoji) return super.resolve(emoji.id);
return super.resolve(emoji);
}
/**
* Resolves an EmojiResolvable to an Emoji ID string.
* @param {EmojiResolvable} emoji The Emoji resolvable to identify
* @returns {?Snowflake}
*/
resolveID(emoji) {
if (emoji instanceof ReactionEmoji) return emoji.id;
return super.resolveID(emoji);
}
/**
* Data that can be resolved to give an emoji identifier. This can be:
* * The unicode representation of an emoji
* * The `<a:name:id>`, `<:name:id>`, `a:name:id` or `name:id` emoji identifier string of an emoji
* * An EmojiResolvable
* @typedef {string|EmojiResolvable} EmojiIdentifierResolvable
*/
/**
* Resolves an EmojiResolvable to an emoji identifier.
* @param {EmojiIdentifierResolvable} emoji The emoji resolvable to resolve
* @returns {?string}
*/
resolveIdentifier(emoji) {
const emojiResolvable = this.resolve(emoji);
if (emojiResolvable) return emojiResolvable.identifier;
if (emoji instanceof ReactionEmoji) return emoji.identifier;
if (typeof emoji === 'string') {
const res = parseEmoji(emoji);
if (res && res.name.length) {
emoji = `${res.animated ? 'a:' : ''}${res.name}${res.id ? `:${res.id}` : ''}`;
}
if (!emoji.includes('%')) return encodeURIComponent(emoji);
return emoji;
}
return null;
}
}
module.exports = BaseGuildEmojiManager;

View file

@ -1,20 +1,18 @@
'use strict'; 'use strict';
const BaseManager = require('./BaseManager'); const BaseGuildEmojiManager = require('./BaseGuildEmojiManager');
const { TypeError } = require('../errors'); const { TypeError } = require('../errors');
const GuildEmoji = require('../structures/GuildEmoji');
const ReactionEmoji = require('../structures/ReactionEmoji');
const Collection = require('../util/Collection'); const Collection = require('../util/Collection');
const DataResolver = require('../util/DataResolver'); const DataResolver = require('../util/DataResolver');
const { parseEmoji } = require('../util/Util');
/** /**
* Manages API methods for GuildEmojis and stores their cache. * Manages API methods for GuildEmojis and stores their cache.
* @extends {BaseManager} * @extends {BaseGuildEmojiManager}
*/ */
class GuildEmojiManager extends BaseManager { class GuildEmojiManager extends BaseGuildEmojiManager {
constructor(guild, iterable) { constructor(guild, iterable) {
super(guild.client, iterable, GuildEmoji); super(guild.client, iterable);
/** /**
* The guild this manager belongs to * The guild this manager belongs to
* @type {Guild} * @type {Guild}
@ -22,12 +20,6 @@ class GuildEmojiManager extends BaseManager {
this.guild = guild; this.guild = guild;
} }
/**
* The cache of GuildEmojis
* @type {Collection<Snowflake, GuildEmoji>}
* @name GuildEmojiManager#cache
*/
add(data, cache) { add(data, cache) {
return super.add(data, cache, { extras: [this.guild] }); return super.add(data, cache, { extras: [this.guild] });
} }
@ -74,62 +66,6 @@ class GuildEmojiManager extends BaseManager {
.emojis.post({ data, reason }) .emojis.post({ data, reason })
.then(emoji => this.client.actions.GuildEmojiCreate.handle(this.guild, emoji).emoji); .then(emoji => this.client.actions.GuildEmojiCreate.handle(this.guild, emoji).emoji);
} }
/**
* Data that can be resolved into an GuildEmoji object. This can be:
* * A custom emoji ID
* * A GuildEmoji object
* * A ReactionEmoji object
* @typedef {Snowflake|GuildEmoji|ReactionEmoji} EmojiResolvable
*/
/**
* Resolves an EmojiResolvable to an Emoji object.
* @param {EmojiResolvable} emoji The Emoji resolvable to identify
* @returns {?GuildEmoji}
*/
resolve(emoji) {
if (emoji instanceof ReactionEmoji) return super.resolve(emoji.id);
return super.resolve(emoji);
}
/**
* Resolves an EmojiResolvable to an Emoji ID string.
* @param {EmojiResolvable} emoji The Emoji resolvable to identify
* @returns {?Snowflake}
*/
resolveID(emoji) {
if (emoji instanceof ReactionEmoji) return emoji.id;
return super.resolveID(emoji);
}
/**
* Data that can be resolved to give an emoji identifier. This can be:
* * The unicode representation of an emoji
* * The `<a:name:id>`, `<:name:id>`, `:name:id` or `a:name:id` emoji identifier string of an emoji
* * An EmojiResolvable
* @typedef {string|EmojiResolvable} EmojiIdentifierResolvable
*/
/**
* Resolves an EmojiResolvable to an emoji identifier.
* @param {EmojiIdentifierResolvable} emoji The emoji resolvable to resolve
* @returns {?string}
*/
resolveIdentifier(emoji) {
const emojiResolvable = this.resolve(emoji);
if (emojiResolvable) return emojiResolvable.identifier;
if (emoji instanceof ReactionEmoji) return emoji.identifier;
if (typeof emoji === 'string') {
const res = parseEmoji(emoji);
if (res && res.name.length) {
emoji = `${res.animated ? 'a:' : ''}${res.name}${res.id ? `:${res.id}` : ''}`;
}
if (!emoji.includes('%')) return encodeURIComponent(emoji);
else return emoji;
}
return null;
}
} }
module.exports = GuildEmojiManager; module.exports = GuildEmojiManager;

View file

@ -210,6 +210,7 @@ class GuildMemberManager extends BaseManager {
* .catch(console.error); * .catch(console.error);
*/ */
ban(user, options = { days: 0 }) { ban(user, options = { days: 0 }) {
if (typeof options !== 'object') return Promise.reject(new TypeError('INVALID_TYPE', 'options', 'object', true));
if (options.days) options.delete_message_days = options.days; if (options.days) options.delete_message_days = options.days;
const id = this.client.users.resolveID(user); const id = this.client.users.resolveID(user);
if (!id) return Promise.reject(new Error('BAN_RESOLVE_ID', true)); if (!id) return Promise.reject(new Error('BAN_RESOLVE_ID', true));

View file

@ -2,6 +2,7 @@
const BaseManager = require('./BaseManager'); const BaseManager = require('./BaseManager');
const Role = require('../structures/Role'); const Role = require('../structures/Role');
const Collection = require('../util/Collection');
const Permissions = require('../util/Permissions'); const Permissions = require('../util/Permissions');
const { resolveColor } = require('../util/Util'); const { resolveColor } = require('../util/Util');
@ -31,10 +32,10 @@ class RoleManager extends BaseManager {
/** /**
* Obtains one or more roles from Discord, or the role cache if they're already available. * Obtains one or more roles from Discord, or the role cache if they're already available.
* @param {Snowflake} [id] ID or IDs of the role(s) * @param {Snowflake} [id] ID of the role to fetch
* @param {boolean} [cache=true] Whether to cache the new roles objects if it weren't already * @param {boolean} [cache=true] Whether to cache the new role object(s) if they weren't already
* @param {boolean} [force=false] Whether to skip the cache check and request the API * @param {boolean} [force=false] Whether to skip the cache check and request the API
* @returns {Promise<Role|RoleManager>} * @returns {Promise<?Role|Collection<Snowflake, Role>>}
* @example * @example
* // Fetch all roles from the guild * // Fetch all roles from the guild
* message.guild.roles.fetch() * message.guild.roles.fetch()
@ -53,9 +54,10 @@ class RoleManager extends BaseManager {
} }
// We cannot fetch a single role, as of this commit's date, Discord API throws with 405 // We cannot fetch a single role, as of this commit's date, Discord API throws with 405
const roles = await this.client.api.guilds(this.guild.id).roles.get(); const data = await this.client.api.guilds(this.guild.id).roles.get();
for (const role of roles) this.add(role, cache); const roles = new Collection();
return id ? this.cache.get(id) || null : this; for (const role of data) roles.set(role.id, this.add(role, cache));
return id ? roles.get(id) || null : roles;
} }
/** /**

View file

@ -74,13 +74,21 @@ class APIMessage {
return this.target instanceof Message; 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. * Makes the content of this message.
* @returns {?(string|string[])} * @returns {?(string|string[])}
*/ */
makeContent() { makeContent() {
const GuildMember = require('./GuildMember');
let content; let content;
if (this.options.content === null) { if (this.options.content === null) {
content = ''; content = '';
@ -110,25 +118,14 @@ class APIMessage {
const isCode = typeof this.options.code !== 'undefined' && this.options.code !== false; const isCode = typeof this.options.code !== 'undefined' && this.options.code !== false;
const splitOptions = isSplit ? { ...this.options.split } : undefined; const splitOptions = isSplit ? { ...this.options.split } : undefined;
let mentionPart = ''; if (content) {
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 (isCode) { if (isCode) {
const codeName = typeof this.options.code === 'string' ? this.options.code : ''; 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) { if (isSplit) {
splitOptions.prepend = `${splitOptions.prepend || ''}\`\`\`${codeName}\n`; splitOptions.prepend = `${splitOptions.prepend || ''}\`\`\`${codeName}\n`;
splitOptions.append = `\n\`\`\`${splitOptions.append || ''}`; splitOptions.append = `\n\`\`\`${splitOptions.append || ''}`;
} }
} else if (mentionPart) {
content = `${mentionPart}${content}`;
} }
if (isSplit) { if (isSplit) {
@ -151,8 +148,11 @@ class APIMessage {
let nonce; let nonce;
if (typeof this.options.nonce !== 'undefined') { if (typeof this.options.nonce !== 'undefined') {
nonce = parseInt(this.options.nonce); nonce = this.options.nonce;
if (isNaN(nonce) || nonce < 0) throw new RangeError('MESSAGE_NONCE_TYPE'); // eslint-disable-next-line max-len
if (typeof nonce === 'number' ? !Number.isInteger(nonce) : typeof nonce !== 'string') {
throw new RangeError('MESSAGE_NONCE_TYPE');
}
} }
const embedLikes = []; const embedLikes = [];
@ -176,25 +176,28 @@ class APIMessage {
if (this.isMessage) { if (this.isMessage) {
// eslint-disable-next-line eqeqeq // eslint-disable-next-line eqeqeq
flags = this.options.flags != null ? new MessageFlags(this.options.flags).bitfield : this.target.flags.bitfield; 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 = let allowedMentions =
typeof this.options.allowedMentions === 'undefined' typeof this.options.allowedMentions === 'undefined'
? this.target.client.options.allowedMentions ? this.target.client.options.allowedMentions
: this.options.allowedMentions; : this.options.allowedMentions;
if (this.options.reply) {
const id = this.target.client.users.resolveID(this.options.reply);
if (allowedMentions) { if (allowedMentions) {
// Clone the object as not to alter the ClientOptions object
allowedMentions = Util.cloneObject(allowedMentions); allowedMentions = Util.cloneObject(allowedMentions);
const parsed = allowedMentions.parse && allowedMentions.parse.includes('users'); allowedMentions.replied_user = allowedMentions.repliedUser;
// Check if the mention won't be parsed, and isn't supplied in `users` delete allowedMentions.repliedUser;
if (!parsed && !(allowedMentions.users && allowedMentions.users.includes(id))) {
if (!allowedMentions.users) allowedMentions.users = [];
allowedMentions.users.push(id);
} }
} else {
allowedMentions = { users: [id] }; 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 };
} }
} }
@ -206,8 +209,10 @@ class APIMessage {
embeds, embeds,
username, username,
avatar_url: avatarURL, avatar_url: avatarURL,
allowed_mentions: typeof content === 'undefined' ? undefined : allowedMentions, allowed_mentions:
typeof content === 'undefined' && typeof message_reference === 'undefined' ? undefined : allowedMentions,
flags, flags,
message_reference,
}; };
return this; return this;
} }

View file

@ -0,0 +1,102 @@
'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 || null;
this._patch(data);
}
_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],
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;

View file

@ -17,7 +17,7 @@ class BaseGuildEmoji extends Emoji {
*/ */
this.guild = guild; this.guild = guild;
this.requireColons = null; this.requiresColons = null;
this.managed = null; this.managed = null;
this.available = null; this.available = null;

View file

@ -82,7 +82,7 @@ class Emoji extends Base {
* @example * @example
* // Send a custom emoji from a guild: * // Send a custom emoji from a guild:
* const emoji = guild.emojis.cache.first(); * const emoji = guild.emojis.cache.first();
* msg.reply(`Hello! ${emoji}`); * msg.channel.send(`Hello! ${emoji}`);
* @example * @example
* // Send the emoji used in a reaction to the channel the reaction is part of * // 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}`); * reaction.message.channel.send(`The emoji used was: ${reaction.emoji}`);

View file

@ -637,18 +637,6 @@ class Guild extends Base {
return this.voiceStates.cache.get(this.client.user.id); return this.voiceStates.cache.get(this.client.user.id);
} }
/**
* Returns the GuildMember form of a User object, if the user is present in the guild.
* @param {UserResolvable} user The user that you want to obtain the GuildMember of
* @returns {?GuildMember}
* @example
* // Get the guild member of a user
* const member = guild.member(message.author);
*/
member(user) {
return this.members.resolve(user);
}
/** /**
* Fetches this guild. * Fetches this guild.
* @returns {Promise<Guild>} * @returns {Promise<Guild>}
@ -1444,6 +1432,23 @@ class Guild extends Base {
.then(() => this); .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. * Leaves the guild.
* @returns {Promise<Guild>} * @returns {Promise<Guild>}

View file

@ -198,8 +198,9 @@ class GuildTemplate extends Base {
* @readonly * @readonly
*/ */
get guild() { get guild() {
return this.client.guilds.get(this.guildID) || null; return this.client.guilds.cache.get(this.guildID) || null;
} }
/** /**
* The URL of this template * The URL of this template
* @type {string} * @type {string}

View file

@ -0,0 +1,133 @@
'use strict';
const APIMessage = require('./APIMessage');
const Base = require('./Base');
const Snowflake = require('../util/Snowflake');
/**
* Represents an interaction, see {@link InteractionClient}.
* @extends {Base}
*/
class Interaction extends Base {
constructor(client, data, syncHandle) {
super(client);
this.syncHandle = syncHandle;
this._patch(data);
}
_patch(data) {
/**
* 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}
* @readonly
*/
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;
}
/**
* The timestamp the interaction was created at.
* @type {number}
* @readonly
*/
get createdTimestamp() {
return Snowflake.deconstruct(this.id).timestamp;
}
/**
* The time the interaction was created at.
* @type {Date}
* @readonly
*/
get createdAt() {
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;
if (content instanceof APIMessage) {
apiMessage = content.resolveData();
} else {
apiMessage = APIMessage.create(this, content, options).resolveData();
if (Array.isArray(apiMessage.data.content)) {
throw new Error('Message is too long');
}
}
const resolved = await apiMessage.resolveFiles();
if (!this.syncHandle.reply(resolved)) {
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,
data: resolved.data,
files: resolved.files,
});
}
}
}
module.exports = Interaction;

View file

@ -205,11 +205,11 @@ class Message extends Base {
this.flags = new MessageFlags(data.flags).freeze(); 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 * @typedef {Object} MessageReference
* @property {string} channelID ID of the channel the message was crossposted from * @property {string} channelID ID of the channel the message was referenced
* @property {?string} guildID ID of the guild the message was crossposted from * @property {?string} guildID ID of the guild the message was referenced
* @property {?string} messageID ID of the message that was crossposted * @property {?string} messageID ID of the message that was referenced
*/ */
/** /**
@ -223,6 +223,10 @@ class Message extends Base {
messageID: data.message_reference.message_id, messageID: data.message_reference.message_id,
} }
: null; : null;
if (data.referenced_message) {
this.channel.messages.add(data.referenced_message);
}
} }
/** /**
@ -284,7 +288,7 @@ class Message extends Base {
* @readonly * @readonly
*/ */
get member() { get member() {
return this.guild ? this.guild.member(this.author) || null : null; return this.guild ? this.guild.members.resolve(this.author) || null : null;
} }
/** /**
@ -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 * Whether the message is crosspostable by the client user
* @type {boolean} * @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 {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<Message|Message[]>} * @returns {Promise<Message|Message[]>}
* @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) { reply(content, options) {
return this.channel.send( return this.channel.send(
content instanceof APIMessage content instanceof APIMessage
? content ? content
: APIMessage.transformOptions(content, options, { reply: this.member || this.author }), : APIMessage.transformOptions(content, options, {
replyTo: this,
}),
); );
} }

View file

@ -138,7 +138,7 @@ class MessageMentions {
if (!this.guild) return null; if (!this.guild) return null;
this._members = new Collection(); this._members = new Collection();
this.users.forEach(user => { this.users.forEach(user => {
const member = this.guild.member(user); const member = this.guild.members.resolve(user);
if (member) this._members.set(member.user.id, member); if (member) this._members.set(member.user.id, member);
}); });
return this._members; return this._members;

View file

@ -28,12 +28,6 @@ class MessageReaction {
*/ */
this.message = message; this.message = message;
/**
* Whether the client has given this reaction
* @type {boolean}
*/
this.me = data.me;
/** /**
* A manager of the users that have given this reaction * A manager of the users that have given this reaction
* @type {ReactionUserManager} * @type {ReactionUserManager}
@ -54,6 +48,12 @@ class MessageReaction {
*/ */
this.count = data.count; this.count = data.count;
} }
/**
* Whether the client has given this reaction
* @type {boolean}
*/
this.me = data.me;
} }
/** /**

View file

@ -127,7 +127,7 @@ class Role extends Base {
*/ */
get editable() { get editable() {
if (this.managed) return false; if (this.managed) return false;
const clientMember = this.guild.member(this.client.user); const clientMember = this.guild.members.resolve(this.client.user);
if (!clientMember.permissions.has(Permissions.FLAGS.MANAGE_ROLES)) return false; if (!clientMember.permissions.has(Permissions.FLAGS.MANAGE_ROLES)) return false;
return clientMember.roles.highest.comparePositionTo(this) > 0; return clientMember.roles.highest.comparePositionTo(this) > 0;
} }

View file

@ -28,7 +28,6 @@ class User extends Base {
this.id = data.id; this.id = data.id;
this.system = null; this.system = null;
this.locale = null;
this.flags = null; this.flags = null;
this._patch(data); this._patch(data);
@ -81,14 +80,6 @@ class User extends Base {
this.system = Boolean(data.system); this.system = Boolean(data.system);
} }
if ('locale' in data) {
/**
* The locale of the user's client (ISO 639-1)
* @type {?string}
*/
this.locale = data.locale;
}
if ('public_flags' in data) { if ('public_flags' in data) {
/** /**
* The flags for this user * The flags for this user
@ -287,7 +278,7 @@ class User extends Base {
/** /**
* Fetches this user's flags. * 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<UserFlags>} * @returns {Promise<UserFlags>}
*/ */
async fetchFlags(force = false) { async fetchFlags(force = false) {
@ -299,7 +290,7 @@ class User extends Base {
/** /**
* Fetches this user. * 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<User>} * @returns {Promise<User>}
*/ */
fetch(force = false) { fetch(force = false) {

View file

@ -1,6 +1,7 @@
'use strict'; 'use strict';
const EventEmitter = require('events'); const EventEmitter = require('events');
const { TypeError } = require('../../errors');
const Collection = require('../../util/Collection'); const Collection = require('../../util/Collection');
const Util = require('../../util/Util'); const Util = require('../../util/Util');
@ -74,6 +75,10 @@ class Collector extends EventEmitter {
*/ */
this._idletimeout = null; this._idletimeout = null;
if (typeof filter !== 'function') {
throw new TypeError('INVALID_TYPE', 'filter', 'function');
}
this.handleCollect = this.handleCollect.bind(this); this.handleCollect = this.handleCollect.bind(this);
this.handleDispose = this.handleDispose.bind(this); this.handleDispose = this.handleDispose.bind(this);

View file

@ -65,7 +65,7 @@ class TextBasedChannel {
* @property {string|boolean} [code] Language for optional codeblock formatting to apply * @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 * @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 * 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 {MessageMentionTypes[]} [parse] Types of mentions to be parsed
* @property {Snowflake[]} [users] Snowflakes of Users to be parsed as mentions * @property {Snowflake[]} [users] Snowflakes of Users to be parsed as mentions
* @property {Snowflake[]} [roles] Snowflakes of Roles 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
*/ */
/** /**

View file

@ -77,14 +77,14 @@ exports.DefaultOptions = {
/** /**
* HTTP options * HTTP options
* @typedef {Object} HTTPOptions * @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} [api='https://discord.com/api'] Base url of the API
* @property {string} [cdn='https://cdn.discordapp.com'] Base url of the CDN * @property {string} [cdn='https://cdn.discordapp.com'] Base url of the CDN
* @property {string} [invite='https://discord.gg'] Base url of invites * @property {string} [invite='https://discord.gg'] Base url of invites
* @property {string} [template='https://discord.new'] Base url of templates * @property {string} [template='https://discord.new'] Base url of templates
*/ */
http: { http: {
version: 7, version: 8,
api: 'https://discord.com/api', api: 'https://discord.com/api',
cdn: 'https://cdn.discordapp.com', cdn: 'https://cdn.discordapp.com',
invite: 'https://discord.gg', invite: 'https://discord.gg',
@ -282,6 +282,7 @@ exports.Events = {
SHARD_READY: 'shardReady', SHARD_READY: 'shardReady',
SHARD_RESUME: 'shardResume', SHARD_RESUME: 'shardResume',
INVALIDATED: 'invalidated', INVALIDATED: 'invalidated',
INTERACTION_CREATE: 'interactionCreate',
RAW: 'raw', RAW: 'raw',
}; };
@ -345,6 +346,7 @@ exports.PartialTypes = keyMirror(['USER', 'CHANNEL', 'GUILD_MEMBER', 'MESSAGE',
* * VOICE_STATE_UPDATE * * VOICE_STATE_UPDATE
* * VOICE_SERVER_UPDATE * * VOICE_SERVER_UPDATE
* * WEBHOOKS_UPDATE * * WEBHOOKS_UPDATE
* * INTERACTION_CREATE
* @typedef {string} WSEventType * @typedef {string} WSEventType
*/ */
exports.WSEvents = keyMirror([ exports.WSEvents = keyMirror([
@ -384,6 +386,7 @@ exports.WSEvents = keyMirror([
'VOICE_STATE_UPDATE', 'VOICE_STATE_UPDATE',
'VOICE_SERVER_UPDATE', 'VOICE_SERVER_UPDATE',
'WEBHOOKS_UPDATE', 'WEBHOOKS_UPDATE',
'INTERACTION_CREATE',
]); ]);
/** /**
@ -403,6 +406,7 @@ exports.WSEvents = keyMirror([
* * CHANNEL_FOLLOW_ADD * * CHANNEL_FOLLOW_ADD
* * GUILD_DISCOVERY_DISQUALIFIED * * GUILD_DISCOVERY_DISQUALIFIED
* * GUILD_DISCOVERY_REQUALIFIED * * GUILD_DISCOVERY_REQUALIFIED
* * REPLY
* @typedef {string} MessageType * @typedef {string} MessageType
*/ */
exports.MessageTypes = [ exports.MessageTypes = [
@ -422,6 +426,10 @@ exports.MessageTypes = [
null, null,
'GUILD_DISCOVERY_DISQUALIFIED', 'GUILD_DISCOVERY_DISQUALIFIED',
'GUILD_DISCOVERY_REQUALIFIED', 'GUILD_DISCOVERY_REQUALIFIED',
null,
null,
null,
'REPLY',
]; ];
/** /**
@ -671,6 +679,33 @@ exports.WebhookTypes = [
'Channel Follower', 'Channel Follower',
]; ];
exports.ApplicationCommandOptionType = {
SUB_COMMAND: 1,
SUB_COMMAND_GROUP: 2,
STRING: 3,
INTEGER: 4,
BOOLEAN: 5,
USER: 6,
CHANNEL: 7,
ROLE: 8,
};
Object.entries(exports.ApplicationCommandOptionType).forEach(([k, v]) => {
exports.ApplicationCommandOptionType[v] = k;
});
exports.InteractionType = {
PING: 1,
APPLICATION_COMMAND: 2,
};
exports.InteractionResponseType = {
PONG: 1,
ACKNOWLEDGE: 2,
CHANNEL_MESSAGE: 3,
CHANNEL_MESSAGE_WITH_SOURCE: 4,
ACKNOWLEDGE_WITH_SOURCE: 5,
};
function keyMirror(arr) { function keyMirror(arr) {
let tmp = Object.create(null); let tmp = Object.create(null);
for (const value of arr) tmp[value] = value; for (const value of arr) tmp[value] = value;

View file

@ -22,6 +22,7 @@ class MessageFlags extends BitField {}
* * `SUPPRESS_EMBEDS` * * `SUPPRESS_EMBEDS`
* * `SOURCE_MESSAGE_DELETED` * * `SOURCE_MESSAGE_DELETED`
* * `URGENT` * * `URGENT`
* * `EPHEMERAL`
* @type {Object} * @type {Object}
* @see {@link https://discord.com/developers/docs/resources/channel#message-object-message-flags} * @see {@link https://discord.com/developers/docs/resources/channel#message-object-message-flags}
*/ */
@ -31,6 +32,7 @@ MessageFlags.FLAGS = {
SUPPRESS_EMBEDS: 1 << 2, SUPPRESS_EMBEDS: 1 << 2,
SOURCE_MESSAGE_DELETED: 1 << 3, SOURCE_MESSAGE_DELETED: 1 << 3,
URGENT: 1 << 4, URGENT: 1 << 4,
EPHEMERAL: 1 << 6,
}; };
module.exports = MessageFlags; module.exports = MessageFlags;

View file

@ -5,16 +5,19 @@ const libs = {
open: sodium.api.crypto_secretbox_open_easy, open: sodium.api.crypto_secretbox_open_easy,
close: sodium.api.crypto_secretbox_easy, close: sodium.api.crypto_secretbox_easy,
random: n => sodium.randombytes_buf(n), random: n => sodium.randombytes_buf(n),
verify: sodium.api.crypto_sign_verify_detached,
}), }),
'libsodium-wrappers': sodium => ({ 'libsodium-wrappers': sodium => ({
open: sodium.crypto_secretbox_open_easy, open: sodium.crypto_secretbox_open_easy,
close: sodium.crypto_secretbox_easy, close: sodium.crypto_secretbox_easy,
random: n => sodium.randombytes_buf(n), random: n => sodium.randombytes_buf(n),
verify: sodium.crypto_sign_verify_detached,
}), }),
tweetnacl: tweetnacl => ({ tweetnacl: tweetnacl => ({
open: tweetnacl.secretbox.open, open: tweetnacl.secretbox.open,
close: tweetnacl.secretbox, close: tweetnacl.secretbox,
random: n => tweetnacl.randomBytes(n), random: n => tweetnacl.randomBytes(n),
verify: (s, d, p) => tweetnacl.sign.detached.verify(d, s, p),
}), }),
}; };

View file

@ -36,9 +36,9 @@ client.on('message', message => {
// Clean content and log each character // Clean content and log each character
console.log(Util.cleanContent(args.join(' '), message).split('')); console.log(Util.cleanContent(args.join(' '), message).split(''));
if (command === 'test1') message.reply(tests[0]); if (command === 'test1') message.channel.send(tests[0]);
else if (command === 'test2') message.reply(tests[1]); else if (command === 'test2') message.channel.send(tests[1]);
else if (command === 'test3') message.reply(tests[2]); else if (command === 'test3') message.channel.send(tests[2]);
}); });
client.login(token).catch(console.error); client.login(token).catch(console.error);

View file

@ -116,7 +116,8 @@ client.on('message', message => {
if (message.content.startsWith('kick')) { if (message.content.startsWith('kick')) {
message.guild message.guild
.member(message.mentions.users.first()) .members
.resolve(message.mentions.users.first())
.kick() .kick()
.then(member => { .then(member => {
console.log(member); console.log(member);
@ -134,7 +135,7 @@ client.on('message', message => {
} }
message.channel.send('last one...').then(m => { message.channel.send('last one...').then(m => {
const diff = Date.now() - start; 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`);
}); });
} }
@ -205,7 +206,7 @@ client.on('message', msg => {
.join() .join()
.then(conn => { .then(conn => {
con = conn; con = conn;
msg.reply('done'); msg.channel.send('done');
const s = ytdl(song, { filter: 'audioonly' }, { passes: 3 }); const s = ytdl(song, { filter: 'audioonly' }, { passes: 3 });
s.on('error', e => console.log(`e w stream 2 ${e}`)); s.on('error', e => console.log(`e w stream 2 ${e}`));
disp = conn.playStream(s); disp = conn.playStream(s);

View file

@ -32,7 +32,6 @@ const tests = [
m => m.channel.send(fill('x'), { split: true }), m => m.channel.send(fill('x'), { split: true }),
m => m.channel.send(fill('1'), { code: 'js', 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(fill('xyz '), { split: { char: ' ' } }),
m => m.channel.send('x', { embed: { description: 'a' } }), 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({ files: [await read(fileA)] }),
async m => async m =>
m.channel.send(fill('x'), { m.channel.send(fill('x'), {
reply: m.author,
code: 'js', code: 'js',
split: true, split: true,
embed: embed().setImage('attachment://zero.png'), embed: embed().setImage('attachment://zero.png'),
@ -111,7 +109,6 @@ const tests = [
m => m.channel.send({ files: [{ attachment: readStream(fileA) }] }), m => m.channel.send({ files: [{ attachment: readStream(fileA) }] }),
async m => async m =>
m.channel.send(fill('xyz '), { m.channel.send(fill('xyz '), {
reply: m.author,
code: 'js', code: 'js',
split: { char: ' ', prepend: 'hello! ', append: '!!!' }, split: { char: ' ', prepend: 'hello! ', append: '!!!' },
embed: embed().setImage('attachment://zero.png'), embed: embed().setImage('attachment://zero.png'),

View file

@ -31,16 +31,13 @@ const commands = {
} }
message.channel.send(res, { code: 'js' }); message.channel.send(res, { code: 'js' });
}, },
ping: message => message.reply('pong'), ping: message => message.channel.send('pong'),
}; };
client.on('message', message => { client.on('message', message => {
if (!message.content.startsWith(prefix) || message.author.bot) return; if (!message.content.startsWith(prefix) || message.author.bot) return;
message.content = message.content message.content = message.content.replace(prefix, '').trim().split(' ');
.replace(prefix, '')
.trim()
.split(' ');
const command = message.content.shift(); const command = message.content.shift();
message.content = message.content.join(' '); message.content = message.content.join(' ');

View file

@ -43,20 +43,15 @@ client.on('message', m => {
conn.receiver.createStream(m.author, true).on('data', b => console.log(b.toString())); conn.receiver.createStream(m.author, true).on('data', b => console.log(b.toString()));
conn.player.on('error', (...e) => console.log('player', ...e)); conn.player.on('error', (...e) => console.log('player', ...e));
if (!connections.has(m.guild.id)) connections.set(m.guild.id, { conn, queue: [] }); 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 })); conn.play(ytdl('https://www.youtube.com/watch?v=_XXOSf0s2nk', { filter: 'audioonly' }, { passes: 3 }));
}); });
} else { } else {
m.reply('Specify a voice channel!'); m.channel.send('Specify a voice channel!');
} }
} else if (m.content.startsWith('#eval') && m.author.id === '66564597481480192') { } else if (m.content.startsWith('#eval') && m.author.id === '66564597481480192') {
try { try {
const com = eval( const com = eval(m.content.split(' ').slice(1).join(' '));
m.content
.split(' ')
.slice(1)
.join(' '),
);
m.channel.send(com, { code: true }); m.channel.send(com, { code: true });
} catch (e) { } catch (e) {
console.log(e); console.log(e);

View file

@ -32,7 +32,6 @@ const tests = [
(m, hook) => hook.send(fill('x'), { split: true }), (m, hook) => hook.send(fill('x'), { split: true }),
(m, hook) => hook.send(fill('1'), { code: 'js', 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(fill('xyz '), { split: { char: ' ' } }),
(m, hook) => hook.send({ embeds: [{ description: 'a' }] }), (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({ files: [await read(fileA)] }),
async (m, hook) => async (m, hook) =>
hook.send(fill('x'), { hook.send(fill('x'), {
reply: m.author,
code: 'js', code: 'js',
split: true, split: true,
embeds: [embed().setImage('attachment://zero.png')], embeds: [embed().setImage('attachment://zero.png')],
@ -108,7 +106,6 @@ const tests = [
(m, hook) => hook.send({ files: [{ attachment: readStream(fileA) }] }), (m, hook) => hook.send({ files: [{ attachment: readStream(fileA) }] }),
async (m, hook) => async (m, hook) =>
hook.send(fill('xyz '), { hook.send(fill('xyz '), {
reply: m.author,
code: 'js', code: 'js',
split: { char: ' ', prepend: 'hello! ', append: '!!!' }, split: { char: ' ', prepend: 'hello! ', append: '!!!' },
embeds: [embed().setImage('attachment://zero.png')], embeds: [embed().setImage('attachment://zero.png')],

39
typings/index.d.ts vendored
View file

@ -199,7 +199,7 @@ declare module 'discord.js' {
private _validateOptions(options?: ClientOptions): void; private _validateOptions(options?: ClientOptions): void;
public channels: ChannelManager; public channels: ChannelManager;
public readonly emojis: GuildEmojiManager; public readonly emojis: BaseGuildEmojiManager;
public guilds: GuildManager; public guilds: GuildManager;
public readyAt: Date | null; public readyAt: Date | null;
public readonly readyTimestamp: number | null; public readonly readyTimestamp: number | null;
@ -658,7 +658,6 @@ declare module 'discord.js' {
public fetchWidget(): Promise<GuildWidget>; public fetchWidget(): Promise<GuildWidget>;
public iconURL(options?: ImageURLOptions & { dynamic?: boolean }): string | null; public iconURL(options?: ImageURLOptions & { dynamic?: boolean }): string | null;
public leave(): Promise<Guild>; public leave(): Promise<Guild>;
public member(user: UserResolvable): GuildMember | null;
public setAFKChannel(afkChannel: ChannelResolvable | null, reason?: string): Promise<Guild>; public setAFKChannel(afkChannel: ChannelResolvable | null, reason?: string): Promise<Guild>;
public setAFKTimeout(afkTimeout: number, reason?: string): Promise<Guild>; public setAFKTimeout(afkTimeout: number, reason?: string): Promise<Guild>;
public setBanner(banner: Base64Resolvable | null, reason?: string): Promise<Guild>; public setBanner(banner: Base64Resolvable | null, reason?: string): Promise<Guild>;
@ -976,7 +975,7 @@ declare module 'discord.js' {
public id: Snowflake; public id: Snowflake;
public readonly member: GuildMember | null; public readonly member: GuildMember | null;
public mentions: MessageMentions; public mentions: MessageMentions;
public nonce: string | null; public nonce: string | number | null;
public readonly partial: false; public readonly partial: false;
public readonly pinnable: boolean; public readonly pinnable: boolean;
public pinned: boolean; public pinned: boolean;
@ -988,6 +987,7 @@ declare module 'discord.js' {
public webhookID: Snowflake | null; public webhookID: Snowflake | null;
public flags: Readonly<MessageFlags>; public flags: Readonly<MessageFlags>;
public reference: MessageReference | null; public reference: MessageReference | null;
public readonly referencedMessage: Message | null;
public awaitReactions( public awaitReactions(
filter: CollectorFilter, filter: CollectorFilter,
options?: AwaitReactionsOptions, options?: AwaitReactionsOptions,
@ -1529,7 +1529,6 @@ declare module 'discord.js' {
public flags: Readonly<UserFlags> | null; public flags: Readonly<UserFlags> | null;
public id: Snowflake; public id: Snowflake;
public lastMessageID: Snowflake | null; public lastMessageID: Snowflake | null;
public locale: string | null;
public readonly partial: false; public readonly partial: false;
public readonly presence: Presence; public readonly presence: Presence;
public system: boolean | null; public system: boolean | null;
@ -1872,11 +1871,6 @@ declare module 'discord.js' {
//#region Managers //#region Managers
export class ChannelManager extends BaseManager<Snowflake, Channel, ChannelResolvable> {
constructor(client: Client, iterable: Iterable<any>);
public fetch(id: Snowflake, cache?: boolean, force?: boolean): Promise<Channel>;
}
export abstract class BaseManager<K, Holds, R> { export abstract class BaseManager<K, Holds, R> {
constructor(client: Client, iterable: Iterable<any>, holds: Constructable<Holds>, cacheType: Collection<K, Holds>); constructor(client: Client, iterable: Iterable<any>, holds: Constructable<Holds>, cacheType: Collection<K, Holds>);
public holds: Constructable<Holds>; public holds: Constructable<Holds>;
@ -1889,6 +1883,16 @@ declare module 'discord.js' {
public valueOf(): Collection<K, Holds>; public valueOf(): Collection<K, Holds>;
} }
export class BaseGuildEmojiManager extends BaseManager<Snowflake, GuildEmoji, EmojiResolvable> {
constructor(client: Client, iterable?: Iterable<any>);
public resolveIdentifier(emoji: EmojiIdentifierResolvable): string | null;
}
export class ChannelManager extends BaseManager<Snowflake, Channel, ChannelResolvable> {
constructor(client: Client, iterable: Iterable<any>);
public fetch(id: Snowflake, cache?: boolean, force?: boolean): Promise<Channel>;
}
export class GuildChannelManager extends BaseManager<Snowflake, GuildChannel, GuildChannelResolvable> { export class GuildChannelManager extends BaseManager<Snowflake, GuildChannel, GuildChannelResolvable> {
constructor(guild: Guild, iterable?: Iterable<any>); constructor(guild: Guild, iterable?: Iterable<any>);
public guild: Guild; public guild: Guild;
@ -1901,7 +1905,7 @@ declare module 'discord.js' {
): Promise<TextChannel | VoiceChannel | CategoryChannel>; ): Promise<TextChannel | VoiceChannel | CategoryChannel>;
} }
export class GuildEmojiManager extends BaseManager<Snowflake, GuildEmoji, EmojiResolvable> { export class GuildEmojiManager extends BaseGuildEmojiManager {
constructor(guild: Guild, iterable?: Iterable<any>); constructor(guild: Guild, iterable?: Iterable<any>);
public guild: Guild; public guild: Guild;
public create( public create(
@ -1909,7 +1913,6 @@ declare module 'discord.js' {
name: string, name: string,
options?: GuildEmojiCreateOptions, options?: GuildEmojiCreateOptions,
): Promise<GuildEmoji>; ): Promise<GuildEmoji>;
public resolveIdentifier(emoji: EmojiIdentifierResolvable): string | null;
} }
export class GuildEmojiRoleManager { export class GuildEmojiRoleManager {
@ -2013,7 +2016,7 @@ declare module 'discord.js' {
public create(options?: { data?: RoleData; reason?: string }): Promise<Role>; public create(options?: { data?: RoleData; reason?: string }): Promise<Role>;
public fetch(id: Snowflake, cache?: boolean, force?: boolean): Promise<Role | null>; public fetch(id: Snowflake, cache?: boolean, force?: boolean): Promise<Role | null>;
public fetch(id?: Snowflake, cache?: boolean, force?: boolean): Promise<this>; public fetch(id?: Snowflake, cache?: boolean, force?: boolean): Promise<Collection<Snowflake, Role>>;
} }
export class UserManager extends BaseManager<Snowflake, User, UserResolvable> { export class UserManager extends BaseManager<Snowflake, User, UserResolvable> {
@ -2824,13 +2827,14 @@ declare module 'discord.js' {
parse?: MessageMentionTypes[]; parse?: MessageMentionTypes[];
roles?: Snowflake[]; roles?: Snowflake[];
users?: Snowflake[]; users?: Snowflake[];
repliedUser?: boolean;
} }
type MessageMentionTypes = 'roles' | 'users' | 'everyone'; type MessageMentionTypes = 'roles' | 'users' | 'everyone';
interface MessageOptions { interface MessageOptions {
tts?: boolean; tts?: boolean;
nonce?: string; nonce?: string | number;
content?: StringResolvable; content?: StringResolvable;
embed?: MessageEmbed | MessageEmbedOptions; embed?: MessageEmbed | MessageEmbedOptions;
disableMentions?: 'none' | 'all' | 'everyone'; disableMentions?: 'none' | 'all' | 'everyone';
@ -2838,7 +2842,7 @@ declare module 'discord.js' {
files?: (FileOptions | BufferResolvable | Stream | MessageAttachment)[]; files?: (FileOptions | BufferResolvable | Stream | MessageAttachment)[];
code?: string | boolean; code?: string | boolean;
split?: boolean | SplitOptions; split?: boolean | SplitOptions;
reply?: UserResolvable; replyTo?: MessageResolvable;
} }
type MessageReactionResolvable = MessageReaction | Snowflake; type MessageReactionResolvable = MessageReaction | Snowflake;
@ -2868,7 +2872,8 @@ declare module 'discord.js' {
| 'USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_3' | 'USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_3'
| 'CHANNEL_FOLLOW_ADD' | 'CHANNEL_FOLLOW_ADD'
| 'GUILD_DISCOVERY_DISQUALIFIED' | 'GUILD_DISCOVERY_DISQUALIFIED'
| 'GUILD_DISCOVERY_REQUALIFIED'; | 'GUILD_DISCOVERY_REQUALIFIED'
| 'REPLY';
interface OverwriteData { interface OverwriteData {
allow?: PermissionResolvable; allow?: PermissionResolvable;
@ -3055,11 +3060,9 @@ declare module 'discord.js' {
type PartialTypes = 'USER' | 'CHANNEL' | 'GUILD_MEMBER' | 'MESSAGE' | 'REACTION'; type PartialTypes = 'USER' | 'CHANNEL' | 'GUILD_MEMBER' | 'MESSAGE' | 'REACTION';
interface PartialUser interface PartialUser extends Omit<Partialize<User, 'bot' | 'flags' | 'system' | 'tag' | 'username'>, 'deleted'> {
extends Omit<Partialize<User, 'bot' | 'flags' | 'locale' | 'system' | 'tag' | 'username'>, 'deleted'> {
bot: User['bot']; bot: User['bot'];
flags: User['flags']; flags: User['flags'];
locale: User['locale'];
system: User['system']; system: User['system'];
readonly tag: null; readonly tag: null;
username: null; username: null;