From 03aa19aae4df53e3dbf935fbc671310193ec3766 Mon Sep 17 00:00:00 2001 From: andrew Date: Mon, 29 May 2023 18:02:20 +0930 Subject: [PATCH 01/27] . --- .../securesms/SessionDialogBuilder.kt | 86 +++++++++++++++++++ .../conversation/v2/ConversationActivityV2.kt | 49 +++++------ app/src/main/res/values/styles.xml | 2 + 3 files changed, 113 insertions(+), 24 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/SessionDialogBuilder.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/SessionDialogBuilder.kt b/app/src/main/java/org/thoughtcrime/securesms/SessionDialogBuilder.kt new file mode 100644 index 000000000..71b48c6bb --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/SessionDialogBuilder.kt @@ -0,0 +1,86 @@ +package org.thoughtcrime.securesms + +import android.content.Context +import android.view.View +import android.view.ViewGroup.LayoutParams.MATCH_PARENT +import android.view.ViewGroup.LayoutParams.WRAP_CONTENT +import android.widget.Button +import android.widget.LinearLayout +import android.widget.LinearLayout.VERTICAL +import android.widget.TextView +import androidx.annotation.StringRes +import androidx.annotation.StyleRes +import androidx.appcompat.app.AlertDialog +import androidx.core.view.setMargins +import androidx.core.view.updateMargins +import network.loki.messenger.R +import org.thoughtcrime.securesms.util.toPx + +class SessionDialogBuilder(val context: Context) { + + private val dialog: AlertDialog = AlertDialog.Builder(context).create() + + private val root = LinearLayout(context).apply { orientation = VERTICAL } + .also(dialog::setView) + + fun title(@StringRes id: Int) { + TextView(context, null, 0, R.style.TextAppearance_AppCompat_Title) + .apply { textAlignment = View.TEXT_ALIGNMENT_CENTER } + .apply { setText(id) } + .apply { layoutParams = LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT) + .apply { setMargins(toPx(20, resources)) } + }.let(root::addView) + } + + fun text(@StringRes id: Int, style: Int = 0) { + TextView(context, null, 0, style) + .apply { textAlignment = View.TEXT_ALIGNMENT_CENTER } + .apply { setText(id) } + .apply { layoutParams = LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT) + .apply { toPx(40, resources).let { updateMargins(it, 0, it, 0) } } + }.let(root::addView) + } + + fun buttons(build: ButtonsBuilder.() -> Unit) { + ButtonsBuilder(context, dialog).build(build).let(root::addView) + } + + fun show(): AlertDialog = dialog.apply { show() } +} + +class ButtonsBuilder(val context: Context, val dialog: AlertDialog) { + val root = LinearLayout(context) + + fun destructiveButton(@StringRes text: Int, @StringRes contentDescription: Int, listener: () -> Unit = {}) { + button(text, contentDescription, R.style.Widget_Session_Button_Dialog_DestructiveText, listener) + } + + fun cancelButton() = button(android.R.string.cancel) + + fun button( + @StringRes text: Int, + @StringRes contentDescriptionRes: Int = 0, + @StyleRes style: Int = R.style.Widget_Session_Button_Dialog_UnimportantText, + listener: (() -> Unit) = {}) { + Button(context, null, 0, style) + .apply { setText(text) } + .apply { setOnClickListener { + listener.invoke() + dialog.dismiss() + contentDescription = resources.getString(contentDescriptionRes) + } } + .apply { + layoutParams = LinearLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT, 1f) + .apply { setMargins(toPx(20, resources)) } + } + .let(root::addView) + } + + internal fun build(build: ButtonsBuilder.() -> Unit): LinearLayout { + build() + return root + } +} + +fun Context.sessionDialog(build: SessionDialogBuilder.() -> Unit): AlertDialog = + SessionDialogBuilder(this).apply { build() }.show() diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt index f9385d14b..0ad0542ea 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt @@ -21,7 +21,6 @@ import android.widget.Toast import androidx.activity.viewModels import androidx.annotation.DimenRes import androidx.appcompat.app.AlertDialog -import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProvider @@ -113,6 +112,7 @@ import org.thoughtcrime.securesms.mms.* import org.thoughtcrime.securesms.permissions.Permissions import org.thoughtcrime.securesms.reactions.ReactionsDialogFragment import org.thoughtcrime.securesms.reactions.any.ReactWithAnyEmojiDialogFragment +import org.thoughtcrime.securesms.sessionDialog import org.thoughtcrime.securesms.util.* import java.lang.ref.WeakReference import java.util.* @@ -962,21 +962,20 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe } override fun block(deleteThread: Boolean) { - val title = R.string.RecipientPreferenceActivity_block_this_contact_question - val message = R.string.RecipientPreferenceActivity_you_will_no_longer_receive_messages_and_calls_from_this_contact - val dialog = AlertDialog.Builder(this) - .setTitle(title) - .setMessage(message) - .setNegativeButton(android.R.string.cancel, null) - .setPositiveButton(R.string.RecipientPreferenceActivity_block) { _, _ -> - viewModel.block() - if (deleteThread) { - viewModel.deleteThread() - finish() + sessionDialog { + title(R.string.RecipientPreferenceActivity_block_this_contact_question) + text(R.string.RecipientPreferenceActivity_you_will_no_longer_receive_messages_and_calls_from_this_contact) + buttons { + destructiveButton(R.string.RecipientPreferenceActivity_block, R.string.AccessibilityId_block_confirm) { + viewModel.block() + if (deleteThread) { + viewModel.deleteThread() + finish() + } } - }.show() - val button = dialog.getButton(DialogInterface.BUTTON_POSITIVE) - button.setContentDescription("Confirm block") + cancelButton() + } + } } override fun copySessionID(sessionId: String) { @@ -1016,15 +1015,17 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe } override fun unblock() { - val title = R.string.ConversationActivity_unblock_this_contact_question - val message = R.string.ConversationActivity_you_will_once_again_be_able_to_receive_messages_and_calls_from_this_contact - AlertDialog.Builder(this) - .setTitle(title) - .setMessage(message) - .setNegativeButton(android.R.string.cancel, null) - .setPositiveButton(R.string.ConversationActivity_unblock) { _, _ -> - viewModel.unblock() - }.show() + sessionDialog { + title(R.string.ConversationActivity_unblock_this_contact_question) + text(R.string.ConversationActivity_you_will_once_again_be_able_to_receive_messages_and_calls_from_this_contact) + buttons { + destructiveButton( + R.string.ConversationActivity_unblock, + R.string.AccessibilityId_block_confirm + ) { viewModel.unblock() } + cancelButton() + } + } } // `position` is the adapter position; not the visual position diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 8c79423d5..5da603275 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -71,6 +71,7 @@ + + + + diff --git a/app/src/main/res/xml/preferences_app_protection.xml b/app/src/main/res/xml/preferences_app_protection.xml index 611971993..12607ee76 100644 --- a/app/src/main/res/xml/preferences_app_protection.xml +++ b/app/src/main/res/xml/preferences_app_protection.xml @@ -12,7 +12,6 @@ android:title="@string/preferences_app_protection__screen_lock" android:summary="@string/preferences_app_protection__lock_signal_access_with_android_screen_lock_or_fingerprint" /> - + convo is Conversation.OneToOne && convo.sessionId == definitelyRealId + } + assertEquals(1, numErased) + assertEquals(1, convos.sizeOneToOnes()) + } + + @Test + fun test_open_group_urls() { + val (base1, room1, pk1) = BaseCommunityInfo.parseFullUrl( + "https://example.com/" + + "someroom?public_key=0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + )!! + + val (base2, room2, pk2) = BaseCommunityInfo.parseFullUrl( + "HTTPS://EXAMPLE.COM/" + + "someroom?public_key=0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF" + )!! + + val (base3, room3, pk3) = BaseCommunityInfo.parseFullUrl( + "HTTPS://EXAMPLE.COM/r/" + + "someroom?public_key=0123456789aBcdEF0123456789abCDEF0123456789ABCdef0123456789ABCDEF" + )!! + + val (base4, room4, pk4) = BaseCommunityInfo.parseFullUrl( + "http://example.com/r/" + + "someroom?public_key=0123456789aBcdEF0123456789abCDEF0123456789ABCdef0123456789ABCDEF" + )!! + + val (base5, room5, pk5) = BaseCommunityInfo.parseFullUrl( + "HTTPS://EXAMPLE.com:443/r/" + + "someroom?public_key=0123456789aBcdEF0123456789abCDEF0123456789ABCdef0123456789ABCDEF" + )!! + + val (base6, room6, pk6) = BaseCommunityInfo.parseFullUrl( + "HTTP://EXAMPLE.com:80/r/" + + "someroom?public_key=0123456789aBcdEF0123456789abCDEF0123456789ABCdef0123456789ABCDEF" + )!! + + val (base7, room7, pk7) = BaseCommunityInfo.parseFullUrl( + "http://example.com:80/r/" + + "someroom?public_key=ASNFZ4mrze8BI0VniavN7wEjRWeJq83vASNFZ4mrze8" + )!! + val (base8, room8, pk8) = BaseCommunityInfo.parseFullUrl( + "http://example.com:80/r/" + + "someroom?public_key=yrtwk3hjixg66yjdeiuauk6p7hy1gtm8tgih55abrpnsxnpm3zzo" + )!! + + assertEquals("https://example.com", base1) + assertEquals("http://example.com", base4) + assertEquals(base1, base2) + assertEquals(base1, base3) + assertNotEquals(base1, base4) + assertEquals(base1, base5) + assertEquals(base4, base6) + assertEquals(base4, base7) + assertEquals(base4, base8) + assertEquals("someroom", room1) + assertEquals("someroom", room2) + assertEquals("someroom", room3) + assertEquals("someroom", room4) + assertEquals("someroom", room5) + assertEquals("someroom", room6) + assertEquals("someroom", room7) + assertEquals("someroom", room8) + 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.getOrConstructCommunity("http://Example.ORG:5678", "SudokuRoom", openGroupPubKey) + val ogCommunity = og.baseCommunityInfo + + assertEquals("http://example.org:5678", ogCommunity.baseUrl) // Note: lower-case + assertEquals("sudokuroom", ogCommunity.room) // Note: lower-case + assertEquals(64, ogCommunity.pubKeyHex.length) + assertEquals("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", ogCommunity.pubKeyHex) + + og.unread = true + + convos.set(og) + + val (_, seqNo) = convos.push() + + assertEquals(1, seqNo) + + convos.confirmPushed(seqNo, "fakehash1") + + assertTrue(convos.needsDump()) + assertFalse(convos.needsPush()) + + val convos2 = ConversationVolatileConfig.newInstance(keyPair.secretKey, convos.dump()) + 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.getCommunity("http://EXAMPLE.org:5678", "sudokuRoom")!! + val x2Info = x2.baseCommunityInfo + assertEquals("http://example.org:5678", x2Info.baseUrl) + assertEquals("sudokuroom", x2Info.room) + assertEquals(x2Info.pubKeyHex, Hex.toStringCondensed(openGroupPubKey)) + assertTrue(x2.unread) + + val anotherId = "051111111111111111111111111111111111111111111111111111111111111111" + val c2 = convos.getOrConstructOneToOne(anotherId) + c2.unread = true + convos2.set(c2) + + val c3 = convos.getOrConstructLegacyGroup( + "05cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" + ) + c3.lastRead = nowMs - 50 + convos2.set(c3) + + assertTrue(convos2.needsPush()) + + val (toPush2, seqNo2) = convos2.push() + assertEquals(2, seqNo2) + + convos2.confirmPushed(seqNo2, "fakehash2") + convos.merge("fakehash2" to toPush2) + + assertFalse(convos.needsPush()) + assertEquals(seqNo2, convos.push().seqNo) + + val seen = mutableListOf() + for ((ind, conv) in listOf(convos, convos2).withIndex()) { + Log.e("Test","Testing seen from convo #$ind") + seen.clear() + assertEquals(4, conv.size()) + assertEquals(2, conv.sizeOneToOnes()) + assertEquals(1, conv.sizeCommunities()) + assertEquals(1, conv.sizeLegacyClosedGroups()) + assertFalse(conv.empty()) + val allConvos = conv.all() + for (convo in allConvos) { + when (convo) { + is Conversation.OneToOne -> seen.add("1-to-1: ${convo.sessionId}") + is Conversation.Community -> seen.add("og: ${convo.baseCommunityInfo.baseUrl}/r/${convo.baseCommunityInfo.room}") + is Conversation.LegacyGroup -> seen.add("cl: ${convo.groupId}") + } + } + + assertTrue(seen.contains("1-to-1: 051111111111111111111111111111111111111111111111111111111111111111")) + assertTrue(seen.contains("1-to-1: 055000000000000000000000000000000000000000000000000000000000000000")) + assertTrue(seen.contains("og: http://example.org:5678/r/sudokuroom")) + assertTrue(seen.contains("cl: 05cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc")) + assertTrue(seen.size == 4) // for some reason iterative checks aren't working in test cases + } + + assertFalse(convos.needsPush()) + convos.eraseOneToOne("052000000000000000000000000000000000000000000000000000000000000000") + assertFalse(convos.needsPush()) + convos.eraseOneToOne("055000000000000000000000000000000000000000000000000000000000000000") + assertTrue(convos.needsPush()) + + assertEquals(1, convos.allOneToOnes().size) + assertEquals("051111111111111111111111111111111111111111111111111111111111111111", + convos.allOneToOnes().map(Conversation.OneToOne::sessionId).first() + ) + assertEquals(1, convos.allCommunities().size) + assertEquals("http://example.org:5678", + convos.allCommunities().map { it.baseCommunityInfo.baseUrl }.first() + ) + assertEquals(1, convos.allLegacyClosedGroups().size) + assertEquals("05cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + convos.allLegacyClosedGroups().map(Conversation.LegacyGroup::groupId).first() + ) + } + +} \ No newline at end of file diff --git a/libsession-util/src/main/AndroidManifest.xml b/libsession-util/src/main/AndroidManifest.xml new file mode 100644 index 000000000..65483324a --- /dev/null +++ b/libsession-util/src/main/AndroidManifest.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/libsession-util/src/main/cpp/CMakeLists.txt b/libsession-util/src/main/cpp/CMakeLists.txt new file mode 100644 index 000000000..d01523a24 --- /dev/null +++ b/libsession-util/src/main/cpp/CMakeLists.txt @@ -0,0 +1,66 @@ +# For more information about using CMake with Android Studio, read the +# documentation: https://d.android.com/studio/projects/add-native-code.html + +# Sets the minimum version of CMake required to build the native library. + +cmake_minimum_required(VERSION 3.18.1) + +# Declares and names the project. + +project("session_util") + +# Compiles in C++17 mode +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + +set(CMAKE_BUILD_TYPE Release) + +# Creates and names a library, sets it as either STATIC +# or SHARED, and provides the relative paths to its source code. +# You can define multiple libraries, and CMake builds them for you. +# Gradle automatically packages shared libraries with your APK. + +set(STATIC_BUNDLE ON) +add_subdirectory(../../../libsession-util libsession) + +set(SOURCES + user_profile.cpp + user_groups.cpp + config_base.cpp + contacts.cpp + conversation.cpp + util.cpp) + +add_library( # Sets the name of the library. + session_util + # Sets the library as a shared library. + SHARED + # Provides a relative path to your source file(s). + ${SOURCES}) + +# Searches for a specified prebuilt library and stores the path as a +# variable. Because CMake includes system libraries in the search path by +# default, you only need to specify the name of the public NDK library +# you want to add. CMake verifies that the library exists before +# completing its build. + +find_library( # Sets the name of the path variable. + log-lib + + # Specifies the name of the NDK library that + # you want CMake to locate. + log) + +# Specifies libraries CMake should link to your target library. You +# can link multiple libraries, such as libraries you define in this +# build script, prebuilt third-party libraries, or system libraries. + +target_link_libraries( # Specifies the target library. + session_util + PUBLIC + libsession::config + libsession::crypto + # Links the target library to the log library + # included in the NDK. + ${log-lib}) diff --git a/libsession-util/src/main/cpp/config_base.cpp b/libsession-util/src/main/cpp/config_base.cpp new file mode 100644 index 000000000..eed3ec56a --- /dev/null +++ b/libsession-util/src/main/cpp/config_base.cpp @@ -0,0 +1,154 @@ +#include "config_base.h" +#include "util.h" + +extern "C" { +JNIEXPORT jboolean JNICALL +Java_network_loki_messenger_libsession_1util_ConfigBase_dirty(JNIEnv *env, jobject thiz) { + std::lock_guard lock{util::util_mutex_}; + auto* configBase = ptrToConfigBase(env, thiz); + return configBase->is_dirty(); +} + +JNIEXPORT jboolean JNICALL +Java_network_loki_messenger_libsession_1util_ConfigBase_needsPush(JNIEnv *env, jobject thiz) { + std::lock_guard lock{util::util_mutex_}; + auto config = ptrToConfigBase(env, thiz); + return config->needs_push(); +} + +JNIEXPORT jboolean JNICALL +Java_network_loki_messenger_libsession_1util_ConfigBase_needsDump(JNIEnv *env, jobject thiz) { + std::lock_guard lock{util::util_mutex_}; + auto config = ptrToConfigBase(env, thiz); + return config->needs_dump(); +} + +JNIEXPORT jobject JNICALL +Java_network_loki_messenger_libsession_1util_ConfigBase_push(JNIEnv *env, jobject thiz) { + std::lock_guard lock{util::util_mutex_}; + auto config = ptrToConfigBase(env, thiz); + auto push_tuple = config->push(); + auto to_push_str = std::get<1>(push_tuple); + auto to_delete = std::get<2>(push_tuple); + + jbyteArray returnByteArray = util::bytes_from_ustring(env, to_push_str); + jlong seqNo = std::get<0>(push_tuple); + jclass returnObjectClass = env->FindClass("network/loki/messenger/libsession_util/util/ConfigPush"); + jclass stackClass = env->FindClass("java/util/Stack"); + jmethodID methodId = env->GetMethodID(returnObjectClass, "", "([BJLjava/util/List;)V"); + jmethodID stack_init = env->GetMethodID(stackClass, "", "()V"); + jobject our_stack = env->NewObject(stackClass, stack_init); + jmethodID push_stack = env->GetMethodID(stackClass, "push", "(Ljava/lang/Object;)Ljava/lang/Object;"); + for (auto entry : to_delete) { + auto entry_jstring = env->NewStringUTF(entry.data()); + env->CallObjectMethod(our_stack, push_stack, entry_jstring); + } + jobject returnObject = env->NewObject(returnObjectClass, methodId, returnByteArray, seqNo, our_stack); + return returnObject; +} + +JNIEXPORT void JNICALL +Java_network_loki_messenger_libsession_1util_ConfigBase_free(JNIEnv *env, jobject thiz) { + auto config = ptrToConfigBase(env, thiz); + delete config; +} + +JNIEXPORT jbyteArray JNICALL +Java_network_loki_messenger_libsession_1util_ConfigBase_dump(JNIEnv *env, jobject thiz) { + std::lock_guard lock{util::util_mutex_}; + auto config = ptrToConfigBase(env, thiz); + auto dumped = config->dump(); + jbyteArray bytes = util::bytes_from_ustring(env, dumped); + return bytes; +} + +JNIEXPORT jstring JNICALL +Java_network_loki_messenger_libsession_1util_ConfigBase_encryptionDomain(JNIEnv *env, + jobject thiz) { + auto conf = ptrToConfigBase(env, thiz); + return env->NewStringUTF(conf->encryption_domain()); +} + +JNIEXPORT void JNICALL +Java_network_loki_messenger_libsession_1util_ConfigBase_confirmPushed(JNIEnv *env, jobject thiz, + jlong seq_no, + jstring new_hash_jstring) { + std::lock_guard lock{util::util_mutex_}; + auto conf = ptrToConfigBase(env, thiz); + auto new_hash = env->GetStringUTFChars(new_hash_jstring, nullptr); + conf->confirm_pushed(seq_no, new_hash); + env->ReleaseStringUTFChars(new_hash_jstring, new_hash); +} + +#pragma clang diagnostic push +#pragma ide diagnostic ignored "bugprone-reserved-identifier" +JNIEXPORT jint JNICALL +Java_network_loki_messenger_libsession_1util_ConfigBase_merge___3Lkotlin_Pair_2(JNIEnv *env, jobject thiz, + jobjectArray to_merge) { + std::lock_guard lock{util::util_mutex_}; + auto conf = ptrToConfigBase(env, thiz); + size_t number = env->GetArrayLength(to_merge); + std::vector> configs = {}; + for (int i = 0; i < number; i++) { + auto jElement = (jobject) env->GetObjectArrayElement(to_merge, i); + auto pair = extractHashAndData(env, jElement); + configs.push_back(pair); + } + return conf->merge(configs); +} + +JNIEXPORT jint JNICALL +Java_network_loki_messenger_libsession_1util_ConfigBase_merge__Lkotlin_Pair_2(JNIEnv *env, jobject thiz, + jobject to_merge) { + std::lock_guard lock{util::util_mutex_}; + auto conf = ptrToConfigBase(env, thiz); + std::vector> configs = {extractHashAndData(env, to_merge)}; + return conf->merge(configs); +} + +#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(); +} +extern "C" +JNIEXPORT jclass JNICALL +Java_network_loki_messenger_libsession_1util_ConfigBase_00024Companion_kindFor(JNIEnv *env, + jobject thiz, + jint config_namespace) { + auto user_class = env->FindClass("network/loki/messenger/libsession_util/UserProfile"); + auto contact_class = env->FindClass("network/loki/messenger/libsession_util/Contacts"); + auto convo_volatile_class = env->FindClass("network/loki/messenger/libsession_util/ConversationVolatileConfig"); + auto group_list_class = env->FindClass("network/loki/messenger/libsession_util/UserGroupsConfig"); + switch (config_namespace) { + case (int)session::config::Namespace::UserProfile: + return user_class; + case (int)session::config::Namespace::Contacts: + return contact_class; + case (int)session::config::Namespace::ConvoInfoVolatile: + return convo_volatile_class; + case (int)session::config::Namespace::UserGroups: + return group_list_class; + default: + return nullptr; + } +} + +extern "C" +JNIEXPORT jobject JNICALL +Java_network_loki_messenger_libsession_1util_ConfigBase_currentHashes(JNIEnv *env, jobject thiz) { + std::lock_guard lock{util::util_mutex_}; + auto conf = ptrToConfigBase(env, thiz); + jclass stack = env->FindClass("java/util/Stack"); + jmethodID init = env->GetMethodID(stack, "", "()V"); + jobject our_stack = env->NewObject(stack, init); + jmethodID push = env->GetMethodID(stack, "push", "(Ljava/lang/Object;)Ljava/lang/Object;"); + auto vec = conf->current_hashes(); + for (std::string element: vec) { + env->CallObjectMethod(our_stack, push, env->NewStringUTF(element.data())); + } + return our_stack; +} \ No newline at end of file diff --git a/libsession-util/src/main/cpp/config_base.h b/libsession-util/src/main/cpp/config_base.h new file mode 100644 index 000000000..836fb04ef --- /dev/null +++ b/libsession-util/src/main/cpp/config_base.h @@ -0,0 +1,28 @@ +#ifndef SESSION_ANDROID_CONFIG_BASE_H +#define SESSION_ANDROID_CONFIG_BASE_H + +#include "session/config/base.hpp" +#include "util.h" +#include +#include + +inline session::config::ConfigBase* ptrToConfigBase(JNIEnv *env, jobject obj) { + jclass baseClass = env->FindClass("network/loki/messenger/libsession_util/ConfigBase"); + jfieldID pointerField = env->GetFieldID(baseClass, "pointer", "J"); + return (session::config::ConfigBase*) env->GetLongField(obj, pointerField); +} + +inline std::pair extractHashAndData(JNIEnv *env, jobject kotlin_pair) { + jclass pair = env->FindClass("kotlin/Pair"); + jfieldID first = env->GetFieldID(pair, "first", "Ljava/lang/Object;"); + jfieldID second = env->GetFieldID(pair, "second", "Ljava/lang/Object;"); + jstring hash_as_jstring = static_cast(env->GetObjectField(kotlin_pair, first)); + jbyteArray data_as_jbytes = static_cast(env->GetObjectField(kotlin_pair, second)); + auto hash_as_string = env->GetStringUTFChars(hash_as_jstring, nullptr); + auto data_as_ustring = util::ustring_from_bytes(env, data_as_jbytes); + auto ret_pair = std::pair{hash_as_string, data_as_ustring}; + env->ReleaseStringUTFChars(hash_as_jstring, hash_as_string); + return ret_pair; +} + +#endif \ No newline at end of file diff --git a/libsession-util/src/main/cpp/contacts.cpp b/libsession-util/src/main/cpp/contacts.cpp new file mode 100644 index 000000000..7d0490480 --- /dev/null +++ b/libsession-util/src/main/cpp/contacts.cpp @@ -0,0 +1,100 @@ +#include "contacts.h" +#include "util.h" + +extern "C" +JNIEXPORT jobject JNICALL +Java_network_loki_messenger_libsession_1util_Contacts_get(JNIEnv *env, jobject thiz, + jstring session_id) { + std::lock_guard lock{util::util_mutex_}; + auto contacts = ptrToContacts(env, thiz); + auto session_id_chars = env->GetStringUTFChars(session_id, nullptr); + auto contact = contacts->get(session_id_chars); + env->ReleaseStringUTFChars(session_id, session_id_chars); + if (!contact) return nullptr; + jobject j_contact = serialize_contact(env, contact.value()); + return j_contact; +} + +extern "C" +JNIEXPORT jobject JNICALL +Java_network_loki_messenger_libsession_1util_Contacts_getOrConstruct(JNIEnv *env, jobject thiz, + jstring session_id) { + std::lock_guard lock{util::util_mutex_}; + auto contacts = ptrToContacts(env, thiz); + auto session_id_chars = env->GetStringUTFChars(session_id, nullptr); + auto contact = contacts->get_or_construct(session_id_chars); + env->ReleaseStringUTFChars(session_id, session_id_chars); + return serialize_contact(env, contact); +} + +extern "C" +JNIEXPORT void JNICALL +Java_network_loki_messenger_libsession_1util_Contacts_set(JNIEnv *env, jobject thiz, + jobject contact) { + std::lock_guard lock{util::util_mutex_}; + auto contacts = ptrToContacts(env, thiz); + auto contact_info = deserialize_contact(env, contact, contacts); + contacts->set(contact_info); +} + +extern "C" +JNIEXPORT jboolean JNICALL +Java_network_loki_messenger_libsession_1util_Contacts_erase(JNIEnv *env, jobject thiz, + jstring session_id) { + std::lock_guard lock{util::util_mutex_}; + auto contacts = ptrToContacts(env, thiz); + auto session_id_chars = env->GetStringUTFChars(session_id, nullptr); + + bool result = contacts->erase(session_id_chars); + env->ReleaseStringUTFChars(session_id, session_id_chars); + return result; +} +extern "C" +#pragma clang diagnostic push +#pragma ide diagnostic ignored "bugprone-reserved-identifier" +JNIEXPORT jobject JNICALL +Java_network_loki_messenger_libsession_1util_Contacts_00024Companion_newInstance___3B(JNIEnv *env, + jobject thiz, + jbyteArray ed25519_secret_key) { + std::lock_guard lock{util::util_mutex_}; + auto secret_key = util::ustring_from_bytes(env, ed25519_secret_key); + auto* contacts = new session::config::Contacts(secret_key, std::nullopt); + + jclass contactsClass = env->FindClass("network/loki/messenger/libsession_util/Contacts"); + jmethodID constructor = env->GetMethodID(contactsClass, "", "(J)V"); + jobject newConfig = env->NewObject(contactsClass, constructor, reinterpret_cast(contacts)); + + return newConfig; +} +extern "C" +JNIEXPORT jobject JNICALL +Java_network_loki_messenger_libsession_1util_Contacts_00024Companion_newInstance___3B_3B( + JNIEnv *env, jobject thiz, jbyteArray ed25519_secret_key, jbyteArray initial_dump) { + std::lock_guard lock{util::util_mutex_}; + auto secret_key = util::ustring_from_bytes(env, ed25519_secret_key); + auto initial = util::ustring_from_bytes(env, initial_dump); + + auto* contacts = new session::config::Contacts(secret_key, initial); + + jclass contactsClass = env->FindClass("network/loki/messenger/libsession_util/Contacts"); + jmethodID constructor = env->GetMethodID(contactsClass, "", "(J)V"); + jobject newConfig = env->NewObject(contactsClass, constructor, reinterpret_cast(contacts)); + + return newConfig; +} +#pragma clang diagnostic pop +extern "C" +JNIEXPORT jobject JNICALL +Java_network_loki_messenger_libsession_1util_Contacts_all(JNIEnv *env, jobject thiz) { + std::lock_guard lock{util::util_mutex_}; + auto contacts = ptrToContacts(env, thiz); + jclass stack = env->FindClass("java/util/Stack"); + jmethodID init = env->GetMethodID(stack, "", "()V"); + jobject our_stack = env->NewObject(stack, init); + jmethodID push = env->GetMethodID(stack, "push", "(Ljava/lang/Object;)Ljava/lang/Object;"); + for (const auto& contact : *contacts) { + auto contact_obj = serialize_contact(env, contact); + env->CallObjectMethod(our_stack, push, contact_obj); + } + return our_stack; +} \ No newline at end of file diff --git a/libsession-util/src/main/cpp/contacts.h b/libsession-util/src/main/cpp/contacts.h new file mode 100644 index 000000000..c5496a68c --- /dev/null +++ b/libsession-util/src/main/cpp/contacts.h @@ -0,0 +1,109 @@ +#ifndef SESSION_ANDROID_CONTACTS_H +#define SESSION_ANDROID_CONTACTS_H + +#include +#include "session/config/contacts.hpp" +#include "util.h" + +inline session::config::Contacts *ptrToContacts(JNIEnv *env, jobject obj) { + jclass contactsClass = env->FindClass("network/loki/messenger/libsession_util/Contacts"); + jfieldID pointerField = env->GetFieldID(contactsClass, "pointer", "J"); + return (session::config::Contacts *) env->GetLongField(obj, pointerField); +} + +inline jobject serialize_contact(JNIEnv *env, session::config::contact_info info) { + jclass contactClass = env->FindClass("network/loki/messenger/libsession_util/util/Contact"); + jmethodID constructor = env->GetMethodID(contactClass, "", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ZZZLnetwork/loki/messenger/libsession_util/util/UserPic;ILnetwork/loki/messenger/libsession_util/util/ExpiryMode;)V"); + jstring id = env->NewStringUTF(info.session_id.data()); + jstring name = env->NewStringUTF(info.name.data()); + jstring nickname = env->NewStringUTF(info.nickname.data()); + jboolean approved, approvedMe, blocked; + approved = info.approved; + approvedMe = info.approved_me; + blocked = info.blocked; + auto created = info.created; + jobject profilePic = util::serialize_user_pic(env, info.profile_picture); + jobject returnObj = env->NewObject(contactClass, constructor, id, name, nickname, approved, + approvedMe, blocked, profilePic, info.priority, + util::serialize_expiry(env, info.exp_mode, info.exp_timer)); + return returnObj; +} + +inline session::config::contact_info +deserialize_contact(JNIEnv *env, jobject info, session::config::Contacts *conf) { + jclass contactClass = env->FindClass("network/loki/messenger/libsession_util/util/Contact"); + + jfieldID getId, getName, getNick, getApproved, getApprovedMe, getBlocked, getUserPic, getPriority, getExpiry, getHidden; + getId = env->GetFieldID(contactClass, "id", "Ljava/lang/String;"); + getName = env->GetFieldID(contactClass, "name", "Ljava/lang/String;"); + getNick = env->GetFieldID(contactClass, "nickname", "Ljava/lang/String;"); + getApproved = env->GetFieldID(contactClass, "approved", "Z"); + getApprovedMe = env->GetFieldID(contactClass, "approvedMe", "Z"); + getBlocked = env->GetFieldID(contactClass, "blocked", "Z"); + getUserPic = env->GetFieldID(contactClass, "profilePicture", + "Lnetwork/loki/messenger/libsession_util/util/UserPic;"); + getPriority = env->GetFieldID(contactClass, "priority", "I"); + getExpiry = env->GetFieldID(contactClass, "expiryMode", "Lnetwork/loki/messenger/libsession_util/util/ExpiryMode;"); + jstring name, nickname, session_id; + session_id = static_cast(env->GetObjectField(info, getId)); + name = static_cast(env->GetObjectField(info, getName)); + nickname = static_cast(env->GetObjectField(info, getNick)); + bool approved, approvedMe, blocked, hidden; + int priority = env->GetIntField(info, getPriority); + approved = env->GetBooleanField(info, getApproved); + approvedMe = env->GetBooleanField(info, getApprovedMe); + blocked = env->GetBooleanField(info, getBlocked); + jobject user_pic = env->GetObjectField(info, getUserPic); + jobject expiry_mode = env->GetObjectField(info, getExpiry); + + auto expiry_pair = util::deserialize_expiry(env, expiry_mode); + + std::string url; + session::ustring key; + + if (user_pic != nullptr) { + auto deserialized_pic = util::deserialize_user_pic(env, user_pic); + auto url_jstring = deserialized_pic.first; + auto url_bytes = env->GetStringUTFChars(url_jstring, nullptr); + url = std::string(url_bytes); + env->ReleaseStringUTFChars(url_jstring, url_bytes); + key = util::ustring_from_bytes(env, deserialized_pic.second); + } + + auto session_id_bytes = env->GetStringUTFChars(session_id, nullptr); + auto name_bytes = name ? env->GetStringUTFChars(name, nullptr) : nullptr; + auto nickname_bytes = nickname ? env->GetStringUTFChars(nickname, nullptr) : nullptr; + + auto contact_info = conf->get_or_construct(session_id_bytes); + if (name_bytes) { + contact_info.name = name_bytes; + } + if (nickname_bytes) { + contact_info.nickname = nickname_bytes; + } + contact_info.approved = approved; + contact_info.approved_me = approvedMe; + contact_info.blocked = blocked; + if (!url.empty() && !key.empty()) { + contact_info.profile_picture = session::config::profile_pic(url, key); + } else { + contact_info.profile_picture = session::config::profile_pic(); + } + + env->ReleaseStringUTFChars(session_id, session_id_bytes); + if (name_bytes) { + env->ReleaseStringUTFChars(name, name_bytes); + } + if (nickname_bytes) { + env->ReleaseStringUTFChars(nickname, nickname_bytes); + } + + contact_info.priority = priority; + contact_info.exp_mode = expiry_pair.first; + contact_info.exp_timer = std::chrono::seconds(expiry_pair.second); + + return contact_info; +} + + +#endif //SESSION_ANDROID_CONTACTS_H diff --git a/libsession-util/src/main/cpp/conversation.cpp b/libsession-util/src/main/cpp/conversation.cpp new file mode 100644 index 000000000..4f0f531de --- /dev/null +++ b/libsession-util/src/main/cpp/conversation.cpp @@ -0,0 +1,352 @@ +#include +#include "conversation.h" + +#pragma clang diagnostic push + +extern "C" +#pragma ide diagnostic ignored "bugprone-reserved-identifier" +JNIEXPORT jobject JNICALL +Java_network_loki_messenger_libsession_1util_ConversationVolatileConfig_00024Companion_newInstance___3B( + JNIEnv *env, jobject thiz, jbyteArray ed25519_secret_key) { + std::lock_guard lock{util::util_mutex_}; + auto secret_key = util::ustring_from_bytes(env, ed25519_secret_key); + auto* convo_info_volatile = new session::config::ConvoInfoVolatile(secret_key, std::nullopt); + + jclass convoClass = env->FindClass("network/loki/messenger/libsession_util/ConversationVolatileConfig"); + jmethodID constructor = env->GetMethodID(convoClass, "", "(J)V"); + jobject newConfig = env->NewObject(convoClass, constructor, reinterpret_cast(convo_info_volatile)); + + return newConfig; +} +extern "C" +#pragma ide diagnostic ignored "bugprone-reserved-identifier" +JNIEXPORT jobject JNICALL +Java_network_loki_messenger_libsession_1util_ConversationVolatileConfig_00024Companion_newInstance___3B_3B( + JNIEnv *env, jobject thiz, jbyteArray ed25519_secret_key, jbyteArray initial_dump) { + std::lock_guard lock{util::util_mutex_}; + auto secret_key = util::ustring_from_bytes(env, ed25519_secret_key); + auto initial = util::ustring_from_bytes(env, initial_dump); + auto* convo_info_volatile = new session::config::ConvoInfoVolatile(secret_key, initial); + + jclass convoClass = env->FindClass("network/loki/messenger/libsession_util/ConversationVolatileConfig"); + jmethodID constructor = env->GetMethodID(convoClass, "", "(J)V"); + jobject newConfig = env->NewObject(convoClass, constructor, reinterpret_cast(convo_info_volatile)); + + return newConfig; +} + + + +extern "C" +JNIEXPORT jint JNICALL +Java_network_loki_messenger_libsession_1util_ConversationVolatileConfig_sizeOneToOnes(JNIEnv *env, + jobject thiz) { + std::lock_guard lock{util::util_mutex_}; + auto conversations = ptrToConvoInfo(env, thiz); + return conversations->size_1to1(); +} + +#pragma clang diagnostic pop +extern "C" +JNIEXPORT jint JNICALL +Java_network_loki_messenger_libsession_1util_ConversationVolatileConfig_eraseAll(JNIEnv *env, + jobject thiz, + jobject predicate) { + std::lock_guard lock{util::util_mutex_}; + auto conversations = ptrToConvoInfo(env, thiz); + + jclass predicate_class = env->FindClass("kotlin/jvm/functions/Function1"); + jmethodID predicate_call = env->GetMethodID(predicate_class, "invoke", "(Ljava/lang/Object;)Ljava/lang/Object;"); + + jclass bool_class = env->FindClass("java/lang/Boolean"); + jmethodID bool_get = env->GetMethodID(bool_class, "booleanValue", "()Z"); + + int removed = 0; + auto to_erase = std::vector(); + + for (auto it = conversations->begin(); it != conversations->end(); ++it) { + auto result = env->CallObjectMethod(predicate, predicate_call, serialize_any(env, *it)); + bool bool_result = env->CallBooleanMethod(result, bool_get); + if (bool_result) { + to_erase.push_back(*it); + } + } + + for (auto & entry : to_erase) { + if (conversations->erase(entry)) { + removed++; + } + } + + return removed; +} + +extern "C" +JNIEXPORT jint JNICALL +Java_network_loki_messenger_libsession_1util_ConversationVolatileConfig_size(JNIEnv *env, + jobject thiz) { + std::lock_guard lock{util::util_mutex_}; + 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) { + std::lock_guard lock{util::util_mutex_}; + 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) { + std::lock_guard lock{util::util_mutex_}; + + 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$Community"); + jclass legacy_closed_group = env->FindClass("network/loki/messenger/libsession_util/util/Conversation$LegacyGroup"); + + jclass to_store_class = env->GetObjectClass(to_store); + if (env->IsSameObject(to_store_class, one_to_one)) { + // store as 1to1 + convos->set(deserialize_one_to_one(env, to_store, convos)); + } else if (env->IsSameObject(to_store_class,open_group)) { + // store as open_group + convos->set(deserialize_community(env, to_store, convos)); + } else if (env->IsSameObject(to_store_class,legacy_closed_group)) { + // store as legacy_closed_group + convos->set(deserialize_legacy_closed_group(env, to_store, convos)); + } +} +extern "C" +JNIEXPORT jobject JNICALL +Java_network_loki_messenger_libsession_1util_ConversationVolatileConfig_getOneToOne(JNIEnv *env, + jobject thiz, + jstring pub_key_hex) { + std::lock_guard lock{util::util_mutex_}; + auto convos = ptrToConvoInfo(env, thiz); + auto param = env->GetStringUTFChars(pub_key_hex, nullptr); + auto internal = convos->get_1to1(param); + env->ReleaseStringUTFChars(pub_key_hex, param); + if (internal) { + return serialize_one_to_one(env, *internal); + } + return nullptr; +} +extern "C" +JNIEXPORT jobject JNICALL +Java_network_loki_messenger_libsession_1util_ConversationVolatileConfig_getOrConstructOneToOne( + JNIEnv *env, jobject thiz, jstring pub_key_hex) { + std::lock_guard lock{util::util_mutex_}; + auto convos = ptrToConvoInfo(env, thiz); + auto param = env->GetStringUTFChars(pub_key_hex, nullptr); + auto internal = convos->get_or_construct_1to1(param); + env->ReleaseStringUTFChars(pub_key_hex, param); + return serialize_one_to_one(env, internal); +} +extern "C" +JNIEXPORT jboolean JNICALL +Java_network_loki_messenger_libsession_1util_ConversationVolatileConfig_eraseOneToOne(JNIEnv *env, + jobject thiz, + jstring pub_key_hex) { + std::lock_guard lock{util::util_mutex_}; + auto convos = ptrToConvoInfo(env, thiz); + auto param = env->GetStringUTFChars(pub_key_hex, nullptr); + auto result = convos->erase_1to1(param); + env->ReleaseStringUTFChars(pub_key_hex, param); + return result; +} + +extern "C" +JNIEXPORT jobject JNICALL +Java_network_loki_messenger_libsession_1util_ConversationVolatileConfig_getCommunity__Ljava_lang_String_2Ljava_lang_String_2( + JNIEnv *env, jobject thiz, jstring base_url, jstring room) { + std::lock_guard lock{util::util_mutex_}; + auto convos = ptrToConvoInfo(env, thiz); + auto base_url_chars = env->GetStringUTFChars(base_url, nullptr); + auto room_chars = env->GetStringUTFChars(room, nullptr); + auto open = convos->get_community(base_url_chars, room_chars); + if (open) { + auto serialized = serialize_open_group(env, *open); + return serialized; + } + return nullptr; +} +extern "C" +JNIEXPORT jobject JNICALL +Java_network_loki_messenger_libsession_1util_ConversationVolatileConfig_getOrConstructCommunity__Ljava_lang_String_2Ljava_lang_String_2_3B( + JNIEnv *env, jobject thiz, jstring base_url, jstring room, jbyteArray pub_key) { + std::lock_guard lock{util::util_mutex_}; + auto convos = ptrToConvoInfo(env, thiz); + auto base_url_chars = env->GetStringUTFChars(base_url, nullptr); + auto room_chars = env->GetStringUTFChars(room, nullptr); + auto pub_key_ustring = util::ustring_from_bytes(env, pub_key); + auto open = convos->get_or_construct_community(base_url_chars, room_chars, pub_key_ustring); + auto serialized = serialize_open_group(env, open); + return serialized; +} +extern "C" +JNIEXPORT jobject JNICALL +Java_network_loki_messenger_libsession_1util_ConversationVolatileConfig_getOrConstructCommunity__Ljava_lang_String_2Ljava_lang_String_2Ljava_lang_String_2( + JNIEnv *env, jobject thiz, jstring base_url, jstring room, jstring pub_key_hex) { + std::lock_guard lock{util::util_mutex_}; + auto convos = ptrToConvoInfo(env, thiz); + auto base_url_chars = env->GetStringUTFChars(base_url, nullptr); + auto room_chars = env->GetStringUTFChars(room, nullptr); + auto hex_chars = env->GetStringUTFChars(pub_key_hex, nullptr); + auto open = convos->get_or_construct_community(base_url_chars, room_chars, hex_chars); + env->ReleaseStringUTFChars(base_url, base_url_chars); + env->ReleaseStringUTFChars(room, room_chars); + env->ReleaseStringUTFChars(pub_key_hex, hex_chars); + auto serialized = serialize_open_group(env, open); + return serialized; +} +extern "C" +JNIEXPORT jboolean JNICALL +Java_network_loki_messenger_libsession_1util_ConversationVolatileConfig_eraseCommunity__Lnetwork_loki_messenger_libsession_1util_util_Conversation_Community_2(JNIEnv *env, + jobject thiz, + jobject open_group) { + std::lock_guard lock{util::util_mutex_}; + auto convos = ptrToConvoInfo(env, thiz); + auto deserialized = deserialize_community(env, open_group, convos); + return convos->erase(deserialized); +} +extern "C" +JNIEXPORT jboolean JNICALL +Java_network_loki_messenger_libsession_1util_ConversationVolatileConfig_eraseCommunity__Ljava_lang_String_2Ljava_lang_String_2( + JNIEnv *env, jobject thiz, jstring base_url, jstring room) { + std::lock_guard lock{util::util_mutex_}; + auto convos = ptrToConvoInfo(env, thiz); + auto base_url_chars = env->GetStringUTFChars(base_url, nullptr); + auto room_chars = env->GetStringUTFChars(room, nullptr); + auto result = convos->erase_community(base_url_chars, room_chars); + env->ReleaseStringUTFChars(base_url, base_url_chars); + env->ReleaseStringUTFChars(room, room_chars); + return result; +} +extern "C" +JNIEXPORT jobject JNICALL +Java_network_loki_messenger_libsession_1util_ConversationVolatileConfig_getLegacyClosedGroup( + JNIEnv *env, jobject thiz, jstring group_id) { + std::lock_guard lock{util::util_mutex_}; + auto convos = ptrToConvoInfo(env, thiz); + auto id_chars = env->GetStringUTFChars(group_id, nullptr); + auto lgc = convos->get_legacy_group(id_chars); + env->ReleaseStringUTFChars(group_id, id_chars); + if (lgc) { + auto serialized = serialize_legacy_group(env, *lgc); + return serialized; + } + return nullptr; +} +extern "C" +JNIEXPORT jobject JNICALL +Java_network_loki_messenger_libsession_1util_ConversationVolatileConfig_getOrConstructLegacyGroup( + JNIEnv *env, jobject thiz, jstring group_id) { + std::lock_guard lock{util::util_mutex_}; + auto convos = ptrToConvoInfo(env, thiz); + auto id_chars = env->GetStringUTFChars(group_id, nullptr); + auto lgc = convos->get_or_construct_legacy_group(id_chars); + env->ReleaseStringUTFChars(group_id, id_chars); + return serialize_legacy_group(env, lgc); +} +extern "C" +JNIEXPORT jboolean JNICALL +Java_network_loki_messenger_libsession_1util_ConversationVolatileConfig_eraseLegacyClosedGroup( + JNIEnv *env, jobject thiz, jstring group_id) { + std::lock_guard lock{util::util_mutex_}; + auto convos = ptrToConvoInfo(env, thiz); + auto id_chars = env->GetStringUTFChars(group_id, nullptr); + auto result = convos->erase_legacy_group(id_chars); + env->ReleaseStringUTFChars(group_id, id_chars); + return result; +} +extern "C" +JNIEXPORT jboolean JNICALL +Java_network_loki_messenger_libsession_1util_ConversationVolatileConfig_erase(JNIEnv *env, + jobject thiz, + jobject conversation) { + std::lock_guard lock{util::util_mutex_}; + auto convos = ptrToConvoInfo(env, thiz); + auto deserialized = deserialize_any(env, conversation, convos); + if (!deserialized.has_value()) return false; + return convos->erase(*deserialized); +} +extern "C" +JNIEXPORT jint JNICALL +Java_network_loki_messenger_libsession_1util_ConversationVolatileConfig_sizeCommunities(JNIEnv *env, + jobject thiz) { + std::lock_guard lock{util::util_mutex_}; + auto convos = ptrToConvoInfo(env, thiz); + return convos->size_communities(); +} +extern "C" +JNIEXPORT jint JNICALL +Java_network_loki_messenger_libsession_1util_ConversationVolatileConfig_sizeLegacyClosedGroups( + JNIEnv *env, jobject thiz) { + std::lock_guard lock{util::util_mutex_}; + auto convos = ptrToConvoInfo(env, thiz); + return convos->size_legacy_groups(); +} +extern "C" +JNIEXPORT jobject JNICALL +Java_network_loki_messenger_libsession_1util_ConversationVolatileConfig_all(JNIEnv *env, + jobject thiz) { + std::lock_guard lock{util::util_mutex_}; + auto convos = ptrToConvoInfo(env, thiz); + jclass stack = env->FindClass("java/util/Stack"); + jmethodID init = env->GetMethodID(stack, "", "()V"); + jobject our_stack = env->NewObject(stack, init); + jmethodID push = env->GetMethodID(stack, "push", "(Ljava/lang/Object;)Ljava/lang/Object;"); + for (const auto& convo : *convos) { + auto contact_obj = serialize_any(env, convo); + env->CallObjectMethod(our_stack, push, contact_obj); + } + return our_stack; +} +extern "C" +JNIEXPORT jobject JNICALL +Java_network_loki_messenger_libsession_1util_ConversationVolatileConfig_allOneToOnes(JNIEnv *env, + jobject thiz) { + std::lock_guard lock{util::util_mutex_}; + auto convos = ptrToConvoInfo(env, thiz); + jclass stack = env->FindClass("java/util/Stack"); + jmethodID init = env->GetMethodID(stack, "", "()V"); + jobject our_stack = env->NewObject(stack, init); + jmethodID push = env->GetMethodID(stack, "push", "(Ljava/lang/Object;)Ljava/lang/Object;"); + for (auto contact = convos->begin_1to1(); contact != convos->end(); ++contact) + env->CallObjectMethod(our_stack, push, serialize_one_to_one(env, *contact)); + return our_stack; +} +extern "C" +JNIEXPORT jobject JNICALL +Java_network_loki_messenger_libsession_1util_ConversationVolatileConfig_allCommunities(JNIEnv *env, + jobject thiz) { + std::lock_guard lock{util::util_mutex_}; + auto convos = ptrToConvoInfo(env, thiz); + jclass stack = env->FindClass("java/util/Stack"); + jmethodID init = env->GetMethodID(stack, "", "()V"); + jobject our_stack = env->NewObject(stack, init); + jmethodID push = env->GetMethodID(stack, "push", "(Ljava/lang/Object;)Ljava/lang/Object;"); + for (auto contact = convos->begin_communities(); contact != convos->end(); ++contact) + env->CallObjectMethod(our_stack, push, serialize_open_group(env, *contact)); + return our_stack; +} +extern "C" +JNIEXPORT jobject JNICALL +Java_network_loki_messenger_libsession_1util_ConversationVolatileConfig_allLegacyClosedGroups( + JNIEnv *env, jobject thiz) { + std::lock_guard lock{util::util_mutex_}; + auto convos = ptrToConvoInfo(env, thiz); + jclass stack = env->FindClass("java/util/Stack"); + jmethodID init = env->GetMethodID(stack, "", "()V"); + jobject our_stack = env->NewObject(stack, init); + jmethodID push = env->GetMethodID(stack, "push", "(Ljava/lang/Object;)Ljava/lang/Object;"); + for (auto contact = convos->begin_legacy_groups(); contact != convos->end(); ++contact) + env->CallObjectMethod(our_stack, push, serialize_legacy_group(env, *contact)); + return our_stack; +} \ No newline at end of file diff --git a/libsession-util/src/main/cpp/conversation.h b/libsession-util/src/main/cpp/conversation.h new file mode 100644 index 000000000..45e453a59 --- /dev/null +++ b/libsession-util/src/main/cpp/conversation.h @@ -0,0 +1,122 @@ +#ifndef SESSION_ANDROID_CONVERSATION_H +#define SESSION_ANDROID_CONVERSATION_H + +#include +#include "util.h" +#include "session/config/convo_info_volatile.hpp" + +inline session::config::ConvoInfoVolatile *ptrToConvoInfo(JNIEnv *env, jobject obj) { + jclass contactsClass = env->FindClass("network/loki/messenger/libsession_util/ConversationVolatileConfig"); + jfieldID pointerField = env->GetFieldID(contactsClass, "pointer", "J"); + return (session::config::ConvoInfoVolatile *) env->GetLongField(obj, pointerField); +} + +inline jobject serialize_one_to_one(JNIEnv *env, session::config::convo::one_to_one one_to_one) { + jclass clazz = env->FindClass("network/loki/messenger/libsession_util/util/Conversation$OneToOne"); + jmethodID constructor = env->GetMethodID(clazz, "", "(Ljava/lang/String;JZ)V"); + auto session_id = env->NewStringUTF(one_to_one.session_id.data()); + auto last_read = one_to_one.last_read; + auto unread = one_to_one.unread; + jobject serialized = env->NewObject(clazz, constructor, session_id, last_read, unread); + return serialized; +} + +inline jobject serialize_open_group(JNIEnv *env, session::config::convo::community community) { + jclass clazz = env->FindClass("network/loki/messenger/libsession_util/util/Conversation$Community"); + auto base_community = util::serialize_base_community(env, community); + jmethodID constructor = env->GetMethodID(clazz, "", + "(Lnetwork/loki/messenger/libsession_util/util/BaseCommunityInfo;JZ)V"); + auto last_read = community.last_read; + auto unread = community.unread; + jobject serialized = env->NewObject(clazz, constructor, base_community, last_read, unread); + return serialized; +} + +inline jobject serialize_legacy_group(JNIEnv *env, session::config::convo::legacy_group group) { + jclass clazz = env->FindClass("network/loki/messenger/libsession_util/util/Conversation$LegacyGroup"); + jmethodID constructor = env->GetMethodID(clazz, "", "(Ljava/lang/String;JZ)V"); + auto group_id = env->NewStringUTF(group.id.data()); + auto last_read = group.last_read; + auto unread = group.unread; + jobject serialized = env->NewObject(clazz, constructor, group_id, last_read, unread); + return serialized; +} + +inline jobject serialize_any(JNIEnv *env, session::config::convo::any any) { + if (auto* dm = std::get_if(&any)) { + return serialize_one_to_one(env, *dm); + } else if (auto* og = std::get_if(&any)) { + return serialize_open_group(env, *og); + } else if (auto* lgc = std::get_if(&any)) { + return serialize_legacy_group(env, *lgc); + } + return nullptr; +} + +inline session::config::convo::one_to_one deserialize_one_to_one(JNIEnv *env, jobject info, session::config::ConvoInfoVolatile *conf) { + auto clazz = env->FindClass("network/loki/messenger/libsession_util/util/Conversation$OneToOne"); + auto id_getter = env->GetFieldID(clazz, "sessionId", "Ljava/lang/String;"); + auto last_read_getter = env->GetFieldID(clazz, "lastRead", "J"); + auto unread_getter = env->GetFieldID(clazz, "unread", "Z"); + jstring id = static_cast(env->GetObjectField(info, id_getter)); + auto id_chars = env->GetStringUTFChars(id, nullptr); + std::string id_string = std::string{id_chars}; + auto deserialized = conf->get_or_construct_1to1(id_string); + deserialized.last_read = env->GetLongField(info, last_read_getter); + deserialized.unread = env->GetBooleanField(info, unread_getter); + env->ReleaseStringUTFChars(id, id_chars); + return deserialized; +} + +inline session::config::convo::community deserialize_community(JNIEnv *env, jobject info, session::config::ConvoInfoVolatile *conf) { + auto clazz = env->FindClass("network/loki/messenger/libsession_util/util/Conversation$Community"); + auto base_community_getter = env->GetFieldID(clazz, "baseCommunityInfo", "Lnetwork/loki/messenger/libsession_util/util/BaseCommunityInfo;"); + auto last_read_getter = env->GetFieldID(clazz, "lastRead", "J"); + auto unread_getter = env->GetFieldID(clazz, "unread", "Z"); + + auto base_community_info = env->GetObjectField(info, base_community_getter); + + auto base_community_deserialized = util::deserialize_base_community(env, base_community_info); + auto deserialized = conf->get_or_construct_community( + base_community_deserialized.base_url(), + base_community_deserialized.room(), + base_community_deserialized.pubkey() + ); + + deserialized.last_read = env->GetLongField(info, last_read_getter); + deserialized.unread = env->GetBooleanField(info, unread_getter); + + return deserialized; +} + +inline session::config::convo::legacy_group deserialize_legacy_closed_group(JNIEnv *env, jobject info, session::config::ConvoInfoVolatile *conf) { + auto clazz = env->FindClass("network/loki/messenger/libsession_util/util/Conversation$LegacyGroup"); + auto group_id_getter = env->GetFieldID(clazz, "groupId", "Ljava/lang/String;"); + auto last_read_getter = env->GetFieldID(clazz, "lastRead", "J"); + auto unread_getter = env->GetFieldID(clazz, "unread", "Z"); + auto group_id = static_cast(env->GetObjectField(info, group_id_getter)); + auto group_id_bytes = env->GetStringUTFChars(group_id, nullptr); + auto group_id_string = std::string{group_id_bytes}; + auto deserialized = conf->get_or_construct_legacy_group(group_id_string); + deserialized.last_read = env->GetLongField(info, last_read_getter); + deserialized.unread = env->GetBooleanField(info, unread_getter); + env->ReleaseStringUTFChars(group_id, group_id_bytes); + return deserialized; +} + +inline std::optional deserialize_any(JNIEnv *env, jobject convo, session::config::ConvoInfoVolatile *conf) { + auto oto_class = env->FindClass("network/loki/messenger/libsession_util/util/Conversation$OneToOne"); + auto og_class = env->FindClass("network/loki/messenger/libsession_util/util/Conversation$Community"); + auto lgc_class = env->FindClass("network/loki/messenger/libsession_util/util/Conversation$LegacyGroup"); + auto object_class = env->GetObjectClass(convo); + if (env->IsSameObject(object_class, oto_class)) { + return session::config::convo::any{deserialize_one_to_one(env, convo, conf)}; + } else if (env->IsSameObject(object_class, og_class)) { + return session::config::convo::any{deserialize_community(env, convo, conf)}; + } else if (env->IsSameObject(object_class, lgc_class)) { + return session::config::convo::any{deserialize_legacy_closed_group(env, convo, conf)}; + } + return std::nullopt; +} + +#endif //SESSION_ANDROID_CONVERSATION_H \ No newline at end of file diff --git a/libsession-util/src/main/cpp/user_groups.cpp b/libsession-util/src/main/cpp/user_groups.cpp new file mode 100644 index 000000000..4f2b0e6b8 --- /dev/null +++ b/libsession-util/src/main/cpp/user_groups.cpp @@ -0,0 +1,273 @@ +#pragma clang diagnostic push +#pragma ide diagnostic ignored "bugprone-reserved-identifier" +#include "user_groups.h" + + +#pragma clang diagnostic push +#pragma ide diagnostic ignored "bugprone-reserved-identifier" +extern "C" +JNIEXPORT jobject JNICALL +Java_network_loki_messenger_libsession_1util_UserGroupsConfig_00024Companion_newInstance___3B( + JNIEnv *env, jobject thiz, jbyteArray ed25519_secret_key) { + std::lock_guard lock{util::util_mutex_}; + auto secret_key = util::ustring_from_bytes(env, ed25519_secret_key); + + auto* user_groups = new session::config::UserGroups(secret_key, std::nullopt); + + jclass contactsClass = env->FindClass("network/loki/messenger/libsession_util/UserGroupsConfig"); + jmethodID constructor = env->GetMethodID(contactsClass, "", "(J)V"); + jobject newConfig = env->NewObject(contactsClass, constructor, reinterpret_cast(user_groups)); + + return newConfig; +} + +extern "C" +JNIEXPORT jobject JNICALL +Java_network_loki_messenger_libsession_1util_UserGroupsConfig_00024Companion_newInstance___3B_3B( + JNIEnv *env, jobject thiz, jbyteArray ed25519_secret_key, jbyteArray initial_dump) { + std::lock_guard lock{util::util_mutex_}; + auto secret_key = util::ustring_from_bytes(env, ed25519_secret_key); + auto initial = util::ustring_from_bytes(env, initial_dump); + + auto* user_groups = new session::config::UserGroups(secret_key, initial); + + jclass contactsClass = env->FindClass("network/loki/messenger/libsession_util/UserGroupsConfig"); + jmethodID constructor = env->GetMethodID(contactsClass, "", "(J)V"); + jobject newConfig = env->NewObject(contactsClass, constructor, reinterpret_cast(user_groups)); + + return newConfig; +} +#pragma clang diagnostic pop + +extern "C" +JNIEXPORT jint JNICALL +Java_network_loki_messenger_libsession_1util_util_GroupInfo_00024LegacyGroupInfo_00024Companion_NAME_1MAX_1LENGTH( + JNIEnv *env, jobject thiz) { + std::lock_guard lock{util::util_mutex_}; + return session::config::legacy_group_info::NAME_MAX_LENGTH; +} + +extern "C" +JNIEXPORT jobject JNICALL +Java_network_loki_messenger_libsession_1util_UserGroupsConfig_getCommunityInfo(JNIEnv *env, + jobject thiz, + jstring base_url, + jstring room) { + std::lock_guard lock{util::util_mutex_}; + auto conf = ptrToUserGroups(env, thiz); + auto base_url_bytes = env->GetStringUTFChars(base_url, nullptr); + auto room_bytes = env->GetStringUTFChars(room, nullptr); + + auto community = conf->get_community(base_url_bytes, room_bytes); + + jobject community_info = nullptr; + + if (community) { + community_info = serialize_community_info(env, *community); + } + env->ReleaseStringUTFChars(base_url, base_url_bytes); + env->ReleaseStringUTFChars(room, room_bytes); + return community_info; +} + +extern "C" +JNIEXPORT jobject JNICALL +Java_network_loki_messenger_libsession_1util_UserGroupsConfig_getLegacyGroupInfo(JNIEnv *env, + jobject thiz, + jstring session_id) { + std::lock_guard lock{util::util_mutex_}; + auto conf = ptrToUserGroups(env, thiz); + auto id_bytes = env->GetStringUTFChars(session_id, nullptr); + auto legacy_group = conf->get_legacy_group(id_bytes); + jobject return_group = nullptr; + if (legacy_group) { + return_group = serialize_legacy_group_info(env, *legacy_group); + } + env->ReleaseStringUTFChars(session_id, id_bytes); + return return_group; +} + +extern "C" +JNIEXPORT jobject JNICALL +Java_network_loki_messenger_libsession_1util_UserGroupsConfig_getOrConstructCommunityInfo( + JNIEnv *env, jobject thiz, jstring base_url, jstring room, jstring pub_key_hex) { + std::lock_guard lock{util::util_mutex_}; + auto conf = ptrToUserGroups(env, thiz); + auto base_url_bytes = env->GetStringUTFChars(base_url, nullptr); + auto room_bytes = env->GetStringUTFChars(room, nullptr); + auto pub_hex_bytes = env->GetStringUTFChars(pub_key_hex, nullptr); + + auto group = conf->get_or_construct_community(base_url_bytes, room_bytes, pub_hex_bytes); + + env->ReleaseStringUTFChars(base_url, base_url_bytes); + env->ReleaseStringUTFChars(room, room_bytes); + env->ReleaseStringUTFChars(pub_key_hex, pub_hex_bytes); + return serialize_community_info(env, group); +} + +extern "C" +JNIEXPORT jobject JNICALL +Java_network_loki_messenger_libsession_1util_UserGroupsConfig_getOrConstructLegacyGroupInfo( + JNIEnv *env, jobject thiz, jstring session_id) { + std::lock_guard lock{util::util_mutex_}; + auto conf = ptrToUserGroups(env, thiz); + auto id_bytes = env->GetStringUTFChars(session_id, nullptr); + auto group = conf->get_or_construct_legacy_group(id_bytes); + env->ReleaseStringUTFChars(session_id, id_bytes); + return serialize_legacy_group_info(env, group); +} + +extern "C" +JNIEXPORT void JNICALL +Java_network_loki_messenger_libsession_1util_UserGroupsConfig_set__Lnetwork_loki_messenger_libsession_1util_util_GroupInfo_2( + JNIEnv *env, jobject thiz, jobject group_info) { + std::lock_guard lock{util::util_mutex_}; + auto conf = ptrToUserGroups(env, thiz); + auto community_info = env->FindClass("network/loki/messenger/libsession_util/util/GroupInfo$CommunityGroupInfo"); + auto legacy_info = env->FindClass("network/loki/messenger/libsession_util/util/GroupInfo$LegacyGroupInfo"); + auto object_class = env->GetObjectClass(group_info); + if (env->IsSameObject(community_info, object_class)) { + auto deserialized = deserialize_community_info(env, group_info, conf); + conf->set(deserialized); + } else if (env->IsSameObject(legacy_info, object_class)) { + auto deserialized = deserialize_legacy_group_info(env, group_info, conf); + conf->set(deserialized); + } +} + + +extern "C" +JNIEXPORT void JNICALL +Java_network_loki_messenger_libsession_1util_UserGroupsConfig_erase__Lnetwork_loki_messenger_libsession_1util_util_GroupInfo_2( + JNIEnv *env, jobject thiz, jobject group_info) { + std::lock_guard lock{util::util_mutex_}; + auto conf = ptrToUserGroups(env, thiz); + auto communityInfo = env->FindClass("network/loki/messenger/libsession_util/util/GroupInfo$CommunityGroupInfo"); + auto legacyInfo = env->FindClass("network/loki/messenger/libsession_util/util/GroupInfo$LegacyGroupInfo"); + if (env->GetObjectClass(group_info) == communityInfo) { + auto deserialized = deserialize_community_info(env, group_info, conf); + conf->erase(deserialized); + } else if (env->GetObjectClass(group_info) == legacyInfo) { + auto deserialized = deserialize_legacy_group_info(env, group_info, conf); + conf->erase(deserialized); + } +} + +extern "C" +JNIEXPORT jint JNICALL +Java_network_loki_messenger_libsession_1util_UserGroupsConfig_sizeCommunityInfo(JNIEnv *env, + jobject thiz) { + std::lock_guard lock{util::util_mutex_}; + auto conf = ptrToUserGroups(env, thiz); + return conf->size_communities(); +} + +extern "C" +JNIEXPORT jint JNICALL +Java_network_loki_messenger_libsession_1util_UserGroupsConfig_sizeLegacyGroupInfo(JNIEnv *env, + jobject thiz) { + std::lock_guard lock{util::util_mutex_}; + auto conf = ptrToUserGroups(env, thiz); + return conf->size_legacy_groups(); +} + +extern "C" +JNIEXPORT jint JNICALL +Java_network_loki_messenger_libsession_1util_UserGroupsConfig_size(JNIEnv *env, jobject thiz) { + std::lock_guard lock{util::util_mutex_}; + auto conf = ptrToConvoInfo(env, thiz); + return conf->size(); +} + +inline jobject iterator_as_java_stack(JNIEnv *env, const session::config::UserGroups::iterator& begin, const session::config::UserGroups::iterator& end) { + jclass stack = env->FindClass("java/util/Stack"); + jmethodID init = env->GetMethodID(stack, "", "()V"); + jobject our_stack = env->NewObject(stack, init); + jmethodID push = env->GetMethodID(stack, "push", "(Ljava/lang/Object;)Ljava/lang/Object;"); + for (auto it = begin; it != end;) { + // do something with it + auto item = *it; + jobject serialized = nullptr; + if (auto* lgc = std::get_if(&item)) { + serialized = serialize_legacy_group_info(env, *lgc); + } else if (auto* community = std::get_if(&item)) { + serialized = serialize_community_info(env, *community); + } + if (serialized != nullptr) { + env->CallObjectMethod(our_stack, push, serialized); + } + it++; + } + return our_stack; +} + +extern "C" +JNIEXPORT jobject JNICALL +Java_network_loki_messenger_libsession_1util_UserGroupsConfig_all(JNIEnv *env, jobject thiz) { + std::lock_guard lock{util::util_mutex_}; + auto conf = ptrToUserGroups(env, thiz); + jobject all_stack = iterator_as_java_stack(env, conf->begin(), conf->end()); + return all_stack; +} + +extern "C" +JNIEXPORT jobject JNICALL +Java_network_loki_messenger_libsession_1util_UserGroupsConfig_allCommunityInfo(JNIEnv *env, + jobject thiz) { + std::lock_guard lock{util::util_mutex_}; + auto conf = ptrToUserGroups(env, thiz); + jobject community_stack = iterator_as_java_stack(env, conf->begin_communities(), conf->end()); + return community_stack; +} + +extern "C" +JNIEXPORT jobject JNICALL +Java_network_loki_messenger_libsession_1util_UserGroupsConfig_allLegacyGroupInfo(JNIEnv *env, + jobject thiz) { + std::lock_guard lock{util::util_mutex_}; + auto conf = ptrToUserGroups(env, thiz); + jobject legacy_stack = iterator_as_java_stack(env, conf->begin_legacy_groups(), conf->end()); + return legacy_stack; +} + +extern "C" +JNIEXPORT jboolean JNICALL +Java_network_loki_messenger_libsession_1util_UserGroupsConfig_eraseCommunity__Lnetwork_loki_messenger_libsession_1util_util_BaseCommunityInfo_2(JNIEnv *env, + jobject thiz, + jobject base_community_info) { + std::lock_guard lock{util::util_mutex_}; + auto conf = ptrToUserGroups(env, thiz); + auto base_community = util::deserialize_base_community(env, base_community_info); + return conf->erase_community(base_community.base_url(),base_community.room()); +} + +extern "C" +JNIEXPORT jboolean JNICALL +Java_network_loki_messenger_libsession_1util_UserGroupsConfig_eraseCommunity__Ljava_lang_String_2Ljava_lang_String_2( + JNIEnv *env, jobject thiz, jstring server, jstring room) { + std::lock_guard lock{util::util_mutex_}; + auto conf = ptrToUserGroups(env, thiz); + auto server_bytes = env->GetStringUTFChars(server, nullptr); + auto room_bytes = env->GetStringUTFChars(room, nullptr); + auto community = conf->get_community(server_bytes, room_bytes); + bool deleted = false; + if (community) { + deleted = conf->erase(*community); + } + env->ReleaseStringUTFChars(server, server_bytes); + env->ReleaseStringUTFChars(room, room_bytes); + return deleted; +} + +extern "C" +JNIEXPORT jboolean JNICALL +Java_network_loki_messenger_libsession_1util_UserGroupsConfig_eraseLegacyGroup(JNIEnv *env, + jobject thiz, + jstring session_id) { + std::lock_guard lock{util::util_mutex_}; + auto conf = ptrToUserGroups(env, thiz); + auto session_id_bytes = env->GetStringUTFChars(session_id, nullptr); + bool return_bool = conf->erase_legacy_group(session_id_bytes); + env->ReleaseStringUTFChars(session_id, session_id_bytes); + return return_bool; +} \ No newline at end of file diff --git a/libsession-util/src/main/cpp/user_groups.h b/libsession-util/src/main/cpp/user_groups.h new file mode 100644 index 000000000..c4754fe11 --- /dev/null +++ b/libsession-util/src/main/cpp/user_groups.h @@ -0,0 +1,139 @@ + +#ifndef SESSION_ANDROID_USER_GROUPS_H +#define SESSION_ANDROID_USER_GROUPS_H + +#include "jni.h" +#include "util.h" +#include "conversation.h" +#include "session/config/user_groups.hpp" + +inline session::config::UserGroups* ptrToUserGroups(JNIEnv *env, jobject obj) { + jclass configClass = env->FindClass("network/loki/messenger/libsession_util/UserGroupsConfig"); + jfieldID pointerField = env->GetFieldID(configClass, "pointer", "J"); + return (session::config::UserGroups*) env->GetLongField(obj, pointerField); +} + +inline void deserialize_members_into(JNIEnv *env, jobject members_map, session::config::legacy_group_info& to_append_group) { + jclass map_class = env->FindClass("java/util/Map"); + jclass map_entry_class = env->FindClass("java/util/Map$Entry"); + jclass set_class = env->FindClass("java/util/Set"); + jclass iterator_class = env->FindClass("java/util/Iterator"); + jclass boxed_bool = env->FindClass("java/lang/Boolean"); + + jmethodID get_entry_set = env->GetMethodID(map_class, "entrySet", "()Ljava/util/Set;"); + jmethodID get_at = env->GetMethodID(set_class, "iterator", "()Ljava/util/Iterator;"); + jmethodID has_next = env->GetMethodID(iterator_class, "hasNext", "()Z"); + jmethodID next = env->GetMethodID(iterator_class, "next", "()Ljava/lang/Object;"); + jmethodID get_key = env->GetMethodID(map_entry_class, "getKey", "()Ljava/lang/Object;"); + jmethodID get_value = env->GetMethodID(map_entry_class, "getValue", "()Ljava/lang/Object;"); + jmethodID get_bool_value = env->GetMethodID(boxed_bool, "booleanValue", "()Z"); + + jobject entry_set = env->CallObjectMethod(members_map, get_entry_set); + jobject iterator = env->CallObjectMethod(entry_set, get_at); + + while (env->CallBooleanMethod(iterator, has_next)) { + jobject entry = env->CallObjectMethod(iterator, next); + jstring key = static_cast(env->CallObjectMethod(entry, get_key)); + jobject boxed = env->CallObjectMethod(entry, get_value); + bool is_admin = env->CallBooleanMethod(boxed, get_bool_value); + auto member_string = env->GetStringUTFChars(key, nullptr); + to_append_group.insert(member_string, is_admin); + env->ReleaseStringUTFChars(key, member_string); + } +} + +inline session::config::legacy_group_info deserialize_legacy_group_info(JNIEnv *env, jobject info, session::config::UserGroups* conf) { + auto clazz = env->FindClass("network/loki/messenger/libsession_util/util/GroupInfo$LegacyGroupInfo"); + auto id_field = env->GetFieldID(clazz, "sessionId", "Ljava/lang/String;"); + auto name_field = env->GetFieldID(clazz, "name", "Ljava/lang/String;"); + auto members_field = env->GetFieldID(clazz, "members", "Ljava/util/Map;"); + auto enc_pub_key_field = env->GetFieldID(clazz, "encPubKey", "[B"); + auto enc_sec_key_field = env->GetFieldID(clazz, "encSecKey", "[B"); + auto priority_field = env->GetFieldID(clazz, "priority", "I"); + auto disappearing_timer_field = env->GetFieldID(clazz, "disappearingTimer", "J"); + auto joined_at_field = env->GetFieldID(clazz, "joinedAt", "J"); + jstring id = static_cast(env->GetObjectField(info, id_field)); + jstring name = static_cast(env->GetObjectField(info, name_field)); + jobject members_map = env->GetObjectField(info, members_field); + jbyteArray enc_pub_key = static_cast(env->GetObjectField(info, enc_pub_key_field)); + jbyteArray enc_sec_key = static_cast(env->GetObjectField(info, enc_sec_key_field)); + int priority = env->GetIntField(info, priority_field); + long joined_at = env->GetLongField(info, joined_at_field); + + auto id_bytes = env->GetStringUTFChars(id, nullptr); + auto name_bytes = env->GetStringUTFChars(name, nullptr); + auto enc_pub_key_bytes = util::ustring_from_bytes(env, enc_pub_key); + auto enc_sec_key_bytes = util::ustring_from_bytes(env, enc_sec_key); + + auto info_deserialized = conf->get_or_construct_legacy_group(id_bytes); + + auto current_members = info_deserialized.members(); + for (auto member = current_members.begin(); member != current_members.end(); ++member) { + info_deserialized.erase(member->first); + } + deserialize_members_into(env, members_map, info_deserialized); + info_deserialized.name = name_bytes; + info_deserialized.enc_pubkey = enc_pub_key_bytes; + info_deserialized.enc_seckey = enc_sec_key_bytes; + info_deserialized.priority = priority; + info_deserialized.disappearing_timer = std::chrono::seconds(env->GetLongField(info, disappearing_timer_field)); + info_deserialized.joined_at = joined_at; + env->ReleaseStringUTFChars(id, id_bytes); + env->ReleaseStringUTFChars(name, name_bytes); + return info_deserialized; +} + +inline session::config::community_info deserialize_community_info(JNIEnv *env, jobject info, session::config::UserGroups* conf) { + auto clazz = env->FindClass("network/loki/messenger/libsession_util/util/GroupInfo$CommunityGroupInfo"); + auto base_info = env->GetFieldID(clazz, "community", "Lnetwork/loki/messenger/libsession_util/util/BaseCommunityInfo;"); + auto priority = env->GetFieldID(clazz, "priority", "I"); + jobject base_community_info = env->GetObjectField(info, base_info); + auto deserialized_base_info = util::deserialize_base_community(env, base_community_info); + int deserialized_priority = env->GetIntField(info, priority); + auto community_info = conf->get_or_construct_community(deserialized_base_info.base_url(), deserialized_base_info.room(), deserialized_base_info.pubkey_hex()); + community_info.priority = deserialized_priority; + return community_info; +} + +inline jobject serialize_members(JNIEnv *env, std::map members_map) { + jclass map_class = env->FindClass("java/util/HashMap"); + jclass boxed_bool = env->FindClass("java/lang/Boolean"); + jmethodID map_constructor = env->GetMethodID(map_class, "", "()V"); + jmethodID insert = env->GetMethodID(map_class, "put", "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;"); + jmethodID new_bool = env->GetMethodID(boxed_bool, "", "(Z)V"); + + jobject new_map = env->NewObject(map_class, map_constructor); + for (auto it = members_map.begin(); it != members_map.end(); it++) { + auto session_id = env->NewStringUTF(it->first.data()); + bool is_admin = it->second; + auto jbool = env->NewObject(boxed_bool, new_bool, is_admin); + env->CallObjectMethod(new_map, insert, session_id, jbool); + } + return new_map; +} + +inline jobject serialize_legacy_group_info(JNIEnv *env, session::config::legacy_group_info info) { + jstring session_id = env->NewStringUTF(info.session_id.data()); + jstring name = env->NewStringUTF(info.name.data()); + jobject members = serialize_members(env, info.members()); + jbyteArray enc_pubkey = util::bytes_from_ustring(env, info.enc_pubkey); + jbyteArray enc_seckey = util::bytes_from_ustring(env, info.enc_seckey); + int priority = info.priority; + long joined_at = info.joined_at; + + jclass legacy_group_class = env->FindClass("network/loki/messenger/libsession_util/util/GroupInfo$LegacyGroupInfo"); + jmethodID constructor = env->GetMethodID(legacy_group_class, "", "(Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;[B[BIJJ)V"); + jobject serialized = env->NewObject(legacy_group_class, constructor, session_id, name, members, enc_pubkey, enc_seckey, priority, (jlong) info.disappearing_timer.count(), joined_at); + return serialized; +} + +inline jobject serialize_community_info(JNIEnv *env, session::config::community_info info) { + auto priority = info.priority; + auto serialized_info = util::serialize_base_community(env, info); + auto clazz = env->FindClass("network/loki/messenger/libsession_util/util/GroupInfo$CommunityGroupInfo"); + jmethodID constructor = env->GetMethodID(clazz, "", "(Lnetwork/loki/messenger/libsession_util/util/BaseCommunityInfo;I)V"); + jobject serialized = env->NewObject(clazz, constructor, serialized_info, priority); + return serialized; +} + +#endif //SESSION_ANDROID_USER_GROUPS_H diff --git a/libsession-util/src/main/cpp/user_profile.cpp b/libsession-util/src/main/cpp/user_profile.cpp new file mode 100644 index 000000000..78b671ef0 --- /dev/null +++ b/libsession-util/src/main/cpp/user_profile.cpp @@ -0,0 +1,98 @@ +#include "user_profile.h" +#include "util.h" + +extern "C" { +#pragma clang diagnostic push +#pragma ide diagnostic ignored "bugprone-reserved-identifier" +JNIEXPORT jobject JNICALL +Java_network_loki_messenger_libsession_1util_UserProfile_00024Companion_newInstance___3B_3B( + JNIEnv *env, jobject thiz, jbyteArray ed25519_secret_key, jbyteArray initial_dump) { + std::lock_guard lock{util::util_mutex_}; + auto secret_key = util::ustring_from_bytes(env, ed25519_secret_key); + auto initial = util::ustring_from_bytes(env, initial_dump); + auto* profile = new session::config::UserProfile(secret_key, std::optional(initial)); + + jclass userClass = env->FindClass("network/loki/messenger/libsession_util/UserProfile"); + jmethodID constructor = env->GetMethodID(userClass, "", "(J)V"); + jobject newConfig = env->NewObject(userClass, constructor, reinterpret_cast(profile)); + + return newConfig; +} + +JNIEXPORT jobject JNICALL +Java_network_loki_messenger_libsession_1util_UserProfile_00024Companion_newInstance___3B( + JNIEnv* env, + jobject, + jbyteArray secretKey) { + std::lock_guard lock{util::util_mutex_}; + auto* profile = new session::config::UserProfile(util::ustring_from_bytes(env, secretKey), std::nullopt); + + jclass userClass = env->FindClass("network/loki/messenger/libsession_util/UserProfile"); + jmethodID constructor = env->GetMethodID(userClass, "", "(J)V"); + jobject newConfig = env->NewObject(userClass, constructor, reinterpret_cast(profile)); + + return newConfig; +} +#pragma clang diagnostic pop + +JNIEXPORT void JNICALL +Java_network_loki_messenger_libsession_1util_UserProfile_setName( + JNIEnv* env, + jobject thiz, + jstring newName) { + std::lock_guard lock{util::util_mutex_}; + auto profile = ptrToProfile(env, thiz); + auto name_chars = env->GetStringUTFChars(newName, nullptr); + profile->set_name(name_chars); + env->ReleaseStringUTFChars(newName, name_chars); +} + +JNIEXPORT jstring JNICALL +Java_network_loki_messenger_libsession_1util_UserProfile_getName(JNIEnv *env, jobject thiz) { + std::lock_guard lock{util::util_mutex_}; + auto profile = ptrToProfile(env, thiz); + auto name = profile->get_name(); + if (name == std::nullopt) return nullptr; + jstring returnString = env->NewStringUTF(name->data()); + return returnString; +} + +JNIEXPORT jobject JNICALL +Java_network_loki_messenger_libsession_1util_UserProfile_getPic(JNIEnv *env, jobject thiz) { + std::lock_guard lock{util::util_mutex_}; + auto profile = ptrToProfile(env, thiz); + auto pic = profile->get_profile_pic(); + + jobject returnObject = util::serialize_user_pic(env, pic); + + return returnObject; +} + +JNIEXPORT void JNICALL +Java_network_loki_messenger_libsession_1util_UserProfile_setPic(JNIEnv *env, jobject thiz, + jobject user_pic) { + std::lock_guard lock{util::util_mutex_}; + auto profile = ptrToProfile(env, thiz); + auto pic = util::deserialize_user_pic(env, user_pic); + auto url = env->GetStringUTFChars(pic.first, nullptr); + auto key = util::ustring_from_bytes(env, pic.second); + profile->set_profile_pic(url, key); + env->ReleaseStringUTFChars(pic.first, url); +} + +} +extern "C" +JNIEXPORT void JNICALL +Java_network_loki_messenger_libsession_1util_UserProfile_setNtsPriority(JNIEnv *env, jobject thiz, + jint priority) { + std::lock_guard lock{util::util_mutex_}; + auto profile = ptrToProfile(env, thiz); + profile->set_nts_priority(priority); +} +extern "C" +JNIEXPORT jint JNICALL +Java_network_loki_messenger_libsession_1util_UserProfile_getNtsPriority(JNIEnv *env, jobject thiz) { + std::lock_guard lock{util::util_mutex_}; + auto profile = ptrToProfile(env, thiz); + return profile->get_nts_priority(); +} \ No newline at end of file diff --git a/libsession-util/src/main/cpp/user_profile.h b/libsession-util/src/main/cpp/user_profile.h new file mode 100644 index 000000000..cb1b8d973 --- /dev/null +++ b/libsession-util/src/main/cpp/user_profile.h @@ -0,0 +1,14 @@ +#ifndef SESSION_ANDROID_USER_PROFILE_H +#define SESSION_ANDROID_USER_PROFILE_H + +#include "session/config/user_profile.hpp" +#include +#include + +inline session::config::UserProfile* ptrToProfile(JNIEnv* env, jobject obj) { + jclass configClass = env->FindClass("network/loki/messenger/libsession_util/UserProfile"); + jfieldID pointerField = env->GetFieldID(configClass, "pointer", "J"); + return (session::config::UserProfile*) env->GetLongField(obj, pointerField); +} + +#endif \ No newline at end of file diff --git a/libsession-util/src/main/cpp/util.cpp b/libsession-util/src/main/cpp/util.cpp new file mode 100644 index 000000000..69469eac1 --- /dev/null +++ b/libsession-util/src/main/cpp/util.cpp @@ -0,0 +1,167 @@ +#include "util.h" +#include +#include + +namespace util { + + std::mutex util_mutex_ = std::mutex(); + + jbyteArray bytes_from_ustring(JNIEnv* env, session::ustring_view from_str) { + size_t length = from_str.length(); + auto jlength = (jsize)length; + jbyteArray new_array = env->NewByteArray(jlength); + env->SetByteArrayRegion(new_array, 0, jlength, (jbyte*)from_str.data()); + return new_array; + } + + session::ustring ustring_from_bytes(JNIEnv* env, jbyteArray byteArray) { + size_t len = env->GetArrayLength(byteArray); + auto bytes = env->GetByteArrayElements(byteArray, nullptr); + + session::ustring st{reinterpret_cast(bytes), len}; + env->ReleaseByteArrayElements(byteArray, bytes, 0); + return st; + } + + jobject serialize_user_pic(JNIEnv *env, session::config::profile_pic pic) { + jclass returnObjectClass = env->FindClass("network/loki/messenger/libsession_util/util/UserPic"); + jmethodID constructor = env->GetMethodID(returnObjectClass, "", "(Ljava/lang/String;[B)V"); + jstring url = env->NewStringUTF(pic.url.data()); + jbyteArray byteArray = util::bytes_from_ustring(env, pic.key); + return env->NewObject(returnObjectClass, constructor, url, byteArray); + } + + std::pair deserialize_user_pic(JNIEnv *env, jobject user_pic) { + jclass userPicClass = env->FindClass("network/loki/messenger/libsession_util/util/UserPic"); + jfieldID picField = env->GetFieldID(userPicClass, "url", "Ljava/lang/String;"); + jfieldID keyField = env->GetFieldID(userPicClass, "key", "[B"); + auto pic = (jstring)env->GetObjectField(user_pic, picField); + auto key = (jbyteArray)env->GetObjectField(user_pic, keyField); + return {pic, key}; + } + + jobject serialize_base_community(JNIEnv *env, const session::config::community& community) { + jclass base_community_clazz = env->FindClass("network/loki/messenger/libsession_util/util/BaseCommunityInfo"); + jmethodID base_community_constructor = env->GetMethodID(base_community_clazz, "", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V"); + auto base_url = env->NewStringUTF(community.base_url().data()); + auto room = env->NewStringUTF(community.room().data()); + auto pubkey_jstring = env->NewStringUTF(community.pubkey_hex().data()); + jobject ret = env->NewObject(base_community_clazz, base_community_constructor, base_url, room, pubkey_jstring); + return ret; + } + + session::config::community deserialize_base_community(JNIEnv *env, jobject base_community) { + jclass base_community_clazz = env->FindClass("network/loki/messenger/libsession_util/util/BaseCommunityInfo"); + jfieldID base_url_field = env->GetFieldID(base_community_clazz, "baseUrl", "Ljava/lang/String;"); + jfieldID room_field = env->GetFieldID(base_community_clazz, "room", "Ljava/lang/String;"); + jfieldID pubkey_hex_field = env->GetFieldID(base_community_clazz, "pubKeyHex", "Ljava/lang/String;"); + auto base_url = (jstring)env->GetObjectField(base_community,base_url_field); + auto room = (jstring)env->GetObjectField(base_community, room_field); + auto pub_key_hex = (jstring)env->GetObjectField(base_community, pubkey_hex_field); + auto base_url_chars = env->GetStringUTFChars(base_url, nullptr); + auto room_chars = env->GetStringUTFChars(room, nullptr); + auto pub_key_hex_chars = env->GetStringUTFChars(pub_key_hex, nullptr); + + auto community = session::config::community(base_url_chars, room_chars, pub_key_hex_chars); + + env->ReleaseStringUTFChars(base_url, base_url_chars); + env->ReleaseStringUTFChars(room, room_chars); + env->ReleaseStringUTFChars(pub_key_hex, pub_key_hex_chars); + return community; + } + + jobject serialize_expiry(JNIEnv *env, const session::config::expiration_mode& mode, const std::chrono::seconds& time_seconds) { + jclass none = env->FindClass("network/loki/messenger/libsession_util/util/ExpiryMode$NONE"); + jfieldID none_instance = env->GetStaticFieldID(none, "INSTANCE", "Lnetwork/loki/messenger/libsession_util/util/ExpiryMode$NONE;"); + jclass after_send = env->FindClass("network/loki/messenger/libsession_util/util/ExpiryMode$AfterSend"); + jmethodID send_init = env->GetMethodID(after_send, "", "(J)V"); + jclass after_read = env->FindClass("network/loki/messenger/libsession_util/util/ExpiryMode$AfterRead"); + jmethodID read_init = env->GetMethodID(after_read, "", "(J)V"); + + if (mode == session::config::expiration_mode::none) { + return env->GetStaticObjectField(none, none_instance); + } else if (mode == session::config::expiration_mode::after_send) { + return env->NewObject(after_send, send_init, time_seconds.count()); + } else if (mode == session::config::expiration_mode::after_read) { + return env->NewObject(after_read, read_init, time_seconds.count()); + } + return nullptr; + } + + std::pair deserialize_expiry(JNIEnv *env, jobject expiry_mode) { + jclass parent = env->FindClass("network/loki/messenger/libsession_util/util/ExpiryMode"); + jclass after_read = env->FindClass("network/loki/messenger/libsession_util/util/ExpiryMode$AfterRead"); + jclass after_send = env->FindClass("network/loki/messenger/libsession_util/util/ExpiryMode$AfterSend"); + jfieldID duration_seconds = env->GetFieldID(parent, "expirySeconds", "J"); + + jclass object_class = env->GetObjectClass(expiry_mode); + + if (object_class == after_read) { + return std::pair(session::config::expiration_mode::after_read, env->GetLongField(expiry_mode, duration_seconds)); + } else if (object_class == after_send) { + return std::pair(session::config::expiration_mode::after_send, env->GetLongField(expiry_mode, duration_seconds)); + } + return std::pair(session::config::expiration_mode::none, 0); + } + +} + +extern "C" +JNIEXPORT jobject JNICALL +Java_network_loki_messenger_libsession_1util_util_Sodium_ed25519KeyPair(JNIEnv *env, jobject thiz, jbyteArray seed) { + std::array ed_pk; // NOLINT(cppcoreguidelines-pro-type-member-init) + std::array ed_sk; // NOLINT(cppcoreguidelines-pro-type-member-init) + auto seed_bytes = util::ustring_from_bytes(env, seed); + crypto_sign_ed25519_seed_keypair(ed_pk.data(), ed_sk.data(), seed_bytes.data()); + + jclass kp_class = env->FindClass("network/loki/messenger/libsession_util/util/KeyPair"); + jmethodID kp_constructor = env->GetMethodID(kp_class, "", "([B[B)V"); + + jbyteArray pk_jarray = util::bytes_from_ustring(env, session::ustring_view {ed_pk.data(), ed_pk.size()}); + jbyteArray sk_jarray = util::bytes_from_ustring(env, session::ustring_view {ed_sk.data(), ed_sk.size()}); + + jobject return_obj = env->NewObject(kp_class, kp_constructor, pk_jarray, sk_jarray); + return return_obj; +} +extern "C" +JNIEXPORT jbyteArray JNICALL +Java_network_loki_messenger_libsession_1util_util_Sodium_ed25519PkToCurve25519(JNIEnv *env, + jobject thiz, + jbyteArray pk) { + auto ed_pk = util::ustring_from_bytes(env, pk); + std::array curve_pk; // NOLINT(cppcoreguidelines-pro-type-member-init) + int success = crypto_sign_ed25519_pk_to_curve25519(curve_pk.data(), ed_pk.data()); + if (success != 0) { + jclass exception = env->FindClass("java/lang/Exception"); + env->ThrowNew(exception, "Invalid crypto_sign_ed25519_pk_to_curve25519 operation"); + return nullptr; + } + jbyteArray curve_pk_jarray = util::bytes_from_ustring(env, session::ustring_view {curve_pk.data(), curve_pk.size()}); + return curve_pk_jarray; +} +extern "C" +JNIEXPORT jobject JNICALL +Java_network_loki_messenger_libsession_1util_util_BaseCommunityInfo_00024Companion_parseFullUrl( + JNIEnv *env, jobject thiz, jstring full_url) { + auto bytes = env->GetStringUTFChars(full_url, nullptr); + auto [base, room, pk] = session::config::community::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" +JNIEXPORT jstring JNICALL +Java_network_loki_messenger_libsession_1util_util_BaseCommunityInfo_fullUrl(JNIEnv *env, + jobject thiz) { + auto deserialized = util::deserialize_base_community(env, thiz); + auto full_url = deserialized.full_url(); + return env->NewStringUTF(full_url.data()); +} \ No newline at end of file diff --git a/libsession-util/src/main/cpp/util.h b/libsession-util/src/main/cpp/util.h new file mode 100644 index 000000000..9348e8bd7 --- /dev/null +++ b/libsession-util/src/main/cpp/util.h @@ -0,0 +1,24 @@ +#ifndef SESSION_ANDROID_UTIL_H +#define SESSION_ANDROID_UTIL_H + +#include +#include +#include +#include "session/types.hpp" +#include "session/config/profile_pic.hpp" +#include "session/config/user_groups.hpp" +#include "session/config/expiring.hpp" + +namespace util { + extern std::mutex util_mutex_; + jbyteArray bytes_from_ustring(JNIEnv* env, session::ustring_view from_str); + session::ustring ustring_from_bytes(JNIEnv* env, jbyteArray byteArray); + jobject serialize_user_pic(JNIEnv *env, session::config::profile_pic pic); + std::pair deserialize_user_pic(JNIEnv *env, jobject user_pic); + jobject serialize_base_community(JNIEnv *env, const session::config::community& base_community); + session::config::community deserialize_base_community(JNIEnv *env, jobject base_community); + jobject serialize_expiry(JNIEnv *env, const session::config::expiration_mode& mode, const std::chrono::seconds& time_seconds); + std::pair deserialize_expiry(JNIEnv *env, jobject expiry_mode); +} + +#endif \ 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 new file mode 100644 index 000000000..52fb541d7 --- /dev/null +++ b/libsession-util/src/main/java/network/loki/messenger/libsession_util/Config.kt @@ -0,0 +1,200 @@ +package network.loki.messenger.libsession_util + +import network.loki.messenger.libsession_util.util.BaseCommunityInfo +import network.loki.messenger.libsession_util.util.ConfigPush +import network.loki.messenger.libsession_util.util.Contact +import network.loki.messenger.libsession_util.util.Conversation +import network.loki.messenger.libsession_util.util.GroupInfo +import network.loki.messenger.libsession_util.util.UserPic +import org.session.libsignal.protos.SignalServiceProtos.SharedConfigMessage.Kind +import org.session.libsignal.utilities.IdPrefix +import org.session.libsignal.utilities.Log + + +sealed class ConfigBase(protected val /* yucky */ pointer: Long) { + companion object { + init { + System.loadLibrary("session_util") + } + external fun kindFor(configNamespace: Int): Class + + fun ConfigBase.protoKindFor(): Kind = when (this) { + is UserProfile -> Kind.USER_PROFILE + is Contacts -> Kind.CONTACTS + is ConversationVolatileConfig -> Kind.CONVO_INFO_VOLATILE + is UserGroupsConfig -> Kind.GROUPS + } + + // TODO: time in future to activate (hardcoded to 1st jan 2024 for testing, change before release) + private const val ACTIVATE_TIME = 1690761600000 + + fun isNewConfigEnabled(forced: Boolean, currentTime: Long) = + forced || currentTime >= ACTIVATE_TIME + + const val PRIORITY_HIDDEN = -1 + const val PRIORITY_VISIBLE = 0 + const val PRIORITY_PINNED = 1 + + } + + external fun dirty(): Boolean + external fun needsPush(): Boolean + external fun needsDump(): Boolean + external fun push(): ConfigPush + external fun dump(): ByteArray + external fun encryptionDomain(): String + external fun confirmPushed(seqNo: Long, newHash: String) + external fun merge(toMerge: Array>): Int + external fun currentHashes(): List + + external fun configNamespace(): Int + + // Singular merge + external fun merge(toMerge: Pair): Int + + external fun free() + +} + +class Contacts(pointer: Long) : ConfigBase(pointer) { + companion object { + init { + System.loadLibrary("session_util") + } + external fun newInstance(ed25519SecretKey: ByteArray): Contacts + external fun newInstance(ed25519SecretKey: ByteArray, initialDump: ByteArray): Contacts + } + + external fun get(sessionId: String): Contact? + external fun getOrConstruct(sessionId: String): Contact + external fun all(): List + external fun set(contact: Contact) + external fun erase(sessionId: String): Boolean + + /** + * Similar to [updateIfExists], but will create the underlying contact if it doesn't exist before passing to [updateFunction] + */ + fun upsertContact(sessionId: String, updateFunction: Contact.()->Unit = {}) { + if (sessionId.startsWith(IdPrefix.BLINDED.value)) { + Log.w("Loki", "Trying to create a contact with a blinded ID prefix") + return + } else if (sessionId.startsWith(IdPrefix.UN_BLINDED.value)) { + Log.w("Loki", "Trying to create a contact with an un-blinded ID prefix") + return + } else if (sessionId.startsWith(IdPrefix.BLINDEDV2.value)) { + Log.w("Loki", "Trying to create a contact with a blindedv2 ID prefix") + return + } + val contact = getOrConstruct(sessionId) + updateFunction(contact) + set(contact) + } + + /** + * Updates the contact by sessionId with a given [updateFunction], and applies to the underlying config. + * the [updateFunction] doesn't run if there is no contact + */ + fun updateIfExists(sessionId: String, updateFunction: Contact.()->Unit) { + if (sessionId.startsWith(IdPrefix.BLINDED.value)) { + Log.w("Loki", "Trying to create a contact with a blinded ID prefix") + return + } else if (sessionId.startsWith(IdPrefix.UN_BLINDED.value)) { + Log.w("Loki", "Trying to create a contact with an un-blinded ID prefix") + return + } else if (sessionId.startsWith(IdPrefix.BLINDEDV2.value)) { + Log.w("Loki", "Trying to create a contact with a blindedv2 ID prefix") + return + } + val contact = get(sessionId) ?: return + updateFunction(contact) + set(contact) + } +} + +class UserProfile(pointer: Long) : ConfigBase(pointer) { + companion object { + init { + System.loadLibrary("session_util") + } + external fun newInstance(ed25519SecretKey: ByteArray): UserProfile + external fun newInstance(ed25519SecretKey: ByteArray, initialDump: ByteArray): UserProfile + } + + external fun setName(newName: String) + external fun getName(): String? + external fun getPic(): UserPic + external fun setPic(userPic: UserPic) + external fun setNtsPriority(priority: Int) + external fun getNtsPriority(): Int +} + +class ConversationVolatileConfig(pointer: Long): ConfigBase(pointer) { + companion object { + init { + System.loadLibrary("session_util") + } + external fun newInstance(ed25519SecretKey: ByteArray): ConversationVolatileConfig + external fun newInstance(ed25519SecretKey: ByteArray, initialDump: ByteArray): ConversationVolatileConfig + } + + external fun getOneToOne(pubKeyHex: String): Conversation.OneToOne? + external fun getOrConstructOneToOne(pubKeyHex: String): Conversation.OneToOne + external fun eraseOneToOne(pubKeyHex: String): Boolean + + external fun getCommunity(baseUrl: String, room: String): Conversation.Community? + external fun getOrConstructCommunity(baseUrl: String, room: String, pubKeyHex: String): Conversation.Community + external fun getOrConstructCommunity(baseUrl: String, room: String, pubKey: ByteArray): Conversation.Community + external fun eraseCommunity(community: Conversation.Community): Boolean + external fun eraseCommunity(baseUrl: String, room: String): Boolean + + external fun getLegacyClosedGroup(groupId: String): Conversation.LegacyGroup? + external fun getOrConstructLegacyGroup(groupId: String): Conversation.LegacyGroup + external fun eraseLegacyClosedGroup(groupId: String): Boolean + external fun erase(conversation: Conversation): Boolean + + external fun set(toStore: Conversation) + + /** + * Erase all conversations that do not satisfy the `predicate`, similar to [MutableList.removeAll] + */ + external fun eraseAll(predicate: (Conversation) -> Boolean): Int + + external fun sizeOneToOnes(): Int + external fun sizeCommunities(): Int + external fun sizeLegacyClosedGroups(): Int + external fun size(): Int + + external fun empty(): Boolean + + external fun allOneToOnes(): List + external fun allCommunities(): List + external fun allLegacyClosedGroups(): List + external fun all(): List + +} + +class UserGroupsConfig(pointer: Long): ConfigBase(pointer) { + companion object { + init { + System.loadLibrary("session_util") + } + external fun newInstance(ed25519SecretKey: ByteArray): UserGroupsConfig + external fun newInstance(ed25519SecretKey: ByteArray, initialDump: ByteArray): UserGroupsConfig + } + + external fun getCommunityInfo(baseUrl: String, room: String): GroupInfo.CommunityGroupInfo? + external fun getLegacyGroupInfo(sessionId: String): GroupInfo.LegacyGroupInfo? + external fun getOrConstructCommunityInfo(baseUrl: String, room: String, pubKeyHex: String): GroupInfo.CommunityGroupInfo + external fun getOrConstructLegacyGroupInfo(sessionId: String): GroupInfo.LegacyGroupInfo + external fun set(groupInfo: GroupInfo) + external fun erase(communityInfo: GroupInfo) + external fun eraseCommunity(baseCommunityInfo: BaseCommunityInfo): Boolean + external fun eraseCommunity(server: String, room: String): Boolean + external fun eraseLegacyGroup(sessionId: String): Boolean + external fun sizeCommunityInfo(): Int + external fun sizeLegacyGroupInfo(): Int + external fun size(): Int + external fun all(): List + external fun allCommunityInfo(): List + external fun allLegacyGroupInfo(): List +} \ No newline at end of file diff --git a/libsession-util/src/main/java/network/loki/messenger/libsession_util/util/BaseCommunity.kt b/libsession-util/src/main/java/network/loki/messenger/libsession_util/util/BaseCommunity.kt new file mode 100644 index 000000000..a48d082a6 --- /dev/null +++ b/libsession-util/src/main/java/network/loki/messenger/libsession_util/util/BaseCommunity.kt @@ -0,0 +1,11 @@ +package network.loki.messenger.libsession_util.util + +data class BaseCommunityInfo(val baseUrl: String, val room: String, val pubKeyHex: String) { + companion object { + init { + System.loadLibrary("session_util") + } + external fun parseFullUrl(fullUrl: String): Triple? + } + external fun fullUrl(): String +} \ No newline at end of file diff --git a/libsession-util/src/main/java/network/loki/messenger/libsession_util/util/Contact.kt b/libsession-util/src/main/java/network/loki/messenger/libsession_util/util/Contact.kt new file mode 100644 index 000000000..8cc22a6af --- /dev/null +++ b/libsession-util/src/main/java/network/loki/messenger/libsession_util/util/Contact.kt @@ -0,0 +1,13 @@ +package network.loki.messenger.libsession_util.util + +data class Contact( + val id: String, + var name: String = "", + var nickname: String = "", + var approved: Boolean = false, + var approvedMe: Boolean = false, + var blocked: Boolean = false, + var profilePicture: UserPic = UserPic.DEFAULT, + var priority: Int = 0, + var expiryMode: ExpiryMode, +) \ No newline at end of file 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 new file mode 100644 index 000000000..97930e8b4 --- /dev/null +++ b/libsession-util/src/main/java/network/loki/messenger/libsession_util/util/Conversation.kt @@ -0,0 +1,25 @@ +package network.loki.messenger.libsession_util.util + +sealed class Conversation { + + abstract var lastRead: Long + abstract var unread: Boolean + + data class OneToOne( + val sessionId: String, + override var lastRead: Long, + override var unread: Boolean + ): Conversation() + + data class Community( + val baseCommunityInfo: BaseCommunityInfo, + override var lastRead: Long, + override var unread: Boolean + ) : Conversation() + + data class LegacyGroup( + val groupId: String, + override var lastRead: Long, + override var unread: Boolean + ): Conversation() +} \ No newline at end of file diff --git a/libsession-util/src/main/java/network/loki/messenger/libsession_util/util/ExpiryMode.kt b/libsession-util/src/main/java/network/loki/messenger/libsession_util/util/ExpiryMode.kt new file mode 100644 index 000000000..58e98a439 --- /dev/null +++ b/libsession-util/src/main/java/network/loki/messenger/libsession_util/util/ExpiryMode.kt @@ -0,0 +1,7 @@ +package network.loki.messenger.libsession_util.util + +sealed class ExpiryMode(val expirySeconds: Long) { + object NONE: ExpiryMode(0) + class AfterSend(seconds: Long): ExpiryMode(seconds) + class AfterRead(seconds: Long): ExpiryMode(seconds) +} \ No newline at end of file diff --git a/libsession-util/src/main/java/network/loki/messenger/libsession_util/util/GroupInfo.kt b/libsession-util/src/main/java/network/loki/messenger/libsession_util/util/GroupInfo.kt new file mode 100644 index 000000000..c8ace0a9a --- /dev/null +++ b/libsession-util/src/main/java/network/loki/messenger/libsession_util/util/GroupInfo.kt @@ -0,0 +1,53 @@ +package network.loki.messenger.libsession_util.util + +sealed class GroupInfo { + + data class CommunityGroupInfo(val community: BaseCommunityInfo, val priority: Int) : GroupInfo() + + data class LegacyGroupInfo( + val sessionId: String, + val name: String, + val members: Map, + val encPubKey: ByteArray, + val encSecKey: ByteArray, + val priority: Int, + val disappearingTimer: Long, + val joinedAt: Long + ): GroupInfo() { + companion object { + @Suppress("FunctionName") + external fun NAME_MAX_LENGTH(): Int + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as LegacyGroupInfo + + if (sessionId != other.sessionId) return false + if (name != other.name) return false + if (members != other.members) return false + if (!encPubKey.contentEquals(other.encPubKey)) return false + if (!encSecKey.contentEquals(other.encSecKey)) return false + if (priority != other.priority) return false + if (disappearingTimer != other.disappearingTimer) return false + if (joinedAt != other.joinedAt) return false + + return true + } + + override fun hashCode(): Int { + var result = sessionId.hashCode() + result = 31 * result + name.hashCode() + result = 31 * result + members.hashCode() + result = 31 * result + encPubKey.contentHashCode() + result = 31 * result + encSecKey.contentHashCode() + result = 31 * result + priority + result = 31 * result + disappearingTimer.hashCode() + result = 31 * result + joinedAt.hashCode() + return result + } + } + +} \ No newline at end of file diff --git a/libsession-util/src/main/java/network/loki/messenger/libsession_util/util/Sodium.kt b/libsession-util/src/main/java/network/loki/messenger/libsession_util/util/Sodium.kt new file mode 100644 index 000000000..6168bd216 --- /dev/null +++ b/libsession-util/src/main/java/network/loki/messenger/libsession_util/util/Sodium.kt @@ -0,0 +1,9 @@ +package network.loki.messenger.libsession_util.util + +object Sodium { + init { + System.loadLibrary("session_util") + } + external fun ed25519KeyPair(seed: ByteArray): KeyPair + external fun ed25519PkToCurve25519(pk: ByteArray): ByteArray +} \ No newline at end of file diff --git a/libsession-util/src/main/java/network/loki/messenger/libsession_util/util/Utils.kt b/libsession-util/src/main/java/network/loki/messenger/libsession_util/util/Utils.kt new file mode 100644 index 000000000..4222395b5 --- /dev/null +++ b/libsession-util/src/main/java/network/loki/messenger/libsession_util/util/Utils.kt @@ -0,0 +1,67 @@ +package network.loki.messenger.libsession_util.util + +data class ConfigPush(val config: ByteArray, val seqNo: Long, val obsoleteHashes: List) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as ConfigPush + + if (!config.contentEquals(other.config)) return false + if (seqNo != other.seqNo) return false + if (obsoleteHashes != other.obsoleteHashes) return false + + return true + } + + override fun hashCode(): Int { + var result = config.contentHashCode() + result = 31 * result + seqNo.hashCode() + result = 31 * result + obsoleteHashes.hashCode() + return result + } + +} + +data class UserPic(val url: String, val key: ByteArray) { + companion object { + val DEFAULT = UserPic("", byteArrayOf()) + } + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as UserPic + + if (url != other.url) return false + if (!key.contentEquals(other.key)) return false + + return true + } + + override fun hashCode(): Int { + var result = url.hashCode() + result = 31 * result + key.contentHashCode() + return result + } +} + +data class KeyPair(val pubKey: ByteArray, val secretKey: ByteArray) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as KeyPair + + if (!pubKey.contentEquals(other.pubKey)) return false + if (!secretKey.contentEquals(other.secretKey)) return false + + return true + } + + override fun hashCode(): Int { + var result = pubKey.contentHashCode() + result = 31 * result + secretKey.contentHashCode() + return result + } +} \ No newline at end of file diff --git a/libsession-util/src/test/java/network/loki/messenger/libsession_util/ExampleUnitTest.kt b/libsession-util/src/test/java/network/loki/messenger/libsession_util/ExampleUnitTest.kt new file mode 100644 index 000000000..3d156bfd4 --- /dev/null +++ b/libsession-util/src/test/java/network/loki/messenger/libsession_util/ExampleUnitTest.kt @@ -0,0 +1,14 @@ +package network.loki.messenger.libsession_util + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + +} \ No newline at end of file diff --git a/libsession/build.gradle b/libsession/build.gradle index dd8959958..045648e09 100644 --- a/libsession/build.gradle +++ b/libsession/build.gradle @@ -18,6 +18,7 @@ android { dependencies { implementation project(":libsignal") + implementation project(":libsession-util") implementation project(":liblazysodium") implementation "net.java.dev.jna:jna:5.8.0@aar" implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion" @@ -34,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" @@ -46,10 +46,6 @@ dependencies { testImplementation 'org.assertj:assertj-core:3.11.1' testImplementation "org.mockito:mockito-inline:4.0.0" testImplementation "org.mockito.kotlin:mockito-kotlin:$mockitoKotlinVersion" - testImplementation 'org.powermock:powermock-api-mockito:1.6.1' - testImplementation 'org.powermock:powermock-module-junit4:1.6.1' - testImplementation 'org.powermock:powermock-module-junit4-rule:1.6.1' - testImplementation 'org.powermock:powermock-classloading-xstream:1.6.1' testImplementation "androidx.test:core:$testCoreVersion" testImplementation "androidx.arch.core:core-testing:2.1.0" testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion" diff --git a/libsession/src/main/java/org/session/libsession/avatars/ResourceContactPhoto.java b/libsession/src/main/java/org/session/libsession/avatars/ResourceContactPhoto.java index a448b3f7a..f78089e25 100644 --- a/libsession/src/main/java/org/session/libsession/avatars/ResourceContactPhoto.java +++ b/libsession/src/main/java/org/session/libsession/avatars/ResourceContactPhoto.java @@ -33,7 +33,7 @@ public class ResourceContactPhoto implements FallbackContactPhoto { Drawable background = TextDrawable.builder().buildRound(" ", inverted ? Color.WHITE : color); RoundedDrawable foreground = (RoundedDrawable) RoundedDrawable.fromDrawable(context.getResources().getDrawable(resourceId)); - foreground.setScaleType(ImageView.ScaleType.CENTER_INSIDE); + foreground.setScaleType(ImageView.ScaleType.CENTER_CROP); if (inverted) { foreground.setColorFilter(color, PorterDuff.Mode.SRC_ATOP); diff --git a/libsession/src/main/java/org/session/libsession/database/StorageProtocol.kt b/libsession/src/main/java/org/session/libsession/database/StorageProtocol.kt index 9a8820247..4fff83383 100644 --- a/libsession/src/main/java/org/session/libsession/database/StorageProtocol.kt +++ b/libsession/src/main/java/org/session/libsession/database/StorageProtocol.kt @@ -2,12 +2,14 @@ package org.session.libsession.database import android.content.Context import android.net.Uri +import network.loki.messenger.libsession_util.ConfigBase import org.session.libsession.messaging.BlindedIdMapping import org.session.libsession.messaging.calls.CallMessageType 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 @@ -30,6 +32,7 @@ import org.session.libsession.utilities.recipients.Recipient.RecipientSettings import org.session.libsignal.crypto.ecc.ECKeyPair import org.session.libsignal.messages.SignalServiceAttachmentPointer import org.session.libsignal.messages.SignalServiceGroup +import network.loki.messenger.libsession_util.util.Contact as LibSessionContact interface StorageProtocol { @@ -38,6 +41,9 @@ interface StorageProtocol { fun getUserX25519KeyPair(): ECKeyPair fun getUserProfile(): Profile fun setProfileAvatar(recipient: Recipient, profileAvatar: String?) + fun setProfilePicture(recipient: Recipient, newProfilePicture: String?, newProfileKey: ByteArray?) + fun setUserProfilePicture(newProfilePicture: String?, newProfileKey: ByteArray?) + fun clearUserPic() // Signal fun getOrGenerateRegistrationID(): Int @@ -50,8 +56,10 @@ interface StorageProtocol { fun getMessageSendJob(messageSendJobID: String): MessageSendJob? fun getMessageReceiveJob(messageReceiveJobID: String): Job? fun getGroupAvatarDownloadJob(server: String, room: String, imageId: String?): Job? + fun getConfigSyncJob(destination: Destination): Job? fun resumeMessageSendJobIfNeeded(messageSendJobID: String) fun isJobCanceled(job: Job): Boolean + fun cancelPendingMessageSendJobs(threadID: Long) // Authorization fun getAuthToken(room: String, server: String): String? @@ -67,7 +75,7 @@ interface StorageProtocol { fun updateOpenGroup(openGroup: OpenGroup) fun getOpenGroup(threadId: Long): OpenGroup? fun addOpenGroup(urlAsString: String): OpenGroupApi.RoomInfo? - fun onOpenGroupAdded(server: String) + fun onOpenGroupAdded(server: String, room: String) fun hasBackgroundGroupAddJob(groupJoinUrl: String): Boolean fun setOpenGroupServerMessageID(messageID: Long, serverID: Long, threadID: Long, isSms: Boolean) fun getOpenGroup(room: String, server: String): OpenGroup? @@ -119,6 +127,8 @@ interface StorageProtocol { // Closed Groups fun getGroup(groupID: String): GroupRecord? fun createGroup(groupID: String, title: String?, members: List
, avatar: SignalServiceAttachmentPointer?, relay: String?, admins: List
, formationTimestamp: Long) + fun createInitialConfigGroup(groupPublicKey: String, name: String, members: Map, formationTimestamp: Long, encryptionKeyPair: ECKeyPair) + fun updateGroupConfig(groupPublicKey: String) fun isGroupActive(groupPublicKey: String): Boolean fun setActive(groupID: String, value: Boolean) fun getZombieMembers(groupID: String): Set @@ -129,7 +139,7 @@ interface StorageProtocol { fun getAllActiveClosedGroupPublicKeys(): Set fun addClosedGroupPublicKey(groupPublicKey: String) fun removeClosedGroupPublicKey(groupPublicKey: String) - fun addClosedGroupEncryptionKeyPair(encryptionKeyPair: ECKeyPair, groupPublicKey: String) + fun addClosedGroupEncryptionKeyPair(encryptionKeyPair: ECKeyPair, groupPublicKey: String, timestamp: Long) fun removeAllClosedGroupEncryptionKeyPairs(groupPublicKey: String) fun insertIncomingInfoMessage(context: Context, senderPublicKey: String, groupID: String, type: SignalServiceGroup.Type, name: String, members: Collection, admins: Collection, sentTimestamp: Long) @@ -140,18 +150,20 @@ interface StorageProtocol { fun getLatestClosedGroupEncryptionKeyPair(groupPublicKey: String): ECKeyPair? fun updateFormationTimestamp(groupID: String, formationTimestamp: Long) fun updateTimestampUpdated(groupID: String, updatedTimestamp: Long) - fun setExpirationTimer(groupID: String, duration: Int) + fun setExpirationTimer(address: String, duration: Int) // Groups - fun getAllGroups(): List + fun getAllGroups(includeInactive: Boolean): List // Settings fun setProfileSharing(address: Address, value: Boolean) + // Thread fun getOrCreateThreadIdFor(address: Address): Long - fun getOrCreateThreadIdFor(publicKey: String, groupPublicKey: String?, openGroupID: String?): Long + fun getThreadIdFor(publicKey: String, groupPublicKey: String?, openGroupID: String?, createThread: Boolean): Long? fun getThreadId(publicKeyOrOpenGroupID: String): Long? + fun getThreadId(openGroup: OpenGroup): Long? fun getThreadId(address: Address): Long? fun getThreadId(recipient: Recipient): Long? fun getThreadIdForMms(mmsId: Long): Long @@ -159,7 +171,10 @@ interface StorageProtocol { fun trimThread(threadID: Long, threadLimit: Int) fun trimThreadBefore(threadID: Long, timestamp: Long) fun getMessageCount(threadID: Long): Long - fun deleteConversation(threadId: Long) + fun setPinned(threadID: Long, isPinned: Boolean) + fun isPinned(threadID: Long): Boolean + fun deleteConversation(threadID: Long) + fun setThreadDate(threadId: Long, newDate: Long) // Contacts fun getContactWithSessionID(sessionID: String): Contact? @@ -167,6 +182,7 @@ interface StorageProtocol { fun setContact(contact: Contact) fun getRecipientForThread(threadId: Long): Recipient? fun getRecipientSettings(address: Address): RecipientSettings? + fun addLibSessionContacts(contacts: List) fun addContacts(contacts: List) // Attachments @@ -177,13 +193,14 @@ interface StorageProtocol { /** * Returns the ID of the `TSIncomingMessage` that was constructed. */ - fun persist(message: VisibleMessage, quotes: QuoteModel?, linkPreview: List, groupPublicKey: String?, openGroupID: String?, attachments: List, runIncrement: Boolean, runThreadUpdate: Boolean): Long? - fun markConversationAsRead(threadId: Long, updateLastSeen: Boolean) - fun incrementUnread(threadId: Long, amount: Int, unreadMentionAmount: Int) + fun persist(message: VisibleMessage, quotes: QuoteModel?, linkPreview: List, groupPublicKey: String?, openGroupID: String?, attachments: List, runThreadUpdate: Boolean): Long? + fun markConversationAsRead(threadId: Long, lastSeenTime: Long, force: Boolean = false) + fun getLastSeen(threadId: Long): Long fun updateThread(threadId: Long, unarchive: Boolean) fun insertDataExtractionNotificationMessage(senderPublicKey: String, message: DataExtractionNotificationInfoMessage, sentTimestamp: Long) fun insertMessageRequestResponse(response: MessageRequestResponse) fun setRecipientApproved(recipient: Recipient, approved: Boolean) + fun getRecipientApproved(address: Address): Boolean fun setRecipientApprovedMe(recipient: Recipient, approvedMe: Boolean) fun insertCallMessage(senderPublicKey: String, callMessageType: CallMessageType, sentTimestamp: Long) fun conversationHasOutgoing(userPublicKey: String): Boolean @@ -203,6 +220,12 @@ interface StorageProtocol { fun removeReaction(emoji: String, messageTimestamp: Long, author: String, notifyUnread: Boolean) fun updateReactionIfNeeded(message: Message, sender: String, openGroupSentTimestamp: Long) fun deleteReactions(messageId: Long, mms: Boolean) - fun unblock(toUnblock: Iterable) + fun setBlocked(recipients: Iterable, isBlocked: Boolean, fromConfigUpdate: Boolean = false) + fun setRecipientHash(recipient: Recipient, recipientHash: String?) fun blockedContacts(): List + + // Shared configs + fun notifyConfigUpdates(forConfigObject: ConfigBase) + fun conversationInConfig(publicKey: String?, groupPublicKey: String?, openGroupId: String?, visibleOnly: Boolean): Boolean + fun canPerformConfigChange(variant: String, publicKey: String, changeTimestampMs: Long): Boolean } diff --git a/libsession/src/main/java/org/session/libsession/messaging/MessagingModuleConfiguration.kt b/libsession/src/main/java/org/session/libsession/messaging/MessagingModuleConfiguration.kt index 37c391dfd..043719677 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/MessagingModuleConfiguration.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/MessagingModuleConfiguration.kt @@ -4,12 +4,14 @@ import android.content.Context import com.goterl.lazysodium.utils.KeyPair import org.session.libsession.database.MessageDataProvider import org.session.libsession.database.StorageProtocol +import org.session.libsession.utilities.ConfigFactoryProtocol class MessagingModuleConfiguration( val context: Context, val storage: StorageProtocol, val messageDataProvider: MessageDataProvider, - val getUserED25519KeyPair: ()-> KeyPair? + val getUserED25519KeyPair: () -> KeyPair?, + val configFactory: ConfigFactoryProtocol ) { companion object { diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/AttachmentDownloadJob.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/AttachmentDownloadJob.kt index ef1d7567b..b9eaf8d50 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/AttachmentDownloadJob.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/AttachmentDownloadJob.kt @@ -42,7 +42,7 @@ class AttachmentDownloadJob(val attachmentID: Long, val databaseMessageID: Long) private val TS_INCOMING_MESSAGE_ID_KEY = "tsIncoming_message_id" } - override fun execute(dispatcherName: String) { + override suspend fun execute(dispatcherName: String) { val storage = MessagingModuleConfiguration.shared.storage val messageDataProvider = MessagingModuleConfiguration.shared.messageDataProvider val threadID = storage.getThreadIdForMms(databaseMessageID) diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/AttachmentUploadJob.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/AttachmentUploadJob.kt index cd4189a65..19b6555b5 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/AttachmentUploadJob.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/AttachmentUploadJob.kt @@ -16,7 +16,11 @@ import org.session.libsession.utilities.DecodedAudio import org.session.libsession.utilities.InputStreamMediaDataSource import org.session.libsession.utilities.UploadResult import org.session.libsignal.messages.SignalServiceAttachmentStream -import org.session.libsignal.streams.* +import org.session.libsignal.streams.AttachmentCipherOutputStream +import org.session.libsignal.streams.AttachmentCipherOutputStreamFactory +import org.session.libsignal.streams.DigestingRequestBody +import org.session.libsignal.streams.PaddingInputStream +import org.session.libsignal.streams.PlaintextOutputStreamFactory import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.PushAttachmentData import org.session.libsignal.utilities.Util @@ -45,7 +49,7 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess private val MESSAGE_SEND_JOB_ID_KEY = "message_send_job_id" } - override fun execute(dispatcherName: String) { + override suspend fun execute(dispatcherName: String) { try { val storage = MessagingModuleConfiguration.shared.storage val messageDataProvider = MessagingModuleConfiguration.shared.messageDataProvider diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/BackgroundGroupAddJob.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/BackgroundGroupAddJob.kt index c5ec1bc74..20442e559 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/BackgroundGroupAddJob.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/BackgroundGroupAddJob.kt @@ -3,9 +3,7 @@ package org.session.libsession.messaging.jobs import okhttp3.HttpUrl import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.open_groups.OpenGroup -import org.session.libsession.messaging.open_groups.OpenGroupApi import org.session.libsession.messaging.utilities.Data -import org.session.libsession.utilities.GroupUtil import org.session.libsession.utilities.OpenGroupUrlParser import org.session.libsignal.utilities.Log @@ -29,7 +27,7 @@ class BackgroundGroupAddJob(val joinUrl: String): Job { return "$server.$room" } - override fun execute(dispatcherName: String) { + override suspend fun execute(dispatcherName: String) { try { val openGroup = OpenGroupUrlParser.parseUrl(joinUrl) val storage = MessagingModuleConfiguration.shared.storage @@ -40,8 +38,7 @@ class BackgroundGroupAddJob(val joinUrl: String): Job { return } storage.addOpenGroup(openGroup.joinUrl()) - Log.d(KEY, "onOpenGroupAdded(${openGroup.server})") - storage.onOpenGroupAdded(openGroup.server) + storage.onOpenGroupAdded(openGroup.server, openGroup.room) } catch (e: Exception) { Log.e("OpenGroupDispatcher", "Failed to add group because",e) delegate?.handleJobFailed(this, dispatcherName, e) diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/BatchMessageReceiveJob.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/BatchMessageReceiveJob.kt index fa07a7d9c..3aea8a1e3 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/BatchMessageReceiveJob.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/BatchMessageReceiveJob.kt @@ -7,15 +7,26 @@ import kotlinx.coroutines.awaitAll import kotlinx.coroutines.runBlocking import nl.komponents.kovenant.Promise import nl.komponents.kovenant.task -import org.session.libsession.database.StorageProtocol import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.messages.Message +import org.session.libsession.messaging.messages.control.CallMessage +import org.session.libsession.messaging.messages.control.ClosedGroupControlMessage +import org.session.libsession.messaging.messages.control.ConfigurationMessage +import org.session.libsession.messaging.messages.control.DataExtractionNotification 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.ParsedMessage import org.session.libsession.messaging.messages.visible.VisibleMessage import org.session.libsession.messaging.open_groups.OpenGroupApi -import org.session.libsession.messaging.sending_receiving.* +import org.session.libsession.messaging.sending_receiving.MessageReceiver +import org.session.libsession.messaging.sending_receiving.handle +import org.session.libsession.messaging.sending_receiving.handleOpenGroupReactions +import org.session.libsession.messaging.sending_receiving.handleUnsendRequest +import org.session.libsession.messaging.sending_receiving.handleVisibleMessage import org.session.libsession.messaging.utilities.Data import org.session.libsession.messaging.utilities.SessionId import org.session.libsession.messaging.utilities.SodiumUtilities @@ -49,6 +60,9 @@ class BatchMessageReceiveJob( const val BATCH_DEFAULT_NUMBER = 512 + // used for processing messages that don't have a thread and shouldn't create one + const val NO_THREAD_MAPPING = -1L + // Keys used for database storage private val NUM_MESSAGES_KEY = "numMessages" private val DATA_KEY = "data" @@ -57,16 +71,27 @@ class BatchMessageReceiveJob( private val OPEN_GROUP_ID_KEY = "open_group_id" } - private fun getThreadId(message: Message, storage: StorageProtocol): Long { - val senderOrSync = when (message) { - is VisibleMessage -> message.syncTarget ?: message.sender!! - is ExpirationTimerUpdate -> message.syncTarget ?: message.sender!! - else -> message.sender!! + private fun shouldCreateThread(parsedMessage: ParsedMessage): Boolean { + val message = parsedMessage.message + if (message is VisibleMessage) return true + else { // message is control message otherwise + return when(message) { + is SharedConfigurationMessage -> false + is ClosedGroupControlMessage -> false // message.kind is ClosedGroupControlMessage.Kind.New && !message.isSenderSelf + is DataExtractionNotification -> false + is MessageRequestResponse -> false + is ExpirationTimerUpdate -> false + is ConfigurationMessage -> false + is TypingIndicator -> false + is UnsendRequest -> false + is ReadReceipt -> false + is CallMessage -> false // TODO: maybe + else -> false // shouldn't happen, or I guess would be Visible + } } - return storage.getOrCreateThreadIdFor(senderOrSync, message.groupPublicKey, openGroupID) } - override fun execute(dispatcherName: String) { + override suspend fun execute(dispatcherName: String) { executeAsync(dispatcherName).get() } @@ -77,15 +102,16 @@ class BatchMessageReceiveJob( val context = MessagingModuleConfiguration.shared.context val localUserPublicKey = storage.getUserPublicKey() val serverPublicKey = openGroupID?.let { storage.getOpenGroupPublicKey(it.split(".").dropLast(1).joinToString(".")) } + val currentClosedGroups = storage.getAllActiveClosedGroupPublicKeys() // parse and collect IDs messages.forEach { messageParameters -> val (data, serverHash, openGroupMessageServerID) = messageParameters try { - val (message, proto) = MessageReceiver.parse(data, openGroupMessageServerID, openGroupPublicKey = serverPublicKey) + val (message, proto) = MessageReceiver.parse(data, openGroupMessageServerID, openGroupPublicKey = serverPublicKey, currentClosedGroups = currentClosedGroups) message.serverHash = serverHash - val threadID = getThreadId(message, storage) val parsedParams = ParsedMessage(messageParameters, message, proto) + val threadID = Message.getThreadId(message, openGroupID, storage, shouldCreateThread(parsedParams)) ?: NO_THREAD_MAPPING if (!threadMap.containsKey(threadID)) { threadMap[threadID] = mutableListOf(parsedParams) } else { @@ -115,77 +141,101 @@ class BatchMessageReceiveJob( // iterate over threads and persist them (persistence is the longest constant in the batch process operation) runBlocking(Dispatchers.IO) { - val deferredThreadMap = threadMap.entries.map { (threadId, messages) -> - async { - // The LinkedHashMap should preserve insertion order - val messageIds = linkedMapOf>() - messages.forEach { (parameters, message, proto) -> - try { - when (message) { - is VisibleMessage -> { - val messageId = MessageReceiver.handleVisibleMessage(message, proto, openGroupID, - runIncrement = false, - runThreadUpdate = false, - runProfileUpdate = true - ) - - if (messageId != null && message.reaction == null) { - val isUserBlindedSender = message.sender == serverPublicKey?.let { SodiumUtilities.blindedKeyPair(it, MessagingModuleConfiguration.shared.getUserED25519KeyPair()!!) }?.let { SessionId( - IdPrefix.BLINDED, it.publicKey.asBytes).hexString } - messageIds[messageId] = Pair( - (message.sender == localUserPublicKey || isUserBlindedSender), - message.hasMention + fun processMessages(threadId: Long, messages: List) = async { + // The LinkedHashMap should preserve insertion order + val messageIds = linkedMapOf>() + val myLastSeen = storage.getLastSeen(threadId) + var newLastSeen = if (myLastSeen == -1L) 0 else myLastSeen + messages.forEach { (parameters, message, proto) -> + try { + when (message) { + is VisibleMessage -> { + val isUserBlindedSender = + message.sender == serverPublicKey?.let { + SodiumUtilities.blindedKeyPair( + it, + MessagingModuleConfiguration.shared.getUserED25519KeyPair()!! ) + }?.let { + SessionId( + IdPrefix.BLINDED, it.publicKey.asBytes + ).hexString } - parameters.openGroupMessageServerID?.let { - MessageReceiver.handleOpenGroupReactions(threadId, it, parameters.reactions) + val sentTimestamp = message.sentTimestamp!! + if (message.sender == localUserPublicKey || isUserBlindedSender) { + if (sentTimestamp > newLastSeen) { + newLastSeen = + sentTimestamp // use sent timestamp here since that is technically the last one we have } } + val messageId = MessageReceiver.handleVisibleMessage( + message, proto, openGroupID, threadId, + runThreadUpdate = false, + runProfileUpdate = true + ) - is UnsendRequest -> { - val deletedMessageId = MessageReceiver.handleUnsendRequest(message) - - // If we removed a message then ensure it isn't in the 'messageIds' - if (deletedMessageId != null) { - messageIds.remove(deletedMessageId) - } + if (messageId != null && message.reaction == null) { + messageIds[messageId] = Pair( + (message.sender == localUserPublicKey || isUserBlindedSender), + message.hasMention + ) } + parameters.openGroupMessageServerID?.let { + MessageReceiver.handleOpenGroupReactions( + threadId, + it, + parameters.reactions + ) + } + } - else -> MessageReceiver.handle(message, proto, openGroupID) - } - } catch (e: Exception) { - Log.e(TAG, "Couldn't process message (id: $id)", e) - if (e is MessageReceiver.Error && !e.isRetryable) { - Log.e(TAG, "Message failed permanently (id: $id)", e) - } else { - Log.e(TAG, "Message failed (id: $id)", e) - failures += parameters + is UnsendRequest -> { + val deletedMessageId = + MessageReceiver.handleUnsendRequest(message) + + // If we removed a message then ensure it isn't in the 'messageIds' + if (deletedMessageId != null) { + messageIds.remove(deletedMessageId) + } } + + else -> MessageReceiver.handle(message, proto, threadId, openGroupID) + } + } catch (e: Exception) { + Log.e(TAG, "Couldn't process message (id: $id)", e) + if (e is MessageReceiver.Error && !e.isRetryable) { + Log.e(TAG, "Message failed permanently (id: $id)", e) + } else { + Log.e(TAG, "Message failed (id: $id)", e) + failures += parameters } } - // increment unreads, notify, and update thread - val unreadFromMine = messageIds.map { it.value.first }.indexOfLast { it } - var trueUnreadCount = messageIds.filter { !it.value.first }.size - var trueUnreadMentionCount = messageIds.filter { !it.value.first && it.value.second }.size - if (unreadFromMine >= 0) { - storage.markConversationAsRead(threadId, false) - - val trueUnreadIds = messageIds.keys.toList().subList(unreadFromMine + 1, messageIds.keys.count()) - trueUnreadCount = trueUnreadIds.size - trueUnreadMentionCount = messageIds - .filter { trueUnreadIds.contains(it.key) && !it.value.first && it.value.second } - .size - } - if (trueUnreadCount > 0) { - storage.incrementUnread(threadId, trueUnreadCount, trueUnreadMentionCount) - } - storage.updateThread(threadId, true) - SSKEnvironment.shared.notificationManager.updateNotification(context, threadId) } + // increment unreads, notify, and update thread + // last seen will be the current last seen if not changed (re-computes the read counts for thread record) + // might have been updated from a different thread at this point + val currentLastSeen = storage.getLastSeen(threadId).let { if (it == -1L) 0 else it } + if (currentLastSeen > newLastSeen) { + newLastSeen = currentLastSeen + } + if (newLastSeen > 0 || currentLastSeen == 0L) { + storage.markConversationAsRead(threadId, newLastSeen, force = true) + } + storage.updateThread(threadId, true) + SSKEnvironment.shared.notificationManager.updateNotification(context, threadId) + } + + val withoutDefault = threadMap.entries.filter { it.key != NO_THREAD_MAPPING } + val noThreadMessages = threadMap[NO_THREAD_MAPPING] ?: listOf() + val deferredThreadMap = withoutDefault.map { (threadId, messages) -> + processMessages(threadId, messages) } // await all thread processing deferredThreadMap.awaitAll() + if (noThreadMessages.isNotEmpty()) { + processMessages(NO_THREAD_MAPPING, noThreadMessages).await() + } } if (failures.isEmpty()) { handleSuccess(dispatcherName) diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/ConfigurationSyncJob.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/ConfigurationSyncJob.kt new file mode 100644 index 000000000..ec8de4416 --- /dev/null +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/ConfigurationSyncJob.kt @@ -0,0 +1,206 @@ +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.libsession.utilities.TextSecurePreferences +import org.session.libsignal.utilities.Log +import java.util.concurrent.atomic.AtomicBoolean + +// only contact (self) and closed group destinations will be supported +data class ConfigurationSyncJob(val destination: Destination): Job { + + override var delegate: JobDelegate? = null + override var id: String? = null + override var failureCount: Int = 0 + override val maxFailureCount: Int = 10 + + val shouldRunAgain = AtomicBoolean(false) + + override suspend fun execute(dispatcherName: String) { + val storage = MessagingModuleConfiguration.shared.storage + val forcedConfig = TextSecurePreferences.hasForcedNewConfig(MessagingModuleConfiguration.shared.context) + val currentTime = SnodeAPI.nowWithOffset + val userEdKeyPair = MessagingModuleConfiguration.shared.getUserED25519KeyPair() + val userPublicKey = 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(forcedConfig, currentTime) + // 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) + ) { + Log.w(TAG, "No need to run config sync job, TODO") + return delegate?.handleJobSucceeded(this, dispatcherName) ?: Unit + } + + // 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 = configFactory.getUserConfigs().filter { config -> config.needsPush() } + + // don't run anything if we don't need to push anything + if (configsRequiringPush.isEmpty()) return delegate.handleJobSucceeded(this, dispatcherName) + + // need to get the current hashes before we call `push()` + val toDeleteHashes = mutableListOf() + + // allow null results here so the list index matches configsRequiringPush + val sentTimestamp: Long = SnodeAPI.nowWithOffset + val batchObjects: List?> = configsRequiringPush.map { config -> + val (data, seqNo, obsoleteHashes) = config.push() + toDeleteHashes += obsoleteHashes + 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 = toDeleteHashes.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, dispatcherName, NullPointerException("One or more requests had a null batch request info")) + } + + val allRequests = mutableListOf() + allRequests += batchObjects.requireNoNulls().map { (_, request) -> request } + // add in the deletion if we have any hashes + if (toDeleteRequest != null) { + allRequests += toDeleteRequest + Log.d(TAG, "Including delete request for current hashes") + } + + 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) + // we are always adding in deletions at the end + val deletionResponse = if (toDeleteRequest != null && responseList.isNotEmpty()) responseList.last() else null + val deletedHashes = deletionResponse?.let { + @Suppress("UNCHECKED_CAST") + // get the sub-request body + (deletionResponse["body"] as? RawResponse)?.let { body -> + // get the swarm dict + body["swarm"] as? RawResponse + }?.mapValues { (_, swarmDict) -> + // get the deleted values from dict + ((swarmDict as? RawResponse)?.get("deleted") as? List)?.toSet() ?: emptySet() + }?.values?.reduce { acc, strings -> + // create an intersection of all deleted hashes (common between all swarm nodes) + acc intersect strings + } + } ?: 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 responseBody = response["body"] as? RawResponse + val insertHash = responseBody?.get("hash") as? String ?: run { + Log.w(TAG, "No hash returned for the configuration in namespace ${config.configNamespace()}") + return@forEachIndexed + } + Log.d(TAG, "Hash ${insertHash.take(4)} returned from store request for new config") + + // confirm pushed seqno + val thisSeqNo = toPushMessage.seqNo + config.confirmPushed(thisSeqNo, insertHash) + Log.d(TAG, "Successfully removed the deleted hashes from ${config.javaClass.simpleName}") + // dump and write config after successful + if (config.needsDump()) { // usually this will be true? + configFactory.persist(config, toPushMessage.sentTimestamp ?: sentTimestamp) + } + } + } catch (e: Exception) { + Log.e(TAG, "Error performing batch request", e) + return delegate.handleJobFailed(this, dispatcherName, e) + } + delegate.handleJobSucceeded(this, dispatcherName) + if (shouldRunAgain.get() && storage.getConfigSyncJob(destination) == null) { + // reschedule if something has updated since we started this job + JobQueue.shared.add(ConfigurationSyncJob(destination)) + } + } + + 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 { + val (type, address) = when (destination) { + is Destination.Contact -> CONTACT_TYPE to destination.publicKey + is Destination.ClosedGroup -> GROUP_TYPE to destination.groupPublicKey + else -> return Data.EMPTY + } + return Data.Builder() + .putInt(DESTINATION_TYPE_KEY, type) + .putString(DESTINATION_ADDRESS_KEY, address) + .build() + } + + override fun getFactoryKey(): String = KEY + + companion object { + const val TAG = "ConfigSyncJob" + const val KEY = "ConfigSyncJob" + + // Keys used for DB storage + const val DESTINATION_ADDRESS_KEY = "destinationAddress" + const val DESTINATION_TYPE_KEY = "destinationType" + + // type mappings + const val CONTACT_TYPE = 1 + const val GROUP_TYPE = 2 + + } + + class Factory: Job.Factory { + override fun create(data: Data): ConfigurationSyncJob? { + if (!data.hasInt(DESTINATION_TYPE_KEY) || !data.hasString(DESTINATION_ADDRESS_KEY)) return null + + val address = data.getString(DESTINATION_ADDRESS_KEY) + val destination = when (data.getInt(DESTINATION_TYPE_KEY)) { + CONTACT_TYPE -> Destination.Contact(address) + GROUP_TYPE -> Destination.ClosedGroup(address) + else -> return null + } + + return ConfigurationSyncJob(destination) + } + } +} \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/GroupAvatarDownloadJob.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/GroupAvatarDownloadJob.kt index 07fd6254d..f0831b8bb 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/GroupAvatarDownloadJob.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/GroupAvatarDownloadJob.kt @@ -13,14 +13,18 @@ class GroupAvatarDownloadJob(val server: String, val room: String, val imageId: override var failureCount: Int = 0 override val maxFailureCount: Int = 10 - override fun execute(dispatcherName: String) { + override suspend fun execute(dispatcherName: String) { if (imageId == null) { delegate?.handleJobFailedPermanently(this, dispatcherName, Exception("GroupAvatarDownloadJob now requires imageId")) return } - val storage = MessagingModuleConfiguration.shared.storage - val storedImageId = storage.getOpenGroup(room, server)?.imageId + val openGroup = storage.getOpenGroup(room, server) + if (openGroup == null || storage.getThreadId(openGroup) == null) { + delegate?.handleJobFailedPermanently(this, dispatcherName, Exception("GroupAvatarDownloadJob openGroup is null")) + return + } + val storedImageId = openGroup.imageId if (storedImageId == null || storedImageId != imageId) { delegate?.handleJobFailedPermanently(this, dispatcherName, Exception("GroupAvatarDownloadJob imageId does not match the OpenGroup")) diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/Job.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/Job.kt index 74e324f0e..7f3bf9b17 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/Job.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/Job.kt @@ -17,7 +17,7 @@ interface Job { internal const val MAX_BUFFER_SIZE = 1_000_000 // bytes } - fun execute(dispatcherName: String) + suspend fun execute(dispatcherName: String) fun serialize(): Data diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/JobQueue.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/JobQueue.kt index 03b9546c4..b437808f9 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/JobQueue.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/JobQueue.kt @@ -94,7 +94,7 @@ class JobQueue : JobDelegate { } } - private fun Job.process(dispatcherName: String) { + private suspend fun Job.process(dispatcherName: String) { Log.d(dispatcherName,"processJob: ${javaClass.simpleName} (id: $id)") delegate = this@JobQueue @@ -122,7 +122,7 @@ class JobQueue : JobDelegate { while (isActive) { when (val job = queue.receive()) { - is NotifyPNServerJob, is AttachmentUploadJob, is MessageSendJob -> { + is NotifyPNServerJob, is AttachmentUploadJob, is MessageSendJob, is ConfigurationSyncJob -> { txQueue.send(job) } is RetrieveProfileAvatarJob, @@ -226,6 +226,7 @@ class JobQueue : JobDelegate { BackgroundGroupAddJob.KEY, OpenGroupDeleteJob.KEY, RetrieveProfileAvatarJob.KEY, + ConfigurationSyncJob.KEY, ) allJobTypes.forEach { type -> resumePendingJobs(type) diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/MessageReceiveJob.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/MessageReceiveJob.kt index 2ba33b563..1ac482d5b 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/MessageReceiveJob.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/MessageReceiveJob.kt @@ -3,6 +3,7 @@ package org.session.libsession.messaging.jobs import nl.komponents.kovenant.Promise import nl.komponents.kovenant.deferred import org.session.libsession.messaging.MessagingModuleConfiguration +import org.session.libsession.messaging.messages.Message import org.session.libsession.messaging.sending_receiving.MessageReceiver import org.session.libsession.messaging.sending_receiving.handle import org.session.libsession.messaging.utilities.Data @@ -25,20 +26,22 @@ class MessageReceiveJob(val data: ByteArray, val serverHash: String? = null, val private val OPEN_GROUP_ID_KEY = "open_group_id" } - override fun execute(dispatcherName: String) { + override suspend fun execute(dispatcherName: String) { executeAsync(dispatcherName).get() } fun executeAsync(dispatcherName: String): Promise { val deferred = deferred() try { - val isRetry: Boolean = failureCount != 0 + val storage = MessagingModuleConfiguration.shared.storage val serverPublicKey = openGroupID?.let { - MessagingModuleConfiguration.shared.storage.getOpenGroupPublicKey(it.split(".").dropLast(1).joinToString(".")) + storage.getOpenGroupPublicKey(it.split(".").dropLast(1).joinToString(".")) } - val (message, proto) = MessageReceiver.parse(this.data, this.openGroupMessageServerID, openGroupPublicKey = serverPublicKey) + val currentClosedGroups = storage.getAllActiveClosedGroupPublicKeys() + val (message, proto) = MessageReceiver.parse(this.data, this.openGroupMessageServerID, openGroupPublicKey = serverPublicKey, currentClosedGroups = currentClosedGroups) + val threadId = Message.getThreadId(message, this.openGroupID, storage, false) message.serverHash = serverHash - MessageReceiver.handle(message, proto, this.openGroupID) + MessageReceiver.handle(message, proto, threadId ?: -1, this.openGroupID) this.handleSuccess(dispatcherName) deferred.resolve(Unit) } catch (e: Exception) { diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/MessageSendJob.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/MessageSendJob.kt index 524338592..2a152d0a0 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/MessageSendJob.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/MessageSendJob.kt @@ -10,7 +10,6 @@ import org.session.libsession.messaging.messages.Message import org.session.libsession.messaging.messages.visible.VisibleMessage import org.session.libsession.messaging.sending_receiving.MessageSender import org.session.libsession.messaging.utilities.Data -import org.session.libsession.snode.OnionRequestAPI import org.session.libsignal.utilities.HTTP import org.session.libsignal.utilities.Log @@ -33,7 +32,7 @@ class MessageSendJob(val message: Message, val destination: Destination) : Job { private val DESTINATION_KEY = "destination" } - override fun execute(dispatcherName: String) { + override suspend fun execute(dispatcherName: String) { val messageDataProvider = MessagingModuleConfiguration.shared.messageDataProvider val message = message as? VisibleMessage val storage = MessagingModuleConfiguration.shared.storage @@ -65,7 +64,8 @@ class MessageSendJob(val message: Message, val destination: Destination) : Job { return } // Wait for all attachments to upload before continuing } - val promise = MessageSender.send(this.message, this.destination).success { + val isSync = destination is Destination.Contact && destination.publicKey == sender + val promise = MessageSender.send(this.message, this.destination, isSync).success { this.handleSuccess(dispatcherName) }.fail { exception -> var logStacktrace = true diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/NotifyPNServerJob.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/NotifyPNServerJob.kt index 25fb2194c..be5854497 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/NotifyPNServerJob.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/NotifyPNServerJob.kt @@ -8,15 +8,13 @@ import okhttp3.MediaType import okhttp3.Request import okhttp3.RequestBody import org.session.libsession.messaging.jobs.Job.Companion.MAX_BUFFER_SIZE - import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI import org.session.libsession.messaging.utilities.Data -import org.session.libsession.snode.SnodeMessage import org.session.libsession.snode.OnionRequestAPI +import org.session.libsession.snode.SnodeMessage import org.session.libsession.snode.Version - -import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.JsonUtil +import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.retryIfNeeded class NotifyPNServerJob(val message: SnodeMessage) : Job { @@ -32,7 +30,7 @@ class NotifyPNServerJob(val message: SnodeMessage) : Job { private val MESSAGE_KEY = "message" } - override fun execute(dispatcherName: String) { + override suspend fun execute(dispatcherName: String) { val server = PushNotificationAPI.server val parameters = mapOf( "data" to message.data, "send_to" to message.recipient ) val url = "${server}/notify" diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/OpenGroupDeleteJob.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/OpenGroupDeleteJob.kt index 4c76f8763..333c87ba7 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/OpenGroupDeleteJob.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/OpenGroupDeleteJob.kt @@ -19,7 +19,7 @@ class OpenGroupDeleteJob(private val messageServerIds: LongArray, private val th override var failureCount: Int = 0 override val maxFailureCount: Int = 1 - override fun execute(dispatcherName: String) { + override suspend fun execute(dispatcherName: String) { val dataProvider = MessagingModuleConfiguration.shared.messageDataProvider val numberToDelete = messageServerIds.size Log.d(TAG, "Deleting $numberToDelete messages") diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/RetrieveProfileAvatarJob.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/RetrieveProfileAvatarJob.kt index 5c617fbdb..9ca2534f6 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/RetrieveProfileAvatarJob.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/RetrieveProfileAvatarJob.kt @@ -1,16 +1,14 @@ package org.session.libsession.messaging.jobs -import android.text.TextUtils import org.session.libsession.avatars.AvatarHelper import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.utilities.Data +import org.session.libsession.utilities.Address import org.session.libsession.utilities.DownloadUtilities.downloadFile import org.session.libsession.utilities.TextSecurePreferences.Companion.setProfileAvatarId +import org.session.libsession.utilities.TextSecurePreferences.Companion.setProfilePictureURL import org.session.libsession.utilities.Util.copy import org.session.libsession.utilities.Util.equals -import org.session.libsession.utilities.Address -import org.session.libsession.utilities.TextSecurePreferences -import org.session.libsession.utilities.TextSecurePreferences.Companion.setProfilePictureURL import org.session.libsession.utilities.recipients.Recipient import org.session.libsignal.streams.ProfileCipherInputStream import org.session.libsignal.utilities.Log @@ -19,12 +17,13 @@ import java.io.FileInputStream import java.io.FileOutputStream import java.io.InputStream import java.security.SecureRandom +import java.util.concurrent.ConcurrentSkipListSet -class RetrieveProfileAvatarJob(private val profileAvatar: String?, private val recipientAddress: Address): Job { +class RetrieveProfileAvatarJob(private val profileAvatar: String?, val recipientAddress: Address): Job { override var delegate: JobDelegate? = null override var id: String? = null override var failureCount: Int = 0 - override val maxFailureCount: Int = 0 + override val maxFailureCount: Int = 3 companion object { val TAG = RetrieveProfileAvatarJob::class.simpleName @@ -33,20 +32,30 @@ class RetrieveProfileAvatarJob(private val profileAvatar: String?, private val r // Keys used for database storage private const val PROFILE_AVATAR_KEY = "profileAvatar" private const val RECEIPIENT_ADDRESS_KEY = "recipient" + + val errorUrls = ConcurrentSkipListSet() + } - override fun execute(dispatcherName: String) { + override suspend fun execute(dispatcherName: String) { + val delegate = delegate ?: return + if (profileAvatar in errorUrls) return delegate.handleJobFailed(this, dispatcherName, Exception("Profile URL 404'd this app instance")) val context = MessagingModuleConfiguration.shared.context val storage = MessagingModuleConfiguration.shared.storage val recipient = Recipient.from(context, recipientAddress, true) val profileKey = recipient.resolve().profileKey if (profileKey == null || (profileKey.size != 32 && profileKey.size != 16)) { - Log.w(TAG, "Recipient profile key is gone!") - return + return delegate.handleJobFailedPermanently(this, dispatcherName, Exception("Recipient profile key is gone!")) } - if (AvatarHelper.avatarFileExists(context, recipient.resolve().address) && equals(profileAvatar, recipient.resolve().profileAvatar)) { + // Commit '78d1e9d' (fix: open group threads and avatar downloads) had this commented out so + // it's now limited to just the current user case + if ( + recipient.isLocalNumber && + AvatarHelper.avatarFileExists(context, recipient.resolve().address) && + equals(profileAvatar, recipient.resolve().profileAvatar) + ) { Log.w(TAG, "Already retrieved profile avatar: $profileAvatar") return } @@ -72,16 +81,23 @@ class RetrieveProfileAvatarJob(private val profileAvatar: String?, private val r val decryptDestination = File.createTempFile("avatar", ".jpg", context.cacheDir) copy(avatarStream, FileOutputStream(decryptDestination)) decryptDestination.renameTo(AvatarHelper.getAvatarFile(context, recipient.address)) + + if (recipient.isLocalNumber) { + setProfileAvatarId(context, SecureRandom().nextInt()) + setProfilePictureURL(context, profileAvatar) + } + + storage.setProfileAvatar(recipient, profileAvatar) + } catch (e: Exception) { + Log.e("Loki", "Failed to download profile avatar", e) + if (failureCount + 1 >= maxFailureCount) { + errorUrls += profileAvatar + } + return delegate.handleJobFailed(this, dispatcherName, e) } finally { downloadDestination.delete() } - - if (recipient.isLocalNumber) { - setProfileAvatarId(context, SecureRandom().nextInt()) - setProfilePictureURL(context, profileAvatar) - } - - storage.setProfileAvatar(recipient, profileAvatar) + return delegate.handleJobSucceeded(this, dispatcherName) } override fun serialize(): Data { diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/SessionJobManagerFactories.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/SessionJobManagerFactories.kt index cfe792274..46c87d5b9 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/SessionJobManagerFactories.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/SessionJobManagerFactories.kt @@ -16,6 +16,7 @@ class SessionJobManagerFactories { GroupAvatarDownloadJob.KEY to GroupAvatarDownloadJob.Factory(), BackgroundGroupAddJob.KEY to BackgroundGroupAddJob.Factory(), OpenGroupDeleteJob.KEY to OpenGroupDeleteJob.Factory(), + ConfigurationSyncJob.KEY to ConfigurationSyncJob.Factory() ) } } diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/TrimThreadJob.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/TrimThreadJob.kt index d082ac708..cc388b037 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/TrimThreadJob.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/TrimThreadJob.kt @@ -20,7 +20,7 @@ class TrimThreadJob(val threadId: Long, val openGroupId: String?) : Job { const val THREAD_LENGTH_TRIGGER_SIZE = 2000 } - override fun execute(dispatcherName: String) { + override suspend fun execute(dispatcherName: String) { val context = MessagingModuleConfiguration.shared.context val trimmingEnabled = TextSecurePreferences.isThreadLengthTrimmingEnabled(context) val storage = MessagingModuleConfiguration.shared.storage diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/Message.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/Message.kt index d201daa98..dd1d5f185 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/Message.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/Message.kt @@ -1,6 +1,9 @@ package org.session.libsession.messaging.messages import com.google.protobuf.ByteString +import org.session.libsession.database.StorageProtocol +import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate +import org.session.libsession.messaging.messages.visible.VisibleMessage import org.session.libsession.utilities.GroupUtil import org.session.libsignal.protos.SignalServiceProtos @@ -11,6 +14,7 @@ abstract class Message { var receivedTimestamp: Long? = null var recipient: String? = null var sender: String? = null + var isSenderSelf: Boolean = false var groupPublicKey: String? = null var openGroupServerMessageID: Long? = null var serverHash: String? = null @@ -18,6 +22,17 @@ abstract class Message { open val ttl: Long = 14 * 24 * 60 * 60 * 1000 open val isSelfSendValid: Boolean = false + companion object { + fun getThreadId(message: Message, openGroupID: String?, storage: StorageProtocol, shouldCreateThread: Boolean): Long? { + val senderOrSync = when (message) { + is VisibleMessage -> message.syncTarget ?: message.sender!! + is ExpirationTimerUpdate -> message.syncTarget ?: message.sender!! + else -> message.sender!! + } + return storage.getThreadIdFor(senderOrSync, message.groupPublicKey, openGroupID, createThread = shouldCreateThread) + } + } + open fun isValid(): Boolean { val sentTimestamp = sentTimestamp if (sentTimestamp != null && sentTimestamp <= 0) { return false } diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/control/ConfigurationMessage.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/control/ConfigurationMessage.kt index 30a47ab85..eae9a7673 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/control/ConfigurationMessage.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/control/ConfigurationMessage.kt @@ -122,9 +122,9 @@ class ConfigurationMessage(var closedGroups: List, var openGroups: val displayName = TextSecurePreferences.getProfileName(context) ?: return null val profilePicture = TextSecurePreferences.getProfilePictureURL(context) val profileKey = ProfileKeyUtil.getProfileKey(context) - val groups = storage.getAllGroups() + val groups = storage.getAllGroups(includeInactive = false) for (group in groups) { - if (group.isClosedGroup) { + if (group.isClosedGroup && group.isActive) { if (!group.members.contains(Address.fromSerialized(storage.getUserPublicKey()!!))) continue val groupPublicKey = GroupUtil.doubleDecodeGroupID(group.encodedId).toHexString() val encryptionKeyPair = storage.getLatestClosedGroupEncryptionKeyPair(groupPublicKey) ?: continue diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/control/SharedConfigurationMessage.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/control/SharedConfigurationMessage.kt new file mode 100644 index 000000000..72b247496 --- /dev/null +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/control/SharedConfigurationMessage.kt @@ -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() + } +} \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiver.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiver.kt index f5965d5f2..34022b739 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiver.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiver.kt @@ -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 @@ -34,13 +35,14 @@ object MessageReceiver { object NoThread: Error("Couldn't find thread for message.") object SelfSend: Error("Message addressed at self.") object InvalidGroupPublicKey: Error("Invalid group public key.") + object NoGroupThread: Error("No thread exists for this group.") object NoGroupKeyPair: Error("Missing group key pair.") object NoUserED25519KeyPair : Error("Couldn't find user ED25519 key pair.") internal val isRetryable: Boolean = when (this) { is DuplicateMessage, is InvalidMessage, is UnknownMessage, is UnknownEnvelopeType, is InvalidSignature, is NoData, - is SenderBlocked, is SelfSend -> false + is SenderBlocked, is SelfSend, is NoGroupThread -> false else -> true } } @@ -51,6 +53,7 @@ object MessageReceiver { isOutgoing: Boolean? = null, otherBlindedPublicKey: String? = null, openGroupPublicKey: String? = null, + currentClosedGroups: Set? ): Pair { val storage = MessagingModuleConfiguration.shared.storage val userPublicKey = storage.getUserPublicKey() @@ -70,7 +73,7 @@ object MessageReceiver { } else { when (envelope.type) { SignalServiceProtos.Envelope.Type.SESSION_MESSAGE -> { - if (IdPrefix.fromValue(envelope.source) == IdPrefix.BLINDED) { + if (IdPrefix.fromValue(envelope.source)?.isBlinded() == true) { openGroupPublicKey ?: throw Error.InvalidGroupPublicKey otherBlindedPublicKey ?: throw Error.DecryptionFailed val decryptionResult = MessageDecrypter.decryptBlinded( @@ -139,6 +142,7 @@ object MessageReceiver { UnsendRequest.fromProto(proto) ?: MessageRequestResponse.fromProto(proto) ?: CallMessage.fromProto(proto) ?: + SharedConfigurationMessage.fromProto(proto) ?: VisibleMessage.fromProto(proto) ?: run { throw Error.UnknownMessage } @@ -147,6 +151,9 @@ object MessageReceiver { if (!message.isSelfSendValid && (sender == userPublicKey || isUserBlindedSender)) { throw Error.SelfSend } + if (sender == userPublicKey || isUserBlindedSender) { + message.isSenderSelf = true + } // Guard against control messages in open groups if (isOpenGroupMessage && message !is VisibleMessage) { throw Error.InvalidMessage @@ -167,12 +174,16 @@ 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 (groupPublicKey != null && groupPublicKey !in (currentClosedGroups ?: emptySet())) { + throw Error.NoGroupThread + } + 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) diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt index fa0a49a64..804b2f15b 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt @@ -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 @@ -61,7 +62,7 @@ object MessageSender { } // Convenience - fun send(message: Message, destination: Destination, isSyncMessage: Boolean = false): Promise { + fun send(message: Message, destination: Destination, isSyncMessage: Boolean): Promise { return if (destination is Destination.LegacyOpenGroup || destination is Destination.OpenGroup || destination is Destination.OpenGroupInbox) { sendToOpenGroupDestination(destination, message) } else { @@ -69,71 +70,115 @@ 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 + ) + } + // One-on-One Chats & Closed Groups private fun sendToSnodeDestination(destination: Destination, message: Message, isSyncMessage: Boolean = false): Promise { val deferred = deferred() 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 = SnodeAPI.nowWithOffset // Visible messages will already have their sent timestamp set - } - val messageSendTime = SnodeAPI.nowWithOffset + // 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, isSyncMessage) - 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 = when { @@ -143,29 +188,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 +196,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) @@ -414,24 +433,24 @@ object MessageSender { @JvmStatic fun send(message: Message, address: Address) { - val threadID = MessagingModuleConfiguration.shared.storage.getOrCreateThreadIdFor(address) + val threadID = MessagingModuleConfiguration.shared.storage.getThreadId(address) message.threadID = threadID val destination = Destination.from(address) val job = MessageSendJob(message, destination) JobQueue.shared.add(job) } - fun sendNonDurably(message: VisibleMessage, attachments: List, address: Address): Promise { + fun sendNonDurably(message: VisibleMessage, attachments: List, address: Address, isSyncMessage: Boolean): Promise { val attachmentIDs = MessagingModuleConfiguration.shared.messageDataProvider.getAttachmentIDsFor(message.id!!) message.attachmentIDs.addAll(attachmentIDs) - return sendNonDurably(message, address) + return sendNonDurably(message, address, isSyncMessage) } - fun sendNonDurably(message: Message, address: Address): Promise { - val threadID = MessagingModuleConfiguration.shared.storage.getOrCreateThreadIdFor(address) + fun sendNonDurably(message: Message, address: Address, isSyncMessage: Boolean): Promise { + val threadID = MessagingModuleConfiguration.shared.storage.getThreadId(address) message.threadID = threadID val destination = Destination.from(address) - return send(message, destination) + return send(message, destination, isSyncMessage) } // Closed groups diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderClosedGroupHandler.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderClosedGroupHandler.kt index f62fd5a93..a98b6b1b6 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderClosedGroupHandler.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderClosedGroupHandler.kt @@ -18,14 +18,14 @@ import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.recipients.Recipient import org.session.libsignal.crypto.ecc.Curve import org.session.libsignal.crypto.ecc.ECKeyPair -import org.session.libsignal.utilities.guava.Optional import org.session.libsignal.messages.SignalServiceGroup import org.session.libsignal.protos.SignalServiceProtos +import org.session.libsignal.utilities.Hex +import org.session.libsignal.utilities.Log +import org.session.libsignal.utilities.ThreadUtils +import org.session.libsignal.utilities.guava.Optional import org.session.libsignal.utilities.hexEncodedPublicKey import org.session.libsignal.utilities.removingIdPrefixIfNeeded -import org.session.libsignal.utilities.Hex -import org.session.libsignal.utilities.ThreadUtils -import org.session.libsignal.utilities.Log import java.util.* import java.util.concurrent.ConcurrentHashMap @@ -60,16 +60,20 @@ fun MessageSender.create(name: String, members: Collection): Promise): Promise): Promise, name: String) { - val context = MessagingModuleConfiguration.shared.context - val storage = MessagingModuleConfiguration.shared.storage - val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey) - val group = storage.getGroup(groupID) ?: run { - Log.d("Loki", "Can't update nonexistent closed group.") - throw Error.NoThread - } - // Update name if needed - if (name != group.title) { setName(groupPublicKey, name) } - // Add members if needed - val addedMembers = members - group.members.map { it.serialize() } - if (!addedMembers.isEmpty()) { addMembers(groupPublicKey, addedMembers) } - // Remove members if needed - val removedMembers = group.members.map { it.serialize() } - members - if (removedMembers.isEmpty()) { removeMembers(groupPublicKey, removedMembers) } -} - fun MessageSender.setName(groupPublicKey: String, newName: String) { val context = MessagingModuleConfiguration.shared.context val storage = MessagingModuleConfiguration.shared.storage @@ -252,15 +240,15 @@ fun MessageSender.leave(groupPublicKey: String, notifyUser: Boolean = true): Pro val sentTime = SnodeAPI.nowWithOffset closedGroupControlMessage.sentTimestamp = sentTime storage.setActive(groupID, false) - sendNonDurably(closedGroupControlMessage, Address.fromSerialized(groupID)).success { + sendNonDurably(closedGroupControlMessage, Address.fromSerialized(groupID), isSyncMessage = false).success { // Notify the user val infoType = SignalServiceGroup.Type.QUIT - val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID)) if (notifyUser) { + val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID)) storage.insertOutgoingInfoMessage(context, groupID, infoType, name, updatedMembers, admins, threadID, sentTime) } // Remove the group private key and unsubscribe from PNs - MessageReceiver.disableLocalGroupAndUnsubscribe(groupPublicKey, groupID, userPublicKey) + MessageReceiver.disableLocalGroupAndUnsubscribe(groupPublicKey, groupID, userPublicKey, true) deferred.resolve(Unit) }.fail { storage.setActive(groupID, true) @@ -292,7 +280,7 @@ fun MessageSender.generateAndSendNewEncryptionKeyPair(groupPublicKey: String, ta // Distribute it sendEncryptionKeyPair(groupPublicKey, newKeyPair, targetMembers)?.success { // Store it * after * having sent out the message to the group - storage.addClosedGroupEncryptionKeyPair(newKeyPair, groupPublicKey) + storage.addClosedGroupEncryptionKeyPair(newKeyPair, groupPublicKey, SnodeAPI.nowWithOffset) pendingKeyPairs[groupPublicKey] = Optional.absent() } } @@ -312,7 +300,8 @@ fun MessageSender.sendEncryptionKeyPair(groupPublicKey: String, newKeyPair: ECKe val closedGroupControlMessage = ClosedGroupControlMessage(kind) closedGroupControlMessage.sentTimestamp = sentTime return if (force) { - MessageSender.sendNonDurably(closedGroupControlMessage, Address.fromSerialized(destination)) + val isSync = MessagingModuleConfiguration.shared.storage.getUserPublicKey() == destination + MessageSender.sendNonDurably(closedGroupControlMessage, Address.fromSerialized(destination), isSyncMessage = isSync) } else { MessageSender.send(closedGroupControlMessage, Address.fromSerialized(destination)) null diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt index 523ad450b..19278aadd 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt @@ -1,11 +1,11 @@ package org.session.libsession.messaging.sending_receiving import android.text.TextUtils +import network.loki.messenger.libsession_util.ConfigBase import org.session.libsession.avatars.AvatarHelper import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.jobs.BackgroundGroupAddJob import org.session.libsession.messaging.jobs.JobQueue -import org.session.libsession.messaging.jobs.RetrieveProfileAvatarJob import org.session.libsession.messaging.messages.Message import org.session.libsession.messaging.messages.control.CallMessage import org.session.libsession.messaging.messages.control.ClosedGroupControlMessage @@ -42,6 +42,7 @@ import org.session.libsignal.crypto.ecc.DjbECPublicKey import org.session.libsignal.crypto.ecc.ECKeyPair import org.session.libsignal.messages.SignalServiceGroup import org.session.libsignal.protos.SignalServiceProtos +import org.session.libsignal.protos.SignalServiceProtos.SharedConfigMessage import org.session.libsignal.utilities.Base64 import org.session.libsignal.utilities.IdPrefix import org.session.libsignal.utilities.Log @@ -58,7 +59,10 @@ internal fun MessageReceiver.isBlocked(publicKey: String): Boolean { return recipient.isBlocked } -fun MessageReceiver.handle(message: Message, proto: SignalServiceProtos.Content, openGroupID: String?) { +fun MessageReceiver.handle(message: Message, proto: SignalServiceProtos.Content, threadId: Long, openGroupID: String?) { + // Do nothing if the message was outdated + if (MessageReceiver.messageIsOutdated(message, threadId, openGroupID)) { return } + when (message) { is ReadReceipt -> handleReadReceipt(message) is TypingIndicator -> handleTypingIndicator(message) @@ -68,8 +72,8 @@ fun MessageReceiver.handle(message: Message, proto: SignalServiceProtos.Content, is ConfigurationMessage -> handleConfigurationMessage(message) is UnsendRequest -> handleUnsendRequest(message) is MessageRequestResponse -> handleMessageRequestResponse(message) - is VisibleMessage -> handleVisibleMessage(message, proto, openGroupID, - runIncrement = true, + is VisibleMessage -> handleVisibleMessage( + message, proto, openGroupID, threadId, runThreadUpdate = true, runProfileUpdate = true ) @@ -77,6 +81,33 @@ fun MessageReceiver.handle(message: Message, proto: SignalServiceProtos.Content, } } +fun MessageReceiver.messageIsOutdated(message: Message, threadId: Long, openGroupID: String?): Boolean { + when (message) { + is ReadReceipt -> return false // No visible artifact created so better to keep for more reliable read states + is UnsendRequest -> return false // We should always process the removal of messages just in case + } + + // Determine the state of the conversation and the validity of the message + val storage = MessagingModuleConfiguration.shared.storage + val userPublicKey = storage.getUserPublicKey()!! + val threadRecipient = storage.getRecipientForThread(threadId) + val conversationVisibleInConfig = storage.conversationInConfig( + if (message.groupPublicKey == null) threadRecipient?.address?.serialize() else null, + message.groupPublicKey, + openGroupID, + true + ) + val canPerformChange = storage.canPerformConfigChange( + if (threadRecipient?.address?.serialize() == userPublicKey) SharedConfigMessage.Kind.USER_PROFILE.name else SharedConfigMessage.Kind.CONTACTS.name, + userPublicKey, + message.sentTimestamp!! + ) + + // If the thread is visible or the message was sent more recently than the last config message (minus + // buffer period) then we should process the message, if not then the message is outdated + return (!conversationVisibleInConfig && !canPerformChange) +} + // region Control Messages private fun MessageReceiver.handleReadReceipt(message: ReadReceipt) { val context = MessagingModuleConfiguration.shared.context @@ -129,6 +160,7 @@ private fun MessageReceiver.handleDataExtractionNotification(message: DataExtrac if (message.groupPublicKey != null) return val storage = MessagingModuleConfiguration.shared.storage val senderPublicKey = message.sender!! + val notification: DataExtractionNotificationInfoMessage = when(message.kind) { is DataExtractionNotification.Kind.Screenshot -> DataExtractionNotificationInfoMessage(DataExtractionNotificationInfoMessage.Kind.SCREENSHOT) is DataExtractionNotification.Kind.MediaSaved -> DataExtractionNotificationInfoMessage(DataExtractionNotificationInfoMessage.Kind.MEDIA_SAVED) @@ -149,11 +181,17 @@ private fun handleConfigurationMessage(message: ConfigurationMessage) { TextSecurePreferences.setConfigurationMessageSynced(context, true) TextSecurePreferences.setLastProfileUpdateTime(context, message.sentTimestamp!!) + val isForceSync = TextSecurePreferences.hasForcedNewConfig(context) + val currentTime = SnodeAPI.nowWithOffset + if (ConfigBase.isNewConfigEnabled(isForceSync, currentTime)) { + TextSecurePreferences.setHasLegacyConfig(context, true) + if (!firstTimeSync) return + } val allClosedGroupPublicKeys = storage.getAllClosedGroupPublicKeys() for (closedGroup in message.closedGroups) { if (allClosedGroupPublicKeys.contains(closedGroup.publicKey)) { // just handle the closed group encryption key pairs to avoid sync'd devices getting out of sync - storage.addClosedGroupEncryptionKeyPair(closedGroup.encryptionKeyPair!!, closedGroup.publicKey) + storage.addClosedGroupEncryptionKeyPair(closedGroup.encryptionKeyPair!!, closedGroup.publicKey, message.sentTimestamp!!) } else if (firstTimeSync) { // only handle new closed group if it's first time sync handleNewClosedGroup(message.sender!!, message.sentTimestamp!!, closedGroup.publicKey, closedGroup.name, @@ -166,9 +204,9 @@ private fun handleConfigurationMessage(message: ConfigurationMessage) { .replace(OpenGroupApi.httpDefaultServer, OpenGroupApi.defaultServer) }) { if (allV2OpenGroups.contains(openGroup)) continue - Log.d("OpenGroup", "All open groups doesn't contain $openGroup") + Log.d("OpenGroup", "All open groups doesn't contain open group") if (!storage.hasBackgroundGroupAddJob(openGroup)) { - Log.d("OpenGroup", "Doesn't contain background job for $openGroup, adding") + Log.d("OpenGroup", "Doesn't contain background job for open group, adding") JobQueue.shared.add(BackgroundGroupAddJob(openGroup)) } } @@ -182,10 +220,7 @@ private fun handleConfigurationMessage(message: ConfigurationMessage) { && TextSecurePreferences.getProfilePictureURL(context) != message.profilePicture) { val profileKey = Base64.encodeBytes(message.profileKey) ProfileKeyUtil.setEncodedProfileKey(context, profileKey) - profileManager.setProfileKey(context, recipient, message.profileKey) - if (!message.profilePicture.isNullOrEmpty() && TextSecurePreferences.getProfilePictureURL(context) != message.profilePicture) { - JobQueue.shared.add(RetrieveProfileAvatarJob(message.profilePicture!!, recipient.address)) - } + profileManager.setProfilePicture(context, recipient, message.profilePicture, message.profileKey) } storage.addContacts(message.contacts) } @@ -215,24 +250,28 @@ fun handleMessageRequestResponse(message: MessageRequestResponse) { } //endregion -fun MessageReceiver.handleVisibleMessage(message: VisibleMessage, - proto: SignalServiceProtos.Content, - openGroupID: String?, - runIncrement: Boolean, - runThreadUpdate: Boolean, - runProfileUpdate: Boolean): Long? { +fun MessageReceiver.handleVisibleMessage( + message: VisibleMessage, + proto: SignalServiceProtos.Content, + openGroupID: String?, + threadId: Long, + runThreadUpdate: Boolean, + runProfileUpdate: Boolean +): Long? { val storage = MessagingModuleConfiguration.shared.storage val context = MessagingModuleConfiguration.shared.context val userPublicKey = storage.getUserPublicKey() val messageSender: String? = message.sender + + // Do nothing if the message was outdated + if (MessageReceiver.messageIsOutdated(message, threadId, openGroupID)) { return null } + // Get or create thread // FIXME: In case this is an open group this actually * doesn't * create the thread if it doesn't yet // exist. This is intentional, but it's very non-obvious. - val threadID = storage.getOrCreateThreadIdFor(message.syncTarget ?: messageSender!!, message.groupPublicKey, openGroupID) - if (threadID < 0) { + val threadID = storage.getThreadIdFor(message.syncTarget ?: messageSender!!, message.groupPublicKey, openGroupID, createThread = true) // Thread doesn't exist; should only be reached in a case where we are processing open group messages for a no longer existent thread - throw MessageReceiver.Error.NoThread - } + ?: throw MessageReceiver.Error.NoThread val threadRecipient = storage.getRecipientForThread(threadID) val userBlindedKey = openGroupID?.let { val openGroup = storage.getOpenGroup(threadID) ?: return@let null @@ -259,9 +298,10 @@ fun MessageReceiver.handleVisibleMessage(message: VisibleMessage, val profileKeyChanged = (recipient.profileKey == null || !MessageDigest.isEqual(recipient.profileKey, newProfileKey)) if ((profileKeyValid && profileKeyChanged) || (profileKeyValid && needsProfilePicture)) { - profileManager.setProfileKey(context, recipient, newProfileKey!!) + profileManager.setProfilePicture(context, recipient, profile.profilePictureURL, newProfileKey) profileManager.setUnidentifiedAccessMode(context, recipient, Recipient.UnidentifiedAccessMode.UNKNOWN) - profileManager.setProfilePictureURL(context, recipient, profile.profilePictureURL!!) + } else if (newProfileKey == null || newProfileKey.isEmpty() || profile.profilePictureURL.isNullOrEmpty()) { + profileManager.setProfilePicture(context, recipient, null, null) } } } @@ -344,7 +384,7 @@ fun MessageReceiver.handleVisibleMessage(message: VisibleMessage, message.threadID = threadID val messageID = storage.persist(message, quoteModel, linkPreviews, message.groupPublicKey, openGroupID, - attachments, runIncrement, runThreadUpdate + attachments, runThreadUpdate ) ?: return null val openGroupServerID = message.openGroupServerMessageID if (openGroupServerID != null) { @@ -437,12 +477,34 @@ private fun MessageReceiver.handleClosedGroupControlMessage(message: ClosedGroup is ClosedGroupControlMessage.Kind.MembersRemoved -> handleClosedGroupMembersRemoved(message) is ClosedGroupControlMessage.Kind.MemberLeft -> handleClosedGroupMemberLeft(message) } + if ( + message.kind !is ClosedGroupControlMessage.Kind.New && + MessagingModuleConfiguration.shared.storage.canPerformConfigChange( + SharedConfigMessage.Kind.GROUPS.name, + MessagingModuleConfiguration.shared.storage.getUserPublicKey()!!, + message.sentTimestamp!! + ) + ) { + // update the config + val closedGroupPublicKey = message.getPublicKey() + val storage = MessagingModuleConfiguration.shared.storage + storage.updateGroupConfig(closedGroupPublicKey) + } } +private fun ClosedGroupControlMessage.getPublicKey(): String = kind!!.let { when (it) { + is ClosedGroupControlMessage.Kind.New -> it.publicKey.toByteArray().toHexString() + is ClosedGroupControlMessage.Kind.EncryptionKeyPair -> it.publicKey?.toByteArray()?.toHexString() ?: groupPublicKey!! + is ClosedGroupControlMessage.Kind.MemberLeft -> groupPublicKey!! + is ClosedGroupControlMessage.Kind.MembersAdded -> groupPublicKey!! + is ClosedGroupControlMessage.Kind.MembersRemoved -> groupPublicKey!! + is ClosedGroupControlMessage.Kind.NameChange -> groupPublicKey!! +}} + private fun MessageReceiver.handleNewClosedGroup(message: ClosedGroupControlMessage) { val kind = message.kind!! as? ClosedGroupControlMessage.Kind.New ?: return val recipient = Recipient.from(MessagingModuleConfiguration.shared.context, Address.fromSerialized(message.sender!!), false) - if (!recipient.isApproved && !recipient.isLocalNumber) return + if (!recipient.isApproved && !recipient.isLocalNumber) return Log.e("Loki", "not accepting new closed group from unapproved recipient") val groupPublicKey = kind.publicKey.toByteArray().toHexString() val members = kind.members.map { it.toByteArray().toHexString() } val admins = kind.admins.map { it.toByteArray().toHexString() } @@ -453,10 +515,24 @@ private fun MessageReceiver.handleNewClosedGroup(message: ClosedGroupControlMess private fun handleNewClosedGroup(sender: String, sentTimestamp: Long, groupPublicKey: String, name: String, encryptionKeyPair: ECKeyPair, members: List, admins: List, formationTimestamp: Long, expireTimer: Int) { val context = MessagingModuleConfiguration.shared.context val storage = MessagingModuleConfiguration.shared.storage - val userPublicKey = TextSecurePreferences.getLocalNumber(context) - // Create the group + val userPublicKey = storage.getUserPublicKey()!! val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey) val groupExists = storage.getGroup(groupID) != null + + if (!storage.canPerformConfigChange(SharedConfigMessage.Kind.GROUPS.name, userPublicKey, sentTimestamp)) { + // If the closed group already exists then store the encryption keys (since the config only stores + // the latest key we won't be able to decrypt older messages if we were added to the group within + // the last two weeks and the key has been rotated - unfortunately if the user was added more than + // two weeks ago and the keys were rotated within the last two weeks then we won't be able to decrypt + // messages received before the key rotation) + if (groupExists) { + storage.addClosedGroupEncryptionKeyPair(encryptionKeyPair, groupPublicKey, sentTimestamp) + storage.updateGroupConfig(groupPublicKey) + } + return + } + + // Create the group if (groupExists) { // Update the group if (!storage.isGroupActive(groupPublicKey)) { @@ -475,18 +551,15 @@ private fun handleNewClosedGroup(sender: String, sentTimestamp: Long, groupPubli // Add the group to the user's set of public keys to poll for storage.addClosedGroupPublicKey(groupPublicKey) // Store the encryption key pair - storage.addClosedGroupEncryptionKeyPair(encryptionKeyPair, groupPublicKey) + storage.addClosedGroupEncryptionKeyPair(encryptionKeyPair, groupPublicKey, sentTimestamp) + storage.createInitialConfigGroup(groupPublicKey, name, GroupUtil.createConfigMemberMap(members, admins), formationTimestamp, encryptionKeyPair) // Set expiration timer storage.setExpirationTimer(groupID, expireTimer) // Notify the PN server - PushNotificationAPI.performOperation(PushNotificationAPI.ClosedGroupOperation.Subscribe, groupPublicKey, storage.getUserPublicKey()!!) - // Notify the user - if (userPublicKey == sender && !groupExists) { - val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID)) - storage.insertOutgoingInfoMessage(context, groupID, SignalServiceGroup.Type.CREATION, name, members, admins, threadID, sentTimestamp) - } else if (userPublicKey != sender) { - storage.insertIncomingInfoMessage(context, sender, groupID, SignalServiceGroup.Type.CREATION, name, members, admins, sentTimestamp) - } + PushNotificationAPI.performOperation(PushNotificationAPI.ClosedGroupOperation.Subscribe, groupPublicKey, userPublicKey) + // Create thread + val threadId = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID)) + storage.setThreadDate(threadId, formationTimestamp) // Start polling ClosedGroupPollerV2.shared.startPolling(groupPublicKey) } @@ -527,7 +600,7 @@ private fun MessageReceiver.handleClosedGroupEncryptionKeyPair(message: ClosedGr Log.d("Loki", "Ignoring duplicate closed group encryption key pair.") return } - storage.addClosedGroupEncryptionKeyPair(keyPair, groupPublicKey) + storage.addClosedGroupEncryptionKeyPair(keyPair, groupPublicKey, message.sentTimestamp!!) Log.d("Loki", "Received a new closed group encryption key pair.") } @@ -555,7 +628,12 @@ private fun MessageReceiver.handleClosedGroupNameChanged(message: ClosedGroupCon val members = group.members.map { it.serialize() } val admins = group.admins.map { it.serialize() } val name = kind.name - storage.updateTitle(groupID, name) + + // Only update the group in storage if it isn't invalidated by the config state + if (storage.canPerformConfigChange(SharedConfigMessage.Kind.GROUPS.name, userPublicKey!!, message.sentTimestamp!!)) { + storage.updateTitle(groupID, name) + } + // Notify the user if (userPublicKey == senderPublicKey) { val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID)) @@ -589,12 +667,16 @@ private fun MessageReceiver.handleClosedGroupMembersAdded(message: ClosedGroupCo val updateMembers = kind.members.map { it.toByteArray().toHexString() } val newMembers = members + updateMembers - storage.updateMembers(groupID, newMembers.map { Address.fromSerialized(it) }) - // Update zombie members in case the added members are zombies - val zombies = storage.getZombieMembers(groupID) - if (zombies.intersect(updateMembers).isNotEmpty()) { - storage.setZombieMembers(groupID, zombies.minus(updateMembers).map { Address.fromSerialized(it) }) + // Only update the group in storage if it isn't invalidated by the config state + if (storage.canPerformConfigChange(SharedConfigMessage.Kind.GROUPS.name, userPublicKey, message.sentTimestamp!!)) { + storage.updateMembers(groupID, newMembers.map { Address.fromSerialized(it) }) + + // Update zombie members in case the added members are zombies + val zombies = storage.getZombieMembers(groupID) + if (zombies.intersect(updateMembers).isNotEmpty()) { + storage.setZombieMembers(groupID, zombies.minus(updateMembers).map { Address.fromSerialized(it) }) + } } // Notify the user @@ -676,13 +758,18 @@ private fun MessageReceiver.handleClosedGroupMembersRemoved(message: ClosedGroup Log.d("Loki", "Received a MEMBERS_REMOVED instead of a MEMBERS_LEFT from sender: $senderPublicKey.") } val wasCurrentUserRemoved = userPublicKey in removedMembers - // Admin should send a MEMBERS_LEFT message but handled here just in case - if (didAdminLeave || wasCurrentUserRemoved) { - disableLocalGroupAndUnsubscribe(groupPublicKey, groupID, userPublicKey) - } else { - storage.updateMembers(groupID, newMembers.map { Address.fromSerialized(it) }) - // Update zombie members - storage.setZombieMembers(groupID, zombies.minus(removedMembers).map { Address.fromSerialized(it) }) + + // Only update the group in storage if it isn't invalidated by the config state + if (storage.canPerformConfigChange(SharedConfigMessage.Kind.GROUPS.name, userPublicKey, message.sentTimestamp!!)) { + // Admin should send a MEMBERS_LEFT message but handled here just in case + if (didAdminLeave || wasCurrentUserRemoved) { + disableLocalGroupAndUnsubscribe(groupPublicKey, groupID, userPublicKey, true) + return + } else { + storage.updateMembers(groupID, newMembers.map { Address.fromSerialized(it) }) + // Update zombie members + storage.setZombieMembers(groupID, zombies.minus(removedMembers).map { Address.fromSerialized(it) }) + } } // Notify the user @@ -731,24 +818,30 @@ private fun MessageReceiver.handleClosedGroupMemberLeft(message: ClosedGroupCont val didAdminLeave = admins.contains(senderPublicKey) val updatedMemberList = members - senderPublicKey val userLeft = (userPublicKey == senderPublicKey) - if (didAdminLeave || userLeft) { - disableLocalGroupAndUnsubscribe(groupPublicKey, groupID, userPublicKey) - } else { - storage.updateMembers(groupID, updatedMemberList.map { Address.fromSerialized(it) }) - // Update zombie members - val zombies = storage.getZombieMembers(groupID) - storage.setZombieMembers(groupID, zombies.plus(senderPublicKey).map { Address.fromSerialized(it) }) + + // Only update the group in storage if it isn't invalidated by the config state + if (storage.canPerformConfigChange(SharedConfigMessage.Kind.GROUPS.name, userPublicKey, message.sentTimestamp!!)) { + if (didAdminLeave || userLeft) { + disableLocalGroupAndUnsubscribe(groupPublicKey, groupID, userPublicKey, delete = userLeft) + + if (userLeft) { + return + } + } else { + storage.updateMembers(groupID, updatedMemberList.map { Address.fromSerialized(it) }) + // Update zombie members + val zombies = storage.getZombieMembers(groupID) + storage.setZombieMembers(groupID, zombies.plus(senderPublicKey).map { Address.fromSerialized(it) }) + } } + // Notify the user - if (userLeft) { - val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID)) - storage.insertOutgoingInfoMessage(context, groupID, SignalServiceGroup.Type.QUIT, name, members, admins, threadID, message.sentTimestamp!!) - } else { + if (!userLeft) { storage.insertIncomingInfoMessage(context, senderPublicKey, groupID, SignalServiceGroup.Type.QUIT, name, members, admins, message.sentTimestamp!!) } } -private fun isValidGroupUpdate(group: GroupRecord, sentTimestamp: Long, senderPublicKey: String): Boolean { +private fun isValidGroupUpdate(group: GroupRecord, sentTimestamp: Long, senderPublicKey: String): Boolean { val oldMembers = group.members.map { it.serialize() } // Check that the message isn't from before the group was created if (group.formationTimestamp > sentTimestamp) { @@ -763,7 +856,7 @@ private fun isValidGroupUpdate(group: GroupRecord, sentTimestamp: Long, senderPu return true } -fun MessageReceiver.disableLocalGroupAndUnsubscribe(groupPublicKey: String, groupID: String, userPublicKey: String) { +fun MessageReceiver.disableLocalGroupAndUnsubscribe(groupPublicKey: String, groupID: String, userPublicKey: String, delete: Boolean) { val storage = MessagingModuleConfiguration.shared.storage storage.removeClosedGroupPublicKey(groupPublicKey) // Remove the key pairs @@ -775,5 +868,11 @@ fun MessageReceiver.disableLocalGroupAndUnsubscribe(groupPublicKey: String, grou PushNotificationAPI.performOperation(PushNotificationAPI.ClosedGroupOperation.Unsubscribe, groupPublicKey, userPublicKey) // Stop polling ClosedGroupPollerV2.shared.stopPolling(groupPublicKey) + + if (delete) { + val threadId = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID)) + storage.cancelPendingMessageSendJobs(threadId) + storage.deleteConversation(threadId) + } } // endregion diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/OpenGroupPoller.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/OpenGroupPoller.kt index 387381c9c..b9baadcab 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/OpenGroupPoller.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/OpenGroupPoller.kt @@ -12,6 +12,7 @@ import org.session.libsession.messaging.jobs.MessageReceiveJob import org.session.libsession.messaging.jobs.MessageReceiveParameters import org.session.libsession.messaging.jobs.OpenGroupDeleteJob import org.session.libsession.messaging.jobs.TrimThreadJob +import org.session.libsession.messaging.messages.Message import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate import org.session.libsession.messaging.messages.visible.VisibleMessage import org.session.libsession.messaging.open_groups.Endpoint @@ -169,6 +170,7 @@ class OpenGroupPoller(private val server: String, private val executorService: S is Endpoint.Outbox, is Endpoint.OutboxSince -> { handleDirectMessages(server, true, response.body as List) } + else -> { /* We don't care about the result of any other calls (won't be polled for) */} } if (secondToLastJob == null && !isCaughtUp) { isCaughtUp = true @@ -205,7 +207,7 @@ class OpenGroupPoller(private val server: String, private val executorService: S val storage = MessagingModuleConfiguration.shared.storage storage.setServerCapabilities(server, capabilities.capabilities) } - + private fun handleMessages( server: String, roomToken: String, @@ -260,7 +262,8 @@ class OpenGroupPoller(private val server: String, private val executorService: S null, fromOutbox, if (fromOutbox) it.recipient else it.sender, - serverPublicKey + serverPublicKey, + emptySet() // this shouldn't be necessary as we are polling open groups here ) if (fromOutbox) { val mapping = mappingCache[it.recipient] ?: storage.getOrCreateBlindedIdMapping( @@ -277,7 +280,8 @@ class OpenGroupPoller(private val server: String, private val executorService: S } mappingCache[it.recipient] = mapping } - MessageReceiver.handle(message, proto, null) + val threadId = Message.getThreadId(message, null, MessagingModuleConfiguration.shared.storage, false) + MessageReceiver.handle(message, proto, threadId ?: -1, null) } catch (e: Exception) { Log.e("Loki", "Couldn't handle direct message", e) } diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/Poller.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/Poller.kt index 4a39b70c0..f0b20436f 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/Poller.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/Poller.kt @@ -1,5 +1,14 @@ package org.session.libsession.messaging.sending_receiving.pollers +import android.util.SparseArray +import androidx.core.util.valueIterator +import kotlinx.coroutines.Dispatchers +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.UserGroupsConfig +import network.loki.messenger.libsession_util.UserProfile import nl.komponents.kovenant.Deferred import nl.komponents.kovenant.Promise import nl.komponents.kovenant.deferred @@ -10,17 +19,23 @@ 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 +import org.session.libsession.utilities.ConfigFactoryProtocol import org.session.libsignal.utilities.Log +import org.session.libsignal.utilities.Namespace import org.session.libsignal.utilities.Snode import java.security.SecureRandom import java.util.Timer import java.util.TimerTask +import kotlin.time.Duration.Companion.days private class PromiseCanceledException : Exception("Promise canceled.") -class Poller { +class Poller(private val configFactory: ConfigFactoryProtocol, debounceTimer: Timer) { var userPublicKey = MessagingModuleConfiguration.shared.storage.getUserPublicKey() ?: "" private var hasStarted: Boolean = false private val usedSnodes: MutableSet = mutableSetOf() @@ -97,23 +112,159 @@ class Poller { } } + private fun processPersonalMessages(snode: Snode, rawMessages: RawResponse) { + val messages = SnodeAPI.parseRawMessagesResponse(rawMessages, 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) + } + } + + private fun processConfig(snode: Snode, rawMessages: RawResponse, namespace: Int, forConfigObject: ConfigBase?) { + if (forConfigObject == null) return + + val messages = SnodeAPI.parseRawMessagesResponse( + rawMessages, + snode, + userPublicKey, + namespace, + updateLatestHash = true, + updateStoredHashes = true, + ) + + if (messages.isEmpty()) { + // no new messages to process + return + } + + var latestMessageTimestamp: Long? = null + messages.forEach { (envelope, hash) -> + try { + val (message, _) = MessageReceiver.parse(data = envelope.toByteArray(), + // assume no groups in personal poller messages + openGroupServerID = null, currentClosedGroups = emptySet() + ) + // sanity checks + if (message !is SharedConfigurationMessage) { + Log.w("Loki", "shared config message handled in configs wasn't SharedConfigurationMessage but was ${message.javaClass.simpleName}") + return@forEach + } + forConfigObject.merge(hash!! to message.data) + latestMessageTimestamp = if ((message.sentTimestamp ?: 0L) > (latestMessageTimestamp ?: 0L)) { message.sentTimestamp } else { latestMessageTimestamp } + } catch (e: Exception) { + Log.e("Loki", e) + } + } + // process new results + if (forConfigObject.needsDump()) { + configFactory.persist(forConfigObject, latestMessageTimestamp ?: SnodeAPI.nowWithOffset) + } + } + private fun poll(snode: Snode, deferred: Deferred): Promise { if (!hasStarted) { return Promise.ofFail(PromiseCanceledException()) } - return SnodeAPI.getRawMessages(snode, userPublicKey).bind { rawResponse -> - isCaughtUp = true - if (deferred.promise.isDone()) { - task { Unit } // The long polling connection has been canceled; don't recurse - } else { - val messages = SnodeAPI.parseRawMessagesResponse(rawResponse, snode, userPublicKey) - val parameters = messages.map { (envelope, serverHash) -> - MessageReceiveParameters(envelope.toByteArray(), serverHash = serverHash) + return task { + runBlocking(Dispatchers.IO) { + val requestSparseArray = SparseArray() + // get messages + SnodeAPI.buildAuthenticatedRetrieveBatchRequest(snode, userPublicKey, maxSize = -2)!!.also { personalMessages -> + // namespaces here should always be set + requestSparseArray[personalMessages.namespace!!] = personalMessages } - parameters.chunked(BatchMessageReceiveJob.BATCH_DEFAULT_NUMBER).forEach { chunk -> - val job = BatchMessageReceiveJob(chunk) - JobQueue.shared.add(job) + // get the latest convo info volatile + val hashesToExtend = mutableSetOf() + configFactory.getUserConfigs().mapNotNull { config -> + hashesToExtend += config.currentHashes() + SnodeAPI.buildAuthenticatedRetrieveBatchRequest( + snode, userPublicKey, + config.configNamespace(), + maxSize = -8 + ) + }.forEach { request -> + // namespaces here should always be set + requestSparseArray[request.namespace!!] = request } - poll(snode, deferred) + val requests = + requestSparseArray.valueIterator().asSequence().toMutableList() + + if (hashesToExtend.isNotEmpty()) { + SnodeAPI.buildAuthenticatedAlterTtlBatchRequest( + messageHashes = hashesToExtend.toList(), + publicKey = userPublicKey, + newExpiry = SnodeAPI.nowWithOffset + 14.days.inWholeMilliseconds, + extend = true + )?.let { extensionRequest -> + requests += extensionRequest + } + } + + SnodeAPI.getRawBatchResponse(snode, userPublicKey, requests).bind { rawResponses -> + isCaughtUp = true + if (deferred.promise.isDone()) { + return@bind Promise.ofSuccess(Unit) + } else { + val responseList = (rawResponses["results"] as List) + // 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 + // TODO: add in specific ordering of config namespaces for processing + listOfNotNull( + configFactory.user?.configNamespace(), + configFactory.contacts?.configNamespace(), + configFactory.userGroups?.configNamespace(), + configFactory.convoVolatile?.configNamespace() + ).map { + it to requestSparseArray.indexOfKey(it) + }.filter { (_, i) -> i >= 0 }.forEach { (key, requestIndex) -> + responseList.getOrNull(requestIndex)?.let { rawResponse -> + if (rawResponse["code"] as? Int != 200) { + Log.e("Loki", "Batch sub-request had non-200 response code, returned code ${(rawResponse["code"] as? Int) ?: "[unknown]"}") + return@forEach + } + val body = rawResponse["body"] as? RawResponse + if (body == null) { + Log.e("Loki", "Batch sub-request didn't contain a body") + return@forEach + } + if (key == Namespace.DEFAULT) { + return@forEach // continue, skip default namespace + } else { + when (ConfigBase.kindFor(key)) { + UserProfile::class.java -> processConfig(snode, body, key, configFactory.user) + Contacts::class.java -> processConfig(snode, body, key, configFactory.contacts) + ConversationVolatileConfig::class.java -> processConfig(snode, body, key, configFactory.convoVolatile) + UserGroupsConfig::class.java -> processConfig(snode, body, key, configFactory.userGroups) + } + } + } + } + + // the first response will be the personal messages (we want these to be processed after config messages) + val personalResponseIndex = requestSparseArray.indexOfKey(Namespace.DEFAULT) + if (personalResponseIndex >= 0) { + responseList.getOrNull(personalResponseIndex)?.let { rawResponse -> + if (rawResponse["code"] as? Int != 200) { + Log.e("Loki", "Batch sub-request for personal messages had non-200 response code, returned code ${(rawResponse["code"] as? Int) ?: "[unknown]"}") + } else { + val body = rawResponse["body"] as? RawResponse + if (body == null) { + Log.e("Loki", "Batch sub-request for personal messages didn't contain a body") + } else { + processPersonalMessages(snode, body) + } + } + } + } + + poll(snode, deferred) + } + }.fail { + Log.e("Loki", "Failed to get raw batch response", it) + poll(snode, deferred) + } } } } diff --git a/libsession/src/main/java/org/session/libsession/messaging/utilities/UpdateMessageBuilder.kt b/libsession/src/main/java/org/session/libsession/messaging/utilities/UpdateMessageBuilder.kt index 35328b974..e4db056d8 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/utilities/UpdateMessageBuilder.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/utilities/UpdateMessageBuilder.kt @@ -74,6 +74,7 @@ object UpdateMessageBuilder { context.getString(R.string.ConversationItem_group_action_left, senderName) } } + is UpdateMessageData.Kind.OpenGroupInvitation -> { /*Handled externally*/ } } return message } diff --git a/libsession/src/main/java/org/session/libsession/snode/OnionRequestAPI.kt b/libsession/src/main/java/org/session/libsession/snode/OnionRequestAPI.kt index 087c8e29d..8851dfc2b 100644 --- a/libsession/src/main/java/org/session/libsession/snode/OnionRequestAPI.kt +++ b/libsession/src/main/java/org/session/libsession/snode/OnionRequestAPI.kt @@ -419,6 +419,8 @@ object OnionRequestAPI { Log.d("Loki","Destination server returned ${exception.statusCode}") } else if (message == "Loki Server error") { Log.d("Loki", "message was $message") + } else if (exception.statusCode == 404) { + // 404 is probably file server missing a file, don't rebuild path or mark a snode as bad here } else { // Only drop snode/path if not receiving above two exception cases handleUnspecificError() } @@ -446,8 +448,8 @@ object OnionRequestAPI { val payloadData = JsonUtil.toJson(payload).toByteArray() return sendOnionRequest(Destination.Snode(snode), payloadData, version).recover { exception -> val error = when (exception) { - is HTTP.HTTPRequestFailedException -> SnodeAPI.handleSnodeError(exception.statusCode, exception.json, snode, publicKey) is HTTPRequestFailedAtDestinationException -> SnodeAPI.handleSnodeError(exception.statusCode, exception.json, snode, publicKey) + is HTTP.HTTPRequestFailedException -> SnodeAPI.handleSnodeError(exception.statusCode, exception.json, snode, publicKey) else -> null } if (error != null) { throw error } diff --git a/libsession/src/main/java/org/session/libsession/snode/SnodeAPI.kt b/libsession/src/main/java/org/session/libsession/snode/SnodeAPI.kt index ebd66d3a3..b1a274773 100644 --- a/libsession/src/main/java/org/session/libsession/snode/SnodeAPI.kt +++ b/libsession/src/main/java/org/session/libsession/snode/SnodeAPI.kt @@ -28,12 +28,12 @@ import org.session.libsignal.utilities.HTTP import org.session.libsignal.utilities.Hex import org.session.libsignal.utilities.JsonUtil import org.session.libsignal.utilities.Log +import org.session.libsignal.utilities.Namespace import org.session.libsignal.utilities.Snode import org.session.libsignal.utilities.ThreadUtils import org.session.libsignal.utilities.prettifiedDescription import org.session.libsignal.utilities.retryIfNeeded import java.security.SecureRandom -import java.util.Date import java.util.Locale import kotlin.collections.component1 import kotlin.collections.component2 @@ -102,6 +102,14 @@ object SnodeAPI { object ValidationFailed : Error("ONS name validation failed.") } + // Batch + data class SnodeBatchRequestInfo( + val method: String, + val params: Map, + @Transient + val namespace: Int? + ) // assume signatures, pubkey and namespaces are attached in parameters if required + // Internal API internal fun invoke( method: Snode.Method, @@ -319,26 +327,32 @@ object SnodeAPI { fun getRawMessages(snode: Snode, publicKey: String, requiresAuth: Boolean = true, namespace: Int = 0): RawResponsePromise { // Get last message hash val lastHashValue = database.getLastMessageHashValue(snode, publicKey, namespace) ?: "" - val parameters = mutableMapOf( + val parameters = mutableMapOf( "pubKey" to publicKey, "last_hash" to lastHashValue, ) // Construct signature if (requiresAuth) { val userED25519KeyPair = try { - MessagingModuleConfiguration.shared.getUserED25519KeyPair() ?: return Promise.ofFail(Error.NoKeyPair) + MessagingModuleConfiguration.shared.getUserED25519KeyPair() + ?: return Promise.ofFail(Error.NoKeyPair) } catch (e: Exception) { Log.e("Loki", "Error getting KeyPair", e) return Promise.ofFail(Error.NoKeyPair) } - val timestamp = Date().time + SnodeAPI.clockOffset + val timestamp = System.currentTimeMillis() + clockOffset val ed25519PublicKey = userED25519KeyPair.publicKey.asHexString val signature = ByteArray(Sign.BYTES) val verificationData = if (namespace != 0) "retrieve$namespace$timestamp".toByteArray() else "retrieve$timestamp".toByteArray() try { - sodium.cryptoSignDetached(signature, verificationData, verificationData.size.toLong(), userED25519KeyPair.secretKey.asBytes) + sodium.cryptoSignDetached( + signature, + verificationData, + verificationData.size.toLong(), + userED25519KeyPair.secretKey.asBytes + ) } catch (exception: Exception) { return Promise.ofFail(Error.SigningFailed) } @@ -354,7 +368,251 @@ object SnodeAPI { } // Make the request - return invoke(Snode.Method.GetMessages, snode, parameters, publicKey) + return invoke(Snode.Method.Retrieve, snode, parameters, publicKey) + } + + fun buildAuthenticatedStoreBatchInfo(publicKey: String, namespace: Int, message: SnodeMessage): SnodeBatchRequestInfo? { + val params = mutableMapOf() + // 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, 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.DeleteMessage.rawValue, + params, + null + ) + } + + fun buildAuthenticatedRetrieveBatchRequest(snode: Snode, publicKey: String, namespace: Int = 0, maxSize: Int? = null): SnodeBatchRequestInfo? { + val lastHashValue = database.getLastMessageHashValue(snode, publicKey, namespace) ?: "" + val params = mutableMapOf( + "pubkey" to publicKey, + "last_hash" to lastHashValue, + ) + val userEd25519KeyPair = try { + MessagingModuleConfiguration.shared.getUserED25519KeyPair() ?: return null + } catch (e: Exception) { + return null + } + val ed25519PublicKey = userEd25519KeyPair.publicKey.asHexString + val timestamp = System.currentTimeMillis() + clockOffset + val signature = ByteArray(Sign.BYTES) + val verificationData = if (namespace == 0) "retrieve$timestamp".toByteArray() + else "retrieve$namespace$timestamp".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["timestamp"] = timestamp + params["pubkey_ed25519"] = ed25519PublicKey + params["signature"] = Base64.encodeBytes(signature) + if (namespace != 0) { + params["namespace"] = namespace + } + if (maxSize != null) { + params["max_size"] = maxSize + } + return SnodeBatchRequestInfo( + Snode.Method.Retrieve.rawValue, + params, + namespace + ) + } + + fun buildAuthenticatedAlterTtlBatchRequest( + messageHashes: List, + newExpiry: Long, + publicKey: String, + shorten: Boolean = false, + extend: Boolean = false): SnodeBatchRequestInfo? { + val params = buildAlterTtlParams(messageHashes, newExpiry, publicKey, extend, shorten) ?: return null + return SnodeBatchRequestInfo( + Snode.Method.Expire.rawValue, + params, + null + ) + } + + fun getRawBatchResponse(snode: Snode, publicKey: String, requests: List, sequence: Boolean = false): RawResponsePromise { + val parameters = mutableMapOf( + "requests" to requests + ) + return invoke(if (sequence) Snode.Method.Sequence else Snode.Method.Batch, snode, parameters, publicKey).success { rawResponses -> + val responseList = (rawResponses["results"] as List) + responseList.forEachIndexed { index, response -> + if (response["code"] as? Int != 200) { + Log.w("Loki", "response code was not 200") + handleSnodeError( + response["code"] as? Int ?: 0, + response, + snode, + publicKey + ) + } + } + } + } + + fun getExpiries(messageHashes: List, publicKey: String) : RawResponsePromise { + val userEd25519KeyPair = MessagingModuleConfiguration.shared.getUserED25519KeyPair() ?: return Promise.ofFail(NullPointerException("No user key pair")) + return retryIfNeeded(maxRetryCount) { + val timestamp = System.currentTimeMillis() + clockOffset + val params = mutableMapOf( + "pubkey" to publicKey, + "messages" to messageHashes, + "timestamp" to timestamp + ) + val signData = "${Snode.Method.GetExpiries.rawValue}$timestamp${messageHashes.joinToString(separator = "")}".toByteArray() + + val ed25519PublicKey = userEd25519KeyPair.publicKey.asHexString + val signature = ByteArray(Sign.BYTES) + try { + sodium.cryptoSignDetached( + signature, + signData, + signData.size.toLong(), + userEd25519KeyPair.secretKey.asBytes + ) + } catch (e: Exception) { + Log.e("Loki", "Signing data failed with user secret key", e) + return@retryIfNeeded Promise.ofFail(e) + } + params["pubkey_ed25519"] = ed25519PublicKey + params["signature"] = Base64.encodeBytes(signature) + getSingleTargetSnode(publicKey).bind { snode -> + invoke(Snode.Method.GetExpiries, snode, params, publicKey) + } + } + } + + fun alterTtl(messageHashes: List, newExpiry: Long, publicKey: String, extend: Boolean = false, shorten: Boolean = false): RawResponsePromise { + return retryIfNeeded(maxRetryCount) { + val params = buildAlterTtlParams(messageHashes, newExpiry, publicKey, extend, shorten) + ?: return@retryIfNeeded Promise.ofFail( + Exception("Couldn't build signed params for alterTtl request for newExpiry=$newExpiry, extend=$extend, shorten=$shorten") + ) + getSingleTargetSnode(publicKey).bind { snode -> + invoke(Snode.Method.Expire, snode, params, publicKey) + } + } + } + + private fun buildAlterTtlParams( // TODO: in future this will probably need to use the closed group subkeys / admin keys for group swarms + messageHashes: List, + newExpiry: Long, + publicKey: String, + extend: Boolean = false, + shorten: Boolean = false): Map? { + val userEd25519KeyPair = MessagingModuleConfiguration.shared.getUserED25519KeyPair() ?: return null + val params = mutableMapOf( + "expiry" to newExpiry, + "messages" to messageHashes, + ) + if (extend) { + params["extend"] = true + } else if (shorten) { + params["shorten"] = true + } + val shortenOrExtend = if (extend) "extend" else if (shorten) "shorten" else "" + + val signData = "${Snode.Method.Expire.rawValue}$shortenOrExtend$newExpiry${messageHashes.joinToString(separator = "")}".toByteArray() + + val ed25519PublicKey = userEd25519KeyPair.publicKey.asHexString + val signature = ByteArray(Sign.BYTES) + try { + sodium.cryptoSignDetached( + signature, + signData, + signData.size.toLong(), + userEd25519KeyPair.secretKey.asBytes + ) + } catch (e: Exception) { + Log.e("Loki", "Signing data failed with user secret key", e) + return null + } + params["pubkey"] = publicKey + params["pubkey_ed25519"] = ed25519PublicKey + params["signature"] = Base64.encodeBytes(signature) + + return params } fun getMessages(publicKey: String): MessageListPromise { @@ -483,13 +741,14 @@ object SnodeAPI { retryIfNeeded(maxRetryCount) { getNetworkTime(snode).bind { (_, timestamp) -> val signature = ByteArray(Sign.BYTES) - val verificationData = (Snode.Method.DeleteAll.rawValue + timestamp.toString()).toByteArray() + val verificationData = (Snode.Method.DeleteAll.rawValue + Namespace.ALL + timestamp.toString()).toByteArray() sodium.cryptoSignDetached(signature, verificationData, verificationData.size.toLong(), userED25519KeyPair.secretKey.asBytes) val deleteMessageParams = mapOf( "pubkey" to userPublicKey, "pubkey_ed25519" to userED25519KeyPair.publicKey.asHexString, "timestamp" to timestamp, - "signature" to Base64.encodeBytes(signature) + "signature" to Base64.encodeBytes(signature), + "namespace" to Namespace.ALL, ) invoke(Snode.Method.DeleteAll, snode, deleteMessageParams, userPublicKey).map { rawResponse -> parseDeletions(userPublicKey, timestamp, rawResponse) @@ -502,11 +761,13 @@ object SnodeAPI { } } - fun parseRawMessagesResponse(rawResponse: RawResponse, snode: Snode, publicKey: String, namespace: Int = 0): List> { + fun parseRawMessagesResponse(rawResponse: RawResponse, snode: Snode, publicKey: String, namespace: Int = 0, updateLatestHash: Boolean = true, updateStoredHashes: Boolean = true): List> { val messages = rawResponse["messages"] as? List<*> return if (messages != null) { - updateLastMessageHashValueIfPossible(snode, publicKey, messages, namespace) - val newRawMessages = removeDuplicates(publicKey, messages, namespace) + if (updateLatestHash) { + updateLastMessageHashValueIfPossible(snode, publicKey, messages, namespace) + } + val newRawMessages = removeDuplicates(publicKey, messages, namespace, updateStoredHashes) return parseEnvelopes(newRawMessages) } else { listOf() @@ -523,7 +784,7 @@ object SnodeAPI { } } - private fun removeDuplicates(publicKey: String, rawMessages: List<*>, namespace: Int): List<*> { + private fun removeDuplicates(publicKey: String, rawMessages: List<*>, namespace: Int, updateStoredHashes: Boolean): List<*> { val originalMessageHashValues = database.getReceivedMessageHashValues(publicKey, namespace)?.toMutableSet() ?: mutableSetOf() val receivedMessageHashValues = originalMessageHashValues.toMutableSet() val result = rawMessages.filter { rawMessage -> @@ -538,7 +799,7 @@ object SnodeAPI { false } } - if (originalMessageHashValues != receivedMessageHashValues) { + if (originalMessageHashValues != receivedMessageHashValues && updateStoredHashes) { database.setReceivedMessageHashValues(publicKey, receivedMessageHashValues, namespace) } return result @@ -575,11 +836,11 @@ object SnodeAPI { Log.e("Loki", "Failed to delete all messages from: $hexSnodePublicKey due to error: $reason ($statusCode).") false } else { - val hashes = json["deleted"] as List // Hashes of deleted messages + val hashes = (json["deleted"] as Map>).flatMap { (_, hashes) -> hashes }.sorted() // Hashes of deleted messages val signature = json["signature"] as String val snodePublicKey = Key.fromHexString(hexSnodePublicKey) // The signature looks like ( PUBKEY_HEX || TIMESTAMP || DELETEDHASH[0] || ... || DELETEDHASH[N] ) - val message = (userPublicKey + timestamp.toString() + hashes.fold("") { a, v -> a + v }).toByteArray() + val message = (userPublicKey + timestamp.toString() + hashes.joinToString(separator = "")).toByteArray() sodium.cryptoSignVerifyDetached(Base64.decode(signature), message, message.size, snodePublicKey.asBytes) } } @@ -635,6 +896,10 @@ object SnodeAPI { Log.d("Loki", "Got a 421 without an associated public key.") } } + 404 -> { + Log.d("Loki", "404, probably no file found") + return Error.Generic + } else -> { handleBadSnode() Log.d("Loki", "Unhandled response code: ${statusCode}.") diff --git a/libsession/src/main/java/org/session/libsession/utilities/Address.kt b/libsession/src/main/java/org/session/libsession/utilities/Address.kt index 7b774602e..c8cd11d4b 100644 --- a/libsession/src/main/java/org/session/libsession/utilities/Address.kt +++ b/libsession/src/main/java/org/session/libsession/utilities/Address.kt @@ -5,11 +5,11 @@ import android.os.Parcel import android.os.Parcelable import android.util.Pair import androidx.annotation.VisibleForTesting -import org.session.libsession.utilities.DelimiterUtil -import org.session.libsession.utilities.GroupUtil -import org.session.libsignal.utilities.guava.Optional +import org.session.libsignal.utilities.IdPrefix import org.session.libsignal.utilities.Util -import java.util.* +import org.session.libsignal.utilities.guava.Optional +import java.util.Collections +import java.util.LinkedList import java.util.concurrent.atomic.AtomicReference import java.util.regex.Matcher import java.util.regex.Pattern @@ -27,6 +27,8 @@ class Address private constructor(address: String) : Parcelable, Comparable + fun persist(forConfigObject: ConfigBase, timestamp: Long) + + fun conversationInConfig(publicKey: String?, groupPublicKey: String?, openGroupId: String?, visibleOnly: Boolean): Boolean + fun canPerformChange(variant: String, publicKey: String, changeTimestampMs: Long): Boolean +} + +interface ConfigFactoryUpdateListener { + fun notifyUpdates(forConfigObject: ConfigBase) +} \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/utilities/DownloadUtilities.kt b/libsession/src/main/java/org/session/libsession/utilities/DownloadUtilities.kt index b850baa25..27b6b244b 100644 --- a/libsession/src/main/java/org/session/libsession/utilities/DownloadUtilities.kt +++ b/libsession/src/main/java/org/session/libsession/utilities/DownloadUtilities.kt @@ -4,7 +4,9 @@ import okhttp3.HttpUrl import org.session.libsession.messaging.file_server.FileServerApi import org.session.libsignal.utilities.HTTP import org.session.libsignal.utilities.Log -import java.io.* +import java.io.File +import java.io.FileOutputStream +import java.io.OutputStream object DownloadUtilities { @@ -14,7 +16,7 @@ object DownloadUtilities { @JvmStatic fun downloadFile(destination: File, url: String) { val outputStream = FileOutputStream(destination) // Throws - var remainingAttempts = 4 + var remainingAttempts = 2 var exception: Exception? = null while (remainingAttempts > 0) { remainingAttempts -= 1 diff --git a/libsession/src/main/java/org/session/libsession/utilities/GroupUtil.kt b/libsession/src/main/java/org/session/libsession/utilities/GroupUtil.kt index 3458e06eb..bfab2585d 100644 --- a/libsession/src/main/java/org/session/libsession/utilities/GroupUtil.kt +++ b/libsession/src/main/java/org/session/libsession/utilities/GroupUtil.kt @@ -1,9 +1,9 @@ package org.session.libsession.utilities +import network.loki.messenger.libsession_util.util.GroupInfo import org.session.libsignal.messages.SignalServiceGroup import org.session.libsignal.utilities.Hex import java.io.IOException -import kotlin.jvm.Throws object GroupUtil { const val CLOSED_GROUP_PREFIX = "__textsecure_group__!" @@ -97,4 +97,28 @@ object GroupUtil { fun doubleDecodeGroupID(groupID: String): ByteArray { return getDecodedGroupIDAsData(getDecodedGroupID(groupID)) } + + @JvmStatic + @Throws(IOException::class) + fun doubleDecodeGroupId(groupID: String): String { + return Hex.toStringCondensed(getDecodedGroupIDAsData(getDecodedGroupID(groupID))) + } + + fun createConfigMemberMap( + members: Collection, + admins: Collection + ): Map { + // Start with admins + val memberMap = admins.associate { + it to true + }.toMutableMap() + + // Add the remaining members (there may be duplicates, so only add ones that aren't already in there from admins) + for (member in members) { + if (!memberMap.contains(member)) { + memberMap[member] = false + } + } + return memberMap + } } \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/utilities/ProfileKeyUtil.java b/libsession/src/main/java/org/session/libsession/utilities/ProfileKeyUtil.java index 9e3842fc6..4550965ae 100644 --- a/libsession/src/main/java/org/session/libsession/utilities/ProfileKeyUtil.java +++ b/libsession/src/main/java/org/session/libsession/utilities/ProfileKeyUtil.java @@ -1,23 +1,24 @@ package org.session.libsession.utilities; import android.content.Context; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; import org.session.libsignal.utilities.Base64; -import org.session.libsession.utilities.TextSecurePreferences; -import org.session.libsession.utilities.Util; import java.io.IOException; public class ProfileKeyUtil { + public static final int PROFILE_KEY_BYTES = 32; + public static synchronized @NonNull byte[] getProfileKey(@NonNull Context context) { try { String encodedProfileKey = TextSecurePreferences.getProfileKey(context); if (encodedProfileKey == null) { - encodedProfileKey = Util.getSecret(32); + encodedProfileKey = Util.getSecret(PROFILE_KEY_BYTES); TextSecurePreferences.setProfileKey(context, encodedProfileKey); } @@ -36,7 +37,7 @@ public class ProfileKeyUtil { } public static synchronized @NonNull String generateEncodedProfileKey(@NonNull Context context) { - return Util.getSecret(32); + return Util.getSecret(PROFILE_KEY_BYTES); } public static synchronized void setEncodedProfileKey(@NonNull Context context, @Nullable String key) { diff --git a/libsession/src/main/java/org/session/libsession/utilities/SSKEnvironment.kt b/libsession/src/main/java/org/session/libsession/utilities/SSKEnvironment.kt index b750b3940..f647cc0f4 100644 --- a/libsession/src/main/java/org/session/libsession/utilities/SSKEnvironment.kt +++ b/libsession/src/main/java/org/session/libsession/utilities/SSKEnvironment.kt @@ -1,9 +1,9 @@ package org.session.libsession.utilities import android.content.Context +import org.session.libsession.messaging.contacts.Contact 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( @@ -30,10 +30,10 @@ 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 setName(context: Context, recipient: Recipient, name: String?) + fun setProfilePicture(context: Context, recipient: Recipient, profilePictureURL: String?, profileKey: ByteArray?) fun setUnidentifiedAccessMode(context: Context, recipient: Recipient, unidentifiedAccessMode: Recipient.UnidentifiedAccessMode) + fun contactUpdatedInternal(contact: Contact): String? } interface MessageExpirationManagerProtocol { diff --git a/libsession/src/main/java/org/session/libsession/utilities/TextSecurePreferences.kt b/libsession/src/main/java/org/session/libsession/utilities/TextSecurePreferences.kt index 807c40b43..d6ed96373 100644 --- a/libsession/src/main/java/org/session/libsession/utilities/TextSecurePreferences.kt +++ b/libsession/src/main/java/org/session/libsession/utilities/TextSecurePreferences.kt @@ -12,7 +12,6 @@ import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.asSharedFlow -import org.session.libsession.BuildConfig import org.session.libsession.R import org.session.libsession.utilities.TextSecurePreferences.Companion.AUTOPLAY_AUDIO_MESSAGES import org.session.libsession.utilities.TextSecurePreferences.Companion.CALL_NOTIFICATIONS_ENABLED @@ -103,6 +102,8 @@ interface TextSecurePreferences { fun setUpdateApkDigest(value: String?) fun getUpdateApkDigest(): String? fun getLocalNumber(): String? + fun getHasLegacyConfig(): Boolean + fun setHasLegacyConfig(newValue: Boolean) fun setLocalNumber(localNumber: String) fun removeLocalNumber() fun isEnterSendsEnabled(): Boolean @@ -178,6 +179,7 @@ interface TextSecurePreferences { fun setThemeStyle(themeStyle: String) fun setFollowSystemSettings(followSystemSettings: Boolean) fun autoplayAudioMessages(): Boolean + fun hasForcedNewConfig(): Boolean fun hasPreference(key: String): Boolean fun clearAll() @@ -264,6 +266,10 @@ interface TextSecurePreferences { const val AUTOPLAY_AUDIO_MESSAGES = "pref_autoplay_audio" const val FINGERPRINT_KEY_GENERATED = "fingerprint_key_generated" const val SELECTED_ACCENT_COLOR = "selected_accent_color" + + const val HAS_RECEIVED_LEGACY_CONFIG = "has_received_legacy_config" + const val HAS_FORCED_NEW_CONFIG = "has_forced_new_config" + const val GREEN_ACCENT = "accent_green" const val BLUE_ACCENT = "accent_blue" const val PURPLE_ACCENT = "accent_purple" @@ -625,6 +631,17 @@ interface TextSecurePreferences { return getStringPreference(context, LOCAL_NUMBER_PREF, null) } + @JvmStatic + fun getHasLegacyConfig(context: Context): Boolean { + return getBooleanPreference(context, HAS_RECEIVED_LEGACY_CONFIG, false) + } + + @JvmStatic + fun setHasLegacyConfig(context: Context, newValue: Boolean) { + setBooleanPreference(context, HAS_RECEIVED_LEGACY_CONFIG, newValue) + _events.tryEmit(HAS_RECEIVED_LEGACY_CONFIG) + } + fun setLocalNumber(context: Context, localNumber: String) { setStringPreference(context, LOCAL_NUMBER_PREF, localNumber.toLowerCase()) } @@ -795,6 +812,11 @@ interface TextSecurePreferences { setIntegerPreference(context, NOTIFICATION_MESSAGES_CHANNEL_VERSION, version) } + @JvmStatic + fun hasForcedNewConfig(context: Context): Boolean { + return getBooleanPreference(context, HAS_FORCED_NEW_CONFIG, false) + } + @JvmStatic fun getBooleanPreference(context: Context, key: String?, defaultValue: Boolean): Boolean { return getDefaultSharedPreferences(context).getBoolean(key, defaultValue) @@ -1279,6 +1301,15 @@ class AppTextSecurePreferences @Inject constructor( return getStringPreference(TextSecurePreferences.LOCAL_NUMBER_PREF, null) } + override fun getHasLegacyConfig(): Boolean { + return getBooleanPreference(TextSecurePreferences.HAS_RECEIVED_LEGACY_CONFIG, false) + } + + override fun setHasLegacyConfig(newValue: Boolean) { + setBooleanPreference(TextSecurePreferences.HAS_RECEIVED_LEGACY_CONFIG, newValue) + TextSecurePreferences._events.tryEmit(TextSecurePreferences.HAS_RECEIVED_LEGACY_CONFIG) + } + override fun setLocalNumber(localNumber: String) { setStringPreference(TextSecurePreferences.LOCAL_NUMBER_PREF, localNumber.toLowerCase()) } @@ -1422,6 +1453,9 @@ class AppTextSecurePreferences @Inject constructor( setIntegerPreference(TextSecurePreferences.NOTIFICATION_MESSAGES_CHANNEL_VERSION, version) } + override fun hasForcedNewConfig(): Boolean = + getBooleanPreference(TextSecurePreferences.HAS_FORCED_NEW_CONFIG, false) + override fun getBooleanPreference(key: String?, defaultValue: Boolean): Boolean { return getDefaultSharedPreferences(context).getBoolean(key, defaultValue) } diff --git a/libsession/src/main/java/org/session/libsession/utilities/recipients/Recipient.java b/libsession/src/main/java/org/session/libsession/utilities/recipients/Recipient.java index a7fa75dd2..e2d193a93 100644 --- a/libsession/src/main/java/org/session/libsession/utilities/recipients/Recipient.java +++ b/libsession/src/main/java/org/session/libsession/utilities/recipients/Recipient.java @@ -99,6 +99,7 @@ public class Recipient implements RecipientModifiedListener { private boolean profileSharing; private String notificationChannel; private boolean forceSmsSelection; + private String wrapperHash; private @NonNull UnidentifiedAccessMode unidentifiedAccessMode = UnidentifiedAccessMode.ENABLED; @@ -279,6 +280,7 @@ public class Recipient implements RecipientModifiedListener { this.profileSharing = details.profileSharing; this.unidentifiedAccessMode = details.unidentifiedAccessMode; this.forceSmsSelection = details.forceSmsSelection; + this.wrapperHash = details.wrapperHash; this.participants.addAll(details.participants); this.resolving = false; @@ -325,7 +327,7 @@ public class Recipient implements RecipientModifiedListener { return contact.displayName(Contact.ContactContext.REGULAR); } else { Contact contact = storage.getContactWithSessionID(sessionID); - if (contact == null) { return sessionID; } + if (contact == null) { return null; } return contact.displayName(Contact.ContactContext.REGULAR); } } @@ -440,6 +442,10 @@ public class Recipient implements RecipientModifiedListener { return address.isOpenGroup(); } + public boolean isOpenGroupOutboxRecipient() { + return address.isOpenGroupOutbox(); + } + public boolean isOpenGroupInboxRecipient() { return address.isOpenGroupInbox(); } @@ -483,7 +489,13 @@ public class Recipient implements RecipientModifiedListener { public synchronized String toShortString() { String name = getName(); - return (name != null ? name : address.serialize()); + if (name != null) return name; + String sessionId = address.serialize(); + if (sessionId.length() < 4) return sessionId; // so substrings don't throw out of bounds exceptions + int takeAmount = 4; + String start = sessionId.substring(0, takeAmount); + String end = sessionId.substring(sessionId.length()-takeAmount); + return start+"..."+end; } public synchronized @NonNull Drawable getFallbackContactPhotoDrawable(Context context, boolean inverted) { @@ -717,6 +729,14 @@ public class Recipient implements RecipientModifiedListener { return unidentifiedAccessMode; } + public String getWrapperHash() { + return wrapperHash; + } + + public void setWrapperHash(String wrapperHash) { + this.wrapperHash = wrapperHash; + } + public void setUnidentifiedAccessMode(@NonNull UnidentifiedAccessMode unidentifiedAccessMode) { synchronized (this) { this.unidentifiedAccessMode = unidentifiedAccessMode; @@ -739,12 +759,12 @@ public class Recipient implements RecipientModifiedListener { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Recipient recipient = (Recipient) o; - return resolving == recipient.resolving && mutedUntil == recipient.mutedUntil && notifyType == recipient.notifyType && blocked == recipient.blocked && approved == recipient.approved && approvedMe == recipient.approvedMe && expireMessages == recipient.expireMessages && address.equals(recipient.address) && Objects.equals(name, recipient.name) && Objects.equals(customLabel, recipient.customLabel) && Objects.equals(groupAvatarId, recipient.groupAvatarId) && Arrays.equals(profileKey, recipient.profileKey) && Objects.equals(profileName, recipient.profileName) && Objects.equals(profileAvatar, recipient.profileAvatar); + return resolving == recipient.resolving && mutedUntil == recipient.mutedUntil && notifyType == recipient.notifyType && blocked == recipient.blocked && approved == recipient.approved && approvedMe == recipient.approvedMe && expireMessages == recipient.expireMessages && address.equals(recipient.address) && Objects.equals(name, recipient.name) && Objects.equals(customLabel, recipient.customLabel) && Objects.equals(groupAvatarId, recipient.groupAvatarId) && Arrays.equals(profileKey, recipient.profileKey) && Objects.equals(profileName, recipient.profileName) && Objects.equals(profileAvatar, recipient.profileAvatar) && Objects.equals(wrapperHash, recipient.wrapperHash); } @Override public int hashCode() { - int result = Objects.hash(address, name, customLabel, resolving, groupAvatarId, mutedUntil, notifyType, blocked, approved, approvedMe, expireMessages, profileName, profileAvatar); + int result = Objects.hash(address, name, customLabel, resolving, groupAvatarId, mutedUntil, notifyType, blocked, approved, approvedMe, expireMessages, profileName, profileAvatar, wrapperHash); result = 31 * result + Arrays.hashCode(profileKey); return result; } @@ -848,6 +868,7 @@ public class Recipient implements RecipientModifiedListener { private final String notificationChannel; private final UnidentifiedAccessMode unidentifiedAccessMode; private final boolean forceSmsSelection; + private final String wrapperHash; public RecipientSettings(boolean blocked, boolean approved, boolean approvedMe, long muteUntil, int notifyType, @@ -869,7 +890,8 @@ public class Recipient implements RecipientModifiedListener { boolean profileSharing, @Nullable String notificationChannel, @NonNull UnidentifiedAccessMode unidentifiedAccessMode, - boolean forceSmsSelection) + boolean forceSmsSelection, + String wrapperHash) { this.blocked = blocked; this.approved = approved; @@ -895,6 +917,7 @@ public class Recipient implements RecipientModifiedListener { this.notificationChannel = notificationChannel; this.unidentifiedAccessMode = unidentifiedAccessMode; this.forceSmsSelection = forceSmsSelection; + this.wrapperHash = wrapperHash; } public @Nullable MaterialColor getColor() { @@ -992,6 +1015,11 @@ public class Recipient implements RecipientModifiedListener { public boolean isForceSmsSelection() { return forceSmsSelection; } + + public String getWrapperHash() { + return wrapperHash; + } + } diff --git a/libsession/src/main/java/org/session/libsession/utilities/recipients/RecipientProvider.java b/libsession/src/main/java/org/session/libsession/utilities/recipients/RecipientProvider.java index 03c225e20..75ebd837b 100644 --- a/libsession/src/main/java/org/session/libsession/utilities/recipients/RecipientProvider.java +++ b/libsession/src/main/java/org/session/libsession/utilities/recipients/RecipientProvider.java @@ -177,6 +177,7 @@ class RecipientProvider { @Nullable final String notificationChannel; @NonNull final UnidentifiedAccessMode unidentifiedAccessMode; final boolean forceSmsSelection; + final String wrapperHash; RecipientDetails(@Nullable String name, @Nullable Long groupAvatarId, boolean systemContact, boolean isLocalNumber, @Nullable RecipientSettings settings, @@ -209,6 +210,7 @@ class RecipientProvider { this.notificationChannel = settings != null ? settings.getNotificationChannel() : null; this.unidentifiedAccessMode = settings != null ? settings.getUnidentifiedAccessMode() : UnidentifiedAccessMode.DISABLED; this.forceSmsSelection = settings != null && settings.isForceSmsSelection(); + this.wrapperHash = settings != null ? settings.getWrapperHash() : null; if (name == null && settings != null) this.name = settings.getSystemDisplayName(); else this.name = name; diff --git a/libsession/src/main/res/values/attrs.xml b/libsession/src/main/res/values/attrs.xml index 59e987c54..64fab0950 100644 --- a/libsession/src/main/res/values/attrs.xml +++ b/libsession/src/main/res/values/attrs.xml @@ -220,18 +220,6 @@ - - - - - - - - - - - - diff --git a/libsession/src/test/java/org/session/libsession/utilities/OpenGroupUrlParserTest.kt b/libsession/src/test/java/org/session/libsession/utilities/CommunityUrlParserTest.kt similarity index 98% rename from libsession/src/test/java/org/session/libsession/utilities/OpenGroupUrlParserTest.kt rename to libsession/src/test/java/org/session/libsession/utilities/CommunityUrlParserTest.kt index 38a244699..64d1c21fb 100644 --- a/libsession/src/test/java/org/session/libsession/utilities/OpenGroupUrlParserTest.kt +++ b/libsession/src/test/java/org/session/libsession/utilities/CommunityUrlParserTest.kt @@ -1,9 +1,9 @@ package org.session.libsession.utilities +import org.junit.Assert.assertEquals import org.junit.Test -import org.junit.Assert.* -class OpenGroupUrlParserTest { +class CommunityUrlParserTest { @Test fun parseUrlTest() { diff --git a/libsignal/protobuf/SignalService.proto b/libsignal/protobuf/SignalService.proto index 50c521833..68dd35ce6 100644 --- a/libsignal/protobuf/SignalService.proto +++ b/libsignal/protobuf/SignalService.proto @@ -51,6 +51,7 @@ message Content { optional DataExtractionNotification dataExtractionNotification = 8; optional UnsendRequest unsendRequest = 9; optional MessageRequestResponse messageRequestResponse = 10; + optional SharedConfigMessage sharedConfigMessage = 11; } message KeyPair { @@ -238,6 +239,25 @@ message MessageRequestResponse { optional DataMessage.LokiProfile profile = 3; } +message SharedConfigMessage { + enum Kind { + USER_PROFILE = 1; + CONTACTS = 2; + CONVO_INFO_VOLATILE = 3; + 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 { enum Type { diff --git a/libsignal/src/main/java/org/session/libsignal/protos/SignalServiceProtos.java b/libsignal/src/main/java/org/session/libsignal/protos/SignalServiceProtos.java index 7c44087f8..8e26b05d9 100644 --- a/libsignal/src/main/java/org/session/libsignal/protos/SignalServiceProtos.java +++ b/libsignal/src/main/java/org/session/libsignal/protos/SignalServiceProtos.java @@ -2468,6 +2468,20 @@ public final class SignalServiceProtos { * optional .signalservice.MessageRequestResponse messageRequestResponse = 10; */ org.session.libsignal.protos.SignalServiceProtos.MessageRequestResponseOrBuilder getMessageRequestResponseOrBuilder(); + + // optional .signalservice.SharedConfigMessage sharedConfigMessage = 11; + /** + * optional .signalservice.SharedConfigMessage sharedConfigMessage = 11; + */ + boolean hasSharedConfigMessage(); + /** + * optional .signalservice.SharedConfigMessage sharedConfigMessage = 11; + */ + org.session.libsignal.protos.SignalServiceProtos.SharedConfigMessage getSharedConfigMessage(); + /** + * optional .signalservice.SharedConfigMessage sharedConfigMessage = 11; + */ + org.session.libsignal.protos.SignalServiceProtos.SharedConfigMessageOrBuilder getSharedConfigMessageOrBuilder(); } /** * Protobuf type {@code signalservice.Content} @@ -2624,6 +2638,19 @@ public final class SignalServiceProtos { bitField0_ |= 0x00000080; break; } + case 90: { + org.session.libsignal.protos.SignalServiceProtos.SharedConfigMessage.Builder subBuilder = null; + if (((bitField0_ & 0x00000100) == 0x00000100)) { + subBuilder = sharedConfigMessage_.toBuilder(); + } + sharedConfigMessage_ = input.readMessage(org.session.libsignal.protos.SignalServiceProtos.SharedConfigMessage.PARSER, extensionRegistry); + if (subBuilder != null) { + subBuilder.mergeFrom(sharedConfigMessage_); + sharedConfigMessage_ = subBuilder.buildPartial(); + } + bitField0_ |= 0x00000100; + break; + } } } } catch (com.google.protobuf.InvalidProtocolBufferException e) { @@ -2840,6 +2867,28 @@ public final class SignalServiceProtos { return messageRequestResponse_; } + // optional .signalservice.SharedConfigMessage sharedConfigMessage = 11; + public static final int SHAREDCONFIGMESSAGE_FIELD_NUMBER = 11; + private org.session.libsignal.protos.SignalServiceProtos.SharedConfigMessage sharedConfigMessage_; + /** + * optional .signalservice.SharedConfigMessage sharedConfigMessage = 11; + */ + public boolean hasSharedConfigMessage() { + return ((bitField0_ & 0x00000100) == 0x00000100); + } + /** + * optional .signalservice.SharedConfigMessage sharedConfigMessage = 11; + */ + public org.session.libsignal.protos.SignalServiceProtos.SharedConfigMessage getSharedConfigMessage() { + return sharedConfigMessage_; + } + /** + * optional .signalservice.SharedConfigMessage sharedConfigMessage = 11; + */ + public org.session.libsignal.protos.SignalServiceProtos.SharedConfigMessageOrBuilder getSharedConfigMessageOrBuilder() { + return sharedConfigMessage_; + } + private void initFields() { dataMessage_ = org.session.libsignal.protos.SignalServiceProtos.DataMessage.getDefaultInstance(); callMessage_ = org.session.libsignal.protos.SignalServiceProtos.CallMessage.getDefaultInstance(); @@ -2849,6 +2898,7 @@ public final class SignalServiceProtos { dataExtractionNotification_ = org.session.libsignal.protos.SignalServiceProtos.DataExtractionNotification.getDefaultInstance(); unsendRequest_ = org.session.libsignal.protos.SignalServiceProtos.UnsendRequest.getDefaultInstance(); messageRequestResponse_ = org.session.libsignal.protos.SignalServiceProtos.MessageRequestResponse.getDefaultInstance(); + sharedConfigMessage_ = org.session.libsignal.protos.SignalServiceProtos.SharedConfigMessage.getDefaultInstance(); } private byte memoizedIsInitialized = -1; public final boolean isInitialized() { @@ -2903,6 +2953,12 @@ public final class SignalServiceProtos { return false; } } + if (hasSharedConfigMessage()) { + if (!getSharedConfigMessage().isInitialized()) { + memoizedIsInitialized = 0; + return false; + } + } memoizedIsInitialized = 1; return true; } @@ -2934,6 +2990,9 @@ public final class SignalServiceProtos { if (((bitField0_ & 0x00000080) == 0x00000080)) { output.writeMessage(10, messageRequestResponse_); } + if (((bitField0_ & 0x00000100) == 0x00000100)) { + output.writeMessage(11, sharedConfigMessage_); + } getUnknownFields().writeTo(output); } @@ -2975,6 +3034,10 @@ public final class SignalServiceProtos { size += com.google.protobuf.CodedOutputStream .computeMessageSize(10, messageRequestResponse_); } + if (((bitField0_ & 0x00000100) == 0x00000100)) { + size += com.google.protobuf.CodedOutputStream + .computeMessageSize(11, sharedConfigMessage_); + } size += getUnknownFields().getSerializedSize(); memoizedSerializedSize = size; return size; @@ -3091,6 +3154,7 @@ public final class SignalServiceProtos { getDataExtractionNotificationFieldBuilder(); getUnsendRequestFieldBuilder(); getMessageRequestResponseFieldBuilder(); + getSharedConfigMessageFieldBuilder(); } } private static Builder create() { @@ -3147,6 +3211,12 @@ public final class SignalServiceProtos { messageRequestResponseBuilder_.clear(); } bitField0_ = (bitField0_ & ~0x00000080); + if (sharedConfigMessageBuilder_ == null) { + sharedConfigMessage_ = org.session.libsignal.protos.SignalServiceProtos.SharedConfigMessage.getDefaultInstance(); + } else { + sharedConfigMessageBuilder_.clear(); + } + bitField0_ = (bitField0_ & ~0x00000100); return this; } @@ -3239,6 +3309,14 @@ public final class SignalServiceProtos { } else { result.messageRequestResponse_ = messageRequestResponseBuilder_.build(); } + if (((from_bitField0_ & 0x00000100) == 0x00000100)) { + to_bitField0_ |= 0x00000100; + } + if (sharedConfigMessageBuilder_ == null) { + result.sharedConfigMessage_ = sharedConfigMessage_; + } else { + result.sharedConfigMessage_ = sharedConfigMessageBuilder_.build(); + } result.bitField0_ = to_bitField0_; onBuilt(); return result; @@ -3279,6 +3357,9 @@ public final class SignalServiceProtos { if (other.hasMessageRequestResponse()) { mergeMessageRequestResponse(other.getMessageRequestResponse()); } + if (other.hasSharedConfigMessage()) { + mergeSharedConfigMessage(other.getSharedConfigMessage()); + } this.mergeUnknownFields(other.getUnknownFields()); return this; } @@ -3332,6 +3413,12 @@ public final class SignalServiceProtos { return false; } } + if (hasSharedConfigMessage()) { + if (!getSharedConfigMessage().isInitialized()) { + + return false; + } + } return true; } @@ -4290,6 +4377,123 @@ public final class SignalServiceProtos { return messageRequestResponseBuilder_; } + // optional .signalservice.SharedConfigMessage sharedConfigMessage = 11; + private org.session.libsignal.protos.SignalServiceProtos.SharedConfigMessage sharedConfigMessage_ = org.session.libsignal.protos.SignalServiceProtos.SharedConfigMessage.getDefaultInstance(); + private com.google.protobuf.SingleFieldBuilder< + org.session.libsignal.protos.SignalServiceProtos.SharedConfigMessage, org.session.libsignal.protos.SignalServiceProtos.SharedConfigMessage.Builder, org.session.libsignal.protos.SignalServiceProtos.SharedConfigMessageOrBuilder> sharedConfigMessageBuilder_; + /** + * optional .signalservice.SharedConfigMessage sharedConfigMessage = 11; + */ + public boolean hasSharedConfigMessage() { + return ((bitField0_ & 0x00000100) == 0x00000100); + } + /** + * optional .signalservice.SharedConfigMessage sharedConfigMessage = 11; + */ + public org.session.libsignal.protos.SignalServiceProtos.SharedConfigMessage getSharedConfigMessage() { + if (sharedConfigMessageBuilder_ == null) { + return sharedConfigMessage_; + } else { + return sharedConfigMessageBuilder_.getMessage(); + } + } + /** + * optional .signalservice.SharedConfigMessage sharedConfigMessage = 11; + */ + public Builder setSharedConfigMessage(org.session.libsignal.protos.SignalServiceProtos.SharedConfigMessage value) { + if (sharedConfigMessageBuilder_ == null) { + if (value == null) { + throw new NullPointerException(); + } + sharedConfigMessage_ = value; + onChanged(); + } else { + sharedConfigMessageBuilder_.setMessage(value); + } + bitField0_ |= 0x00000100; + return this; + } + /** + * optional .signalservice.SharedConfigMessage sharedConfigMessage = 11; + */ + public Builder setSharedConfigMessage( + org.session.libsignal.protos.SignalServiceProtos.SharedConfigMessage.Builder builderForValue) { + if (sharedConfigMessageBuilder_ == null) { + sharedConfigMessage_ = builderForValue.build(); + onChanged(); + } else { + sharedConfigMessageBuilder_.setMessage(builderForValue.build()); + } + bitField0_ |= 0x00000100; + return this; + } + /** + * optional .signalservice.SharedConfigMessage sharedConfigMessage = 11; + */ + public Builder mergeSharedConfigMessage(org.session.libsignal.protos.SignalServiceProtos.SharedConfigMessage value) { + if (sharedConfigMessageBuilder_ == null) { + if (((bitField0_ & 0x00000100) == 0x00000100) && + sharedConfigMessage_ != org.session.libsignal.protos.SignalServiceProtos.SharedConfigMessage.getDefaultInstance()) { + sharedConfigMessage_ = + org.session.libsignal.protos.SignalServiceProtos.SharedConfigMessage.newBuilder(sharedConfigMessage_).mergeFrom(value).buildPartial(); + } else { + sharedConfigMessage_ = value; + } + onChanged(); + } else { + sharedConfigMessageBuilder_.mergeFrom(value); + } + bitField0_ |= 0x00000100; + return this; + } + /** + * optional .signalservice.SharedConfigMessage sharedConfigMessage = 11; + */ + public Builder clearSharedConfigMessage() { + if (sharedConfigMessageBuilder_ == null) { + sharedConfigMessage_ = org.session.libsignal.protos.SignalServiceProtos.SharedConfigMessage.getDefaultInstance(); + onChanged(); + } else { + sharedConfigMessageBuilder_.clear(); + } + bitField0_ = (bitField0_ & ~0x00000100); + return this; + } + /** + * optional .signalservice.SharedConfigMessage sharedConfigMessage = 11; + */ + public org.session.libsignal.protos.SignalServiceProtos.SharedConfigMessage.Builder getSharedConfigMessageBuilder() { + bitField0_ |= 0x00000100; + onChanged(); + return getSharedConfigMessageFieldBuilder().getBuilder(); + } + /** + * optional .signalservice.SharedConfigMessage sharedConfigMessage = 11; + */ + public org.session.libsignal.protos.SignalServiceProtos.SharedConfigMessageOrBuilder getSharedConfigMessageOrBuilder() { + if (sharedConfigMessageBuilder_ != null) { + return sharedConfigMessageBuilder_.getMessageOrBuilder(); + } else { + return sharedConfigMessage_; + } + } + /** + * optional .signalservice.SharedConfigMessage sharedConfigMessage = 11; + */ + private com.google.protobuf.SingleFieldBuilder< + org.session.libsignal.protos.SignalServiceProtos.SharedConfigMessage, org.session.libsignal.protos.SignalServiceProtos.SharedConfigMessage.Builder, org.session.libsignal.protos.SignalServiceProtos.SharedConfigMessageOrBuilder> + getSharedConfigMessageFieldBuilder() { + if (sharedConfigMessageBuilder_ == null) { + sharedConfigMessageBuilder_ = new com.google.protobuf.SingleFieldBuilder< + org.session.libsignal.protos.SignalServiceProtos.SharedConfigMessage, org.session.libsignal.protos.SignalServiceProtos.SharedConfigMessage.Builder, org.session.libsignal.protos.SignalServiceProtos.SharedConfigMessageOrBuilder>( + sharedConfigMessage_, + getParentForChildren(), + isClean()); + sharedConfigMessage_ = null; + } + return sharedConfigMessageBuilder_; + } + // @@protoc_insertion_point(builder_scope:signalservice.Content) } @@ -22196,6 +22400,823 @@ public final class SignalServiceProtos { // @@protoc_insertion_point(class_scope:signalservice.MessageRequestResponse) } + public interface SharedConfigMessageOrBuilder + extends com.google.protobuf.MessageOrBuilder { + + // required .signalservice.SharedConfigMessage.Kind kind = 1; + /** + * required .signalservice.SharedConfigMessage.Kind kind = 1; + * + *
+     * @required
+     * 
+ */ + boolean hasKind(); + /** + * required .signalservice.SharedConfigMessage.Kind kind = 1; + * + *
+     * @required
+     * 
+ */ + org.session.libsignal.protos.SignalServiceProtos.SharedConfigMessage.Kind getKind(); + + // required int64 seqno = 2; + /** + * required int64 seqno = 2; + * + *
+     * @required
+     * 
+ */ + boolean hasSeqno(); + /** + * required int64 seqno = 2; + * + *
+     * @required
+     * 
+ */ + long getSeqno(); + + // required bytes data = 3; + /** + * required bytes data = 3; + * + *
+     * @required
+     * 
+ */ + boolean hasData(); + /** + * required bytes data = 3; + * + *
+     * @required
+     * 
+ */ + com.google.protobuf.ByteString getData(); + } + /** + * Protobuf type {@code signalservice.SharedConfigMessage} + */ + public static final class SharedConfigMessage extends + com.google.protobuf.GeneratedMessage + implements SharedConfigMessageOrBuilder { + // Use SharedConfigMessage.newBuilder() to construct. + private SharedConfigMessage(com.google.protobuf.GeneratedMessage.Builder builder) { + super(builder); + this.unknownFields = builder.getUnknownFields(); + } + private SharedConfigMessage(boolean noInit) { this.unknownFields = com.google.protobuf.UnknownFieldSet.getDefaultInstance(); } + + private static final SharedConfigMessage defaultInstance; + public static SharedConfigMessage getDefaultInstance() { + return defaultInstance; + } + + public SharedConfigMessage getDefaultInstanceForType() { + return defaultInstance; + } + + private final com.google.protobuf.UnknownFieldSet unknownFields; + @java.lang.Override + public final com.google.protobuf.UnknownFieldSet + getUnknownFields() { + return this.unknownFields; + } + private SharedConfigMessage( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + initFields(); + int mutable_bitField0_ = 0; + com.google.protobuf.UnknownFieldSet.Builder unknownFields = + com.google.protobuf.UnknownFieldSet.newBuilder(); + try { + boolean done = false; + while (!done) { + int tag = input.readTag(); + switch (tag) { + case 0: + done = true; + break; + default: { + if (!parseUnknownField(input, unknownFields, + extensionRegistry, tag)) { + done = true; + } + break; + } + case 8: { + int rawValue = input.readEnum(); + org.session.libsignal.protos.SignalServiceProtos.SharedConfigMessage.Kind value = org.session.libsignal.protos.SignalServiceProtos.SharedConfigMessage.Kind.valueOf(rawValue); + if (value == null) { + unknownFields.mergeVarintField(1, rawValue); + } else { + bitField0_ |= 0x00000001; + kind_ = value; + } + break; + } + case 16: { + bitField0_ |= 0x00000002; + seqno_ = input.readInt64(); + break; + } + case 26: { + bitField0_ |= 0x00000004; + data_ = input.readBytes(); + break; + } + } + } + } catch (com.google.protobuf.InvalidProtocolBufferException e) { + throw e.setUnfinishedMessage(this); + } catch (java.io.IOException e) { + throw new com.google.protobuf.InvalidProtocolBufferException( + e.getMessage()).setUnfinishedMessage(this); + } finally { + this.unknownFields = unknownFields.build(); + makeExtensionsImmutable(); + } + } + public static final com.google.protobuf.Descriptors.Descriptor + getDescriptor() { + return org.session.libsignal.protos.SignalServiceProtos.internal_static_signalservice_SharedConfigMessage_descriptor; + } + + protected com.google.protobuf.GeneratedMessage.FieldAccessorTable + internalGetFieldAccessorTable() { + return org.session.libsignal.protos.SignalServiceProtos.internal_static_signalservice_SharedConfigMessage_fieldAccessorTable + .ensureFieldAccessorsInitialized( + org.session.libsignal.protos.SignalServiceProtos.SharedConfigMessage.class, org.session.libsignal.protos.SignalServiceProtos.SharedConfigMessage.Builder.class); + } + + public static com.google.protobuf.Parser PARSER = + new com.google.protobuf.AbstractParser() { + public SharedConfigMessage parsePartialFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return new SharedConfigMessage(input, extensionRegistry); + } + }; + + @java.lang.Override + public com.google.protobuf.Parser getParserForType() { + return PARSER; + } + + /** + * Protobuf enum {@code signalservice.SharedConfigMessage.Kind} + */ + public enum Kind + implements com.google.protobuf.ProtocolMessageEnum { + /** + * USER_PROFILE = 1; + */ + USER_PROFILE(0, 1), + /** + * CONTACTS = 2; + */ + CONTACTS(1, 2), + /** + * CONVO_INFO_VOLATILE = 3; + */ + CONVO_INFO_VOLATILE(2, 3), + /** + * GROUPS = 4; + */ + GROUPS(3, 4), + /** + * CLOSED_GROUP_INFO = 5; + */ + CLOSED_GROUP_INFO(4, 5), + /** + * CLOSED_GROUP_MEMBERS = 6; + */ + CLOSED_GROUP_MEMBERS(5, 6), + /** + * ENCRYPTION_KEYS = 7; + */ + ENCRYPTION_KEYS(6, 7), + ; + + /** + * USER_PROFILE = 1; + */ + public static final int USER_PROFILE_VALUE = 1; + /** + * CONTACTS = 2; + */ + public static final int CONTACTS_VALUE = 2; + /** + * CONVO_INFO_VOLATILE = 3; + */ + public static final int CONVO_INFO_VOLATILE_VALUE = 3; + /** + * GROUPS = 4; + */ + public static final int GROUPS_VALUE = 4; + /** + * CLOSED_GROUP_INFO = 5; + */ + public static final int CLOSED_GROUP_INFO_VALUE = 5; + /** + * CLOSED_GROUP_MEMBERS = 6; + */ + public static final int CLOSED_GROUP_MEMBERS_VALUE = 6; + /** + * ENCRYPTION_KEYS = 7; + */ + public static final int ENCRYPTION_KEYS_VALUE = 7; + + + public final int getNumber() { return value; } + + public static Kind valueOf(int value) { + switch (value) { + case 1: return USER_PROFILE; + case 2: return CONTACTS; + case 3: return CONVO_INFO_VOLATILE; + case 4: return GROUPS; + case 5: return CLOSED_GROUP_INFO; + case 6: return CLOSED_GROUP_MEMBERS; + case 7: return ENCRYPTION_KEYS; + default: return null; + } + } + + public static com.google.protobuf.Internal.EnumLiteMap + internalGetValueMap() { + return internalValueMap; + } + private static com.google.protobuf.Internal.EnumLiteMap + internalValueMap = + new com.google.protobuf.Internal.EnumLiteMap() { + public Kind findValueByNumber(int number) { + return Kind.valueOf(number); + } + }; + + public final com.google.protobuf.Descriptors.EnumValueDescriptor + getValueDescriptor() { + return getDescriptor().getValues().get(index); + } + public final com.google.protobuf.Descriptors.EnumDescriptor + getDescriptorForType() { + return getDescriptor(); + } + public static final com.google.protobuf.Descriptors.EnumDescriptor + getDescriptor() { + return org.session.libsignal.protos.SignalServiceProtos.SharedConfigMessage.getDescriptor().getEnumTypes().get(0); + } + + private static final Kind[] VALUES = values(); + + public static Kind valueOf( + com.google.protobuf.Descriptors.EnumValueDescriptor desc) { + if (desc.getType() != getDescriptor()) { + throw new java.lang.IllegalArgumentException( + "EnumValueDescriptor is not for this type."); + } + return VALUES[desc.getIndex()]; + } + + private final int index; + private final int value; + + private Kind(int index, int value) { + this.index = index; + this.value = value; + } + + // @@protoc_insertion_point(enum_scope:signalservice.SharedConfigMessage.Kind) + } + + private int bitField0_; + // required .signalservice.SharedConfigMessage.Kind kind = 1; + public static final int KIND_FIELD_NUMBER = 1; + private org.session.libsignal.protos.SignalServiceProtos.SharedConfigMessage.Kind kind_; + /** + * required .signalservice.SharedConfigMessage.Kind kind = 1; + * + *
+     * @required
+     * 
+ */ + public boolean hasKind() { + return ((bitField0_ & 0x00000001) == 0x00000001); + } + /** + * required .signalservice.SharedConfigMessage.Kind kind = 1; + * + *
+     * @required
+     * 
+ */ + public org.session.libsignal.protos.SignalServiceProtos.SharedConfigMessage.Kind getKind() { + return kind_; + } + + // required int64 seqno = 2; + public static final int SEQNO_FIELD_NUMBER = 2; + private long seqno_; + /** + * required int64 seqno = 2; + * + *
+     * @required
+     * 
+ */ + public boolean hasSeqno() { + return ((bitField0_ & 0x00000002) == 0x00000002); + } + /** + * required int64 seqno = 2; + * + *
+     * @required
+     * 
+ */ + public long getSeqno() { + return seqno_; + } + + // required bytes data = 3; + public static final int DATA_FIELD_NUMBER = 3; + private com.google.protobuf.ByteString data_; + /** + * required bytes data = 3; + * + *
+     * @required
+     * 
+ */ + public boolean hasData() { + return ((bitField0_ & 0x00000004) == 0x00000004); + } + /** + * required bytes data = 3; + * + *
+     * @required
+     * 
+ */ + public com.google.protobuf.ByteString getData() { + return data_; + } + + private void initFields() { + kind_ = org.session.libsignal.protos.SignalServiceProtos.SharedConfigMessage.Kind.USER_PROFILE; + seqno_ = 0L; + data_ = com.google.protobuf.ByteString.EMPTY; + } + private byte memoizedIsInitialized = -1; + public final boolean isInitialized() { + byte isInitialized = memoizedIsInitialized; + if (isInitialized != -1) return isInitialized == 1; + + if (!hasKind()) { + memoizedIsInitialized = 0; + return false; + } + if (!hasSeqno()) { + memoizedIsInitialized = 0; + return false; + } + if (!hasData()) { + memoizedIsInitialized = 0; + return false; + } + memoizedIsInitialized = 1; + return true; + } + + public void writeTo(com.google.protobuf.CodedOutputStream output) + throws java.io.IOException { + getSerializedSize(); + if (((bitField0_ & 0x00000001) == 0x00000001)) { + output.writeEnum(1, kind_.getNumber()); + } + if (((bitField0_ & 0x00000002) == 0x00000002)) { + output.writeInt64(2, seqno_); + } + if (((bitField0_ & 0x00000004) == 0x00000004)) { + output.writeBytes(3, data_); + } + getUnknownFields().writeTo(output); + } + + private int memoizedSerializedSize = -1; + public int getSerializedSize() { + int size = memoizedSerializedSize; + if (size != -1) return size; + + size = 0; + if (((bitField0_ & 0x00000001) == 0x00000001)) { + size += com.google.protobuf.CodedOutputStream + .computeEnumSize(1, kind_.getNumber()); + } + if (((bitField0_ & 0x00000002) == 0x00000002)) { + size += com.google.protobuf.CodedOutputStream + .computeInt64Size(2, seqno_); + } + if (((bitField0_ & 0x00000004) == 0x00000004)) { + size += com.google.protobuf.CodedOutputStream + .computeBytesSize(3, data_); + } + size += getUnknownFields().getSerializedSize(); + memoizedSerializedSize = size; + return size; + } + + private static final long serialVersionUID = 0L; + @java.lang.Override + protected java.lang.Object writeReplace() + throws java.io.ObjectStreamException { + return super.writeReplace(); + } + + public static org.session.libsignal.protos.SignalServiceProtos.SharedConfigMessage parseFrom( + com.google.protobuf.ByteString data) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data); + } + public static org.session.libsignal.protos.SignalServiceProtos.SharedConfigMessage parseFrom( + com.google.protobuf.ByteString data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data, extensionRegistry); + } + public static org.session.libsignal.protos.SignalServiceProtos.SharedConfigMessage parseFrom(byte[] data) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data); + } + public static org.session.libsignal.protos.SignalServiceProtos.SharedConfigMessage parseFrom( + byte[] data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data, extensionRegistry); + } + public static org.session.libsignal.protos.SignalServiceProtos.SharedConfigMessage parseFrom(java.io.InputStream input) + throws java.io.IOException { + return PARSER.parseFrom(input); + } + public static org.session.libsignal.protos.SignalServiceProtos.SharedConfigMessage parseFrom( + java.io.InputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return PARSER.parseFrom(input, extensionRegistry); + } + public static org.session.libsignal.protos.SignalServiceProtos.SharedConfigMessage parseDelimitedFrom(java.io.InputStream input) + throws java.io.IOException { + return PARSER.parseDelimitedFrom(input); + } + public static org.session.libsignal.protos.SignalServiceProtos.SharedConfigMessage parseDelimitedFrom( + java.io.InputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return PARSER.parseDelimitedFrom(input, extensionRegistry); + } + public static org.session.libsignal.protos.SignalServiceProtos.SharedConfigMessage parseFrom( + com.google.protobuf.CodedInputStream input) + throws java.io.IOException { + return PARSER.parseFrom(input); + } + public static org.session.libsignal.protos.SignalServiceProtos.SharedConfigMessage parseFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return PARSER.parseFrom(input, extensionRegistry); + } + + public static Builder newBuilder() { return Builder.create(); } + public Builder newBuilderForType() { return newBuilder(); } + public static Builder newBuilder(org.session.libsignal.protos.SignalServiceProtos.SharedConfigMessage prototype) { + return newBuilder().mergeFrom(prototype); + } + public Builder toBuilder() { return newBuilder(this); } + + @java.lang.Override + protected Builder newBuilderForType( + com.google.protobuf.GeneratedMessage.BuilderParent parent) { + Builder builder = new Builder(parent); + return builder; + } + /** + * Protobuf type {@code signalservice.SharedConfigMessage} + */ + public static final class Builder extends + com.google.protobuf.GeneratedMessage.Builder + implements org.session.libsignal.protos.SignalServiceProtos.SharedConfigMessageOrBuilder { + public static final com.google.protobuf.Descriptors.Descriptor + getDescriptor() { + return org.session.libsignal.protos.SignalServiceProtos.internal_static_signalservice_SharedConfigMessage_descriptor; + } + + protected com.google.protobuf.GeneratedMessage.FieldAccessorTable + internalGetFieldAccessorTable() { + return org.session.libsignal.protos.SignalServiceProtos.internal_static_signalservice_SharedConfigMessage_fieldAccessorTable + .ensureFieldAccessorsInitialized( + org.session.libsignal.protos.SignalServiceProtos.SharedConfigMessage.class, org.session.libsignal.protos.SignalServiceProtos.SharedConfigMessage.Builder.class); + } + + // Construct using org.session.libsignal.protos.SignalServiceProtos.SharedConfigMessage.newBuilder() + private Builder() { + maybeForceBuilderInitialization(); + } + + private Builder( + com.google.protobuf.GeneratedMessage.BuilderParent parent) { + super(parent); + maybeForceBuilderInitialization(); + } + private void maybeForceBuilderInitialization() { + if (com.google.protobuf.GeneratedMessage.alwaysUseFieldBuilders) { + } + } + private static Builder create() { + return new Builder(); + } + + public Builder clear() { + super.clear(); + kind_ = org.session.libsignal.protos.SignalServiceProtos.SharedConfigMessage.Kind.USER_PROFILE; + bitField0_ = (bitField0_ & ~0x00000001); + seqno_ = 0L; + bitField0_ = (bitField0_ & ~0x00000002); + data_ = com.google.protobuf.ByteString.EMPTY; + bitField0_ = (bitField0_ & ~0x00000004); + return this; + } + + public Builder clone() { + return create().mergeFrom(buildPartial()); + } + + public com.google.protobuf.Descriptors.Descriptor + getDescriptorForType() { + return org.session.libsignal.protos.SignalServiceProtos.internal_static_signalservice_SharedConfigMessage_descriptor; + } + + public org.session.libsignal.protos.SignalServiceProtos.SharedConfigMessage getDefaultInstanceForType() { + return org.session.libsignal.protos.SignalServiceProtos.SharedConfigMessage.getDefaultInstance(); + } + + public org.session.libsignal.protos.SignalServiceProtos.SharedConfigMessage build() { + org.session.libsignal.protos.SignalServiceProtos.SharedConfigMessage result = buildPartial(); + if (!result.isInitialized()) { + throw newUninitializedMessageException(result); + } + return result; + } + + public org.session.libsignal.protos.SignalServiceProtos.SharedConfigMessage buildPartial() { + org.session.libsignal.protos.SignalServiceProtos.SharedConfigMessage result = new org.session.libsignal.protos.SignalServiceProtos.SharedConfigMessage(this); + int from_bitField0_ = bitField0_; + int to_bitField0_ = 0; + if (((from_bitField0_ & 0x00000001) == 0x00000001)) { + to_bitField0_ |= 0x00000001; + } + result.kind_ = kind_; + if (((from_bitField0_ & 0x00000002) == 0x00000002)) { + to_bitField0_ |= 0x00000002; + } + result.seqno_ = seqno_; + if (((from_bitField0_ & 0x00000004) == 0x00000004)) { + to_bitField0_ |= 0x00000004; + } + result.data_ = data_; + result.bitField0_ = to_bitField0_; + onBuilt(); + return result; + } + + public Builder mergeFrom(com.google.protobuf.Message other) { + if (other instanceof org.session.libsignal.protos.SignalServiceProtos.SharedConfigMessage) { + return mergeFrom((org.session.libsignal.protos.SignalServiceProtos.SharedConfigMessage)other); + } else { + super.mergeFrom(other); + return this; + } + } + + public Builder mergeFrom(org.session.libsignal.protos.SignalServiceProtos.SharedConfigMessage other) { + if (other == org.session.libsignal.protos.SignalServiceProtos.SharedConfigMessage.getDefaultInstance()) return this; + if (other.hasKind()) { + setKind(other.getKind()); + } + if (other.hasSeqno()) { + setSeqno(other.getSeqno()); + } + if (other.hasData()) { + setData(other.getData()); + } + this.mergeUnknownFields(other.getUnknownFields()); + return this; + } + + public final boolean isInitialized() { + if (!hasKind()) { + + return false; + } + if (!hasSeqno()) { + + return false; + } + if (!hasData()) { + + return false; + } + return true; + } + + public Builder mergeFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + org.session.libsignal.protos.SignalServiceProtos.SharedConfigMessage parsedMessage = null; + try { + parsedMessage = PARSER.parsePartialFrom(input, extensionRegistry); + } catch (com.google.protobuf.InvalidProtocolBufferException e) { + parsedMessage = (org.session.libsignal.protos.SignalServiceProtos.SharedConfigMessage) e.getUnfinishedMessage(); + throw e; + } finally { + if (parsedMessage != null) { + mergeFrom(parsedMessage); + } + } + return this; + } + private int bitField0_; + + // required .signalservice.SharedConfigMessage.Kind kind = 1; + private org.session.libsignal.protos.SignalServiceProtos.SharedConfigMessage.Kind kind_ = org.session.libsignal.protos.SignalServiceProtos.SharedConfigMessage.Kind.USER_PROFILE; + /** + * required .signalservice.SharedConfigMessage.Kind kind = 1; + * + *
+       * @required
+       * 
+ */ + public boolean hasKind() { + return ((bitField0_ & 0x00000001) == 0x00000001); + } + /** + * required .signalservice.SharedConfigMessage.Kind kind = 1; + * + *
+       * @required
+       * 
+ */ + public org.session.libsignal.protos.SignalServiceProtos.SharedConfigMessage.Kind getKind() { + return kind_; + } + /** + * required .signalservice.SharedConfigMessage.Kind kind = 1; + * + *
+       * @required
+       * 
+ */ + public Builder setKind(org.session.libsignal.protos.SignalServiceProtos.SharedConfigMessage.Kind value) { + if (value == null) { + throw new NullPointerException(); + } + bitField0_ |= 0x00000001; + kind_ = value; + onChanged(); + return this; + } + /** + * required .signalservice.SharedConfigMessage.Kind kind = 1; + * + *
+       * @required
+       * 
+ */ + public Builder clearKind() { + bitField0_ = (bitField0_ & ~0x00000001); + kind_ = org.session.libsignal.protos.SignalServiceProtos.SharedConfigMessage.Kind.USER_PROFILE; + onChanged(); + return this; + } + + // required int64 seqno = 2; + private long seqno_ ; + /** + * required int64 seqno = 2; + * + *
+       * @required
+       * 
+ */ + public boolean hasSeqno() { + return ((bitField0_ & 0x00000002) == 0x00000002); + } + /** + * required int64 seqno = 2; + * + *
+       * @required
+       * 
+ */ + public long getSeqno() { + return seqno_; + } + /** + * required int64 seqno = 2; + * + *
+       * @required
+       * 
+ */ + public Builder setSeqno(long value) { + bitField0_ |= 0x00000002; + seqno_ = value; + onChanged(); + return this; + } + /** + * required int64 seqno = 2; + * + *
+       * @required
+       * 
+ */ + public Builder clearSeqno() { + bitField0_ = (bitField0_ & ~0x00000002); + seqno_ = 0L; + onChanged(); + return this; + } + + // required bytes data = 3; + private com.google.protobuf.ByteString data_ = com.google.protobuf.ByteString.EMPTY; + /** + * required bytes data = 3; + * + *
+       * @required
+       * 
+ */ + public boolean hasData() { + return ((bitField0_ & 0x00000004) == 0x00000004); + } + /** + * required bytes data = 3; + * + *
+       * @required
+       * 
+ */ + public com.google.protobuf.ByteString getData() { + return data_; + } + /** + * required bytes data = 3; + * + *
+       * @required
+       * 
+ */ + public Builder setData(com.google.protobuf.ByteString value) { + if (value == null) { + throw new NullPointerException(); + } + bitField0_ |= 0x00000004; + data_ = value; + onChanged(); + return this; + } + /** + * required bytes data = 3; + * + *
+       * @required
+       * 
+ */ + public Builder clearData() { + bitField0_ = (bitField0_ & ~0x00000004); + data_ = getDefaultInstance().getData(); + onChanged(); + return this; + } + + // @@protoc_insertion_point(builder_scope:signalservice.SharedConfigMessage) + } + + static { + defaultInstance = new SharedConfigMessage(true); + defaultInstance.initFields(); + } + + // @@protoc_insertion_point(class_scope:signalservice.SharedConfigMessage) + } + public interface ReceiptMessageOrBuilder extends com.google.protobuf.MessageOrBuilder { @@ -26081,6 +27102,11 @@ public final class SignalServiceProtos { private static com.google.protobuf.GeneratedMessage.FieldAccessorTable internal_static_signalservice_MessageRequestResponse_fieldAccessorTable; + private static com.google.protobuf.Descriptors.Descriptor + internal_static_signalservice_SharedConfigMessage_descriptor; + private static + com.google.protobuf.GeneratedMessage.FieldAccessorTable + internal_static_signalservice_SharedConfigMessage_fieldAccessorTable; private static com.google.protobuf.Descriptors.Descriptor internal_static_signalservice_ReceiptMessage_descriptor; private static @@ -26115,7 +27141,7 @@ public final class SignalServiceProtos { "\002(\004\0223\n\006action\030\002 \002(\0162#.signalservice.Typi" + "ngMessage.Action\"\"\n\006Action\022\013\n\007STARTED\020\000\022" + "\013\n\007STOPPED\020\001\"2\n\rUnsendRequest\022\021\n\ttimesta", - "mp\030\001 \002(\004\022\016\n\006author\030\002 \002(\t\"\345\003\n\007Content\022/\n\013" + + "mp\030\001 \002(\004\022\016\n\006author\030\002 \002(\t\"\246\004\n\007Content\022/\n\013" + "dataMessage\030\001 \001(\0132\032.signalservice.DataMe" + "ssage\022/\n\013callMessage\030\003 \001(\0132\032.signalservi" + "ce.CallMessage\0225\n\016receiptMessage\030\005 \001(\0132\035" + @@ -26127,96 +27153,104 @@ public final class SignalServiceProtos { "e.DataExtractionNotification\0223\n\runsendRe", "quest\030\t \001(\0132\034.signalservice.UnsendReques" + "t\022E\n\026messageRequestResponse\030\n \001(\0132%.sign" + - "alservice.MessageRequestResponse\"0\n\007KeyP" + - "air\022\021\n\tpublicKey\030\001 \002(\014\022\022\n\nprivateKey\030\002 \002" + - "(\014\"\226\001\n\032DataExtractionNotification\022<\n\004typ" + - "e\030\001 \002(\0162..signalservice.DataExtractionNo" + - "tification.Type\022\021\n\ttimestamp\030\002 \001(\004\"\'\n\004Ty" + - "pe\022\016\n\nSCREENSHOT\020\001\022\017\n\013MEDIA_SAVED\020\002\"\361\r\n\013" + - "DataMessage\022\014\n\004body\030\001 \001(\t\0225\n\013attachments" + - "\030\002 \003(\0132 .signalservice.AttachmentPointer", - "\022*\n\005group\030\003 \001(\0132\033.signalservice.GroupCon" + - "text\022\r\n\005flags\030\004 \001(\r\022\023\n\013expireTimer\030\005 \001(\r" + - "\022\022\n\nprofileKey\030\006 \001(\014\022\021\n\ttimestamp\030\007 \001(\004\022" + - "/\n\005quote\030\010 \001(\0132 .signalservice.DataMessa" + - "ge.Quote\0223\n\007preview\030\n \003(\0132\".signalservic" + - "e.DataMessage.Preview\0225\n\010reaction\030\013 \001(\0132" + - "#.signalservice.DataMessage.Reaction\0227\n\007" + - "profile\030e \001(\0132&.signalservice.DataMessag" + - "e.LokiProfile\022K\n\023openGroupInvitation\030f \001" + - "(\0132..signalservice.DataMessage.OpenGroup", - "Invitation\022W\n\031closedGroupControlMessage\030" + - "h \001(\01324.signalservice.DataMessage.Closed" + - "GroupControlMessage\022\022\n\nsyncTarget\030i \001(\t\032" + - "\225\002\n\005Quote\022\n\n\002id\030\001 \002(\004\022\016\n\006author\030\002 \002(\t\022\014\n" + - "\004text\030\003 \001(\t\022F\n\013attachments\030\004 \003(\01321.signa" + - "lservice.DataMessage.Quote.QuotedAttachm" + - "ent\032\231\001\n\020QuotedAttachment\022\023\n\013contentType\030" + - "\001 \001(\t\022\020\n\010fileName\030\002 \001(\t\0223\n\tthumbnail\030\003 \001" + - "(\0132 .signalservice.AttachmentPointer\022\r\n\005" + - "flags\030\004 \001(\r\"\032\n\005Flags\022\021\n\rVOICE_MESSAGE\020\001\032", - "V\n\007Preview\022\013\n\003url\030\001 \002(\t\022\r\n\005title\030\002 \001(\t\022/" + - "\n\005image\030\003 \001(\0132 .signalservice.Attachment" + - "Pointer\032:\n\013LokiProfile\022\023\n\013displayName\030\001 " + - "\001(\t\022\026\n\016profilePicture\030\002 \001(\t\0320\n\023OpenGroup" + - "Invitation\022\013\n\003url\030\001 \002(\t\022\014\n\004name\030\003 \002(\t\032\374\003" + - "\n\031ClosedGroupControlMessage\022G\n\004type\030\001 \002(" + - "\01629.signalservice.DataMessage.ClosedGrou" + - "pControlMessage.Type\022\021\n\tpublicKey\030\002 \001(\014\022" + - "\014\n\004name\030\003 \001(\t\0221\n\021encryptionKeyPair\030\004 \001(\013" + - "2\026.signalservice.KeyPair\022\017\n\007members\030\005 \003(", - "\014\022\016\n\006admins\030\006 \003(\014\022U\n\010wrappers\030\007 \003(\0132C.si" + - "gnalservice.DataMessage.ClosedGroupContr" + - "olMessage.KeyPairWrapper\022\027\n\017expirationTi" + - "mer\030\010 \001(\r\032=\n\016KeyPairWrapper\022\021\n\tpublicKey" + - "\030\001 \002(\014\022\030\n\020encryptedKeyPair\030\002 \002(\014\"r\n\004Type" + - "\022\007\n\003NEW\020\001\022\027\n\023ENCRYPTION_KEY_PAIR\020\003\022\017\n\013NA" + - "ME_CHANGE\020\004\022\021\n\rMEMBERS_ADDED\020\005\022\023\n\017MEMBER" + - "S_REMOVED\020\006\022\017\n\013MEMBER_LEFT\020\007\032\222\001\n\010Reactio" + - "n\022\n\n\002id\030\001 \002(\004\022\016\n\006author\030\002 \002(\t\022\r\n\005emoji\030\003" + - " \001(\t\022:\n\006action\030\004 \002(\0162*.signalservice.Dat", - "aMessage.Reaction.Action\"\037\n\006Action\022\t\n\005RE" + - "ACT\020\000\022\n\n\006REMOVE\020\001\"$\n\005Flags\022\033\n\027EXPIRATION" + - "_TIMER_UPDATE\020\002\"\352\001\n\013CallMessage\022-\n\004type\030" + - "\001 \002(\0162\037.signalservice.CallMessage.Type\022\014" + - "\n\004sdps\030\002 \003(\t\022\027\n\017sdpMLineIndexes\030\003 \003(\r\022\017\n" + - "\007sdpMids\030\004 \003(\t\022\014\n\004uuid\030\005 \002(\t\"f\n\004Type\022\r\n\t" + - "PRE_OFFER\020\006\022\t\n\005OFFER\020\001\022\n\n\006ANSWER\020\002\022\026\n\022PR" + - "OVISIONAL_ANSWER\020\003\022\022\n\016ICE_CANDIDATES\020\004\022\014" + - "\n\010END_CALL\020\005\"\245\004\n\024ConfigurationMessage\022E\n" + - "\014closedGroups\030\001 \003(\0132/.signalservice.Conf", - "igurationMessage.ClosedGroup\022\022\n\nopenGrou" + - "ps\030\002 \003(\t\022\023\n\013displayName\030\003 \001(\t\022\026\n\016profile" + - "Picture\030\004 \001(\t\022\022\n\nprofileKey\030\005 \001(\014\022=\n\010con" + - "tacts\030\006 \003(\0132+.signalservice.Configuratio" + - "nMessage.Contact\032\233\001\n\013ClosedGroup\022\021\n\tpubl" + - "icKey\030\001 \001(\014\022\014\n\004name\030\002 \001(\t\0221\n\021encryptionK" + - "eyPair\030\003 \001(\0132\026.signalservice.KeyPair\022\017\n\007" + - "members\030\004 \003(\014\022\016\n\006admins\030\005 \003(\014\022\027\n\017expirat" + - "ionTimer\030\006 \001(\r\032\223\001\n\007Contact\022\021\n\tpublicKey\030" + - "\001 \002(\014\022\014\n\004name\030\002 \002(\t\022\026\n\016profilePicture\030\003 ", - "\001(\t\022\022\n\nprofileKey\030\004 \001(\014\022\022\n\nisApproved\030\005 " + - "\001(\010\022\021\n\tisBlocked\030\006 \001(\010\022\024\n\014didApproveMe\030\007" + - " \001(\010\"y\n\026MessageRequestResponse\022\022\n\nisAppr" + - "oved\030\001 \002(\010\022\022\n\nprofileKey\030\002 \001(\014\0227\n\007profil" + - "e\030\003 \001(\0132&.signalservice.DataMessage.Loki" + - "Profile\"u\n\016ReceiptMessage\0220\n\004type\030\001 \002(\0162" + - "\".signalservice.ReceiptMessage.Type\022\021\n\tt" + - "imestamp\030\002 \003(\004\"\036\n\004Type\022\014\n\010DELIVERY\020\000\022\010\n\004" + - "READ\020\001\"\354\001\n\021AttachmentPointer\022\n\n\002id\030\001 \002(\006" + - "\022\023\n\013contentType\030\002 \001(\t\022\013\n\003key\030\003 \001(\014\022\014\n\004si", - "ze\030\004 \001(\r\022\021\n\tthumbnail\030\005 \001(\014\022\016\n\006digest\030\006 " + - "\001(\014\022\020\n\010fileName\030\007 \001(\t\022\r\n\005flags\030\010 \001(\r\022\r\n\005" + - "width\030\t \001(\r\022\016\n\006height\030\n \001(\r\022\017\n\007caption\030\013" + - " \001(\t\022\013\n\003url\030e \001(\t\"\032\n\005Flags\022\021\n\rVOICE_MESS" + - "AGE\020\001\"\365\001\n\014GroupContext\022\n\n\002id\030\001 \001(\014\022.\n\004ty" + - "pe\030\002 \001(\0162 .signalservice.GroupContext.Ty" + - "pe\022\014\n\004name\030\003 \001(\t\022\017\n\007members\030\004 \003(\t\0220\n\006ava" + - "tar\030\005 \001(\0132 .signalservice.AttachmentPoin" + - "ter\022\016\n\006admins\030\006 \003(\t\"H\n\004Type\022\013\n\007UNKNOWN\020\000" + - "\022\n\n\006UPDATE\020\001\022\013\n\007DELIVER\020\002\022\010\n\004QUIT\020\003\022\020\n\014R", - "EQUEST_INFO\020\004B3\n\034org.session.libsignal.p" + - "rotosB\023SignalServiceProtos" + "alservice.MessageRequestResponse\022?\n\023shar" + + "edConfigMessage\030\013 \001(\0132\".signalservice.Sh" + + "aredConfigMessage\"0\n\007KeyPair\022\021\n\tpublicKe" + + "y\030\001 \002(\014\022\022\n\nprivateKey\030\002 \002(\014\"\226\001\n\032DataExtr" + + "actionNotification\022<\n\004type\030\001 \002(\0162..signa" + + "lservice.DataExtractionNotification.Type" + + "\022\021\n\ttimestamp\030\002 \001(\004\"\'\n\004Type\022\016\n\nSCREENSHO" + + "T\020\001\022\017\n\013MEDIA_SAVED\020\002\"\361\r\n\013DataMessage\022\014\n\004", + "body\030\001 \001(\t\0225\n\013attachments\030\002 \003(\0132 .signal" + + "service.AttachmentPointer\022*\n\005group\030\003 \001(\013" + + "2\033.signalservice.GroupContext\022\r\n\005flags\030\004" + + " \001(\r\022\023\n\013expireTimer\030\005 \001(\r\022\022\n\nprofileKey\030" + + "\006 \001(\014\022\021\n\ttimestamp\030\007 \001(\004\022/\n\005quote\030\010 \001(\0132" + + " .signalservice.DataMessage.Quote\0223\n\007pre" + + "view\030\n \003(\0132\".signalservice.DataMessage.P" + + "review\0225\n\010reaction\030\013 \001(\0132#.signalservice" + + ".DataMessage.Reaction\0227\n\007profile\030e \001(\0132&" + + ".signalservice.DataMessage.LokiProfile\022K", + "\n\023openGroupInvitation\030f \001(\0132..signalserv" + + "ice.DataMessage.OpenGroupInvitation\022W\n\031c" + + "losedGroupControlMessage\030h \001(\01324.signals" + + "ervice.DataMessage.ClosedGroupControlMes" + + "sage\022\022\n\nsyncTarget\030i \001(\t\032\225\002\n\005Quote\022\n\n\002id" + + "\030\001 \002(\004\022\016\n\006author\030\002 \002(\t\022\014\n\004text\030\003 \001(\t\022F\n\013" + + "attachments\030\004 \003(\01321.signalservice.DataMe" + + "ssage.Quote.QuotedAttachment\032\231\001\n\020QuotedA" + + "ttachment\022\023\n\013contentType\030\001 \001(\t\022\020\n\010fileNa" + + "me\030\002 \001(\t\0223\n\tthumbnail\030\003 \001(\0132 .signalserv", + "ice.AttachmentPointer\022\r\n\005flags\030\004 \001(\r\"\032\n\005" + + "Flags\022\021\n\rVOICE_MESSAGE\020\001\032V\n\007Preview\022\013\n\003u" + + "rl\030\001 \002(\t\022\r\n\005title\030\002 \001(\t\022/\n\005image\030\003 \001(\0132 " + + ".signalservice.AttachmentPointer\032:\n\013Loki" + + "Profile\022\023\n\013displayName\030\001 \001(\t\022\026\n\016profileP" + + "icture\030\002 \001(\t\0320\n\023OpenGroupInvitation\022\013\n\003u" + + "rl\030\001 \002(\t\022\014\n\004name\030\003 \002(\t\032\374\003\n\031ClosedGroupCo" + + "ntrolMessage\022G\n\004type\030\001 \002(\01629.signalservi" + + "ce.DataMessage.ClosedGroupControlMessage" + + ".Type\022\021\n\tpublicKey\030\002 \001(\014\022\014\n\004name\030\003 \001(\t\0221", + "\n\021encryptionKeyPair\030\004 \001(\0132\026.signalservic" + + "e.KeyPair\022\017\n\007members\030\005 \003(\014\022\016\n\006admins\030\006 \003" + + "(\014\022U\n\010wrappers\030\007 \003(\0132C.signalservice.Dat" + + "aMessage.ClosedGroupControlMessage.KeyPa" + + "irWrapper\022\027\n\017expirationTimer\030\010 \001(\r\032=\n\016Ke" + + "yPairWrapper\022\021\n\tpublicKey\030\001 \002(\014\022\030\n\020encry" + + "ptedKeyPair\030\002 \002(\014\"r\n\004Type\022\007\n\003NEW\020\001\022\027\n\023EN" + + "CRYPTION_KEY_PAIR\020\003\022\017\n\013NAME_CHANGE\020\004\022\021\n\r" + + "MEMBERS_ADDED\020\005\022\023\n\017MEMBERS_REMOVED\020\006\022\017\n\013" + + "MEMBER_LEFT\020\007\032\222\001\n\010Reaction\022\n\n\002id\030\001 \002(\004\022\016", + "\n\006author\030\002 \002(\t\022\r\n\005emoji\030\003 \001(\t\022:\n\006action\030" + + "\004 \002(\0162*.signalservice.DataMessage.Reacti" + + "on.Action\"\037\n\006Action\022\t\n\005REACT\020\000\022\n\n\006REMOVE" + + "\020\001\"$\n\005Flags\022\033\n\027EXPIRATION_TIMER_UPDATE\020\002" + + "\"\352\001\n\013CallMessage\022-\n\004type\030\001 \002(\0162\037.signals" + + "ervice.CallMessage.Type\022\014\n\004sdps\030\002 \003(\t\022\027\n" + + "\017sdpMLineIndexes\030\003 \003(\r\022\017\n\007sdpMids\030\004 \003(\t\022" + + "\014\n\004uuid\030\005 \002(\t\"f\n\004Type\022\r\n\tPRE_OFFER\020\006\022\t\n\005" + + "OFFER\020\001\022\n\n\006ANSWER\020\002\022\026\n\022PROVISIONAL_ANSWE" + + "R\020\003\022\022\n\016ICE_CANDIDATES\020\004\022\014\n\010END_CALL\020\005\"\245\004", + "\n\024ConfigurationMessage\022E\n\014closedGroups\030\001" + + " \003(\0132/.signalservice.ConfigurationMessag" + + "e.ClosedGroup\022\022\n\nopenGroups\030\002 \003(\t\022\023\n\013dis" + + "playName\030\003 \001(\t\022\026\n\016profilePicture\030\004 \001(\t\022\022" + + "\n\nprofileKey\030\005 \001(\014\022=\n\010contacts\030\006 \003(\0132+.s" + + "ignalservice.ConfigurationMessage.Contac" + + "t\032\233\001\n\013ClosedGroup\022\021\n\tpublicKey\030\001 \001(\014\022\014\n\004" + + "name\030\002 \001(\t\0221\n\021encryptionKeyPair\030\003 \001(\0132\026." + + "signalservice.KeyPair\022\017\n\007members\030\004 \003(\014\022\016" + + "\n\006admins\030\005 \003(\014\022\027\n\017expirationTimer\030\006 \001(\r\032", + "\223\001\n\007Contact\022\021\n\tpublicKey\030\001 \002(\014\022\014\n\004name\030\002" + + " \002(\t\022\026\n\016profilePicture\030\003 \001(\t\022\022\n\nprofileK" + + "ey\030\004 \001(\014\022\022\n\nisApproved\030\005 \001(\010\022\021\n\tisBlocke" + + "d\030\006 \001(\010\022\024\n\014didApproveMe\030\007 \001(\010\"y\n\026Message" + + "RequestResponse\022\022\n\nisApproved\030\001 \002(\010\022\022\n\np" + + "rofileKey\030\002 \001(\014\0227\n\007profile\030\003 \001(\0132&.signa" + + "lservice.DataMessage.LokiProfile\"\375\001\n\023Sha" + + "redConfigMessage\0225\n\004kind\030\001 \002(\0162\'.signals" + + "ervice.SharedConfigMessage.Kind\022\r\n\005seqno" + + "\030\002 \002(\003\022\014\n\004data\030\003 \002(\014\"\221\001\n\004Kind\022\020\n\014USER_PR", + "OFILE\020\001\022\014\n\010CONTACTS\020\002\022\027\n\023CONVO_INFO_VOLA" + + "TILE\020\003\022\n\n\006GROUPS\020\004\022\025\n\021CLOSED_GROUP_INFO\020" + + "\005\022\030\n\024CLOSED_GROUP_MEMBERS\020\006\022\023\n\017ENCRYPTIO" + + "N_KEYS\020\007\"u\n\016ReceiptMessage\0220\n\004type\030\001 \002(\016" + + "2\".signalservice.ReceiptMessage.Type\022\021\n\t" + + "timestamp\030\002 \003(\004\"\036\n\004Type\022\014\n\010DELIVERY\020\000\022\010\n" + + "\004READ\020\001\"\354\001\n\021AttachmentPointer\022\n\n\002id\030\001 \002(" + + "\006\022\023\n\013contentType\030\002 \001(\t\022\013\n\003key\030\003 \001(\014\022\014\n\004s" + + "ize\030\004 \001(\r\022\021\n\tthumbnail\030\005 \001(\014\022\016\n\006digest\030\006" + + " \001(\014\022\020\n\010fileName\030\007 \001(\t\022\r\n\005flags\030\010 \001(\r\022\r\n", + "\005width\030\t \001(\r\022\016\n\006height\030\n \001(\r\022\017\n\007caption\030" + + "\013 \001(\t\022\013\n\003url\030e \001(\t\"\032\n\005Flags\022\021\n\rVOICE_MES" + + "SAGE\020\001\"\365\001\n\014GroupContext\022\n\n\002id\030\001 \001(\014\022.\n\004t" + + "ype\030\002 \001(\0162 .signalservice.GroupContext.T" + + "ype\022\014\n\004name\030\003 \001(\t\022\017\n\007members\030\004 \003(\t\0220\n\006av" + + "atar\030\005 \001(\0132 .signalservice.AttachmentPoi" + + "nter\022\016\n\006admins\030\006 \003(\t\"H\n\004Type\022\013\n\007UNKNOWN\020" + + "\000\022\n\n\006UPDATE\020\001\022\013\n\007DELIVER\020\002\022\010\n\004QUIT\020\003\022\020\n\014" + + "REQUEST_INFO\020\004B3\n\034org.session.libsignal." + + "protosB\023SignalServiceProtos" }; com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner assigner = new com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner() { @@ -26246,7 +27280,7 @@ public final class SignalServiceProtos { internal_static_signalservice_Content_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_signalservice_Content_descriptor, - new java.lang.String[] { "DataMessage", "CallMessage", "ReceiptMessage", "TypingMessage", "ConfigurationMessage", "DataExtractionNotification", "UnsendRequest", "MessageRequestResponse", }); + new java.lang.String[] { "DataMessage", "CallMessage", "ReceiptMessage", "TypingMessage", "ConfigurationMessage", "DataExtractionNotification", "UnsendRequest", "MessageRequestResponse", "SharedConfigMessage", }); internal_static_signalservice_KeyPair_descriptor = getDescriptor().getMessageTypes().get(4); internal_static_signalservice_KeyPair_fieldAccessorTable = new @@ -26343,20 +27377,26 @@ public final class SignalServiceProtos { com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_signalservice_MessageRequestResponse_descriptor, new java.lang.String[] { "IsApproved", "ProfileKey", "Profile", }); - internal_static_signalservice_ReceiptMessage_descriptor = + internal_static_signalservice_SharedConfigMessage_descriptor = getDescriptor().getMessageTypes().get(10); + internal_static_signalservice_SharedConfigMessage_fieldAccessorTable = new + com.google.protobuf.GeneratedMessage.FieldAccessorTable( + internal_static_signalservice_SharedConfigMessage_descriptor, + new java.lang.String[] { "Kind", "Seqno", "Data", }); + internal_static_signalservice_ReceiptMessage_descriptor = + getDescriptor().getMessageTypes().get(11); internal_static_signalservice_ReceiptMessage_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_signalservice_ReceiptMessage_descriptor, new java.lang.String[] { "Type", "Timestamp", }); internal_static_signalservice_AttachmentPointer_descriptor = - getDescriptor().getMessageTypes().get(11); + getDescriptor().getMessageTypes().get(12); internal_static_signalservice_AttachmentPointer_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_signalservice_AttachmentPointer_descriptor, new java.lang.String[] { "Id", "ContentType", "Key", "Size", "Thumbnail", "Digest", "FileName", "Flags", "Width", "Height", "Caption", "Url", }); internal_static_signalservice_GroupContext_descriptor = - getDescriptor().getMessageTypes().get(12); + getDescriptor().getMessageTypes().get(13); internal_static_signalservice_GroupContext_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_signalservice_GroupContext_descriptor, diff --git a/libsignal/src/main/java/org/session/libsignal/utilities/IdPrefix.kt b/libsignal/src/main/java/org/session/libsignal/utilities/IdPrefix.kt index 154b91ee2..26c62ba50 100644 --- a/libsignal/src/main/java/org/session/libsignal/utilities/IdPrefix.kt +++ b/libsignal/src/main/java/org/session/libsignal/utilities/IdPrefix.kt @@ -1,12 +1,15 @@ package org.session.libsignal.utilities enum class IdPrefix(val value: String) { - STANDARD("05"), BLINDED("15"), UN_BLINDED("00"); + STANDARD("05"), BLINDED("15"), UN_BLINDED("00"), BLINDEDV2("25"); + + fun isBlinded() = value == BLINDED.value || value == BLINDEDV2.value companion object { fun fromValue(rawValue: String): IdPrefix? = when(rawValue.take(2)) { STANDARD.value -> STANDARD BLINDED.value -> BLINDED + BLINDEDV2.value -> BLINDEDV2 UN_BLINDED.value -> UN_BLINDED else -> null } diff --git a/libsignal/src/main/java/org/session/libsignal/utilities/Namespace.kt b/libsignal/src/main/java/org/session/libsignal/utilities/Namespace.kt index 1c635d993..ba04e516a 100644 --- a/libsignal/src/main/java/org/session/libsignal/utilities/Namespace.kt +++ b/libsignal/src/main/java/org/session/libsignal/utilities/Namespace.kt @@ -1,7 +1,7 @@ package org.session.libsignal.utilities object Namespace { + const val ALL = "all" const val DEFAULT = 0 const val UNAUTHENTICATED_CLOSED_GROUP = -10 - const val CONFIGURATION = 5 } \ No newline at end of file diff --git a/libsignal/src/main/java/org/session/libsignal/utilities/Snode.kt b/libsignal/src/main/java/org/session/libsignal/utilities/Snode.kt index cfbedb733..28f8aeb03 100644 --- a/libsignal/src/main/java/org/session/libsignal/utilities/Snode.kt +++ b/libsignal/src/main/java/org/session/libsignal/utilities/Snode.kt @@ -5,12 +5,16 @@ class Snode(val address: String, val port: Int, val publicKeySet: KeySet?) { public enum class Method(val rawValue: String) { GetSwarm("get_snodes_for_pubkey"), - GetMessages("retrieve"), + Retrieve("retrieve"), SendMessage("store"), DeleteMessage("delete"), OxenDaemonRPCCall("oxend_request"), Info("info"), - DeleteAll("delete_all") + DeleteAll("delete_all"), + Batch("batch"), + Sequence("sequence"), + Expire("expire"), + GetExpiries("get_expiries") } data class KeySet(val ed25519Key: String, val x25519Key: String) diff --git a/settings.gradle b/settings.gradle index 3a4251047..7ab26e097 100644 --- a/settings.gradle +++ b/settings.gradle @@ -3,4 +3,5 @@ rootProject.name = "session-android" include ':app' include ':liblazysodium' include ':libsession' -include ':libsignal' \ No newline at end of file +include ':libsignal' +include ':libsession-util' From 9523953bd98f36611cecfc779b2a57b37a72cbe5 Mon Sep 17 00:00:00 2001 From: hjubb Date: Fri, 14 Jul 2023 18:29:47 +1000 Subject: [PATCH 25/27] build: update build number --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 2fbac7105..fc710a8e0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -156,7 +156,7 @@ dependencies { testImplementation 'org.robolectric:shadows-multidex:4.4' } -def canonicalVersionCode = 353 +def canonicalVersionCode = 354 def canonicalVersionName = "1.17.0" def postFixSize = 10 From d4ab49ebbb9c02e1495c9519d86e7ed4b4fc04cc Mon Sep 17 00:00:00 2001 From: hjubb Date: Fri, 14 Jul 2023 18:31:25 +1000 Subject: [PATCH 26/27] build: submodule build command update --- BUILDING.md | 1 + 1 file changed, 1 insertion(+) diff --git a/BUILDING.md b/BUILDING.md index e78207d96..f88509c68 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -32,6 +32,7 @@ Setting up a development environment and building from Android Studio 4. Android Studio should detect the presence of a project file and ask you whether to open it. Click "yes". 5. Default config options should be good enough. 6. Project initialization and building should proceed. +7. Clone submodules with `git submodule update --init --recursive` Contributing code ----------------- From e7608763a052405d83c48da3fef6846ee22804a3 Mon Sep 17 00:00:00 2001 From: hjubb Date: Fri, 14 Jul 2023 18:47:10 +1000 Subject: [PATCH 27/27] fix: tests pass with forcing config --- .../java/network/loki/messenger/LibSessionTests.kt | 5 +++++ .../securesms/conversation/v2/ConversationActivityV2.kt | 3 --- .../securesms/conversation/v2/dialogs/BlockedDialog.kt | 7 ++----- .../thoughtcrime/securesms/dependencies/ConfigFactory.kt | 2 +- 4 files changed, 8 insertions(+), 9 deletions(-) diff --git a/app/src/androidTest/java/network/loki/messenger/LibSessionTests.kt b/app/src/androidTest/java/network/loki/messenger/LibSessionTests.kt index 1638d8383..59cb8ede0 100644 --- a/app/src/androidTest/java/network/loki/messenger/LibSessionTests.kt +++ b/app/src/androidTest/java/network/loki/messenger/LibSessionTests.kt @@ -1,5 +1,7 @@ package network.loki.messenger +import androidx.core.content.edit +import androidx.preference.PreferenceManager import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import androidx.test.platform.app.InstrumentationRegistry @@ -62,6 +64,9 @@ class LibSessionTests { @Before fun setupUser() { + PreferenceManager.getDefaultSharedPreferences(InstrumentationRegistry.getInstrumentation().targetContext.applicationContext).edit { + putBoolean(TextSecurePreferences.HAS_FORCED_NEW_CONFIG, true).apply() + } val newBytes = randomSeedBytes().toByteArray() val context = InstrumentationRegistry.getInstrumentation().targetContext.applicationContext val kp = KeyPairUtilities.generate(newBytes) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt index 7b742a9be..e7b194549 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt @@ -6,7 +6,6 @@ import android.animation.ValueAnimator import android.content.ClipData import android.content.ClipboardManager import android.content.Context -import android.content.DialogInterface import android.content.Intent import android.content.res.Resources import android.database.Cursor @@ -36,7 +35,6 @@ import android.widget.RelativeLayout import android.widget.Toast import androidx.activity.viewModels import androidx.annotation.DimenRes -import androidx.appcompat.app.AlertDialog import androidx.core.text.set import androidx.core.text.toSpannable import androidx.core.view.drawToBitmap @@ -123,7 +121,6 @@ import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageViewDel import org.thoughtcrime.securesms.conversation.v2.search.SearchBottomBar import org.thoughtcrime.securesms.conversation.v2.search.SearchViewModel import org.thoughtcrime.securesms.conversation.v2.utilities.AttachmentManager -import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog import org.thoughtcrime.securesms.conversation.v2.utilities.MentionManagerUtilities import org.thoughtcrime.securesms.conversation.v2.utilities.MentionUtilities import org.thoughtcrime.securesms.conversation.v2.utilities.ResendMessageUtilities diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/BlockedDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/BlockedDialog.kt index 7479c4a9e..c0ff1cbb1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/BlockedDialog.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/BlockedDialog.kt @@ -1,22 +1,19 @@ package org.thoughtcrime.securesms.conversation.v2.dialogs -import android.content.Context import android.app.Dialog +import android.content.Context import android.graphics.Typeface import android.os.Bundle import android.text.Spannable import android.text.SpannableStringBuilder import android.text.style.StyleSpan -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch import androidx.fragment.app.DialogFragment import network.loki.messenger.R +import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.contacts.Contact import org.session.libsession.utilities.recipients.Recipient import org.thoughtcrime.securesms.createSessionDialog import org.thoughtcrime.securesms.dependencies.DatabaseComponent -import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities /** Shown upon sending a message to a user that's blocked. */ class BlockedDialog(private val recipient: Recipient, private val context: Context) : DialogFragment() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ConfigFactory.kt b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ConfigFactory.kt index 48eda4500..d664ffedb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ConfigFactory.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ConfigFactory.kt @@ -50,7 +50,7 @@ class ConfigFactory( private val userGroupsLock = Object() private var _userGroups: UserGroupsConfig? = null - private val isConfigForcedOn = TextSecurePreferences.hasForcedNewConfig(context) + private val isConfigForcedOn by lazy { TextSecurePreferences.hasForcedNewConfig(context) } private val listeners: MutableList = mutableListOf() fun registerListener(listener: ConfigFactoryUpdateListener) {