diff --git a/www/common/common-messaging.js b/www/common/common-messaging.js index 8e27b3fd9..a9d618225 100644 --- a/www/common/common-messaging.js +++ b/www/common/common-messaging.js @@ -79,9 +79,6 @@ define([ }; Msg.updateMyData = function (store, curve) { - if (store.messenger) { - store.messenger.updateMyData(); - } var myData = createData(store.proxy); if (store.proxy.friends) { store.proxy.friends.me = myData; @@ -103,5 +100,28 @@ define([ eachFriend(store.proxy.friends || {}, todo); }; + Msg.removeFriend = function (store, curvePublic, cb) { + var proxy = store.proxy; + var friend = proxy.friends[curvePublic]; + if (!friend) { return void cb({error: 'ENOENT'}); } + if (!friend.notifications || !friend.channel) { return void cb({error: 'EINVAL'}); } + + store.mailbox.sendTo('UNFRIEND', { + curvePublic: proxy.curvePublic + }, { + channel: friend.notifications, + curvePublic: friend.curvePublic + }, function (obj) { + if (obj && obj.error) { + return void cb(obj); + } + store.messenger.onFriendRemoved(curvePublic, friend.channel); + delete proxy.friends[curvePublic]; + Realtime.whenRealtimeSyncs(store.realtime, function () { + cb(obj); + }); + }); + }; + return Msg; }); diff --git a/www/common/common-messenger.js b/www/common/common-messenger.js index be4fa9aa2..c02d9d4eb 100644 --- a/www/common/common-messenger.js +++ b/www/common/common-messenger.js @@ -464,6 +464,7 @@ define([ if (!req.cb) { // This is the initial history for a pad chat + // XXX delete range request if (type === 'HISTORY_RANGE') { if (!getChannel(req.chanId)) { return; } if (!Array.isArray(parsed[2])) { return; } diff --git a/www/common/migrate-user-object.js b/www/common/migrate-user-object.js index fe42868d7..4684b200d 100644 --- a/www/common/migrate-user-object.js +++ b/www/common/migrate-user-object.js @@ -3,11 +3,11 @@ define([ '/common/common-feedback.js', '/common/common-hash.js', '/common/common-util.js', - '/common/common-messenger.js', + '/common/common-messaging.js', '/common/outer/mailbox.js', '/bower_components/nthen/index.js', '/bower_components/chainpad-crypto/crypto.js', -], function (AppConfig, Feedback, Hash, Util, Messenger, Mailbox, nThen, Crypto) { +], function (AppConfig, Feedback, Hash, Util, Messaging, Mailbox, nThen, Crypto) { // Start migration check // Versions: // 1: migrate pad attributes @@ -198,7 +198,7 @@ define([ var ctx = { store: store }; - var myData = Messenger.createData(userObject); + var myData = Messaging.createData(userObject); var close = function (chan) { var channel = channels[chan]; diff --git a/www/common/outer/async-store.js b/www/common/outer/async-store.js index d2db7413c..1b1824597 100644 --- a/www/common/outer/async-store.js +++ b/www/common/outer/async-store.js @@ -16,6 +16,7 @@ define([ '/common/outer/mailbox.js', '/common/outer/profile.js', '/common/outer/team.js', + '/common/outer/messenger.js', '/common/outer/network-config.js', '/customize/application_config.js', @@ -27,7 +28,8 @@ define([ '/bower_components/saferphore/index.js', ], function (Sortify, UserObject, ProxyManager, Migrate, Hash, Util, Constants, Feedback, Realtime, Messaging, Messenger, - SF, Cursor, OnlyOffice, Mailbox, Profile, Team, NetConfig, AppConfig, + SF, Cursor, OnlyOffice, Mailbox, Profile, Team, _Messenger, + NetConfig, AppConfig, Crypto, ChainPad, CpNetflux, Listmap, nThen, Saferphore) { var create = function () { @@ -2028,6 +2030,7 @@ define([ loadMessenger(); loadCursor(); loadOnlyOffice(); + loadUniversal(_Messenger, 'messenger', waitFor); loadUniversal(Profile, 'profile', waitFor); loadUniversal(Team, 'team', waitFor); cleanFriendRequests(); diff --git a/www/common/outer/mailbox-handlers.js b/www/common/outer/mailbox-handlers.js index aa65d6cfd..487eaecdc 100644 --- a/www/common/outer/mailbox-handlers.js +++ b/www/common/outer/mailbox-handlers.js @@ -154,7 +154,7 @@ define([ friend[key] = msg.content[key]; }); if (ctx.store.messenger) { - ctx.store.messenger.onFriendUpdate(curve, friend); + ctx.store.messenger.onFriendUpdate(curve); } ctx.updateMetadata(); cb(true); diff --git a/www/common/outer/messenger.js b/www/common/outer/messenger.js new file mode 100644 index 000000000..9f3e9aad0 --- /dev/null +++ b/www/common/outer/messenger.js @@ -0,0 +1,927 @@ +define([ + '/bower_components/chainpad-crypto/crypto.js', + '/common/common-hash.js', + '/common/common-util.js', + '/common/common-realtime.js', + '/common/common-messaging.js', + '/common/common-constants.js', + '/customize/messages.js', + '/customize/application_config.js', + + '/bower_components/nthen/index.js', +], function (Crypto, Hash, Util, Realtime, Messaging, Constants, Messages, AppConfig, nThen) { + 'use strict'; + var Curve = Crypto.Curve; + + var Msg = {}; + + var Types = { + message: 'MSG', + unfriend: 'UNFRIEND', + mapId: 'MAP_ID', + mapIdAck: 'MAP_ID_ACK' + }; + + var clone = function (o) { + return JSON.parse(JSON.stringify(o)); + }; + + var convertToUint8 = function (obj) { + var l = Object.keys(obj).length; + var u = new Uint8Array(l); + for (var i = 0; i we asked for an invalid last known hash + if (parsed.error && parsed.error === "EINVAL") { + setChannelHead(ctx, parsed.channel, '', function () { + getChannelMessagesSince(ctx, channel, {}, {}); + }); + return; + } + + // End of initial history + if (parsed.state && parsed.state === 1 && parsed.channel) { + return void onChannelReady(ctx, parsed.channel); + } + } + + // Initial history message + channel = ctx.channels[parsed[3]]; + if (!channel) { return; } + pushMsg(ctx, channel, parsed[4]); + }; + + var onMessage = function (ctx, msg, sender, chan) { + var channel = ctx.channels[chan.id]; + if (!channel) { return; } + pushMsg(ctx, channel, msg); + }; + + var removeFriend = function (ctx, curvePublic, _cb) { + var cb = Util.once(_cb); + if (typeof(cb) !== 'function') { return void console.error('NO_CALLBACK'); } + var proxy = ctx.store.proxy; + var data = getFriend(proxy, curvePublic); + + if (!data) { + // friend is not valid + console.error('friend is not valid'); + return void cb({error: 'INVALID_FRIEND'}); + } + + var channel = ctx.channels[data.channel]; + if (!channel) { + return void cb({error: "NO_SUCH_CHANNEL"}); + } + + // Unfriend with mailbox + if (ctx.store.mailbox && data.curvePublic && data.notifications) { + Messaging.removeFriend(ctx.store, curvePublic, function (obj) { + if (obj && obj.error) { return void cb({error:obj.error}); } + cb(obj); + }); + return; + } + + // Unfriend with channel + try { + var msg = [Types.unfriend, proxy.curvePublic, +new Date()]; + var msgStr = JSON.stringify(msg); + var cryptMsg = channel.encrypt(msgStr); + channel.wc.bcast(cryptMsg).then(function () {}, function (err) { + if (err) { return void cb({error:err}); } + // XXX call onFriendRemoved here (leave the channel and emit notification to inner) + removeFromFriendList(ctx, curvePublic, function () { + cb(); + }); + }); + } catch (e) { + cb({error: e}); + } + }; + + var openChannel = function (ctx, data) { + var proxy = ctx.store.proxy; + var network = ctx.store.network; + var hk = network.historyKeeper; + + var keys = data.keys; + var encryptor = data.encryptor || Curve.createEncryptor(keys); // XXX encryption + var channel = { + id: data.channel, + isFriendChat: data.isFriendChat, + isPadChat: data.isPadChat, + padChan: data.padChan, // Channel ID of the pad linked to this pad chat + readOnly: data.readOnly, + ready: false, + onReady: Util.mkEvent(true), + sending: false, + messages: [], + clients: data.clients || [], + mapId: {}, + }; + + if (data.onReady) { channel.onReady.reg(data.onReady); } + + channel.encrypt = function (msg) { + if (channel.readOnly) { return; } + return encryptor.encrypt(msg); + }; + channel.decrypt = data.decrypt || function (msg) { + return encryptor.decrypt(msg); + }; + + var onJoining = function (peer) { + if (peer === hk) { return; } + if (channel.readOnly) { return; } + + // Join event will be sent once we are able to ID this peer + var myData = createData(proxy); + delete myData.channel; + var msg = [Types.mapId, myData, channel.wc.myID]; + var msgStr = JSON.stringify(msg); + var cryptMsg = channel.encrypt(msgStr); + var data = { + channel: channel.id, + msg: cryptMsg + }; + network.sendto(peer, JSON.stringify(data)); + }; + + var onLeaving = function (peer) { + if (peer === hk) { return; } + + // update status + var otherData = channel.mapId[peer]; + if (!otherData) { return; } + + // Make sure the leaving user is not connected with another netflux id + if (channel.wc.members.some(function (nId) { + return channel.mapId[nId] + && channel.mapId[nId].curvePublic === otherData.curvePublic; + })) { return; } + + // Send the notification + ctx.emit('LEAVE', { + info: otherData, + id: channel.id + }, channel.clients); + }; + + var onOpen = function (chan) { + channel.wc = chan; + ctx.channels[data.channel] = channel; + + chan.on('message', function (msg, sender) { + onMessage(ctx, msg, sender, chan); + }); + + chan.on('join', onJoining); + chan.on('leave', onLeaving); + + getChannelMessagesSince(ctx, channel, data, keys); + }; + network.join(data.channel).then(onOpen, function (err) { + console.error(err); + }); + network.on('reconnect', function () { + if (channel && channel.stopped) { return; } + if (!ctx.channels[data.channel]) { return; } + + network.join(data.channel).then(onOpen, function (err) { + console.error(err); + }); + }); + }; + + var sendMessage = function (ctx, id, payload, cb) { + var channel = ctx.channels[id]; + if (!channel) { return void cb({error: 'NO_CHANNEL'}); } + if (channel.readOnly) { return void cb({error: 'FORBIDDEN'}); } + + var network = ctx.store.network; + if (!network.webChannels.some(function (wc) { + if (wc.id === channel.wc.id) { return true; } + })) { + return void cb({error: 'NO_SUCH_CHANNEL'}); + } + + var proxy = ctx.store.proxy; + var msg = [Types.message, proxy.curvePublic, +new Date(), payload]; + if (!channel.isFriendChat) { + var name = proxy[Constants.displayNameKey] || + Messages.anonymous + '#' + proxy.uid.slice(0,5); + msg.push(name); + } + // XXX encryption + var msgStr = JSON.stringify(msg); + var cryptMsg = channel.encrypt(msgStr); + + channel.wc.bcast(cryptMsg).then(function () { + pushMsg(ctx, channel, cryptMsg); + cb(); + }, function (err) { + cb({error: err}); + }); + }; + + // Display green status if one member is not me + var getStatus = function (ctx, chanId, cb) { + var channel = ctx.channels[chanId]; + if (!channel) { return void cb('NO_SUCH_CHANNEL'); } + var proxy = ctx.store.proxy; + var online = channel.wc.members.some(function (nId) { + if (nId === ctx.store.network.historyKeeper) { return; } + var data = channel.mapId[nId] || undefined; + if (!data) { return false; } + return data.curvePublic !== proxy.curvePublic; + }); + cb(online); + }; + + var getMyInfo = function (ctx, cb) { + var proxy = ctx.store.proxy; + cb({ + curvePublic: proxy.curvePublic, + displayName: proxy[Constants.displayNameKey] + }); + }; + + var loadFriend = function (ctx, clientId, friend, _cb) { + var cb = Util.once(Util.mkAsync(_cb)); + + var chanId = friend.channel; + var channel = ctx.channels[chanId]; + if (channel) { + // Fired instantly if already ready + return void channel.onReady.reg(function () { + if (channel.clients.indexOf(clientId) === -1) { + channel.clients.push(clientId); + } + cb(); + }); + } + + // XXX encryption + var proxy = ctx.store.proxy; + var keys = Curve.deriveKeys(friend.curvePublic, proxy.curvePrivate); + var data = { + keys: keys, + channel: friend.channel, + lastKnownHash: friend.lastKnownHash, + owners: [proxy.edPublic, friend.edPublic], + isFriendChat: true, + clients: clientId ? [clientId] : ctx.friendsClients, + onReady: cb + }; + openChannel(data); + }; + + var initFriends = function (ctx, clientId, cb) { + var friends = getFriendList(ctx.store.proxy); + + nThen(function (waitFor) { + // Load or get all friends channels + Object.keys(friends).forEach(function (key) { + if (key === 'me') { return; } + var friend = clone(friends[key]); + if (typeof(friend) !== 'object') { return; } + if (!friend.channel) { return; } + loadFriend(ctx, clientId, friend, waitFor()); + }); + }).nThen(function () { + if (ctx.friendsClients.indexOf(clientId) === -1) { + ctx.friendsClients.push(clientId); + } + cb(); + }); + }; + + var getRooms = function (ctx, data, cb) { + var proxy = ctx.store.proxy; + + // Get a single friend's room (on friend added) + if (data && data.curvePublic) { + var curvePublic = data.curvePublic; + // We need to get data about a new friend's room + var friend = getFriend(proxy, curvePublic); + if (!friend) { return void cb({error: 'NO_SUCH_FRIEND'}); } + var channel = ctx.channels[friend.channel]; + if (!channel) { return void cb({error: 'NO_SUCH_CHANNEL'}); } + return void cb([{ + id: channel.id, + isFriendChat: true, + name: friend.displayName, + lastKnownHash: friend.lastKnownHash, + curvePublic: friend.curvePublic, + messages: channel.messages + }]); + } + + // Pad chat room + if (data && data.padChat) { + var pCChannel = ctx.channels[data.padChat]; + if (!pCChannel) { return void cb({error: 'NO_SUCH_CHANNEL'}); } + return void cb([{ + id: pCChannel.id, + isPadChat: true, + messages: pCChannel.messages + }]); + } + + // Existing friends... + var rooms = Object.keys(ctx.channels).map(function (id) { + var r = ctx.channels[id]; + var name, lastKnownHash, curvePublic; + if (r.isFriendChat) { + var friend = getFriendFromChannel(ctx, id); + if (!friend) { return null; } + name = friend.displayName; + lastKnownHash = friend.lastKnownHash; + curvePublic = friend.curvePublic; + } else if (r.isPadChat) { + return; + } else { + // TODO room get metadata (name) && lastKnownHash + } + return { + id: r.id, + isFriendChat: r.isFriendChat, + name: name, + lastKnownHash: lastKnownHash, + curvePublic: curvePublic, + messages: r.messages + }; + }).filter(function (x) { return x; }); + cb(rooms); + }; + + // Gte the static userlist of a room (eveyrone who has accessed to the room) + var getUserList = function (ctx, data, cb) { + var room = ctx.channels[data.id]; + if (!room) { return void cb({error: 'NO_SUCH_CHANNEL'}); } + if (room.isFriendChat) { + var friend = getFriendFromChannel(ctx, data.id); + if (!friend) { return void cb({error: 'NO_SUCH_FRIEND'}); } + cb([friend]); + } else { + // TODO room userlist in rooms... + // (this is the static userlist, not the netflux one) + cb([]); + } + }; + + var openPadChat = function (ctx, clientId, data, _cb) { + var chanId = data.channel; + + var cb = Util.once(Util.mkAsync(function () { + ctx.emit('PADCHAT_READY', chanId, [clientId]); + _cb(); + })); + + var channel = ctx.channels[chanId]; + if (channel) { + return void channel.onReady.reg(function () { + if (channel.clients.indexOf(clientId) === -1) { + channel.clients.push(clientId); + } + cb(); + }); + } + + var secret = data.secret; + if (secret.keys.cryptKey) { + secret.keys.cryptKey = convertToUint8(secret.keys.cryptKey); + } + var encryptor = Crypto.createEncryptor(secret.keys); + var vKey = (secret.keys && secret.keys.validateKey) || ctx.validateKeys[secret.channel]; + var chanData = { + padChan: data.secret && data.secret.channel, + readOnly: typeof(secret.keys) === "object" && !secret.keys.validateKey, + encryptor: encryptor, + channel: data.channel, + isPadChat: true, + decrypt: function (msg) { + return encryptor.decrypt(msg, vKey); + }, + clients: [clientId], + onReady: cb + }; + openChannel(chanData); + }; + + var clearOwnedChannel = function (ctx, id, cb) { + var channel = ctx.clients[id]; + if (!channel) { return void cb({error: 'NO_CHANNEL'}); } + if (!ctx.store.rpc) { return void cb({error: 'RPC_NOT_READY'}); } + ctx.store.rpc.clearOwnedChannel(id, function (err) { + cb({error:err}); + if (!err) { + channel.messages = []; + ctx.emit('CLEAR_CHANNEL', id, channel.clients); + } + }); + }; + + // Remove a client from all the team they're subscribed to + var removeClient = function (ctx, cId) { + var friendsIdx = ctx.friendsClients.indexOf(cId); + if (friendsIdx !== -1) { + ctx.friendsClients.splice(friendsIdx, 1); + } + Object.keys(ctx.channels).forEach(function (id) { + var channel = ctx.channels[id]; + var clients = channel.clients; + var idx = clients.indexOf(cId); + if (idx !== -1) { clients.splice(idx, 1); } + + if (clients.length === 0) { + if (channel.wc) { channel.wc.leave(); } + channel.stopped = true; + delete ctx.channels[id]; + return true; + } + + }); + }; + + var getAllClients = function (ctx) { + var all = []; + Array.prototype.push.apply(all, ctx.friendsClients); + Object.keys(ctx.channels).forEach(function (id) { + Array.prototype.push.apply(all, ctx.channels[id].clients); + }); + return Util.deduplicateString(all); + }; + + Msg.init = function (cfg, waitFor, emit) { + var messenger = {}; + var store = cfg.store; + if (AppConfig.availablePadTypes.indexOf('contacts') === -1) { return; } + if (!store.loggedIn || !store.proxy.edPublic) { return; } + var ctx = { + store: store, + updateMetadata: cfg.updateMetadata, + pinPads: cfg.pinPads, + emit: emit, + friendsClients: [], + channels: {}, + validateKeys: {} // XXX encryption + }; + + + ctx.store.network.on('message', function(msg, sender) { + onDirectMessage(ctx, msg, sender); + }); + ctx.store.network.on('disconnect', function () { + ctx.emit('DISCONNECT', null, getAllClients()); + }); + ctx.store.network.on('reconnect', function () { + ctx.emit('RECONNECT', null, getAllClients()); + }); + + messenger.onFriendUpdate = function (curve) { + var friend = getFriend(store.proxy, curve); + if (!friend || !friend.channel) { return; } + var chan = ctx.channels[friend.channel]; + if (chan) { + ctx.emit('UPDATE_DATA', { + info: clone(friend), + channel: friend.channel + }, chan.clients); + } + }; + // Friend added in our contacts in the current worker + messenger.onFriendAdded = function (friendData) { + if (!ctx.friendsClients.length) { return; } + + var friend = getFriend(ctx.store.proxy, friendData.curvePublic); + if (typeof(friend) !== 'object') { return; } + var channel = friend.channel; + if (!channel) { return; } + + loadFriend(ctx, null, friend, function () { + emit('FRIEND', { + curvePublic: friend.curvePublic, + }, ctx.friendsClients); + }); + }; + messenger.onFriendRemoved = function (curvePublic, chanId) { + var channel = ctx.channels[chanId]; + if (!channel) { return; } + if (channel.wc) { + channel.wc.leave(Types.unfriend); + } + delete ctx.channels[channel.id]; + emit('UNFRIEND', { + curvePublic: curvePublic, + fromMe: true + }, ctx.friendsClients); + }; + + messenger.storeValidateKey = function (chan, key) { + ctx.validateKeys[chan] = key; + }; + messenger.leavePad = function (padChan) { + // Leave chat and prevent reconnect when we leave a pad + delete ctx.validateKeys[padChan]; + Object.keys(ctx.channels).some(function (chatChan) { + var channel = ctx.channels[chatChan]; + if (channel.padChan !== padChan) { return; } + if (channel.wc) { channel.wc.leave(); } + channel.stopped = true; + delete ctx.channels[chatChan]; + return true; + }); + }; + + messenger.removeClient = function (clientId) { + removeClient(ctx, clientId); + }; + messenger.execCommand = function (clientId, obj, cb) { + console.log(obj); + var cmd = obj.cmd; + var data = obj.data; + if (cmd === 'INIT_FRIENDS') { + return void initFriends(ctx, clientId, cb); + } + if (cmd === 'GET_ROOMS') { + return void getRooms(ctx, data, cb); + } + if (cmd === 'GET_USERLIST') { + return void getUserList(ctx, data, cb); + } + if (cmd === 'OPEN_PAD_CHAT') { + return void openPadChat(ctx, clientId, data, cb); + } + if (cmd === 'GET_MY_INFO') { + return void getMyInfo(ctx, cb); + } + if (cmd === 'REMOVE_FRIEND') { + return void removeFriend(ctx, data, cb); + } + if (cmd === 'GET_STATUS') { + return void getStatus(ctx, data, cb); + } + if (cmd === 'GET_MORE_HISTORY') { + return void getMoreHistory(ctx, data.id, data.sig, data.count, cb); + } + if (cmd === 'SEND_MESSAGE') { + return void sendMessage(ctx, data.id, data.content, cb); + } + if (cmd === 'SET_CHANNEL_HEAD') { + return void setChannelHead(ctx, data.id, data.sig, cb); + } + if (cmd === 'CLEAR_OWNED_CHANNEL') { + return void clearOwnedChannel(ctx, data, cb); + } + }; + + return messenger; + }; + + return Msg; +});