diff --git a/NetfluxWebsocketSrv.js b/NetfluxWebsocketSrv.js index 0c78d4d8c..d2f5f0fb9 100644 --- a/NetfluxWebsocketSrv.js +++ b/NetfluxWebsocketSrv.js @@ -101,9 +101,8 @@ const handleMessage = function (ctx, user, msg) { if (cmd === 'MSG') { if (obj === HISTORY_KEEPER_ID) { let parsed; - try { parsed = JSON.parse(json[2]); } catch (err) { return; } + try { parsed = JSON.parse(json[2]); } catch (err) { console.error(err); return; } if (parsed[0] === 'GET_HISTORY') { - console.log('getHistory ' + parsed[1]); getHistory(ctx, parsed[1], function (msg) { sendMsg(ctx, user, [0, HISTORY_KEEPER_ID, 'MSG', user.id, JSON.stringify(msg)]); }); @@ -168,6 +167,7 @@ let run = module.exports.run = function (storage, socketServer) { }); }, 5000); socketServer.on('connection', function(socket) { + if(socket.upgradeReq.url !== '/cryptpad_websocket') { return; } let conn = socket.upgradeReq.connection; let user = { addr: conn.remoteAddress + '|' + conn.remotePort, diff --git a/WebRTCSrv.js b/WebRTCSrv.js index 172d9f756..2c1bc81e2 100644 --- a/WebRTCSrv.js +++ b/WebRTCSrv.js @@ -10,6 +10,7 @@ var run = module.exports.run = function(server) { socket.on('message', (data) => { try { let msg = JSON.parse(data) + console.log(msg) if (msg.hasOwnProperty('key')) { for (let master of server.clients) { if (master.key === msg.key) { diff --git a/customize.dist/index.html b/customize.dist/index.html index 60cefe347..cdf6e9934 100644 --- a/customize.dist/index.html +++ b/customize.dist/index.html @@ -127,7 +127,7 @@ document.getElementById('buttons').setAttribute('style', ''); document.getElementById('create-pad').setAttribute('href', '/pad/'); if(Config.webrtcURL !== '') { - document.getElementById('create-rtcpad').setAttribute('href', '/pad/?webrtc=1'); + document.getElementById('create-rtcpad').setAttribute('href', '/padrtc/'); } document.getElementById('create-sheet').setAttribute('href', '/sheet/#' + Crypto.genKey()); document.getElementById('create-code').setAttribute('href', '/code/#' + Crypto.genKey()); diff --git a/www/common/es6-promise.min.js b/www/common/es6-promise.min.js new file mode 100644 index 000000000..f26f3c8ce --- /dev/null +++ b/www/common/es6-promise.min.js @@ -0,0 +1,9 @@ +/*! + * @overview es6-promise - a tiny implementation of Promises/A+. + * @copyright Copyright (c) 2014 Yehuda Katz, Tom Dale, Stefan Penner and contributors (Conversion to ES6 API by Jake Archibald) + * @license Licensed under MIT license + * See https://raw.githubusercontent.com/jakearchibald/es6-promise/master/LICENSE + * @version 3.2.1 + */ + +(function(){"use strict";function t(t){return"function"==typeof t||"object"==typeof t&&null!==t}function e(t){return"function"==typeof t}function n(t){G=t}function r(t){Q=t}function o(){return function(){process.nextTick(a)}}function i(){return function(){B(a)}}function s(){var t=0,e=new X(a),n=document.createTextNode("");return e.observe(n,{characterData:!0}),function(){n.data=t=++t%2}}function u(){var t=new MessageChannel;return t.port1.onmessage=a,function(){t.port2.postMessage(0)}}function c(){return function(){setTimeout(a,1)}}function a(){for(var t=0;J>t;t+=2){var e=tt[t],n=tt[t+1];e(n),tt[t]=void 0,tt[t+1]=void 0}J=0}function f(){try{var t=require,e=t("vertx");return B=e.runOnLoop||e.runOnContext,i()}catch(n){return c()}}function l(t,e){var n=this,r=new this.constructor(p);void 0===r[rt]&&k(r);var o=n._state;if(o){var i=arguments[o-1];Q(function(){x(o,r,i,n._result)})}else E(n,r,t,e);return r}function h(t){var e=this;if(t&&"object"==typeof t&&t.constructor===e)return t;var n=new e(p);return g(n,t),n}function p(){}function _(){return new TypeError("You cannot resolve a promise with itself")}function d(){return new TypeError("A promises callback cannot return that same promise.")}function v(t){try{return t.then}catch(e){return ut.error=e,ut}}function y(t,e,n,r){try{t.call(e,n,r)}catch(o){return o}}function m(t,e,n){Q(function(t){var r=!1,o=y(n,e,function(n){r||(r=!0,e!==n?g(t,n):S(t,n))},function(e){r||(r=!0,j(t,e))},"Settle: "+(t._label||" unknown promise"));!r&&o&&(r=!0,j(t,o))},t)}function b(t,e){e._state===it?S(t,e._result):e._state===st?j(t,e._result):E(e,void 0,function(e){g(t,e)},function(e){j(t,e)})}function w(t,n,r){n.constructor===t.constructor&&r===et&&constructor.resolve===nt?b(t,n):r===ut?j(t,ut.error):void 0===r?S(t,n):e(r)?m(t,n,r):S(t,n)}function g(e,n){e===n?j(e,_()):t(n)?w(e,n,v(n)):S(e,n)}function A(t){t._onerror&&t._onerror(t._result),T(t)}function S(t,e){t._state===ot&&(t._result=e,t._state=it,0!==t._subscribers.length&&Q(T,t))}function j(t,e){t._state===ot&&(t._state=st,t._result=e,Q(A,t))}function E(t,e,n,r){var o=t._subscribers,i=o.length;t._onerror=null,o[i]=e,o[i+it]=n,o[i+st]=r,0===i&&t._state&&Q(T,t)}function T(t){var e=t._subscribers,n=t._state;if(0!==e.length){for(var r,o,i=t._result,s=0;si;i++)e.resolve(t[i]).then(n,r)}:function(t,e){e(new TypeError("You must pass an array to race."))})}function F(t){var e=this,n=new e(p);return j(n,t),n}function D(){throw new TypeError("You must pass a resolver function as the first argument to the promise constructor")}function K(){throw new TypeError("Failed to construct 'Promise': Please use the 'new' operator, this object constructor cannot be called as a function.")}function L(t){this[rt]=O(),this._result=this._state=void 0,this._subscribers=[],p!==t&&("function"!=typeof t&&D(),this instanceof L?C(this,t):K())}function N(t,e){this._instanceConstructor=t,this.promise=new t(p),this.promise[rt]||k(this.promise),Array.isArray(e)?(this._input=e,this.length=e.length,this._remaining=e.length,this._result=new Array(this.length),0===this.length?S(this.promise,this._result):(this.length=this.length||0,this._enumerate(),0===this._remaining&&S(this.promise,this._result))):j(this.promise,U())}function U(){return new Error("Array Methods must be provided an Array")}function W(){var t;if("undefined"!=typeof global)t=global;else if("undefined"!=typeof self)t=self;else try{t=Function("return this")()}catch(e){throw new Error("polyfill failed because global object is unavailable in this environment")}var n=t.Promise;(!n||"[object Promise]"!==Object.prototype.toString.call(n.resolve())||n.cast)&&(t.Promise=pt)}var z;z=Array.isArray?Array.isArray:function(t){return"[object Array]"===Object.prototype.toString.call(t)};var B,G,H,I=z,J=0,Q=function(t,e){tt[J]=t,tt[J+1]=e,J+=2,2===J&&(G?G(a):H())},R="undefined"!=typeof window?window:void 0,V=R||{},X=V.MutationObserver||V.WebKitMutationObserver,Z="undefined"==typeof self&&"undefined"!=typeof process&&"[object process]"==={}.toString.call(process),$="undefined"!=typeof Uint8ClampedArray&&"undefined"!=typeof importScripts&&"undefined"!=typeof MessageChannel,tt=new Array(1e3);H=Z?o():X?s():$?u():void 0===R&&"function"==typeof require?f():c();var et=l,nt=h,rt=Math.random().toString(36).substring(16),ot=void 0,it=1,st=2,ut=new M,ct=new M,at=0,ft=Y,lt=q,ht=F,pt=L;L.all=ft,L.race=lt,L.resolve=nt,L.reject=ht,L._setScheduler=n,L._setAsap=r,L._asap=Q,L.prototype={constructor:L,then:et,"catch":function(t){return this.then(null,t)}};var _t=N;N.prototype._enumerate=function(){for(var t=this.length,e=this._input,n=0;this._state===ot&&t>n;n++)this._eachEntry(e[n],n)},N.prototype._eachEntry=function(t,e){var n=this._instanceConstructor,r=n.resolve;if(r===nt){var o=v(t);if(o===et&&t._state!==ot)this._settledAt(t._state,e,t._result);else if("function"!=typeof o)this._remaining--,this._result[e]=t;else if(n===pt){var i=new n(p);w(i,t,o),this._willSettleAt(i,e)}else this._willSettleAt(new n(function(e){e(t)}),e)}else this._willSettleAt(r(t),e)},N.prototype._settledAt=function(t,e,n){var r=this.promise;r._state===ot&&(this._remaining--,t===st?j(r,n):this._result[e]=n),0===this._remaining&&S(r,this._result)},N.prototype._willSettleAt=function(t,e){var n=this;E(t,void 0,function(t){n._settledAt(it,e,t)},function(t){n._settledAt(st,e,t)})};var dt=W,vt={Promise:pt,polyfill:dt};"function"==typeof define&&define.amd?define(function(){return vt}):"undefined"!=typeof module&&module.exports?module.exports=vt:"undefined"!=typeof this&&(this.ES6Promise=vt),dt()}).call(this); \ No newline at end of file diff --git a/www/common/netflux-client.js b/www/common/netflux-client.js new file mode 100644 index 000000000..38c6bf44b --- /dev/null +++ b/www/common/netflux-client.js @@ -0,0 +1,225 @@ +/*global: WebSocket */ +define(() => { +'use strict'; +const MAX_LAG_BEFORE_PING = 15000; +const MAX_LAG_BEFORE_DISCONNECT = 30000; +const PING_CYCLE = 5000; +const REQUEST_TIMEOUT = 5000; + +const now = () => new Date().getTime(); + +const networkSendTo = (ctx, peerId, content) => { + const seq = ctx.seq++; + ctx.ws.send(JSON.stringify([seq, 'MSG', peerId, content])); + return new Promise((res, rej) => { + ctx.requests[seq] = { reject: rej, resolve: res, time: now() }; + }); +}; + +const channelBcast = (ctx, chanId, content) => { + const chan = ctx.channels[chanId]; + if (!chan) { throw new Error("no such channel " + chanId); } + const seq = ctx.seq++; + ctx.ws.send(JSON.stringify([seq, 'MSG', chanId, content])); + return new Promise((res, rej) => { + ctx.requests[seq] = { reject: rej, resolve: res, time: now() }; + }); +}; + +const channelLeave = (ctx, chanId, reason) => { + const chan = ctx.channels[chanId]; + if (!chan) { throw new Error("no such channel " + chanId); } + delete ctx.channels[chanId]; + ctx.ws.send(JSON.stringify([ctx.seq++, 'LEAVE', chanId, reason])); +}; + +const makeEventHandlers = (ctx, mappings) => { + return (name, handler) => { + const handlers = mappings[name]; + if (!handlers) { throw new Error("no such event " + name); } + handlers.push(handler); + }; +}; + +const mkChannel = (ctx, id) => { + const internal = { + onMessage: [], + onJoin: [], + onLeave: [], + members: [], + jSeq: ctx.seq++ + }; + const chan = { + _: internal, + id: id, + members: internal.members, + bcast: (msg) => channelBcast(ctx, chan.id, msg), + leave: (reason) => channelLeave(ctx, chan.id, reason), + on: makeEventHandlers(ctx, { message: + internal.onMessage, join: internal.onJoin, leave: internal.onLeave }) + }; + ctx.requests[internal.jSeq] = chan; + ctx.ws.send(JSON.stringify([internal.jSeq, 'JOIN', id])); + + return new Promise((res, rej) => { + chan._.resolve = res; + chan._.reject = rej; + }) +}; + +const mkNetwork = (ctx) => { + const network = { + webChannels: ctx.channels, + getLag: () => (ctx.lag), + sendto: (peerId, content) => (networkSendTo(ctx, peerId, content)), + join: (chanId) => (mkChannel(ctx, chanId)), + on: makeEventHandlers(ctx, { message: ctx.onMessage, disconnect: ctx.onDisconnect }) + }; + network.__defineGetter__("webChannels", () => { + return Object.keys(ctx.channels).map((k) => (ctx.channels[k])); + }); + return network; +}; + +const onMessage = (ctx, evt) => { + let msg; + try { msg = JSON.parse(evt.data); } catch (e) { console.log(e.stack); return; } + if (msg[0] !== 0) { + const req = ctx.requests[msg[0]]; + if (!req) { + console.log("error: " + JSON.stringify(msg)); + return; + } + delete ctx.requests[msg[0]]; + if (msg[1] === 'ACK') { + if (req.ping) { // ACK of a PING + ctx.lag = now() - Number(req.ping); + return; + } + req.resolve(); + } else if (msg[1] === 'JACK') { + if (req._) { + // Channel join request... + if (!msg[2]) { throw new Error("wrong type of ACK for channel join"); } + req.id = msg[2]; + ctx.channels[req.id] = req; + return; + } + req.resolve(); + } else if (msg[1] === 'ERROR') { + req.reject({ type: msg[2], message: msg[3] }); + } else { + req.reject({ type: 'UNKNOWN', message: JSON.stringify(msg) }); + } + return; + } + if (msg[2] === 'IDENT') { + ctx.uid = msg[3]; + + setInterval(() => { + if (now() - ctx.timeOfLastMessage < MAX_LAG_BEFORE_PING) { return; } + let seq = ctx.seq++; + let currentDate = now(); + ctx.requests[seq] = {time: now(), ping: currentDate}; + ctx.ws.send(JSON.stringify([seq, 'PING', currentDate])); + if (now() - ctx.timeOfLastMessage > MAX_LAG_BEFORE_DISCONNECT) { + ctx.ws.close(); + } + }, PING_CYCLE); + + return; + } else if (!ctx.uid) { + // extranious message, waiting for an ident. + return; + } + if (msg[2] === 'PING') { + msg[1] = 'PONG'; + ctx.ws.send(JSON.stringify(msg)); + return; + } + + if (msg[2] === 'MSG') { + let handlers; + if (msg[3] === ctx.uid) { + handlers = ctx.onMessage; + } else { + const chan = ctx.channels[msg[3]]; + if (!chan) { + console.log("message to non-existant chan " + JSON.stringify(msg)); + return; + } + handlers = chan._.onMessage; + } + handlers.forEach((h) => { + try { h(msg[4], msg[1]); } catch (e) { console.log(e.stack); } + }); + } + + if (msg[2] === 'LEAVE') { + const chan = ctx.channels[msg[3]]; + if (!chan) { + console.log("leaving non-existant chan " + JSON.stringify(msg)); + return; + } + chan._.onLeave.forEach((h) => { + try { h(msg[1], msg[4]); } catch (e) { console.log(e.stack); } + }); + } + + if (msg[2] === 'JOIN') { + const chan = ctx.channels[msg[3]]; + if (!chan) { + console.log("ERROR: join to non-existant chan " + JSON.stringify(msg)); + return; + } + // have we yet fully joined the chan? + const synced = (chan._.members.indexOf(ctx.uid) !== -1); + chan._.members.push(msg[1]); + if (!synced && msg[1] === ctx.uid) { + // sync the channel join event + chan.myID = ctx.uid; + chan._.resolve(chan); + } + if (synced) { + chan._.onJoin.forEach((h) => { + try { h(msg[1]); } catch (e) { console.log(e.stack); } + }); + } + } +}; + +const connect = (websocketURL) => { + let ctx = { + ws: new WebSocket(websocketURL), + seq: 1, + lag: 0, + uid: null, + network: null, + channels: {}, + onMessage: [], + onDisconnect: [], + requests: {} + }; + setInterval(() => { + for (let id in ctx.requests) { + const req = ctx.requests[id]; + if (now() - req.time > REQUEST_TIMEOUT) { + delete ctx.requests[id]; + req.reject({ type: 'TIMEOUT', message: 'waited ' + now() - req.time + 'ms' }); + } + } + }, 5000); + ctx.network = mkNetwork(ctx); + ctx.ws.onmessage = (msg) => (onMessage(ctx, msg)); + ctx.ws.onclose = (evt) => { + ctx.onDisconnect.forEach((h) => { + try { h(evt.reason); } catch (e) { console.log(e.stack); } + }); + }; + return new Promise((resolve, reject) => { + ctx.ws.onopen = () => resolve(ctx.network); + }); +}; + +return { connect: connect }; +}); diff --git a/www/common/netflux.js b/www/common/netflux.js index 5a59d7876..d01302980 100644 --- a/www/common/netflux.js +++ b/www/common/netflux.js @@ -1342,6 +1342,7 @@ return /******/ (function(modules) { // webpackBootstrap if (msg[0] !== 0 && msg[1] !== 'ACK') { return; } + if (msg[2] === 'IDENT' && msg[1] === '') { socket.uid = msg[3]; webChannel.myID = msg[3]; @@ -1401,7 +1402,7 @@ return /******/ (function(modules) { // webpackBootstrap // Trigger onJoining() when another user is joining the channel // Register the user in the list of peers in the channel if (webChannel.peers.length === 0 && msg[1].length === 16) { - // We've just catched the history keeper + // We've just catched the history keeper (16 characters length name) history_keeper = msg[1]; webChannel.hc = history_keeper; } diff --git a/www/common/realtime-input.js b/www/common/realtime-input.js index c646d8054..908d20110 100644 --- a/www/common/realtime-input.js +++ b/www/common/realtime-input.js @@ -17,10 +17,11 @@ window.Reflect = { has: (x,y) => { return (y in x); } }; define([ '/common/messages.js', - '/common/netflux.js', + '/common/netflux-client.js', '/common/crypto.js', '/common/toolbar.js', '/_socket/text-patcher.js', + '/common/es6-promise.min.js', '/common/chainpad.js', '/bower_components/jquery/dist/jquery.min.js', ], function (Messages, Netflux, Crypto, Toolbar, TextPatcher) { @@ -75,7 +76,6 @@ define([ function (config) { var websocketUrl = config.websocketURL; - var webrtcUrl = config.webrtcURL; var userName = config.userName; var channel = config.channel; var chanKey = config.cryptKey; @@ -122,25 +122,20 @@ define([ content.length + ':' + content; }; - var onPeerMessage = function(toId, type, wc) { - if(type === 6) { - messagesHistory.forEach(function(msg) { - wc.sendTo(toId, '1:y'+msg); - }); - wc.sendTo(toId, '0'); - } - }; - var whoami = new RegExp(userName.replace(/[\/\+]/g, function (c) { return '\\' +c; })); - var onMessage = function(peer, msg, wc) { + var onMessage = function(peer, msg, wc, network) { - if(msg === 0 || msg === '0') { - onReady(wc); + var hc = (wc && wc.history_keeper) ? wc.history_keeper : null; + if(wc && (msg === 0 || msg === '0')) { + onReady(wc, network); return; } + else if (peer === hc){ + msg = JSON.parse(msg)[4]; + } var message = chainpadAdapter.msgIn(peer, msg); verbose(message); @@ -176,8 +171,10 @@ define([ users: [] }; var onJoining = function(peer) { + if(peer.length !== 32) { return; } var list = userList.users; - if(list.indexOf(peer) === -1) { + var index = list.indexOf(peer); + if(index === -1) { userList.users.push(peer); } userList.onChange(); @@ -216,7 +213,7 @@ define([ if(parsed.content[0] === 4) { // PING message from Chainpad parsed.content[0] = 5; onMessage('', '1:y'+mkMessage(parsed.user, parsed.channelId, parsed.content)); - wc.sendPing(); + // wc.sendPing(); return; } return Crypto.encrypt(msg, cryptKey); @@ -227,20 +224,6 @@ define([ key: '' }; - var rtc = true; - - if(!getParameterByName("webrtc") || !webrtcUrl) { - rtc = false; - options.signaling = websocketUrl; - options.topology = 'StarTopologyService'; - options.protocol = 'WebSocketProtocolService'; - options.connector = 'WebSocketService'; - options.openWebChannel = true; - } - else { - options.signaling = webrtcUrl; - } - var createRealtime = function(chan) { return ChainPad.create(userName, passwd, @@ -251,12 +234,12 @@ define([ }); }; - var onReady = function(wc) { + var onReady = function(wc, network) { if(config.onInit) { config.onInit({ myID: wc.myID, realtime: realtime, - webChannel: wc, + getLag: network.getLag, userList: userList }); } @@ -274,18 +257,21 @@ define([ } } - var onOpen = function(wc) { + var onOpen = function(wc, network) { channel = wc.id; window.location.hash = channel + '|' + chanKey; + + // Add the existing peers in the userList + wc.members.forEach(onJoining); + // Add the handlers to the WebChannel - wc.onmessage = function(peer, msg) { // On receiving message - onMessage(peer, msg, wc); - }; - wc.onJoining = onJoining; // On user joining the session - wc.onLeaving = onLeaving; // On user leaving the session - wc.onPeerMessage = function(peerId, type) { - onPeerMessage(peerId, type, wc); - }; + wc.on('message', function (msg, sender) { //Channel msg + onMessage(sender, msg, wc, network); + }); + wc.on('join', onJoining); + wc.on('leave', onLeaving); + + if(config.setMyID) { config.setMyID({ myID: wc.myID @@ -299,7 +285,7 @@ define([ // Filter messages sent by Chainpad to make it compatible with Netflux message = chainpadAdapter.msgOut(message, wc); if(message) { - wc.send(message).then(function() { + wc.bcast(message).then(function() { // Send the message back to Chainpad once it is sent to the recipients. onMessage(wc.myID, message); }, function(err) { @@ -311,17 +297,11 @@ define([ // Get the channel history var hc; - if(rtc) { - wc.channels.forEach(function (c) { if(!hc) { hc = c; } }); - if(hc) { - wc.getHistory(hc.peerID); - } - } - else { - // TODO : Improve WebSocket service to use the latest Netflux's API - wc.peers.forEach(function (p) { if (!hc || p.linkQuality > hc.linkQuality) { hc = p; } }); - hc.send(JSON.stringify(['GET_HISTORY', wc.id])); - } + wc.members.forEach(function (p) { + if (p.length === 16) { hc = p; } + }); + wc.history_keeper = hc; + if (hc) { network.sendto(hc, JSON.stringify(['GET_HISTORY', wc.id])); } toReturn.patchText = TextPatcher.create({ @@ -331,58 +311,30 @@ define([ realtime.start(); }; - var createRTCChannel = function () { - // Check if the WebRTC channel exists and create it if necessary - var webchannel = Netflux.create(); - webchannel.openForJoining(options).then(function(data) { - onOpen(webchannel); - onReady(webchannel); - }, function(error) { - warn(error); - }); - }; + var findChannelById = function(webChannels, channelId) { + var webChannel; + webChannels.forEach(function(chan) { + if(chan.id == channelId) { webChannel = chan; return;} + }); + return webChannel; + } - var joinChannel = function() { - // Connect to the WebSocket/WebRTC channel - Netflux.join(channel, options).then(function(wc) { - onOpen(wc); - }, function(error) { - if(rtc && error.code === 1008) {// Unexisting RTC channel - createRTCChannel(); + // Connect to the WebSocket channel + Netflux.connect(websocketUrl).then(function(network) { + network.on('message', function (msg, sender) { // Direct message + var wchan = findChannelById(network.webChannels, channel); + if(wchan) { + onMessage(sender, msg, wchan, network); } - else { warn(error); } }); - }; - joinChannel(); - - var checkConnection = function(wc) { - if(wc.channels && wc.channels.size > 0) { - var channels = Array.from(wc.channels); - var channel = channels[0]; - - var socketChecker = setInterval(function () { - if (channel.checkSocket(realtime)) { - warn("Socket disconnected!"); - - recoverableErrorCount += 1; - - if (recoverableErrorCount >= MAX_RECOVERABLE_ERRORS) { - warn("Giving up!"); - realtime.abort(); - try { channel.close(); } catch (e) { warn(e); } - if (config.onAbort) { - config.onAbort({ - socket: channel - }); - } - if (socketChecker) { clearInterval(socketChecker); } - } - } else { - // it's working as expected, continue - } - }, 200); - } - }; + network.join(channel || null).then(function(wc) { + onOpen(wc, network); + }, function(error) { + console.error(error); + }) + }, function(error) { + warn(error); + }); return toReturn; }; diff --git a/www/common/toolbar.js b/www/common/toolbar.js index c4692bcfe..738d20c57 100644 --- a/www/common/toolbar.js +++ b/www/common/toolbar.js @@ -132,7 +132,7 @@ define([ userList.forEach(function(user) { if(user !== myUserName) { var data = (userData) ? (userData[user] || null) : null; - var userName = (data) ? data.name : null; + var userName = (data) ? data.name : user; if(userName) { if(i === 0) list = ' : '; list += userName + ', '; @@ -170,9 +170,9 @@ define([ return $container.find('#'+id)[0]; }; - var checkLag = function (webChannel, lagElement) { - if(typeof webChannel.getLag !== "function") { return; } - var lag = webChannel.getLag(); + var checkLag = function (getLag, lagElement) { + if(typeof getLag !== "function") { return; } + var lag = getLag(); var lagMsg = Messages.lag + ' '; if(lag) { var lagSec = lag/1000; @@ -214,7 +214,7 @@ define([ localStorage['CryptPad_RECENTPADS'] = JSON.stringify(out); }; - var create = function ($container, myUserName, realtime, webChannel, userList, config) { + var create = function ($container, myUserName, realtime, getLag, userList, config) { var toolbar = createRealtimeToolbar($container); createEscape(toolbar.find('.rtwysiwyg-toolbar-leftside')); var userListElement = createUserList(toolbar.find('.rtwysiwyg-toolbar-leftside')); @@ -223,7 +223,7 @@ define([ var userData = config.userData; var changeNameID = config.changeNameID; - // Check if the suer is allowed to change his name + // Check if the user is allowed to change his name if(changeNameID) { // Create the button and update the element containing the user list userListElement = createChangeName($container, userListElement, changeNameID); @@ -253,7 +253,7 @@ define([ setInterval(function () { if (!connected) { return; } - checkLag(webChannel, lagElement); + checkLag(getLag, lagElement); }, 3000); return { diff --git a/www/pad/main.js b/www/pad/main.js index 0ec1877b7..b5f43e722 100644 --- a/www/pad/main.js +++ b/www/pad/main.js @@ -91,53 +91,25 @@ define([ var diffOptions = { preDiffApply: function (info) { - /* DiffDOM will filter out magicline plugin elements - in practice this will make it impossible to use it - while someone else is typing, which could be annoying. - - we should check when such an element is going to be - removed, and prevent that from happening. */ + /* Don't remove local instances of the magicline plugin */ if (info.node && info.node.tagName === 'SPAN' && - info.node.contentEditable === false) { - // it seems to be a magicline plugin element... - if (info.diff.action === 'removeElement') { - // and you're about to remove it... - // this probably isn't what you want - - /* - I have never seen this in the console, but the - magic line is still getting removed on remote - edits. This suggests that it's getting removed - by something other than diffDom. - */ - console.log("preventing removal of the magic line!"); - - // return true to prevent diff application - return true; - } + info.node.getAttribute('contentEditable') === 'false') { + return true; } - // no use trying to recover the cursor if it doesn't exist if (!cursor.exists()) { return; } - - /* frame is either 0, 1, 2, or 3, depending on which - cursor frames were affected: none, first, last, or both - */ var frame = info.frame = cursor.inNode(info.node); - if (!frame) { return; } - - if (typeof info.diff.oldValue === 'string' && typeof info.diff.newValue === 'string') { - var pushes = cursor.pushDelta(info.diff.oldValue, info.diff.newValue); - + if (typeof info.diff.oldValue === 'string' && + typeof info.diff.newValue === 'string') { + var pushes = cursor.pushDelta(info.diff.oldValue, + info.diff.newValue); if (frame & 1) { - // push cursor start if necessary if (pushes.commonStart < cursor.Range.start.offset) { cursor.Range.start.offset += pushes.delta; } } if (frame & 2) { - // push cursor end if necessary if (pushes.commonStart < cursor.Range.end.offset) { cursor.Range.end.offset += pushes.delta; } @@ -149,7 +121,7 @@ define([ if (info.node) { if (info.frame & 1) { cursor.fixStart(info.node); } if (info.frame & 2) { cursor.fixEnd(info.node); } - } else { console.error("info.node did not exist"); } + } else { console.log("info.node did not exist"); } var sel = cursor.makeSelection(); var range = cursor.makeRange(); @@ -220,9 +192,8 @@ define([ // provide initialstate... initialState: JSON.stringify(Hyperjson.fromDOM(inner, isNotMagicLine)), - // the websocket URL (deprecated?) + // the websocket URL websocketURL: Config.websocketURL, - webrtcURL: Config.webrtcURL, // our username userName: userName, @@ -286,7 +257,7 @@ define([ userData: userList, changeNameID: 'cryptpad-changeName' }; - toolbar = info.realtime.toolbar = Toolbar.create($bar, info.myID, info.realtime, info.webChannel, info.userList, config); + toolbar = info.realtime.toolbar = Toolbar.create($bar, info.myID, info.realtime, info.getLag, info.userList, config); createChangeName('cryptpad-changeName', $bar); /* TODO handle disconnects and such*/ }; @@ -357,14 +328,6 @@ define([ inner.addEventListener('keydown', cursor.brFix); editor.on('change', propogate); - // editor.on('change', function () { - // var hjson = Convert.core.hyperjson.fromDOM(inner); - // if(myData !== {}) { - // hjson[hjson.length] = {metadata: userList}; - // } - // $textarea.val(JSON.stringify(hjson)); - // rti.bumpSharejs(); - // }); }); }; diff --git a/www/padrtc/index.html b/www/padrtc/index.html new file mode 100644 index 000000000..fccdfff02 --- /dev/null +++ b/www/padrtc/index.html @@ -0,0 +1,79 @@ + + + + + + + + + +
+ + + + + diff --git a/www/padrtc/inner.html b/www/padrtc/inner.html new file mode 100644 index 000000000..bf79dcd0d --- /dev/null +++ b/www/padrtc/inner.html @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/www/padrtc/main.js b/www/padrtc/main.js new file mode 100644 index 000000000..ba983e799 --- /dev/null +++ b/www/padrtc/main.js @@ -0,0 +1,356 @@ +define([ + '/api/config?cb=' + Math.random().toString(16).substring(2), + '/common/messages.js', + '/common/crypto.js', + '/padrtc/realtime-input.js', + '/common/hyperjson.js', + '/common/hyperscript.js', + '/common/toolbar.js', + '/common/cursor.js', + '/common/json-ot.js', + '/bower_components/diff-dom/diffDOM.js', + '/bower_components/jquery/dist/jquery.min.js', + '/customize/pad.js' +], function (Config, Messages, Crypto, realtimeInput, Hyperjson, Hyperscript, Toolbar, Cursor, JsonOT) { + var $ = window.jQuery; + var ifrw = $('#pad-iframe')[0].contentWindow; + var Ckeditor; // to be initialized later... + var DiffDom = window.diffDOM; + + window.Toolbar = Toolbar; + window.Hyperjson = Hyperjson; + + var hjsonToDom = function (H) { + return Hyperjson.callOn(H, Hyperscript); + }; + + var module = window.REALTIME_MODULE = { + localChangeInProgress: 0 + }; + + var userName = Crypto.rand64(8), + toolbar; + + var isNotMagicLine = function (el) { + // factor as: + // return !(el.tagName === 'SPAN' && el.contentEditable === 'false'); + var filter = (el.tagName === 'SPAN' && el.contentEditable === 'false'); + if (filter) { + console.log("[hyperjson.serializer] prevented an element" + + "from being serialized:", el); + return false; + } + return true; + }; + + var andThen = function (Ckeditor) { + // $(window).on('hashchange', function() { + // window.location.reload(); + // }); + var key; + var channel = ''; + if (window.location.href.indexOf('#') === -1) { + key = Crypto.genKey(); + // window.location.href = window.location.href + '#' + Crypto.genKey(); + // return; + } + else { + var hash = window.location.hash.substring(1); + var sep = hash.indexOf('|'); + channel = hash.substr(0,sep); + key = hash.substr(sep+1); + } + + var fixThings = false; + // var key = Crypto.parseKey(window.location.hash.substring(1)); + var editor = window.editor = Ckeditor.replace('editor1', { + // https://dev.ckeditor.com/ticket/10907 + needsBrFiller: fixThings, + needsNbspFiller: fixThings, + removeButtons: 'Source,Maximize', + // magicline plugin inserts html crap into the document which is not part of the + // document itself and causes problems when it's sent across the wire and reflected back + removePlugins: 'resize' + }); + + editor.on('instanceReady', function (Ckeditor) { + editor.execCommand('maximize'); + var documentBody = ifrw.$('iframe')[0].contentDocument.body; + + documentBody.innerHTML = Messages.initialState; + + var inner = window.inner = documentBody; + var cursor = window.cursor = Cursor(inner); + + var setEditable = function (bool) { + inner.setAttribute('contenteditable', + (typeof (bool) !== 'undefined'? bool : true)); + }; + + // don't let the user edit until the pad is ready + setEditable(false); + + var diffOptions = { + preDiffApply: function (info) { + /* Don't remove local instances of the magicline plugin */ + if (info.node && info.node.tagName === 'SPAN' && + info.node.getAttribute('contentEditable') === 'false') { + return true; + } + + if (!cursor.exists()) { return; } + var frame = info.frame = cursor.inNode(info.node); + if (!frame) { return; } + if (typeof info.diff.oldValue === 'string' && + typeof info.diff.newValue === 'string') { + var pushes = cursor.pushDelta(info.diff.oldValue, + info.diff.newValue); + if (frame & 1) { + if (pushes.commonStart < cursor.Range.start.offset) { + cursor.Range.start.offset += pushes.delta; + } + } + if (frame & 2) { + if (pushes.commonStart < cursor.Range.end.offset) { + cursor.Range.end.offset += pushes.delta; + } + } + } + }, + postDiffApply: function (info) { + if (info.frame) { + if (info.node) { + if (info.frame & 1) { cursor.fixStart(info.node); } + if (info.frame & 2) { cursor.fixEnd(info.node); } + } else { console.log("info.node did not exist"); } + + var sel = cursor.makeSelection(); + var range = cursor.makeRange(); + + cursor.fixSelection(sel, range); + } + } + }; + + var now = function () { return new Date().getTime(); }; + + var initializing = true; + var userList = {}; // List of pretty name of all users (mapped with their server ID) + var toolbarList; // List of users still connected to the channel (server IDs) + var addToUserList = function(data) { + for (var attrname in data) { userList[attrname] = data[attrname]; } + if(toolbarList && typeof toolbarList.onChange === "function") { + toolbarList.onChange(userList); + } + }; + + var myData = {}; + var myUserName = ''; // My "pretty name" + var myID; // My server ID + + var setMyID = function(info) { + myID = info.myID || null; + myUserName = myID; + }; + + var createChangeName = function(id, $container) { + var buttonElmt = $container.find('#'+id)[0]; + buttonElmt.addEventListener("click", function() { + var newName = prompt("Change your name :", myUserName) + if (newName && newName.trim()) { + var myUserNameTemp = newName.trim(); + if(newName.trim().length > 32) { + myUserNameTemp = myUserNameTemp.substr(0, 32); + } + myUserName = myUserNameTemp; + myData[myID] = { + name: myUserName + }; + addToUserList(myData); + editor.fire( 'change' ); + } + }); + }; + + var DD = new DiffDom(diffOptions); + + // apply patches, and try not to lose the cursor in the process! + var applyHjson = function (shjson) { + // var hjson = JSON.parse(shjson); + // var peerUserList = hjson[hjson.length-1]; + // if(peerUserList.metadata) { + // var userData = peerUserList.metadata; + // addToUserList(userData); + // delete hjson[hjson.length-1]; + // } + var userDocStateDom = hjsonToDom(JSON.parse(shjson)); + userDocStateDom.setAttribute("contenteditable", "true"); // lol wtf + var patch = (DD).diff(inner, userDocStateDom); + (DD).apply(inner, patch); + }; + + var realtimeOptions = { + // provide initialstate... + initialState: JSON.stringify(Hyperjson.fromDOM(inner, isNotMagicLine)), + + // the websocket URL (deprecated?) + websocketURL: Config.websocketURL, + webrtcURL: Config.webrtcURL, + + // our username + userName: userName, + + // the channel we will communicate over + channel: channel, + + // our encryption key + cryptKey: key, + + // configuration :D + doc: inner, + + setMyID: setMyID, + + // really basic operational transform + transformFunction : JsonOT.validate + // pass in websocket/netflux object TODO + }; + + var onRemote = realtimeOptions.onRemote = function (info) { + if (initializing) { return; } + + var shjson = info.realtime.getUserDoc(); + + // remember where the cursor is + cursor.update(); + + // Extract the user list (metadata) from the hyperjson + var hjson = JSON.parse(shjson); + var peerUserList = hjson[hjson.length-1]; + if(peerUserList.metadata) { + var userData = peerUserList.metadata; + // Update the local user data + userList = userData; + // Send the new data to the toolbar + if(toolbarList && typeof toolbarList.onChange === "function") { + toolbarList.onChange(userList); + } + hjson.pop(); + } + + // build a dom from HJSON, diff, and patch the editor + applyHjson(shjson); + + // Build a new stringified Chainpad hyperjson without metadata to compare with the one build from the dom + shjson = JSON.stringify(hjson); + + var hjson2 = Hyperjson.fromDOM(inner); + var shjson2 = JSON.stringify(hjson2); + if (shjson2 !== shjson) { + console.error("shjson2 !== shjson"); + module.realtimeInput.patchText(shjson2); + } + }; + + var onInit = realtimeOptions.onInit = function (info) { + var $bar = $('#pad-iframe')[0].contentWindow.$('#cke_1_toolbox'); + toolbarList = info.userList; + var config = { + userData: userList, + changeNameID: 'cryptpad-changeName' + }; + toolbar = info.realtime.toolbar = Toolbar.create($bar, info.myID, info.realtime, info.webChannel, info.userList, config); + createChangeName('cryptpad-changeName', $bar); + /* TODO handle disconnects and such*/ + }; + + var onReady = realtimeOptions.onReady = function (info) { + console.log("Unlocking editor"); + initializing = false; + setEditable(true); + var shjson = info.realtime.getUserDoc(); + applyHjson(shjson); + }; + + var onAbort = realtimeOptions.onAbort = function (info) { + console.log("Aborting the session!"); + // stop the user from continuing to edit + setEditable(false); + // TODO inform them that the session was torn down + toolbar.failed(); + }; + + + + + + var rti = module.realtimeInput = realtimeInput.start(realtimeOptions); + + /* catch `type="_moz"` before it goes over the wire */ + var brFilter = function (hj) { + if (hj[1].type === '_moz') { hj[1].type = undefined; } + return hj; + }; + + // $textarea.val(JSON.stringify(Convert.dom.to.hjson(inner))); + + /* It's incredibly important that you assign 'rti.onLocal' + It's used inside of realtimeInput to make sure that all changes + make it into chainpad. + + It's being assigned this way because it can't be passed in, and + and can't be easily returned from realtime input without making + the code less extensible. + */ + var propogate = rti.onLocal = function () { + /* if the problem were a matter of external patches being + applied while a local patch were in progress, then we would + expect to be able to check and find + 'module.localChangeInProgress' with a non-zero value while + we were applying a remote change. + */ + var hjson = Hyperjson.fromDOM(inner, isNotMagicLine, brFilter); + if(Object.keys(myData).length > 0) { + hjson[hjson.length] = {metadata: userList}; + } + var shjson = JSON.stringify(hjson); + if (!rti.patchText(shjson)) { + return; + } + rti.onEvent(shjson); + }; + + /* hitting enter makes a new line, but places the cursor inside + of the
instead of the

. This makes it such that you + cannot type until you click, which is rather unnacceptable. + If the cursor is ever inside such a
, you probably want + to push it out to the parent element, which ought to be a + paragraph tag. This needs to be done on keydown, otherwise + the first such keypress will not be inserted into the P. */ + inner.addEventListener('keydown', cursor.brFix); + + editor.on('change', propogate); + // editor.on('change', function () { + // var hjson = Convert.core.hyperjson.fromDOM(inner); + // if(myData !== {}) { + // hjson[hjson.length] = {metadata: userList}; + // } + // $textarea.val(JSON.stringify(hjson)); + // rti.bumpSharejs(); + // }); + }); + }; + + var interval = 100; + var first = function () { + Ckeditor = ifrw.CKEDITOR; + if (Ckeditor) { + andThen(Ckeditor); + } else { + console.log("Ckeditor was not defined. Trying again in %sms",interval); + setTimeout(first, interval); + } + }; + + $(first); +}); diff --git a/www/padrtc/netflux.js b/www/padrtc/netflux.js new file mode 100644 index 000000000..d01302980 --- /dev/null +++ b/www/padrtc/netflux.js @@ -0,0 +1,1473 @@ +(function webpackUniversalModuleDefinition(root, factory) { + if(typeof exports === 'object' && typeof module === 'object') + module.exports = factory(); + else if(typeof define === 'function' && define.amd) + define([], factory); + else if(typeof exports === 'object') + exports["nf"] = factory(); + else + root["nf"] = factory(); +})(this, function() { +return /******/ (function(modules) { // webpackBootstrap +/******/ // The module cache +/******/ var installedModules = {}; + +/******/ // The require function +/******/ function __webpack_require__(moduleId) { + +/******/ // Check if module is in cache +/******/ if(installedModules[moduleId]) +/******/ return installedModules[moduleId].exports; + +/******/ // Create a new module (and put it into the cache) +/******/ var module = installedModules[moduleId] = { +/******/ exports: {}, +/******/ id: moduleId, +/******/ loaded: false +/******/ }; + +/******/ // Execute the module function +/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); + +/******/ // Flag the module as loaded +/******/ module.loaded = true; + +/******/ // Return the exports of the module +/******/ return module.exports; +/******/ } + + +/******/ // expose the modules object (__webpack_modules__) +/******/ __webpack_require__.m = modules; + +/******/ // expose the module cache +/******/ __webpack_require__.c = installedModules; + +/******/ // __webpack_public_path__ +/******/ __webpack_require__.p = ""; + +/******/ // Load entry module and return exports +/******/ return __webpack_require__(0); +/******/ }) +/************************************************************************/ +/******/ ([ +/* 0 */ +/***/ function(module, exports, __webpack_require__) { + + 'use strict'; + + var _Facade = __webpack_require__(1); + + var _Facade2 = _interopRequireDefault(_Facade); + + function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + + module.exports = new _Facade2.default(); + +/***/ }, +/* 1 */ +/***/ function(module, exports, __webpack_require__) { + + 'use strict'; + + Object.defineProperty(exports, "__esModule", { + value: true + }); + + var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + + var _WebChannel = __webpack_require__(2); + + var _WebChannel2 = _interopRequireDefault(_WebChannel); + + var _ServiceProvider = __webpack_require__(4); + + var _ServiceProvider2 = _interopRequireDefault(_ServiceProvider); + + function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + + function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + + var Facade = function () { + function Facade() { + var options = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0]; + + _classCallCheck(this, Facade); + + this.defaults = { + webrtc: {} + }; + this.settings = Object.assign({}, this.defaults, options); + } + + _createClass(Facade, [{ + key: 'create', + value: function create() { + var options = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0]; + + return new _WebChannel2.default(); + } + }, { + key: 'join', + value: function join(key) { + var options = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1]; + + var defaults = { + connector: 'WebRTCService', + protocol: 'ExchangeProtocolService' + }; + var settings = Object.assign({}, defaults, options); + var connector = _ServiceProvider2.default.get(settings.connector); + var protocol = _ServiceProvider2.default.get(settings.protocol); + var connectorOptions = { signaling: settings.signaling, facade: this }; + return new Promise(function (resolve, reject) { + connector.join(key, connectorOptions).then(function (channel) { + var webChannel = new _WebChannel2.default(options); + channel.webChannel = webChannel; + channel.onmessage = protocol.onmessage; + webChannel.channels.add(channel); + webChannel.onopen = function () { + resolve(webChannel); + }; + }, reject); + }); + } + }, { + key: 'invite', + value: function invite() { + // TODO + } + }, { + key: '_onJoining', + value: function _onJoining() { + // TODO + } + }, { + key: '_onLeaving', + value: function _onLeaving() { + // TODO + } + }, { + key: '_onMessage', + value: function _onMessage() { + // TODO + } + }, { + key: '_onPeerMessage', + value: function _onPeerMessage() { + // TODO + } + }, { + key: '_onInvite', + value: function _onInvite() { + // TODO + } + }]); + + return Facade; + }(); + + exports.default = Facade; + +/***/ }, +/* 2 */ +/***/ function(module, exports, __webpack_require__) { + + 'use strict'; + + Object.defineProperty(exports, "__esModule", { + value: true + }); + + var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + + var _constants = __webpack_require__(3); + + var cs = _interopRequireWildcard(_constants); + + var _ServiceProvider = __webpack_require__(4); + + var _ServiceProvider2 = _interopRequireDefault(_ServiceProvider); + + function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + + function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } + + function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + + var WebChannel = function () { + function WebChannel() { + var options = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0]; + + _classCallCheck(this, WebChannel); + + this.defaults = { + connector: cs.WEBRTC_SERVICE, + topology: cs.FULLYCONNECTED_SERVICE, + protocol: cs.EXCHANGEPROTOCOL_SERVICE + }; + this.settings = Object.assign({}, this.defaults, options); + + // Private attributes + this.protocol = cs.EXCHANGEPROTOCOL_SERVICE; + + // Public attributes + this.id; + this.myID = this._generateID(); + this.channels = new Set(); + this.onjoining; + this.onleaving; + this.onmessage; + } + + _createClass(WebChannel, [{ + key: 'leave', + value: function leave() {} + }, { + key: 'send', + value: function send(data) { + var channel = this; + return new Promise(function (resolve, reject) { + if (channel.channels.size === 0) { + resolve(); + } + var protocol = _ServiceProvider2.default.get(channel.settings.protocol); + channel.topologyService.broadcast(channel, protocol.message(cs.USER_DATA, { id: channel.myID, data: data })).then(resolve, reject); + }); + } + }, { + key: 'sendPing', + value: function sendPing() { + var channel = this; + return new Promise(function (resolve, reject) { + if (channel.channels.size === 0) { + resolve(); + } + var protocol = _ServiceProvider2.default.get(channel.settings.protocol); + channel.topologyService.broadcast(channel, protocol.message(cs.PING, { data: '' })).then(resolve, reject); + }); + } + }, { + key: 'getHistory', + value: function getHistory(historyKeeperID) { + var channel = this; + return new Promise(function (resolve, reject) { + var protocol = _ServiceProvider2.default.get(channel.settings.protocol); + channel.topologyService.sendTo(historyKeeperID, channel, protocol.message(cs.GET_HISTORY, { id: channel.myID, data: '' })).then(resolve, reject); + }); + } + }, { + key: 'sendTo', + value: function sendTo(id, msg) { + var channel = this; + return new Promise(function (resolve, reject) { + var protocol = _ServiceProvider2.default.get(channel.settings.protocol); + channel.topologyService.sendTo(id, channel, protocol.message(cs.USER_DATA, { id: channel.myID, data: msg })).then(resolve, reject); + }); + } + }, { + key: 'openForJoining', + value: function openForJoining() { + var _this = this; + + var options = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0]; + + var settings = Object.assign({}, this.settings, options); + var connector = _ServiceProvider2.default.get(settings.connector); + return connector.open(function (channel) { + // 1) New dataChannel connection established. + // NEXT: add it to the network + var protocol = _ServiceProvider2.default.get(_this.protocol); + _this.topologyService = _ServiceProvider2.default.get(_this.settings.topology); + channel.webChannel = _this; + channel.onmessage = protocol.onmessage; + + // 2.1) Send to the new client the webChannel topology name + channel.send(protocol.message(cs.JOIN_START, _this.settings.topology)); + + // 2.2) Ask to topology to add the new client to this webChannel + _this.topologyService.addStart(channel, _this).then(function (id) { + _this.topologyService.broadcast(_this, protocol.message(cs.JOIN_FINISH, id)); + _this.onJoining(id); + }); + }, settings).then(function (data) { + return data; + }); + } + }, { + key: 'closeForJoining', + value: function closeForJoining() {} + }, { + key: 'isInviting', + value: function isInviting() {} + }, { + key: '_generateID', + value: function _generateID() { + var MIN_LENGTH = 10; + var DELTA_LENGTH = 10; + var MASK = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; + var result = ''; + var length = MIN_LENGTH + Math.round(Math.random() * DELTA_LENGTH); + + for (var i = 0; i < length; i++) { + result += MASK[Math.round(Math.random() * (MASK.length - 1))]; + } + return result; + } + }, { + key: 'topology', + set: function set(topologyServiceName) { + this.settings.topology = topologyServiceName; + this.topologyService = _ServiceProvider2.default.get(topologyServiceName); + }, + get: function get() { + return this.settigns.topology; + } + }]); + + return WebChannel; + }(); + + exports.default = WebChannel; + +/***/ }, +/* 3 */ +/***/ function(module, exports) { + + 'use strict'; + + Object.defineProperty(exports, "__esModule", { + value: true + }); + // API user's message + var USER_DATA = exports.USER_DATA = 0; + var GET_HISTORY = exports.GET_HISTORY = 6; + + // Internal messages + var JOIN_START = exports.JOIN_START = 2; + var JOIN_FINISH = exports.JOIN_FINISH = 4; + var YOUR_NEW_ID = exports.YOUR_NEW_ID = 5; + var PING = exports.PING = 7; + + // Internal message to a specific Service + var SERVICE_DATA = exports.SERVICE_DATA = 3; + + var WEBRTC_SERVICE = exports.WEBRTC_SERVICE = 'WebRTCService'; + var WEBSOCKET_SERVICE = exports.WEBSOCKET_SERVICE = 'WebSocketService'; + var FULLYCONNECTED_SERVICE = exports.FULLYCONNECTED_SERVICE = 'FullyConnectedService'; + var STAR_SERVICE = exports.STAR_SERVICE = 'StarTopologyService'; + var EXCHANGEPROTOCOL_SERVICE = exports.EXCHANGEPROTOCOL_SERVICE = 'ExchangeProtocolService'; + var WSPROTOCOL_SERVICE = exports.WSPROTOCOL_SERVICE = 'WebSocketProtocolService'; + +/***/ }, +/* 4 */ +/***/ function(module, exports, __webpack_require__) { + + 'use strict'; + + Object.defineProperty(exports, "__esModule", { + value: true + }); + + var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + + var _constants = __webpack_require__(3); + + var cs = _interopRequireWildcard(_constants); + + var _FullyConnectedService = __webpack_require__(5); + + var _FullyConnectedService2 = _interopRequireDefault(_FullyConnectedService); + + var _StarTopologyService = __webpack_require__(6); + + var _StarTopologyService2 = _interopRequireDefault(_StarTopologyService); + + var _WebRTCService = __webpack_require__(7); + + var _WebRTCService2 = _interopRequireDefault(_WebRTCService); + + var _WebSocketService = __webpack_require__(8); + + var _WebSocketService2 = _interopRequireDefault(_WebSocketService); + + var _ExchangeProtocolService = __webpack_require__(9); + + var _ExchangeProtocolService2 = _interopRequireDefault(_ExchangeProtocolService); + + var _WebSocketProtocolService = __webpack_require__(10); + + var _WebSocketProtocolService2 = _interopRequireDefault(_WebSocketProtocolService); + + function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + + function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } + + function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + + var services = new Map(); + + var ServiceProvider = function () { + function ServiceProvider() { + _classCallCheck(this, ServiceProvider); + } + + _createClass(ServiceProvider, null, [{ + key: 'get', + value: function get(code) { + var options = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1]; + + var service = undefined; + switch (code) { + case cs.WEBRTC_SERVICE: + service = new _WebRTCService2.default(options); + break; + case cs.WEBSOCKET_SERVICE: + service = new _WebSocketService2.default(options); + break; + case cs.FULLYCONNECTED_SERVICE: + service = new _FullyConnectedService2.default(options); + break; + case cs.STAR_SERVICE: + service = new _StarTopologyService2.default(options); + break; + case cs.EXCHANGEPROTOCOL_SERVICE: + service = new _ExchangeProtocolService2.default(options); + break; + case cs.WSPROTOCOL_SERVICE: + service = new _WebSocketProtocolService2.default(options); + break; + } + services.set(code, service); + return service; + } + }]); + + return ServiceProvider; + }(); + + exports.default = ServiceProvider; + +/***/ }, +/* 5 */ +/***/ function(module, exports, __webpack_require__) { + + 'use strict'; + + Object.defineProperty(exports, "__esModule", { + value: true + }); + + var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + + var _constants = __webpack_require__(3); + + var cs = _interopRequireWildcard(_constants); + + var _ServiceProvider = __webpack_require__(4); + + var _ServiceProvider2 = _interopRequireDefault(_ServiceProvider); + + function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + + function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } + + function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + + var FullyConnectedService = function () { + function FullyConnectedService() { + var options = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0]; + + _classCallCheck(this, FullyConnectedService); + } + + _createClass(FullyConnectedService, [{ + key: 'addStart', + value: function addStart(channel, webChannel) { + var _this = this; + + var protocol = _ServiceProvider2.default.get(cs.EXCHANGEPROTOCOL_SERVICE); + return new Promise(function (resolve, reject) { + channel.peerID = _this._generateID(); + channel.send(protocol.message(cs.YOUR_NEW_ID, { + newID: channel.peerID, + myID: webChannel.myID + })); + if (Reflect.has(webChannel, 'aboutToJoin') && webChannel.aboutToJoin instanceof Map) { + webChannel.aboutToJoin.set(channel.peerID, channel); + } else { + webChannel.aboutToJoin = new Map(); + } + + if (webChannel.channels.size === 0) { + webChannel.channels.add(channel); + channel.onclose = function () { + webChannel.onLeaving(channel.peerID); + webChannel.channels.delete(channel); + }; + resolve(channel.peerID); + } else { + (function () { + webChannel.successfullyConnected = new Map(); + webChannel.successfullyConnected.set(channel.peerID, 0); + webChannel.connectionSucceed = function (id, withId) { + var counter = webChannel.successfullyConnected.get(withId); + webChannel.successfullyConnected.set(withId, ++counter); + if (webChannel.successfullyConnected.get(withId) === webChannel.channels.size) { + _this.addFinish(webChannel, withId); + resolve(withId); + } + }; + var connector = _ServiceProvider2.default.get(cs.WEBRTC_SERVICE); + webChannel.channels.forEach(function (c) { + connector.connect(channel.peerID, webChannel, c.peerID); + }); + })(); + } + }); + } + }, { + key: 'addFinish', + value: function addFinish(webChannel, id) { + if (id != webChannel.myID) { + (function () { + var channel = webChannel.aboutToJoin.get(id); + webChannel.channels.add(webChannel.aboutToJoin.get(id)); + channel.onclose = function () { + webChannel.onLeaving(channel.peerID); + webChannel.channels.delete(channel); + }; + //webChannel.aboutToJoin.delete(id) + if (Reflect.has(webChannel, 'successfullyConnected')) { + webChannel.successfullyConnected.delete(id); + } + })(); + } else { + webChannel.onopen(); + } + } + }, { + key: 'broadcast', + value: function broadcast(webChannel, data) { + return new Promise(function (resolve, reject) { + var _iteratorNormalCompletion = true; + var _didIteratorError = false; + var _iteratorError = undefined; + + try { + for (var _iterator = webChannel.channels[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { + var c = _step.value; + + c.send(data); + } + } catch (err) { + _didIteratorError = true; + _iteratorError = err; + } finally { + try { + if (!_iteratorNormalCompletion && _iterator.return) { + _iterator.return(); + } + } finally { + if (_didIteratorError) { + throw _iteratorError; + } + } + } + + resolve(); + }); + } + }, { + key: 'sendTo', + value: function sendTo(id, webChannel, data) { + return new Promise(function (resolve, reject) { + var _iteratorNormalCompletion2 = true; + var _didIteratorError2 = false; + var _iteratorError2 = undefined; + + try { + for (var _iterator2 = webChannel.channels[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) { + var c = _step2.value; + + if (c.peerID == id) { + c.send(data); + } + } + } catch (err) { + _didIteratorError2 = true; + _iteratorError2 = err; + } finally { + try { + if (!_iteratorNormalCompletion2 && _iterator2.return) { + _iterator2.return(); + } + } finally { + if (_didIteratorError2) { + throw _iteratorError2; + } + } + } + + resolve(); + }); + } + }, { + key: 'leave', + value: function leave(webChannel) { + this.broadcast(webChannel); + } + }, { + key: '_generateID', + value: function _generateID() { + var MIN_LENGTH = 10; + var DELTA_LENGTH = 10; + var MASK = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; + var length = MIN_LENGTH + Math.round(Math.random() * DELTA_LENGTH); + var maskLastIndex = MASK.length - 1; + var result = ''; + + for (var i = 0; i < length; i++) { + result += MASK[Math.round(Math.random() * maskLastIndex)]; + } + return result; + } + }]); + + return FullyConnectedService; + }(); + + exports.default = FullyConnectedService; + +/***/ }, +/* 6 */ +/***/ function(module, exports) { + + 'use strict'; + + Object.defineProperty(exports, "__esModule", { + value: true + }); + + var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + + function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + + var StarTopologyService = function () { + function StarTopologyService() { + var options = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0]; + + _classCallCheck(this, StarTopologyService); + } + + _createClass(StarTopologyService, [{ + key: 'broadcast', + value: function broadcast(webChannel, data) { + return new Promise(function (resolve, reject) { + var _iteratorNormalCompletion = true; + var _didIteratorError = false; + var _iteratorError = undefined; + + try { + for (var _iterator = webChannel.channels[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { + var c = _step.value; + + var msg = undefined; + // Create the string message + var date = new Date().getTime(); + if (data.type === 'PING') { + msg = JSON.stringify([c.seq++, 'PING', date]); + } else { + msg = JSON.stringify([c.seq++, data.type, webChannel.id, data.msg]); + } + // Store the message with his sequence number to know what message has caused the reception of an ACK + // This is used in WebSocketProtocolService + var srvMsg = JSON.parse(msg); + srvMsg.shift(); + srvMsg.unshift(webChannel.myID); + srvMsg.unshift(0); + webChannel.waitingAck[c.seq - 1] = { resolve: resolve, reject: reject, time: date, data: srvMsg }; + // Send the message to the server + c.send(msg); + } + // resolve(); + } catch (err) { + _didIteratorError = true; + _iteratorError = err; + } finally { + try { + if (!_iteratorNormalCompletion && _iterator.return) { + _iterator.return(); + } + } finally { + if (_didIteratorError) { + throw _iteratorError; + } + } + } + }); + } + }, { + key: 'sendTo', + value: function sendTo(id, webChannel, data) { + return new Promise(function (resolve, reject) { + var _iteratorNormalCompletion2 = true; + var _didIteratorError2 = false; + var _iteratorError2 = undefined; + + try { + for (var _iterator2 = webChannel.channels[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) { + var c = _step2.value; + + var msg = JSON.stringify([c.seq++, data.type, id, data.msg]); + c.send(msg); + } + } catch (err) { + _didIteratorError2 = true; + _iteratorError2 = err; + } finally { + try { + if (!_iteratorNormalCompletion2 && _iterator2.return) { + _iterator2.return(); + } + } finally { + if (_didIteratorError2) { + throw _iteratorError2; + } + } + } + + resolve(); + }); + } + }]); + + return StarTopologyService; + }(); + + exports.default = StarTopologyService; + +/***/ }, +/* 7 */ +/***/ function(module, exports, __webpack_require__) { + + 'use strict'; + + Object.defineProperty(exports, "__esModule", { + value: true + }); + + var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + + var _constants = __webpack_require__(3); + + var cs = _interopRequireWildcard(_constants); + + var _ServiceProvider = __webpack_require__(4); + + var _ServiceProvider2 = _interopRequireDefault(_ServiceProvider); + + function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + + function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } + + function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + + var WEBRTC_DATA = 0; + var CONNECT_WITH = 1; + var CONNECT_WITH_SUCCEED = 2; + + var WebRTCService = function () { + function WebRTCService() { + var options = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0]; + + _classCallCheck(this, WebRTCService); + + this.NAME = this.constructor.name; + this.protocol = _ServiceProvider2.default.get(cs.EXCHANGEPROTOCOL_SERVICE); + this.defaults = { + signaling: 'ws://localhost:8000', + webRTCOptions: { + iceServers: [{ + urls: ['stun:23.21.150.121', 'stun:stun.l.google.com:19302'] + }, { + urls: 'turn:numb.viagenie.ca', + credential: 'webrtcdemo', + username: 'louis%40mozilla.com' + }] + } + }; + this.settings = Object.assign({}, this.defaults, options); + + // Declare WebRTC related global(window) constructors + this.RTCPeerConnection = window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection || window.msRTCPeerConnection; + + this.RTCIceCandidate = window.RTCIceCandidate || window.mozRTCIceCandidate || window.RTCIceCandidate || window.msRTCIceCandidate; + + this.RTCSessionDescription = window.RTCSessionDescription || window.mozRTCSessionDescription || window.webkitRTCSessionDescription || window.msRTCSessionDescription; + } + + _createClass(WebRTCService, [{ + key: 'connect', + value: function connect(newPeerID, webChannel, peerID) { + webChannel.topologyService.sendTo(peerID, webChannel, this._msg(CONNECT_WITH, { key: newPeerID, intermediaryID: webChannel.myID })); + } + }, { + key: 'open', + value: function open(onchannel) { + var _this = this; + + var options = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1]; + + var defaults = { + key: this._randomKey() + }; + var settings = Object.assign({}, this.settings, defaults, options); + + return new Promise(function (resolve, reject) { + var connections = []; + var socket = new window.WebSocket(settings.signaling); + socket.onopen = function () { + socket.send(JSON.stringify({ key: settings.key })); + resolve({ url: _this.settings.signaling, key: settings.key }); + }; + socket.onmessage = function (e) { + var msg = JSON.parse(e.data); + if (Reflect.has(msg, 'id') && Reflect.has(msg, 'data')) { + if (Reflect.has(msg.data, 'offer')) { + (function () { + var connection = new _this.RTCPeerConnection(settings.webRTCOptions); + connections.push(connection); + connection.ondatachannel = function (e) { + e.channel.onopen = function () { + onchannel(e.channel); + }; + }; + connection.onicecandidate = function (e) { + if (e.candidate !== null) { + var candidate = { + candidate: e.candidate.candidate, + sdpMLineIndex: e.candidate.sdpMLineIndex + }; + socket.send(JSON.stringify({ id: msg.id, data: { candidate: candidate } })); + } + }; + var sd = Object.assign(new _this.RTCSessionDescription(), msg.data.offer); + connection.setRemoteDescription(sd, function () { + connection.createAnswer(function (answer) { + connection.setLocalDescription(answer, function () { + socket.send(JSON.stringify({ + id: msg.id, + data: { answer: connection.localDescription.toJSON() } })); + }, function () {}); + }, function () {}); + }, function () {}); + })(); + } else if (Reflect.has(msg.data, 'candidate')) { + var candidate = new _this.RTCIceCandidate(msg.data.candidate); + connections[msg.id].addIceCandidate(candidate); + } + } + }; + socket.onerror = reject; + }); + } + }, { + key: 'join', + value: function join(key) { + var _this2 = this; + + var options = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1]; + + var settings = Object.assign({}, this.settings, options); + return new Promise(function (resolve, reject) { + var connection = undefined; + var socket = new window.WebSocket(settings.signaling); + socket.onopen = function () { + connection = new _this2.RTCPeerConnection(settings.webRTCOptions); + connection.onicecandidate = function (e) { + if (e.candidate !== null) { + var candidate = { + candidate: e.candidate.candidate, + sdpMLineIndex: e.candidate.sdpMLineIndex + }; + socket.send(JSON.stringify({ data: { candidate: candidate } })); + } + }; + var dc = connection.createDataChannel(key); + dc.onopen = function () { + resolve(dc); + }; + connection.createOffer(function (offer) { + connection.setLocalDescription(offer, function () { + socket.send(JSON.stringify({ join: key, data: { offer: connection.localDescription.toJSON() } })); + }, reject); + }, reject); + }; + socket.onclose = function (e) { + reject(e); + }; + socket.onmessage = function (e) { + var msg = JSON.parse(e.data); + if (Reflect.has(msg, 'data')) { + if (Reflect.has(msg.data, 'answer')) { + var sd = Object.assign(new _this2.RTCSessionDescription(), msg.data.answer); + connection.setRemoteDescription(sd, function () {}, reject); + } else if (Reflect.has(msg.data, 'candidate')) { + var candidate = new _this2.RTCIceCandidate(msg.data.candidate); + connection.addIceCandidate(candidate); + } else { + reject(); + } + } else { + reject(); + } + }; + socket.onerror = reject; + }); + } + }, { + key: '_randomKey', + value: function _randomKey() { + var MIN_LENGTH = 10; + var DELTA_LENGTH = 10; + var MASK = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; + var result = ''; + var length = MIN_LENGTH + Math.round(Math.random() * DELTA_LENGTH); + + for (var i = 0; i < length; i++) { + result += MASK[Math.round(Math.random() * (MASK.length - 1))]; + } + return result; + } + }, { + key: '_msg', + value: function _msg(code, data) { + var msg = { service: this.constructor.name }; + msg.data = {}; + msg.data.code = code; + Object.assign(msg.data, data); + return this.protocol.message(cs.SERVICE_DATA, msg); + } + }, { + key: 'onmessage', + value: function onmessage(channel, msg) { + var _this3 = this; + + var webChannel = channel.webChannel; + if (!Reflect.has(webChannel, 'connections')) { + webChannel.connections = new Map(); + } + switch (msg.code) { + case WEBRTC_DATA: + if (webChannel.myID === msg.recipientPeerID) { + if (Reflect.has(msg, 'sdp')) { + if (msg.sdp.type === 'offer') { + (function () { + var connection = new _this3.RTCPeerConnection(_this3.settings.webRTCOptions); + webChannel.connections.set(msg.senderPeerID, connection); + connection.ondatachannel = function (e) { + e.channel.onopen = function () { + e.channel.peerID = msg.senderPeerID; + e.channel.webChannel = webChannel; + e.channel.onmessage = _this3.protocol.onmessage; + webChannel.channels.add(e.channel); + e.channel.onclose = function () { + webChannel.onLeaving(e.channel.peerID); + webChannel.channels.delete(e.channel); + }; + }; + }; + connection.onicecandidate = function (e) { + if (e.candidate !== null) { + var candidate = { + candidate: e.candidate.candidate, + sdpMLineIndex: e.candidate.sdpMLineIndex + }; + channel.send(_this3._msg(WEBRTC_DATA, { + senderPeerID: webChannel.myID, + recipientPeerID: msg.senderPeerID, + candidate: candidate + })); + } + }; + var sd = Object.assign(new _this3.RTCSessionDescription(), msg.sdp); + connection.setRemoteDescription(sd, function () { + connection.createAnswer(function (answer) { + connection.setLocalDescription(answer, function () { + channel.send(_this3._msg(WEBRTC_DATA, { + senderPeerID: webChannel.myID, + recipientPeerID: msg.senderPeerID, + sdp: connection.localDescription.toJSON() + })); + }, function () {}); + }, function () {}); + }, function () {}); + })(); + } else if (msg.sdp.type === 'answer') { + var sd = Object.assign(new this.RTCSessionDescription(), msg.sdp); + webChannel.connections.get(msg.senderPeerID).setRemoteDescription(sd, function () {}, function () {}); + } + } else if (Reflect.has(msg, 'candidate')) { + webChannel.connections.get(msg.senderPeerID).addIceCandidate(new this.RTCIceCandidate(msg.candidate)); + } + } else { + var data = this._msg(WEBRTC_DATA, msg); + if (webChannel.aboutToJoin.has(msg.recipientPeerID)) { + webChannel.aboutToJoin.get(msg.recipientPeerID).send(data); + } else { + webChannel.topologyService.sendTo(msg.recipientPeerID, webChannel, data); + } + } + break; + case CONNECT_WITH: + var connection = new this.RTCPeerConnection(this.settings.webRTCOptions); + connection.onicecandidate = function (e) { + if (e.candidate !== null) { + var candidate = { + candidate: e.candidate.candidate, + sdpMLineIndex: e.candidate.sdpMLineIndex + }; + webChannel.topologyService.sendTo(msg.intermediaryID, webChannel, _this3._msg(WEBRTC_DATA, { + senderPeerID: webChannel.myID, + recipientPeerID: msg.key, + candidate: candidate + })); + } + }; + var dc = connection.createDataChannel(msg.key); + dc.onopen = function () { + if (!Reflect.has(webChannel, 'aboutToJoin')) { + webChannel.aboutToJoin = new Map(); + } + webChannel.aboutToJoin.set(dc.label, dc); + dc.onmessage = _this3.protocol.onmessage; + dc.peerID = dc.label; + dc.webChannel = webChannel; + webChannel.topologyService.sendTo(msg.intermediaryID, webChannel, _this3._msg(CONNECT_WITH_SUCCEED, { + senderPeerID: webChannel.myID, + recipientPeerID: dc.label + })); + }; + connection.createOffer(function (offer) { + connection.setLocalDescription(offer, function () { + webChannel.topologyService.sendTo(msg.intermediaryID, webChannel, _this3._msg(WEBRTC_DATA, { + senderPeerID: webChannel.myID, + recipientPeerID: msg.key, + sdp: connection.localDescription.toJSON() + })); + webChannel.connections.set(msg.key, connection); + }, function () {}); + }, function () {}); + break; + case CONNECT_WITH_SUCCEED: + webChannel.connectionSucceed(msg.senderPeerID, msg.recipientPeerID); + break; + } + } + }]); + + return WebRTCService; + }(); + + exports.default = WebRTCService; + +/***/ }, +/* 8 */ +/***/ function(module, exports, __webpack_require__) { + + 'use strict'; + + Object.defineProperty(exports, "__esModule", { + value: true + }); + + var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + + var _constants = __webpack_require__(3); + + var cs = _interopRequireWildcard(_constants); + + var _ServiceProvider = __webpack_require__(4); + + var _ServiceProvider2 = _interopRequireDefault(_ServiceProvider); + + function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + + function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } + + function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + + var WebSocketService = function () { + function WebSocketService() { + var options = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0]; + + _classCallCheck(this, WebSocketService); + + this.NAME = this.constructor.name; + this.protocol = _ServiceProvider2.default.get(cs.EXCHANGEPROTOCOL_SERVICE); + this.defaults = { + signaling: 'ws://localhost:9000', + REQUEST_TIMEOUT: 5000 + }; + this.settings = Object.assign({}, this.defaults, options); + } + + _createClass(WebSocketService, [{ + key: 'join', + value: function join(key) { + var options = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1]; + + var settings = Object.assign({}, this.settings, options); + return new Promise(function (resolve, reject) { + var connection = undefined; + var socket = new window.WebSocket(settings.signaling); + setInterval(function () { + if (socket.webChannel && socket.webChannel.waitingAck) { + var waitingAck = socket.webChannel.waitingAck; + for (var id in waitingAck) { + var req = waitingAck[id]; + var now = new Date().getTime(); + if (now - req.time > settings.REQUEST_TIMEOUT) { + delete socket.webChannel.waitingAck[id]; + req.reject({ type: 'TIMEOUT', message: 'waited ' + now - req.time + 'ms' }); + } + } + } + }, 5000); + socket.seq = 1; + socket.facade = options.facade || null; + socket.onopen = function () { + if (key && key !== '') { + socket.send(JSON.stringify([socket.seq++, 'JOIN', key])); + } else { + socket.send(JSON.stringify([socket.seq++, 'JOIN'])); + } + resolve(socket); + }; + socket.onerror = reject; + }); + } + }]); + + return WebSocketService; + }(); + + exports.default = WebSocketService; + +/***/ }, +/* 9 */ +/***/ function(module, exports, __webpack_require__) { + + 'use strict'; + + Object.defineProperty(exports, "__esModule", { + value: true + }); + + var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + + var _constants = __webpack_require__(3); + + var cs = _interopRequireWildcard(_constants); + + var _ServiceProvider = __webpack_require__(4); + + var _ServiceProvider2 = _interopRequireDefault(_ServiceProvider); + + function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + + function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } + + function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + + var ExchangeProtocolService = function () { + function ExchangeProtocolService() { + var options = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0]; + + _classCallCheck(this, ExchangeProtocolService); + } + + _createClass(ExchangeProtocolService, [{ + key: 'onmessage', + value: function onmessage(e) { + var msg = JSON.parse(e.data); + var channel = e.currentTarget; + var webChannel = channel.webChannel; + + switch (msg.code) { + case cs.USER_DATA: + webChannel.onmessage(msg.id, msg.data); + break; + case cs.GET_HISTORY: + webChannel.onPeerMessage(msg.id, msg.code); + break; + case cs.SERVICE_DATA: + var service = _ServiceProvider2.default.get(msg.service); + service.onmessage(channel, msg.data); + break; + case cs.YOUR_NEW_ID: + // TODO: change names + webChannel.myID = msg.newID; + channel.peerID = msg.myID; + break; + case cs.JOIN_START: + // 2.1) Send to the new client the webChannel topology + webChannel.topology = msg.topology; + webChannel.topologyService = _ServiceProvider2.default.get(msg.topology); + break; + case cs.JOIN_FINISH: + webChannel.topologyService.addFinish(webChannel, msg.id); + if (msg.id != webChannel.myID) { + // A new user has just registered + webChannel.onJoining(msg.id); + } else { + (function () { + // We're fully synced, trigger onJoining for all existing users + var waitForOnJoining = function waitForOnJoining() { + if (typeof webChannel.onJoining !== "function") { + setTimeout(waitForOnJoining, 500); + return; + } + var _iteratorNormalCompletion = true; + var _didIteratorError = false; + var _iteratorError = undefined; + + try { + for (var _iterator = webChannel.channels[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { + var c = _step.value; + + webChannel.onJoining(c.peerID); + } + } catch (err) { + _didIteratorError = true; + _iteratorError = err; + } finally { + try { + if (!_iteratorNormalCompletion && _iterator.return) { + _iterator.return(); + } + } finally { + if (_didIteratorError) { + throw _iteratorError; + } + } + } + }; + waitForOnJoining(); + })(); + } + break; + } + } + }, { + key: 'message', + value: function message(code, data) { + var msg = { code: code }; + switch (code) { + case cs.USER_DATA: + msg.id = data.id; + msg.data = data.data; + break; + case cs.GET_HISTORY: + msg.id = data.id; + break; + case cs.SERVICE_DATA: + msg.service = data.service; + msg.data = Object.assign({}, data.data); + break; + case cs.YOUR_NEW_ID: + msg.newID = data.newID; + msg.myID = data.myID; + break; + case cs.JOIN_START: + msg.topology = data; + break; + case cs.JOIN_FINISH: + msg.id = data; + break; + } + return JSON.stringify(msg); + } + }]); + + return ExchangeProtocolService; + }(); + + exports.default = ExchangeProtocolService; + +/***/ }, +/* 10 */ +/***/ function(module, exports, __webpack_require__) { + + 'use strict'; + + Object.defineProperty(exports, "__esModule", { + value: true + }); + + var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + + var _constants = __webpack_require__(3); + + var cs = _interopRequireWildcard(_constants); + + var _ServiceProvider = __webpack_require__(4); + + var _ServiceProvider2 = _interopRequireDefault(_ServiceProvider); + + function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + + function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } + + function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + + var WebSocketProtocolService = function () { + function WebSocketProtocolService() { + var options = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0]; + + _classCallCheck(this, WebSocketProtocolService); + } + + _createClass(WebSocketProtocolService, [{ + key: 'onmessage', + value: function onmessage(e) { + var msg = JSON.parse(e.data); + var socket = e.currentTarget; + var webChannel = socket.webChannel; + var topology = cs.STAR_SERVICE; + var topologyService = _ServiceProvider2.default.get(topology); + var history_keeper = webChannel.hc; + + if (msg[0] !== 0 && msg[1] !== 'ACK') { + return; + } + + if (msg[2] === 'IDENT' && msg[1] === '') { + socket.uid = msg[3]; + webChannel.myID = msg[3]; + webChannel.peers = []; + webChannel.waitingAck = []; + webChannel.topology = topology; + return; + } + if (msg[1] === 'PING') { + msg[1] = 'PONG'; + socket.send(JSON.stringify(msg)); + return; + } + if (msg[1] === 'ACK') { + var seq = msg[0]; + if (webChannel.waitingAck[seq]) { + var waitingAck = webChannel.waitingAck[seq]; + waitingAck.resolve(); + var newMsg = waitingAck.data; + if (newMsg[2] === 'PING') { + // PING message : set the lag + var lag = new Date().getTime() - newMsg[3]; + webChannel.getLag = function () { + return lag; + }; + } + delete webChannel.waitingAck[seq]; + } + return; + } + // We have received a new direct message from another user + if (msg[2] === 'MSG' && msg[3] === socket.uid) { + // If it comes from the history keeper, send it to the user + if (msg[1] === history_keeper) { + if (msg[4] === 0) { + webChannel.onmessage(msg[1], msg[4]); + return; + } + var msgHistory = JSON.parse(msg[4]); + webChannel.onmessage(msgHistory[1], msgHistory[4]); + } + return; + } + if (msg[2] === 'JOIN' && (webChannel.id == null || webChannel.id === msg[3])) { + if (!webChannel.id) { + // New unnamed channel : get its name from the first "JOIN" message + if (!window.location.hash) { + var chanName = window.location.hash = msg[3]; + } + webChannel.id = msg[3]; + } + + if (msg[1] === socket.uid) { + // If the user catches himself registering, he is synchronized with the server + webChannel.onopen(); + } else { + // Trigger onJoining() when another user is joining the channel + // Register the user in the list of peers in the channel + if (webChannel.peers.length === 0 && msg[1].length === 16) { + // We've just catched the history keeper (16 characters length name) + history_keeper = msg[1]; + webChannel.hc = history_keeper; + } + var linkQuality = msg[1] === history_keeper ? 1000 : 0; + var sendToPeer = function sendToPeer(data) { + topologyService.sendTo(msg[1], webChannel, { type: 'MSG', msg: data }); + }; + var peer = { id: msg[1], connector: socket, linkQuality: linkQuality, send: sendToPeer }; + if (webChannel.peers.indexOf(peer) === -1) { + webChannel.peers.push(peer); + } + + if (msg[1] !== history_keeper) { + // Trigger onJoining with that peer once the function is loaded (i.e. once the channel is synced) + var waitForOnJoining = function waitForOnJoining() { + if (typeof webChannel.onJoining !== "function") { + setTimeout(waitForOnJoining, 500); + return; + } + webChannel.onJoining(msg[1]); + }; + waitForOnJoining(); + } + } + return; + } + // We have received a new message in that channel from another peer + if (msg[2] === 'MSG' && msg[3] === webChannel.id) { + // Find the peer who sent the message and display it + //TODO Use Peer instead of peer.id (msg[1]) : + if (typeof webChannel.onmessage === "function") webChannel.onmessage(msg[1], msg[4]); + return; + } + // Someone else has left the channel, remove him from the list of peers + if (msg[2] === 'LEAVE' && msg[3] === webChannel.id) { + //TODO Use Peer instead of peer.id (msg[1]) : + if (typeof webChannel.onLeaving === "function") webChannel.onLeaving(msg[1], webChannel); + return; + } + } + }, { + key: 'message', + value: function message(code, data) { + var type = undefined; + switch (code) { + case cs.USER_DATA: + type = 'MSG'; + break; + case cs.JOIN_START: + type = 'JOIN'; + break; + case cs.PING: + type = 'PING'; + break; + } + return { type: type, msg: data.data }; + } + }]); + + return WebSocketProtocolService; + }(); + + exports.default = WebSocketProtocolService; + +/***/ } +/******/ ]) +}); +; \ No newline at end of file diff --git a/www/padrtc/realtime-input.js b/www/padrtc/realtime-input.js new file mode 100644 index 000000000..af0f22a1d --- /dev/null +++ b/www/padrtc/realtime-input.js @@ -0,0 +1,397 @@ +/* + * Copyright 2014 XWiki SAS + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +window.Reflect = { has: (x,y) => { return (y in x); } }; +define([ + '/common/messages.js', + '/padrtc/netflux.js', + '/common/crypto.js', + '/common/toolbar.js', + '/_socket/text-patcher.js', + '/common/es6-promise.min.js', + '/common/chainpad.js', + '/bower_components/jquery/dist/jquery.min.js', +], function (Messages, Netflux, Crypto, Toolbar, TextPatcher) { + var $ = window.jQuery; + var ChainPad = window.ChainPad; + var PARANOIA = true; + var module = { exports: {} }; + + /** + * If an error is encountered but it is recoverable, do not immediately fail + * but if it keeps firing errors over and over, do fail. + */ + var MAX_RECOVERABLE_ERRORS = 15; + + var debug = function (x) { console.log(x); }, + warn = function (x) { console.error(x); }, + verbose = function (x) { console.log(x); }; + verbose = function () {}; // comment out to enable verbose logging + + // ------------------ Trapping Keyboard Events ---------------------- // + + var bindEvents = function (element, events, callback, unbind) { + for (var i = 0; i < events.length; i++) { + var e = events[i]; + if (element.addEventListener) { + if (unbind) { + element.removeEventListener(e, callback, false); + } else { + element.addEventListener(e, callback, false); + } + } else { + if (unbind) { + element.detachEvent('on' + e, callback); + } else { + element.attachEvent('on' + e, callback); + } + } + } + }; + + var getParameterByName = function (name, url) { + if (!url) { url = window.location.href; } + name = name.replace(/[\[\]]/g, "\\$&"); + var regex = new RegExp("[?&]" + name + "(=([^&#]*)|&|#|$)"), + results = regex.exec(url); + if (!results) { return null; } + if (!results[2]) { return ''; } + return decodeURIComponent(results[2].replace(/\+/g, " ")); + }; + + var start = module.exports.start = + function (config) + { + var websocketUrl = config.websocketURL; + var webrtcUrl = config.webrtcURL; + var userName = config.userName; + var channel = config.channel; + var chanKey = config.cryptKey; + var cryptKey = Crypto.parseKey(chanKey).cryptKey; + var passwd = 'y'; + + // make sure configuration is defined + config = config || {}; + + var doc = config.doc || null; + + var allMessages = []; + var initializing = true; + var recoverableErrorCount = 0; + var toReturn = {}; + var messagesHistory = []; + var chainpadAdapter = {}; + var realtime; + + // define this in case it gets called before the rest of our stuff is ready. + var onEvent = toReturn.onEvent = function (newText) { }; + + var parseMessage = function (msg) { + var res ={}; + // two or more? use a for + ['pass','user','channelId','content'].forEach(function(attr){ + var len=msg.slice(0,msg.indexOf(':')), + // taking an offset lets us slice out the prop + // and saves us one string copy + o=len.length+1, + prop=res[attr]=msg.slice(o,Number(len)+o); + // slice off the property and its descriptor + msg = msg.slice(prop.length+o); + }); + // content is the only attribute that's not a string + res.content=JSON.parse(res.content); + return res; + }; + + var mkMessage = function (user, chan, content) { + content = JSON.stringify(content); + return user.length + ':' + user + + chan.length + ':' + chan + + content.length + ':' + content; + }; + + var onPeerMessage = function(toId, type, wc) { + if(type === 6) { + messagesHistory.forEach(function(msg) { + wc.sendTo(toId, '1:y'+msg); + }); + wc.sendTo(toId, '0'); + } + }; + + var whoami = new RegExp(userName.replace(/[\/\+]/g, function (c) { + return '\\' +c; + })); + + var onMessage = function(peer, msg, wc) { + + if(msg === 0 || msg === '0') { + onReady(wc); + return; + } + var message = chainpadAdapter.msgIn(peer, msg); + + verbose(message); + allMessages.push(message); + // if (!initializing) { + // if (toReturn.onLocal) { + // toReturn.onLocal(); + // } + // } + realtime.message(message); + if (/\[5,/.test(message)) { verbose("pong"); } + + if (!initializing) { + if (/\[2,/.test(message)) { + //verbose("Got a patch"); + if (whoami.test(message)) { + //verbose("Received own message"); + } else { + //verbose("Received remote message"); + // obviously this is only going to get called if + if (config.onRemote) { + config.onRemote({ + realtime: realtime + }); + } + } + } + } + }; + + var userList = { + onChange : function() {}, + users: [] + }; + var onJoining = function(peer) { + var list = userList.users; + if(list.indexOf(peer) === -1) { + userList.users.push(peer); + } + userList.onChange(); + }; + + var onLeaving = function(peer) { + var list = userList.users; + var index = list.indexOf(peer); + if(index !== -1) { + userList.users.splice(index, 1); + } + userList.onChange(); + }; + + chainpadAdapter = { + msgIn : function(peerId, msg) { + var parsed = parseMessage(msg); + // Remove the password from the message + var passLen = msg.substring(0,msg.indexOf(':')); + var message = msg.substring(passLen.length+1 + Number(passLen)); + try { + var decryptedMsg = Crypto.decrypt(message, cryptKey); + messagesHistory.push(decryptedMsg); + return decryptedMsg; + } catch (err) { + return message; + } + + }, + msgOut : function(msg, wc) { + var parsed = parseMessage(msg); + if(parsed.content[0] === 0) { // We're registering : send a REGISTER_ACK to Chainpad + onMessage('', '1:y'+mkMessage('', channel, [1,0])); + return; + } + if(parsed.content[0] === 4) { // PING message from Chainpad + parsed.content[0] = 5; + onMessage('', '1:y'+mkMessage(parsed.user, parsed.channelId, parsed.content)); + wc.sendPing(); + return; + } + return Crypto.encrypt(msg, cryptKey); + } + }; + + var options = {}; + + var rtc = true; + + if(channel.trim().length > 0) { + options.key = channel; + } + if(!webrtcUrl) { + rtc = false; + options.signaling = websocketUrl; + options.topology = 'StarTopologyService'; + options.protocol = 'WebSocketProtocolService'; + options.connector = 'WebSocketService'; + options.openWebChannel = true; + } + else { + options.signaling = webrtcUrl; + } + + var createRealtime = function(chan) { + return ChainPad.create(userName, + passwd, + channel, + config.initialState || {}, + { + transformFunction: config.transformFunction + }); + }; + + var onReady = function(wc) { + if(config.onInit) { + config.onInit({ + myID: wc.myID, + realtime: realtime, + getLag: wc.getLag, + userList: userList + }); + } + // Trigger onJoining with our own Cryptpad username to tell the toolbar that we are synced + onJoining(wc.myID); + + // we're fully synced + initializing = false; + + // execute an onReady callback if one was supplied + if (config.onReady) { + config.onReady({ + realtime: realtime + }); + } + } + + var onOpen = function(wc) { + channel = wc.id; + window.location.hash = channel + '|' + chanKey; + // Add the handlers to the WebChannel + wc.onmessage = function(peer, msg) { // On receiving message + onMessage(peer, msg, wc); + }; + wc.onJoining = onJoining; // On user joining the session + wc.onLeaving = onLeaving; // On user leaving the session + wc.onPeerMessage = function(peerId, type) { + onPeerMessage(peerId, type, wc); + }; + if(config.setMyID) { + config.setMyID({ + myID: wc.myID + }); + } + // Open a Chainpad session + realtime = createRealtime(); + + // Sending a message... + realtime.onMessage(function(message) { + // Filter messages sent by Chainpad to make it compatible with Netflux + message = chainpadAdapter.msgOut(message, wc); + if(message) { + wc.send(message).then(function() { + // Send the message back to Chainpad once it is sent to the recipients. + onMessage(wc.myID, message); + }, function(err) { + // The message has not been sent, display the error. + console.error(err); + }); + } + }); + + // Get the channel history + var hc; + if(rtc) { + wc.channels.forEach(function (c) { if(!hc) { hc = c; } }); + if(hc) { + wc.getHistory(hc.peerID); + } + } + else { + // TODO : Improve WebSocket service to use the latest Netflux's API + wc.peers.forEach(function (p) { if (!hc || p.linkQuality > hc.linkQuality) { hc = p; } }); + hc.send(JSON.stringify(['GET_HISTORY', wc.id])); + } + + + toReturn.patchText = TextPatcher.create({ + realtime: realtime + }); + + realtime.start(); + }; + + var createRTCChannel = function () { + // Check if the WebRTC channel exists and create it if necessary + var webchannel = Netflux.create(); + webchannel.openForJoining(options).then(function(data) { + console.log(data); + webchannel.id = data.key + onOpen(webchannel); + onReady(webchannel); + }, function(error) { + warn(error); + }); + }; + + var joinChannel = function() { + // Connect to the WebSocket/WebRTC channel + Netflux.join(channel, options).then(function(wc) { + if(channel.trim().length > 0) { + wc.id = channel + } + onOpen(wc); + }, function(error) { + if(rtc && error.code === 1008) {// Unexisting RTC channel + createRTCChannel(); + } + else { warn(error); } + }); + }; + joinChannel(); + + var checkConnection = function(wc) { + if(wc.channels && wc.channels.size > 0) { + var channels = Array.from(wc.channels); + var channel = channels[0]; + + var socketChecker = setInterval(function () { + if (channel.checkSocket(realtime)) { + warn("Socket disconnected!"); + + recoverableErrorCount += 1; + + if (recoverableErrorCount >= MAX_RECOVERABLE_ERRORS) { + warn("Giving up!"); + realtime.abort(); + try { channel.close(); } catch (e) { warn(e); } + if (config.onAbort) { + config.onAbort({ + socket: channel + }); + } + if (socketChecker) { clearInterval(socketChecker); } + } + } else { + // it's working as expected, continue + } + }, 200); + } + }; + + return toReturn; + }; + return module.exports; +});