From c36387175d88642840a2b83b6faedca45868d55d Mon Sep 17 00:00:00 2001
From: 0x330a <92654767+0x330a@users.noreply.github.com>
Date: Fri, 24 Feb 2023 17:05:33 +1100
Subject: [PATCH] fix: add some more contact syncing: nicknames, approved
 statuses, blocked statuses

---
 .../securesms/database/Storage.kt             | 29 ++++++++++++++-----
 .../securesms/home/UserDetailsBottomSheet.kt  |  7 ++---
 .../sskenvironment/ProfileManager.kt          |  4 +--
 .../util/ConfigurationMessageUtilities.kt     |  5 +++-
 .../libsession/database/StorageProtocol.kt    |  2 +-
 .../messaging/jobs/ConfigurationSyncJob.kt    | 26 ++++++++---------
 .../sending_receiving/pollers/Poller.kt       |  5 +---
 .../libsession/utilities/ProfileKeyUtil.java  |  9 +++---
 .../libsession/utilities/SSKEnvironment.kt    |  2 ++
 9 files changed, 52 insertions(+), 37 deletions(-)

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 3875a0fffb..74319eaabe 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt
@@ -7,6 +7,7 @@ import network.loki.messenger.libsession_util.Contacts
 import network.loki.messenger.libsession_util.ConversationVolatileConfig
 import network.loki.messenger.libsession_util.UserProfile
 import network.loki.messenger.libsession_util.util.Conversation
+import network.loki.messenger.libsession_util.util.UserPic
 import org.session.libsession.avatars.AvatarHelper
 import org.session.libsession.database.StorageProtocol
 import org.session.libsession.messaging.BlindedIdMapping
@@ -318,7 +319,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper, private val configF
         }
 
         // update pfp
-        if (userPic == null) {
+        if (userPic == UserPic.DEFAULT) {
             // clear picture if userPic is null
             TextSecurePreferences.setProfileKey(context, null)
             ProfileKeyUtil.setEncodedProfileKey(context, null)
@@ -335,10 +336,23 @@ class Storage(context: Context, helper: SQLCipherOpenHelper, private val configF
 
     private fun updateContacts(contacts: Contacts) {
         val extracted = contacts.all().toList()
+        val profileManager = SSKEnvironment.shared.profileManager
         extracted.forEach { contact ->
             val address = fromSerialized(contact.id)
             val recipient = Recipient.from(context, address, false)
-            setBlocked(listOf(recipient), contact.blocked)
+            setBlocked(listOf(recipient), contact.blocked, fromConfigUpdate = true)
+            setRecipientApproved(recipient, contact.approved)
+            setRecipientApprovedMe(recipient, contact.approvedMe)
+            profileManager.setName(context, recipient, contact.name)
+            profileManager.setNickname(context, recipient, contact.nickname)
+            if (contact.profilePicture != UserPic.DEFAULT) {
+                val (url, key) = contact.profilePicture
+                if (key.size != ProfileKeyUtil.PROFILE_KEY_BYTES) return@forEach
+                profileManager.setProfileKey(context, recipient, key)
+                profileManager.setUnidentifiedAccessMode(context, recipient, Recipient.UnidentifiedAccessMode.UNKNOWN)
+                profileManager.setProfilePictureURL(context, recipient, url)
+            }
+            Log.d("Loki-DBG", "Updated contact $contact")
         }
     }
 
@@ -356,7 +370,6 @@ class Storage(context: Context, helper: SQLCipherOpenHelper, private val configF
                     getOrCreateThreadIdFor("",null, it)
                 }
             }
-            Log.d("Loki-DBG", "Should update thread $threadId")
             if (threadId >= 0) {
                 markConversationAsRead(threadId, conversation.lastRead)
                 updateThread(threadId, false)
@@ -761,6 +774,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper, private val configF
 
     override fun setContact(contact: Contact) {
         DatabaseComponent.get(context).sessionContactDatabase().setContact(contact)
+        SSKEnvironment.shared.profileManager.contactUpdatedInternal(contact)
     }
 
     override fun getRecipientForThread(threadId: Long): Recipient? {
@@ -823,7 +837,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper, private val configF
                 threadDatabase.setHasSent(threadId, true)
             }
             if (contact.isBlocked == true) {
-                setBlocked(listOf(recipient), true)
+                setBlocked(listOf(recipient), true, fromConfigUpdate = true)
                 threadDatabase.deleteConversation(threadId)
             }
         }
@@ -1125,16 +1139,17 @@ class Storage(context: Context, helper: SQLCipherOpenHelper, private val configF
         DatabaseComponent.get(context).reactionDatabase().deleteMessageReactions(MessageId(messageId, mms))
     }
 
-    override fun setBlocked(recipients: List<Recipient>, isBlocked: Boolean) {
+    override fun setBlocked(recipients: List<Recipient>, isBlocked: Boolean, fromConfigUpdate: Boolean) {
         val recipientDb = DatabaseComponent.get(context).recipientDatabase()
         recipientDb.setBlocked(recipients, isBlocked)
         recipients.filter { it.isContactRecipient }.forEach { recipient ->
             configFactory.contacts?.upsertContact(recipient.address.serialize()) {
-                this.blocked = true
+                this.blocked = isBlocked
             }
         }
         val contactsConfig = configFactory.contacts ?: return
-        if (contactsConfig.needsDump()) {
+        if (contactsConfig.needsPush() && !fromConfigUpdate) {
+            Log.d("Loki-DBG", "Needs to push contacts after blocking ${recipients.map { it.toShortString() }}")
             ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(context)
         }
     }
diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/UserDetailsBottomSheet.kt b/app/src/main/java/org/thoughtcrime/securesms/home/UserDetailsBottomSheet.kt
index f3915abff6..86193f9619 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/home/UserDetailsBottomSheet.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/home/UserDetailsBottomSheet.kt
@@ -25,7 +25,6 @@ import org.session.libsession.utilities.recipients.Recipient
 import org.session.libsignal.utilities.IdPrefix
 import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
 import org.thoughtcrime.securesms.database.ThreadDatabase
-import org.thoughtcrime.securesms.dependencies.DatabaseComponent
 import org.thoughtcrime.securesms.mms.GlideApp
 import org.thoughtcrime.securesms.util.UiModeUtilities
 import javax.inject.Inject
@@ -131,10 +130,10 @@ class UserDetailsBottomSheet: BottomSheetDialogFragment() {
             newNickName = nicknameEditText.text.toString()
         }
         val publicKey = recipient.address.serialize()
-        val contactDB = DatabaseComponent.get(requireContext()).sessionContactDatabase()
-        val contact = contactDB.getContactWithSessionID(publicKey) ?: Contact(publicKey)
+        val storage = MessagingModuleConfiguration.shared.storage
+        val contact = storage.getContactWithSessionID(publicKey) ?: Contact(publicKey)
         contact.nickname = newNickName
-        contactDB.setContact(contact)
+        storage.setContact(contact)
         nameTextView.text = recipient.name ?: publicKey // Uses the Contact API internally
     }
 
diff --git a/app/src/main/java/org/thoughtcrime/securesms/sskenvironment/ProfileManager.kt b/app/src/main/java/org/thoughtcrime/securesms/sskenvironment/ProfileManager.kt
index 8d115aa4b4..82bab07448 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/sskenvironment/ProfileManager.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/sskenvironment/ProfileManager.kt
@@ -82,7 +82,7 @@ class ProfileManager(private val context: Context, private val configFactory: Co
         database.setUnidentifiedAccessMode(recipient, unidentifiedAccessMode)
     }
 
-    private fun contactUpdatedInternal(contact: Contact) {
+    override fun contactUpdatedInternal(contact: Contact) {
         val contactConfig = configFactory.contacts ?: return
         contactConfig.upsertContact(contact.sessionID) {
             this.name = contact.name.orEmpty()
@@ -93,7 +93,7 @@ class ProfileManager(private val context: Context, private val configFactory: Co
                 this.profilePicture = UserPic(url, key)
             }
         }
-        if (contactConfig.needsDump()) {
+        if (contactConfig.needsPush()) {
             ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(context)
         }
     }
diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/ConfigurationMessageUtilities.kt b/app/src/main/java/org/thoughtcrime/securesms/util/ConfigurationMessageUtilities.kt
index 54800d4883..c98f512b87 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/util/ConfigurationMessageUtilities.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/util/ConfigurationMessageUtilities.kt
@@ -70,7 +70,10 @@ object ConfigurationMessageUtilities {
             // don't schedule job if we already have one
             val ourDestination = Destination.Contact(userPublicKey)
             val currentStorageJob = storage.getConfigSyncJob(ourDestination)
-            if (currentStorageJob != null && !(currentStorageJob as ConfigurationSyncJob).isRunning.get()) return Promise.ofFail(NullPointerException("A job is already pending or in progress, don't schedule another job"))
+            if (currentStorageJob != null) {
+                (currentStorageJob as ConfigurationSyncJob).shouldRunAgain.set(true)
+                return Promise.ofFail(NullPointerException("A job is already pending or in progress, don't schedule another job"))
+            }
             val newConfigSync = ConfigurationSyncJob(ourDestination)
             Log.d("Loki", "Scheduling new ConfigurationSyncJob")
             JobQueue.shared.add(newConfigSync)
diff --git a/libsession/src/main/java/org/session/libsession/database/StorageProtocol.kt b/libsession/src/main/java/org/session/libsession/database/StorageProtocol.kt
index ef7eec8f0e..3f246af80e 100644
--- a/libsession/src/main/java/org/session/libsession/database/StorageProtocol.kt
+++ b/libsession/src/main/java/org/session/libsession/database/StorageProtocol.kt
@@ -204,7 +204,7 @@ interface StorageProtocol {
     fun removeReaction(emoji: String, messageTimestamp: Long, author: String, notifyUnread: Boolean)
     fun updateReactionIfNeeded(message: Message, sender: String, openGroupSentTimestamp: Long)
     fun deleteReactions(messageId: Long, mms: Boolean)
-    fun setBlocked(recipients: List<Recipient>, isBlocked: Boolean)
+    fun setBlocked(recipients: List<Recipient>, isBlocked: Boolean, fromConfigUpdate: Boolean = false)
     fun blockedContacts(): List<Recipient>
 
     // Shared configs
diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/ConfigurationSyncJob.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/ConfigurationSyncJob.kt
index c804ba002d..03dcbd8c05 100644
--- a/libsession/src/main/java/org/session/libsession/messaging/jobs/ConfigurationSyncJob.kt
+++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/ConfigurationSyncJob.kt
@@ -21,18 +21,12 @@ data class ConfigurationSyncJob(val destination: Destination): Job {
     override var failureCount: Int = 0
     override val maxFailureCount: Int = 1
 
-    val isRunning = AtomicBoolean(false)
+    val shouldRunAgain = AtomicBoolean(false)
 
-    suspend fun wrap(body: suspend ()->Unit) {
-        isRunning.set(true)
-        body()
-        isRunning.set(false)
-    }
-
-
-    override suspend fun execute(dispatcherName: String) = wrap {
+    override suspend fun execute(dispatcherName: String) {
+        val storage = MessagingModuleConfiguration.shared.storage
         val userEdKeyPair = MessagingModuleConfiguration.shared.getUserED25519KeyPair()
-        val userPublicKey = MessagingModuleConfiguration.shared.storage.getUserPublicKey()
+        val userPublicKey = storage.getUserPublicKey()
         val delegate = delegate
         if (destination is Destination.ClosedGroup // TODO: closed group configs will be handled in closed group feature
             // if we haven't enabled the new configs don't run
@@ -47,7 +41,7 @@ data class ConfigurationSyncJob(val destination: Destination): Job {
             || (destination is Destination.Contact && destination.publicKey != userPublicKey)
         ) {
             Log.w(TAG, "No need to run config sync job, TODO")
-            return@wrap delegate?.handleJobSucceeded(this, dispatcherName) ?: Unit
+            return delegate?.handleJobSucceeded(this, dispatcherName) ?: Unit
         }
 
         // configFactory singleton instance will come in handy for modifying hashes and fetching configs for namespace etc
@@ -61,7 +55,7 @@ data class ConfigurationSyncJob(val destination: Destination): Job {
         ).filter { config -> config.needsPush() }
 
         // don't run anything if we don't need to push anything
-        if (configsRequiringPush.isEmpty()) return@wrap delegate.handleJobSucceeded(this, dispatcherName)
+        if (configsRequiringPush.isEmpty()) return delegate.handleJobSucceeded(this, dispatcherName)
 
         // allow null results here so the list index matches configsRequiringPush
         val batchObjects: List<Pair<SharedConfigurationMessage, SnodeAPI.SnodeBatchRequestInfo>?> = configsRequiringPush.map { config ->
@@ -88,7 +82,7 @@ data class ConfigurationSyncJob(val destination: Destination): Job {
 
         if (batchObjects.any { it == null }) {
             // stop running here, something like a signing error occurred
-            return@wrap delegate.handleJobFailedPermanently(this, dispatcherName, NullPointerException("One or more requests had a null batch request info"))
+            return delegate.handleJobFailedPermanently(this, dispatcherName, NullPointerException("One or more requests had a null batch request info"))
         }
 
         val allRequests = mutableListOf<SnodeAPI.SnodeBatchRequestInfo>()
@@ -156,9 +150,13 @@ data class ConfigurationSyncJob(val destination: Destination): Job {
             }
         } catch (e: Exception) {
             Log.e(TAG, "Error performing batch request", e)
-            return@wrap delegate.handleJobFailedPermanently(this, dispatcherName, e)
+            return delegate.handleJobFailedPermanently(this, dispatcherName, e)
         }
         delegate.handleJobSucceeded(this, dispatcherName)
+        if (shouldRunAgain.get() && storage.getConfigSyncJob(destination) == null) {
+            // reschedule if something has updated since we started this job
+            JobQueue.shared.add(ConfigurationSyncJob(destination))
+        }
     }
 
     fun Destination.destinationPublicKey(): String = when (this) {
diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/Poller.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/Poller.kt
index 08b531e4a2..acc58eefba 100644
--- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/Poller.kt
+++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/Poller.kt
@@ -43,7 +43,7 @@ class Poller(private val configFactory: ConfigFactoryProtocol, debounceTimer: Ti
     var isCaughtUp = false
     var configPollingJob: Job? = null
     
-    val configDebouncer = WindowDebouncer(3000, debounceTimer)
+    private val configDebouncer = WindowDebouncer(3000, debounceTimer)
 
     // region Settings
     companion object {
@@ -144,9 +144,6 @@ class Poller(private val configFactory: ConfigFactoryProtocol, debounceTimer: Ti
             return
         }
 
-        Log.d("Loki-DBG", "Received configs with hashes: ${messages.map { it.second }}")
-        Log.d("Loki-DBG", "Hashes we have for config: ${configFactory.getHashesFor(forConfigObject)}")
-
         messages.forEach { (envelope, hash) ->
             try {
                 val (message, _) = MessageReceiver.parse(data = envelope.toByteArray(), openGroupServerID = null)
diff --git a/libsession/src/main/java/org/session/libsession/utilities/ProfileKeyUtil.java b/libsession/src/main/java/org/session/libsession/utilities/ProfileKeyUtil.java
index 9e3842fc67..4550965ae7 100644
--- a/libsession/src/main/java/org/session/libsession/utilities/ProfileKeyUtil.java
+++ b/libsession/src/main/java/org/session/libsession/utilities/ProfileKeyUtil.java
@@ -1,23 +1,24 @@
 package org.session.libsession.utilities;
 
 import android.content.Context;
+
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
 import org.session.libsignal.utilities.Base64;
-import org.session.libsession.utilities.TextSecurePreferences;
-import org.session.libsession.utilities.Util;
 
 import java.io.IOException;
 
 public class ProfileKeyUtil {
 
+  public static final int PROFILE_KEY_BYTES = 32;
+
   public static synchronized @NonNull byte[] getProfileKey(@NonNull Context context) {
     try {
       String encodedProfileKey = TextSecurePreferences.getProfileKey(context);
 
       if (encodedProfileKey == null) {
-        encodedProfileKey = Util.getSecret(32);
+        encodedProfileKey = Util.getSecret(PROFILE_KEY_BYTES);
         TextSecurePreferences.setProfileKey(context, encodedProfileKey);
       }
 
@@ -36,7 +37,7 @@ public class ProfileKeyUtil {
   }
 
   public static synchronized @NonNull String generateEncodedProfileKey(@NonNull Context context) {
-    return Util.getSecret(32);
+    return Util.getSecret(PROFILE_KEY_BYTES);
   }
 
   public static synchronized void setEncodedProfileKey(@NonNull Context context, @Nullable String key) {
diff --git a/libsession/src/main/java/org/session/libsession/utilities/SSKEnvironment.kt b/libsession/src/main/java/org/session/libsession/utilities/SSKEnvironment.kt
index eeebf17c18..e939655885 100644
--- a/libsession/src/main/java/org/session/libsession/utilities/SSKEnvironment.kt
+++ b/libsession/src/main/java/org/session/libsession/utilities/SSKEnvironment.kt
@@ -1,6 +1,7 @@
 package org.session.libsession.utilities
 
 import android.content.Context
+import org.session.libsession.messaging.contacts.Contact
 import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate
 import org.session.libsession.messaging.sending_receiving.notifications.MessageNotifier
 import org.session.libsession.utilities.recipients.Recipient
@@ -33,6 +34,7 @@ class SSKEnvironment(
         fun setProfilePictureURL(context: Context, recipient: Recipient, profilePictureURL: String)
         fun setProfileKey(context: Context, recipient: Recipient, profileKey: ByteArray?)
         fun setUnidentifiedAccessMode(context: Context, recipient: Recipient, unidentifiedAccessMode: Recipient.UnidentifiedAccessMode)
+        fun contactUpdatedInternal(contact: Contact)
     }
 
     interface MessageExpirationManagerProtocol {