From abaa76520734b3090f5ab8d7041a5806f8d1b4e3 Mon Sep 17 00:00:00 2001 From: 0x330a <92654767+0x330a@users.noreply.github.com> Date: Mon, 6 Nov 2023 17:59:05 +1100 Subject: [PATCH] feat: invite contact uses multiple contacts now, fix compile issues and write to members based on success / failure state and pending --- .../securesms/ApplicationContext.java | 2 +- .../conversation/v2/ConversationActivityV2.kt | 3 +- .../securesms/database/SessionJobDatabase.kt | 10 +- .../securesms/database/Storage.kt | 10 +- .../messaging/MessagingModuleConfiguration.kt | 2 +- .../messaging/jobs/InviteContactJob.kt | 86 ----------- .../messaging/jobs/InviteContactsJob.kt | 146 ++++++++++++++++++ .../libsession/messaging/jobs/JobQueue.kt | 4 +- 8 files changed, 162 insertions(+), 101 deletions(-) delete mode 100644 libsession/src/main/java/org/session/libsession/messaging/jobs/InviteContactJob.kt create mode 100644 libsession/src/main/java/org/session/libsession/messaging/jobs/InviteContactsJob.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java index 1facc4a68..9ee835545 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java @@ -222,7 +222,7 @@ public class ApplicationContext extends Application implements DefaultLifecycleO messageDataProvider, ()-> KeyPairUtilities.INSTANCE.getUserED25519KeyPair(this), configFactory - ); + ); callMessageProcessor = new CallMessageProcessor(this, textSecurePreferences, ProcessLifecycleOwner.get().getLifecycle(), storage); Log.i(TAG, "onCreate()"); startKovenant(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt index 9f0a38749..a22a0bcb9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt @@ -241,7 +241,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe private val viewModel: ConversationViewModel by viewModels { var threadId = intent.getLongExtra(THREAD_ID, -1L) if (threadId == -1L) { - intent.getParcelableExtra(ADDRESS, Address::class.java)?.let { it -> + intent.getParcelableExtra
(ADDRESS)?.let { it -> threadId = threadDb.getThreadIdIfExistsFor(it.serialize()) if (threadId == -1L) { val sessionId = SessionId(it.serialize()) @@ -423,6 +423,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe updatePlaceholder() setUpBlockedBanner() binding!!.searchBottomBar.setEventListener(this) + binding!!.toolbarContent.profilePictureView.setOnClickListener(this) updateSendAfterApprovalText() showOrHideInputIfNeeded() setUpMessageRequestsBar() diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/SessionJobDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/SessionJobDatabase.kt index 43567bd5d..c400a7e71 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/SessionJobDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/SessionJobDatabase.kt @@ -7,7 +7,7 @@ import org.session.libsession.messaging.jobs.AttachmentDownloadJob import org.session.libsession.messaging.jobs.AttachmentUploadJob import org.session.libsession.messaging.jobs.BackgroundGroupAddJob import org.session.libsession.messaging.jobs.GroupAvatarDownloadJob -import org.session.libsession.messaging.jobs.InviteContactJob +import org.session.libsession.messaging.jobs.InviteContactsJob import org.session.libsession.messaging.jobs.Job import org.session.libsession.messaging.jobs.MessageReceiveJob import org.session.libsession.messaging.jobs.MessageSendJob @@ -74,11 +74,11 @@ class SessionJobDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa return result.firstOrNull { job -> job.attachmentID == attachmentID } } - fun getGroupInviteJob(groupSessionId: String, memberSessionId: String): InviteContactJob? { + fun getGroupInviteJob(groupSessionId: String, memberSessionId: String): InviteContactsJob? { val database = databaseHelper.readableDatabase - return database.getAll(sessionJobTable, "$jobType = ?", arrayOf(InviteContactJob.KEY)) { cursor -> - jobFromCursor(cursor) as? InviteContactJob - }.firstOrNull { it != null && it.groupSessionId == groupSessionId && it.memberSessionId == memberSessionId } + return database.getAll(sessionJobTable, "$jobType = ?", arrayOf(InviteContactsJob.KEY)) { cursor -> + jobFromCursor(cursor) as? InviteContactsJob + }.firstOrNull { it != null && it.groupSessionId == groupSessionId && it.memberSessionIds.contains(memberSessionId) } } fun getMessageSendJob(messageSendJobID: String): MessageSendJob? { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt index 2323b4997..a3b4ca64a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt @@ -27,7 +27,7 @@ import org.session.libsession.messaging.jobs.AttachmentUploadJob import org.session.libsession.messaging.jobs.BackgroundGroupAddJob import org.session.libsession.messaging.jobs.ConfigurationSyncJob import org.session.libsession.messaging.jobs.GroupAvatarDownloadJob -import org.session.libsession.messaging.jobs.InviteContactJob +import org.session.libsession.messaging.jobs.InviteContactsJob import org.session.libsession.messaging.jobs.Job import org.session.libsession.messaging.jobs.JobQueue import org.session.libsession.messaging.jobs.MessageReceiveJob @@ -1026,10 +1026,10 @@ open class Storage( setRecipientApprovedMe(groupRecipient, true) setRecipientApproved(groupRecipient, true) pollerFactory.updatePollers() - members.forEach { contact -> - val job = InviteContactJob(group.groupSessionId.hexString(), contact.sessionID) - JobQueue.shared.add(job) - } + + val memberArray = members.map(Contact::sessionID).toTypedArray() + val job = InviteContactsJob(group.groupSessionId.hexString(), memberArray) + JobQueue.shared.add(job) return Optional.of(groupRecipient) } catch (e: Exception) { Log.e("Group Config", e) diff --git a/libsession/src/main/java/org/session/libsession/messaging/MessagingModuleConfiguration.kt b/libsession/src/main/java/org/session/libsession/messaging/MessagingModuleConfiguration.kt index 3d48325bf..6dac1426e 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/MessagingModuleConfiguration.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/MessagingModuleConfiguration.kt @@ -13,7 +13,7 @@ class MessagingModuleConfiguration( val device: Device, val messageDataProvider: MessageDataProvider, val getUserED25519KeyPair: () -> KeyPair?, - val configFactory: ConfigFactoryProtocol + val configFactory: ConfigFactoryProtocol, ) { companion object { diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/InviteContactJob.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/InviteContactJob.kt deleted file mode 100644 index 9ce3def63..000000000 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/InviteContactJob.kt +++ /dev/null @@ -1,86 +0,0 @@ -package org.session.libsession.messaging.jobs - -import com.google.protobuf.ByteString -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.SodiumUtilities -import org.session.libsession.snode.SnodeAPI -import org.session.libsignal.protos.SignalServiceProtos.DataMessage.GroupUpdateInviteMessage -import org.session.libsignal.protos.SignalServiceProtos.DataMessage.GroupUpdateMessage -import org.session.libsignal.protos.SignalServiceProtos.DataMessage.LokiProfile -import org.session.libsignal.utilities.Log -import org.session.libsignal.utilities.SessionId - -class InviteContactJob(val groupSessionId: String, val memberSessionId: String): Job { - - sealed class InviteError(message: String): Exception(message) { - object NO_GROUP_KEYS: InviteError("No group keys config for this group") - } - - 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 delegate = delegate ?: return - val configs = MessagingModuleConfiguration.shared.configFactory - val storage = MessagingModuleConfiguration.shared.storage - val adminKey = configs.userGroups?.getClosedGroup(groupSessionId)?.adminKey - ?: return delegate.handleJobFailedPermanently(this, dispatcherName, NullPointerException("No admin key")) - val subAccount = configs.getGroupKeysConfig(SessionId.from(groupSessionId))?.use { keys -> - keys.makeSubAccount(SessionId.from(memberSessionId)) - } ?: return delegate.handleJobFailedPermanently(this, dispatcherName, InviteError.NO_GROUP_KEYS) - val timestamp = SnodeAPI.nowWithOffset - val messageToSign = "INVITE$memberSessionId$timestamp" - val signature = SodiumUtilities.sign(messageToSign.toByteArray(), adminKey) - val userProfile = storage.getUserProfile() - val lokiProfile = LokiProfile.newBuilder() - .setDisplayName(userProfile.displayName) - if (userProfile.profilePictureURL?.isNotEmpty() == true) { - lokiProfile.profilePicture = userProfile.profilePictureURL - } - val groupInvite = GroupUpdateInviteMessage.newBuilder() - .setGroupSessionId(groupSessionId) - .setMemberAuthData(ByteString.copyFrom(subAccount)) - .setAdminSignature(ByteString.copyFrom(signature)) - .setName(userProfile.displayName) - .setProfile(lokiProfile.build()) - if (userProfile.profileKey?.isNotEmpty() == true) { - groupInvite.profileKey = ByteString.copyFrom(userProfile.profileKey) - } - val message = GroupUpdateMessage.newBuilder() - .setInviteMessage(groupInvite) - .build() - val update = GroupUpdated(message).apply { - sentTimestamp = timestamp - } - try { - MessageSender.send(update, Destination.Contact(memberSessionId), false).get() - Log.d("InviteContactJob", "Sent invite message successfully") - delegate.handleJobSucceeded(this, dispatcherName) - } catch (e: Exception) { - Log.e("InviteContactJob", e) - delegate.handleJobFailed(this, dispatcherName, e) - } - - } - - override fun serialize(): Data = - Data.Builder() - .putString(GROUP, groupSessionId) - .putString(MEMBER, memberSessionId) - .build() - - override fun getFactoryKey(): String = KEY - -} \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/InviteContactsJob.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/InviteContactsJob.kt new file mode 100644 index 000000000..b9c6cfffd --- /dev/null +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/InviteContactsJob.kt @@ -0,0 +1,146 @@ +package org.session.libsession.messaging.jobs + +import com.google.protobuf.ByteString +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.withContext +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.SodiumUtilities +import org.session.libsession.snode.SnodeAPI +import org.session.libsignal.protos.SignalServiceProtos.DataMessage.GroupUpdateInviteMessage +import org.session.libsignal.protos.SignalServiceProtos.DataMessage.GroupUpdateMessage +import org.session.libsignal.protos.SignalServiceProtos.DataMessage.LokiProfile +import org.session.libsignal.utilities.SessionId +import org.session.libsignal.utilities.prettifiedDescription + +class InviteContactsJob(val groupSessionId: String, val memberSessionIds: Array) : 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 delegate = delegate ?: return + val configs = MessagingModuleConfiguration.shared.configFactory + val storage = MessagingModuleConfiguration.shared.storage + val adminKey = configs.userGroups?.getClosedGroup(groupSessionId)?.adminKey + ?: return delegate.handleJobFailedPermanently( + this, + dispatcherName, + NullPointerException("No admin key") + ) + + withContext(Dispatchers.IO) { + val sessionId = SessionId.from(groupSessionId) + val members = configs.getGroupMemberConfig(sessionId) + val info = configs.getGroupInfoConfig(sessionId) + val keys = configs.getGroupKeysConfig(sessionId, info, members, free = false) + + if (members == null || info == null || keys == null) { + return@withContext delegate.handleJobFailedPermanently( + this@InviteContactsJob, + dispatcherName, + NullPointerException("One of the group configs was null") + ) + } + + val requests = memberSessionIds.map { memberSessionId -> + async { + // Make the request for this member + val member = members.get(memberSessionId) ?: return@async run { + InviteResult.failure( + memberSessionId, + NullPointerException("No group member ${memberSessionId.prettifiedDescription()} in members config") + ) + } + members.set(member.copy(invitePending = true, inviteFailed = false)) + val subAccount = keys.makeSubAccount(SessionId.from(memberSessionId)) + + val timestamp = SnodeAPI.nowWithOffset + val messageToSign = "INVITE$memberSessionId$timestamp" + val signature = SodiumUtilities.sign(messageToSign.toByteArray(), adminKey) + val userProfile = storage.getUserProfile() + val lokiProfile = LokiProfile.newBuilder() + .setDisplayName(userProfile.displayName) + if (userProfile.profilePictureURL?.isNotEmpty() == true) { + lokiProfile.profilePicture = userProfile.profilePictureURL + } + val groupInvite = GroupUpdateInviteMessage.newBuilder() + .setGroupSessionId(groupSessionId) + .setMemberAuthData(ByteString.copyFrom(subAccount)) + .setAdminSignature(ByteString.copyFrom(signature)) + .setName(userProfile.displayName) + .setProfile(lokiProfile.build()) + if (userProfile.profileKey?.isNotEmpty() == true) { + groupInvite.profileKey = ByteString.copyFrom(userProfile.profileKey) + } + val message = GroupUpdateMessage.newBuilder() + .setInviteMessage(groupInvite) + .build() + val update = GroupUpdated(message).apply { + sentTimestamp = timestamp + } + try { + MessageSender.send(update, Destination.Contact(memberSessionId), false) + .get() + InviteResult.success(memberSessionId) + } catch (e: Exception) { + InviteResult.failure(memberSessionId, e) + } + } + } + val results = requests.awaitAll() + results.forEach { result -> + if (result.success) { + // update invite pending / invite failed + val toSet = members.get(result.memberSessionId)!!.copy( + inviteFailed = false, + invitePending = false + ) + members.set(toSet) + } else { + // update invite failed + val toSet = members.get(result.memberSessionId)?.copy( + inviteFailed = true, + invitePending = false + ) ?: return@forEach + members.set(toSet) + } + } + } + } + + @Suppress("DataClassPrivateConstructor") + data class InviteResult private constructor( + val memberSessionId: String, + val success: Boolean, + val error: Exception? = null + ) { + companion object { + fun success(memberSessionId: String) = InviteResult(memberSessionId, success = true) + fun failure(memberSessionId: String, error: Exception) = + InviteResult(memberSessionId, success = false, error) + } + } + + override fun serialize(): Data = + Data.Builder() + .putString(GROUP, groupSessionId) + .putStringArray(MEMBER, memberSessionIds) + .build() + + override fun getFactoryKey(): String = KEY + +} \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/JobQueue.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/JobQueue.kt index 2f37a363c..c38b3f143 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/JobQueue.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/JobQueue.kt @@ -126,7 +126,7 @@ class JobQueue : JobDelegate { is AttachmentUploadJob, is MessageSendJob, is ConfigurationSyncJob, - is InviteContactJob -> { + is InviteContactsJob -> { txQueue.send(job) } is RetrieveProfileAvatarJob, @@ -231,7 +231,7 @@ class JobQueue : JobDelegate { OpenGroupDeleteJob.KEY, RetrieveProfileAvatarJob.KEY, ConfigurationSyncJob.KEY, - InviteContactJob.KEY + InviteContactsJob.KEY ) allJobTypes.forEach { type -> resumePendingJobs(type)