|
|
|
|
define([
|
|
|
|
|
], function () {
|
|
|
|
|
var OO = {};
|
|
|
|
|
|
|
|
|
|
var getHistory = function (ctx, client, cb) {
|
|
|
|
|
var c = ctx.clients[client];
|
|
|
|
|
if (!c) { return void cb({error: 'ENOENT'}); }
|
|
|
|
|
var chan = ctx.channels[c.channel];
|
|
|
|
|
if (!chan) { return void cb({error: 'ENOCHAN'}); }
|
|
|
|
|
cb();
|
|
|
|
|
chan.history.forEach(function (msg) {
|
|
|
|
|
ctx.emit('MESSAGE', {
|
|
|
|
|
msg: msg,
|
|
|
|
|
validateKey: chan.validateKey
|
|
|
|
|
}, [client]);
|
|
|
|
|
});
|
|
|
|
|
ctx.emit('HISTORY_SYNCED', {}, [client]);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
var openChannel = function (ctx, obj, client, cb) {
|
|
|
|
|
var channel = obj.channel;
|
|
|
|
|
var padChan = obj.padChan;
|
|
|
|
|
var network = ctx.store.network;
|
|
|
|
|
var first = true;
|
|
|
|
|
|
|
|
|
|
var c = ctx.clients[client];
|
|
|
|
|
if (!c) {
|
|
|
|
|
c = ctx.clients[client] = {
|
|
|
|
|
channel: channel,
|
|
|
|
|
};
|
|
|
|
|
} else {
|
|
|
|
|
return void cb();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var chan = ctx.channels[channel];
|
|
|
|
|
if (chan) {
|
|
|
|
|
// This channel is already open in another tab
|
|
|
|
|
|
|
|
|
|
// ==> Use our netflux ID to create our client ID
|
|
|
|
|
if (!c.id) { c.id = chan.wc.myID + '-' + client; }
|
|
|
|
|
|
|
|
|
|
getHistory(ctx, client, function () {
|
|
|
|
|
ctx.emit('READY', '', [client]);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ==> And push the new tab to the list
|
|
|
|
|
chan.clients.push(client);
|
|
|
|
|
return void cb();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var txid = Math.floor(Math.random() * 1000000);
|
|
|
|
|
var onOpen = function (wc) {
|
|
|
|
|
|
|
|
|
|
ctx.channels[channel] = ctx.channels[channel] || {
|
|
|
|
|
history: [],
|
|
|
|
|
validateKey: obj.validateKey
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
chan = ctx.channels[channel];
|
|
|
|
|
chan.padChan = padChan;
|
|
|
|
|
|
|
|
|
|
// Create our client ID using the netflux ID
|
|
|
|
|
if (!c.id) { c.id = wc.myID + '-' + client; }
|
|
|
|
|
|
|
|
|
|
// If this is a reconnect, we have a new netflux ID so we're going to fix
|
|
|
|
|
// all our client IDs.
|
|
|
|
|
if (chan.clients) {
|
|
|
|
|
chan.clients.forEach(function (cl) {
|
|
|
|
|
if (ctx.clients[cl]) {
|
|
|
|
|
ctx.clients[cl].id = wc.myID + '-' + cl;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
wc.on('join', function () {
|
|
|
|
|
});
|
|
|
|
|
wc.on('leave', function () {
|
|
|
|
|
});
|
|
|
|
|
wc.on('message', function (msg) {
|
|
|
|
|
chan.history.push(msg);
|
|
|
|
|
ctx.emit('MESSAGE', {
|
|
|
|
|
msg: msg,
|
|
|
|
|
validateKey: chan.validateKey
|
|
|
|
|
}, chan.clients);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
chan.wc = wc;
|
|
|
|
|
chan.sendMsg = function (msg, cb) {
|
|
|
|
|
cb = cb || function () {};
|
|
|
|
|
wc.bcast(msg).then(function () {
|
|
|
|
|
chan.history.push(msg);
|
|
|
|
|
cb();
|
|
|
|
|
}, function (err) {
|
|
|
|
|
cb({error: err});
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (first) {
|
|
|
|
|
chan.clients = [client];
|
|
|
|
|
chan.lastCpHash = obj.lastCpHash;
|
|
|
|
|
first = false;
|
|
|
|
|
cb();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var hk = network.historyKeeper;
|
|
|
|
|
var cfg = {
|
|
|
|
|
txid: txid,
|
|
|
|
|
lastKnownHash: chan.lastKnownHash || chan.lastCpHash,
|
|
|
|
|
metadata: {
|
|
|
|
|
validateKey: obj.validateKey,
|
|
|
|
|
owners: obj.owners,
|
|
|
|
|
expire: obj.expire
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
var msg = ['GET_HISTORY', wc.id, cfg];
|
|
|
|
|
// Add the validateKey if we are the channel creator and we have a validateKey
|
|
|
|
|
if (hk) {
|
|
|
|
|
network.sendto(hk, JSON.stringify(msg)).then(function () {
|
|
|
|
|
}, function (err) {
|
|
|
|
|
console.error(err);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
network.on('message', function (msg, sender) {
|
|
|
|
|
if (!ctx.channels[channel]) { return; }
|
|
|
|
|
var hk = network.historyKeeper;
|
|
|
|
|
if (sender !== hk) { return; }
|
|
|
|
|
|
|
|
|
|
// Parse the message
|
|
|
|
|
var parsed;
|
|
|
|
|
try {
|
|
|
|
|
parsed = JSON.parse(msg);
|
|
|
|
|
} catch (e) {}
|
|
|
|
|
if (!parsed) { return; }
|
|
|
|
|
|
|
|
|
|
// If there is a txid, make sure it's ours or abort
|
|
|
|
|
if (parsed.txid && parsed.txid !== txid) { return; }
|
|
|
|
|
|
|
|
|
|
// Keep only metadata messages for the current channel
|
|
|
|
|
if (parsed.channel && parsed.channel !== channel) { return; }
|
|
|
|
|
// Ignore the metadata message
|
|
|
|
|
if (parsed.validateKey && parsed.channel) {
|
|
|
|
|
if (!chan.validateKey) {
|
|
|
|
|
chan.validateKey = parsed.validateKey;
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
// End of history: emit READY
|
|
|
|
|
if (parsed.state && parsed.state === 1 && parsed.channel) {
|
|
|
|
|
ctx.emit('READY', '', chan.clients);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (parsed.error && parsed.channel) { return; }
|
|
|
|
|
|
|
|
|
|
// If there is a txid, make sure it's ours or abort
|
|
|
|
|
if (Array.isArray(parsed) && parsed[0] && parsed[0] !== txid) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
msg = parsed[4];
|
|
|
|
|
|
|
|
|
|
// Keep only the history for our channel
|
|
|
|
|
if (parsed[3] !== channel) { return; }
|
|
|
|
|
|
|
|
|
|
var hash = msg.slice(0,64);
|
|
|
|
|
if (hash === chan.lastKnownHash || hash === chan.lastCpHash) { return; }
|
|
|
|
|
|
|
|
|
|
chan.lastKnownHash = hash;
|
|
|
|
|
ctx.emit('MESSAGE', {
|
|
|
|
|
msg: msg,
|
|
|
|
|
}, chan.clients);
|
|
|
|
|
chan.history.push(msg);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
network.join(channel).then(onOpen, function (err) {
|
|
|
|
|
return void cb({error: err});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
network.on('reconnect', function () {
|
|
|
|
|
if (!ctx.channels[channel]) { return; }
|
|
|
|
|
network.join(channel).then(onOpen, function (err) {
|
|
|
|
|
console.error(err);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
var updateHash = function (ctx, data, clientId, cb) {
|
|
|
|
|
var c = ctx.clients[clientId];
|
|
|
|
|
if (!c) { return void cb({ error: 'NOT_IN_CHANNEL' }); }
|
|
|
|
|
var chan = ctx.channels[c.channel];
|
|
|
|
|
if (!chan) { return void cb({ error: 'INVALID_CHANNEL' }); }
|
|
|
|
|
var hash = data;
|
|
|
|
|
var index = -1;
|
|
|
|
|
chan.history.some(function (msg, idx) {
|
|
|
|
|
if (msg.slice(0,64) === hash) {
|
|
|
|
|
index = idx + 1;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
if (index !== -1) {
|
|
|
|
|
chan.history = chan.history.slice(index);
|
|
|
|
|
}
|
|
|
|
|
cb();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
var sendMessage = function (ctx, data, clientId, cb) {
|
|
|
|
|
var c = ctx.clients[clientId];
|
|
|
|
|
if (!c) { return void cb({ error: 'NOT_IN_CHANNEL' }); }
|
|
|
|
|
var chan = ctx.channels[c.channel];
|
|
|
|
|
if (!chan) { return void cb({ error: 'INVALID_CHANNEL' }); }
|
|
|
|
|
// Prepare the callback: broadcast the message to the other local tabs
|
|
|
|
|
// if the message is sent
|
|
|
|
|
var _cb = function (obj) {
|
|
|
|
|
if (obj && obj.error) { return void cb(obj); }
|
|
|
|
|
ctx.emit('MESSAGE', {
|
|
|
|
|
msg: data.msg
|
|
|
|
|
}, chan.clients.filter(function (cl) {
|
|
|
|
|
return cl !== clientId;
|
|
|
|
|
}));
|
|
|
|
|
cb();
|
|
|
|
|
};
|
|
|
|
|
// Send the message
|
|
|
|
|
if (data.isCp) {
|
|
|
|
|
return void chan.sendMsg(data.isCp, _cb);
|
|
|
|
|
}
|
|
|
|
|
chan.sendMsg(data.msg, _cb);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
var reencrypt = function (ctx, data, cId, cb) {
|
|
|
|
|
var channel = data.channel;
|
|
|
|
|
var network = ctx.store.network;
|
|
|
|
|
|
|
|
|
|
var onOpen = function (wc) {
|
|
|
|
|
var hk = network.historyKeeper;
|
|
|
|
|
var cfg = {
|
|
|
|
|
metadata: data.metadata
|
|
|
|
|
};
|
|
|
|
|
var msg = ['GET_HISTORY', wc.id, cfg];
|
|
|
|
|
network.sendto(hk, JSON.stringify(msg));
|
|
|
|
|
data.msgs.forEach(function (msg) {
|
|
|
|
|
wc.bcast(msg);
|
|
|
|
|
});
|
|
|
|
|
wc.leave();
|
|
|
|
|
cb();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
ctx.store.anon_rpc.send("IS_NEW_CHANNEL", channel, function (e, response) {
|
|
|
|
|
if (e) { return void cb({error: e}); }
|
|
|
|
|
var isNew;
|
|
|
|
|
if (response && response.length && typeof(response[0]) === 'boolean') {
|
|
|
|
|
isNew = response[0];
|
|
|
|
|
} else {
|
|
|
|
|
cb({error: 'INVALID_RESPONSE'});
|
|
|
|
|
}
|
|
|
|
|
if (!isNew) { return void cb({error: 'EEXISTS'}); }
|
|
|
|
|
|
|
|
|
|
// Channel is new: we can push our reencrypted history
|
|
|
|
|
network.join(channel).then(onOpen, function (err) {
|
|
|
|
|
return void cb({error: err});
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
var leaveChannel = function (ctx, padChan) {
|
|
|
|
|
// Leave channel and prevent reconnect when we leave a pad
|
|
|
|
|
Object.keys(ctx.channels).some(function (ooChan) {
|
|
|
|
|
var channel = ctx.channels[ooChan];
|
|
|
|
|
if (channel.padChan !== padChan) { return; }
|
|
|
|
|
if (channel.wc) { channel.wc.leave(); }
|
|
|
|
|
delete ctx.channels[ooChan];
|
|
|
|
|
return true;
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Remove the client from all its channels when a tab is closed
|
|
|
|
|
var removeClient = function (ctx, clientId) {
|
|
|
|
|
var filter = function (c) {
|
|
|
|
|
return c !== clientId;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Remove the client from our channels
|
|
|
|
|
var chan;
|
|
|
|
|
for (var k in ctx.channels) {
|
|
|
|
|
chan = ctx.channels[k];
|
|
|
|
|
chan.clients = chan.clients.filter(filter);
|
|
|
|
|
if (chan.clients.length === 0) {
|
|
|
|
|
if (chan.wc) { chan.wc.leave(); }
|
|
|
|
|
delete ctx.channels[k];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (ctx.clients[clientId]) {
|
|
|
|
|
var oldChannel = ctx.clients[clientId].channel;
|
|
|
|
|
var oldChan = ctx.channels[oldChannel];
|
|
|
|
|
if (oldChan) {
|
|
|
|
|
ctx.emit('LEAVE', {id: clientId}, [oldChan.clients[0]]);
|
|
|
|
|
}
|
|
|
|
|
delete ctx.clients[clientId];
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
OO.init = function (store, emit) {
|
|
|
|
|
var oo = {};
|
|
|
|
|
var ctx = {
|
|
|
|
|
store: store,
|
|
|
|
|
emit: emit,
|
|
|
|
|
channels: {},
|
|
|
|
|
clients: {}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
oo.removeClient = function (clientId) {
|
|
|
|
|
removeClient(ctx, clientId);
|
|
|
|
|
};
|
|
|
|
|
oo.leavePad = function (padChan) {
|
|
|
|
|
leaveChannel(ctx, padChan);
|
|
|
|
|
};
|
|
|
|
|
oo.execCommand = function (clientId, obj, cb) {
|
|
|
|
|
var cmd = obj.cmd;
|
|
|
|
|
var data = obj.data;
|
|
|
|
|
if (cmd === 'SEND_MESSAGE') {
|
|
|
|
|
return void sendMessage(ctx, data, clientId, cb);
|
|
|
|
|
}
|
|
|
|
|
if (cmd === 'UPDATE_HASH') {
|
|
|
|
|
return void updateHash(ctx, data, clientId, cb);
|
|
|
|
|
}
|
|
|
|
|
if (cmd === 'OPEN_CHANNEL') {
|
|
|
|
|
return void openChannel(ctx, data, clientId, cb);
|
|
|
|
|
}
|
|
|
|
|
if (cmd === 'GET_HISTORY') {
|
|
|
|
|
return void getHistory(ctx, clientId, cb);
|
|
|
|
|
}
|
|
|
|
|
if (cmd === 'REENCRYPT') {
|
|
|
|
|
return void reencrypt(ctx, data, clientId, cb);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return oo;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return OO;
|
|
|
|
|
});
|