Merge branch 'master' into refactor/webpacks

This commit is contained in:
Gus Caplan
2017-12-02 15:11:44 -06:00
committed by GitHub
68 changed files with 1038 additions and 659 deletions

View File

@@ -127,7 +127,11 @@
"semi-spacing": "error",
"semi": "error",
"space-before-blocks": "error",
"space-before-function-paren": ["error", "never"],
"space-before-function-paren": ["error", {
"anonymous": "never",
"named": "never",
"asyncArrow": "always"
}],
"space-in-parens": "error",
"space-infix-ops": "error",
"space-unary-ops": "error",

View File

@@ -1,19 +1,19 @@
language: node_js
node_js:
- "8"
- 8
- 9
install: npm install
script: bash ./travis/test.sh
jobs:
include:
- stage: deploy
node_js: 9
script: bash ./travis/deploy.sh
env:
- ENCRYPTION_LABEL="af862fa96d3e"
- COMMIT_AUTHOR_EMAIL="amishshah.2k@gmail.com"
cache:
directories:
- node_modules
install: npm install
jobs:
include:
- stage: test
script: bash ./travis/test.sh
- stage: deploy
script: bash ./travis/deploy.sh
env:
global:
- ENCRYPTION_LABEL: "af862fa96d3e"
- COMMIT_AUTHOR_EMAIL: "amishshah.2k@gmail.com"
dist: trusty
sudo: false

View File

@@ -10,8 +10,7 @@
<a href="https://www.npmjs.com/package/discord.js"><img src="https://img.shields.io/npm/dt/discord.js.svg?maxAge=3600" alt="NPM downloads" /></a>
<a href="https://travis-ci.org/hydrabolt/discord.js"><img src="https://travis-ci.org/hydrabolt/discord.js.svg" alt="Build status" /></a>
<a href="https://david-dm.org/hydrabolt/discord.js"><img src="https://img.shields.io/david/hydrabolt/discord.js.svg?maxAge=3600" alt="Dependencies" /></a>
<a href="https://www.patreon.com/discordjs"><img
src="https://img.shields.io/badge/donate-patreon-F96854.svg" alt="Patreon" /></a>
<a href="https://www.patreon.com/discordjs"><img src="https://img.shields.io/badge/donate-patreon-F96854.svg" alt="Patreon" /></a>
</p>
<p>
<a href="https://nodei.co/npm/discord.js/"><img src="https://nodei.co/npm/discord.js.png?downloads=true&stars=true" alt="NPM info" /></a>
@@ -31,9 +30,9 @@ discord.js is a powerful [node.js](https://nodejs.org) module that allows you to
**Node.js 8.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`
With voice support ([opusscript](https://www.npmjs.com/package/opusscript)): `npm install discord.js opusscript --save`
Without voice support: `npm i discord.js`
With voice support ([node-opus](https://www.npmjs.com/package/node-opus)): `npm i discord.js node-opus`
With voice support ([opusscript](https://www.npmjs.com/package/opusscript)): `npm i discord.js opusscript`
### Audio engines
The preferred audio engine is node-opus, as it performs significantly better than opusscript. When both are available, discord.js will automatically choose node-opus.
@@ -41,13 +40,13 @@ Using opusscript is only recommended for development environments where node-opu
For production bots, using node-opus should be considered a necessity, especially if they're going to be running on multiple servers.
### Optional packages
- [zlib-sync](https://www.npmjs.com/package/zlib-sync) for significantly faster WebSocket data inflation (`npm install zlib-sync`)
- [bufferutil](https://www.npmjs.com/package/bufferutil) to greatly speed up the WebSocket when *not* using uws (`npm install bufferutil --save`)
- [erlpack](https://github.com/hammerandchisel/erlpack) for significantly faster WebSocket data (de)serialisation (`npm install discordapp/erlpack --save`)
- [zlib-sync](https://www.npmjs.com/package/zlib-sync) for significantly faster WebSocket data inflation (`npm i zlib-sync`)
- [erlpack](https://github.com/discordapp/erlpack) for significantly faster WebSocket data (de)serialisation (`npm i discordapp/erlpack`)
- One of the following packages can be installed for faster voice packet encryption and decryption:
- [sodium](https://www.npmjs.com/package/sodium) (`npm install sodium --save`)
- [libsodium.js](https://www.npmjs.com/package/libsodium-wrappers) (`npm install libsodium-wrappers --save`)
- [uws](https://www.npmjs.com/package/uws) for a much faster WebSocket connection (`npm install uws --save`)
- [sodium](https://www.npmjs.com/package/sodium) (`npm i sodium`)
- [libsodium.js](https://www.npmjs.com/package/libsodium-wrappers) (`npm i libsodium-wrappers`)
- [uws](https://www.npmjs.com/package/uws) for a much faster WebSocket connection (`npm i uws`)
- [bufferutil](https://www.npmjs.com/package/bufferutil) for a much faster WebSocket connection when *not* using uws (`npm i bufferutil`)
## Example usage
```js
@@ -70,11 +69,14 @@ client.login('your token');
## Links
* [Website](https://discord.js.org/) ([source](https://github.com/hydrabolt/discord.js-site))
* [Documentation](https://discord.js.org/#/docs)
* [Discord.js server](https://discord.gg/bRCvFy9)
* [Discord API server](https://discord.gg/rV4BwdK)
* [Discord.js Discord server](https://discord.gg/bRCvFy9)
* [Discord API Discord server](https://discord.gg/discord-api)
* [GitHub](https://github.com/hydrabolt/discord.js)
* [NPM](https://www.npmjs.com/package/discord.js)
* [Related libraries](https://discordapi.com/unofficial/libs.html) (see also [discord-rpc](https://www.npmjs.com/package/discord-rpc))
* [Related libraries](https://discordapi.com/unofficial/libs.html)
### Extensions
* [discord-rpc](https://www.npmjs.com/package/discord-rpc) ([github](https://github.com/devsnek/discord-rpc))
## Contributing
Before creating an issue, please ensure that it hasn't already been reported/suggested, and double-check the

View File

@@ -1,7 +1,7 @@
<div align="center">
<br />
<p>
<a href="https://discord.js.org"><img src="https://discord.js.org/static/logo.svg" width="546" alt="discord.js" /></a>
<a href="https://discord.js.org"><img src="https://discord.js.org/static/logo.svg" width="546" alt="discord.js" id="djs-logo" /></a>
</p>
<br />
<p>

View File

@@ -34,12 +34,11 @@
"runkitExampleFilename": "./docs/examples/ping.js",
"unpkg": "./webpack/discord.min.js",
"dependencies": {
"long": "^3.0.0",
"pako": "^1.0.0",
"prism-media": "^0.0.2",
"snekfetch": "^3.0.0",
"snekfetch": "^3.5.0",
"tweetnacl": "^1.0.0",
"ws": "^3.0.0"
"ws": "^3.3.1"
},
"peerDependencies": {
"bufferutil": "^3.0.0",
@@ -54,11 +53,11 @@
"devDependencies": {
"@types/node": "^8.0.0",
"discord.js-docgen": "hydrabolt/discord.js-docgen",
"eslint": "^4.0.0",
"eslint": "^4.11.0",
"jsdoc-strip-async-await": "^0.1.0",
"json-filter-loader": "^1.0.0",
"uglifyjs-webpack-plugin": "^1.0.0-beta.2",
"webpack": "^3.0.0"
"webpack": "^3.8.0"
},
"engines": {
"node": ">=8.0.0"

View File

@@ -42,6 +42,7 @@ class BaseClient extends EventEmitter {
/**
* API shortcut
* @type {Object}
* @readonly
* @private
*/
get api() {

View File

@@ -163,6 +163,7 @@ class Client extends BaseClient {
/**
* Timestamp of the latest ping's start time
* @type {number}
* @readonly
* @private
*/
get _pingTimestamp() {

View File

@@ -22,6 +22,7 @@ class ClientManager {
/**
* The status of the client
* @readonly
* @type {number}
*/
get status() {

View File

@@ -3,7 +3,7 @@ const BaseClient = require('./BaseClient');
/**
* The webhook client.
* @extends {Webhook}
* @implements {Webhook}
* @extends {BaseClient}
*/
class WebhookClient extends BaseClient {

View File

@@ -14,13 +14,11 @@ class ActionsManager {
this.register(require('./ChannelUpdate'));
this.register(require('./GuildDelete'));
this.register(require('./GuildUpdate'));
this.register(require('./GuildMemberGet'));
this.register(require('./GuildMemberRemove'));
this.register(require('./GuildBanRemove'));
this.register(require('./GuildRoleCreate'));
this.register(require('./GuildRoleDelete'));
this.register(require('./GuildRoleUpdate'));
this.register(require('./UserGet'));
this.register(require('./UserUpdate'));
this.register(require('./UserNoteUpdate'));
this.register(require('./GuildSync'));

View File

@@ -1,10 +0,0 @@
const Action = require('./Action');
class GuildMemberGetAction extends Action {
handle(guild, data) {
const member = guild.members.create(data);
return { member };
}
}
module.exports = GuildMemberGetAction;

View File

@@ -1,11 +0,0 @@
const Action = require('./Action');
class UserGetAction extends Action {
handle(data) {
const client = this.client;
const user = client.users.create(data);
return { user };
}
}
module.exports = UserGetAction;

View File

@@ -236,7 +236,7 @@ class VoiceBroadcast extends VolumeInterface {
}
/**
* Plays an arbitrary input that can be [handled by ffmpeg](https://ffmpeg.org/ffmpeg-protocols.html#Description)
* Plays an arbitrary input that can be [handled by ffmpeg](https://ffmpeg.org/ffmpeg-protocols.html#Description).
* @param {string} input The arbitrary input
* @param {StreamOptions} [options] Options for playing the stream
* @returns {VoiceBroadcast}

View File

@@ -15,7 +15,7 @@ const libs = {
exports.methods = {};
(async() => {
(async () => {
for (const libName of Object.keys(libs)) {
try {
const lib = require(libName);

View File

@@ -272,7 +272,7 @@ class WebSocketConnection extends EventEmitter {
try {
const packet = WebSocket.unpack(this.inflate.result);
this.onPacket(packet);
if (this.client.listenerCount('raw')) this.client.emit('raw', data);
if (this.client.listenerCount('raw')) this.client.emit('raw', packet);
} catch (err) {
this.client.emit('debug', err);
}

View File

@@ -1,6 +1,6 @@
const AbstractHandler = require('./AbstractHandler');
const { Events } = require('../../../../util/Constants');
const ClientUser = require('../../../../structures/ClientUser');
let ClientUser;
class ReadyHandler extends AbstractHandler {
handle(packet) {
@@ -12,6 +12,7 @@ class ReadyHandler extends AbstractHandler {
data.user.user_settings = data.user_settings;
data.user.user_guild_settings = data.user_guild_settings;
if (!ClientUser) ClientUser = require('../../../../structures/ClientUser');
const clientUser = new ClientUser(client, data.user);
client.user = clientUser;
client.readyAt = new Date();

View File

@@ -20,9 +20,13 @@ const Messages = {
SHARDING_REQUIRED: 'This session would have handled too many guilds - Sharding is required.',
SHARDING_CHILD_CONNECTION: 'Failed to send message to shard\'s process.',
SHARDING_PARENT_CONNECTION: 'Failed to send message to master process.',
SHARDING_NO_SHARDS: 'No shards have been spawned',
SHARDING_IN_PROCESS: 'Shards are still being spawned',
SHARDING_ALREADY_SPAWNED: count => `Already spawned ${count} shards`,
SHARDING_NO_SHARDS: 'No shards have been spawned.',
SHARDING_IN_PROCESS: 'Shards are still being spawned.',
SHARDING_ALREADY_SPAWNED: count => `Already spawned ${count} shards.`,
SHARDING_PROCESS_EXISTS: id => `Shard ${id} already has an active process.`,
SHARDING_READY_TIMEOUT: id => `Shard ${id}'s Client took too long to become ready.`,
SHARDING_READY_DISCONNECTED: id => `Shard ${id}'s Client disconnected before becoming ready.`,
SHARDING_READY_DIED: id => `Shard ${id}'s process exited before its Client became ready.`,
COLOR_RANGE: 'Color must be within the range 0 - 16777215 (0xFFFFFF).',
COLOR_CONVERT: 'Unable to convert color to a number.',

View File

@@ -15,23 +15,44 @@ module.exports = {
DataResolver: require('./util/DataResolver'),
DataStore: require('./stores/DataStore'),
DiscordAPIError: require('./rest/DiscordAPIError'),
EvaluatedPermissions: require('./util/Permissions'),
Permissions: require('./util/Permissions'),
Snowflake: require('./util/Snowflake'),
SnowflakeUtil: require('./util/Snowflake'),
Structures: require('./util/Structures'),
Util: Util,
util: Util,
version: require('../package.json').version,
// Stores
ChannelStore: require('./stores/ChannelStore'),
ClientPresenceStore: require('./stores/ClientPresenceStore'),
EmojiStore: require('./stores/EmojiStore'),
GuildChannelStore: require('./stores/GuildChannelStore'),
GuildMemberStore: require('./stores/GuildMemberStore'),
GuildStore: require('./stores/GuildStore'),
ReactionUserStore: require('./stores/ReactionUserStore'),
MessageStore: require('./stores/MessageStore'),
PresenceStore: require('./stores/PresenceStore'),
RoleStore: require('./stores/RoleStore'),
UserStore: require('./stores/UserStore'),
// Shortcuts to Util methods
escapeMarkdown: Util.escapeMarkdown,
fetchRecommendedShards: Util.fetchRecommendedShards,
splitMessage: Util.splitMessage,
// Structures
Base: require('./structures/Base'),
Activity: require('./structures/Presence').Activity,
CategoryChannel: require('./structures/CategoryChannel'),
Channel: require('./structures/Channel'),
ClientUser: require('./structures/ClientUser'),
ClientApplication: require('./structures/ClientApplication'),
get ClientUser() {
// This is a getter so that it properly extends any custom User class
return require('./structures/ClientUser');
},
ClientUserChannelOverride: require('./structures/ClientUserChannelOverride'),
ClientUserGuildSettings: require('./structures/ClientUserGuildSettings'),
ClientUserSettings: require('./structures/ClientUserSettings'),
Collector: require('./structures/interfaces/Collector'),
DMChannel: require('./structures/DMChannel'),
@@ -48,15 +69,17 @@ module.exports = {
MessageEmbed: require('./structures/MessageEmbed'),
MessageMentions: require('./structures/MessageMentions'),
MessageReaction: require('./structures/MessageReaction'),
ClientApplication: require('./structures/ClientApplication'),
PermissionOverwrites: require('./structures/PermissionOverwrites'),
Presence: require('./structures/Presence').Presence,
ReactionEmoji: require('./structures/ReactionEmoji'),
ReactionCollector: require('./structures/ReactionCollector'),
ReactionEmoji: require('./structures/ReactionEmoji'),
RichPresenceAssets: require('./structures/Presence').RichPresenceAssets,
Role: require('./structures/Role'),
TextChannel: require('./structures/TextChannel'),
User: require('./structures/User'),
UserConnection: require('./structures/UserConnection'),
VoiceChannel: require('./structures/VoiceChannel'),
VoiceRegion: require('./structures/VoiceRegion'),
Webhook: require('./structures/Webhook'),
WebSocket: require('./WebSocket'),

View File

@@ -12,12 +12,22 @@ class RESTManager {
this.globallyRateLimited = false;
this.tokenPrefix = tokenPrefix;
this.versioned = true;
this.timeDifferences = [];
}
get api() {
return routeBuilder(this);
}
get timeDifference() {
return Math.round(this.timeDifferences.reduce((a, b) => a + b, 0) / this.timeDifferences.length);
}
set timeDifference(ms) {
this.timeDifferences.unshift(ms);
if (this.timeDifferences.length > 5) this.timeDifferences.length = 5;
}
getAuth() {
const token = this.client.token || this.client.accessToken;
const prefixed = !!this.client.application || (this.client.user && this.client.user.bot);

View File

@@ -9,7 +9,6 @@ class RequestHandler {
this.limit = Infinity;
this.resetTime = null;
this.remaining = 1;
this.timeDifference = 0;
this.queue = [];
}
@@ -32,7 +31,7 @@ class RequestHandler {
const finish = timeout => {
if (timeout || this.limited) {
if (!timeout) {
timeout = this.resetTime - Date.now() + this.timeDifference + this.client.options.restTimeOffset;
timeout = this.resetTime - Date.now() + this.manager.timeDifference + this.client.options.restTimeOffset;
}
// eslint-disable-next-line prefer-promise-reject-errors
reject({ timeout });
@@ -40,17 +39,18 @@ class RequestHandler {
/**
* Emitted when the client hits a rate limit while making a request
* @event Client#rateLimit
* @prop {number} timeout Timeout in ms
* @prop {number} limit Number of requests that can be made to this endpoint
* @prop {number} timeDifference Delta-T in ms between your system and Discord servers
* @prop {string} method HTTP method used for request that triggered this event
* @prop {string} path Path used for request that triggered this event
* @prop {string} route Route used for request that triggered this event
* @param {Object} rateLimitInfo Object containing the rate limit info
* @param {number} rateLimitInfo.timeout Timeout in ms
* @param {number} rateLimitInfo.limit Number of requests that can be made to this endpoint
* @param {number} rateLimitInfo.timeDifference Delta-T in ms between your system and Discord servers
* @param {string} rateLimitInfo.method HTTP method used for request that triggered this event
* @param {string} rateLimitInfo.path Path used for request that triggered this event
* @param {string} rateLimitInfo.route Route used for request that triggered this event
*/
this.client.emit(RATE_LIMIT, {
timeout,
limit: this.limit,
timeDifference: this.timeDifference,
timeDifference: this.manager.timeDifference,
method: item.request.method,
path: item.request.path,
route: item.request.route,
@@ -66,7 +66,7 @@ class RequestHandler {
this.limit = Number(res.headers['x-ratelimit-limit']);
this.resetTime = Number(res.headers['x-ratelimit-reset']) * 1000;
this.remaining = Number(res.headers['x-ratelimit-remaining']);
this.timeDifference = Date.now() - new Date(res.headers.date).getTime();
this.manager.timeDifference = Date.now() - new Date(res.headers.date).getTime();
}
if (err) {
if (err.status === 429) {

View File

@@ -1,18 +1,24 @@
const childProcess = require('child_process');
const EventEmitter = require('events');
const path = require('path');
const Util = require('../util/Util');
const { Error } = require('../errors');
/**
* Represents a Shard spawned by the ShardingManager.
* A self-contained shard created by the {@link ShardingManager}. Each one has a {@link ChildProcess} that contains
* an instance of the bot and its {@link Client}. When its child process exits for any reason, the shard will spawn a
* new one to replace it as necessary.
* @extends EventEmitter
*/
class Shard {
class Shard extends EventEmitter {
/**
* @param {ShardingManager} manager The sharding manager
* @param {number} id The ID of this shard
* @param {Array} [args=[]] Command line arguments to pass to the script
* @param {ShardingManager} manager Manager that is spawning this shard
* @param {number} id ID of this shard
* @param {string[]} [args=[]] Command line arguments to pass to the script
*/
constructor(manager, id, args = []) {
super();
/**
* Manager that created the shard
* @type {ShardingManager}
@@ -26,7 +32,13 @@ class Shard {
this.id = id;
/**
* The environment variables for the shard
* Arguments for the shard's process
* @type {string[]}
*/
this.args = args;
/**
* Environment variables for the shard's process
* @type {Object}
*/
this.env = Object.assign({}, process.env, {
@@ -36,19 +48,81 @@ class Shard {
});
/**
* Process of the shard
* @type {ChildProcess}
* Whether the shard's {@link Client} is ready
* @type {boolean}
*/
this.process = childProcess.fork(path.resolve(this.manager.file), args, {
env: this.env,
});
this.process.on('message', this._handleMessage.bind(this));
this.process.once('exit', () => {
if (this.manager.respawn) this.manager.createShard(this.id);
});
this.ready = false;
/**
* Process of the shard
* @type {?ChildProcess}
*/
this.process = null;
/**
* Ongoing promises for calls to {@link Shard#eval}, mapped by the `script` they were called with
* @type {Map<string, Promise>}
* @private
*/
this._evals = new Map();
/**
* Ongoing promises for calls to {@link Shard#fetchClientValue}, mapped by the `prop` they were called with
* @type {Map<string, Promise>}
* @private
*/
this._fetches = new Map();
/**
* Listener function for the {@link ChildProcess}' `exit` event
* @type {Function}
* @private
*/
this._exitListener = this._handleExit.bind(this, undefined);
}
/**
* Forks a child process for the shard.
* <warn>You should not need to call this manually.</warn>
* @param {boolean} [waitForReady=true] Whether to wait until the {@link Client} has become ready before resolving
* @returns {Promise<ChildProcess>}
*/
async spawn(waitForReady = true) {
if (this.process) throw new Error('SHARDING_PROCESS_EXISTS', this.id);
this.process = childProcess.fork(path.resolve(this.manager.file), this.args, { env: this.env })
.on('message', this._handleMessage.bind(this))
.on('exit', this._exitListener);
/**
* Emitted upon the creation of the shard's child process.
* @event Shard#spawn
* @param {ChildProcess} process Child process that was created
*/
this.emit('spawn', this.process);
if (!waitForReady) return this.process;
await new Promise((resolve, reject) => {
this.once('ready', resolve);
this.once('disconnect', () => reject(new Error('SHARDING_READY_DISCONNECTED', this.id)));
this.once('death', () => reject(new Error('SHARDING_READY_DIED', this.id)));
setTimeout(() => reject(new Error('SHARDING_READY_TIMEOUT', this.id)), 30000);
});
return this.process;
}
/**
* Kills and restarts the shard's process.
* @param {number} [delay=500] How long to wait between killing the process and restarting it (in milliseconds)
* @param {boolean} [waitForReady=true] Whether to wait the {@link Client} has become ready before resolving
* @returns {Promise<ChildProcess>}
*/
async respawn(delay = 500, waitForReady = true) {
this.process.removeListener('exit', this._exitListener);
this.process.kill();
this._handleExit(false);
if (delay > 0) await Util.delayFor(delay);
return this.spawn(waitForReady);
}
/**
@@ -100,7 +174,7 @@ class Shard {
}
/**
* Evaluates a script on the shard, in the context of the client.
* Evaluates a script on the shard, in the context of the {@link Client}.
* @param {string} script JavaScript to run on the shard
* @returns {Promise<*>} Result of the script execution
*/
@@ -134,6 +208,39 @@ class Shard {
*/
_handleMessage(message) {
if (message) {
// Shard is ready
if (message._ready) {
this.ready = true;
/**
* Emitted upon the shard's {@link Client#ready} event.
* @event Shard#ready
*/
this.emit('ready');
return;
}
// Shard has disconnected
if (message._disconnect) {
this.ready = false;
/**
* Emitted upon the shard's {@link Client#disconnect} event.
* @event Shard#disconnect
*/
this.emit('disconnect');
return;
}
// Shard is attempting to reconnect
if (message._reconnecting) {
this.ready = false;
/**
* Emitted upon the shard's {@link Client#reconnecting} event.
* @event Shard#reconnecting
*/
this.emit('reconnecting');
return;
}
// Shard is requesting a property fetch
if (message._sFetchProp) {
this.manager.fetchClientValues(message._sFetchProp).then(
@@ -151,15 +258,44 @@ class Shard {
);
return;
}
// Shard is requesting a respawn of all shards
if (message._sRespawnAll) {
const { shardDelay, respawnDelay, waitForReady } = message._sRespawnAll;
this.manager.respawnAll(shardDelay, respawnDelay, waitForReady).catch(() => {
// Do nothing
});
return;
}
}
/**
* Emitted upon recieving a message from a shard.
* @event ShardingManager#message
* @param {Shard} shard Shard that sent the message
* Emitted upon recieving a message from the child process.
* @event Shard#message
* @param {*} message Message that was received
*/
this.manager.emit('message', this, message);
this.emit('message', message);
}
/**
* Handles the shard's process exiting.
* @param {boolean} [respawn=this.manager.respawn] Whether to spawn the shard again
* @private
*/
_handleExit(respawn = this.manager.respawn) {
/**
* Emitted upon the shard's child process exiting.
* @event Shard#death
* @param {ChildProcess} process Child process that exited
*/
this.emit('death', this.process);
this.ready = false;
this.process = null;
this._evals.clear();
this._fetches.clear();
if (respawn) this.spawn().catch(err => this.emit('error', err));
}
}

View File

@@ -3,15 +3,19 @@ const { Events } = require('../util/Constants');
const { Error } = require('../errors');
/**
* Helper class for sharded clients spawned as a child process, such as from a ShardingManager.
* Helper class for sharded clients spawned as a child process, such as from a {@link ShardingManager}.
* Utilises IPC to send and receive data to/from the master process and other shards.
*/
class ShardClientUtil {
/**
* @param {Client} client The client of the current shard
* @param {Client} client Client of the current shard
*/
constructor(client) {
this.client = client;
process.on('message', this._handleMessage.bind(this));
client.on('ready', () => { process.send({ _ready: true }); });
client.on('disconnect', () => { process.send({ _disconnect: true }); });
client.on('reconnecting', () => { process.send({ _reconnecting: true }); });
}
/**
@@ -49,13 +53,14 @@ class ShardClientUtil {
/**
* Fetches a client property value of each shard.
* @param {string} prop Name of the client property to get, using periods for nesting
* @returns {Promise<Array>}
* @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);
* @see {@link ShardingManager#fetchClientValues}
*/
fetchClientValues(prop) {
return new Promise((resolve, reject) => {
@@ -74,9 +79,10 @@ class ShardClientUtil {
}
/**
* Evaluates a script on all shards, in the context of the Clients.
* Evaluates a script on all shards, in the context of the {@link Clients}.
* @param {string} script JavaScript to run on each shard
* @returns {Promise<Array>} Results of the script execution
* @returns {Promise<Array<*>>} Results of the script execution
* @see {@link ShardingManager#broadcastEval}
*/
broadcastEval(script) {
return new Promise((resolve, reject) => {
@@ -94,6 +100,19 @@ class ShardClientUtil {
});
}
/**
* Requests a respawn of all shards.
* @param {number} [shardDelay=5000] How long to wait between shards (in milliseconds)
* @param {number} [respawnDelay=500] How long to wait between killing a shard's process and restarting it
* (in milliseconds)
* @param {boolean} [waitForReady=true] Whether to wait for a shard to become ready before continuing to another
* @returns {Promise<void>} Resolves upon the message being sent
* @see {@link ShardingManager#respawnAll}
*/
respawnAll(shardDelay = 5000, respawnDelay = 500, waitForReady = true) {
return this.send({ _sRespawnAll: { shardDelay, respawnDelay, waitForReady } });
}
/**
* Handles an IPC message.
* @param {*} message Message received

View File

@@ -7,9 +7,12 @@ const Util = require('../util/Util');
const { Error, TypeError, RangeError } = require('../errors');
/**
* This is a utility class that can be used to help you spawn shards of your client. Each shard is completely separate
* from the other. The Shard Manager takes a path to a file and spawns it under the specified amount of shards safely.
* If you do not select an amount of shards, the manager will automatically decide the best amount.
* This is a utility class that makes multi-process sharding of a bot an easy and painless experience.
* It works by spawning a self-contained {@link ChildProcess} for each individual shard, each containing its own
* instance of your bot's {@link Client}. They all have a line of communication with the master process, and there are
* several useful methods that utilise it in order to simplify tasks that are normally difficult with sharding. It can
* spawn a specific number of shards or the amount that Discord suggests for the bot, and takes a path to your main bot
* script to launch for each one.
* @extends {EventEmitter}
*/
class ShardingManager extends EventEmitter {
@@ -82,33 +85,33 @@ class ShardingManager extends EventEmitter {
/**
* Spawns a single shard.
* @param {number} id The ID of the shard to spawn. **This is usually not necessary**
* @returns {Promise<Shard>}
* @param {number} [id=this.shards.size] ID of the shard to spawn -
* **This is usually not necessary to manually specify.**
* @returns {Shard}
*/
createShard(id = this.shards.size) {
const shard = new Shard(this, id, this.shardArgs);
this.shards.set(id, shard);
/**
* Emitted upon launching a shard.
* @event ShardingManager#launch
* @param {Shard} shard Shard that was launched
* Emitted upon creating a shard.
* @event ShardingManager#shardCreate
* @param {Shard} shard Shard that was created
*/
this.emit('launch', shard);
return Promise.resolve(shard);
this.emit('shardCreate', shard);
return shard;
}
/**
* Spawns multiple shards.
* @param {number} [amount=this.totalShards] Number of shards to spawn
* @param {number} [delay=7500] How long to wait in between spawning each shard (in milliseconds)
* @param {number} [delay=5500] How long to wait in between spawning each shard (in milliseconds)
* @param {boolean} [waitForReady=true] Whether to wait for a shard to become ready before continuing to another
* @returns {Promise<Collection<number, Shard>>}
*/
spawn(amount = this.totalShards, delay = 7500) {
async spawn(amount = this.totalShards, delay = 5500, waitForReady = true) {
// Obtain/verify the number of shards to spawn
if (amount === 'auto') {
return Util.fetchRecommendedShards(this.token).then(count => {
this.totalShards = count;
return this._spawn(count, delay);
});
amount = await Util.fetchRecommendedShards(this.token);
} else {
if (typeof amount !== 'number' || isNaN(amount)) {
throw new TypeError('CLIENT_INVALID_OPTION', 'Amount of shards', 'a number.');
@@ -117,41 +120,22 @@ class ShardingManager extends EventEmitter {
if (amount !== Math.floor(amount)) {
throw new TypeError('CLIENT_INVALID_OPTION', 'Amount of shards', 'an integer.');
}
return this._spawn(amount, delay);
}
}
/**
* Actually spawns shards, unlike that poser above >:(
* @param {number} amount Number of shards to spawn
* @param {number} delay How long to wait in between spawning each shard (in milliseconds)
* @returns {Promise<Collection<number, Shard>>}
* @private
*/
_spawn(amount, delay) {
return new Promise(resolve => {
if (this.shards.size >= amount) throw new Error('SHARDING_ALREADY_SPAWNED', this.shards.size);
this.totalShards = amount;
// Make sure this many shards haven't already been spawned
if (this.shards.size >= amount) throw new Error('SHARDING_ALREADY_SPAWNED', this.shards.size);
this.totalShards = amount;
this.createShard();
if (this.shards.size >= this.totalShards) {
resolve(this.shards);
return;
}
// Spawn the shards
for (let s = 1; s <= amount; s++) {
const promises = [];
const shard = this.createShard();
promises.push(shard.spawn(waitForReady));
if (delay > 0 && s !== amount) promises.push(Util.delayFor(delay));
await Promise.all(promises); // eslint-disable-line no-await-in-loop
}
if (delay <= 0) {
while (this.shards.size < this.totalShards) this.createShard();
resolve(this.shards);
} else {
const interval = setInterval(() => {
this.createShard();
if (this.shards.size >= this.totalShards) {
clearInterval(interval);
resolve(this.shards);
}
}, delay);
}
});
return this.shards;
}
/**
@@ -166,9 +150,9 @@ class ShardingManager extends EventEmitter {
}
/**
* Evaluates a script on all shards, in the context of the Clients.
* Evaluates a script on all shards, in the context of the {@link Client}s.
* @param {string} script JavaScript to run on each shard
* @returns {Promise<Array>} Results of the script execution
* @returns {Promise<Array<*>>} Results of the script execution
*/
broadcastEval(script) {
const promises = [];
@@ -179,7 +163,7 @@ class ShardingManager extends EventEmitter {
/**
* Fetches a client property value of each shard.
* @param {string} prop Name of the client property to get, using periods for nesting
* @returns {Promise<Array>}
* @returns {Promise<Array<*>>}
* @example
* manager.fetchClientValues('guilds.size')
* .then(results => {
@@ -194,6 +178,24 @@ class ShardingManager extends EventEmitter {
for (const shard of this.shards.values()) promises.push(shard.fetchClientValue(prop));
return Promise.all(promises);
}
/**
* Kills all running shards and respawns them.
* @param {number} [shardDelay=5000] How long to wait between shards (in milliseconds)
* @param {number} [respawnDelay=500] How long to wait between killing a shard's process and restarting it
* (in milliseconds)
* @param {boolean} [waitForReady=true] Whether to wait for a shard to become ready before continuing to another
* @returns {Promise<Collection<string, Shard>>}
*/
async respawnAll(shardDelay = 5000, respawnDelay = 500, waitForReady = true) {
let s = 0;
for (const shard of this.shards) {
const promises = [shard.respawn(respawnDelay, waitForReady)];
if (++s < this.shards.size && shardDelay > 0) promises.push(Util.delayFor(shardDelay));
await Promise.all(promises); // eslint-disable-line no-await-in-loop
}
return this.shards;
}
}
module.exports = ShardingManager;

View File

@@ -95,7 +95,7 @@ class ChannelStore extends DataStore {
* @memberof ChannelStore
* @instance
* @param {ChannelResolvable} channel The channel resolvable to resolve
* @returns {?string}
* @returns {?Snowflake}
*/
}

View File

@@ -1,4 +1,5 @@
const Collection = require('../util/Collection');
let Structures;
/**
* Manages the creation, retrieval and deletion of a specific data model.
@@ -7,8 +8,9 @@ const Collection = require('../util/Collection');
class DataStore extends Collection {
constructor(client, iterable, holds) {
super();
if (!Structures) Structures = require('../util/Structures');
Object.defineProperty(this, 'client', { value: client });
Object.defineProperty(this, 'holds', { value: holds });
Object.defineProperty(this, 'holds', { value: Structures.get(holds.name) || holds });
if (iterable) for (const item of iterable) this.create(item);
}
@@ -37,7 +39,7 @@ class DataStore extends Collection {
/**
* Resolves a data entry to a instance ID.
* @param {string|Instance} idOrInstance The id or instance of something in this DataStore
* @returns {?string}
* @returns {?Snowflake}
*/
resolveID(idOrInstance) {
if (idOrInstance instanceof this.holds) return idOrInstance.id;

View File

@@ -38,7 +38,7 @@ class EmojiStore extends DataStore {
/**
* Resolves a EmojiResolvable to a Emoji ID string.
* @param {EmojiResolvable} emoji The Emoji resolvable to identify
* @returns {?string}
* @returns {?Snowflake}
*/
resolveID(emoji) {
if (emoji instanceof ReactionEmoji) return emoji.id;

View File

@@ -42,7 +42,7 @@ class GuildChannelStore extends DataStore {
* @memberof GuildChannelStore
* @instance
* @param {GuildChannelResolvable} channel The GuildChannel resolvable to resolve
* @returns {?string}
* @returns {?Snowflake}
*/
}

View File

@@ -41,7 +41,7 @@ class GuildMemberStore extends DataStore {
/**
* Resolves a GuildMemberResolvable to an member ID string.
* @param {GuildMemberResolvable} member The user that is part of the guild
* @returns {?string}
* @returns {?Snowflake}
*/
resolveID(member) {
const memberResolveable = super.resolveID(member);

View File

@@ -33,7 +33,7 @@ class GuildStore extends DataStore {
* @memberof GuildStore
* @instance
* @param {GuildResolvable} guild The guild resolvable to identify
* @returns {?string}
* @returns {?Snowflake}
*/
}

View File

@@ -112,7 +112,7 @@ class MessageStore extends DataStore {
* @memberof MessageStore
* @instance
* @param {MessageResolvable} message The message resolvable to resolve
* @returns {?string}
* @returns {?Snowflake}
*/
}

View File

@@ -39,7 +39,7 @@ class PresenceStore extends DataStore {
/**
* Resolves a PresenceResolvable to a Presence ID string.
* @param {PresenceResolvable} presence The presence resolvable to resolve
* @returns {?string}
* @returns {?Snowflake}
*/
resolveID(presence) {
const presenceResolveable = super.resolveID(presence);

View File

@@ -38,7 +38,7 @@ class ReactionStore extends DataStore {
* @memberof ReactionStore
* @instance
* @param {MessageReactionResolvable} role The role resolvable to resolve
* @returns {?string}
* @returns {?Snowflake}
*/
}

View File

@@ -0,0 +1,33 @@
const DataStore = require('./DataStore');
/**
* A data store to store User models who reacted to a MessageReaction.
* @extends {DataStore}
*/
class ReactionUserStore extends DataStore {
constructor(client, iterable, reaction) {
super(client, iterable, require('../structures/User'));
this.reaction = reaction;
}
/**
* Fetches all the users that gave this reaction. Resolves with a collection of users, mapped by their IDs.
* @param {Object} [options] Options for fetching the users
* @param {number} [options.limit=100] The maximum amount of users to fetch, defaults to 100
* @param {Snowflake} [options.before] Limit fetching users to those with an id lower than the supplied id
* @param {Snowflake} [options.after] Limit fetching users to those with an id greater than the supplied id
* @returns {Promise<ReactionUserStore<Snowflake, User>>}
*/
async fetch({ limit = 100, after, before } = {}) {
const message = this.reaction.message;
const users = await this.client.api.channels[message.channel.id].messages[message.id]
.reactions[this.reaction.emoji.identifier]
.get({ query: { limit, before, after } });
for (const rawUser of users) {
const user = this.client.users.create(rawUser);
this.set(user.id, user);
}
return this;
}
}
module.exports = ReactionUserStore;

View File

@@ -38,7 +38,7 @@ class RoleStore extends DataStore {
* @memberof RoleStore
* @instance
* @param {RoleResolvable} role The role resolvable to resolve
* @returns {?string}
* @returns {?Snowflake}
*/
}

View File

@@ -35,7 +35,7 @@ class UserStore extends DataStore {
/**
* Resolves a UserResolvable to a user ID string.
* @param {UserResolvable} user The UserResolvable to identify
* @returns {?string}
* @returns {?Snowflake}
*/
resolveID(user) {
if (user instanceof GuildMember) return user.user.id;

View File

@@ -6,7 +6,7 @@ const GuildChannel = require('./GuildChannel');
*/
class CategoryChannel extends GuildChannel {
/**
* The channels that are part of this category
* Channels that are part of this category
* @type {?Collection<Snowflake, GuildChannel>}
* @readonly
*/

View File

@@ -66,32 +66,37 @@ class Channel extends Base {
}
static create(client, data, guild) {
const DMChannel = require('./DMChannel');
const GroupDMChannel = require('./GroupDMChannel');
const TextChannel = require('./TextChannel');
const VoiceChannel = require('./VoiceChannel');
const CategoryChannel = require('./CategoryChannel');
const GuildChannel = require('./GuildChannel');
const Structures = require('../util/Structures');
let channel;
if (data.type === ChannelTypes.DM) {
const DMChannel = Structures.get('DMChannel');
channel = new DMChannel(client, data);
} else if (data.type === ChannelTypes.GROUP) {
const GroupDMChannel = Structures.get('GroupDMChannel');
channel = new GroupDMChannel(client, data);
} else {
guild = guild || client.guilds.get(data.guild_id);
if (guild) {
switch (data.type) {
case ChannelTypes.TEXT:
case ChannelTypes.TEXT: {
const TextChannel = Structures.get('TextChannel');
channel = new TextChannel(guild, data);
break;
case ChannelTypes.VOICE:
}
case ChannelTypes.VOICE: {
const VoiceChannel = Structures.get('VoiceChannel');
channel = new VoiceChannel(guild, data);
break;
case ChannelTypes.CATEGORY:
}
case ChannelTypes.CATEGORY: {
const CategoryChannel = Structures.get('CategoryChannel');
channel = new CategoryChannel(guild, data);
break;
default:
}
default: {
const GuildChannel = Structures.get('GuildChannel');
channel = new GuildChannel(guild, data);
}
}
guild.channels.set(channel.id, channel);
}

View File

@@ -150,11 +150,12 @@ class ClientApplication extends Base {
* @returns {Promise<Object>}
*/
fetchAssets() {
return this.client.api.applications(this.id).assets.get()
const types = Object.keys(ClientApplicationAssetTypes);
return this.client.api.oauth2.applications(this.id).assets.get()
.then(assets => assets.map(a => ({
id: a.id,
name: a.name,
type: Object.keys(ClientApplicationAssetTypes)[a.type - 1],
type: types[a.type - 1],
})));
}
@@ -167,7 +168,7 @@ class ClientApplication extends Base {
*/
createAsset(name, data, type) {
return DataResolver.resolveBase64(data).then(b64 =>
this.client.api.applications(this.id).assets.post({ data: {
this.client.api.oauth2.applications(this.id).assets.post({ data: {
name,
data: b64,
type: ClientApplicationAssetTypes[type.toUpperCase()],
@@ -177,7 +178,7 @@ class ClientApplication extends Base {
/**
* Resets the app's secret.
* <warn>This is only available when using a user account.</warn>
* @returns {ClientApplication}
* @returns {Promise<ClientApplication>}
*/
resetSecret() {
return this.client.api.oauth2.applications[this.id].reset.post()
@@ -187,7 +188,7 @@ class ClientApplication extends Base {
/**
* Resets the app's bot token.
* <warn>This is only available when using a user account.</warn>
* @returns {ClientApplication}
* @returns {Promise<ClientApplication>}
*/
resetToken() {
return this.client.api.oauth2.applications[this.id].bot.reset.post()
@@ -195,8 +196,12 @@ class ClientApplication extends Base {
}
/**
* When concatenated with a string, this automatically concatenates the app name rather than the app object.
* When concatenated with a string, this automatically returns the application's name instead of the
* ClientApplication object.
* @returns {string}
* @example
* // Logs: Application name: My App
* console.log(`Application name: ${application}`);
*/
toString() {
return this.name;

View File

@@ -1,4 +1,4 @@
const User = require('./User');
const Structures = require('../util/Structures');
const Collection = require('../util/Collection');
const ClientUserSettings = require('./ClientUserSettings');
const ClientUserGuildSettings = require('./ClientUserGuildSettings');
@@ -11,7 +11,7 @@ const Guild = require('./Guild');
* Represents the logged in client's Discord user.
* @extends {User}
*/
class ClientUser extends User {
class ClientUser extends Structures.get('User') {
_patch(data) {
super._patch(data);
@@ -88,6 +88,8 @@ class ClientUser extends User {
this.guildSettings.set(settings.guild_id, new ClientUserGuildSettings(this.client, settings));
}
}
if (data.token) this.client.token = data.token;
}
/**
@@ -246,7 +248,7 @@ class ClientUser extends User {
/**
* Fetches messages that mentioned the client's user.
* <warn>This is only available when using a user account.</warn>
* @param {Object} [options] Options for the fetch
* @param {Object} [options={}] Options for the fetch
* @param {number} [options.limit=25] Maximum number of mentions to retrieve
* @param {boolean} [options.roles=true] Whether to include role mentions
* @param {boolean} [options.everyone=true] Whether to include everyone/here mentions

View File

@@ -27,9 +27,12 @@ class DMChannel extends Channel {
}
/**
* When concatenated with a string, this automatically concatenates the recipient's mention instead of the
* DM channel object.
* When concatenated with a string, this automatically returns the recipient's mention instead of the
* DMChannel object.
* @returns {string}
* @example
* // Logs: Hello from <@123456789012345678>!
* console.log(`Hello from ${channel}!`);
*/
toString() {
return this.recipient.toString();

View File

@@ -190,7 +190,7 @@ class Emoji extends Base {
}
/**
* When concatenated with a string, this automatically returns the emoji mention rather than the object.
* When concatenated with a string, this automatically concatenates the emoji's mention instead of the Emoji object.
* @returns {string}
* @example
* // Send an emoji:

View File

@@ -203,14 +203,12 @@ class GroupDMChannel extends Channel {
}
/**
* When concatenated with a string, this automatically concatenates the channel's name instead of the Channel object.
* When concatenated with a string, this automatically returns the channel's name instead of the
* GroupDMChannel object.
* @returns {string}
* @example
* // Logs: Hello from My Group DM!
* console.log(`Hello from ${channel}!`);
* @example
* // Logs: Hello from My Group DM!
* console.log(`Hello from ' + channel + '!');
*/
toString() {
return this.name;

View File

@@ -114,8 +114,18 @@ class Guild extends Base {
this.large = Boolean('large' in data ? data.large : this.large);
/**
* An array of guild features
* @type {Object[]}
* An array of enabled guild features, here are the possible values:
* * INVITE_SPLASH
* * MORE_EMOJI
* * VERIFIED
* * VIP_REGIONS
* * VANITY_URL
* @typedef {string} Features
*/
/**
* An array of guild features partnered guilds have enabled
* @type {Features[]}
*/
this.features = data.features;
@@ -311,7 +321,7 @@ class Guild extends Base {
/**
* System channel for this guild
* @type {?GuildChannel}
* @type {?TextChannel}
* @readonly
*/
get systemChannel() {
@@ -401,7 +411,7 @@ class Guild extends Base {
}
}
/*
/**
* The `@everyone` role of the guild
* @type {Role}
* @readonly
@@ -431,10 +441,16 @@ class Guild extends Base {
return this.members.resolve(user);
}
/**
* An object containing information about a guild member's ban.
* @typedef {Object} BanInfo
* @property {User} user User that was banned
* @property {?string} reason Reason the user was banned
*/
/**
* Fetches a collection of banned users in this guild.
* The returned collection contains user objects keyed under `user` and reasons keyed under `reason`.
* @returns {Promise<Collection<Snowflake, Object>>}
* @returns {Promise<Collection<Snowflake, BanInfo>>}
*/
fetchBans() {
return this.client.api.guilds(this.id).bans.get().then(bans =>
@@ -496,7 +512,7 @@ class Guild extends Base {
* @param {Snowflake|GuildAuditLogsEntry} [options.after] Limit to entries from after specified entry
* @param {number} [options.limit] Limit number of entries
* @param {UserResolvable} [options.user] Only show entries involving this user
* @param {ActionType|number} [options.type] Only show entries involving this action type
* @param {AuditLogAction|number} [options.type] Only show entries involving this action type
* @returns {Promise<GuildAuditLogs>}
*/
fetchAuditLogs(options = {}) {
@@ -528,7 +544,9 @@ class Guild extends Base {
* @returns {Promise<GuildMember>}
*/
addMember(user, options) {
if (this.members.has(user.id)) return Promise.resolve(this.members.get(user.id));
user = this.client.users.resolveID(user);
if (!user) return Promise.reject(new TypeError('INVALID_TYPE', 'user', 'UserResolvable'));
if (this.members.has(user)) return Promise.resolve(this.members.get(user));
options.access_token = options.accessToken;
if (options.roles) {
const roles = [];
@@ -541,8 +559,8 @@ class Guild extends Base {
roles.push(role.id);
}
}
return this.client.api.guilds(this.id).members(user.id).put({ data: options })
.then(data => this.client.actions.GuildMemberGet.handle(this, data).member);
return this.client.api.guilds(this.id).members(user).put({ data: options })
.then(data => this.members.create(data));
}
/**
@@ -794,6 +812,7 @@ class Guild extends Base {
* @returns {Promise<Guild>}
*/
allowDMs(allow) {
if (this.client.user.bot) return Promise.reject(new Error('FEATURE_USER_ONLY'));
const settings = this.client.user.settings;
if (allow) return settings.removeRestrictedGuild(this);
else return settings.addRestrictedGuild(this);
@@ -802,11 +821,10 @@ class Guild extends Base {
/**
* Bans a user from the guild.
* @param {UserResolvable} user The user to ban
* @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 {Object} [options] Options for the ban
* @param {number} [options.days=0] Number of days of messages to delete
* @param {string} [options.reason] Reason for banning
* @returns {Promise<GuildMember|User|string>} Result object will be resolved as specifically as possible.
* @returns {Promise<GuildMember|User|Snowflake>} Result object will be resolved as specifically as possible.
* If the GuildMember cannot be resolved, the User will instead be attempted to be resolved. If that also cannot
* be resolved, the user ID will be the result.
* @example
@@ -979,7 +997,7 @@ class Guild extends Base {
}
/**
* Creates a new role in the guild with given information
* Creates a new role in the guild with given information.
* <warn>The position will silently reset to 1 if an invalid one is provided, or none.</warn>
* @param {Object} [options] Options
* @param {RoleData} [options.data] The data to update the role with
@@ -1000,7 +1018,7 @@ class Guild extends Base {
* reason: 'we needed a role for Super Cool People',
* })
* .then(role => console.log(`Created role ${role}`))
* .catch(console.error)
* .catch(console.error);
*/
createRole({ data = {}, reason } = {}) {
if (data.color) data.color = Util.resolveColor(data.color);
@@ -1054,8 +1072,7 @@ class Guild extends Base {
.then(emoji => this.client.actions.GuildEmojiCreate.handle(this, emoji).emoji);
}
return DataResolver.resolveImage(attachment)
.then(image => this.createEmoji(image, name, { roles, reason }));
return DataResolver.resolveImage(attachment).then(image => this.createEmoji(image, name, { roles, reason }));
}
/**
@@ -1123,19 +1140,44 @@ class Guild extends Base {
}
/**
* When concatenated with a string, this automatically concatenates the guild's name instead of the guild object.
* When concatenated with a string, this automatically returns the guild's name instead of the Guild object.
* @returns {string}
* @example
* // Logs: Hello from My Guild!
* console.log(`Hello from ${guild}!`);
* @example
* // Logs: Hello from My Guild!
* console.log('Hello from ' + guild + '!');
*/
toString() {
return this.name;
}
/**
* Creates a collection of this guild's roles, sorted by their position and IDs.
* @returns {Collection<Role>}
* @private
*/
_sortedRoles() {
return Util.discordSort(this.roles);
}
/**
* Creates a collection of this guild's or a specific category's channels, sorted by their position and IDs.
* @param {GuildChannel} [channel] Category to get the channels of
* @returns {Collection<GuildChannel>}
* @private
*/
_sortedChannels(channel) {
const category = channel.type === ChannelTypes.CATEGORY;
return Util.discordSort(this.channels.filter(c =>
c.type === channel.type && (category || c.parent === channel.parent)
));
}
/**
* Handles a user speaking update in a voice channel.
* @param {Snowflake} user ID of the user that the update is for
* @param {boolean} speaking Whether the user is speaking
* @private
*/
_memberSpeakUpdate(user, speaking) {
const member = this.members.get(user);
if (member && member.speaking !== speaking) {
@@ -1149,23 +1191,15 @@ class Guild extends Base {
this.client.emit(Events.GUILD_MEMBER_SPEAKING, member, speaking);
}
}
_sortedRoles() {
return Util.discordSort(this.roles);
}
_sortedChannels(channel) {
const category = channel.type === ChannelTypes.CATEGORY;
return Util.discordSort(this.channels.filter(c =>
c.type === channel.type && (category || c.parent === channel.parent)));
}
}
// TODO: Document this thing
class VoiceStateCollection extends Collection {
constructor(guild) {
super();
this.guild = guild;
}
set(id, voiceState) {
const member = this.guild.members.get(id);
if (member) {

View File

@@ -2,6 +2,24 @@ const Collection = require('../util/Collection');
const Snowflake = require('../util/Snowflake');
const Webhook = require('./Webhook');
/**
* The target type of an entry, e.g. `GUILD`. Here are the available types:
* * GUILD
* * CHANNEL
* * USER
* * ROLE
* * INVITE
* * WEBHOOK
* * EMOJI
* * MESSAGE
* @typedef {string} AuditLogTargetType
*/
/**
* Key mirror of all available audit log targets.
* @name GuildAuditLogs.Targets
* @type {AuditLogTargetType}
*/
const Targets = {
ALL: 'ALL',
GUILD: 'GUILD',
@@ -15,6 +33,43 @@ const Targets = {
UNKNOWN: 'UNKNOWN',
};
/**
* The action of an entry. Here are the available actions:
* * ALL: null
* * GUILD_UPDATE: 1
* * CHANNEL_CREATE: 10
* * CHANNEL_UPDATE: 11
* * CHANNEL_DELETE: 12
* * CHANNEL_OVERWRITE_CREATE: 13
* * CHANNEL_OVERWRITE_UPDATE: 14
* * CHANNEL_OVERWRITE_DELETE: 15
* * MEMBER_KICK: 20
* * MEMBER_PRUNE: 21
* * MEMBER_BAN_ADD: 22
* * MEMBER_BAN_REMOVE: 23
* * MEMBER_UPDATE: 24
* * MEMBER_ROLE_UPDATE: 25
* * ROLE_CREATE: 30
* * ROLE_UPDATE: 31
* * ROLE_DELETE: 32
* * INVITE_CREATE: 40
* * INVITE_UPDATE: 41
* * INVITE_DELETE: 42
* * WEBHOOK_CREATE: 50
* * WEBHOOK_UPDATE: 51
* * WEBHOOK_DELETE: 50
* * EMOJI_CREATE: 60
* * EMOJI_UPDATE: 61
* * EMOJI_DELETE: 62
* * MESSAGE_DELETE: 72
* @typedef {?number|string} AuditLogAction
*/
/**
* All available actions keyed under their names to their numeric values.
* @name GuildAuditLogs.Actions
* @type {AuditLogAction}
*/
const Actions = {
ALL: null,
GUILD_UPDATE: 1,
@@ -85,20 +140,7 @@ class GuildAuditLogs {
}
/**
* The target type of an entry, e.g. `GUILD`. Here are the available types:
* * GUILD
* * CHANNEL
* * USER
* * ROLE
* * INVITE
* * WEBHOOK
* * EMOJI
* * MESSAGE
* @typedef {string} TargetType
*/
/**
* The target for an audit log entry. It can be one of:
* The target of an entry. It can be one of:
* * A guild
* * A user
* * A role
@@ -106,13 +148,13 @@ class GuildAuditLogs {
* * An invite
* * A webhook
* * An object where the keys represent either the new value or the old value
* @typedef {?Object|Guild|User|Role|Emoji|Invite|Webhook} EntryTarget
* @typedef {?Object|Guild|User|Role|Emoji|Invite|Webhook} AuditLogEntryTarget
*/
/**
* Finds the target type from the entry action.
* @param {number} target The action target
* @returns {?string}
* @param {AuditLogAction} target The action target
* @returns {AuditLogTargetType}
*/
static targetType(target) {
if (target < 10) return Targets.GUILD;
@@ -131,13 +173,14 @@ class GuildAuditLogs {
* * CREATE
* * DELETE
* * UPDATE
* @typedef {string} ActionType
* * ALL
* @typedef {string} AuditLogActionType
*/
/**
* Finds the action type from the entry action.
* @param {string} action The action target
* @returns {string}
* @param {AuditLogAction} action The action target
* @returns {AuditLogActionType}
*/
static actionType(action) {
if ([
@@ -187,19 +230,19 @@ class GuildAuditLogsEntry {
const targetType = GuildAuditLogs.targetType(data.action_type);
/**
* The target type of this entry
* @type {TargetType}
* @type {AuditLogTargetType}
*/
this.targetType = targetType;
/**
* The action type of this entry
* @type {ActionType}
* @type {AuditLogActionType}
*/
this.actionType = GuildAuditLogs.actionType(data.action_type);
/**
* Specific action type of this entry
* @type {string}
* Specific action type of this entry in its string presentation
* @type {AuditLogAction}
*/
this.action = Object.keys(Actions).find(k => Actions[k] === data.action_type);
@@ -271,7 +314,7 @@ class GuildAuditLogsEntry {
if (targetType === Targets.UNKNOWN) {
/**
* The target of this entry
* @type {EntryTarget}
* @type {AuditLogEntryTarget}
*/
this.target = this.changes.reduce((o, c) => {
o[c.key] = c.new || c.old;

View File

@@ -248,6 +248,7 @@ class GuildChannel extends Channel {
* @property {string} [name] The name of the channel
* @property {number} [position] The position of the channel
* @property {string} [topic] The topic of the text channel
* @property {boolean} [nsfw] Whether the channel is NSFW
* @property {number} [bitrate] The bitrate of the voice channel
* @property {number} [userLimit] The user limit of the voice channel
* @property {Snowflake} [parentID] The parent ID of the channel
@@ -290,8 +291,9 @@ class GuildChannel extends Channel {
data: {
name: (data.name || this.name).trim(),
topic: data.topic,
nsfw: data.nsfw,
bitrate: data.bitrate || (this.bitrate ? this.bitrate * 1000 : undefined),
user_limit: data.userLimit != null ? data.userLimit : this.userLimit, // eslint-disable-line eqeqeq
user_limit: typeof data.userLimit !== 'undefined' ? data.userLimit : this.userLimit,
parent_id: data.parentID,
lock_permissions: data.lockPermissions,
permission_overwrites: data.permissionOverwrites,
@@ -491,11 +493,8 @@ class GuildChannel extends Channel {
* When concatenated with a string, this automatically returns the channel's mention instead of the Channel object.
* @returns {string}
* @example
* // Outputs: Hello from #general
* console.log(`Hello from ${channel}`);
* @example
* // Outputs: Hello from #general
* console.log('Hello from ' + channel);
* // Logs: Hello from <#123456789012345678>!
* console.log(`Hello from ${channel}!`);
*/
toString() {
return `<#${this.id}>`;

View File

@@ -77,36 +77,42 @@ class GuildMember extends Base {
/**
* Whether this member is deafened server-wide
* @type {boolean}
* @readonly
*/
get serverDeaf() { return this.voiceState.deaf; }
/**
* Whether this member is muted server-wide
* @type {boolean}
* @readonly
*/
get serverMute() { return this.voiceState.mute; }
/**
* Whether this member is self-muted
* @type {boolean}
* @readonly
*/
get selfMute() { return this.voiceState.self_mute; }
/**
* Whether this member is self-deafened
* @type {boolean}
* @readonly
*/
get selfDeaf() { return this.voiceState.self_deaf; }
/**
* The voice session ID of this member (if any)
* @type {?Snowflake}
* @readonly
*/
get voiceSessionID() { return this.voiceState.session_id; }
/**
* The voice channel ID of this member, (if any)
* @type {?Snowflake}
* @readonly
*/
get voiceChannelID() { return this.voiceState.channel_id; }
@@ -125,7 +131,7 @@ class GuildMember extends Base {
* @readonly
*/
get presence() {
return this.frozenPresence || this.guild.presences.get(this.id) || new Presence();
return this.frozenPresence || this.guild.presences.get(this.id) || new Presence(this.client);
}
/**
@@ -294,19 +300,13 @@ class GuildMember extends Base {
/**
* Checks if any of the member's roles have a permission.
* @param {PermissionResolvable|PermissionResolvable[]} permission Permission(s) to check for
* @param {boolean} [explicit=false] Whether to require the role to explicitly have the exact permission
* **(deprecated)**
* @param {boolean} [checkAdmin] Whether to allow the administrator permission to override
* (takes priority over `explicit`)
* @param {boolean} [checkOwner] Whether to allow being the guild's owner to override
* (takes priority over `explicit`)
* @param {boolean} [checkAdmin=true] Whether to allow the administrator permission to override
* @param {boolean} [checkOwner=true] Whether to allow being the guild's owner to override
* @returns {boolean}
*/
hasPermission(permission, explicit = false, checkAdmin, checkOwner) {
if (typeof checkAdmin === 'undefined') checkAdmin = !explicit;
if (typeof checkOwner === 'undefined') checkOwner = !explicit;
hasPermission(permission, checkAdmin = true, checkOwner = true) {
if (checkOwner && this.user.id === this.guild.ownerID) return true;
return this.roles.some(r => r.permissions.has(permission, undefined, checkAdmin));
return this.roles.some(r => r.permissions.has(permission, checkAdmin));
}
/**
@@ -521,8 +521,7 @@ class GuildMember extends Base {
/**
* Bans 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 {Object} [options] Options for the ban
* @param {number} [options.days=0] Number of days of messages to delete
* @param {string} [options.reason] Reason for banning
* @returns {Promise<GuildMember>}
@@ -535,10 +534,10 @@ class GuildMember extends Base {
}
/**
* When concatenated with a string, this automatically concatenates the user's mention instead of the Member object.
* When concatenated with a string, this automatically returns the user's mention instead of the GuildMember object.
* @returns {string}
* @example
* // Logs: Hello from <@123456789>!
* // Logs: Hello from <@123456789012345678>!
* console.log(`Hello from ${member}!`);
*/
toString() {

View File

@@ -8,9 +8,9 @@ const Collection = require('../util/Collection');
const ReactionStore = require('../stores/ReactionStore');
const { MessageTypes } = require('../util/Constants');
const Permissions = require('../util/Permissions');
const GuildMember = require('./GuildMember');
const Base = require('./Base');
const { Error, TypeError } = require('../errors');
const { createMessage } = require('./shared');
/**
* Represents a message on Discord.
@@ -368,41 +368,22 @@ class Message extends Base {
* .then(msg => console.log(`Updated the content of a message from ${msg.author}`))
* .catch(console.error);
*/
edit(content, options) {
async edit(content, options) {
if (!options && typeof content === 'object' && !(content instanceof Array)) {
options = content;
content = '';
content = null;
} else if (!options) {
options = {};
}
if (options instanceof Embed) options = { embed: options };
if (!options.content) options.content = content;
if (typeof options.content !== 'undefined') content = options.content;
if (typeof content !== 'undefined') content = Util.resolveString(content);
let { embed, code, reply } = options;
if (embed) embed = new Embed(embed)._apiTransform();
// Wrap everything in a code block
if (typeof code !== 'undefined' && (typeof code !== 'boolean' || code === true)) {
content = Util.escapeMarkdown(Util.resolveString(content), true);
content = `\`\`\`${typeof code !== 'boolean' ? code || '' : ''}\n${content}\n\`\`\``;
}
// Add the reply prefix
if (reply && this.channel.type !== 'dm') {
const id = this.client.users.resolveID(reply);
const mention = `<@${reply instanceof GuildMember && reply.nickname ? '!' : ''}${id}>`;
content = `${mention}${content ? `, ${content}` : ''}`;
}
const { data, files } = await createMessage(this, options);
return this.client.api.channels[this.channel.id].messages[this.id]
.patch({ data: { content, embed } })
.then(data => {
.patch({ data, files })
.then(d => {
const clone = this._clone();
clone._patch(data);
clone._patch(d);
return clone;
});
}

View File

@@ -132,12 +132,12 @@ class MessageEmbed {
proxyIconURL: data.footer.proxyIconURL || data.footer.proxy_icon_url,
} : null;
/**
* The files of this embed
* @type {?Object}
* @property {Array<FileOptions|string|MessageAttachment>} files Files to attach
*/
if (data.files) {
/**
* The files of this embed
* @type {?Object}
* @property {Array<FileOptions|string|MessageAttachment>} files Files to attach
*/
this.files = data.files.map(file => {
if (file instanceof MessageAttachment) {
return typeof file.file === 'string' ? file.file : Util.cloneObject(file.file);
@@ -158,7 +158,7 @@ class MessageEmbed {
/**
* The hexadecimal version of the embed color, with a leading hash
* @type {string}
* @type {?string}
* @readonly
*/
get hexColor() {

View File

@@ -117,19 +117,27 @@ class MessageMentions {
}
/**
* Check if a user is mentioned.
* Checks if a user, guild member, role, or channel is mentioned.
* Takes into account user mentions, role mentions, and @everyone/@here mentions.
* @param {UserResolvable|GuildMember|Role|GuildChannel} data User/GuildMember/Role/Channel to check
* @param {boolean} [strict=true] If role mentions and everyone/here mentions should be included
* @param {Object} [options] Options
* @param {boolean} [options.ignoreDirect=false] - Whether to ignore direct mentions to the item
* @param {boolean} [options.ignoreRoles=false] - Whether to ignore role mentions to a guild member
* @param {boolean} [options.ignoreEveryone=false] - Whether to ignore everyone/here mentions
* @returns {boolean}
*/
has(data, strict = true) {
if (strict && this.everyone) return true;
if (strict && data instanceof GuildMember) {
has(data, { ignoreDirect = false, ignoreRoles = false, ignoreEveryone = false } = {}) {
if (!ignoreEveryone && this.everyone) return true;
if (!ignoreRoles && data instanceof GuildMember) {
for (const role of this.roles.values()) if (data.roles.has(role.id)) return true;
}
const id = data.id || data;
return this.users.has(id) || this.channels.has(id) || this.roles.has(id);
if (!ignoreDirect) {
const id = data.id || data;
return this.users.has(id) || this.channels.has(id) || this.roles.has(id);
}
return false;
}
}

View File

@@ -1,6 +1,6 @@
const Collection = require('../util/Collection');
const Emoji = require('./Emoji');
const ReactionEmoji = require('./ReactionEmoji');
const ReactionUserStore = require('../stores/ReactionUserStore');
const { Error } = require('../errors');
/**
@@ -28,9 +28,9 @@ class MessageReaction {
/**
* The users that have given this reaction, mapped by their ID
* @type {Collection<Snowflake, User>}
* @type {ReactionUserStore<Snowflake, User>}
*/
this.users = new Collection();
this.users = new ReactionUserStore(client, undefined, this);
this._emoji = new ReactionEmoji(this, data.emoji.name, data.emoji.id);
}
@@ -77,26 +77,6 @@ class MessageReaction {
);
}
/**
* Fetches all the users that gave this reaction. Resolves with a collection of users, mapped by their IDs.
* @param {Object} [options] Options for fetching the users
* @param {number} [options.limit=100] The maximum amount of users to fetch, defaults to 100
* @param {Snowflake} [options.after] Limit fetching users to those with an id greater than the supplied id
* @returns {Promise<Collection<Snowflake, User>>}
*/
async fetchUsers({ limit = 100, after } = {}) {
const message = this.message;
const users = await message.client.api.channels[message.channel.id].messages[message.id]
.reactions[this.emoji.identifier]
.get({ query: { limit, after } });
for (const rawUser of users) {
const user = message.client.users.create(rawUser);
this.users.set(user.id, user);
}
this.count = this.users.size;
return this.users;
}
_add(user) {
if (!this.users.has(user.id)) {
this.users.set(user.id, user);

View File

@@ -19,7 +19,7 @@ class Presence {
* * **`dnd`** - user is in Do Not Disturb
* @type {string}
*/
this.status = data.status || this.status;
this.status = data.status || this.status || 'offline';
const activity = data.game || data.activity;
/**
@@ -38,7 +38,7 @@ class Presence {
}
/**
* Whether this presence is equal to another
* Whether this presence is equal to another.
* @param {Presence} presence The presence to compare with
* @returns {boolean}
*/
@@ -160,21 +160,22 @@ class RichPresenceAssets {
/**
* ID of the large image asset
* @type {?string}
* @type {?Snowflake}
*/
this.largeImage = assets.large_image || null;
/**
* ID of the small image asset
* @type {?string}
* @type {?Snowflake}
*/
this.smallImage = assets.small_image || null;
}
/**
* Gets the URL of the small image asset
* @param {string} format Format of the image
* @param {number} size Size of the image
* @param {Object} [options] Options for the image url
* @param {string} [options.format] Format of the image
* @param {number} [options.size] Size of the image
* @returns {?string} The small image URL
*/
smallImageURL({ format, size } = {}) {
@@ -185,8 +186,9 @@ class RichPresenceAssets {
/**
* Gets the URL of the large image asset
* @param {string} format Format of the image
* @param {number} size Size of the image
* @param {Object} [options] Options for the image url
* @param {string} [options.format] Format of the image
* @param {number} [options.size] Size of the image
* @returns {?string} The large image URL
*/
largeImageURL({ format, size } = {}) {

View File

@@ -83,7 +83,17 @@ class ReactionCollector extends Collector {
* @returns {?Snowflake|string}
*/
dispose(reaction) {
return reaction.message.id === this.message.id && !reaction.count ? ReactionCollector.key(reaction) : null;
if (reaction.message.id !== this.message.id) return null;
/**
* Emitted whenever a reaction is removed from a message. Will emit on all reaction removals,
* as opposed to {@link Collector#dispose} which will only be emitted when the entire reaction
* is removed.
* @event ReactionCollector#remove
* @param {MessageReaction} reaction The reaction that was removed
*/
if (this.collected.has(reaction)) this.emit('remove', reaction);
return reaction.count ? null : ReactionCollector.key(reaction);
}
/**

View File

@@ -35,11 +35,12 @@ class ReactionEmoji {
}
/**
* Creates the text required to form a graphical emoji on Discord.
* When concatenated with a string, this automatically returns the text required to form a graphical emoji on Discord
* instead of the ReactionEmoji object.
* @returns {string}
* @example
* // Send the emoji used in a reaction to the channel the reaction is part of
* reaction.message.channel.send(`The emoji used is ${reaction.emoji}`);
* @returns {string}
* reaction.message.channel.send(`The emoji used was: ${reaction.emoji}`);
*/
toString() {
return this.id ? `<:${this.name}:${this.id}>` : this.name;

View File

@@ -332,8 +332,11 @@ class Role extends Base {
}
/**
* When concatenated with a string, this automatically concatenates the role mention rather than the Role object.
* When concatenated with a string, this automatically returns the role's mention instead of the Role object.
* @returns {string}
* @example
* // Logs: Role: <@&123456789012345678>
* console.log(`Role: ${role}`);
*/
toString() {
if (this.id === this.guild.id) return '@everyone';

View File

@@ -38,6 +38,16 @@ class TextChannel extends GuildChannel {
if (data.messages) for (const message of data.messages) this.messages.create(message);
}
/**
* Sets whether this channel is flagged as NSFW.
* @param {boolean} nsfw Whether the channel should be considered NSFW
* @param {string} [reason] Reason for changing the channel's NSFW flag
* @returns {Promise<TextChannel>}
*/
setNSFW(nsfw, reason) {
return this.edit({ nsfw }, reason);
}
/**
* Fetches all webhooks for the channel.
* @returns {Promise<Collection<Snowflake, Webhook>>}

View File

@@ -20,6 +20,13 @@ class User extends Base {
*/
this.id = data.id;
/**
* Whether or not the user is a bot
* @type {boolean}
* @name User#bot
*/
this.bot = Boolean(data.bot);
this._patch(data);
}
@@ -40,18 +47,11 @@ class User extends Base {
/**
* The ID of the user's avatar
* @type {string}
* @type {?string}
* @name User#avatar
*/
if (typeof data.avatar !== 'undefined') this.avatar = data.avatar;
/**
* Whether or not the user is a bot
* @type {boolean}
* @name User#bot
*/
if (typeof this.bot === 'undefined' && typeof data.bot !== 'undefined') this.bot = Boolean(data.bot);
/**
* The ID of the last message sent by the user, if one was sent
* @type {?Snowflake}
@@ -63,8 +63,6 @@ class User extends Base {
* @type {?Message}
*/
this.lastMessage = null;
if (data.token) this.client.token = data.token;
}
/**
@@ -95,7 +93,7 @@ class User extends Base {
for (const guild of this.client.guilds.values()) {
if (guild.presences.has(this.id)) return guild.presences.get(this.id);
}
return new Presence();
return new Presence(this.client);
}
/**
@@ -250,10 +248,10 @@ class User extends Base {
}
/**
* When concatenated with a string, this automatically concatenates the user's mention instead of the User object.
* When concatenated with a string, this automatically returns the user's mention instead of the User object.
* @returns {string}
* @example
* // logs: Hello from <@123456789>!
* // Logs: Hello from <@123456789012345678>!
* console.log(`Hello from ${user}!`);
*/
toString() {

View File

@@ -1,9 +1,5 @@
const Util = require('../util/Util');
const DataResolver = require('../util/DataResolver');
const Embed = require('./MessageEmbed');
const MessageAttachment = require('./MessageAttachment');
const MessageEmbed = require('./MessageEmbed');
const { browser } = require('../util/Constants');
const { createMessage } = require('./shared');
/**
* Represents a webhook.
@@ -79,7 +75,6 @@ class Webhook {
* (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|BufferResolvable} [file] A file to send with the message
* @property {FileOptions[]|string[]} [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
@@ -99,115 +94,37 @@ class Webhook {
* .catch(console.error);
*/
/* eslint-enable max-len */
send(content, options) { // eslint-disable-line complexity
async send(content, options) { // eslint-disable-line complexity
if (!options && typeof content === 'object' && !(content instanceof Array)) {
options = content;
content = '';
content = null;
} else if (!options) {
options = {};
}
if (!options.content) options.content = content;
if (options instanceof MessageAttachment) options = { files: [options.file] };
if (options instanceof MessageEmbed) options = { embeds: [options] };
if (options.embed) options = { embeds: [options.embed] };
const { data, files } = await createMessage(this, options);
if (content instanceof Array || options instanceof Array) {
const which = content instanceof Array ? content : options;
const attachments = which.filter(item => item instanceof MessageAttachment);
const embeds = which.filter(item => item instanceof MessageEmbed);
if (attachments.length) options = { files: attachments };
if (embeds.length) options = { embeds };
if ((embeds.length || attachments.length) && content instanceof Array) content = '';
}
if (!options.username) options.username = this.name;
if (options.avatarURL) {
options.avatar_url = options.avatarURL;
options.avatarURL = null;
}
if (content) {
content = Util.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```';
}
if (data.content instanceof Array) {
const messages = [];
for (let i = 0; i < data.content.length; i++) {
const opt = i === data.content.length - 1 ? { embeds: data.embeds, files } : {};
Object.assign(opt, { avatarURL: data.avatar_url, content: data.content[i], username: data.username });
// eslint-disable-next-line no-await-in-loop
const message = await this.send(data.content[i], opt);
messages.push(message);
}
if (disableEveryone || (typeof disableEveryone === 'undefined' && this.client.options.disableEveryone)) {
content = content.replace(/@(everyone|here)/g, '@\u200b$1');
}
if (split) content = Util.splitMessage(content, split);
}
options.content = content;
if (options.embeds) options.embeds = options.embeds.map(embed => new Embed(embed)._apiTransform());
if (options.files) {
for (let i = 0; i < options.files.length; i++) {
let file = options.files[i];
if (typeof file === 'string' || (!browser && Buffer.isBuffer(file))) file = { attachment: file };
if (!file.name) {
if (typeof file.attachment === 'string') {
file.name = Util.basename(file.attachment);
} else if (file.attachment && file.attachment.path) {
file.name = Util.basename(file.attachment.path);
} else if (file instanceof MessageAttachment) {
file = { attachment: file.file, name: Util.basename(file.file) || 'file.jpg' };
} else {
file.name = 'file.jpg';
}
} else if (file instanceof MessageAttachment) {
file = file.file;
}
options.files[i] = file;
}
return Promise.all(options.files.map(file =>
DataResolver.resolveFile(file.attachment).then(resource => {
file.file = resource;
return file;
})
)).then(files => this.client.api.webhooks(this.id, this.token).post({
data: options,
query: { wait: true },
files,
auth: false,
}));
return messages;
}
if (content instanceof Array) {
return new Promise((resolve, reject) => {
const messages = [];
(function sendChunk() {
const opt = content.length ? null : { embeds: options.embeds, files: options.files };
this.client.api.webhooks(this.id, this.token).post({
data: { content: content.shift(), opt },
query: { wait: true },
auth: false,
})
.then(message => {
messages.push(message);
if (content.length === 0) return resolve(messages);
return sendChunk.call(this);
})
.catch(reject);
}.call(this));
});
}
return this.client.api.webhooks(this.id, this.token).post({
data: options,
data, files,
query: { wait: true },
auth: false,
}).then(data => {
if (!this.client.channels) return data;
return this.client.channels.get(data.channel_id).messages.create(data, false);
}).then(d => {
if (!this.client.channels) return d;
return this.client.channels.get(d.channel_id).messages.create(d, false);
});
}

View File

@@ -1,12 +1,7 @@
const MessageCollector = require('../MessageCollector');
const Shared = require('../shared');
const Util = require('../../util/Util');
const { browser } = require('../../util/Constants');
const Snowflake = require('../../util/Snowflake');
const Collection = require('../../util/Collection');
const DataResolver = require('../../util/DataResolver');
const MessageAttachment = require('../../structures/MessageAttachment');
const MessageEmbed = require('../../structures/MessageEmbed');
const { RangeError, TypeError } = require('../../errors');
/**
@@ -80,61 +75,12 @@ class TextBasedChannel {
send(content, options) { // eslint-disable-line complexity
if (!options && typeof content === 'object' && !(content instanceof Array)) {
options = content;
content = '';
content = null;
} else if (!options) {
options = {};
}
if (options instanceof MessageEmbed) options = { embed: options };
if (options instanceof MessageAttachment) options = { files: [options.file] };
if (content instanceof Array || options instanceof Array) {
const which = content instanceof Array ? content : options;
const attachments = which.filter(item => item instanceof MessageAttachment);
if (attachments.length) {
options = { files: attachments };
if (content instanceof Array) content = '';
}
}
if (!options.content) options.content = content;
if (options.embed && options.embed.files) {
if (options.files) options.files = options.files.concat(options.embed.files);
else options.files = options.embed.files;
}
if (options.files) {
for (let i = 0; i < options.files.length; i++) {
let file = options.files[i];
if (typeof file === 'string' || (!browser && Buffer.isBuffer(file))) file = { attachment: file };
if (!file.name) {
if (typeof file.attachment === 'string') {
file.name = Util.basename(file.attachment);
} else if (file.attachment && file.attachment.path) {
file.name = Util.basename(file.attachment.path);
} else if (file instanceof MessageAttachment) {
file = { attachment: file.file, name: Util.basename(file.file) || 'file.jpg' };
} else {
file.name = 'file.jpg';
}
} else if (file instanceof MessageAttachment) {
file = file.file;
}
options.files[i] = file;
}
return Promise.all(options.files.map(file =>
DataResolver.resolveFile(file.attachment).then(resource => {
file.file = resource;
return file;
})
)).then(files => {
options.files = files;
return Shared.sendMessage(this, options);
});
}
return Shared.sendMessage(this, options);
}
@@ -158,26 +104,45 @@ class TextBasedChannel {
/**
* Starts a typing indicator in the channel.
* @param {number} [count] The number of times startTyping should be considered to have been called
* @param {number} [count=1] The number of times startTyping should be considered to have been called
* @returns {Promise} Resolves once the bot stops typing gracefully, or rejects when an error occurs
* @example
* // Start typing in a channel
* // Start typing in a channel, or increase the typing count by one
* channel.startTyping();
* @example
* // Start typing in a channel with a typing count of five, or set it to five
* channel.startTyping(5);
*/
startTyping(count) {
if (typeof count !== 'undefined' && count < 1) throw new RangeError('TYPING_COUNT');
if (!this.client.user._typing.has(this.id)) {
const endpoint = this.client.api.channels[this.id].typing;
this.client.user._typing.set(this.id, {
count: count || 1,
interval: this.client.setInterval(() => {
endpoint.post();
}, 9000),
});
endpoint.post();
} else {
if (this.client.user._typing.has(this.id)) {
const entry = this.client.user._typing.get(this.id);
entry.count = count || entry.count + 1;
return entry.promise;
}
const entry = {};
entry.promise = new Promise((resolve, reject) => {
const endpoint = this.client.api.channels[this.id].typing;
Object.assign(entry, {
count: count || 1,
interval: this.client.setInterval(() => {
endpoint.post().catch(error => {
this.client.clearInterval(entry.interval);
this.client.user._typing.delete(this.id);
reject(error);
});
}, 9000),
resolve,
});
endpoint.post().catch(error => {
this.client.clearInterval(entry.interval);
this.client.user._typing.delete(this.id);
reject(error);
});
this.client.user._typing.set(this.id, entry);
});
return entry.promise;
}
/**
@@ -186,10 +151,10 @@ class TextBasedChannel {
* <info>It can take a few seconds for the client user to stop typing.</info>
* @param {boolean} [force=false] Whether or not to reset the call count and force the indicator to stop
* @example
* // Stop typing in a channel
* // Reduce the typing count by one and stop typing if it reached 0
* channel.stopTyping();
* @example
* // Force typing to fully stop in a channel
* // Force typing to fully stop regardless of typing count
* channel.stopTyping(true);
*/
stopTyping(force = false) {
@@ -199,6 +164,7 @@ class TextBasedChannel {
if (entry.count <= 0 || force) {
this.client.clearInterval(entry.interval);
this.client.user._typing.delete(this.id);
entry.resolve();
}
}
}

View File

@@ -0,0 +1,108 @@
const Embed = require('../MessageEmbed');
const DataResolver = require('../../util/DataResolver');
const MessageEmbed = require('../MessageEmbed');
const MessageAttachment = require('../MessageAttachment');
const { browser } = require('../../util/Constants');
const Util = require('../../util/Util');
// eslint-disable-next-line complexity
module.exports = async function createMessage(channel, options) {
const User = require('../User');
const GuildMember = require('../GuildMember');
const Webhook = require('../Webhook');
const WebhookClient = require('../../client/WebhookClient');
const webhook = channel instanceof Webhook || channel instanceof WebhookClient;
if (typeof options.nonce !== 'undefined') {
options.nonce = parseInt(options.nonce);
if (isNaN(options.nonce) || options.nonce < 0) throw new RangeError('MESSAGE_NONCE_TYPE');
}
if (options instanceof MessageEmbed) options = webhook ? { embeds: [options] } : { embed: options };
if (options instanceof MessageAttachment) options = { files: [options.file] };
if (options.reply && !(channel instanceof User || channel instanceof GuildMember) && channel.type !== 'dm') {
const id = channel.client.users.resolveID(options.reply);
const mention = `<@${options.reply instanceof GuildMember && options.reply.nickname ? '!' : ''}${id}>`;
if (options.split) options.split.prepend = `${mention}, ${options.split.prepend || ''}`;
options.content = `${mention}${typeof options.content !== 'undefined' ? `, ${options.content}` : ''}`;
}
if (options.content) {
options.content = Util.resolveString(options.content);
if (options.split && typeof options.split !== 'object') options.split = {};
// Wrap everything in a code block
if (typeof options.code !== 'undefined' && (typeof options.code !== 'boolean' || options.code === true)) {
options.content = Util.escapeMarkdown(options.content, true);
options.content =
`\`\`\`${typeof options.code !== 'boolean' ? options.code || '' : ''}\n${options.content}\n\`\`\``;
if (options.split) {
options.split.prepend = `\`\`\`${typeof options.code !== 'boolean' ? options.code || '' : ''}\n`;
options.split.append = '\n```';
}
}
// Add zero-width spaces to @everyone/@here
if (options.disableEveryone ||
(typeof options.disableEveryone === 'undefined' && channel.client.options.disableEveryone)) {
options.content = options.content.replace(/@(everyone|here)/g, '@\u200b$1');
}
if (options.split) options.content = Util.splitMessage(options.content, options.split);
}
if (options.embed && options.embed.files) {
if (options.files) options.files = options.files.concat(options.embed.files);
else options.files = options.embed.files;
}
if (options.embed && webhook) options.embeds = [new Embed(options.embed)._apiTransform()];
else if (options.embed) options.embed = new Embed(options.embed)._apiTransform();
else if (options.embeds) options.embeds = options.embeds.map(e => new Embed(e)._apiTransform());
let files;
if (options.files) {
for (let i = 0; i < options.files.length; i++) {
let file = options.files[i];
if (typeof file === 'string' || (!browser && Buffer.isBuffer(file))) file = { attachment: file };
if (!file.name) {
if (typeof file.attachment === 'string') {
file.name = Util.basename(file.attachment);
} else if (file.attachment && file.attachment.path) {
file.name = Util.basename(file.attachment.path);
} else if (file instanceof MessageAttachment) {
file = { attachment: file.file, name: Util.basename(file.file) || 'file.jpg' };
} else {
file.name = 'file.jpg';
}
} else if (file instanceof MessageAttachment) {
file = file.file;
}
options.files[i] = file;
}
files = await Promise.all(options.files.map(file =>
DataResolver.resolveFile(file.attachment).then(resource => {
file.file = resource;
return file;
})
));
}
if (webhook) {
if (!options.username) options.username = this.name;
if (options.avatarURL) options.avatar_url = options.avatarURL;
}
return { data: {
content: options.content,
tts: options.tts,
nonce: options.nonce,
embed: options.embed,
embeds: options.embeds,
username: options.username,
avatar_url: options.avatar_url,
}, files };
};

View File

@@ -1,4 +1,4 @@
const long = require('long');
const Util = require('../../util/Util');
const { TypeError } = require('../../errors');
/**
@@ -11,7 +11,7 @@ const { TypeError } = require('../../errors');
* @property {ChannelResolvable} [channel] Channel to limit search to (only for guild search endpoint)
* @property {UserResolvable} [author] Author to limit search
* @property {string} [authorType] One of `user`, `bot`, `webhook`, or add `-` to negate (e.g. `-webhook`)
* @property {string} [sortBy='recent'] `recent` or `relevant`
* @property {string} [sortBy='timestamp'] `timestamp` or `relevant`
* @property {string} [sortOrder='descending'] `ascending` or `descending`
* @property {number} [contextSize=2] How many messages to get around the matched message (0 to 2)
* @property {number} [limit=25] Maximum number of results to get (1 to 25)
@@ -40,17 +40,17 @@ module.exports = function search(target, options) {
if (typeof options === 'string') options = { content: options };
if (options.before) {
if (!(options.before instanceof Date)) options.before = new Date(options.before);
options.maxID = long.fromNumber(options.before.getTime() - 14200704e5).shiftLeft(22).toString();
options.maxID = Util.binaryToID((options.before.getTime() - 14200704e5).toString(2) + '0'.repeat(22));
}
if (options.after) {
if (!(options.after instanceof Date)) options.after = new Date(options.after);
options.minID = long.fromNumber(options.after.getTime() - 14200704e5).shiftLeft(22).toString();
options.minID = Util.binaryToID((options.after.getTime() - 14200704e5).toString(2) + '0'.repeat(22));
}
if (options.during) {
if (!(options.during instanceof Date)) options.during = new Date(options.during);
const t = options.during.getTime() - 14200704e5;
options.minID = long.fromNumber(t).shiftLeft(22).toString();
options.maxID = long.fromNumber(t + 864e5).shiftLeft(22).toString();
options.minID = Util.binaryToID(t.toString(2) + '0'.repeat(22));
options.maxID = Util.binaryToID((t + 864e5).toString(2) + '0'.repeat(22));
}
if (options.channel) options.channel = target.client.channels.resolveID(options.channel);
if (options.author) options.author = target.client.users.resolveID(options.author);

View File

@@ -1,65 +1,23 @@
const Util = require('../../util/Util');
const Embed = require('../MessageEmbed');
const { RangeError } = require('../../errors');
const createMessage = require('./CreateMessage');
module.exports = function sendMessage(channel, options) { // eslint-disable-line complexity
module.exports = async function sendMessage(channel, options) { // eslint-disable-line complexity
const User = require('../User');
const GuildMember = require('../GuildMember');
if (channel instanceof User || channel instanceof GuildMember) return channel.createDM().then(dm => dm.send(options));
let { content, nonce, reply, code, disableEveryone, tts, embed, files, split } = options;
if (embed) embed = new Embed(embed)._apiTransform();
const { data, files } = await createMessage(channel, options);
if (typeof nonce !== 'undefined') {
nonce = parseInt(nonce);
if (isNaN(nonce) || nonce < 0) throw new RangeError('MESSAGE_NONCE_TYPE');
}
// Add the reply prefix
if (reply && !(channel instanceof User || channel instanceof GuildMember) && channel.type !== 'dm') {
const id = channel.client.users.resolveID(reply);
const mention = `<@${reply instanceof GuildMember && reply.nickname ? '!' : ''}${id}>`;
if (split) split.prepend = `${mention}, ${split.prepend || ''}`;
content = `${mention}${typeof content !== 'undefined' ? `, ${content}` : ''}`;
}
if (content) {
content = Util.resolveString(content);
if (split && typeof split !== 'object') split = {};
// Wrap everything in a code block
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```';
}
if (data.content instanceof Array) {
const messages = [];
for (let i = 0; i < data.content.length; i++) {
const opt = i === data.content.length - 1 ? { tts: data.tts, embed: data.embed, files } : { tts: data.tts };
// eslint-disable-next-line no-await-in-loop
const message = await channel.send(data.content[i], opt);
messages.push(message);
}
// Add zero-width spaces to @everyone/@here
if (disableEveryone || (typeof disableEveryone === 'undefined' && channel.client.options.disableEveryone)) {
content = content.replace(/@(everyone|here)/g, '@\u200b$1');
}
if (split) content = Util.splitMessage(content, split);
return messages;
}
if (content instanceof Array) {
return new Promise((resolve, reject) => {
const messages = [];
(function sendChunk() {
const opt = content.length ? { tts } : { tts, embed, files };
channel.send(content.shift(), opt).then(message => {
messages.push(message);
if (content.length === 0) return resolve(messages);
return sendChunk();
}).catch(reject);
}());
});
}
return channel.client.api.channels[channel.id].messages.post({
data: { content, tts, nonce, embed },
files,
}).then(data => channel.client.actions.MessageCreate.handle(data).message);
return channel.client.api.channels[channel.id].messages.post({ data, files })
.then(d => channel.client.actions.MessageCreate.handle(d).message);
};

View File

@@ -1,4 +1,5 @@
module.exports = {
search: require('./Search'),
sendMessage: require('./SendMessage'),
createMessage: require('./CreateMessage'),
};

View File

@@ -52,7 +52,7 @@ exports.DefaultOptions = {
* WebSocket options (these are left as snake_case to match the API)
* @typedef {Object} WebsocketOptions
* @property {number} [large_threshold=250] Number of members in a guild to be considered large
* @property {boolean} [compress=true] Whether to compress data sent on the connection
* @property {boolean} [compress=false] Whether to compress data sent on the connection
* (defaults to `false` for browsers)
*/
ws: {
@@ -114,6 +114,7 @@ exports.Endpoints = {
Asset: name => `${root}/assets/${name}`,
DefaultAvatar: number => `${root}/embed/avatars/${number}.png`,
Avatar: (userID, hash, format = 'default', size) => {
if (userID === '1') return hash;
if (format === 'default') format = hash.startsWith('a_') ? 'gif' : 'webp';
return makeImageUrl(`${root}/avatars/${userID}/${hash}`, { format, size });
},

View File

@@ -1,4 +1,4 @@
const Long = require('long');
const Util = require('../util/Util');
// Discord epoch (2015-01-01T00:00:00.000Z)
const EPOCH = 1420070400000;
@@ -31,8 +31,9 @@ class SnowflakeUtil {
*/
static generate() {
if (INCREMENT >= 4095) INCREMENT = 0;
const BINARY = `${pad((Date.now() - EPOCH).toString(2), 42)}0000100000${pad((INCREMENT++).toString(2), 12)}`;
return Long.fromString(BINARY, 2).toString();
// eslint-disable-next-line max-len
const BINARY = `${(Date.now() - EPOCH).toString(2).padStart(42, '0')}0000100000${(INCREMENT++).toString(2).padStart(12, '0')}`;
return Util.binaryToID(BINARY);
}
/**
@@ -52,7 +53,7 @@ class SnowflakeUtil {
* @returns {DeconstructedSnowflake} Deconstructed snowflake
*/
static deconstruct(snowflake) {
const BINARY = pad(Long.fromString(snowflake).toString(2), 64);
const BINARY = Util.idToBinary(snowflake).toString(2).padStart(64, '0');
const res = {
timestamp: parseInt(BINARY.substring(0, 42), 2) + EPOCH,
workerID: parseInt(BINARY.substring(42, 47), 2),
@@ -68,8 +69,4 @@ class SnowflakeUtil {
}
}
function pad(v, n, c = '0') {
return String(v).length >= n ? String(v) : (String(c).repeat(n) + v).slice(-n);
}
module.exports = SnowflakeUtil;

80
src/util/Structures.js Normal file
View File

@@ -0,0 +1,80 @@
/**
* Allows for the extension of built-in Discord.js structures that are instantiated by {@link DataStore DataStores}.
*/
class Structures {
constructor() {
throw new Error(`The ${this.constructor.name} class may not be instantiated.`);
}
/**
* Retrieves a structure class.
* @param {string} structure Name of the structure to retrieve
* @returns {Function}
*/
static get(structure) {
if (typeof structure === 'string') return structures[structure];
throw new TypeError(`"structure" argument must be a string (received ${typeof structure})`);
}
/**
* Extends a structure.
* @param {string} structure Name of the structure class to extend
* @param {Function} extender Function that takes the base class to extend as its only parameter and returns the
* extended class/prototype
* @returns {Function} Extended class/prototype returned from the extender
* @example
* const { Structures } = require('discord.js');
*
* Structures.extend('Guild', Guild => {
* class CoolGuild extends Guild {
* constructor(client, data) {
* super(client, data);
* this.cool = true;
* }
* }
*
* return CoolGuild;
* });
*/
static extend(structure, extender) {
if (!structures[structure]) throw new RangeError(`"${structure}" is not a valid extensible structure.`);
if (typeof extender !== 'function') {
const received = `(received ${typeof extender})`;
throw new TypeError(
`"extender" argument must be a function that returns the extended structure class/prototype ${received}`
);
}
const extended = extender(structures[structure]);
if (typeof extended !== 'function') {
throw new TypeError('The extender function must return the extended structure class/prototype.');
}
if (Object.getPrototypeOf(extended) !== structures[structure]) {
throw new Error(
'The class/prototype returned from the extender function must extend the existing structure class/prototype.'
);
}
structures[structure] = extended;
return extended;
}
}
const structures = {
Emoji: require('../structures/Emoji'),
DMChannel: require('../structures/DMChannel'),
GroupDMChannel: require('../structures/GroupDMChannel'),
TextChannel: require('../structures/TextChannel'),
VoiceChannel: require('../structures/VoiceChannel'),
CategoryChannel: require('../structures/CategoryChannel'),
GuildChannel: require('../structures/GuildChannel'),
GuildMember: require('../structures/GuildMember'),
Guild: require('../structures/Guild'),
Message: require('../structures/Message'),
MessageReaction: require('../structures/MessageReaction'),
Presence: require('../structures/Presence').Presence,
Role: require('../structures/Role'),
User: require('../structures/User'),
};
module.exports = Structures;

View File

@@ -1,4 +1,3 @@
const Long = require('long');
const snekfetch = require('snekfetch');
const { Colors, DefaultOptions, Endpoints } = require('./Constants');
const { Error: DiscordError, RangeError, TypeError } = require('../errors');
@@ -22,9 +21,7 @@ class Util {
static splitMessage(text, { maxLength = 1950, char = '\n', prepend = '', append = '' } = {}) {
if (text.length <= maxLength) return text;
const splitText = text.split(char);
if (splitText.length === 1) {
throw new RangeError('SPLIT_MAX_LEN');
}
if (splitText.length === 1) throw new RangeError('SPLIT_MAX_LEN');
const messages = [''];
let msg = 0;
for (let i = 0; i < splitText.length; i++) {
@@ -84,10 +81,7 @@ class Util {
const [name, id] = text.split(':');
return { name, id };
} else {
return {
name: text,
id: null,
};
return { name: text, id: null };
}
}
@@ -226,7 +220,6 @@ class Util {
* @param {StringResolvable} data The string resolvable to resolve
* @returns {string}
*/
static resolveString(data) {
if (typeof data === 'string') return data;
if (data instanceof Array) return data.join('\n');
@@ -234,39 +227,34 @@ class Util {
}
/**
* Can be a Hex Literal, Hex String, Number, RGB Array, or one of the following
* Can be a number, hex string, an RGB array like:
* ```js
* [255, 0, 255] // purple
* ```
* [
* 'DEFAULT',
* 'AQUA',
* 'GREEN',
* 'BLUE',
* 'PURPLE',
* 'GOLD',
* 'ORANGE',
* 'RED',
* 'GREY',
* 'DARKER_GREY',
* 'NAVY',
* 'DARK_AQUA',
* 'DARK_GREEN',
* 'DARK_BLUE',
* 'DARK_PURPLE',
* 'DARK_GOLD',
* 'DARK_ORANGE',
* 'DARK_RED',
* 'DARK_GREY',
* 'LIGHT_GREY',
* 'DARK_NAVY',
* 'RANDOM',
* ]
* ```
* or something like
* ```
* [255, 0, 255]
* ```
* for purple
* @typedef {string|number|Array} ColorResolvable
* or one of the following strings:
* - `DEFAULT`
* - `AQUA`
* - `GREEN`
* - `BLUE`
* - `PURPLE`
* - `GOLD`
* - `ORANGE`
* - `RED`
* - `GREY`
* - `DARKER_GREY`
* - `NAVY`
* - `DARK_AQUA`
* - `DARK_GREEN`
* - `DARK_BLUE`
* - `DARK_PURPLE`
* - `DARK_GOLD`
* - `DARK_ORANGE`
* - `DARK_RED`
* - `DARK_GREY`
* - `LIGHT_GREY`
* - `DARK_NAVY`
* - `RANDOM`
* @typedef {string|number|number[]} ColorResolvable
*/
/**
@@ -274,7 +262,6 @@ class Util {
* @param {ColorResolvable} color Color to resolve
* @returns {number} A color
*/
static resolveColor(color) {
if (typeof color === 'string') {
if (color === 'RANDOM') return Math.floor(Math.random() * (0xFFFFFF + 1));
@@ -283,25 +270,36 @@ class Util {
color = (color[0] << 16) + (color[1] << 8) + color[2];
}
if (color < 0 || color > 0xFFFFFF) {
throw new RangeError('COLOR_RANGE');
} else if (color && isNaN(color)) {
throw new TypeError('COLOR_CONVERT');
}
if (color < 0 || color > 0xFFFFFF) throw new RangeError('COLOR_RANGE');
else if (color && isNaN(color)) throw new TypeError('COLOR_CONVERT');
return color;
}
/**
* Sorts by Discord's position and then by ID.
* Sorts by Discord's position and ID.
* @param {Collection} collection Collection of objects to sort
* @returns {Collection}
*/
static discordSort(collection) {
return collection
.sort((a, b) => a.rawPosition - b.rawPosition || Long.fromString(a.id).sub(Long.fromString(b.id)).toNumber());
return collection.sort((a, b) =>
a.rawPosition - b.rawPosition ||
parseInt(a.id.slice(0, -10)) - parseInt(b.id.slice(0, -10)) ||
parseInt(a.id.slice(10)) - parseInt(b.id.slice(10))
);
}
/**
* Sets the position of a Channel or Role.
* @param {Channel|Role} item Object to set the position of
* @param {number} position New position for the object
* @param {boolean} relative Whether `position` is relative to its current position
* @param {Collection<string, Channel|Role>} sorted A collection of the objects sorted properly
* @param {APIRouter} route Route to call PATCH on
* @param {string} [reason] Reason for the change
* @returns {Promise<Object[]>} Updated item list, with `id` and `position` properties
* @private
*/
static setPosition(item, position, relative, sorted, route, reason) {
let updatedItems = sorted.array();
Util.moveElementInArray(updatedItems, item, position, relative);
@@ -309,13 +307,77 @@ class Util {
return route.patch({ data: updatedItems, reason }).then(() => updatedItems);
}
/**
* Alternative to Node's `path.basename` that we have for some (probably stupid) reason.
* @param {string} path Path to get the basename of
* @param {string} [ext] File extension to remove
* @returns {string} Basename of the path
* @private
*/
static basename(path, ext) {
let f = splitPathRe.exec(path).slice(1)[2];
if (ext && f.substr(-1 * ext.length) === ext) {
f = f.substr(0, f.length - ext.length);
}
if (ext && f.substr(-1 * ext.length) === ext) f = f.substr(0, f.length - ext.length);
return f;
}
/**
* Transforms a snowflake from a decimal string to a bit string.
* @param {Snowflake} num Snowflake to be transformed
* @returns {string}
* @private
*/
static idToBinary(num) {
let bin = '';
let high = parseInt(num.slice(0, -10)) || 0;
let low = parseInt(num.slice(-10));
while (low > 0 || high > 0) {
bin = String(low & 1) + bin;
low = Math.floor(low / 2);
if (high > 0) {
low += 5000000000 * (high % 2);
high = Math.floor(high / 2);
}
}
return bin;
}
/**
* Transforms a snowflake from a bit string to a decimal string.
* @param {string} num Bit string to be transformed
* @returns {Snowflake}
* @private
*/
static binaryToID(num) {
let dec = '';
while (num.length > 50) {
const high = parseInt(num.slice(0, -32), 2);
const low = parseInt((high % 10).toString(2) + num.slice(-32), 2);
dec = (low % 10).toString() + dec;
num = Math.floor(high / 10).toString(2) + Math.floor(low / 10).toString(2).padStart(32, '0');
}
num = parseInt(num, 2);
while (num > 0) {
dec = (num % 10).toString() + dec;
num = Math.floor(num / 10);
}
return dec;
}
/**
* Creates a Promise that resolves after a specified duration.
* @param {number} ms How long to wait before resolving (in milliseconds)
* @returns {Promise<void>}
* @private
*/
static delayFor(ms) {
return new Promise(resolve => {
setTimeout(resolve, ms);
});
}
}
module.exports = Util;

View File

@@ -24,12 +24,6 @@ else
SOURCE_TYPE="branch"
fi
# For Node != 8, do nothing
if [ "$TRAVIS_NODE_VERSION" != "8" ]; then
echo -e "\e[36m\e[1mBuild triggered with Node v${TRAVIS_NODE_VERSION} - doing nothing."
exit 0
fi
# Run the build
npm run docs
NODE_ENV=production npm run build:browser
@@ -87,4 +81,3 @@ git config user.name "Travis CI"
git config user.email "$COMMIT_AUTHOR_EMAIL"
git commit -m "Webpack build for ${SOURCE_TYPE} ${SOURCE}: ${SHA}" || true
git push $SSH_REPO $TARGET_BRANCH

Submodule typings updated: 697fc933de...0b5b13f4a5