Extract login into GroupManagerV2
parent
32f95337d5
commit
80e3e563ce
@ -0,0 +1,464 @@
|
|||||||
|
package org.thoughtcrime.securesms.groups
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import com.google.protobuf.ByteString
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import network.loki.messenger.libsession_util.util.INVITE_STATUS_SENT
|
||||||
|
import network.loki.messenger.libsession_util.util.Sodium
|
||||||
|
import network.loki.messenger.libsession_util.util.UserPic
|
||||||
|
import org.session.libsession.database.StorageProtocol
|
||||||
|
import org.session.libsession.messaging.contacts.Contact
|
||||||
|
import org.session.libsession.messaging.groups.GroupManagerV2
|
||||||
|
import org.session.libsession.messaging.jobs.ConfigurationSyncJob.Companion.messageInformation
|
||||||
|
import org.session.libsession.messaging.jobs.InviteContactsJob
|
||||||
|
import org.session.libsession.messaging.jobs.JobQueue
|
||||||
|
import org.session.libsession.messaging.messages.Destination
|
||||||
|
import org.session.libsession.messaging.messages.control.GroupUpdated
|
||||||
|
import org.session.libsession.messaging.sending_receiving.MessageSender
|
||||||
|
import org.session.libsession.messaging.utilities.MessageAuthentication.buildDeleteMemberContentSignature
|
||||||
|
import org.session.libsession.messaging.utilities.MessageAuthentication.buildMemberChangeSignature
|
||||||
|
import org.session.libsession.messaging.utilities.SodiumUtilities
|
||||||
|
import org.session.libsession.snode.OwnedSwarmAuth
|
||||||
|
import org.session.libsession.snode.SnodeAPI
|
||||||
|
import org.session.libsession.snode.SnodeMessage
|
||||||
|
import org.session.libsession.snode.model.BatchResponse
|
||||||
|
import org.session.libsession.snode.utilities.await
|
||||||
|
import org.session.libsession.utilities.Address
|
||||||
|
import org.session.libsession.utilities.withGroupConfigsOrNull
|
||||||
|
import org.session.libsignal.protos.SignalServiceProtos.DataMessage
|
||||||
|
import org.session.libsignal.protos.SignalServiceProtos.DataMessage.GroupUpdateDeleteMemberContentMessage
|
||||||
|
import org.session.libsignal.protos.SignalServiceProtos.DataMessage.GroupUpdateMemberChangeMessage
|
||||||
|
import org.session.libsignal.protos.SignalServiceProtos.DataMessage.GroupUpdateMessage
|
||||||
|
import org.session.libsignal.utilities.AccountId
|
||||||
|
import org.session.libsignal.utilities.Base64
|
||||||
|
import org.session.libsignal.utilities.Namespace
|
||||||
|
import org.thoughtcrime.securesms.database.LokiMessageDatabase
|
||||||
|
import org.thoughtcrime.securesms.database.MmsSmsDatabase
|
||||||
|
import org.thoughtcrime.securesms.dependencies.ConfigFactory
|
||||||
|
import org.thoughtcrime.securesms.dependencies.PollerFactory
|
||||||
|
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
class GroupManagerV2Impl @Inject constructor(
|
||||||
|
val storage: StorageProtocol,
|
||||||
|
val configFactory: ConfigFactory,
|
||||||
|
val mmsSmsDatabase: MmsSmsDatabase,
|
||||||
|
val lokiDatabase: LokiMessageDatabase,
|
||||||
|
val pollerFactory: PollerFactory,
|
||||||
|
@ApplicationContext val application: Context,
|
||||||
|
) : GroupManagerV2 {
|
||||||
|
/**
|
||||||
|
* Require admin access to a group, and return the admin key.
|
||||||
|
*
|
||||||
|
* @throws IllegalArgumentException if the group does not exist or no admin key is found.
|
||||||
|
*/
|
||||||
|
private fun requireAdminAccess(group: AccountId): ByteArray {
|
||||||
|
return checkNotNull(configFactory
|
||||||
|
.userGroups
|
||||||
|
?.getClosedGroup(group.hexString)
|
||||||
|
?.adminKey
|
||||||
|
?.takeIf { it.isNotEmpty() }) { "Only admin is allowed to invite members" }
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun inviteMembers(
|
||||||
|
group: AccountId,
|
||||||
|
newMembers: List<AccountId>,
|
||||||
|
shareHistory: Boolean
|
||||||
|
) {
|
||||||
|
val adminKey = requireAdminAccess(group)
|
||||||
|
val groupAuth = OwnedSwarmAuth.ofClosedGroup(group, adminKey)
|
||||||
|
|
||||||
|
configFactory.withGroupConfigsOrNull(group) { infoConfig, membersConfig, keysConfig ->
|
||||||
|
// Construct the new members in the config
|
||||||
|
for (newMember in newMembers) {
|
||||||
|
val toSet = membersConfig.get(newMember.hexString)
|
||||||
|
?.let { existing ->
|
||||||
|
if (existing.inviteFailed || existing.invitePending) {
|
||||||
|
existing.copy(inviteStatus = INVITE_STATUS_SENT, supplement = shareHistory)
|
||||||
|
} else {
|
||||||
|
existing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?: membersConfig.getOrConstruct(newMember.hexString).let {
|
||||||
|
val contact = storage.getContactWithAccountID(newMember.hexString)
|
||||||
|
it.copy(
|
||||||
|
name = contact?.name,
|
||||||
|
profilePicture = contact?.profilePicture ?: UserPic.DEFAULT,
|
||||||
|
inviteStatus = INVITE_STATUS_SENT,
|
||||||
|
supplement = shareHistory
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
membersConfig.set(toSet)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Persist the member change to the db now for the UI to reflect the status change
|
||||||
|
val timestamp = SnodeAPI.nowWithOffset
|
||||||
|
configFactory.persistGroupConfigDump(membersConfig, group, timestamp)
|
||||||
|
|
||||||
|
val batchRequests = mutableListOf<SnodeAPI.SnodeBatchRequestInfo>()
|
||||||
|
val messagesToDelete = mutableListOf<String>() // List of message hashes
|
||||||
|
|
||||||
|
// Depends on whether we want to share history, we may need to rekey or just adding supplement keys
|
||||||
|
if (shareHistory) {
|
||||||
|
for (member in newMembers) {
|
||||||
|
val memberKey = keysConfig.supplementFor(member.hexString)
|
||||||
|
batchRequests.add(SnodeAPI.buildAuthenticatedStoreBatchInfo(
|
||||||
|
namespace = keysConfig.namespace(),
|
||||||
|
message = SnodeMessage(
|
||||||
|
recipient = group.hexString,
|
||||||
|
data = Base64.encodeBytes(memberKey),
|
||||||
|
ttl = SnodeMessage.CONFIG_TTL,
|
||||||
|
timestamp = timestamp
|
||||||
|
),
|
||||||
|
auth = groupAuth,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
keysConfig.rekey(infoConfig, membersConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call un-revocate API on new members, in case they have been removed before
|
||||||
|
batchRequests += SnodeAPI.buildAuthenticatedUnrevokeSubKeyBatchRequest(
|
||||||
|
groupAdminAuth = groupAuth,
|
||||||
|
subAccountTokens = newMembers.map(keysConfig::getSubAccountToken)
|
||||||
|
)
|
||||||
|
|
||||||
|
keysConfig.messageInformation(groupAuth)?.let {
|
||||||
|
batchRequests += it.batch
|
||||||
|
}
|
||||||
|
batchRequests += infoConfig.messageInformation(messagesToDelete, groupAuth).batch
|
||||||
|
batchRequests += membersConfig.messageInformation(messagesToDelete, groupAuth).batch
|
||||||
|
|
||||||
|
if (messagesToDelete.isNotEmpty()) {
|
||||||
|
batchRequests += SnodeAPI.buildAuthenticatedDeleteBatchInfo(
|
||||||
|
auth = groupAuth,
|
||||||
|
messageHashes = messagesToDelete
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call the API
|
||||||
|
val swarmNode = SnodeAPI.getSingleTargetSnode(group.hexString).await()
|
||||||
|
val response = SnodeAPI.getBatchResponse(swarmNode, group.hexString, batchRequests)
|
||||||
|
|
||||||
|
// Make sure every request is successful
|
||||||
|
response.requireAllRequestsSuccessful("Failed to invite members")
|
||||||
|
|
||||||
|
// Persist the keys config
|
||||||
|
configFactory.saveGroupConfigs(keysConfig, infoConfig, membersConfig)
|
||||||
|
|
||||||
|
// Send the invitation message to the new members
|
||||||
|
JobQueue.shared.add(InviteContactsJob(group.hexString, newMembers.map { it.hexString }.toTypedArray()))
|
||||||
|
|
||||||
|
// Send a member change message to the group
|
||||||
|
val signature = SodiumUtilities.sign(
|
||||||
|
buildMemberChangeSignature(GroupUpdateMemberChangeMessage.Type.ADDED, timestamp),
|
||||||
|
adminKey
|
||||||
|
)
|
||||||
|
|
||||||
|
val updatedMessage = GroupUpdated(
|
||||||
|
GroupUpdateMessage.newBuilder()
|
||||||
|
.setMemberChangeMessage(
|
||||||
|
GroupUpdateMemberChangeMessage.newBuilder()
|
||||||
|
.addAllMemberSessionIds(newMembers.map { it.hexString })
|
||||||
|
.setType(GroupUpdateMemberChangeMessage.Type.ADDED)
|
||||||
|
.setAdminSignature(ByteString.copyFrom(signature))
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
).apply { this.sentTimestamp = timestamp }
|
||||||
|
MessageSender.send(updatedMessage, Address.fromSerialized(group.hexString))
|
||||||
|
storage.insertGroupInfoChange(updatedMessage, group)
|
||||||
|
|
||||||
|
group
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override suspend fun removeMembers(
|
||||||
|
groupAccountId: AccountId,
|
||||||
|
removedMembers: List<AccountId>,
|
||||||
|
removeMessages: Boolean
|
||||||
|
) {
|
||||||
|
doRemoveMembers(
|
||||||
|
group = groupAccountId,
|
||||||
|
removedMembers = removedMembers,
|
||||||
|
sendRemovedMessage = true,
|
||||||
|
removeMemberMessages = removeMessages
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun handleMemberLeft(message: GroupUpdated, closedGroupId: AccountId) {
|
||||||
|
val userGroups = configFactory.userGroups ?: return
|
||||||
|
val closedGroupHexString = closedGroupId.hexString
|
||||||
|
val closedGroup = userGroups.getClosedGroup(closedGroupId.hexString) ?: return
|
||||||
|
if (closedGroup.hasAdminKey()) {
|
||||||
|
// re-key and do a new config removing the previous member
|
||||||
|
doRemoveMembers(
|
||||||
|
closedGroupId,
|
||||||
|
listOf(AccountId(message.sender!!)),
|
||||||
|
sendRemovedMessage = false,
|
||||||
|
removeMemberMessages = false
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
configFactory.getGroupMemberConfig(closedGroupId)?.use { memberConfig ->
|
||||||
|
// if the leaving member is an admin, disable the group and remove it
|
||||||
|
// This is just to emulate the "existing" group behaviour, this will need to be removed in future
|
||||||
|
if (memberConfig.get(message.sender!!)?.admin == true) {
|
||||||
|
pollerFactory.pollerFor(closedGroupId)?.stop()
|
||||||
|
storage.getThreadId(Address.fromSerialized(closedGroupHexString))
|
||||||
|
?.let(storage::deleteConversation)
|
||||||
|
configFactory.removeGroup(closedGroupId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun leaveGroup(group: AccountId, deleteOnLeave: Boolean) {
|
||||||
|
val canSendGroupMessage = configFactory.userGroups?.getClosedGroup(group.hexString)?.kicked != true
|
||||||
|
val address = Address.fromSerialized(group.hexString)
|
||||||
|
|
||||||
|
if (canSendGroupMessage) {
|
||||||
|
MessageSender.sendNonDurably(
|
||||||
|
message = GroupUpdated(
|
||||||
|
GroupUpdateMessage.newBuilder()
|
||||||
|
.setMemberLeftMessage(DataMessage.GroupUpdateMemberLeftMessage.getDefaultInstance())
|
||||||
|
.build()
|
||||||
|
),
|
||||||
|
address = address,
|
||||||
|
isSyncMessage = false
|
||||||
|
).await()
|
||||||
|
|
||||||
|
MessageSender.sendNonDurably(
|
||||||
|
message = GroupUpdated(
|
||||||
|
GroupUpdateMessage.newBuilder()
|
||||||
|
.setMemberLeftNotificationMessage(DataMessage.GroupUpdateMemberLeftNotificationMessage.getDefaultInstance())
|
||||||
|
.build()
|
||||||
|
),
|
||||||
|
address = address,
|
||||||
|
isSyncMessage = false
|
||||||
|
).await()
|
||||||
|
}
|
||||||
|
|
||||||
|
pollerFactory.pollerFor(group)?.stop()
|
||||||
|
// TODO: set "deleted" and post to -10 group namespace?
|
||||||
|
if (deleteOnLeave) {
|
||||||
|
storage.getThreadId(address)?.let(storage::deleteConversation)
|
||||||
|
configFactory.removeGroup(group)
|
||||||
|
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(application)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun promoteMember(group: AccountId, members: List<AccountId>) {
|
||||||
|
val adminKey = requireAdminAccess(group)
|
||||||
|
|
||||||
|
configFactory.withGroupConfigsOrNull(group) { info, membersConfig, keys ->
|
||||||
|
for (member in members) {
|
||||||
|
val promoted = membersConfig.get(member.hexString)?.setPromoteSent() ?: continue
|
||||||
|
membersConfig.set(promoted)
|
||||||
|
|
||||||
|
val message = GroupUpdated(
|
||||||
|
GroupUpdateMessage.newBuilder()
|
||||||
|
.setPromoteMessage(
|
||||||
|
DataMessage.GroupUpdatePromoteMessage.newBuilder()
|
||||||
|
.setGroupIdentitySeed(ByteString.copyFrom(adminKey))
|
||||||
|
.setName(info.getName())
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
MessageSender.send(message, Address.fromSerialized(group.hexString))
|
||||||
|
}
|
||||||
|
|
||||||
|
configFactory.saveGroupConfigs(keys, info, membersConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
val groupDestination = Destination.ClosedGroup(group.hexString)
|
||||||
|
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(groupDestination)
|
||||||
|
val timestamp = SnodeAPI.nowWithOffset
|
||||||
|
val signature = SodiumUtilities.sign(
|
||||||
|
buildMemberChangeSignature(GroupUpdateMemberChangeMessage.Type.PROMOTED, timestamp),
|
||||||
|
adminKey
|
||||||
|
)
|
||||||
|
val message = GroupUpdated(
|
||||||
|
GroupUpdateMessage.newBuilder()
|
||||||
|
.setMemberChangeMessage(
|
||||||
|
GroupUpdateMemberChangeMessage.newBuilder()
|
||||||
|
.addAllMemberSessionIds(members.map { it.hexString })
|
||||||
|
.setType(GroupUpdateMemberChangeMessage.Type.PROMOTED)
|
||||||
|
.setAdminSignature(ByteString.copyFrom(signature))
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
).apply {
|
||||||
|
sentTimestamp = timestamp
|
||||||
|
}
|
||||||
|
|
||||||
|
MessageSender.send(message, Address.fromSerialized(groupDestination.publicKey))
|
||||||
|
storage.insertGroupInfoChange(message, group)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun doRemoveMembers(group: AccountId,
|
||||||
|
removedMembers: List<AccountId>,
|
||||||
|
sendRemovedMessage: Boolean,
|
||||||
|
removeMemberMessages: Boolean) {
|
||||||
|
val adminKey = requireAdminAccess(group)
|
||||||
|
val groupAuth = OwnedSwarmAuth.ofClosedGroup(group, adminKey)
|
||||||
|
|
||||||
|
configFactory.withGroupConfigsOrNull(group) { info, members, keys ->
|
||||||
|
// To remove a member from a group, we need to first:
|
||||||
|
// 1. Notify the swarm that this member's key has bene revoked
|
||||||
|
// 2. Send a "kicked" message to a special namespace that the kicked member can still read
|
||||||
|
// 3. Optionally, send "delete member messages" to the group. (So that every device in the group
|
||||||
|
// delete this member's messages locally.)
|
||||||
|
// These three steps will be included in a sequential call as they all need to be done in order.
|
||||||
|
// After these steps are all done, we will do the following:
|
||||||
|
// Update the group configs to remove the member, sync if needed, then
|
||||||
|
// delete the member's messages locally and remotely.
|
||||||
|
val messageSendTimestamp = SnodeAPI.nowWithOffset
|
||||||
|
|
||||||
|
val essentialRequests = buildList {
|
||||||
|
this += SnodeAPI.buildAuthenticatedRevokeSubKeyBatchRequest(
|
||||||
|
groupAdminAuth = groupAuth,
|
||||||
|
subAccountTokens = removedMembers.map(keys::getSubAccountToken)
|
||||||
|
)
|
||||||
|
|
||||||
|
this += Sodium.encryptForMultipleSimple(
|
||||||
|
messages = removedMembers.map{"${it.hexString}-${keys.currentGeneration()}".encodeToByteArray()}.toTypedArray(),
|
||||||
|
recipients = removedMembers.map { it.pubKeyBytes }.toTypedArray(),
|
||||||
|
ed25519SecretKey = adminKey,
|
||||||
|
domain = Sodium.KICKED_DOMAIN
|
||||||
|
).let { encryptedForMembers ->
|
||||||
|
SnodeAPI.buildAuthenticatedStoreBatchInfo(
|
||||||
|
namespace = Namespace.REVOKED_GROUP_MESSAGES(),
|
||||||
|
message = SnodeMessage(
|
||||||
|
recipient = group.hexString,
|
||||||
|
data = Base64.encodeBytes(encryptedForMembers),
|
||||||
|
ttl = SnodeMessage.CONFIG_TTL,
|
||||||
|
timestamp = messageSendTimestamp
|
||||||
|
),
|
||||||
|
auth = groupAuth
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (removeMemberMessages) {
|
||||||
|
val adminSignature =
|
||||||
|
SodiumUtilities.sign(
|
||||||
|
buildDeleteMemberContentSignature(
|
||||||
|
memberIds = removedMembers,
|
||||||
|
messageHashes = emptyList(),
|
||||||
|
timestamp = messageSendTimestamp
|
||||||
|
), adminKey)
|
||||||
|
|
||||||
|
this += SnodeAPI.buildAuthenticatedStoreBatchInfo(
|
||||||
|
namespace = Namespace.CLOSED_GROUP_MESSAGES(),
|
||||||
|
message = MessageSender.buildWrappedMessageToSnode(
|
||||||
|
destination = Destination.ClosedGroup(group.hexString),
|
||||||
|
message = GroupUpdated(GroupUpdateMessage.newBuilder()
|
||||||
|
.setDeleteMemberContent(
|
||||||
|
GroupUpdateDeleteMemberContentMessage.newBuilder()
|
||||||
|
.addAllMemberSessionIds(removedMembers.map { it.hexString })
|
||||||
|
.setAdminSignature(ByteString.copyFrom(adminSignature))
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
).apply { sentTimestamp = messageSendTimestamp },
|
||||||
|
isSyncMessage = false
|
||||||
|
),
|
||||||
|
auth = groupAuth
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val snode = SnodeAPI.getSingleTargetSnode(group.hexString).await()
|
||||||
|
val responses = SnodeAPI.getBatchResponse(snode, group.hexString, essentialRequests, sequence = true)
|
||||||
|
|
||||||
|
responses.requireAllRequestsSuccessful("Failed to execute essential steps for removing member")
|
||||||
|
|
||||||
|
// Next step: update group configs, rekey, remove member messages if required
|
||||||
|
val messagesToDelete = mutableListOf<String>()
|
||||||
|
for (member in removedMembers) {
|
||||||
|
members.erase(member.hexString)
|
||||||
|
}
|
||||||
|
|
||||||
|
keys.rekey(info, members)
|
||||||
|
|
||||||
|
if (removeMemberMessages) {
|
||||||
|
val threadId = storage.getThreadId(Address.fromSerialized(group.hexString))
|
||||||
|
if (threadId != null) {
|
||||||
|
for (member in removedMembers) {
|
||||||
|
for (msg in mmsSmsDatabase.getUserMessages(threadId, member.hexString)) {
|
||||||
|
val serverHash = lokiDatabase.getMessageServerHash(msg.id, msg.isMms)
|
||||||
|
if (serverHash != null) {
|
||||||
|
messagesToDelete.add(serverHash)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
storage.deleteMessagesByUser(threadId, member.hexString)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val requests = buildList {
|
||||||
|
keys.messageInformation(groupAuth)?.let {
|
||||||
|
this += "Sync keys config messages" to it.batch
|
||||||
|
}
|
||||||
|
|
||||||
|
this += "Sync info config messages" to info.messageInformation(messagesToDelete, groupAuth).batch
|
||||||
|
this += "Sync member config messages" to members.messageInformation(messagesToDelete, groupAuth).batch
|
||||||
|
this += "Delete outdated config and member messages" to SnodeAPI.buildAuthenticatedDeleteBatchInfo(groupAuth, messagesToDelete)
|
||||||
|
}
|
||||||
|
|
||||||
|
val response = SnodeAPI.getBatchResponse(
|
||||||
|
snode = snode,
|
||||||
|
publicKey = group.hexString,
|
||||||
|
requests = requests.map { it.second }
|
||||||
|
)
|
||||||
|
|
||||||
|
response.requireAllRequestsSuccessful("Failed to remove members")
|
||||||
|
|
||||||
|
// Persist the changes
|
||||||
|
configFactory.saveGroupConfigs(keys, info, members)
|
||||||
|
|
||||||
|
if (sendRemovedMessage) {
|
||||||
|
val timestamp = messageSendTimestamp
|
||||||
|
val signature = SodiumUtilities.sign(
|
||||||
|
buildMemberChangeSignature(GroupUpdateMemberChangeMessage.Type.REMOVED, timestamp),
|
||||||
|
adminKey
|
||||||
|
)
|
||||||
|
|
||||||
|
val updateMessage = GroupUpdateMessage.newBuilder()
|
||||||
|
.setMemberChangeMessage(
|
||||||
|
GroupUpdateMemberChangeMessage.newBuilder()
|
||||||
|
.addAllMemberSessionIds(removedMembers.map { it.hexString })
|
||||||
|
.setType(GroupUpdateMemberChangeMessage.Type.REMOVED)
|
||||||
|
.setAdminSignature(ByteString.copyFrom(signature))
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
val message = GroupUpdated(
|
||||||
|
updateMessage
|
||||||
|
).apply { sentTimestamp = timestamp }
|
||||||
|
MessageSender.send(message, Destination.ClosedGroup(group.hexString), false)
|
||||||
|
storage.insertGroupInfoChange(message, group)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(
|
||||||
|
Destination.ClosedGroup(group.hexString)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun BatchResponse.requireAllRequestsSuccessful(errorMessage: String) {
|
||||||
|
val firstError = this.results.firstOrNull { it.code != 200 }
|
||||||
|
require(firstError == null) { "$errorMessage: ${firstError!!.body}" }
|
||||||
|
}
|
||||||
|
|
||||||
|
private val Contact.profilePicture: UserPic? get() {
|
||||||
|
val url = this.profilePictureURL
|
||||||
|
val key = this.profilePictureEncryptionKey
|
||||||
|
return if (url != null && key != null) {
|
||||||
|
UserPic(url, key)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1 +1 @@
|
|||||||
Subproject commit 0193c36e0dad461385d6407a00f33b7314e6d740
|
Subproject commit 995e22dcbf08b3cb9e2ad595859e4cd9a4ed8776
|
@ -0,0 +1,28 @@
|
|||||||
|
package org.session.libsession.messaging.groups
|
||||||
|
|
||||||
|
import org.session.libsession.messaging.messages.control.GroupUpdated
|
||||||
|
import org.session.libsignal.utilities.AccountId
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Business logic handling group v2 operations like inviting members,
|
||||||
|
* removing members, promoting members, leaving groups, etc.
|
||||||
|
*/
|
||||||
|
interface GroupManagerV2 {
|
||||||
|
suspend fun inviteMembers(
|
||||||
|
group: AccountId,
|
||||||
|
newMembers: List<AccountId>,
|
||||||
|
shareHistory: Boolean
|
||||||
|
)
|
||||||
|
|
||||||
|
suspend fun removeMembers(
|
||||||
|
groupAccountId: AccountId,
|
||||||
|
removedMembers: List<AccountId>,
|
||||||
|
removeMessages: Boolean
|
||||||
|
)
|
||||||
|
|
||||||
|
suspend fun handleMemberLeft(message: GroupUpdated, closedGroupId: AccountId)
|
||||||
|
|
||||||
|
suspend fun leaveGroup(group: AccountId, deleteOnLeave: Boolean)
|
||||||
|
|
||||||
|
suspend fun promoteMember(group: AccountId, members: List<AccountId>)
|
||||||
|
}
|
Loading…
Reference in New Issue