cryptpad/www/common/outer/roster.js
2022-11-04 16:42:04 +01:00

954 lines
35 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

(function () {
var factory = function (Util, Hash, CPNetflux, Sortify, nThen, Crypto, Feedback) {
var Roster = {};
// this constant is somewhat arbitrary.
// Adjust it as you like to suit performance expectations
var CHECKPOINT_INTERVAL = 25;
var TIMEOUT_INTERVAL = 30000; // TIMEOUT after 30s
/*
roster: {
state: {
members: {
user0CurveKey: {
notifications: "", // required
displayName: "", // required
role: "OWNER|ADMIN|MEMBER|VIEWER", // VIEWER if not specified
profile: "",
title: ""
},
user1CurveKey: {
...
}
},
metadata: {
// guaranteed to be strings, but may be empty
topic: '',
name: '',
avatar: '',
// anything else you use may not be defined
}
}
}
*/
var isMap = function (obj) {
return Boolean(obj && typeof(obj) === 'object' && !Array.isArray(obj));
};
var getMessageId = function (msgString) {
return msgString.slice(0, 64);
};
var canCheckpoint = function (author, members) {
// if you're here then you've received a checkpoint message
// that you don't necessarily trust.
// find the author's role from your knoweldge of the state
var role = Util.find(members, [author, 'role']);
// and check if it is 'OWNER' or 'ADMIN'
return ['OWNER', 'ADMIN'].indexOf(role) !== -1;
};
var isValidRole = function (role) {
return ['OWNER', 'ADMIN', 'MEMBER', 'VIEWER'].indexOf(role) !== -1;
};
var isSelfDowngrade = function (author, curve, role, state) {
// Make sure you want to describe yourself
var selfDescribe = author === curve && state[curve];
if (!selfDescribe) { return false; }
// ADMIN and OWNER can always update roles
// we only need to allow MEMBER to downgrade themselves to VIEWER
var authorRole = Util.find(state, [author, 'role']);
if (authorRole === "MEMBER") { return role === 'VIEWER'; }
};
var canAddRole = function (author, role, members) {
var authorRole = Util.find(members, [author, 'role']);
if (!authorRole) { return false; }
// nobody can add an invalid role
if (!isValidRole(role)) { return false; }
// owners can add any valid role they want
if (authorRole === 'OWNER') { return true; }
// admins can add other admins or members or viewers
if (authorRole === "ADMIN") { return ['ADMIN', 'MEMBER', 'VIEWER'].indexOf(role) !== -1; }
// (MEMBER, other) can't add anyone of any role
return false;
};
var isValidId = function (id) {
return typeof(id) === 'string' && id.length === 44;
};
var canDescribeTarget = function (author, curve, state) {
// you must be in the group to describe anyone
if (!state[curve]) { return false; }
// anyone can describe themself
if (author === curve && state[curve]) { return true; }
var authorRole = Util.find(state, [author, 'role']);
var targetRole = Util.find(state, [curve, 'role']);
// something is really wrong if there's no authorRole
if (!authorRole) { return false; }
// owners can do whatever they want
if (authorRole === 'OWNER') { return true; }
// admins can describe anyone escept owners
if (authorRole === 'ADMIN' && targetRole !== 'OWNER') { return true; }
// members can't describe others
return false;
};
var canRemoveRole = function (author, role, members) {
var authorRole = Util.find(members, [author, 'role']);
if (!authorRole) { return false; }
// owners can remove anyone they want
if (authorRole === 'OWNER') { return true; }
// admins can remove other admins or members
if (authorRole === "ADMIN") { return ["ADMIN", "MEMBER", "VIEWER"].indexOf(role) !== -1; }
// MEMBERS and non-members cannot remove anyone of any role
return false;
};
var canUpdateMetadata = function (author, members) {
var authorRole = Util.find(members, [author, 'role']);
return Boolean(authorRole && ['OWNER', 'ADMIN'].indexOf(authorRole) !== -1);
};
var shouldCheckpoint = function (me, ref) {
// if you can't send valid checkpoints, don't try
if (!canCheckpoint(me, ref.state.members)) { return false; }
// avoid sending checkpoints too often
// it's a balance between network constraints
// and the size of the roster's log
var since = ref.internal.sinceLastCheckpoint;
if (!since || typeof(since) !== 'number' || since < CHECKPOINT_INTERVAL) {
return false;
}
// if you can't think of any other reason not to...
return true;
};
var commands = Roster.commands = {};
/* Commands are functions with the signature
(args_any, base46_author_string, roster_map, optional_base64_message_id) => boolean
they:
* throw if any of their arguments are invalid
* return true if their application to previous state results in a change
* mutate the local account of the current state
changes to the state can be simulated locally before being sent.
if the simulation throws or returns false, don't send.
*/
// the author is trying to add someone to the roster
// owners can add any role
commands.ADD = function (args, author, roster) {
if (!isMap(args)) { throw new Error("INVALID ARGS"); }
if (!roster.internal.initialized) { throw new Error("UNITIALIZED"); }
if (typeof(roster.state.members) === 'undefined') {
throw new Error("CANNOT_ADD_TO_UNITIALIZED_ROSTER");
}
var members = roster.state.members;
// iterate over everything and make sure it is valid, throw if not
Object.keys(args).forEach(function (curve) {
// FIXME only allow valid curve keys, anything else is pollution
if (!isValidId(curve)) {
console.log(curve, curve.length);
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 (members[curve]) { throw new Error("ALREADY_PRESENT"); }
var data = args[curve];
// if no role was provided, assume MEMBER
if (typeof(data.role) !== 'string') { data.role = 'MEMBER'; }
if (!canAddRole(author, data.role, members)) {
throw new Error("INSUFFICIENT_PERMISSIONS");
}
if (typeof(data.displayName) !== 'string') { throw new Error("DISPLAYNAME_REQUIRED"); }
if (typeof(data.notifications) !== 'string') { throw new Error("NOTIFICATIONS_REQUIRED"); }
});
var changed = false;
// then iterate again and apply it
Object.keys(args).forEach(function (curve) {
// this will result in a change
changed = true;
members[curve] = args[curve];
});
return changed;
};
commands.RM = function (args, author, roster) {
if (!Array.isArray(args)) { throw new Error("INVALID_ARGS"); }
if (typeof(roster.state.members) === 'undefined') {
throw new Error("CANNOT_RM_FROM_UNITIALIZED_ROSTER");
}
var members = roster.state.members;
// validate first...
args.forEach(function (curve) {
if (!isValidId(curve)) { throw new Error("INVALID_CURVE_KEY"); }
// even members can remove themselves
if (curve === author) { return; }
// but if it concerns anyone else, validate that the author has sufficient permissions
var role = members[curve].role;
if (!canRemoveRole(author, role, members)) { throw new Error("INSUFFICIENT_PERMISSIONS"); }
});
var changed = false;
args.forEach(function (curve) {
// don't try to remove something that isn't there
if (!members[curve]) { return; }
changed = true;
delete members[curve];
});
return changed;
};
commands.DESCRIBE = function (args, author, roster) {
if (!args || typeof(args) !== 'object' || Array.isArray(args)) {
throw new Error("INVALID_ARGUMENTS");
}
if (typeof(roster.state.members) === 'undefined') {
throw new Error("NOT_READY");
}
var members = roster.state.members;
// iterate over all the data and make sure it is valid, throw otherwise
Object.keys(args).forEach(function (curve) {
if (!isValidId(curve)) { throw new Error("INVALID_ID"); }
if (!members[curve]) { throw new Error("NOT_PRESENT"); }
if (!canDescribeTarget(author, curve, members)) { throw new Error("INSUFFICIENT_PERMISSIONS"); }
var data = args[curve];
if (!isMap(data)) { throw new Error("INVALID_ARGUMENTS"); }
var current = Util.clone(members[curve]);
if (typeof(data.role) === 'string') { // they're trying to change the role...
// throw if they're trying to upgrade to something greater
if (!isSelfDowngrade(author, curve, data.role, members) &&
!canAddRole(author, data.role, members)) {
throw new Error("INSUFFICIENT_PERMISSIONS");
}
}
// 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');
}
if (['undefined', 'string'].indexOf(typeof(data.displayName)) === -1) {
throw new Error("INVALID_DISPLAYNAME");
}
// DESCRIBE commands must initialize a mailbox channel if it isn't already present
if (typeof(current.notifications) !== 'string' && typeof(data.notifications) !== 'string') {
throw new Error('NOTIFICATIONS_REQUIRED');
}
if (['undefined', 'string'].indexOf(typeof(data.notifications)) === -1) {
throw new Error("INVALID_NOTIFICATIONS");
}
});
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(members[curve]);
var data = args[curve];
Object.keys(data).forEach(function (key) {
// when null is passed as new data and it wasn't considered an invalid change
// remove it from the map. This is how you delete things properly
if (typeof(current[key]) !== 'undefined' && data[key] === null) { return void delete current[key]; }
current[key] = data[key];
});
if (Sortify(current) !== Sortify(members[curve])) {
changed = true;
members[curve] = current;
}
});
return changed;
};
commands.CHECKPOINT = function (args, author, roster) {
// args: complete state
// args should be a map
if (!isMap(args)) { throw new Error("INVALID_CHECKPOINT_STATE"); }
if (!roster.internal.initialized) {
//console.log("INITIALIZING");
// either you're connecting from the beginning of the log
// or from a trusted lastKnownHash.
// Either way, initialize the roster state
roster.state = args;
var metadata = roster.state.metadata = roster.state.metadata || {};
metadata.topic = metadata.topic || '';
metadata.name = metadata.name || '';
metadata.avatar = metadata.avatar || '';
roster.internal.initialized = true;
return true;
} else if (Sortify(args) !== Sortify(roster.state)) {
// a checkpoint must reinsert the previous state
throw new Error("CHECKPOINT_DOES_NOT_MATCH_PREVIOUS_STATE");
}
// otherwise, you're iterating over the log from a previous checkpoint
// so you should know everyone's role
// owners and admins can checkpoint. members and non-members cannot
if (!canCheckpoint(author, roster.state.members)) { throw new Error("INSUFFICIENT_PERMISSIONS"); }
// set the state, and indicate that a change was made
roster.state = args;
return true;
};
var MANDATORY_METADATA_FIELDS = [
'avatar',
'name',
'topic',
];
// only admin/owner can change group metadata
commands.METADATA = function (args, author, roster) {
if (!isMap(args)) { throw new Error("INVALID_ARGS"); }
if (!canUpdateMetadata(author, roster.state.members)) { throw new Error("INSUFFICIENT_PERMISSIONS"); }
// validate inputs
Object.keys(args).forEach(function (k) {
if (args[k] === null) {
if (MANDATORY_METADATA_FIELDS.indexOf(k) === -1) { return; }
throw new Error('CANNOT_REMOVE_MANDATORY_METADATA');
}
// 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) {
if (typeof(roster.state.metadata[k]) !== 'undefined' && args[k] === null) {
changed = true;
delete roster.state.metadata[k];
}
// ignore things that won't cause changes
if (args[k] === roster.state.metadata[k]) { return; }
changed = true;
roster.state.metadata[k] = args[k];
});
return changed;
};
commands.INVITE = function (args, author, roster) {
// an invitation is created with an ephemeral curve public key
// that key is ultimately given to the user you'd like on your team
// that user can exploit their possession of the public key to remove
// the pending invitation with their actual data.
if (!isMap(args)) { throw new Error('INVALID_ARGS'); }
if (!roster.internal.initialized) { throw new Error("UNINITIALIED"); }
if (typeof(roster.state.members) === 'undefined') {
throw new Error("CANNOT+INVITE_TO_UNINITIALIED_ROSTER");
}
var members = roster.state.members;
Object.keys(args).forEach(function (curve) {
if (!isValidId(curve)) {
console.log(curve, curve.length);
throw new Error("INVALID_CURVE_KEY");
}
// reject commandws wehere the members are not proper objects
if (!isMap(args[curve])) { throw new Error("INVALID_CONTENT"); }
if (members[curve]) { throw new Error("ARLEADY_PRESENT"); }
var data = args[curve];
// if no role was provided, assume VIEWER
if (typeof(data.role) !== 'string') { data.role = "VIEWER"; }
// assume that invitations are 'pending' unless stated otherwise
if (typeof(data.pending) === 'undefined') { data.pending = true; }
if (!canAddRole(author, data.role, members)) {
throw new Error("INSUFFICIENT_PERMISSIONS");
}
if (typeof(data.displayName) !== 'string' || !data.displayName) { throw new Error("DISPLAYNAME_REQUIRED"); }
//if (typeof(data.notifications) !== 'string') { throw new Error("NOTIFICATIONS_REQUIRED"); }
});
/*
{
<ephemeralCurveKey>: {
role: ??? || 'VIEWER',
displayName: '',
pending: true,
}
}
*/
var changed = false;
Object.keys(args).forEach(function (curve) {
changed = true;
members[curve] = args[curve];
});
return changed;
};
commands.ACCEPT = function (args, author, roster) {
if (!roster.internal.initialized) { throw new Error("UNINITIALIED"); }
if (typeof(roster.state.members) === 'undefined') {
throw new Error("CANNOT_ADD_TO_UNINITIALIED_ROSTER");
}
// an ACCEPT command replaces a pending invitation's curve key with a new one
// after which the invited member can use their actual curve key to describe themselves
// the author must have been invited already...
var members = roster.state.members;
// so you must already be in the members list
if (!isMap(members[author])) { throw new Error("INSUFFICIENT_PERMISSIONS"); }
// and your membership must indicate that you are 'pending'
if (!members[author].pending) { throw new Error("ALREADY_PRESENT"); }
// args should be a string
if (typeof(args) !== 'string') { throw new Error("INVALID_ARGS"); }
// ...and a valid curve key
if (!isValidId(args)) { throw new Error("INVALID_CURVE_KEY"); }
var curve = args;
// and the curve key must not already be a member
if (typeof(members[curve]) !== 'undefined') { throw new Error("MEMBER_ALREADY_PRESENT"); }
// copy the new profile from the old one
var clone = Util.clone(members[author]);
delete clone.remaining;
delete clone.totalUses;
delete clone.inviteChannel;
delete clone.previewChannel;
members[curve] = clone;
// XXX
var remaining = members[author].remaining || 1;
if (remaining === -1) { return true; } // Infinite uses, keep the link
if (remaining > 1) { // Remove 1 use
members[author].remaining = remaining - 1;
} else { // Disable link
delete members[author];
}
return true;
};
var handleCommand = function (content, author, roster) {
if (!(Array.isArray(content) && typeof(author) === 'string')) {
throw new Error("INVALID ARGUMENTS");
}
var command = content[0];
if (typeof(commands[command]) !== 'function') { throw new Error('INVALID_COMMAND'); }
return commands[command](content[1], author, roster);
};
var simulate = function (content, author, roster) {
return handleCommand(content, author, Util.clone(roster));
};
Roster.create = function (config, _cb) {
if (typeof(_cb) !== 'function') { throw new Error("EXPECTED_CALLBACK"); }
var cb = Util.once(Util.mkAsync(_cb));
if (!config.network) { return void cb("EXPECTED_NETWORK"); }
if (!config.channel || typeof(config.channel) !== 'string' || config.channel.length !== 32) { return void cb("EXPECTED_CHANNEL"); }
if (!config.keys || typeof(config.keys) !== 'object') { return void cb("EXPECTED_CRYPTO_KEYS"); }
if (!config.store) { return void cb("EXPECTED_STORE"); }
var response = Util.response(function (label, info) {
console.error('ROSTER_RESPONSE__' + label, info);
});
var store = config.store;
var keys = config.keys;
var me = keys.myCurvePublic;
var channel = config.channel;
var lastKnownHash = config.lastKnownHash || -1;
// make sure we don't send -1 (ask for full history) when we are trying to create a new team
if (config.newTeam) {
lastKnownHash = undefined;
}
var ref = {
state: {
members: { },
metadata: { },
},
internal: {
initialized: false,
sinceLastCheckpoint: 0,
lastCheckpointHash: lastKnownHash,
},
};
var roster = {};
var events = {
change: Util.mkEvent(),
checkpoint: Util.mkEvent(),
};
roster.on = function (key, handler) {
if (typeof(events[key]) !== 'object') { throw new Error("unsupported event"); }
events[key].reg(handler);
return roster;
};
roster.off = function (key, handler) {
if (typeof(events[key]) !== 'object') { throw new Error("unsupported event"); }
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 () {
//if (!isMap(ref.state)) { return; }
return Util.clone(ref.state);
};
roster.getLastCheckpointHash = function () {
return ref.internal.lastCheckpointHash || -1;
};
var clearPendingCheckpoints = function () {
// clear any pending checkpoints you might have...
if (ref.internal.pendingCheckpointId) {
response.clear(ref.internal.pendingCheckpointId);
delete ref.internal.pendingCheckpointId;
}
clearTimeout(ref.internal.checkpointTimeout);
delete ref.internal.checkpointTimeout;
};
var webChannel;
roster.stop = function () {
if (ref.internal.cpNetflux && typeof(ref.internal.cpNetflux.stop) === "function") {
ref.internal.cpNetflux.stop();
clearPendingCheckpoints();
} else {
console.log("FAILED TO LEAVE");
}
};
var ready = false;
var onCacheReady = function () {
if (!config.onCacheReady) { return; }
var state = ref.state;
if (!Object.keys(state.members || {}).length) {
// No member, corrupted cache
try {
ref.internal.cpNetflux.resetCache();
} catch (e) { console.error(e); }
return void config.onCacheReady({error: "CORRUPTED"});
}
config.onCacheReady(roster);
};
var onReady = function (info) {
//console.log("READY");
webChannel = info;
ready = true;
cb(void 0, roster);
};
// onError (deleted or expired)
// you won't be able to connect
// onMetadataUpdate
// update owners?
// deleted while you are open
// emit an event
var onChannelError = function (info) {
if (Feedback) { Feedback.send('ROSTER_CHANNEL_ERROR='+(info && info.type)); }
if (info && info.type === "EUNKNOWN") {
// chainpad-netflux should recover by itself
return;
}
if (!ready) { return void cb(info); }
console.error("CHANNEL_ERROR", info);
};
var onConnectionChange = function (info) {
if (info.state) { return; }
// Disconnect: don't send event anymore until ready
ready = false;
};
var onConnect = function (/* wc, sendMessage */) {
console.log("ROSTER CONNECTED");
};
var isReady = function () {
return Boolean(ready && me);
};
var onMessage = function (msg, user, vKey, isCp , hash, author) {
// count messages received since the last checkpoint
// even if they fail to parse
ref.internal.sinceLastCheckpoint++;
var parsed = Util.tryParse(msg);
if (!parsed) { return void console.error("could not parse"); }
var changed;
var error;
try {
changed = handleCommand(parsed, author, ref);
} catch (err) {
error = err.message;
}
var id = getMessageId(hash);
if (response.expected(id)) {
if (error) { return void response.handle(id, [error]); }
try {
if (!changed) {
response.handle(id, ['NO_CHANGE']);
console.log(msg);
} else {
response.handle(id, [void 0, roster.getState()]);
}
} catch (err) {
console.log('CAUGHT', err);
}
}
// if a checkpoint was successfully applied, emit an event
if (parsed[0] === 'CHECKPOINT' && changed) {
if (isReady()) { events.checkpoint.fire(hash); }
// reset the counter for messages since the last checkpoint
ref.internal.sinceLastCheckpoint = 0;
ref.internal.lastCheckpointHash = hash;
} else if (changed) {
if (isReady()) { events.change.fire(); }
}
// CHECKPOINT logic...
clearPendingCheckpoints();
if (!isReady() || !shouldCheckpoint(me, ref)) { return; }
// a random number of seconds between 5 and 25
var delay = (1000 * Math.floor(Math.random() * 20)) + 5000;
// if you're here then you can and should send a checkpoint
// but since multiple users who can and should might be online at once
// and since they'll all trigger this process at the same time...
// we want to stagger attempts at random intervals
setTimeout(function () {
ref.internal.pendingCheckpointId = roster.checkpoint(function (err) {
if (err) { console.error(err); }
});
}, delay);
};
var isCacheCheckpoint = function (msg, author) {
var parsed = Util.tryParse(msg);
if (parsed[0] !== 'CHECKPOINT') { return false; }
var changed = simulate(parsed, author, ref);
return changed;
};
var metadata, crypto;
var send = function (msg, cb) {
if (!isReady()) { return void cb("NOT_READY"); }
var anon_rpc = store.anon_rpc;
if (!anon_rpc) { return void cb("ANON_RPC_NOT_READY"); }
var changed = false;
try {
// simulate the command before you send it
changed = simulate(msg, keys.myCurvePublic, ref);
} catch (err) {
return void cb(err.message);
}
if (!changed) { return void cb("NO_CHANGE"); }
var ciphertext = crypto.encrypt(Sortify(msg));
var id = getMessageId(ciphertext);
//console.log("Sending with id [%s]", id, msg);
//console.log();
response.expect(id, cb, TIMEOUT_INTERVAL);
anon_rpc.send('WRITE_PRIVATE_MESSAGE', [
channel,
ciphertext
], function (err) {
if (err) { return response.handle(id, [err.message || err]); }
});
return id;
};
roster.init = function (_data, _cb) {
var cb = Util.once(Util.mkAsync(_cb));
if (ref.internal.initialized) { return void cb("ALREADY_INITIALIZED"); }
if (!isMap(_data)) { return void cb("INVALID_ARGUMENTS"); }
var data = Util.clone(_data);
data.role = 'OWNER';
var members = {};
members[me] = data;
send([ 'CHECKPOINT', { members: members } ], cb);
};
// commands
roster.checkpoint = function (_cb) {
var cb = Util.once(Util.mkAsync(_cb));
send([ 'CHECKPOINT', Util.clone(ref.state)], cb);
};
roster.add = function (_data, _cb) {
var cb = Util.once(Util.mkAsync(_cb));
//var state = ref.state;
if (!ref.internal.initialized) { return cb("UNINITIALIZED"); }
if (!isMap(_data)) { return void cb("INVALID_ARGUMENTS"); }
var data = Util.clone(_data);
// don't add members that are already present
// use DESCRIBE to amend
Object.keys(data).forEach(function (curve) {
if (!isValidId(curve) || isMap(ref.state.members[curve])) { return delete data[curve]; }
});
send([ 'ADD', data ], cb);
};
roster.remove = function (_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 data = Util.clone(_data);
var toRemove = [];
var current = Object.keys(state.members);
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) {
var cb = Util.once(Util.mkAsync(_cb));
var state = ref.state;
if (!state) { return cb("UNINITIALIZED"); }
if (!isMap(_data)) { return void cb("INVALID_ARGUMENTS"); }
var data = Util.clone(_data);
if (Object.keys(data).some(function (curve) {
var member = data[curve];
if (!isMap(member)) { delete data[curve]; }
// validate that you're trying to describe a user that is present
if (!isMap(state.members[curve])) { return true; }
// don't send fields that won't result in a change
Object.keys(member).forEach(function (k) {
if (member[k] === state.members[curve][k]) { delete member[k]; }
});
})) {
// returning true in the above loop indicates that something was invalid
return void cb("INVALID_ARGUMENTS");
}
send(['DESCRIBE', data], cb);
};
roster.metadata = function (_data, _cb) {
var cb = Util.once(Util.mkAsync(_cb));
var metadata = ref.state.metadata;
if (!isMap(_data)) { return void cb("INVALID_ARGUMENTS"); }
var data = Util.clone(_data);
Object.keys(data).forEach(function (k) {
if (data[k] === metadata[k]) { delete data[k]; }
});
send(['METADATA', data], cb);
};
// supports multiple invite
roster.invite = function (_data, _cb) {
var cb = Util.once(Util.mkAsync(_cb));
var state = ref.state;
if (!state) { return cb("UNINITIALIZED"); }
if (!ref.internal.initialized) { return cb("UNINITIALIZED"); }
if (!isMap(_data)) { return void cb("INVALID_ARGUMENTS"); }
var data = Util.clone(_data);
Object.keys(data).forEach(function (curve) {
if (!isValidId(curve) || isMap(ref.state.members[curve])) { return delete data[curve]; }
});
send(['INVITE', data], cb);
};
roster.accept = function (_data, _cb) {
var cb = Util.once(Util.mkAsync(_cb));
if (typeof(_data) !== 'string' || !isValidId(_data)) {
return void cb("INVALID_ARGUMENTS");
}
send([ 'ACCEPT', _data ], cb);
};
nThen(function (w) {
// get metadata so we know the owners and validateKey
if (!store.anon_rpc) { return; }
store.anon_rpc.send('GET_METADATA', channel, function (err, data) {
if (err) {
w.abort();
return void console.error(err);
}
metadata = ref.internal.metadata = (data && data[0]) || undefined;
});
}).nThen(function (w) {
if (!config.keys.teamEdPublic && metadata && metadata.validateKey) {
config.keys.teamEdPublic = metadata.validateKey;
}
if (!config.keys.teamEdPublic) {
w.abort();
return void cb("NO_VALIDATE_KEY");
}
try {
crypto = Crypto.Team.createEncryptor(config.keys);
} catch (err) {
w.abort();
return void cb(err);
}
}).nThen(function () {
if (typeof(lastKnownHash) === 'string') {
console.log("Synchronizing from checkpoint");
}
ref.internal.cpNetflux = CPNetflux.start({
// if you don't have a lastKnownHash you will need the full history
// passing -1 forces the server to send all messages, otherwise
// malicious users with the signing key could send cp| messages
// and fool new users into initializing their session incorrectly
lastKnownHash: lastKnownHash,
network: config.network,
channel: config.channel,
crypto: crypto,
validateKey: config.keys.teamEdPublic,
owners: config.owners,
Cache: config.Cache,
isCacheCheckpoint: isCacheCheckpoint,
onCacheReady: onCacheReady,
onChannelError: onChannelError,
onReady: onReady,
onConnect: onConnect,
onConnectionChange: onConnectionChange,
onMessage: onMessage,
noChainPad: true,
});
});
};
return Roster;
};
if (typeof(module) !== 'undefined' && module.exports) {
module.exports = factory(
require("../common-util"),
require("../common-hash"),
require("../../bower_components/chainpad-netflux/chainpad-netflux.js"),
require("../../bower_components/json.sortify"),
require("nthen"),
require("../../bower_components/chainpad-crypto/crypto"),
null // no feedback here
);
} else if ((typeof(define) !== 'undefined' && define !== null) && (define.amd !== null)) {
require.config({ paths: { 'json.sortify': '/bower_components/json.sortify/dist/JSON.sortify' } });
define([
'/common/common-util.js',
'/common/common-hash.js',
'chainpad-netflux',
'json.sortify',
'/bower_components/nthen/index.js',
'/bower_components/chainpad-crypto/crypto.js',
'/common/common-feedback.js',
//'/bower_components/tweetnacl/nacl-fast.min.js',
], function (Util, Hash, CPNF, Sortify, nThen, Crypto, Feedback) {
return factory.apply(null, [
Util,
Hash,
CPNF,
Sortify,
nThen,
Crypto,
Feedback
]);
});
} else {
// I'm not gonna bother supporting any other kind of instanciation
}
}());