|  |  |  | @ -73,91 +73,56 @@ object ClosedGroupsProtocol { | 
		
	
		
			
				|  |  |  |  |         return groupID | 
		
	
		
			
				|  |  |  |  |     } | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |     public fun addMembers(context: Context, newMembers: Collection<String>, groupPublicKey: String) { | 
		
	
		
			
				|  |  |  |  |         // Prepare | 
		
	
		
			
				|  |  |  |  |         val sskDatabase = DatabaseFactory.getSSKDatabase(context) | 
		
	
		
			
				|  |  |  |  |     @JvmStatic | 
		
	
		
			
				|  |  |  |  |     public fun leave(context: Context, groupPublicKey: String) { | 
		
	
		
			
				|  |  |  |  |         val userPublicKey = TextSecurePreferences.getLocalNumber(context) | 
		
	
		
			
				|  |  |  |  |         val groupDB = DatabaseFactory.getGroupDatabase(context) | 
		
	
		
			
				|  |  |  |  |         val groupID = doubleEncodeGroupID(groupPublicKey) | 
		
	
		
			
				|  |  |  |  |         val group = groupDB.getGroup(groupID).orNull() | 
		
	
		
			
				|  |  |  |  |         if (group == null) { | 
		
	
		
			
				|  |  |  |  |             Log.d("Loki", "Can't add users to nonexistent closed group.") | 
		
	
		
			
				|  |  |  |  |             Log.d("Loki", "Can't leave nonexistent closed group.") | 
		
	
		
			
				|  |  |  |  |             return | 
		
	
		
			
				|  |  |  |  |         } | 
		
	
		
			
				|  |  |  |  |         val name = group.title | 
		
	
		
			
				|  |  |  |  |         val admins = group.admins.map { it.serialize() } | 
		
	
		
			
				|  |  |  |  |         val adminsAsData = admins.map { Hex.fromStringCondensed(it) } | 
		
	
		
			
				|  |  |  |  |         val groupPrivateKey = DatabaseFactory.getSSKDatabase(context).getClosedGroupPrivateKey(groupPublicKey) | 
		
	
		
			
				|  |  |  |  |         if (groupPrivateKey == null) { | 
		
	
		
			
				|  |  |  |  |             Log.d("Loki", "Couldn't get private key for closed group.") | 
		
	
		
			
				|  |  |  |  |             return | 
		
	
		
			
				|  |  |  |  |         } | 
		
	
		
			
				|  |  |  |  |         // Add the members to the member list | 
		
	
		
			
				|  |  |  |  |         val members = group.members.map { it.serialize() }.toMutableSet() | 
		
	
		
			
				|  |  |  |  |         members.addAll(newMembers) | 
		
	
		
			
				|  |  |  |  |         val membersAsData = members.map { Hex.fromStringCondensed(it) } | 
		
	
		
			
				|  |  |  |  |         // Generate ratchets for the new members | 
		
	
		
			
				|  |  |  |  |         val senderKeys: List<ClosedGroupSenderKey> = newMembers.map { publicKey -> | 
		
	
		
			
				|  |  |  |  |             val ratchet = SharedSenderKeysImplementation.shared.generateRatchet(groupPublicKey, publicKey) | 
		
	
		
			
				|  |  |  |  |             ClosedGroupSenderKey(Hex.fromStringCondensed(ratchet.chainKey), ratchet.keyIndex, Hex.fromStringCondensed(publicKey)) | 
		
	
		
			
				|  |  |  |  |         } | 
		
	
		
			
				|  |  |  |  |         // Send a closed group update message to the existing members with the new members' ratchets (this message is aimed at the group) | 
		
	
		
			
				|  |  |  |  |         val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.Info(Hex.fromStringCondensed(groupPublicKey), name, | 
		
	
		
			
				|  |  |  |  |             senderKeys, membersAsData, adminsAsData) | 
		
	
		
			
				|  |  |  |  |         val job = ClosedGroupUpdateMessageSendJob(groupPublicKey, closedGroupUpdateKind) | 
		
	
		
			
				|  |  |  |  |         ApplicationContext.getInstance(context).jobManager.add(job) | 
		
	
		
			
				|  |  |  |  |         // Establish sessions if needed | 
		
	
		
			
				|  |  |  |  |         establishSessionsWithMembersIfNeeded(context, newMembers) | 
		
	
		
			
				|  |  |  |  |         // Send closed group update messages to the new members using established channels | 
		
	
		
			
				|  |  |  |  |         val allSenderKeys = sskDatabase.getAllClosedGroupSenderKeys(groupPublicKey) + senderKeys | 
		
	
		
			
				|  |  |  |  |         for (member in members) { | 
		
	
		
			
				|  |  |  |  |             @Suppress("NAME_SHADOWING") | 
		
	
		
			
				|  |  |  |  |             val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.New(Hex.fromStringCondensed(groupPublicKey), name, | 
		
	
		
			
				|  |  |  |  |                 Hex.fromStringCondensed(groupPrivateKey), allSenderKeys, membersAsData, adminsAsData) | 
		
	
		
			
				|  |  |  |  |             @Suppress("NAME_SHADOWING") | 
		
	
		
			
				|  |  |  |  |             val job = ClosedGroupUpdateMessageSendJob(member, closedGroupUpdateKind) | 
		
	
		
			
				|  |  |  |  |             ApplicationContext.getInstance(context).jobManager.add(job) | 
		
	
		
			
				|  |  |  |  |         } | 
		
	
		
			
				|  |  |  |  |         // Update the group | 
		
	
		
			
				|  |  |  |  |         groupDB.updateMembers(groupID, members.map { Address.fromSerialized(it) }) | 
		
	
		
			
				|  |  |  |  |         // Notify the user | 
		
	
		
			
				|  |  |  |  |         val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false)) | 
		
	
		
			
				|  |  |  |  |         insertOutgoingInfoMessage(context, groupID, GroupContext.Type.UPDATE, name, members, admins, threadID) | 
		
	
		
			
				|  |  |  |  |         val oldMembers = group.members.map { it.serialize() }.toSet() | 
		
	
		
			
				|  |  |  |  |         val newMembers = oldMembers.minus(userPublicKey) | 
		
	
		
			
				|  |  |  |  |         update(context, groupPublicKey, newMembers, name) | 
		
	
		
			
				|  |  |  |  |     } | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |     @JvmStatic | 
		
	
		
			
				|  |  |  |  |     public fun leave(context: Context, groupPublicKey: String) { | 
		
	
		
			
				|  |  |  |  |         val userPublicKey = TextSecurePreferences.getLocalNumber(context) | 
		
	
		
			
				|  |  |  |  |         removeMembers(context, setOf( userPublicKey ), groupPublicKey) | 
		
	
		
			
				|  |  |  |  |     } | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |     public fun removeMembers(context: Context, membersToRemove: Collection<String>, groupPublicKey: String) { | 
		
	
		
			
				|  |  |  |  |     public fun update(context: Context, groupPublicKey: String, members: Collection<String>, name: String) { | 
		
	
		
			
				|  |  |  |  |         val userPublicKey = TextSecurePreferences.getLocalNumber(context) | 
		
	
		
			
				|  |  |  |  |         val sskDatabase = DatabaseFactory.getSSKDatabase(context) | 
		
	
		
			
				|  |  |  |  |         val isUserLeaving = membersToRemove.contains(userPublicKey) | 
		
	
		
			
				|  |  |  |  |         if (isUserLeaving && membersToRemove.count() != 1) { | 
		
	
		
			
				|  |  |  |  |             Log.d("Loki", "Can't remove self and others simultaneously.") | 
		
	
		
			
				|  |  |  |  |             return | 
		
	
		
			
				|  |  |  |  |         } | 
		
	
		
			
				|  |  |  |  |         val groupDB = DatabaseFactory.getGroupDatabase(context) | 
		
	
		
			
				|  |  |  |  |         val groupID = doubleEncodeGroupID(groupPublicKey) | 
		
	
		
			
				|  |  |  |  |         val group = groupDB.getGroup(groupID).orNull() | 
		
	
		
			
				|  |  |  |  |         if (group == null) { | 
		
	
		
			
				|  |  |  |  |             Log.d("Loki", "Can't add users to nonexistent closed group.") | 
		
	
		
			
				|  |  |  |  |             Log.d("Loki", "Can't update nonexistent closed group.") | 
		
	
		
			
				|  |  |  |  |             return | 
		
	
		
			
				|  |  |  |  |         } | 
		
	
		
			
				|  |  |  |  |         val name = group.title | 
		
	
		
			
				|  |  |  |  |         val oldMembers = group.members.map { it.serialize() }.toSet() | 
		
	
		
			
				|  |  |  |  |         val membersAsData = members.map { Hex.fromStringCondensed(it) } | 
		
	
		
			
				|  |  |  |  |         val admins = group.admins.map { it.serialize() } | 
		
	
		
			
				|  |  |  |  |         val adminsAsData = admins.map { Hex.fromStringCondensed(it) } | 
		
	
		
			
				|  |  |  |  |         // Remove the members from the member list | 
		
	
		
			
				|  |  |  |  |         val members = group.members.map { it.serialize() }.toSet().minus(membersToRemove) | 
		
	
		
			
				|  |  |  |  |         val membersAsData = members.map { Hex.fromStringCondensed(it) } | 
		
	
		
			
				|  |  |  |  |         val groupPrivateKey = DatabaseFactory.getSSKDatabase(context).getClosedGroupPrivateKey(groupPublicKey) | 
		
	
		
			
				|  |  |  |  |         if (groupPrivateKey == null) { | 
		
	
		
			
				|  |  |  |  |             Log.d("Loki", "Couldn't get private key for closed group.") | 
		
	
		
			
				|  |  |  |  |             return | 
		
	
		
			
				|  |  |  |  |         } | 
		
	
		
			
				|  |  |  |  |         val wasAnyUserRemoved = members.toSet().intersect(oldMembers) != oldMembers.toSet() | 
		
	
		
			
				|  |  |  |  |         if (wasAnyUserRemoved) { | 
		
	
		
			
				|  |  |  |  |             val removedMembers = oldMembers.minus(members) | 
		
	
		
			
				|  |  |  |  |             val isUserLeaving = removedMembers.contains(userPublicKey) | 
		
	
		
			
				|  |  |  |  |             if (isUserLeaving && removedMembers.count() != 1) { | 
		
	
		
			
				|  |  |  |  |                 Log.d("Loki", "Can't remove self and others simultaneously.") | 
		
	
		
			
				|  |  |  |  |                 return | 
		
	
		
			
				|  |  |  |  |             } | 
		
	
		
			
				|  |  |  |  |             // Send the update to the group (don't include new ratchets as everyone should regenerate new ratchets individually) | 
		
	
		
			
				|  |  |  |  |             val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.Info(Hex.fromStringCondensed(groupPublicKey), | 
		
	
		
			
				|  |  |  |  |                 name, setOf(), membersAsData, adminsAsData) | 
		
	
		
			
				|  |  |  |  |             val job = ClosedGroupUpdateMessageSendJob(groupPublicKey, closedGroupUpdateKind) | 
		
	
		
			
				|  |  |  |  |             job.setContext(context) | 
		
	
		
			
				|  |  |  |  |             job.onRun() // Run the job immediately | 
		
	
		
			
				|  |  |  |  |         // Delete all ratchets (it's important that this happens after sending out the update) | 
		
	
		
			
				|  |  |  |  |             // Delete all ratchets (it's important that this happens * after * sending out the update) | 
		
	
		
			
				|  |  |  |  |             sskDatabase.removeAllClosedGroupRatchets(groupPublicKey) | 
		
	
		
			
				|  |  |  |  |             // Remove the group from the user's set of public keys to poll for if the user is leaving. Otherwise generate a new ratchet and | 
		
	
		
			
				|  |  |  |  |             // send it out to all members (minus the removed ones) using established channels. | 
		
	
	
		
			
				
					|  |  |  | @ -171,6 +136,7 @@ object ClosedGroupsProtocol { | 
		
	
		
			
				|  |  |  |  |                 val userRatchet = SharedSenderKeysImplementation.shared.generateRatchet(groupPublicKey, userPublicKey) | 
		
	
		
			
				|  |  |  |  |                 val userSenderKey = ClosedGroupSenderKey(Hex.fromStringCondensed(userRatchet.chainKey), userRatchet.keyIndex, Hex.fromStringCondensed(userPublicKey)) | 
		
	
		
			
				|  |  |  |  |                 for (member in members) { | 
		
	
		
			
				|  |  |  |  |                     if (member == userPublicKey) { continue } | 
		
	
		
			
				|  |  |  |  |                     @Suppress("NAME_SHADOWING") | 
		
	
		
			
				|  |  |  |  |                     val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.SenderKey(Hex.fromStringCondensed(groupPublicKey), userSenderKey) | 
		
	
		
			
				|  |  |  |  |                     @Suppress("NAME_SHADOWING") | 
		
	
	
		
			
				
					|  |  |  | @ -178,26 +144,33 @@ object ClosedGroupsProtocol { | 
		
	
		
			
				|  |  |  |  |                     ApplicationContext.getInstance(context).jobManager.add(job) | 
		
	
		
			
				|  |  |  |  |                 } | 
		
	
		
			
				|  |  |  |  |             } | 
		
	
		
			
				|  |  |  |  |         // Update the group | 
		
	
		
			
				|  |  |  |  |         groupDB.updateMembers(groupID, members.map { Address.fromSerialized(it) }) | 
		
	
		
			
				|  |  |  |  |         // Notify the user | 
		
	
		
			
				|  |  |  |  |         val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false)) | 
		
	
		
			
				|  |  |  |  |         insertOutgoingInfoMessage(context, groupID, GroupContext.Type.QUIT, name, members, admins, threadID) | 
		
	
		
			
				|  |  |  |  |     } | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |     public fun update(context: Context, groupPublicKey: String, members: Collection<String>, name: String, admins: Collection<String>) { | 
		
	
		
			
				|  |  |  |  |         val groupDB = DatabaseFactory.getGroupDatabase(context) | 
		
	
		
			
				|  |  |  |  |         val groupID = doubleEncodeGroupID(groupPublicKey) | 
		
	
		
			
				|  |  |  |  |         if (groupDB.getGroup(groupID).orNull() == null) { | 
		
	
		
			
				|  |  |  |  |             Log.d("Loki", "Can't update nonexistent closed group.") | 
		
	
		
			
				|  |  |  |  |             return | 
		
	
		
			
				|  |  |  |  |         } else { | 
		
	
		
			
				|  |  |  |  |             // Generate ratchets for any new members | 
		
	
		
			
				|  |  |  |  |             val newMembers = members.minus(oldMembers) | 
		
	
		
			
				|  |  |  |  |             val senderKeys: List<ClosedGroupSenderKey> = newMembers.map { publicKey -> | 
		
	
		
			
				|  |  |  |  |                 val ratchet = SharedSenderKeysImplementation.shared.generateRatchet(groupPublicKey, publicKey) | 
		
	
		
			
				|  |  |  |  |                 ClosedGroupSenderKey(Hex.fromStringCondensed(ratchet.chainKey), ratchet.keyIndex, Hex.fromStringCondensed(publicKey)) | 
		
	
		
			
				|  |  |  |  |             } | 
		
	
		
			
				|  |  |  |  |         // Send the update to the group | 
		
	
		
			
				|  |  |  |  |         val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.Info(Hex.fromStringCondensed(groupPublicKey), | 
		
	
		
			
				|  |  |  |  |             name, setOf(), members.map { Hex.fromStringCondensed(it) }, admins.map { Hex.fromStringCondensed(it) }) | 
		
	
		
			
				|  |  |  |  |             // Send a closed group update message to the existing members with the new members' ratchets (this message is aimed at the group) | 
		
	
		
			
				|  |  |  |  |             val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.Info(Hex.fromStringCondensed(groupPublicKey), name, | 
		
	
		
			
				|  |  |  |  |                 senderKeys, membersAsData, adminsAsData) | 
		
	
		
			
				|  |  |  |  |             val job = ClosedGroupUpdateMessageSendJob(groupPublicKey, closedGroupUpdateKind) | 
		
	
		
			
				|  |  |  |  |             ApplicationContext.getInstance(context).jobManager.add(job) | 
		
	
		
			
				|  |  |  |  |             // Establish sessions if needed | 
		
	
		
			
				|  |  |  |  |             establishSessionsWithMembersIfNeeded(context, newMembers) | 
		
	
		
			
				|  |  |  |  |             // Send closed group update messages to the new members using established channels | 
		
	
		
			
				|  |  |  |  |             val allSenderKeys = sskDatabase.getAllClosedGroupSenderKeys(groupPublicKey) + senderKeys | 
		
	
		
			
				|  |  |  |  |             for (member in newMembers) { | 
		
	
		
			
				|  |  |  |  |                 @Suppress("NAME_SHADOWING") | 
		
	
		
			
				|  |  |  |  |                 val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.New(Hex.fromStringCondensed(groupPublicKey), name, | 
		
	
		
			
				|  |  |  |  |                     Hex.fromStringCondensed(groupPrivateKey), allSenderKeys, membersAsData, adminsAsData) | 
		
	
		
			
				|  |  |  |  |                 @Suppress("NAME_SHADOWING") | 
		
	
		
			
				|  |  |  |  |                 val job = ClosedGroupUpdateMessageSendJob(member, closedGroupUpdateKind) | 
		
	
		
			
				|  |  |  |  |                 ApplicationContext.getInstance(context).jobManager.add(job) | 
		
	
		
			
				|  |  |  |  |             } | 
		
	
		
			
				|  |  |  |  |         } | 
		
	
		
			
				|  |  |  |  |         // Update the group | 
		
	
		
			
				|  |  |  |  |         groupDB.updateTitle(groupID, name) | 
		
	
		
			
				|  |  |  |  |         groupDB.updateMembers(groupID, members.map { Address.fromSerialized(it) }) | 
		
	
		
			
				|  |  |  |  |         // Notify the user | 
		
	
		
			
				|  |  |  |  |         val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false)) | 
		
	
	
		
			
				
					|  |  |  | 
 |