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
13 changed files with 277 additions and 94 deletions

View File

@@ -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

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.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)

View File

@@ -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<Unit, Exception>): Promise<Unit, Exception> {
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<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) ->
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)
}
}
}

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<*>
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 {

View File

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

View File

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