more WIP for roster
This commit is contained in:
parent
84518c0775
commit
073543fe3d
@ -10,6 +10,7 @@ var Rpc = require("../../www/common/rpc");
|
|||||||
var Hash = require("../../www/common/common-hash");
|
var Hash = require("../../www/common/common-hash");
|
||||||
var CpNetflux = require("../../www/bower_components/chainpad-netflux");
|
var CpNetflux = require("../../www/bower_components/chainpad-netflux");
|
||||||
var Roster = require("./roster");
|
var Roster = require("./roster");
|
||||||
|
var Util = require("../../lib/common-util");
|
||||||
|
|
||||||
var createMailbox = function (config, cb) {
|
var createMailbox = function (config, cb) {
|
||||||
var webchannel;
|
var webchannel;
|
||||||
@ -68,6 +69,10 @@ var createUser = function (config, cb) {
|
|||||||
return void cb(err);
|
return void cb(err);
|
||||||
}
|
}
|
||||||
user = client;
|
user = client;
|
||||||
|
user.destroy = Util.mkEvent(true);
|
||||||
|
user.destroy.reg(function () {
|
||||||
|
user.network.disconnect();
|
||||||
|
});
|
||||||
}));
|
}));
|
||||||
}).nThen(function (w) {
|
}).nThen(function (w) {
|
||||||
// make all the parameters you'll need
|
// make all the parameters you'll need
|
||||||
@ -87,6 +92,9 @@ var createUser = function (config, cb) {
|
|||||||
return void console.error('ANON_RPC_CONNECT_ERR');
|
return void console.error('ANON_RPC_CONNECT_ERR');
|
||||||
}
|
}
|
||||||
user.anonRpc = rpc;
|
user.anonRpc = rpc;
|
||||||
|
user.destroy.reg(function () {
|
||||||
|
user.anonRpc.destroy();
|
||||||
|
});
|
||||||
}));
|
}));
|
||||||
|
|
||||||
Pinpad.create(network, user.edKeys, w(function (err, rpc) {
|
Pinpad.create(network, user.edKeys, w(function (err, rpc) {
|
||||||
@ -97,6 +105,9 @@ var createUser = function (config, cb) {
|
|||||||
return console.log('RPC_CONNECT_ERR');
|
return console.log('RPC_CONNECT_ERR');
|
||||||
}
|
}
|
||||||
user.rpc = rpc;
|
user.rpc = rpc;
|
||||||
|
user.destroy.reg(function () {
|
||||||
|
user.rpc.destroy();
|
||||||
|
});
|
||||||
}));
|
}));
|
||||||
|
|
||||||
Pinpad.create(network, config.teamEdKeys, w(function (err, rpc) {
|
Pinpad.create(network, config.teamEdKeys, w(function (err, rpc) {
|
||||||
@ -106,6 +117,9 @@ var createUser = function (config, cb) {
|
|||||||
return console.log('RPC_CONNECT_ERR');
|
return console.log('RPC_CONNECT_ERR');
|
||||||
}
|
}
|
||||||
user.team_rpc = rpc;
|
user.team_rpc = rpc;
|
||||||
|
user.destroy.reg(function () {
|
||||||
|
user.team_rpc.destroy();
|
||||||
|
});
|
||||||
}));
|
}));
|
||||||
}).nThen(function (w) {
|
}).nThen(function (w) {
|
||||||
user.rpc.reset([], w(function (err, hash) {
|
user.rpc.reset([], w(function (err, hash) {
|
||||||
@ -169,7 +183,7 @@ var createUser = function (config, cb) {
|
|||||||
return void cb(err);
|
return void cb(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('PIN_RESPONSE', hash);
|
//console.log('PIN_RESPONSE', hash);
|
||||||
|
|
||||||
if (hash[0] === EMPTY_ARRAY_HASH) { throw new Error("PIN_DIDNT_WORK"); }
|
if (hash[0] === EMPTY_ARRAY_HASH) { throw new Error("PIN_DIDNT_WORK"); }
|
||||||
user.latestPinHash = hash;
|
user.latestPinHash = hash;
|
||||||
@ -201,7 +215,7 @@ var createUser = function (config, cb) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (hash[0] !== EMPTY_ARRAY_HASH) {
|
if (hash[0] !== EMPTY_ARRAY_HASH) {
|
||||||
console.log('UNPIN_RESPONSE', hash);
|
//console.log('UNPIN_RESPONSE', hash);
|
||||||
throw new Error("UNPIN_DIDNT_WORK");
|
throw new Error("UNPIN_DIDNT_WORK");
|
||||||
}
|
}
|
||||||
user.latestPinHash = hash[0];
|
user.latestPinHash = hash[0];
|
||||||
@ -215,15 +229,14 @@ var createUser = function (config, cb) {
|
|||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
}).nThen(function () {
|
}).nThen(function () {
|
||||||
user.cleanup = function (cb) {
|
|
||||||
// TODO remove your mailbox
|
|
||||||
|
|
||||||
|
user.cleanup = function (cb) {
|
||||||
|
//console.log("Destroying user");
|
||||||
|
// TODO remove your mailbox
|
||||||
|
user.destroy.fire();
|
||||||
cb = cb;
|
cb = cb;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
cb(void 0, user);
|
cb(void 0, user);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@ -233,8 +246,7 @@ var alice, bob, oscar;
|
|||||||
var sharedConfig = {
|
var sharedConfig = {
|
||||||
teamEdKeys: makeEdKeys(),
|
teamEdKeys: makeEdKeys(),
|
||||||
teamCurveKeys: makeCurveKeys(),
|
teamCurveKeys: makeCurveKeys(),
|
||||||
rosterChannel: Hash.createChannelId(),
|
rosterSeed: Crypto.Team.createSeed(),
|
||||||
//rosterHash: makeRosterHash(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
nThen(function (w) {
|
nThen(function (w) {
|
||||||
@ -249,7 +261,6 @@ nThen(function (w) {
|
|||||||
}));
|
}));
|
||||||
}).nThen(function (w) {
|
}).nThen(function (w) {
|
||||||
// TODO oscar creates the team roster
|
// TODO oscar creates the team roster
|
||||||
//Roster = Roster;
|
|
||||||
|
|
||||||
// user edPublic (for ownership)
|
// user edPublic (for ownership)
|
||||||
// user curve keys (for encryption and authentication)
|
// user curve keys (for encryption and authentication)
|
||||||
@ -261,19 +272,11 @@ nThen(function (w) {
|
|||||||
// network
|
// network
|
||||||
// owners:
|
// owners:
|
||||||
|
|
||||||
/*
|
var rosterKeys = Crypto.Team.deriveMemberKeys(sharedConfig.rosterSeed, oscar.curveKeys);
|
||||||
var team = {
|
|
||||||
curve: sharedConfig.teamCurveKeys,
|
|
||||||
ed: sharedConfig.teamEdKeys,
|
|
||||||
};
|
|
||||||
*/
|
|
||||||
|
|
||||||
var rosterSeed = Crypto.Team.createSeed();
|
|
||||||
var rosterKeys = Crypto.Team.deriveMemberKeys(rosterSeed, oscar.curveKeys);
|
|
||||||
|
|
||||||
Roster.create({
|
Roster.create({
|
||||||
network: oscar.network,
|
network: oscar.network,
|
||||||
channel: rosterKeys.channel, //sharedConfig.rosterChannel,
|
channel: rosterKeys.channel,
|
||||||
owners: [
|
owners: [
|
||||||
oscar.edKeys.edPublic
|
oscar.edKeys.edPublic
|
||||||
],
|
],
|
||||||
@ -286,32 +289,37 @@ nThen(function (w) {
|
|||||||
return void console.trace(err);
|
return void console.trace(err);
|
||||||
}
|
}
|
||||||
oscar.roster = roster;
|
oscar.roster = roster;
|
||||||
|
oscar.destroy.reg(function () {
|
||||||
|
roster.stop();
|
||||||
|
});
|
||||||
}));
|
}));
|
||||||
}).nThen(function (w) {
|
}).nThen(function (w) {
|
||||||
var roster = oscar.roster;
|
var roster = oscar.roster;
|
||||||
|
|
||||||
roster.on('change', function () {
|
oscar.lastKnownHash = -1;
|
||||||
setTimeout(function () {
|
|
||||||
|
|
||||||
console.log("\nCHANGE");
|
roster.on('change', function () {
|
||||||
console.log("roster.getState()", roster.getState());
|
oscar.currentRoster = roster.getState();
|
||||||
console.log();
|
//console.log("new state = %s\n", JSON.stringify(oscar.currentRoster));
|
||||||
});
|
}).on('checkpoint', function (hash) {
|
||||||
|
oscar.lastKnownHash = hash;
|
||||||
});
|
});
|
||||||
|
|
||||||
var state = roster.getState();
|
//var state = roster.getState();
|
||||||
console.log("CURRENT ROSTER STATE:", state);
|
//console.log("CURRENT ROSTER STATE:", state);
|
||||||
|
|
||||||
roster.init({
|
roster.init({
|
||||||
name: oscar.name,
|
displayName: oscar.name,
|
||||||
|
|
||||||
//profile: '',
|
//profile: '',
|
||||||
// mailbox: '',
|
// mailbox: '',
|
||||||
//title: '',
|
//title: '',
|
||||||
}, w(function (err) {
|
}, w(function (err) {
|
||||||
if (err) { return void console.error(err); }
|
if (err) { return void console.error(err); }
|
||||||
|
//console.log("INITIALIZED");
|
||||||
}));
|
}));
|
||||||
}).nThen(function (w) {
|
}).nThen(function (w) {
|
||||||
console.log("ALICE && BOB");
|
//console.log("ALICE && BOB");
|
||||||
createUser(sharedConfig, w(function (err, _alice) {
|
createUser(sharedConfig, w(function (err, _alice) {
|
||||||
if (err) {
|
if (err) {
|
||||||
w.abort();
|
w.abort();
|
||||||
@ -319,7 +327,7 @@ nThen(function (w) {
|
|||||||
}
|
}
|
||||||
alice = _alice;
|
alice = _alice;
|
||||||
alice.name = 'alice';
|
alice.name = 'alice';
|
||||||
console.log("Initialized Alice");
|
//console.log("Initialized Alice");
|
||||||
}));
|
}));
|
||||||
createUser(sharedConfig, w(function (err, _bob) {
|
createUser(sharedConfig, w(function (err, _bob) {
|
||||||
if (err) {
|
if (err) {
|
||||||
@ -328,29 +336,63 @@ nThen(function (w) {
|
|||||||
}
|
}
|
||||||
bob = _bob;
|
bob = _bob;
|
||||||
bob.name = 'bob';
|
bob.name = 'bob';
|
||||||
console.log("Initialized Bob");
|
//console.log("Initialized Bob");
|
||||||
}));
|
}));
|
||||||
}).nThen(function () {
|
}).nThen(function (w) {
|
||||||
|
setTimeout(w(), 500);
|
||||||
|
|
||||||
|
}).nThen(function (w) {
|
||||||
|
// Alice loads the roster...
|
||||||
|
var rosterKeys = Crypto.Team.deriveMemberKeys(sharedConfig.rosterSeed, alice.curveKeys);
|
||||||
|
|
||||||
|
Roster.create({
|
||||||
|
network: alice.network,
|
||||||
|
channel: rosterKeys.channel,
|
||||||
|
//owners: [], // Alice doesn't know who the owners might be...
|
||||||
|
keys: rosterKeys,
|
||||||
|
anon_rpc: alice.anonRpc,
|
||||||
|
lastKnownHash: void 0, // alice should fetch everything from the beginning of time...
|
||||||
|
}, w(function (err, roster) {
|
||||||
|
if (err) {
|
||||||
|
w.abort();
|
||||||
|
return void console.error(err);
|
||||||
|
}
|
||||||
|
alice.roster = roster;
|
||||||
|
alice.destroy.reg(function () {
|
||||||
|
roster.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (JSON.stringify(alice.roster.getState()) !== JSON.stringify(oscar.roster.getState())) {
|
||||||
|
console.error("Alice and Oscar have different roster states!");
|
||||||
|
throw new Error();
|
||||||
|
} else {
|
||||||
|
console.log("Alice and Oscar have the same roster state");
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}).nThen(function (w) {
|
||||||
// TODO oscar adds alice and bob to the team as members
|
// TODO oscar adds alice and bob to the team as members
|
||||||
var roster = oscar.roster;
|
var roster = oscar.roster;
|
||||||
|
|
||||||
var data = {};
|
var data = {};
|
||||||
data[alice.curveKeys.curvePublic] = {
|
data[alice.curveKeys.curvePublic] = {
|
||||||
name: alice.name,
|
displayName: alice.name,
|
||||||
role: 'MEMBER',
|
// role: 'MEMBER', // MEMBER is implicit
|
||||||
|
notifications: '',
|
||||||
};
|
};
|
||||||
data[bob.curveKeys.curvePublic] = {
|
data[bob.curveKeys.curvePublic] = {
|
||||||
name: bob.name,
|
displayName: bob.name,
|
||||||
role: 'MEMBER',
|
//role: 'MEMBER',
|
||||||
|
notifications: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
roster.add(data, function (err) {
|
roster.add(data, w(function (err) {
|
||||||
if (err) {
|
if (err) { return void console.error(err); }
|
||||||
return void console.error(err);
|
//console.log("SENT ADD COMMAND");
|
||||||
}
|
}));
|
||||||
console.log("SENT ADD COMMAND");
|
|
||||||
});
|
|
||||||
}).nThen(function () {
|
}).nThen(function () {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// TODO alice and bob describe themselves...
|
// TODO alice and bob describe themselves...
|
||||||
|
|
||||||
}).nThen(function () {
|
}).nThen(function () {
|
||||||
@ -368,7 +410,6 @@ nThen(function (w) {
|
|||||||
text: "CAMEMBERT",
|
text: "CAMEMBERT",
|
||||||
}
|
}
|
||||||
}), bob.curveKeys.curvePublic);
|
}), bob.curveKeys.curvePublic);
|
||||||
|
|
||||||
alice.anonRpc.send('WRITE_PRIVATE_MESSAGE', [bob.mailboxChannel, message], w(function (err, response) {
|
alice.anonRpc.send('WRITE_PRIVATE_MESSAGE', [bob.mailboxChannel, message], w(function (err, response) {
|
||||||
if (err) {
|
if (err) {
|
||||||
return void console.error(err);
|
return void console.error(err);
|
||||||
@ -410,8 +451,11 @@ nThen(function (w) {
|
|||||||
|
|
||||||
});
|
});
|
||||||
}).nThen(function () {
|
}).nThen(function () {
|
||||||
alice.shutdown();
|
//alice.shutdown();
|
||||||
bob.shutdown();
|
//bob.shutdown();
|
||||||
|
alice.cleanup();
|
||||||
|
bob.cleanup();
|
||||||
|
oscar.cleanup();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -22,6 +22,10 @@ var factory = function (Util, Hash, CPNetflux, Sortify, nThen, Crypto) {
|
|||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
var isMap = function (obj) {
|
||||||
|
return Boolean(obj && typeof(obj) === 'object' && !Array.isArray(obj));
|
||||||
|
};
|
||||||
|
|
||||||
var canCheckpoint = function (author, state) {
|
var canCheckpoint = function (author, state) {
|
||||||
// if you're here then you've received a checkpoint message
|
// if you're here then you've received a checkpoint message
|
||||||
// that you don't necessarily trust.
|
// that you don't necessarily trust.
|
||||||
@ -90,6 +94,11 @@ var factory = function (Util, Hash, CPNetflux, Sortify, nThen, Crypto) {
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var canUpdateMetadata = function (author, state) {
|
||||||
|
var authorRole = Util.find(state, [author, 'role']);
|
||||||
|
return Boolean(authorRole && ['OWNER', 'ADMIN'].indexOf(authorRole) !== -1);
|
||||||
|
};
|
||||||
|
|
||||||
var shouldCheckpoint = function (state) {
|
var shouldCheckpoint = function (state) {
|
||||||
//
|
//
|
||||||
|
|
||||||
@ -123,30 +132,29 @@ var factory = function (Util, Hash, CPNetflux, Sortify, nThen, Crypto) {
|
|||||||
throw new Error("CANNOT_ADD_TO_UNITIALIZED_ROSTER");
|
throw new Error("CANNOT_ADD_TO_UNITIALIZED_ROSTER");
|
||||||
}
|
}
|
||||||
|
|
||||||
// XXX reject if not all of these are present
|
// iterate over everything and make sure it is valid, throw if not
|
||||||
// displayName
|
|
||||||
// notifications (channel)
|
|
||||||
// XXX if no role is passed, assume MEMBER
|
|
||||||
|
|
||||||
var changed = false;
|
|
||||||
Object.keys(args).forEach(function (curve) {
|
Object.keys(args).forEach(function (curve) {
|
||||||
// FIXME only allow valid curve keys, anything else is pollution
|
// FIXME only allow valid curve keys, anything else is pollution
|
||||||
if (curve.length !== 44) {
|
if (!isValidId(curve)) {
|
||||||
console.log(curve, curve.length);
|
console.log(curve, curve.length);
|
||||||
throw new Error("INVALID_CURVE_KEY");
|
throw new Error("INVALID_CURVE_KEY");
|
||||||
}
|
}
|
||||||
|
// reject commands where the members are not proper objects
|
||||||
|
if (!isMap(args[curve])) { throw new Error("INVALID_CONTENT"); }
|
||||||
|
if (roster.state[curve]) { throw new Error("ALREADY_PRESENT"); }
|
||||||
|
|
||||||
var data = args[curve];
|
var data = args[curve];
|
||||||
|
// if no role was provided, assume MEMBER
|
||||||
|
if (typeof(data.role) !== 'string') { data.role = 'MEMBER'; }
|
||||||
|
|
||||||
|
if (typeof(data.displayName) !== 'string') { throw new Error("DISPLAYNAME_REQUIRED"); }
|
||||||
|
if (typeof(data.notifications) !== 'string') { throw new Error("NOTIFICATIONS_REQUIRED"); }
|
||||||
|
});
|
||||||
|
|
||||||
// ignore anything that isn't a proper object
|
var changed = false;
|
||||||
if (!data || typeof(data) !== 'object' || Array.isArray(data)) {
|
// then iterate again and apply it
|
||||||
return;
|
Object.keys(args).forEach(function (curve) {
|
||||||
}
|
var data = args[curve];
|
||||||
|
|
||||||
// ignore instructions to ADD someone who is already in the roster
|
|
||||||
if (roster.state[curve]) { return; }
|
|
||||||
|
|
||||||
if (!canAddRole(author, data.role, roster.state)) { return; }
|
if (!canAddRole(author, data.role, roster.state)) { return; }
|
||||||
|
|
||||||
// this will result in a change
|
// this will result in a change
|
||||||
@ -166,15 +174,12 @@ var factory = function (Util, Hash, CPNetflux, Sortify, nThen, Crypto) {
|
|||||||
|
|
||||||
var changed = false;
|
var changed = false;
|
||||||
args.forEach(function (curve) {
|
args.forEach(function (curve) {
|
||||||
if (isValidId(curve)) { throw new Error("INVALID_CURVE_KEY"); }
|
if (!isValidId(curve)) { throw new Error("INVALID_CURVE_KEY"); }
|
||||||
|
|
||||||
// don't try to remove something that isn't there
|
// don't try to remove something that isn't there
|
||||||
if (!roster.state[curve]) { return; }
|
if (!roster.state[curve]) { return; }
|
||||||
|
|
||||||
var role = roster.state[curve].role;
|
var role = roster.state[curve].role;
|
||||||
|
|
||||||
if (!canRemoveRole(author, role, roster.state)) { return; }
|
if (!canRemoveRole(author, role, roster.state)) { return; }
|
||||||
|
|
||||||
changed = true;
|
changed = true;
|
||||||
delete roster.state[curve];
|
delete roster.state[curve];
|
||||||
});
|
});
|
||||||
@ -187,42 +192,47 @@ var factory = function (Util, Hash, CPNetflux, Sortify, nThen, Crypto) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (typeof(roster.state) === 'undefined') {
|
if (typeof(roster.state) === 'undefined') {
|
||||||
throw new Error("CANNOT_DESCRIBE_MEMBERS_OF_UNITIALIZED_ROSTER");
|
throw new Error("NOT_READY");
|
||||||
}
|
}
|
||||||
|
|
||||||
var changed = false;
|
// iterate over all the data and make sure it is valid, throw otherwise
|
||||||
Object.keys(args).forEach(function (curve) {
|
Object.keys(args).forEach(function (curve) {
|
||||||
if (!isValidId(curve)) { return; }
|
if (!isValidId(curve)) { throw new Error("INVALID_ID"); }
|
||||||
if (!roster.state[curve]) { return; }
|
if (!roster.state[curve]) { throw new Error("NOT_PRESENT"); }
|
||||||
|
|
||||||
if (!canDescribeTarget(author, curve, roster.state)) { return; }
|
if (!canDescribeTarget(author, curve, roster.state)) { throw new Error("INSUFFICIENT_PERMISSIONS"); }
|
||||||
|
|
||||||
|
var data = args[curve];
|
||||||
|
if (!isMap(data)) { throw new Error("INVALID_ARGUMENTS"); }
|
||||||
|
|
||||||
|
var current = Util.clone(roster.state[curve]);
|
||||||
|
|
||||||
|
// DESCRIBE commands must initialize a displayName if it isn't already present
|
||||||
|
if (typeof(current.displayName) !== 'string' && typeof(data.displayName) !== 'string') { throw new Error('DISPLAYNAME_REQUIRED'); }
|
||||||
|
|
||||||
|
// DESCRIBE commands must initialize a mailbox channel if it isn't already present
|
||||||
|
if (typeof(current.notifications) !== 'string' && typeof(data.displayName) !== 'string') { throw new Error('NOTIFICATIONS_REQUIRED'); }
|
||||||
|
});
|
||||||
|
|
||||||
|
var changed = false;
|
||||||
|
// then do a second pass and apply it if there were changes
|
||||||
|
Object.keys(args).forEach(function (curve) {
|
||||||
|
var current = Util.clone(roster.state[curve]);
|
||||||
|
|
||||||
var data = args[curve];
|
var data = args[curve];
|
||||||
if (!data || typeof(data) !== 'object' || Array.isArray(data)) { return; }
|
|
||||||
|
|
||||||
var current = roster.state[curve];
|
|
||||||
Object.keys(data).forEach(function (key) {
|
Object.keys(data).forEach(function (key) {
|
||||||
if (current[key] === data[key]) { return; }
|
if (current[key] === data[key]) { return; }
|
||||||
changed = true;
|
|
||||||
current[key] = data[key];
|
current[key] = data[key];
|
||||||
});
|
});
|
||||||
});
|
|
||||||
return changed;
|
|
||||||
|
|
||||||
|
if (Sortify(current) !== Sortify(roster.state[curve])) {
|
||||||
/*
|
changed = true;
|
||||||
args: {
|
roster.state[curve] = current;
|
||||||
userkey: {
|
|
||||||
field: newValue
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
*/
|
});
|
||||||
|
|
||||||
// owners can update information about any team member
|
return changed;
|
||||||
// admins can update information about members
|
|
||||||
// members can update information about themselves
|
|
||||||
// non-members cannot update anything
|
|
||||||
//roster = roster;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// XXX what about concurrent checkpoints? Let's solve for race conditions...
|
// XXX what about concurrent checkpoints? Let's solve for race conditions...
|
||||||
@ -255,14 +265,29 @@ var factory = function (Util, Hash, CPNetflux, Sortify, nThen, Crypto) {
|
|||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
// describe the team {name, description}, (only admin/owner)
|
// only admin/owner can change group metadata
|
||||||
commands.TOPIC = function (/* args, author, roster */) {
|
commands.METADATA = function (args, author, roster) {
|
||||||
|
if (!isMap(args)) { throw new Error("INVALID_ARGS"); }
|
||||||
|
|
||||||
};
|
if (!canUpdateMetadata(author, roster.state)) { throw new Error("INSUFFICIENT_PERMISSIONS"); }
|
||||||
|
|
||||||
// add a link to an avatar (only owner/admin can do this)
|
// validate inputs
|
||||||
commands.AVATAR = function (/* args, author, roster */) {
|
Object.keys(args).forEach(function (k) {
|
||||||
|
// can't set metadata to anything other than strings
|
||||||
|
// use empty string to unset a value if you must
|
||||||
|
if (typeof(args[k]) !== 'string') { throw new Error("INVALID_ARGUMENTS"); }
|
||||||
|
});
|
||||||
|
|
||||||
|
var changed = false;
|
||||||
|
// {topic, name, avatar} are all strings...
|
||||||
|
Object.keys(args).forEach(function (k) {
|
||||||
|
// ignore things that won't cause changes
|
||||||
|
if (args[k] === roster.metadata[k]) { return; }
|
||||||
|
|
||||||
|
changed = true;
|
||||||
|
roster.metadata[k] = args[k];
|
||||||
|
});
|
||||||
|
return changed;
|
||||||
};
|
};
|
||||||
|
|
||||||
var handleCommand = function (content, author, roster) {
|
var handleCommand = function (content, author, roster) {
|
||||||
@ -276,12 +301,8 @@ var factory = function (Util, Hash, CPNetflux, Sortify, nThen, Crypto) {
|
|||||||
return commands[command](content[1], author, roster);
|
return commands[command](content[1], author, roster);
|
||||||
};
|
};
|
||||||
|
|
||||||
var clone = function (o) {
|
|
||||||
return JSON.parse(JSON.stringify(o));
|
|
||||||
};
|
|
||||||
|
|
||||||
var simulate = function (content, author, roster) {
|
var simulate = function (content, author, roster) {
|
||||||
return handleCommand(content, author, clone(roster));
|
return handleCommand(content, author, Util.clone(roster));
|
||||||
};
|
};
|
||||||
|
|
||||||
var getMessageId = function (msgString) {
|
var getMessageId = function (msgString) {
|
||||||
@ -298,8 +319,7 @@ var factory = function (Util, Hash, CPNetflux, Sortify, nThen, Crypto) {
|
|||||||
if (!config.anon_rpc) { return void cb("EXPECTED_ANON_RPC"); }
|
if (!config.anon_rpc) { return void cb("EXPECTED_ANON_RPC"); }
|
||||||
|
|
||||||
|
|
||||||
|
var response = Util.response();
|
||||||
|
|
||||||
|
|
||||||
var anon_rpc = config.anon_rpc;
|
var anon_rpc = config.anon_rpc;
|
||||||
|
|
||||||
@ -308,7 +328,16 @@ var factory = function (Util, Hash, CPNetflux, Sortify, nThen, Crypto) {
|
|||||||
var me = keys.myCurvePublic;
|
var me = keys.myCurvePublic;
|
||||||
var channel = config.channel;
|
var channel = config.channel;
|
||||||
|
|
||||||
var ref = {};
|
var ref = {
|
||||||
|
// topic, name, and avatar are all guaranteed to be strings, though they might be empty
|
||||||
|
metadata: {
|
||||||
|
topic: '',
|
||||||
|
name: '',
|
||||||
|
avatar: '',
|
||||||
|
},
|
||||||
|
internal: {},
|
||||||
|
};
|
||||||
|
|
||||||
var roster = {};
|
var roster = {};
|
||||||
|
|
||||||
var events = {
|
var events = {
|
||||||
@ -319,25 +348,46 @@ var factory = function (Util, Hash, CPNetflux, Sortify, nThen, Crypto) {
|
|||||||
roster.on = function (key, handler) {
|
roster.on = function (key, handler) {
|
||||||
if (typeof(events[key]) !== 'object') { throw new Error("unsupported event"); }
|
if (typeof(events[key]) !== 'object') { throw new Error("unsupported event"); }
|
||||||
events[key].reg(handler);
|
events[key].reg(handler);
|
||||||
|
return roster;
|
||||||
};
|
};
|
||||||
|
|
||||||
roster.off = function (key, handler) {
|
roster.off = function (key, handler) {
|
||||||
if (typeof(events[key]) !== 'object') { throw new Error("unsupported event"); }
|
if (typeof(events[key]) !== 'object') { throw new Error("unsupported event"); }
|
||||||
events[key].unreg(handler);
|
events[key].unreg(handler);
|
||||||
|
return roster;
|
||||||
|
};
|
||||||
|
|
||||||
|
roster.once = function (key, handler) {
|
||||||
|
if (typeof(events[key]) !== 'object') { throw new Error("unsupported event"); }
|
||||||
|
var f = function () {
|
||||||
|
handler.apply(null, Array.prototype.slice.call(arguments));
|
||||||
|
events[key].unreg(f);
|
||||||
|
};
|
||||||
|
events[key].reg(f);
|
||||||
|
return roster;
|
||||||
};
|
};
|
||||||
|
|
||||||
roster.getState = function () {
|
roster.getState = function () {
|
||||||
return ref.state;
|
if (!isMap(ref.state)) { return; }
|
||||||
|
|
||||||
|
// XXX return parent element instead of .state ?
|
||||||
|
return Util.clone(ref.state);
|
||||||
};
|
};
|
||||||
|
|
||||||
// XXX you must be able to 'leave' a roster session
|
var webChannel;
|
||||||
|
|
||||||
roster.stop = function () {
|
roster.stop = function () {
|
||||||
// shut down the chainpad-netflux session and...
|
if (webChannel && typeof(webChannel.leave) === 'function') {
|
||||||
// cpNf.leave();
|
webChannel.leave();
|
||||||
|
} else {
|
||||||
|
console.log("FAILED TO LEAVE");
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
var ready = false;
|
var ready = false;
|
||||||
var onReady = function (/* info */) {
|
var onReady = function (info) {
|
||||||
|
//console.log("READY");
|
||||||
|
webChannel = info;
|
||||||
ready = true;
|
ready = true;
|
||||||
cb(void 0, roster);
|
cb(void 0, roster);
|
||||||
};
|
};
|
||||||
@ -359,50 +409,60 @@ var factory = function (Util, Hash, CPNetflux, Sortify, nThen, Crypto) {
|
|||||||
console.log("ROSTER CONNECTED");
|
console.log("ROSTER CONNECTED");
|
||||||
};
|
};
|
||||||
|
|
||||||
// XXX reuse code from RPC ?
|
var isReady = function () {
|
||||||
var pending = {};
|
return Boolean(ready && me);
|
||||||
//var timeouts = {};
|
};
|
||||||
|
|
||||||
var onMessage = function (msg, user, vKey, isCp , hash, author) {
|
var onMessage = function (msg, user, vKey, isCp , hash, author) {
|
||||||
//console.log("onMessage");
|
|
||||||
//console.log(typeof(msg), msg);
|
|
||||||
var parsed = Util.tryParse(msg);
|
var parsed = Util.tryParse(msg);
|
||||||
|
|
||||||
if (!parsed) { return void console.error("could not parse"); }
|
if (!parsed) { return void console.error("could not parse"); }
|
||||||
|
|
||||||
var changed;
|
var changed;
|
||||||
|
var error;
|
||||||
try {
|
try {
|
||||||
changed = handleCommand(parsed, author, ref);
|
changed = handleCommand(parsed, author, ref);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
error = err;
|
||||||
}
|
}
|
||||||
|
|
||||||
var id = getMessageId(hash);
|
var id = getMessageId(hash);
|
||||||
if (typeof(pending[id]) === 'function') {
|
|
||||||
// it was your message, execute a callback
|
if (response.expected(id)) {
|
||||||
if (!changed) {
|
if (error) { return void response.handle(id, [error]); }
|
||||||
pending[id]("NO_CHANGE");
|
try {
|
||||||
} else {
|
if (!changed) {
|
||||||
pending[id](void 0, clone(roster.state));
|
response.handle(id, ['NO_CHANGE']);
|
||||||
|
} else {
|
||||||
|
response.handle(id, [void 0, roster.getState()]);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.log('CAUGHT', err);
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// it was not your message, or it timed out...
|
|
||||||
// execute change ?
|
|
||||||
console.log("HASH", hash);
|
|
||||||
}
|
}
|
||||||
if (changed) { events.change.fire(); }
|
/*
|
||||||
|
else {
|
||||||
|
if (isReady()) {
|
||||||
|
console.log("unexpected message [%s]", hash, msg);
|
||||||
|
console.log("received by %s", me);
|
||||||
|
}
|
||||||
|
// it was not your message, or it timed out...
|
||||||
|
}*/
|
||||||
|
|
||||||
return void console.log(msg);
|
// if a checkpoint was successfully applied, emit an event
|
||||||
|
if (parsed[0] === 'CHECKPOINT' && changed) {
|
||||||
|
events.checkpoint.fire(hash);
|
||||||
|
} else if (changed) {
|
||||||
|
events.change.fire();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
var isReady = function () {
|
|
||||||
return Boolean(ready && me);
|
|
||||||
};
|
|
||||||
|
|
||||||
var metadata, crypto;
|
var metadata, crypto;
|
||||||
var send = function (msg, _cb) {
|
var send = function (msg, cb) {
|
||||||
var cb = Util.once(Util.mkAsync(_cb));
|
if (!isReady()) {
|
||||||
if (!isReady()) { return void cb("NOT_READY"); }
|
return void cb("NOT_READY");
|
||||||
|
}
|
||||||
|
|
||||||
var changed = false;
|
var changed = false;
|
||||||
try {
|
try {
|
||||||
@ -411,23 +471,30 @@ var factory = function (Util, Hash, CPNetflux, Sortify, nThen, Crypto) {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
return void cb(err);
|
return void cb(err);
|
||||||
}
|
}
|
||||||
if (!changed) { return void cb("NO_CHANGE"); }
|
if (!changed) {
|
||||||
|
return void cb("NO_CHANGE");
|
||||||
|
}
|
||||||
|
|
||||||
var ciphertext = crypto.encrypt(Sortify(msg));
|
var ciphertext = crypto.encrypt(Sortify(msg));
|
||||||
|
|
||||||
var id = getMessageId(ciphertext);
|
var id = getMessageId(ciphertext);
|
||||||
|
|
||||||
|
//console.log("Sending with id [%s]", id, msg);
|
||||||
|
//console.log();
|
||||||
|
|
||||||
|
response.expect(id, cb, 3000);
|
||||||
anon_rpc.send('WRITE_PRIVATE_MESSAGE', [
|
anon_rpc.send('WRITE_PRIVATE_MESSAGE', [
|
||||||
channel,
|
channel,
|
||||||
ciphertext
|
ciphertext
|
||||||
], function (err) {
|
], function (err) {
|
||||||
if (err) { return void cb(err); }
|
if (err) { return response.handle(id, [err]); }
|
||||||
pending[id] = cb;
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
roster.init = function (_data, cb) {
|
roster.init = function (_data, _cb) {
|
||||||
var data = clone(_data);
|
var cb = Util.once(Util.mkAsync(_cb));
|
||||||
|
if (ref.state) { return void cb("ALREADY_INITIALIZED"); }
|
||||||
|
var data = Util.clone(_data);
|
||||||
data.role = 'OWNER';
|
data.role = 'OWNER';
|
||||||
var state = {};
|
var state = {};
|
||||||
state[me] = data;
|
state[me] = data;
|
||||||
@ -435,22 +502,75 @@ var factory = function (Util, Hash, CPNetflux, Sortify, nThen, Crypto) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// commands
|
// commands
|
||||||
roster.checkpoint = function () {
|
roster.checkpoint = function (_cb) {
|
||||||
|
var cb = Util.once(Util.mkAsync(_cb));
|
||||||
|
var state = ref.state;
|
||||||
|
if (!state) { return cb("UNINITIALIZED"); }
|
||||||
send([ 'CHECKPOINT', ref.state], cb);
|
send([ 'CHECKPOINT', ref.state], cb);
|
||||||
};
|
};
|
||||||
|
|
||||||
roster.add = function (data, cb) {
|
roster.add = function (data, _cb) {
|
||||||
|
var cb = Util.once(Util.mkAsync(_cb));
|
||||||
|
var state = ref.state;
|
||||||
|
if (!state) { return cb("UNINITIALIZED"); }
|
||||||
|
if (!isMap(data)) { return void cb("INVALID_ARGUMENTS"); }
|
||||||
|
|
||||||
|
// don't add members that are already present
|
||||||
|
// use DESCRIBE to amend
|
||||||
|
Object.keys(data).forEach(function (curve) {
|
||||||
|
if (!isValidId(curve) || isMap(state[curve])) { return delete data[curve]; }
|
||||||
|
});
|
||||||
|
|
||||||
send([ 'ADD', data ], cb);
|
send([ 'ADD', data ], cb);
|
||||||
};
|
};
|
||||||
|
|
||||||
roster.remove = function (data, cb) {
|
roster.remove = function (data, _cb) {
|
||||||
send([ 'REMOVE', data ], cb);
|
var cb = Util.once(Util.mkAsync(_cb));
|
||||||
|
var state = ref.state;
|
||||||
|
if (!state) { return cb("UNINITIALIZED"); }
|
||||||
|
|
||||||
|
if (!Array.isArray(data)) { return void cb("INVALID_ARGUMENTS"); }
|
||||||
|
|
||||||
|
var toRemove = [];
|
||||||
|
var current = Object.keys(state);
|
||||||
|
data.forEach(function (curve) {
|
||||||
|
// don't try to remove elements which are not in the current state
|
||||||
|
if (current.indexOf(curve) === -1) { return; }
|
||||||
|
toRemove.push(curve);
|
||||||
|
});
|
||||||
|
|
||||||
|
send([ 'RM', toRemove ], cb);
|
||||||
};
|
};
|
||||||
|
|
||||||
roster.describe = function (data, cb) {
|
roster.describe = function (data, _cb) {
|
||||||
|
var cb = Util.once(Util.mkAsync(_cb));
|
||||||
|
var state = ref.state;
|
||||||
|
if (!state) { return cb("UNINITIALIZED"); }
|
||||||
|
if (!isMap(data)) { return void cb("INVALID_ARGUMENTS"); }
|
||||||
|
|
||||||
|
Object.keys(data).forEach(function (curve) {
|
||||||
|
var member = data[curve];
|
||||||
|
if (!isMap(member)) { delete data[curve]; }
|
||||||
|
// don't send fields that won't result in a change
|
||||||
|
Object.keys(member).forEach(function (k) {
|
||||||
|
if (member[k] === state[curve][k]) { delete member[k]; }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
send(['DESCRIBE', data], cb);
|
send(['DESCRIBE', data], cb);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
roster.metadata = function (data, _cb) {
|
||||||
|
var cb = Util.once(Util.mkAsync(_cb));
|
||||||
|
var metadata = ref.metadata;
|
||||||
|
if (!isMap(data)) { return void cb("INVALID_ARGUMENTS"); }
|
||||||
|
|
||||||
|
Object.keys(data).forEach(function (k) {
|
||||||
|
if (data[k] === metadata[k]) { delete data[k]; }
|
||||||
|
});
|
||||||
|
send(['METADATA', data], cb);
|
||||||
|
};
|
||||||
|
|
||||||
nThen(function (w) {
|
nThen(function (w) {
|
||||||
// get metadata so we know the owners and validateKey
|
// get metadata so we know the owners and validateKey
|
||||||
anon_rpc.send('GET_METADATA', channel, function (err, data) {
|
anon_rpc.send('GET_METADATA', channel, function (err, data) {
|
||||||
@ -458,7 +578,7 @@ var factory = function (Util, Hash, CPNetflux, Sortify, nThen, Crypto) {
|
|||||||
w.abort();
|
w.abort();
|
||||||
return void console.error(err);
|
return void console.error(err);
|
||||||
}
|
}
|
||||||
metadata = ref.metadata = (data && data[0]) || undefined;
|
metadata = ref.internal.metadata = (data && data[0]) || undefined;
|
||||||
console.log("TEAM_METADATA", metadata);
|
console.log("TEAM_METADATA", metadata);
|
||||||
});
|
});
|
||||||
}).nThen(function (w) {
|
}).nThen(function (w) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user