diff --git a/src/client/Client.js b/src/client/Client.js index 553b9c584..203863451 100644 --- a/src/client/Client.js +++ b/src/client/Client.js @@ -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.'); } } diff --git a/src/client/ClientDataResolver.js b/src/client/ClientDataResolver.js index 1c268b73d..4de4c7dee 100644 --- a/src/client/ClientDataResolver.js +++ b/src/client/ClientDataResolver.js @@ -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')); } /** diff --git a/src/client/ClientManager.js b/src/client/ClientManager.js index 4239e2e4a..4b7f0102b 100644 --- a/src/client/ClientManager.js +++ b/src/client/ClientManager.js @@ -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); diff --git a/src/client/rest/APIRequest.js b/src/client/rest/APIRequest.js index 5f21f25dc..a96249a7a 100644 --- a/src/client/rest/APIRequest.js +++ b/src/client/rest/APIRequest.js @@ -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() { diff --git a/src/client/rest/RESTManager.js b/src/client/rest/RESTManager.js index 5a7e9f0ed..ca060b3b9 100644 --- a/src/client/rest/RESTManager.js +++ b/src/client/rest/RESTManager.js @@ -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'); } } diff --git a/src/client/voice/ClientVoiceManager.js b/src/client/voice/ClientVoiceManager.js index a6a95eee9..400a752cc 100644 --- a/src/client/voice/ClientVoiceManager.js +++ b/src/client/voice/ClientVoiceManager.js @@ -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); diff --git a/src/client/voice/VoiceConnection.js b/src/client/voice/VoiceConnection.js index ff218623f..0c4af468c 100644 --- a/src/client/voice/VoiceConnection.js +++ b/src/client/voice/VoiceConnection.js @@ -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(); diff --git a/src/client/voice/VoiceUDPClient.js b/src/client/voice/VoiceUDPClient.js index 0e148b754..6e12d5e7b 100644 --- a/src/client/voice/VoiceUDPClient.js +++ b/src/client/voice/VoiceUDPClient.js @@ -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); }); diff --git a/src/client/voice/VoiceWebSocket.js b/src/client/voice/VoiceWebSocket.js index 9179232fe..025c23ecc 100644 --- a/src/client/voice/VoiceWebSocket.js +++ b/src/client/voice/VoiceWebSocket.js @@ -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); }); diff --git a/src/client/voice/opus/OpusEngineList.js b/src/client/voice/opus/OpusEngineList.js index 152481745..9ff9ad7bf 100644 --- a/src/client/voice/opus/OpusEngineList.js +++ b/src/client/voice/opus/OpusEngineList.js @@ -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'); }; diff --git a/src/client/voice/receiver/VoiceReceiver.js b/src/client/voice/receiver/VoiceReceiver.js index 7b1b6c8e0..e57a39d9c 100644 --- a/src/client/voice/receiver/VoiceReceiver.js +++ b/src/client/voice/receiver/VoiceReceiver.js @@ -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; diff --git a/src/errors/DJSError.js b/src/errors/DJSError.js new file mode 100644 index 000000000..b922b711d --- /dev/null +++ b/src/errors/DJSError.js @@ -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), +}; diff --git a/src/errors/Messages.js b/src/errors/Messages.js new file mode 100644 index 000000000..ffaaec720 --- /dev/null +++ b/src/errors/Messages.js @@ -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); diff --git a/src/errors/index.js b/src/errors/index.js new file mode 100644 index 000000000..39b7582df --- /dev/null +++ b/src/errors/index.js @@ -0,0 +1,2 @@ +module.exports = require('./DJSError'); +module.exports.Messages = require('./Messages'); diff --git a/src/sharding/Shard.js b/src/sharding/Shard.js index a73447b85..f0cc51098 100644 --- a/src/sharding/Shard.js +++ b/src/sharding/Shard.js @@ -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'); }); } diff --git a/src/sharding/ShardClientUtil.js b/src/sharding/ShardClientUtil.js index b41deb5d1..549353df2 100644 --- a/src/sharding/ShardClientUtil.js +++ b/src/sharding/ShardClientUtil.js @@ -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'); }); } diff --git a/src/sharding/ShardingManager.js b/src/sharding/ShardingManager.js index e105f3e3f..bfae42f22 100644 --- a/src/sharding/ShardingManager.js +++ b/src/sharding/ShardingManager.js @@ -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); diff --git a/src/structures/ClientUser.js b/src/structures/ClientUser.js index cd04ad8f3..e0a2f3ba2 100644 --- a/src/structures/ClientUser.js +++ b/src/structures/ClientUser.js @@ -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 { diff --git a/src/structures/Guild.js b/src/structures/Guild.js index 3b6f0d9ef..2dd62514a 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -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); } diff --git a/src/structures/GuildMember.js b/src/structures/GuildMember.js index 201a2b792..92969e493 100644 --- a/src/structures/GuildMember.js +++ b/src/structures/GuildMember.js @@ -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); } diff --git a/src/structures/Message.js b/src/structures/Message.js index 84bafec68..d4b7f001c 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -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() diff --git a/src/structures/MessageEmbed.js b/src/structures/MessageEmbed.js index 35d222b1c..21ef03c68 100644 --- a/src/structures/MessageEmbed.js +++ b/src/structures/MessageEmbed.js @@ -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; } diff --git a/src/structures/interfaces/TextBasedChannel.js b/src/structures/interfaces/TextBasedChannel.js index 5d4957ded..08b745dda 100644 --- a/src/structures/interfaces/TextBasedChannel.js +++ b/src/structures/interfaces/TextBasedChannel.js @@ -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'); } /** diff --git a/src/structures/shared/Search.js b/src/structures/shared/Search.js index 63606f5c9..76ed72eef 100644 --- a/src/structures/shared/Search.js +++ b/src/structures/shared/Search.js @@ -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 => { diff --git a/src/structures/shared/SendMessage.js b/src/structures/shared/SendMessage.js index bd93e4dbe..d9e328e9d 100644 --- a/src/structures/shared/SendMessage.js +++ b/src/structures/shared/SendMessage.js @@ -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) { diff --git a/src/util/Constants.js b/src/util/Constants.js index 98b359b4c..6da968eea 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -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 = { diff --git a/src/util/Permissions.js b/src/util/Permissions.js index 04b1864fd..b6dd20478 100644 --- a/src/util/Permissions.js +++ b/src/util/Permissions.js @@ -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; } } diff --git a/src/util/Util.js b/src/util/Util.js index e48d736c8..ef61cec5f 100644 --- a/src/util/Util.js +++ b/src/util/Util.js @@ -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;