feat: add storage with hashes and some basic profile update logic in config factory probably move that somewhere else

This commit is contained in:
0x330a 2023-02-07 17:30:45 +11:00
parent c8d520c3ce
commit c639d57471
No known key found for this signature in database
GPG Key ID: 267811D6E6A2698C
13 changed files with 277 additions and 94 deletions

View File

@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.database
import android.content.Context import android.content.Context
import androidx.core.content.contentValuesOf import androidx.core.content.contentValuesOf
import androidx.core.database.getBlobOrNull import androidx.core.database.getBlobOrNull
import androidx.core.database.getStringOrNull
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
class ConfigDatabase(context: Context, helper: SQLCipherOpenHelper): Database(context, helper) { 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 = ?" 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<String>) {
val db = writableDatabase val db = writableDatabase
val contentValues = contentValuesOf( val contentValues = contentValuesOf(
VARIANT to variant, VARIANT to variant,
PUBKEY to publicKey, 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)) 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<ByteArray,List<String>>? {
val db = readableDatabase val db = readableDatabase
val query = db.query(TABLE_NAME, arrayOf(DATA), VARIANT_AND_PUBKEY_WHERE, arrayOf(variant, publicKey),null, null, null) val query = db.query(TABLE_NAME, arrayOf(DATA), VARIANT_AND_PUBKEY_WHERE, arrayOf(variant, publicKey),null, null, null)
val bytes = query?.use { cursor -> return query?.use { cursor ->
if (!cursor.moveToFirst()) return null if (!cursor.moveToFirst()) return@use null
cursor.getBlobOrNull(cursor.getColumnIndex(DATA)) 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
} }
} }

View File

@ -7,11 +7,22 @@ import org.session.libsession.database.StorageProtocol
import org.session.libsession.messaging.BlindedIdMapping import org.session.libsession.messaging.BlindedIdMapping
import org.session.libsession.messaging.calls.CallMessageType import org.session.libsession.messaging.calls.CallMessageType
import org.session.libsession.messaging.contacts.Contact 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.Message
import org.session.libsession.messaging.messages.control.ConfigurationMessage import org.session.libsession.messaging.messages.control.ConfigurationMessage
import org.session.libsession.messaging.messages.control.MessageRequestResponse 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.Attachment
import org.session.libsession.messaging.messages.visible.Profile import org.session.libsession.messaging.messages.visible.Profile
import org.session.libsession.messaging.messages.visible.Reaction 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.SodiumUtilities
import org.session.libsession.messaging.utilities.UpdateMessageData import org.session.libsession.messaging.utilities.UpdateMessageData
import org.session.libsession.snode.OnionRequestAPI 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.Address.Companion.fromSerialized
import org.session.libsession.utilities.GroupRecord import org.session.libsession.utilities.GroupRecord
import org.session.libsession.utilities.GroupUtil import org.session.libsession.utilities.GroupUtil
@ -70,13 +81,12 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
return Profile(displayName, profileKey, profilePictureUrl) return Profile(displayName, profileKey, profilePictureUrl)
} }
override fun setUserProfilePictureURL(newValue: String) { override fun setUserProfilePictureURL(newProfilePicture: String?) {
val ourRecipient = fromSerialized(getUserPublicKey()!!).let { val ourRecipient = fromSerialized(getUserPublicKey()!!).let {
Recipient.from(context, it, false) Recipient.from(context, it, false)
} }
TextSecurePreferences.setProfilePictureURL(context, newValue) TextSecurePreferences.setProfilePictureURL(context, newProfilePicture)
RetrieveProfileAvatarJob(ourRecipient, newValue) ApplicationContext.getInstance(context).jobManager.add(RetrieveProfileAvatarJob(ourRecipient, newProfilePicture))
ApplicationContext.getInstance(context).jobManager.add(RetrieveProfileAvatarJob(ourRecipient, newValue))
} }
override fun getOrGenerateRegistrationID(): Int { override fun getOrGenerateRegistrationID(): Int {

View File

@ -1,13 +1,26 @@
package org.thoughtcrime.securesms.dependencies 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.Contacts
import network.loki.messenger.libsession_util.ConversationVolatileConfig import network.loki.messenger.libsession_util.ConversationVolatileConfig
import network.loki.messenger.libsession_util.UserProfile 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.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.protos.SignalServiceProtos.SharedConfigMessage
import org.session.libsignal.utilities.Base64
import org.thoughtcrime.securesms.database.ConfigDatabase import org.thoughtcrime.securesms.database.ConfigDatabase
import java.util.concurrent.ConcurrentSkipListSet
class ConfigFactory(private val configDatabase: ConfigDatabase, private val maybeGetUserInfo: ()->Pair<ByteArray, String>?): class ConfigFactory(private val context: Context,
private val configDatabase: ConfigDatabase,
private val storage: StorageProtocol,
private val maybeGetUserInfo: ()->Pair<ByteArray, String>?):
ConfigFactoryProtocol { ConfigFactoryProtocol {
fun keyPairChanged() { // this should only happen restoring or clearing data 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 val userLock = Object()
private var _userConfig: UserProfile? = null private var _userConfig: UserProfile? = null
private val contactLock = Object() private val userHashes = ConcurrentSkipListSet<String>()
private val contactsLock = Object()
private var _contacts: Contacts? = null private var _contacts: Contacts? = null
private val contactsHashes = ConcurrentSkipListSet<String>()
private val convoVolatileLock = Object() private val convoVolatileLock = Object()
private var _convoVolatileConfig: ConversationVolatileConfig? = null private var _convoVolatileConfig: ConversationVolatileConfig? = null
private val convoHashes = ConcurrentSkipListSet<String>()
override val user: UserProfile? = synchronized(userLock) { override val user: UserProfile? = synchronized(userLock) {
if (_userConfig == null) { if (_userConfig == null) {
val (secretKey, publicKey) = maybeGetUserInfo() ?: return@synchronized 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) { _userConfig = if (userDump != null) {
UserProfile.newInstance(secretKey, userDump) val (bytes, hashes) = userDump
userHashes.clear()
userHashes.addAll(hashes)
UserProfile.newInstance(secretKey, bytes)
} else { } else {
UserProfile.newInstance(secretKey) UserProfile.newInstance(secretKey)
} }
@ -39,12 +58,15 @@ class ConfigFactory(private val configDatabase: ConfigDatabase, private val mayb
_userConfig _userConfig
} }
override val contacts: Contacts? = synchronized(contactLock) { override val contacts: Contacts? = synchronized(contactsLock) {
if (_contacts == null) { if (_contacts == null) {
val (secretKey, publicKey) = maybeGetUserInfo() ?: return@synchronized 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 = if (contactsDump != null) {
Contacts.newInstance(secretKey, contactsDump) val (bytes, hashes) = contactsDump
contactsHashes.clear()
contactsHashes.addAll(hashes)
Contacts.newInstance(secretKey, bytes)
} else { } else {
Contacts.newInstance(secretKey) Contacts.newInstance(secretKey)
} }
@ -55,9 +77,12 @@ class ConfigFactory(private val configDatabase: ConfigDatabase, private val mayb
override val convoVolatile: ConversationVolatileConfig? = synchronized(convoVolatileLock) { override val convoVolatile: ConversationVolatileConfig? = synchronized(convoVolatileLock) {
if (_convoVolatileConfig == null) { if (_convoVolatileConfig == null) {
val (secretKey, publicKey) = maybeGetUserInfo() ?: return@synchronized 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) { _convoVolatileConfig = if (convoDump != null) {
ConversationVolatileConfig.newInstance(secretKey, convoDump) val (bytes, hashes) = convoDump
convoHashes.clear()
convoHashes.addAll(hashes)
ConversationVolatileConfig.newInstance(secretKey, bytes)
} else { } else {
ConversationVolatileConfig.newInstance(secretKey) 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 dumped = user?.dump() ?: return
val (_, publicKey) = maybeGetUserInfo() ?: 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 dumped = contacts?.dump() ?: return
val (_, publicKey) = maybeGetUserInfo() ?: 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 dumped = convoVolatile?.dump() ?: return
val (_, publicKey) = maybeGetUserInfo() ?: 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) {
}
}

View File

@ -6,6 +6,7 @@ import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import org.session.libsession.database.StorageProtocol
import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.TextSecurePreferences
import org.thoughtcrime.securesms.crypto.KeyPairUtilities import org.thoughtcrime.securesms.crypto.KeyPairUtilities
import org.thoughtcrime.securesms.database.ConfigDatabase import org.thoughtcrime.securesms.database.ConfigDatabase
@ -22,8 +23,8 @@ object SessionUtilModule {
@Provides @Provides
@Singleton @Singleton
fun provideConfigFactory(@ApplicationContext context: Context, configDatabase: ConfigDatabase): ConfigFactory = fun provideConfigFactory(@ApplicationContext context: Context, configDatabase: ConfigDatabase, storage: StorageProtocol): ConfigFactory =
ConfigFactory(configDatabase) { ConfigFactory(context, configDatabase, storage) {
val localUserPublicKey = TextSecurePreferences.getLocalNumber(context) val localUserPublicKey = TextSecurePreferences.getLocalNumber(context)
val secretKey = maybeUserEdSecretKey(context) val secretKey = maybeUserEdSecretKey(context)
if (localUserPublicKey == null || secretKey == null) null if (localUserPublicKey == null || secretKey == null) null

View File

@ -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 // New API
val sessionID = recipient.address.serialize() val sessionID = recipient.address.serialize()
val contactDatabase = DatabaseComponent.get(context).sessionContactDatabase() val contactDatabase = DatabaseComponent.get(context).sessionContactDatabase()

View File

@ -7,56 +7,69 @@ import network.loki.messenger.libsession_util.util.Contact
import network.loki.messenger.libsession_util.util.UserPic import network.loki.messenger.libsession_util.util.UserPic
import nl.komponents.kovenant.Promise import nl.komponents.kovenant.Promise
import org.session.libsession.messaging.MessagingModuleConfiguration 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.messages.control.ConfigurationMessage
import org.session.libsession.messaging.sending_receiving.MessageSender
import org.session.libsession.utilities.Address import org.session.libsession.utilities.Address
import org.session.libsession.utilities.TextSecurePreferences
object ConfigurationMessageUtilities { object ConfigurationMessageUtilities {
const val isNewConfigEnabled = true
@JvmStatic @JvmStatic
fun syncConfigurationIfNeeded(context: Context) { fun syncConfigurationIfNeeded(context: Context) {
return // add if check here to schedule new config job process and return early
// val userPublicKey = TextSecurePreferences.getLocalNumber(context) ?: return if (isNewConfigEnabled) {
// val lastSyncTime = TextSecurePreferences.getLastConfigurationSyncTime(context) // schedule job if none exist
// val now = System.currentTimeMillis() TODO()
// if (now - lastSyncTime < 7 * 24 * 60 * 60 * 1000) return }
// val contacts = ContactUtilities.getAllContacts(context).filter { recipient -> val userPublicKey = TextSecurePreferences.getLocalNumber(context) ?: return
// !recipient.name.isNullOrEmpty() && !recipient.isLocalNumber && recipient.address.serialize().isNotEmpty() val lastSyncTime = TextSecurePreferences.getLastConfigurationSyncTime(context)
// }.map { recipient -> val now = System.currentTimeMillis()
// ConfigurationMessage.Contact( if (now - lastSyncTime < 7 * 24 * 60 * 60 * 1000) return
// publicKey = recipient.address.serialize(), val contacts = ContactUtilities.getAllContacts(context).filter { recipient ->
// name = recipient.name!!, !recipient.name.isNullOrEmpty() && !recipient.isLocalNumber && recipient.address.serialize().isNotEmpty()
// profilePicture = recipient.profileAvatar, }.map { recipient ->
// profileKey = recipient.profileKey, ConfigurationMessage.Contact(
// isApproved = recipient.isApproved, publicKey = recipient.address.serialize(),
// isBlocked = recipient.isBlocked, name = recipient.name!!,
// didApproveMe = recipient.hasApprovedMe() profilePicture = recipient.profileAvatar,
// ) profileKey = recipient.profileKey,
// } isApproved = recipient.isApproved,
// val configurationMessage = ConfigurationMessage.getCurrent(contacts) ?: return isBlocked = recipient.isBlocked,
// MessageSender.send(configurationMessage, Address.fromSerialized(userPublicKey)) didApproveMe = recipient.hasApprovedMe()
// TextSecurePreferences.setLastConfigurationSyncTime(context, now) )
}
val configurationMessage = ConfigurationMessage.getCurrent(contacts) ?: return
MessageSender.send(configurationMessage, Address.fromSerialized(userPublicKey))
TextSecurePreferences.setLastConfigurationSyncTime(context, now)
} }
fun forceSyncConfigurationNowIfNeeded(context: Context): Promise<Unit, Exception> { fun forceSyncConfigurationNowIfNeeded(context: Context): Promise<Unit, Exception> {
return Promise.ofSuccess(Unit) // add if check here to schedule new config job process and return early
// val userPublicKey = TextSecurePreferences.getLocalNumber(context) ?: return Promise.ofSuccess(Unit) if (isNewConfigEnabled) {
// val contacts = ContactUtilities.getAllContacts(context).filter { recipient -> // schedule job if none exist
// !recipient.isGroupRecipient && !recipient.name.isNullOrEmpty() && !recipient.isLocalNumber && recipient.address.serialize().isNotEmpty() TODO()
// }.map { recipient -> }
// ConfigurationMessage.Contact( val userPublicKey = TextSecurePreferences.getLocalNumber(context) ?: return Promise.ofSuccess(Unit)
// publicKey = recipient.address.serialize(), val contacts = ContactUtilities.getAllContacts(context).filter { recipient ->
// name = recipient.name!!, !recipient.isGroupRecipient && !recipient.name.isNullOrEmpty() && !recipient.isLocalNumber && recipient.address.serialize().isNotEmpty()
// profilePicture = recipient.profileAvatar, }.map { recipient ->
// profileKey = recipient.profileKey, ConfigurationMessage.Contact(
// isApproved = recipient.isApproved, publicKey = recipient.address.serialize(),
// isBlocked = recipient.isBlocked, name = recipient.name!!,
// didApproveMe = recipient.hasApprovedMe() profilePicture = recipient.profileAvatar,
// ) profileKey = recipient.profileKey,
// } isApproved = recipient.isApproved,
// val configurationMessage = ConfigurationMessage.getCurrent(contacts) ?: return Promise.ofSuccess(Unit) isBlocked = recipient.isBlocked,
// val promise = MessageSender.send(configurationMessage, Destination.from(Address.fromSerialized(userPublicKey))) didApproveMe = recipient.hasApprovedMe()
// TextSecurePreferences.setLastConfigurationSyncTime(context, System.currentTimeMillis()) )
// return promise }
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 private fun maybeUserSecretKey() = MessagingModuleConfiguration.shared.getUserED25519KeyPair()?.secretKey?.asBytes
@ -110,7 +123,7 @@ object ConfigurationMessageUtilities {
return dump return dump
} }
fun generateConversationDump(context: Context): ByteArray? { fun generateConversationVolatileDump(context: Context): ByteArray? {
TODO() TODO()
} }

View File

@ -37,7 +37,7 @@ interface StorageProtocol {
fun getUserPublicKey(): String? fun getUserPublicKey(): String?
fun getUserX25519KeyPair(): ECKeyPair fun getUserX25519KeyPair(): ECKeyPair
fun getUserProfile(): Profile fun getUserProfile(): Profile
fun setUserProfilePictureURL(newProfilePicture: String) fun setUserProfilePictureURL(newProfilePicture: String?)
// Signal // Signal
fun getOrGenerateRegistrationID(): Int fun getOrGenerateRegistrationID(): Int

View File

@ -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()
}
}

View File

@ -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.ExpirationTimerUpdate
import org.session.libsession.messaging.messages.control.MessageRequestResponse import org.session.libsession.messaging.messages.control.MessageRequestResponse
import org.session.libsession.messaging.messages.control.ReadReceipt 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.TypingIndicator
import org.session.libsession.messaging.messages.control.UnsendRequest import org.session.libsession.messaging.messages.control.UnsendRequest
import org.session.libsession.messaging.messages.visible.VisibleMessage import org.session.libsession.messaging.messages.visible.VisibleMessage
@ -138,6 +139,7 @@ object MessageReceiver {
UnsendRequest.fromProto(proto) ?: UnsendRequest.fromProto(proto) ?:
MessageRequestResponse.fromProto(proto) ?: MessageRequestResponse.fromProto(proto) ?:
CallMessage.fromProto(proto) ?: CallMessage.fromProto(proto) ?:
SharedConfigurationMessage.fromProto(proto) ?:
VisibleMessage.fromProto(proto) ?: run { VisibleMessage.fromProto(proto) ?: run {
throw Error.UnknownMessage 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 // 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 // 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. // 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: // Allow duplicates in this case to avoid the following situation:
// • The app performed a background poll or received a push notification // • The app performed a background poll or received a push notification
// • This method was invoked and the received message timestamps table was updated // • This method was invoked and the received message timestamps table was updated
// • Processing wasn't finished // • Processing wasn't finished
// • The user doesn't see the new closed group // • 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 { } else {
if (storage.isDuplicateMessage(envelope.timestamp)) { throw Error.DuplicateMessage } if (storage.isDuplicateMessage(envelope.timestamp)) { throw Error.DuplicateMessage }
storage.addReceivedMessageTimestamp(envelope.timestamp) storage.addReceivedMessageTimestamp(envelope.timestamp)

View File

@ -7,6 +7,8 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import network.loki.messenger.libsession_util.ConfigBase 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 network.loki.messenger.libsession_util.UserProfile
import nl.komponents.kovenant.Deferred import nl.komponents.kovenant.Deferred
import nl.komponents.kovenant.Promise 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.BatchMessageReceiveJob
import org.session.libsession.messaging.jobs.JobQueue import org.session.libsession.messaging.jobs.JobQueue
import org.session.libsession.messaging.jobs.MessageReceiveParameters 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.RawResponse
import org.session.libsession.snode.SnodeAPI import org.session.libsession.snode.SnodeAPI
import org.session.libsession.snode.SnodeModule import org.session.libsession.snode.SnodeModule
@ -121,18 +125,36 @@ class Poller(private val configFactory: ConfigFactoryProtocol) {
} }
} }
private fun processUserConfig(snode: Snode, rawMessages: RawResponse) { private fun processConfig(snode: Snode, rawMessages: RawResponse, namespace: Int, forConfigObject: ConfigBase?) {
SnodeAPI.parseRawMessagesResponse(rawMessages, snode, userPublicKey) 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 ?
private fun processContactsConfig(snode: Snode, rawMessages: RawResponse) { 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)
} }
private fun processConvoVolatileConfig(snode: Snode, rawMessages: RawResponse) {
} }
// process new results
configFactory.persist(forConfigObject)
configFactory.notifyUpdates(forConfigObject)
}
private fun poll(snode: Snode, deferred: Deferred<Unit, Exception>): Promise<Unit, Exception> { private fun poll(snode: Snode, deferred: Deferred<Unit, Exception>): Promise<Unit, Exception> {
if (!hasStarted) { return Promise.ofFail(PromiseCanceledException()) } if (!hasStarted) { return Promise.ofFail(PromiseCanceledException()) }
@ -160,15 +182,20 @@ class Poller(private val configFactory: ConfigFactoryProtocol) {
if (deferred.promise.isDone()) { if (deferred.promise.isDone()) {
return@bind Promise.ofSuccess(Unit) return@bind Promise.ofSuccess(Unit)
} else { } else {
// TODO: remove log after testing responses
Log.d("Loki-DBG", JsonUtil.toJson(rawResponses)) Log.d("Loki-DBG", JsonUtil.toJson(rawResponses))
val requestList = (rawResponses["results"] as List<RawResponse>) val requestList = (rawResponses["results"] as List<RawResponse>)
// 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) -> requestSparseArray.keyIterator().withIndex().forEach { (requestIndex, key) ->
requestList.getOrNull(requestIndex)?.let { rawResponse -> requestList.getOrNull(requestIndex)?.let { rawResponse ->
if (key == Namespace.DEFAULT) { if (key == Namespace.DEFAULT) {
processPersonalMessages(snode, rawResponse) processPersonalMessages(snode, rawResponse)
} else { } else {
when (ConfigBase.kindFor(key)) { 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)
} }
} }
} }

View File

@ -628,10 +628,12 @@ object SnodeAPI {
} }
} }
fun parseRawMessagesResponse(rawResponse: RawResponse, snode: Snode, publicKey: String, namespace: Int = 0): List<Pair<SignalServiceProtos.Envelope, String?>> { fun parseRawMessagesResponse(rawResponse: RawResponse, snode: Snode, publicKey: String, namespace: Int = 0, updateLatestHash: Boolean = true): List<Pair<SignalServiceProtos.Envelope, String?>> {
val messages = rawResponse["messages"] as? List<*> val messages = rawResponse["messages"] as? List<*>
return if (messages != null) { return if (messages != null) {
if (updateLatestHash) {
updateLastMessageHashValueIfPossible(snode, publicKey, messages, namespace) updateLastMessageHashValueIfPossible(snode, publicKey, messages, namespace)
}
val newRawMessages = removeDuplicates(publicKey, messages, namespace) val newRawMessages = removeDuplicates(publicKey, messages, namespace)
return parseEnvelopes(newRawMessages) return parseEnvelopes(newRawMessages)
} else { } else {

View File

@ -1,5 +1,6 @@
package org.session.libsession.utilities 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.Contacts
import network.loki.messenger.libsession_util.ConversationVolatileConfig import network.loki.messenger.libsession_util.ConversationVolatileConfig
import network.loki.messenger.libsession_util.UserProfile import network.loki.messenger.libsession_util.UserProfile
@ -8,7 +9,7 @@ interface ConfigFactoryProtocol {
val user: UserProfile? val user: UserProfile?
val contacts: Contacts? val contacts: Contacts?
val convoVolatile: ConversationVolatileConfig? val convoVolatile: ConversationVolatileConfig?
fun saveUserConfigDump() fun persist(forConfigObject: ConfigBase)
fun saveContactConfigDump() fun appendHash(configObject: ConfigBase, hash: String)
fun saveConvoVolatileConfigDump() fun notifyUpdates(forConfigObject: ConfigBase)
} }

View File

@ -3,7 +3,6 @@ package org.session.libsession.utilities
import android.content.Context import android.content.Context
import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate
import org.session.libsession.messaging.sending_receiving.notifications.MessageNotifier import org.session.libsession.messaging.sending_receiving.notifications.MessageNotifier
import org.session.libsession.utilities.Address
import org.session.libsession.utilities.recipients.Recipient import org.session.libsession.utilities.recipients.Recipient
class SSKEnvironment( class SSKEnvironment(
@ -32,7 +31,7 @@ class SSKEnvironment(
fun setNickname(context: Context, recipient: Recipient, nickname: String?) fun setNickname(context: Context, recipient: Recipient, nickname: String?)
fun setName(context: Context, recipient: Recipient, name: String) fun setName(context: Context, recipient: Recipient, name: String)
fun setProfilePictureURL(context: Context, recipient: Recipient, profilePictureURL: 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) fun setUnidentifiedAccessMode(context: Context, recipient: Recipient, unidentifiedAccessMode: Recipient.UnidentifiedAccessMode)
} }