From 08baa3f8ad0f1fece36b421e304770615f59b997 Mon Sep 17 00:00:00 2001 From: NotSugden <28943913+NotSugden@users.noreply.github.com> Date: Wed, 16 Sep 2020 22:57:23 +0100 Subject: [PATCH 01/12] refactor: use custom error class --- src/client.js | 13 +++++++------ src/errors/index.js | 32 ++++++++++++++++++++++++++++++++ src/transports/ipc.js | 5 +++-- 3 files changed, 42 insertions(+), 8 deletions(-) create mode 100644 src/errors/index.js diff --git a/src/client.js b/src/client.js index 55063e3..7abcb4d 100644 --- a/src/client.js +++ b/src/client.js @@ -6,6 +6,7 @@ const fetch = require('node-fetch'); const transports = require('./transports'); const { RPCCommands, RPCEvents, RelationshipTypes } = require('./constants'); const { pid: getPid, uuid } = require('./util'); +const { Error, TypeError, RangeError } = require('./errors'); function subKey(event, args) { return `${event}${JSON.stringify(args)}`; @@ -48,7 +49,7 @@ class RPCClient extends EventEmitter { const Transport = transports[options.transport]; if (!Transport) { - throw new TypeError('RPC_INVALID_TRANSPORT', options.transport); + throw new TypeError('INVALID_TYPE', 'options.transport', '\'ipc\' or \'websocket\'', options.transport); } this.fetch = (method, path, { data, query } = {}) => @@ -104,7 +105,7 @@ class RPCClient extends EventEmitter { } this._connectPromise = new Promise((resolve, reject) => { this.clientId = clientId; - const timeout = setTimeout(() => reject(new Error('RPC_CONNECTION_TIMEOUT')), 10e3); + const timeout = setTimeout(() => reject(new Error('CONNECTION_TIMEOUT')), 10e3); timeout.unref(); this.once('connected', () => { clearTimeout(timeout); @@ -112,10 +113,10 @@ class RPCClient extends EventEmitter { }); this.transport.once('close', () => { this._expecting.forEach((e) => { - e.reject(new Error('connection closed')); + e.reject(new Error('CONNECTION_CLOSED')); }); this.emit('disconnected'); - reject(new Error('connection closed')); + reject(new Error('CONNECTION_CLOSED')); }); this.transport.connect().catch(reject); }); @@ -495,10 +496,10 @@ class RPCClient extends EventEmitter { timestamps.end = Math.round(timestamps.end.getTime()); } if (timestamps.start > 2147483647000) { - throw new RangeError('timestamps.start must fit into a unix timestamp'); + throw new RangeError('TIMESTAMP_TOO_LARGE', 'args.startTimestamp'); } if (timestamps.end > 2147483647000) { - throw new RangeError('timestamps.end must fit into a unix timestamp'); + throw new RangeError('TIMESTAMP_TOO_LARGE', 'args.endTimestamp'); } } if ( diff --git a/src/errors/index.js b/src/errors/index.js new file mode 100644 index 0000000..f94a0bf --- /dev/null +++ b/src/errors/index.js @@ -0,0 +1,32 @@ +'use strict'; + +const errorMessages = { + INVALID_TYPE: (prop, expected, found) => + `Recieved '${prop}' is expected to be ${expected}${found ? ` Recieved ${found}` : ''}.`, + CONNECTION_CLOSED: 'Connection closed.', + CONNECTION_TIMEOUT: 'Connection timed out.', + COULD_NOT_CONNECT: 'Couldn\'t connect.', + COULD_NOT_FIND_ENDPOINT: 'Couldn\'t find the RPC API Endpoint.', + TIMESTAMP_TOO_LARGE: (name) => `'${name}' Must fit into a unix timestamp.`, +}; + +const makeError = (BaseClass) => { + class RPCError extends BaseClass { + constructor(code, ...args) { + const message = errorMessages[code] || code; + super(typeof message === 'function' ? message(...args) : message); + + this.code = code; + } + + get name() { + return errorMessages[this.code] ? `${super.name} [${this.code}]` : super.name; + } + } + + return RPCError; +}; + +exports.Error = makeError(Error); +exports.TypeError = makeError(TypeError); +exports.RangeError = makeError(RangeError); diff --git a/src/transports/ipc.js b/src/transports/ipc.js index 8d9dce1..da811af 100644 --- a/src/transports/ipc.js +++ b/src/transports/ipc.js @@ -4,6 +4,7 @@ const net = require('net'); const EventEmitter = require('events'); const fetch = require('node-fetch'); const { uuid } = require('../util'); +const { Error } = require('../errors'); const OPCodes = { HANDSHAKE: 0, @@ -29,7 +30,7 @@ function getIPC(id = 0) { if (id < 10) { resolve(getIPC(id + 1)); } else { - reject(new Error('Could not connect')); + reject(new Error('COULD_NOT_CONNECT')); } }; const sock = net.createConnection(path, () => { @@ -42,7 +43,7 @@ function getIPC(id = 0) { async function findEndpoint(tries = 0) { if (tries > 30) { - throw new Error('Could not find endpoint'); + throw new Error('COULD_NOT_FIND_ENDPOINT'); } const endpoint = `http://127.0.0.1:${6463 + (tries % 10)}`; try { From 5f575da0f670c185b90ff3d0bd8a3032b8b5285f Mon Sep 17 00:00:00 2001 From: NotSugden <28943913+NotSugden@users.noreply.github.com> Date: Thu, 17 Sep 2020 01:04:05 +0100 Subject: [PATCH 02/12] refactor: clean up code --- .gitignore | 1 + src/client.js | 496 +++++++++++++++++++++++++----------------- src/constants.js | 1 + src/errors/index.js | 1 + src/transports/ipc.js | 12 +- 5 files changed, 303 insertions(+), 208 deletions(-) diff --git a/.gitignore b/.gitignore index 97ee4bc..7dfc6f3 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ package-lock.json docs.json browser.js test/auth.js +.vscode diff --git a/src/client.js b/src/client.js index 7abcb4d..019e2f0 100644 --- a/src/client.js +++ b/src/client.js @@ -4,13 +4,38 @@ const EventEmitter = require('events'); const { setTimeout, clearTimeout } = require('timers'); const fetch = require('node-fetch'); const transports = require('./transports'); -const { RPCCommands, RPCEvents, RelationshipTypes } = require('./constants'); +const { BASE_API_URL, RPCCommands, RPCEvents, RelationshipTypes } = require('./constants'); const { pid: getPid, uuid } = require('./util'); const { Error, TypeError, RangeError } = require('./errors'); -function subKey(event, args) { - return `${event}${JSON.stringify(args)}`; -} +const subKey = (event, args) => `${event}${JSON.stringify(args)}`; + +const formatVoiceSettings = (data) => ({ + automaticGainControl: data.automatic_gain_control, + echoCancellation: data.echo_cancellation, + noiseSuppression: data.noise_suppression, + qos: data.qos, + silenceWarning: data.silence_warning, + deaf: data.deaf, + mute: data.mute, + input: { + availableDevices: data.input.available_devices, + device: data.input.device_id, + volume: data.input.volume, + }, + output: { + availableDevices: data.output.available_devices, + device: data.output.device_id, + volume: data.output.volume, + }, + mode: { + type: data.mode.type, + autoThreshold: data.mode.auto_threshold, + threshold: data.mode.threshold, + shortcut: data.mode.shortcut, + delay: data.mode.delay, + }, +}); /** * @typedef {RPCClientOptions} @@ -52,32 +77,14 @@ class RPCClient extends EventEmitter { throw new TypeError('INVALID_TYPE', 'options.transport', '\'ipc\' or \'websocket\'', options.transport); } - this.fetch = (method, path, { data, query } = {}) => - fetch(`${this.fetch.endpoint}${path}${query ? new URLSearchParams(query) : ''}`, { - method, - body: data, - headers: { - Authorization: `Bearer ${this.accessToken}`, - }, - }).then(async (r) => { - const body = await r.json(); - if (!r.ok) { - const e = new Error(r.status); - e.body = body; - throw e; - } - return body; - }); - - this.fetch.endpoint = 'https://discord.com/api'; - /** * Raw transport userd * @type {RPCTransport} * @private */ this.transport = new Transport(this); - this.transport.on('message', this._onRpcMessage.bind(this)); + this._onRpcClose = this._onRpcClose.bind(this); + this._onRpcMessage = this._onRpcMessage.bind(this); /** * Map of nonces being expected from the transport @@ -93,7 +100,42 @@ class RPCClient extends EventEmitter { */ this._subscriptions = new Map(); - this._connectPromise = undefined; + this._connectPromise = null; + + /** + * Whether or not the client is connected + * @type {boolean} + */ + this.connected = false; + } + + /** + * @typedef {Object} RequestOptions + * @prop {Record} [data] Request data + * @prop {string|[string, string][]|URLSearchParams} [query] Request query + */ + + /** + * Makes an API Request. + * @param {string} method Request method + * @param {string} path Request path + * @param {RequestOptions} [options] Request Options + */ + async fetch(method, path, { data, query } = {}) { + const response = await fetch(`${BASE_API_URL}/${path}${query ? `?${new URLSearchParams(query)}` : ''}`, { + method, + body: data, + headers: { + Authorization: `Bearer ${this.accessToken}`, + }, + }); + const body = await response.json(); + if (!response.ok) { + const error = new Error(response.status); + error.body = body; + throw error; + } + return body; } /** @@ -104,21 +146,32 @@ class RPCClient extends EventEmitter { return this._connectPromise; } this._connectPromise = new Promise((resolve, reject) => { - this.clientId = clientId; - const timeout = setTimeout(() => reject(new Error('CONNECTION_TIMEOUT')), 10e3); - timeout.unref(); - this.once('connected', () => { + /* eslint-disable no-use-before-define */ + const removeListeners = () => { + this.transport.off('close', onClose); + this.off('connected', onConnect); + this.off('destroyed', onClose); clearTimeout(timeout); + }; + /* eslint-enable no-use-before-define */ + const onConnect = (() => { + this.connected = true; + removeListeners(); resolve(this); }); - this.transport.once('close', () => { - this._expecting.forEach((e) => { - e.reject(new Error('CONNECTION_CLOSED')); - }); - this.emit('disconnected'); - reject(new Error('CONNECTION_CLOSED')); - }); - this.transport.connect().catch(reject); + const onClose = (error) => { + removeListeners(); + this.destroy(); + reject(error || new Error('CONNECTION_CLOSED')); + }; + this.once('destroyed', onClose); + this.once('connected', onConnect); + this.transport.once('close', onClose); + this._setupListeners(); + + this.clientId = clientId; + this.transport.connect().catch(onClose); + const timeout = setTimeout(onClose, 10e3).unref(); }); return this._connectPromise; } @@ -161,42 +214,86 @@ class RPCClient extends EventEmitter { * @returns {Promise} * @private */ - request(cmd, args, evt) { + request(command, args, event) { + if (!this.connected) { + return Promise.reject(new Error('NOT_CONNECTED')); + } return new Promise((resolve, reject) => { const nonce = uuid(); - this.transport.send({ cmd, args, evt, nonce }); + this.transport.send({ cmd: command, args, evt: event, nonce }); this._expecting.set(nonce, { resolve, reject }); }); } /** - * Message handler + * Add event listeners to transport + * @private + */ + _setupListeners() { + this.transport.on('message', this._onRpcMessage); + this.transport.once('close', this._onRpcClose); + } + + /** + * Remove all attached event listeners on transport + * @param {boolean} [emitClose=false] Whether to emit the `close` event rather than clearing it + * @private + */ + _removeListeners(emitClose = false) { + this.transport.off('message', this._onRpcMessage); + if (emitClose) { + this.transport.emit('close'); + } else { + this.transport.off('close', this._onRpcClose); + } + } + + /** + * RPC Close handler. + * @private + */ + _onRpcClose() { + for (const { reject } of this._expecting) { + reject(new Error('CONNECTION_CLOSED')); + } + this._expecting.clear(); + } + + /** + * Message handler. * @param {Object} message message + * @param {?Object} message.args The arguments for the event, if any + * @param {string} message.cmd The command sent + * @param {string} message.evt The event + * @param {object} message.data The data for this message + * @param {?string} message.nonce The nonce * @private */ - _onRpcMessage(message) { - if (message.cmd === RPCCommands.DISPATCH && message.evt === RPCEvents.READY) { - if (message.data.user) { - this.user = message.data.user; - } - this.emit('connected'); - } else if (this._expecting.has(message.nonce)) { - const { resolve, reject } = this._expecting.get(message.nonce); - if (message.evt === 'ERROR') { - const e = new Error(message.data.message); - e.code = message.data.code; - e.data = message.data; - reject(e); + _onRpcMessage({ args, cmd: command, data, evt: event, nonce }) { + if (nonce && this._expecting.has(nonce)) { + const { resolve, reject } = this._expecting.get(nonce); + if (event === 'ERROR') { + const error = new Error(data.message); + error.code = data.code; + error.data = data; + reject(error); } else { - resolve(message.data); + resolve(data); } - this._expecting.delete(message.nonce); - } else { - const subid = subKey(message.evt, message.args); - if (!this._subscriptions.has(subid)) { + this._expecting.delete(nonce); + } else if (command === RPCCommands.DISPATCH) { + if (event === RPCEvents.READY) { + if (data.user) { + this.user = data.user; + } + this.emit('connected'); + return; + } + const subId = subKey(event, args); + if (!this._subscriptions.has(subId)) { return; } - this._subscriptions.get(subid)(message.data); + this._subscriptions.get(subId)(args); } } @@ -208,7 +305,7 @@ class RPCClient extends EventEmitter { */ async authorize({ scopes, clientSecret, rpcToken, redirectUri } = {}) { if (clientSecret && rpcToken === true) { - const body = await this.fetch('POST', '/oauth2/token/rpc', { + const body = await this.fetch('POST', 'oauth2/token/rpc', { data: new URLSearchParams({ client_id: this.clientId, client_secret: clientSecret, @@ -223,7 +320,7 @@ class RPCClient extends EventEmitter { rpc_token: rpcToken, }); - const response = await this.fetch('POST', '/oauth2/token', { + const response = await this.fetch('POST', 'oauth2/token', { data: new URLSearchParams({ client_id: this.clientId, client_secret: clientSecret, @@ -242,15 +339,13 @@ class RPCClient extends EventEmitter { * @returns {Promise} * @private */ - authenticate(accessToken) { - return this.request('AUTHENTICATE', { access_token: accessToken }) - .then(({ application, user }) => { - this.accessToken = accessToken; - this.application = application; - this.user = user; - this.emit('ready'); - return this; - }); + async authenticate(accessToken) { + const { application, user } = await this.request('AUTHENTICATE', { access_token: accessToken }); + this.accessToken = accessToken; + this.application = application; + this.user = user; + this.emit('ready'); + return this; } @@ -269,8 +364,9 @@ class RPCClient extends EventEmitter { * @param {number} [timeout] Timeout request * @returns {Promise>} */ - getGuilds(timeout) { - return this.request(RPCCommands.GET_GUILDS, { timeout }); + async getGuilds(timeout) { + const { guilds } = await this.request(RPCCommands.GET_GUILDS, { timeout }); + return guilds; } /** @@ -321,16 +417,16 @@ class RPCClient extends EventEmitter { */ setCertifiedDevices(devices) { return this.request(RPCCommands.SET_CERTIFIED_DEVICES, { - devices: devices.map((d) => ({ - type: d.type, - id: d.uuid, - vendor: d.vendor, - model: d.model, - related: d.related, - echo_cancellation: d.echoCancellation, - noise_suppression: d.noiseSuppression, - automatic_gain_control: d.automaticGainControl, - hardware_mute: d.hardwareMute, + devices: devices.map((data) => ({ + type: data.type, + id: data.uuid, + vendor: data.vendor, + model: data.model, + related: data.related, + echo_cancellation: data.echoCancellation, + noise_suppression: data.noiseSuppression, + automatic_gain_control: data.automaticGainControl, + hardware_mute: data.hardwareMute, })), }); } @@ -389,34 +485,9 @@ class RPCClient extends EventEmitter { * Get current voice settings * @returns {Promise} */ - getVoiceSettings() { - return this.request(RPCCommands.GET_VOICE_SETTINGS) - .then((s) => ({ - automaticGainControl: s.automatic_gain_control, - echoCancellation: s.echo_cancellation, - noiseSuppression: s.noise_suppression, - qos: s.qos, - silenceWarning: s.silence_warning, - deaf: s.deaf, - mute: s.mute, - input: { - availableDevices: s.input.available_devices, - device: s.input.device_id, - volume: s.input.volume, - }, - output: { - availableDevices: s.output.available_devices, - device: s.output.device_id, - volume: s.output.volume, - }, - mode: { - type: s.mode.type, - autoThreshold: s.mode.auto_threshold, - threshold: s.mode.threshold, - shortcut: s.mode.shortcut, - delay: s.mode.delay, - }, - })); + async getVoiceSettings() { + const data = await this.request(RPCCommands.GET_VOICE_SETTINGS); + return formatVoiceSettings(data); } /** @@ -425,8 +496,8 @@ class RPCClient extends EventEmitter { * @param {Object} args Settings * @returns {Promise} */ - setVoiceSettings(args) { - return this.request(RPCCommands.SET_VOICE_SETTINGS, { + async setVoiceSettings(args) { + const data = await this.request(RPCCommands.SET_VOICE_SETTINGS, { automatic_gain_control: args.automaticGainControl, echo_cancellation: args.echoCancellation, noise_suppression: args.noiseSuppression, @@ -450,6 +521,7 @@ class RPCClient extends EventEmitter { delay: args.mode.delay, } : undefined, }); + return formatVoiceSettings(data); } /** @@ -460,84 +532,101 @@ class RPCClient extends EventEmitter { * @param {Function} callback Callback handling keys * @returns {Promise} */ - captureShortcut(callback) { - const subid = subKey(RPCEvents.CAPTURE_SHORTCUT_CHANGE); + async captureShortcut(callback) { + const subId = subKey(RPCEvents.CAPTURE_SHORTCUT_CHANGE); const stop = () => { - this._subscriptions.delete(subid); + this._subscriptions.delete(subId); return this.request(RPCCommands.CAPTURE_SHORTCUT, { action: 'STOP' }); }; - this._subscriptions.set(subid, ({ shortcut }) => { + this._subscriptions.set(subId, ({ shortcut }) => { callback(shortcut, stop); }); - return this.request(RPCCommands.CAPTURE_SHORTCUT, { action: 'START' }) - .then(() => stop); + await this.request(RPCCommands.CAPTURE_SHORTCUT, { action: 'START' }); + return stop; } + /** + * @typedef {Date|number|string} DateResolvable + */ + + /** + * @typedef {Object} PresenceData + * @prop {DateResolvable} [endTimestamp] End of the activity + * @prop {DateResolvable} [startTimestamp] Start of this activity + * @prop {string} [largeImageKey] The asset name for the large image + * @prop {string} [largeImageText] The hover text for the large image + * @prop {string} [smallImageKey] The asset name for the small image + * @prop {string} [smallImageText] The hover text for the small image + * @prop {string} [partyId] The party ID + * @prop {number} [partyMax] The party max + * @prop {number} [partySize] The party size + * @prop {string} [joinSecret] The join secret + * @prop {string} [matchSecret] The match secret + * @prop {string} [spectateSecret] The spectate secret + * @prop {boolean} [instance] Whether this activity is an instanced game session + */ + /** * Sets the presence for the logged in user. - * @param {object} args The rich presence to pass. + * @param {PresenceData} data The rich presence to pass. * @param {number} [pid] The application's process ID. Defaults to the executing process' PID. * @returns {Promise} */ - setActivity(args = {}, pid = getPid()) { - let timestamps; - let assets; - let party; - let secrets; - if (args.startTimestamp || args.endTimestamp) { - timestamps = { - start: args.startTimestamp, - end: args.endTimestamp, - }; - if (timestamps.start instanceof Date) { - timestamps.start = Math.round(timestamps.start.getTime()); - } - if (timestamps.end instanceof Date) { - timestamps.end = Math.round(timestamps.end.getTime()); + setActivity(data = {}, pid = getPid()) { + const activity = { + instance: Boolean(data.instance), + }; + + const timestamps = activity.timestamps = {}; + if ('endTimestamp' in data) { + const timestamp = timestamps.end = new Date(data.endTimestamp).getTime(); + if (timestamp > 2147483647000) { + throw new RangeError('TIMESTAMP_TOO_LARGE', 'args.endTimestamp'); } - if (timestamps.start > 2147483647000) { + } + if ('startTimestamp' in data) { + const timestamp = timestamps.start = new Date(data.startTimestamp).getTime(); + if (timestamp > 2147483647000) { throw new RangeError('TIMESTAMP_TOO_LARGE', 'args.startTimestamp'); } - if (timestamps.end > 2147483647000) { - throw new RangeError('TIMESTAMP_TOO_LARGE', 'args.endTimestamp'); - } } - if ( - args.largeImageKey || args.largeImageText - || args.smallImageKey || args.smallImageText - ) { - assets = { - large_image: args.largeImageKey, - large_text: args.largeImageText, - small_image: args.smallImageKey, - small_text: args.smallImageText, - }; + + const assets = activity.assets = {}; + if ('largeImageKey' in data) { + assets.large_image = data.largeImageKey; } - if (args.partySize || args.partyId || args.partyMax) { - party = { id: args.partyId }; - if (args.partySize || args.partyMax) { - party.size = [args.partySize, args.partyMax]; - } + if ('largeImageText' in data) { + assets.large_text = data.largeImageText; } - if (args.matchSecret || args.joinSecret || args.spectateSecret) { - secrets = { - match: args.matchSecret, - join: args.joinSecret, - spectate: args.spectateSecret, - }; + if ('smallImageKey' in data) { + assets.small_image = data.smallImageKey; + } + if ('smallImageText' in data) { + assets.small_text = data.smallImageText; + } + + const party = activity.party = {}; + if ('partyId' in data) { + party.id = data.partyId; + } + if ('partyMax' in data && 'partySize' in data) { + party.size = [data.partySize, data.partyMax]; + } + + const secrets = activity.secrets = {}; + if ('joinSecret' in data) { + secrets.join = data.joinSecret; + } + if ('matchSecret' in data) { + secrets.match = data.matchSecret; + } + if ('spectateSecret' in data) { + secrets.spectate = data.spectateSecret; } return this.request(RPCCommands.SET_ACTIVITY, { pid, - activity: { - state: args.state, - details: args.details, - timestamps, - assets, - party, - secrets, - instance: !!args.instance, - }, + activity, }); } @@ -547,8 +636,8 @@ class RPCClient extends EventEmitter { * @param {number} [pid] The application's process ID. Defaults to the executing process' PID. * @returns {Promise} */ - clearActivity(pid = getPid()) { - return this.request(RPCCommands.SET_ACTIVITY, { + async clearActivity(pid = getPid()) { + await this.request(RPCCommands.SET_ACTIVITY, { pid, }); } @@ -558,8 +647,8 @@ class RPCClient extends EventEmitter { * @param {User} user The user to invite * @returns {Promise} */ - sendJoinInvite(user) { - return this.request(RPCCommands.SEND_ACTIVITY_JOIN_INVITE, { + async sendJoinInvite(user) { + await this.request(RPCCommands.SEND_ACTIVITY_JOIN_INVITE, { user_id: user.id || user, }); } @@ -569,8 +658,8 @@ class RPCClient extends EventEmitter { * @param {User} user The user whose game you want to request to join * @returns {Promise} */ - sendJoinRequest(user) { - return this.request(RPCCommands.SEND_ACTIVITY_JOIN_REQUEST, { + async sendJoinRequest(user) { + await this.request(RPCCommands.SEND_ACTIVITY_JOIN_REQUEST, { user_id: user.id || user, }); } @@ -580,8 +669,8 @@ class RPCClient extends EventEmitter { * @param {User} user The user whose request you wish to reject * @returns {Promise} */ - closeJoinRequest(user) { - return this.request(RPCCommands.CLOSE_ACTIVITY_JOIN_REQUEST, { + async closeJoinRequest(user) { + await this.request(RPCCommands.CLOSE_ACTIVITY_JOIN_REQUEST, { user_id: user.id || user, }); } @@ -594,8 +683,8 @@ class RPCClient extends EventEmitter { }); } - updateLobby(lobby, { type, owner, capacity, metadata } = {}) { - return this.request(RPCCommands.UPDATE_LOBBY, { + async updateLobby(lobby, { type, owner, capacity, metadata } = {}) { + await this.request(RPCCommands.UPDATE_LOBBY, { id: lobby.id || lobby, type, owner_id: (owner && owner.id) || owner, @@ -604,8 +693,8 @@ class RPCClient extends EventEmitter { }); } - deleteLobby(lobby) { - return this.request(RPCCommands.DELETE_LOBBY, { + async deleteLobby(lobby) { + await this.request(RPCCommands.DELETE_LOBBY, { id: lobby.id || lobby, }); } @@ -617,34 +706,34 @@ class RPCClient extends EventEmitter { }); } - sendToLobby(lobby, data) { - return this.request(RPCCommands.SEND_TO_LOBBY, { - id: lobby.id || lobby, + async sendToLobby(lobby, data) { + await this.request(RPCCommands.SEND_TO_LOBBY, { + lobby_id: lobby.id || lobby, data, }); } - disconnectFromLobby(lobby) { - return this.request(RPCCommands.DISCONNECT_FROM_LOBBY, { + async disconnectFromLobby(lobby) { + await this.request(RPCCommands.DISCONNECT_FROM_LOBBY, { id: lobby.id || lobby, }); } - updateLobbyMember(lobby, user, metadata) { - return this.request(RPCCommands.UPDATE_LOBBY_MEMBER, { + async updateLobbyMember(lobby, user, metadata) { + await this.request(RPCCommands.UPDATE_LOBBY_MEMBER, { lobby_id: lobby.id || lobby, user_id: user.id || user, metadata, }); } - getRelationships() { + async getRelationships() { const types = Object.keys(RelationshipTypes); - return this.request(RPCCommands.GET_RELATIONSHIPS) - .then((o) => o.relationships.map((r) => ({ - ...r, - type: types[r.type], - }))); + const { relationships } = await this.request(RPCCommands.GET_RELATIONSHIPS); + return relationships.map((data) => ({ + ...data, + type: types[data.type], + })); } /** @@ -654,19 +743,20 @@ class RPCClient extends EventEmitter { * @param {Function} callback Callback when an event for the subscription is triggered * @returns {Promise} */ - subscribe(event, args, callback) { + async subscribe(event, args, callback) { if (!callback && typeof args === 'function') { callback = args; args = undefined; } - return this.request(RPCCommands.SUBSCRIBE, args, event).then(() => { - const subid = subKey(event, args); - this._subscriptions.set(subid, callback); - return { - unsubscribe: () => this.request(RPCCommands.UNSUBSCRIBE, args, event) - .then(() => this._subscriptions.delete(subid)), - }; - }); + await this.request(RPCCommands.SUBSCRIBE, args, event); + const subId = subKey(event, args); + this._subscriptions.set(subId, callback); + return { + unsubscribe: async () => { + await this.request(RPCCommands.UNSUBSCRIBE, args, event); + this._subscriptions.delete(subId); + }, + }; } /** @@ -674,6 +764,8 @@ class RPCClient extends EventEmitter { */ async destroy() { this.transport.close(); + this._removeListeners(true); + this.emit('destroyed'); } } diff --git a/src/constants.js b/src/constants.js index 441f832..0fcb124 100644 --- a/src/constants.js +++ b/src/constants.js @@ -8,6 +8,7 @@ function keyMirror(arr) { return tmp; } +exports.BASE_API_URL = 'https://discord.com/api'; exports.browser = typeof window !== 'undefined'; diff --git a/src/errors/index.js b/src/errors/index.js index f94a0bf..3e6de52 100644 --- a/src/errors/index.js +++ b/src/errors/index.js @@ -8,6 +8,7 @@ const errorMessages = { COULD_NOT_CONNECT: 'Couldn\'t connect.', COULD_NOT_FIND_ENDPOINT: 'Couldn\'t find the RPC API Endpoint.', TIMESTAMP_TOO_LARGE: (name) => `'${name}' Must fit into a unix timestamp.`, + NOT_CONNECTED: 'The client isn\'t connected', }; const makeError = (BaseClass) => { diff --git a/src/transports/ipc.js b/src/transports/ipc.js index da811af..ce56483 100644 --- a/src/transports/ipc.js +++ b/src/transports/ipc.js @@ -26,18 +26,18 @@ function getIPCPath(id) { function getIPC(id = 0) { return new Promise((resolve, reject) => { const path = getIPCPath(id); - const onerror = () => { + const onError = () => { if (id < 10) { resolve(getIPC(id + 1)); } else { reject(new Error('COULD_NOT_CONNECT')); } }; - const sock = net.createConnection(path, () => { - sock.removeListener('error', onerror); - resolve(sock); + const socket = net.createConnection(path, () => { + socket.removeListener('error', onError); + resolve(socket); }); - sock.once('error', onerror); + socket.once('error', onError); }); } @@ -130,7 +130,7 @@ class IPCTransport extends EventEmitter { if (data.cmd === 'AUTHORIZE' && data.evt !== 'ERROR') { findEndpoint().then((endpoint) => { this.client.request.endpoint = endpoint; - }); + }).catch((error) => this.client.emit('error', error)); } this.emit('message', data); break; From 7b445154dfe806157544725570e33948d015748a Mon Sep 17 00:00:00 2001 From: NotSugden <28943913+NotSugden@users.noreply.github.com> Date: Thu, 17 Sep 2020 09:02:08 +0100 Subject: [PATCH 03/12] revert: revert docs change --- src/client.js | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/client.js b/src/client.js index 019e2f0..e50e43b 100644 --- a/src/client.js +++ b/src/client.js @@ -260,13 +260,8 @@ class RPCClient extends EventEmitter { } /** - * Message handler. + * Message handler * @param {Object} message message - * @param {?Object} message.args The arguments for the event, if any - * @param {string} message.cmd The command sent - * @param {string} message.evt The event - * @param {object} message.data The data for this message - * @param {?string} message.nonce The nonce * @private */ _onRpcMessage({ args, cmd: command, data, evt: event, nonce }) { From 35f801c1dbe9b49a673f977cb7b59a1a27ed9188 Mon Sep 17 00:00:00 2001 From: NotSugden <28943913+NotSugden@users.noreply.github.com> Date: Thu, 17 Sep 2020 19:10:33 +0100 Subject: [PATCH 04/12] refactor: resolve close promise with void --- src/transports/ipc.js | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/transports/ipc.js b/src/transports/ipc.js index b7de1c7..0cd0e85 100644 --- a/src/transports/ipc.js +++ b/src/transports/ipc.js @@ -128,9 +128,13 @@ class IPCTransport extends EventEmitter { return; } if (data.cmd === 'AUTHORIZE' && data.evt !== 'ERROR') { - findEndpoint().then((endpoint) => { - this.client.request.endpoint = endpoint; - }).catch((error) => this.client.emit('error', error)); + findEndpoint() + .then((endpoint) => { + this.client.request.endpoint = endpoint; + }) + .catch((error) => { + this.client.emit('error', error); + }); } this.emit('message', data); break; @@ -144,8 +148,8 @@ class IPCTransport extends EventEmitter { }); } - onClose(e) { - this.emit('close', e); + onClose(event) { + this.emit('close', event); } send(data, op = OPCodes.FRAME) { @@ -153,8 +157,10 @@ class IPCTransport extends EventEmitter { } async close() { - return new Promise((r) => { - this.once('close', r); + return new Promise((resolve) => { + this.once('close', () => { + resolve(); + }); this.send({}, OPCodes.CLOSE); this.socket.end(); }); From 60a157ed08ca0ea67ca2102bb28dc10641522b2d Mon Sep 17 00:00:00 2001 From: NotSugden <28943913+NotSugden@users.noreply.github.com> Date: Thu, 17 Sep 2020 19:45:47 +0100 Subject: [PATCH 05/12] refactor(transports/ipc): cleaner decode function --- src/transports/ipc.js | 173 +++++++++++++++++++++--------------------- 1 file changed, 86 insertions(+), 87 deletions(-) diff --git a/src/transports/ipc.js b/src/transports/ipc.js index 0cd0e85..5cbe55a 100644 --- a/src/transports/ipc.js +++ b/src/transports/ipc.js @@ -14,91 +14,86 @@ const OPCodes = { PONG: 4, }; -function getIPCPath(id) { +const getIPCPath = (id) => { if (process.platform === 'win32') { return `\\\\?\\pipe\\discord-ipc-${id}`; } const { env: { XDG_RUNTIME_DIR, TMPDIR, TMP, TEMP } } = process; const prefix = XDG_RUNTIME_DIR || TMPDIR || TMP || TEMP || '/tmp'; return `${prefix.replace(/\/$/, '')}/discord-ipc-${id}`; -} +}; -function getIPC(id = 0) { - return new Promise((resolve, reject) => { - const path = getIPCPath(id); - const onError = () => { - if (id < 10) { - resolve(getIPC(id + 1)); - } else { - reject(new Error('COULD_NOT_CONNECT')); - } - }; - const socket = net.createConnection(path, () => { - socket.removeListener('error', onError); - resolve(socket); - }); - socket.once('error', onError); +const getIPC = (id = 0) => new Promise((resolve, reject) => { + const path = getIPCPath(id); + const onError = () => { + if (id < 10) { + resolve(getIPC(id + 1)); + } else { + reject(new Error('COULD_NOT_CONNECT')); + } + }; + const socket = net.createConnection(path, () => { + socket.removeListener('error', onError); + resolve(socket); }); -} + socket.once('error', onError); +}); -async function findEndpoint(tries = 0) { +const findEndpoint = async (tries = 0) => { if (tries > 30) { throw new Error('COULD_NOT_FIND_ENDPOINT'); } const endpoint = `http://127.0.0.1:${6463 + (tries % 10)}`; try { - const r = await fetch(endpoint); - if (r.status === 404) { + const response = await fetch(endpoint); + if (response.status === 404) { return endpoint; } - return findEndpoint(tries + 1); - } catch (e) { - return findEndpoint(tries + 1); - } -} + } catch { } // eslint-disable-line no-empty + return findEndpoint(tries + 1); +}; -function encode(op, data) { +const encode = (op, data) => { data = JSON.stringify(data); - const len = Buffer.byteLength(data); - const packet = Buffer.alloc(8 + len); + const length = Buffer.byteLength(data); + const packet = Buffer.alloc(length + 8); packet.writeInt32LE(op, 0); - packet.writeInt32LE(len, 4); - packet.write(data, 8, len); + packet.writeInt32LE(length, 4); + packet.write(data, 8, length); return packet; -} - -const working = { - full: '', - op: undefined, }; -function decode(socket, callback) { - const packet = socket.read(); - if (!packet) { - return; - } +const decode = (socket) => { + let op; + let jsonString = ''; - let { op } = working; - let raw; - if (working.full === '') { - op = working.op = packet.readInt32LE(0); - const len = packet.readInt32LE(4); - raw = packet.slice(8, len + 8); - } else { - raw = packet.toString(); - } + const read = () => { + const packet = socket.read(); + if (!packet) { + return null; + } + let part; + + if (jsonString === '') { + op = packet.readInt32LE(0); + const length = packet.readInt32LE(4); + part = packet.slice(8, length + 8); + } else { + part = packet.toString(); + } - try { - const data = JSON.parse(working.full + raw); - callback({ op, data }); // eslint-disable-line callback-return - working.full = ''; - working.op = undefined; - } catch (err) { - working.full += raw; - } + jsonString += part; - decode(socket, callback); -} + try { + const data = JSON.parse(jsonString); + return { data, op }; + } catch { + return read(); + } + }; + + return read(); +}; class IPCTransport extends EventEmitter { constructor(client) { @@ -109,6 +104,7 @@ class IPCTransport extends EventEmitter { async connect() { const socket = this.socket = await getIPC(); + socket.on('close', this.onClose.bind(this)); socket.on('error', this.onClose.bind(this)); this.emit('open'); @@ -118,33 +114,36 @@ class IPCTransport extends EventEmitter { })); socket.pause(); socket.on('readable', () => { - decode(socket, ({ op, data }) => { - switch (op) { - case OPCodes.PING: - this.send(data, OPCodes.PONG); - break; - case OPCodes.FRAME: - if (!data) { - return; - } - if (data.cmd === 'AUTHORIZE' && data.evt !== 'ERROR') { - findEndpoint() - .then((endpoint) => { - this.client.request.endpoint = endpoint; - }) - .catch((error) => { - this.client.emit('error', error); - }); - } - this.emit('message', data); - break; - case OPCodes.CLOSE: - this.emit('close', data); - break; - default: - break; - } - }); + const decoded = decode(socket); + if (!decoded) { + return; + } + const { data, op } = decoded; + switch (op) { + case OPCodes.PING: + this.send(data, OPCodes.PONG); + break; + case OPCodes.FRAME: + if (!data) { + return; + } + if (data.cmd === 'AUTHORIZE' && data.evt !== 'ERROR') { + findEndpoint() + .then((endpoint) => { + this.client.request.endpoint = endpoint; + }) + .catch((error) => { + this.client.emit('error', error); + }); + } + this.emit('message', data); + break; + case OPCodes.CLOSE: + this.emit('close', data); + break; + default: + break; + } }); } From be19d5a4796c6b5eb7d1d2fa18c5d760e4225ff2 Mon Sep 17 00:00:00 2001 From: NotSugden <28943913+NotSugden@users.noreply.github.com> Date: Fri, 18 Sep 2020 18:24:10 +0100 Subject: [PATCH 06/12] chore: use lowercase filename in package.json --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index e35c3f1..93b877e 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,6 @@ "erlpack": false, "electron": false, "register-scheme": false, - "./src/transports/IPC.js": false + "./src/transports/ipc.js": false } -} +} \ No newline at end of file From 1827b65483e5df763f45ffc9463c7d885e3a5329 Mon Sep 17 00:00:00 2001 From: NotSugden <28943913+NotSugden@users.noreply.github.com> Date: Thu, 17 Sep 2020 01:04:05 +0100 Subject: [PATCH 07/12] refactor: clean up code --- src/client.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/client.js b/src/client.js index 458bfc1..cae91ba 100644 --- a/src/client.js +++ b/src/client.js @@ -262,6 +262,11 @@ class RPCClient extends EventEmitter { /** * Message handler * @param {Object} message message + * @param {?Object} message.args The arguments for the event, if any + * @param {string} message.cmd The command sent + * @param {string} message.evt The event + * @param {object} message.data The data for this message + * @param {?string} message.nonce The nonce * @private */ _onRpcMessage({ args, cmd: command, data, evt: event, nonce }) { From b68e8e34d51af9e3d7c926f20317d249b23a904d Mon Sep 17 00:00:00 2001 From: NotSugden <28943913+NotSugden@users.noreply.github.com> Date: Thu, 17 Sep 2020 09:02:08 +0100 Subject: [PATCH 08/12] revert: revert docs change --- src/client.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/client.js b/src/client.js index cae91ba..458bfc1 100644 --- a/src/client.js +++ b/src/client.js @@ -262,11 +262,6 @@ class RPCClient extends EventEmitter { /** * Message handler * @param {Object} message message - * @param {?Object} message.args The arguments for the event, if any - * @param {string} message.cmd The command sent - * @param {string} message.evt The event - * @param {object} message.data The data for this message - * @param {?string} message.nonce The nonce * @private */ _onRpcMessage({ args, cmd: command, data, evt: event, nonce }) { From 1764e516753f3b2ec55dc0d7f18e3644edb6a219 Mon Sep 17 00:00:00 2001 From: NotSugden <28943913+NotSugden@users.noreply.github.com> Date: Thu, 17 Sep 2020 19:45:47 +0100 Subject: [PATCH 09/12] refactor(transports/ipc): cleaner decode function From 34c38bb90f3b46bee1f2c2e2f67f485c78066750 Mon Sep 17 00:00:00 2001 From: NotSugden <28943913+NotSugden@users.noreply.github.com> Date: Thu, 17 Sep 2020 01:04:05 +0100 Subject: [PATCH 10/12] refactor: clean up code --- src/client.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/client.js b/src/client.js index 458bfc1..cae91ba 100644 --- a/src/client.js +++ b/src/client.js @@ -262,6 +262,11 @@ class RPCClient extends EventEmitter { /** * Message handler * @param {Object} message message + * @param {?Object} message.args The arguments for the event, if any + * @param {string} message.cmd The command sent + * @param {string} message.evt The event + * @param {object} message.data The data for this message + * @param {?string} message.nonce The nonce * @private */ _onRpcMessage({ args, cmd: command, data, evt: event, nonce }) { From 79ad240641f1b4673c4d95c994586b5b47578ce0 Mon Sep 17 00:00:00 2001 From: NotSugden <28943913+NotSugden@users.noreply.github.com> Date: Thu, 17 Sep 2020 09:02:08 +0100 Subject: [PATCH 11/12] revert: revert docs change --- src/client.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/client.js b/src/client.js index cae91ba..458bfc1 100644 --- a/src/client.js +++ b/src/client.js @@ -262,11 +262,6 @@ class RPCClient extends EventEmitter { /** * Message handler * @param {Object} message message - * @param {?Object} message.args The arguments for the event, if any - * @param {string} message.cmd The command sent - * @param {string} message.evt The event - * @param {object} message.data The data for this message - * @param {?string} message.nonce The nonce * @private */ _onRpcMessage({ args, cmd: command, data, evt: event, nonce }) { From 1a358e5674b728437b48707fc94169272219bc7d Mon Sep 17 00:00:00 2001 From: NotSugden <28943913+NotSugden@users.noreply.github.com> Date: Thu, 17 Sep 2020 19:45:47 +0100 Subject: [PATCH 12/12] refactor(transports/ipc): cleaner decode function