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,78 +1929,90 @@
} }
} }
if ( if (initialMessage.group) {
initialMessage.group && if (
initialMessage.group.members && initialMessage.group.type === GROUP_TYPES.REQUEST_INFO &&
initialMessage.group.type === GROUP_TYPES.UPDATE !newGroup
) { ) {
if (newGroup) { conversation.sendGroupInfo([source]);
conversation.updateGroupAdmins(initialMessage.group.admins); return null;
} else if (
conversation.setFriendRequestStatus( initialMessage.group.members &&
window.friends.friendRequestStatusEnum.friends initialMessage.group.type === GROUP_TYPES.UPDATE
); ) {
} if (newGroup) {
conversation.updateGroupAdmins(initialMessage.group.admins);
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) { conversation.setFriendRequestStatus(
window.log.warn( window.friends.friendRequestStatusEnum.friends
'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 = const membersMissing =
_.difference( _.difference(
conversation.get('members'), conversation.get('members'),
initialMessage.group.members initialMessage.group.members
).length > 0; ).length > 0;
if (membersMissing) { if (membersMissing) {
window.log.warn('Non-admin attempts to remove group members'); window.log.warn('Non-admin attempts to remove group members');
} }
const messageAllowed = !nameChanged && !membersMissing; const messageAllowed = !nameChanged && !membersMissing;
if (!messageAllowed) { if (!messageAllowed) {
confirm(); confirm();
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( textsecure.storage.protocol.sessions,
textsecure.storage.protocol.sessions, s => s.number === memberPubKey
s => s.number === memberPubKey );
);
const ourPubKey = textsecure.storage.user.getNumber(); const ourPubKey = textsecure.storage.user.getNumber();
if (!haveSession && memberPubKey !== ourPubKey) { if (!haveSession && memberPubKey !== ourPubKey) {
ConversationController.getOrCreateAndWait( ConversationController.getOrCreateAndWait(
memberPubKey,
'private'
).then(() => {
textsecure.messaging.sendMessageToNumber(
memberPubKey, memberPubKey,
'(If you see this message, you must be using an out-of-date client)', 'private'
[], ).then(() => {
undefined, textsecure.messaging.sendMessageToNumber(
[], memberPubKey,
Date.now(), '(If you see this message, you must be using an out-of-date client)',
undefined, [],
undefined, undefined,
{ messageType: 'friend-request', sessionRequest: true } [],
); Date.now(),
}); undefined,
} undefined,
}); { messageType: 'friend-request', sessionRequest: true }
);
});
}
});
} 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,40 +664,67 @@ 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();
} }
// 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);
},
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'); const primaryDeviceKey = window.storage.get('primaryDevicePubKey');
const allOurDevices = (await libloki.storage.getAllDevicePubKeysForPrimaryPubKey( if (!primaryDeviceKey) {
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
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
const contentMessage = new textsecure.protobuf.Content(); .map(c => libloki.api.createGroupSyncProtoMessage([c]))
contentMessage.syncMessage = syncMessage; .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
);
});
const silent = true; return Promise.all(syncPromises);
return this.sendIndividualProto(
primaryDeviceKey,
contentMessage,
Date.now(),
silent,
{} // options
);
}, },
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