mirror of
https://github.com/oxen-io/session-android.git
synced 2025-12-12 08:13:14 +00:00
feat: expand on the config sync job, finish basic implementation to test against
This commit is contained in:
@@ -35,7 +35,6 @@ dependencies {
|
||||
implementation 'com.annimon:stream:1.1.8'
|
||||
implementation 'com.makeramen:roundedimageview:2.1.0'
|
||||
implementation 'com.esotericsoftware:kryo:5.1.1'
|
||||
implementation "com.google.protobuf:protobuf-java:$protobufVersion"
|
||||
implementation "com.fasterxml.jackson.core:jackson-databind:$jacksonDatabindVersion"
|
||||
implementation "com.github.oxen-io.session-android-curve-25519:curve25519-java:$curve25519Version"
|
||||
implementation "com.squareup.okhttp3:okhttp:$okhttpVersion"
|
||||
|
||||
@@ -8,6 +8,7 @@ import org.session.libsession.messaging.contacts.Contact
|
||||
import org.session.libsession.messaging.jobs.AttachmentUploadJob
|
||||
import org.session.libsession.messaging.jobs.Job
|
||||
import org.session.libsession.messaging.jobs.MessageSendJob
|
||||
import org.session.libsession.messaging.messages.Destination
|
||||
import org.session.libsession.messaging.messages.Message
|
||||
import org.session.libsession.messaging.messages.control.ConfigurationMessage
|
||||
import org.session.libsession.messaging.messages.control.MessageRequestResponse
|
||||
@@ -50,6 +51,7 @@ interface StorageProtocol {
|
||||
fun getMessageSendJob(messageSendJobID: String): MessageSendJob?
|
||||
fun getMessageReceiveJob(messageReceiveJobID: String): Job?
|
||||
fun getGroupAvatarDownloadJob(server: String, room: String): Job?
|
||||
fun getConfigSyncJob(destination: Destination): Job?
|
||||
fun resumeMessageSendJobIfNeeded(messageSendJobID: String)
|
||||
fun isJobCanceled(job: Job): Boolean
|
||||
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
package org.session.libsession.messaging.jobs
|
||||
|
||||
import network.loki.messenger.libsession_util.ConfigBase
|
||||
import network.loki.messenger.libsession_util.ConfigBase.Companion.protoKindFor
|
||||
import nl.komponents.kovenant.functional.bind
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.messaging.messages.Destination
|
||||
import org.session.libsession.messaging.messages.control.SharedConfigurationMessage
|
||||
import org.session.libsession.messaging.sending_receiving.MessageSender
|
||||
import org.session.libsession.messaging.utilities.Data
|
||||
import org.session.libsession.snode.RawResponse
|
||||
import org.session.libsession.snode.SnodeAPI
|
||||
import org.session.libsignal.utilities.Log
|
||||
|
||||
// only contact (self) and closed group destinations will be supported
|
||||
@@ -16,20 +22,122 @@ data class ConfigurationSyncJob(val destination: Destination): Job {
|
||||
|
||||
override suspend fun execute() {
|
||||
val userEdKeyPair = MessagingModuleConfiguration.shared.getUserED25519KeyPair()
|
||||
if (destination is Destination.ClosedGroup
|
||||
val userPublicKey = MessagingModuleConfiguration.shared.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
|
||||
|| !ConfigBase.isNewConfigEnabled
|
||||
// if we don't have a user ed key pair for signing updates
|
||||
|| userEdKeyPair == null
|
||||
// this will be useful to not handle null delegate cases
|
||||
|| delegate == null
|
||||
// check our local identity key exists
|
||||
|| userPublicKey.isNullOrEmpty()
|
||||
// don't allow pushing configs for non-local user
|
||||
|| (destination is Destination.Contact && destination.publicKey != userPublicKey)
|
||||
) {
|
||||
// TODO: currently we only deal with single destination until closed groups refactor / implement LCG
|
||||
Log.w(TAG, "Not handling config sync job, TODO")
|
||||
Log.w(TAG, "No need to run config sync job, TODO")
|
||||
delegate?.handleJobSucceeded(this)
|
||||
return
|
||||
}
|
||||
|
||||
// configFactory singleton instance will come in handy for modifying hashes and fetching configs for namespace etc
|
||||
val configFactory = MessagingModuleConfiguration.shared.configFactory
|
||||
|
||||
// get latest states, filter out configs that don't need push
|
||||
val configsRequiringPush = listOfNotNull(
|
||||
configFactory.user,
|
||||
configFactory.contacts,
|
||||
configFactory.convoVolatile
|
||||
).filter { config -> config.needsPush() }
|
||||
|
||||
// don't run anything if we don't need to push anything
|
||||
if (configsRequiringPush.isEmpty()) return delegate.handleJobSucceeded(this)
|
||||
|
||||
// allow null results here so the list index matches configsRequiringPush
|
||||
val batchObjects: List<Pair<SharedConfigurationMessage, SnodeAPI.SnodeBatchRequestInfo>?> = configsRequiringPush.map { config ->
|
||||
val (data, seqNo) = config.push()
|
||||
SharedConfigurationMessage(config.protoKindFor(), data, seqNo) to config
|
||||
}.map { (message, config) ->
|
||||
// return a list of batch request objects
|
||||
val snodeMessage = MessageSender.buildWrappedMessageToSnode(destination, message, true)
|
||||
val authenticated = SnodeAPI.buildAuthenticatedStoreBatchInfo(
|
||||
destination.destinationPublicKey(),
|
||||
config.configNamespace(),
|
||||
snodeMessage
|
||||
) ?: return@map null // this entry will be null otherwise
|
||||
message to authenticated // to keep track of seqNo for calling confirmPushed later
|
||||
}
|
||||
|
||||
val toDeleteRequest = configsRequiringPush.map { base ->
|
||||
configFactory.getHashesFor(base)
|
||||
// accumulate by adding together
|
||||
}.reduce(List<String>::plus).let { toDeleteFromAllNamespaces ->
|
||||
if (toDeleteFromAllNamespaces.isEmpty()) null
|
||||
else SnodeAPI.buildAuthenticatedDeleteBatchInfo(destination.destinationPublicKey(), toDeleteFromAllNamespaces)
|
||||
}
|
||||
|
||||
if (batchObjects.any { it == null }) {
|
||||
// stop running here, something like a signing error occurred
|
||||
return delegate.handleJobFailedPermanently(this, NullPointerException("One or more requests had a null batch request info"))
|
||||
}
|
||||
|
||||
val allRequests = mutableListOf<SnodeAPI.SnodeBatchRequestInfo>()
|
||||
allRequests += batchObjects.requireNoNulls().map { (_, request) -> request }
|
||||
// add in the deletion if we have any hashes
|
||||
if (toDeleteRequest != null) allRequests += toDeleteRequest
|
||||
|
||||
val batchResponse = SnodeAPI.getSingleTargetSnode(destination.destinationPublicKey()).bind { snode ->
|
||||
SnodeAPI.getRawBatchResponse(
|
||||
snode,
|
||||
destination.destinationPublicKey(),
|
||||
allRequests,
|
||||
sequence = true
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
val rawResponses = batchResponse.get()
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val responseList = (rawResponses["results"] as List<RawResponse>)
|
||||
// we are always adding in deletions at the end
|
||||
val deletionResponse = if (toDeleteRequest != null) responseList.last() else null
|
||||
val deletedHashes = deletionResponse?.let {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
deletionResponse["deleted"] as? List<String>
|
||||
}?.toSet() ?: emptySet()
|
||||
|
||||
// at this point responseList index should line up with configsRequiringPush index
|
||||
configsRequiringPush.forEachIndexed { index, config ->
|
||||
val (toPushMessage, _) = batchObjects[index]!!
|
||||
val response = responseList[index]
|
||||
val insertHash = response["hash"] as? String ?: run {
|
||||
Log.w(TAG, "No hash returned for the configuration in namespace ${config.configNamespace()}")
|
||||
return@forEachIndexed
|
||||
}
|
||||
|
||||
// confirm pushed seqno
|
||||
val thisSeqNo = toPushMessage.seqNo
|
||||
config.confirmPushed(thisSeqNo)
|
||||
// wipe any of the existing hashes which we deleted (they may or may not be in this namespace)
|
||||
if (configFactory.removeHashesFor(config, deletedHashes.toSet())) {
|
||||
Log.d(TAG, "Successfully removed the deleted hashes from ${config.javaClass.simpleName}")
|
||||
}
|
||||
// store the new hash in list of hashes to track against
|
||||
configFactory.appendHash(config, insertHash)
|
||||
// dump and write config after successful
|
||||
configFactory.persist(config)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error performing batch request")
|
||||
}
|
||||
delegate.handleJobSucceeded(this)
|
||||
}
|
||||
|
||||
fun Destination.destinationPublicKey(): String = when (this) {
|
||||
is Destination.Contact -> publicKey
|
||||
is Destination.ClosedGroup -> groupPublicKey
|
||||
else -> throw NullPointerException("Not public key for this destination")
|
||||
}
|
||||
|
||||
override fun serialize(): Data {
|
||||
@@ -74,5 +182,4 @@ data class ConfigurationSyncJob(val destination: Destination): Job {
|
||||
return ConfigurationSyncJob(destination)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -217,6 +217,7 @@ class JobQueue : JobDelegate {
|
||||
GroupAvatarDownloadJob.KEY,
|
||||
BackgroundGroupAddJob.KEY,
|
||||
OpenGroupDeleteJob.KEY,
|
||||
ConfigurationSyncJob.KEY,
|
||||
)
|
||||
allJobTypes.forEach { type ->
|
||||
resumePendingJobs(type)
|
||||
|
||||
@@ -13,6 +13,7 @@ import org.session.libsession.messaging.messages.control.ClosedGroupControlMessa
|
||||
import org.session.libsession.messaging.messages.control.ConfigurationMessage
|
||||
import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate
|
||||
import org.session.libsession.messaging.messages.control.MessageRequestResponse
|
||||
import org.session.libsession.messaging.messages.control.SharedConfigurationMessage
|
||||
import org.session.libsession.messaging.messages.control.UnsendRequest
|
||||
import org.session.libsession.messaging.messages.visible.LinkPreview
|
||||
import org.session.libsession.messaging.messages.visible.Quote
|
||||
@@ -70,70 +71,113 @@ object MessageSender {
|
||||
}
|
||||
|
||||
// One-on-One Chats & Closed Groups
|
||||
@Throws(Exception::class)
|
||||
fun buildWrappedMessageToSnode(destination: Destination, message: Message, isSyncMessage: Boolean): SnodeMessage {
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val userPublicKey = storage.getUserPublicKey()
|
||||
// Set the timestamp, sender and recipient
|
||||
val messageSendTime = SnodeAPI.nowWithOffset
|
||||
if (message.sentTimestamp == null) {
|
||||
message.sentTimestamp =
|
||||
messageSendTime // Visible messages will already have their sent timestamp set
|
||||
}
|
||||
|
||||
message.sender = userPublicKey
|
||||
|
||||
when (destination) {
|
||||
is Destination.Contact -> message.recipient = destination.publicKey
|
||||
is Destination.ClosedGroup -> message.recipient = destination.groupPublicKey
|
||||
else -> throw IllegalStateException("Destination should not be an open group.")
|
||||
}
|
||||
|
||||
val isSelfSend = (message.recipient == userPublicKey)
|
||||
// Validate the message
|
||||
if (!message.isValid()) {
|
||||
throw Error.InvalidMessage
|
||||
}
|
||||
// Stop here if this is a self-send, unless it's:
|
||||
// • a configuration message
|
||||
// • a sync message
|
||||
// • a closed group control message of type `new`
|
||||
var isNewClosedGroupControlMessage = false
|
||||
if (message is ClosedGroupControlMessage && message.kind is ClosedGroupControlMessage.Kind.New) isNewClosedGroupControlMessage =
|
||||
true
|
||||
if (isSelfSend
|
||||
&& message !is ConfigurationMessage
|
||||
&& !isSyncMessage
|
||||
&& !isNewClosedGroupControlMessage
|
||||
&& message !is UnsendRequest
|
||||
&& message !is SharedConfigurationMessage
|
||||
) {
|
||||
throw Error.InvalidMessage
|
||||
}
|
||||
// Attach the user's profile if needed
|
||||
if (message is VisibleMessage) {
|
||||
message.profile = storage.getUserProfile()
|
||||
}
|
||||
if (message is MessageRequestResponse) {
|
||||
message.profile = storage.getUserProfile()
|
||||
}
|
||||
// Convert it to protobuf
|
||||
val proto = message.toProto() ?: throw Error.ProtoConversionFailed
|
||||
// Serialize the protobuf
|
||||
val plaintext = PushTransportDetails.getPaddedMessageBody(proto.toByteArray())
|
||||
// Encrypt the serialized protobuf
|
||||
val ciphertext = when (destination) {
|
||||
is Destination.Contact -> MessageEncrypter.encrypt(plaintext, destination.publicKey)
|
||||
is Destination.ClosedGroup -> {
|
||||
val encryptionKeyPair =
|
||||
MessagingModuleConfiguration.shared.storage.getLatestClosedGroupEncryptionKeyPair(
|
||||
destination.groupPublicKey
|
||||
)!!
|
||||
MessageEncrypter.encrypt(plaintext, encryptionKeyPair.hexEncodedPublicKey)
|
||||
}
|
||||
else -> throw IllegalStateException("Destination should not be open group.")
|
||||
}
|
||||
// Wrap the result
|
||||
val kind: SignalServiceProtos.Envelope.Type
|
||||
val senderPublicKey: String
|
||||
when (destination) {
|
||||
is Destination.Contact -> {
|
||||
kind = SignalServiceProtos.Envelope.Type.SESSION_MESSAGE
|
||||
senderPublicKey = ""
|
||||
}
|
||||
is Destination.ClosedGroup -> {
|
||||
kind = SignalServiceProtos.Envelope.Type.CLOSED_GROUP_MESSAGE
|
||||
senderPublicKey = destination.groupPublicKey
|
||||
}
|
||||
else -> throw IllegalStateException("Destination should not be open group.")
|
||||
}
|
||||
val wrappedMessage = MessageWrapper.wrap(kind, message.sentTimestamp!!, senderPublicKey, ciphertext)
|
||||
val base64EncodedData = Base64.encodeBytes(wrappedMessage)
|
||||
// Send the result
|
||||
return SnodeMessage(
|
||||
message.recipient!!,
|
||||
base64EncodedData,
|
||||
message.ttl,
|
||||
messageSendTime
|
||||
)
|
||||
}
|
||||
|
||||
private fun sendToSnodeDestination(destination: Destination, message: Message, isSyncMessage: Boolean = false): Promise<Unit, Exception> {
|
||||
val deferred = deferred<Unit, Exception>()
|
||||
val promise = deferred.promise
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val userPublicKey = storage.getUserPublicKey()
|
||||
// Set the timestamp, sender and recipient
|
||||
if (message.sentTimestamp == null) {
|
||||
message.sentTimestamp = System.currentTimeMillis() // Visible messages will already have their sent timestamp set
|
||||
}
|
||||
|
||||
val messageSendTime = System.currentTimeMillis()
|
||||
// recipient will be set later, so initialize it as a function here
|
||||
val isSelfSend = { message.recipient == userPublicKey }
|
||||
|
||||
message.sender = userPublicKey
|
||||
val isSelfSend = (message.recipient == userPublicKey)
|
||||
// Set the failure handler (need it here already for precondition failure handling)
|
||||
fun handleFailure(error: Exception) {
|
||||
handleFailedMessageSend(message, error)
|
||||
if (destination is Destination.Contact && message is VisibleMessage && !isSelfSend) {
|
||||
if (destination is Destination.Contact && message is VisibleMessage && !isSelfSend()) {
|
||||
SnodeModule.shared.broadcaster.broadcast("messageFailed", message.sentTimestamp!!)
|
||||
}
|
||||
deferred.reject(error)
|
||||
}
|
||||
try {
|
||||
when (destination) {
|
||||
is Destination.Contact -> message.recipient = destination.publicKey
|
||||
is Destination.ClosedGroup -> message.recipient = destination.groupPublicKey
|
||||
else -> throw IllegalStateException("Destination should not be an open group.")
|
||||
}
|
||||
// Validate the message
|
||||
if (!message.isValid()) { throw Error.InvalidMessage }
|
||||
// Stop here if this is a self-send, unless it's:
|
||||
// • a configuration message
|
||||
// • a sync message
|
||||
// • a closed group control message of type `new`
|
||||
var isNewClosedGroupControlMessage = false
|
||||
if (message is ClosedGroupControlMessage && message.kind is ClosedGroupControlMessage.Kind.New) isNewClosedGroupControlMessage = true
|
||||
if (isSelfSend && message !is ConfigurationMessage && !isSyncMessage && !isNewClosedGroupControlMessage && message !is UnsendRequest) {
|
||||
handleSuccessfulMessageSend(message, destination)
|
||||
deferred.resolve(Unit)
|
||||
return promise
|
||||
}
|
||||
// Attach the user's profile if needed
|
||||
if (message is VisibleMessage) {
|
||||
message.profile = storage.getUserProfile()
|
||||
}
|
||||
if (message is MessageRequestResponse) {
|
||||
message.profile = storage.getUserProfile()
|
||||
}
|
||||
// Convert it to protobuf
|
||||
val proto = message.toProto() ?: throw Error.ProtoConversionFailed
|
||||
// Serialize the protobuf
|
||||
val plaintext = PushTransportDetails.getPaddedMessageBody(proto.toByteArray())
|
||||
// Encrypt the serialized protobuf
|
||||
val ciphertext = when (destination) {
|
||||
is Destination.Contact -> MessageEncrypter.encrypt(plaintext, destination.publicKey)
|
||||
is Destination.ClosedGroup -> {
|
||||
val encryptionKeyPair = MessagingModuleConfiguration.shared.storage.getLatestClosedGroupEncryptionKeyPair(destination.groupPublicKey)!!
|
||||
MessageEncrypter.encrypt(plaintext, encryptionKeyPair.hexEncodedPublicKey)
|
||||
}
|
||||
else -> throw IllegalStateException("Destination should not be open group.")
|
||||
}
|
||||
// Wrap the result
|
||||
val kind: SignalServiceProtos.Envelope.Type
|
||||
val senderPublicKey: String
|
||||
val snodeMessage = buildWrappedMessageToSnode(destination, message, isSyncMessage)
|
||||
// TODO: this might change in future for config messages
|
||||
val forkInfo = SnodeAPI.forkInfo
|
||||
val namespaces: List<Int> = when {
|
||||
@@ -143,29 +187,6 @@ object MessageSender {
|
||||
&& forkInfo.hasNamespaces() -> listOf(Namespace.UNAUTHENTICATED_CLOSED_GROUP, Namespace.DEFAULT)
|
||||
else -> listOf(Namespace.DEFAULT)
|
||||
}
|
||||
when (destination) {
|
||||
is Destination.Contact -> {
|
||||
kind = SignalServiceProtos.Envelope.Type.SESSION_MESSAGE
|
||||
senderPublicKey = ""
|
||||
}
|
||||
is Destination.ClosedGroup -> {
|
||||
kind = SignalServiceProtos.Envelope.Type.CLOSED_GROUP_MESSAGE
|
||||
senderPublicKey = destination.groupPublicKey
|
||||
}
|
||||
else -> throw IllegalStateException("Destination should not be open group.")
|
||||
}
|
||||
val wrappedMessage = MessageWrapper.wrap(kind, message.sentTimestamp!!, senderPublicKey, ciphertext)
|
||||
// Send the result
|
||||
if (destination is Destination.Contact && message is VisibleMessage && !isSelfSend) {
|
||||
SnodeModule.shared.broadcaster.broadcast("calculatingPoW", messageSendTime)
|
||||
}
|
||||
val base64EncodedData = Base64.encodeBytes(wrappedMessage)
|
||||
// Send the result
|
||||
val timestamp = messageSendTime + SnodeAPI.clockOffset
|
||||
val snodeMessage = SnodeMessage(message.recipient!!, base64EncodedData, message.ttl, timestamp)
|
||||
if (destination is Destination.Contact && message is VisibleMessage && !isSelfSend) {
|
||||
SnodeModule.shared.broadcaster.broadcast("sendingMessage", messageSendTime)
|
||||
}
|
||||
namespaces.map { namespace -> SnodeAPI.sendMessage(snodeMessage, requiresAuth = false, namespace = namespace) }.let { promises ->
|
||||
var isSuccess = false
|
||||
val promiseCount = promises.size
|
||||
@@ -174,9 +195,6 @@ object MessageSender {
|
||||
promise.success {
|
||||
if (isSuccess) { return@success } // Succeed as soon as the first promise succeeds
|
||||
isSuccess = true
|
||||
if (destination is Destination.Contact && message is VisibleMessage && !isSelfSend) {
|
||||
SnodeModule.shared.broadcaster.broadcast("messageSent", messageSendTime)
|
||||
}
|
||||
val hash = it["hash"] as? String
|
||||
message.serverHash = hash
|
||||
handleSuccessfulMessageSend(message, destination, isSyncMessage)
|
||||
|
||||
@@ -163,7 +163,8 @@ class Poller(private val configFactory: ConfigFactoryProtocol) {
|
||||
val requestSparseArray = SparseArray<SnodeAPI.SnodeBatchRequestInfo>()
|
||||
// get messages
|
||||
SnodeAPI.buildAuthenticatedRetrieveBatchRequest(snode, userPublicKey)!!.also { personalMessages ->
|
||||
requestSparseArray[personalMessages.namespace] = personalMessages
|
||||
// namespaces here should always be set
|
||||
requestSparseArray[personalMessages.namespace!!] = personalMessages
|
||||
}
|
||||
// get the latest convo info volatile
|
||||
listOfNotNull(configFactory.user, configFactory.contacts, configFactory.convoVolatile).mapNotNull { config ->
|
||||
@@ -172,7 +173,8 @@ class Poller(private val configFactory: ConfigFactoryProtocol) {
|
||||
config.configNamespace()
|
||||
)
|
||||
}.forEach { request ->
|
||||
requestSparseArray[request.namespace] = request
|
||||
// namespaces here should always be set
|
||||
requestSparseArray[request.namespace!!] = request
|
||||
}
|
||||
|
||||
val requests = requestSparseArray.valueIterator().asSequence().toList()
|
||||
|
||||
@@ -101,7 +101,7 @@ object SnodeAPI {
|
||||
val method: String,
|
||||
val params: Map<String, Any>,
|
||||
@Transient
|
||||
val namespace: Int
|
||||
val namespace: Int?
|
||||
) // assume signatures, pubkey and namespaces are attached in parameters if required
|
||||
|
||||
// Internal API
|
||||
@@ -365,10 +365,93 @@ object SnodeAPI {
|
||||
return invoke(Snode.Method.Retrieve, snode, parameters, publicKey)
|
||||
}
|
||||
|
||||
fun buildAuthenticatedStoreBatchInfo(publicKey: String, namespace: Int, message: SnodeMessage): SnodeBatchRequestInfo? {
|
||||
val params = mutableMapOf<String, Any>()
|
||||
// load the message data params into the sub request
|
||||
// currently loads:
|
||||
// pubKey
|
||||
// data
|
||||
// ttl
|
||||
// timestamp
|
||||
params.putAll(message.toJSON())
|
||||
params["namespace"] = namespace
|
||||
|
||||
// used for sig generation since it is also the value used in timestamp parameter
|
||||
val messageTimestamp = message.timestamp
|
||||
|
||||
val userEd25519KeyPair = try {
|
||||
MessagingModuleConfiguration.shared.getUserED25519KeyPair() ?: return null
|
||||
} catch (e: Exception) {
|
||||
return null
|
||||
}
|
||||
|
||||
val ed25519PublicKey = userEd25519KeyPair.publicKey.asHexString
|
||||
val signature = ByteArray(Sign.BYTES)
|
||||
val verificationData = "store$namespace$messageTimestamp".toByteArray()
|
||||
try {
|
||||
sodium.cryptoSignDetached(
|
||||
signature,
|
||||
verificationData,
|
||||
verificationData.size.toLong(),
|
||||
userEd25519KeyPair.secretKey.asBytes
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.e("Loki", "Signing data failed with user secret key", e)
|
||||
}
|
||||
// timestamp already set
|
||||
params["pubkey_ed25519"] = ed25519PublicKey
|
||||
params["signature"] = Base64.encodeBytes(signature)
|
||||
return SnodeBatchRequestInfo(
|
||||
Snode.Method.SendMessage.rawValue,
|
||||
params,
|
||||
namespace
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Message hashes can be shared across multiple namespaces (for a single public key destination)
|
||||
* @param publicKey the destination's identity public key to delete from (05...)
|
||||
* @param messageHashes a list of stored message hashes to delete from the server
|
||||
* @param required indicates that *at least one* message in the list is deleted from the server, otherwise it will return 404
|
||||
*/
|
||||
fun buildAuthenticatedDeleteBatchInfo(publicKey: String, messageHashes: List<String>, required: Boolean = false): SnodeBatchRequestInfo? {
|
||||
val params = mutableMapOf(
|
||||
"pubkey" to publicKey,
|
||||
"required" to required, // could be omitted technically but explicit here
|
||||
"messages" to messageHashes
|
||||
)
|
||||
val userEd25519KeyPair = try {
|
||||
MessagingModuleConfiguration.shared.getUserED25519KeyPair() ?: return null
|
||||
} catch (e: Exception) {
|
||||
return null
|
||||
}
|
||||
val ed25519PublicKey = userEd25519KeyPair.publicKey.asHexString
|
||||
val signature = ByteArray(Sign.BYTES)
|
||||
val verificationData = "delete${messageHashes.joinToString("")}".toByteArray()
|
||||
try {
|
||||
sodium.cryptoSignDetached(
|
||||
signature,
|
||||
verificationData,
|
||||
verificationData.size.toLong(),
|
||||
userEd25519KeyPair.secretKey.asBytes
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.e("Loki", "Signing data failed with user secret key", e)
|
||||
return null
|
||||
}
|
||||
params["pubkey_ed25519"] = ed25519PublicKey
|
||||
params["signature"] = Base64.encodeBytes(signature)
|
||||
return SnodeBatchRequestInfo(
|
||||
Snode.Method.Retrieve.rawValue,
|
||||
params,
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
fun buildAuthenticatedRetrieveBatchRequest(snode: Snode, publicKey: String, namespace: Int = 0): SnodeBatchRequestInfo? {
|
||||
val lastHashValue = database.getLastMessageHashValue(snode, publicKey, namespace) ?: ""
|
||||
val params = mutableMapOf<String, Any>(
|
||||
"pubKey" to publicKey,
|
||||
"pubkey" to publicKey,
|
||||
"last_hash" to lastHashValue,
|
||||
)
|
||||
val userEd25519KeyPair = try {
|
||||
@@ -405,11 +488,11 @@ object SnodeAPI {
|
||||
)
|
||||
}
|
||||
|
||||
fun getRawBatchResponse(snode: Snode, publicKey: String, requests: List<SnodeBatchRequestInfo>): RawResponsePromise {
|
||||
fun getRawBatchResponse(snode: Snode, publicKey: String, requests: List<SnodeBatchRequestInfo>, sequence: Boolean = false): RawResponsePromise {
|
||||
val parameters = mutableMapOf<String, Any>(
|
||||
"requests" to requests
|
||||
)
|
||||
return invoke(Snode.Method.Batch, snode, parameters, publicKey)
|
||||
return invoke(if (sequence) Snode.Method.Sequence else Snode.Method.Batch, snode, parameters, publicKey)
|
||||
}
|
||||
|
||||
fun getExpiries(messageHashes: List<String>, publicKey: String) : RawResponsePromise {
|
||||
|
||||
@@ -12,4 +12,6 @@ interface ConfigFactoryProtocol {
|
||||
fun persist(forConfigObject: ConfigBase)
|
||||
fun appendHash(configObject: ConfigBase, hash: String)
|
||||
fun notifyUpdates(forConfigObject: ConfigBase)
|
||||
fun getHashesFor(forConfigObject: ConfigBase): List<String>
|
||||
fun removeHashesFor(config: ConfigBase, deletedHashes: Set<String>): Boolean
|
||||
}
|
||||
Reference in New Issue
Block a user