diff --git a/lib/Client/Client.js b/lib/Client/Client.js index 862d6311f..97ec4df12 100644 --- a/lib/Client/Client.js +++ b/lib/Client/Client.js @@ -532,6 +532,24 @@ var Client = (function (_EventEmitter) { }); }; + //def joinVoiceChannel + + Client.prototype.joinVoiceChannel = function joinVoiceChannel(channel) { + var callback = arguments.length <= 1 || arguments[1] === undefined ? function (err) {} : arguments[1]; + + var self = this; + return new Promise(function (resolve, reject) { + + self.internal.joinVoiceChannel(channel).then(function (chan) { + callback(null, chan); + resolve(chan); + })["catch"](function (err) { + callback(err); + reject(err); + }); + }); + }; + _createClass(Client, [{ key: "users", get: function get() { diff --git a/lib/Client/InternalClient.js b/lib/Client/InternalClient.js index e1ab0193a..f7a7d25a7 100644 --- a/lib/Client/InternalClient.js +++ b/lib/Client/InternalClient.js @@ -22,7 +22,8 @@ var User = require("../Structures/User.js"), Server = require("../Structures/Server.js"), Message = require("../Structures/Message.js"), Role = require("../Structures/Role.js"), - Invite = require("../Structures/Invite.js"); + Invite = require("../Structures/Invite.js"), + VoiceConnection = require("../Voice/VoiceConnection.js"); var zlib; @@ -43,9 +44,76 @@ var InternalClient = (function () { this.channels = new Cache(); this.servers = new Cache(); this.private_channels = new Cache(); + this.voiceConnection = null; this.resolver = new Resolver(this); } + //def leaveVoiceChannel + + InternalClient.prototype.leaveVoiceChannel = function leaveVoiceChannel() { + var self = this; + return new Promise(function (resolve, reject) { + if (self.voiceConnection) { + self.voiceConnection.destroy(); + self.voiceConnection = null; + resolve(); + } else { + resolve(); + } + }); + }; + + //def joinVoiceChannel + + InternalClient.prototype.joinVoiceChannel = function joinVoiceChannel(chann) { + var self = this; + return new Promise(function (resolve, reject) { + + var channel = self.resolver.resolveVoiceChannel(chann); + + if (channel) { + var next = function next() { + var session, + token, + server = channel.server, + endpoint; + + var check = function check(m) { + var data = JSON.parse(m); + if (data.t === "VOICE_STATE_UPDATE") { + session = data.d.session_id; + } else if (data.t === "VOICE_SERVER_UPDATE") { + token = data.d.token; + endpoint = data.d.endpoint; + var chan = self.voiceConnection = new VoiceConnection(channel, self.client, session, token, server, endpoint); + + chan.on("ready", resolve); + chan.on("error", reject); + + self.client.emit("debug", "removed temporary voice websocket listeners"); + self.websocket.removeListener("message", check); + } + }; + + self.websocket.on("message", check); + self.sendWS({ + op: 4, + d: { + "guild_id": server.id, + "channel_id": channel.id, + "self_mute": false, + "self_deaf": false + } + }); + }; + + self.leaveVoiceChannel().then(next); + } else { + reject(new Error("voice channel does not exist")); + } + }); + }; + // def createServer InternalClient.prototype.createServer = function createServer(name) { @@ -718,7 +786,6 @@ var InternalClient = (function () { } request.put(Endpoints.CHANNEL_PERMISSIONS(channel.id) + "/" + data.id).set("authorization", self.token).send(data).end(function (err) { - console.log(err); if (err) { reject(err); } else { diff --git a/lib/Client/Resolver/Resolver.js b/lib/Client/Resolver/Resolver.js index ceec4167f..a0a6e55ca 100644 --- a/lib/Client/Resolver/Resolver.js +++ b/lib/Client/Resolver/Resolver.js @@ -116,6 +116,14 @@ var Resolver = (function () { return found; }; + Resolver.prototype.resolveVoiceChannel = function resolveVoiceChannel(resource) { + // resolveChannel will also work but this is more apt + if (resource instanceof VoiceChannel) { + return resource; + } + return null; + }; + Resolver.prototype.resolveChannel = function resolveChannel(resource) { /* accepts a Message, Channel, Server, String ID, User diff --git a/lib/Voice/AudioEncoder.js b/lib/Voice/AudioEncoder.js new file mode 100644 index 000000000..01e0fb638 --- /dev/null +++ b/lib/Voice/AudioEncoder.js @@ -0,0 +1,98 @@ +"use strict"; + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +var cpoc = require("child_process"); + +var opus; +try { + opus = require("node-opus"); +} catch (e) { + // no opus! +} +var VoicePacket = require("./VoicePacket.js"); + +var AudioEncoder = (function () { + function AudioEncoder() { + _classCallCheck(this, AudioEncoder); + + if (opus) { + this.opus = new opus.OpusEncoder(48000, 1); + } + } + + AudioEncoder.prototype.opusBuffer = function opusBuffer(buffer) { + + return this.opus.encode(buffer, 1920); + }; + + AudioEncoder.prototype.encodeStream = function encodeStream(stream) { + var callback = arguments.length <= 1 || arguments[1] === undefined ? function (err, buffer) {} : arguments[1]; + + var self = this; + return new Promise(function (resolve, reject) { + var enc = cpoc.spawn("ffmpeg", ["-f", "s16le", "-ar", "48000", "-ac", "1", // this can be 2 but there's no point, discord makes it mono on playback, wasted bandwidth. + "-af", "volume=1", "pipe:1", "-i", "-"]); + + stream.pipe(enc.stdin); + + enc.stdout.once("readable", function () { + callback(null, { + proc: enc, + stream: enc.stdout, + instream: stream + }); + resolve({ + proc: enc, + stream: enc.stdout, + instream: stream + }); + }); + + enc.stdout.on("end", function () { + callback("end"); + reject("end"); + }); + + enc.stdout.on("close", function () { + callback("close"); + reject("close"); + }); + }); + }; + + AudioEncoder.prototype.encodeFile = function encodeFile(file) { + var callback = arguments.length <= 1 || arguments[1] === undefined ? function (err, buffer) {} : arguments[1]; + + var self = this; + return new Promise(function (resolve, reject) { + var enc = cpoc.spawn("ffmpeg", ["-f", "s16le", "-ar", "48000", "-ac", "1", // this can be 2 but there's no point, discord makes it mono on playback, wasted bandwidth. + "-af", "volume=1", "pipe:1", "-i", file]); + + enc.stdout.once("readable", function () { + callback(null, { + proc: enc, + stream: enc.stdout + }); + resolve({ + proc: enc, + stream: enc.stdout + }); + }); + + enc.stdout.on("end", function () { + callback("end"); + reject("end"); + }); + + enc.stdout.on("close", function () { + callback("close"); + reject("close"); + }); + }); + }; + + return AudioEncoder; +})(); + +module.exports = AudioEncoder; \ No newline at end of file diff --git a/lib/Voice/StreamIntent.js b/lib/Voice/StreamIntent.js new file mode 100644 index 000000000..97953194c --- /dev/null +++ b/lib/Voice/StreamIntent.js @@ -0,0 +1,22 @@ +"use strict"; +// represents an intent of streaming music + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + +var EventEmitter = require("events"); + +var StreamIntent = (function (_EventEmitter) { + _inherits(StreamIntent, _EventEmitter); + + function StreamIntent() { + _classCallCheck(this, StreamIntent); + + _EventEmitter.call(this); + } + + return StreamIntent; +})(EventEmitter); + +module.exports = StreamIntent; \ No newline at end of file diff --git a/lib/Voice/VoiceConnection.js b/lib/Voice/VoiceConnection.js new file mode 100644 index 000000000..ea2f4812b --- /dev/null +++ b/lib/Voice/VoiceConnection.js @@ -0,0 +1,323 @@ +"use strict"; +/* + Major credit to izy521 who is the creator of + https://github.com/izy521/discord.io, + + without his help voice chat in discord.js would not have + been possible! +*/ + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + +var WebSocket = require("ws"); +var dns = require("dns"); +var udp = require("dgram"); +var fs = require("fs"); +var AudioEncoder = require("./AudioEncoder.js"); +var VoicePacket = require("./VoicePacket.js"); +var StreamIntent = require("./StreamIntent.js"); +var EventEmitter = require("events"); + +var VoiceConnection = (function (_EventEmitter) { + _inherits(VoiceConnection, _EventEmitter); + + function VoiceConnection(channel, client, session, token, server, endpoint) { + _classCallCheck(this, VoiceConnection); + + _EventEmitter.call(this); + this.id = channel.id; + this.voiceChannel = channel; + this.client = client; + this.session = session; + this.token = token; + this.server = server; + this.endpoint = endpoint.replace(":80", ""); + this.vWS = null; // vWS means voice websocket + this.ready = false; + this.vWSData = {}; + this.encoder = new AudioEncoder(); + this.udp = null; + this.playingIntent = null; + this.playing = false; + this.streamTime = 0; + this.streamProc = null; + this.KAI = null; + this.init(); + } + + VoiceConnection.prototype.destroy = function destroy() { + this.stopPlaying(); + if (this.KAI) clearInterval(this.KAI); + this.vWS.close(); + this.udp.close(); + }; + + VoiceConnection.prototype.stopPlaying = function stopPlaying() { + this.playing = false; + this.playingIntent = null; + if (this.instream) { + this.instream.end(); + this.instream.destroy(); + } + }; + + VoiceConnection.prototype.playStream = function playStream(stream) { + + var self = this; + + var startTime = Date.now(); + var sequence = 0; + var time = 0; + var count = 0; + + var length = 20; + + if (self.playingIntent) { + self.stopPlaying(); + } + self.playing = true; + var retStream = new StreamIntent(); + var onWarning = false; + self.playingIntent = retStream; + + function send() { + + if (!self.playingIntent || !self.playing) { + self.setSpeaking(false); + retStream.emit("end"); + self; + return; + } + try { + var buffer = stream.read(1920); + if (!buffer) { + setTimeout(send, length * 10); // give chance for some data in 200ms to appear + return; + } + + if (buffer.length !== 1920) { + if (onWarning) { + retStream.emit("end"); + stream.destroy(); + self.setSpeaking(false); + return; + } else { + onWarning = true; + setTimeout(send, length * 10); // give chance for some data in 200ms to appear + return; + } + } + + count++; + sequence + 10 < 65535 ? sequence += 1 : sequence = 0; + time + 9600 < 4294967295 ? time += 960 : time = 0; + + self.sendBuffer(buffer, sequence, time, function (e) {}); + + var nextTime = startTime + count * length; + + self.streamTime = count * length; + + setTimeout(send, length + (nextTime - Date.now())); + if (!self.playing) self.setSpeaking(true); + + retStream.emit("time", self.streamTime); + } catch (e) { + retStream.emit("error", e); + } + } + self.setSpeaking(true); + send(); + + return retStream; + }; + + VoiceConnection.prototype.setSpeaking = function setSpeaking(value) { + this.playing = value; + if (this.vWS.readyState === WebSocket.OPEN) this.vWS.send(JSON.stringify({ + op: 5, + d: { + speaking: value, + delay: 0 + } + })); + }; + + VoiceConnection.prototype.sendPacket = function sendPacket(packet) { + var callback = arguments.length <= 1 || arguments[1] === undefined ? function (err) {} : arguments[1]; + + var self = this; + self.playing = true; + try { + if (self.vWS.readyState === WebSocket.OPEN) self.udp.send(packet, 0, packet.length, self.vWSData.port, self.endpoint, callback); + } catch (e) { + self.playing = false; + callback(e); + return false; + } + }; + + VoiceConnection.prototype.sendBuffer = function sendBuffer(rawbuffer, sequence, timestamp, callback) { + var self = this; + self.playing = true; + try { + if (!self.encoder.opus) { + self.playing = false; + self.emit("error", "No Opus!"); + self.client.emit("debug", "Tried to use node-opus, but opus not available - install it!"); + return; + } + var buffer = self.encoder.opusBuffer(rawbuffer); + var packet = new VoicePacket(buffer, sequence, timestamp, self.vWSData.ssrc); + return self.sendPacket(packet, callback); + } catch (e) { + self.playing = false; + self.emit("error", e); + return false; + } + }; + + VoiceConnection.prototype.test = function test() { + this.playFile("C:/users/amish/desktop/audio.mp3").then(function (stream) { + stream.on("time", function (time) { + console.log("Time", time); + }); + }); + }; + + VoiceConnection.prototype.playFile = function playFile(stream) { + var _this = this; + + var callback = arguments.length <= 1 || arguments[1] === undefined ? function (err, str) {} : arguments[1]; + + var self = this; + return new Promise(function (resolve, reject) { + _this.encoder.encodeFile(stream)["catch"](error).then(function (data) { + self.streamProc = data.proc; + var intent = self.playStream(data.stream); + resolve(intent); + callback(null, intent); + }); + function error() { + var e = arguments.length <= 0 || arguments[0] === undefined ? true : arguments[0]; + + reject(e); + callback(e); + } + }); + }; + + VoiceConnection.prototype.playRawStream = function playRawStream(stream) { + var _this2 = this; + + var callback = arguments.length <= 1 || arguments[1] === undefined ? function (err, str) {} : arguments[1]; + + var self = this; + return new Promise(function (resolve, reject) { + _this2.encoder.encodeStream(stream)["catch"](error).then(function (data) { + self.streamProc = data.proc; + self.instream = data.instream; + var intent = self.playStream(data.stream); + resolve(intent); + callback(null, intent); + }); + function error() { + var e = arguments.length <= 0 || arguments[0] === undefined ? true : arguments[0]; + + reject(e); + callback(e); + } + }); + }; + + VoiceConnection.prototype.init = function init() { + var _this3 = this; + + var self = this; + dns.lookup(this.endpoint, function (err, address, family) { + self.endpoint = address; + var vWS = self.vWS = new WebSocket("wss://" + _this3.endpoint, null, { rejectUnauthorized: false }); + var udpClient = self.udp = udp.createSocket("udp4"); + + var firstPacket = true; + + var discordIP = "", + discordPort = ""; + + udpClient.bind({ exclusive: true }); + udpClient.on('message', function (msg, rinfo) { + var buffArr = JSON.parse(JSON.stringify(msg)).data; + if (firstPacket === true) { + for (var i = 4; i < buffArr.indexOf(0, i); i++) { + discordIP += String.fromCharCode(buffArr[i]); + } + discordPort = msg.readUIntLE(msg.length - 2, 2).toString(10); + + var wsDiscPayload = { + "op": 1, + "d": { + "protocol": "udp", + "data": { + "address": discordIP, + "port": Number(discordPort), + "mode": self.vWSData.modes[0] //Plain + } + } + }; + vWS.send(JSON.stringify(wsDiscPayload)); + firstPacket = false; + } + }); + + vWS.on("open", function () { + vWS.send(JSON.stringify({ + op: 0, + d: { + server_id: self.server.id, + user_id: self.client.internal.user.id, + session_id: self.session, + token: self.token + } + })); + }); + + var KAI; + + vWS.on("message", function (msg) { + var data = JSON.parse(msg); + switch (data.op) { + case 2: + self.vWSData = data.d; + + KAI = setInterval(function () { + if (vWS.readyState === WebSocket.OPEN) vWS.send(JSON.stringify({ + op: 3, + d: null + })); + }, data.d.heartbeat_interval); + self.KAI = KAI; + + var udpPacket = new Buffer(70); + udpPacket.writeUIntBE(data.d.ssrc, 0, 4); + udpClient.send(udpPacket, 0, udpPacket.length, data.d.port, self.endpoint, function (err) { + if (err) self.emit("error", err); + }); + break; + case 4: + + self.ready = true; + self.mode = data.d.mode; + self.emit("ready", self); + + break; + } + }); + }); + }; + + return VoiceConnection; +})(EventEmitter); + +module.exports = VoiceConnection; \ No newline at end of file diff --git a/lib/Voice/VoicePacket.js b/lib/Voice/VoicePacket.js new file mode 100644 index 000000000..f432bf241 --- /dev/null +++ b/lib/Voice/VoicePacket.js @@ -0,0 +1,26 @@ +"use strict"; + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +var VoicePacket = function VoicePacket(data, sequence, time, ssrc) { + _classCallCheck(this, VoicePacket); + + var audioBuffer = data, + returnBuffer = new Buffer(audioBuffer.length + 12); + + returnBuffer.fill(0); + returnBuffer[0] = 0x80; + returnBuffer[1] = 0x78; + + returnBuffer.writeUIntBE(sequence, 2, 2); + returnBuffer.writeUIntBE(time, 4, 4); + returnBuffer.writeUIntBE(ssrc, 8, 4); + + for (var i = 0; i < audioBuffer.length; i++) { + returnBuffer[i + 12] = audioBuffer[i]; + } + + return returnBuffer; +}; + +module.exports = VoicePacket; \ No newline at end of file diff --git a/lib/index.js b/lib/index.js index 2c2c1a317..31c91c19c 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,54 +1,17 @@ "use strict"; module.exports = { - Client: require("./Client/Client.js") -}; - -var a = new module.exports.Client(); -a.on("debug", function (m) { - return console.log("[debug]", m); -}); -a.on("warn", function (m) { - return console.log("[warn]", m); -}); -var start = Date.now(); -a.on("message", function (m) { - if (m.content === "$$$") { - a.internal.setTopic(m.channel, "a channel topic!"); - } -}); -a.on("userTypingStart", function (user, chan) { - console.log(user.username + " typing"); -}); -a.on("userTypingStop", function (user, chan) { - console.log(user.username + " stopped typing"); -}); -a.on("ready", function () { - for (var _iterator = a.internal.servers, _isArray = Array.isArray(_iterator), _i = 0, _iterator = _isArray ? _iterator : _iterator[Symbol.iterator]();;) { - var _ref; - - if (_isArray) { - if (_i >= _iterator.length) break; - _ref = _iterator[_i++]; - } else { - _i = _iterator.next(); - if (_i.done) break; - _ref = _i.value; - } - - var server = _ref; - - if (server.name === "craptown") { - a.leaveServer(server); - } - } -}); - -function error(e) { - throw e; - process.exit(0); -} - -a.login(process.env["discordEmail"], process.env["discordPass"])["catch"](function (e) { - return console.log(e); -}); \ No newline at end of file + Client: require("./Client/Client"), + Channel: require("./Structures/Channel"), + ChannelPermissions: require("./Structures/ChannelPermissions"), + Invite: require("./Structures/Invite"), + Message: require("./Structures/Message"), + PermissionOverwrite: require("./Structures/PermissionOverwrite"), + PMChannel: require("./Structures/PMChannel"), + Role: require("./Structures/Role"), + Server: require("./Structures/Server"), + ServerChannel: require("./Structures/ServerChannel"), + TextChannel: require("./Structures/TextChannel"), + User: require("./Structures/User"), + VoiceChannel: require("./Structures/VoiceChannel") +}; \ No newline at end of file diff --git a/package.json b/package.json index 8cf0b88a9..4c6ebd14f 100644 --- a/package.json +++ b/package.json @@ -34,5 +34,8 @@ "grunt-browserify": "^4.0.0", "grunt-contrib-uglify": "^0.9.2", "load-grunt-tasks": "^3.2.0" + }, + "optionalDependencies": { + "node-opus": "^0.1.11" } } diff --git a/src/Client/Client.js b/src/Client/Client.js index cbfae507d..4fe32102b 100644 --- a/src/Client/Client.js +++ b/src/Client/Client.js @@ -513,6 +513,24 @@ class Client extends EventEmitter { }) } + + //def joinVoiceChannel + joinVoiceChannel(channel, callback=function(err){}){ + var self = this; + return new Promise((resolve, reject)=>{ + + self.internal.joinVoiceChannel(channel) + .then(chan => { + callback(null, chan); + resolve(chan); + }) + .catch(err => { + callback(err); + reject(err); + }); + + }); + } } module.exports = Client; \ No newline at end of file diff --git a/src/Client/InternalClient.js b/src/Client/InternalClient.js index 13bc9ae38..ef2977e95 100644 --- a/src/Client/InternalClient.js +++ b/src/Client/InternalClient.js @@ -20,7 +20,8 @@ var User = require("../Structures/User.js"), Server = require("../Structures/Server.js"), Message = require("../Structures/Message.js"), Role = require("../Structures/Role.js"), - Invite = require("../Structures/Invite.js"); + Invite = require("../Structures/Invite.js"), + VoiceConnection = require("../Voice/VoiceConnection.js"); var zlib; @@ -39,8 +40,74 @@ class InternalClient { this.channels = new Cache(); this.servers = new Cache(); this.private_channels = new Cache(); + this.voiceConnection = null; this.resolver = new Resolver(this); } + + //def leaveVoiceChannel + leaveVoiceChannel(){ + var self = this; + return new Promise((resolve, reject) => { + if(self.voiceConnection){ + self.voiceConnection.destroy(); + self.voiceConnection = null; + resolve(); + }else{ + resolve(); + } + }); + } + + //def joinVoiceChannel + joinVoiceChannel(chann){ + var self = this; + return new Promise((resolve, reject) => { + + var channel = self.resolver.resolveVoiceChannel(chann); + + if(channel){ + + self.leaveVoiceChannel().then(next); + + function next(){ + var session, token, server = channel.server, endpoint; + + var check = (m) => { + var data = JSON.parse(m); + if(data.t === "VOICE_STATE_UPDATE"){ + session = data.d.session_id; + }else if(data.t === "VOICE_SERVER_UPDATE"){ + token = data.d.token; + endpoint = data.d.endpoint; + var chan = self.voiceConnection = new VoiceConnection(channel, self.client, session, token, server, endpoint); + + chan.on("ready", resolve); + chan.on("error", reject); + + self.client.emit("debug", "removed temporary voice websocket listeners"); + self.websocket.removeListener("message", check); + + } + }; + + self.websocket.on("message", check); + self.sendWS({ + op : 4, + d : { + "guild_id" : server.id, + "channel_id" : channel.id, + "self_mute" : false, + "self_deaf" : false + } + }); + } + }else{ + reject(new Error("voice channel does not exist")); + } + + }); + } + // def createServer createServer(name, region = "london") { var self = this; @@ -766,7 +833,6 @@ class InternalClient { .set("authorization", self.token) .send(data) .end(function (err) { - console.log(err); if (err) { reject(err); } else { diff --git a/src/Client/Resolver/Resolver.js b/src/Client/Resolver/Resolver.js index 1d7252715..6dc88e457 100644 --- a/src/Client/Resolver/Resolver.js +++ b/src/Client/Resolver/Resolver.js @@ -98,6 +98,14 @@ class Resolver { return found; } + + resolveVoiceChannel(resource) { + // resolveChannel will also work but this is more apt + if(resource instanceof VoiceChannel){ + return resource; + } + return null; + } resolveChannel(resource) { /* diff --git a/src/Voice/AudioEncoder.js b/src/Voice/AudioEncoder.js new file mode 100644 index 000000000..7293d5ef1 --- /dev/null +++ b/src/Voice/AudioEncoder.js @@ -0,0 +1,101 @@ +"use strict"; + +var cpoc = require("child_process"); + +var opus; +try{ + opus = require("node-opus"); +}catch(e){ + // no opus! +} +var VoicePacket = require("./VoicePacket.js"); + +class AudioEncoder{ + constructor(){ + if(opus){ + this.opus = new opus.OpusEncoder(48000, 1); + } + } + + opusBuffer(buffer){ + + return this.opus.encode(buffer, 1920); + + } + + encodeStream(stream, callback=function(err, buffer){}){ + var self = this; + return new Promise((resolve, reject) => { + var enc = cpoc.spawn("ffmpeg" , [ + "-f", "s16le", + "-ar", "48000", + "-ac", "1", // this can be 2 but there's no point, discord makes it mono on playback, wasted bandwidth. + "-af", "volume=1", + "pipe:1", + "-i", "-" + ]); + + stream.pipe(enc.stdin); + + enc.stdout.once("readable", function() { + callback(null, { + proc : enc, + stream : enc.stdout, + instream : stream + }); + resolve({ + proc : enc, + stream : enc.stdout, + instream : stream + }); + }); + + enc.stdout.on("end", function() { + callback("end"); + reject("end"); + }); + + enc.stdout.on("close", function() { + callback("close"); + reject("close"); + }); + }); + } + + encodeFile(file, callback=function(err, buffer){}){ + var self = this; + return new Promise((resolve, reject) => { + var enc = cpoc.spawn("ffmpeg" , [ + "-f", "s16le", + "-ar", "48000", + "-ac", "1", // this can be 2 but there's no point, discord makes it mono on playback, wasted bandwidth. + "-af", "volume=1", + "pipe:1", + "-i", file + ]); + + enc.stdout.once("readable", function() { + callback(null, { + proc : enc, + stream : enc.stdout + }); + resolve({ + proc : enc, + stream : enc.stdout + }); + }); + + enc.stdout.on("end", function() { + callback("end"); + reject("end"); + }); + + enc.stdout.on("close", function() { + callback("close"); + reject("close"); + }); + }); + } +} + +module.exports = AudioEncoder; \ No newline at end of file diff --git a/src/Voice/StreamIntent.js b/src/Voice/StreamIntent.js new file mode 100644 index 000000000..e5340ecb7 --- /dev/null +++ b/src/Voice/StreamIntent.js @@ -0,0 +1,11 @@ +"use strict"; +// represents an intent of streaming music +var EventEmitter = require("events"); + +class StreamIntent extends EventEmitter{ + constructor(){ + super(); + } +} + +module.exports = StreamIntent; \ No newline at end of file diff --git a/src/Voice/VoiceConnection.js b/src/Voice/VoiceConnection.js new file mode 100644 index 000000000..040d00ea0 --- /dev/null +++ b/src/Voice/VoiceConnection.js @@ -0,0 +1,315 @@ +"use strict"; +/* + Major credit to izy521 who is the creator of + https://github.com/izy521/discord.io, + + without his help voice chat in discord.js would not have + been possible! +*/ + +var WebSocket = require("ws"); +var dns = require("dns"); +var udp = require("dgram"); +var fs = require("fs"); +var AudioEncoder = require("./AudioEncoder.js"); +var VoicePacket = require("./VoicePacket.js"); +var StreamIntent = require("./StreamIntent.js"); +var EventEmitter = require("events"); + +class VoiceConnection extends EventEmitter { + constructor(channel, client, session, token, server, endpoint) { + super(); + this.id = channel.id; + this.voiceChannel = channel; + this.client = client; + this.session = session; + this.token = token; + this.server = server; + this.endpoint = endpoint.replace(":80", ""); + this.vWS = null; // vWS means voice websocket + this.ready = false; + this.vWSData = {}; + this.encoder = new AudioEncoder(); + this.udp = null; + this.playingIntent = null; + this.playing = false; + this.streamTime = 0; + this.streamProc = null; + this.KAI = null; + this.init(); + } + + destroy() { + this.stopPlaying(); + if(this.KAI) + clearInterval(this.KAI); + this.vWS.close(); + this.udp.close(); + } + + stopPlaying() { + this.playing = false; + this.playingIntent = null; + if(this.instream){ + this.instream.end(); + this.instream.destroy(); + } + } + + playStream(stream) { + + var self = this; + + var startTime = Date.now(); + var sequence = 0; + var time = 0; + var count = 0; + + var length = 20; + + if (self.playingIntent) { + self.stopPlaying(); + } + self.playing = true; + var retStream = new StreamIntent(); + var onWarning = false; + self.playingIntent = retStream; + + function send() { + + if (!self.playingIntent || !self.playing) { + self.setSpeaking(false); + retStream.emit("end"); + self + return; + } + try { + var buffer = stream.read(1920); + if (!buffer) { + setTimeout(send, length * 10); // give chance for some data in 200ms to appear + return; + } + + if (buffer.length !== 1920) { + if (onWarning) { + retStream.emit("end"); + stream.destroy(); + self.setSpeaking(false); + return; + } else { + onWarning = true; + setTimeout(send, length * 10); // give chance for some data in 200ms to appear + return; + } + } + + count++; + sequence + 10 < 65535 ? sequence += 1 : sequence = 0; + time + 9600 < 4294967295 ? time += 960 : time = 0; + + self.sendBuffer(buffer, sequence, time, (e) => { }); + + var nextTime = startTime + (count * length); + + self.streamTime = count * length; + + setTimeout(send, length + (nextTime - Date.now())); + if (!self.playing) + self.setSpeaking(true); + + retStream.emit("time", self.streamTime); + + + } catch (e) { + retStream.emit("error", e); + } + } + self.setSpeaking(true); + send(); + + return retStream; + } + + setSpeaking(value) { + this.playing = value; + if (this.vWS.readyState === WebSocket.OPEN) + this.vWS.send(JSON.stringify({ + op: 5, + d: { + speaking: value, + delay: 0 + } + })); + } + + sendPacket(packet, callback = function (err) { }) { + var self = this; + self.playing = true; + try { + if (self.vWS.readyState === WebSocket.OPEN) + self.udp.send(packet, 0, packet.length, self.vWSData.port, self.endpoint, callback); + } catch (e) { + self.playing = false; + callback(e); + return false; + } + } + + sendBuffer(rawbuffer, sequence, timestamp, callback) { + var self = this; + self.playing = true; + try { + if(!self.encoder.opus){ + self.playing=false; + self.emit("error", "No Opus!"); + self.client.emit("debug", "Tried to use node-opus, but opus not available - install it!"); + return; + } + var buffer = self.encoder.opusBuffer(rawbuffer); + var packet = new VoicePacket(buffer, sequence, timestamp, self.vWSData.ssrc); + return self.sendPacket(packet, callback); + + } catch (e) { + self.playing = false; + self.emit("error", e); + return false; + } + } + + test() { + this.playFile("C:/users/amish/desktop/audio.mp3") + .then(stream => { + stream.on("time", time => { + console.log("Time", time); + }) + }) + } + + playFile(stream, callback = function (err, str) { }) { + var self = this; + return new Promise((resolve, reject) => { + this.encoder + .encodeFile(stream) + .catch(error) + .then(data => { + self.streamProc = data.proc; + var intent = self.playStream(data.stream); + resolve(intent); + callback(null, intent); + + }); + function error(e = true) { + reject(e); + callback(e); + } + }) + } + + playRawStream(stream, callback = function (err, str) { }) { + var self = this; + return new Promise((resolve, reject) => { + this.encoder + .encodeStream(stream) + .catch(error) + .then(data => { + self.streamProc = data.proc; + self.instream = data.instream; + var intent = self.playStream(data.stream); + resolve(intent); + callback(null, intent); + + }); + function error(e = true) { + reject(e); + callback(e); + } + }) + } + + init() { + var self = this; + dns.lookup(this.endpoint, (err, address, family) => { + self.endpoint = address; + var vWS = self.vWS = new WebSocket("wss://" + this.endpoint, null, { rejectUnauthorized: false }); + var udpClient = self.udp = udp.createSocket("udp4"); + + var firstPacket = true; + + var discordIP = "", discordPort = ""; + + udpClient.bind({ exclusive: true }); + udpClient.on('message', function (msg, rinfo) { + var buffArr = JSON.parse(JSON.stringify(msg)).data; + if (firstPacket === true) { + for (var i = 4; i < buffArr.indexOf(0, i); i++) { + discordIP += String.fromCharCode(buffArr[i]); + } + discordPort = msg.readUIntLE(msg.length - 2, 2).toString(10); + + var wsDiscPayload = { + "op": 1, + "d": { + "protocol": "udp", + "data": { + "address": discordIP, + "port": Number(discordPort), + "mode": self.vWSData.modes[0] //Plain + } + } + } + vWS.send(JSON.stringify(wsDiscPayload)); + firstPacket = false; + } + }); + + vWS.on("open", () => { + vWS.send(JSON.stringify({ + op: 0, + d: { + server_id: self.server.id, + user_id: self.client.internal.user.id, + session_id: self.session, + token: self.token + } + })); + }); + + var KAI; + + vWS.on("message", (msg) => { + var data = JSON.parse(msg); + switch (data.op) { + case 2: + self.vWSData = data.d; + + KAI = setInterval(() => { + if (vWS.readyState === WebSocket.OPEN) + vWS.send(JSON.stringify({ + op: 3, + d: null + })); + }, data.d.heartbeat_interval); + self.KAI = KAI; + + var udpPacket = new Buffer(70); + udpPacket.writeUIntBE(data.d.ssrc, 0, 4); + udpClient.send(udpPacket, 0, udpPacket.length, data.d.port, self.endpoint, err => { + if (err) + self.emit("error", err) + }); + break; + case 4: + + self.ready = true; + self.mode = data.d.mode; + self.emit("ready", self); + + break; + } + }); + + }); + } +} + +module.exports = VoiceConnection; \ No newline at end of file diff --git a/src/Voice/VoicePacket.js b/src/Voice/VoicePacket.js new file mode 100644 index 000000000..d5a107e42 --- /dev/null +++ b/src/Voice/VoicePacket.js @@ -0,0 +1,26 @@ +"use strict"; + +class VoicePacket{ + constructor(data, sequence, time, ssrc){ + + var audioBuffer = data, + returnBuffer = new Buffer(audioBuffer.length + 12); + + returnBuffer.fill(0); + returnBuffer[0] = 0x80; + returnBuffer[1] = 0x78; + + returnBuffer.writeUIntBE(sequence, 2, 2); + returnBuffer.writeUIntBE(time, 4, 4); + returnBuffer.writeUIntBE(ssrc, 8, 4); + + for (var i=0; i console.log("[debug]",m)); -a.on("warn", (m) => console.log("[warn]", m)); -var start = Date.now(); -a.on("message", m => { - if(m.content === "$$$"){ - a.internal.setTopic(m.channel, "a channel topic!"); - } -}); -a.on("userTypingStart", (user, chan) => { - console.log(user.username + " typing"); -}); -a.on("userTypingStop", (user, chan) => { - console.log(user.username + " stopped typing"); -}); -a.on("ready", () => { - for(var server of a.internal.servers){ - if(server.name === "craptown"){ - a.leaveServer(server); - } - } -}); - -function error(e){ - throw e; - process.exit(0); -} - - -a.login(process.env["discordEmail"], process.env["discordPass"]).catch((e)=>console.log(e)); \ No newline at end of file + Client : require("./Client/Client"), + Channel : require("./Structures/Channel"), + ChannelPermissions : require("./Structures/ChannelPermissions"), + Invite : require("./Structures/Invite"), + Message : require("./Structures/Message"), + PermissionOverwrite : require("./Structures/PermissionOverwrite"), + PMChannel : require("./Structures/PMChannel"), + Role : require("./Structures/Role"), + Server : require("./Structures/Server"), + ServerChannel : require("./Structures/ServerChannel"), + TextChannel : require("./Structures/TextChannel"), + User : require("./Structures/User"), + VoiceChannel : require("./Structures/VoiceChannel"), +} \ No newline at end of file diff --git a/test/bot.1.js b/test/bot.1.js index cbf0d1afd..afcd544ff 100644 --- a/test/bot.1.js +++ b/test/bot.1.js @@ -1,69 +1,63 @@ var Discord = require("../"); -var Member = require("../lib/Member.js"); -var mybot = new Discord.Client({ - compress : true, - catchup : "all" -}); -var fs = require("fs"); -var request = require("request").defaults({ encoding: null }); +var client = new Discord.Client(); +var request = require("superagent"); +client.on("debug", (m) => console.log("[debug]", m)); +client.on("warn", (m) => console.log("[warn]", m)); +var start = Date.now(); -Discord.patchStrings(); - -var server, channel, message, sentMessage = false; - -mybot.on("message", function (message) { - - console.log("Everyone mentioned? " + doned); - doned++; - if (message.content.substr(0, 3) !== "$$$") { - return; - } - - // we can go ahead :) - - var user; - if (message.mentions.length > 0) { - user = message.mentions[0]; - } else { - user = message.sender; - } - - mybot.reply(message, "Hello! It has been " + ((Date.now() - message.timestamp) - this.timeoffset) + "ms since you sent that."); -}); - -var doned = 0; - -mybot.once("ready", function () { - console.log("im ready"); - - for (var server of mybot.servers) { - if (server.name === "test-server") { - mybot.leaveServer(server); +client.on("message", m => { + if (m.content === "&init") { + for (var channel of m.channel.server.channels) { + if (channel instanceof Discord.VoiceChannel) { + client.reply(m, channel.name + " - " + channel.id); + client.joinVoiceChannel(channel).catch(error); + break; + } } } + if (m.content.startsWith("$$$ stop")) { + if (client.internal.voiceConnection) { + client.internal.voiceConnection.stopPlaying(); + } + return; + } + if (m.content.startsWith("$$$")) { + var chan; + var rest = m.content.split(" "); + rest.splice(0, 1); + rest = rest.join(" "); + if (client.internal.voiceConnection) { + client.reply(m, "ok, I'll play that for you"); + var connection = client.internal.voiceConnection; + connection.playFile("C:/users/amish/desktop/" + rest); + } + } if (m.content.startsWith("pipeit")) { + var chan; + var rest = m.content.split(" "); + rest.splice(0, 1); + rest = rest.join(" "); + if (client.internal.voiceConnection) { + var connection = client.internal.voiceConnection; + + connection.playFile(rest).then(intent => { + client.reply(m, "playing!").then((msg) => { + + intent.on("end", () => { + client.updateMessage(msg, "that song has finished now."); + }); + + }); + + }); + } + } }); -mybot.on("messageUpdate", function(newMessage, oldMessage){ - mybot.reply(newMessage, JSON.stringify(newMessage.embeds)); -}) - -mybot.on("serverUpdate", function (oldserver, newserver) { - console.log("server changed! " + mybot.servers.length); -}) - - -mybot.on("channelUpdate", function (oldChan, newChan) { - -}); - - -function dump(msg) { - console.log("dump", msg); +function error(e) { + console.log(e.stack); + process.exit(0); } -function error(err) { - console.log(err); -} -mybot.login(process.env["ds_email"], process.env["ds_password"]).catch(error); \ No newline at end of file +client.login(process.env["discordEmail"], process.env["discordPass"]).catch((e) => console.log(e)); \ No newline at end of file