mirror of
https://github.com/oxen-io/session-android.git
synced 2024-11-25 11:05:25 +00:00
Put back invite contacts job
This commit is contained in:
parent
771d63e902
commit
b9f5f940a1
@ -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
|
||||||
|
@ -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,56 +219,38 @@ 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 = keysConfig.namespace(),
|
namespace = Namespace.ENCRYPTION_KEYS(),
|
||||||
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 = timestamp
|
timestamp = SnodeAPI.nowWithOffset,
|
||||||
),
|
),
|
||||||
auth = groupAuth,
|
auth = groupAuth,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
keysConfig.rekey(infoConfig, membersConfig)
|
configs.rekeys()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
newMembers.map { configs.groupKeys.makeSubAccount(group) }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Call un-revocate API on new members, in case they have been removed before
|
// Call un-revocate API on new members, in case they have been removed before
|
||||||
batchRequests += SnodeAPI.buildAuthenticatedUnrevokeSubKeyBatchRequest(
|
batchRequests += SnodeAPI.buildAuthenticatedUnrevokeSubKeyBatchRequest(
|
||||||
groupAdminAuth = groupAuth,
|
groupAdminAuth = groupAuth,
|
||||||
subAccountTokens = newMembers.map(keysConfig::getSubAccountToken)
|
subAccountTokens = subAccountTokens
|
||||||
)
|
)
|
||||||
|
|
||||||
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
|
// Call the API
|
||||||
val swarmNode = SnodeAPI.getSingleTargetSnode(group.hexString).await()
|
val swarmNode = SnodeAPI.getSingleTargetSnode(group.hexString).await()
|
||||||
val response = SnodeAPI.getBatchResponse(swarmNode, group.hexString, batchRequests)
|
val response = SnodeAPI.getBatchResponse(swarmNode, group.hexString, batchRequests)
|
||||||
@ -273,9 +258,6 @@ class GroupManagerV2Impl @Inject constructor(
|
|||||||
// Make sure every request is successful
|
// Make sure every request is successful
|
||||||
response.requireAllRequestsSuccessful("Failed to invite members")
|
response.requireAllRequestsSuccessful("Failed to invite members")
|
||||||
|
|
||||||
// Persist the keys config
|
|
||||||
configFactory.saveGroupConfigs(keysConfig, infoConfig, membersConfig)
|
|
||||||
|
|
||||||
// Send the invitation message to the new members
|
// Send the invitation message to the new members
|
||||||
JobQueue.shared.add(
|
JobQueue.shared.add(
|
||||||
InviteContactsJob(
|
InviteContactsJob(
|
||||||
@ -285,6 +267,7 @@ class GroupManagerV2Impl @Inject constructor(
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Send a member change message to the group
|
// Send a member change message to the group
|
||||||
|
val timestamp = SnodeAPI.nowWithOffset
|
||||||
val signature = SodiumUtilities.sign(
|
val signature = SodiumUtilities.sign(
|
||||||
buildMemberChangeSignature(GroupUpdateMemberChangeMessage.Type.ADDED, timestamp),
|
buildMemberChangeSignature(GroupUpdateMemberChangeMessage.Type.ADDED, timestamp),
|
||||||
adminKey
|
adminKey
|
||||||
@ -302,11 +285,7 @@ class GroupManagerV2Impl @Inject constructor(
|
|||||||
).apply { this.sentTimestamp = timestamp }
|
).apply { this.sentTimestamp = timestamp }
|
||||||
MessageSender.send(updatedMessage, Address.fromSerialized(group.hexString))
|
MessageSender.send(updatedMessage, Address.fromSerialized(group.hexString))
|
||||||
storage.insertGroupInfoChange(updatedMessage, group)
|
storage.insertGroupInfoChange(updatedMessage, group)
|
||||||
|
|
||||||
group
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
override suspend fun removeMembers(
|
override suspend fun removeMembers(
|
||||||
groupAccountId: AccountId,
|
groupAccountId: AccountId,
|
||||||
@ -383,7 +362,8 @@ 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 =
|
||||||
|
withContext(dispatcher) {
|
||||||
val adminKey = requireAdminAccess(group)
|
val adminKey = requireAdminAccess(group)
|
||||||
|
|
||||||
configFactory.withGroupConfigsOrNull(group) { info, membersConfig, keys ->
|
configFactory.withGroupConfigsOrNull(group) { info, membersConfig, keys ->
|
||||||
@ -624,12 +604,14 @@ class GroupManagerV2Impl @Inject constructor(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun respondToInvitation(groupId: AccountId, approved: Boolean) = withContext(dispatcher) {
|
override suspend fun respondToInvitation(groupId: AccountId, approved: Boolean) =
|
||||||
|
withContext(dispatcher) {
|
||||||
val groups = requireNotNull(configFactory.userGroups) {
|
val groups = requireNotNull(configFactory.userGroups) {
|
||||||
"User groups config is not available"
|
"User groups config is not available"
|
||||||
}
|
}
|
||||||
|
|
||||||
val threadId = checkNotNull(storage.getThreadId(Address.fromSerialized(groupId.hexString))) {
|
val threadId =
|
||||||
|
checkNotNull(storage.getThreadId(Address.fromSerialized(groupId.hexString))) {
|
||||||
"No thread has been created for the group"
|
"No thread has been created for the group"
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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(
|
||||||
|
group.copy(
|
||||||
authData = null,
|
authData = null,
|
||||||
adminKey = null,
|
adminKey = null,
|
||||||
name = groupName
|
name = groupName
|
||||||
))
|
)
|
||||||
|
)
|
||||||
|
|
||||||
configFactory.persist(userGroups, SnodeAPI.nowWithOffset)
|
configFactory.persist(userGroups, SnodeAPI.nowWithOffset)
|
||||||
|
|
||||||
@ -899,12 +894,17 @@ class GroupManagerV2Impl @Inject constructor(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun setName(groupId: AccountId, newName: String): Unit = withContext(dispatcher) {
|
override suspend fun setName(groupId: AccountId, newName: String): Unit =
|
||||||
|
withContext(dispatcher) {
|
||||||
val adminKey = requireAdminAccess(groupId)
|
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,
|
||||||
|
forPublicKey = groupId.hexString
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val groupDestination = Destination.ClosedGroup(groupId.hexString)
|
val groupDestination = Destination.ClosedGroup(groupId.hexString)
|
||||||
@ -928,7 +928,8 @@ class GroupManagerV2Impl @Inject constructor(
|
|||||||
sentTimestamp = timestamp
|
sentTimestamp = timestamp
|
||||||
}
|
}
|
||||||
|
|
||||||
MessageSender.sendNonDurably(message, Address.fromSerialized(groupId.hexString), false).await()
|
MessageSender.sendNonDurably(message, Address.fromSerialized(groupId.hexString), false)
|
||||||
|
.await()
|
||||||
storage.insertGroupInfoChange(message, groupId)
|
storage.insertGroupInfoChange(message, groupId)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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,7 +1013,8 @@ 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 =
|
||||||
|
requireNotNull(storage.getThreadId(Address.fromSerialized(groupId.hexString))) {
|
||||||
"No thread ID found for the group"
|
"No thread ID found for the group"
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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)
|
||||||
)
|
)
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user