Merge branch '11.1-dev' into stable

This commit is contained in:
Amish Shah
2017-09-03 18:13:53 +01:00
81 changed files with 1928 additions and 908 deletions

42
.gitignore vendored
View File

@@ -1,21 +1,21 @@
# Packages
node_modules/
yarn.lock
# Log files
logs/
*.log
# Authentication
test/auth.json
test/auth.js
docs/deploy/deploy_key
docs/deploy/deploy_key.pub
deploy/deploy_key
deploy/deploy_key.pub
# Miscellaneous
.tmp/
.vscode/
docs/docs.json
webpack/
# Packages
node_modules/
yarn.lock
# Log files
logs/
*.log
# Authentication
test/auth.json
test/auth.js
docs/deploy/deploy_key
docs/deploy/deploy_key.pub
deploy/deploy_key
deploy/deploy_key.pub
# Miscellaneous
.tmp/
.vscode/
docs/docs.json
webpack/

1
.npmrc Normal file
View File

@@ -0,0 +1 @@
package-json=false

View File

@@ -1,11 +1,21 @@
{
"ecmaVersion": 6,
"ecmaVersion": 7,
"libs": [],
"loadEagerly": [
"./src/*.js"
],
"dontLoad": [
"node_modules/**"
],
"plugins": {
"node": {
"dontLoad": "node_modules/**",
"load": "",
"modules": ""
"es_modules": {},
"node": {},
"doc_comment": {
"fullDocs": true,
"strong": true
},
"webpack": {
"configPath": "./webpack.config.js",
}
}
}

View File

@@ -1,16 +1,20 @@
language: node_js
node_js:
- "6"
- "7"
cache:
directories:
- node_modules
install: npm install
script:
- bash ./deploy/deploy.sh
env:
global:
- ENCRYPTION_LABEL: "af862fa96d3e"
- COMMIT_AUTHOR_EMAIL: "amishshah.2k@gmail.com"
dist: trusty
sudo: false
language: node_js
node_js:
- "6"
- "7"
cache:
directories:
- node_modules
install: npm install
script: bash ./deploy/test.sh
jobs:
include:
- stage: build
node_js: "6"
script: bash ./deploy/deploy.sh
env:
global:
- ENCRYPTION_LABEL: "af862fa96d3e"
- COMMIT_AUTHOR_EMAIL: "amishshah.2k@gmail.com"
dist: trusty
sudo: false

9
browser.js Normal file
View File

@@ -0,0 +1,9 @@
const browser = typeof window !== 'undefined';
const webpack = !!process.env.__DISCORD_WEBPACK__;
const Discord = require('./');
module.exports = Discord;
if (browser && webpack) window.Discord = Discord; // eslint-disable-line no-undef
// eslint-disable-next-line no-console
else if (!browser) console.warn('Warning: Attempting to use browser version of Discord.js in a non-browser environment!');

View File

@@ -3,15 +3,7 @@
set -e
function tests {
npm run lint
npm run docs:test
VERSIONED=false npm run webpack
exit 0
}
function build {
npm run lint
npm run docs
VERSIONED=false npm run webpack
}
@@ -22,10 +14,10 @@ if [[ "$TRAVIS_BRANCH" == revert-* ]]; then
exit 0
fi
# For PRs, only run tests
# For PRs, do nothing
if [ "$TRAVIS_PULL_REQUEST" != "false" ]; then
echo -e "\e[36m\e[1mBuild triggered for PR #${TRAVIS_PULL_REQUEST} to branch \"${TRAVIS_BRANCH}\" - only running tests."
tests
echo -e "\e[36m\e[1mBuild triggered for PR #${TRAVIS_PULL_REQUEST} to branch \"${TRAVIS_BRANCH}\" - doing nothing."
exit 0
fi
# Figure out the source of the build
@@ -39,10 +31,10 @@ else
SOURCE_TYPE="branch"
fi
# For Node != 6, only run tests
# For Node != 6, do nothing
if [ "$TRAVIS_NODE_VERSION" != "6" ]; then
echo -e "\e[36m\e[1mBuild triggered with Node v${TRAVIS_NODE_VERSION} - only running tests."
tests
echo -e "\e[36m\e[1mBuild triggered with Node v${TRAVIS_NODE_VERSION} - doing nothing."
exit 0
fi
build

34
deploy/test.sh Normal file
View File

@@ -0,0 +1,34 @@
#!/bin/bash
set -e
function tests {
npm run lint
npm run docs:test
exit 0
}
# For revert branches, do nothing
if [[ "$TRAVIS_BRANCH" == revert-* ]]; then
echo -e "\e[36m\e[1mTest triggered for reversion branch \"${TRAVIS_BRANCH}\" - doing nothing."
exit 0
fi
# For PRs
if [ "$TRAVIS_PULL_REQUEST" != "false" ]; then
echo -e "\e[36m\e[1mTest triggered for PR #${TRAVIS_PULL_REQUEST} to branch \"${TRAVIS_BRANCH}\" - only running tests."
tests
fi
# Figure out the source of the test
if [ -n "$TRAVIS_TAG" ]; then
echo -e "\e[36m\e[1mTest triggered for tag \"${TRAVIS_TAG}\"."
else
echo -e "\e[36m\e[1mTest triggered for branch \"${TRAVIS_BRANCH}\"."
fi
# For Node != 6
if [ "$TRAVIS_NODE_VERSION" != "6" ]; then
echo -e "\e[36m\e[1mTest triggered with Node v${TRAVIS_NODE_VERSION} - only running tests."
tests
fi

View File

@@ -1 +1 @@
## [View the documentation here.](https://discord.js.org/#/docs)
## [View the documentation here.](https://discord.js.org/#/docs)

View File

@@ -19,11 +19,7 @@ client.on('ready', () => {
// Create an event listener for new guild members
client.on('guildMemberAdd', member => {
// Send the message to the guilds default channel (usually #general), mentioning the member
member.guild.defaultChannel.send(`Welcome to the server, ${member}!`);
// If you want to send the message to a designated channel on a server instead
// you can do the following:
// Send the message to a designated channel on a server:
const channel = member.guild.channels.find('name', 'member-log');
// Do nothing if the channel wasn't found on this server
if (!channel) return;

View File

@@ -1,3 +1,7 @@
# Version 11.2.0
v11.2.0 fixes a lot of bugs we encountered along the 11.1.0 release, as well as support for new features such as Message Attachments and UserGuildSettings.
See [the changelog](https://github.com/hydrabolt/discord.js/releases/tag/11.2.0) for a full list of changes, including information about deprecations.
# Version 11.1.0
v11.1.0 features improved voice and gateway stability, as well as support for new features such as audit logs and searching for messages.
See [the changelog](https://github.com/hydrabolt/discord.js/releases/tag/11.1.0) for a full list of changes, including
@@ -118,9 +122,9 @@ The guild parameter that has been dropped from the guild-related events can stil
## Dates and timestamps
All dates/timestamps on the structures have been refactored to have a consistent naming scheme and availability.
All of them are named similarly to this:
**Date:** `Message.createdAt`
**Timestamp:** `Message.createdTimestamp`
All of them are named similarly to this:
**Date:** `Message.createdAt`
**Timestamp:** `Message.createdTimestamp`
See the docs for each structure to see which date/timestamps are available on them.
@@ -149,7 +153,7 @@ A couple more important details:
* `Client.servers.length` ==> `client.guilds.size` (all instances of `server` are now `guild`)
## No more callbacks!
Version 9 eschews callbacks in favour of Promises. This means all code relying on callbacks must be changed.
Version 9 eschews callbacks in favour of Promises. This means all code relying on callbacks must be changed.
For example, the following code:
```js

View File

@@ -17,8 +17,8 @@
</div>
# Welcome!
Welcome to the discord.js v11.1.0 documentation.
v11.1.0 features improved voice and gateway stability, as well as support for new features such as audit logs and searching for messages.
Welcome to the discord.js v11.2.0 documentation.
v11.2.0 fixes a lot of bugs we encountered along the 11.1.0 release, as well as support for new features such as Message Attachments and UserGuildSettings.
## About
discord.js is a powerful [node.js](https://nodejs.org) module that allows you to interact with the
@@ -30,11 +30,11 @@ discord.js is a powerful [node.js](https://nodejs.org) module that allows you to
- 100% coverage of the Discord API
## Installation
**Node.js 6.0.0 or newer is required.**
**Node.js 6.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 --save`
With voice support ([node-opus](https://www.npmjs.com/package/node-opus)): `npm install discord.js node-opus --save`
Without voice support: `npm install discord.js --save`
With voice support ([node-opus](https://www.npmjs.com/package/node-opus)): `npm install discord.js node-opus --save`
With voice support ([opusscript](https://www.npmjs.com/package/opusscript)): `npm install discord.js opusscript --save`
### Audio engines
@@ -79,8 +79,8 @@ client.login('your token');
## Contributing
Before creating an issue, please ensure that it hasn't already been reported/suggested, and double-check the
[documentation](https://discord.js.org/#/docs).
See [the contribution guide](https://github.com/hydrabolt/discord.js/blob/master/CONTRIBUTING.md) if you'd like to submit a PR.
[documentation](https://discord.js.org/#/docs).
See [the contribution guide](https://github.com/hydrabolt/discord.js/blob/master/.github/CONTRIBUTING.md) if you'd like to submit a PR.
## Help
If you don't understand something in the documentation, you are experiencing problems, or you just need a gentle

View File

@@ -4,7 +4,7 @@ Voice in discord.js can be used for many things, such as music bots, recording o
In discord.js, you can use voice by connecting to a `VoiceChannel` to obtain a `VoiceConnection`, where you can start streaming and receiving audio.
To get started, make sure you have:
* ffmpeg - `npm install --global ffmpeg-binaries`
* ffmpeg - `npm install ffmpeg-binaries`
* an opus encoder, choose one from below:
* `npm install opusscript`
* `npm install node-opus`

View File

@@ -30,7 +30,7 @@ The usage of the API isn't any different from using it in Node.js.
client.on('message', msg => {
const guildTag = msg.channel.type === 'text' ? `[${msg.guild.name}]` : '[DM]';
const channelTag = msg.channel.type === 'text' ? `[#${msg.channel.name}]` : '';
console.log(`${guildTag}${channelTag} ${msg.author.username}#${msg.author.discriminator}: ${msg.content}`);
console.log(`${guildTag}${channelTag} ${msg.author.tag}: ${msg.content}`);
});
client.login('some crazy token');

View File

@@ -1,6 +1,6 @@
{
"name": "discord.js",
"version": "11.1.0",
"version": "11.2.0",
"description": "A powerful library for interacting with the Discord API",
"main": "./src/index",
"types": "./typings/index.d.ts",
@@ -34,26 +34,26 @@
"dependencies": {
"long": "^3.2.0",
"prism-media": "^0.0.1",
"snekfetch": "^3.1.0",
"tweetnacl": "^0.14.0",
"ws": "^2.0.0"
"snekfetch": "^3.3.0",
"tweetnacl": "^1.0.0",
"ws": "^3.1.0"
},
"peerDependencies": {
"bufferutil": "^3.0.0",
"bufferutil": "^3.0.2",
"erlpack": "hammerandchisel/erlpack",
"node-opus": "^0.2.5",
"node-opus": "^0.2.6",
"opusscript": "^0.0.3",
"sodium": "^2.0.1",
"libsodium-wrappers": "^0.5.1",
"uws": "^0.14.1"
"libsodium-wrappers": "^0.5.4",
"uws": "^0.14.5"
},
"devDependencies": {
"@types/node": "^7.0.0",
"@types/node": "^7.0.43",
"discord.js-docgen": "hydrabolt/discord.js-docgen",
"eslint": "^3.19.0",
"parallel-webpack": "^1.6.0",
"uglify-js": "mishoo/UglifyJS2#harmony",
"webpack": "^2.2.0"
"eslint": "^4.6.0",
"parallel-webpack": "^2.1.0",
"uglifyjs-webpack-plugin": "^1.0.0-beta.1",
"webpack": "^3.5.5"
},
"engines": {
"node": ">=6.0.0"

View File

@@ -1,5 +1,4 @@
const os = require('os');
const EventEmitter = require('events').EventEmitter;
const EventEmitter = require('events');
const Constants = require('../util/Constants');
const Permissions = require('../util/Permissions');
const Util = require('../util/Util');
@@ -120,6 +119,7 @@ class Client extends EventEmitter {
*/
this.presences = new Collection();
Object.defineProperty(this, 'token', { writable: true });
if (!this.token && 'CLIENT_TOKEN' in process.env) {
/**
* Authorization token for the logged in user/bot
@@ -249,7 +249,7 @@ class Client extends EventEmitter {
* @readonly
*/
get browser() {
return os.platform() === 'browser';
return typeof window !== 'undefined';
}
/**
@@ -419,9 +419,9 @@ class Client extends EventEmitter {
*/
setTimeout(fn, delay, ...args) {
const timeout = setTimeout(() => {
fn();
fn(...args);
this._timeouts.delete(timeout);
}, delay, ...args);
}, delay);
this._timeouts.add(timeout);
return timeout;
}

View File

@@ -28,7 +28,7 @@ class ClientDataResolver {
/**
* Data that resolves to give a User object. This can be:
* * A User object
* * A user ID
* * A Snowflake
* * A Message object (resolves to the message author)
* * A Guild object (owner of the guild)
* * A GuildMember object
@@ -65,7 +65,7 @@ class ClientDataResolver {
/**
* Data that resolves to give a Guild object. This can be:
* * A Guild object
* * A Guild ID
* * A Snowflake
* @typedef {Guild|Snowflake} GuildResolvable
*/
@@ -106,7 +106,7 @@ class ClientDataResolver {
* * A Channel object
* * A Message object (the channel the message was sent in)
* * A Guild object (the #general channel)
* * A channel ID
* * A Snowflake
* @typedef {Channel|Guild|Message|Snowflake} ChannelResolvable
*/
@@ -174,6 +174,20 @@ class ClientDataResolver {
return String(data);
}
/**
* Resolves a Base64Resolvable, a string, or a BufferResolvable to a Base 64 image.
* @param {BufferResolvable|Base64Resolvable} image The image to be resolved
* @returns {Promise<?string>}
*/
resolveImage(image) {
if (!image) return Promise.resolve(null);
if (typeof image === 'string' && image.startsWith('data:')) {
return Promise.resolve(image);
}
return this.resolveFile(image).then(this.resolveBase64);
}
/**
* Data that resolves to give a Base64 string, typically for image uploading. This can be:
* * A Buffer
@@ -192,19 +206,25 @@ class ClientDataResolver {
}
/**
* Data that can be resolved to give a Buffer. This can be:
* * A Buffer
* * The path to a local file
* * A URL
* @typedef {string|Buffer} BufferResolvable
*/
* Data that can be resolved to give a Buffer. This can be:
* * A Buffer
* * The path to a local file
* * A URL
* * A Stream
* @typedef {string|Buffer} BufferResolvable
*/
/**
* Resolves a BufferResolvable to a Buffer.
* @param {BufferResolvable} resource The buffer resolvable to resolve
* @returns {Promise<Buffer>}
*/
resolveBuffer(resource) {
* @external Stream
* @see {@link https://nodejs.org/api/stream.html}
*/
/**
* Resolves a BufferResolvable to a Buffer.
* @param {BufferResolvable|Stream} resource The buffer or stream resolvable to resolve
* @returns {Promise<Buffer>}
*/
resolveFile(resource) {
if (resource instanceof Buffer) return Promise.resolve(resource);
if (this.client.browser && resource instanceof ArrayBuffer) return Promise.resolve(convertToBuffer(resource));
@@ -212,11 +232,11 @@ class ClientDataResolver {
return new Promise((resolve, reject) => {
if (/^https?:\/\//.test(resource)) {
snekfetch.get(resource)
.end((err, res) => {
if (err) return reject(err);
if (!(res.body instanceof Buffer)) return reject(new TypeError('The response body isn\'t a Buffer.'));
return resolve(res.body);
});
.end((err, res) => {
if (err) return reject(err);
if (!(res.body instanceof Buffer)) return reject(new TypeError('The response body isn\'t a Buffer.'));
return resolve(res.body);
});
} else {
const file = path.resolve(resource);
fs.stat(file, (err, stats) => {
@@ -229,6 +249,13 @@ class ClientDataResolver {
});
}
});
} else if (resource.pipe && typeof resource.pipe === 'function') {
return new Promise((resolve, reject) => {
const buffers = [];
resource.once('error', reject);
resource.on('data', data => buffers.push(data));
resource.once('end', () => resolve(Buffer.concat(buffers)));
});
}
return Promise.reject(new TypeError('The resource must be a string or Buffer.'));

View File

@@ -43,7 +43,7 @@ class ClientManager {
const gateway = `${res.url}/?v=${protocolVersion}&encoding=${WebSocketConnection.ENCODING}`;
this.client.emit(Constants.Events.DEBUG, `Using gateway ${gateway}`);
this.client.ws.connect(gateway);
this.client.ws.once('close', event => {
this.client.ws.connection.once('close', event => {
if (event.code === 4004) reject(new Error(Constants.Errors.BAD_LOGIN));
if (event.code === 4010) reject(new Error(Constants.Errors.INVALID_SHARD));
if (event.code === 4011) reject(new Error(Constants.Errors.SHARDING_REQUIRED));

View File

@@ -65,9 +65,9 @@ class WebhookClient extends Webhook {
*/
setTimeout(fn, delay, ...args) {
const timeout = setTimeout(() => {
fn();
fn(...args);
this._timeouts.delete(timeout);
}, delay, ...args);
}, delay);
this._timeouts.add(timeout);
return timeout;
}

View File

@@ -2,7 +2,7 @@ const snekfetch = require('snekfetch');
const Constants = require('../../util/Constants');
class APIRequest {
constructor(rest, method, path, auth, data, files) {
constructor(rest, method, path, auth, data, files, reason) {
this.rest = rest;
this.client = rest.client;
this.method = method;
@@ -11,6 +11,7 @@ class APIRequest {
this.data = data;
this.files = files;
this.route = this.getRoute(this.path);
this.reason = reason;
}
getRoute(url) {
@@ -36,6 +37,7 @@ class APIRequest {
const API = `${this.client.options.http.host}/api/v${this.client.options.http.version}`;
const request = snekfetch[this.method](`${API}${this.path}`);
if (this.auth) request.set('Authorization', this.getAuth());
if (this.reason) request.set('X-Audit-Log-Reason', encodeURIComponent(this.reason));
if (!this.rest.client.browser) request.set('User-Agent', this.rest.userAgentManager.userAgent);
if (this.files) {
for (const file of this.files) if (file && file.file) request.attach(file.name, file.file, file.name);

View File

@@ -1,12 +1,19 @@
/**
* Represents an error from the Discord API.
* @extends Error
*/
class DiscordAPIError extends Error {
constructor(error) {
constructor(path, error) {
super();
const flattened = error.errors ? `\n${this.constructor.flattenErrors(error.errors).join('\n')}` : '';
const flattened = this.constructor.flattenErrors(error.errors || error).join('\n');
this.name = 'DiscordAPIError';
this.message = `${error.message}${flattened}`;
this.message = error.message && flattened ? `${error.message}\n${flattened}` : error.message || flattened;
/**
* The path of the request relative to the HTTP endpoint
* @type {string}
*/
this.path = path;
/**
* HTTP error code returned by Discord
@@ -18,15 +25,23 @@ class DiscordAPIError extends Error {
/**
* Flattens an errors object returned from the API into an array.
* @param {Object} obj Discord errors object
* @param {string} [key] idklol
* @param {string} [key] Used internally to determine key names of nested fields
* @returns {string[]}
* @private
*/
static flattenErrors(obj, key = '') {
let messages = [];
for (const k of Object.keys(obj)) {
if (k === 'message') continue;
const newKey = key ? isNaN(k) ? `${key}.${k}` : `${key}[${k}]` : k;
if (obj[k]._errors) {
messages.push(`${newKey}: ${obj[k]._errors.map(e => e.message).join(' ')}`);
} else if (obj[k].code || obj[k].message) {
messages.push(`${obj[k].code ? `${obj[k].code}: ` : ''}: ${obj[k].message}`.trim());
} else if (typeof obj[k] === 'string') {
messages.push(obj[k]);
} else {
messages = messages.concat(this.flattenErrors(obj[k], newKey));
}

View File

@@ -42,8 +42,8 @@ class RESTManager {
}
}
makeRequest(method, url, auth, data, file) {
const apiRequest = new APIRequest(this, method, url, auth, data, file);
makeRequest(method, url, auth, data, file, reason) {
const apiRequest = new APIRequest(this, method, url, auth, data, file, reason);
if (!this.handlers[apiRequest.route]) {
const RequestHandlerType = this.getRequestHandler();
this.handlers[apiRequest.route] = new RequestHandlerType(this, apiRequest.route);

View File

@@ -102,12 +102,12 @@ class RESTMethods {
if (content instanceof Array) {
const messages = [];
(function sendChunk(list, index) {
const options = index === list.length ? { tts, embed } : { tts };
chan.send(list[index], options, index === list.length ? files : null).then(message => {
const options = index === list.length - 1 ? { tts, embed, files } : { tts };
chan.send(list[index], options).then(message => {
messages.push(message);
if (index >= list.length - 1) return resolve(messages);
return sendChunk(list, ++index);
});
}).catch(reject);
}(content, 0));
} else {
this.rest.makeRequest('post', Endpoints.Channel(chan).messages, true, {
@@ -227,6 +227,7 @@ class RESTMethods {
embed_type: options.embedType,
attachment_filename: options.attachmentFilename,
attachment_extension: options.attachmentExtension,
include_nsfw: options.nsfw,
};
for (const key in options) if (options[key] === undefined) delete options[key];
@@ -251,13 +252,13 @@ class RESTMethods {
});
}
createChannel(guild, channelName, channelType, overwrites) {
createChannel(guild, channelName, channelType, overwrites, reason) {
if (overwrites instanceof Collection) overwrites = overwrites.array();
return this.rest.makeRequest('post', Endpoints.Guild(guild).channels, true, {
name: channelName,
type: channelType,
permission_overwrites: overwrites,
}).then(data => this.client.actions.ChannelCreate.handle(data).channel);
}, undefined, reason).then(data => this.client.actions.ChannelCreate.handle(data).channel);
}
createDM(recipient) {
@@ -284,29 +285,42 @@ class RESTMethods {
.then(() => channel);
}
removeUserFromGroupDM(channel, userId) {
return this.rest.makeRequest('delete', Endpoints.Channel(channel).Recipient(userId), true)
.then(() => channel);
}
updateGroupDMChannel(channel, _data) {
const data = {};
data.name = _data.name;
data.icon = _data.icon;
return this.rest.makeRequest('patch', Endpoints.Channel(channel), true, data).then(() => channel);
}
getExistingDM(recipient) {
return this.client.channels.find(channel =>
channel.recipient && channel.recipient.id === recipient.id
);
}
deleteChannel(channel) {
deleteChannel(channel, reason) {
if (channel instanceof User || channel instanceof GuildMember) channel = this.getExistingDM(channel);
if (!channel) return Promise.reject(new Error('No channel to delete.'));
return this.rest.makeRequest('delete', Endpoints.Channel(channel), true).then(data => {
data.id = channel.id;
return this.client.actions.ChannelDelete.handle(data).channel;
});
return this.rest.makeRequest('delete', Endpoints.Channel(channel), true, undefined, undefined, reason)
.then(data => {
data.id = channel.id;
return this.client.actions.ChannelDelete.handle(data).channel;
});
}
updateChannel(channel, _data) {
updateChannel(channel, _data, reason) {
const data = {};
data.name = (_data.name || channel.name).trim();
data.topic = _data.topic || channel.topic;
data.position = _data.position || channel.position;
data.bitrate = _data.bitrate || channel.bitrate;
data.user_limit = _data.userLimit || channel.userLimit;
return this.rest.makeRequest('patch', Endpoints.Channel(channel), true, data).then(newData =>
return this.rest.makeRequest('patch', Endpoints.Channel(channel), true, data, undefined, reason).then(newData =>
this.client.actions.ChannelUpdate.handle(newData).updated
);
}
@@ -361,7 +375,7 @@ class RESTMethods {
const user = this.client.user;
const data = {};
data.username = _data.username || user.username;
data.avatar = this.client.resolver.resolveBase64(_data.avatar) || user.avatar;
data.avatar = typeof _data.avatar === 'undefined' ? user.avatar : this.client.resolver.resolveBase64(_data.avatar);
if (!user.bot) {
data.email = _data.email || user.email;
data.password = password;
@@ -372,58 +386,57 @@ class RESTMethods {
);
}
updateGuild(guild, _data) {
const data = {};
if (_data.name) data.name = _data.name;
if (_data.region) data.region = _data.region;
if (_data.verificationLevel) data.verification_level = Number(_data.verificationLevel);
if (_data.afkChannel) data.afk_channel_id = this.client.resolver.resolveChannel(_data.afkChannel).id;
if (_data.afkTimeout) data.afk_timeout = Number(_data.afkTimeout);
if (_data.icon) data.icon = this.client.resolver.resolveBase64(_data.icon);
if (_data.owner) data.owner_id = this.client.resolver.resolveUser(_data.owner).id;
if (_data.splash) data.splash = this.client.resolver.resolveBase64(_data.splash);
return this.rest.makeRequest('patch', Endpoints.Guild(guild), true, data).then(newData =>
updateGuild(guild, data, reason) {
return this.rest.makeRequest('patch', Endpoints.Guild(guild), true, data, undefined, reason).then(newData =>
this.client.actions.GuildUpdate.handle(newData).updated
);
}
kickGuildMember(guild, member, reason) {
const url = `${Endpoints.Guild(guild).Member(member)}?reason=${reason}`;
return this.rest.makeRequest('delete', url, true).then(() =>
this.client.actions.GuildMemberRemove.handle({
guild_id: guild.id,
user: member.user,
}).member
);
return this.rest.makeRequest(
'delete', Endpoints.Guild(guild).Member(member), true,
undefined, undefined, reason)
.then(() =>
this.client.actions.GuildMemberRemove.handle({
guild_id: guild.id,
user: member.user,
}).member
);
}
createGuildRole(guild, data) {
createGuildRole(guild, data, reason) {
if (data.color) data.color = this.client.resolver.resolveColor(data.color);
if (data.permissions) data.permissions = Permissions.resolve(data.permissions);
return this.rest.makeRequest('post', Endpoints.Guild(guild).roles, true, data).then(role =>
this.client.actions.GuildRoleCreate.handle({
return this.rest.makeRequest('post', Endpoints.Guild(guild).roles, true, data, undefined, reason).then(r => {
const { role } = this.client.actions.GuildRoleCreate.handle({
guild_id: guild.id,
role,
}).role
);
role: r,
});
if (data.position) return role.setPosition(data.position, reason);
return role;
});
}
deleteGuildRole(role) {
return this.rest.makeRequest('delete', Endpoints.Guild(role.guild).Role(role.id), true).then(() =>
this.client.actions.GuildRoleDelete.handle({
guild_id: role.guild.id,
role_id: role.id,
}).role
);
deleteGuildRole(role, reason) {
return this.rest.makeRequest(
'delete', Endpoints.Guild(role.guild).Role(role.id), true,
undefined, undefined, reason)
.then(() =>
this.client.actions.GuildRoleDelete.handle({
guild_id: role.guild.id,
role_id: role.id,
}).role
);
}
setChannelOverwrite(channel, payload) {
return this.rest.makeRequest('put', `${Endpoints.Channel(channel).permissions}/${payload.id}`, true, payload);
}
deletePermissionOverwrites(overwrite) {
deletePermissionOverwrites(overwrite, reason) {
return this.rest.makeRequest(
'delete', `${Endpoints.Channel(overwrite.channel).permissions}/${overwrite.id}`, true
'delete', `${Endpoints.Channel(overwrite.channel).permissions}/${overwrite.id}`,
true, undefined, undefined, reason
).then(() => overwrite);
}
@@ -464,8 +477,11 @@ class RESTMethods {
});
}
updateGuildMember(member, data) {
if (data.channel) data.channel_id = this.client.resolver.resolveChannel(data.channel).id;
updateGuildMember(member, data, reason) {
if (data.channel) {
data.channel_id = this.client.resolver.resolveChannel(data.channel).id;
data.channel = null;
}
if (data.roles) data.roles = data.roles.map(role => role instanceof Role ? role.id : role);
let endpoint = Endpoints.Member(member);
@@ -477,12 +493,12 @@ class RESTMethods {
}
}
return this.rest.makeRequest('patch', endpoint, true, data).then(newData =>
return this.rest.makeRequest('patch', endpoint, true, data, undefined, reason).then(newData =>
member.guild._updateMember(member, newData).mem
);
}
addMemberRole(member, role) {
addMemberRole(member, role, reason) {
return new Promise((resolve, reject) => {
if (member._roles.includes(role.id)) return resolve(member);
@@ -497,15 +513,16 @@ class RESTMethods {
const timeout = this.client.setTimeout(() =>
this.client.removeListener(Constants.Events.GUILD_MEMBER_UPDATE, listener), 10e3);
return this.rest.makeRequest('put', Endpoints.Member(member).Role(role.id), true).catch(err => {
this.client.removeListener(Constants.Events.GUILD_BAN_REMOVE, listener);
this.client.clearTimeout(timeout);
reject(err);
});
return this.rest.makeRequest('put', Endpoints.Member(member).Role(role.id), true, undefined, undefined, reason)
.catch(err => {
this.client.removeListener(Constants.Events.GUILD_BAN_REMOVE, listener);
this.client.clearTimeout(timeout);
reject(err);
});
});
}
removeMemberRole(member, role) {
removeMemberRole(member, role, reason) {
return new Promise((resolve, reject) => {
if (!member._roles.includes(role.id)) return resolve(member);
@@ -520,11 +537,12 @@ class RESTMethods {
const timeout = this.client.setTimeout(() =>
this.client.removeListener(Constants.Events.GUILD_MEMBER_UPDATE, listener), 10e3);
return this.rest.makeRequest('delete', Endpoints.Member(member).Role(role.id), true).catch(err => {
this.client.removeListener(Constants.Events.GUILD_BAN_REMOVE, listener);
this.client.clearTimeout(timeout);
reject(err);
});
return this.rest.makeRequest('delete', Endpoints.Member(member).Role(role.id), true, undefined, undefined, reason)
.catch(err => {
this.client.removeListener(Constants.Events.GUILD_BAN_REMOVE, listener);
this.client.clearTimeout(timeout);
reject(err);
});
});
}
@@ -548,7 +566,7 @@ class RESTMethods {
});
}
unbanGuildMember(guild, member) {
unbanGuildMember(guild, member, reason) {
return new Promise((resolve, reject) => {
const id = this.client.resolver.resolveUserID(member);
if (!id) throw new Error('Couldn\'t resolve the user ID to unban.');
@@ -567,11 +585,12 @@ class RESTMethods {
reject(new Error('Took too long to receive the ban remove event.'));
}, 10000);
this.rest.makeRequest('delete', `${Endpoints.Guild(guild).bans}/${id}`, true).catch(err => {
this.client.removeListener(Constants.Events.GUILD_BAN_REMOVE, listener);
this.client.clearTimeout(timeout);
reject(err);
});
this.rest.makeRequest('delete', `${Endpoints.Guild(guild).bans}/${id}`, true, undefined, undefined, reason)
.catch(err => {
this.client.removeListener(Constants.Events.GUILD_BAN_REMOVE, listener);
this.client.clearTimeout(timeout);
reject(err);
});
});
}
@@ -587,7 +606,7 @@ class RESTMethods {
);
}
updateGuildRole(role, _data) {
updateGuildRole(role, _data, reason) {
const data = {};
data.name = _data.name || role.name;
data.position = typeof _data.position !== 'undefined' ? _data.position : role.position;
@@ -598,12 +617,13 @@ class RESTMethods {
if (_data.permissions) data.permissions = Permissions.resolve(_data.permissions);
else data.permissions = role.permissions;
return this.rest.makeRequest('patch', Endpoints.Guild(role.guild).Role(role.id), true, data).then(_role =>
this.client.actions.GuildRoleUpdate.handle({
role: _role,
guild_id: role.guild.id,
}).updated
);
return this.rest.makeRequest('patch', Endpoints.Guild(role.guild).Role(role.id), true, data, undefined, reason)
.then(_role =>
this.client.actions.GuildRoleUpdate.handle({
role: _role,
guild_id: role.guild.id,
}).updated
);
}
pinMessage(message) {
@@ -620,17 +640,19 @@ class RESTMethods {
return this.rest.makeRequest('get', Endpoints.Channel(channel).pins, true);
}
createChannelInvite(channel, options) {
createChannelInvite(channel, options, reason) {
const payload = {};
payload.temporary = options.temporary;
payload.max_age = options.maxAge;
payload.max_uses = options.maxUses;
return this.rest.makeRequest('post', Endpoints.Channel(channel).invites, true, payload)
payload.unique = options.unique;
return this.rest.makeRequest('post', Endpoints.Channel(channel).invites, true, payload, undefined, reason)
.then(invite => new Invite(this.client, invite));
}
deleteInvite(invite) {
return this.rest.makeRequest('delete', Endpoints.Invite(invite.code), true).then(() => invite);
deleteInvite(invite, reason) {
return this.rest.makeRequest('delete', Endpoints.Invite(invite.code), true, undefined, undefined, reason)
.then(() => invite);
}
getInvite(code) {
@@ -650,28 +672,31 @@ class RESTMethods {
});
}
pruneGuildMembers(guild, days, dry) {
return this.rest.makeRequest(dry ? 'get' : 'post', `${Endpoints.Guild(guild).prune}?days=${days}`, true)
pruneGuildMembers(guild, days, dry, reason) {
return this.rest.makeRequest(dry ?
'get' :
'post',
`${Endpoints.Guild(guild).prune}?days=${days}`, true, undefined, undefined, reason)
.then(data => data.pruned);
}
createEmoji(guild, image, name, roles) {
createEmoji(guild, image, name, roles, reason) {
const data = { image, name };
if (roles) data.roles = roles.map(r => r.id ? r.id : r);
return this.rest.makeRequest('post', Endpoints.Guild(guild).emojis, true, data)
return this.rest.makeRequest('post', Endpoints.Guild(guild).emojis, true, data, undefined, reason)
.then(emoji => this.client.actions.GuildEmojiCreate.handle(guild, emoji).emoji);
}
updateEmoji(emoji, _data) {
updateEmoji(emoji, _data, reason) {
const data = {};
if (_data.name) data.name = _data.name;
if (_data.roles) data.roles = _data.roles.map(r => r.id ? r.id : r);
return this.rest.makeRequest('patch', Endpoints.Guild(emoji.guild).Emoji(emoji.id), true, data)
return this.rest.makeRequest('patch', Endpoints.Guild(emoji.guild).Emoji(emoji.id), true, data, undefined, reason)
.then(newEmoji => this.client.actions.GuildEmojiUpdate.handle(emoji, newEmoji).emoji);
}
deleteEmoji(emoji) {
return this.rest.makeRequest('delete', Endpoints.Guild(emoji.guild).Emoji(emoji.id), true)
deleteEmoji(emoji, reason) {
return this.rest.makeRequest('delete', Endpoints.Guild(emoji.guild).Emoji(emoji.id), true, undefined, reason)
.then(() => this.client.actions.GuildEmojiDelete.handle(emoji).data);
}
@@ -714,8 +739,8 @@ class RESTMethods {
});
}
createWebhook(channel, name, avatar) {
return this.rest.makeRequest('post', Endpoints.Channel(channel).webhooks, true, { name, avatar })
createWebhook(channel, name, avatar, reason) {
return this.rest.makeRequest('post', Endpoints.Channel(channel).webhooks, true, { name, avatar }, undefined, reason)
.then(data => new Webhook(this.client, data));
}
@@ -730,25 +755,36 @@ class RESTMethods {
});
}
deleteWebhook(webhook) {
return this.rest.makeRequest('delete', Endpoints.Webhook(webhook.id, webhook.token), false);
deleteWebhook(webhook, reason) {
return this.rest.makeRequest(
'delete', Endpoints.Webhook(webhook.id, webhook.token),
false, undefined, undefined, reason);
}
sendWebhookMessage(webhook, content, { avatarURL, tts, disableEveryone, embeds, username } = {}, file = null) {
username = username || webhook.name;
if (typeof content !== 'undefined') content = this.client.resolver.resolveString(content);
if (content) {
if (disableEveryone || (typeof disableEveryone === 'undefined' && this.client.options.disableEveryone)) {
content = content.replace(/@(everyone|here)/g, '@\u200b$1');
sendWebhookMessage(webhook, content, { avatarURL, tts, embeds, username } = {}, files = null) {
return new Promise((resolve, reject) => {
username = username || webhook.name;
if (content instanceof Array) {
const messages = [];
(function sendChunk(list, index) {
const options = index === list.length - 1 ? { tts, embeds, files } : { tts };
webhook.send(list[index], options).then(message => {
messages.push(message);
if (index >= list.length - 1) return resolve(messages);
return sendChunk(list, ++index);
}).catch(reject);
}(content, 0));
} else {
this.rest.makeRequest('post', `${Endpoints.Webhook(webhook.id, webhook.token)}?wait=true`, false, {
username,
avatar_url: avatarURL,
content,
tts,
embeds,
}, files).then(resolve, reject);
}
}
return this.rest.makeRequest('post', `${Endpoints.Webhook(webhook.id, webhook.token)}?wait=true`, false, {
username,
avatar_url: avatarURL,
content,
tts,
embeds,
}, file);
});
}
sendSlackWebhookMessage(webhook, body) {
@@ -763,12 +799,13 @@ class RESTMethods {
);
}
fetchMeMentions(options) {
if (options.guild) options.guild = options.guild.id ? options.guild.id : options.guild;
fetchMentions(options) {
if (options.guild instanceof Guild) options.guild = options.guild.id;
Util.mergeDefault({ limit: 25, roles: true, everyone: true, guild: null }, options);
return this.rest.makeRequest(
'get',
Endpoints.User('@me').mentions(options.limit, options.roles, options.everyone, options.guild)
).then(res => res.body.map(m => new Message(this.client.channels.get(m.channel_id), m, this.client)));
'get', Endpoints.User('@me').Mentions(options.limit, options.roles, options.everyone, options.guild), true
).then(data => data.map(m => new Message(this.client.channels.get(m.channel_id), m, this.client)));
}
addFriend(user) {
@@ -833,7 +870,7 @@ class RESTMethods {
'put', Endpoints.Message(message).Reaction(emoji).User('@me'), true
).then(() =>
message._addReaction(Util.parseEmoji(emoji), message.client.user)
);
);
}
removeMessageReaction(message, emoji, userID) {
@@ -864,7 +901,8 @@ class RESTMethods {
}
resetApplication(id) {
return this.rest.makeRequest('post', Endpoints.OAUTH2.Application(id).reset, true)
return this.rest.makeRequest('post', Endpoints.OAUTH2.Application(id).resetToken, true)
.then(() => this.rest.makeRequest('post', Endpoints.OAUTH2.Application(id).resetSecret, true))
.then(app => new OAuth2Application(this.client, app));
}
@@ -894,6 +932,10 @@ class RESTMethods {
patchUserSettings(data) {
return this.rest.makeRequest('patch', Constants.Endpoints.User('@me').settings, true, data);
}
patchClientUserGuildSettings(guildID, data) {
return this.rest.makeRequest('patch', Constants.Endpoints.User('@me').Guild(guildID).settings, true, data);
}
}
module.exports = RESTMethods;

View File

@@ -40,8 +40,14 @@ class BurstRequestHandler extends RequestHandler {
this.handle();
this.resetTimeout = null;
}, Number(res.headers['retry-after']) + this.client.options.restTimeOffset);
} else if (err.status >= 500 && err.status < 600) {
this.queue.unshift(item);
this.resetTimeout = this.client.setTimeout(() => {
this.handle();
this.resetTimeout = null;
}, 1e3 + this.client.options.restTimeOffset);
} else {
item.reject(err.status === 400 ? new DiscordAPIError(res.body) : err);
item.reject(err.status >= 400 && err.status < 500 ? new DiscordAPIError(res.request.path, res.body) : err);
this.handle();
}
} else {

View File

@@ -64,8 +64,11 @@ class SequentialRequestHandler extends RequestHandler {
resolve();
}, Number(res.headers['retry-after']) + this.restManager.client.options.restTimeOffset);
if (res.headers['x-ratelimit-global']) this.globalLimit = true;
} else if (err.status >= 500 && err.status < 600) {
this.queue.unshift(item);
this.restManager.client.setTimeout(resolve, 1e3 + this.client.options.restTimeOffset);
} else {
item.reject(err.status >= 400 && err.status < 500 ? new DiscordAPIError(res.body) : err);
item.reject(err.status >= 400 && err.status < 500 ? new DiscordAPIError(res.request.path, res.body) : err);
resolve(err);
}
} else {

View File

@@ -18,7 +18,7 @@ const ffmpegArguments = [
* ```js
* const broadcast = client.createVoiceBroadcast();
* broadcast.playFile('./music.mp3');
* // play "music.mp3" in all voice connections that the client is in
* // Play "music.mp3" in all voice connections that the client is in
* for (const connection of client.voiceConnections.values()) {
* connection.playBroadcast(broadcast);
* }
@@ -136,15 +136,15 @@ class VoiceBroadcast extends VolumeInterface {
* const broadcast = client.createVoiceBroadcast();
*
* voiceChannel.join()
* .then(connection => {
* const stream = ytdl('https://www.youtube.com/watch?v=XAWgeLF9EVQ', { filter : 'audioonly' });
* broadcast.playStream(stream);
* const dispatcher = connection.playBroadcast(broadcast);
* })
* .catch(console.error);
* .then(connection => {
* const stream = ytdl('https://www.youtube.com/watch?v=XAWgeLF9EVQ', { filter : 'audioonly' });
* broadcast.playStream(stream);
* const dispatcher = connection.playBroadcast(broadcast);
* })
* .catch(console.error);
*/
playStream(stream, { seek = 0, volume = 1, passes = 1 } = {}) {
const options = { seek, volume, passes, stream };
playStream(stream, options = {}) {
this.setVolume(options.volume || 1);
return this._playTranscodable(stream, options);
}
@@ -158,25 +158,23 @@ class VoiceBroadcast extends VolumeInterface {
* const broadcast = client.createVoiceBroadcast();
*
* voiceChannel.join()
* .then(connection => {
* broadcast.playFile('C:/Users/Discord/Desktop/music.mp3');
* const dispatcher = connection.playBroadcast(broadcast);
* })
* .catch(console.error);
* .then(connection => {
* broadcast.playFile('C:/Users/Discord/Desktop/music.mp3');
* const dispatcher = connection.playBroadcast(broadcast);
* })
* .catch(console.error);
*/
playFile(file, { seek = 0, volume = 1, passes = 1 } = {}) {
const options = { seek, volume, passes };
playFile(file, options = {}) {
this.setVolume(options.volume || 1);
return this._playTranscodable(`file:${file}`, options);
}
_playTranscodable(media, options) {
OpusEncoders.guaranteeOpusEngine();
this.killCurrentTranscoder();
const transcoder = this.prism.transcode({
type: 'ffmpeg',
media,
ffmpegArguments: ffmpegArguments.concat(['-ss', String(options.seek)]),
ffmpegArguments: ffmpegArguments.concat(['-ss', String(options.seek || 0)]),
});
/**
* Emitted whenever an error occurs.
@@ -206,31 +204,28 @@ class VoiceBroadcast extends VolumeInterface {
}
/**
* Plays a stream of 16-bit signed stereo PCM at 48KHz.
* Plays a stream of 16-bit signed stereo PCM.
* @param {ReadableStream} stream The audio stream to play
* @param {StreamOptions} [options] Options for playing the stream
* @returns {VoiceBroadcast}
*/
playConvertedStream(stream, { seek = 0, volume = 1, passes = 1 } = {}) {
OpusEncoders.guaranteeOpusEngine();
playConvertedStream(stream, options = {}) {
this.killCurrentTranscoder();
const options = { seek, volume, passes, stream };
this.currentTranscoder = { options };
this.setVolume(options.volume || 1);
this.currentTranscoder = { options: { stream } };
stream.once('readable', () => this._startPlaying());
return this;
}
/**
* Plays an Opus encoded stream at 48KHz.
* Plays an Opus encoded stream.
* <warn>Note that inline volume is not compatible with this method.</warn>
* @param {ReadableStream} stream The Opus audio stream to play
* @param {StreamOptions} [options] Options for playing the stream
* @returns {StreamDispatcher}
*/
playOpusStream(stream, { seek = 0, passes = 1 } = {}) {
const options = { seek, passes, stream };
this.currentTranscoder = { options, opus: true };
playOpusStream(stream) {
this.currentTranscoder = { options: { stream }, opus: true };
stream.once('readable', () => this._startPlaying());
return this;
}
@@ -241,10 +236,9 @@ class VoiceBroadcast extends VolumeInterface {
* @param {StreamOptions} [options] Options for playing the stream
* @returns {VoiceBroadcast}
*/
playArbitraryInput(input, { seek = 0, volume = 1, passes = 1 } = {}) {
this.guaranteeOpusEngine();
const options = { seek, volume, passes, input };
playArbitraryInput(input, options = {}) {
this.setVolume(options.volume || 1);
options.input = input;
return this._playTranscodable(input, options);
}
@@ -272,10 +266,6 @@ class VoiceBroadcast extends VolumeInterface {
}
}
guaranteeOpusEngine() {
if (!this.opusEncoder) throw new Error('Couldn\'t find an Opus engine.');
}
_startPlaying() {
if (this.tickInterval) clearInterval(this.tickInterval);
// Old code?

View File

@@ -8,12 +8,13 @@ const EventEmitter = require('events').EventEmitter;
const Prism = require('prism-media');
/**
* Represents a connection to a voice channel in Discord.
* Represents a connection to a guild's voice server.
* ```js
* // Obtained using:
* voiceChannel.join().then(connection => {
* voiceChannel.join()
* .then(connection => {
*
* });
* });
* ```
* @extends {EventEmitter}
*/
@@ -132,6 +133,7 @@ class VoiceConnection extends EventEmitter {
*/
setSpeaking(value) {
if (this.speaking === value) return;
if (this.status !== Constants.VoiceStatus.CONNECTED) return;
this.speaking = value;
this.sockets.ws.sendPacket({
op: Constants.VoiceOPCodes.SPEAKING,
@@ -163,7 +165,7 @@ class VoiceConnection extends EventEmitter {
}
/**
* Set the token and endpoint required to connect to the the voice servers.
* Set the token and endpoint required to connect to the voice servers.
* @param {string} token The voice token
* @param {string} endpoint The voice endpoint
* @returns {void}
@@ -245,7 +247,6 @@ class VoiceConnection extends EventEmitter {
*/
authenticateFailed(reason) {
clearTimeout(this.connectTimeout);
this.status = Constants.VoiceStatus.DISCONNECTED;
if (this.status === Constants.VoiceStatus.AUTHENTICATING) {
/**
* Emitted when we fail to initiate a voice connection.
@@ -256,6 +257,7 @@ class VoiceConnection extends EventEmitter {
} else {
this.emit('error', new Error(reason));
}
this.status = Constants.VoiceStatus.DISCONNECTED;
}
/**
@@ -430,6 +432,8 @@ class VoiceConnection extends EventEmitter {
* @property {number} [seek=0] The time to seek to
* @property {number} [volume=1] The volume to play at
* @property {number} [passes=1] How many times to send the voice packet to reduce packet loss
* @property {number|string} [bitrate=48000] The bitrate (quality) of the audio.
* If set to 'auto', the voice channel's bitrate will be used
*/
/**
@@ -440,10 +444,10 @@ class VoiceConnection extends EventEmitter {
* @example
* // Play files natively
* voiceChannel.join()
* .then(connection => {
* const dispatcher = connection.playFile('C:/Users/Discord/Desktop/music.mp3');
* })
* .catch(console.error);
* .then(connection => {
* const dispatcher = connection.playFile('C:/Users/Discord/Desktop/music.mp3');
* })
* .catch(console.error);
*/
playFile(file, options) {
return this.player.playUnknownStream(`file:${file}`, options);
@@ -469,18 +473,18 @@ class VoiceConnection extends EventEmitter {
* const ytdl = require('ytdl-core');
* const streamOptions = { seek: 0, volume: 1 };
* voiceChannel.join()
* .then(connection => {
* const stream = ytdl('https://www.youtube.com/watch?v=XAWgeLF9EVQ', { filter : 'audioonly' });
* const dispatcher = connection.playStream(stream, streamOptions);
* })
* .catch(console.error);
* .then(connection => {
* const stream = ytdl('https://www.youtube.com/watch?v=XAWgeLF9EVQ', { filter : 'audioonly' });
* const dispatcher = connection.playStream(stream, streamOptions);
* })
* .catch(console.error);
*/
playStream(stream, options) {
return this.player.playUnknownStream(stream, options);
}
/**
* Plays a stream of 16-bit signed stereo PCM at 48KHz.
* Plays a stream of 16-bit signed stereo PCM.
* @param {ReadableStream} stream The audio stream to play
* @param {StreamOptions} [options] Options for playing the stream
* @returns {StreamDispatcher}
@@ -490,7 +494,7 @@ class VoiceConnection extends EventEmitter {
}
/**
* Plays an Opus encoded stream at 48KHz.
* Plays an Opus encoded stream.
* <warn>Note that inline volume is not compatible with this method.</warn>
* @param {ReadableStream} stream The Opus audio stream to play
* @param {StreamOptions} [options] Options for playing the stream
@@ -503,6 +507,7 @@ class VoiceConnection extends EventEmitter {
/**
* Plays a voice broadcast.
* @param {VoiceBroadcast} broadcast The broadcast to play
* @param {StreamOptions} [options] Options for playing the stream
* @returns {StreamDispatcher}
* @example
* // Play a broadcast
@@ -511,12 +516,13 @@ class VoiceConnection extends EventEmitter {
* .playFile('./test.mp3');
* const dispatcher = voiceConnection.playBroadcast(broadcast);
*/
playBroadcast(broadcast) {
return this.player.playBroadcast(broadcast);
playBroadcast(broadcast, options) {
return this.player.playBroadcast(broadcast, options);
}
/**
* Creates a VoiceReceiver so you can start listening to voice data. It's recommended to only create one of these.
* Creates a VoiceReceiver so you can start listening to voice data.
* It's recommended to only create one of these.
* @returns {VoiceReceiver}
*/
createReceiver() {

View File

@@ -1,5 +1,6 @@
const VolumeInterface = require('../util/VolumeInterface');
const VoiceBroadcast = require('../VoiceBroadcast');
const Constants = require('../../../util/Constants');
const secretbox = require('../util/Secretbox');
@@ -88,12 +89,12 @@ class StreamDispatcher extends VolumeInterface {
}
/**
* Stops sending voice packets to the voice connection (stream may still progress however)
* Stops sending voice packets to the voice connection (stream may still progress however).
*/
pause() { this.setPaused(true); }
/**
* Resumes sending voice packets to the voice connection (may be further on in the stream than when paused)
* Resumes sending voice packets to the voice connection (may be further on in the stream than when paused).
*/
resume() { this.setPaused(false); }
@@ -108,6 +109,7 @@ class StreamDispatcher extends VolumeInterface {
setSpeaking(value) {
if (this.speaking === value) return;
if (this.player.voiceConnection.status !== Constants.VoiceStatus.CONNECTED) return;
this.speaking = value;
/**
* Emitted when the dispatcher starts/stops speaking.
@@ -117,6 +119,16 @@ class StreamDispatcher extends VolumeInterface {
this.emit('speaking', value);
}
/**
* Set the bitrate of the current Opus encoder.
* @param {number} bitrate New bitrate, in kbps
* If set to 'auto', the voice channel's bitrate will be used
*/
setBitrate(bitrate) {
this.player.setBitrate(bitrate);
}
sendBuffer(buffer, sequence, timestamp, opusPacket) {
opusPacket = opusPacket || this.player.opusEncoder.encode(buffer);
const packet = this.createPacket(sequence, timestamp, opusPacket);
@@ -128,7 +140,7 @@ class StreamDispatcher extends VolumeInterface {
/**
* Emitted whenever the dispatcher has debug information.
* @event StreamDispatcher#debug
* @param {string} info the debug info
* @param {string} info The debug info
*/
this.setSpeaking(true);
while (repeats--) {
@@ -282,7 +294,7 @@ class StreamDispatcher extends VolumeInterface {
this.emit(type, reason);
/**
* Emitted once the dispatcher ends.
* @param {string} [reason] the reason the dispatcher ended
* @param {string} [reason] The reason the dispatcher ended
* @event StreamDispatcher#end
*/
if (type !== 'end') this.emit('end', `destroyed due to ${type} - ${reason}`);

View File

@@ -5,20 +5,37 @@
class BaseOpus {
/**
* @param {Object} [options] The options to apply to the Opus engine
* @param {boolean} [options.fec] Whether to enable forward error correction (defaults to false)
* @param {number} [options.plp] The expected packet loss percentage (0-1 inclusive, defaults to 0)
* @param {number} [options.bitrate=48] The desired bitrate (kbps)
* @param {boolean} [options.fec=false] Whether to enable forward error correction
* @param {number} [options.plp=0] The expected packet loss percentage
*/
constructor(options = {}) {
constructor({ bitrate = 48, fec = false, plp = 0 } = {}) {
this.ctl = {
BITRATE: 4002,
FEC: 4012,
PLP: 4014,
};
this.options = options;
this.samplingRate = 48000;
this.channels = 2;
/**
* The desired bitrate (kbps)
* @type {number}
*/
this.bitrate = bitrate;
/**
* Miscellaneous Opus options
* @type {Object}
*/
this.options = { fec, plp };
}
init() {
try {
this.setBitrate(this.bitrate);
// Set FEC (forward error correction)
if (this.options.fec) this.setFEC(this.options.fec);

View File

@@ -10,10 +10,14 @@ class NodeOpusEngine extends OpusEngine {
} catch (err) {
throw err;
}
this.encoder = new opus.OpusEncoder(48000, 2);
this.encoder = new opus.OpusEncoder(this.samplingRate, this.channels);
super.init();
}
setBitrate(bitrate) {
this.encoder.applyEncoderCTL(this.ctl.BITRATE, Math.min(128, Math.max(16, bitrate)) * 1000);
}
setFEC(enabled) {
this.encoder.applyEncoderCTL(this.ctl.FEC, enabled ? 1 : 0);
}

View File

@@ -3,13 +3,14 @@ const list = [
require('./OpusScriptEngine'),
];
let opusEngineFound;
function fetch(Encoder, engineOptions) {
try {
return new Encoder(engineOptions);
} catch (err) {
return null;
if (err.message.includes('Cannot find module')) return null;
// The Opus engine exists, but another error occurred.
throw err;
}
}
@@ -22,10 +23,6 @@ exports.fetch = engineOptions => {
const fetched = fetch(encoder, engineOptions);
if (fetched) return fetched;
}
return null;
};
exports.guaranteeOpusEngine = () => {
if (typeof opusEngineFound === 'undefined') opusEngineFound = Boolean(exports.fetch());
if (!opusEngineFound) throw new Error('Couldn\'t find an Opus engine.');
throw new Error('OPUS_ENGINE_MISSING');
};

View File

@@ -10,10 +10,14 @@ class OpusScriptEngine extends OpusEngine {
} catch (err) {
throw err;
}
this.encoder = new OpusScript(48000, 2);
this.encoder = new OpusScript(this.samplingRate, this.channels);
super.init();
}
setBitrate(bitrate) {
this.encoder.encoderCTL(this.ctl.BITRATE, Math.min(128, Math.max(16, bitrate)) * 1000);
}
setFEC(enabled) {
this.encoder.encoderCTL(this.ctl.FEC, enabled ? 1 : 0);
}

View File

@@ -30,11 +30,6 @@ class AudioPlayer extends EventEmitter {
* @type {Prism}
*/
this.prism = new Prism();
/**
* The opus encoder that the player uses
* @type {NodeOpusEngine|OpusScriptEngine}
*/
this.opusEncoder = OpusEncoders.fetch();
this.streams = new Collection();
this.currentStream = {};
this.streamingData = {
@@ -67,6 +62,7 @@ class AudioPlayer extends EventEmitter {
destroy() {
if (this.opusEncoder) this.opusEncoder.destroy();
this.opusEncoder = null;
}
destroyCurrentStream() {
@@ -83,13 +79,25 @@ class AudioPlayer extends EventEmitter {
this.currentStream = {};
}
playUnknownStream(stream, { seek = 0, volume = 1, passes = 1 } = {}) {
OpusEncoders.guaranteeOpusEngine();
const options = { seek, volume, passes };
/**
* Set the bitrate of the current Opus encoder.
* @param {number} value New bitrate, in kbps
* If set to 'auto', the voice channel's bitrate will be used
*/
setBitrate(value) {
if (!value) return;
if (!this.opusEncoder) return;
const bitrate = value === 'auto' ? this.voiceConnection.channel.bitrate : value;
this.opusEncoder.setBitrate(bitrate);
}
playUnknownStream(stream, options = {}) {
this.destroy();
this.opusEncoder = OpusEncoders.fetch(options);
const transcoder = this.prism.transcode({
type: 'ffmpeg',
media: stream,
ffmpegArguments: ffmpegArguments.concat(['-ss', String(seek)]),
ffmpegArguments: ffmpegArguments.concat(['-ss', String(options.seek || 0)]),
});
this.destroyCurrentStream();
this.currentStream = {
@@ -105,9 +113,10 @@ class AudioPlayer extends EventEmitter {
return this.playPCMStream(transcoder.output, options, true);
}
playPCMStream(stream, { seek = 0, volume = 1, passes = 1 } = {}, fromUnknown = false) {
OpusEncoders.guaranteeOpusEngine();
const options = { seek, volume, passes };
playPCMStream(stream, options = {}, fromUnknown = false) {
this.destroy();
this.opusEncoder = OpusEncoders.fetch(options);
this.setBitrate(options.bitrate);
const dispatcher = this.createDispatcher(stream, options);
if (fromUnknown) {
this.currentStream.dispatcher = dispatcher;
@@ -122,8 +131,8 @@ class AudioPlayer extends EventEmitter {
return dispatcher;
}
playOpusStream(stream, { seek = 0, passes = 1 } = {}) {
const options = { seek, passes, opus: true };
playOpusStream(stream, options = {}) {
options.opus = true;
this.destroyCurrentStream();
const dispatcher = this.createDispatcher(stream, options);
this.currentStream = {
@@ -134,8 +143,7 @@ class AudioPlayer extends EventEmitter {
return dispatcher;
}
playBroadcast(broadcast, { volume = 1, passes = 1 } = {}) {
const options = { volume, passes };
playBroadcast(broadcast, options) {
this.destroyCurrentStream();
const dispatcher = this.createDispatcher(broadcast, options);
this.currentStream = {
@@ -148,7 +156,9 @@ class AudioPlayer extends EventEmitter {
return dispatcher;
}
createDispatcher(stream, options) {
createDispatcher(stream, { seek = 0, volume = 1, passes = 1 } = {}) {
const options = { seek, volume, passes };
const dispatcher = new StreamDispatcher(this, stream, options);
dispatcher.on('end', () => this.destroyCurrentStream());
dispatcher.on('error', () => this.destroyCurrentStream());

View File

@@ -10,9 +10,10 @@ nonce.fill(0);
* Receives voice data from a voice connection.
* ```js
* // Obtained using:
* voiceChannel.join().then(connection => {
* const receiver = connection.createReceiver();
* });
* voiceChannel.join()
* .then(connection => {
* const receiver = connection.createReceiver();
* });
* ```
* @extends {EventEmitter}
*/

View File

@@ -1,4 +1,4 @@
const browser = require('os').platform() === 'browser';
const browser = typeof window !== 'undefined';
const EventEmitter = require('events');
const Constants = require('../../util/Constants');
const zlib = require('zlib');
@@ -29,12 +29,12 @@ const WebSocket = (function findWebSocket() {
class WebSocketConnection extends EventEmitter {
/**
* @param {WebSocketManager} manager The WebSocket manager
* @param {string} gateway WebSocket gateway to connect to
* @param {string} gateway The WebSocket gateway to connect to
*/
constructor(manager, gateway) {
super();
/**
* WebSocket Manager of this connection
* The WebSocket Manager of this connection
* @type {WebSocketManager}
*/
this.manager = manager;
@@ -115,6 +115,10 @@ class WebSocketConnection extends EventEmitter {
this.debug('Tried to mark self as ready, but already ready');
return;
}
/**
* Emitted when the client becomes ready to start working.
* @event Client#ready
*/
this.status = Constants.Status.READY;
this.client.emit(Constants.Events.READY);
this.packetManager.handleQueue();
@@ -228,7 +232,7 @@ class WebSocketConnection extends EventEmitter {
/**
* Creates a connection to a gateway.
* @param {string} gateway Gateway to connect to
* @param {string} gateway The gateway to connect to
* @param {number} [after=0] How long to wait before connecting
* @param {boolean} [force=false] Whether or not to force a new connection even if one already exists
* @returns {boolean}
@@ -280,12 +284,13 @@ class WebSocketConnection extends EventEmitter {
* @returns {boolean}
*/
onMessage(event) {
let data;
try {
event.data = this.unpack(event.data);
data = this.unpack(event.data);
} catch (err) {
this.emit('debug', err);
}
return this.onPacket(event.data);
return this.onPacket(data);
}
/**
@@ -351,11 +356,19 @@ class WebSocketConnection extends EventEmitter {
/**
* Called whenever an error occurs with the WebSocket.
* @param {Error} error Error that occurred
* @param {Error} error The error that occurred
*/
onError(error) {
if (error && error.message === 'uWs client connection error') {
this.reconnect();
return;
}
/**
* Emitted whenever the client's WebSocket encounters a connection error.
* @event Client#error
* @param {Error} error The encountered error
*/
this.client.emit(Constants.Events.ERROR, error);
if (error.message === 'uWs client connection error') this.reconnect();
}
/**
@@ -431,7 +444,7 @@ class WebSocketConnection extends EventEmitter {
* @returns {void}
*/
identify(after) {
if (after) return this.client.setTimeout(this.identify.apply(this), after);
if (after) return this.client.setTimeout(this.identify.bind(this), after);
return this.sessionID ? this.identifyResume() : this.identifyNew();
}

View File

@@ -3,7 +3,7 @@ const Constants = require('../../util/Constants');
const WebSocketConnection = require('./WebSocketConnection');
/**
* WebSocket Manager of the client
* WebSocket Manager of the client.
* @private
*/
class WebSocketManager extends EventEmitter {
@@ -23,7 +23,7 @@ class WebSocketManager extends EventEmitter {
}
/**
* Sends a heartbeat on the available connection
* Sends a heartbeat on the available connection.
* @returns {void}
*/
heartbeat() {
@@ -67,7 +67,7 @@ class WebSocketManager extends EventEmitter {
/**
* Connects the client to a gateway.
* @param {string} gateway Gateway to connect to
* @param {string} gateway The gateway to connect to
* @returns {boolean}
*/
connect(gateway) {

View File

@@ -39,6 +39,7 @@ class WebSocketPacketManager {
this.register(Constants.WSEvents.USER_UPDATE, require('./handlers/UserUpdate'));
this.register(Constants.WSEvents.USER_NOTE_UPDATE, require('./handlers/UserNoteUpdate'));
this.register(Constants.WSEvents.USER_SETTINGS_UPDATE, require('./handlers/UserSettingsUpdate'));
this.register(Constants.WSEvents.USER_GUILD_SETTINGS_UPDATE, require('./handlers/UserGuildSettingsUpdate'));
this.register(Constants.WSEvents.VOICE_STATE_UPDATE, require('./handlers/VoiceStateUpdate'));
this.register(Constants.WSEvents.TYPING_START, require('./handlers/TypingStart'));
this.register(Constants.WSEvents.MESSAGE_CREATE, require('./handlers/MessageCreate'));

View File

@@ -26,7 +26,7 @@ class GuildMembersChunkHandler extends AbstractHandler {
/**
* Emitted whenever a chunk of guild members is received (all members come from the same guild).
* @event Client#guildMembersChunk
* @param {Collection<Snowflake, GuildMember>} members The members in the chunk
* @param {GuildMember[]} members The members in the chunk
* @param {Guild} guild The guild related to the member chunk
*/

View File

@@ -10,6 +10,7 @@ class ReadyHandler extends AbstractHandler {
client.ws.heartbeat();
data.user.user_settings = data.user_settings;
data.user.user_guild_settings = data.user_guild_settings;
const clientUser = new ClientUser(client, data.user);
client.user = clientUser;

View File

@@ -14,7 +14,7 @@ class ResumedHandler extends AbstractHandler {
const replayed = ws.sequence - ws.closeSequence;
ws.debug(`RESUMED ${ws._trace.join(' -> ')} | replayed ${replayed} events.`);
client.emit('resume', replayed);
client.emit(Constants.Events.RESUME, replayed);
ws.heartbeat();
}
}

View File

@@ -0,0 +1,18 @@
const AbstractHandler = require('./AbstractHandler');
const Constants = require('../../../../util/Constants');
class UserGuildSettingsUpdateHandler extends AbstractHandler {
handle(packet) {
const client = this.packetManager.client;
client.user.guildSettings.get(packet.d.guild_id).patch(packet.d);
client.emit(Constants.Events.USER_GUILD_SETTINGS_UPDATE, client.user.guildSettings.get(packet.d.guild_id));
}
}
/**
* Emitted whenever the client user's settings update.
* @event Client#clientUserGuildSettingsUpdate
* @param {ClientUserGuildSettings} clientUserGuildSettings The new client user guild settings
*/
module.exports = UserGuildSettingsUpdateHandler;

View File

@@ -11,6 +11,7 @@ module.exports = {
// Utilities
Collection: require('./util/Collection'),
Constants: require('./util/Constants'),
DiscordAPIError: require('./client/rest/DiscordAPIError'),
EvaluatedPermissions: require('./util/Permissions'),
Permissions: require('./util/Permissions'),
Snowflake: require('./util/Snowflake'),
@@ -25,6 +26,7 @@ module.exports = {
splitMessage: Util.splitMessage,
// Structures
Attachment: require('./structures/Attachment'),
Channel: require('./structures/Channel'),
ClientUser: require('./structures/ClientUser'),
ClientUserSettings: require('./structures/ClientUserSettings'),
@@ -59,5 +61,3 @@ module.exports = {
VoiceChannel: require('./structures/VoiceChannel'),
Webhook: require('./structures/Webhook'),
};
if (require('os').platform() === 'browser') window.Discord = module.exports; // eslint-disable-line no-undef

View File

@@ -69,9 +69,11 @@ class Shard {
* @param {string} prop Name of the client property to get, using periods for nesting
* @returns {Promise<*>}
* @example
* shard.fetchClientValue('guilds.size').then(count => {
* console.log(`${count} guilds in shard ${shard.id}`);
* }).catch(console.error);
* shard.fetchClientValue('guilds.size')
* .then(count => {
* console.log(`${count} guilds in shard ${shard.id}`);
* })
* .catch(console.error);
*/
fetchClientValue(prop) {
if (this._fetches.has(prop)) return this._fetches.get(prop);

View File

@@ -49,9 +49,11 @@ class ShardClientUtil {
* @param {string} prop Name of the client property to get, using periods for nesting
* @returns {Promise<Array>}
* @example
* client.shard.fetchClientValues('guilds.size').then(results => {
* console.log(`${results.reduce((prev, val) => prev + val, 0)} total guilds`);
* }).catch(console.error);
* client.shard.fetchClientValues('guilds.size')
* .then(results => {
* console.log(`${results.reduce((prev, val) => prev + val, 0)} total guilds`);
* })
* .catch(console.error);
*/
fetchClientValues(prop) {
return new Promise((resolve, reject) => {

View File

@@ -176,9 +176,11 @@ class ShardingManager extends EventEmitter {
* @param {string} prop Name of the client property to get, using periods for nesting
* @returns {Promise<Array>}
* @example
* manager.fetchClientValues('guilds.size').then(results => {
* console.log(`${results.reduce((prev, val) => prev + val, 0)} total guilds`);
* }).catch(console.error);
* manager.fetchClientValues('guilds.size')
* .then(results => {
* console.log(`${results.reduce((prev, val) => prev + val, 0)} total guilds`);
* })
* .catch(console.error);
*/
fetchClientValues(prop) {
if (this.shards.size === 0) return Promise.reject(new Error('No shards have been spawned.'));

View File

@@ -0,0 +1,75 @@
/**
* Represents an attachment in a message.
* @param {BufferResolvable|Stream} file The file
* @param {string} [name] The name of the file, if any
*/
class Attachment {
constructor(file, name) {
this.file = null;
if (name) this.setAttachment(file, name);
else this._attach(file);
}
/**
* The name of the file
* @type {?string}
* @readonly
*/
get name() {
return this.file.name;
}
/**
* The file
* @type {?BufferResolvable|Stream}
* @readonly
*/
get attachment() {
return this.file.attachment;
}
/**
* Set the file of this attachment.
* @param {BufferResolvable|Stream} file The file
* @param {string} name The name of the file
* @returns {Attachment} This attachment
*/
setAttachment(file, name) {
this.file = { attachment: file, name };
return this;
}
/**
* Set the file of this attachment.
* @param {BufferResolvable|Stream} attachment The file
* @returns {Attachment} This attachment
*/
setFile(attachment) {
this.file = { attachment };
return this;
}
/**
* Set the name of this attachment.
* @param {string} name The name of the image
* @returns {Attachment} This attachment
*/
setName(name) {
this.file.name = name;
return this;
}
/**
* Set the file of this attachment.
* @param {BufferResolvable|Stream} file The file
* @param {string} name The name of the file
* @returns {void}
* @private
*/
_attach(file, name) {
if (typeof file === 'string') this.file = file;
else this.setAttachment(file, name);
}
}
module.exports = Attachment;

View File

@@ -58,8 +58,8 @@ class Channel {
* @example
* // Delete the channel
* channel.delete()
* .then() // Success
* .catch(console.error); // Log error
* .then() // Success
* .catch(console.error); // Log error
*/
delete() {
return this.client.rest.methods.deleteChannel(this);

View File

@@ -1,6 +1,7 @@
const User = require('./User');
const Collection = require('../util/Collection');
const ClientUserSettings = require('./ClientUserSettings');
const ClientUserGuildSettings = require('./ClientUserGuildSettings');
const Constants = require('../util/Constants');
/**
@@ -72,7 +73,19 @@ class ClientUser extends User {
* <warn>This is only filled when using a user account.</warn>
* @type {?ClientUserSettings}
*/
if (data.user_settings) this.settings = new ClientUserSettings(this, data.user_settings);
this.settings = data.user_settings ? new ClientUserSettings(this, data.user_settings) : null;
/**
* All of the user's guild settings
* <warn>This is only filled when using a user account</warn>
* @type {Collection<Snowflake, ClientUserGuildSettings>}
*/
this.guildSettings = new Collection();
if (data.user_guild_settings) {
for (const settings of data.user_guild_settings) {
this.guildSettings.set(settings.guild_id, new ClientUserGuildSettings(settings, this.client));
}
}
}
edit(data) {
@@ -89,8 +102,8 @@ class ClientUser extends User {
* @example
* // Set username
* client.user.setUsername('discordjs')
* .then(user => console.log(`My new username is ${user.username}`))
* .catch(console.error);
* .then(user => console.log(`My new username is ${user.username}`))
* .catch(console.error);
*/
setUsername(username, password) {
return this.client.rest.methods.updateCurrentUser({ username }, password);
@@ -105,8 +118,8 @@ class ClientUser extends User {
* @example
* // Set email
* client.user.setEmail('bob@gmail.com', 'some amazing password 123')
* .then(user => console.log(`My new email is ${user.email}`))
* .catch(console.error);
* .then(user => console.log(`My new email is ${user.email}`))
* .catch(console.error);
*/
setEmail(email, password) {
return this.client.rest.methods.updateCurrentUser({ email }, password);
@@ -121,8 +134,8 @@ class ClientUser extends User {
* @example
* // Set password
* client.user.setPassword('some new amazing password 456', 'some amazing password 123')
* .then(user => console.log('New password set!'))
* .catch(console.error);
* .then(user => console.log('New password set!'))
* .catch(console.error);
*/
setPassword(newPassword, oldPassword) {
return this.client.rest.methods.updateCurrentUser({ password: newPassword }, oldPassword);
@@ -135,17 +148,13 @@ class ClientUser extends User {
* @example
* // Set avatar
* client.user.setAvatar('./avatar.png')
* .then(user => console.log(`New avatar set!`))
* .catch(console.error);
* .then(user => console.log(`New avatar set!`))
* .catch(console.error);
*/
setAvatar(avatar) {
if (typeof avatar === 'string' && avatar.startsWith('data:')) {
return this.client.rest.methods.updateCurrentUser({ avatar });
} else {
return this.client.resolver.resolveBuffer(avatar).then(data =>
this.client.rest.methods.updateCurrentUser({ avatar: data })
);
}
return this.client.resolver.resolveImage(avatar).then(data =>
this.client.rest.methods.updateCurrentUser({ avatar: data })
);
}
/**
@@ -190,7 +199,7 @@ class ClientUser extends User {
if (data.game) {
game = data.game;
if (game.url) game.type = 1;
game.type = game.url ? 1 : 0;
} else if (typeof data.game !== 'undefined') {
game = null;
}
@@ -215,10 +224,10 @@ class ClientUser extends User {
/**
* A user's status. Must be one of:
* - `online`
* - `idle`
* - `invisible`
* - `dnd` (do not disturb)
* * `online`
* * `idle`
* * `invisible`
* * `dnd` (do not disturb)
* @typedef {string} PresenceStatus
*/
@@ -265,7 +274,7 @@ class ClientUser extends User {
* @param {Guild|Snowflake} [options.guild] Limit the search to a specific guild
* @returns {Promise<Message[]>}
*/
fetchMentions(options = { limit: 25, roles: true, everyone: true, guild: null }) {
fetchMentions(options = {}) {
return this.client.rest.methods.fetchMentions(options);
}
@@ -295,16 +304,15 @@ class ClientUser extends User {
* Creates a guild.
* <warn>This is only available when using a user account.</warn>
* @param {string} name The name of the guild
* @param {string} region The region for the server
* @param {string} [region] The region for the server
* @param {BufferResolvable|Base64Resolvable} [icon=null] The icon for the guild
* @returns {Promise<Guild>} The guild that was created
*/
createGuild(name, region, icon = null) {
if (!icon) return this.client.rest.methods.createGuild({ name, icon, region });
if (typeof icon === 'string' && icon.startsWith('data:')) {
return this.client.rest.methods.createGuild({ name, icon, region });
} else {
return this.client.resolver.resolveBuffer(icon).then(data =>
return this.client.resolver.resolveImage(icon).then(data =>
this.client.rest.methods.createGuild({ name, icon: data, region })
);
}

View File

@@ -0,0 +1,30 @@
const Constants = require('../util/Constants');
/**
* A wrapper around the ClientUser's channel overrides.
*/
class ClientUserChannelOverride {
constructor(data) {
this.patch(data);
}
/**
* Patch the data contained in this class with new partial data.
* @param {Object} data Data to patch this with
* @returns {void}
* @private
*/
patch(data) {
for (const key of Object.keys(Constants.UserChannelOverrideMap)) {
const value = Constants.UserChannelOverrideMap[key];
if (!data.hasOwnProperty(key)) continue;
if (typeof value === 'function') {
this[value.name] = value(data[key]);
} else {
this[value] = data[key];
}
}
}
}
module.exports = ClientUserChannelOverride;

View File

@@ -0,0 +1,60 @@
const Constants = require('../util/Constants');
const Collection = require('../util/Collection');
const ClientUserChannelOverride = require('./ClientUserChannelOverride');
/**
* A wrapper around the ClientUser's guild settings.
*/
class ClientUserGuildSettings {
constructor(data, client) {
/**
* The client that created the instance of the ClientUserGuildSettings
* @name ClientUserGuildSettings#client
* @type {Client}
* @readonly
*/
Object.defineProperty(this, 'client', { value: client });
/**
* The ID of the guild this settings are for
* @type {Snowflake}
*/
this.guildID = data.guild_id;
this.channelOverrides = new Collection();
this.patch(data);
}
/**
* Patch the data contained in this class with new partial data.
* @param {Object} data Data to patch this with
* @returns {void}
* @private
*/
patch(data) {
for (const key of Object.keys(Constants.UserGuildSettingsMap)) {
const value = Constants.UserGuildSettingsMap[key];
if (!data.hasOwnProperty(key)) continue;
if (key === 'channel_overrides') {
for (const channel of data[key]) {
this.channelOverrides.set(channel.channel_id,
new ClientUserChannelOverride(channel));
}
} else if (typeof value === 'function') {
this[value.name] = value(data[key]);
} else {
this[value] = data[key];
}
}
}
/**
* Update a specific property of the guild settings.
* @param {string} name Name of property
* @param {value} value Value to patch
* @returns {Promise<Object>}
*/
update(name, value) {
return this.client.rest.methods.patchClientUserGuildSettings(this.guildID, { [name]: value });
}
}
module.exports = ClientUserGuildSettings;

View File

@@ -13,6 +13,8 @@ class ClientUserSettings {
/**
* Patch the data contained in this class with new partial data.
* @param {Object} data Data to patch this with
* @returns {void}
* @private
*/
patch(data) {
for (const key of Object.keys(Constants.UserSettingsMap)) {
@@ -29,7 +31,7 @@ class ClientUserSettings {
/**
* Update a specific property of of user settings.
* @param {string} name Name of property
* @param {value} value Value to patch
* @param {*} value Value to patch
* @returns {Promise<Object>}
*/
update(name, value) {
@@ -37,6 +39,7 @@ class ClientUserSettings {
}
/**
* Sets the position at which this guild will appear in the Discord client.
* @param {Guild} guild The guild to move
* @param {number} position Absolute or relative position
* @param {boolean} [relative=false] Whether to position relatively or absolutely

View File

@@ -53,6 +53,7 @@ class DMChannel extends Channel {
get typing() {}
get typingCount() {}
createCollector() {}
createMessageCollector() {}
awaitMessages() {}
// Doesn't work on DM channels; bulkDelete() {}
acknowledge() {}

View File

@@ -112,24 +112,26 @@ class Emoji {
/**
* Edits the emoji.
* @param {EmojiEditData} data The new data for the emoji
* @param {string} [reason] Reason for editing this emoji
* @returns {Promise<Emoji>}
* @example
* // Edit a emoji
* // Edit an emoji
* emoji.edit({name: 'newemoji'})
* .then(e => console.log(`Edited emoji ${e}`))
* .catch(console.error);
* .then(e => console.log(`Edited emoji ${e}`))
* .catch(console.error);
*/
edit(data) {
return this.client.rest.methods.updateEmoji(this, data);
edit(data, reason) {
return this.client.rest.methods.updateEmoji(this, data, reason);
}
/**
* Set the name of the emoji.
* @param {string} name The new name for the emoji
* @param {string} [reason] The reason for changing the emoji's name
* @returns {Promise<Emoji>}
*/
setName(name) {
return this.edit({ name });
setName(name, reason) {
return this.edit({ name }, reason);
}
/**

View File

@@ -1,6 +1,7 @@
const Channel = require('./Channel');
const TextBasedChannel = require('./interfaces/TextBasedChannel');
const Collection = require('../util/Collection');
const Constants = require('../util/Constants');
/*
{ type: 3,
@@ -47,8 +48,8 @@ class GroupDMChannel extends Channel {
this.name = data.name;
/**
* A hash of the Group DM icon.
* @type {string}
* A hash of this Group DM icon
* @type {?string}
*/
this.icon = data.icon;
@@ -70,11 +71,13 @@ class GroupDMChannel extends Channel {
*/
this.applicationID = data.application_id;
/**
* Nicknames for group members
* @type {?Collection<Snowflake, string>}
*/
if (data.nicks) this.nicks = new Collection(data.nicks.map(n => [n.id, n.nick]));
if (data.nicks) {
/**
* Nicknames for group members
* @type {?Collection<Snowflake, string>}
*/
this.nicks = new Collection(data.nicks.map(n => [n.id, n.nick]));
}
if (!this.recipients) {
/**
@@ -103,6 +106,23 @@ class GroupDMChannel extends Channel {
return this.client.users.get(this.ownerID);
}
/**
* The URL to this guild's icon
* @type {?string}
* @readonly
*/
get iconURL() {
if (!this.icon) return null;
return Constants.Endpoints.Channel(this).Icon(this.client.options.http.cdn, this.icon);
}
edit(data) {
const _data = {};
if (data.name) _data.name = data.name;
if (typeof data.icon !== 'undefined') _data.icon = data.icon;
return this.client.rest.methods.updateGroupDMChannel(this, _data);
}
/**
* Whether this channel equals another channel. It compares all properties, so for most operations
* it is advisable to just compare `channel.id === channel2.id` as it is much faster and is often
@@ -128,6 +148,7 @@ class GroupDMChannel extends Channel {
* Add a user to the DM
* @param {UserResolvable|string} accessTokenOrID Access token or user resolvable
* @param {string} [nick] Permanent nickname to give the user (only available if a bot is creating the DM)
* @returns {Promise<GroupDMChannel>}
*/
addUser(accessTokenOrID, nick) {
@@ -138,6 +159,39 @@ class GroupDMChannel extends Channel {
});
}
/**
* Set a new GroupDMChannel icon.
* @param {Base64Resolvable|BufferResolvable} icon The new icon of the group dm
* @returns {Promise<GroupDMChannel>}
* @example
* // Edit the group dm icon
* channel.setIcon('./icon.png')
* .then(updated => console.log('Updated the channel icon'))
* .catch(console.error);
*/
setIcon(icon) {
return this.client.resolver.resolveImage(icon).then(data => this.edit({ icon: data }));
}
/**
* Sets a new name for this Group DM.
* @param {string} name New name for this Group DM
* @returns {Promise<GroupDMChannel>}
*/
setName(name) {
return this.edit({ name });
}
/**
* Removes an user from this Group DM.
* @param {UserResolvable} user User to remove
* @returns {Promise<GroupDMChannel>}
*/
removeUser(user) {
const id = this.client.resolver.resolveUserID(user);
return this.client.rest.methods.removeUserFromGroupDM(this, id);
}
/**
* When concatenated with a string, this automatically concatenates the channel's name instead of the Channel object.
* @returns {string}
@@ -169,6 +223,7 @@ class GroupDMChannel extends Channel {
get typing() {}
get typingCount() {}
createCollector() {}
createMessageCollector() {}
awaitMessages() {}
// Doesn't work on Group DMs; bulkDelete() {}
acknowledge() {}

View File

@@ -1,3 +1,4 @@
const util = require('util');
const Long = require('long');
const User = require('./User');
const Role = require('./Role');
@@ -17,7 +18,7 @@ const Snowflake = require('../util/Snowflake');
class Guild {
constructor(client, data) {
/**
* The client that created the instance of the the guild
* The client that created the instance of the guild
* @name Guild#client
* @type {Client}
* @readonly
@@ -62,8 +63,8 @@ class Guild {
*/
this.id = data.id;
} else {
this.available = true;
this.setup(data);
if (!data.channels) this.available = false;
}
}
@@ -133,6 +134,12 @@ class Guild {
*/
this.afkChannelID = data.afk_channel_id;
/**
* The ID of the system channel
* @type {?Snowflake}
*/
this.systemChannelID = data.system_channel_id;
/**
* Whether embedded images are enabled on this guild
* @type {boolean}
@@ -212,7 +219,8 @@ class Guild {
if (!this.emojis) {
/**
* A collection of emojis that are in this guild. The key is the emoji's ID, the value is the emoji.
* A collection of emojis that are in this guild
* The key is the emoji's ID, the value is the emoji
* @type {Collection<Snowflake, Emoji>}
*/
this.emojis = new Collection();
@@ -262,6 +270,15 @@ class Guild {
return Constants.Endpoints.Guild(this).Icon(this.client.options.http.cdn, this.icon);
}
/**
* The acronym that shows up in place of a guild icon.
* @type {string}
* @readonly
*/
get nameAcronym() {
return this.name.replace(/\w+/g, name => name[0]).replace(/\s/g, '');
}
/**
* The URL to this guild's splash
* @type {?string}
@@ -281,6 +298,24 @@ class Guild {
return this.members.get(this.ownerID);
}
/**
* AFK voice channel for this guild
* @type {?VoiceChannel}
* @readonly
*/
get afkChannel() {
return this.client.channels.get(this.afkChannelID) || null;
}
/**
* System channel for this guild
* @type {?GuildChannel}
* @readonly
*/
get systemChannel() {
return this.client.channels.get(this.systemChannelID) || null;
}
/**
* If the client is connected to any voice channel in this guild, this will be the relevant VoiceConnection
* @type {?VoiceConnection}
@@ -291,19 +326,11 @@ class Guild {
return this.client.voice.connections.get(this.id) || null;
}
/**
* The `#general` TextChannel of the guild
* @type {TextChannel}
* @readonly
*/
get defaultChannel() {
return this.channels.get(this.id);
}
/**
* The position of this guild
* <warn>This is only available when using a user account.</warn>
* @type {?number}
* @readonly
*/
get position() {
if (this.client.user.bot) return null;
@@ -311,6 +338,66 @@ class Guild {
return this.client.user.settings.guildPositions.indexOf(this.id);
}
/**
* Whether the guild is muted
* <warn>This is only available when using a user account.</warn>
* @type {?boolean}
* @readonly
*/
get muted() {
if (this.client.user.bot) return null;
try {
return this.client.user.guildSettings.get(this.id).muted;
} catch (err) {
return false;
}
}
/**
* The type of message that should notify you
* <warn>This is only available when using a user account.</warn>
* @type {?MessageNotificationType}
* @readonly
*/
get messageNotifications() {
if (this.client.user.bot) return null;
try {
return this.client.user.guildSettings.get(this.id).messageNotifications;
} catch (err) {
return null;
}
}
/**
* Whether to receive mobile push notifications
* <warn>This is only available when using a user account.</warn>
* @type {?boolean}
* @readonly
*/
get mobilePush() {
if (this.client.user.bot) return null;
try {
return this.client.user.guildSettings.get(this.id).mobilePush;
} catch (err) {
return false;
}
}
/**
* Whether to suppress everyone messages
* <warn>This is only available when using a user account.</warn>
* @type {?boolean}
* @readonly
*/
get suppressEveryone() {
if (this.client.user.bot) return null;
try {
return this.client.user.guildSettings.get(this.id).suppressEveryone;
} catch (err) {
return null;
}
}
/**
* The `@everyone` role of the guild
* @type {Role}
@@ -366,7 +453,8 @@ class Guild {
}
/**
* Fetch a collection of invites to this guild. Resolves with a collection mapping invites by their codes.
* Fetch a collection of invites to this guild.
* Resolves with a collection mapping invites by their codes.
* @returns {Promise<Collection<string, Invite>>}
*/
fetchInvites() {
@@ -424,7 +512,7 @@ class Guild {
/**
* Fetch a single guild member from a user.
* @param {UserResolvable} user The user to fetch the member for
* @param {boolean} [cache=true] Insert the user into the users cache
* @param {boolean} [cache=true] Insert the member into the members cache
* @returns {Promise<GuildMember>}
*/
fetchMember(user, cache = true) {
@@ -475,9 +563,7 @@ class Guild {
* Performs a search within the entire guild.
* <warn>This is only available when using a user account.</warn>
* @param {MessageSearchOptions} [options={}] Options to pass to the search
* @returns {Promise<Array<Message[]>>}
* An array containing arrays of messages. Each inner array is a search context cluster.
* The message which has triggered the result will have the `hit` property set to `true`.
* @returns {Promise<MessageSearchResult>}
* @example
* guild.search({
* content: 'discord.js',
@@ -497,7 +583,9 @@ class Guild {
* @property {string} [name] The name of the guild
* @property {string} [region] The region of the guild
* @property {number} [verificationLevel] The verification level of the guild
* @property {number} [explicitContentFilter] The level of the explicit content filter
* @property {ChannelResolvable} [afkChannel] The AFK channel of the guild
* @property {ChannelResolvable} [systemChannel] The system channel of the guild
* @property {number} [afkTimeout] The AFK timeout of the guild
* @property {Base64Resolvable} [icon] The icon of the guild
* @property {GuildMemberResolvable} [owner] The owner of the guild
@@ -507,23 +595,52 @@ class Guild {
/**
* Updates the guild with new information - e.g. a new name.
* @param {GuildEditData} data The data to update the guild with
* @param {string} [reason] Reason for editing the guild
* @returns {Promise<Guild>}
* @example
* // Set the guild name and region
* guild.edit({
* name: 'Discord Guild',
* region: 'london',
* name: 'Discord Guild',
* region: 'london',
* })
* .then(updated => console.log(`New guild name ${updated.name} in region ${updated.region}`))
* .catch(console.error);
* .then(updated => console.log(`New guild name ${updated.name} in region ${updated.region}`))
* .catch(console.error);
*/
edit(data) {
return this.client.rest.methods.updateGuild(this, data);
edit(data, reason) {
const _data = {};
if (data.name) _data.name = data.name;
if (data.region) _data.region = data.region;
if (typeof data.verificationLevel !== 'undefined') _data.verification_level = Number(data.verificationLevel);
if (typeof data.afkChannel !== 'undefined') {
_data.afk_channel_id = this.client.resolver.resolveChannelID(data.afkChannel);
}
if (typeof data.systemChannel !== 'undefined') {
_data.system_channel_id = this.client.resolver.resolveChannelID(data.systemChannel);
}
if (data.afkTimeout) _data.afk_timeout = Number(data.afkTimeout);
if (typeof data.icon !== 'undefined') _data.icon = data.icon;
if (data.owner) _data.owner_id = this.client.resolver.resolveUser(data.owner).id;
if (typeof data.splash !== 'undefined') _data.splash = data.splash;
if (typeof data.explicitContentFilter !== 'undefined') {
_data.explicit_content_filter = Number(data.explicitContentFilter);
}
return this.client.rest.methods.updateGuild(this, _data, reason);
}
/**
* Edit the level of the explicit content filter.
* @param {number} explicitContentFilter The new level of the explicit content filter
* @param {string} [reason] Reason for changing the level of the guild's explicit content filter
* @returns {Promise<Guild>}
*/
setExplicitContentFilter(explicitContentFilter, reason) {
return this.edit({ explicitContentFilter }, reason);
}
/**
* Edit the name of the guild.
* @param {string} name The new name of the guild
* @param {string} [reason] Reason for changing the guild's name
* @returns {Promise<Guild>}
* @example
* // Edit the guild name
@@ -531,13 +648,14 @@ class Guild {
* .then(updated => console.log(`Updated guild name to ${guild.name}`))
* .catch(console.error);
*/
setName(name) {
return this.edit({ name });
setName(name, reason) {
return this.edit({ name }, reason);
}
/**
* Edit the region of the guild.
* @param {string} region The new region of the guild
* @param {string} [reason] Reason for changing the guild's region
* @returns {Promise<Guild>}
* @example
* // Edit the guild region
@@ -545,13 +663,14 @@ class Guild {
* .then(updated => console.log(`Updated guild region to ${guild.region}`))
* .catch(console.error);
*/
setRegion(region) {
return this.edit({ region });
setRegion(region, reason) {
return this.edit({ region }, reason);
}
/**
* Edit the verification level of the guild.
* @param {number} verificationLevel The new verification level of the guild
* @param {string} [reason] Reason for changing the guild's verification level
* @returns {Promise<Guild>}
* @example
* // Edit the guild verification level
@@ -559,13 +678,14 @@ class Guild {
* .then(updated => console.log(`Updated guild verification level to ${guild.verificationLevel}`))
* .catch(console.error);
*/
setVerificationLevel(verificationLevel) {
return this.edit({ verificationLevel });
setVerificationLevel(verificationLevel, reason) {
return this.edit({ verificationLevel }, reason);
}
/**
* Edit the AFK channel of the guild.
* @param {ChannelResolvable} afkChannel The new AFK channel
* @param {string} [reason] Reason for changing the guild's AFK channel
* @returns {Promise<Guild>}
* @example
* // Edit the guild AFK channel
@@ -573,13 +693,24 @@ class Guild {
* .then(updated => console.log(`Updated guild AFK channel to ${guild.afkChannel}`))
* .catch(console.error);
*/
setAFKChannel(afkChannel) {
return this.edit({ afkChannel });
setAFKChannel(afkChannel, reason) {
return this.edit({ afkChannel }, reason);
}
/**
* Edit the system channel of the guild.
* @param {ChannelResolvable} systemChannel The new system channel
* @param {string} [reason] Reason for changing the guild's system channel
* @returns {Promise<Guild>}
*/
setSystemChannel(systemChannel, reason) {
return this.edit({ systemChannel }, reason);
}
/**
* Edit the AFK timeout of the guild.
* @param {number} afkTimeout The time in seconds that a user must be idle to be considered AFK
* @param {string} [reason] Reason for changing the guild's AFK timeout
* @returns {Promise<Guild>}
* @example
* // Edit the guild AFK channel
@@ -587,27 +718,29 @@ class Guild {
* .then(updated => console.log(`Updated guild AFK timeout to ${guild.afkTimeout}`))
* .catch(console.error);
*/
setAFKTimeout(afkTimeout) {
return this.edit({ afkTimeout });
setAFKTimeout(afkTimeout, reason) {
return this.edit({ afkTimeout }, reason);
}
/**
* Set a new guild icon.
* @param {Base64Resolvable} icon The new icon of the guild
* @param {Base64Resolvable|BufferResolvable} icon The new icon of the guild
* @param {string} [reason] Reason for changing the guild's icon
* @returns {Promise<Guild>}
* @example
* // Edit the guild icon
* guild.setIcon(fs.readFileSync('./icon.png'))
* guild.setIcon('./icon.png')
* .then(updated => console.log('Updated the guild icon'))
* .catch(console.error);
*/
setIcon(icon) {
return this.edit({ icon });
setIcon(icon, reason) {
return this.client.resolver.resolveImage(icon).then(data => this.edit({ icon: data, reason }));
}
/**
* Sets a new owner of the guild.
* @param {GuildMemberResolvable} owner The new owner of the guild
* @param {string} [reason] Reason for setting the new owner
* @returns {Promise<Guild>}
* @example
* // Edit the guild owner
@@ -615,25 +748,28 @@ class Guild {
* .then(updated => console.log(`Updated the guild owner to ${updated.owner.username}`))
* .catch(console.error);
*/
setOwner(owner) {
return this.edit({ owner });
setOwner(owner, reason) {
return this.edit({ owner }, reason);
}
/**
* Set a new guild splash screen.
* @param {Base64Resolvable} splash The new splash screen of the guild
* @param {BufferResolvable|Base64Resolvable} splash The new splash screen of the guild
* @param {string} [reason] Reason for changing the guild's splash screen
* @returns {Promise<Guild>}
* @example
* // Edit the guild splash
* guild.setIcon(fs.readFileSync('./splash.png'))
* guild.setSplash('./splash.png')
* .then(updated => console.log('Updated the guild splash'))
* .catch(console.error);
*/
setSplash(splash) {
return this.edit({ splash });
return this.client.resolver.resolveImage(splash).then(data => this.edit({ splash: data }));
}
/**
* Sets the position of the guild in the guild listing.
* <warn>This is only available when using a user account.</warn>
* @param {number} position Absolute or relative position
* @param {boolean} [relative=false] Whether to position relatively or absolutely
* @returns {Promise<Guild>}
@@ -648,7 +784,7 @@ class Guild {
/**
* Marks all messages in this guild as read.
* <warn>This is only available when using a user account.</warn>
* @returns {Promise<Guild>} This guild
* @returns {Promise<Guild>}
*/
acknowledge() {
return this.client.rest.methods.ackGuild(this);
@@ -656,6 +792,7 @@ class Guild {
/**
* Allow direct messages from guild members.
* <warn>This is only available when using a user account.</warn>
* @param {boolean} allow Whether to allow direct messages
* @returns {Promise<Guild>}
*/
@@ -678,8 +815,8 @@ class Guild {
* @example
* // Ban a user by ID (or with a user/guild member object)
* guild.ban('some user ID')
* .then(user => console.log(`Banned ${user.username || user.id || user} from ${guild.name}`))
* .catch(console.error);
* .then(user => console.log(`Banned ${user.username || user.id || user} from ${guild.name}`))
* .catch(console.error);
*/
ban(user, options = {}) {
if (typeof options === 'number') {
@@ -694,21 +831,23 @@ class Guild {
/**
* Unbans a user from the guild.
* @param {UserResolvable} user The user to unban
* @param {string} [reason] Reason for unbanning the user
* @returns {Promise<User>}
* @example
* // Unban a user by ID (or with a user/guild member object)
* guild.unban('some user ID')
* .then(user => console.log(`Unbanned ${user.username} from ${guild.name}`))
* .catch(console.error);
* .then(user => console.log(`Unbanned ${user.username} from ${guild.name}`))
* .catch(console.error);
*/
unban(user) {
return this.client.rest.methods.unbanGuildMember(this, user);
unban(user, reason) {
return this.client.rest.methods.unbanGuildMember(this, user, reason);
}
/**
* Prunes members from the guild based on how long they have been inactive.
* @param {number} days Number of days of inactivity required to kick
* @param {boolean} [dry=false] If true, will return number of users that will be kicked, without actually doing it
* @param {string} [reason] Reason for this prune
* @returns {Promise<number>} The number of members that were/will be kicked
* @example
* // See how many members will be pruned
@@ -721,9 +860,9 @@ class Guild {
* .then(pruned => console.log(`I just pruned ${pruned} people!`))
* .catch(console.error);
*/
pruneMembers(days, dry = false) {
pruneMembers(days, dry = false, reason) {
if (typeof days !== 'number') throw new TypeError('Days must be a number.');
return this.client.rest.methods.pruneGuildMembers(this, days, dry);
return this.client.rest.methods.pruneGuildMembers(this, days, dry, reason);
}
/**
@@ -738,16 +877,17 @@ class Guild {
* Creates a new channel in the guild.
* @param {string} name The name of the new channel
* @param {string} type The type of the new channel, either `text` or `voice`
* @param {Array<PermissionOverwrites|Object>} overwrites Permission overwrites to apply to the new channel
* @param {Array<PermissionOverwrites|Object>} [overwrites] Permission overwrites to apply to the new channel
* @param {string} [reason] Reason for creating this channel
* @returns {Promise<TextChannel|VoiceChannel>}
* @example
* // Create a new text channel
* guild.createChannel('new-general', 'text')
* .then(channel => console.log(`Created new channel ${channel}`))
* .catch(console.error);
* .then(channel => console.log(`Created new channel ${channel}`))
* .catch(console.error);
*/
createChannel(name, type, overwrites) {
return this.client.rest.methods.createChannel(this, name, type, overwrites);
createChannel(name, type, overwrites, reason) {
return this.client.rest.methods.createChannel(this, name, type, overwrites, reason);
}
/**
@@ -763,8 +903,8 @@ class Guild {
* @returns {Promise<Guild>}
* @example
* guild.updateChannels([{ channel: channelID, position: newChannelIndex }])
* .then(guild => console.log(`Updated channel positions for ${guild.id}`))
* .catch(console.error);
* .then(guild => console.log(`Updated channel positions for ${guild.id}`))
* .catch(console.error);
*/
setChannelPositions(channelPositions) {
return this.client.rest.methods.updateChannelPositions(this.id, channelPositions);
@@ -773,23 +913,24 @@ class Guild {
/**
* Creates a new role in the guild with given information
* @param {RoleData} [data] The data to update the role with
* @param {string} [reason] Reason for creating this role
* @returns {Promise<Role>}
* @example
* // Create a new role
* guild.createRole()
* .then(role => console.log(`Created role ${role}`))
* .catch(console.error);
* .then(role => console.log(`Created role ${role}`))
* .catch(console.error);
* @example
* // Create a new role with data
* guild.createRole({
* name: 'Super Cool People',
* color: 'BLUE',
* })
* .then(role => console.log(`Created role ${role}`))
* .catch(console.error)
* .then(role => console.log(`Created role ${role}`))
* .catch(console.error)
*/
createRole(data = {}) {
return this.client.rest.methods.createGuildRole(this, data);
createRole(data = {}, reason) {
return this.client.rest.methods.createGuildRole(this, data, reason);
}
/**
@@ -797,39 +938,38 @@ class Guild {
* @param {BufferResolvable|Base64Resolvable} attachment The image for the emoji
* @param {string} name The name for the emoji
* @param {Collection<Snowflake, Role>|Role[]} [roles] Roles to limit the emoji to
* @param {string} [reason] Reason for creating the emoji
* @returns {Promise<Emoji>} The created emoji
* @example
* // Create a new emoji from a url
* guild.createEmoji('https://i.imgur.com/w3duR07.png', 'rip')
* .then(emoji => console.log(`Created new emoji with name ${emoji.name}!`))
* .catch(console.error);
* .then(emoji => console.log(`Created new emoji with name ${emoji.name}!`))
* .catch(console.error);
* @example
* // Create a new emoji from a file on your computer
* guild.createEmoji('./memes/banana.png', 'banana')
* .then(emoji => console.log(`Created new emoji with name ${emoji.name}!`))
* .catch(console.error);
* .then(emoji => console.log(`Created new emoji with name ${emoji.name}!`))
* .catch(console.error);
*/
createEmoji(attachment, name, roles) {
return new Promise(resolve => {
if (typeof attachment === 'string' && attachment.startsWith('data:')) {
resolve(this.client.rest.methods.createEmoji(this, attachment, name, roles));
} else {
this.client.resolver.resolveBuffer(attachment).then(data => {
const dataURI = this.client.resolver.resolveBase64(data);
resolve(this.client.rest.methods.createEmoji(this, dataURI, name, roles));
});
}
});
createEmoji(attachment, name, roles, reason) {
if (typeof attachment === 'string' && attachment.startsWith('data:')) {
return this.client.rest.methods.createEmoji(this, attachment, name, roles, reason);
} else {
return this.client.resolver.resolveImage(attachment).then(data =>
this.client.rest.methods.createEmoji(this, data, name, roles, reason)
);
}
}
/**
* Delete an emoji.
* @param {Emoji|string} emoji The emoji to delete
* @param {string} [reason] Reason for deleting the emoji
* @returns {Promise}
*/
deleteEmoji(emoji) {
deleteEmoji(emoji, reason) {
if (!(emoji instanceof Emoji)) emoji = this.emojis.get(emoji);
return this.client.rest.methods.deleteEmoji(emoji);
return this.client.rest.methods.deleteEmoji(emoji, reason);
}
/**
@@ -838,8 +978,8 @@ class Guild {
* @example
* // Leave a guild
* guild.leave()
* .then(g => console.log(`Left the guild ${g}`))
* .catch(console.error);
* .then(g => console.log(`Left the guild ${g}`))
* .catch(console.error);
*/
leave() {
return this.client.rest.methods.leaveGuild(this);
@@ -851,8 +991,8 @@ class Guild {
* @example
* // Delete a guild
* guild.delete()
* .then(g => console.log(`Deleted the guild ${g}`))
* .catch(console.error);
* .then(g => console.log(`Deleted the guild ${g}`))
* .catch(console.error);
*/
delete() {
return this.client.rest.methods.deleteGuild(this);
@@ -1063,10 +1203,22 @@ class Guild {
_sortPositionWithID(collection) {
return collection.sort((a, b) =>
a.position !== b.position ?
a.position - b.position :
Long.fromString(a.id).sub(Long.fromString(b.id)).toNumber()
a.position - b.position :
Long.fromString(a.id).sub(Long.fromString(b.id)).toNumber()
);
}
}
/**
* The `#general` TextChannel of the guild
* @name Guild#defaultChannel
* @type {TextChannel}
* @readonly
*/
Object.defineProperty(Guild.prototype, 'defaultChannel', {
get: util.deprecate(function defaultChannel() {
return this.channels.get(this.id);
}, 'Guild#defaultChannel: This property is obsolete, will be removed in v12.0.0, and may not function as expected.'),
});
module.exports = Guild;

View File

@@ -2,6 +2,7 @@ const Collection = require('../util/Collection');
const Snowflake = require('../util/Snowflake');
const Targets = {
ALL: 'ALL',
GUILD: 'GUILD',
CHANNEL: 'CHANNEL',
USER: 'USER',
@@ -9,9 +10,11 @@ const Targets = {
INVITE: 'INVITE',
WEBHOOK: 'WEBHOOK',
EMOJI: 'EMOJI',
MESSAGE: 'MESSAGE',
};
const Actions = {
ALL: null,
GUILD_UPDATE: 1,
CHANNEL_CREATE: 10,
CHANNEL_UPDATE: 11,
@@ -37,6 +40,7 @@ const Actions = {
EMOJI_CREATE: 60,
EMOJI_UPDATE: 61,
EMOJI_DELETE: 62,
MESSAGE_DELETE: 72,
};
@@ -60,13 +64,11 @@ class GuildAuditLogs {
/**
* Handles possible promises for entry targets.
* @returns {GuildAuditLogs}
* @returns {Promise<GuildAuditLogs>}
*/
static build(...args) {
return new Promise(resolve => {
const logs = new GuildAuditLogs(...args);
Promise.all(logs.entries.map(e => e.target)).then(() => resolve(logs));
});
const logs = new GuildAuditLogs(...args);
return Promise.all(logs.entries.map(e => e.target)).then(() => logs);
}
/**
@@ -82,6 +84,7 @@ class GuildAuditLogs {
if (target < 50) return Targets.INVITE;
if (target < 60) return Targets.WEBHOOK;
if (target < 70) return Targets.EMOJI;
if (target < 80) return Targets.MESSAGE;
return null;
}
@@ -112,6 +115,7 @@ class GuildAuditLogs {
Actions.INVITE_DELETE,
Actions.WEBHOOK_DELETE,
Actions.EMOJI_DELETE,
Actions.MESSAGE_DELETE,
].includes(action)) return 'DELETE';
if ([
@@ -119,6 +123,7 @@ class GuildAuditLogs {
Actions.CHANNEL_UPDATE,
Actions.CHANNEL_OVERWRITE_UPDATE,
Actions.MEMBER_UPDATE,
Actions.MEMBER_ROLE_UPDATE,
Actions.ROLE_UPDATE,
Actions.INVITE_UPDATE,
Actions.WEBHOOK_UPDATE,
@@ -166,10 +171,18 @@ class GuildAuditLogsEntry {
this.executor = guild.client.users.get(data.user_id);
/**
* Specific property changes
* @type {Object[]}
* An entry in the audit log representing a specific change.
* @typedef {object} AuditLogChange
* @property {string} key The property that was changed, e.g. `nick` for nickname changes
* @property {*} [old] The old value of the change, e.g. for nicknames, the old nickname
* @property {*} [new] The new value of the change, e.g. for nicknames, the new nickname
*/
this.changes = data.changes ? data.changes.map(c => ({ name: c.key, old: c.old_value, new: c.new_value })) : null;
/**
* Specific property changes
* @type {AuditLogChange[]}
*/
this.changes = data.changes ? data.changes.map(c => ({ key: c.key, old: c.old_value, new: c.new_value })) : null;
/**
* The ID of this entry
@@ -188,15 +201,20 @@ class GuildAuditLogsEntry {
removed: data.options.members_removed,
days: data.options.delete_member_days,
};
} else if (data.action_type === Actions.MESSAGE_DELETE) {
this.extra = {
count: data.options.count,
channel: guild.channels.get(data.options.channel_id),
};
} else {
switch (data.options.type) {
case 'member':
this.extra = guild.members.get(this.options.id);
if (!this.extra) this.extra = { id: this.options.id };
this.extra = guild.members.get(data.options.id);
if (!this.extra) this.extra = { id: data.options.id };
break;
case 'role':
this.extra = guild.roles.get(this.options.id);
if (!this.extra) this.extra = { id: this.options.id, name: this.options.role_name };
this.extra = guild.roles.get(data.options.id);
if (!this.extra) this.extra = { id: data.options.id, name: data.options.role_name };
break;
default:
break;
@@ -217,12 +235,14 @@ class GuildAuditLogsEntry {
return this.target;
});
} else if (targetType === Targets.INVITE) {
const change = this.changes.find(c => c.name === 'code');
const change = this.changes.find(c => c.key === 'code');
this.target = guild.fetchInvites()
.then(invites => {
this.target = invites.find(i => i.code === (change.new || change.old));
this.target = invites.find(i => i.code === (change.new_value || change.old_value));
return this.target;
});
} else if (targetType === Targets.MESSAGE) {
this.target = guild.client.users.get(data.target_id);
} else {
this.target = guild[`${targetType.toLowerCase()}s`].get(data.target_id);
}

View File

@@ -3,6 +3,7 @@ const Role = require('./Role');
const PermissionOverwrites = require('./PermissionOverwrites');
const Permissions = require('../util/Permissions');
const Collection = require('../util/Collection');
const Constants = require('../util/Constants');
/**
* Represents a guild channel (i.e. text channels and voice channels).
@@ -72,6 +73,9 @@ class GuildChannel extends Channel {
const roles = member.roles;
for (const role of roles.values()) permissions |= role.permissions;
const admin = Boolean(permissions & Permissions.FLAGS.ADMINISTRATOR);
if (admin) return new Permissions(Permissions.ALL);
const overwrites = this.overwritesFor(member, true, roles);
if (overwrites.everyone) {
@@ -91,9 +95,6 @@ class GuildChannel extends Channel {
permissions |= overwrites.member.allow;
}
const admin = Boolean(permissions & Permissions.FLAGS.ADMINISTRATOR);
if (admin) permissions = Permissions.ALL;
return new Permissions(member, permissions);
}
@@ -136,18 +137,19 @@ class GuildChannel extends Channel {
/**
* Overwrites the permissions for a user or role in this channel.
* @param {RoleResolvable|UserResolvable} userOrRole The user or role to update
* @param {Role|Snowflake|UserResolvable} userOrRole The user or role to update
* @param {PermissionOverwriteOptions} options The configuration for the update
* @param {string} [reason] Reason for creating/editing this overwrite
* @returns {Promise}
* @example
* // Overwrite permissions for a message author
* message.channel.overwritePermissions(message.author, {
* SEND_MESSAGES: false
* SEND_MESSAGES: false
* })
* .then(() => console.log('Done!'))
* .catch(console.error);
* .then(() => console.log('Done!'))
* .catch(console.error);
*/
overwritePermissions(userOrRole, options) {
overwritePermissions(userOrRole, options, reason) {
const payload = {
allow: 0,
deny: 0,
@@ -186,7 +188,7 @@ class GuildChannel extends Channel {
}
}
return this.client.rest.methods.setChannelOverwrite(this, payload);
return this.client.rest.methods.setChannelOverwrite(this, payload, reason);
}
/**
@@ -202,29 +204,31 @@ class GuildChannel extends Channel {
/**
* Edits the channel.
* @param {ChannelData} data The new data for the channel
* @param {string} [reason] Reason for editing this channel
* @returns {Promise<GuildChannel>}
* @example
* // Edit a channel
* channel.edit({name: 'new-channel'})
* .then(c => console.log(`Edited channel ${c}`))
* .catch(console.error);
* .then(c => console.log(`Edited channel ${c}`))
* .catch(console.error);
*/
edit(data) {
return this.client.rest.methods.updateChannel(this, data);
edit(data, reason) {
return this.client.rest.methods.updateChannel(this, data, reason);
}
/**
* Set a new name for the guild channel.
* @param {string} name The new name for the guild channel
* @param {string} [reason] Reason for changing the guild channel's name
* @returns {Promise<GuildChannel>}
* @example
* // Set a new channel name
* channel.setName('not_general')
* .then(newChannel => console.log(`Channel's new name is ${newChannel.name}`))
* .catch(console.error);
* .then(newChannel => console.log(`Channel's new name is ${newChannel.name}`))
* .catch(console.error);
*/
setName(name) {
return this.edit({ name });
setName(name, reason) {
return this.edit({ name }, reason);
}
/**
@@ -235,8 +239,8 @@ class GuildChannel extends Channel {
* @example
* // Set a new channel position
* channel.setPosition(2)
* .then(newChannel => console.log(`Channel's new position is ${newChannel.position}`))
* .catch(console.error);
* .then(newChannel => console.log(`Channel's new position is ${newChannel.position}`))
* .catch(console.error);
*/
setPosition(position, relative) {
return this.guild.setChannelPosition(this, position, relative).then(() => this);
@@ -245,34 +249,32 @@ class GuildChannel extends Channel {
/**
* Set a new topic for the guild channel.
* @param {string} topic The new topic for the guild channel
* @param {string} [reason] Reason for changing the guild channel's topic
* @returns {Promise<GuildChannel>}
* @example
* // Set a new channel topic
* channel.setTopic('needs more rate limiting')
* .then(newChannel => console.log(`Channel's new topic is ${newChannel.topic}`))
* .catch(console.error);
* .then(newChannel => console.log(`Channel's new topic is ${newChannel.topic}`))
* .catch(console.error);
*/
setTopic(topic) {
return this.client.rest.methods.updateChannel(this, { topic });
setTopic(topic, reason) {
return this.edit({ topic }, reason);
}
/**
* Options given when creating a guild channel invite.
* @typedef {Object} InviteOptions
*/
/**
* Create an invite to this guild channel.
* @param {InviteOptions} [options={}] Options for the invite
* <warn>This is only available when using a bot account.</warn>
* @param {Object} [options={}] Options for the invite
* @param {boolean} [options.temporary=false] Whether members that joined via the invite should be automatically
* kicked after 24 hours if they have not yet received a role
* @param {number} [options.maxAge=86400] How long the invite should last (in seconds, 0 for forever)
* @param {number} [options.maxUses=0] Maximum number of uses
* @param {boolean} [options.unique=false] Create a unique invite, or use an existing one with similar settings
* @param {string} [reason] Reason for creating the invite
* @returns {Promise<Invite>}
*/
createInvite(options = {}) {
return this.client.rest.methods.createChannelInvite(this, options);
createInvite(options = {}, reason) {
return this.client.rest.methods.createChannelInvite(this, options, reason);
}
/**
@@ -280,13 +282,28 @@ class GuildChannel extends Channel {
* @param {string} [name=this.name] Optional name for the new channel, otherwise it has the name of this channel
* @param {boolean} [withPermissions=true] Whether to clone the channel with this channel's permission overwrites
* @param {boolean} [withTopic=true] Whether to clone the channel with this channel's topic
* @param {string} [reason] Reason for cloning this channel
* @returns {Promise<GuildChannel>}
*/
clone(name = this.name, withPermissions = true, withTopic = true) {
return this.guild.createChannel(name, this.type, withPermissions ? this.permissionOverwrites : [])
clone(name = this.name, withPermissions = true, withTopic = true, reason) {
return this.guild.createChannel(name, this.type, withPermissions ? this.permissionOverwrites : [], reason)
.then(channel => withTopic ? channel.setTopic(this.topic) : channel);
}
/**
* Deletes this channel.
* @param {string} [reason] Reason for deleting this channel
* @returns {Promise<GuildChannel>}
* @example
* // Delete the channel
* channel.delete('making room for new channels')
* .then(channel => console.log(`Deleted ${channel.name} to make room for new channels`))
* .catch(console.error); // Log error
*/
delete(reason) {
return this.client.rest.methods.deleteChannel(this, reason);
}
/**
* Checks if this channel has the same type, topic, position, name, overwrites and ID as another channel.
* In most cases, a simple `channel.id === channel2.id` will do, and is much faster too.
@@ -322,6 +339,36 @@ class GuildChannel extends Channel {
this.permissionsFor(this.client.user).has(Permissions.FLAGS.MANAGE_CHANNELS);
}
/**
* Whether the channel is muted
* <warn>This is only available when using a user account.</warn>
* @type {?boolean}
* @readonly
*/
get muted() {
if (this.client.user.bot) return null;
try {
return this.client.user.guildSettings.get(this.guild.id).channelOverrides.get(this.id).muted;
} catch (err) {
return false;
}
}
/**
* The type of message that should notify you
* <warn>This is only available when using a user account.</warn>
* @type {?MessageNotificationType}
* @readonly
*/
get messageNotifications() {
if (this.client.user.bot) return null;
try {
return this.client.user.guildSettings.get(this.guild.id).channelOverrides.get(this.id).messageNotifications;
} catch (err) {
return Constants.MessageNotificationTypes[3];
}
}
/**
* When concatenated with a string, this automatically returns the channel's mention instead of the Channel object.
* @returns {string}

View File

@@ -345,28 +345,31 @@ class GuildMember {
/**
* Edit a guild member.
* @param {GuildMemberEditData} data The data to edit the member with
* @param {string} [reason] Reason for editing this user
* @returns {Promise<GuildMember>}
*/
edit(data) {
return this.client.rest.methods.updateGuildMember(this, data);
edit(data, reason) {
return this.client.rest.methods.updateGuildMember(this, data, reason);
}
/**
* Mute/unmute a user.
* @param {boolean} mute Whether or not the member should be muted
* @param {string} [reason] Reason for muting or unmuting
* @returns {Promise<GuildMember>}
*/
setMute(mute) {
return this.edit({ mute });
setMute(mute, reason) {
return this.edit({ mute }, reason);
}
/**
* Deafen/undeafen a user.
* @param {boolean} deaf Whether or not the member should be deafened
* @param {string} [reason] Reason for deafening or undeafening
* @returns {Promise<GuildMember>}
*/
setDeaf(deaf) {
return this.edit({ deaf });
setDeaf(deaf, reason) {
return this.edit({ deaf }, reason);
}
/**
@@ -381,29 +384,32 @@ class GuildMember {
/**
* Sets the roles applied to the member.
* @param {Collection<Snowflake, Role>|Role[]|Snowflake[]} roles The roles or role IDs to apply
* @param {string} [reason] Reason for applying the roles
* @returns {Promise<GuildMember>}
*/
setRoles(roles) {
return this.edit({ roles });
setRoles(roles, reason) {
return this.edit({ roles }, reason);
}
/**
* Adds a single role to the member.
* @param {Role|Snowflake} role The role or ID of the role to add
* @param {string} [reason] Reason for adding the role
* @returns {Promise<GuildMember>}
*/
addRole(role) {
addRole(role, reason) {
if (!(role instanceof Role)) role = this.guild.roles.get(role);
if (!role) throw new TypeError('Supplied parameter was neither a Role nor a Snowflake.');
return this.client.rest.methods.addMemberRole(this, role);
if (!role) return Promise.reject(new TypeError('Supplied parameter was neither a Role nor a Snowflake.'));
return this.client.rest.methods.addMemberRole(this, role, reason);
}
/**
* Adds multiple roles to the member.
* @param {Collection<Snowflake, Role>|Role[]|Snowflake[]} roles The roles or role IDs to add
* @param {string} [reason] Reason for adding the roles
* @returns {Promise<GuildMember>}
*/
addRoles(roles) {
addRoles(roles, reason) {
let allRoles;
if (roles instanceof Collection) {
allRoles = this._roles.slice();
@@ -411,26 +417,28 @@ class GuildMember {
} else {
allRoles = this._roles.concat(roles);
}
return this.edit({ roles: allRoles });
return this.edit({ roles: allRoles }, reason);
}
/**
* Removes a single role from the member.
* @param {Role|Snowflake} role The role or ID of the role to remove
* @param {string} [reason] Reason for removing the role
* @returns {Promise<GuildMember>}
*/
removeRole(role) {
removeRole(role, reason) {
if (!(role instanceof Role)) role = this.guild.roles.get(role);
if (!role) throw new TypeError('Supplied parameter was neither a Role nor a Snowflake.');
return this.client.rest.methods.removeMemberRole(this, role);
if (!role) return Promise.reject(new TypeError('Supplied parameter was neither a Role nor a Snowflake.'));
return this.client.rest.methods.removeMemberRole(this, role, reason);
}
/**
* Removes multiple roles from the member.
* @param {Collection<Snowflake, Role>|Role[]|Snowflake[]} roles The roles or role IDs to remove
* @param {string} [reason] Reason for removing the roles
* @returns {Promise<GuildMember>}
*/
removeRoles(roles) {
removeRoles(roles, reason) {
const allRoles = this._roles.slice();
if (roles instanceof Collection) {
for (const role of roles.values()) {
@@ -443,16 +451,17 @@ class GuildMember {
if (index >= 0) allRoles.splice(index, 1);
}
}
return this.edit({ roles: allRoles });
return this.edit({ roles: allRoles }, reason);
}
/**
* Set the nickname for the guild member.
* @param {string} nick The nickname for the guild member
* @param {string} [reason] Reason for setting the nickname
* @returns {Promise<GuildMember>}
*/
setNickname(nick) {
return this.edit({ nick });
setNickname(nick, reason) {
return this.edit({ nick }, reason);
}
/**
@@ -481,7 +490,7 @@ class GuildMember {
}
/**
* Ban this guild member
* Ban this guild member.
* @param {Object|number|string} [options] Ban options. If a number, the number of days to delete messages for, if a
* string, the ban reason. Supplying an object allows you to do both.
* @param {number} [options.days=0] Number of days of messages to delete

View File

@@ -2,27 +2,6 @@ const PartialGuild = require('./PartialGuild');
const PartialGuildChannel = require('./PartialGuildChannel');
const Constants = require('../util/Constants');
/*
{ max_age: 86400,
code: 'CG9A5',
guild:
{ splash: null,
id: '123123123',
icon: '123123123',
name: 'name' },
created_at: '2016-08-28T19:07:04.763368+00:00',
temporary: false,
uses: 0,
max_uses: 0,
inviter:
{ username: '123',
discriminator: '4204',
bot: true,
id: '123123123',
avatar: '123123123' },
channel: { type: 0, id: '123123', name: 'heavy-testing' } }
*/
/**
* Represents an invitation to a guild channel.
* <warn>The only guaranteed properties are `code`, `guild` and `channel`. Other properties can be missing.</warn>
@@ -54,6 +33,30 @@ class Invite {
*/
this.code = data.code;
/**
* The approximate number of online members of the guild this invite is for
* @type {number}
*/
this.presenceCount = data.approximate_presence_count;
/**
* The approximate total number of members of the guild this invite is for
* @type {number}
*/
this.memberCount = data.approximate_member_count;
/**
* The number of text channels the guild this invite goes to has
* @type {number}
*/
this.textChannelCount = data.guild.text_channel_count;
/**
* The number of voice channels the guild this invite goes to has
* @type {number}
*/
this.voiceChannelCount = data.guild.voice_channel_count;
/**
* Whether or not this invite is temporary
* @type {boolean}
@@ -138,10 +141,11 @@ class Invite {
/**
* Deletes this invite.
* @param {string} [reason] Reason for deleting this invite
* @returns {Promise<Invite>}
*/
delete() {
return this.client.rest.methods.deleteInvite(this);
delete(reason) {
return this.client.rest.methods.deleteInvite(this, reason);
}
/**

View File

@@ -1,6 +1,7 @@
const Mentions = require('./MessageMentions');
const Attachment = require('./MessageAttachment');
const Embed = require('./MessageEmbed');
const RichEmbed = require('./RichEmbed');
const MessageReaction = require('./MessageReaction');
const ReactionCollector = require('./ReactionCollector');
const Util = require('../util/Util');
@@ -33,7 +34,7 @@ class Message {
setup(data) { // eslint-disable-line complexity
/**
* The ID of the message (unique in the channel it was sent)
* The ID of the message
* @type {Snowflake}
*/
this.id = data.id;
@@ -57,8 +58,8 @@ class Message {
this.author = this.client.dataManager.newUser(data.author);
/**
* Represents the author of the message as a guild member. Only available if the message comes from a guild
* where the author is still a member.
* Represents the author of the message as a guild member
* Only available if the message comes from a guild where the author is still a member
* @type {?GuildMember}
*/
this.member = this.guild ? this.guild.member(this.author) || null : null;
@@ -209,8 +210,8 @@ class Message {
}
/**
* The message contents with all mentions replaced by the equivalent text. If mentions cannot be resolved to a name,
* the relevant mention in the message content will not be converted
* The message contents with all mentions replaced by the equivalent text.
* If mentions cannot be resolved to a name, the relevant mention in the message content will not be converted.
* @type {string}
* @readonly
*/
@@ -254,8 +255,8 @@ class Message {
* @example
* // Create a reaction collector
* const collector = message.createReactionCollector(
* (reaction, user) => reaction.emoji.id === '👌' && user.id === 'someID',
* { time: 15000 }
* (reaction, user) => reaction.emoji.name === '👌' && user.id === 'someID',
* { time: 15000 }
* );
* collector.on('collect', r => console.log(`Collected ${r.emoji.name}`));
* collector.on('end', collected => console.log(`Collected ${collected.size} items`));
@@ -271,8 +272,8 @@ class Message {
*/
/**
* Similar to createCollector but in promise form. Resolves with a collection of reactions that pass the specified
* filter.
* Similar to createMessageCollector but in promise form.
* Resolves with a collection of reactions that pass the specified filter.
* @param {CollectorFilter} filter The filter function to use
* @param {AwaitReactionsOptions} [options={}] Optional options to pass to the internal collector
* @returns {Promise<Collection<string, MessageReaction>>}
@@ -365,13 +366,13 @@ class Message {
/**
* Edit the content of the message.
* @param {StringResolvable} [content] The new content for the message
* @param {MessageEditOptions} [options] The options to provide
* @param {MessageEditOptions|RichEmbed} [options] The options to provide
* @returns {Promise<Message>}
* @example
* // Update the content of a message
* message.edit('This is my new content!')
* .then(msg => console.log(`Updated the content of a message from ${msg.author}`))
* .catch(console.error);
* .then(msg => console.log(`Updated the content of a message from ${msg.author}`))
* .catch(console.error);
*/
edit(content, options) {
if (!options && typeof content === 'object' && !(content instanceof Array)) {
@@ -380,6 +381,7 @@ class Message {
} else if (!options) {
options = {};
}
if (options instanceof RichEmbed) options = { embed: options };
return this.client.rest.methods.updateMessage(this, content, options);
}
@@ -388,6 +390,7 @@ class Message {
* @param {string} lang The language for the code block
* @param {StringResolvable} content The new content for the message
* @returns {Promise<Message>}
* @deprecated
*/
editCode(lang, content) {
content = Util.escapeMarkdown(this.client.resolver.resolveString(content), true);
@@ -437,8 +440,8 @@ class Message {
* @example
* // Delete a message
* message.delete()
* .then(msg => console.log(`Deleted message from ${msg.author}`))
* .catch(console.error);
* .then(msg => console.log(`Deleted message from ${msg.author}`))
* .catch(console.error);
*/
delete(timeout = 0) {
if (timeout <= 0) {
@@ -460,8 +463,8 @@ class Message {
* @example
* // Reply to a message
* message.reply('Hey, I\'m a reply!')
* .then(msg => console.log(`Sent a reply to ${msg.author}`))
* .catch(console.error);
* .then(msg => console.log(`Sent a reply to ${msg.author}`))
* .catch(console.error);
*/
reply(content, options) {
if (!options && typeof content === 'object' && !(content instanceof Array)) {
@@ -533,7 +536,7 @@ class Message {
}
_addReaction(emoji, user) {
const emojiID = emoji.id ? `${emoji.name}:${emoji.id}` : encodeURIComponent(emoji.name);
const emojiID = emoji.id ? `${emoji.name}:${emoji.id}` : emoji.name;
let reaction;
if (this.reactions.has(emojiID)) {
reaction = this.reactions.get(emojiID);
@@ -542,13 +545,15 @@ class Message {
reaction = new MessageReaction(this, emoji, 0, user.id === this.client.user.id);
this.reactions.set(emojiID, reaction);
}
if (!reaction.users.has(user.id)) reaction.users.set(user.id, user);
reaction.count++;
if (!reaction.users.has(user.id)) {
reaction.users.set(user.id, user);
reaction.count++;
}
return reaction;
}
_removeReaction(emoji, user) {
const emojiID = emoji.id ? `${emoji.name}:${emoji.id}` : encodeURIComponent(emoji.name);
const emojiID = emoji.id ? `${emoji.name}:${emoji.id}` : emoji.name;
if (this.reactions.has(emojiID)) {
const reaction = this.reactions.get(emojiID);
if (reaction.users.has(user.id)) {

View File

@@ -12,7 +12,6 @@ const util = require('util');
* @extends {Collector}
*/
class MessageCollector extends Collector {
/**
* @param {TextChannel|DMChannel|GroupDMChannel} channel The channel
* @param {CollectorFilter} filter The filter to be applied to this collector
@@ -23,7 +22,8 @@ class MessageCollector extends Collector {
super(channel.client, filter, options);
/**
* @type {TextBasedChannel} channel The channel
* The channel
* @type {TextBasedChannel}
*/
this.channel = channel;
@@ -61,7 +61,7 @@ class MessageCollector extends Collector {
/**
* Handle an incoming message for possible collection.
* @param {Message} message The message that could be collected
* @returns {?{key: Snowflake, value: Message}} Message data to collect
* @returns {?{key: Snowflake, value: Message}}
* @private
*/
handle(message) {

View File

@@ -102,7 +102,7 @@ class MessageMentions {
/**
* Any channels that were mentioned
* @type {?Collection<Snowflake, GuildChannel>}
* @type {Collection<Snowflake, GuildChannel>}
* @readonly
*/
get channels() {
@@ -124,7 +124,7 @@ class MessageMentions {
MessageMentions.EVERYONE_PATTERN = /@(everyone|here)/g;
/**
* Regular expression that globally matches user mentions like `<#81440962496172032>`
* Regular expression that globally matches user mentions like `<@81440962496172032>`
* @type {RegExp}
*/
MessageMentions.USERS_PATTERN = /<@!?[0-9]+>/g;

View File

@@ -37,7 +37,7 @@ class OAuth2Application {
/**
* The app's icon hash
* @type {string}
* @type {?string}
*/
this.icon = data.icon;
@@ -124,6 +124,7 @@ class OAuth2Application {
/**
* Reset the app's secret and bot token.
* <warn>This is only available when using a user account.</warn>
* @returns {OAuth2Application}
*/
reset() {

View File

@@ -33,10 +33,11 @@ class PermissionOverwrites {
/**
* Delete this Permission Overwrite.
* @param {string} [reason] Reason for deleting this overwrite
* @returns {Promise<PermissionOverwrites>}
*/
delete() {
return this.channel.client.rest.methods.deletePermissionOverwrites(this);
delete(reason) {
return this.channel.client.rest.methods.deletePermissionOverwrites(this, reason);
}
}

View File

@@ -13,7 +13,6 @@ const Collection = require('../util/Collection');
* @extends {Collector}
*/
class ReactionCollector extends Collector {
/**
* @param {Message} message The message upon which to collect reactions
* @param {CollectorFilter} filter The filter to apply to this collector
@@ -46,7 +45,7 @@ class ReactionCollector extends Collector {
/**
* Handle an incoming reaction for possible collection.
* @param {MessageReaction} reaction The reaction to possibly collect
* @returns {?{key: Snowflake, value: MessageReaction}} Reaction data to collect
* @returns {?{key: Snowflake, value: MessageReaction}}
* @private
*/
handle(reaction) {

View File

@@ -38,7 +38,7 @@ class ReactionEmoji {
* Creates the text required to form a graphical emoji on Discord.
* @example
* // Send the emoji used in a reaction to the channel the reaction is part of
* reaction.message.channel.sendMessage(`The emoji used is ${reaction.emoji}`);
* reaction.message.channel.send(`The emoji used is ${reaction.emoji}`);
* @returns {string}
*/
toString() {

View File

@@ -1,4 +1,5 @@
const ClientDataResolver = require('../client/ClientDataResolver');
const Attachment = require('./Attachment');
let ClientDataResolver;
/**
* A rich embed to be sent with a message with a fluent interface for creation.
@@ -68,7 +69,7 @@ class RichEmbed {
/**
* File to upload alongside this Embed
* @type {string}
* @type {FileOptions|string|Attachment}
*/
this.file = data.file;
}
@@ -113,6 +114,7 @@ class RichEmbed {
* @returns {RichEmbed} This embed
*/
setColor(color) {
if (!ClientDataResolver) ClientDataResolver = require('../client/ClientDataResolver');
this.color = ClientDataResolver.resolveColor(color);
return this;
}
@@ -203,11 +205,13 @@ class RichEmbed {
/**
* Sets the file to upload alongside the embed. This file can be accessed via `attachment://fileName.extension` when
* setting an embed image or author/footer icons. Only one file may be attached.
* @param {FileOptions|string} file Local path or URL to the file to attach, or valid FileOptions for a file to attach
* @param {FileOptions|string|Attachment} file Local path or URL to the file to attach,
* or valid FileOptions for a file to attach
* @returns {RichEmbed} This embed
*/
attachFile(file) {
if (this.file) throw new RangeError('You may not upload more than one file at once.');
if (file instanceof Attachment) file = file.file;
this.file = file;
return this;
}

View File

@@ -135,7 +135,7 @@ class Role {
}
/**
* Get an object mapping permission names to whether or not the role enables that permission
* Get an object mapping permission names to whether or not the role enables that permission.
* @returns {Object<string, boolean>}
* @example
* // Print the serialized role permissions
@@ -195,64 +195,68 @@ class Role {
* @property {ColorResolvable} [color] The color of the role, either a hex string or a base 10 number
* @property {boolean} [hoist] Whether or not the role should be hoisted
* @property {number} [position] The position of the role
* @property {string[]} [permissions] The permissions of the role
* @property {PermissionResolvable[]|number} [permissions] The permissions of the role
* @property {boolean} [mentionable] Whether or not the role should be mentionable
*/
/**
* Edits the role.
* @param {RoleData} data The new data for the role
* @param {string} [reason] The reason for editing this role
* @returns {Promise<Role>}
* @example
* // Edit a role
* role.edit({name: 'new role'})
* .then(r => console.log(`Edited role ${r}`))
* .catch(console.error);
* .then(r => console.log(`Edited role ${r}`))
* .catch(console.error);
*/
edit(data) {
return this.client.rest.methods.updateGuildRole(this, data);
edit(data, reason) {
return this.client.rest.methods.updateGuildRole(this, data, reason);
}
/**
* Set a new name for the role.
* @param {string} name The new name of the role
* @param {string} [reason] Reason for changing the role's name
* @returns {Promise<Role>}
* @example
* // Set the name of the role
* role.setName('new role')
* .then(r => console.log(`Edited name of role ${r}`))
* .catch(console.error);
* .then(r => console.log(`Edited name of role ${r}`))
* .catch(console.error);
*/
setName(name) {
return this.edit({ name });
setName(name, reason) {
return this.edit({ name }, reason);
}
/**
* Set a new color for the role.
* @param {ColorResolvable} color The color of the role
* @param {string} [reason] Reason for changing the role's color
* @returns {Promise<Role>}
* @example
* // Set the color of a role
* role.setColor('#FF0000')
* .then(r => console.log(`Set color of role ${r}`))
* .catch(console.error);
* .then(r => console.log(`Set color of role ${r}`))
* .catch(console.error);
*/
setColor(color) {
return this.edit({ color });
setColor(color, reason) {
return this.edit({ color }, reason);
}
/**
* Set whether or not the role should be hoisted.
* @param {boolean} hoist Whether or not to hoist the role
* @param {string} [reason] Reason for setting whether or not the role should be hoisted
* @returns {Promise<Role>}
* @example
* // Set the hoist of the role
* role.setHoist(true)
* .then(r => console.log(`Role hoisted: ${r.hoist}`))
* .catch(console.error);
* .then(r => console.log(`Role hoisted: ${r.hoist}`))
* .catch(console.error);
*/
setHoist(hoist) {
return this.edit({ hoist });
setHoist(hoist, reason) {
return this.edit({ hoist }, reason);
}
/**
@@ -263,8 +267,8 @@ class Role {
* @example
* // Set the position of the role
* role.setPosition(1)
* .then(r => console.log(`Role position: ${r.position}`))
* .catch(console.error);
* .then(r => console.log(`Role position: ${r.position}`))
* .catch(console.error);
*/
setPosition(position, relative) {
return this.guild.setRolePosition(this, position, relative).then(() => this);
@@ -273,42 +277,45 @@ class Role {
/**
* Set the permissions of the role.
* @param {string[]} permissions The permissions of the role
* @param {string} [reason] Reason for changing the role's permissions
* @returns {Promise<Role>}
* @example
* // Set the permissions of the role
* role.setPermissions(['KICK_MEMBERS', 'BAN_MEMBERS'])
* .then(r => console.log(`Role updated ${r}`))
* .catch(console.error);
* .then(r => console.log(`Role updated ${r}`))
* .catch(console.error);
*/
setPermissions(permissions) {
return this.edit({ permissions });
setPermissions(permissions, reason) {
return this.edit({ permissions }, reason);
}
/**
* Set whether this role is mentionable.
* @param {boolean} mentionable Whether this role should be mentionable
* @param {string} [reason] Reason for setting whether or not this role should be mentionable
* @returns {Promise<Role>}
* @example
* // Make the role mentionable
* role.setMentionable(true)
* .then(r => console.log(`Role updated ${r}`))
* .catch(console.error);
* .then(r => console.log(`Role updated ${r}`))
* .catch(console.error);
*/
setMentionable(mentionable) {
return this.edit({ mentionable });
setMentionable(mentionable, reason) {
return this.edit({ mentionable }, reason);
}
/**
* Deletes the role.
* @param {string} [reason] Reason for deleting the role
* @returns {Promise<Role>}
* @example
* // Delete a role
* role.delete()
* .then(r => console.log(`Deleted role ${r}`))
* .catch(console.error);
* .then(r => console.log(`Deleted role ${r}`))
* .catch(console.error);
*/
delete() {
return this.client.rest.methods.deleteGuildRole(this);
delete(reason) {
return this.client.rest.methods.deleteGuildRole(this, reason);
}
/**

View File

@@ -24,6 +24,13 @@ class TextChannel extends GuildChannel {
*/
this.topic = data.topic;
/**
* If the Discord considers this channel NSFW
* @type {boolean}
* @readonly
*/
this.nsfw = Boolean(data.nsfw);
this.lastMessageID = data.last_message_id;
}
@@ -42,15 +49,6 @@ class TextChannel extends GuildChannel {
return members;
}
/**
* If the Discord considers this channel NSFW
* @type {boolean}
* @readonly
*/
get nsfw() {
return /^nsfw(-|$)/.test(this.name);
}
/**
* Fetch all webhooks for the channel.
* @returns {Promise<Collection<Snowflake, Webhook>>}
@@ -62,23 +60,22 @@ class TextChannel extends GuildChannel {
/**
* Create a webhook for the channel.
* @param {string} name The name of the webhook
* @param {BufferResolvable|Base64Resolvable} avatar The avatar for the webhook
* @param {BufferResolvable|Base64Resolvable} [avatar] The avatar for the webhook
* @param {string} [reason] Reason for creating this webhook
* @returns {Promise<Webhook>} webhook The created webhook
* @example
* channel.createWebhook('Snek', 'http://snek.s3.amazonaws.com/topSnek.png')
* .then(webhook => console.log(`Created webhook ${webhook}`))
* .catch(console.error)
* channel.createWebhook('Snek', 'https://i.imgur.com/mI8XcpG.jpg')
* .then(webhook => console.log(`Created webhook ${webhook}`))
* .catch(console.error)
*/
createWebhook(name, avatar) {
return new Promise(resolve => {
if (typeof avatar === 'string' && avatar.startsWith('data:')) {
resolve(this.client.rest.methods.createWebhook(this, name, avatar));
} else {
this.client.resolver.resolveBuffer(avatar).then(data =>
resolve(this.client.rest.methods.createWebhook(this, name, data))
);
}
});
createWebhook(name, avatar, reason) {
if (typeof avatar === 'string' && avatar.startsWith('data:')) {
return this.client.rest.methods.createWebhook(this, name, avatar, reason);
} else {
return this.client.resolver.resolveImage(avatar).then(data =>
this.client.rest.methods.createWebhook(this, name, data, reason)
);
}
}
// These are here only for documentation purposes - they are implemented by TextBasedChannel

View File

@@ -10,9 +10,9 @@ const Snowflake = require('../util/Snowflake');
class User {
constructor(client, data) {
/**
* The client that created the instance of the the user
* The client that created the instance of the user
* @name User#client
* @type {}
* @type {Client}
* @readonly
*/
Object.defineProperty(this, 'client', { value: client });

View File

@@ -13,7 +13,7 @@ class UserProfile {
this.user = user;
/**
* The client that created the instance of the the UserProfile.
* The client that created the instance of the UserProfile
* @name UserProfile#client
* @type {Client}
* @readonly

View File

@@ -25,7 +25,7 @@ class VoiceChannel extends GuildChannel {
* The bitrate of this voice channel
* @type {number}
*/
this.bitrate = data.bitrate;
this.bitrate = data.bitrate * 0.001;
/**
* The maximum amount of users allowed in this channel - 0 means unlimited.
@@ -76,31 +76,34 @@ class VoiceChannel extends GuildChannel {
}
/**
* Sets the bitrate of the channel.
* Sets the bitrate of the channel (in kbps).
* @param {number} bitrate The new bitrate
* @param {string} [reason] Reason for changing the channel's bitrate
* @returns {Promise<VoiceChannel>}
* @example
* // Set the bitrate of a voice channel
* voiceChannel.setBitrate(48000)
* .then(vc => console.log(`Set bitrate to ${vc.bitrate} for ${vc.name}`))
* .catch(console.error);
* voiceChannel.setBitrate(48)
* .then(vc => console.log(`Set bitrate to ${vc.bitrate}kbps for ${vc.name}`))
* .catch(console.error);
*/
setBitrate(bitrate) {
return this.edit({ bitrate });
setBitrate(bitrate, reason) {
bitrate *= 1000;
return this.edit({ bitrate }, reason);
}
/**
* Sets the user limit of the channel.
* @param {number} userLimit The new user limit
* @param {string} [reason] Reason for changing the user limit
* @returns {Promise<VoiceChannel>}
* @example
* // Set the user limit of a voice channel
* voiceChannel.setUserLimit(42)
* .then(vc => console.log(`Set user limit to ${vc.userLimit} for ${vc.name}`))
* .catch(console.error);
* .then(vc => console.log(`Set user limit to ${vc.userLimit} for ${vc.name}`))
* .catch(console.error);
*/
setUserLimit(userLimit) {
return this.edit({ userLimit });
setUserLimit(userLimit, reason) {
return this.edit({ userLimit }, reason);
}
/**
@@ -109,8 +112,8 @@ class VoiceChannel extends GuildChannel {
* @example
* // Join a voice channel
* voiceChannel.join()
* .then(connection => console.log('Connected!'))
* .catch(console.error);
* .then(connection => console.log('Connected!'))
* .catch(console.error);
*/
join() {
if (this.client.browser) return Promise.reject(new Error('Voice connections are not available in browsers.'));

View File

@@ -1,4 +1,7 @@
const path = require('path');
const Util = require('../util/Util');
const Attachment = require('./Attachment');
const RichEmbed = require('./RichEmbed');
/**
* Represents a webhook.
@@ -36,7 +39,7 @@ class Webhook {
/**
* The avatar for the webhook
* @type {string}
* @type {?string}
*/
this.avatar = data.avatar;
@@ -76,11 +79,12 @@ class Webhook {
* @property {string} [avatarURL] Avatar URL override for the message
* @property {boolean} [tts=false] Whether or not the message should be spoken aloud
* @property {string} [nonce=''] The nonce for the message
* @property {Object[]} [embeds] An array of embeds for the message
* @property {Array<RichEmbed|Object>} [embeds] An array of embeds for the message
* (see [here](https://discordapp.com/developers/docs/resources/channel#embed-object) for more details)
* @property {boolean} [disableEveryone=this.client.options.disableEveryone] Whether or not @everyone and @here
* should be replaced with plain-text
* @property {FileOptions|string} [file] A file to send with the message
* @property {FileOptions|BufferResolvable|Attachment} [file] A file to send with the message **(deprecated)**
* @property {FileOptions[]|BufferResolvable[]|Attachment[]} [files] Files to send with the message
* @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.
@@ -89,39 +93,87 @@ class Webhook {
/**
* Send a message with this webhook.
* @param {StringResolvable} content The content to send
* @param {WebhookMessageOptions} [options={}] The options to provide
* @param {WebhookMessageOptions|Attachment|RichEmbed} [options] The options to provide
* can also be just a RichEmbed or Attachment
* @returns {Promise<Message|Message[]>}
* @example
* // Send a message
* webhook.send('hello!')
* .then(message => console.log(`Sent message: ${message.content}`))
* .catch(console.error);
* .then(message => console.log(`Sent message: ${message.content}`))
* .catch(console.error);
*/
send(content, options) {
send(content, options) { // eslint-disable-line complexity
if (!options && typeof content === 'object' && !(content instanceof Array)) {
options = content;
content = '';
} else if (!options) {
options = {};
}
if (options.file) {
if (typeof options.file === 'string') options.file = { attachment: options.file };
if (!options.file.name) {
if (typeof options.file.attachment === 'string') {
options.file.name = path.basename(options.file.attachment);
} else if (options.file.attachment && options.file.attachment.path) {
options.file.name = path.basename(options.file.attachment.path);
} else {
options.file.name = 'file.jpg';
if (options instanceof Attachment) options = { files: [options] };
if (options instanceof RichEmbed) options = { embeds: [options] };
if (content) {
content = this.client.resolver.resolveString(content);
let { split, code, disableEveryone } = options;
if (split && typeof split !== 'object') split = {};
if (typeof code !== 'undefined' && (typeof code !== 'boolean' || code === true)) {
content = Util.escapeMarkdown(content, true);
content = `\`\`\`${typeof code !== 'boolean' ? code || '' : ''}\n${content}\n\`\`\``;
if (split) {
split.prepend = `\`\`\`${typeof code !== 'boolean' ? code || '' : ''}\n`;
split.append = '\n```';
}
}
return this.client.resolver.resolveBuffer(options.file.attachment).then(file =>
this.client.rest.methods.sendWebhookMessage(this, content, options, {
file,
name: options.file.name,
})
);
if (disableEveryone || (typeof disableEveryone === 'undefined' && this.client.options.disableEveryone)) {
content = content.replace(/@(everyone|here)/g, '@\u200b$1');
}
if (split) content = Util.splitMessage(content, split);
}
if (options.file) {
if (options.files) options.files.push(options.file);
else options.files = [options.file];
}
if (options.embeds) {
const files = [];
for (const embed of options.embeds) {
if (embed.file) files.push(embed.file);
}
if (options.files) options.files.push(...files);
else options.files = files;
}
if (options.files) {
for (let i = 0; i < options.files.length; i++) {
let file = options.files[i];
if (typeof file === 'string' || Buffer.isBuffer(file)) file = { attachment: file };
if (!file.name) {
if (typeof file.attachment === 'string') {
file.name = path.basename(file.attachment);
} else if (file.attachment && file.attachment.path) {
file.name = path.basename(file.attachment.path);
} else if (file instanceof Attachment) {
file = { attachment: file.file, name: path.basename(file.file) || 'file.jpg' };
} else {
file.name = 'file.jpg';
}
} else if (file instanceof Attachment) {
file = file.file;
}
options.files[i] = file;
}
return Promise.all(options.files.map(file =>
this.client.resolver.resolveFile(file.attachment).then(resource => {
file.file = resource;
return file;
})
)).then(files => this.client.rest.methods.sendWebhookMessage(this, content, options, files));
}
return this.client.rest.methods.sendWebhookMessage(this, content, options);
}
@@ -130,6 +182,7 @@ class Webhook {
* @param {StringResolvable} content The content to send
* @param {WebhookMessageOptions} [options={}] The options to provide
* @returns {Promise<Message|Message[]>}
* @deprecated
* @example
* // Send a message
* webhook.sendMessage('hello!')
@@ -147,6 +200,7 @@ class Webhook {
* @param {StringResolvable} [content] Text message to send with the attachment
* @param {WebhookMessageOptions} [options] The options to provide
* @returns {Promise<Message>}
* @deprecated
*/
sendFile(attachment, name, content, options = {}) {
return this.send(content, Object.assign(options, { file: { attachment, name } }));
@@ -158,6 +212,7 @@ class Webhook {
* @param {StringResolvable} content Content of the code block
* @param {WebhookMessageOptions} options The options to provide
* @returns {Promise<Message|Message[]>}
* @deprecated
*/
sendCode(lang, content, options = {}) {
return this.send(content, Object.assign(options, { code: lang }));
@@ -187,28 +242,25 @@ class Webhook {
/**
* Edit the webhook.
* @param {string} name The new name for the webhook
* @param {BufferResolvable} avatar The new avatar for the webhook
* @param {BufferResolvable} [avatar] The new avatar for the webhook
* @returns {Promise<Webhook>}
*/
edit(name = this.name, avatar) {
if (avatar) {
return this.client.resolver.resolveBuffer(avatar).then(file => {
const dataURI = this.client.resolver.resolveBase64(file);
return this.client.rest.methods.editWebhook(this, name, dataURI);
});
return this.client.resolver.resolveImage(avatar).then(data =>
this.client.rest.methods.editWebhook(this, name, data)
);
}
return this.client.rest.methods.editWebhook(this, name).then(data => {
this.setup(data);
return this;
});
return this.client.rest.methods.editWebhook(this, name);
}
/**
* Delete the webhook.
* @param {string} [reason] Reason for deleting the webhook
* @returns {Promise}
*/
delete() {
return this.client.rest.methods.deleteWebhook(this);
delete(reason) {
return this.client.rest.methods.deleteWebhook(this, reason);
}
}

View File

@@ -5,7 +5,8 @@ const EventEmitter = require('events').EventEmitter;
* Filter to be applied to the collector.
* @typedef {Function} CollectorFilter
* @param {...*} args Any arguments received by the listener
* @returns {boolean} To collect or not collect
* @param {Collection} collection The items collected by this collector
* @returns {boolean}
*/
/**
@@ -78,7 +79,7 @@ class Collector extends EventEmitter {
*/
_handle(...args) {
const collect = this.handle(...args);
if (!collect || !this.filter(...args)) return;
if (!collect || !this.filter(...args, this.collected)) return;
this.collected.set(collect.key, collect.value);

View File

@@ -2,6 +2,8 @@ const path = require('path');
const Message = require('../Message');
const MessageCollector = require('../MessageCollector');
const Collection = require('../../util/Collection');
const Attachment = require('../../structures/Attachment');
const RichEmbed = require('../../structures/RichEmbed');
const util = require('util');
/**
@@ -38,8 +40,8 @@ class TextBasedChannel {
* (see [here](https://discordapp.com/developers/docs/resources/channel#embed-object) for more details)
* @property {boolean} [disableEveryone=this.client.options.disableEveryone] Whether or not @everyone and @here
* should be replaced with plain-text
* @property {FileOptions|string} [file] A file to send with the message **(deprecated)**
* @property {FileOptions[]|string[]} [files] Files to send with the message
* @property {FileOptions|BufferResolvable|Attachment} [file] A file to send with the message **(deprecated)**
* @property {FileOptions[]|BufferResolvable[]|Attachment[]} [files] Files to send with the message
* @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
@@ -64,13 +66,14 @@ class TextBasedChannel {
/**
* Send a message to this channel.
* @param {StringResolvable} [content] Text for the message
* @param {MessageOptions} [options={}] Options for the message
* @param {MessageOptions|Attachment|RichEmbed} [options] Options for the message,
* can also be just a RichEmbed or Attachment
* @returns {Promise<Message|Message[]>}
* @example
* // Send a message
* channel.send('hello!')
* .then(message => console.log(`Sent message: ${message.content}`))
* .catch(console.error);
* .then(message => console.log(`Sent message: ${message.content}`))
* .catch(console.error);
*/
send(content, options) {
if (!options && typeof content === 'object' && !(content instanceof Array)) {
@@ -80,7 +83,13 @@ class TextBasedChannel {
options = {};
}
if (options.embed && options.embed.file) options.file = options.embed.file;
if (options instanceof Attachment) options = { files: [options.file] };
if (options instanceof RichEmbed) options = { embed: options };
if (options.embed && options.embed.file) {
if (options.files) options.files.push(options.embed.file);
else options.files = [options.embed.file];
}
if (options.file) {
if (options.files) options.files.push(options.file);
@@ -88,24 +97,28 @@ class TextBasedChannel {
}
if (options.files) {
for (const i in options.files) {
for (let i = 0; i < options.files.length; i++) {
let file = options.files[i];
if (typeof file === 'string') file = { attachment: file };
if (typeof file === 'string' || Buffer.isBuffer(file)) file = { attachment: file };
if (!file.name) {
if (typeof file.attachment === 'string') {
file.name = path.basename(file.attachment);
} else if (file.attachment && file.attachment.path) {
file.name = path.basename(file.attachment.path);
} else if (file instanceof Attachment) {
file = { attachment: file.file, name: path.basename(file.file) || 'file.jpg' };
} else {
file.name = 'file.jpg';
}
} else if (file instanceof Attachment) {
file = file.file;
}
options.files[i] = file;
}
return Promise.all(options.files.map(file =>
this.client.resolver.resolveBuffer(file.attachment).then(buffer => {
file.file = buffer;
this.client.resolver.resolveFile(file.attachment).then(resource => {
file.file = resource;
return file;
})
)).then(files => this.client.rest.methods.sendMessage(this, content, options, files));
@@ -158,8 +171,8 @@ class TextBasedChannel {
* @example
* // Get messages
* channel.fetchMessages({limit: 10})
* .then(messages => console.log(`Received ${messages.size} messages`))
* .catch(console.error);
* .then(messages => console.log(`Received ${messages.size} messages`))
* .catch(console.error);
*/
fetchMessages(options = {}) {
return this.client.rest.methods.getChannelMessages(this, options).then(data => {
@@ -214,15 +227,21 @@ class TextBasedChannel {
* @property {Date} [before] Date to find messages before
* @property {Date} [after] Date to find messages before
* @property {Date} [during] Date to find messages during (range of date to date + 24 hours)
* @property {boolean} [nsfw=false] Include results from NSFW channels
*/
/**
* @typedef {Object} MessageSearchResult
* @property {number} totalResults Total result count
* @property {Message[][]} messages Array of message results
* The message which has triggered the result will have the `hit` property set to `true`
*/
/**
* Performs a search within the channel.
* <warn>This is only available when using a user account.</warn>
* @param {MessageSearchOptions} [options={}] Options to pass to the search
* @returns {Promise<Array<Message[]>>}
* An array containing arrays of messages. Each inner array is a search context cluster
* The message which has triggered the result will have the `hit` property set to `true`
* @returns {Promise<MessageSearchResult>}
* @example
* channel.search({
* content: 'discord.js',
@@ -319,11 +338,11 @@ class TextBasedChannel {
* @returns {MessageCollector}
* @example
* // Create a message collector
* const collector = channel.createCollector(
* m => m.content.includes('discord'),
* { time: 15000 }
* const collector = channel.createMessageCollector(
* m => m.content.includes('discord'),
* { time: 15000 }
* );
* collector.on('message', m => console.log(`Collected ${m.content}`));
* collector.on('collect', m => console.log(`Collected ${m.content}`));
* collector.on('end', collected => console.log(`Collected ${collected.size} items`));
*/
createMessageCollector(filter, options = {}) {
@@ -347,8 +366,8 @@ class TextBasedChannel {
* const filter = m => m.content.startsWith('!vote');
* // Errors: ['time'] treats ending because of the time limit as an error
* channel.awaitMessages(filter, { max: 4, time: 60000, errors: ['time'] })
* .then(collected => console.log(collected.size))
* .catch(collected => console.log(`After a minute, only ${collected.size} out of 4 voted.`));
* .then(collected => console.log(collected.size))
* .catch(collected => console.log(`After a minute, only ${collected.size} out of 4 voted.`));
*/
awaitMessages(filter, options = {}) {
return new Promise((resolve, reject) => {

View File

@@ -59,59 +59,99 @@ class Collection extends Map {
}
/**
* Obtains the first item in this collection.
* @returns {*}
* Obtains the first value(s) in this collection.
* @param {number} [count] Number of values to obtain from the beginning
* @returns {*|Array<*>} The single value if `count` is undefined, or an array of values of `count` length
*/
first() {
return this.values().next().value;
first(count) {
if (count === undefined) return this.values().next().value;
if (typeof count !== 'number') throw new TypeError('The count must be a number.');
if (!Number.isInteger(count) || count < 1) throw new RangeError('The count must be an integer greater than 0.');
count = Math.min(this.size, count);
const arr = new Array(count);
const iter = this.values();
for (let i = 0; i < count; i++) arr[i] = iter.next().value;
return arr;
}
/**
* Obtains the first key in this collection.
* @returns {*}
* Obtains the first key(s) in this collection.
* @param {number} [count] Number of keys to obtain from the beginning
* @returns {*|Array<*>} The single key if `count` is undefined, or an array of keys of `count` length
*/
firstKey() {
return this.keys().next().value;
firstKey(count) {
if (count === undefined) return this.keys().next().value;
if (typeof count !== 'number') throw new TypeError('The count must be a number.');
if (!Number.isInteger(count) || count < 1) throw new RangeError('The count must be an integer greater than 0.');
count = Math.min(this.size, count);
const arr = new Array(count);
const iter = this.iter();
for (let i = 0; i < count; i++) arr[i] = iter.next().value;
return arr;
}
/**
* Obtains the last item in this collection. This relies on the `array()` method, and thus the caching mechanism
* applies here as well.
* @returns {*}
* Obtains the last value(s) in this collection. This relies on {@link Collection#array}, and thus the caching
* mechanism applies here as well.
* @param {number} [count] Number of values to obtain from the end
* @returns {*|Array<*>} The single value if `count` is undefined, or an array of values of `count` length
*/
last() {
last(count) {
const arr = this.array();
return arr[arr.length - 1];
if (count === undefined) return arr[arr.length - 1];
if (typeof count !== 'number') throw new TypeError('The count must be a number.');
if (!Number.isInteger(count) || count < 1) throw new RangeError('The count must be an integer greater than 0.');
return arr.slice(-count);
}
/**
* Obtains the last key in this collection. This relies on the `keyArray()` method, and thus the caching mechanism
* applies here as well.
* @returns {*}
* Obtains the last key(s) in this collection. This relies on {@link Collection#keyArray}, and thus the caching
* mechanism applies here as well.
* @param {number} [count] Number of keys to obtain from the end
* @returns {*|Array<*>} The single key if `count` is undefined, or an array of keys of `count` length
*/
lastKey() {
lastKey(count) {
const arr = this.keyArray();
return arr[arr.length - 1];
if (count === undefined) return arr[arr.length - 1];
if (typeof count !== 'number') throw new TypeError('The count must be a number.');
if (!Number.isInteger(count) || count < 1) throw new RangeError('The count must be an integer greater than 0.');
return arr.slice(-count);
}
/**
* Obtains a random item from this collection. This relies on the `array()` method, and thus the caching mechanism
* applies here as well.
* @returns {*}
* Obtains random value(s) from this collection. This relies on {@link Collection#array}, and thus the caching
* mechanism applies here as well.
* @param {number} [count] Number of values to obtain randomly
* @returns {*|Array<*>} The single value if `count` is undefined, or an array of values of `count` length
*/
random() {
const arr = this.array();
return arr[Math.floor(Math.random() * arr.length)];
random(count) {
let arr = this.array();
if (count === undefined) return arr[Math.floor(Math.random() * arr.length)];
if (typeof count !== 'number') throw new TypeError('The count must be a number.');
if (!Number.isInteger(count) || count < 1) throw new RangeError('The count must be an integer greater than 0.');
if (arr.length === 0) return [];
const rand = new Array(count);
arr = arr.slice();
for (let i = 0; i < count; i++) rand[i] = arr.splice(Math.floor(Math.random() * arr.length), 1)[0];
return rand;
}
/**
* Obtains a random key from this collection. This relies on the `keyArray()` method, and thus the caching mechanism
* applies here as well.
* @returns {*}
* Obtains random key(s) from this collection. This relies on {@link Collection#keyArray}, and thus the caching
* mechanism applies here as well.
* @param {number} [count] Number of keys to obtain randomly
* @returns {*|Array<*>} The single key if `count` is undefined, or an array of keys of `count` length
*/
randomKey() {
const arr = this.keyArray();
return arr[Math.floor(Math.random() * arr.length)];
randomKey(count) {
let arr = this.keyArray();
if (count === undefined) return arr[Math.floor(Math.random() * arr.length)];
if (typeof count !== 'number') throw new TypeError('The count must be a number.');
if (!Number.isInteger(count) || count < 1) throw new RangeError('The count must be an integer greater than 0.');
if (arr.length === 0) return [];
const rand = new Array(count);
arr = arr.slice();
for (let i = 0; i < count; i++) rand[i] = arr.splice(Math.floor(Math.random() * arr.length), 1)[0];
return rand;
}
/**

View File

@@ -29,6 +29,7 @@ exports.Package = require('../../package.json');
* 100% certain you don't need, as many are important, but not obviously so. The safest one to disable with the
* most impact is typically `TYPING_START`.
* @property {WebsocketOptions} [ws] Options for the WebSocket
* @property {HTTPOptions} [http] HTTP options
*/
exports.DefaultOptions = {
apiRequestMethod: 'sequential',
@@ -63,6 +64,15 @@ exports.DefaultOptions = {
},
version: 6,
},
/**
* HTTP options
* @typedef {Object} HTTPOptions
* @property {number} [version=7] API version to use
* @property {string} [api='https://discordapp.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
*/
http: {
version: 7,
host: 'https://discordapp.com',
@@ -102,7 +112,10 @@ const Endpoints = exports.Endpoints = {
relationships: `${base}/relationships`,
settings: `${base}/settings`,
Relationship: uID => `${base}/relationships/${uID}`,
Guild: guildID => `${base}/guilds/${guildID}`,
Guild: guildID => ({
toString: () => `${base}/guilds/${guildID}`,
settings: `${base}/guilds/${guildID}/settings`,
}),
Note: id => `${base}/notes/${id}`,
Mentions: (limit, roles, everyone, guildID) =>
`${base}/mentions?limit=${limit}&roles=${roles}&everyone=${everyone}${guildID ? `&guild_id=${guildID}` : ''}`,
@@ -133,7 +146,7 @@ const Endpoints = exports.Endpoints = {
ack: `${base}/ack`,
settings: `${base}/settings`,
auditLogs: `${base}/audit-logs`,
Emoji: emojiID => Endpoints.CDN(root).Emoji(emojiID),
Emoji: emojiID => `${base}/emojis/${emojiID}`,
Icon: (root, hash) => Endpoints.CDN(root).Icon(guildID, hash),
Splash: (root, hash) => Endpoints.CDN(root).Splash(guildID, hash),
Role: roleID => `${base}/roles/${roleID}`,
@@ -164,6 +177,7 @@ const Endpoints = exports.Endpoints = {
webhooks: `${base}/webhooks`,
search: `${base}/messages/search`,
pins: `${base}/pins`,
Icon: (root, hash) => Endpoints.CDN(root).GDMIcon(channelID, hash),
Pin: messageID => `${base}/pins/${messageID}`,
Recipient: recipientID => `${base}/recipients/${recipientID}`,
Message: messageID => {
@@ -192,6 +206,7 @@ const Endpoints = exports.Endpoints = {
Asset: name => `${root}/assets/${name}`,
Avatar: (userID, hash) => `${root}/avatars/${userID}/${hash}.${hash.startsWith('a_') ? 'gif' : 'png'}?size=2048`,
Icon: (guildID, hash) => `${root}/icons/${guildID}/${hash}.jpg`,
GDMIcon: (channelID, hash) => `${root}/channel-icons/${channelID}/${hash}.jpg?size=2048`,
Splash: (guildID, hash) => `${root}/splashes/${guildID}/${hash}.jpg`,
};
},
@@ -200,7 +215,8 @@ const Endpoints = exports.Endpoints = {
const base = `/oauth2/applications/${appID}`;
return {
toString: () => base,
reset: `${base}/reset`,
resetSecret: `${base}/reset`,
resetToken: `${base}/bot/reset`,
};
},
App: appID => `/oauth2/authorize?client_id=${appID}`,
@@ -212,7 +228,7 @@ const Endpoints = exports.Endpoints = {
toString: () => '/gateway',
bot: '/gateway/bot',
},
Invite: inviteID => `/invite/${inviteID}`,
Invite: inviteID => `/invite/${inviteID}?with_counts=true`,
inviteLink: id => `https://discord.gg/${id}`,
Webhook: (webhookID, token) => `/webhooks/${webhookID}${token ? `/${token}` : ''}`,
};
@@ -220,12 +236,12 @@ const Endpoints = exports.Endpoints = {
/**
* The current status of the client. Here are the available statuses:
* - READY
* - CONNECTING
* - RECONNECTING
* - IDLE
* - NEARLY
* - DISCONNECTED
* * READY
* * CONNECTING
* * RECONNECTING
* * IDLE
* * NEARLY
* * DISCONNECTED
* @typedef {number} Status
*/
exports.Status = {
@@ -239,11 +255,11 @@ exports.Status = {
/**
* The current status of a voice connection. Here are the available statuses:
* - CONNECTED
* - CONNECTING
* - AUTHENTICATING
* - RECONNECTING
* - DISCONNECTED
* * CONNECTED
* * CONNECTING
* * AUTHENTICATING
* * RECONNECTING
* * DISCONNECTED
* @typedef {number} VoiceStatus
*/
exports.VoiceStatus = {
@@ -287,6 +303,7 @@ exports.VoiceOPCodes = {
exports.Events = {
READY: 'ready',
RESUME: 'resume',
GUILD_CREATE: 'guildCreate',
GUILD_DELETE: 'guildDelete',
GUILD_UPDATE: 'guildUpdate',
@@ -320,6 +337,7 @@ exports.Events = {
USER_UPDATE: 'userUpdate',
USER_NOTE_UPDATE: 'userNoteUpdate',
USER_SETTINGS_UPDATE: 'clientUserSettingsUpdate',
USER_GUILD_SETTINGS_UPDATE: 'clientUserGuildSettingsUpdate',
PRESENCE_UPDATE: 'presenceUpdate',
VOICE_STATE_UPDATE: 'voiceStateUpdate',
TYPING_START: 'typingStart',
@@ -333,41 +351,41 @@ exports.Events = {
/**
* The type of a websocket message event, e.g. `MESSAGE_CREATE`. Here are the available events:
* - READY
* - RESUMED
* - GUILD_SYNC
* - GUILD_CREATE
* - GUILD_DELETE
* - GUILD_UPDATE
* - GUILD_MEMBER_ADD
* - GUILD_MEMBER_REMOVE
* - GUILD_MEMBER_UPDATE
* - GUILD_MEMBERS_CHUNK
* - GUILD_ROLE_CREATE
* - GUILD_ROLE_DELETE
* - GUILD_ROLE_UPDATE
* - GUILD_BAN_ADD
* - GUILD_BAN_REMOVE
* - CHANNEL_CREATE
* - CHANNEL_DELETE
* - CHANNEL_UPDATE
* - CHANNEL_PINS_UPDATE
* - MESSAGE_CREATE
* - MESSAGE_DELETE
* - MESSAGE_UPDATE
* - MESSAGE_DELETE_BULK
* - MESSAGE_REACTION_ADD
* - MESSAGE_REACTION_REMOVE
* - MESSAGE_REACTION_REMOVE_ALL
* - USER_UPDATE
* - USER_NOTE_UPDATE
* - USER_SETTINGS_UPDATE
* - PRESENCE_UPDATE
* - VOICE_STATE_UPDATE
* - TYPING_START
* - VOICE_SERVER_UPDATE
* - RELATIONSHIP_ADD
* - RELATIONSHIP_REMOVE
* * READY
* * RESUMED
* * GUILD_SYNC
* * GUILD_CREATE
* * GUILD_DELETE
* * GUILD_UPDATE
* * GUILD_MEMBER_ADD
* * GUILD_MEMBER_REMOVE
* * GUILD_MEMBER_UPDATE
* * GUILD_MEMBERS_CHUNK
* * GUILD_ROLE_CREATE
* * GUILD_ROLE_DELETE
* * GUILD_ROLE_UPDATE
* * GUILD_BAN_ADD
* * GUILD_BAN_REMOVE
* * CHANNEL_CREATE
* * CHANNEL_DELETE
* * CHANNEL_UPDATE
* * CHANNEL_PINS_UPDATE
* * MESSAGE_CREATE
* * MESSAGE_DELETE
* * MESSAGE_UPDATE
* * MESSAGE_DELETE_BULK
* * MESSAGE_REACTION_ADD
* * MESSAGE_REACTION_REMOVE
* * MESSAGE_REACTION_REMOVE_ALL
* * USER_UPDATE
* * USER_NOTE_UPDATE
* * USER_SETTINGS_UPDATE
* * PRESENCE_UPDATE
* * VOICE_STATE_UPDATE
* * TYPING_START
* * VOICE_SERVER_UPDATE
* * RELATIONSHIP_ADD
* * RELATIONSHIP_REMOVE
* @typedef {string} WSEventType
*/
exports.WSEvents = {
@@ -401,6 +419,7 @@ exports.WSEvents = {
USER_UPDATE: 'USER_UPDATE',
USER_NOTE_UPDATE: 'USER_NOTE_UPDATE',
USER_SETTINGS_UPDATE: 'USER_SETTINGS_UPDATE',
USER_GUILD_SETTINGS_UPDATE: 'USER_GUILD_SETTINGS_UPDATE',
PRESENCE_UPDATE: 'PRESENCE_UPDATE',
VOICE_STATE_UPDATE: 'VOICE_STATE_UPDATE',
TYPING_START: 'TYPING_START',
@@ -409,6 +428,18 @@ exports.WSEvents = {
RELATIONSHIP_REMOVE: 'RELATIONSHIP_REMOVE',
};
/**
* The type of a message, e.g. `DEFAULT`. Here are the available types:
* * DEFAULT
* * RECIPIENT_ADD
* * RECIPIENT_REMOVE
* * CALL
* * CHANNEL_NAME_CHANGE
* * CHANNEL_ICON_CHANGE
* * PINS_ADD
* * GUILD_MEMBER_JOIN
* @typedef {string} MessageType
*/
exports.MessageTypes = [
'DEFAULT',
'RECIPIENT_ADD',
@@ -420,6 +451,21 @@ exports.MessageTypes = [
'GUILD_MEMBER_JOIN',
];
/**
* The type of a message notification setting. Here are the available types:
* * EVERYTHING
* * MENTIONS
* * NOTHING
* * INHERIT (only for GuildChannel)
* @typedef {string} MessageNotificationType
*/
exports.MessageNotificationTypes = [
'EVERYTHING',
'MENTIONS',
'NOTHING',
'INHERIT',
];
exports.DefaultAvatars = {
BLURPLE: '6debd47ed13483642cf09e832ed0bc1b',
GREY: '322c936a8c8be1b803cd94861bdfa868',
@@ -543,8 +589,8 @@ exports.UserSettingsMap = {
explicit_content_filter: function explicitContentFilter(type) { // eslint-disable-line func-name-matching
/**
* Safe direct messaging; force people's messages with images to be scanned before they are sent to you
* one of `DISABLED`, `NON_FRIENDS`, `FRIENDS_AND_NON_FRIENDS`
* Safe direct messaging; force people's messages with images to be scanned before they are sent to you.
* One of `DISABLED`, `NON_FRIENDS`, `FRIENDS_AND_NON_FRIENDS`
* @name ClientUserSettings#explicitContentFilter
* @type {string}
*/
@@ -567,6 +613,58 @@ exports.UserSettingsMap = {
},
};
exports.UserGuildSettingsMap = {
message_notifications: function messageNotifications(type) { // eslint-disable-line func-name-matching
/**
* The type of message that should notify you
* @name ClientUserGuildSettings#messageNotifications
* @type {MessageNotificationType}
*/
return exports.MessageNotificationTypes[type];
},
/**
* Whether to receive mobile push notifications
* @name ClientUserGuildSettings#mobilePush
* @type {boolean}
*/
mobile_push: 'mobilePush',
/**
* Whether the guild is muted
* @name ClientUserGuildSettings#muted
* @type {boolean}
*/
muted: 'muted',
/**
* Whether to suppress everyone mention
* @name ClientUserGuildSettings#suppressEveryone
* @type {boolean}
*/
suppress_everyone: 'suppressEveryone',
/**
* A collection containing all the channel overrides
* @name ClientUserGuildSettings#channelOverrides
* @type {Collection<ClientUserChannelOverride>}
*/
channel_overrides: 'channelOverrides',
};
exports.UserChannelOverrideMap = {
message_notifications: function messageNotifications(type) { // eslint-disable-line func-name-matching
/**
* The type of message that should notify you
* @name ClientUserChannelOverride#messageNotifications
* @type {MessageNotificationType}
*/
return exports.MessageNotificationTypes[type];
},
/**
* Whether the channel is muted
* @name ClientUserChannelOverride#muted
* @type {boolean}
*/
muted: 'muted',
};
exports.Colors = {
DEFAULT: 0x000000,
AQUA: 0x1ABC9C,
@@ -594,3 +692,96 @@ exports.Colors = {
DARK_BUT_NOT_BLACK: 0x2C2F33,
NOT_QUITE_BLACK: 0x23272A,
};
/**
* An error encountered while performing an API request. Here are the potential errors:
* * UNKNOWN_ACCOUNT
* * UNKNOWN_APPLICATION
* * UNKNOWN_CHANNEL
* * UNKNOWN_GUILD
* * UNKNOWN_INTEGRATION
* * UNKNOWN_INVITE
* * UNKNOWN_MEMBER
* * UNKNOWN_MESSAGE
* * UNKNOWN_OVERWRITE
* * UNKNOWN_PROVIDER
* * UNKNOWN_ROLE
* * UNKNOWN_TOKEN
* * UNKNOWN_USER
* * UNKNOWN_EMOJI
* * BOT_PROHIBITED_ENDPOINT
* * BOT_ONLY_ENDPOINT
* * MAXIMUM_GUILDS
* * MAXIMUM_FRIENDS
* * MAXIMUM_PINS
* * MAXIMUM_ROLES
* * MAXIMUM_REACTIONS
* * UNAUTHORIZED
* * MISSING_ACCESS
* * INVALID_ACCOUNT_TYPE
* * CANNOT_EXECUTE_ON_DM
* * EMBED_DISABLED
* * CANNOT_EDIT_MESSAGE_BY_OTHER
* * CANNOT_SEND_EMPTY_MESSAGE
* * CANNOT_MESSAGE_USER
* * CANNOT_SEND_MESSAGES_IN_VOICE_CHANNEL
* * CHANNEL_VERIFICATION_LEVEL_TOO_HIGH
* * OAUTH2_APPLICATION_BOT_ABSENT
* * MAXIMUM_OAUTH2_APPLICATIONS
* * INVALID_OAUTH_STATE
* * MISSING_PERMISSIONS
* * INVALID_AUTHENTICATION_TOKEN
* * NOTE_TOO_LONG
* * INVALID_BULK_DELETE_QUANTITY
* * CANNOT_PIN_MESSAGE_IN_OTHER_CHANNEL
* * CANNOT_EXECUTE_ON_SYSTEM_MESSAGE
* * BULK_DELETE_MESSAGE_TOO_OLD
* * INVITE_ACCEPTED_TO_GUILD_NOT_CONTANING_BOT
* * REACTION_BLOCKED
* @typedef {string} APIError
*/
exports.APIErrors = {
UNKNOWN_ACCOUNT: 10001,
UNKNOWN_APPLICATION: 10002,
UNKNOWN_CHANNEL: 10003,
UNKNOWN_GUILD: 10004,
UNKNOWN_INTEGRATION: 10005,
UNKNOWN_INVITE: 10006,
UNKNOWN_MEMBER: 10007,
UNKNOWN_MESSAGE: 10008,
UNKNOWN_OVERWRITE: 10009,
UNKNOWN_PROVIDER: 10010,
UNKNOWN_ROLE: 10011,
UNKNOWN_TOKEN: 10012,
UNKNOWN_USER: 10013,
UNKNOWN_EMOJI: 10014,
BOT_PROHIBITED_ENDPOINT: 20001,
BOT_ONLY_ENDPOINT: 20002,
MAXIMUM_GUILDS: 30001,
MAXIMUM_FRIENDS: 30002,
MAXIMUM_PINS: 30003,
MAXIMUM_ROLES: 30005,
MAXIMUM_REACTIONS: 30010,
UNAUTHORIZED: 40001,
MISSING_ACCESS: 50001,
INVALID_ACCOUNT_TYPE: 50002,
CANNOT_EXECUTE_ON_DM: 50003,
EMBED_DISABLED: 50004,
CANNOT_EDIT_MESSAGE_BY_OTHER: 50005,
CANNOT_SEND_EMPTY_MESSAGE: 50006,
CANNOT_MESSAGE_USER: 50007,
CANNOT_SEND_MESSAGES_IN_VOICE_CHANNEL: 50008,
CHANNEL_VERIFICATION_LEVEL_TOO_HIGH: 50009,
OAUTH2_APPLICATION_BOT_ABSENT: 50010,
MAXIMUM_OAUTH2_APPLICATIONS: 50011,
INVALID_OAUTH_STATE: 50012,
MISSING_PERMISSIONS: 50013,
INVALID_AUTHENTICATION_TOKEN: 50014,
NOTE_TOO_LONG: 50015,
INVALID_BULK_DELETE_QUANTITY: 50016,
CANNOT_PIN_MESSAGE_IN_OTHER_CHANNEL: 50019,
CANNOT_EXECUTE_ON_SYSTEM_MESSAGE: 50021,
BULK_DELETE_MESSAGE_TOO_OLD: 50034,
INVITE_ACCEPTED_TO_GUILD_NOT_CONTANING_BOT: 50036,
REACTION_BLOCKED: 90001,
};

View File

@@ -103,7 +103,7 @@ class Permissions {
}
/**
* Gets an object mapping permission name (like `READ_MESSAGES`) to a {@link boolean} indicating whether the
* Gets an object mapping permission name (like `VIEW_CHANNEL`) to a {@link boolean} indicating whether the
* permission is available.
* @param {boolean} [checkAdmin=true] Whether to allow the administrator permission to override
* @returns {Object}
@@ -152,8 +152,8 @@ class Permissions {
/**
* Data that can be resolved to give a permission number. This can be:
* - A string (see {@link Permissions.flags})
* - A permission number
* * A string (see {@link Permissions.FLAGS})
* * A permission number
* @typedef {string|number} PermissionResolvable
*/
@@ -180,7 +180,8 @@ class Permissions {
* - `MANAGE_GUILD` (edit the guild information, region, etc.)
* - `ADD_REACTIONS` (add new reactions to messages)
* - `VIEW_AUDIT_LOG`
* - `READ_MESSAGES`
* - `VIEW_CHANNEL`
* - `READ_MESSAGES` **(deprecated)**
* - `SEND_MESSAGES`
* - `SEND_TTS_MESSAGES`
* - `MANAGE_MESSAGES` (delete messages and reactions)
@@ -215,6 +216,7 @@ Permissions.FLAGS = {
ADD_REACTIONS: 1 << 6,
VIEW_AUDIT_LOG: 1 << 7,
VIEW_CHANNEL: 1 << 10,
READ_MESSAGES: 1 << 10,
SEND_MESSAGES: 1 << 11,
SEND_TTS_MESSAGES: 1 << 12,
@@ -268,8 +270,8 @@ Permissions.prototype.missingPermissions = util.deprecate(Permissions.prototype.
'EvaluatedPermissions#missingPermissions is deprecated, use Permissions#missing instead');
Object.defineProperty(Permissions.prototype, 'member', {
get: util
.deprecate(Object.getOwnPropertyDescriptor(Permissions.prototype, 'member').get,
'EvaluatedPermissions#member is deprecated'),
.deprecate(Object.getOwnPropertyDescriptor(Permissions.prototype, 'member').get,
'EvaluatedPermissions#member is deprecated'),
});
module.exports = Permissions;

View File

@@ -66,9 +66,9 @@ class Util {
/**
* Parses emoji info out of a string. The string must be one of:
* - A UTF-8 emoji (no ID)
* - A URL-encoded UTF-8 emoji (no ID)
* - A Discord custom emoji (`<:name:id>`)
* * A UTF-8 emoji (no ID)
* * A URL-encoded UTF-8 emoji (no ID)
* * A Discord custom emoji (`<:name:id>`)
* @param {string} text Emoji string to parse
* @returns {Object} Object with `name` and `id` properties
* @private

View File

@@ -1,78 +1,78 @@
/* eslint no-console: 0 */
'use strict';
const Discord = require('../');
const ytdl = require('ytdl-core');
const client = new Discord.Client({ fetchAllMembers: false, apiRequestMethod: 'sequential' });
const auth = require('./auth.json');
client.login(auth.token).then(() => console.log('logged')).catch(console.error);
const connections = new Map();
let broadcast;
client.on('message', m => {
if (!m.guild) return;
if (m.content.startsWith('/join')) {
const channel = m.guild.channels.get(m.content.split(' ')[1]) || m.member.voiceChannel;
if (channel && channel.type === 'voice') {
channel.join().then(conn => {
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!');
});
} else {
m.reply('Specify a voice channel!');
}
} else if (m.content.startsWith('/play')) {
if (connections.has(m.guild.id)) {
const connData = connections.get(m.guild.id);
const queue = connData.queue;
const url = m.content.split(' ').slice(1).join(' ')
.replace(/</g, '')
.replace(/>/g, '');
queue.push({ url, m });
if (queue.length > 1) {
m.reply(`OK, that's going to play after ${queue.length - 1} songs`);
return;
}
doQueue(connData);
}
} else if (m.content.startsWith('/skip')) {
if (connections.has(m.guild.id)) {
const connData = connections.get(m.guild.id);
if (connData.dispatcher) {
connData.dispatcher.end();
}
}
} else if (m.content.startsWith('#eval') && m.author.id === '66564597481480192') {
try {
const com = eval(m.content.split(' ').slice(1).join(' '));
m.channel.sendMessage(`\`\`\`\n${com}\`\`\``);
} catch (e) {
console.log(e);
m.channel.sendMessage(`\`\`\`\n${e}\`\`\``);
}
}
});
function doQueue(connData) {
const conn = connData.conn;
const queue = connData.queue;
const item = queue[0];
if (!item) return;
const stream = ytdl(item.url, { filter: 'audioonly' }, { passes: 3 });
const dispatcher = conn.playStream(stream);
stream.on('info', info => {
item.m.reply(`OK, playing **${info.title}**`);
});
dispatcher.on('end', () => {
queue.shift();
doQueue(connData);
});
dispatcher.on('error', (...e) => console.log('dispatcher', ...e));
connData.dispatcher = dispatcher;
}
/* eslint no-console: 0 */
'use strict';
const Discord = require('../');
const ytdl = require('ytdl-core');
const client = new Discord.Client({ fetchAllMembers: false, apiRequestMethod: 'sequential' });
const auth = require('./auth.json');
client.login(auth.token).then(() => console.log('logged')).catch(console.error);
const connections = new Map();
let broadcast;
client.on('message', m => {
if (!m.guild) return;
if (m.content.startsWith('/join')) {
const channel = m.guild.channels.get(m.content.split(' ')[1]) || m.member.voiceChannel;
if (channel && channel.type === 'voice') {
channel.join().then(conn => {
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!');
});
} else {
m.reply('Specify a voice channel!');
}
} else if (m.content.startsWith('/play')) {
if (connections.has(m.guild.id)) {
const connData = connections.get(m.guild.id);
const queue = connData.queue;
const url = m.content.split(' ').slice(1).join(' ')
.replace(/</g, '')
.replace(/>/g, '');
queue.push({ url, m });
if (queue.length > 1) {
m.reply(`OK, that's going to play after ${queue.length - 1} songs`);
return;
}
doQueue(connData);
}
} else if (m.content.startsWith('/skip')) {
if (connections.has(m.guild.id)) {
const connData = connections.get(m.guild.id);
if (connData.dispatcher) {
connData.dispatcher.end();
}
}
} else if (m.content.startsWith('#eval') && m.author.id === '66564597481480192') {
try {
const com = eval(m.content.split(' ').slice(1).join(' '));
m.channel.sendMessage(`\`\`\`\n${com}\`\`\``);
} catch (e) {
console.log(e);
m.channel.sendMessage(`\`\`\`\n${e}\`\`\``);
}
}
});
function doQueue(connData) {
const conn = connData.conn;
const queue = connData.queue;
const item = queue[0];
if (!item) return;
const stream = ytdl(item.url, { filter: 'audioonly' }, { passes: 3 });
const dispatcher = conn.playStream(stream);
stream.on('info', info => {
item.m.reply(`OK, playing **${info.title}**`);
});
dispatcher.on('end', () => {
queue.shift();
doQueue(connData);
});
dispatcher.on('error', (...e) => console.log('dispatcher', ...e));
connData.dispatcher = dispatcher;
}

Submodule typings updated: b500eb2331...697fc933de

View File

@@ -5,19 +5,26 @@
const webpack = require('webpack');
const createVariants = require('parallel-webpack').createVariants;
const UglifyJSPlugin = require('uglifyjs-webpack-plugin');
const version = require('./package.json').version;
const createConfig = options => {
const plugins = [
new webpack.DefinePlugin({ 'global.GENTLY': false }),
new webpack.optimize.ModuleConcatenationPlugin(),
new webpack.DefinePlugin({
'process.env': {
__DISCORD_WEBPACK__: '"true"',
},
}),
];
if (options.minify) plugins.push(new webpack.optimize.UglifyJsPlugin({ minimize: true }));
if (options.minify) plugins.push(new UglifyJSPlugin({ uglifyOptions: { output: { comments: false } } }));
const filename = `./webpack/discord${process.env.VERSIONED === 'false' ? '' : '.' + version}${options.minify ? '.min' : ''}.js`; // eslint-disable-line
return {
entry: './src/index.js',
entry: './browser.js',
output: {
path: __dirname,
filename,