diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationAdapter.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationAdapter.java index 7c531681f6..ceabfc7526 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationAdapter.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationAdapter.java @@ -30,7 +30,6 @@ import android.widget.TextView; import com.annimon.stream.Stream; - import org.thoughtcrime.securesms.BindableConversationItem; import org.thoughtcrime.securesms.conversation.ConversationAdapter.HeaderViewHolder; import org.thoughtcrime.securesms.database.DatabaseFactory; diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/EditClosedGroupActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/activities/EditClosedGroupActivity.kt index 0d197ffbc7..e1039c854e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/EditClosedGroupActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/activities/EditClosedGroupActivity.kt @@ -16,6 +16,7 @@ import androidx.recyclerview.widget.RecyclerView import kotlinx.android.synthetic.main.activity_settings.* import network.loki.messenger.R import nl.komponents.kovenant.Promise +import nl.komponents.kovenant.task import nl.komponents.kovenant.ui.failUi import nl.komponents.kovenant.ui.successUi import org.session.libsignal.service.loki.utilities.toHexString @@ -239,6 +240,9 @@ class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() { val members = this.members.map { Recipient.from(this, Address.fromSerialized(it), false) }.toSet() + val originalMembers = this.originalMembers.map { + Recipient.from(this, Address.fromSerialized(it), false) + }.toSet() val admins = members.toSet() //TODO For now, consider all the users to be admins. @@ -272,11 +276,25 @@ class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() { if (isSSKBasedClosedGroup) { isLoading = true loaderContainer.fadeIn() - val promise: Promise - if (!members.contains(Recipient.from(this, Address.fromSerialized(userPublicKey), false))) { - promise = ClosedGroupsProtocolV2.leave(this, groupPublicKey!!) + val promise: Promise = if (!members.contains(Recipient.from(this, Address.fromSerialized(userPublicKey), false))) { + ClosedGroupsProtocolV2.leave(this, groupPublicKey!!) } else { - promise = ClosedGroupsProtocolV2.update(this, groupPublicKey!!, members.map { it.address.serialize() }, name) +// TODO: uncomment when we switch to sending new explicit updates after clients update +// task { +// val name = +// if (hasNameChanged) ClosedGroupsProtocolV2.explicitNameChange(this@EditClosedGroupActivity,groupPublicKey!!,name) +// else Promise.of(Unit) +// name.get() +// members.filterNot { it in originalMembers }.let { adds -> +// if (adds.isNotEmpty()) ClosedGroupsProtocolV2.explicitAddMembers(this@EditClosedGroupActivity, groupPublicKey!!, adds.map { it.address.serialize() }) +// else Promise.of(Unit) +// }.get() +// originalMembers.filterNot { it in members }.let { removes -> +// if (removes.isNotEmpty()) ClosedGroupsProtocolV2.explicitRemoveMembers(this@EditClosedGroupActivity, groupPublicKey!!, removes.map { it.address.serialize() }) +// else Promise.of(Unit) +// }.get() +// } + ClosedGroupsProtocolV2.update(this, groupPublicKey!!, members.map { it.address.serialize() }, name) } promise.successUi { loaderContainer.fadeOut() diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupUpdateMessageSendJobV2.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupUpdateMessageSendJobV2.kt index f840cb861b..e5c8168614 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupUpdateMessageSendJobV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupUpdateMessageSendJobV2.kt @@ -31,6 +31,10 @@ class ClosedGroupUpdateMessageSendJobV2 private constructor(parameters: Paramete sealed class Kind { class New(val publicKey: ByteArray, val name: String, val encryptionKeyPair: ECKeyPair, val members: Collection, val admins: Collection) : Kind() class Update(val name: String, val members: Collection) : Kind() + object Leave : Kind() + class RemoveMembers(val members: Collection) : Kind() + class AddMembers(val members: Collection) : Kind() + class NameChange(val name: String) : Kind() class EncryptionKeyPair(val wrappers: Collection) : Kind() // The new encryption key pair encrypted for each member individually } @@ -88,6 +92,23 @@ class ClosedGroupUpdateMessageSendJobV2 private constructor(parameters: Paramete val members = kind.members.joinToString(" - ") { it.toHexString() } builder.putString("members", members) } + is Kind.RemoveMembers -> { + builder.putString("kind", "RemoveMembers") + val members = kind.members.joinToString(" - ") { it.toHexString() } + builder.putString("members", members) + } + Kind.Leave -> { + builder.putString("kind", "Leave") + } + is Kind.AddMembers -> { + builder.putString("kind", "AddMembers") + val members = kind.members.joinToString(" - ") { it.toHexString() } + builder.putString("members", members) + } + is Kind.NameChange -> { + builder.putString("kind", "NameChange") + builder.putString("name", kind.name) + } is Kind.EncryptionKeyPair -> { builder.putString("kind", "EncryptionKeyPair") val wrappers = kind.wrappers.joinToString(" - ") { Json.encodeToString(it) } @@ -123,6 +144,21 @@ class ClosedGroupUpdateMessageSendJobV2 private constructor(parameters: Paramete val wrappers: Collection = data.getString("wrappers").split(" - ").map { Json.decodeFromString(it) } kind = Kind.EncryptionKeyPair(wrappers) } + "RemoveMembers" -> { + val members = data.getString("members").split(" - ").map { Hex.fromStringCondensed(it) } + kind = Kind.RemoveMembers(members) + } + "AddMembers" -> { + val members = data.getString("members").split(" - ").map { Hex.fromStringCondensed(it) } + kind = Kind.AddMembers(members) + } + "NameChange" -> { + val name = data.getString("name") + kind = Kind.NameChange(name) + } + "Leave" -> { + kind = Kind.Leave + } else -> throw Exception("Invalid closed group update message kind: $rawKind.") } return ClosedGroupUpdateMessageSendJobV2(parameters, destination, kind) @@ -154,6 +190,21 @@ class ClosedGroupUpdateMessageSendJobV2 private constructor(parameters: Paramete closedGroupUpdate.type = SignalServiceProtos.ClosedGroupUpdateV2.Type.ENCRYPTION_KEY_PAIR closedGroupUpdate.addAllWrappers(kind.wrappers.map { it.toProto() }) } + Kind.Leave -> { + closedGroupUpdate.type = SignalServiceProtos.ClosedGroupUpdateV2.Type.MEMBER_LEFT + } + is Kind.RemoveMembers -> { + closedGroupUpdate.type = SignalServiceProtos.ClosedGroupUpdateV2.Type.MEMBERS_REMOVED + closedGroupUpdate.addAllMembers(kind.members.map { ByteString.copyFrom(it) }) + } + is Kind.AddMembers -> { + closedGroupUpdate.type = SignalServiceProtos.ClosedGroupUpdateV2.Type.MEMBERS_ADDED + closedGroupUpdate.addAllMembers(kind.members.map { ByteString.copyFrom(it) }) + } + is Kind.NameChange -> { + closedGroupUpdate.type = SignalServiceProtos.ClosedGroupUpdateV2.Type.NAME_CHANGE + closedGroupUpdate.name = kind.name + } } dataMessage.closedGroupUpdateV2 = closedGroupUpdate.build() contentMessage.dataMessage = dataMessage.build() @@ -162,10 +213,9 @@ class ClosedGroupUpdateMessageSendJobV2 private constructor(parameters: Paramete val address = SignalServiceAddress(destination) val recipient = recipient(context, destination) val udAccess = UnidentifiedAccessUtil.getAccessFor(context, recipient) - val ttl: Int - when (kind) { - is Kind.EncryptionKeyPair -> ttl = 4 * 24 * 60 * 60 * 1000 - else -> ttl = TTLUtilities.getTTL(TTLUtilities.MessageType.ClosedGroupUpdate) + val ttl = when (kind) { + is Kind.EncryptionKeyPair -> 4 * 24 * 60 * 60 * 1000 + else -> TTLUtilities.getTTL(TTLUtilities.MessageType.ClosedGroupUpdate) } try { // isClosedGroup can always be false as it's only used in the context of legacy closed groups diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocolV2.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocolV2.kt index ad544e9327..b3b48ca1ad 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocolV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocolV2.kt @@ -5,6 +5,7 @@ import android.util.Log import com.google.protobuf.ByteString import nl.komponents.kovenant.Promise import nl.komponents.kovenant.deferred +import nl.komponents.kovenant.task import org.session.libsignal.libsignal.ecc.Curve import org.session.libsignal.libsignal.ecc.DjbECPrivateKey import org.session.libsignal.libsignal.ecc.DjbECPublicKey @@ -12,21 +13,25 @@ import org.session.libsignal.libsignal.ecc.ECKeyPair import org.session.libsignal.libsignal.util.guava.Optional import org.session.libsignal.service.api.messages.SignalServiceGroup import org.session.libsignal.service.internal.push.SignalServiceProtos +import org.session.libsignal.service.internal.push.SignalServiceProtos.GroupContext import org.session.libsignal.utilities.ThreadUtils import org.session.libsignal.service.loki.utilities.hexEncodedPublicKey import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded import org.session.libsignal.service.loki.utilities.toHexString import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.database.DatabaseFactory +import org.thoughtcrime.securesms.database.GroupDatabase import org.thoughtcrime.securesms.loki.api.LokiPushNotificationManager import org.thoughtcrime.securesms.loki.api.LokiPushNotificationManager.ClosedGroupOperation import org.thoughtcrime.securesms.loki.api.SessionProtocolImpl +import org.thoughtcrime.securesms.loki.database.LokiAPIDatabase import org.thoughtcrime.securesms.mms.OutgoingGroupMediaMessage import org.thoughtcrime.securesms.sms.IncomingGroupMessage import org.thoughtcrime.securesms.sms.IncomingTextMessage import org.session.libsignal.utilities.Hex import org.session.libsession.messaging.threads.Address +import org.session.libsession.messaging.threads.GroupRecord import org.session.libsession.messaging.threads.recipients.Recipient import org.session.libsession.utilities.GroupUtil import org.session.libsession.utilities.TextSecurePreferences @@ -36,7 +41,7 @@ import java.util.* import kotlin.jvm.Throws object ClosedGroupsProtocolV2 { - val groupSizeLimit = 100 + const val groupSizeLimit = 100 sealed class Error(val description: String) : Exception() { object NoThread : Error("Couldn't find a thread associated with the given group public key") @@ -44,7 +49,7 @@ object ClosedGroupsProtocolV2 { object InvalidUpdate : Error("Invalid group update.") } - public fun createClosedGroup(context: Context, name: String, members: Collection): Promise { + fun createClosedGroup(context: Context, name: String, members: Collection): Promise { val deferred = deferred() ThreadUtils.queue { // Prepare @@ -60,7 +65,7 @@ object ClosedGroupsProtocolV2 { val admins = setOf( userPublicKey ) val adminsAsData = admins.map { Hex.fromStringCondensed(it) } DatabaseFactory.getGroupDatabase(context).create(groupID, name, LinkedList(members.map { Address.fromSerialized(it) }), - null, null, LinkedList(admins.map { Address.fromSerialized(it) })) + null, null, LinkedList(admins.map { Address.fromSerialized(it!!) })) DatabaseFactory.getRecipientDatabase(context).setProfileSharing(Recipient.from(context, Address.fromSerialized(groupID), false), true) // Send a closed group update message to all members individually val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJobV2.Kind.New(Hex.fromStringCondensed(groupPublicKey), name, encryptionKeyPair, membersAsData, adminsAsData) @@ -76,7 +81,7 @@ object ClosedGroupsProtocolV2 { apiDB.addClosedGroupEncryptionKeyPair(encryptionKeyPair, groupPublicKey) // Notify the user val threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false)) - insertOutgoingInfoMessage(context, groupID, SignalServiceProtos.GroupContext.Type.UPDATE, name, members, admins, threadID) + insertOutgoingInfoMessage(context, groupID, GroupContext.Type.UPDATE, name, members, admins, threadID) // Notify the PN server LokiPushNotificationManager.performOperation(context, ClosedGroupOperation.Subscribe, groupPublicKey, userPublicKey) // Fulfill the promise @@ -87,7 +92,155 @@ object ClosedGroupsProtocolV2 { } @JvmStatic - public fun leave(context: Context, groupPublicKey: String): Promise { + fun explicitLeave(context: Context, groupPublicKey: String): Promise { + val deferred = deferred() + ThreadUtils.queue { + val userPublicKey = TextSecurePreferences.getLocalNumber(context)!! + val apiDB = DatabaseFactory.getLokiAPIDatabase(context) + val groupDB = DatabaseFactory.getGroupDatabase(context) + val groupID = doubleEncodeGroupID(groupPublicKey) + val group = groupDB.getGroup(groupID).orNull() + val updatedMembers = group.members.map { it.serialize() }.toSet() - userPublicKey + val admins = group.admins.map { it.serialize() } + val name = group.title + if (group == null) { + Log.d("Loki", "Can't leave nonexistent closed group.") + return@queue deferred.reject(Error.NoThread) + } + // Send the update to the group + @Suppress("NAME_SHADOWING") + val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, ClosedGroupUpdateMessageSendJobV2.Kind.Leave) + job.setContext(context) + job.onRun() // Run the job immediately + // Remove the group private key and unsubscribe from PNs + disableLocalGroupAndUnsubscribe(context, apiDB, groupPublicKey, groupDB, groupID, userPublicKey) + // Notify the user + val infoType = GroupContext.Type.QUIT + val threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false)) + insertOutgoingInfoMessage(context, groupID, infoType, name, updatedMembers, admins, threadID) + deferred.resolve(Unit) + } + return deferred.promise + } + + @JvmStatic + fun explicitAddMembers(context: Context, groupPublicKey: String, membersToAdd: List): Promise { + return task { + val apiDB = DatabaseFactory.getLokiAPIDatabase(context) + val groupDB = DatabaseFactory.getGroupDatabase(context) + val groupID = doubleEncodeGroupID(groupPublicKey) + val group = groupDB.getGroup(groupID).orNull() + if (group == null) { + Log.d("Loki", "Can't leave nonexistent closed group.") + return@task Error.NoThread + } + val updatedMembers = group.members.map { it.serialize() }.toSet() + membersToAdd + // Save the new group members + groupDB.updateMembers(groupID, updatedMembers.map { Address.fromSerialized(it) }) + val membersAsData = updatedMembers.map { Hex.fromStringCondensed(it) } + val newMembersAsData = membersToAdd.map { Hex.fromStringCondensed(it) } + val admins = group.admins.map { it.serialize() } + val adminsAsData = admins.map { Hex.fromStringCondensed(it) } + val encryptionKeyPair = apiDB.getLatestClosedGroupEncryptionKeyPair(groupPublicKey) + if (encryptionKeyPair == null) { + Log.d("Loki", "Couldn't get encryption key pair for closed group.") + return@task Error.NoKeyPair + } + val name = group.title + // Send the update to the group + val memberUpdateKind = ClosedGroupUpdateMessageSendJobV2.Kind.AddMembers(newMembersAsData) + val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, memberUpdateKind) + job.setContext(context) + job.onRun() // Run the job immediately + // Send closed group update messages to any new members individually + for (member in membersToAdd) { + @Suppress("NAME_SHADOWING") + val closedGroupNewKind = ClosedGroupUpdateMessageSendJobV2.Kind.New(Hex.fromStringCondensed(groupPublicKey), name, encryptionKeyPair, membersAsData, adminsAsData) + @Suppress("NAME_SHADOWING") + val newMemberJob = ClosedGroupUpdateMessageSendJobV2(member, closedGroupNewKind) + ApplicationContext.getInstance(context).jobManager.add(newMemberJob) + } + // Notify the user + val infoType = GroupContext.Type.UPDATE + val threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false)) + insertOutgoingInfoMessage(context, groupID, infoType, name, updatedMembers, admins, threadID) + } + } + + @JvmStatic + fun explicitRemoveMembers(context: Context, groupPublicKey: String, membersToRemove: List): Promise { + return task { + val userPublicKey = TextSecurePreferences.getLocalNumber(context) + val apiDB = DatabaseFactory.getLokiAPIDatabase(context) + val groupDB = DatabaseFactory.getGroupDatabase(context) + val groupID = doubleEncodeGroupID(groupPublicKey) + val group = groupDB.getGroup(groupID).orNull() + if (group == null) { + Log.d("Loki", "Can't leave nonexistent closed group.") + return@task Error.NoThread + } + val updatedMembers = group.members.map { it.serialize() }.toSet() - membersToRemove + // Save the new group members + groupDB.updateMembers(groupID, updatedMembers.map { Address.fromSerialized(it) }) + val removeMembersAsData = membersToRemove.map { Hex.fromStringCondensed(it) } + val admins = group.admins.map { it.serialize() } + val encryptionKeyPair = apiDB.getLatestClosedGroupEncryptionKeyPair(groupPublicKey) + if (encryptionKeyPair == null) { + Log.d("Loki", "Couldn't get encryption key pair for closed group.") + return@task Error.NoKeyPair + } + if (membersToRemove.any { it in admins } && updatedMembers.isNotEmpty()) { + Log.d("Loki", "Can't remove admin from closed group unless the group is destroyed entirely.") + return@task Error.InvalidUpdate + } + val name = group.title + // Send the update to the group + val memberUpdateKind = ClosedGroupUpdateMessageSendJobV2.Kind.RemoveMembers(removeMembersAsData) + val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, memberUpdateKind) + job.setContext(context) + job.onRun() // Run the job immediately + val isCurrentUserAdmin = admins.contains(userPublicKey) + if (isCurrentUserAdmin) { + generateAndSendNewEncryptionKeyPair(context, groupPublicKey, updatedMembers) + } + // Notify the user + val infoType = GroupContext.Type.UPDATE + val threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false)) + insertOutgoingInfoMessage(context, groupID, infoType, name, updatedMembers, admins, threadID) + } + } + + @JvmStatic + fun explicitNameChange(context: Context, groupPublicKey: String, newName: String): Promise { + val deferred = deferred() + ThreadUtils.queue { + val groupDB = DatabaseFactory.getGroupDatabase(context) + val groupID = doubleEncodeGroupID(groupPublicKey) + val group = groupDB.getGroup(groupID).orNull() + val members = group.members.map { it.serialize() }.toSet() + val admins = group.admins.map { it.serialize() } + if (group == null) { + Log.d("Loki", "Can't leave nonexistent closed group.") + return@queue deferred.reject(Error.NoThread) + } + // Send the update to the group + val kind = ClosedGroupUpdateMessageSendJobV2.Kind.NameChange(newName) + val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, kind) + job.setContext(context) + job.onRun() // Run the job immediately + // Update the group + groupDB.updateTitle(groupID, newName) + // Notify the user + val infoType = GroupContext.Type.UPDATE + val threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false)) + insertOutgoingInfoMessage(context, groupID, infoType, newName, members, admins, threadID) + deferred.resolve(Unit) + } + return deferred.promise + } + + @JvmStatic + fun leave(context: Context, groupPublicKey: String): Promise { val userPublicKey = TextSecurePreferences.getLocalNumber(context)!! val groupDB = DatabaseFactory.getGroupDatabase(context) val groupID = doubleEncodeGroupID(groupPublicKey) @@ -108,7 +261,7 @@ object ClosedGroupsProtocolV2 { return update(context, groupPublicKey, newMembers, name) } - public fun update(context: Context, groupPublicKey: String, members: Collection, name: String): Promise { + fun update(context: Context, groupPublicKey: String, members: Collection, name: String): Promise { val deferred = deferred() ThreadUtils.queue { val userPublicKey = TextSecurePreferences.getLocalNumber(context)!! @@ -181,7 +334,7 @@ object ClosedGroupsProtocolV2 { groupDB.updateMembers(groupID, members.map { Address.fromSerialized(it) }) } // Notify the user - val infoType = if (isUserLeaving) SignalServiceProtos.GroupContext.Type.QUIT else SignalServiceProtos.GroupContext.Type.UPDATE + val infoType = if (isUserLeaving) GroupContext.Type.QUIT else GroupContext.Type.UPDATE val threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false)) insertOutgoingInfoMessage(context, groupID, infoType, name, members, admins, threadID) deferred.resolve(Unit) @@ -223,29 +376,41 @@ object ClosedGroupsProtocolV2 { } @JvmStatic - public fun handleMessage(context: Context, closedGroupUpdate: SignalServiceProtos.ClosedGroupUpdateV2, sentTimestamp: Long, groupPublicKey: String, senderPublicKey: String) { - if (!isValid(closedGroupUpdate)) { return; } + fun handleMessage(context: Context, closedGroupUpdate: SignalServiceProtos.ClosedGroupUpdateV2, sentTimestamp: Long, groupPublicKey: String, senderPublicKey: String) { + if (!isValid(closedGroupUpdate, senderPublicKey)) { return } when (closedGroupUpdate.type) { SignalServiceProtos.ClosedGroupUpdateV2.Type.NEW -> handleNewClosedGroup(context, closedGroupUpdate, senderPublicKey) + SignalServiceProtos.ClosedGroupUpdateV2.Type.MEMBERS_REMOVED -> handleClosedGroupMembersRemoved(context, closedGroupUpdate, sentTimestamp, groupPublicKey, senderPublicKey) + SignalServiceProtos.ClosedGroupUpdateV2.Type.MEMBERS_ADDED -> handleClosedGroupMembersAdded(context, closedGroupUpdate, sentTimestamp, groupPublicKey, senderPublicKey) + SignalServiceProtos.ClosedGroupUpdateV2.Type.NAME_CHANGE -> handleClosedGroupNameChange(context, closedGroupUpdate, sentTimestamp, groupPublicKey, senderPublicKey) + SignalServiceProtos.ClosedGroupUpdateV2.Type.MEMBER_LEFT -> handleClosedGroupMemberLeft(context, sentTimestamp, groupPublicKey, senderPublicKey) SignalServiceProtos.ClosedGroupUpdateV2.Type.UPDATE -> handleClosedGroupUpdate(context, closedGroupUpdate, sentTimestamp, groupPublicKey, senderPublicKey) SignalServiceProtos.ClosedGroupUpdateV2.Type.ENCRYPTION_KEY_PAIR -> handleGroupEncryptionKeyPair(context, closedGroupUpdate, groupPublicKey, senderPublicKey) else -> { - // Do nothing + Log.d("Loki","Can't handle closed group update of unknown type: ${closedGroupUpdate.type}") } } } - private fun isValid(closedGroupUpdate: SignalServiceProtos.ClosedGroupUpdateV2): Boolean { - when (closedGroupUpdate.type) { + private fun isValid(closedGroupUpdate: SignalServiceProtos.ClosedGroupUpdateV2, senderPublicKey: String): Boolean { + return when (closedGroupUpdate.type) { SignalServiceProtos.ClosedGroupUpdateV2.Type.NEW -> { - return !closedGroupUpdate.publicKey.isEmpty && !closedGroupUpdate.name.isNullOrEmpty() && !(closedGroupUpdate.encryptionKeyPair.privateKey ?: ByteString.copyFrom(ByteArray(0))).isEmpty - && !(closedGroupUpdate.encryptionKeyPair.publicKey ?: ByteString.copyFrom(ByteArray(0))).isEmpty && closedGroupUpdate.membersCount > 0 && closedGroupUpdate.adminsCount > 0 + (!closedGroupUpdate.publicKey.isEmpty && !closedGroupUpdate.name.isNullOrEmpty() && !(closedGroupUpdate.encryptionKeyPair.privateKey ?: ByteString.copyFrom(ByteArray(0))).isEmpty + && !(closedGroupUpdate.encryptionKeyPair.publicKey ?: ByteString.copyFrom(ByteArray(0))).isEmpty && closedGroupUpdate.membersCount > 0 && closedGroupUpdate.adminsCount > 0) } - SignalServiceProtos.ClosedGroupUpdateV2.Type.UPDATE -> { - return !closedGroupUpdate.name.isNullOrEmpty() + SignalServiceProtos.ClosedGroupUpdateV2.Type.MEMBERS_ADDED, + SignalServiceProtos.ClosedGroupUpdateV2.Type.MEMBERS_REMOVED -> { + closedGroupUpdate.membersCount > 0 } - SignalServiceProtos.ClosedGroupUpdateV2.Type.ENCRYPTION_KEY_PAIR -> return true - else -> return false + SignalServiceProtos.ClosedGroupUpdateV2.Type.MEMBER_LEFT -> { + senderPublicKey.isNotEmpty() + } + SignalServiceProtos.ClosedGroupUpdateV2.Type.UPDATE, + SignalServiceProtos.ClosedGroupUpdateV2.Type.NAME_CHANGE -> { + !closedGroupUpdate.name.isNullOrEmpty() + } + SignalServiceProtos.ClosedGroupUpdateV2.Type.ENCRYPTION_KEY_PAIR -> true + else -> false } } @@ -277,12 +442,144 @@ object ClosedGroupsProtocolV2 { val encryptionKeyPair = ECKeyPair(DjbECPublicKey(encryptionKeyPairAsProto.publicKey.toByteArray().removing05PrefixIfNeeded()), DjbECPrivateKey(encryptionKeyPairAsProto.privateKey.toByteArray())) apiDB.addClosedGroupEncryptionKeyPair(encryptionKeyPair, groupPublicKey) // Notify the user - insertIncomingInfoMessage(context, senderPublicKey, groupID, SignalServiceProtos.GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins) + insertIncomingInfoMessage(context, senderPublicKey, groupID, GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins) // Notify the PN server LokiPushNotificationManager.performOperation(context, ClosedGroupOperation.Subscribe, groupPublicKey, userPublicKey) } - public fun handleClosedGroupUpdate(context: Context, closedGroupUpdate: SignalServiceProtos.ClosedGroupUpdateV2, sentTimestamp: Long, groupPublicKey: String, senderPublicKey: String) { + fun handleClosedGroupMembersRemoved(context: Context, closedGroupUpdate: SignalServiceProtos.ClosedGroupUpdateV2, sentTimestamp: Long, groupPublicKey: String, senderPublicKey: String) { + val apiDB = DatabaseFactory.getLokiAPIDatabase(context) + val groupDB = DatabaseFactory.getGroupDatabase(context) + val groupID = doubleEncodeGroupID(groupPublicKey) + val group = groupDB.getGroup(groupID).orNull() + if (group == null) { + Log.d("Loki", "Ignoring closed group info message for nonexistent group.") + return + } + val userPublicKey = TextSecurePreferences.getLocalNumber(context)!! + val name = group.title + // Check common group update logic + val members = group.members.map { it.serialize() } + val admins = group.admins.map { it.toString() } + + // Users that are part of this remove update + val updateMembers = closedGroupUpdate.membersList.map { it.toByteArray().toHexString() } + + if (!isValidGroupUpdate(group, sentTimestamp, senderPublicKey)) { + return + } + // If admin leaves the group is disbanded + val didAdminLeave = admins.any { it in updateMembers } + // newMembers to save is old members minus removed members + val newMembers = members - updateMembers + // user should be posting MEMBERS_LEFT so this should not be encountered + val senderLeft = senderPublicKey in updateMembers + if (senderLeft) { + Log.d("Loki", "Received a MEMBERS_REMOVED instead of a MEMBERS_LEFT from sender $senderPublicKey") + } + val wasCurrentUserRemoved = userPublicKey in updateMembers + + // admin should send a MEMBERS_LEFT message but handled here in case + if (didAdminLeave || wasCurrentUserRemoved) { + disableLocalGroupAndUnsubscribe(context, apiDB, groupPublicKey, groupDB, groupID, userPublicKey) + } else { + val isCurrentUserAdmin = admins.contains(userPublicKey) + groupDB.updateMembers(groupID, newMembers.map { Address.fromSerialized(it) }) + if (isCurrentUserAdmin) { + generateAndSendNewEncryptionKeyPair(context, groupPublicKey, newMembers) + } + } + val (contextType, signalType) = + if (senderLeft) GroupContext.Type.QUIT to SignalServiceGroup.Type.QUIT + else GroupContext.Type.UPDATE to SignalServiceGroup.Type.UPDATE + + insertIncomingInfoMessage(context, senderPublicKey, groupID, contextType, signalType, name, members, admins) + } + + fun handleClosedGroupMembersAdded(context: Context, closedGroupUpdate: SignalServiceProtos.ClosedGroupUpdateV2, sentTimestamp: Long, groupPublicKey: String, senderPublicKey: String) { + val groupDB = DatabaseFactory.getGroupDatabase(context) + val groupID = doubleEncodeGroupID(groupPublicKey) + val group = groupDB.getGroup(groupID).orNull() + if (group == null) { + Log.d("Loki", "Ignoring closed group info message for nonexistent group.") + return + } + if (!isValidGroupUpdate(group, sentTimestamp, senderPublicKey)) { + return + } + val name = group.title + // Check common group update logic + val members = group.members.map { it.serialize() } + val admins = group.admins.map { it.serialize() } + + // Users that are part of this remove update + val updateMembers = closedGroupUpdate.membersList.map { it.toByteArray().toHexString() } + // newMembers to save is old members minus removed members + val newMembers = members + updateMembers + groupDB.updateMembers(groupID, newMembers.map { Address.fromSerialized(it) }) + + insertIncomingInfoMessage(context, senderPublicKey, groupID, GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins) + } + + fun handleClosedGroupNameChange(context: Context, closedGroupUpdate: SignalServiceProtos.ClosedGroupUpdateV2, sentTimestamp: Long, groupPublicKey: String, senderPublicKey: String) { + // Check that the sender is a member of the group (before the update) + val groupDB = DatabaseFactory.getGroupDatabase(context) + val groupID = doubleEncodeGroupID(groupPublicKey) + val group = groupDB.getGroup(groupID).orNull() + if (group == null) { + Log.d("Loki", "Ignoring closed group info message for nonexistent group.") + return + } + // Check common group update logic + if (!isValidGroupUpdate(group, sentTimestamp, senderPublicKey)) { + return + } + val members = group.members.map { it.serialize() } + val admins = group.admins.map { it.serialize() } + val name = closedGroupUpdate.name + groupDB.updateTitle(groupID, name) + + insertIncomingInfoMessage(context, senderPublicKey, groupID, GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins) + } + + private fun handleClosedGroupMemberLeft(context: Context, sentTimestamp: Long, groupPublicKey: String, senderPublicKey: String) { + // Check the user leaving isn't us, will already be handled + val userPublicKey = TextSecurePreferences.getLocalNumber(context)!! + if (senderPublicKey == userPublicKey) { + return + } + val apiDB = DatabaseFactory.getLokiAPIDatabase(context) + val groupDB = DatabaseFactory.getGroupDatabase(context) + val groupID = doubleEncodeGroupID(groupPublicKey) + val group = groupDB.getGroup(groupID).orNull() + if (group == null) { + Log.d("Loki", "Ignoring closed group info message for nonexistent group.") + return + } + val name = group.title + // Check common group update logic + val members = group.members.map { it.serialize() } + val admins = group.admins.map { it.toString() } + if (!isValidGroupUpdate(group, sentTimestamp, senderPublicKey)) { + return + } + // If admin leaves the group is disbanded + val didAdminLeave = admins.contains(senderPublicKey) + val updatedMemberList = members - senderPublicKey + + if (didAdminLeave) { + disableLocalGroupAndUnsubscribe(context, apiDB, groupPublicKey, groupDB, groupID, userPublicKey) + } else { + val isCurrentUserAdmin = admins.contains(userPublicKey) + groupDB.updateMembers(groupID, updatedMemberList.map { Address.fromSerialized(it) }) + if (isCurrentUserAdmin) { + generateAndSendNewEncryptionKeyPair(context, groupPublicKey, updatedMemberList) + } + } + insertIncomingInfoMessage(context, senderPublicKey, groupID, GroupContext.Type.QUIT, SignalServiceGroup.Type.QUIT, name, members, admins) + } + + private fun handleClosedGroupUpdate(context: Context, closedGroupUpdate: SignalServiceProtos.ClosedGroupUpdateV2, sentTimestamp: Long, groupPublicKey: String, senderPublicKey: String) { // Prepare val userPublicKey = TextSecurePreferences.getLocalNumber(context)!! val apiDB = DatabaseFactory.getLokiAPIDatabase(context) @@ -297,15 +594,8 @@ object ClosedGroupsProtocolV2 { return } val oldMembers = group.members.map { it.serialize() } - val newMembers = members.toSet().minus(oldMembers) - // Check that the message isn't from before the group was created - if (group.createAt > sentTimestamp) { - Log.d("Loki", "Ignoring closed group update from before thread was created.") - return - } - // Check that the sender is a member of the group (before the update) - if (!oldMembers.contains(senderPublicKey)) { - Log.d("Loki", "Ignoring closed group info message from non-member.") + // Check common group update logic + if (!isValidGroupUpdate(group, sentTimestamp, senderPublicKey)) { return } // Check that the admin wasn't removed unless the group was destroyed entirely @@ -316,20 +606,13 @@ object ClosedGroupsProtocolV2 { // Remove the group from the user's set of public keys to poll for if the current user was removed val wasCurrentUserRemoved = !members.contains(userPublicKey) if (wasCurrentUserRemoved) { - apiDB.removeClosedGroupPublicKey(groupPublicKey) - // Remove the key pairs - apiDB.removeAllClosedGroupEncryptionKeyPairs(groupPublicKey) - // Mark the group as inactive - groupDB.setActive(groupID, false) - groupDB.removeMember(groupID, Address.fromSerialized(userPublicKey)) - // Notify the PN server - LokiPushNotificationManager.performOperation(context, ClosedGroupOperation.Unsubscribe, groupPublicKey, userPublicKey) + disableLocalGroupAndUnsubscribe(context, apiDB, groupPublicKey, groupDB, groupID, userPublicKey) } // Generate and distribute a new encryption key pair if needed val wasAnyUserRemoved = (members.toSet().intersect(oldMembers) != oldMembers.toSet()) val isCurrentUserAdmin = group.admins.map { it.toString() }.contains(userPublicKey) if (wasAnyUserRemoved && isCurrentUserAdmin) { - generateAndSendNewEncryptionKeyPair(context, groupPublicKey, members.minus(newMembers)) + generateAndSendNewEncryptionKeyPair(context, groupPublicKey, members) } // Update the group groupDB.updateTitle(groupID, name) @@ -339,11 +622,39 @@ object ClosedGroupsProtocolV2 { } // Notify the user val wasSenderRemoved = !members.contains(senderPublicKey) - val type0 = if (wasSenderRemoved) SignalServiceProtos.GroupContext.Type.QUIT else SignalServiceProtos.GroupContext.Type.UPDATE + val type0 = if (wasSenderRemoved) GroupContext.Type.QUIT else GroupContext.Type.UPDATE val type1 = if (wasSenderRemoved) SignalServiceGroup.Type.QUIT else SignalServiceGroup.Type.UPDATE insertIncomingInfoMessage(context, senderPublicKey, groupID, type0, type1, name, members, group.admins.map { it.toString() }) } + private fun disableLocalGroupAndUnsubscribe(context: Context, apiDB: LokiAPIDatabase, groupPublicKey: String, groupDB: GroupDatabase, groupID: String, userPublicKey: String) { + apiDB.removeClosedGroupPublicKey(groupPublicKey) + // Remove the key pairs + apiDB.removeAllClosedGroupEncryptionKeyPairs(groupPublicKey) + // Mark the group as inactive + groupDB.setActive(groupID, false) + groupDB.removeMember(groupID, Address.fromSerialized(userPublicKey)) + // Notify the PN server + LokiPushNotificationManager.performOperation(context, ClosedGroupOperation.Unsubscribe, groupPublicKey, userPublicKey) + } + + private fun isValidGroupUpdate(group: GroupRecord, + sentTimestamp: Long, + senderPublicKey: String): Boolean { + val oldMembers = group.members.map { it.serialize() } + // Check that the message isn't from before the group was created + if (group.createdAt > sentTimestamp) { + Log.d("Loki", "Ignoring closed group update from before thread was created.") + return false + } + // Check that the sender is a member of the group (before the update) + if (senderPublicKey !in oldMembers) { + Log.d("Loki", "Ignoring closed group info message from non-member.") + return false + } + return true + } + private fun handleGroupEncryptionKeyPair(context: Context, closedGroupUpdate: SignalServiceProtos.ClosedGroupUpdateV2, groupPublicKey: String, senderPublicKey: String) { // Prepare val userPublicKey = TextSecurePreferences.getLocalNumber(context) @@ -373,9 +684,9 @@ object ClosedGroupsProtocolV2 { Log.d("Loki", "Received a new closed group encryption key pair") } - private fun insertIncomingInfoMessage(context: Context, senderPublicKey: String, groupID: String, type0: SignalServiceProtos.GroupContext.Type, type1: SignalServiceGroup.Type, + private fun insertIncomingInfoMessage(context: Context, senderPublicKey: String, groupID: String, type0: GroupContext.Type, type1: SignalServiceGroup.Type, name: String, members: Collection, admins: Collection) { - val groupContextBuilder = SignalServiceProtos.GroupContext.newBuilder() + val groupContextBuilder = GroupContext.newBuilder() .setId(ByteString.copyFrom(GroupUtil.getDecodedGroupIDAsData(groupID))) .setType(type0) .setName(name) @@ -388,10 +699,10 @@ object ClosedGroupsProtocolV2 { smsDB.insertMessageInbox(infoMessage) } - private fun insertOutgoingInfoMessage(context: Context, groupID: String, type: SignalServiceProtos.GroupContext.Type, name: String, + private fun insertOutgoingInfoMessage(context: Context, groupID: String, type: GroupContext.Type, name: String, members: Collection, admins: Collection, threadID: Long) { val recipient = Recipient.from(context, Address.fromSerialized(groupID), false) - val groupContextBuilder = SignalServiceProtos.GroupContext.newBuilder() + val groupContextBuilder = GroupContext.newBuilder() .setId(ByteString.copyFrom(GroupUtil.getDecodedGroupIDAsData(groupID))) .setType(type) .setName(name) diff --git a/libsession/src/main/java/org/session/libsession/messaging/threads/GroupRecord.kt b/libsession/src/main/java/org/session/libsession/messaging/threads/GroupRecord.kt index 2213fd85c1..28a64929f3 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/threads/GroupRecord.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/threads/GroupRecord.kt @@ -9,7 +9,7 @@ class GroupRecord( val encodedId: String, val title: String, members: String?, val avatar: ByteArray?, val avatarId: Long?, val avatarKey: ByteArray?, val avatarContentType: String?, val relay: String?, val isActive: Boolean, val avatarDigest: ByteArray?, val isMms: Boolean, - val url: String?, admins: String?, val createAt: Long + val url: String?, admins: String?, val createdAt: Long ) { var members: List
= LinkedList
() var admins: List
= LinkedList
() diff --git a/libsignal/protobuf/SignalService.proto b/libsignal/protobuf/SignalService.proto index 777cf834b8..050db80ae0 100644 --- a/libsignal/protobuf/SignalService.proto +++ b/libsignal/protobuf/SignalService.proto @@ -230,6 +230,10 @@ message ClosedGroupUpdateV2 { NEW = 1; // publicKey, name, encryptionKeyPair, members, admins UPDATE = 2; // name, members ENCRYPTION_KEY_PAIR = 3; // wrappers + NAME_CHANGE = 4; // name + MEMBERS_ADDED = 5; // members + MEMBERS_REMOVED = 6; // members + MEMBER_LEFT = 7; } message KeyPair { diff --git a/libsignal/src/main/java/org/session/libsignal/service/internal/push/SignalServiceProtos.java b/libsignal/src/main/java/org/session/libsignal/service/internal/push/SignalServiceProtos.java index a4acbeaffa..2cc6aa1ee3 100644 --- a/libsignal/src/main/java/org/session/libsignal/service/internal/push/SignalServiceProtos.java +++ b/libsignal/src/main/java/org/session/libsignal/service/internal/push/SignalServiceProtos.java @@ -24685,37 +24685,34 @@ public final class SignalServiceProtos { * */ ENCRYPTION_KEY_PAIR(2, 3), - /** - * NAME_CHANGE = 4; - * - *
-         * name
-         * 
- */ - NAME_CHANGE(3, 4), - /** - * MEMBERS_ADDED = 5; - * - *
-         * members
-         * 
- */ - MEMBERS_ADDED(4, 5), - /** - * MEMBERS_REMOVED = 6; - * - *
-         * members
-         * 
- */ - MEMBERS_REMOVED(5, 6), - /** - * MEMBER_LEFT = 7; - * - *
-         * 
- */ - MEMBER_LEFT(6, 7), + /** + * NAME_CHANGE = 4; + * + *
+       * name
+       * 
+ */ + NAME_CHANGE(3, 4), + /** + * MEMBERS_ADDED = 5; + * + *
+       * members
+       * 
+ */ + MEMBERS_ADDED(4, 5), + /** + * MEMBERS_REMOVED = 6; + * + *
+       * members
+       * 
+ */ + MEMBERS_REMOVED(5, 6), + /** + * MEMBER_LEFT = 7; + */ + MEMBER_LEFT(6, 7), ; /** @@ -24742,38 +24739,34 @@ public final class SignalServiceProtos { * */ public static final int ENCRYPTION_KEY_PAIR_VALUE = 3; - /** - * NAME_CHANGE = 4; - * - *
-         * name
-         * 
- */ - public static final int NAME_CHANGE_VALUE = 4; - /** - * MEMBERS_ADDED = 5; - * - *
-         * members
-         * 
- */ - public static final int MEMBERS_ADDED_VALUE = 5; - /** - * MEMBERS_REMOVED = 6; - * - *
-         * members
-         * 
- */ - public static final int MEMBERS_REMOVED_VALUE = 6; - /** - * MEMBER_LEFT = 7; - * - *
-         * 
- */ - public static final int MEMBER_LEFT_VALUE = 7; - + /** + * NAME_CHANGE = 4; + * + *
+       * name
+       * 
+ */ + public static final int NAME_CHANGE_VALUE = 4; + /** + * MEMBERS_ADDED = 5; + * + *
+       * members
+       * 
+ */ + public static final int MEMBERS_ADDED_VALUE = 5; + /** + * MEMBERS_REMOVED = 6; + * + *
+       * members
+       * 
+ */ + public static final int MEMBERS_REMOVED_VALUE = 6; + /** + * MEMBER_LEFT = 7; + */ + public static final int MEMBER_LEFT_VALUE = 7; public final int getNumber() { return value; } @@ -49481,7 +49474,7 @@ public final class SignalServiceProtos { "\022\026\n\022PROFILE_KEY_UPDATE\020\004\022\035\n\030DEVICE_UNLIN" + "KING_REQUEST\020\200\001\"A\n\017LokiUserProfile\022\023\n\013di" + "splayName\030\001 \001(\t\022\031\n\021profilePictureURL\030\002 \001", - "(\t\"\301\003\n\023ClosedGroupUpdateV2\0225\n\004type\030\001 \002(\016" + + "(\t\"\213\004\n\023ClosedGroupUpdateV2\0225\n\004type\030\001 \002(\016" + "2\'.signalservice.ClosedGroupUpdateV2.Typ" + "e\022\021\n\tpublicKey\030\002 \001(\014\022\014\n\004name\030\003 \001(\t\022E\n\021en" + "cryptionKeyPair\030\004 \001(\0132*.signalservice.Cl" + @@ -49491,98 +49484,100 @@ public final class SignalServiceProtos { "Wrapper\0320\n\007KeyPair\022\021\n\tpublicKey\030\001 \002(\014\022\022\n" + "\nprivateKey\030\002 \002(\014\032=\n\016KeyPairWrapper\022\021\n\tp" + "ublicKey\030\001 \002(\014\022\030\n\020encryptedKeyPair\030\002 \002(\014", - "\"4\n\004Type\022\007\n\003NEW\020\001\022\n\n\006UPDATE\020\002\022\027\n\023ENCRYPT" + - "ION_KEY_PAIR\020\003\"\357\002\n\021ClosedGroupUpdate\022\014\n\004" + - "name\030\001 \001(\t\022\026\n\016groupPublicKey\030\002 \001(\014\022\027\n\017gr" + - "oupPrivateKey\030\003 \001(\014\022>\n\nsenderKeys\030\004 \003(\0132" + - "*.signalservice.ClosedGroupUpdate.Sender" + - "Key\022\017\n\007members\030\005 \003(\014\022\016\n\006admins\030\006 \003(\014\0223\n\004" + - "type\030\007 \001(\0162%.signalservice.ClosedGroupUp" + - "date.Type\032B\n\tSenderKey\022\020\n\010chainKey\030\001 \001(\014" + - "\022\020\n\010keyIndex\030\002 \001(\r\022\021\n\tpublicKey\030\003 \001(\014\"A\n" + - "\004Type\022\007\n\003NEW\020\000\022\010\n\004INFO\020\001\022\026\n\022SENDER_KEY_R", - "EQUEST\020\002\022\016\n\nSENDER_KEY\020\003\"\036\n\013NullMessage\022" + - "\017\n\007padding\030\001 \001(\014\"u\n\016ReceiptMessage\0220\n\004ty" + - "pe\030\001 \001(\0162\".signalservice.ReceiptMessage." + - "Type\022\021\n\ttimestamp\030\002 \003(\004\"\036\n\004Type\022\014\n\010DELIV" + - "ERY\020\000\022\010\n\004READ\020\001\"\214\001\n\rTypingMessage\022\021\n\ttim" + - "estamp\030\001 \001(\004\0223\n\006action\030\002 \001(\0162#.signalser" + - "vice.TypingMessage.Action\022\017\n\007groupId\030\003 \001" + - "(\014\"\"\n\006Action\022\013\n\007STARTED\020\000\022\013\n\007STOPPED\020\001\"\253" + - "\001\n\010Verified\022\023\n\013destination\030\001 \001(\t\022\023\n\013iden" + - "tityKey\030\002 \001(\014\022,\n\005state\030\003 \001(\0162\035.signalser", - "vice.Verified.State\022\023\n\013nullMessage\030\004 \001(\014" + - "\"2\n\005State\022\013\n\007DEFAULT\020\000\022\014\n\010VERIFIED\020\001\022\016\n\n" + - "UNVERIFIED\020\002\"\325\014\n\013SyncMessage\022-\n\004sent\030\001 \001" + - "(\0132\037.signalservice.SyncMessage.Sent\0225\n\010c" + - "ontacts\030\002 \001(\0132#.signalservice.SyncMessag" + - "e.Contacts\0221\n\006groups\030\003 \001(\0132!.signalservi" + - "ce.SyncMessage.Groups\0223\n\007request\030\004 \001(\0132\"" + - ".signalservice.SyncMessage.Request\022-\n\004re" + - "ad\030\005 \003(\0132\037.signalservice.SyncMessage.Rea" + - "d\0223\n\007blocked\030\006 \001(\0132\".signalservice.SyncM", - "essage.Blocked\022)\n\010verified\030\007 \001(\0132\027.signa" + - "lservice.Verified\022?\n\rconfiguration\030\t \001(\013" + - "2(.signalservice.SyncMessage.Configurati" + - "on\022\017\n\007padding\030\010 \001(\014\022M\n\024stickerPackOperat" + - "ion\030\n \003(\0132/.signalservice.SyncMessage.St" + - "ickerPackOperation\022?\n\nopenGroups\030d \003(\0132+" + - ".signalservice.SyncMessage.OpenGroupDeta" + - "ils\032\236\002\n\004Sent\022\023\n\013destination\030\001 \001(\t\022\021\n\ttim" + - "estamp\030\002 \001(\004\022+\n\007message\030\003 \001(\0132\032.signalse" + - "rvice.DataMessage\022 \n\030expirationStartTime", - "stamp\030\004 \001(\004\022V\n\022unidentifiedStatus\030\005 \003(\0132" + - ":.signalservice.SyncMessage.Sent.Unident" + - "ifiedDeliveryStatus\032G\n\032UnidentifiedDeliv" + - "eryStatus\022\023\n\013destination\030\001 \001(\t\022\024\n\014uniden" + - "tified\030\002 \001(\010\032a\n\010Contacts\022.\n\004blob\030\001 \001(\0132 " + - ".signalservice.AttachmentPointer\022\027\n\010comp" + - "lete\030\002 \001(\010:\005false\022\014\n\004data\030e \001(\014\032F\n\006Group" + - "s\022.\n\004blob\030\001 \001(\0132 .signalservice.Attachme" + - "ntPointer\022\014\n\004data\030e \001(\014\032,\n\007Blocked\022\017\n\007nu" + - "mbers\030\001 \003(\t\022\020\n\010groupIds\030\002 \003(\014\032\217\001\n\007Reques", - "t\0225\n\004type\030\001 \001(\0162\'.signalservice.SyncMess" + - "age.Request.Type\"M\n\004Type\022\013\n\007UNKNOWN\020\000\022\014\n" + - "\010CONTACTS\020\001\022\n\n\006GROUPS\020\002\022\013\n\007BLOCKED\020\003\022\021\n\r" + - "CONFIGURATION\020\004\032)\n\004Read\022\016\n\006sender\030\001 \001(\t\022" + - "\021\n\ttimestamp\030\002 \001(\004\032}\n\rConfiguration\022\024\n\014r" + - "eadReceipts\030\001 \001(\010\022&\n\036unidentifiedDeliver" + - "yIndicators\030\002 \001(\010\022\030\n\020typingIndicators\030\003 " + - "\001(\010\022\024\n\014linkPreviews\030\004 \001(\010\032\234\001\n\024StickerPac" + - "kOperation\022\016\n\006packId\030\001 \001(\014\022\017\n\007packKey\030\002 " + - "\001(\014\022B\n\004type\030\003 \001(\01624.signalservice.SyncMe", - "ssage.StickerPackOperation.Type\"\037\n\004Type\022" + - "\013\n\007INSTALL\020\000\022\n\n\006REMOVE\020\001\0322\n\020OpenGroupDet" + - "ails\022\013\n\003url\030\001 \001(\t\022\021\n\tchannelID\030\002 \001(\r\"\354\001\n" + - "\021AttachmentPointer\022\n\n\002id\030\001 \001(\006\022\023\n\013conten" + - "tType\030\002 \001(\t\022\013\n\003key\030\003 \001(\014\022\014\n\004size\030\004 \001(\r\022\021" + - "\n\tthumbnail\030\005 \001(\014\022\016\n\006digest\030\006 \001(\014\022\020\n\010fil" + - "eName\030\007 \001(\t\022\r\n\005flags\030\010 \001(\r\022\r\n\005width\030\t \001(" + - "\r\022\016\n\006height\030\n \001(\r\022\017\n\007caption\030\013 \001(\t\022\013\n\003ur" + - "l\030e \001(\t\"\032\n\005Flags\022\021\n\rVOICE_MESSAGE\020\001\"\243\002\n\014" + - "GroupContext\022\n\n\002id\030\001 \001(\014\022.\n\004type\030\002 \001(\0162 ", - ".signalservice.GroupContext.Type\022\014\n\004name" + - "\030\003 \001(\t\022\017\n\007members\030\004 \003(\t\0220\n\006avatar\030\005 \001(\0132" + - " .signalservice.AttachmentPointer\022\016\n\006adm" + - "ins\030\006 \003(\t\022\023\n\nnewMembers\030\346\007 \003(\t\022\027\n\016remove" + - "dMembers\030\347\007 \003(\t\"H\n\004Type\022\013\n\007UNKNOWN\020\000\022\n\n\006" + - "UPDATE\020\001\022\013\n\007DELIVER\020\002\022\010\n\004QUIT\020\003\022\020\n\014REQUE" + - "ST_INFO\020\004\"\231\002\n\016ContactDetails\022\016\n\006number\030\001" + - " \001(\t\022\014\n\004name\030\002 \001(\t\0224\n\006avatar\030\003 \001(\0132$.sig" + - "nalservice.ContactDetails.Avatar\022\r\n\005colo" + - "r\030\004 \001(\t\022)\n\010verified\030\005 \001(\0132\027.signalservic", - "e.Verified\022\022\n\nprofileKey\030\006 \001(\014\022\017\n\007blocke" + - "d\030\007 \001(\010\022\023\n\013expireTimer\030\010 \001(\r\022\020\n\010nickname" + - "\030e \001(\t\032-\n\006Avatar\022\023\n\013contentType\030\001 \001(\t\022\016\n" + - "\006length\030\002 \001(\r\"\367\001\n\014GroupDetails\022\n\n\002id\030\001 \001" + - "(\014\022\014\n\004name\030\002 \001(\t\022\017\n\007members\030\003 \003(\t\0222\n\006ava" + - "tar\030\004 \001(\0132\".signalservice.GroupDetails.A" + - "vatar\022\024\n\006active\030\005 \001(\010:\004true\022\023\n\013expireTim" + - "er\030\006 \001(\r\022\r\n\005color\030\007 \001(\t\022\017\n\007blocked\030\010 \001(\010" + - "\022\016\n\006admins\030\t \003(\t\032-\n\006Avatar\022\023\n\013contentTyp" + - "e\030\001 \001(\t\022\016\n\006length\030\002 \001(\rBB\n+org.session.l", - "ibsignal.service.internal.pushB\023SignalSe" + - "rviceProtos" + "\"~\n\004Type\022\007\n\003NEW\020\001\022\n\n\006UPDATE\020\002\022\027\n\023ENCRYPT" + + "ION_KEY_PAIR\020\003\022\017\n\013NAME_CHANGE\020\004\022\021\n\rMEMBE" + + "RS_ADDED\020\005\022\023\n\017MEMBERS_REMOVED\020\006\022\017\n\013MEMBE" + + "R_LEFT\020\007\"\357\002\n\021ClosedGroupUpdate\022\014\n\004name\030\001" + + " \001(\t\022\026\n\016groupPublicKey\030\002 \001(\014\022\027\n\017groupPri" + + "vateKey\030\003 \001(\014\022>\n\nsenderKeys\030\004 \003(\0132*.sign" + + "alservice.ClosedGroupUpdate.SenderKey\022\017\n" + + "\007members\030\005 \003(\014\022\016\n\006admins\030\006 \003(\014\0223\n\004type\030\007" + + " \001(\0162%.signalservice.ClosedGroupUpdate.T" + + "ype\032B\n\tSenderKey\022\020\n\010chainKey\030\001 \001(\014\022\020\n\010ke", + "yIndex\030\002 \001(\r\022\021\n\tpublicKey\030\003 \001(\014\"A\n\004Type\022" + + "\007\n\003NEW\020\000\022\010\n\004INFO\020\001\022\026\n\022SENDER_KEY_REQUEST" + + "\020\002\022\016\n\nSENDER_KEY\020\003\"\036\n\013NullMessage\022\017\n\007pad" + + "ding\030\001 \001(\014\"u\n\016ReceiptMessage\0220\n\004type\030\001 \001" + + "(\0162\".signalservice.ReceiptMessage.Type\022\021" + + "\n\ttimestamp\030\002 \003(\004\"\036\n\004Type\022\014\n\010DELIVERY\020\000\022" + + "\010\n\004READ\020\001\"\214\001\n\rTypingMessage\022\021\n\ttimestamp" + + "\030\001 \001(\004\0223\n\006action\030\002 \001(\0162#.signalservice.T" + + "ypingMessage.Action\022\017\n\007groupId\030\003 \001(\014\"\"\n\006" + + "Action\022\013\n\007STARTED\020\000\022\013\n\007STOPPED\020\001\"\253\001\n\010Ver", + "ified\022\023\n\013destination\030\001 \001(\t\022\023\n\013identityKe" + + "y\030\002 \001(\014\022,\n\005state\030\003 \001(\0162\035.signalservice.V" + + "erified.State\022\023\n\013nullMessage\030\004 \001(\014\"2\n\005St" + + "ate\022\013\n\007DEFAULT\020\000\022\014\n\010VERIFIED\020\001\022\016\n\nUNVERI" + + "FIED\020\002\"\325\014\n\013SyncMessage\022-\n\004sent\030\001 \001(\0132\037.s" + + "ignalservice.SyncMessage.Sent\0225\n\010contact" + + "s\030\002 \001(\0132#.signalservice.SyncMessage.Cont" + + "acts\0221\n\006groups\030\003 \001(\0132!.signalservice.Syn" + + "cMessage.Groups\0223\n\007request\030\004 \001(\0132\".signa" + + "lservice.SyncMessage.Request\022-\n\004read\030\005 \003", + "(\0132\037.signalservice.SyncMessage.Read\0223\n\007b" + + "locked\030\006 \001(\0132\".signalservice.SyncMessage" + + ".Blocked\022)\n\010verified\030\007 \001(\0132\027.signalservi" + + "ce.Verified\022?\n\rconfiguration\030\t \001(\0132(.sig" + + "nalservice.SyncMessage.Configuration\022\017\n\007" + + "padding\030\010 \001(\014\022M\n\024stickerPackOperation\030\n " + + "\003(\0132/.signalservice.SyncMessage.StickerP" + + "ackOperation\022?\n\nopenGroups\030d \003(\0132+.signa" + + "lservice.SyncMessage.OpenGroupDetails\032\236\002" + + "\n\004Sent\022\023\n\013destination\030\001 \001(\t\022\021\n\ttimestamp", + "\030\002 \001(\004\022+\n\007message\030\003 \001(\0132\032.signalservice." + + "DataMessage\022 \n\030expirationStartTimestamp\030" + + "\004 \001(\004\022V\n\022unidentifiedStatus\030\005 \003(\0132:.sign" + + "alservice.SyncMessage.Sent.UnidentifiedD" + + "eliveryStatus\032G\n\032UnidentifiedDeliverySta" + + "tus\022\023\n\013destination\030\001 \001(\t\022\024\n\014unidentified" + + "\030\002 \001(\010\032a\n\010Contacts\022.\n\004blob\030\001 \001(\0132 .signa" + + "lservice.AttachmentPointer\022\027\n\010complete\030\002" + + " \001(\010:\005false\022\014\n\004data\030e \001(\014\032F\n\006Groups\022.\n\004b" + + "lob\030\001 \001(\0132 .signalservice.AttachmentPoin", + "ter\022\014\n\004data\030e \001(\014\032,\n\007Blocked\022\017\n\007numbers\030" + + "\001 \003(\t\022\020\n\010groupIds\030\002 \003(\014\032\217\001\n\007Request\0225\n\004t" + + "ype\030\001 \001(\0162\'.signalservice.SyncMessage.Re" + + "quest.Type\"M\n\004Type\022\013\n\007UNKNOWN\020\000\022\014\n\010CONTA" + + "CTS\020\001\022\n\n\006GROUPS\020\002\022\013\n\007BLOCKED\020\003\022\021\n\rCONFIG" + + "URATION\020\004\032)\n\004Read\022\016\n\006sender\030\001 \001(\t\022\021\n\ttim" + + "estamp\030\002 \001(\004\032}\n\rConfiguration\022\024\n\014readRec" + + "eipts\030\001 \001(\010\022&\n\036unidentifiedDeliveryIndic" + + "ators\030\002 \001(\010\022\030\n\020typingIndicators\030\003 \001(\010\022\024\n" + + "\014linkPreviews\030\004 \001(\010\032\234\001\n\024StickerPackOpera", + "tion\022\016\n\006packId\030\001 \001(\014\022\017\n\007packKey\030\002 \001(\014\022B\n" + + "\004type\030\003 \001(\01624.signalservice.SyncMessage." + + "StickerPackOperation.Type\"\037\n\004Type\022\013\n\007INS" + + "TALL\020\000\022\n\n\006REMOVE\020\001\0322\n\020OpenGroupDetails\022\013" + + "\n\003url\030\001 \001(\t\022\021\n\tchannelID\030\002 \001(\r\"\354\001\n\021Attac" + + "hmentPointer\022\n\n\002id\030\001 \001(\006\022\023\n\013contentType\030" + + "\002 \001(\t\022\013\n\003key\030\003 \001(\014\022\014\n\004size\030\004 \001(\r\022\021\n\tthum" + + "bnail\030\005 \001(\014\022\016\n\006digest\030\006 \001(\014\022\020\n\010fileName\030" + + "\007 \001(\t\022\r\n\005flags\030\010 \001(\r\022\r\n\005width\030\t \001(\r\022\016\n\006h" + + "eight\030\n \001(\r\022\017\n\007caption\030\013 \001(\t\022\013\n\003url\030e \001(", + "\t\"\032\n\005Flags\022\021\n\rVOICE_MESSAGE\020\001\"\243\002\n\014GroupC" + + "ontext\022\n\n\002id\030\001 \001(\014\022.\n\004type\030\002 \001(\0162 .signa" + + "lservice.GroupContext.Type\022\014\n\004name\030\003 \001(\t" + + "\022\017\n\007members\030\004 \003(\t\0220\n\006avatar\030\005 \001(\0132 .sign" + + "alservice.AttachmentPointer\022\016\n\006admins\030\006 " + + "\003(\t\022\023\n\nnewMembers\030\346\007 \003(\t\022\027\n\016removedMembe" + + "rs\030\347\007 \003(\t\"H\n\004Type\022\013\n\007UNKNOWN\020\000\022\n\n\006UPDATE" + + "\020\001\022\013\n\007DELIVER\020\002\022\010\n\004QUIT\020\003\022\020\n\014REQUEST_INF" + + "O\020\004\"\231\002\n\016ContactDetails\022\016\n\006number\030\001 \001(\t\022\014" + + "\n\004name\030\002 \001(\t\0224\n\006avatar\030\003 \001(\0132$.signalser", + "vice.ContactDetails.Avatar\022\r\n\005color\030\004 \001(" + + "\t\022)\n\010verified\030\005 \001(\0132\027.signalservice.Veri" + + "fied\022\022\n\nprofileKey\030\006 \001(\014\022\017\n\007blocked\030\007 \001(" + + "\010\022\023\n\013expireTimer\030\010 \001(\r\022\020\n\010nickname\030e \001(\t" + + "\032-\n\006Avatar\022\023\n\013contentType\030\001 \001(\t\022\016\n\006lengt" + + "h\030\002 \001(\r\"\367\001\n\014GroupDetails\022\n\n\002id\030\001 \001(\014\022\014\n\004" + + "name\030\002 \001(\t\022\017\n\007members\030\003 \003(\t\0222\n\006avatar\030\004 " + + "\001(\0132\".signalservice.GroupDetails.Avatar\022" + + "\024\n\006active\030\005 \001(\010:\004true\022\023\n\013expireTimer\030\006 \001" + + "(\r\022\r\n\005color\030\007 \001(\t\022\017\n\007blocked\030\010 \001(\010\022\016\n\006ad", + "mins\030\t \003(\t\032-\n\006Avatar\022\023\n\013contentType\030\001 \001(" + + "\t\022\016\n\006length\030\002 \001(\rBB\n+org.session.libsign" + + "al.service.internal.pushB\023SignalServiceP" + + "rotos" }; com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner assigner = new com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner() {