From c6be28909296147091d30057ad537f63f17acad4 Mon Sep 17 00:00:00 2001 From: Mikunj Date: Fri, 7 Feb 2020 15:53:40 +1100 Subject: [PATCH 01/10] Fix leaving closed groups --- js/models/conversations.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/js/models/conversations.js b/js/models/conversations.js index 7471c5798..bdadd6a9b 100644 --- a/js/models/conversations.js +++ b/js/models/conversations.js @@ -221,9 +221,7 @@ return !!(this.id && this.id.match(/^publicChat:/)); }, isClosedGroup() { - return ( - this.get('type') === Message.GROUP && !this.isPublic() && !this.isRss() - ); + return this.get('type') === Message.GROUP && !this.isPublic() && !this.isRss(); }, isClosable() { return !this.isRss() || this.get('closable'); From d20d31b57426d4be8e75189bc62d4283fee914ab Mon Sep 17 00:00:00 2001 From: Mikunj Date: Fri, 7 Feb 2020 16:40:25 +1100 Subject: [PATCH 02/10] Linting --- js/models/conversations.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/js/models/conversations.js b/js/models/conversations.js index bdadd6a9b..7471c5798 100644 --- a/js/models/conversations.js +++ b/js/models/conversations.js @@ -221,7 +221,9 @@ return !!(this.id && this.id.match(/^publicChat:/)); }, isClosedGroup() { - return this.get('type') === Message.GROUP && !this.isPublic() && !this.isRss(); + return ( + this.get('type') === Message.GROUP && !this.isPublic() && !this.isRss() + ); }, isClosable() { return !this.isRss() || this.get('closable'); From fc6ca57e1e6b1669855eb45cd103b2df84d6d11c Mon Sep 17 00:00:00 2001 From: Mikunj Date: Mon, 10 Feb 2020 12:06:05 +1100 Subject: [PATCH 03/10] Added support for group request info --- js/background.js | 3 +- js/models/conversations.js | 17 ++++ js/models/messages.js | 128 ++++++++++++++++-------------- libtextsecure/message_receiver.js | 4 + libtextsecure/sendmessage.js | 11 ++- 5 files changed, 102 insertions(+), 61 deletions(-) diff --git a/js/background.js b/js/background.js index 859909b61..0a3b4f3be 100644 --- a/js/background.js +++ b/js/background.js @@ -787,6 +787,7 @@ 'group' ); + convo.updateGroupAdmins([primaryDeviceKey]); convo.updateGroup(ev.groupDetails); // Group conversations are automatically 'friends' @@ -795,8 +796,6 @@ window.friends.friendRequestStatusEnum.friends ); - convo.updateGroupAdmins([primaryDeviceKey]); - appView.openConversation(groupId, {}); }; diff --git a/js/models/conversations.js b/js/models/conversations.js index 7471c5798..b4f2a2485 100644 --- a/js/models/conversations.js +++ b/js/models/conversations.js @@ -2232,6 +2232,7 @@ this.get('name'), this.get('avatar'), this.get('members'), + this.get('groupAdmins'), groupUpdate.recipients, options ) @@ -2239,6 +2240,21 @@ ); }, + sendGroupInfo(recipients) { + if (this.isClosedGroup()) { + const options = this.getSendOptions(); + textsecure.messaging.updateGroup( + this.id, + this.get('name'), + this.get('avatar'), + this.get('members'), + this.get('groupAdmins'), + recipients, + options + ); + } + }, + async leaveGroup() { const now = Date.now(); if (this.get('type') === 'group') { @@ -2323,6 +2339,7 @@ const ourNumber = textsecure.storage.user.getNumber(); return !stillUnread.some( m => + m.propsForMessage && m.propsForMessage.text && m.propsForMessage.text.indexOf(`@${ourNumber}`) !== -1 ); diff --git a/js/models/messages.js b/js/models/messages.js index c79ca9c11..4aef00b64 100644 --- a/js/models/messages.js +++ b/js/models/messages.js @@ -1929,78 +1929,90 @@ } } - if ( - initialMessage.group && - initialMessage.group.members && - initialMessage.group.type === GROUP_TYPES.UPDATE - ) { - if (newGroup) { - conversation.updateGroupAdmins(initialMessage.group.admins); + if (initialMessage.group) { + if ( + initialMessage.group.type === GROUP_TYPES.REQUEST_INFO && + !newGroup + ) { + conversation.sendGroupInfo([source]); + return null; + } else if ( + initialMessage.group.members && + initialMessage.group.type === GROUP_TYPES.UPDATE + ) { + if (newGroup) { + conversation.updateGroupAdmins(initialMessage.group.admins); - conversation.setFriendRequestStatus( - window.friends.friendRequestStatusEnum.friends - ); - } + conversation.setFriendRequestStatus( + window.friends.friendRequestStatusEnum.friends + ); + } const fromAdmin = conversation .get('groupAdmins') .includes(primarySource); - if (!fromAdmin) { - // Make sure the message is not removing members / renaming the group - const nameChanged = - conversation.get('name') !== initialMessage.group.name; + if (!fromAdmin) { + // Make sure the message is not removing members / renaming the group + const nameChanged = + conversation.get('name') !== initialMessage.group.name; - if (nameChanged) { - window.log.warn( - 'Non-admin attempts to change the name of the group' - ); - } + if (nameChanged) { + window.log.warn( + 'Non-admin attempts to change the name of the group' + ); + } - const membersMissing = - _.difference( - conversation.get('members'), - initialMessage.group.members - ).length > 0; + const membersMissing = + _.difference( + conversation.get('members'), + initialMessage.group.members + ).length > 0; - if (membersMissing) { - window.log.warn('Non-admin attempts to remove group members'); - } + if (membersMissing) { + window.log.warn('Non-admin attempts to remove group members'); + } - const messageAllowed = !nameChanged && !membersMissing; + const messageAllowed = !nameChanged && !membersMissing; - if (!messageAllowed) { - confirm(); - return null; + if (!messageAllowed) { + confirm(); + return null; + } } - } - // For every member, see if we need to establish a session: - initialMessage.group.members.forEach(memberPubKey => { - const haveSession = _.some( - textsecure.storage.protocol.sessions, - s => s.number === memberPubKey - ); + // For every member, see if we need to establish a session: + initialMessage.group.members.forEach(memberPubKey => { + const haveSession = _.some( + textsecure.storage.protocol.sessions, + s => s.number === memberPubKey + ); - const ourPubKey = textsecure.storage.user.getNumber(); - if (!haveSession && memberPubKey !== ourPubKey) { - ConversationController.getOrCreateAndWait( - memberPubKey, - 'private' - ).then(() => { - textsecure.messaging.sendMessageToNumber( + const ourPubKey = textsecure.storage.user.getNumber(); + if (!haveSession && memberPubKey !== ourPubKey) { + ConversationController.getOrCreateAndWait( memberPubKey, - '(If you see this message, you must be using an out-of-date client)', - [], - undefined, - [], - Date.now(), - undefined, - undefined, - { messageType: 'friend-request', sessionRequest: true } - ); - }); - } - }); + 'private' + ).then(() => { + textsecure.messaging.sendMessageToNumber( + memberPubKey, + '(If you see this message, you must be using an out-of-date client)', + [], + undefined, + [], + Date.now(), + undefined, + undefined, + { messageType: 'friend-request', sessionRequest: true } + ); + }); + } + }); + } else if (newGroup) { + // We have an unknown group, we should request info + textsecure.messaging.requestGroupInfo(conversationId, [ + primarySource, + ]); + } } const isSessionRequest = diff --git a/libtextsecure/message_receiver.js b/libtextsecure/message_receiver.js index 757cfbcfa..893d549cc 100644 --- a/libtextsecure/message_receiver.js +++ b/libtextsecure/message_receiver.js @@ -1786,6 +1786,10 @@ MessageReceiver.prototype.extend({ decrypted.group.members = []; decrypted.group.avatar = null; break; + case textsecure.protobuf.GroupContext.Type.REQUEST_INFO: + decrypted.body = null; + decrypted.attachments = []; + break; default: this.removeFromCache(envelope); throw new Error('Unknown group message type'); diff --git a/libtextsecure/sendmessage.js b/libtextsecure/sendmessage.js index 57ebb6a6a..9386d2409 100644 --- a/libtextsecure/sendmessage.js +++ b/libtextsecure/sendmessage.js @@ -1107,7 +1107,7 @@ MessageSender.prototype = { return this.sendMessage(attrs, options); }, - updateGroup(groupId, name, avatar, members, recipients, options) { + updateGroup(groupId, name, avatar, members, admins, recipients, options) { const proto = new textsecure.protobuf.DataMessage(); proto.group = new textsecure.protobuf.GroupContext(); @@ -1164,6 +1164,14 @@ MessageSender.prototype = { }); }, + requestGroupInfo(groupId, groupNumbers, options) { + const proto = new textsecure.protobuf.DataMessage(); + proto.group = new textsecure.protobuf.GroupContext(); + proto.group.id = stringToArrayBuffer(groupId); + proto.group.type = textsecure.protobuf.GroupContext.Type.REQUEST_INFO; + return this.sendGroupProto(groupNumbers, proto, Date.now(), options); + }, + leaveGroup(groupId, groupNumbers, options) { const proto = new textsecure.protobuf.DataMessage(); proto.group = new textsecure.protobuf.GroupContext(); @@ -1263,6 +1271,7 @@ textsecure.MessageSender = function MessageSenderWrapper(username, password) { this.addNumberToGroup = sender.addNumberToGroup.bind(sender); this.setGroupName = sender.setGroupName.bind(sender); this.setGroupAvatar = sender.setGroupAvatar.bind(sender); + this.requestGroupInfo = sender.requestGroupInfo.bind(sender); this.leaveGroup = sender.leaveGroup.bind(sender); this.sendSyncMessage = sender.sendSyncMessage.bind(sender); this.getProfile = sender.getProfile.bind(sender); From 62825faa6119c1025c2a2b5a60e1bb05ac255cfd Mon Sep 17 00:00:00 2001 From: Mikunj Date: Mon, 10 Feb 2020 13:01:27 +1100 Subject: [PATCH 04/10] Don't perform admin check if it's a new group that we are creating --- js/models/messages.js | 56 +++++++++++++++++++++---------------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/js/models/messages.js b/js/models/messages.js index 4aef00b64..c9455c098 100644 --- a/js/models/messages.js +++ b/js/models/messages.js @@ -1946,38 +1946,38 @@ conversation.setFriendRequestStatus( window.friends.friendRequestStatusEnum.friends ); - } - - const fromAdmin = conversation - .get('groupAdmins') - .includes(primarySource); - - if (!fromAdmin) { - // Make sure the message is not removing members / renaming the group - const nameChanged = - conversation.get('name') !== initialMessage.group.name; - - if (nameChanged) { - window.log.warn( - 'Non-admin attempts to change the name of the group' - ); - } + } else { + const fromAdmin = conversation + .get('groupAdmins') + .includes(primarySource); + + if (!fromAdmin) { + // Make sure the message is not removing members / renaming the group + const nameChanged = + conversation.get('name') !== initialMessage.group.name; + + if (nameChanged) { + window.log.warn( + 'Non-admin attempts to change the name of the group' + ); + } - const membersMissing = - _.difference( - conversation.get('members'), - initialMessage.group.members - ).length > 0; + const membersMissing = + _.difference( + conversation.get('members'), + initialMessage.group.members + ).length > 0; - if (membersMissing) { - window.log.warn('Non-admin attempts to remove group members'); - } + if (membersMissing) { + window.log.warn('Non-admin attempts to remove group members'); + } - const messageAllowed = !nameChanged && !membersMissing; + const messageAllowed = !nameChanged && !membersMissing; - if (!messageAllowed) { - confirm(); - return null; + if (!messageAllowed) { + confirm(); + return null; + } } } // For every member, see if we need to establish a session: From b61dd6a83914b56f58b5ece90e7569f8ddadb228 Mon Sep 17 00:00:00 2001 From: Mikunj Date: Mon, 10 Feb 2020 15:20:34 +1100 Subject: [PATCH 05/10] Don't send groups in contact sync message --- js/models/messages.js | 2 +- libloki/api.js | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/js/models/messages.js b/js/models/messages.js index c9455c098..7b024886b 100644 --- a/js/models/messages.js +++ b/js/models/messages.js @@ -2008,7 +2008,7 @@ } }); } else if (newGroup) { - // We have an unknown group, we should request info + // We have an unknown group, we should request info from the sender textsecure.messaging.requestGroupInfo(conversationId, [ primarySource, ]); diff --git a/libloki/api.js b/libloki/api.js index 9b658a07f..431ff22ee 100644 --- a/libloki/api.js +++ b/libloki/api.js @@ -109,8 +109,9 @@ } async function createContactSyncProtoMessage(conversations) { // Extract required contacts information out of conversations + const sessionContacts = conversations.filter(c => c.isPrivate()); const rawContacts = await Promise.all( - conversations.map(async conversation => { + sessionContacts.map(async conversation => { const profile = conversation.getLokiProfile(); const number = conversation.getNumber(); const name = profile From abf298ba25c0c51cdbd3bbfbb57558e5bac4d223 Mon Sep 17 00:00:00 2001 From: Mikunj Date: Wed, 19 Feb 2020 10:32:30 +1100 Subject: [PATCH 06/10] Added sending of group sync message --- libloki/api.js | 29 +++++++++++++++++++++++++++++ libtextsecure/account_manager.js | 2 ++ libtextsecure/message_receiver.js | 3 +-- libtextsecure/sendmessage.js | 16 ++++++++++++++++ protos/SignalService.proto | 2 ++ 5 files changed, 50 insertions(+), 2 deletions(-) diff --git a/libloki/api.js b/libloki/api.js index 431ff22ee..efbd6891b 100644 --- a/libloki/api.js +++ b/libloki/api.js @@ -152,6 +152,34 @@ }); return syncMessage; } + async function createGroupSyncProtoMessage(conversations) { + // We only want to sync across closed groups that we haven't left + const sessionGroups = conversations.filter(c => c.isClosedGroup() && !c.get('left') && c.isFriend()); + const rawGroups = await Promise.all( + sessionGroups.map(async conversation => ({ + id: conversation.id, + name: conversation.get('name'), + members: conversation.get('members') || [], + blocked: conversation.isBlocked(), + expireTimer: conversation.get('expireTimer'), + admins: conversation.get('groupAdmins') || [], + })) + ); + // Convert raw groups to an array of buffers + const groupDetails = rawGroups + .map(x => new textsecure.protobuf.GroupDetails(x)) + .map(x => x.encode()); + // Serialise array of byteBuffers into 1 byteBuffer + const byteBuffer = serialiseByteBuffers(groupDetails); + const data = new Uint8Array(byteBuffer.toArrayBuffer()); + const groups = new textsecure.protobuf.SyncMessage.Groups({ + data, + }); + const syncMessage = new textsecure.protobuf.SyncMessage({ + groups, + }); + return syncMessage; + } async function sendPairingAuthorisation(authorisation, recipientPubKey) { const pairingAuthorisation = createPairingAuthorisationProtoMessage( authorisation @@ -222,5 +250,6 @@ createPairingAuthorisationProtoMessage, sendUnpairingMessageToSecondary, createContactSyncProtoMessage, + createGroupSyncProtoMessage, }; })(); diff --git a/libtextsecure/account_manager.js b/libtextsecure/account_manager.js index 1dfca9417..d43b39ca3 100644 --- a/libtextsecure/account_manager.js +++ b/libtextsecure/account_manager.js @@ -634,6 +634,8 @@ blockSync: true, } ); + // Send group sync message + await textsecure.messaging.sendGroupSyncMessage(window.getConversations()) }, validatePubKeyHex(pubKey) { const c = new Whisper.Conversation({ diff --git a/libtextsecure/message_receiver.js b/libtextsecure/message_receiver.js index 893d549cc..39cc611d6 100644 --- a/libtextsecure/message_receiver.js +++ b/libtextsecure/message_receiver.js @@ -1574,11 +1574,10 @@ MessageReceiver.prototype.extend({ }, handleGroups(envelope, groups) { window.log.info('group sync'); - const { blob } = groups; // Note: we do not return here because we don't want to block the next message on // this attachment download and a lot of processing of that attachment. - this.handleAttachment(blob).then(attachmentPointer => { + this.handleAttachment(groups).then(attachmentPointer => { const groupBuffer = new GroupBuffer(attachmentPointer.data); let groupDetails = groupBuffer.next(); const promises = []; diff --git a/libtextsecure/sendmessage.js b/libtextsecure/sendmessage.js index 9386d2409..40393a314 100644 --- a/libtextsecure/sendmessage.js +++ b/libtextsecure/sendmessage.js @@ -700,6 +700,22 @@ MessageSender.prototype = { ); }, + async sendGroupSyncMessage(conversations) { + const ourNumber = textsecure.storage.user.getNumber(); + const syncMessage = await libloki.api.createGroupSyncProtoMessage(conversations); + const contentMessage = new textsecure.protobuf.Content(); + contentMessage.syncMessage = syncMessage; + + const silent = true; + return this.sendIndividualProto( + ourNumber, + contentMessage, + Date.now(), + silent, + {} // options + ); + }, + sendRequestContactSyncMessage(options) { const myNumber = textsecure.storage.user.getNumber(); const myDevice = textsecure.storage.user.getDeviceId(); diff --git a/protos/SignalService.proto b/protos/SignalService.proto index 41f6ed5ab..f383448fe 100644 --- a/protos/SignalService.proto +++ b/protos/SignalService.proto @@ -282,6 +282,7 @@ message SyncMessage { message Groups { optional AttachmentPointer blob = 1; + optional bytes data = 101; } message Blocked { @@ -390,4 +391,5 @@ message GroupDetails { optional uint32 expireTimer = 6; optional string color = 7; optional bool blocked = 8; + repeated string admins = 9; } From f35493ce9fe562bd09737f17488254f8cdc5e265 Mon Sep 17 00:00:00 2001 From: Mikunj Date: Wed, 19 Feb 2020 10:39:30 +1100 Subject: [PATCH 07/10] Linting --- js/views/invite_friends_dialog_view.js | 5 +++- libloki/api.js | 18 +++++++------- libtextsecure/account_manager.js | 4 +++- libtextsecure/sendmessage.js | 4 +++- .../session/LeftPaneChannelSection.tsx | 24 ++++++++++++------- 5 files changed, 36 insertions(+), 19 deletions(-) diff --git a/js/views/invite_friends_dialog_view.js b/js/views/invite_friends_dialog_view.js index 1cc7c0ec0..ddf8d5745 100644 --- a/js/views/invite_friends_dialog_view.js +++ b/js/views/invite_friends_dialog_view.js @@ -74,7 +74,10 @@ newMembers.length + existingMembers.length > window.CONSTANTS.SMALL_GROUP_SIZE_LIMIT ) { - const msg = window.i18n('maxGroupMembersError', window.CONSTANTS.SMALL_GROUP_SIZE_LIMIT); + const msg = window.i18n( + 'maxGroupMembersError', + window.CONSTANTS.SMALL_GROUP_SIZE_LIMIT + ); window.pushToast({ title: msg, diff --git a/libloki/api.js b/libloki/api.js index efbd6891b..4df0ec2be 100644 --- a/libloki/api.js +++ b/libloki/api.js @@ -154,16 +154,18 @@ } async function createGroupSyncProtoMessage(conversations) { // We only want to sync across closed groups that we haven't left - const sessionGroups = conversations.filter(c => c.isClosedGroup() && !c.get('left') && c.isFriend()); + const sessionGroups = conversations.filter( + c => c.isClosedGroup() && !c.get('left') && c.isFriend() + ); const rawGroups = await Promise.all( sessionGroups.map(async conversation => ({ - id: conversation.id, - name: conversation.get('name'), - members: conversation.get('members') || [], - blocked: conversation.isBlocked(), - expireTimer: conversation.get('expireTimer'), - admins: conversation.get('groupAdmins') || [], - })) + id: conversation.id, + name: conversation.get('name'), + members: conversation.get('members') || [], + blocked: conversation.isBlocked(), + expireTimer: conversation.get('expireTimer'), + admins: conversation.get('groupAdmins') || [], + })) ); // Convert raw groups to an array of buffers const groupDetails = rawGroups diff --git a/libtextsecure/account_manager.js b/libtextsecure/account_manager.js index d43b39ca3..4775bdd5e 100644 --- a/libtextsecure/account_manager.js +++ b/libtextsecure/account_manager.js @@ -635,7 +635,9 @@ } ); // Send group sync message - await textsecure.messaging.sendGroupSyncMessage(window.getConversations()) + await textsecure.messaging.sendGroupSyncMessage( + window.getConversations() + ); }, validatePubKeyHex(pubKey) { const c = new Whisper.Conversation({ diff --git a/libtextsecure/sendmessage.js b/libtextsecure/sendmessage.js index 40393a314..4e557ba93 100644 --- a/libtextsecure/sendmessage.js +++ b/libtextsecure/sendmessage.js @@ -702,7 +702,9 @@ MessageSender.prototype = { async sendGroupSyncMessage(conversations) { const ourNumber = textsecure.storage.user.getNumber(); - const syncMessage = await libloki.api.createGroupSyncProtoMessage(conversations); + const syncMessage = await libloki.api.createGroupSyncProtoMessage( + conversations + ); const contentMessage = new textsecure.protobuf.Content(); contentMessage.syncMessage = syncMessage; diff --git a/ts/components/session/LeftPaneChannelSection.tsx b/ts/components/session/LeftPaneChannelSection.tsx index 569d9bec1..fd543ebef 100644 --- a/ts/components/session/LeftPaneChannelSection.tsx +++ b/ts/components/session/LeftPaneChannelSection.tsx @@ -399,13 +399,18 @@ export class LeftPaneChannelSection extends React.Component { groupMembers: Array ) { // Validate groupName and groupMembers length - if (groupName.length === 0 || - groupName.length > window.CONSTANTS.MAX_GROUP_NAME_LENGTH) { - window.pushToast({ - title: window.i18n('invalidGroupName', window.CONSTANTS.MAX_GROUP_NAME_LENGTH), - type: 'error', - id: 'invalidGroupName', - }); + if ( + groupName.length === 0 || + groupName.length > window.CONSTANTS.MAX_GROUP_NAME_LENGTH + ) { + window.pushToast({ + title: window.i18n( + 'invalidGroupName', + window.CONSTANTS.MAX_GROUP_NAME_LENGTH + ), + type: 'error', + id: 'invalidGroupName', + }); return; } @@ -416,7 +421,10 @@ export class LeftPaneChannelSection extends React.Component { groupMembers.length >= window.CONSTANTS.SMALL_GROUP_SIZE_LIMIT ) { window.pushToast({ - title: window.i18n('invalidGroupSize', window.CONSTANTS.SMALL_GROUP_SIZE_LIMIT), + title: window.i18n( + 'invalidGroupSize', + window.CONSTANTS.SMALL_GROUP_SIZE_LIMIT + ), type: 'error', id: 'invalidGroupSize', }); From 0eaebcbcac1eef86df5ab076ba66c1cb097669fa Mon Sep 17 00:00:00 2001 From: Mikunj Date: Wed, 19 Feb 2020 12:31:01 +1100 Subject: [PATCH 08/10] Don't send contact sync message with pairing authorisation. Don't send secondary devices in contact sync messages. --- js/background.js | 2 + js/models/conversations.js | 2 +- js/modules/loki_app_dot_net_api.js | 2 +- libloki/api.js | 44 ++++++++------ libtextsecure/account_manager.js | 8 +-- libtextsecure/sendmessage.js | 98 ++++++++++++++++-------------- 6 files changed, 86 insertions(+), 70 deletions(-) diff --git a/js/background.js b/js/background.js index 0a3b4f3be..5ce84a8fc 100644 --- a/js/background.js +++ b/js/background.js @@ -1371,6 +1371,8 @@ await window.lokiFileServerAPI.updateOurDeviceMapping(); // TODO: we should ensure the message was sent and retry automatically if not await libloki.api.sendUnpairingMessageToSecondary(pubKey); + // Remove all traces of the device + ConversationController.deleteContact(pubKey); Whisper.events.trigger('refreshLinkedDeviceList'); }); } diff --git a/js/models/conversations.js b/js/models/conversations.js index b4f2a2485..1889996a7 100644 --- a/js/models/conversations.js +++ b/js/models/conversations.js @@ -935,7 +935,7 @@ if (newStatus === FriendRequestStatusEnum.friends) { if (!blockSync) { // Sync contact - this.wrapSend(textsecure.messaging.sendContactSyncMessage(this)); + this.wrapSend(textsecure.messaging.sendContactSyncMessage([this])); } // Only enable sending profileKey after becoming friends this.set({ profileSharing: true }); diff --git a/js/modules/loki_app_dot_net_api.js b/js/modules/loki_app_dot_net_api.js index c9664abef..8158a5bc2 100644 --- a/js/modules/loki_app_dot_net_api.js +++ b/js/modules/loki_app_dot_net_api.js @@ -229,7 +229,7 @@ class LokiAppDotNetServerAPI { window.storage.get('primaryDevicePubKey') || textsecure.storage.user.getNumber(); const profileConvo = ConversationController.get(ourNumber); - const profile = profileConvo.getLokiProfile(); + const profile = profileConvo && profileConvo.getLokiProfile(); const profileName = profile && profile.displayName; // if doesn't match, write it to the network if (tokenRes.response.data.user.name !== profileName) { diff --git a/libloki/api.js b/libloki/api.js index 4df0ec2be..ef1baccf4 100644 --- a/libloki/api.js +++ b/libloki/api.js @@ -1,4 +1,4 @@ -/* global window, textsecure, Whisper, dcodeIO, StringView, ConversationController */ +/* global window, textsecure, dcodeIO, StringView, ConversationController */ // eslint-disable-next-line func-names (function() { @@ -109,7 +109,14 @@ } async function createContactSyncProtoMessage(conversations) { // Extract required contacts information out of conversations - const sessionContacts = conversations.filter(c => c.isPrivate()); + const sessionContacts = conversations.filter( + c => c.isPrivate() && !c.isSecondaryDevice() + ); + + if (sessionContacts.length === 0) { + return null; + } + const rawContacts = await Promise.all( sessionContacts.map(async conversation => { const profile = conversation.getLokiProfile(); @@ -152,21 +159,25 @@ }); return syncMessage; } - async function createGroupSyncProtoMessage(conversations) { + function createGroupSyncProtoMessage(conversations) { // We only want to sync across closed groups that we haven't left const sessionGroups = conversations.filter( c => c.isClosedGroup() && !c.get('left') && c.isFriend() ); - const rawGroups = await Promise.all( - sessionGroups.map(async conversation => ({ - id: conversation.id, - name: conversation.get('name'), - members: conversation.get('members') || [], - blocked: conversation.isBlocked(), - expireTimer: conversation.get('expireTimer'), - admins: conversation.get('groupAdmins') || [], - })) - ); + + if (sessionGroups.length === 0) { + return null; + } + + const rawGroups = sessionGroups.map(conversation => ({ + id: window.Signal.Crypto.bytesFromString(conversation.id), + name: conversation.get('name'), + members: conversation.get('members') || [], + blocked: conversation.isBlocked(), + expireTimer: conversation.get('expireTimer'), + admins: conversation.get('groupAdmins') || [], + })); + // Convert raw groups to an array of buffers const groupDetails = rawGroups .map(x => new textsecure.protobuf.GroupDetails(x)) @@ -210,13 +221,6 @@ profile, profileKey, }); - // Attach contact list - const conversations = await window.Signal.Data.getConversationsWithFriendStatus( - window.friends.friendRequestStatusEnum.friends, - { ConversationCollection: Whisper.ConversationCollection } - ); - const syncMessage = await createContactSyncProtoMessage(conversations); - content.syncMessage = syncMessage; content.dataMessage = dataMessage; } // Send diff --git a/libtextsecure/account_manager.js b/libtextsecure/account_manager.js index 4775bdd5e..088422630 100644 --- a/libtextsecure/account_manager.js +++ b/libtextsecure/account_manager.js @@ -634,10 +634,10 @@ blockSync: true, } ); - // Send group sync message - await textsecure.messaging.sendGroupSyncMessage( - window.getConversations() - ); + // Send sync messages + const conversations = window.getConversations().models; + textsecure.messaging.sendContactSyncMessage(conversations); + textsecure.messaging.sendGroupSyncMessage(conversations); }, validatePubKeyHex(pubKey) { const c = new Whisper.Conversation({ diff --git a/libtextsecure/sendmessage.js b/libtextsecure/sendmessage.js index 4e557ba93..3d39d472d 100644 --- a/libtextsecure/sendmessage.js +++ b/libtextsecure/sendmessage.js @@ -664,58 +664,67 @@ MessageSender.prototype = { return Promise.resolve(); }, - async sendContactSyncMessage(contactConversation) { - if (!contactConversation.isPrivate()) { - return Promise.resolve(); - } - + async sendContactSyncMessage(conversations) { + // If we havn't got a primaryDeviceKey then we are in the middle of pairing + // primaryDevicePubKey is set to our own number if we are the master device const primaryDeviceKey = window.storage.get('primaryDevicePubKey'); - const allOurDevices = (await libloki.storage.getAllDevicePubKeysForPrimaryPubKey( - primaryDeviceKey - )) - // Don't send to ourselves - .filter(pubKey => pubKey !== textsecure.storage.user.getNumber()); - if ( - allOurDevices.includes(contactConversation.id) || - !primaryDeviceKey || - allOurDevices.length === 0 - ) { - // If we havn't got a primaryDeviceKey then we are in the middle of pairing + if (!primaryDeviceKey) { return Promise.resolve(); } - const syncMessage = await libloki.api.createContactSyncProtoMessage([ - contactConversation, - ]); - const contentMessage = new textsecure.protobuf.Content(); - contentMessage.syncMessage = syncMessage; - - const silent = true; - return this.sendIndividualProto( - primaryDeviceKey, - contentMessage, - Date.now(), - silent, - {} // options + // We need to sync across 3 contacts at a time + // This is to avoid hitting storage server limit + const chunked = _.chunk(conversations, 3); + const syncMessages = await Promise.all( + chunked.map(c => libloki.api.createContactSyncProtoMessage(c)) ); + const syncPromises = syncMessages + .filter(message => message != null) + .map(syncMessage => { + const contentMessage = new textsecure.protobuf.Content(); + contentMessage.syncMessage = syncMessage; + + const silent = true; + return this.sendIndividualProto( + primaryDeviceKey, + contentMessage, + Date.now(), + silent, + {} // options + ); + }); + + return Promise.all(syncPromises); }, - async sendGroupSyncMessage(conversations) { - const ourNumber = textsecure.storage.user.getNumber(); - const syncMessage = await libloki.api.createGroupSyncProtoMessage( - conversations - ); - const contentMessage = new textsecure.protobuf.Content(); - contentMessage.syncMessage = syncMessage; + sendGroupSyncMessage(conversations) { + // If we havn't got a primaryDeviceKey then we are in the middle of pairing + // primaryDevicePubKey is set to our own number if we are the master device + const primaryDeviceKey = window.storage.get('primaryDevicePubKey'); + if (!primaryDeviceKey) { + return Promise.resolve(); + } - const silent = true; - return this.sendIndividualProto( - ourNumber, - contentMessage, - Date.now(), - silent, - {} // options - ); + // We need to sync across 1 group at a time + // This is because we could hit the storage server limit with one group + const syncPromises = conversations + .map(c => libloki.api.createGroupSyncProtoMessage([c])) + .filter(message => message != null) + .map(syncMessage => { + const contentMessage = new textsecure.protobuf.Content(); + contentMessage.syncMessage = syncMessage; + + const silent = true; + return this.sendIndividualProto( + primaryDeviceKey, + contentMessage, + Date.now(), + silent, + {} // options + ); + }); + + return Promise.all(syncPromises); }, sendRequestContactSyncMessage(options) { @@ -1277,6 +1286,7 @@ textsecure.MessageSender = function MessageSenderWrapper(username, password) { sender ); this.sendContactSyncMessage = sender.sendContactSyncMessage.bind(sender); + this.sendGroupSyncMessage = sender.sendGroupSyncMessage.bind(sender); this.sendRequestConfigurationSyncMessage = sender.sendRequestConfigurationSyncMessage.bind( sender ); From a03185248c660cee536a45ebc8d070064c83a7c0 Mon Sep 17 00:00:00 2001 From: Mikunj Date: Wed, 19 Feb 2020 13:28:26 +1100 Subject: [PATCH 09/10] Fix check for valid sender when handling sync message --- libloki/storage.js | 2 +- libtextsecure/message_receiver.js | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/libloki/storage.js b/libloki/storage.js index 3a2d084e2..a5b5c27de 100644 --- a/libloki/storage.js +++ b/libloki/storage.js @@ -131,7 +131,7 @@ if (deviceMapping.isPrimary === '0') { const { primaryDevicePubKey } = authorisations.find( - authorisation => authorisation.secondaryDevicePubKey === pubKey + authorisation => authorisation && authorisation.secondaryDevicePubKey === pubKey ) || {}; if (primaryDevicePubKey) { // do NOT call getprimaryDeviceMapping recursively diff --git a/libtextsecure/message_receiver.js b/libtextsecure/message_receiver.js index 39cc611d6..a33eca0eb 100644 --- a/libtextsecure/message_receiver.js +++ b/libtextsecure/message_receiver.js @@ -1469,18 +1469,20 @@ MessageReceiver.prototype.extend({ this.removeFromCache(envelope); }, async handleSyncMessage(envelope, syncMessage) { + // We should only accept sync messages from our devices const ourNumber = textsecure.storage.user.getNumber(); - // NOTE: Maybe we should be caching this list? - const ourDevices = await libloki.storage.getAllDevicePubKeysForPrimaryPubKey( + const ourPrimaryNumber = window.storage.get('primaryDevicePubKey'); + const ourOtherDevices = await libloki.storage.getAllDevicePubKeysForPrimaryPubKey( window.storage.get('primaryDevicePubKey') ); - const validSyncSender = - ourDevices && ourDevices.some(devicePubKey => devicePubKey === ourNumber); + const ourDevices = new Set([ourNumber, ourPrimaryNumber, ...ourOtherDevices]); + const validSyncSender = ourDevices.has(envelope.source); if (!validSyncSender) { throw new Error( "Received sync message from a device we aren't paired with" ); } + if (syncMessage.sent) { const sentMessage = syncMessage.sent; const to = sentMessage.message.group From d00abed7da61e42ca3a1dd146d77d075012411ea Mon Sep 17 00:00:00 2001 From: Mikunj Date: Wed, 19 Feb 2020 13:30:10 +1100 Subject: [PATCH 10/10] Linting --- libloki/storage.js | 3 ++- libtextsecure/message_receiver.js | 6 +++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/libloki/storage.js b/libloki/storage.js index a5b5c27de..0e4cd3dda 100644 --- a/libloki/storage.js +++ b/libloki/storage.js @@ -131,7 +131,8 @@ if (deviceMapping.isPrimary === '0') { const { primaryDevicePubKey } = authorisations.find( - authorisation => authorisation && authorisation.secondaryDevicePubKey === pubKey + authorisation => + authorisation && authorisation.secondaryDevicePubKey === pubKey ) || {}; if (primaryDevicePubKey) { // do NOT call getprimaryDeviceMapping recursively diff --git a/libtextsecure/message_receiver.js b/libtextsecure/message_receiver.js index a33eca0eb..81adb351f 100644 --- a/libtextsecure/message_receiver.js +++ b/libtextsecure/message_receiver.js @@ -1475,7 +1475,11 @@ MessageReceiver.prototype.extend({ const ourOtherDevices = await libloki.storage.getAllDevicePubKeysForPrimaryPubKey( window.storage.get('primaryDevicePubKey') ); - const ourDevices = new Set([ourNumber, ourPrimaryNumber, ...ourOtherDevices]); + const ourDevices = new Set([ + ourNumber, + ourPrimaryNumber, + ...ourOtherDevices, + ]); const validSyncSender = ourDevices.has(envelope.source); if (!validSyncSender) { throw new Error(