diff --git a/libsession-util/src/androidTest/java/network/loki/messenger/libsession_util/InstrumentedTests.kt b/libsession-util/src/androidTest/java/network/loki/messenger/libsession_util/InstrumentedTests.kt index 0917a346f5..a76f4672ce 100644 --- a/libsession-util/src/androidTest/java/network/loki/messenger/libsession_util/InstrumentedTests.kt +++ b/libsession-util/src/androidTest/java/network/loki/messenger/libsession_util/InstrumentedTests.kt @@ -352,4 +352,199 @@ class InstrumentedTests { assertEquals(1, convos.sizeOneToOnes()) } + @Test + fun test_open_group_urls() { + val (base1, room1, pk1) = Conversation.OpenGroup.parseFullUrl( + "https://example.com/" + + "SomeRoom?public_key=0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + )!! + + val (base2, room2, pk2) = Conversation.OpenGroup.parseFullUrl( + "HTTPS://EXAMPLE.COM/" + + "sOMErOOM?public_key=0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF" + )!! + + val (base3, room3, pk3) = Conversation.OpenGroup.parseFullUrl( + "HTTPS://EXAMPLE.COM/r/" + + "someroom?public_key=0123456789aBcdEF0123456789abCDEF0123456789ABCdef0123456789ABCDEF" + )!! + + val (base4, room4, pk4) = Conversation.OpenGroup.parseFullUrl( + "http://example.com/r/" + + "someroom?public_key=0123456789aBcdEF0123456789abCDEF0123456789ABCdef0123456789ABCDEF" + )!! + + val (base5, room5, pk5) = Conversation.OpenGroup.parseFullUrl( + "HTTPS://EXAMPLE.com:443/r/" + + "someroom?public_key=0123456789aBcdEF0123456789abCDEF0123456789ABCdef0123456789ABCDEF" + )!! + + val (base6, room6, pk6) = Conversation.OpenGroup.parseFullUrl( + "HTTP://EXAMPLE.com:80/r/" + + "someroom?public_key=0123456789aBcdEF0123456789abCDEF0123456789ABCdef0123456789ABCDEF" + )!! + + val (base7, room7, pk7) = Conversation.OpenGroup.parseFullUrl( + "http://example.com:80/r/" + + "someroom?public_key=ASNFZ4mrze8BI0VniavN7wEjRWeJq83vASNFZ4mrze8" + )!! + val (base8, room8, pk8) = Conversation.OpenGroup.parseFullUrl( + "http://example.com:80/r/" + + "someroom?public_key=yrtwk3hjixg66yjdeiuauk6p7hy1gtm8tgih55abrpnsxnpm3zzo" + )!! + + assertEquals("https://example.com", base1) + assertEquals(base1, base2) + assertEquals(base1, base3) + assertNotEquals(base1, base4) + assertEquals(base4, "http://example.com") + assertEquals(base1, base5) + assertEquals(base4, base6) + assertEquals(base4, base7) + assertEquals(base4, base8) + assertEquals(room1, "someroom") + assertEquals(room2, "someroom") + assertEquals(room3, "someroom") + assertEquals(room4, "someroom") + assertEquals(room5, "someroom") + assertEquals(room6, "someroom") + assertEquals(room7, "someroom") + assertEquals(room8, "someroom") + assertEquals(Hex.toStringCondensed(pk1), "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef") + assertEquals(Hex.toStringCondensed(pk2), "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef") + assertEquals(Hex.toStringCondensed(pk3), "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef") + assertEquals(Hex.toStringCondensed(pk4), "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef") + assertEquals(Hex.toStringCondensed(pk5), "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef") + assertEquals(Hex.toStringCondensed(pk6), "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef") + assertEquals(Hex.toStringCondensed(pk7), "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef") + assertEquals(Hex.toStringCondensed(pk8), "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef") + + } + + @Test + fun test_conversations() { + val convos = ConversationVolatileConfig.newInstance(keyPair.secretKey) + val definitelyRealId = "055000000000000000000000000000000000000000000000000000000000000000" + assertNull(convos.getOneToOne(definitelyRealId)) + assertTrue(convos.empty()) + assertEquals(0, convos.size()) + + val c = convos.getOrConstructOneToOne(definitelyRealId) + + assertEquals(definitelyRealId, c.sessionId) + assertEquals(0, c.lastRead) + + assertFalse(convos.needsPush()) + assertFalse(convos.needsDump()) + assertEquals(0, convos.push().seqNo) + + val nowMs = System.currentTimeMillis() + + c.lastRead = nowMs + + convos.set(c) + + assertNull(convos.getLegacyClosedGroup(definitelyRealId)) + assertNotNull(convos.getOneToOne(definitelyRealId)) + assertEquals(nowMs, convos.getOneToOne(definitelyRealId)?.lastRead) + + assertTrue(convos.needsPush()) + assertTrue(convos.needsDump()) + + val openGroupPubKey = Hex.fromStringCondensed("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef") + + val og = convos.getOrConstructOpenGroup("http://Example.ORG:5678", "SudokuRoom", openGroupPubKey) + + assertEquals("http://example.org:5678", og.baseUrl) // Note: lower-case + assertEquals("sudokuroom", og.room) // Note: lower-case + assertEquals(32, og.pubKey.size); + assertEquals("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", og.pubKeyHex) + + og.unread = true + + convos.set(og) + + val (toPush, seqNo) = convos.push() + + assertEquals(1, seqNo) + + convos.confirmPushed(seqNo) + + assertTrue(convos.needsDump()) + assertFalse(convos.needsPush()) + + val convos2 = ConversationVolatileConfig.newInstance(keyPair.secretKey, toPush) + assertFalse(convos.needsPush()) + assertFalse(convos.needsDump()) + assertEquals(1, convos.push().seqNo) + assertFalse(convos.needsDump()) + + val x1 = convos2.getOneToOne(definitelyRealId)!! + assertEquals(nowMs, x1.lastRead) + assertEquals(definitelyRealId, x1.sessionId) + assertEquals(false, x1.unread) + + val x2 = convos2.getOpenGroup("http://EXAMPLE.org:5678", "sudokuRoom", openGroupPubKey)!! + assertEquals("http://example.org:5678", x2.baseUrl) + assertEquals("sudokuroom", x2.room) + assertEquals(x2.pubKeyHex, Hex.toStringCondensed(openGroupPubKey)) + assertTrue(x2.unread) + + val anotherId = "051111111111111111111111111111111111111111111111111111111111111111" + val c2 = convos.getOrConstructLegacyClosedGroup(anotherId) + c2.unread = true + convos2.set(c2) + + val c3 = convos.getOrConstructLegacyClosedGroup( + "05cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" + ) + c3.lastRead = nowMs - 50 + convos2.set(c3) + + assertTrue(convos2.needsPush()) + + val (toPush2, seqNo2) = convos2.push() + assertEquals(2, seqNo2) + + convos.merge(toPush2) + convos2.confirmPushed(seqNo2) + + assertFalse(convos.needsPush()) + assertEquals(seqNo2, convos.push().seqNo) + + val seen = mutableListOf() + for (conv in listOf(convos, convos2)) { + seen.clear() + assertEquals(4, conv.size()) + assertEquals(2, conv.sizeOneToOnes()) + assertEquals(1, conv.sizeOpenGroups()) + assertEquals(1, conv.sizeLegacyClosedGroups()) + assertFalse(conv.empty()) + for (convo in conv.all()) { + when (convo) { + is Conversation.OneToOne -> seen.add("1-to-1: ${convo.sessionId}") + is Conversation.OpenGroup -> seen.add("og: ${convo.baseUrl}/r/${convo.room}") + is Conversation.LegacyClosedGroup -> seen.add("cl: ${convo.groupId}") + } + } + assertEquals(listOf( + "1-to-1: " + + "051111111111111111111111111111111111111111111111111111111111111111", + "1-to-1: " + + "055000000000000000000000000000000000000000000000000000000000000000", + "og: http://example.org:5678/r/sudokuroom", + "cl: " + + "05ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" + ), seen) + } + + assertFalse(convos.needsPush()) + convos.eraseOneToOne("052000000000000000000000000000000000000000000000000000000000000000") + assertFalse(convos.needsPush()) + convos.eraseOneToOne("055000000000000000000000000000000000000000000000000000000000000000") + assertTrue(convos.needsPush()) + + + } + } \ No newline at end of file diff --git a/libsession-util/src/main/cpp/conversation.cpp b/libsession-util/src/main/cpp/conversation.cpp index 3ffd240c80..ac00ee9dd8 100644 --- a/libsession-util/src/main/cpp/conversation.cpp +++ b/libsession-util/src/main/cpp/conversation.cpp @@ -2,6 +2,26 @@ #include "conversation.h" #pragma clang diagnostic push + +extern "C" +JNIEXPORT jobject JNICALL +Java_network_loki_messenger_libsession_1util_util_Conversation_00024OpenGroup_00024Companion_parseFullUrl( + JNIEnv *env, jobject thiz, jstring full_url) { + auto bytes = env->GetStringUTFChars(full_url, nullptr); + auto [base, room, pk] = session::config::convo::open_group::parse_full_url(bytes); + env->ReleaseStringUTFChars(full_url, bytes); + + jclass clazz = env->FindClass("kotlin/Triple"); + jmethodID constructor = env->GetMethodID(clazz, "", "(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)V"); + + auto base_j = env->NewStringUTF(base.data()); + auto room_j = env->NewStringUTF(room.data()); + auto pk_jbytes = util::bytes_from_ustring(env, pk); + + jobject triple = env->NewObject(clazz, constructor, base_j, room_j, pk_jbytes); + return triple; +} + extern "C" #pragma ide diagnostic ignored "bugprone-reserved-identifier" JNIEXPORT jobject JNICALL @@ -32,15 +52,7 @@ Java_network_loki_messenger_libsession_1util_ConversationVolatileConfig_00024Com return newConfig; } -extern "C" -JNIEXPORT void JNICALL -Java_network_loki_messenger_libsession_1util_ConversationVolatileConfig_set(JNIEnv *env, - jobject thiz, - jobject to_store) { - auto conversations = ptrToConvoInfo(env, thiz); - auto one_to_one = deserialize_one_to_one(env, to_store); - conversations->set(*one_to_one); -} + extern "C" JNIEXPORT jint JNICALL @@ -78,4 +90,43 @@ Java_network_loki_messenger_libsession_1util_ConversationVolatileConfig_eraseAll } return removed; +} + +extern "C" +JNIEXPORT jint JNICALL +Java_network_loki_messenger_libsession_1util_ConversationVolatileConfig_size(JNIEnv *env, + jobject thiz) { + auto config = ptrToConvoInfo(env, thiz); + return (jint)config->size(); +} +extern "C" +JNIEXPORT jboolean JNICALL +Java_network_loki_messenger_libsession_1util_ConversationVolatileConfig_empty(JNIEnv *env, + jobject thiz) { + auto config = ptrToConvoInfo(env, thiz); + return config->empty(); +} +extern "C" +JNIEXPORT void JNICALL +Java_network_loki_messenger_libsession_1util_ConversationVolatileConfig_set(JNIEnv *env, + jobject thiz, + jobject to_store) { + + auto convos = ptrToConvoInfo(env, thiz); + + jclass one_to_one = env->FindClass("network/loki/messenger/libsession_util/util/Conversation$OneToOne"); + jclass open_group = env->FindClass("network/loki/messenger/libsession_util/util/Conversation$OpenGroup"); + jclass legacy_closed_group = env->FindClass("network/loki/messenger/libsession_util/util/Conversation$LegacyClosedGroup"); + + jclass to_store_class = env->GetObjectClass(to_store); + if (to_store_class == one_to_one) { + // store as 1to1 + convos->set(*deserialize_one_to_one(env, to_store)); + } else if (to_store_class == open_group) { + // store as open_group + convos->set(*deserialize_open_group(env, to_store)); + } else if (to_store_class == legacy_closed_group) { + // store as legacy_closed_group + convos->set(*deserialize_legacy_closed_group(env, to_store)); + } } \ No newline at end of file diff --git a/libsession-util/src/main/java/network/loki/messenger/libsession_util/Config.kt b/libsession-util/src/main/java/network/loki/messenger/libsession_util/Config.kt index 1f670a22fb..f8c3feee33 100644 --- a/libsession-util/src/main/java/network/loki/messenger/libsession_util/Config.kt +++ b/libsession-util/src/main/java/network/loki/messenger/libsession_util/Config.kt @@ -78,16 +78,18 @@ class ConversationVolatileConfig(pointer: Long): ConfigBase(pointer) { external fun getOneToOne(pubKeyHex: String): Conversation.OneToOne? external fun getOrConstructOneToOne(pubKeyHex: String): Conversation.OneToOne - external fun set(toStore: Conversation.OneToOne) external fun getOpenGroup(baseUrl: String, room: String, pubKeyHex: String): Conversation.OpenGroup? + external fun getOpenGroup(baseUrl: String, room: String, pubKey: ByteArray): Conversation.OpenGroup? external fun getOrConstructOpenGroup(baseUrl: String, room: String, pubKey: ByteArray): Conversation.OpenGroup + external fun getOrConstructOpenGroup(baseUrl: String, room: String, pubKeyHex: String): Conversation.OpenGroup external fun getLegacyClosedGroup(groupId: String): Conversation.LegacyClosedGroup? external fun getOrConstructLegacyClosedGroup(groupId: String): Conversation.LegacyClosedGroup - external fun erase(conversation: Conversation): Boolean + external fun set(toStore: Conversation) + /** * Erase all conversations that do not satisfy the `predicate`, similar to [MutableList.removeAll] */ @@ -96,6 +98,9 @@ class ConversationVolatileConfig(pointer: Long): ConfigBase(pointer) { external fun sizeOneToOnes(): Int external fun sizeOpenGroups(): Int external fun sizeLegacyClosedGroups(): Int + external fun size(): Int + + external fun empty(): Boolean external fun all(): List diff --git a/libsession-util/src/main/java/network/loki/messenger/libsession_util/util/Conversation.kt b/libsession-util/src/main/java/network/loki/messenger/libsession_util/util/Conversation.kt index 02cdd647b4..1ba4955af0 100644 --- a/libsession-util/src/main/java/network/loki/messenger/libsession_util/util/Conversation.kt +++ b/libsession-util/src/main/java/network/loki/messenger/libsession_util/util/Conversation.kt @@ -1,23 +1,35 @@ package network.loki.messenger.libsession_util.util +import org.session.libsignal.utilities.Hex + sealed class Conversation { - abstract val lastRead: Long - abstract val unread: Boolean + abstract var lastRead: Long + abstract var unread: Boolean data class OneToOne( val sessionId: String, - override val lastRead: Long, - override val unread: Boolean + override var lastRead: Long, + override var unread: Boolean ): Conversation() data class OpenGroup( val baseUrl: String, val room: String, // lowercase val pubKey: ByteArray, - override val lastRead: Long, - override val unread: Boolean + override var lastRead: Long, + override var unread: Boolean ) : Conversation() { + + val pubKeyHex: String + get() = Hex.toStringCondensed(pubKey) + companion object { + init { + System.loadLibrary("session_util") + } + external fun parseFullUrl(fullUrl: String): Triple? + } + override fun equals(other: Any?): Boolean { if (this === other) return true if (javaClass != other?.javaClass) return false @@ -43,7 +55,7 @@ sealed class Conversation { data class LegacyClosedGroup( val groupId: String, - override val lastRead: Long, - override val unread: Boolean + override var lastRead: Long, + override var unread: Boolean ): Conversation() } \ No newline at end of file