diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/ConfigDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/ConfigDatabase.kt index 4d53204f07..1c020eab1b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/ConfigDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/ConfigDatabase.kt @@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.database import android.content.Context import androidx.core.content.contentValuesOf import androidx.core.database.getBlobOrNull +import androidx.core.database.getStringOrNull import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper class ConfigDatabase(context: Context, helper: SQLCipherOpenHelper): Database(context, helper) { @@ -21,24 +22,26 @@ class ConfigDatabase(context: Context, helper: SQLCipherOpenHelper): Database(co private const val VARIANT_AND_PUBKEY_WHERE = "$VARIANT = ? AND $PUBKEY = ?" } - fun storeConfig(variant: String, publicKey: String, data: ByteArray) { + fun storeConfig(variant: String, publicKey: String, data: ByteArray, hashes: List) { val db = writableDatabase val contentValues = contentValuesOf( VARIANT to variant, PUBKEY to publicKey, - DATA to data + DATA to data, + COMBINED_MESSAGE_HASHES to hashes.joinToString(",") ) db.insertOrUpdate(TABLE_NAME, contentValues, VARIANT_AND_PUBKEY_WHERE, arrayOf(variant, publicKey)) } - fun retrieveConfig(variant: String, publicKey: String): ByteArray? { + fun retrieveConfigAndHashes(variant: String, publicKey: String): Pair>? { val db = readableDatabase val query = db.query(TABLE_NAME, arrayOf(DATA), VARIANT_AND_PUBKEY_WHERE, arrayOf(variant, publicKey),null, null, null) - val bytes = query?.use { cursor -> - if (!cursor.moveToFirst()) return null - cursor.getBlobOrNull(cursor.getColumnIndex(DATA)) + return query?.use { cursor -> + if (!cursor.moveToFirst()) return@use null + val bytes = cursor.getBlobOrNull(cursor.getColumnIndex(DATA)) ?: return@use null + val hashes = cursor.getStringOrNull(cursor.getColumnIndex(COMBINED_MESSAGE_HASHES))?.split(",") ?: emptyList() + bytes to hashes } - return bytes } } \ No newline at end of file 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 f7e2d9d06b..272f12b65f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt @@ -7,11 +7,22 @@ import org.session.libsession.database.StorageProtocol import org.session.libsession.messaging.BlindedIdMapping import org.session.libsession.messaging.calls.CallMessageType import org.session.libsession.messaging.contacts.Contact -import org.session.libsession.messaging.jobs.* +import org.session.libsession.messaging.jobs.AttachmentUploadJob +import org.session.libsession.messaging.jobs.GroupAvatarDownloadJob +import org.session.libsession.messaging.jobs.Job +import org.session.libsession.messaging.jobs.JobQueue +import org.session.libsession.messaging.jobs.MessageReceiveJob +import org.session.libsession.messaging.jobs.MessageSendJob import org.session.libsession.messaging.messages.Message import org.session.libsession.messaging.messages.control.ConfigurationMessage import org.session.libsession.messaging.messages.control.MessageRequestResponse -import org.session.libsession.messaging.messages.signal.* +import org.session.libsession.messaging.messages.signal.IncomingEncryptedMessage +import org.session.libsession.messaging.messages.signal.IncomingGroupMessage +import org.session.libsession.messaging.messages.signal.IncomingMediaMessage +import org.session.libsession.messaging.messages.signal.IncomingTextMessage +import org.session.libsession.messaging.messages.signal.OutgoingGroupMediaMessage +import org.session.libsession.messaging.messages.signal.OutgoingMediaMessage +import org.session.libsession.messaging.messages.signal.OutgoingTextMessage import org.session.libsession.messaging.messages.visible.Attachment import org.session.libsession.messaging.messages.visible.Profile import org.session.libsession.messaging.messages.visible.Reaction @@ -28,7 +39,7 @@ import org.session.libsession.messaging.utilities.SessionId import org.session.libsession.messaging.utilities.SodiumUtilities import org.session.libsession.messaging.utilities.UpdateMessageData import org.session.libsession.snode.OnionRequestAPI -import org.session.libsession.utilities.* +import org.session.libsession.utilities.Address import org.session.libsession.utilities.Address.Companion.fromSerialized import org.session.libsession.utilities.GroupRecord import org.session.libsession.utilities.GroupUtil @@ -70,13 +81,12 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, return Profile(displayName, profileKey, profilePictureUrl) } - override fun setUserProfilePictureURL(newValue: String) { + override fun setUserProfilePictureURL(newProfilePicture: String?) { val ourRecipient = fromSerialized(getUserPublicKey()!!).let { Recipient.from(context, it, false) } - TextSecurePreferences.setProfilePictureURL(context, newValue) - RetrieveProfileAvatarJob(ourRecipient, newValue) - ApplicationContext.getInstance(context).jobManager.add(RetrieveProfileAvatarJob(ourRecipient, newValue)) + TextSecurePreferences.setProfilePictureURL(context, newProfilePicture) + ApplicationContext.getInstance(context).jobManager.add(RetrieveProfileAvatarJob(ourRecipient, newProfilePicture)) } override fun getOrGenerateRegistrationID(): Int { diff --git a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ConfigFactory.kt b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ConfigFactory.kt index b1bfdcb7e5..8940dac92e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ConfigFactory.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ConfigFactory.kt @@ -1,13 +1,26 @@ package org.thoughtcrime.securesms.dependencies +import android.content.Context +import network.loki.messenger.libsession_util.ConfigBase import network.loki.messenger.libsession_util.Contacts import network.loki.messenger.libsession_util.ConversationVolatileConfig import network.loki.messenger.libsession_util.UserProfile +import org.session.libsession.database.StorageProtocol +import org.session.libsession.utilities.Address import org.session.libsession.utilities.ConfigFactoryProtocol +import org.session.libsession.utilities.ProfileKeyUtil +import org.session.libsession.utilities.SSKEnvironment +import org.session.libsession.utilities.TextSecurePreferences +import org.session.libsession.utilities.recipients.Recipient import org.session.libsignal.protos.SignalServiceProtos.SharedConfigMessage +import org.session.libsignal.utilities.Base64 import org.thoughtcrime.securesms.database.ConfigDatabase +import java.util.concurrent.ConcurrentSkipListSet -class ConfigFactory(private val configDatabase: ConfigDatabase, private val maybeGetUserInfo: ()->Pair?): +class ConfigFactory(private val context: Context, + private val configDatabase: ConfigDatabase, + private val storage: StorageProtocol, + private val maybeGetUserInfo: ()->Pair?): ConfigFactoryProtocol { fun keyPairChanged() { // this should only happen restoring or clearing data @@ -21,17 +34,23 @@ class ConfigFactory(private val configDatabase: ConfigDatabase, private val mayb private val userLock = Object() private var _userConfig: UserProfile? = null - private val contactLock = Object() + private val userHashes = ConcurrentSkipListSet() + private val contactsLock = Object() private var _contacts: Contacts? = null + private val contactsHashes = ConcurrentSkipListSet() private val convoVolatileLock = Object() private var _convoVolatileConfig: ConversationVolatileConfig? = null + private val convoHashes = ConcurrentSkipListSet() override val user: UserProfile? = synchronized(userLock) { if (_userConfig == null) { val (secretKey, publicKey) = maybeGetUserInfo() ?: return@synchronized null - val userDump = configDatabase.retrieveConfig(SharedConfigMessage.Kind.USER_PROFILE.name, publicKey) + val userDump = configDatabase.retrieveConfigAndHashes(SharedConfigMessage.Kind.USER_PROFILE.name, publicKey) _userConfig = if (userDump != null) { - UserProfile.newInstance(secretKey, userDump) + val (bytes, hashes) = userDump + userHashes.clear() + userHashes.addAll(hashes) + UserProfile.newInstance(secretKey, bytes) } else { UserProfile.newInstance(secretKey) } @@ -39,12 +58,15 @@ class ConfigFactory(private val configDatabase: ConfigDatabase, private val mayb _userConfig } - override val contacts: Contacts? = synchronized(contactLock) { + override val contacts: Contacts? = synchronized(contactsLock) { if (_contacts == null) { val (secretKey, publicKey) = maybeGetUserInfo() ?: return@synchronized null - val contactsDump = configDatabase.retrieveConfig(SharedConfigMessage.Kind.CONTACTS.name, publicKey) + val contactsDump = configDatabase.retrieveConfigAndHashes(SharedConfigMessage.Kind.CONTACTS.name, publicKey) _contacts = if (contactsDump != null) { - Contacts.newInstance(secretKey, contactsDump) + val (bytes, hashes) = contactsDump + contactsHashes.clear() + contactsHashes.addAll(hashes) + Contacts.newInstance(secretKey, bytes) } else { Contacts.newInstance(secretKey) } @@ -55,9 +77,12 @@ class ConfigFactory(private val configDatabase: ConfigDatabase, private val mayb override val convoVolatile: ConversationVolatileConfig? = synchronized(convoVolatileLock) { if (_convoVolatileConfig == null) { val (secretKey, publicKey) = maybeGetUserInfo() ?: return@synchronized null - val convoDump = configDatabase.retrieveConfig(SharedConfigMessage.Kind.CONVO_INFO_VOLATILE.name, publicKey) + val convoDump = configDatabase.retrieveConfigAndHashes(SharedConfigMessage.Kind.CONVO_INFO_VOLATILE.name, publicKey) _convoVolatileConfig = if (convoDump != null) { - ConversationVolatileConfig.newInstance(secretKey, convoDump) + val (bytes, hashes) = convoDump + convoHashes.clear() + convoHashes.addAll(hashes) + ConversationVolatileConfig.newInstance(secretKey, bytes) } else { ConversationVolatileConfig.newInstance(secretKey) } @@ -66,21 +91,84 @@ class ConfigFactory(private val configDatabase: ConfigDatabase, private val mayb } - override fun saveUserConfigDump() { + private fun persistUserConfigDump() = synchronized(userLock) { val dumped = user?.dump() ?: return val (_, publicKey) = maybeGetUserInfo() ?: return - configDatabase.storeConfig(SharedConfigMessage.Kind.USER_PROFILE.name, publicKey, dumped) + configDatabase.storeConfig(SharedConfigMessage.Kind.USER_PROFILE.name, publicKey, dumped, userHashes.toList()) } - override fun saveContactConfigDump() { + private fun persistContactsConfigDump() = synchronized(contactsLock) { val dumped = contacts?.dump() ?: return val (_, publicKey) = maybeGetUserInfo() ?: return - configDatabase.storeConfig(SharedConfigMessage.Kind.CONTACTS.name, publicKey, dumped) + configDatabase.storeConfig(SharedConfigMessage.Kind.CONTACTS.name, publicKey, dumped, contactsHashes.toList()) } - override fun saveConvoVolatileConfigDump() { + private fun persistConvoVolatileConfigDump() = synchronized (convoVolatileLock) { val dumped = convoVolatile?.dump() ?: return val (_, publicKey) = maybeGetUserInfo() ?: return - configDatabase.storeConfig(SharedConfigMessage.Kind.CONVO_INFO_VOLATILE.name, publicKey, dumped) + configDatabase.storeConfig(SharedConfigMessage.Kind.CONVO_INFO_VOLATILE.name, publicKey, dumped, convoHashes.toList()) } + + override fun persist(forConfigObject: ConfigBase) { + when (forConfigObject) { + is UserProfile -> persistUserConfigDump() + is Contacts -> persistContactsConfigDump() + is ConversationVolatileConfig -> persistConvoVolatileConfigDump() + } + } + + override fun appendHash(configObject: ConfigBase, hash: String) { + when (configObject) { + is UserProfile -> userHashes.add(hash) + is Contacts -> contactsHashes.add(hash) + is ConversationVolatileConfig -> convoHashes.add(hash) + } + } + + override fun notifyUpdates(forConfigObject: ConfigBase) { + when (forConfigObject) { + is UserProfile -> updateUser(forConfigObject) + is Contacts -> updateContacts(forConfigObject) + is ConversationVolatileConfig -> updateConvoVolatile(forConfigObject) + } + } + + private fun updateUser(userProfile: UserProfile) { + val (_, userPublicKey) = maybeGetUserInfo() ?: return + // would love to get rid of recipient and context from this + val recipient = Recipient.from(context, Address.fromSerialized(userPublicKey), false) + // update name + val name = userProfile.getName() ?: return + val userPic = userProfile.getPic() + val profileManager = SSKEnvironment.shared.profileManager + if (name.isNotEmpty()) { + TextSecurePreferences.setProfileName(context, name) + profileManager.setName(context, recipient, name) + } + + // update pfp + if (userPic == null) { + // clear picture if userPic is null + TextSecurePreferences.setProfileKey(context, null) + ProfileKeyUtil.setEncodedProfileKey(context, null) + profileManager.setProfileKey(context, recipient, null) + storage.setUserProfilePictureURL(null) + } else if (userPic.key.isNotEmpty() && userPic.url.isNotEmpty() + && TextSecurePreferences.getProfilePictureURL(context) != userPic.url) { + val profileKey = Base64.encodeBytes(userPic.key) + ProfileKeyUtil.setEncodedProfileKey(context, profileKey) + profileManager.setProfileKey(context, recipient, userPic.key) + storage.setUserProfilePictureURL(userPic.url) + } + } + + private fun updateContacts(contacts: Contacts) { + + } + + private fun updateConvoVolatile(convos: ConversationVolatileConfig) { + + } + + } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/dependencies/SessionUtilModule.kt b/app/src/main/java/org/thoughtcrime/securesms/dependencies/SessionUtilModule.kt index 001e818711..8c83389dab 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/dependencies/SessionUtilModule.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/dependencies/SessionUtilModule.kt @@ -6,6 +6,7 @@ import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent +import org.session.libsession.database.StorageProtocol import org.session.libsession.utilities.TextSecurePreferences import org.thoughtcrime.securesms.crypto.KeyPairUtilities import org.thoughtcrime.securesms.database.ConfigDatabase @@ -22,8 +23,8 @@ object SessionUtilModule { @Provides @Singleton - fun provideConfigFactory(@ApplicationContext context: Context, configDatabase: ConfigDatabase): ConfigFactory = - ConfigFactory(configDatabase) { + fun provideConfigFactory(@ApplicationContext context: Context, configDatabase: ConfigDatabase, storage: StorageProtocol): ConfigFactory = + ConfigFactory(context, configDatabase, storage) { val localUserPublicKey = TextSecurePreferences.getLocalNumber(context) val secretKey = maybeUserEdSecretKey(context) if (localUserPublicKey == null || secretKey == null) null 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 c46f75bff8..a1a4199d81 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/sskenvironment/ProfileManager.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/sskenvironment/ProfileManager.kt @@ -54,7 +54,7 @@ class ProfileManager : SSKEnvironment.ProfileManagerProtocol { } } - override fun setProfileKey(context: Context, recipient: Recipient, profileKey: ByteArray) { + override fun setProfileKey(context: Context, recipient: Recipient, profileKey: ByteArray?) { // New API val sessionID = recipient.address.serialize() val contactDatabase = DatabaseComponent.get(context).sessionContactDatabase() 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 70f849f0b1..8180039e8a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/ConfigurationMessageUtilities.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/ConfigurationMessageUtilities.kt @@ -7,56 +7,69 @@ import network.loki.messenger.libsession_util.util.Contact import network.loki.messenger.libsession_util.util.UserPic import nl.komponents.kovenant.Promise import org.session.libsession.messaging.MessagingModuleConfiguration +import org.session.libsession.messaging.messages.Destination import org.session.libsession.messaging.messages.control.ConfigurationMessage +import org.session.libsession.messaging.sending_receiving.MessageSender import org.session.libsession.utilities.Address +import org.session.libsession.utilities.TextSecurePreferences object ConfigurationMessageUtilities { + const val isNewConfigEnabled = true + @JvmStatic fun syncConfigurationIfNeeded(context: Context) { - return -// val userPublicKey = TextSecurePreferences.getLocalNumber(context) ?: return -// val lastSyncTime = TextSecurePreferences.getLastConfigurationSyncTime(context) -// val now = System.currentTimeMillis() -// if (now - lastSyncTime < 7 * 24 * 60 * 60 * 1000) return -// val contacts = ContactUtilities.getAllContacts(context).filter { recipient -> -// !recipient.name.isNullOrEmpty() && !recipient.isLocalNumber && recipient.address.serialize().isNotEmpty() -// }.map { recipient -> -// ConfigurationMessage.Contact( -// publicKey = recipient.address.serialize(), -// name = recipient.name!!, -// profilePicture = recipient.profileAvatar, -// profileKey = recipient.profileKey, -// isApproved = recipient.isApproved, -// isBlocked = recipient.isBlocked, -// didApproveMe = recipient.hasApprovedMe() -// ) -// } -// val configurationMessage = ConfigurationMessage.getCurrent(contacts) ?: return -// MessageSender.send(configurationMessage, Address.fromSerialized(userPublicKey)) -// TextSecurePreferences.setLastConfigurationSyncTime(context, now) + // add if check here to schedule new config job process and return early + if (isNewConfigEnabled) { + // schedule job if none exist + TODO() + } + val userPublicKey = TextSecurePreferences.getLocalNumber(context) ?: return + val lastSyncTime = TextSecurePreferences.getLastConfigurationSyncTime(context) + val now = System.currentTimeMillis() + if (now - lastSyncTime < 7 * 24 * 60 * 60 * 1000) return + val contacts = ContactUtilities.getAllContacts(context).filter { recipient -> + !recipient.name.isNullOrEmpty() && !recipient.isLocalNumber && recipient.address.serialize().isNotEmpty() + }.map { recipient -> + ConfigurationMessage.Contact( + publicKey = recipient.address.serialize(), + name = recipient.name!!, + profilePicture = recipient.profileAvatar, + profileKey = recipient.profileKey, + isApproved = recipient.isApproved, + isBlocked = recipient.isBlocked, + didApproveMe = recipient.hasApprovedMe() + ) + } + val configurationMessage = ConfigurationMessage.getCurrent(contacts) ?: return + MessageSender.send(configurationMessage, Address.fromSerialized(userPublicKey)) + TextSecurePreferences.setLastConfigurationSyncTime(context, now) } fun forceSyncConfigurationNowIfNeeded(context: Context): Promise { - return Promise.ofSuccess(Unit) -// val userPublicKey = TextSecurePreferences.getLocalNumber(context) ?: return Promise.ofSuccess(Unit) -// val contacts = ContactUtilities.getAllContacts(context).filter { recipient -> -// !recipient.isGroupRecipient && !recipient.name.isNullOrEmpty() && !recipient.isLocalNumber && recipient.address.serialize().isNotEmpty() -// }.map { recipient -> -// ConfigurationMessage.Contact( -// publicKey = recipient.address.serialize(), -// name = recipient.name!!, -// profilePicture = recipient.profileAvatar, -// profileKey = recipient.profileKey, -// isApproved = recipient.isApproved, -// isBlocked = recipient.isBlocked, -// didApproveMe = recipient.hasApprovedMe() -// ) -// } -// val configurationMessage = ConfigurationMessage.getCurrent(contacts) ?: return Promise.ofSuccess(Unit) -// val promise = MessageSender.send(configurationMessage, Destination.from(Address.fromSerialized(userPublicKey))) -// TextSecurePreferences.setLastConfigurationSyncTime(context, System.currentTimeMillis()) -// return promise + // add if check here to schedule new config job process and return early + if (isNewConfigEnabled) { + // schedule job if none exist + TODO() + } + val userPublicKey = TextSecurePreferences.getLocalNumber(context) ?: return Promise.ofSuccess(Unit) + val contacts = ContactUtilities.getAllContacts(context).filter { recipient -> + !recipient.isGroupRecipient && !recipient.name.isNullOrEmpty() && !recipient.isLocalNumber && recipient.address.serialize().isNotEmpty() + }.map { recipient -> + ConfigurationMessage.Contact( + publicKey = recipient.address.serialize(), + name = recipient.name!!, + profilePicture = recipient.profileAvatar, + profileKey = recipient.profileKey, + isApproved = recipient.isApproved, + isBlocked = recipient.isBlocked, + didApproveMe = recipient.hasApprovedMe() + ) + } + val configurationMessage = ConfigurationMessage.getCurrent(contacts) ?: return Promise.ofSuccess(Unit) + val promise = MessageSender.send(configurationMessage, Destination.from(Address.fromSerialized(userPublicKey))) + TextSecurePreferences.setLastConfigurationSyncTime(context, System.currentTimeMillis()) + return promise } private fun maybeUserSecretKey() = MessagingModuleConfiguration.shared.getUserED25519KeyPair()?.secretKey?.asBytes @@ -110,7 +123,7 @@ object ConfigurationMessageUtilities { return dump } - fun generateConversationDump(context: Context): ByteArray? { + fun generateConversationVolatileDump(context: Context): ByteArray? { TODO() } 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 c80ef24642..aad8394b4c 100644 --- a/libsession/src/main/java/org/session/libsession/database/StorageProtocol.kt +++ b/libsession/src/main/java/org/session/libsession/database/StorageProtocol.kt @@ -37,7 +37,7 @@ interface StorageProtocol { fun getUserPublicKey(): String? fun getUserX25519KeyPair(): ECKeyPair fun getUserProfile(): Profile - fun setUserProfilePictureURL(newProfilePicture: String) + fun setUserProfilePictureURL(newProfilePicture: String?) // Signal fun getOrGenerateRegistrationID(): Int diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/control/SharedConfigurationMessage.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/control/SharedConfigurationMessage.kt new file mode 100644 index 0000000000..72b2474965 --- /dev/null +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/control/SharedConfigurationMessage.kt @@ -0,0 +1,36 @@ +package org.session.libsession.messaging.messages.control + +import com.google.protobuf.ByteString +import org.session.libsignal.protos.SignalServiceProtos +import org.session.libsignal.protos.SignalServiceProtos.SharedConfigMessage + +class SharedConfigurationMessage(val kind: SharedConfigMessage.Kind, val data: ByteArray, val seqNo: Long): ControlMessage() { + + override val ttl: Long = 30 * 24 * 60 * 60 * 1000L + override val isSelfSendValid: Boolean = true + + companion object { + fun fromProto(proto: SignalServiceProtos.Content): SharedConfigurationMessage? { + if (!proto.hasSharedConfigMessage()) return null + val sharedConfig = proto.sharedConfigMessage + if (!sharedConfig.hasKind() || !sharedConfig.hasData()) return null + return SharedConfigurationMessage(sharedConfig.kind, sharedConfig.data.toByteArray(), sharedConfig.seqno) + } + } + + override fun isValid(): Boolean { + if (!super.isValid()) return false + return data.isNotEmpty() && seqNo >= 0 + } + + override fun toProto(): SignalServiceProtos.Content? { + val sharedConfigurationMessage = SharedConfigMessage.newBuilder() + .setKind(kind) + .setSeqno(seqNo) + .setData(ByteString.copyFrom(data)) + .build() + return SignalServiceProtos.Content.newBuilder() + .setSharedConfigMessage(sharedConfigurationMessage) + .build() + } +} \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiver.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiver.kt index 6a38a551f8..a97e83a2de 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiver.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiver.kt @@ -9,6 +9,7 @@ import org.session.libsession.messaging.messages.control.DataExtractionNotificat import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate import org.session.libsession.messaging.messages.control.MessageRequestResponse import org.session.libsession.messaging.messages.control.ReadReceipt +import org.session.libsession.messaging.messages.control.SharedConfigurationMessage import org.session.libsession.messaging.messages.control.TypingIndicator import org.session.libsession.messaging.messages.control.UnsendRequest import org.session.libsession.messaging.messages.visible.VisibleMessage @@ -138,6 +139,7 @@ object MessageReceiver { UnsendRequest.fromProto(proto) ?: MessageRequestResponse.fromProto(proto) ?: CallMessage.fromProto(proto) ?: + SharedConfigurationMessage.fromProto(proto) ?: VisibleMessage.fromProto(proto) ?: run { throw Error.UnknownMessage } @@ -166,12 +168,13 @@ object MessageReceiver { // If the message failed to process the first time around we retry it later (if the error is retryable). In this case the timestamp // will already be in the database but we don't want to treat the message as a duplicate. The isRetry flag is a simple workaround // for this issue. - if (message is ClosedGroupControlMessage && message.kind is ClosedGroupControlMessage.Kind.New) { + if ((message is ClosedGroupControlMessage && message.kind is ClosedGroupControlMessage.Kind.New) || message is SharedConfigurationMessage) { // Allow duplicates in this case to avoid the following situation: // • The app performed a background poll or received a push notification // • This method was invoked and the received message timestamps table was updated // • Processing wasn't finished // • The user doesn't see the new closed group + // also allow shared configuration messages to be duplicates since we track hashes separately use seqno for conflict resolution } else { if (storage.isDuplicateMessage(envelope.timestamp)) { throw Error.DuplicateMessage } storage.addReceivedMessageTimestamp(envelope.timestamp) 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 8ea2ba0fca..f24d92b456 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 @@ -7,6 +7,8 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.runBlocking import network.loki.messenger.libsession_util.ConfigBase +import network.loki.messenger.libsession_util.Contacts +import network.loki.messenger.libsession_util.ConversationVolatileConfig import network.loki.messenger.libsession_util.UserProfile import nl.komponents.kovenant.Deferred import nl.komponents.kovenant.Promise @@ -18,6 +20,8 @@ import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.jobs.BatchMessageReceiveJob import org.session.libsession.messaging.jobs.JobQueue import org.session.libsession.messaging.jobs.MessageReceiveParameters +import org.session.libsession.messaging.messages.control.SharedConfigurationMessage +import org.session.libsession.messaging.sending_receiving.MessageReceiver import org.session.libsession.snode.RawResponse import org.session.libsession.snode.SnodeAPI import org.session.libsession.snode.SnodeModule @@ -121,19 +125,37 @@ class Poller(private val configFactory: ConfigFactoryProtocol) { } } - private fun processUserConfig(snode: Snode, rawMessages: RawResponse) { - SnodeAPI.parseRawMessagesResponse(rawMessages, snode, userPublicKey) + private fun processConfig(snode: Snode, rawMessages: RawResponse, namespace: Int, forConfigObject: ConfigBase?) { + if (forConfigObject == null) return + + val messages = SnodeAPI.parseRawMessagesResponse( + rawMessages, + snode, + userPublicKey, + namespace, + updateLatestHash = false + ) + messages.forEach { (envelope, hash) -> + try { + val (message, _) = MessageReceiver.parse(data = envelope.toByteArray(), openGroupServerID = null) + // sanity checks + if (message !is SharedConfigurationMessage) { + Log.w("Loki-DBG", "shared config message handled in configs wasn't SharedConfigurationMessage but was ${message.javaClass.simpleName}") + return@forEach + } + // maybe do something with seqNo ? + Log.d("Loki-DBG", "Merging config of kind ${message.kind} into ${forConfigObject.javaClass.simpleName}") + forConfigObject.merge(message.data) + configFactory.appendHash(forConfigObject, hash!!) + } catch (e: Exception) { + Log.e("Loki", e) + } + } + // process new results + configFactory.persist(forConfigObject) + configFactory.notifyUpdates(forConfigObject) } - private fun processContactsConfig(snode: Snode, rawMessages: RawResponse) { - - } - - private fun processConvoVolatileConfig(snode: Snode, rawMessages: RawResponse) { - - } - - private fun poll(snode: Snode, deferred: Deferred): Promise { if (!hasStarted) { return Promise.ofFail(PromiseCanceledException()) } return task { @@ -160,15 +182,20 @@ class Poller(private val configFactory: ConfigFactoryProtocol) { if (deferred.promise.isDone()) { return@bind Promise.ofSuccess(Unit) } else { + // TODO: remove log after testing responses Log.d("Loki-DBG", JsonUtil.toJson(rawResponses)) val requestList = (rawResponses["results"] as List) + // in case we had null configs, the array won't be fully populated + // index of the sparse array key iterator should be the request index, with the key being the namespace requestSparseArray.keyIterator().withIndex().forEach { (requestIndex, key) -> requestList.getOrNull(requestIndex)?.let { rawResponse -> if (key == Namespace.DEFAULT) { processPersonalMessages(snode, rawResponse) } else { when (ConfigBase.kindFor(key)) { - UserProfile -> + UserProfile::class.java -> processConfig(snode, rawResponse, key, configFactory.user) + Contacts::class.java -> processConfig(snode, rawResponse, key, configFactory.contacts) + ConversationVolatileConfig::class.java -> processConfig(snode, rawResponse, key, configFactory.convoVolatile) } } } diff --git a/libsession/src/main/java/org/session/libsession/snode/SnodeAPI.kt b/libsession/src/main/java/org/session/libsession/snode/SnodeAPI.kt index b133c708d1..06cf891012 100644 --- a/libsession/src/main/java/org/session/libsession/snode/SnodeAPI.kt +++ b/libsession/src/main/java/org/session/libsession/snode/SnodeAPI.kt @@ -628,10 +628,12 @@ object SnodeAPI { } } - fun parseRawMessagesResponse(rawResponse: RawResponse, snode: Snode, publicKey: String, namespace: Int = 0): List> { + fun parseRawMessagesResponse(rawResponse: RawResponse, snode: Snode, publicKey: String, namespace: Int = 0, updateLatestHash: Boolean = true): List> { val messages = rawResponse["messages"] as? List<*> return if (messages != null) { - updateLastMessageHashValueIfPossible(snode, publicKey, messages, namespace) + if (updateLatestHash) { + updateLastMessageHashValueIfPossible(snode, publicKey, messages, namespace) + } val newRawMessages = removeDuplicates(publicKey, messages, namespace) return parseEnvelopes(newRawMessages) } else { diff --git a/libsession/src/main/java/org/session/libsession/utilities/ConfigFactoryProtocol.kt b/libsession/src/main/java/org/session/libsession/utilities/ConfigFactoryProtocol.kt index e84130736b..6844aef98e 100644 --- a/libsession/src/main/java/org/session/libsession/utilities/ConfigFactoryProtocol.kt +++ b/libsession/src/main/java/org/session/libsession/utilities/ConfigFactoryProtocol.kt @@ -1,5 +1,6 @@ package org.session.libsession.utilities +import network.loki.messenger.libsession_util.ConfigBase import network.loki.messenger.libsession_util.Contacts import network.loki.messenger.libsession_util.ConversationVolatileConfig import network.loki.messenger.libsession_util.UserProfile @@ -8,7 +9,7 @@ interface ConfigFactoryProtocol { val user: UserProfile? val contacts: Contacts? val convoVolatile: ConversationVolatileConfig? - fun saveUserConfigDump() - fun saveContactConfigDump() - fun saveConvoVolatileConfigDump() + fun persist(forConfigObject: ConfigBase) + fun appendHash(configObject: ConfigBase, hash: String) + fun notifyUpdates(forConfigObject: ConfigBase) } \ No newline at end of file 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 0374b9c001..eeebf17c18 100644 --- a/libsession/src/main/java/org/session/libsession/utilities/SSKEnvironment.kt +++ b/libsession/src/main/java/org/session/libsession/utilities/SSKEnvironment.kt @@ -3,7 +3,6 @@ package org.session.libsession.utilities import android.content.Context import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate import org.session.libsession.messaging.sending_receiving.notifications.MessageNotifier -import org.session.libsession.utilities.Address import org.session.libsession.utilities.recipients.Recipient class SSKEnvironment( @@ -32,7 +31,7 @@ class SSKEnvironment( fun setNickname(context: Context, recipient: Recipient, nickname: String?) fun setName(context: Context, recipient: Recipient, name: String) fun setProfilePictureURL(context: Context, recipient: Recipient, profilePictureURL: String) - fun setProfileKey(context: Context, recipient: Recipient, profileKey: ByteArray) + fun setProfileKey(context: Context, recipient: Recipient, profileKey: ByteArray?) fun setUnidentifiedAccessMode(context: Context, recipient: Recipient, unidentifiedAccessMode: Recipient.UnidentifiedAccessMode) }