diff --git a/.gitignore b/.gitignore index dee8a5889..80a6efd9c 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ logs/ # Authentication test/auth.json +test/auth.js docs/deploy/deploy_key docs/deploy/deploy_key.pub @@ -15,3 +16,4 @@ docs/deploy/deploy_key.pub .tmp/ .vscode/ docs/docs.json +webpack/ diff --git a/.travis.yml b/.travis.yml index 310a7c4b1..ef31870bc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,4 +10,10 @@ env: global: - ENCRYPTION_LABEL: "af862fa96d3e" - COMMIT_AUTHOR_EMAIL: "amishshah.2k@gmail.com" - + - CXX=g++-4.9 +addons: + apt: + sources: + - ubuntu-toolchain-r-test + packages: + - g++-4.9 diff --git a/package.json b/package.json index 7a4c0e72f..4720038fe 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,8 @@ "test": "eslint src && node docs/generator test", "docs": "node docs/generator", "test-docs": "node docs/generator test", - "lint": "eslint src" + "lint": "eslint src", + "web-dist": "npm install && ./node_modules/parallel-webpack/bin/run.js" }, "repository": { "type": "git", @@ -29,16 +30,23 @@ "homepage": "https://github.com/hydrabolt/discord.js#readme", "dependencies": { "superagent": "^2.3.0", - "tweetnacl": "^0.14.0", - "ws": "^1.1.0" + "tweetnacl": "^0.14.3", + "ws": "^1.1.1" }, "peerDependencies": { "node-opus": "^0.2.0", "opusscript": "^0.0.1" }, "devDependencies": { + "bufferutil": "^1.2.1", "eslint": "^3.10.0", - "jsdoc-to-markdown": "^2.0.0" + "jsdoc-to-markdown": "^2.0.0", + "json-loader": "^0.5.4", + "parallel-webpack": "^1.5.0", + "uglify-js": "github:mishoo/UglifyJS2#harmony", + "utf-8-validate": "^1.2.1", + "webpack": "^1.13.3", + "zlibjs": "github:imaya/zlib.js" }, "engines": { "node": ">=6.0.0" diff --git a/src/client/Client.js b/src/client/Client.js index 293af9d24..fd5c029e0 100644 --- a/src/client/Client.js +++ b/src/client/Client.js @@ -23,6 +23,12 @@ class Client extends EventEmitter { constructor(options = {}) { super(); + /** + * Whether the client is in a browser environment + * @type {boolean} + */ + this.browser = typeof window !== 'undefined'; + // Obtain shard details from environment if (!options.shardId && 'SHARD_ID' in process.env) options.shardId = Number(process.env.SHARD_ID); if (!options.shardCount && 'SHARD_COUNT' in process.env) options.shardCount = Number(process.env.SHARD_COUNT); diff --git a/src/client/ClientDataResolver.js b/src/client/ClientDataResolver.js index dfdc7ae2e..2a668d9f3 100644 --- a/src/client/ClientDataResolver.js +++ b/src/client/ClientDataResolver.js @@ -3,13 +3,14 @@ const fs = require('fs'); const request = require('superagent'); const Constants = require('../util/Constants'); -const User = require(`../structures/User`); -const Message = require(`../structures/Message`); -const Guild = require(`../structures/Guild`); -const Channel = require(`../structures/Channel`); -const GuildMember = require(`../structures/GuildMember`); -const Emoji = require(`../structures/Emoji`); -const ReactionEmoji = require(`../structures/ReactionEmoji`); +const convertArrayBuffer = require('../util/ConvertArrayBuffer'); +const User = require('../structures/User'); +const Message = require('../structures/Message'); +const Guild = require('../structures/Guild'); +const Channel = require('../structures/Channel'); +const GuildMember = require('../structures/GuildMember'); +const Emoji = require('../structures/Emoji'); +const ReactionEmoji = require('../structures/ReactionEmoji'); /** * The DataResolver identifies different objects and tries to resolve a specific piece of information from them, e.g. @@ -240,17 +241,19 @@ class ClientDataResolver { */ resolveBuffer(resource) { if (resource instanceof Buffer) return Promise.resolve(resource); + if (this.client.browser && resource instanceof ArrayBuffer) return Promise.resolve(convertArrayBuffer(resource)); if (typeof resource === 'string') { return new Promise((resolve, reject) => { if (/^https?:\/\//.test(resource)) { - request.get(resource) - .set('Content-Type', 'blob') - .end((err, res) => { - if (err) return reject(err); - if (!(res.body instanceof Buffer)) return reject(new TypeError('Body is not a Buffer')); - return resolve(res.body); - }); + const req = request.get(resource).set('Content-Type', 'blob'); + if (this.client.browser) req.responseType('arraybuffer'); + req.end((err, res) => { + if (err) return reject(err); + if (this.client.browser) return resolve(convertArrayBuffer(res.xhr.response)); + if (!(res.body instanceof Buffer)) return reject(new TypeError('Body is not a Buffer')); + return resolve(res.body); + }); } else { const file = path.resolve(resource); fs.stat(file, (err, stats) => { diff --git a/src/client/rest/RESTMethods.js b/src/client/rest/RESTMethods.js index be35d7d20..cfa35eea4 100644 --- a/src/client/rest/RESTMethods.js +++ b/src/client/rest/RESTMethods.js @@ -3,14 +3,13 @@ const Collection = require('../../util/Collection'); const splitMessage = require('../../util/SplitMessage'); const parseEmoji = require('../../util/ParseEmoji'); -const requireStructure = name => require(`../../structures/${name}`); -const User = requireStructure('User'); -const GuildMember = requireStructure('GuildMember'); -const Role = requireStructure('Role'); -const Invite = requireStructure('Invite'); -const Webhook = requireStructure('Webhook'); -const UserProfile = requireStructure('UserProfile'); -const ClientOAuth2Application = requireStructure('ClientOAuth2Application'); +const User = require('../../structures/User'); +const GuildMember = require('../../structures/GuildMember'); +const Role = require('../../structures/Role'); +const Invite = require('../../structures/Invite'); +const Webhook = require('../../structures/Webhook'); +const UserProfile = require('../../structures/UserProfile'); +const ClientOAuth2Application = require('../../structures/ClientOAuth2Application'); class RESTMethods { constructor(restManager) { diff --git a/src/client/voice/ClientVoiceManager.js b/src/client/voice/ClientVoiceManager.js index 09e9982a1..d65384505 100644 --- a/src/client/voice/ClientVoiceManager.js +++ b/src/client/voice/ClientVoiceManager.js @@ -78,6 +78,7 @@ class ClientVoiceManager { */ joinChannel(channel) { return new Promise((resolve, reject) => { + if (this.client.browser) throw new Error('Voice connections are not available in browsers.'); if (this.pending.get(channel.guild.id)) throw new Error('Already connecting to this guild\'s voice server.'); if (!channel.joinable) throw new Error('You do not have permission to join this voice channel.'); diff --git a/src/client/websocket/WebSocketManager.js b/src/client/websocket/WebSocketManager.js index 40d714f4d..002411161 100644 --- a/src/client/websocket/WebSocketManager.js +++ b/src/client/websocket/WebSocketManager.js @@ -1,8 +1,10 @@ -const WebSocket = require('ws'); +const browser = typeof window !== 'undefined'; +const WebSocket = browser ? window.WebSocket : require('ws'); // eslint-disable-line no-undef const EventEmitter = require('events').EventEmitter; const Constants = require('../../util/Constants'); -const zlib = require('zlib'); +const inflate = browser ? require('zlibjs').inflateSync : require('zlib').inflateSync; const PacketManager = require('./packets/WebSocketPacketManager'); +const convertArrayBuffer = require('../../util/ConvertArrayBuffer'); /** * The WebSocket Manager of the Client @@ -78,6 +80,7 @@ class WebSocketManager extends EventEmitter { this.normalReady = false; if (this.status !== Constants.Status.RECONNECTING) this.status = Constants.Status.CONNECTING; this.ws = new WebSocket(gateway); + if (browser) this.ws.binaryType = 'arraybuffer'; this.ws.onopen = () => this.eventOpen(); this.ws.onclose = (d) => this.eventClose(d); this.ws.onmessage = (e) => this.eventMessage(e); @@ -193,11 +196,11 @@ class WebSocketManager extends EventEmitter { */ eventClose(event) { this.emit('close', event); + this.client.clearInterval(this.client.manager.heartbeatInterval); /** * Emitted whenever the client websocket is disconnected * @event Client#disconnect */ - clearInterval(this.client.manager.heartbeatInterval); if (!this.reconnecting) this.client.emit(Constants.Events.DISCONNECT); if (event.code === 4004) return; if (event.code === 4010) return; @@ -211,10 +214,13 @@ class WebSocketManager extends EventEmitter { * @returns {boolean} */ eventMessage(event) { - let packet; + let packet = event.data; try { - if (event.binary) event.data = zlib.inflateSync(event.data).toString(); - packet = JSON.parse(event.data); + if (typeof packet !== 'string') { + if (packet instanceof ArrayBuffer) packet = convertArrayBuffer(packet); + packet = inflate(packet).toString(); + } + packet = JSON.parse(packet); } catch (e) { return this.eventError(new Error(Constants.Errors.BAD_WS_MESSAGE)); } @@ -236,7 +242,7 @@ class WebSocketManager extends EventEmitter { * @param {Error} error The encountered error */ if (this.client.listenerCount('error') > 0) this.client.emit('error', err); - this.tryReconnect(); + this.ws.close(); } _emitReady(normal = true) { diff --git a/src/client/websocket/packets/handlers/Ready.js b/src/client/websocket/packets/handlers/Ready.js index 96d8557d7..aed766bf0 100644 --- a/src/client/websocket/packets/handlers/Ready.js +++ b/src/client/websocket/packets/handlers/Ready.js @@ -1,7 +1,6 @@ const AbstractHandler = require('./AbstractHandler'); -const getStructure = name => require(`../../../../structures/${name}`); -const ClientUser = getStructure('ClientUser'); +const ClientUser = require('../../../../structures/ClientUser'); class ReadyHandler extends AbstractHandler { handle(packet) { diff --git a/src/index.js b/src/index.js index 3eea67ea1..c1d262a7b 100644 --- a/src/index.js +++ b/src/index.js @@ -41,3 +41,5 @@ module.exports = { version: require('../package').version, }; + +if (typeof window !== 'undefined') window.Discord = module.exports; // eslint-disable-line no-undef diff --git a/src/util/Constants.js b/src/util/Constants.js index 7efe40690..ac2f3be2c 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -42,11 +42,12 @@ exports.DefaultOptions = { * Websocket options. These are left as snake_case to match the API. * @typedef {Object} WebsocketOptions * @property {number} [large_threshold=250] Number of members in a guild to be considered large - * @property {boolean} [compress=true] Whether to compress data sent on the connection + * @property {boolean} [compress=true] Whether to compress data sent on the connection. + * Defaults to `false` for browsers. */ ws: { large_threshold: 250, - compress: true, + compress: typeof window === 'undefined', properties: { $os: process ? process.platform : 'discord.js', $browser: 'discord.js', diff --git a/src/util/ConvertArrayBuffer.js b/src/util/ConvertArrayBuffer.js new file mode 100644 index 000000000..26b1cc8b7 --- /dev/null +++ b/src/util/ConvertArrayBuffer.js @@ -0,0 +1,18 @@ +function arrayBufferToBuffer(ab) { + const buffer = new Buffer(ab.byteLength); + const view = new Uint8Array(ab); + for (var i = 0; i < buffer.length; ++i) buffer[i] = view[i]; + return buffer; +} + +function str2ab(str) { + const buffer = new ArrayBuffer(str.length * 2); + const view = new Uint16Array(buffer); + for (var i = 0, strLen = str.length; i < strLen; i++) view[i] = str.charCodeAt(i); + return buffer; +} + +module.exports = function convertArrayBuffer(x) { + if (typeof x === 'string') x = str2ab(x); + return arrayBufferToBuffer(x); +}; diff --git a/test/webpack.html b/test/webpack.html new file mode 100644 index 000000000..006ea774a --- /dev/null +++ b/test/webpack.html @@ -0,0 +1,30 @@ + + + + discord.js Webpack test + + + + + + + + diff --git a/webpack.config.js b/webpack.config.js new file mode 100644 index 000000000..906df9147 --- /dev/null +++ b/webpack.config.js @@ -0,0 +1,43 @@ +/* + ONLY RUN BUILDS WITH `npm run web-dist`! + DO NOT USE NORMAL WEBPACK! IT WILL NOT WORK! +*/ + +const webpack = require('webpack'); +const createVariants = require('parallel-webpack').createVariants; +const version = require('./package.json').version; + +const createConfig = (options) => { + const plugins = [ + new webpack.DefinePlugin({ 'global.GENTLY': false }), + ]; + + if (options.minify) plugins.push(new webpack.optimize.UglifyJsPlugin({ minimize: true })); + + const filename = `./webpack/discord${process.env.VERSIONED === 'false' ? '' : '.' + version}${options.minify ? '.min' : ''}.js`; // eslint-disable-line + + return { + entry: './src/index.js', + output: { + path: __dirname, + filename, + }, + module: { + loaders: [ + { test: /\.json$/, loader: 'json-loader' }, + { test: /\.md$/, loader: 'ignore-loader' }, + ], + }, + node: { + fs: 'empty', + dns: 'mock', + tls: 'mock', + child_process: 'empty', + dgram: 'empty', + __dirname: true, + }, + plugins, + }; +}; + +module.exports = createVariants({}, { minify: [false, true] }, createConfig);