sending & receiving $ make things compile

This commit is contained in:
Ryan ZHAO 2020-12-10 15:33:57 +11:00
parent 072aa0e7c6
commit c0dff9cdea
8 changed files with 143 additions and 124 deletions

View File

@ -5,11 +5,10 @@ import nl.komponents.kovenant.Promise
import nl.komponents.kovenant.deferred import nl.komponents.kovenant.deferred
import nl.komponents.kovenant.functional.map import nl.komponents.kovenant.functional.map
import nl.komponents.kovenant.then import nl.komponents.kovenant.then
import org.session.libsession.messaging.Configuration import org.session.libsession.messaging.MessagingConfiguration
import org.session.libsession.messaging.utilities.DotNetAPI import org.session.libsession.messaging.utilities.DotNetAPI
import org.session.libsession.messaging.fileserver.FileServerAPI import org.session.libsession.messaging.fileserver.FileServerAPI
import org.session.libsession.snode.SnodeAPI
import org.session.libsignal.libsignal.logging.Log import org.session.libsignal.libsignal.logging.Log
import org.session.libsignal.service.internal.util.Base64 import org.session.libsignal.service.internal.util.Base64
@ -56,7 +55,7 @@ object OpenGroupAPI: DotNetAPI() {
// region Public API // region Public API
public fun getMessages(channel: Long, server: String): Promise<List<OpenGroupMessage>, Exception> { public fun getMessages(channel: Long, server: String): Promise<List<OpenGroupMessage>, Exception> {
Log.d("Loki", "Getting messages for open group with ID: $channel on server: $server.") Log.d("Loki", "Getting messages for open group with ID: $channel on server: $server.")
val storage = Configuration.shared.storage val storage = MessagingConfiguration.shared.storage
val parameters = mutableMapOf<String, Any>( "include_annotations" to 1 ) val parameters = mutableMapOf<String, Any>( "include_annotations" to 1 )
val lastMessageServerID = storage.getLastMessageServerID(channel, server) val lastMessageServerID = storage.getLastMessageServerID(channel, server)
if (lastMessageServerID != null) { if (lastMessageServerID != null) {
@ -161,7 +160,7 @@ object OpenGroupAPI: DotNetAPI() {
public fun getDeletedMessageServerIDs(channel: Long, server: String): Promise<List<Long>, Exception> { public fun getDeletedMessageServerIDs(channel: Long, server: String): Promise<List<Long>, Exception> {
Log.d("Loki", "Getting deleted messages for open group with ID: $channel on server: $server.") Log.d("Loki", "Getting deleted messages for open group with ID: $channel on server: $server.")
val storage = Configuration.shared.storage val storage = MessagingConfiguration.shared.storage
val parameters = mutableMapOf<String, Any>() val parameters = mutableMapOf<String, Any>()
val lastDeletionServerID = storage.getLastDeletionServerID(channel, server) val lastDeletionServerID = storage.getLastDeletionServerID(channel, server)
if (lastDeletionServerID != null) { if (lastDeletionServerID != null) {
@ -193,7 +192,7 @@ object OpenGroupAPI: DotNetAPI() {
public fun sendMessage(message: OpenGroupMessage, channel: Long, server: String): Promise<OpenGroupMessage, Exception> { public fun sendMessage(message: OpenGroupMessage, channel: Long, server: String): Promise<OpenGroupMessage, Exception> {
val deferred = deferred<OpenGroupMessage, Exception>() val deferred = deferred<OpenGroupMessage, Exception>()
val storage = Configuration.shared.storage val storage = MessagingConfiguration.shared.storage
val userKeyPair = storage.getUserKeyPair() ?: throw Error.Generic val userKeyPair = storage.getUserKeyPair() ?: throw Error.Generic
val userDisplayName = storage.getUserDisplayName() ?: throw Error.Generic val userDisplayName = storage.getUserDisplayName() ?: throw Error.Generic
Thread { Thread {
@ -287,7 +286,7 @@ object OpenGroupAPI: DotNetAPI() {
val memberCount = countInfo["subscribers"] as? Int ?: (countInfo["subscribers"] as? Long)?.toInt() ?: (countInfo["subscribers"] as String).toInt() val memberCount = countInfo["subscribers"] as? Int ?: (countInfo["subscribers"] as? Long)?.toInt() ?: (countInfo["subscribers"] as String).toInt()
val profilePictureURL = info["avatar"] as String val profilePictureURL = info["avatar"] as String
val publicChatInfo = OpenGroupInfo(displayName, profilePictureURL, memberCount) val publicChatInfo = OpenGroupInfo(displayName, profilePictureURL, memberCount)
Configuration.shared.storage.setUserCount(channel, server, memberCount) MessagingConfiguration.shared.storage.setUserCount(channel, server, memberCount)
publicChatInfo publicChatInfo
} catch (exception: Exception) { } catch (exception: Exception) {
Log.d("Loki", "Couldn't parse info for open group with ID: $channel on server: $server.") Log.d("Loki", "Couldn't parse info for open group with ID: $channel on server: $server.")
@ -298,7 +297,7 @@ object OpenGroupAPI: DotNetAPI() {
} }
public fun updateProfileIfNeeded(channel: Long, server: String, groupID: String, info: OpenGroupInfo, isForcedUpdate: Boolean) { public fun updateProfileIfNeeded(channel: Long, server: String, groupID: String, info: OpenGroupInfo, isForcedUpdate: Boolean) {
val storage = Configuration.shared.storage val storage = MessagingConfiguration.shared.storage
storage.setUserCount(channel, server, info.memberCount) storage.setUserCount(channel, server, info.memberCount)
storage.updateTitle(groupID, info.displayName) storage.updateTitle(groupID, info.displayName)
// Download and update profile picture if needed // Download and update profile picture if needed

View File

@ -1,6 +1,6 @@
package org.session.libsession.messaging.sending_receiving package org.session.libsession.messaging.sending_receiving
import org.session.libsession.messaging.Configuration import org.session.libsession.messaging.MessagingConfiguration
import org.session.libsession.messaging.sending_receiving.MessageReceiver.Error import org.session.libsession.messaging.sending_receiving.MessageReceiver.Error
import org.session.libsession.utilities.AESGCM import org.session.libsession.utilities.AESGCM
@ -21,26 +21,26 @@ import javax.crypto.spec.SecretKeySpec
object MessageReceiverDecryption { object MessageReceiverDecryption {
internal fun decryptWithSignalProtocol(envelope: SignalServiceProtos.Envelope): Pair<ByteArray, String> { internal fun decryptWithSignalProtocol(envelope: SignalServiceProtos.Envelope): Pair<ByteArray, String> {
val storage = Configuration.shared.signalStorage val storage = MessagingConfiguration.shared.signalStorage
val sskDatabase = Configuration.shared.sskDatabase val sskDatabase = MessagingConfiguration.shared.sskDatabase
val sessionResetImp = Configuration.shared.sessionResetImp val sessionResetImp = MessagingConfiguration.shared.sessionResetImp
val certificateValidator = Configuration.shared.certificateValidator val certificateValidator = MessagingConfiguration.shared.certificateValidator
val data = envelope.content val data = envelope.content
if (data.count() == 0) { throw Error.NoData } if (data.count() == 0) { throw Error.NoData }
val userPublicKey = Configuration.shared.storage.getUserPublicKey() ?: throw Error.NoUserPublicKey val userPublicKey = MessagingConfiguration.shared.storage.getUserPublicKey() ?: throw Error.NoUserPublicKey
val localAddress = SignalServiceAddress(userPublicKey) val localAddress = SignalServiceAddress(userPublicKey)
val cipher = SignalServiceCipher(localAddress, storage, sskDatabase, sessionResetImp, certificateValidator) val cipher = SignalServiceCipher(localAddress, storage, sskDatabase, sessionResetImp, certificateValidator)
val result = cipher.decrypt(SignalServiceEnvelope(envelope)) val result = cipher.decrypt(SignalServiceEnvelope(envelope))
return Pair(result, result.sender) return Pair(ByteArray(1), result.sender) // TODO: Return real plaintext
} }
internal fun decryptWithSharedSenderKeys(envelope: SignalServiceProtos.Envelope): Pair<ByteArray, String> { internal fun decryptWithSharedSenderKeys(envelope: SignalServiceProtos.Envelope): Pair<ByteArray, String> {
// 1. ) Check preconditions // 1. ) Check preconditions
val groupPublicKey = envelope.source val groupPublicKey = envelope.source
if (!Configuration.shared.storage.isClosedGroup(groupPublicKey)) { throw Error.InvalidGroupPublicKey } if (!MessagingConfiguration.shared.storage.isClosedGroup(groupPublicKey)) { throw Error.InvalidGroupPublicKey }
val data = envelope.content val data = envelope.content
if (data.count() == 0) { throw Error.NoData } if (data.count() == 0) { throw Error.NoData }
val groupPrivateKey = Configuration.shared.storage.getClosedGroupPrivateKey(groupPublicKey) ?: throw Error.NoGroupPrivateKey val groupPrivateKey = MessagingConfiguration.shared.storage.getClosedGroupPrivateKey(groupPublicKey) ?: throw Error.NoGroupPrivateKey
// 2. ) Parse the wrapper // 2. ) Parse the wrapper
val wrapper = SignalServiceProtos.ClosedGroupCiphertextMessageWrapper.parseFrom(data) val wrapper = SignalServiceProtos.ClosedGroupCiphertextMessageWrapper.parseFrom(data)
val ivAndCiphertext = wrapper.ciphertext.toByteArray() val ivAndCiphertext = wrapper.ciphertext.toByteArray()
@ -54,7 +54,7 @@ object MessageReceiverDecryption {
// 4. ) Parse the closed group ciphertext message // 4. ) Parse the closed group ciphertext message
val closedGroupCiphertextMessage = ClosedGroupCiphertextMessage.from(closedGroupCiphertextMessageAsData) ?: throw Error.ParsingFailed val closedGroupCiphertextMessage = ClosedGroupCiphertextMessage.from(closedGroupCiphertextMessageAsData) ?: throw Error.ParsingFailed
val senderPublicKey = closedGroupCiphertextMessage.senderPublicKey.toHexString() val senderPublicKey = closedGroupCiphertextMessage.senderPublicKey.toHexString()
if (senderPublicKey == Configuration.shared.storage.getUserPublicKey()) { throw Error.SelfSend } if (senderPublicKey == MessagingConfiguration.shared.storage.getUserPublicKey()) { throw Error.SelfSend }
// 5. ) Use the info inside the closed group ciphertext message to decrypt the actual message content // 5. ) Use the info inside the closed group ciphertext message to decrypt the actual message content
val plaintext = SharedSenderKeysImplementation.shared.decrypt(closedGroupCiphertextMessage.ivAndCiphertext, groupPublicKey, senderPublicKey, closedGroupCiphertextMessage.keyIndex) val plaintext = SharedSenderKeysImplementation.shared.decrypt(closedGroupCiphertextMessage.ivAndCiphertext, groupPublicKey, senderPublicKey, closedGroupCiphertextMessage.keyIndex)
// 6. ) Return // 6. ) Return

View File

@ -1,6 +1,6 @@
package org.session.libsession.messaging.sending_receiving package org.session.libsession.messaging.sending_receiving
import org.session.libsession.messaging.Configuration import org.session.libsession.messaging.MessagingConfiguration
import org.session.libsession.messaging.messages.Destination import org.session.libsession.messaging.messages.Destination
import org.session.libsession.messaging.messages.Message import org.session.libsession.messaging.messages.Message
import org.session.libsession.messaging.messages.control.ClosedGroupUpdate import org.session.libsession.messaging.messages.control.ClosedGroupUpdate
@ -9,9 +9,9 @@ import org.session.libsession.messaging.messages.control.ReadReceipt
import org.session.libsession.messaging.messages.control.TypingIndicator import org.session.libsession.messaging.messages.control.TypingIndicator
import org.session.libsession.messaging.messages.visible.VisibleMessage import org.session.libsession.messaging.messages.visible.VisibleMessage
import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI
import org.session.libsession.utilities.LKGroupUtilities import org.session.libsession.messaging.threads.Address
import org.session.libsession.utilities.GroupUtil
import org.session.libsignal.libsignal.util.Hex import org.session.libsignal.libsignal.util.Hex
import org.session.libsignal.service.api.messages.SignalServiceGroup
import org.session.libsignal.service.internal.push.SignalServiceProtos import org.session.libsignal.service.internal.push.SignalServiceProtos
import org.session.libsignal.service.loki.protocol.closedgroups.ClosedGroupRatchet import org.session.libsignal.service.loki.protocol.closedgroups.ClosedGroupRatchet
@ -37,7 +37,7 @@ fun MessageReceiver.handle(message: Message, proto: SignalServiceProtos.Content,
} }
private fun MessageReceiver.handleReadReceipt(message: ReadReceipt) { private fun MessageReceiver.handleReadReceipt(message: ReadReceipt) {
// TODO
} }
private fun MessageReceiver.handleTypingIndicator(message: TypingIndicator) { private fun MessageReceiver.handleTypingIndicator(message: TypingIndicator) {
@ -89,8 +89,8 @@ private fun MessageReceiver.handleClosedGroupUpdate(message: ClosedGroupUpdate)
} }
private fun MessageReceiver.handleNewGroup(message: ClosedGroupUpdate) { private fun MessageReceiver.handleNewGroup(message: ClosedGroupUpdate) {
val storage = Configuration.shared.storage val storage = MessagingConfiguration.shared.storage
val sskDatabase = Configuration.shared.sskDatabase val sskDatabase = MessagingConfiguration.shared.sskDatabase
val kind = message.kind!! as ClosedGroupUpdate.Kind.New val kind = message.kind!! as ClosedGroupUpdate.Kind.New
val groupPublicKey = kind.groupPublicKey.toHexString() val groupPublicKey = kind.groupPublicKey.toHexString()
val name = kind.name val name = kind.name
@ -122,27 +122,24 @@ private fun MessageReceiver.handleNewGroup(message: ClosedGroupUpdate) {
MessageSender.requestSenderKey(groupPublicKey, publicKey) MessageSender.requestSenderKey(groupPublicKey, publicKey)
} }
// Create the group // Create the group
val groupID = LKGroupUtilities.getEncodedClosedGroupIDAsData(groupPublicKey) val groupID = GroupUtil.getEncodedClosedGroupID(groupPublicKey)
val groupDB = DatabaseFactory.getGroupDatabase(context) if (storage.getGroup(groupID) != null) {
if (groupDB.getGroup(groupID).orNull() != null) {
// Update the group // Update the group
groupDB.updateTitle(groupID, name) storage.updateTitle(groupID, name)
groupDB.updateMembers(groupID, members.map { Address.fromSerialized(it) }) storage.updateMembers(groupID, members.map { Address.fromSerialized(it) })
} else { } else {
groupDB.create(groupID, name, LinkedList<Address>(members.map { Address.fromSerialized(it) }), storage.createGroup(groupID, name, LinkedList(members.map { Address.fromSerialized(it) }),
null, null, LinkedList<Address>(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) storage.setProfileSharing(Address.fromSerialized(groupID), true)
// Add the group to the user's set of public keys to poll for // Add the group to the user's set of public keys to poll for
sskDatabase.setClosedGroupPrivateKey(groupPublicKey, groupPrivateKey.toHexString()) sskDatabase.setClosedGroupPrivateKey(groupPublicKey, groupPrivateKey.toHexString())
// Notify the PN server // Notify the PN server
PushNotificationAPI.performOperation(context, ClosedGroupOperation.Subscribe, groupPublicKey, userPublicKey) PushNotificationAPI.performOperation(PushNotificationAPI.ClosedGroupOperation.Subscribe, groupPublicKey, userPublicKey)
// Notify the user // Notify the user
/* TODO
insertIncomingInfoMessage(context, senderPublicKey, groupID, SignalServiceProtos.GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins) insertIncomingInfoMessage(context, senderPublicKey, groupID, SignalServiceProtos.GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins)
// Establish sessions if needed */
establishSessionsWithMembersIfNeeded(context, members)
} }
private fun MessageReceiver.handleGroupUpdate(message: ClosedGroupUpdate) { private fun MessageReceiver.handleGroupUpdate(message: ClosedGroupUpdate) {

View File

@ -1,10 +1,9 @@
package org.session.libsession.messaging.sending_receiving package org.session.libsession.messaging.sending_receiving
import com.google.protobuf.MessageOrBuilder
import nl.komponents.kovenant.Promise import nl.komponents.kovenant.Promise
import nl.komponents.kovenant.deferred import nl.komponents.kovenant.deferred
import org.session.libsession.messaging.Configuration import org.session.libsession.messaging.MessagingConfiguration
import org.session.libsession.messaging.jobs.JobQueue import org.session.libsession.messaging.jobs.JobQueue
import org.session.libsession.messaging.messages.Destination import org.session.libsession.messaging.messages.Destination
import org.session.libsession.messaging.messages.Message import org.session.libsession.messaging.messages.Message
@ -64,7 +63,7 @@ object MessageSender {
fun sendToSnodeDestination(destination: Destination, message: Message): Promise<Unit, Exception> { fun sendToSnodeDestination(destination: Destination, message: Message): Promise<Unit, Exception> {
val deferred = deferred<Unit, Exception>() val deferred = deferred<Unit, Exception>()
val promise = deferred.promise val promise = deferred.promise
val storage = Configuration.shared.storage val storage = MessagingConfiguration.shared.storage
val preconditionFailure = Exception("Destination should not be open groups!") val preconditionFailure = Exception("Destination should not be open groups!")
var snodeMessage: SnodeMessage? = null var snodeMessage: SnodeMessage? = null
message.sentTimestamp ?: run { message.sentTimestamp = System.currentTimeMillis() } /* Visible messages will already have their sent timestamp set */ message.sentTimestamp ?: run { message.sentTimestamp = System.currentTimeMillis() } /* Visible messages will already have their sent timestamp set */
@ -152,7 +151,7 @@ object MessageSender {
fun sendToOpenGroupDestination(destination: Destination, message: Message): Promise<Unit, Exception> { fun sendToOpenGroupDestination(destination: Destination, message: Message): Promise<Unit, Exception> {
val deferred = deferred<Unit, Exception>() val deferred = deferred<Unit, Exception>()
val promise = deferred.promise val promise = deferred.promise
val storage = Configuration.shared.storage val storage = MessagingConfiguration.shared.storage
val preconditionFailure = Exception("Destination should not be contacts or closed groups!") val preconditionFailure = Exception("Destination should not be contacts or closed groups!")
message.sentTimestamp = System.currentTimeMillis() message.sentTimestamp = System.currentTimeMillis()
message.sender = storage.getUserPublicKey() message.sender = storage.getUserPublicKey()

View File

@ -2,16 +2,17 @@
package org.session.libsession.messaging.sending_receiving package org.session.libsession.messaging.sending_receiving
import android.content.Context
import android.util.Log import android.util.Log
import nl.komponents.kovenant.Promise import nl.komponents.kovenant.Promise
import nl.komponents.kovenant.deferred import nl.komponents.kovenant.deferred
import org.session.libsession.messaging.Configuration import org.session.libsession.messaging.MessagingConfiguration
import org.session.libsession.messaging.messages.Destination
import org.session.libsession.messaging.messages.control.ClosedGroupUpdate import org.session.libsession.messaging.messages.control.ClosedGroupUpdate
import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI
import org.session.libsession.utilities.LKGroupUtilities import org.session.libsession.messaging.sending_receiving.MessageSender.Error
import org.session.libsession.messaging.threads.Address
import org.session.libsession.utilities.GroupUtil
import org.session.libsignal.libsignal.ecc.Curve import org.session.libsignal.libsignal.ecc.Curve
import org.session.libsignal.libsignal.util.Hex import org.session.libsignal.libsignal.util.Hex
@ -26,8 +27,9 @@ import java.util.*
fun MessageSender.createClosedGroup(name: String, members: Collection<String>): Promise<String, Exception> { fun MessageSender.createClosedGroup(name: String, members: Collection<String>): Promise<String, Exception> {
val deferred = deferred<String, Exception>() val deferred = deferred<String, Exception>()
// Prepare // Prepare
val storage = MessagingConfiguration.shared.storage
val members = members val members = members
val userPublicKey = Configuration.shared.storage.getUserPublicKey()!! val userPublicKey = storage.getUserPublicKey()!!
// Generate a key pair for the group // Generate a key pair for the group
val groupKeyPair = Curve.generateKeyPair() val groupKeyPair = Curve.generateKeyPair()
val groupPublicKey = groupKeyPair.hexEncodedPublicKey // Includes the "05" prefix val groupPublicKey = groupKeyPair.hexEncodedPublicKey // Includes the "05" prefix
@ -41,12 +43,9 @@ fun MessageSender.createClosedGroup(name: String, members: Collection<String>):
// Create the group // Create the group
val admins = setOf( userPublicKey ) val admins = setOf( userPublicKey )
val adminsAsData = admins.map { Hex.fromStringCondensed(it) } val adminsAsData = admins.map { Hex.fromStringCondensed(it) }
val groupID = LKGroupUtilities.getEncodedClosedGroupIDAsData(groupPublicKey) val groupID = GroupUtil.getEncodedClosedGroupID(groupPublicKey)
/* TODO: storage.createGroup(groupID, name, LinkedList(members.map { Address.fromSerialized(it) }), null, null, LinkedList(admins.map { Address.fromSerialized(it) }))
DatabaseFactory.getGroupDatabase(context).create(groupID, name, LinkedList<Address>(members.map { Address.fromSerialized(it) }), storage.setProfileSharing(Address.fromSerialized(groupID), true)
null, null, LinkedList<Address>(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 using established channels // Send a closed group update message to all members using established channels
val promises = mutableListOf<Promise<Unit, Exception>>() val promises = mutableListOf<Promise<Unit, Exception>>()
for (member in members) { for (member in members) {
@ -55,16 +54,17 @@ fun MessageSender.createClosedGroup(name: String, members: Collection<String>):
senderKeys, membersAsData, adminsAsData) senderKeys, membersAsData, adminsAsData)
val closedGroupUpdate = ClosedGroupUpdate() val closedGroupUpdate = ClosedGroupUpdate()
closedGroupUpdate.kind = closedGroupUpdateKind closedGroupUpdate.kind = closedGroupUpdateKind
val promise = MessageSender.sendNonDurably(closedGroupUpdate, threadID) val address = Address.fromSerialized(member)
val promise = MessageSender.sendNonDurably(closedGroupUpdate, address)
promises.add(promise) promises.add(promise)
} }
// Add the group to the user's set of public keys to poll for // Add the group to the user's set of public keys to poll for
Configuration.shared.sskDatabase.setClosedGroupPrivateKey(groupPublicKey, groupKeyPair.hexEncodedPrivateKey) MessagingConfiguration.shared.sskDatabase.setClosedGroupPrivateKey(groupPublicKey, groupKeyPair.hexEncodedPrivateKey)
// Notify the PN server // Notify the PN server
PushNotificationAPI.performOperation(PushNotificationAPI.ClosedGroupOperation.Subscribe, groupPublicKey, userPublicKey) PushNotificationAPI.performOperation(PushNotificationAPI.ClosedGroupOperation.Subscribe, groupPublicKey, userPublicKey)
// Notify the user // Notify the user
val threadID =storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
/* TODO /* TODO
val threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false))
insertOutgoingInfoMessage(context, groupID, GroupContext.Type.UPDATE, name, members, admins, threadID) insertOutgoingInfoMessage(context, groupID, GroupContext.Type.UPDATE, name, members, admins, threadID)
*/ */
// Fulfill the promise // Fulfill the promise
@ -75,46 +75,47 @@ fun MessageSender.createClosedGroup(name: String, members: Collection<String>):
fun MessageSender.update(groupPublicKey: String, members: Collection<String>, name: String): Promise<Unit, Exception> { fun MessageSender.update(groupPublicKey: String, members: Collection<String>, name: String): Promise<Unit, Exception> {
val deferred = deferred<Unit, Exception>() val deferred = deferred<Unit, Exception>()
val userPublicKey = Configuration.shared.storage.getUserPublicKey()!! val storage = MessagingConfiguration.shared.storage
val sskDatabase = Configuration.shared.sskDatabase val userPublicKey = storage.getUserPublicKey()!!
val groupDB = DatabaseFactory.getGroupDatabase(context) val sskDatabase = MessagingConfiguration.shared.sskDatabase
val groupID = LKGroupUtilities.getEncodedClosedGroupIDAsData(groupPublicKey) val groupID = GroupUtil.getEncodedClosedGroupID(groupPublicKey)
val group = groupDB.getGroup(groupID).orNull() val group = storage.getGroup(groupID)
if (group == null) { if (group == null) {
Log.d("Loki", "Can't update nonexistent closed group.") Log.d("Loki", "Can't update nonexistent closed group.")
return deferred.reject(Error.NoThread) deferred.reject(Error.NoThread)
return deferred.promise
} }
val oldMembers = group.members.map { it.serialize() }.toSet() val oldMembers = group.members.map { it.serialize() }.toSet()
val newMembers = members.minus(oldMembers) val newMembers = members.minus(oldMembers)
val membersAsData = members.map { Hex.fromStringCondensed(it) } val membersAsData = members.map { Hex.fromStringCondensed(it) }
val admins = group.admins.map { it.serialize() } val admins = group.admins.map { it.serialize() }
val adminsAsData = admins.map { Hex.fromStringCondensed(it) } val adminsAsData = admins.map { Hex.fromStringCondensed(it) }
val groupPrivateKey = DatabaseFactory.getSSKDatabase(context).getClosedGroupPrivateKey(groupPublicKey) val groupPrivateKey = sskDatabase.getClosedGroupPrivateKey(groupPublicKey)
if (groupPrivateKey == null) { if (groupPrivateKey == null) {
Log.d("Loki", "Couldn't get private key for closed group.") Log.d("Loki", "Couldn't get private key for closed group.")
return@Thread deferred.reject(Error.NoPrivateKey) deferred.reject(Error.NoPrivateKey)
return deferred.promise
} }
val wasAnyUserRemoved = members.toSet().intersect(oldMembers) != oldMembers.toSet() val wasAnyUserRemoved = members.toSet().intersect(oldMembers) != oldMembers.toSet()
val removedMembers = oldMembers.minus(members) val removedMembers = oldMembers.minus(members)
val isUserLeaving = removedMembers.contains(userPublicKey) val isUserLeaving = removedMembers.contains(userPublicKey)
var newSenderKeys = listOf<ClosedGroupSenderKey>() val newSenderKeys: List<ClosedGroupSenderKey>
if (wasAnyUserRemoved) { if (wasAnyUserRemoved) {
if (isUserLeaving && removedMembers.count() != 1) { if (isUserLeaving && removedMembers.count() != 1) {
Log.d("Loki", "Can't remove self and others simultaneously.") Log.d("Loki", "Can't remove self and others simultaneously.")
return@Thread deferred.reject(Error.InvalidUpdate) deferred.reject(Error.InvalidClosedGroupUpdate)
return deferred.promise
} }
// Establish sessions if needed
establishSessionsWithMembersIfNeeded(context, members)
// Send the update to the existing members using established channels (don't include new ratchets as everyone should regenerate new ratchets individually) // Send the update to the existing members using established channels (don't include new ratchets as everyone should regenerate new ratchets individually)
for (member in oldMembers) { val promises = oldMembers.map { member ->
@Suppress("NAME_SHADOWING") val closedGroupUpdateKind = ClosedGroupUpdate.Kind.Info(Hex.fromStringCondensed(groupPublicKey),
val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.Info(Hex.fromStringCondensed(groupPublicKey),
name, setOf(), membersAsData, adminsAsData) name, setOf(), membersAsData, adminsAsData)
@Suppress("NAME_SHADOWING") val closedGroupUpdate = ClosedGroupUpdate()
val job = ClosedGroupUpdateMessageSendJob(member, closedGroupUpdateKind) closedGroupUpdate.kind = closedGroupUpdateKind
job.setContext(context) val address = Address.fromSerialized(member)
job.onRun() // Run the job immediately MessageSender.sendNonDurably(closedGroupUpdate, address).get()
} }
val allOldRatchets = sskDatabase.getAllClosedGroupRatchets(groupPublicKey, ClosedGroupRatchetCollectionType.Current) val allOldRatchets = sskDatabase.getAllClosedGroupRatchets(groupPublicKey, ClosedGroupRatchetCollectionType.Current)
for (pair in allOldRatchets) { for (pair in allOldRatchets) {
val senderPublicKey = pair.first val senderPublicKey = pair.first
@ -128,30 +129,30 @@ fun MessageSender.update(groupPublicKey: String, members: Collection<String>, na
// send it out to all members (minus the removed ones) using established channels. // send it out to all members (minus the removed ones) using established channels.
if (isUserLeaving) { if (isUserLeaving) {
sskDatabase.removeClosedGroupPrivateKey(groupPublicKey) sskDatabase.removeClosedGroupPrivateKey(groupPublicKey)
groupDB.setActive(groupID, false) storage.setActive(groupID, false)
groupDB.removeMember(groupID, Address.fromSerialized(userPublicKey)) storage.removeMember(groupID, Address.fromSerialized(userPublicKey))
// Notify the PN server // Notify the PN server
LokiPushNotificationManager.performOperation(context, ClosedGroupOperation.Unsubscribe, groupPublicKey, userPublicKey) PushNotificationAPI.performOperation(PushNotificationAPI.ClosedGroupOperation.Unsubscribe, groupPublicKey, userPublicKey)
} else { } else {
// Send closed group update messages to any new members using established channels // Send closed group update messages to any new members using established channels
for (member in newMembers) { for (member in newMembers) {
@Suppress("NAME_SHADOWING") val closedGroupUpdateKind = ClosedGroupUpdate.Kind.New(Hex.fromStringCondensed(groupPublicKey), name,
val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.New(Hex.fromStringCondensed(groupPublicKey), name,
Hex.fromStringCondensed(groupPrivateKey), listOf(), membersAsData, adminsAsData) Hex.fromStringCondensed(groupPrivateKey), listOf(), membersAsData, adminsAsData)
@Suppress("NAME_SHADOWING") val closedGroupUpdate = ClosedGroupUpdate()
val job = ClosedGroupUpdateMessageSendJob(member, closedGroupUpdateKind) closedGroupUpdate.kind = closedGroupUpdateKind
ApplicationContext.getInstance(context).jobManager.add(job) val address = Address.fromSerialized(member)
MessageSender.sendNonDurably(closedGroupUpdate, address)
} }
// Send out the user's new ratchet to all members (minus the removed ones) using established channels // Send out the user's new ratchet to all members (minus the removed ones) using established channels
val userRatchet = SharedSenderKeysImplementation.shared.generateRatchet(groupPublicKey, userPublicKey) val userRatchet = SharedSenderKeysImplementation.shared.generateRatchet(groupPublicKey, userPublicKey)
val userSenderKey = ClosedGroupSenderKey(Hex.fromStringCondensed(userRatchet.chainKey), userRatchet.keyIndex, Hex.fromStringCondensed(userPublicKey)) val userSenderKey = ClosedGroupSenderKey(Hex.fromStringCondensed(userRatchet.chainKey), userRatchet.keyIndex, Hex.fromStringCondensed(userPublicKey))
for (member in members) { for (member in members) {
if (member == userPublicKey) { continue } if (member == userPublicKey) { continue }
@Suppress("NAME_SHADOWING") val closedGroupUpdateKind = ClosedGroupUpdate.Kind.SenderKey(Hex.fromStringCondensed(groupPublicKey), userSenderKey)
val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.SenderKey(Hex.fromStringCondensed(groupPublicKey), userSenderKey) val closedGroupUpdate = ClosedGroupUpdate()
@Suppress("NAME_SHADOWING") closedGroupUpdate.kind = closedGroupUpdateKind
val job = ClosedGroupUpdateMessageSendJob(member, closedGroupUpdateKind) val address = Address.fromSerialized(member)
ApplicationContext.getInstance(context).jobManager.add(job) MessageSender.sendNonDurably(closedGroupUpdate, address)
} }
} }
} else if (newMembers.isNotEmpty()) { } else if (newMembers.isNotEmpty()) {
@ -161,49 +162,68 @@ fun MessageSender.update(groupPublicKey: String, members: Collection<String>, na
ClosedGroupSenderKey(Hex.fromStringCondensed(ratchet.chainKey), ratchet.keyIndex, Hex.fromStringCondensed(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) // 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, val closedGroupUpdateKind = ClosedGroupUpdate.Kind.Info(Hex.fromStringCondensed(groupPublicKey), name,
newSenderKeys, membersAsData, adminsAsData) newSenderKeys, membersAsData, adminsAsData)
val job = ClosedGroupUpdateMessageSendJob(groupPublicKey, closedGroupUpdateKind) val closedGroupUpdate = ClosedGroupUpdate()
ApplicationContext.getInstance(context).jobManager.add(job) closedGroupUpdate.kind = closedGroupUpdateKind
// Establish sessions if needed val address = Address.fromSerialized(groupID)
establishSessionsWithMembersIfNeeded(context, newMembers) MessageSender.send(closedGroupUpdate, address)
// Send closed group update messages to the new members using established channels // Send closed group update messages to the new members using established channels
var allSenderKeys = sskDatabase.getAllClosedGroupSenderKeys(groupPublicKey, ClosedGroupRatchetCollectionType.Current) var allSenderKeys = sskDatabase.getAllClosedGroupSenderKeys(groupPublicKey, ClosedGroupRatchetCollectionType.Current)
allSenderKeys = allSenderKeys.union(newSenderKeys) allSenderKeys = allSenderKeys.union(newSenderKeys)
for (member in newMembers) { for (member in newMembers) {
@Suppress("NAME_SHADOWING") val closedGroupUpdateKind = ClosedGroupUpdate.Kind.New(Hex.fromStringCondensed(groupPublicKey), name,
val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.New(Hex.fromStringCondensed(groupPublicKey), name,
Hex.fromStringCondensed(groupPrivateKey), allSenderKeys, membersAsData, adminsAsData) Hex.fromStringCondensed(groupPrivateKey), allSenderKeys, membersAsData, adminsAsData)
@Suppress("NAME_SHADOWING") val closedGroupUpdate = ClosedGroupUpdate()
val job = ClosedGroupUpdateMessageSendJob(member, closedGroupUpdateKind) closedGroupUpdate.kind = closedGroupUpdateKind
ApplicationContext.getInstance(context).jobManager.add(job) val address = Address.fromSerialized(member)
MessageSender.send(closedGroupUpdate, address)
} }
} else { } else {
val allSenderKeys = sskDatabase.getAllClosedGroupSenderKeys(groupPublicKey, ClosedGroupRatchetCollectionType.Current) val allSenderKeys = sskDatabase.getAllClosedGroupSenderKeys(groupPublicKey, ClosedGroupRatchetCollectionType.Current)
val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.Info(Hex.fromStringCondensed(groupPublicKey), name, val closedGroupUpdateKind = ClosedGroupUpdate.Kind.Info(Hex.fromStringCondensed(groupPublicKey), name,
allSenderKeys, membersAsData, adminsAsData) allSenderKeys, membersAsData, adminsAsData)
val job = ClosedGroupUpdateMessageSendJob(groupPublicKey, closedGroupUpdateKind) val closedGroupUpdate = ClosedGroupUpdate()
ApplicationContext.getInstance(context).jobManager.add(job) closedGroupUpdate.kind = closedGroupUpdateKind
val address = Address.fromSerialized(groupID)
MessageSender.send(closedGroupUpdate, address)
} }
// Update the group // Update the group
groupDB.updateTitle(groupID, name) storage.updateTitle(groupID, name)
if (!isUserLeaving) { if (!isUserLeaving) {
// The call below sets isActive to true, so if the user is leaving we have to use groupDB.remove(...) instead // The call below sets isActive to true, so if the user is leaving we have to use groupDB.remove(...) instead
groupDB.updateMembers(groupID, members.map { Address.fromSerialized(it) }) storage.updateMembers(groupID, members.map { Address.fromSerialized(it) })
} }
// Notify the user // Notify the user
val infoType = if (isUserLeaving) SignalServiceProtos.GroupContext.Type.QUIT else SignalServiceProtos.GroupContext.Type.UPDATE val infoType = if (isUserLeaving) SignalServiceProtos.GroupContext.Type.QUIT else SignalServiceProtos.GroupContext.Type.UPDATE
val threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false)) val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
/* TODO
insertOutgoingInfoMessage(context, groupID, infoType, name, members, admins, threadID) insertOutgoingInfoMessage(context, groupID, infoType, name, members, admins, threadID)
*/
deferred.resolve(Unit) deferred.resolve(Unit)
return deferred.promise return deferred.promise
} }
fun MessageSender.leave(groupPublicKey: String) {
val storage = MessagingConfiguration.shared.storage
val userPublicKey = storage.getUserPublicKey()!!
val groupID = GroupUtil.getEncodedClosedGroupID(groupPublicKey)
val group = storage.getGroup(groupID)
if (group == null) {
Log.d("Loki", "Can't leave nonexistent closed group.")
return
}
val name = group.title
val oldMembers = group.members.map { it.serialize() }.toSet()
val newMembers = oldMembers.minus(userPublicKey)
return update(groupPublicKey, newMembers, name).get()
}
fun MessageSender.requestSenderKey(groupPublicKey: String, senderPublicKey: String) { fun MessageSender.requestSenderKey(groupPublicKey: String, senderPublicKey: String) {
Log.d("Loki", "Requesting sender key for group public key: $groupPublicKey, sender public key: $senderPublicKey.") Log.d("Loki", "Requesting sender key for group public key: $groupPublicKey, sender public key: $senderPublicKey.")
// Send the request val address = Address.fromSerialized(senderPublicKey)
val closedGroupUpdateKind = ClosedGroupUpdate.Kind.SenderKeyRequest(Hex.fromStringCondensed(groupPublicKey)) val closedGroupUpdateKind = ClosedGroupUpdate.Kind.SenderKeyRequest(Hex.fromStringCondensed(groupPublicKey))
val closedGroupUpdate = ClosedGroupUpdate() val closedGroupUpdate = ClosedGroupUpdate()
closedGroupUpdate.kind = closedGroupUpdateKind closedGroupUpdate.kind = closedGroupUpdateKind
MessageSender.send(closedGroupUpdate, Destination.ClosedGroup(groupPublicKey)) MessageSender.send(closedGroupUpdate, address)
} }

View File

@ -1,35 +1,39 @@
package org.session.libsession.messaging.sending_receiving package org.session.libsession.messaging.sending_receiving
import nl.komponents.kovenant.Promise import nl.komponents.kovenant.Promise
import org.session.libsession.messaging.MessagingConfiguration
import org.session.libsession.messaging.jobs.JobQueue import org.session.libsession.messaging.jobs.JobQueue
import org.session.libsession.messaging.jobs.MessageSendJob import org.session.libsession.messaging.jobs.MessageSendJob
import org.session.libsession.messaging.messages.Destination import org.session.libsession.messaging.messages.Destination
import org.session.libsession.messaging.messages.Message import org.session.libsession.messaging.messages.Message
import org.session.libsession.messaging.messages.visible.VisibleMessage import org.session.libsession.messaging.messages.visible.VisibleMessage
import org.session.libsession.messaging.threads.Address
import org.session.libsignal.service.api.messages.SignalServiceAttachment import org.session.libsignal.service.api.messages.SignalServiceAttachment
fun MessageSender.send(message: VisibleMessage, attachments: List<SignalServiceAttachment>, threadID: String) { fun MessageSender.send(message: VisibleMessage, attachments: List<SignalServiceAttachment>, address: Address) {
prep(attachments, message) prep(attachments, message)
send(message, threadID) send(message, address)
} }
fun MessageSender.send(message: Message, threadID: String) { fun MessageSender.send(message: Message, address: Address) {
val threadID = MessagingConfiguration.shared.storage.getOrCreateThreadIdFor(address)
message.threadID = threadID message.threadID = threadID
val destination = Destination.from(threadID) val destination = Destination.from(address)
val job = MessageSendJob(message, destination) val job = MessageSendJob(message, destination)
JobQueue.shared.add(job) JobQueue.shared.add(job)
} }
fun MessageSender.sendNonDurably(message: VisibleMessage, attachments: List<SignalServiceAttachment>, threadID: String): Promise<Unit, Exception> { fun MessageSender.sendNonDurably(message: VisibleMessage, attachments: List<SignalServiceAttachment>, address: Address): Promise<Unit, Exception> {
prep(attachments, message) prep(attachments, message)
// TODO: Deal with attachments // TODO: Deal with attachments
return sendNonDurably(message, threadID) return sendNonDurably(message, address)
} }
fun MessageSender.sendNonDurably(message: Message, threadID: String): Promise<Unit, Exception> { fun MessageSender.sendNonDurably(message: Message, address: Address): Promise<Unit, Exception> {
val threadID = MessagingConfiguration.shared.storage.getOrCreateThreadIdFor(address)
message.threadID = threadID message.threadID = threadID
val destination = Destination.from(threadID) val destination = Destination.from(address)
return MessageSender.send(message, destination) return MessageSender.send(message, destination)
} }

View File

@ -1,7 +1,7 @@
package org.session.libsession.messaging.sending_receiving package org.session.libsession.messaging.sending_receiving
import com.google.protobuf.ByteString import com.google.protobuf.ByteString
import org.session.libsession.messaging.Configuration import org.session.libsession.messaging.MessagingConfiguration
import org.session.libsession.messaging.messages.Message import org.session.libsession.messaging.messages.Message
import org.session.libsession.messaging.sending_receiving.MessageSender.Error import org.session.libsession.messaging.sending_receiving.MessageSender.Error
import org.session.libsession.messaging.utilities.UnidentifiedAccessUtil import org.session.libsession.messaging.utilities.UnidentifiedAccessUtil
@ -21,11 +21,11 @@ import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded
object MessageSenderEncryption { object MessageSenderEncryption {
internal fun encryptWithSignalProtocol(plaintext: ByteArray, message: Message, recipientPublicKey: String): ByteArray{ internal fun encryptWithSignalProtocol(plaintext: ByteArray, message: Message, recipientPublicKey: String): ByteArray{
val storage = Configuration.shared.signalStorage val storage = MessagingConfiguration.shared.signalStorage
val sskDatabase = Configuration.shared.sskDatabase val sskDatabase = MessagingConfiguration.shared.sskDatabase
val sessionResetImp = Configuration.shared.sessionResetImp val sessionResetImp = MessagingConfiguration.shared.sessionResetImp
val localAddress = SignalServiceAddress(recipientPublicKey) val localAddress = SignalServiceAddress(recipientPublicKey)
val certificateValidator = Configuration.shared.certificateValidator val certificateValidator = MessagingConfiguration.shared.certificateValidator
val cipher = SignalServiceCipher(localAddress, storage, sskDatabase, sessionResetImp, certificateValidator) val cipher = SignalServiceCipher(localAddress, storage, sskDatabase, sessionResetImp, certificateValidator)
val signalProtocolAddress = SignalProtocolAddress(recipientPublicKey, 1) val signalProtocolAddress = SignalProtocolAddress(recipientPublicKey, 1)
val unidentifiedAccessPair = UnidentifiedAccessUtil.getAccessFor(recipientPublicKey) val unidentifiedAccessPair = UnidentifiedAccessUtil.getAccessFor(recipientPublicKey)
@ -36,7 +36,7 @@ object MessageSenderEncryption {
internal fun encryptWithSharedSenderKeys(plaintext: ByteArray, groupPublicKey: String): ByteArray { internal fun encryptWithSharedSenderKeys(plaintext: ByteArray, groupPublicKey: String): ByteArray {
// 1. ) Encrypt the data with the user's sender key // 1. ) Encrypt the data with the user's sender key
val userPublicKey = Configuration.shared.storage.getUserPublicKey() ?: throw Error.NoUserPublicKey val userPublicKey = MessagingConfiguration.shared.storage.getUserPublicKey() ?: throw Error.NoUserPublicKey
val ciphertextAndKeyIndex = SharedSenderKeysImplementation.shared.encrypt(plaintext, groupPublicKey, userPublicKey) val ciphertextAndKeyIndex = SharedSenderKeysImplementation.shared.encrypt(plaintext, groupPublicKey, userPublicKey)
val ivAndCiphertext = ciphertextAndKeyIndex.first val ivAndCiphertext = ciphertextAndKeyIndex.first
val keyIndex = ciphertextAndKeyIndex.second val keyIndex = ciphertextAndKeyIndex.second

View File

@ -1,16 +1,16 @@
package org.session.libsession.messaging.sending_receiving.notifications package org.session.libsession.messaging.sending_receiving.notifications
import android.content.Context
import nl.komponents.kovenant.functional.map import nl.komponents.kovenant.functional.map
import okhttp3.* import okhttp3.*
import org.session.libsession.messaging.Configuration import org.session.libsession.messaging.MessagingConfiguration
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsignal.libsignal.logging.Log import org.session.libsignal.libsignal.logging.Log
import org.session.libsignal.service.internal.util.JsonUtil import org.session.libsignal.service.internal.util.JsonUtil
import org.session.libsignal.service.loki.api.onionrequests.OnionRequestAPI import org.session.libsignal.service.loki.api.onionrequests.OnionRequestAPI
import org.session.libsignal.service.loki.utilities.retryIfNeeded import org.session.libsignal.service.loki.utilities.retryIfNeeded
import java.io.IOException
object PushNotificationAPI { object PushNotificationAPI {
val context = MessagingConfiguration.shared.context
val server = "https://live.apns.getsession.org" val server = "https://live.apns.getsession.org"
val serverPublicKey = "642a6585919742e5a2d4dc51244964fbcd8bcab2b75612407de58b810740d049" val serverPublicKey = "642a6585919742e5a2d4dc51244964fbcd8bcab2b75612407de58b810740d049"
private val maxRetryCount = 4 private val maxRetryCount = 4
@ -46,8 +46,8 @@ object PushNotificationAPI {
} }
} }
// Unsubscribe from all closed groups // Unsubscribe from all closed groups
val allClosedGroupPublicKeys = Configuration.shared.sskDatabase.getAllClosedGroupPublicKeys() val allClosedGroupPublicKeys = MessagingConfiguration.shared.sskDatabase.getAllClosedGroupPublicKeys()
val userPublicKey = Configuration.shared.storage.getUserPublicKey()!! val userPublicKey = MessagingConfiguration.shared.storage.getUserPublicKey()!!
allClosedGroupPublicKeys.forEach { closedGroup -> allClosedGroupPublicKeys.forEach { closedGroup ->
performOperation(ClosedGroupOperation.Unsubscribe, closedGroup, userPublicKey) performOperation(ClosedGroupOperation.Unsubscribe, closedGroup, userPublicKey)
} }
@ -76,7 +76,7 @@ object PushNotificationAPI {
} }
} }
// Subscribe to all closed groups // Subscribe to all closed groups
val allClosedGroupPublicKeys = Configuration.shared.sskDatabase.getAllClosedGroupPublicKeys() val allClosedGroupPublicKeys = MessagingConfiguration.shared.sskDatabase.getAllClosedGroupPublicKeys()
allClosedGroupPublicKeys.forEach { closedGroup -> allClosedGroupPublicKeys.forEach { closedGroup ->
performOperation(ClosedGroupOperation.Subscribe, closedGroup, publicKey) performOperation(ClosedGroupOperation.Subscribe, closedGroup, publicKey)
} }