Browse Source
Replace the Netflux old client (netflux.js) by the Netflux2 client.
Replace the Netflux old client (netflux.js) by the Netflux2 client.
Move the WebRTC peer-to-peer use case in /padrtc, which still uses the old Netflux client Use es6-promises.min.js to solve a issue with some browser and the new Netflux clientmaster
14 changed files with 2628 additions and 160 deletions
Split View
Diff Options
-
4NetfluxWebsocketSrv.js
-
1WebRTCSrv.js
-
2customize.dist/index.html
-
9www/common/es6-promise.min.js
-
225www/common/netflux-client.js
-
3www/common/netflux.js
-
156www/common/realtime-input.js
-
14www/common/toolbar.js
-
57www/pad/main.js
-
79www/padrtc/index.html
-
12www/padrtc/inner.html
-
356www/padrtc/main.js
-
1473www/padrtc/netflux.js
-
397www/padrtc/realtime-input.js
9
www/common/es6-promise.min.js
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -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 }; |
|||
}); |
|||
@ -0,0 +1,79 @@ |
|||
<!DOCTYPE html> |
|||
<html> |
|||
<head> |
|||
<meta content="text/html; charset=utf-8" http-equiv="content-type"/> |
|||
<script data-main="main" src="/bower_components/requirejs/require.js"></script> |
|||
<style> |
|||
html, body { |
|||
margin: 0px; |
|||
padding: 0px; |
|||
} |
|||
#pad-iframe { |
|||
position:fixed; |
|||
top:0px; |
|||
left:0px; |
|||
bottom:0px; |
|||
right:0px; |
|||
width:100%; |
|||
height:100%; |
|||
border:none; |
|||
margin:0; |
|||
padding:0; |
|||
overflow:hidden; |
|||
} |
|||
#feedback { |
|||
display: none; |
|||
position: fixed; |
|||
top: 0px; |
|||
right: 0px; |
|||
border: 0px; |
|||
height: 100vh; |
|||
width: 30vw; |
|||
background-color: #222; |
|||
color: #ccc; |
|||
} |
|||
#debug { |
|||
height: 20px; |
|||
position: absolute; |
|||
right: 0px; |
|||
top: 70px; |
|||
} |
|||
#debug button { |
|||
visibility: hidden; |
|||
} |
|||
#debug:hover button { |
|||
visibility: visible; |
|||
} |
|||
</style> |
|||
</head> |
|||
<body> |
|||
<iframe id="pad-iframe" src="inner.html"></iframe> |
|||
<div id="debug"><button>DEBUG</button></div> |
|||
<textarea id="feedback"></textarea> |
|||
<script> |
|||
require(['/bower_components/jquery/dist/jquery.min.js'], function() { |
|||
var $ = window.$; |
|||
$('#debug').on('click', function() { |
|||
if($('#feedback').is(':visible')) { |
|||
$('#pad-iframe').css({ |
|||
'width' : '100%' |
|||
}); |
|||
$('#debug').css({ |
|||
'right' : '0%' |
|||
}); |
|||
} |
|||
else { |
|||
$('#pad-iframe').css({ |
|||
'width' : '70%' |
|||
}); |
|||
$('#debug').css({ |
|||
'right' : '30%' |
|||
}); |
|||
} |
|||
$('#feedback').toggle(); |
|||
}); |
|||
}); |
|||
</script> |
|||
</body> |
|||
</html> |
|||
|
|||
@ -0,0 +1,12 @@ |
|||
<!DOCTYPE html> |
|||
<html> |
|||
<head> |
|||
<meta content="text/html; charset=utf-8" http-equiv="content-type"/> |
|||
<script src="/bower_components/jquery/dist/jquery.min.js"></script> |
|||
<script src="/bower_components/ckeditor/ckeditor.js"></script> |
|||
</head> |
|||
<body> |
|||
<textarea style="display:none" id="editor1" name="editor1"></textarea> |
|||
</body> |
|||
</html> |
|||
|
|||
@ -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 <br> instead of the <p>. This makes it such that you |
|||
cannot type until you click, which is rather unnacceptable. |
|||
If the cursor is ever inside such a <br>, 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); |
|||
}); |
|||
1473
www/padrtc/netflux.js
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -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 <http://www.gnu.org/licenses/>.
|
|||
*/ |
|||
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; |
|||
}); |
|||
Write
Preview
Loading…
Cancel
Save