You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
cryptpad/www/common/outer/team.js

474 lines
17 KiB
JavaScript

This file contains invisible Unicode characters!

This file contains invisible Unicode characters that may be processed differently from what appears below. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to reveal hidden characters.

define([
'/common/common-util.js',
'/common/common-hash.js',
'/common/common-constants.js',
'/common/common-realtime.js',
'/common/proxy-manager.js',
'/common/userObject.js',
'/common/outer/sharedfolder.js',
'/bower_components/chainpad-listmap/chainpad-listmap.js',
'/bower_components/chainpad-crypto/crypto.js',
'/bower_components/chainpad/chainpad.dist.js',
'/bower_components/nthen/index.js',
'/bower_components/tweetnacl/nacl-fast.min.js',
], function (Util, Hash, Constants, Realtime,
ProxyManager, UserObject, SF,
Listmap, Crypto, ChainPad, nThen) {
var Team = {};
var Nacl = window.nacl;
var initializeTeams = function (ctx, cb) {
cb();
};
var registerChangeEvents = function (ctx, team, proxy, fId) {
if (!team) { return; }
proxy.on('change', [], function (o, n, p) {
if (fId) {
// Pin the new pads
if (p[0] === UserObject.FILES_DATA && typeof(n) === "object" && n.channel && !n.owners) {
var toPin = [n.channel];
// Also pin the onlyoffice channels if they exist
if (n.rtChannel) { toPin.push(n.rtChannel); }
if (n.lastVersion) { toPin.push(n.lastVersion); }
team.pin(toPin, function (obj) { console.error(obj); });
}
// Unpin the deleted pads (deleted <=> changed to undefined)
if (p[0] === UserObject.FILES_DATA && typeof(o) === "object" && o.channel && !n) {
var toUnpin = [o.channel];
var c = team.manager.findChannel(o.channel);
var exists = c.some(function (data) {
return data.fId !== fId;
});
if (!exists) { // Unpin
// Also unpin the onlyoffice channels if they exist
if (o.rtChannel) { toUnpin.push(o.rtChannel); }
if (o.lastVersion) { toUnpin.push(o.lastVersion); }
team.unpin(toUnpin, function (obj) { console.error(obj); });
}
}
}
team.sendEvent('DRIVE_CHANGE', {
id: fId,
old: o,
new: n,
path: p
});
});
proxy.on('remove', [], function (o, p) {
team.sendEvent('DRIVE_REMOVE', {
id: fId,
old: o,
path: p
});
});
};
var getTeamChannelList = function (ctx, id) {
// Get the list of pads' channel ID in your drive
// This list is filtered so that it doesn't include pad owned by other users
// It now includes channels from shared folders
var store = ctx.teams[id];
if (!store) { return null; }
var list = store.manager.getChannelsList('pin');
// Teams will always be owned for now
// XXX if the team is not owned, add the teamChannel to the list
/*
var _team = Util.find(ctx, ['store', 'proxy', 'teams', id]);
var secret = Hash.getSecrets('team', _team.hash, _team.password);
var teamChannel = secret.channel;
list.push(userChannel);
*/
// XXX Add the team mailbox
/*
if (store.proxy.mailboxes) {
var mList = Object.keys(store.proxy.mailboxes).map(function (m) {
return store.proxy.mailboxes[m].channel;
});
list = list.concat(mList);
}
*/
list.sort();
return list;
};
var handleSharedFolder = function (ctx, id, sfId, rt) {
var t = ctx.teams[id];
if (!t) { return; }
t.sharedFolders[sfId] = rt;
registerChangeEvents(ctx, t, rt.proxy, sfId);
};
var initRpc = function (ctx, team, data, cb) {
if (team.rpc) { return void cb(); }
if (!data.edPrivate || !data.edPublic) { return void cb('EFORBIDDEN'); }
require(['/common/pinpad.js'], function (Pinpad) {
Pinpad.create(ctx.store.network, data, function (e, call) {
if (e) { return void cb(e); }
team.rpc = call;
cb();
});
});
};
var onReady = function (ctx, id, lm, keys, cId, cb) {
var proxy = lm.proxy;
var team = {
id: id,
proxy: proxy,
listmap: lm,
clients: [],
realtime: lm.realtime,
handleSharedFolder: function (sfId, rt) { handleSharedFolder(ctx, id, sfId, rt); },
sharedFolders: {}, // equivalent of store.sharedFolders in async-store
};
if (cId) { team.clients.push(cId); }
team.sendEvent = function (q, data, sender) {
ctx.emit(q, data, team.clients.filter(function (cId) {
return cId !== sender;
}));
};
team.pin = function (data, cb) { return void cb({error: 'EFORBIDDEN'}); };
team.unpin = function (data, cb) { return void cb({error: 'EFORBIDDEN'}); };
nThen(function (waitFor) {
if (!keys.edPrivate) { return; }
initRpc(ctx, team, keys, waitFor(function (err) {
if (err) { return; }
team.pin = function (data, cb) {
if (!team.rpc) { return void cb({error: 'TEAM_RPC_NOT_READY'}); }
if (typeof(cb) !== 'function') { console.error('expected a callback'); }
team.rpc.pin(data, function (e, hash) {
if (e) { return void cb({error: e}); }
cb({hash: hash});
});
};
team.unpin = function (data, cb) {
if (!team.rpc) { return void cb({error: 'TEAM_RPC_NOT_READY'}); }
if (typeof(cb) !== 'function') { console.error('expected a callback'); }
team.rpc.unpin(data, function (e, hash) {
if (e) { return void cb({error: e}); }
cb({hash: hash});
});
};
}));
}).nThen(function () {
var loadSharedFolder = function (id, data, cb) {
SF.load({
network: ctx.store.network,
store: team
}, id, data, cb);
};
var manager = team.manager = ProxyManager.create(proxy.drive, {
onSync: function (cb) { ctx.Store.onSync(id, cb); },
edPublic: proxy.edPublic,
pin: team.pin,
unpin: team.unpin,
loadSharedFolder: loadSharedFolder,
settings: {
drive: Util.find(ctx.store, ['proxy', 'settings', 'drive'])
}
}, {
outer: true,
removeOwnedChannel: function (channel, cb) {
var data;
if (typeof(channel) === "object") {
channel.teamId = id;
data = channel;
} else {
data = {
channel: channel,
teamId: id
};
}
ctx.Store.removeOwnedChannel('', data, cb);
},
edPublic: proxy.edPublic,
loggedIn: true,
log: function (msg) {
// broadcast to all drive apps
team.sendEvent("DRIVE_LOG", msg);
}
});
team.userObject = manager.user.userObject;
team.userObject.fixFiles();
}).nThen(function (waitFor) {
ctx.teams[id] = team;
registerChangeEvents(ctx, team, proxy);
SF.loadSharedFolders(ctx.Store, ctx.store.network, team, team.userObject, waitFor);
// XXX
// Load members pad
}).nThen(function () {
if (!team.rpc) { return; }
var list = getTeamChannelList(ctx, id);
var local = Hash.hashChannelList(list);
// Check pin list
team.rpc.getServerHash(function (e, hash) {
if (e) { return void console.warn(e); }
if (hash !== local) {
// Reset pin list
team.rpc.reset(list, function (e/*, hash*/) {
if (e) { console.warn(e); }
});
}
});
}).nThen(function () {
if (ctx.onReadyHandlers[id]) {
ctx.onReadyHandlers[id].forEach(function (obj) {
// Callback and subscribe the client to new notifications
if (typeof (obj.cb) === "function") { obj.cb(); }
if (!obj.cId) { return; }
var idx = team.clients.indexOf(obj.cId);
if (idx === -1) {
team.clients.push(obj.cId);
}
});
}
delete ctx.onReadyHandlers[id];
cb();
});
};
var openChannel = function (ctx, teamData, id, cb) {
var secret = Hash.getSecrets('team', teamData.hash, teamData.password);
var crypto = Crypto.createEncryptor(secret.keys);
var cfg = {
data: {},
readOnly: !Boolean(secret.keys.signKey),
network: ctx.store.network,
channel: secret.channel,
crypto: crypto,
ChainPad: ChainPad,
metadata: {
validateKey: secret.keys.validateKey || undefined,
},
userName: 'team',
classic: true
};
var lm = Listmap.create(cfg);
lm.proxy.on('create', function () {
}).on('ready', function () {
onReady(ctx, id, lm, teamData.keys, null, cb);
});
};
var createTeam = function (ctx, data, cId, _cb) {
var cb = Util.once(_cb);
var password = Hash.createChannelId();
var hash = Hash.createRandomHash('team', password);
var secret = Hash.getSecrets('team', hash, password);
var keyPair = Nacl.sign.keyPair(); // keyPair.secretKey , keyPair.publicKey
var membersSecret = Hash.getSecrets('members');
var membersHashes = Hash.getHashes(membersSecret);
var config = {
network: ctx.store.network,
channel: secret.channel,
data: {},
validateKey: secret.keys.validateKey, // derived validation key
crypto: Crypto.createEncryptor(secret.keys),
logLevel: 1,
classic: true,
ChainPad: ChainPad,
owners: [ctx.store.proxy.edPublic]
};
nThen(function (waitFor) {
console.log('pin..');
ctx.pinPads([secret.channel, membersSecret.channel], waitFor(function (obj) {
if (obj && obj.error) {
waitFor.abort();
return void cb(obj);
}
}));
// XXX initialize the members channel with yourself, and mark it as owned!
}).nThen(function () {
console.log('init proxy');
var lm = Listmap.create(config);
var proxy = lm.proxy;
proxy.on('ready', function () {
console.log('ready');
// Store keys in our drive
var id = Util.createRandomInteger();
var keys = {
edPrivate: Nacl.util.encodeBase64(keyPair.secretKey),
edPublic: Nacl.util.encodeBase64(keyPair.publicKey)
};
ctx.store.proxy.teams[id] = {
hash: hash,
password: password,
keys: keys,
members: membersHashes.editHash,
name: data.name
};
// Initialize the team drive
proxy.drive = {};
// Create metadata
proxy.metadata = {
name: name,
members: membersHashes.viewHash,
};
// Add rpc key
proxy.edPublic = Nacl.util.encodeBase64(keyPair.publicKey);
onReady(ctx, id, lm, {
edPrivate: keyPair.secretKey,
edPublic: keyPair.publicKey
}, cId, function () {
cb();
});
}).on('error', function (info) {
if (info && typeof (info.loaded) !== "undefined" && !info.loaded) {
cb({error:'ECONNECT'});
}
});
});
};
var subscribe = function (ctx, id, cId, cb) {
// Unsubscribe from other teams: one tab can only receive events about one team
Object.keys(ctx.teams).forEach(function (teamId) {
var c = ctx.teams[teamId].clients;
var idx = c.indexOf(cId);
if (idx !== -1) {
c.splice(idx, 1);
}
});
// Also remove from pending subscriptions
Object.keys(ctx.onReadyHandlers).forEach(function (teamId) {
var idx = -1;
ctx.onReadyHandlers[teamId].some(function (obj, _idx) {
if (obj.cId === cId) {
idx = _idx;
return true;
}
});
if (idx !== -1) {
ctx.onReadyHandlers[teamId].splice(idx, 1);
}
});
if (!id) { return; }
// If the team is loading, as ourselves in the list
if (ctx.onReadyHandlers[id]) {
var _idx = ctx.onReadyHandlers[id].indexOf(cId);
if (_idx === -1) {
ctx.onReadyHandlers[id].push({
cId: cId,
cb: cb
});
}
return;
}
// Otherwise, subscribe to new notifications
if (!ctx.teams[id]) {
return void cb({error: 'EINVAL'});
}
var clients = ctx.teams[id].clients;
var idx = clients.indexOf(cId);
if (idx === -1) {
clients.push(cId);
}
cb();
};
// Remove a client from all the team they're subscribed to
var removeClient = function (ctx, cId) {
Object.keys(ctx.teams).forEach(function (id) {
// Remove from the subscribers
var clients = ctx.teams[id].clients;
var idx = clients.indexOf(cId);
if (idx !== -1) { clients.splice(idx, 1); }
// And remove from the onReady handlers in case they haven't finished loading
if (ctx.onReadyHandlers[id]) {
var idx2 = ctx.onReadyHandlers.indexOf(cId);
if (idx2 !== -1) { ctx.onReadyHandlers.splice(idx2, 1); }
}
});
};
Team.init = function (cfg, waitFor, emit) {
var team = {};
var store = cfg.store;
if (!store.loggedIn || !store.proxy.edPublic) { return; }
var ctx = {
store: store,
Store: cfg.Store,
pinPads: cfg.pinPads,
emit: emit,
onReadyHandlers: {},
teams: {}
};
var teams = store.proxy.teams = store.proxy.teams || {};
initializeTeams(ctx, waitFor(function (err) {
if (err) { return; }
}));
Object.keys(teams).forEach(function (id) {
ctx.onReadyHandlers[id] = [];
openChannel(ctx, teams[id], id, waitFor(function () {
console.error('team '+id+' ready');
}));
});
team.getTeam = function (id) {
return ctx.teams[id];
};
team.getTeamsData = function () {
var t = {};
Object.keys(teams).forEach(function (id) {
t[id] = {
name: teams[id].name,
edPublic: Util.find(teams[id], ['keys', 'edPublic'])
};
});
return t;
};
team.getTeams = function () {
return Object.keys(ctx.teams);
};
team.removeClient = function (clientId) {
removeClient(ctx, clientId);
};
team.execCommand = function (clientId, obj, cb) {
console.log(obj);
var cmd = obj.cmd;
var data = obj.data;
if (cmd === 'SUBSCRIBE') {
// Only the team app will subscribe to events?
return void subscribe(ctx, data, clientId, cb);
}
if (cmd === 'LIST_TEAMS') {
return void cb(store.proxy.teams);
}
if (cmd === 'CREATE_TEAM') {
return void createTeam(ctx, data, clientId, cb);
}
};
return team;
};
return Team;
});