refactor: config database changes, new protos, adding in support for config base namespace queries

This commit is contained in:
0x330a 2023-01-18 15:51:29 +11:00
parent 00fe77af8a
commit 8bea5e73e6
No known key found for this signature in database
GPG Key ID: 267811D6E6A2698C
8 changed files with 1261 additions and 163 deletions

View File

@ -8,34 +8,39 @@ 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) {
companion object { companion object {
private const val KEY = "key" private const val VARIANT = "variant"
private const val VALUE = "value" private const val PUBKEY = "publicKey"
private const val DATA = "data"
private const val COMBINED_MESSAGE_HASHES = "combined_message_hashes"
private const val TABLE_NAME = "configs_table" private const val TABLE_NAME = "configs_table"
const val CREATE_CONFIG_TABLE_COMMAND = "CREATE TABLE $TABLE_NAME ($KEY TEXT NOT NULL, $VALUE BLOB NOT NULL, PRIMARY KEY($KEY));" const val CREATE_CONFIG_TABLE_COMMAND =
private const val KEY_WHERE = "$KEY = ?" "CREATE TABLE $TABLE_NAME ($VARIANT TEXT NOT NULL, $PUBKEY TEXT NOT NULL, $DATA BLOB, $COMBINED_MESSAGE_HASHES TEXT, PRIMARY KEY($VARIANT, $PUBKEY));"
private const val VARIANT_WHERE = "$VARIANT = ?"
private const val VARIANT_AND_PUBKEY_WHERE = "$VARIANT = ? AND $PUBKEY = ?"
const val USER_KEY = "user" const val USER_KEY = "user"
const val CONTACTS_KEY = "contacts" const val CONTACTS_KEY = "contacts"
// conversations use publicKey / URL // conversations use publicKey / URL
} }
fun storeConfig(key: String, data: ByteArray) { fun storeConfig(variant: String, publicKey: String, data: ByteArray) {
val db = writableDatabase val db = writableDatabase
val contentValues = contentValuesOf( val contentValues = contentValuesOf(
KEY to key, VARIANT to variant,
VALUE to data PUBKEY to publicKey,
DATA to data
) )
db.insertOrUpdate(TABLE_NAME, contentValues, KEY_WHERE, arrayOf(key)) db.insertOrUpdate(TABLE_NAME, contentValues, VARIANT_AND_PUBKEY_WHERE, arrayOf(variant, publicKey))
} }
fun retrieveConfig(key: String): ByteArray? { fun retrieveConfig(variant: String, publicKey: String): ByteArray? {
val db = readableDatabase val db = readableDatabase
val query = db.query(TABLE_NAME, arrayOf(VALUE), KEY_WHERE, arrayOf(key),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 -> val bytes = query?.use { cursor ->
if (!cursor.moveToFirst()) return null if (!cursor.moveToFirst()) return null
cursor.getBlobOrNull(cursor.getColumnIndex(VALUE)) cursor.getBlobOrNull(cursor.getColumnIndex(DATA))
} }
return bytes return bytes
} }

View File

@ -2,57 +2,54 @@ package org.thoughtcrime.securesms.util
import android.content.Context import android.content.Context
import nl.komponents.kovenant.Promise import nl.komponents.kovenant.Promise
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 { object ConfigurationMessageUtilities {
@JvmStatic @JvmStatic
fun syncConfigurationIfNeeded(context: Context) { fun syncConfigurationIfNeeded(context: Context) {
val userPublicKey = TextSecurePreferences.getLocalNumber(context) ?: return return
val lastSyncTime = TextSecurePreferences.getLastConfigurationSyncTime(context) // val userPublicKey = TextSecurePreferences.getLocalNumber(context) ?: return
val now = System.currentTimeMillis() // val lastSyncTime = TextSecurePreferences.getLastConfigurationSyncTime(context)
if (now - lastSyncTime < 7 * 24 * 60 * 60 * 1000) return // val now = System.currentTimeMillis()
val contacts = ContactUtilities.getAllContacts(context).filter { recipient -> // if (now - lastSyncTime < 7 * 24 * 60 * 60 * 1000) return
!recipient.name.isNullOrEmpty() && !recipient.isLocalNumber && recipient.address.serialize().isNotEmpty() // val contacts = ContactUtilities.getAllContacts(context).filter { recipient ->
}.map { recipient -> // !recipient.name.isNullOrEmpty() && !recipient.isLocalNumber && recipient.address.serialize().isNotEmpty()
ConfigurationMessage.Contact( // }.map { recipient ->
publicKey = recipient.address.serialize(), // ConfigurationMessage.Contact(
name = recipient.name!!, // publicKey = recipient.address.serialize(),
profilePicture = recipient.profileAvatar, // name = recipient.name!!,
profileKey = recipient.profileKey, // profilePicture = recipient.profileAvatar,
isApproved = recipient.isApproved, // profileKey = recipient.profileKey,
isBlocked = recipient.isBlocked, // isApproved = recipient.isApproved,
didApproveMe = recipient.hasApprovedMe() // isBlocked = recipient.isBlocked,
) // didApproveMe = recipient.hasApprovedMe()
} // )
val configurationMessage = ConfigurationMessage.getCurrent(contacts) ?: return // }
MessageSender.send(configurationMessage, Address.fromSerialized(userPublicKey)) // val configurationMessage = ConfigurationMessage.getCurrent(contacts) ?: return
TextSecurePreferences.setLastConfigurationSyncTime(context, now) // MessageSender.send(configurationMessage, Address.fromSerialized(userPublicKey))
// TextSecurePreferences.setLastConfigurationSyncTime(context, now)
} }
fun forceSyncConfigurationNowIfNeeded(context: Context): Promise<Unit, Exception> { fun forceSyncConfigurationNowIfNeeded(context: Context): Promise<Unit, Exception> {
val userPublicKey = TextSecurePreferences.getLocalNumber(context) ?: return Promise.ofSuccess(Unit) return Promise.ofSuccess(Unit)
val contacts = ContactUtilities.getAllContacts(context).filter { recipient -> // val userPublicKey = TextSecurePreferences.getLocalNumber(context) ?: return Promise.ofSuccess(Unit)
!recipient.isGroupRecipient && !recipient.name.isNullOrEmpty() && !recipient.isLocalNumber && recipient.address.serialize().isNotEmpty() // val contacts = ContactUtilities.getAllContacts(context).filter { recipient ->
}.map { recipient -> // !recipient.isGroupRecipient && !recipient.name.isNullOrEmpty() && !recipient.isLocalNumber && recipient.address.serialize().isNotEmpty()
ConfigurationMessage.Contact( // }.map { recipient ->
publicKey = recipient.address.serialize(), // ConfigurationMessage.Contact(
name = recipient.name!!, // publicKey = recipient.address.serialize(),
profilePicture = recipient.profileAvatar, // name = recipient.name!!,
profileKey = recipient.profileKey, // profilePicture = recipient.profileAvatar,
isApproved = recipient.isApproved, // profileKey = recipient.profileKey,
isBlocked = recipient.isBlocked, // isApproved = recipient.isApproved,
didApproveMe = recipient.hasApprovedMe() // 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))) // val configurationMessage = ConfigurationMessage.getCurrent(contacts) ?: return Promise.ofSuccess(Unit)
TextSecurePreferences.setLastConfigurationSyncTime(context, System.currentTimeMillis()) // val promise = MessageSender.send(configurationMessage, Destination.from(Address.fromSerialized(userPublicKey)))
return promise // TextSecurePreferences.setLastConfigurationSyncTime(context, System.currentTimeMillis())
// return promise
} }
} }

View File

@ -86,3 +86,9 @@ Java_network_loki_messenger_libsession_1util_ConfigBase_merge___3B(JNIEnv *env,
} }
#pragma clang diagnostic pop #pragma clang diagnostic pop
} }
extern "C"
JNIEXPORT jint JNICALL
Java_network_loki_messenger_libsession_1util_ConfigBase_configNamespace(JNIEnv *env, jobject thiz) {
auto conf = ptrToConfigBase(env, thiz);
return (std::int16_t) conf->storage_namespace();
}

View File

@ -21,6 +21,8 @@ sealed class ConfigBase(protected val /* yucky */ pointer: Long) {
external fun confirmPushed(seqNo: Long) external fun confirmPushed(seqNo: Long)
external fun merge(toMerge: Array<ByteArray>): Int external fun merge(toMerge: Array<ByteArray>): Int
external fun configNamespace(): Int
// Singular merge // Singular merge
external fun merge(toMerge: ByteArray): Int external fun merge(toMerge: ByteArray): Int

View File

@ -1,5 +1,9 @@
package org.session.libsession.messaging.sending_receiving.pollers package org.session.libsession.messaging.sending_receiving.pollers
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.runBlocking
import nl.komponents.kovenant.Deferred import nl.komponents.kovenant.Deferred
import nl.komponents.kovenant.Promise import nl.komponents.kovenant.Promise
import nl.komponents.kovenant.deferred import nl.komponents.kovenant.deferred
@ -100,21 +104,38 @@ class Poller(private val configFactory: ConfigFactoryProtocol) {
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()) }
return SnodeAPI.getRawMessages(snode, userPublicKey).bind { rawResponse -> return task {
isCaughtUp = true runBlocking(Dispatchers.IO) {
if (deferred.promise.isDone()) { // get user profile namespace
task { Unit } // The long polling connection has been canceled; don't recurse val deferredUserConfig = configFactory.userConfig?.let { currentUserConfig ->
} else { async {
val messages = SnodeAPI.parseRawMessagesResponse(rawResponse, snode, userPublicKey) try {
val parameters = messages.map { (envelope, serverHash) -> val currentNetworkConfig = SnodeAPI.getRawMessages(snode, userPublicKey, namespace = currentUserConfig.configNamespace()).get()
MessageReceiveParameters(envelope.toByteArray(), serverHash = serverHash)
} } catch (e: Exception) {
parameters.chunked(BatchMessageReceiveJob.BATCH_DEFAULT_NUMBER).forEach { chunk -> Log.e("Poller", "Error getting current user config from network", e)
val job = BatchMessageReceiveJob(chunk) }
JobQueue.shared.add(job) }
} ?: CompletableDeferred(null)
SnodeAPI.getRawMessages(snode, userPublicKey).bind { rawResponse ->
isCaughtUp = true
if (deferred.promise.isDone()) {
return@bind Promise.ofSuccess(Unit)
} else {
val messages = SnodeAPI.parseRawMessagesResponse(rawResponse, snode, userPublicKey)
val parameters = messages.map { (envelope, serverHash) ->
MessageReceiveParameters(envelope.toByteArray(), serverHash = serverHash)
}
parameters.chunked(BatchMessageReceiveJob.BATCH_DEFAULT_NUMBER).forEach { chunk ->
val job = BatchMessageReceiveJob(chunk)
JobQueue.shared.add(job)
}
poll(snode, deferred)
}
} }
poll(snode, deferred)
} }
} }
} }

View File

@ -310,14 +310,15 @@ object SnodeAPI {
fun getRawMessages(snode: Snode, publicKey: String, requiresAuth: Boolean = true, namespace: Int = 0): RawResponsePromise { fun getRawMessages(snode: Snode, publicKey: String, requiresAuth: Boolean = true, namespace: Int = 0): RawResponsePromise {
// Get last message hash // Get last message hash
val lastHashValue = database.getLastMessageHashValue(snode, publicKey, namespace) ?: "" val lastHashValue = database.getLastMessageHashValue(snode, publicKey, namespace) ?: ""
val parameters = mutableMapOf<String,Any>( val parameters = mutableMapOf<String, Any>(
"pubKey" to publicKey, "pubKey" to publicKey,
"last_hash" to lastHashValue, "last_hash" to lastHashValue,
) )
// Construct signature // Construct signature
if (requiresAuth) { if (requiresAuth) {
val userED25519KeyPair = try { val userED25519KeyPair = try {
MessagingModuleConfiguration.shared.getUserED25519KeyPair() ?: return Promise.ofFail(Error.NoKeyPair) MessagingModuleConfiguration.shared.getUserED25519KeyPair()
?: return Promise.ofFail(Error.NoKeyPair)
} catch (e: Exception) { } catch (e: Exception) {
Log.e("Loki", "Error getting KeyPair", e) Log.e("Loki", "Error getting KeyPair", e)
return Promise.ofFail(Error.NoKeyPair) return Promise.ofFail(Error.NoKeyPair)
@ -329,7 +330,12 @@ object SnodeAPI {
if (namespace != 0) "retrieve$namespace$timestamp".toByteArray() if (namespace != 0) "retrieve$namespace$timestamp".toByteArray()
else "retrieve$timestamp".toByteArray() else "retrieve$timestamp".toByteArray()
try { try {
sodium.cryptoSignDetached(signature, verificationData, verificationData.size.toLong(), userED25519KeyPair.secretKey.asBytes) sodium.cryptoSignDetached(
signature,
verificationData,
verificationData.size.toLong(),
userED25519KeyPair.secretKey.asBytes
)
} catch (exception: Exception) { } catch (exception: Exception) {
return Promise.ofFail(Error.SigningFailed) return Promise.ofFail(Error.SigningFailed)
} }

View File

@ -51,6 +51,7 @@ message Content {
optional DataExtractionNotification dataExtractionNotification = 8; optional DataExtractionNotification dataExtractionNotification = 8;
optional UnsendRequest unsendRequest = 9; optional UnsendRequest unsendRequest = 9;
optional MessageRequestResponse messageRequestResponse = 10; optional MessageRequestResponse messageRequestResponse = 10;
optional SharedConfigMessage sharedConfigMessage = 11;
} }
message KeyPair { message KeyPair {
@ -236,6 +237,25 @@ message MessageRequestResponse {
required bool isApproved = 1; // Whether the request was approved required bool isApproved = 1; // Whether the request was approved
} }
message SharedConfigMessage {
enum Kind {
USER_PROFILE = 1;
CONTACTS = 2;
CONVERSATION_INFO = 3;
LEGACY_CLOSED_GROUPS = 4;
CLOSED_GROUP_INFO = 5;
CLOSED_GROUP_MEMBERS = 6;
ENCRYPTION_KEYS = 7;
}
// @required
required Kind kind = 1;
// @required
required int64 seqno = 2;
// @required
required bytes data = 3;
}
message ReceiptMessage { message ReceiptMessage {
enum Type { enum Type {