Errors Standardization (#1246)

* errors and stuff

* more errors

* all the errors

* fix build
This commit is contained in:
Gus Caplan
2017-06-25 12:48:05 -05:00
committed by Amish Shah
parent 602fe06f88
commit 63e54982f4
28 changed files with 258 additions and 102 deletions

View File

@@ -19,6 +19,7 @@ const Invite = require('../structures/Invite');
const OAuth2Application = require('../structures/OAuth2Application');
const ShardClientUtil = require('../sharding/ShardClientUtil');
const VoiceBroadcast = require('./voice/VoiceBroadcast');
const { Error, TypeError, RangeError } = require('../errors');
/**
* The main hub for interacting with the Discord API, and the starting point for any bot.
@@ -287,7 +288,7 @@ class Client extends EventEmitter {
*/
login(token) {
return new Promise((resolve, reject) => {
if (typeof token !== 'string') throw new Error(Constants.Errors.INVALID_TOKEN);
if (typeof token !== 'string') throw new Error('TOKEN_INVALID');
token = token.replace(/^Bot\s*/i, '');
this.manager.connectToWebSocket(token, resolve, reject);
});
@@ -375,7 +376,9 @@ class Client extends EventEmitter {
* or -1 if the message cache lifetime is unlimited
*/
sweepMessages(lifetime = this.options.messageCacheLifetime) {
if (typeof lifetime !== 'number' || isNaN(lifetime)) throw new TypeError('The lifetime must be a number.');
if (typeof lifetime !== 'number' || isNaN(lifetime)) {
throw new TypeError('CLIENT_INVALID_OPTION', 'Lifetime', 'a number');
}
if (lifetime <= 0) {
this.emit('debug', 'Didn\'t sweep messages - lifetime is unlimited');
return -1;
@@ -524,41 +527,40 @@ class Client extends EventEmitter {
*/
_validateOptions(options = this.options) {
if (typeof options.shardCount !== 'number' || isNaN(options.shardCount)) {
throw new TypeError('The shardCount option must be a number.');
throw new TypeError('CLIENT_INVALID_OPTION', 'shardCount', 'a number');
}
if (typeof options.shardId !== 'number' || isNaN(options.shardId)) {
throw new TypeError('The shardId option must be a number.');
throw new TypeError('CLIENT_INVALID_OPTION', 'shardId', 'a number');
}
if (options.shardCount < 0) throw new RangeError('The shardCount option must be at least 0.');
if (options.shardId < 0) throw new RangeError('The shardId option must be at least 0.');
if (options.shardCount < 0) throw new RangeError('CLIENT_INVALID_OPTION', 'shardCount', 'at least 0');
if (options.shardId < 0) throw new RangeError('CLIENT_INVALID_OPTION', 'shardId', 'at least 0');
if (options.shardId !== 0 && options.shardId >= options.shardCount) {
throw new RangeError('The shardId option must be less than shardCount.');
throw new RangeError('CLIENT_INVALID_OPTION', 'shardId', 'less than shardCount');
}
if (typeof options.messageCacheMaxSize !== 'number' || isNaN(options.messageCacheMaxSize)) {
throw new TypeError('The messageCacheMaxSize option must be a number.');
throw new TypeError('CLIENT_INVALID_OPTION', 'messageCacheMaxSize', 'a number');
}
if (typeof options.messageCacheLifetime !== 'number' || isNaN(options.messageCacheLifetime)) {
throw new TypeError('The messageCacheLifetime option must be a number.');
throw new TypeError('CLIENT_INVALID_OPTION', 'The messageCacheLifetime', 'a number');
}
if (typeof options.messageSweepInterval !== 'number' || isNaN(options.messageSweepInterval)) {
throw new TypeError('The messageSweepInterval option must be a number.');
throw new TypeError('CLIENT_INVALID_OPTION', 'messageSweepInterval', 'a number');
}
if (typeof options.fetchAllMembers !== 'boolean') {
throw new TypeError('The fetchAllMembers option must be a boolean.');
throw new TypeError('CLIENT_INVALID_OPTION', 'fetchAllMembers', 'a boolean');
}
if (typeof options.disableEveryone !== 'boolean') {
throw new TypeError('The disableEveryone option must be a boolean.');
throw new TypeError('CLIENT_INVALID_OPTION', 'disableEveryone', 'a boolean');
}
if (typeof options.restWsBridgeTimeout !== 'number' || isNaN(options.restWsBridgeTimeout)) {
throw new TypeError('The restWsBridgeTimeout option must be a number.');
throw new TypeError('CLIENT_INVALID_OPTION', 'restWsBridgeTimeout', 'a number');
}
if (typeof options.internalSharding !== 'boolean') {
throw new TypeError('The internalSharding option must be a boolean.');
throw new TypeError('CLIENT_INVALID_OPTION', 'internalSharding', 'a boolean');
}
if (options.internalSharding && ('shardCount' in options || 'shardId' in options)) {
throw new TypeError('You cannot specify shardCount/shardId if you are using internal sharding.');
if (!(options.disabledEvents instanceof Array)) {
throw new TypeError('CLIENT_INVALID_OPTION', 'disabledEvents', 'an Array');
}
if (!(options.disabledEvents instanceof Array)) throw new TypeError('The disabledEvents option must be an Array.');
}
}

View File

@@ -10,6 +10,7 @@ const Channel = require('../structures/Channel');
const GuildMember = require('../structures/GuildMember');
const Emoji = require('../structures/Emoji');
const ReactionEmoji = require('../structures/ReactionEmoji');
const { Error, TypeError } = require('../errors');
/**
* The DataResolver identifies different objects and tries to resolve a specific piece of information from them, e.g.
@@ -194,14 +195,14 @@ class ClientDataResolver {
snekfetch.get(resource)
.end((err, res) => {
if (err) return reject(err);
if (!(res.body instanceof Buffer)) return reject(new TypeError('The response body isn\'t a Buffer.'));
if (!(res.body instanceof Buffer)) return reject(new TypeError('REQ_BODY_TYPE'));
return resolve(res.body);
});
} else {
const file = path.resolve(resource);
fs.stat(file, (err, stats) => {
if (err) return reject(err);
if (!stats || !stats.isFile()) return reject(new Error(`The file could not be found: ${file}`));
if (!stats || !stats.isFile()) return reject(new Error('FILE_NOT_FOUND', file));
fs.readFile(file, (err2, data) => {
if (err2) reject(err2); else resolve(data);
});
@@ -211,7 +212,7 @@ class ClientDataResolver {
});
}
return Promise.reject(new TypeError('The resource must be a string or Buffer.'));
return Promise.reject(new TypeError('REQ_RESOURCE_TYPE'));
}
/**

View File

@@ -1,5 +1,6 @@
const Constants = require('../util/Constants');
const WebSocketConnection = require('./websocket/WebSocketConnection');
const { Error } = require('../errors');
/**
* Manages the state and background tasks of the client.
@@ -37,16 +38,16 @@ class ClientManager {
connectToWebSocket(token, resolve, reject) {
this.client.emit(Constants.Events.DEBUG, `Authenticated using token ${token}`);
this.client.token = token;
const timeout = this.client.setTimeout(() => reject(new Error(Constants.Errors.TOOK_TOO_LONG)), 1000 * 300);
const timeout = this.client.setTimeout(() => reject(new Error('INVALID_TOKEN')), 1000 * 300);
this.client.api.gateway.get().then(res => {
const protocolVersion = Constants.DefaultOptions.ws.version;
const gateway = `${res.url}/?v=${protocolVersion}&encoding=${WebSocketConnection.ENCODING}`;
this.client.emit(Constants.Events.DEBUG, `Using gateway ${gateway}`);
this.client.ws.connect(gateway);
this.client.ws.connection.once('close', event => {
if (event.code === 4004) reject(new Error(Constants.Errors.BAD_LOGIN));
if (event.code === 4010) reject(new Error(Constants.Errors.INVALID_SHARD));
if (event.code === 4011) reject(new Error(Constants.Errors.SHARDING_REQUIRED));
if (event.code === 4004) reject(new Error('TOKEN_INVALID'));
if (event.code === 4010) reject(new Error('SHARDING_INVALID'));
if (event.code === 4011) reject(new Error('SHARDING_REQUIRED'));
});
this.client.once(Constants.Events.READY, () => {
resolve(token);

View File

@@ -1,6 +1,6 @@
const querystring = require('querystring');
const snekfetch = require('snekfetch');
const Constants = require('../../util/Constants');
const { Error } = require('../../errors');
class APIRequest {
constructor(rest, method, path, options) {
@@ -28,7 +28,7 @@ class APIRequest {
} else if (this.client.token) {
return this.client.token;
}
throw new Error(Constants.Errors.NO_TOKEN);
throw new Error('TOKEN_MISSING');
}
gen() {

View File

@@ -3,7 +3,7 @@ const SequentialRequestHandler = require('./RequestHandlers/Sequential');
const BurstRequestHandler = require('./RequestHandlers/Burst');
const APIRequest = require('./APIRequest');
const mountApi = require('./APIRouter');
const Constants = require('../../util/Constants');
const { Error } = require('../../errors');
class RESTManager {
constructor(client) {
@@ -39,7 +39,7 @@ class RESTManager {
case 'burst':
return BurstRequestHandler;
default:
throw new Error(Constants.Errors.INVALID_RATE_LIMIT_METHOD);
throw new Error('RATELIMIT_INVALID_METHOD');
}
}

View File

@@ -1,5 +1,6 @@
const Collection = require('../../util/Collection');
const VoiceConnection = require('./VoiceConnection');
const { Error } = require('../../errors');
/**
* Manages all the voice stuff for the client.
@@ -44,11 +45,7 @@ class ClientVoiceManager {
joinChannel(channel) {
return new Promise((resolve, reject) => {
if (!channel.joinable) {
if (channel.full) {
throw new Error('You do not have permission to join this voice channel; it is full.');
} else {
throw new Error('You do not have permission to join this voice channel.');
}
throw new Error('VOICE_JOIN_CHANNEL', channel.full);
}
let connection = this.connections.get(channel.guild.id);

View File

@@ -6,6 +6,7 @@ const AudioPlayer = require('./player/AudioPlayer');
const VoiceReceiver = require('./receiver/VoiceReceiver');
const EventEmitter = require('events').EventEmitter;
const Prism = require('prism-media');
const { Error } = require('../../errors');
/**
* Represents a connection to a guild's voice server.
@@ -341,8 +342,8 @@ class VoiceConnection extends EventEmitter {
*/
connect() {
if (this.status !== Constants.VoiceStatus.RECONNECTING) {
if (this.sockets.ws) throw new Error('There is already an existing WebSocket connection.');
if (this.sockets.udp) throw new Error('There is already an existing UDP connection.');
if (this.sockets.ws) throw new Error('WS_CONNECTION_EXISTS');
if (this.sockets.udp) throw new Error('UDP_CONNECTION_EXISTS');
}
if (this.sockets.ws) this.sockets.ws.shutdown();

View File

@@ -2,6 +2,7 @@ const udp = require('dgram');
const dns = require('dns');
const Constants = require('../../util/Constants');
const EventEmitter = require('events').EventEmitter;
const { Error } = require('../../errors');
/**
* Represents a UDP client for a Voice Connection.
@@ -89,8 +90,8 @@ class VoiceConnectionUDPClient extends EventEmitter {
*/
send(packet) {
return new Promise((resolve, reject) => {
if (!this.socket) throw new Error('Tried to send a UDP packet, but there is no socket available.');
if (!this.discordAddress || !this.discordPort) throw new Error('Malformed UDP address or port.');
if (!this.socket) throw new Error('UDP_SEND_FAIL');
if (!this.discordAddress || !this.discordPort) throw new Error('UDP_ADDRESS_MALFORMED');
this.socket.send(packet, 0, packet.length, this.discordPort, this.discordAddress, error => {
if (error) reject(error); else resolve(packet);
});

View File

@@ -1,6 +1,7 @@
const Constants = require('../../util/Constants');
const SecretKey = require('./util/SecretKey');
const EventEmitter = require('events').EventEmitter;
const { Error } = require('../../errors');
let WebSocket;
try {
@@ -88,9 +89,7 @@ class VoiceWebSocket extends EventEmitter {
*/
send(data) {
return new Promise((resolve, reject) => {
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
throw new Error(`Voice websocket not open to send ${data}.`);
}
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) throw new Error('WS_NOT_OPEN', data);
this.ws.send(data, null, error => {
if (error) reject(error); else resolve(data);
});

View File

@@ -1,3 +1,5 @@
const { Error } = require('../../../errors');
const list = [
require('./NodeOpusEngine'),
require('./OpusScriptEngine'),
@@ -30,5 +32,5 @@ exports.fetch = engineOptions => {
exports.guaranteeOpusEngine = () => {
if (typeof opusEngineFound === 'undefined') opusEngineFound = Boolean(exports.fetch());
if (!opusEngineFound) throw new Error('Couldn\'t find an Opus engine.');
if (!opusEngineFound) throw new Error('OPUS_ENGINE_MISSING');
};

View File

@@ -2,6 +2,7 @@ const EventEmitter = require('events').EventEmitter;
const secretbox = require('../util/Secretbox');
const Readable = require('./VoiceReadable');
const OpusEncoders = require('../opus/OpusEngineList');
const { Error } = require('../../../errors');
const nonce = Buffer.alloc(24);
nonce.fill(0);
@@ -122,8 +123,8 @@ class VoiceReceiver extends EventEmitter {
*/
createOpusStream(user) {
user = this.voiceConnection.voiceManager.client.resolver.resolveUser(user);
if (!user) throw new Error('Couldn\'t resolve the user to create Opus stream.');
if (this.opusStreams.get(user.id)) throw new Error('There is already an existing stream for that user.');
if (!user) throw new Error('VOICE_USER_MISSING');
if (this.opusStreams.get(user.id)) throw new Error('VOICE_STREAM_EXISTS');
const stream = new Readable();
this.opusStreams.set(user.id, stream);
return stream;
@@ -137,8 +138,8 @@ class VoiceReceiver extends EventEmitter {
*/
createPCMStream(user) {
user = this.voiceConnection.voiceManager.client.resolver.resolveUser(user);
if (!user) throw new Error('Couldn\'t resolve the user to create PCM stream.');
if (this.pcmStreams.get(user.id)) throw new Error('There is already an existing stream for that user.');
if (!user) throw new Error('VOICE_USER_MISSING');
if (this.pcmStreams.get(user.id)) throw new Error('VOICE_STREAM_EXISTS');
const stream = new Readable();
this.pcmStreams.set(user.id, stream);
return stream;

65
src/errors/DJSError.js Normal file
View File

@@ -0,0 +1,65 @@
// Heavily inspired by node's `internal/errors` module
const kCode = Symbol('code');
const messages = new Map();
const assert = require('assert');
const util = require('util');
/**
* Extend an error of some sort into a DiscordjsError
* @param {Error} Base Base error to extend
* @returns {DiscordjsError}
*/
function makeDiscordjsError(Base) {
return class DiscordjsError extends Base {
constructor(key, ...args) {
super(message(key, args));
this[kCode] = key;
if (Error.captureStackTrace) Error.captureStackTrace(this, DiscordjsError);
}
get name() {
return `${super.name} [${this[kCode]}]`;
}
get code() {
return this[kCode];
}
};
}
/**
* Format the message for an error
* @param {string} key Error key
* @param {Array<*>} args Arguments to pass for util format or as function args
* @returns {string} Formatted string
*/
function message(key, args) {
assert.strictEqual(typeof key, 'string');
const msg = messages.get(key);
assert(msg, `An invalid error message key was used: ${key}.`);
let fmt = util.format;
if (typeof msg === 'function') {
fmt = msg;
} else {
if (args === undefined || args.length === 0) return msg;
args.unshift(msg);
}
return String(fmt(...args));
}
/**
* Register an error code and message
* @param {string} sym Unique name for the error
* @param {*} val Value of the error
*/
function register(sym, val) {
messages.set(sym, typeof val === 'function' ? val : String(val));
}
module.exports = {
register,
Error: makeDiscordjsError(Error),
TypeError: makeDiscordjsError(TypeError),
RangeError: makeDiscordjsError(RangeError),
};

84
src/errors/Messages.js Normal file
View File

@@ -0,0 +1,84 @@
const { register } = require('./DJSError');
const Messages = {
CLIENT_INVALID_OPTION: (prop, must) => `The ${prop} option must be ${must}`,
TOKEN_INVALID: 'An invalid token was provided.',
TOKEN_MISSING: 'Request to use token, but token was unavailable to the client.',
FEATURE_BOT_ONLY: 'Only bot accounts are able to make use of this feature.',
FEATURE_USER_ONLY: 'Only user accounts are able to make use of this feature.',
WS_BAD_MESSAGE: 'A bad message was received from the websocket; either bad compression, or not JSON.',
WS_CONNECTION_EXISTS: 'There is already an existing WebSocket connection.',
WS_NOT_OPEN: (data = 'data') => `Websocket not open to send ${data}`,
PERMISSIONS_INVALID: 'Invalid permission string or number.',
PERMISSIONS_INVALID_FLAG: 'Invalid bitfield flag string or number',
RATELIMIT_INVALID_METHOD: 'Unknown rate limiting method.',
SHARDING_INVALID: 'Invalid shard settings were provided.',
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`,
COLOR_RANGE: 'Color must be within the range 0 - 16777215 (0xFFFFFF).',
COLOR_CONVERT: 'Unable to convert color to a number.',
EMBED_FIELD_COUNT: 'MessageEmbeds may not exceed 25 fields.',
EMBED_FIELD_NAME: 'MessageEmbed field names may not exceed 256 characters or be empty.',
EMBED_FIELD_VALUE: 'MessageEmbed field values may not exceed 1024 characters or be empty.',
EMBED_FILE_LIMIT: 'You may not upload more than one file at once.',
EMBED_DESCRIPTION: 'MessageEmbed descriptions may not exceed 2048 characters.',
EMBED_FOOTER_TEXT: 'MessageEmbed footer text may not exceed 2048 characters.',
EMBED_TITLE: 'MessageEmbed titles may not exceed 256 characters.',
FILE_NOT_FOUND: file => `File could not be found: ${file}`,
USER_STATUS: 'User status must be a string',
SHARD_MESSAGE_FAILED: 'Failed to send message to master process.',
VOICE_INVALID_HEARTBEAT: 'Tried to set voice heartbeat but no valid interval was specified.',
VOICE_USER_MISSING: 'Couldn\'t resolve the user to create stream.',
VOICE_STREAM_EXISTS: 'There is already an existing stream for that user.',
VOICE_JOIN_CHANNEL: (full = false) =>
`You do not have permission to join this voice channel${full ? '; it is full.' : '.'}`,
OPUS_ENGINE_MISSING: 'Couldn\'t find an Opus engine.',
UDP_SEND_FAIL: 'Tried to send a UDP packet, but there is no socket available.',
UDP_ADDRESS_MALFORMED: 'Malformed UDP address or port.',
UDP_CONNECTION_EXISTS: 'There is already an existing UDP connection.',
REQ_BODY_TYPE: 'The response body isn\'t a Buffer.',
REQ_RESOURCE_TYPE: 'The resource must be a string or Buffer.',
IMAGE_FORMAT: format => `Invalid image format: ${format}`,
IMAGE_SIZE: size => `Invalid image size: ${size}`,
MESSAGE_MISSING: 'Message not found',
MESSAGE_BULK_DELETE_TYPE: 'The messages must be an Array, Collection, or number.',
MESSAGE_NONCE_TYPE: 'Message nonce must fit in an unsigned 64-bit integer.',
TYPING_COUNT: 'Count must be at least 1',
SPLIT_MAX_LEN: 'Message exceeds the max length and contains no split characters.',
BAN_RESOLVE_ID: 'Couldn\'t resolve the user ID to unban.',
PRUNE_DAYS_TYPE: 'Days must be a number',
SEARCH_CHANNEL_TYPE: 'Target must be a TextChannel, DMChannel, GroupDMChannel, or Guild.',
MESSAGE_SPLIT_MISSING: 'Message exceeds the max length and contains no split characters.',
GUILD_CHANNEL_RESOLVE: 'Could not resolve channel to a guild channel.',
EMOJI_TYPE: 'Emoji must be a string or Emoji/ReactionEmoji',
};
for (const [name, message] of Object.entries(Messages)) register(name, message);

2
src/errors/index.js Normal file
View File

@@ -0,0 +1,2 @@
module.exports = require('./DJSError');
module.exports.Messages = require('./Messages');

View File

@@ -1,6 +1,7 @@
const childProcess = require('child_process');
const path = require('path');
const Util = require('../util/Util');
const { Error } = require('../errors');
/**
* Represents a Shard spawned by the ShardingManager.
@@ -60,7 +61,7 @@ class Shard {
const sent = this.process.send(message, err => {
if (err) reject(err); else resolve(this);
});
if (!sent) throw new Error('Failed to send message to shard\'s process.');
if (!sent) throw new Error('SHARDING_CHILD_CONNECTION');
});
}

View File

@@ -1,4 +1,5 @@
const Util = require('../util/Util');
const { Error } = require('../errors');
/**
* Helper class for sharded clients spawned as a child process, such as from a ShardingManager.
@@ -40,7 +41,7 @@ class ShardClientUtil {
const sent = process.send(message, err => {
if (err) reject(err); else resolve();
});
if (!sent) throw new Error('Failed to send message to master process.');
if (!sent) throw new Error('SHARDING_PARENT_CONNECTION');
});
}

View File

@@ -4,6 +4,7 @@ const EventEmitter = require('events').EventEmitter;
const Shard = require('./Shard');
const Collection = require('../util/Collection');
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
@@ -34,10 +35,10 @@ class ShardingManager extends EventEmitter {
* @type {string}
*/
this.file = file;
if (!file) throw new Error('File must be specified.');
if (!file) throw new Error('CLIENT_INVALID_OPTION', 'File', 'specified.');
if (!path.isAbsolute(file)) this.file = path.resolve(process.cwd(), file);
const stats = fs.statSync(this.file);
if (!stats.isFile()) throw new Error('File path does not point to a file.');
if (!stats.isFile()) throw new Error('CLIENT_INVALID_OPTION', 'File', 'a file');
/**
* Amount of shards that this manager is going to spawn
@@ -46,11 +47,11 @@ class ShardingManager extends EventEmitter {
this.totalShards = options.totalShards;
if (this.totalShards !== 'auto') {
if (typeof this.totalShards !== 'number' || isNaN(this.totalShards)) {
throw new TypeError('Amount of shards must be a number.');
throw new TypeError('CLIENT_INVALID_OPTION', 'Amount of shards', 'a number.');
}
if (this.totalShards < 1) throw new RangeError('Amount of shards must be at least 1.');
if (this.totalShards < 1) throw new RangeError('CLIENT_INVALID_OPTION', 'Amount of shards', 'at least 1.');
if (this.totalShards !== Math.floor(this.totalShards)) {
throw new RangeError('Amount of shards must be an integer.');
throw new RangeError('CLIENT_INVALID_OPTION', 'Amount of shards', 'an integer.');
}
}
@@ -109,9 +110,13 @@ class ShardingManager extends EventEmitter {
return this._spawn(count, delay);
});
} else {
if (typeof amount !== 'number' || isNaN(amount)) throw new TypeError('Amount of shards must be a number.');
if (amount < 1) throw new RangeError('Amount of shards must be at least 1.');
if (amount !== Math.floor(amount)) throw new TypeError('Amount of shards must be an integer.');
if (typeof amount !== 'number' || isNaN(amount)) {
throw new TypeError('CLIENT_INVALID_OPTION', 'Amount of shards', 'a number.');
}
if (amount < 1) throw new RangeError('CLIENT_INVALID_OPTION', 'Amount of shards', 'at least 1.');
if (amount !== Math.floor(amount)) {
throw new TypeError('CLIENT_INVALID_OPTION', 'Amount of shards', 'an integer.');
}
return this._spawn(amount, delay);
}
}
@@ -125,7 +130,7 @@ class ShardingManager extends EventEmitter {
*/
_spawn(amount, delay) {
return new Promise(resolve => {
if (this.shards.size >= amount) throw new Error(`Already spawned ${this.shards.size} shards.`);
if (this.shards.size >= amount) throw new Error('SHARDING_ALREADY_SPAWNED', this.shards.size);
this.totalShards = amount;
this.createShard();
@@ -181,8 +186,8 @@ class ShardingManager extends EventEmitter {
* }).catch(console.error);
*/
fetchClientValues(prop) {
if (this.shards.size === 0) return Promise.reject(new Error('No shards have been spawned.'));
if (this.shards.size !== this.totalShards) return Promise.reject(new Error('Still spawning shards.'));
if (this.shards.size === 0) return Promise.reject(new Error('SHARDING_NO_SHARDS'));
if (this.shards.size !== this.totalShards) return Promise.reject(new Error('SHARDING_IN_PROCESS'));
const promises = [];
for (const shard of this.shards.values()) promises.push(shard.fetchClientValue(prop));
return Promise.all(promises);

View File

@@ -6,6 +6,7 @@ const Util = require('../util/Util');
const Guild = require('./Guild');
const Message = require('./Message');
const GroupDMChannel = require('./GroupDMChannel');
const { TypeError } = require('../errors');
/**
* Represents the logged in client's Discord user.
@@ -193,7 +194,7 @@ class ClientUser extends User {
}
if (data.status) {
if (typeof data.status !== 'string') throw new TypeError('Status must be a string');
if (typeof data.status !== 'string') throw new TypeError('STATUS_TYPE');
if (this.bot) {
status = data.status;
} else {

View File

@@ -14,6 +14,7 @@ const Util = require('../util/Util');
const Snowflake = require('../util/Snowflake');
const Permissions = require('../util/Permissions');
const Shared = require('./shared');
const { Error, TypeError } = require('../errors');
/**
* Represents a guild (or a server) on Discord.
@@ -804,8 +805,7 @@ class Guild {
*/
unban(user, reason) {
const id = this.client.resolver.resolveUserID(user);
if (!id) throw new Error('Couldn\'t resolve the user ID to unban.');
if (!id) throw new Error('BAN_RESOLVE_ID');
return this.client.api.guilds(this.id).bans(id).delete({ reason })
.then(() => user);
}
@@ -828,7 +828,7 @@ class Guild {
* .catch(console.error);
*/
pruneMembers({ days = 7, dry = false, reason } = {}) {
if (typeof days !== 'number') throw new TypeError('Days must be a number.');
if (typeof days !== 'number') throw new TypeError('PRUNE_DAYS_TYPE');
return this.client.api.guilds(this.id).prune[dry ? 'get' : 'post']({ query: { days }, reason })
.then(data => data.pruned);
}

View File

@@ -3,6 +3,7 @@ const Role = require('./Role');
const Permissions = require('../util/Permissions');
const Collection = require('../util/Collection');
const Presence = require('./Presence').Presence;
const { Error } = require('../errors');
/**
* Represents a member of a guild on Discord.
@@ -287,7 +288,7 @@ class GuildMember {
*/
permissionsIn(channel) {
channel = this.client.resolver.resolveChannel(channel);
if (!channel || !channel.guild) throw new Error('Could not resolve channel to a guild channel.');
if (!channel || !channel.guild) throw new Error('GUILD_CHANNEL_RESOLVE');
return channel.permissionsFor(this);
}

View File

@@ -7,6 +7,7 @@ const Util = require('../util/Util');
const Collection = require('../util/Collection');
const Constants = require('../util/Constants');
const Permissions = require('../util/Permissions');
const { TypeError } = require('../errors');
let GuildMember;
/**
@@ -428,7 +429,7 @@ class Message {
*/
react(emoji) {
emoji = this.client.resolver.resolveEmojiIdentifier(emoji);
if (!emoji) throw new TypeError('Emoji must be a string or Emoji/ReactionEmoji');
if (!emoji) throw new TypeError('EMOJI_TYPE');
return this.client.api.channels(this.channel.id).messages(this.id).reactions(emoji)['@me']
.put()

View File

@@ -1,4 +1,5 @@
const Util = require('../util/Util');
const { RangeError } = require('../errors');
/**
* Represents an embed in a message (image/video preview, rich embed, etc.)
@@ -165,13 +166,11 @@ class MessageEmbed {
* @returns {MessageEmbed} This embed
*/
addField(name, value, inline = false) {
if (this.fields.length >= 25) throw new RangeError('MessageEmbeds may not exceed 25 fields.');
if (this.fields.length >= 25) throw new RangeError('EMBED_FIELD_COUNT');
name = Util.resolveString(name);
if (name.length > 256) throw new RangeError('MessageEmbed field names may not exceed 256 characters.');
if (!/\S/.test(name)) throw new RangeError('MessageEmbed field names may not be empty.');
if (!String(name) || name.length > 256) throw new RangeError('EMBED_FIELD_NAME');
value = Util.resolveString(value);
if (value.length > 1024) throw new RangeError('MessageEmbed field values may not exceed 1024 characters.');
if (!/\S/.test(value)) throw new RangeError('MessageEmbed field values may not be empty.');
if (!String(name) || value.length > 1024) throw new RangeError('EMBED_FIELD_VALUE');
this.fields.push({ name, value, inline });
return this;
}
@@ -183,7 +182,7 @@ class MessageEmbed {
* @returns {MessageEmbed} This embed
*/
attachFile(file) {
if (this.file) throw new RangeError('You may not upload more than one file at once.');
if (this.file) throw new RangeError('EMBED_FILE_LIMIT');
this.file = file;
return this;
}
@@ -217,7 +216,7 @@ class MessageEmbed {
*/
setDescription(description) {
description = Util.resolveString(description);
if (description.length > 2048) throw new RangeError('MessageEmbed descriptions may not exceed 2048 characters.');
if (description.length > 2048) throw new RangeError('EMBED_DESCRIPTION');
this.description = description;
return this;
}
@@ -230,7 +229,7 @@ class MessageEmbed {
*/
setFooter(text, iconURL) {
text = Util.resolveString(text);
if (text.length > 2048) throw new RangeError('MessageEmbed footer text may not exceed 2048 characters.');
if (text.length > 2048) throw new RangeError('EMBED_FOOTER_TEXT');
this.footer = { text, iconURL };
return this;
}
@@ -272,7 +271,7 @@ class MessageEmbed {
*/
setTitle(title) {
title = Util.resolveString(title);
if (title.length > 256) throw new RangeError('MessageEmbed titles may not exceed 256 characters.');
if (title.length > 256) throw new RangeError('EMBED_TITLE');
this.title = title;
return this;
}

View File

@@ -3,6 +3,7 @@ const MessageCollector = require('../MessageCollector');
const Shared = require('../shared');
const Collection = require('../../util/Collection');
const Snowflake = require('../../util/Snowflake');
const { Error, RangeError, TypeError } = require('../../errors');
/**
* Interface for classes that have text-channel-like features.
@@ -136,7 +137,7 @@ class TextBasedChannel {
return this.fetchMessages({ limit: 1, around: messageID })
.then(messages => {
const msg = messages.get(messageID);
if (!msg) throw new Error('Message not found.');
if (!msg) throw new Error('MESSAGE_MISSING');
return msg;
});
}
@@ -227,7 +228,7 @@ class TextBasedChannel {
* channel.startTyping();
*/
startTyping(count) {
if (typeof count !== 'undefined' && count < 1) throw new RangeError('Count must be at least 1.');
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, {
@@ -361,7 +362,7 @@ class TextBasedChannel {
}).messages
);
}
throw new TypeError('The messages must be an Array, Collection, or number.');
throw new TypeError('MESSAGE_BULK_DELETE_TYPE');
}
/**

View File

@@ -1,4 +1,5 @@
const long = require('long');
const { TypeError } = require('../../errors');
/**
* @typedef {Object} MessageSearchOptions
@@ -84,9 +85,7 @@ module.exports = function search(target, options) {
const Guild = require('../Guild');
const Message = require('../Message');
if (!(target instanceof Channel || target instanceof Guild)) {
throw new TypeError('Target must be a TextChannel, DMChannel, GroupDMChannel, or Guild.');
}
if (!(target instanceof Channel || target instanceof Guild)) throw new TypeError('SEARCH_CHANNEL_TYPE');
let endpoint = target.client.api[target instanceof Channel ? 'channels' : 'guilds'](target.id).messages().search;
return endpoint.get({ query: options }).then(body => {

View File

@@ -1,4 +1,5 @@
const Util = require('../../util/Util');
const { RangeError } = require('../../errors');
module.exports = function sendMessage(channel, options) {
const User = require('../User');
@@ -8,7 +9,7 @@ module.exports = function sendMessage(channel, options) {
if (typeof nonce !== 'undefined') {
nonce = parseInt(nonce);
if (isNaN(nonce) || nonce < 0) throw new RangeError('Message nonce must fit in an unsigned 64-bit integer.');
if (isNaN(nonce) || nonce < 0) throw new RangeError('MESSAGE_NONCE_TYPE');
}
if (content) {

View File

@@ -1,4 +1,5 @@
exports.Package = require('../../package.json');
const { Error, RangeError } = require('../errors');
/**
* Options for a client.
@@ -78,20 +79,6 @@ exports.WSCodes = {
4011: 'Shard would be on too many guilds if connected',
};
exports.Errors = {
NO_TOKEN: 'Request to use token, but token was unavailable to the client.',
NO_BOT_ACCOUNT: 'Only bot accounts are able to make use of this feature.',
NO_USER_ACCOUNT: 'Only user accounts are able to make use of this feature.',
BAD_WS_MESSAGE: 'A bad message was received from the websocket; either bad compression, or not JSON.',
TOOK_TOO_LONG: 'Something took too long to do.',
NOT_A_PERMISSION: 'Invalid permission string or number.',
INVALID_RATE_LIMIT_METHOD: 'Unknown rate limiting method.',
BAD_LOGIN: 'Incorrect login details were provided.',
INVALID_SHARD: 'Invalid shard settings were provided.',
SHARDING_REQUIRED: 'This session would have handled too many guilds - Sharding is required.',
INVALID_TOKEN: 'An invalid token was provided.',
};
const AllowedImageFormats = [
'webp',
'png',
@@ -108,8 +95,8 @@ const AllowedImageSizes = [
];
function checkImage({ size, format }) {
if (format && !AllowedImageFormats.includes(format)) throw new Error(`Invalid image format: ${format}`);
if (size && !AllowedImageSizes.includes(size)) throw new RangeError(`Invalid size: ${size}`);
if (format && !AllowedImageFormats.includes(format)) throw new Error('IMAGE_FORMAT', format);
if (size && !AllowedImageSizes.includes(size)) throw new RangeError('IMAGE_SIZE', size);
}
exports.Endpoints = {

View File

@@ -1,4 +1,4 @@
const Constants = require('../util/Constants');
const { RangeError } = require('../errors');
/**
* Data structure that makes it easy to interact with a permission bitfield. All {@link GuildMember}s have a set of
@@ -95,7 +95,7 @@ class Permissions {
static resolve(permission) {
if (permission instanceof Array) return permission.map(p => this.resolve(p)).reduce((prev, p) => prev | p, 0);
if (typeof permission === 'string') permission = this.FLAGS[permission];
if (typeof permission !== 'number' || permission < 1) throw new RangeError(Constants.Errors.NOT_A_PERMISSION);
if (typeof permission !== 'number' || permission < 1) throw new RangeError('PERMISSION_INVALID');
return permission;
}
}

View File

@@ -1,6 +1,7 @@
const snekfetch = require('snekfetch');
const Constants = require('./Constants');
const ConstantsHttp = Constants.DefaultOptions.http;
const { RangeError, TypeError } = require('../errors');
/**
* Contains various general-purpose utility methods. These functions are also available on the base `Discord` object.
@@ -19,7 +20,9 @@ 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 Error('Message exceeds the max length and contains no split characters.');
if (splitText.length === 1) {
throw new RangeError('SPLIT_MAX_LEN');
}
const messages = [''];
let msg = 0;
for (let i = 0; i < splitText.length; i++) {
@@ -54,7 +57,7 @@ class Util {
*/
static fetchRecommendedShards(token, guildsPerShard = 1000) {
return new Promise((resolve, reject) => {
if (!token) throw new Error('A token must be provided.');
if (!token) throw new Error('TOKEN_MISSING');
snekfetch.get(`${ConstantsHttp.host}/api/v${ConstantsHttp.version}${Constants.Endpoints.botGateway}`)
.set('Authorization', `Bot ${token.replace(/^Bot\s*/i, '')}`)
.end((err, res) => {
@@ -279,9 +282,9 @@ class Util {
}
if (color < 0 || color > 0xFFFFFF) {
throw new RangeError('Color must be within the range 0 - 16777215 (0xFFFFFF).');
throw new RangeError('COLOR_RANGE');
} else if (color && isNaN(color)) {
throw new TypeError('Unable to convert color to a number.');
throw new TypeError('COLOR_CONVERT');
}
return color;