Put back invite contacts job

This commit is contained in:
SessionHero01 2024-09-26 17:23:54 +10:00
parent 771d63e902
commit b9f5f940a1
No known key found for this signature in database
5 changed files with 431 additions and 218 deletions

View File

@ -21,6 +21,7 @@ import network.loki.messenger.libsession_util.util.GroupMember
import org.session.libsession.database.StorageProtocol import org.session.libsession.database.StorageProtocol
import org.session.libsession.messaging.contacts.Contact import org.session.libsession.messaging.contacts.Contact
import org.session.libsession.messaging.groups.GroupManagerV2 import org.session.libsession.messaging.groups.GroupManagerV2
import org.session.libsession.messaging.jobs.InviteContactsJob
import org.session.libsession.messaging.jobs.JobQueue import org.session.libsession.messaging.jobs.JobQueue
import org.session.libsignal.utilities.AccountId import org.session.libsignal.utilities.AccountId
import org.thoughtcrime.securesms.dependencies.ConfigFactory import org.thoughtcrime.securesms.dependencies.ConfigFactory

View File

@ -192,10 +192,13 @@ class GroupManagerV2Impl @Inject constructor(
val adminKey = requireAdminAccess(group) val adminKey = requireAdminAccess(group)
val groupAuth = OwnedSwarmAuth.ofClosedGroup(group, adminKey) val groupAuth = OwnedSwarmAuth.ofClosedGroup(group, adminKey)
configFactory.withGroupConfigsOrNull(group) { infoConfig, membersConfig, keysConfig -> val batchRequests = mutableListOf<SnodeAPI.SnodeBatchRequestInfo>()
// Construct the new members in our config
val subAccountTokens = configFactory.withMutableGroupConfigs(group) { configs ->
// Construct the new members in the config // Construct the new members in the config
for (newMember in newMembers) { for (newMember in newMembers) {
val toSet = membersConfig.get(newMember.hexString) val toSet = configs.groupMembers.get(newMember.hexString)
?.let { existing -> ?.let { existing ->
if (existing.inviteFailed || existing.invitePending) { if (existing.inviteFailed || existing.invitePending) {
existing.copy( existing.copy(
@ -206,7 +209,7 @@ class GroupManagerV2Impl @Inject constructor(
existing existing
} }
} }
?: membersConfig.getOrConstruct(newMember.hexString).let { ?: configs.groupMembers.getOrConstruct(newMember.hexString).let {
val contact = storage.getContactWithAccountID(newMember.hexString) val contact = storage.getContactWithAccountID(newMember.hexString)
it.copy( it.copy(
name = contact?.name, name = contact?.name,
@ -216,98 +219,74 @@ class GroupManagerV2Impl @Inject constructor(
) )
} }
membersConfig.set(toSet) configs.groupMembers.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 // Depends on whether we want to share history, we may need to rekey or just adding supplement keys
if (shareHistory) { if (shareHistory) {
for (member in newMembers) { val memberKey = configs.groupKeys.supplementFor(newMembers.map { it.hexString })
val memberKey = keysConfig.supplementFor(member.hexString) batchRequests.add(
batchRequests.add( SnodeAPI.buildAuthenticatedStoreBatchInfo(
SnodeAPI.buildAuthenticatedStoreBatchInfo( namespace = Namespace.ENCRYPTION_KEYS(),
namespace = keysConfig.namespace(), message = SnodeMessage(
message = SnodeMessage( recipient = group.hexString,
recipient = group.hexString, data = Base64.encodeBytes(memberKey),
data = Base64.encodeBytes(memberKey), ttl = SnodeMessage.CONFIG_TTL,
ttl = SnodeMessage.CONFIG_TTL, timestamp = SnodeAPI.nowWithOffset,
timestamp = timestamp ),
), auth = groupAuth,
auth = groupAuth,
)
) )
} )
} else { } else {
keysConfig.rekey(infoConfig, membersConfig) configs.rekeys()
} }
// Call un-revocate API on new members, in case they have been removed before newMembers.map { configs.groupKeys.makeSubAccount(group) }
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
} }
}
// Call un-revocate API on new members, in case they have been removed before
batchRequests += SnodeAPI.buildAuthenticatedUnrevokeSubKeyBatchRequest(
groupAdminAuth = groupAuth,
subAccountTokens = subAccountTokens
)
// 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")
// 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 timestamp = SnodeAPI.nowWithOffset
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)
}
override suspend fun removeMembers( override suspend fun removeMembers(
groupAccountId: AccountId, groupAccountId: AccountId,
removedMembers: List<AccountId>, removedMembers: List<AccountId>,
@ -383,74 +362,75 @@ class GroupManagerV2Impl @Inject constructor(
} }
} }
override suspend fun promoteMember(group: AccountId, members: List<AccountId>): Unit = withContext(dispatcher) { override suspend fun promoteMember(group: AccountId, members: List<AccountId>): Unit =
val adminKey = requireAdminAccess(group) withContext(dispatcher) {
val adminKey = requireAdminAccess(group)
configFactory.withGroupConfigsOrNull(group) { info, membersConfig, keys -> configFactory.withGroupConfigsOrNull(group) { info, membersConfig, keys ->
// Promote the members by sending a message containing the admin key to each member's swarm, // Promote the members by sending a message containing the admin key to each member's swarm,
// we do this concurrently and then update the group configs after all the messages are sent. // we do this concurrently and then update the group configs after all the messages are sent.
val promoteResult = members.asSequence() val promoteResult = members.asSequence()
.mapNotNull { membersConfig.get(it.hexString) } .mapNotNull { membersConfig.get(it.hexString) }
.map { memberConfig -> .map { memberConfig ->
async { async {
val message = GroupUpdated( val message = GroupUpdated(
GroupUpdateMessage.newBuilder() GroupUpdateMessage.newBuilder()
.setPromoteMessage( .setPromoteMessage(
DataMessage.GroupUpdatePromoteMessage.newBuilder() DataMessage.GroupUpdatePromoteMessage.newBuilder()
.setGroupIdentitySeed(ByteString.copyFrom(adminKey)) .setGroupIdentitySeed(ByteString.copyFrom(adminKey))
.setName(info.getName()) .setName(info.getName())
) )
.build() .build()
) )
try { try {
MessageSender.sendNonDurably( MessageSender.sendNonDurably(
message = message, message = message,
address = Address.fromSerialized(memberConfig.sessionId), address = Address.fromSerialized(memberConfig.sessionId),
isSyncMessage = false isSyncMessage = false
).await() ).await()
memberConfig.setPromoteSent() memberConfig.setPromoteSent()
} catch (ec: Exception) { } catch (ec: Exception) {
Log.e(TAG, "Failed to send promote message", ec) Log.e(TAG, "Failed to send promote message", ec)
memberConfig.setPromoteFailed() memberConfig.setPromoteFailed()
}
} }
} }
} .toList()
.toList()
for (result in promoteResult) { for (result in promoteResult) {
membersConfig.set(result.await()) membersConfig.set(result.await())
}
configFactory.saveGroupConfigs(keys, info, membersConfig)
} }
configFactory.saveGroupConfigs(keys, info, membersConfig) // Send a group update message to the group telling members someone has been promoted
} 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
}
// Send a group update message to the group telling members someone has been promoted MessageSender.send(message, Address.fromSerialized(group.hexString))
val groupDestination = Destination.ClosedGroup(group.hexString) storage.insertGroupInfoChange(message, group)
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(group.hexString))
storage.insertGroupInfoChange(message, group)
}
private suspend fun doRemoveMembers( private suspend fun doRemoveMembers(
group: AccountId, group: AccountId,
removedMembers: List<AccountId>, removedMembers: List<AccountId>,
@ -624,29 +604,31 @@ class GroupManagerV2Impl @Inject constructor(
) )
} }
override suspend fun respondToInvitation(groupId: AccountId, approved: Boolean) = withContext(dispatcher) { override suspend fun respondToInvitation(groupId: AccountId, approved: Boolean) =
val groups = requireNotNull(configFactory.userGroups) { withContext(dispatcher) {
"User groups config is not available" val groups = requireNotNull(configFactory.userGroups) {
} "User groups config is not available"
}
val threadId = checkNotNull(storage.getThreadId(Address.fromSerialized(groupId.hexString))) { val threadId =
"No thread has been created for the group" checkNotNull(storage.getThreadId(Address.fromSerialized(groupId.hexString))) {
} "No thread has been created for the group"
}
val group = requireNotNull(groups.getClosedGroup(groupId.hexString)) { val group = requireNotNull(groups.getClosedGroup(groupId.hexString)) {
"Group must have been created into the config object before responding to an invitation" "Group must have been created into the config object before responding to an invitation"
} }
// Whether approved or not, delete the invite // Whether approved or not, delete the invite
lokiDatabase.deleteGroupInviteReferrer(threadId) lokiDatabase.deleteGroupInviteReferrer(threadId)
if (approved) { if (approved) {
approveGroupInvite(groups, group, threadId) approveGroupInvite(groups, group, threadId)
} else { } else {
groups.eraseClosedGroup(groupId.hexString) groups.eraseClosedGroup(groupId.hexString)
storage.deleteConversation(threadId) storage.deleteConversation(threadId)
}
} }
}
private fun approveGroupInvite( private fun approveGroupInvite(
groups: UserGroupsConfig, groups: UserGroupsConfig,
@ -671,7 +653,10 @@ class GroupManagerV2Impl @Inject constructor(
val responseMessage = GroupUpdated(responseData.build()) val responseMessage = GroupUpdated(responseData.build())
storage.clearMessages(threadId) storage.clearMessages(threadId)
// this will fail the first couple of times :) // this will fail the first couple of times :)
MessageSender.send(responseMessage, Address.fromSerialized(group.groupAccountId.hexString)) MessageSender.send(
responseMessage,
Address.fromSerialized(group.groupAccountId.hexString)
)
} else { } else {
// If we are invited as admin, we can just update the group info ourselves // If we are invited as admin, we can just update the group info ourselves
configFactory.withGroupConfigsOrNull(group.groupAccountId) { info, members, keys -> configFactory.withGroupConfigsOrNull(group.groupAccountId) { info, members, keys ->
@ -801,9 +786,11 @@ class GroupManagerV2Impl @Inject constructor(
return return
} }
val recipient = Recipient.from(application, Address.fromSerialized(groupId.hexString), false) val recipient =
Recipient.from(application, Address.fromSerialized(groupId.hexString), false)
val shouldAutoApprove = storage.getRecipientApproved(Address.fromSerialized(inviter.hexString)) val shouldAutoApprove =
storage.getRecipientApproved(Address.fromSerialized(inviter.hexString))
val closedGroupInfo = GroupInfo.ClosedGroupInfo( val closedGroupInfo = GroupInfo.ClosedGroupInfo(
groupAccountId = groupId, groupAccountId = groupId,
adminKey = authDataOrAdminKey.takeIf { fromPromotion }, adminKey = authDataOrAdminKey.takeIf { fromPromotion },
@ -823,7 +810,12 @@ class GroupManagerV2Impl @Inject constructor(
approveGroupInvite(groups, closedGroupInfo, groupThreadId) approveGroupInvite(groups, closedGroupInfo, groupThreadId)
} else { } else {
lokiDatabase.addGroupInviteReferrer(groupThreadId, inviter.hexString) lokiDatabase.addGroupInviteReferrer(groupThreadId, inviter.hexString)
storage.insertGroupInviteControlMessage(SnodeAPI.nowWithOffset, inviter.hexString, groupId, groupName) storage.insertGroupInviteControlMessage(
SnodeAPI.nowWithOffset,
inviter.hexString,
groupId,
groupName
)
} }
} }
@ -869,7 +861,8 @@ class GroupManagerV2Impl @Inject constructor(
pollerFactory.pollerFor(groupId)?.stop() pollerFactory.pollerFor(groupId)?.stop()
val userId = requireNotNull(storage.getUserPublicKey()) { "No current user available" } val userId = requireNotNull(storage.getUserPublicKey()) { "No current user available" }
val userGroups = requireNotNull(configFactory.userGroups) { "User groups config is not available" } val userGroups =
requireNotNull(configFactory.userGroups) { "User groups config is not available" }
val group = userGroups.getClosedGroup(groupId.hexString) ?: return@withContext val group = userGroups.getClosedGroup(groupId.hexString) ?: return@withContext
// Retrieve the group name one last time from the group info, // Retrieve the group name one last time from the group info,
@ -879,11 +872,13 @@ class GroupManagerV2Impl @Inject constructor(
?.use { it.getName() } ?.use { it.getName() }
?: group.name ?: group.name
userGroups.set(group.copy( userGroups.set(
authData = null, group.copy(
adminKey = null, authData = null,
name = groupName adminKey = null,
)) name = groupName
)
)
configFactory.persist(userGroups, SnodeAPI.nowWithOffset) configFactory.persist(userGroups, SnodeAPI.nowWithOffset)
@ -899,38 +894,44 @@ class GroupManagerV2Impl @Inject constructor(
) )
} }
override suspend fun setName(groupId: AccountId, newName: String): Unit = withContext(dispatcher) { override suspend fun setName(groupId: AccountId, newName: String): Unit =
val adminKey = requireAdminAccess(groupId) withContext(dispatcher) {
val adminKey = requireAdminAccess(groupId)
configFactory.getGroupInfoConfig(groupId)?.use { infoConfig -> configFactory.getGroupInfoConfig(groupId)?.use { infoConfig ->
infoConfig.setName(newName) infoConfig.setName(newName)
configFactory.persist(infoConfig, SnodeAPI.nowWithOffset, forPublicKey = groupId.hexString) configFactory.persist(
} infoConfig,
SnodeAPI.nowWithOffset,
val groupDestination = Destination.ClosedGroup(groupId.hexString) forPublicKey = groupId.hexString
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(groupDestination)
val timestamp = SnodeAPI.nowWithOffset
val signature = SodiumUtilities.sign(
buildInfoChangeVerifier(GroupUpdateInfoChangeMessage.Type.NAME, timestamp),
adminKey
)
val message = GroupUpdated(
GroupUpdateMessage.newBuilder()
.setInfoChangeMessage(
GroupUpdateInfoChangeMessage.newBuilder()
.setUpdatedName(newName)
.setType(GroupUpdateInfoChangeMessage.Type.NAME)
.setAdminSignature(ByteString.copyFrom(signature))
) )
.build() }
).apply {
sentTimestamp = timestamp
}
MessageSender.sendNonDurably(message, Address.fromSerialized(groupId.hexString), false).await() val groupDestination = Destination.ClosedGroup(groupId.hexString)
storage.insertGroupInfoChange(message, groupId) ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(groupDestination)
} val timestamp = SnodeAPI.nowWithOffset
val signature = SodiumUtilities.sign(
buildInfoChangeVerifier(GroupUpdateInfoChangeMessage.Type.NAME, timestamp),
adminKey
)
val message = GroupUpdated(
GroupUpdateMessage.newBuilder()
.setInfoChangeMessage(
GroupUpdateInfoChangeMessage.newBuilder()
.setUpdatedName(newName)
.setType(GroupUpdateInfoChangeMessage.Type.NAME)
.setAdminSignature(ByteString.copyFrom(signature))
)
.build()
).apply {
sentTimestamp = timestamp
}
MessageSender.sendNonDurably(message, Address.fromSerialized(groupId.hexString), false)
.await()
storage.insertGroupInfoChange(message, groupId)
}
override suspend fun requestMessageDeletion( override suspend fun requestMessageDeletion(
groupId: AccountId, groupId: AccountId,
@ -943,7 +944,8 @@ class GroupManagerV2Impl @Inject constructor(
// meanwhile, if we are admin we can just delete those messages from the group swarm, and otherwise // meanwhile, if we are admin we can just delete those messages from the group swarm, and otherwise
// the admins can pick up the group message and delete the messages on our behalf. // the admins can pick up the group message and delete the messages on our behalf.
val userGroups = requireNotNull(configFactory.userGroups) { "User groups config is not available" } val userGroups =
requireNotNull(configFactory.userGroups) { "User groups config is not available" }
val group = requireNotNull(userGroups.getClosedGroup(groupId.hexString)) { val group = requireNotNull(userGroups.getClosedGroup(groupId.hexString)) {
"Group doesn't exist" "Group doesn't exist"
} }
@ -952,21 +954,33 @@ class GroupManagerV2Impl @Inject constructor(
// Check if we can actually delete these messages // Check if we can actually delete these messages
check( check(
group.hasAdminKey() || group.hasAdminKey() ||
storage.ensureMessageHashesAreSender(messageHashes.toSet(), userPubKey, groupId.hexString) storage.ensureMessageHashesAreSender(
messageHashes.toSet(),
userPubKey,
groupId.hexString
)
) { ) {
"Cannot delete messages that are not sent by us" "Cannot delete messages that are not sent by us"
} }
// If we are admin, we can delete the messages from the group swarm // If we are admin, we can delete the messages from the group swarm
group.adminKey?.let { adminKey -> group.adminKey?.let { adminKey ->
deleteMessageFromGroupSwarm(groupId, OwnedSwarmAuth.ofClosedGroup(groupId, adminKey), messageHashes) deleteMessageFromGroupSwarm(
groupId,
OwnedSwarmAuth.ofClosedGroup(groupId, adminKey),
messageHashes
)
} }
// Construct a message to ask members to delete the messages, sign if we are admin, then send // Construct a message to ask members to delete the messages, sign if we are admin, then send
val timestamp = SnodeAPI.nowWithOffset val timestamp = SnodeAPI.nowWithOffset
val signature = group.adminKey?.let { key -> val signature = group.adminKey?.let { key ->
SodiumUtilities.sign( SodiumUtilities.sign(
buildDeleteMemberContentSignature(memberIds = emptyList(), messageHashes, timestamp), buildDeleteMemberContentSignature(
memberIds = emptyList(),
messageHashes,
timestamp
),
key key
) )
} }
@ -976,7 +990,11 @@ class GroupManagerV2Impl @Inject constructor(
GroupUpdateDeleteMemberContentMessage.newBuilder() GroupUpdateDeleteMemberContentMessage.newBuilder()
.addAllMessageHashes(messageHashes) .addAllMessageHashes(messageHashes)
.let { .let {
if (signature != null) it.setAdminSignature(ByteString.copyFrom(signature)) if (signature != null) it.setAdminSignature(
ByteString.copyFrom(
signature
)
)
else it else it
} }
) )
@ -995,9 +1013,10 @@ class GroupManagerV2Impl @Inject constructor(
sender: AccountId, sender: AccountId,
senderIsVerifiedAdmin: Boolean, senderIsVerifiedAdmin: Boolean,
): Unit = withContext(dispatcher) { ): Unit = withContext(dispatcher) {
val threadId = requireNotNull(storage.getThreadId(Address.fromSerialized(groupId.hexString))) { val threadId =
"No thread ID found for the group" requireNotNull(storage.getThreadId(Address.fromSerialized(groupId.hexString))) {
} "No thread ID found for the group"
}
val hashes = deleteMemberContent.messageHashesList val hashes = deleteMemberContent.messageHashesList
val memberIds = deleteMemberContent.memberSessionIdsList val memberIds = deleteMemberContent.memberSessionIdsList
@ -1006,7 +1025,12 @@ class GroupManagerV2Impl @Inject constructor(
if (senderIsVerifiedAdmin) { if (senderIsVerifiedAdmin) {
// We'll delete everything the admin says // We'll delete everything the admin says
storage.deleteMessagesByHash(threadId, hashes) storage.deleteMessagesByHash(threadId, hashes)
} else if (storage.ensureMessageHashesAreSender(hashes.toSet(), sender.hexString, groupId.hexString)) { } else if (storage.ensureMessageHashesAreSender(
hashes.toSet(),
sender.hexString,
groupId.hexString
)
) {
// ensure that all message hashes belong to user // ensure that all message hashes belong to user
// storage delete // storage delete
storage.deleteMessagesByHash(threadId, hashes) storage.deleteMessagesByHash(threadId, hashes)
@ -1023,8 +1047,17 @@ class GroupManagerV2Impl @Inject constructor(
if (!senderIsVerifiedAdmin && adminKey != null) { if (!senderIsVerifiedAdmin && adminKey != null) {
// If the deletion request comes from a non-admin, and we as an admin, will also delete // If the deletion request comes from a non-admin, and we as an admin, will also delete
// the content from the swarm, provided that the messages are actually sent by that user // the content from the swarm, provided that the messages are actually sent by that user
if (storage.ensureMessageHashesAreSender(hashes.toSet(), sender.hexString, groupId.hexString)) { if (storage.ensureMessageHashesAreSender(
deleteMessageFromGroupSwarm(groupId, OwnedSwarmAuth.ofClosedGroup(groupId, adminKey), hashes) hashes.toSet(),
sender.hexString,
groupId.hexString
)
) {
deleteMessageFromGroupSwarm(
groupId,
OwnedSwarmAuth.ofClosedGroup(groupId, adminKey),
hashes
)
} }
// The non-admin user shouldn't be able to delete other user's messages so we will // The non-admin user shouldn't be able to delete other user's messages so we will
@ -1032,7 +1065,11 @@ class GroupManagerV2Impl @Inject constructor(
} }
} }
private suspend fun deleteMessageFromGroupSwarm(groupId: AccountId, auth: OwnedSwarmAuth, hashes: List<String>) { private suspend fun deleteMessageFromGroupSwarm(
groupId: AccountId,
auth: OwnedSwarmAuth,
hashes: List<String>
) {
SnodeAPI.sendBatchRequest( SnodeAPI.sendBatchRequest(
groupId, SnodeAPI.buildAuthenticatedDeleteBatchInfo(auth, hashes) groupId, SnodeAPI.buildAuthenticatedDeleteBatchInfo(auth, hashes)
) )

View File

@ -277,14 +277,15 @@ extern "C"
JNIEXPORT jbyteArray JNICALL JNIEXPORT jbyteArray JNICALL
Java_network_loki_messenger_libsession_1util_GroupKeysConfig_supplementFor(JNIEnv *env, Java_network_loki_messenger_libsession_1util_GroupKeysConfig_supplementFor(JNIEnv *env,
jobject thiz, jobject thiz,
jstring user_session_id) { jobjectArray j_user_session_ids) {
std::lock_guard lock{util::util_mutex_}; std::lock_guard lock{util::util_mutex_};
auto ptr = ptrToKeys(env, thiz); auto ptr = ptrToKeys(env, thiz);
auto string = env->GetStringUTFChars(user_session_id, nullptr); std::vector<std::string> user_session_ids;
auto supplement = ptr->key_supplement(string); for (int i = 0, size = env->GetArrayLength(j_user_session_ids); i < size; i++) {
auto supplement_jbytearray = util::bytes_from_ustring(env, supplement); user_session_ids.push_back(util::string_from_jstring(env, (jstring)(env->GetObjectArrayElement(j_user_session_ids, i))));
env->ReleaseStringUTFChars(user_session_id, string); }
return supplement_jbytearray; auto supplement = ptr->key_supplement(user_session_ids);
return util::bytes_from_ustring(env, supplement);
} }
extern "C" extern "C"
JNIEXPORT jint JNICALL JNIEXPORT jint JNICALL

View File

@ -427,7 +427,7 @@ interface ReadableGroupKeysConfig {
fun dump(): ByteArray fun dump(): ByteArray
fun needsRekey(): Boolean fun needsRekey(): Boolean
fun pendingKey(): ByteArray? fun pendingKey(): ByteArray?
fun supplementFor(userSessionId: String): ByteArray fun supplementFor(userSessionIds: List<String>): ByteArray
fun pendingConfig(): ByteArray? fun pendingConfig(): ByteArray?
fun currentHashes(): List<String> fun currentHashes(): List<String>
fun encrypt(plaintext: ByteArray): ByteArray fun encrypt(plaintext: ByteArray): ByteArray
@ -484,7 +484,11 @@ class GroupKeysConfig private constructor(pointer: Long): ConfigSig(pointer), Mu
membersPtr: Long): Boolean membersPtr: Long): Boolean
external override fun needsRekey(): Boolean external override fun needsRekey(): Boolean
external override fun pendingKey(): ByteArray? external override fun pendingKey(): ByteArray?
external override fun supplementFor(userSessionId: String): ByteArray private external fun supplementFor(userSessionIds: Array<String>): ByteArray
override fun supplementFor(userSessionIds: List<String>): ByteArray {
return supplementFor(userSessionIds.toTypedArray())
}
external override fun pendingConfig(): ByteArray? external override fun pendingConfig(): ByteArray?
external override fun currentHashes(): List<String> external override fun currentHashes(): List<String>
external fun rekey(infoPtr: Long, membersPtr: Long): ByteArray external fun rekey(infoPtr: Long, membersPtr: Long): ByteArray

View File

@ -0,0 +1,170 @@
package org.session.libsession.messaging.jobs
import android.widget.Toast
import com.google.protobuf.ByteString
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.withContext
import org.session.libsession.R
import org.session.libsession.messaging.MessagingModuleConfiguration
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.Data
import org.session.libsession.messaging.utilities.MessageAuthentication.buildGroupInviteSignature
import org.session.libsession.messaging.utilities.SodiumUtilities
import org.session.libsession.snode.SnodeAPI
import org.session.libsession.snode.utilities.await
import org.session.libsession.utilities.StringSubstitutionConstants.GROUP_NAME_KEY
import org.session.libsession.utilities.StringSubstitutionConstants.NAME_KEY
import org.session.libsession.utilities.StringSubstitutionConstants.OTHER_NAME_KEY
import org.session.libsession.utilities.truncateIdForDisplay
import org.session.libsignal.protos.SignalServiceProtos.DataMessage.GroupUpdateInviteMessage
import org.session.libsignal.protos.SignalServiceProtos.DataMessage.GroupUpdateMessage
import org.session.libsignal.utilities.AccountId
import org.session.libsignal.utilities.prettifiedDescription
class InviteContactsJob(val groupSessionId: String, val memberSessionIds: Array<String>) : Job {
companion object {
const val KEY = "InviteContactJob"
private const val GROUP = "group"
private const val MEMBER = "member"
}
override var delegate: JobDelegate? = null
override var id: String? = null
override var failureCount: Int = 0
override val maxFailureCount: Int = 1
override suspend fun execute(dispatcherName: String) {
val configs = MessagingModuleConfiguration.shared.configFactory
val adminKey = requireNotNull(configs.withUserConfigs { it.userGroups.getClosedGroup(groupSessionId) }?.adminKey) {
"User must be admin of group to invite"
}
val sessionId = AccountId(groupSessionId)
coroutineScope {
val requests = memberSessionIds.map { memberSessionId ->
async {
runCatching {
// Make the request for this member
val memberId = AccountId(memberSessionId)
val (groupName, subAccount) = configs.withMutableGroupConfigs(sessionId) { configs ->
configs.groupMembers.set(
configs.groupMembers.getOrConstruct(
memberSessionId
).setInvited()
)
configs.groupInfo.getName() to configs.groupKeys.makeSubAccount(memberId)
}
val timestamp = SnodeAPI.nowWithOffset
val signature = SodiumUtilities.sign(
buildGroupInviteSignature(memberId, timestamp),
adminKey
)
val groupInvite = GroupUpdateInviteMessage.newBuilder()
.setGroupSessionId(groupSessionId)
.setMemberAuthData(ByteString.copyFrom(subAccount))
.setAdminSignature(ByteString.copyFrom(signature))
.setName(groupName)
val message = GroupUpdateMessage.newBuilder()
.setInviteMessage(groupInvite)
.build()
val update = GroupUpdated(message).apply {
sentTimestamp = timestamp
}
MessageSender.send(update, Destination.Contact(memberSessionId), false)
.await()
}
}
}
val results = memberSessionIds.zip(requests.awaitAll())
configs.withMutableGroupConfigs(sessionId) { configs ->
results.forEach { (memberSessionId, result) ->
if (result.isFailure) {
configs.groupMembers.get(memberSessionId)?.let { member ->
configs.groupMembers.set(member.setInviteFailed())
}
}
}
}
val groupName = configs.withGroupConfigs(sessionId) { it.groupInfo.getName() }
val failures = results.filter { it.second.isFailure }
// if there are failed invites, display a message
// assume job "success" even if we fail, the state of invites is tracked outside of this job
if (failures.isNotEmpty()) {
// show the failure toast
val storage = MessagingModuleConfiguration.shared.storage
val toaster = MessagingModuleConfiguration.shared.toaster
when (failures.size) {
1 -> {
val (memberId, _) = failures.first()
val firstString = storage.getContactWithAccountID(memberId)?.name
?: truncateIdForDisplay(memberId)
withContext(Dispatchers.Main) {
toaster.toast(R.string.groupInviteFailedUser, Toast.LENGTH_LONG,
mapOf(
NAME_KEY to firstString,
GROUP_NAME_KEY to groupName
)
)
}
}
2 -> {
val (first, second) = failures
val firstString = first.first.let { storage.getContactWithAccountID(it) }?.name
?: truncateIdForDisplay(first.first)
val secondString = second.first.let { storage.getContactWithAccountID(it) }?.name
?: truncateIdForDisplay(second.first)
withContext(Dispatchers.Main) {
toaster.toast(R.string.groupInviteFailedTwo, Toast.LENGTH_LONG,
mapOf(
NAME_KEY to firstString,
OTHER_NAME_KEY to secondString,
GROUP_NAME_KEY to groupName
)
)
}
}
else -> {
val first = failures.first()
val firstString = first.first.let { storage.getContactWithAccountID(it) }?.name
?: truncateIdForDisplay(first.first)
val remaining = failures.size - 1
withContext(Dispatchers.Main) {
toaster.toast(R.string.groupInviteFailedMultiple, Toast.LENGTH_LONG,
mapOf(
NAME_KEY to firstString,
OTHER_NAME_KEY to remaining.toString(),
GROUP_NAME_KEY to groupName
)
)
}
}
}
}
}
}
override fun serialize(): Data =
Data.Builder()
.putString(GROUP, groupSessionId)
.putStringArray(MEMBER, memberSessionIds)
.build()
override fun getFactoryKey(): String = KEY
}