mirror of https://github.com/xwiki-labs/cryptpad
wip
This commit is contained in:
parent
a612f02be2
commit
4b25ab80d6
|
@ -39,6 +39,7 @@
|
|||
"require-css": "0.1.10",
|
||||
"less": "^2.7.2",
|
||||
"bootstrap": "#v4.0.0-alpha.6",
|
||||
"diff-dom": "2.1.1"
|
||||
"diff-dom": "2.1.1",
|
||||
"nthen": "^0.1.5"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
window.addEventListener('message', function (msg) {
|
||||
var data = JSON.parse(msg.data);
|
||||
if (data.q !== 'INIT') { return; }
|
||||
msg.source.postMessage({ txid: data.txid, content: 'OK' }, '*');
|
||||
msg.source.postMessage(JSON.stringify({ txid: data.txid, content: 'OK' }), '*');
|
||||
if (data.content && data.content.requireConf) { require.config(data.content.requireConf); }
|
||||
require(['/common/sframe-boot2.js'], function () { });
|
||||
});
|
|
@ -1,8 +1,9 @@
|
|||
// This is stage 1, it can be changed but you must bump the version of the project.
|
||||
// Note: This must only be loaded from inside of a sandbox-iframe.
|
||||
define([
|
||||
'/common/requireconfig.js'
|
||||
], function (RequireConfig) {
|
||||
'/common/requireconfig.js',
|
||||
'/common/sframe-channel.js'
|
||||
], function (RequireConfig, SFrameChannel) {
|
||||
require.config(RequireConfig);
|
||||
console.log('boot2');
|
||||
// most of CryptPad breaks if you don't support isArray
|
||||
|
@ -22,5 +23,7 @@ console.log('boot2');
|
|||
window.__defineGetter__('localStorage', function () { return mkFakeStore(); });
|
||||
window.__defineGetter__('sessionStorage', function () { return mkFakeStore(); });
|
||||
|
||||
SFrameChannel.init(window.top, function () { });
|
||||
|
||||
require([document.querySelector('script[data-bootload]').getAttribute('data-bootload')]);
|
||||
});
|
||||
|
|
|
@ -15,39 +15,17 @@
|
|||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
define([
|
||||
'/bower_components/netflux-websocket/netflux-client.js',
|
||||
'/common/sframe-channel.js',
|
||||
'/bower_components/chainpad/chainpad.dist.js',
|
||||
], function (Netflux) {
|
||||
], function (SFrameChannel) {
|
||||
var ChainPad = window.ChainPad;
|
||||
var USE_HISTORY = true;
|
||||
var module = { exports: {} };
|
||||
|
||||
var verbose = function (x) { console.log(x); };
|
||||
verbose = function () {}; // comment out to enable verbose logging
|
||||
|
||||
var unBencode = function (str) { return str.replace(/^\d+:/, ''); };
|
||||
|
||||
module.exports.start = function (config) {
|
||||
console.log(config);
|
||||
var websocketUrl = config.websocketURL;
|
||||
var userName = config.userName;
|
||||
var channel = config.channel;
|
||||
var Crypto = config.crypto;
|
||||
var validateKey = config.validateKey;
|
||||
var readOnly = config.readOnly || false;
|
||||
|
||||
// make sure configuration is defined
|
||||
config = config || {};
|
||||
|
||||
var initializing = true;
|
||||
var toReturn = {};
|
||||
var messagesHistory = [];
|
||||
var chainpadAdapter = {};
|
||||
var realtime;
|
||||
var network = config.network;
|
||||
var lastKnownHash;
|
||||
|
||||
var userList = {
|
||||
var mkUserList = function () {
|
||||
var userList = Object.freeze({
|
||||
change : [],
|
||||
onChange : function(newData) {
|
||||
userList.change.forEach(function (el) {
|
||||
|
@ -55,9 +33,9 @@ define([
|
|||
});
|
||||
},
|
||||
users: []
|
||||
};
|
||||
});
|
||||
|
||||
var onJoining = function(peer) {
|
||||
var onJoining = function (peer) {
|
||||
if(peer.length !== 32) { return; }
|
||||
var list = userList.users;
|
||||
var index = list.indexOf(peer);
|
||||
|
@ -67,100 +45,8 @@ define([
|
|||
userList.onChange();
|
||||
};
|
||||
|
||||
var onReady = function(wc, network) {
|
||||
// Trigger onReady only if not ready yet. This is important because the history keeper sends a direct
|
||||
// message through "network" when it is synced, and it triggers onReady for each channel joined.
|
||||
if (!initializing) { return; }
|
||||
|
||||
realtime.start();
|
||||
|
||||
if(config.setMyID) {
|
||||
config.setMyID({
|
||||
myID: wc.myID
|
||||
});
|
||||
}
|
||||
// Trigger onJoining with our own Cryptpad username to tell the toolbar that we are synced
|
||||
if (!readOnly) {
|
||||
onJoining(wc.myID);
|
||||
}
|
||||
|
||||
// we're fully synced
|
||||
initializing = false;
|
||||
|
||||
if (config.onReady) {
|
||||
config.onReady({
|
||||
realtime: realtime,
|
||||
network: network,
|
||||
userList: userList,
|
||||
myId: wc.myID,
|
||||
leave: wc.leave
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
var onMessage = function(peer, msg, wc, network, direct) {
|
||||
// unpack the history keeper from the webchannel
|
||||
var hk = network.historyKeeper;
|
||||
|
||||
// Old server
|
||||
if(wc && (msg === 0 || msg === '0')) {
|
||||
onReady(wc, network);
|
||||
return;
|
||||
}
|
||||
if (direct && peer !== hk) {
|
||||
return;
|
||||
}
|
||||
if (direct) {
|
||||
var parsed = JSON.parse(msg);
|
||||
if (parsed.validateKey && parsed.channel) {
|
||||
if (parsed.channel === wc.id && !validateKey) {
|
||||
validateKey = parsed.validateKey;
|
||||
}
|
||||
// We have to return even if it is not the current channel:
|
||||
// we don't want to continue with other channels messages here
|
||||
return;
|
||||
}
|
||||
if (parsed.state && parsed.state === 1 && parsed.channel) {
|
||||
if (parsed.channel === wc.id) {
|
||||
onReady(wc, network);
|
||||
}
|
||||
// We have to return even if it is not the current channel:
|
||||
// we don't want to continue with other channels messages here
|
||||
return;
|
||||
}
|
||||
}
|
||||
// The history keeper is different for each channel :
|
||||
// no need to check if the message is related to the current channel
|
||||
if (peer === hk){
|
||||
// if the peer is the 'history keeper', extract their message
|
||||
var parsed1 = JSON.parse(msg);
|
||||
msg = parsed1[4];
|
||||
// Check that this is a message for us
|
||||
if (parsed1[3] !== wc.id) { return; }
|
||||
}
|
||||
|
||||
lastKnownHash = msg.slice(0,64);
|
||||
var message = chainpadAdapter.msgIn(peer, msg);
|
||||
|
||||
verbose(message);
|
||||
|
||||
if (!initializing) {
|
||||
if (config.onLocal) {
|
||||
config.onLocal();
|
||||
}
|
||||
}
|
||||
|
||||
// slice off the bencoded header
|
||||
// Why are we getting bencoded stuff to begin with?
|
||||
// FIXME this shouldn't be necessary
|
||||
message = unBencode(message);//.slice(message.indexOf(':[') + 1);
|
||||
|
||||
// pass the message into Chainpad
|
||||
realtime.message(message);
|
||||
};
|
||||
|
||||
// update UI components to show that one of the other peers has left
|
||||
var onLeaving = function(peer) {
|
||||
var onLeaving = function (peer) {
|
||||
var list = userList.users;
|
||||
var index = list.indexOf(peer);
|
||||
if(index !== -1) {
|
||||
|
@ -169,246 +55,93 @@ define([
|
|||
userList.onChange();
|
||||
};
|
||||
|
||||
// shim between chainpad and netflux
|
||||
chainpadAdapter = {
|
||||
msgIn : function(peerId, msg) {
|
||||
msg = msg.replace(/^cp\|/, '');
|
||||
try {
|
||||
var decryptedMsg = Crypto.decrypt(msg, validateKey);
|
||||
messagesHistory.push(decryptedMsg);
|
||||
return decryptedMsg;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
return msg;
|
||||
}
|
||||
},
|
||||
msgOut : function(msg) {
|
||||
if (readOnly) { return; }
|
||||
try {
|
||||
var cmsg = Crypto.encrypt(msg);
|
||||
if (msg.indexOf('[4') === 0) { cmsg = 'cp|' + cmsg; }
|
||||
return cmsg;
|
||||
} catch (err) {
|
||||
console.log(msg);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
var onReset = function () {
|
||||
userList.users.forEach(onLeaving);
|
||||
};
|
||||
|
||||
var createRealtime = function() {
|
||||
return ChainPad.create({
|
||||
userName: userName,
|
||||
initialState: config.initialState,
|
||||
transformFunction: config.transformFunction,
|
||||
validateContent: config.validateContent,
|
||||
avgSyncMilliseconds: config.avgSyncMilliseconds,
|
||||
logLevel: typeof(config.logLevel) !== 'undefined'? config.logLevel : 1
|
||||
});
|
||||
};
|
||||
|
||||
// We use an object to store the webchannel so that we don't have to push new handlers to chainpad
|
||||
// and remove the old ones when reconnecting and keeping the same 'realtime' object
|
||||
// See realtime.onMessage below: we call wc.bcast(...) but wc may change
|
||||
var wcObject = {};
|
||||
var onOpen = function(wc, network, initialize) {
|
||||
wcObject.wc = wc;
|
||||
channel = wc.id;
|
||||
|
||||
// Add the existing peers in the userList
|
||||
wc.members.forEach(onJoining);
|
||||
|
||||
// Add the handlers to the WebChannel
|
||||
wc.on('message', function (msg, sender) { //Channel msg
|
||||
onMessage(sender, msg, wc, network);
|
||||
});
|
||||
wc.on('join', onJoining);
|
||||
wc.on('leave', onLeaving);
|
||||
|
||||
if (initialize) {
|
||||
toReturn.realtime = realtime = createRealtime();
|
||||
|
||||
realtime._patch = realtime.patch;
|
||||
realtime.patch = function (patch, x, y) {
|
||||
if (initializing) {
|
||||
console.error("attempted to change the content before chainpad was synced");
|
||||
}
|
||||
return realtime._patch(patch, x, y);
|
||||
};
|
||||
realtime._change = realtime.change;
|
||||
realtime.change = function (offset, count, chars) {
|
||||
if (initializing) {
|
||||
console.error("attempted to change the content before chainpad was synced");
|
||||
}
|
||||
return realtime._change(offset, count, chars);
|
||||
};
|
||||
|
||||
if (config.onInit) {
|
||||
config.onInit({
|
||||
myID: wc.myID,
|
||||
realtime: realtime,
|
||||
getLag: network.getLag,
|
||||
userList: userList,
|
||||
network: network,
|
||||
channel: channel
|
||||
});
|
||||
}
|
||||
|
||||
// Sending a message...
|
||||
realtime.onMessage(function(message, cb) {
|
||||
// Filter messages sent by Chainpad to make it compatible with Netflux
|
||||
message = chainpadAdapter.msgOut(message);
|
||||
if(message) {
|
||||
// Do not remove wcObject, it allows us to use a new 'wc' without changing the handler if we
|
||||
// want to keep the same chainpad (realtime) object
|
||||
wcObject.wc.bcast(message).then(function() {
|
||||
cb();
|
||||
}, function(err) {
|
||||
// The message has not been sent, display the error.
|
||||
console.error(err);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
realtime.onPatch(function () {
|
||||
if (config.onRemote) {
|
||||
config.onRemote({
|
||||
realtime: realtime
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Get the channel history
|
||||
if(USE_HISTORY) {
|
||||
var hk;
|
||||
|
||||
wc.members.forEach(function (p) {
|
||||
if (p.length === 16) { hk = p; }
|
||||
});
|
||||
network.historyKeeper = hk;
|
||||
|
||||
var msg = ['GET_HISTORY', wc.id];
|
||||
// Add the validateKey if we are the channel creator and we have a validateKey
|
||||
msg.push(validateKey);
|
||||
msg.push(lastKnownHash);
|
||||
if (hk) { network.sendto(hk, JSON.stringify(msg)); }
|
||||
}
|
||||
else {
|
||||
onReady(wc, network);
|
||||
}
|
||||
};
|
||||
|
||||
// Set a flag to avoid calling onAbort or onConnectionChange when the user is leaving the page
|
||||
var isIntentionallyLeaving = false;
|
||||
window.addEventListener("beforeunload", function () {
|
||||
isIntentionallyLeaving = true;
|
||||
return Object.freeze({
|
||||
list: userList,
|
||||
onJoin: onJoining,
|
||||
onLeave: onLeaving,
|
||||
onReset: onReset
|
||||
});
|
||||
};
|
||||
|
||||
var findChannelById = function(webChannels, channelId) {
|
||||
var webChannel;
|
||||
module.exports.start = function (config) {
|
||||
var onConnectionChange = config.onConnectionChange || function () { };
|
||||
var onRemote = config.onRemote || function () { };
|
||||
var onInit = config.onInit || function () { };
|
||||
var onLocal = config.onLocal || function () { };
|
||||
var setMyID = config.setMyID || function () { };
|
||||
var onReady = config.onReady || function () { };
|
||||
var userName = config.userName;
|
||||
var initialState = config.initialState;
|
||||
var transformFunction = config.transformFunction;
|
||||
var validateContent = config.validateContent;
|
||||
var avgSyncMilliseconds = config.avgSyncMilliseconds;
|
||||
var logLevel = typeof(config.logLevel) !== 'undefined'? config.logLevel : 1;
|
||||
var readOnly = config.readOnly || false;
|
||||
config = undefined;
|
||||
|
||||
// Array.some terminates once a truthy value is returned
|
||||
// best case is faster than forEach, though webchannel arrays seem
|
||||
// to consistently have a length of 1
|
||||
webChannels.some(function(chan) {
|
||||
if(chan.id === channelId) { webChannel = chan; return true;}
|
||||
var chainpad;
|
||||
var userList = mkUserList();
|
||||
var myID;
|
||||
var isReady = false;
|
||||
|
||||
SFrameChannel.on('EV_RT_JOIN', userList.onJoin);
|
||||
SFrameChannel.on('EV_RT_LEAVE', userList.onLeave);
|
||||
SFrameChannel.on('EV_RT_DISCONNECT', function () {
|
||||
isReady = false;
|
||||
userList.onReset();
|
||||
onConnectionChange({ state: false });
|
||||
});
|
||||
SFrameChannel.on('EV_RT_CONNECT', function (content) {
|
||||
content.members.forEach(userList.onJoin);
|
||||
myID = content.myID;
|
||||
isReady = false;
|
||||
if (chainpad) {
|
||||
// it's a reconnect
|
||||
onConnectionChange({ state: true, myId: myID });
|
||||
return;
|
||||
}
|
||||
chainpad = ChainPad.create({
|
||||
userName: userName,
|
||||
initialState: initialState,
|
||||
transformFunction: transformFunction,
|
||||
validateContent: validateContent,
|
||||
avgSyncMilliseconds: avgSyncMilliseconds,
|
||||
logLevel: logLevel
|
||||
});
|
||||
return webChannel;
|
||||
};
|
||||
|
||||
var onConnectError = function (err) {
|
||||
if (config.onError) {
|
||||
config.onError({
|
||||
error: err.type
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
var joinSession = function (endPoint, cb) {
|
||||
// a websocket URL has been provided
|
||||
// connect to it with Netflux.
|
||||
if (typeof(endPoint) === 'string') {
|
||||
Netflux.connect(endPoint).then(cb, onConnectError);
|
||||
} else if (typeof(endPoint.then) === 'function') {
|
||||
// a netflux network promise was provided
|
||||
// connect to it and use a channel
|
||||
endPoint.then(cb, onConnectError);
|
||||
} else {
|
||||
// assume it's a network and try to connect.
|
||||
cb(endPoint);
|
||||
}
|
||||
};
|
||||
|
||||
var firstConnection = true;
|
||||
/* Connect to the Netflux network, or fall back to a WebSocket
|
||||
in theory this lets us connect to more netflux channels using only
|
||||
one network. */
|
||||
var connectTo = function (network) {
|
||||
// join the netflux network, promise to handle opening of the channel
|
||||
network.join(channel || null).then(function(wc) {
|
||||
onOpen(wc, network, firstConnection);
|
||||
firstConnection = false;
|
||||
}, function(error) {
|
||||
console.error(error);
|
||||
chainpad.onMessage(function(message, cb) {
|
||||
SFrameChannel.query('Q_RT_MESSAGE', message, cb);
|
||||
});
|
||||
};
|
||||
|
||||
joinSession(network || websocketUrl, function (network) {
|
||||
// pass messages that come out of netflux into our local handler
|
||||
if (firstConnection) {
|
||||
toReturn.network = network;
|
||||
|
||||
network.on('disconnect', function (reason) {
|
||||
if (isIntentionallyLeaving) { return; }
|
||||
if (reason === "network.disconnect() called") { return; }
|
||||
if (config.onConnectionChange) {
|
||||
config.onConnectionChange({
|
||||
state: false
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (config.onAbort) {
|
||||
config.onAbort({
|
||||
reason: reason
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
network.on('reconnect', function (uid) {
|
||||
if (config.onConnectionChange) {
|
||||
config.onConnectionChange({
|
||||
state: true,
|
||||
myId: uid
|
||||
});
|
||||
var afterReconnecting = function () {
|
||||
initializing = true;
|
||||
userList.users=[];
|
||||
joinSession(network, connectTo);
|
||||
};
|
||||
if (config.beforeReconnecting) {
|
||||
config.beforeReconnecting(function (newKey, newContent) {
|
||||
channel = newKey;
|
||||
config.initialState = newContent;
|
||||
afterReconnecting();
|
||||
});
|
||||
return;
|
||||
}
|
||||
afterReconnecting();
|
||||
}
|
||||
});
|
||||
|
||||
network.on('message', function (msg, sender) { // Direct message
|
||||
var wchan = findChannelById(network.webChannels, channel);
|
||||
if(wchan) {
|
||||
onMessage(sender, msg, wchan, network, true);
|
||||
}
|
||||
});
|
||||
chainpad.onPatch(function () {
|
||||
onRemote({ realtime: chainpad });
|
||||
});
|
||||
onInit({
|
||||
myID: content.myID,
|
||||
realtime: chainpad,
|
||||
userList: userList,
|
||||
readOnly: readOnly
|
||||
});
|
||||
});
|
||||
SFrameChannel.on('Q_RT_MESSAGE', function (content, cb) {
|
||||
if (isReady) {
|
||||
onLocal(); // should be onBeforeMessage
|
||||
}
|
||||
|
||||
connectTo(network);
|
||||
}, onConnectError);
|
||||
|
||||
return toReturn;
|
||||
chainpad.message(content);
|
||||
cb('OK');
|
||||
});
|
||||
SFrameChannel.on('EV_RT_READY', function () {
|
||||
if (isReady) { return; }
|
||||
isReady = true;
|
||||
chainpad.start();
|
||||
setMyID({ myID: myID });
|
||||
// Trigger onJoining with our own Cryptpad username to tell the toolbar that we are synced
|
||||
if (!readOnly) { userList.onJoin(myID); }
|
||||
onReady({ realtime: chainpad });
|
||||
});
|
||||
return;
|
||||
};
|
||||
return module.exports;
|
||||
});
|
||||
});
|
|
@ -15,10 +15,8 @@
|
|||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
define([
|
||||
'/bower_components/netflux-websocket/netflux-client.js',
|
||||
'/bower_components/chainpad/chainpad.dist.js',
|
||||
], function (Netflux) {
|
||||
var ChainPad = window.ChainPad;
|
||||
'/common/sframe-channel.js',
|
||||
], function (SFrameChannel) {
|
||||
var USE_HISTORY = true;
|
||||
var module = { exports: {} };
|
||||
|
||||
|
@ -27,50 +25,53 @@ define([
|
|||
|
||||
var unBencode = function (str) { return str.replace(/^\d+:/, ''); };
|
||||
|
||||
module.exports.start = function (conf) {
|
||||
var websocketUrl = conf.websocketURL;
|
||||
var userName = conf.userName;
|
||||
var start = function (conf) {
|
||||
var channel = conf.channel;
|
||||
var Crypto = conf.crypto;
|
||||
var validateKey = conf.validateKey;
|
||||
var readOnly = conf.readOnly || false;
|
||||
var websocketURL = conf.websocketURL;
|
||||
var network = conf.network;
|
||||
conf = undefined;
|
||||
|
||||
var initializing = true;
|
||||
var toReturn = {};
|
||||
var messagesHistory = [];
|
||||
var chainpadAdapter = {};
|
||||
var realtime;
|
||||
var lastKnownHash;
|
||||
|
||||
var onReady = function(wc, network) {
|
||||
var queue = [];
|
||||
var messageFromInner = function (m, cb) { queue.push([ m, cb ]); };
|
||||
SFrameChannel.on('Q_RT_MESSAGE', function (message, cb) {
|
||||
messageFromInner(message, cb);
|
||||
});
|
||||
|
||||
var onReady = function(wc) {
|
||||
// Trigger onReady only if not ready yet. This is important because the history keeper sends a direct
|
||||
// message through "network" when it is synced, and it triggers onReady for each channel joined.
|
||||
if (!initializing) { return; }
|
||||
|
||||
realtime.start();
|
||||
|
||||
if(setMyID) {
|
||||
setMyID({ myID: wc.myID });
|
||||
}
|
||||
// Trigger onJoining with our own Cryptpad username to tell the toolbar that we are synced
|
||||
if (!readOnly) {
|
||||
onJoining(wc.myID);
|
||||
}
|
||||
|
||||
SFrameChannel.event('EV_RT_READY', null);
|
||||
// we're fully synced
|
||||
initializing = false;
|
||||
};
|
||||
|
||||
if (config.onReady) {
|
||||
config.onReady({
|
||||
realtime: realtime,
|
||||
network: network,
|
||||
userList: userList,
|
||||
myId: wc.myID,
|
||||
leave: wc.leave
|
||||
});
|
||||
// shim between chainpad and netflux
|
||||
var msgIn = function (peerId, msg) {
|
||||
msg = msg.replace(/^cp\|/, '');
|
||||
try {
|
||||
var decryptedMsg = Crypto.decrypt(msg, validateKey);
|
||||
return decryptedMsg;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
return msg;
|
||||
}
|
||||
};
|
||||
|
||||
var msgOut = function (msg) {
|
||||
if (readOnly) { return; }
|
||||
try {
|
||||
var cmsg = Crypto.encrypt(msg);
|
||||
if (msg.indexOf('[4') === 0) { cmsg = 'cp|' + cmsg; }
|
||||
return cmsg;
|
||||
} catch (err) {
|
||||
console.log(msg);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -78,11 +79,6 @@ define([
|
|||
// unpack the history keeper from the webchannel
|
||||
var hk = network.historyKeeper;
|
||||
|
||||
// Old server
|
||||
if(wc && (msg === 0 || msg === '0')) {
|
||||
onReady(wc, network);
|
||||
return;
|
||||
}
|
||||
if (direct && peer !== hk) {
|
||||
return;
|
||||
}
|
||||
|
@ -98,7 +94,7 @@ define([
|
|||
}
|
||||
if (parsed.state && parsed.state === 1 && parsed.channel) {
|
||||
if (parsed.channel === wc.id) {
|
||||
onReady(wc, network);
|
||||
onReady(wc);
|
||||
}
|
||||
// We have to return even if it is not the current channel:
|
||||
// we don't want to continue with other channels messages here
|
||||
|
@ -107,7 +103,7 @@ define([
|
|||
}
|
||||
// The history keeper is different for each channel :
|
||||
// no need to check if the message is related to the current channel
|
||||
if (peer === hk){
|
||||
if (peer === hk) {
|
||||
// if the peer is the 'history keeper', extract their message
|
||||
var parsed1 = JSON.parse(msg);
|
||||
msg = parsed1[4];
|
||||
|
@ -116,146 +112,58 @@ define([
|
|||
}
|
||||
|
||||
lastKnownHash = msg.slice(0,64);
|
||||
var message = chainpadAdapter.msgIn(peer, msg);
|
||||
var message = msgIn(peer, msg);
|
||||
|
||||
verbose(message);
|
||||
|
||||
if (!initializing) {
|
||||
if (config.onLocal) {
|
||||
config.onLocal();
|
||||
}
|
||||
}
|
||||
|
||||
// slice off the bencoded header
|
||||
// Why are we getting bencoded stuff to begin with?
|
||||
// FIXME this shouldn't be necessary
|
||||
message = unBencode(message);//.slice(message.indexOf(':[') + 1);
|
||||
|
||||
// pass the message into Chainpad
|
||||
realtime.message(message);
|
||||
};
|
||||
|
||||
// update UI components to show that one of the other peers has left
|
||||
var onLeaving = function(peer) {
|
||||
var list = userList.users;
|
||||
var index = list.indexOf(peer);
|
||||
if(index !== -1) {
|
||||
userList.users.splice(index, 1);
|
||||
}
|
||||
userList.onChange();
|
||||
};
|
||||
|
||||
// shim between chainpad and netflux
|
||||
chainpadAdapter = {
|
||||
msgIn : function(peerId, msg) {
|
||||
msg = msg.replace(/^cp\|/, '');
|
||||
try {
|
||||
var decryptedMsg = Crypto.decrypt(msg, validateKey);
|
||||
messagesHistory.push(decryptedMsg);
|
||||
return decryptedMsg;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
return msg;
|
||||
}
|
||||
},
|
||||
msgOut : function(msg) {
|
||||
if (readOnly) { return; }
|
||||
try {
|
||||
var cmsg = Crypto.encrypt(msg);
|
||||
if (msg.indexOf('[4') === 0) { cmsg = 'cp|' + cmsg; }
|
||||
return cmsg;
|
||||
} catch (err) {
|
||||
console.log(msg);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var createRealtime = function() {
|
||||
return ChainPad.create({
|
||||
userName: userName,
|
||||
initialState: config.initialState,
|
||||
transformFunction: config.transformFunction,
|
||||
validateContent: config.validateContent,
|
||||
avgSyncMilliseconds: config.avgSyncMilliseconds,
|
||||
logLevel: typeof(config.logLevel) !== 'undefined'? config.logLevel : 1
|
||||
});
|
||||
SFrameChannel.query('Q_RT_MESSAGE', message, function () { });
|
||||
};
|
||||
|
||||
// We use an object to store the webchannel so that we don't have to push new handlers to chainpad
|
||||
// and remove the old ones when reconnecting and keeping the same 'realtime' object
|
||||
// See realtime.onMessage below: we call wc.bcast(...) but wc may change
|
||||
var wcObject = {};
|
||||
var onOpen = function(wc, network, initialize) {
|
||||
var onOpen = function(wc, network, firstConnection) {
|
||||
wcObject.wc = wc;
|
||||
channel = wc.id;
|
||||
|
||||
// Add the existing peers in the userList
|
||||
wc.members.forEach(onJoining);
|
||||
SFrameChannel.event('EV_RT_CONNECT', { myID: wc.myID, members: wc.members, readOnly: readOnly });
|
||||
|
||||
// Add the handlers to the WebChannel
|
||||
wc.on('message', function (msg, sender) { //Channel msg
|
||||
onMessage(sender, msg, wc, network);
|
||||
});
|
||||
wc.on('join', onJoining);
|
||||
wc.on('leave', onLeaving);
|
||||
|
||||
if (initialize) {
|
||||
toReturn.realtime = realtime = createRealtime();
|
||||
|
||||
realtime._patch = realtime.patch;
|
||||
realtime.patch = function (patch, x, y) {
|
||||
if (initializing) {
|
||||
console.error("attempted to change the content before chainpad was synced");
|
||||
}
|
||||
return realtime._patch(patch, x, y);
|
||||
};
|
||||
realtime._change = realtime.change;
|
||||
realtime.change = function (offset, count, chars) {
|
||||
if (initializing) {
|
||||
console.error("attempted to change the content before chainpad was synced");
|
||||
}
|
||||
return realtime._change(offset, count, chars);
|
||||
};
|
||||
|
||||
if (config.onInit) {
|
||||
config.onInit({
|
||||
myID: wc.myID,
|
||||
realtime: realtime,
|
||||
getLag: network.getLag,
|
||||
userList: userList,
|
||||
network: network,
|
||||
channel: channel
|
||||
});
|
||||
}
|
||||
wc.on('join', function (m) { SFrameChannel.event('EV_RT_JOIN', m); });
|
||||
wc.on('leave', function (m) { SFrameChannel.event('EV_RT_LEAVE', m); });
|
||||
|
||||
if (firstConnection) {
|
||||
// Sending a message...
|
||||
realtime.onMessage(function(message, cb) {
|
||||
messageFromInner = function(message, cb) {
|
||||
// Filter messages sent by Chainpad to make it compatible with Netflux
|
||||
message = chainpadAdapter.msgOut(message);
|
||||
if(message) {
|
||||
message = msgOut(message);
|
||||
if (message) {
|
||||
// Do not remove wcObject, it allows us to use a new 'wc' without changing the handler if we
|
||||
// want to keep the same chainpad (realtime) object
|
||||
wcObject.wc.bcast(message).then(function() {
|
||||
cb();
|
||||
cb('OK');
|
||||
}, function(err) {
|
||||
// The message has not been sent, display the error.
|
||||
console.error(err);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
realtime.onPatch(function () {
|
||||
if (config.onRemote) {
|
||||
config.onRemote({
|
||||
realtime: realtime
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
queue.forEach(function (arr) { messageFromInner(arr[0], arr[1]); });
|
||||
}
|
||||
|
||||
// Get the channel history
|
||||
if(USE_HISTORY) {
|
||||
if (USE_HISTORY) {
|
||||
var hk;
|
||||
|
||||
wc.members.forEach(function (p) {
|
||||
|
@ -268,19 +176,17 @@ define([
|
|||
msg.push(validateKey);
|
||||
msg.push(lastKnownHash);
|
||||
if (hk) { network.sendto(hk, JSON.stringify(msg)); }
|
||||
}
|
||||
else {
|
||||
onReady(wc, network);
|
||||
} else {
|
||||
onReady(wc);
|
||||
}
|
||||
};
|
||||
|
||||
// Set a flag to avoid calling onAbort or onConnectionChange when the user is leaving the page
|
||||
var isIntentionallyLeaving = false;
|
||||
window.addEventListener("beforeunload", function () {
|
||||
isIntentionallyLeaving = true;
|
||||
});
|
||||
|
||||
var findChannelById = function(webChannels, channelId) {
|
||||
var findChannelById = function (webChannels, channelId) {
|
||||
var webChannel;
|
||||
|
||||
// Array.some terminates once a truthy value is returned
|
||||
|
@ -292,99 +198,39 @@ define([
|
|||
return webChannel;
|
||||
};
|
||||
|
||||
var onConnectError = function (err) {
|
||||
if (config.onError) {
|
||||
config.onError({
|
||||
error: err.type
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
var joinSession = function (endPoint, cb) {
|
||||
// a websocket URL has been provided
|
||||
// connect to it with Netflux.
|
||||
if (typeof(endPoint) === 'string') {
|
||||
Netflux.connect(endPoint).then(cb, onConnectError);
|
||||
} else if (typeof(endPoint.then) === 'function') {
|
||||
// a netflux network promise was provided
|
||||
// connect to it and use a channel
|
||||
endPoint.then(cb, onConnectError);
|
||||
} else {
|
||||
// assume it's a network and try to connect.
|
||||
cb(endPoint);
|
||||
}
|
||||
};
|
||||
|
||||
var firstConnection = true;
|
||||
/* Connect to the Netflux network, or fall back to a WebSocket
|
||||
in theory this lets us connect to more netflux channels using only
|
||||
one network. */
|
||||
var connectTo = function (network) {
|
||||
var connectTo = function (network, firstConnection) {
|
||||
// join the netflux network, promise to handle opening of the channel
|
||||
network.join(channel || null).then(function(wc) {
|
||||
onOpen(wc, network, firstConnection);
|
||||
firstConnection = false;
|
||||
}, function(error) {
|
||||
console.error(error);
|
||||
});
|
||||
};
|
||||
|
||||
joinSession(network || websocketUrl, function (network) {
|
||||
// pass messages that come out of netflux into our local handler
|
||||
if (firstConnection) {
|
||||
toReturn.network = network;
|
||||
network.on('disconnect', function (reason) {
|
||||
if (isIntentionallyLeaving) { return; }
|
||||
if (reason === "network.disconnect() called") { return; }
|
||||
SFrameChannel.event('EV_RT_DISCONNECT');
|
||||
});
|
||||
|
||||
network.on('disconnect', function (reason) {
|
||||
if (isIntentionallyLeaving) { return; }
|
||||
if (reason === "network.disconnect() called") { return; }
|
||||
if (config.onConnectionChange) {
|
||||
config.onConnectionChange({
|
||||
state: false
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (config.onAbort) {
|
||||
config.onAbort({
|
||||
reason: reason
|
||||
});
|
||||
}
|
||||
});
|
||||
network.on('reconnect', function (uid) {
|
||||
initializing = true;
|
||||
connectTo(network, false);
|
||||
});
|
||||
|
||||
network.on('reconnect', function (uid) {
|
||||
if (config.onConnectionChange) {
|
||||
config.onConnectionChange({
|
||||
state: true,
|
||||
myId: uid
|
||||
});
|
||||
var afterReconnecting = function () {
|
||||
initializing = true;
|
||||
userList.users=[];
|
||||
joinSession(network, connectTo);
|
||||
};
|
||||
if (config.beforeReconnecting) {
|
||||
config.beforeReconnecting(function (newKey, newContent) {
|
||||
channel = newKey;
|
||||
config.initialState = newContent;
|
||||
afterReconnecting();
|
||||
});
|
||||
return;
|
||||
}
|
||||
afterReconnecting();
|
||||
}
|
||||
});
|
||||
|
||||
network.on('message', function (msg, sender) { // Direct message
|
||||
var wchan = findChannelById(network.webChannels, channel);
|
||||
if(wchan) {
|
||||
onMessage(sender, msg, wchan, network, true);
|
||||
}
|
||||
});
|
||||
network.on('message', function (msg, sender) { // Direct message
|
||||
var wchan = findChannelById(network.webChannels, channel);
|
||||
if (wchan) {
|
||||
onMessage(sender, msg, wchan, network, true);
|
||||
}
|
||||
});
|
||||
|
||||
connectTo(network);
|
||||
}, onConnectError);
|
||||
|
||||
return toReturn;
|
||||
connectTo(network, true);
|
||||
};
|
||||
|
||||
return {
|
||||
start: function (config) {
|
||||
SFrameChannel.whenReg('EV_RT_READY', function () { start(config); });
|
||||
}
|
||||
};
|
||||
return module.exports;
|
||||
});
|
||||
|
|
|
@ -1,38 +1,108 @@
|
|||
// This file provides the internal API for talking from inside of the sandbox iframe
|
||||
// The external API is in sframe-ctrl.js
|
||||
define([], function () {
|
||||
var iframe;
|
||||
// This file provides the API for the channel for talking to and from the sandbox iframe.
|
||||
define([
|
||||
'/common/sframe-protocol.js'
|
||||
], function (SFrameProtocol) {
|
||||
var otherWindow;
|
||||
var handlers = {};
|
||||
var queries = {};
|
||||
|
||||
// list of handlers which are registered from the other side...
|
||||
var insideHandlers = [];
|
||||
var callWhenRegistered = {};
|
||||
|
||||
var module = { exports: {} };
|
||||
|
||||
var mkTxid = function () {
|
||||
return Math.random().toString(16).replace('0.', '') + Math.random().toString(16).replace('0.', '');
|
||||
};
|
||||
|
||||
module.exports.init = function (ow, cb) {
|
||||
if (otherWindow) { throw new Error('already initialized'); }
|
||||
var intr;
|
||||
var txid;
|
||||
window.addEventListener('message', function (msg) {
|
||||
var data = JSON.parse(msg.data);
|
||||
if (ow !== msg.source) {
|
||||
console.log("DROP Message from unexpected source");
|
||||
console.log(msg);
|
||||
} else if (!otherWindow) {
|
||||
if (data.txid !== txid) {
|
||||
console.log("DROP Message with weird txid");
|
||||
return;
|
||||
}
|
||||
clearInterval(intr);
|
||||
otherWindow = ow;
|
||||
cb();
|
||||
} else if (typeof(data.q) === 'string' && handlers[data.q]) {
|
||||
handlers[data.q](data, msg);
|
||||
} else if (typeof(data.q) === 'undefined' && queries[data.txid]) {
|
||||
queries[data.txid](data, msg);
|
||||
} else if (data.txid === txid) {
|
||||
// stray message from init
|
||||
return;
|
||||
} else {
|
||||
console.log("DROP Unhandled message");
|
||||
console.log(msg);
|
||||
}
|
||||
});
|
||||
if (window !== window.top) {
|
||||
// we're in the sandbox
|
||||
otherWindow = ow;
|
||||
cb();
|
||||
} else {
|
||||
require(['/common/requireconfig.js'], function (RequireConfig) {
|
||||
txid = mkTxid();
|
||||
intr = setInterval(function () {
|
||||
ow.postMessage(JSON.stringify({
|
||||
txid: txid,
|
||||
content: { requireConf: RequireConfig },
|
||||
q: 'INIT'
|
||||
}), '*');
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
module.exports.query = function (q, content, cb) {
|
||||
if (!iframe) { throw new Error('not yet initialized'); }
|
||||
if (!otherWindow) { throw new Error('not yet initialized'); }
|
||||
if (!SFrameProtocol[q]) {
|
||||
throw new Error('please only make queries are defined in sframe-protocol.js');
|
||||
}
|
||||
var txid = mkTxid();
|
||||
var timeout = setTimeout(function () {
|
||||
delete queries[txid];
|
||||
cb("Timeout making query " + q);
|
||||
});
|
||||
console.log("Timeout making query " + q);
|
||||
}, 30000);
|
||||
queries[txid] = function (data, msg) {
|
||||
clearTimeout(timeout);
|
||||
delete queries[txid];
|
||||
cb(undefined, data.content, msg);
|
||||
};
|
||||
iframe.contentWindow.postMessage(JSON.stringify({
|
||||
otherWindow.postMessage(JSON.stringify({
|
||||
txid: txid,
|
||||
content: content,
|
||||
q: q
|
||||
}), '*');
|
||||
};
|
||||
|
||||
module.exports.registerHandler = function (queryType, handler) {
|
||||
var event = module.exports.event = function (e, content) {
|
||||
if (!otherWindow) { throw new Error('not yet initialized'); }
|
||||
if (!SFrameProtocol[e]) {
|
||||
throw new Error('please only fire events that are defined in sframe-protocol.js');
|
||||
}
|
||||
if (e.indexOf('EV_') !== 0) {
|
||||
throw new Error('please only use events (starting with EV_) for event messages');
|
||||
}
|
||||
otherWindow.postMessage(JSON.stringify({ content: content, q: e }), '*');
|
||||
};
|
||||
|
||||
module.exports.on = function (queryType, handler) {
|
||||
if (!otherWindow) { throw new Error('not yet initialized'); }
|
||||
if (typeof(handlers[queryType]) !== 'undefined') { throw new Error('already registered'); }
|
||||
handlers[queryType] = function (msg) {
|
||||
var data = JSON.parse(msg.data);
|
||||
if (!SFrameProtocol[queryType]) {
|
||||
throw new Error('please only register handlers which are defined in sframe-protocol.js');
|
||||
}
|
||||
handlers[queryType] = function (data, msg) {
|
||||
handler(data.content, function (replyContent) {
|
||||
msg.source.postMessage(JSON.stringify({
|
||||
txid: data.txid,
|
||||
|
@ -40,7 +110,28 @@ define([], function () {
|
|||
}), '*');
|
||||
}, msg);
|
||||
};
|
||||
event('EV_REGISTER_HANDLER', queryType);
|
||||
};
|
||||
|
||||
module.exports.whenReg = function (queryType, handler) {
|
||||
if (!otherWindow) { throw new Error('not yet initialized'); }
|
||||
if (!SFrameProtocol[queryType]) {
|
||||
throw new Error('please only register handlers which are defined in sframe-protocol.js');
|
||||
}
|
||||
if (insideHandlers.indexOf(queryType) > -1) {
|
||||
handler();
|
||||
} else {
|
||||
(callWhenRegistered[queryType] = callWhenRegistered[queryType] || []).push(handler);
|
||||
}
|
||||
};
|
||||
|
||||
handlers['EV_REGISTER_HANDLER'] = function (data) {
|
||||
if (callWhenRegistered[data.content]) {
|
||||
callWhenRegistered[data.content].forEach(function (f) { f(); });
|
||||
delete callWhenRegistered[data.content];
|
||||
}
|
||||
insideHandlers.push(data.content);
|
||||
};
|
||||
|
||||
return module.exports;
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,76 +0,0 @@
|
|||
// This file provides the external API for launching and talking to the sandboxed iframe.
|
||||
// The internal API is in sframe-channel.js
|
||||
define([
|
||||
'/common/requireconfig.js'
|
||||
], function (RequireConfig) {
|
||||
var iframe;
|
||||
var handlers = {};
|
||||
var queries = {};
|
||||
var module = { exports: {} };
|
||||
|
||||
var mkTxid = function () {
|
||||
return Math.random().toString(16).replace('0.', '') + Math.random().toString(16).replace('0.', '');
|
||||
};
|
||||
|
||||
module.exports.init = function (frame, cb) {
|
||||
if (iframe) { throw new Error('already initialized'); }
|
||||
var txid = mkTxid();
|
||||
var intr = setInterval(function () {
|
||||
frame.contentWindow.postMessage(JSON.stringify({
|
||||
txid: txid,
|
||||
content: { requireConf: RequireConfig },
|
||||
q: 'INIT'
|
||||
}), '*');
|
||||
});
|
||||
window.addEventListener('message', function (msg) {
|
||||
var data = JSON.parse(msg.data);
|
||||
if (!iframe) {
|
||||
if (data.txid !== txid) { return; }
|
||||
clearInterval(intr);
|
||||
iframe = frame;
|
||||
cb();
|
||||
} else if (typeof(data.q) === 'string' && handlers[data.q]) {
|
||||
handlers[data.q](data, msg);
|
||||
} else if (typeof(data.q) === 'undefined' && queries[data.txid]) {
|
||||
queries[data.txid](data, msg);
|
||||
} else {
|
||||
console.log("Unhandled message");
|
||||
console.log(msg);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.query = function (q, content, cb) {
|
||||
if (!iframe) { throw new Error('not yet initialized'); }
|
||||
var txid = mkTxid();
|
||||
var timeout = setTimeout(function () {
|
||||
delete queries[txid];
|
||||
cb("Timeout making query " + q);
|
||||
});
|
||||
queries[txid] = function (data, msg) {
|
||||
clearTimeout(timeout);
|
||||
delete queries[txid];
|
||||
cb(undefined, data.content, msg);
|
||||
};
|
||||
iframe.contentWindow.postMessage(JSON.stringify({
|
||||
txid: txid,
|
||||
content: content,
|
||||
q: q
|
||||
}), '*');
|
||||
};
|
||||
|
||||
module.exports.registerHandler = function (queryType, handler) {
|
||||
if (typeof(handlers[queryType]) !== 'undefined') { throw new Error('already registered'); }
|
||||
handlers[queryType] = function (msg) {
|
||||
var data = JSON.parse(msg.data);
|
||||
handler(data.content, function (replyContent) {
|
||||
msg.source.postMessage(JSON.stringify({
|
||||
txid: data.txid,
|
||||
content: replyContent
|
||||
}), '*');
|
||||
}, msg);
|
||||
};
|
||||
};
|
||||
|
||||
return module.exports;
|
||||
});
|
|
@ -1,5 +1,33 @@
|
|||
// This file defines all of the RPC calls
|
||||
// The internal API is in sframe-channel.js
|
||||
// This file defines all of the RPC calls which are used between the inner and outer iframe.
|
||||
// Define *querys* (which expect a response) using Q_<query name>
|
||||
// Define *events* (which expect no response) using EV_<event name>
|
||||
// Please document the queries and events you create, and please please avoid making generic
|
||||
// "do stuff" events/queries which are used for many different things because it makes the
|
||||
// protocol unclear.
|
||||
define({
|
||||
// When the iframe first launches, this query is sent repeatedly by the controller
|
||||
// to wait for it to awake and give it the requirejs config to use.
|
||||
'Q_INIT': true,
|
||||
|
||||
// When either the outside or inside registers a query handler, this is sent.
|
||||
'EV_REGISTER_HANDLER': true,
|
||||
|
||||
// Realtime events called from the outside.
|
||||
// When someone joins the pad, argument is a string with their netflux id.
|
||||
'EV_RT_JOIN': true,
|
||||
// When someone leaves the pad, argument is a string with their netflux id.
|
||||
'EV_RT_LEAVE': true,
|
||||
// When you have been disconnected, no arguments.
|
||||
'EV_RT_DISCONNECT': true,
|
||||
// When you have connected, argument is an object with myID: string, members: list, readOnly: boolean.
|
||||
'EV_RT_CONNECT': true,
|
||||
// Called after the history is finished synchronizing, no arguments.
|
||||
'EV_RT_READY': true,
|
||||
// Called from both outside and inside, argument is a (string) chainpad message.
|
||||
'Q_RT_MESSAGE': true,
|
||||
|
||||
// Called from the outside, this informs the inside whenever the user's data has been changed.
|
||||
// The argument is the object representing the content of the user profile minus the netfluxID
|
||||
// which changes per-reconnect.
|
||||
'EV_USERDATA_UPDATE': true
|
||||
});
|
1195
www/pad2/main.js
1195
www/pad2/main.js
File diff suppressed because it is too large
Load Diff
|
@ -1,13 +1,39 @@
|
|||
|
||||
define([
|
||||
'/common/sframe-ctrl.js',
|
||||
'jquery'
|
||||
], function (SFrameCtrl, $) {
|
||||
'/common/sframe-channel.js',
|
||||
'jquery',
|
||||
'/common/sframe-chainpad-netflux-outer.js',
|
||||
'/bower_components/nthen/index.js',
|
||||
'/common/cryptpad-common.js',
|
||||
'/bower_components/chainpad-crypto/crypto.js'
|
||||
], function (SFrameChannel, $, CpNfOuter, nThen, Cryptpad, Crypto) {
|
||||
console.log('xxx');
|
||||
$(function () {
|
||||
console.log('go');
|
||||
SFrameCtrl.init($('#sbox-iframe')[0], function () {
|
||||
console.log('\n\ndone\n\n');
|
||||
nThen(function (waitFor) {
|
||||
$(waitFor());
|
||||
}).nThen(function (waitFor) {
|
||||
SFrameChannel.init($('#sbox-iframe')[0].contentWindow, waitFor(function () {
|
||||
console.log('sframe initialized');
|
||||
}));
|
||||
Cryptpad.ready(waitFor());
|
||||
}).nThen(function (waitFor) {
|
||||
Cryptpad.onError(function (info) {
|
||||
console.log('error');
|
||||
console.log(info);
|
||||
if (info && info.type === "store") {
|
||||
//onConnectError();
|
||||
}
|
||||
});
|
||||
}).nThen(function (waitFor) {
|
||||
var secret = Cryptpad.getSecrets();
|
||||
var readOnly = secret.keys && !secret.keys.editKeyStr;
|
||||
if (!secret.keys) { secret.keys = secret.key; }
|
||||
|
||||
var outer = CpNfOuter.start({
|
||||
channel: secret.channel,
|
||||
network: Cryptpad.getNetwork(),
|
||||
validateKey: secret.keys.validateKey || undefined,
|
||||
readOnly: readOnly,
|
||||
crypto: Crypto.createEncryptor(secret.keys),
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue