Merge pull request #877 from loki-project/group-sync

Closed group syncing
pull/907/head
Mikunj Varsani 5 years ago committed by GitHub
commit adbc791dcb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -787,6 +787,7 @@
'group' 'group'
); );
convo.updateGroupAdmins([primaryDeviceKey]);
convo.updateGroup(ev.groupDetails); convo.updateGroup(ev.groupDetails);
// Group conversations are automatically 'friends' // Group conversations are automatically 'friends'
@ -795,8 +796,6 @@
window.friends.friendRequestStatusEnum.friends window.friends.friendRequestStatusEnum.friends
); );
convo.updateGroupAdmins([primaryDeviceKey]);
appView.openConversation(groupId, {}); appView.openConversation(groupId, {});
}; };
@ -1372,6 +1371,8 @@
await window.lokiFileServerAPI.updateOurDeviceMapping(); await window.lokiFileServerAPI.updateOurDeviceMapping();
// TODO: we should ensure the message was sent and retry automatically if not // TODO: we should ensure the message was sent and retry automatically if not
await libloki.api.sendUnpairingMessageToSecondary(pubKey); await libloki.api.sendUnpairingMessageToSecondary(pubKey);
// Remove all traces of the device
ConversationController.deleteContact(pubKey);
Whisper.events.trigger('refreshLinkedDeviceList'); Whisper.events.trigger('refreshLinkedDeviceList');
}); });
} }

@ -935,7 +935,7 @@
if (newStatus === FriendRequestStatusEnum.friends) { if (newStatus === FriendRequestStatusEnum.friends) {
if (!blockSync) { if (!blockSync) {
// Sync contact // Sync contact
this.wrapSend(textsecure.messaging.sendContactSyncMessage(this)); this.wrapSend(textsecure.messaging.sendContactSyncMessage([this]));
} }
// Only enable sending profileKey after becoming friends // Only enable sending profileKey after becoming friends
this.set({ profileSharing: true }); this.set({ profileSharing: true });
@ -2232,6 +2232,7 @@
this.get('name'), this.get('name'),
this.get('avatar'), this.get('avatar'),
this.get('members'), this.get('members'),
this.get('groupAdmins'),
groupUpdate.recipients, groupUpdate.recipients,
options 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() { async leaveGroup() {
const now = Date.now(); const now = Date.now();
if (this.get('type') === 'group') { if (this.get('type') === 'group') {
@ -2323,6 +2339,7 @@
const ourNumber = textsecure.storage.user.getNumber(); const ourNumber = textsecure.storage.user.getNumber();
return !stillUnread.some( return !stillUnread.some(
m => m =>
m.propsForMessage &&
m.propsForMessage.text && m.propsForMessage.text &&
m.propsForMessage.text.indexOf(`@${ourNumber}`) !== -1 m.propsForMessage.text.indexOf(`@${ourNumber}`) !== -1
); );

@ -1929,8 +1929,14 @@
} }
} }
if (initialMessage.group) {
if ( if (
initialMessage.group && initialMessage.group.type === GROUP_TYPES.REQUEST_INFO &&
!newGroup
) {
conversation.sendGroupInfo([source]);
return null;
} else if (
initialMessage.group.members && initialMessage.group.members &&
initialMessage.group.type === GROUP_TYPES.UPDATE initialMessage.group.type === GROUP_TYPES.UPDATE
) { ) {
@ -1940,8 +1946,7 @@
conversation.setFriendRequestStatus( conversation.setFriendRequestStatus(
window.friends.friendRequestStatusEnum.friends window.friends.friendRequestStatusEnum.friends
); );
} } else {
const fromAdmin = conversation const fromAdmin = conversation
.get('groupAdmins') .get('groupAdmins')
.includes(primarySource); .includes(primarySource);
@ -1974,6 +1979,7 @@
return null; return null;
} }
} }
}
// For every member, see if we need to establish a session: // For every member, see if we need to establish a session:
initialMessage.group.members.forEach(memberPubKey => { initialMessage.group.members.forEach(memberPubKey => {
const haveSession = _.some( const haveSession = _.some(
@ -2001,6 +2007,12 @@
}); });
} }
}); });
} else if (newGroup) {
// We have an unknown group, we should request info from the sender
textsecure.messaging.requestGroupInfo(conversationId, [
primarySource,
]);
}
} }
const isSessionRequest = const isSessionRequest =

@ -229,7 +229,7 @@ class LokiAppDotNetServerAPI {
window.storage.get('primaryDevicePubKey') || window.storage.get('primaryDevicePubKey') ||
textsecure.storage.user.getNumber(); textsecure.storage.user.getNumber();
const profileConvo = ConversationController.get(ourNumber); const profileConvo = ConversationController.get(ourNumber);
const profile = profileConvo.getLokiProfile(); const profile = profileConvo && profileConvo.getLokiProfile();
const profileName = profile && profile.displayName; const profileName = profile && profile.displayName;
// if doesn't match, write it to the network // if doesn't match, write it to the network
if (tokenRes.response.data.user.name !== profileName) { if (tokenRes.response.data.user.name !== profileName) {

@ -1,4 +1,4 @@
/* global window, textsecure, Whisper, dcodeIO, StringView, ConversationController */ /* global window, textsecure, dcodeIO, StringView, ConversationController */
// eslint-disable-next-line func-names // eslint-disable-next-line func-names
(function() { (function() {
@ -109,8 +109,16 @@
} }
async function createContactSyncProtoMessage(conversations) { async function createContactSyncProtoMessage(conversations) {
// Extract required contacts information out of conversations // Extract required contacts information out of conversations
const sessionContacts = conversations.filter(
c => c.isPrivate() && !c.isSecondaryDevice()
);
if (sessionContacts.length === 0) {
return null;
}
const rawContacts = await Promise.all( const rawContacts = await Promise.all(
conversations.map(async conversation => { sessionContacts.map(async conversation => {
const profile = conversation.getLokiProfile(); const profile = conversation.getLokiProfile();
const number = conversation.getNumber(); const number = conversation.getNumber();
const name = profile const name = profile
@ -151,6 +159,40 @@
}); });
return syncMessage; return syncMessage;
} }
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()
);
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))
.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) { async function sendPairingAuthorisation(authorisation, recipientPubKey) {
const pairingAuthorisation = createPairingAuthorisationProtoMessage( const pairingAuthorisation = createPairingAuthorisationProtoMessage(
authorisation authorisation
@ -179,13 +221,6 @@
profile, profile,
profileKey, 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; content.dataMessage = dataMessage;
} }
// Send // Send
@ -221,5 +256,6 @@
createPairingAuthorisationProtoMessage, createPairingAuthorisationProtoMessage,
sendUnpairingMessageToSecondary, sendUnpairingMessageToSecondary,
createContactSyncProtoMessage, createContactSyncProtoMessage,
createGroupSyncProtoMessage,
}; };
})(); })();

@ -131,7 +131,8 @@
if (deviceMapping.isPrimary === '0') { if (deviceMapping.isPrimary === '0') {
const { primaryDevicePubKey } = const { primaryDevicePubKey } =
authorisations.find( authorisations.find(
authorisation => authorisation.secondaryDevicePubKey === pubKey authorisation =>
authorisation && authorisation.secondaryDevicePubKey === pubKey
) || {}; ) || {};
if (primaryDevicePubKey) { if (primaryDevicePubKey) {
// do NOT call getprimaryDeviceMapping recursively // do NOT call getprimaryDeviceMapping recursively

@ -634,6 +634,10 @@
blockSync: true, blockSync: true,
} }
); );
// Send sync messages
const conversations = window.getConversations().models;
textsecure.messaging.sendContactSyncMessage(conversations);
textsecure.messaging.sendGroupSyncMessage(conversations);
}, },
validatePubKeyHex(pubKey) { validatePubKeyHex(pubKey) {
const c = new Whisper.Conversation({ const c = new Whisper.Conversation({

@ -1469,18 +1469,24 @@ MessageReceiver.prototype.extend({
this.removeFromCache(envelope); this.removeFromCache(envelope);
}, },
async handleSyncMessage(envelope, syncMessage) { async handleSyncMessage(envelope, syncMessage) {
// We should only accept sync messages from our devices
const ourNumber = textsecure.storage.user.getNumber(); const ourNumber = textsecure.storage.user.getNumber();
// NOTE: Maybe we should be caching this list? const ourPrimaryNumber = window.storage.get('primaryDevicePubKey');
const ourDevices = await libloki.storage.getAllDevicePubKeysForPrimaryPubKey( const ourOtherDevices = await libloki.storage.getAllDevicePubKeysForPrimaryPubKey(
window.storage.get('primaryDevicePubKey') window.storage.get('primaryDevicePubKey')
); );
const validSyncSender = const ourDevices = new Set([
ourDevices && ourDevices.some(devicePubKey => devicePubKey === ourNumber); ourNumber,
ourPrimaryNumber,
...ourOtherDevices,
]);
const validSyncSender = ourDevices.has(envelope.source);
if (!validSyncSender) { if (!validSyncSender) {
throw new Error( throw new Error(
"Received sync message from a device we aren't paired with" "Received sync message from a device we aren't paired with"
); );
} }
if (syncMessage.sent) { if (syncMessage.sent) {
const sentMessage = syncMessage.sent; const sentMessage = syncMessage.sent;
const to = sentMessage.message.group const to = sentMessage.message.group
@ -1574,11 +1580,10 @@ MessageReceiver.prototype.extend({
}, },
handleGroups(envelope, groups) { handleGroups(envelope, groups) {
window.log.info('group sync'); 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 // 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 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); const groupBuffer = new GroupBuffer(attachmentPointer.data);
let groupDetails = groupBuffer.next(); let groupDetails = groupBuffer.next();
const promises = []; const promises = [];
@ -1786,6 +1791,10 @@ MessageReceiver.prototype.extend({
decrypted.group.members = []; decrypted.group.members = [];
decrypted.group.avatar = null; decrypted.group.avatar = null;
break; break;
case textsecure.protobuf.GroupContext.Type.REQUEST_INFO:
decrypted.body = null;
decrypted.attachments = [];
break;
default: default:
this.removeFromCache(envelope); this.removeFromCache(envelope);
throw new Error('Unknown group message type'); throw new Error('Unknown group message type');

@ -664,29 +664,53 @@ MessageSender.prototype = {
return Promise.resolve(); return Promise.resolve();
}, },
async sendContactSyncMessage(contactConversation) { async sendContactSyncMessage(conversations) {
if (!contactConversation.isPrivate()) { // 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(); return Promise.resolve();
} }
const primaryDeviceKey = window.storage.get('primaryDevicePubKey'); // We need to sync across 3 contacts at a time
const allOurDevices = (await libloki.storage.getAllDevicePubKeysForPrimaryPubKey( // This is to avoid hitting storage server limit
primaryDeviceKey const chunked = _.chunk(conversations, 3);
)) const syncMessages = await Promise.all(
// Don't send to ourselves chunked.map(c => libloki.api.createContactSyncProtoMessage(c))
.filter(pubKey => pubKey !== textsecure.storage.user.getNumber()); );
if ( const syncPromises = syncMessages
allOurDevices.includes(contactConversation.id) || .filter(message => message != null)
!primaryDeviceKey || .map(syncMessage => {
allOurDevices.length === 0 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);
},
sendGroupSyncMessage(conversations) {
// If we havn't got a primaryDeviceKey then we are in the middle of pairing // 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(); return Promise.resolve();
} }
const syncMessage = await libloki.api.createContactSyncProtoMessage([ // We need to sync across 1 group at a time
contactConversation, // 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(); const contentMessage = new textsecure.protobuf.Content();
contentMessage.syncMessage = syncMessage; contentMessage.syncMessage = syncMessage;
@ -698,6 +722,9 @@ MessageSender.prototype = {
silent, silent,
{} // options {} // options
); );
});
return Promise.all(syncPromises);
}, },
sendRequestContactSyncMessage(options) { sendRequestContactSyncMessage(options) {
@ -1107,7 +1134,7 @@ MessageSender.prototype = {
return this.sendMessage(attrs, options); 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(); const proto = new textsecure.protobuf.DataMessage();
proto.group = new textsecure.protobuf.GroupContext(); proto.group = new textsecure.protobuf.GroupContext();
@ -1164,6 +1191,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) { leaveGroup(groupId, groupNumbers, options) {
const proto = new textsecure.protobuf.DataMessage(); const proto = new textsecure.protobuf.DataMessage();
proto.group = new textsecure.protobuf.GroupContext(); proto.group = new textsecure.protobuf.GroupContext();
@ -1251,6 +1286,7 @@ textsecure.MessageSender = function MessageSenderWrapper(username, password) {
sender sender
); );
this.sendContactSyncMessage = sender.sendContactSyncMessage.bind(sender); this.sendContactSyncMessage = sender.sendContactSyncMessage.bind(sender);
this.sendGroupSyncMessage = sender.sendGroupSyncMessage.bind(sender);
this.sendRequestConfigurationSyncMessage = sender.sendRequestConfigurationSyncMessage.bind( this.sendRequestConfigurationSyncMessage = sender.sendRequestConfigurationSyncMessage.bind(
sender sender
); );
@ -1263,6 +1299,7 @@ textsecure.MessageSender = function MessageSenderWrapper(username, password) {
this.addNumberToGroup = sender.addNumberToGroup.bind(sender); this.addNumberToGroup = sender.addNumberToGroup.bind(sender);
this.setGroupName = sender.setGroupName.bind(sender); this.setGroupName = sender.setGroupName.bind(sender);
this.setGroupAvatar = sender.setGroupAvatar.bind(sender); this.setGroupAvatar = sender.setGroupAvatar.bind(sender);
this.requestGroupInfo = sender.requestGroupInfo.bind(sender);
this.leaveGroup = sender.leaveGroup.bind(sender); this.leaveGroup = sender.leaveGroup.bind(sender);
this.sendSyncMessage = sender.sendSyncMessage.bind(sender); this.sendSyncMessage = sender.sendSyncMessage.bind(sender);
this.getProfile = sender.getProfile.bind(sender); this.getProfile = sender.getProfile.bind(sender);

@ -282,6 +282,7 @@ message SyncMessage {
message Groups { message Groups {
optional AttachmentPointer blob = 1; optional AttachmentPointer blob = 1;
optional bytes data = 101;
} }
message Blocked { message Blocked {
@ -390,4 +391,5 @@ message GroupDetails {
optional uint32 expireTimer = 6; optional uint32 expireTimer = 6;
optional string color = 7; optional string color = 7;
optional bool blocked = 8; optional bool blocked = 8;
repeated string admins = 9;
} }

Loading…
Cancel
Save