Add Session Id blinding (#862)

* feat: Add Session Id blinding

Including modified version of lazysodium-android to expose missing libsodium functions, we could build from a fork which we still need to setup.

* Add v4 onion request handling

* Update SOGS signature construction

* Fix SOGS signature construction

* Update onion request

* Update signature data

* Keep path prefixes for v4 endpoints

* Update SOGS signature message

* Rename to remove api version suffix

* Update onion response parsing

* Refactor file download paths

* Implement request batching

* Refactor batch response handling

* Handle batch endpoint responses

* Update batch endpoint responses

* Update attachment download handling

* Handle file downloads

* Handle inbox messages

* Fix issue with file downloads

* Preserve image bytearray encoding

* Refactor

* Open group message requests

* Check id blinding in user detail bottom sheet rather

* Message validation refactor

* Cache last inbox/outbox server ids

* Update message encryption/decryption

* Refactor

* Refactor

* Bypass user details bottom sheet in open groups for blinded session ids

* Fix capabilities call auth

* Refactor

* Revert default server details

* Update sodium dependency to forked repo

* Fix attachment upload

* Revert "Update sodium dependency to forked repo"

This reverts commit c7db9529f9.

* Add signed sodium lib

* Update contact id truncation and mention logic

* Open group inbox messaging fix

* Refactor

* Update blinded id check

* Fix open group message sends

* Fix crash on open group direct message send

* Direct message refactor

* Direct message encrypt/decrypt fixes

* Use updated curve25519 version

* Updated lazysodium dependency

* Update encryption/decryption calls

* Handle direct message parse errors

* Minor refactor

* Existing chat refactor

* Update encryption & decryption parameters

* Fix authenticated ciphertext size

* Set direct message sync target

* Update direct message thread lookup

* Add blinded id mapping table

* Add blinded id mapping table

* Update threads after sends

* Update open group message timestamp handling

* Filter unblinded contacts

* Format blinded id mentions

* Add message deleted field

* Hide open group inbox id

* Update message request response handling

* Update message request response sender handling

* Fix mentions of blinded ids

* Handle open group poll failure

* fix: add log for failed open group onion request, add decoding body for blinding required error at destination

* fix: change the error check

* Persist group members

* Reschedule polling after capabilities update

* Retry on other exceptions

* Minor refactor

* Open group profile fix

* Group member db schema update

* Fix ban request key

* Update ban response type

* Ban endpoint updates

* Ban endpoint updates

* Delete messages

Co-authored-by: charles <charles@oxen.io>
Co-authored-by: jubb <hjubb@users.noreply.github.com>
This commit is contained in:
ceokot
2022-08-10 18:17:48 +10:00
committed by GitHub
parent b1e954084c
commit bee287bb7e
90 changed files with 3192 additions and 1190 deletions

View File

@@ -104,8 +104,8 @@ dependencies {
implementation project(":libsignal")
implementation project(":libsession")
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinxJsonVersion"
implementation "org.whispersystems:curve25519-java:$curve25519Version"
implementation 'com.goterl:lazysodium-android:5.0.2@aar'
implementation "com.github.oxen-io.session-android-curve-25519:curve25519-java:$curve25519Version"
implementation project(":liblazysodium")
implementation "net.java.dev.jna:jna:5.8.0@aar"
implementation "com.google.protobuf:protobuf-java:$protobufVersion"
implementation "com.fasterxml.jackson.core:jackson-databind:$jacksonDatabindVersion"

View File

@@ -0,0 +1,166 @@
package network.loki.messenger
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.goterl.lazysodium.utils.Key
import com.goterl.lazysodium.utils.KeyPair
import org.hamcrest.CoreMatchers.equalTo
import org.hamcrest.MatcherAssert.assertThat
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.session.libsession.messaging.utilities.SodiumUtilities
import org.session.libsignal.utilities.Base64
import org.session.libsignal.utilities.Hex
import org.session.libsignal.utilities.toHexString
@RunWith(AndroidJUnit4::class)
class SodiumUtilitiesTest {
private val publicKey: String = "88672ccb97f40bb57238989226cf429b575ba355443f47bc76c5ab144a96c65b"
private val privateKey: String = "30d796c1ddb4dc455fd998a98aa275c247494a9a7bde9c1fee86ae45cd585241"
private val edKeySeed: String = "c010d89eccbaf5d1c6d19df766c6eedf965d4a28a56f87c9fc819edb59896dd9"
private val edPublicKey: String = "bac6e71efd7dfa4a83c98ed24f254ab2c267f9ccdb172a5280a0444ad24e89cc"
private val edSecretKey: String = "c010d89eccbaf5d1c6d19df766c6eedf965d4a28a56f87c9fc819edb59896dd9bac6e71efd7dfa4a83c98ed24f254ab2c267f9ccdb172a5280a0444ad24e89cc"
private val blindedPublicKey: String = "98932d4bccbe595a8789d7eb1629cefc483a0eaddc7e20e8fe5c771efafd9af5"
private val serverPublicKey: String = "c3b3c6f32f0ab5a57f853cc4f30f5da7fda5624b0c77b3fb0829de562ada081d"
private val edKeyPair = KeyPair(Key.fromHexString(edPublicKey), Key.fromHexString(edSecretKey))
@Test
fun generateBlindingFactorSuccess() {
val result = SodiumUtilities.generateBlindingFactor(serverPublicKey)
assertThat(result?.toHexString(), equalTo("84e3eb75028a9b73fec031b7448e322a68ca6485fad81ab1bead56f759ebeb0f"))
}
@Test
fun generateBlindingFactorFailure() {
val result = SodiumUtilities.generateBlindingFactor("Test")
assertNull(result?.toHexString())
}
@Test
fun blindedKeyPairSuccess() {
val result = SodiumUtilities.blindedKeyPair(serverPublicKey, edKeyPair)!!
assertThat(result.publicKey.asHexString.lowercase(), equalTo(blindedPublicKey))
assertThat(result.secretKey.asHexString.take(64).lowercase(), equalTo("16663322d6b684e1c9dcc02b9e8642c3affd3bc431a9ea9e63dbbac88ce7a305"))
}
@Test
fun blindedKeyPairFailurePublicKeyLength() {
val result = SodiumUtilities.blindedKeyPair(
serverPublicKey,
KeyPair(Key.fromHexString(edPublicKey.take(4)), Key.fromHexString(edKeySeed))
)
assertNull(result)
}
@Test
fun blindedKeyPairFailureSecretKeyLength() {
val result = SodiumUtilities.blindedKeyPair(
serverPublicKey,
KeyPair(Key.fromHexString(edPublicKey), Key.fromHexString(edSecretKey.take(4)))
)
assertNull(result)
}
@Test
fun blindedKeyPairFailureBlindingFactor() {
val result = SodiumUtilities.blindedKeyPair("Test", edKeyPair)
assertNull(result)
}
@Test
fun sogsSignature() {
val expectedSignature = "dcc086abdd2a740d9260b008fb37e12aa0ff47bd2bd9e177bbbec37fd46705a9072ce747bda66c788c3775cdd7ad60ad15a478e0886779aad5d795fd7bf8350d"
val result = SodiumUtilities.sogsSignature(
"TestMessage".toByteArray(),
Hex.fromStringCondensed(edSecretKey),
Hex.fromStringCondensed("44d82cc15c0a5056825cae7520b6b52d000a23eb0c5ed94c4be2d9dc41d2d409"),
Hex.fromStringCondensed("0bb7815abb6ba5142865895f3e5286c0527ba4d31dbb75c53ce95e91ffe025a2")
)
assertThat(result?.toHexString(), equalTo(expectedSignature))
}
@Test
fun combineKeysSuccess() {
val result = SodiumUtilities.combineKeys(
Hex.fromStringCondensed(edSecretKey),
Hex.fromStringCondensed(edPublicKey)
)
assertThat(result?.toHexString(), equalTo("1159b5d0fcfba21228eb2121a0f59712fa8276fc6e5547ff519685a40b9819e6"))
}
@Test
fun combineKeysFailure() {
val result = SodiumUtilities.combineKeys(
SodiumUtilities.generatePrivateKeyScalar(Hex.fromStringCondensed(edSecretKey))!!,
Hex.fromStringCondensed(publicKey)
)
assertNull(result?.toHexString())
}
@Test
fun sharedBlindedEncryptionKeySuccess() {
val result = SodiumUtilities.sharedBlindedEncryptionKey(
Hex.fromStringCondensed(edSecretKey),
Hex.fromStringCondensed(blindedPublicKey),
Hex.fromStringCondensed(publicKey),
Hex.fromStringCondensed(blindedPublicKey)
)
assertThat(result?.toHexString(), equalTo("388ee09e4c356b91f1cce5cc0aa0cf59e8e8cade69af61685d09c2d2731bc99e"))
}
@Test
fun sharedBlindedEncryptionKeyFailure() {
val result = SodiumUtilities.sharedBlindedEncryptionKey(
Hex.fromStringCondensed(edSecretKey),
Hex.fromStringCondensed(publicKey),
Hex.fromStringCondensed(edPublicKey),
Hex.fromStringCondensed(publicKey)
)
assertNull(result?.toHexString())
}
@Test
fun sessionIdSuccess() {
val result = SodiumUtilities.sessionId("05$publicKey", "15$blindedPublicKey", serverPublicKey)
assertTrue(result)
}
@Test
fun sessionIdFailureInvalidSessionId() {
val result = SodiumUtilities.sessionId("AB$publicKey", "15$blindedPublicKey", serverPublicKey)
assertFalse(result)
}
@Test
fun sessionIdFailureInvalidBlindedId() {
val result = SodiumUtilities.sessionId("05$publicKey", "AB$blindedPublicKey", serverPublicKey)
assertFalse(result)
}
@Test
fun sessionIdFailureBlindingFactor() {
val result = SodiumUtilities.sessionId("05$publicKey", "15$blindedPublicKey", "Test")
assertFalse(result)
}
}

View File

@@ -15,6 +15,7 @@ import org.session.libsession.avatars.ProfileContactPhoto
import org.session.libsession.avatars.ResourceContactPhoto
import org.session.libsession.messaging.contacts.Contact
import org.session.libsession.utilities.Address
import org.session.libsession.utilities.GroupUtil
import org.session.libsession.utilities.recipients.Recipient
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
import org.thoughtcrime.securesms.mms.GlideRequests
@@ -57,6 +58,11 @@ class ProfilePictureView @JvmOverloads constructor(
val apk = members.getOrNull(1)?.serialize() ?: ""
additionalPublicKey = apk
additionalDisplayName = getUserDisplayName(apk)
} else if(recipient.isOpenGroupInboxRecipient) {
val publicKey = GroupUtil.getDecodedOpenGroupInbox(recipient.address.serialize())
this.publicKey = publicKey
displayName = getUserDisplayName(publicKey)
additionalPublicKey = null
} else {
val publicKey = recipient.address.toString()
this.publicKey = publicKey

View File

@@ -58,19 +58,22 @@ import org.session.libsession.messaging.messages.control.DataExtractionNotificat
import org.session.libsession.messaging.messages.signal.OutgoingMediaMessage
import org.session.libsession.messaging.messages.signal.OutgoingTextMessage
import org.session.libsession.messaging.messages.visible.VisibleMessage
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
import org.session.libsession.messaging.open_groups.OpenGroupApi
import org.session.libsession.messaging.sending_receiving.MessageSender
import org.session.libsession.messaging.sending_receiving.attachments.Attachment
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel
import org.session.libsession.messaging.utilities.SessionId
import org.session.libsession.utilities.Address
import org.session.libsession.utilities.Address.Companion.fromSerialized
import org.session.libsession.utilities.GroupUtil
import org.session.libsession.utilities.MediaTypes
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.concurrent.SimpleTask
import org.session.libsession.utilities.recipients.Recipient
import org.session.libsession.utilities.recipients.RecipientModifiedListener
import org.session.libsignal.crypto.MnemonicCodec
import org.session.libsignal.utilities.IdPrefix
import org.session.libsignal.utilities.ListenableFuture
import org.session.libsignal.utilities.guava.Optional
import org.session.libsignal.utilities.hexEncodedPrivateKey
@@ -109,6 +112,7 @@ import org.thoughtcrime.securesms.database.MmsDatabase
import org.thoughtcrime.securesms.database.MmsSmsDatabase
import org.thoughtcrime.securesms.database.SessionContactDatabase
import org.thoughtcrime.securesms.database.SmsDatabase
import org.thoughtcrime.securesms.database.Storage
import org.thoughtcrime.securesms.database.ThreadDatabase
import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
@@ -167,6 +171,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
@Inject lateinit var smsDb: SmsDatabase
@Inject lateinit var mmsDb: MmsDatabase
@Inject lateinit var lokiMessageDb: LokiMessageDatabase
@Inject lateinit var storage: Storage
@Inject lateinit var viewModelFactory: ConversationViewModel.AssistedFactory
private val screenWidth = Resources.getSystem().displayMetrics.widthPixels
@@ -177,9 +182,25 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
private val viewModel: ConversationViewModel by viewModels {
var threadId = intent.getLongExtra(THREAD_ID, -1L)
if (threadId == -1L) {
intent.getParcelableExtra<Address>(ADDRESS)?.let { address ->
val recipient = Recipient.from(this, address, false)
threadId = threadDb.getOrCreateThreadIdFor(recipient)
intent.getParcelableExtra<Address>(ADDRESS)?.let { it ->
threadId = threadDb.getThreadIdIfExistsFor(it.serialize())
if (threadId == -1L) {
val sessionId = SessionId(it.serialize())
val openGroup = lokiThreadDb.getOpenGroupChat(intent.getLongExtra(FROM_GROUP_THREAD_ID, -1))
val address = if (sessionId.prefix == IdPrefix.BLINDED && openGroup != null) {
storage.getOrCreateBlindedIdMapping(sessionId.hexString, openGroup.server, openGroup.publicKey).sessionId?.let {
fromSerialized(it)
} ?: run {
val openGroupInboxId =
"${openGroup.server}!${openGroup.publicKey}!${sessionId.hexString}".toByteArray()
fromSerialized(GroupUtil.getEncodedOpenGroupInboxID(openGroupInboxId))
}
} else {
it
}
val recipient = Recipient.from(this, address, false)
threadId = threadDb.getOrCreateThreadIdFor(recipient)
}
} ?: finish()
}
viewModelFactory.create(threadId)
@@ -263,6 +284,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
// Extras
const val THREAD_ID = "thread_id"
const val ADDRESS = "address"
const val FROM_GROUP_THREAD_ID = "from_group_thread_id"
const val SCROLL_MESSAGE_ID = "scroll_message_id"
const val SCROLL_MESSAGE_AUTHOR = "scroll_message_author"
// Request codes
@@ -508,7 +530,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
private fun getLatestOpenGroupInfoIfNeeded() {
val openGroup = lokiThreadDb.getOpenGroupChat(viewModel.threadId) ?: return
OpenGroupAPIV2.getMemberCount(openGroup.room, openGroup.server).successUi { updateSubtitle() }
OpenGroupApi.getMemberCount(openGroup.room, openGroup.server).successUi { updateSubtitle() }
}
// called from onCreate

View File

@@ -7,7 +7,7 @@ import android.view.View
import android.widget.LinearLayout
import network.loki.messenger.databinding.ViewMentionCandidateBinding
import org.session.libsession.messaging.mentions.Mention
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
import org.thoughtcrime.securesms.groups.OpenGroupManager
import org.thoughtcrime.securesms.mms.GlideRequests
class MentionCandidateView : LinearLayout {
@@ -34,7 +34,7 @@ class MentionCandidateView : LinearLayout {
profilePictureView.root.glide = glide!!
profilePictureView.root.update()
if (openGroupServer != null && openGroupRoom != null) {
val isUserModerator = OpenGroupAPIV2.isUserModerator(mentionCandidate.publicKey, openGroupRoom!!, openGroupServer!!)
val isUserModerator = OpenGroupManager.isUserModerator(context, "$openGroupRoom.$openGroupServer", mentionCandidate.publicKey)
moderatorIconImageView.visibility = if (isUserModerator) View.VISIBLE else View.GONE
} else {
moderatorIconImageView.visibility = View.GONE

View File

@@ -7,7 +7,7 @@ import android.view.View
import android.widget.RelativeLayout
import network.loki.messenger.databinding.ViewMentionCandidateV2Binding
import org.session.libsession.messaging.mentions.Mention
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
import org.thoughtcrime.securesms.groups.OpenGroupManager
import org.thoughtcrime.securesms.mms.GlideRequests
class MentionCandidateView : RelativeLayout {
@@ -34,7 +34,7 @@ class MentionCandidateView : RelativeLayout {
profilePictureView.root.glide = glide!!
profilePictureView.root.update()
if (openGroupServer != null && openGroupRoom != null) {
val isUserModerator = OpenGroupAPIV2.isUserModerator(candidate.publicKey, openGroupRoom!!, openGroupServer!!)
val isUserModerator = OpenGroupManager.isUserModerator(context, "$openGroupRoom.$openGroupServer", candidate.publicKey)
moderatorIconImageView.visibility = if (isUserModerator) View.VISIBLE else View.GONE
} else {
moderatorIconImageView.visibility = View.GONE

View File

@@ -5,13 +5,17 @@ import android.view.ActionMode
import android.view.Menu
import android.view.MenuItem
import network.loki.messenger.R
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.utilities.SessionId
import org.session.libsession.messaging.utilities.SodiumUtilities
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsignal.utilities.IdPrefix
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
import org.thoughtcrime.securesms.conversation.v2.ConversationAdapter
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord
import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
import org.thoughtcrime.securesms.groups.OpenGroupManager
class ConversationActionModeCallback(private val adapter: ConversationAdapter, private val threadID: Long,
private val context: Context) : ActionMode.Callback {
@@ -34,6 +38,9 @@ class ConversationActionModeCallback(private val adapter: ConversationAdapter, p
val openGroup = DatabaseComponent.get(context).lokiThreadDatabase().getOpenGroupChat(threadID)
val thread = DatabaseComponent.get(context).threadDatabase().getRecipientForThreadId(threadID)!!
val userPublicKey = TextSecurePreferences.getLocalNumber(context)!!
val edKeyPair = MessagingModuleConfiguration.shared.getUserED25519KeyPair()!!
val blindedPublicKey = openGroup?.publicKey?.let { SodiumUtilities.blindedKeyPair(it, edKeyPair)?.publicKey?.asBytes }
?.let { SessionId(IdPrefix.BLINDED, it) }?.hexString
fun userCanDeleteSelectedItems(): Boolean {
val allSentByCurrentUser = selectedItems.all { it.isOutgoing }
@@ -41,13 +48,13 @@ class ConversationActionModeCallback(private val adapter: ConversationAdapter, p
if (!ConversationActivityV2.IS_UNSEND_REQUESTS_ENABLED) {
if (openGroup == null) { return true }
if (allSentByCurrentUser) { return true }
return OpenGroupAPIV2.isUserModerator(userPublicKey, openGroup.room, openGroup.server)
return OpenGroupManager.isUserModerator(context, openGroup.groupId, userPublicKey, blindedPublicKey)
}
val allReceivedByCurrentUser = selectedItems.all { !it.isOutgoing }
if (openGroup == null) { return allSentByCurrentUser || allReceivedByCurrentUser }
if (allSentByCurrentUser) { return true }
return OpenGroupAPIV2.isUserModerator(userPublicKey, openGroup.room, openGroup.server)
return OpenGroupManager.isUserModerator(context, openGroup.groupId, userPublicKey, blindedPublicKey)
}
fun userCanBanSelectedUsers(): Boolean {
if (openGroup == null) { return false }
@@ -55,7 +62,7 @@ class ConversationActionModeCallback(private val adapter: ConversationAdapter, p
if (anySentByCurrentUser) { return false } // Users can't ban themselves
val selectedUsers = selectedItems.map { it.recipient.address.toString() }.toSet()
if (selectedUsers.size > 1) { return false }
return OpenGroupAPIV2.isUserModerator(userPublicKey, openGroup.room, openGroup.server)
return OpenGroupManager.isUserModerator(context, openGroup.groupId, userPublicKey, blindedPublicKey)
}
// Delete message
menu.findItem(R.id.menu_context_delete_message).isVisible = userCanDeleteSelectedItems()

View File

@@ -1,6 +1,7 @@
package org.thoughtcrime.securesms.conversation.v2.messages
import android.content.Context
import android.content.Intent
import android.content.res.Resources
import android.graphics.Canvas
import android.graphics.Rect
@@ -23,16 +24,19 @@ import network.loki.messenger.R
import network.loki.messenger.databinding.ViewVisibleMessageBinding
import org.session.libsession.messaging.contacts.Contact
import org.session.libsession.messaging.contacts.Contact.ContactContext
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
import org.session.libsession.utilities.Address
import org.session.libsession.utilities.ViewUtil
import org.session.libsignal.utilities.IdPrefix
import org.session.libsignal.utilities.ThreadUtils
import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
import org.thoughtcrime.securesms.database.LokiThreadDatabase
import org.thoughtcrime.securesms.database.MmsDatabase
import org.thoughtcrime.securesms.database.MmsSmsDatabase
import org.thoughtcrime.securesms.database.SmsDatabase
import org.thoughtcrime.securesms.database.ThreadDatabase
import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.groups.OpenGroupManager
import org.thoughtcrime.securesms.home.UserDetailsBottomSheet
import org.thoughtcrime.securesms.mms.GlideRequests
import org.thoughtcrime.securesms.util.DateUtils
@@ -140,15 +144,27 @@ class VisibleMessageView : LinearLayout {
binding.profilePictureView.root.glide = glide
binding.profilePictureView.root.update(message.individualRecipient)
binding.profilePictureView.root.setOnClickListener {
showUserDetails(senderSessionID, threadID)
if (thread.isOpenGroupRecipient) {
if (IdPrefix.fromValue(senderSessionID) == IdPrefix.BLINDED) {
val intent = Intent(context, ConversationActivityV2::class.java)
intent.putExtra(ConversationActivityV2.FROM_GROUP_THREAD_ID, threadID)
intent.putExtra(ConversationActivityV2.ADDRESS, Address.fromSerialized(senderSessionID))
context.startActivity(intent)
}
} else {
maybeShowUserDetails(senderSessionID, threadID)
}
}
if (thread.isOpenGroupRecipient) {
val openGroup = lokiThreadDb.getOpenGroupChat(threadID) ?: return
val isModerator = OpenGroupAPIV2.isUserModerator(
senderSessionID,
openGroup.room,
openGroup.server
)
var standardPublicKey = ""
var blindedPublicKey: String? = null
if (IdPrefix.fromValue(senderSessionID) == IdPrefix.BLINDED) {
blindedPublicKey = senderSessionID
} else {
standardPublicKey = senderSessionID
}
val isModerator = OpenGroupManager.isUserModerator(context, openGroup.groupId, standardPublicKey, blindedPublicKey)
binding.moderatorIconImageView.isVisible = !message.isOutgoing && isModerator
}
}
@@ -403,7 +419,7 @@ class VisibleMessageView : LinearLayout {
pressCallback = null
}
private fun showUserDetails(publicKey: String, threadID: Long) {
private fun maybeShowUserDetails(publicKey: String, threadID: Long) {
val userDetailsBottomSheet = UserDetailsBottomSheet()
val bundle = bundleOf(
UserDetailsBottomSheet.ARGUMENT_PUBLIC_KEY to publicKey,

View File

@@ -11,6 +11,7 @@ import androidx.core.content.res.ResourcesCompat
import network.loki.messenger.R
import nl.komponents.kovenant.combine.Tuple2
import org.session.libsession.messaging.contacts.Contact
import org.session.libsession.messaging.utilities.SodiumUtilities
import org.session.libsession.utilities.TextSecurePreferences
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
import org.thoughtcrime.securesms.util.UiModeUtilities
@@ -20,39 +21,27 @@ object MentionUtilities {
@JvmStatic
fun highlightMentions(text: CharSequence, threadID: Long, context: Context): String {
val threadDB = DatabaseComponent.get(context).threadDatabase()
val isOpenGroup = threadDB.getRecipientForThreadId(threadID)?.isOpenGroupRecipient ?: false
return highlightMentions(text, false, isOpenGroup, context).toString() // isOutgoingMessage is irrelevant
}
@JvmStatic
fun highlightMentions(text:CharSequence, isOpenGroup: Boolean, context: Context): String {
return highlightMentions(text, false, isOpenGroup, context).toString()
return highlightMentions(text, false, threadID, context).toString() // isOutgoingMessage is irrelevant
}
@JvmStatic
fun highlightMentions(text: CharSequence, isOutgoingMessage: Boolean, threadID: Long, context: Context): SpannableString {
val threadDB = DatabaseComponent.get(context).threadDatabase()
val isOpenGroup = threadDB.getRecipientForThreadId(threadID)?.isOpenGroupRecipient ?: false
return highlightMentions(text, isOutgoingMessage, isOpenGroup, context) // isOutgoingMessage is irrelevant
}
@JvmStatic
fun highlightMentions(text: CharSequence, isOutgoingMessage: Boolean, isOpenGroup: Boolean, context: Context): SpannableString {
@Suppress("NAME_SHADOWING") var text = text
val pattern = Pattern.compile("@[0-9a-fA-F]*")
var matcher = pattern.matcher(text)
val mentions = mutableListOf<Tuple2<Range<Int>, String>>()
var startIndex = 0
val userPublicKey = TextSecurePreferences.getLocalNumber(context)!!
val openGroup = DatabaseComponent.get(context).storage().getOpenGroup(threadID)
if (matcher.find(startIndex)) {
while (true) {
val publicKey = text.subSequence(matcher.start() + 1, matcher.end()).toString() // +1 to get rid of the @
val userDisplayName: String? = if (publicKey.equals(userPublicKey, ignoreCase = true)) {
TextSecurePreferences.getProfileName(context)
val isUserBlindedPublicKey = openGroup?.let { SodiumUtilities.sessionId(userPublicKey, publicKey, it.publicKey) } ?: false
val userDisplayName: String? = if (publicKey.equals(userPublicKey, ignoreCase = true) || isUserBlindedPublicKey) {
context.getString(R.string.MessageRecord_you)
} else {
val contact = DatabaseComponent.get(context).sessionContactDatabase().getContactWithSessionID(publicKey)
@Suppress("NAME_SHADOWING") val context = if (isOpenGroup) Contact.ContactContext.OPEN_GROUP else Contact.ContactContext.REGULAR
@Suppress("NAME_SHADOWING") val context = if (openGroup != null) Contact.ContactContext.OPEN_GROUP else Contact.ContactContext.REGULAR
contact?.displayName(context)
}
if (userDisplayName != null) {

View File

@@ -0,0 +1,88 @@
package org.thoughtcrime.securesms.database
import android.content.ContentValues
import android.content.Context
import android.database.Cursor
import androidx.core.database.getStringOrNull
import org.session.libsession.messaging.BlindedIdMapping
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
class BlindedIdMappingDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper) {
companion object {
const val TABLE_NAME = "blinded_id_mapping"
const val ROW_ID = "_id"
const val BLINDED_PK = "blinded_pk"
const val SESSION_PK = "session_pk"
const val SERVER_URL = "server_url"
const val SERVER_PK = "server_pk"
@JvmField
val CREATE_BLINDED_ID_MAPPING_TABLE_COMMAND = """
CREATE TABLE $TABLE_NAME (
$ROW_ID INTEGER PRIMARY KEY,
$BLINDED_PK TEXT NOT NULL,
$SESSION_PK TEXT DEFAULT NULL,
$SERVER_URL TEXT NOT NULL,
$SERVER_PK TEXT NOT NULL
)
""".trimIndent()
private fun readBlindedIdMapping(cursor: Cursor): BlindedIdMapping {
return BlindedIdMapping(
blindedId = cursor.getString(cursor.getColumnIndexOrThrow(BLINDED_PK)),
sessionId = cursor.getStringOrNull(cursor.getColumnIndexOrThrow(SESSION_PK)),
serverUrl = cursor.getString(cursor.getColumnIndexOrThrow(SERVER_URL)),
serverId = cursor.getString(cursor.getColumnIndexOrThrow(SERVER_PK)),
)
}
}
fun getBlindedIdMapping(blindedId: String): List<BlindedIdMapping> {
val query = "$BLINDED_PK = ?"
val args = arrayOf(blindedId)
val mappings: MutableList<BlindedIdMapping> = mutableListOf()
readableDatabase.query(TABLE_NAME, null, query, args, null, null, null).use { cursor ->
while (cursor.moveToNext()) {
mappings += readBlindedIdMapping(cursor)
}
}
return mappings
}
fun addBlindedIdMapping(blindedIdMapping: BlindedIdMapping) {
writableDatabase.beginTransaction()
try {
val values = ContentValues().apply {
put(BLINDED_PK, blindedIdMapping.blindedId)
put(SERVER_PK, blindedIdMapping.sessionId)
put(SERVER_URL, blindedIdMapping.serverUrl)
put(SERVER_PK, blindedIdMapping.serverId)
}
writableDatabase.insert(TABLE_NAME, null, values)
writableDatabase.setTransactionSuccessful()
} finally {
writableDatabase.endTransaction()
}
}
fun getBlindedIdMappingsExceptFor(server: String): List<BlindedIdMapping> {
val query = "$SESSION_PK IS NOT NULL AND $SERVER_URL <> ?"
val args = arrayOf(server)
val mappings: MutableList<BlindedIdMapping> = mutableListOf()
readableDatabase.query(TABLE_NAME, null, query, args, null, null, null).use { cursor ->
while (cursor.moveToNext()) {
mappings += readBlindedIdMapping(cursor)
}
}
return mappings
}
}

View File

@@ -23,6 +23,8 @@ import android.database.Cursor;
import androidx.annotation.NonNull;
import net.sqlcipher.database.SQLiteDatabase;
import org.session.libsession.utilities.WindowDebouncer;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
@@ -92,4 +94,12 @@ public abstract class Database {
this.databaseHelper = databaseHelper;
}
protected SQLiteDatabase getReadableDatabase() {
return databaseHelper.getReadableDatabase();
}
protected SQLiteDatabase getWritableDatabase() {
return databaseHelper.getWritableDatabase();
}
}

View File

@@ -0,0 +1,72 @@
package org.thoughtcrime.securesms.database
import android.content.ContentValues
import android.content.Context
import android.database.Cursor
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
import org.session.libsession.messaging.open_groups.GroupMember
import org.session.libsession.messaging.open_groups.GroupMemberRole
class GroupMemberDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper) {
companion object {
const val TABLE_NAME = "group_member"
const val GROUP_ID = "group_id"
const val PROFILE_ID = "profile_id"
const val ROLE = "role"
private val allColumns = arrayOf(GROUP_ID, PROFILE_ID, ROLE)
@JvmField
val CREATE_GROUP_MEMBER_TABLE_COMMAND = """
CREATE TABLE $TABLE_NAME (
$GROUP_ID TEXT NOT NULL,
$PROFILE_ID TEXT NOT NULL,
$ROLE TEXT NOT NULL,
PRIMARY KEY ($GROUP_ID, $PROFILE_ID)
)
""".trimIndent()
private fun readGroupMember(cursor: Cursor): GroupMember {
return GroupMember(
groupId = cursor.getString(cursor.getColumnIndexOrThrow(GROUP_ID)),
profileId = cursor.getString(cursor.getColumnIndexOrThrow(PROFILE_ID)),
role = GroupMemberRole.valueOf(cursor.getString(cursor.getColumnIndexOrThrow(ROLE))),
)
}
}
fun getGroupMemberRoles(groupId: String, profileId: String): List<GroupMemberRole> {
val query = "$GROUP_ID = ? AND $PROFILE_ID = ?"
val args = arrayOf(groupId, profileId)
val mappings: MutableList<GroupMember> = mutableListOf()
readableDatabase.query(TABLE_NAME, allColumns, query, args, null, null, null).use { cursor ->
while (cursor.moveToNext()) {
mappings += readGroupMember(cursor)
}
}
return mappings.map { it.role }
}
fun addGroupMember(member: GroupMember) {
writableDatabase.beginTransaction()
try {
val values = ContentValues().apply {
put(GROUP_ID, member.groupId)
put(PROFILE_ID, member.profileId)
put(ROLE, member.role.name)
}
val query = "$GROUP_ID = ? AND $PROFILE_ID = ?"
val args = arrayOf(member.groupId, member.profileId)
writableDatabase.insertOrUpdate(TABLE_NAME, values, query, args)
writableDatabase.setTransactionSuccessful()
} finally {
writableDatabase.endTransaction()
}
}
}

View File

@@ -12,7 +12,7 @@ import org.session.libsignal.utilities.Hex
import org.session.libsignal.utilities.Log
import org.session.libsignal.utilities.PublicKeyValidation
import org.session.libsignal.utilities.Snode
import org.session.libsignal.utilities.removing05PrefixIfNeeded
import org.session.libsignal.utilities.removingIdPrefixIfNeeded
import org.session.libsignal.utilities.toHexString
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
@@ -127,6 +127,21 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
"""
const val INSERT_RECEIVED_HASHES_DATA = "INSERT OR IGNORE INTO $receivedMessageHashValuesTable($publicKey, $receivedMessageHashValues) SELECT $publicKey, $receivedMessageHashValues FROM $legacyReceivedMessageHashValuesTable3;"
const val DROP_LEGACY_RECEIVED_HASHES = "DROP TABLE $legacyReceivedMessageHashValuesTable3;"
// Open group server capabilities
private val serverCapabilitiesTable = "open_group_server_capabilities"
private val capabilities = "capabilities"
@JvmStatic
val createServerCapabilitiesCommand = "CREATE TABLE $serverCapabilitiesTable($server STRING PRIMARY KEY, $capabilities STRING)"
// Last inbox message server IDs
private val lastInboxMessageServerIdTable = "open_group_last_inbox_message_server_id_cache"
private val lastInboxMessageServerId = "last_inbox_message_server_id"
@JvmStatic
val createLastInboxMessageServerIdCommand = "CREATE TABLE $lastInboxMessageServerIdTable($server STRING PRIMARY KEY, $lastInboxMessageServerId INTEGER DEFAULT 0)"
// Last outbox message server IDs
private val lastOutboxMessageServerIdTable = "open_group_last_outbox_message_server_id_cache"
private val lastOutboxMessageServerId = "last_outbox_message_server_id"
@JvmStatic
val createLastOutboxMessageServerIdCommand = "CREATE TABLE $lastOutboxMessageServerIdTable($server STRING PRIMARY KEY, $lastOutboxMessageServerId INTEGER DEFAULT 0)"
// region Deprecated
private val deviceLinkCache = "loki_pairing_authorisation_cache"
@@ -423,14 +438,14 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
override fun getUserX25519KeyPair(): ECKeyPair {
val keyPair = IdentityKeyUtil.getIdentityKeyPair(context)
return ECKeyPair(DjbECPublicKey(keyPair.publicKey.serialize().removing05PrefixIfNeeded()), DjbECPrivateKey(keyPair.privateKey.serialize()))
return ECKeyPair(DjbECPublicKey(keyPair.publicKey.serialize().removingIdPrefixIfNeeded()), DjbECPrivateKey(keyPair.privateKey.serialize()))
}
fun addClosedGroupEncryptionKeyPair(encryptionKeyPair: ECKeyPair, groupPublicKey: String) {
val database = databaseHelper.writableDatabase
val timestamp = Date().time.toString()
val index = "$groupPublicKey-$timestamp"
val encryptionKeyPairPublicKey = encryptionKeyPair.publicKey.serialize().toHexString().removing05PrefixIfNeeded()
val encryptionKeyPairPublicKey = encryptionKeyPair.publicKey.serialize().toHexString().removingIdPrefixIfNeeded()
val encryptionKeyPairPrivateKey = encryptionKeyPair.privateKey.serialize().toHexString()
val row = wrap(mapOf(closedGroupsEncryptionKeyPairIndex to index, Companion.encryptionKeyPairPublicKey to encryptionKeyPairPublicKey,
Companion.encryptionKeyPairPrivateKey to encryptionKeyPairPrivateKey ))
@@ -481,6 +496,53 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
database.delete(closedGroupPublicKeysTable, "${Companion.groupPublicKey} = ?", wrap(groupPublicKey))
}
fun setServerCapabilities(serverName: String, serverCapabilities: List<String>) {
val database = databaseHelper.writableDatabase
val row = wrap(mapOf(server to serverName, capabilities to serverCapabilities.joinToString(",")))
database.insertOrUpdate(serverCapabilitiesTable, row, "$server = ?", wrap(serverName))
}
fun getServerCapabilities(serverName: String): List<String> {
val database = databaseHelper.writableDatabase
return database.get(serverCapabilitiesTable, "$server = ?", wrap(serverName)) { cursor ->
cursor.getString(capabilities)
}?.split(",") ?: emptyList()
}
fun setLastInboxMessageId(serverName: String, newValue: Long) {
val database = databaseHelper.writableDatabase
val row = wrap(mapOf(server to serverName, lastInboxMessageServerId to newValue.toString()))
database.insertOrUpdate(lastInboxMessageServerIdTable, row, "$server = ?", wrap(serverName))
}
fun getLastInboxMessageId(serverName: String): Long? {
val database = databaseHelper.writableDatabase
return database.get(lastInboxMessageServerIdTable, "$server = ?", wrap(serverName)) { cursor ->
cursor.getInt(lastInboxMessageServerId)
}?.toLong()
}
fun removeLastInboxMessageId(serverName: String) {
databaseHelper.writableDatabase.delete(lastInboxMessageServerIdTable, "$server = ?", wrap(serverName))
}
fun setLastOutboxMessageId(serverName: String, newValue: Long) {
val database = databaseHelper.writableDatabase
val row = wrap(mapOf(server to serverName, lastOutboxMessageServerId to newValue.toString()))
database.insertOrUpdate(lastOutboxMessageServerIdTable, row, "$server = ?", wrap(serverName))
}
fun getLastOutboxMessageId(serverName: String): Long? {
val database = databaseHelper.writableDatabase
return database.get(lastOutboxMessageServerIdTable, "$server = ?", wrap(serverName)) { cursor ->
cursor.getInt(lastOutboxMessageServerId)
}?.toLong()
}
fun removeLastOutboxMessageId(serverName: String) {
databaseHelper.writableDatabase.delete(lastOutboxMessageServerIdTable, "$server = ?", wrap(serverName))
}
override fun getForkInfo(): ForkInfo {
val database = databaseHelper.readableDatabase
val queryCursor = database.query(FORK_INFO_TABLE, arrayOf(HF_VALUE, SF_VALUE), "$DUMMY_KEY = $DUMMY_VALUE", null, null, null, null)

View File

@@ -3,7 +3,7 @@ package org.thoughtcrime.securesms.database
import android.content.ContentValues
import android.content.Context
import android.database.Cursor
import org.session.libsession.messaging.open_groups.OpenGroupV2
import org.session.libsession.messaging.open_groups.OpenGroup
import org.session.libsession.utilities.Address
import org.session.libsession.utilities.recipients.Recipient
import org.session.libsignal.utilities.JsonUtil
@@ -30,16 +30,16 @@ class LokiThreadDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa
return DatabaseComponent.get(context).threadDatabase().getOrCreateThreadIdFor(recipient)
}
fun getAllV2OpenGroups(): Map<Long, OpenGroupV2> {
fun getAllOpenGroups(): Map<Long, OpenGroup> {
val database = databaseHelper.readableDatabase
var cursor: Cursor? = null
val result = mutableMapOf<Long, OpenGroupV2>()
val result = mutableMapOf<Long, OpenGroup>()
try {
cursor = database.rawQuery("select * from $publicChatTable", null)
while (cursor != null && cursor.moveToNext()) {
val threadID = cursor.getLong(threadID)
val string = cursor.getString(publicChat)
val openGroup = OpenGroupV2.fromJSON(string)
val openGroup = OpenGroup.fromJSON(string)
if (openGroup != null) result[threadID] = openGroup
}
} catch (e: Exception) {
@@ -50,25 +50,25 @@ class LokiThreadDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa
return result
}
fun getOpenGroupChat(threadID: Long): OpenGroupV2? {
fun getOpenGroupChat(threadID: Long): OpenGroup? {
if (threadID < 0) {
return null
}
val database = databaseHelper.readableDatabase
return database.get(publicChatTable, "${Companion.threadID} = ?", arrayOf(threadID.toString())) { cursor ->
val json = cursor.getString(publicChat)
OpenGroupV2.fromJSON(json)
OpenGroup.fromJSON(json)
}
}
fun setOpenGroupChat(openGroupV2: OpenGroupV2, threadID: Long) {
fun setOpenGroupChat(openGroup: OpenGroup, threadID: Long) {
if (threadID < 0) {
return
}
val database = databaseHelper.writableDatabase
val contentValues = ContentValues(2)
contentValues.put(Companion.threadID, threadID)
contentValues.put(publicChat, JsonUtil.toJson(openGroupV2.toJson()))
contentValues.put(publicChat, JsonUtil.toJson(openGroup.toJson()))
database.insertOrUpdate(publicChatTable, contentValues, "${Companion.threadID} = ?", arrayOf(threadID.toString()))
}

View File

@@ -42,6 +42,8 @@ public abstract class MessagingDatabase extends Database implements MmsSmsColumn
public abstract boolean deleteMessage(long messageId);
public abstract void updateThreadId(long fromId, long toId);
public void addMismatchedIdentity(long messageId, Address address, IdentityKey identityKey) {
try {
addToDocument(messageId, MISMATCHED_IDENTITIES,

View File

@@ -1031,6 +1031,16 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
return threadDeleted
}
override fun updateThreadId(fromId: Long, toId: Long) {
val contentValues = ContentValues(1)
contentValues.put(THREAD_ID, toId)
val db = databaseHelper.writableDatabase
db.update(SmsDatabase.TABLE_NAME, contentValues, "$THREAD_ID = ?", arrayOf("$fromId"))
notifyConversationListeners(toId)
notifyConversationListListeners()
}
fun deleteThread(threadId: Long) {
deleteThreads(setOf(threadId))
}

View File

@@ -93,12 +93,11 @@ public class MmsSmsDatabase extends Database {
MmsSmsDatabase.Reader reader = readerFor(cursor);
MessageRecord messageRecord;
boolean isOwnNumber = Util.isOwnNumber(context, serializedAuthor);
while ((messageRecord = reader.getNext()) != null) {
if ((Util.isOwnNumber(context, serializedAuthor) && messageRecord.isOutgoing()) ||
(!Util.isOwnNumber(context, serializedAuthor)
&& messageRecord.getIndividualRecipient().getAddress().serialize().equals(serializedAuthor)
))
if ((isOwnNumber && messageRecord.isOutgoing()) ||
(!isOwnNumber && messageRecord.getIndividualRecipient().getAddress().serialize().equals(serializedAuthor)))
{
return messageRecord;
}

View File

@@ -255,10 +255,6 @@ public class RecipientDatabase extends Database {
recipient.resolve().setApproved(approved);
}
public void setAllApproved(List<String> addresses) {
}
public void setApprovedMe(@NonNull Recipient recipient, boolean approvedMe) {
ContentValues values = new ContentValues();
values.put(APPROVED_ME, approvedMe ? 1 : 0);

View File

@@ -575,6 +575,17 @@ public class SmsDatabase extends MessagingDatabase {
return threadDeleted;
}
@Override
public void updateThreadId(long fromId, long toId) {
ContentValues contentValues = new ContentValues(1);
contentValues.put(MmsSmsColumns.THREAD_ID, toId);
SQLiteDatabase db = databaseHelper.getWritableDatabase();
db.update(TABLE_NAME, contentValues, THREAD_ID + " = ?", new String[] {fromId + ""});
notifyConversationListeners(toId);
notifyConversationListListeners();
}
private boolean isDuplicate(IncomingTextMessage message, long threadId) {
SQLiteDatabase database = databaseHelper.getReadableDatabase();
Cursor cursor = database.query(TABLE_NAME, null, DATE_SENT + " = ? AND " + ADDRESS + " = ? AND " + THREAD_ID + " = ?",

View File

@@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.database
import android.content.Context
import android.net.Uri
import org.session.libsession.database.StorageProtocol
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
@@ -22,12 +23,15 @@ import org.session.libsession.messaging.messages.signal.OutgoingMediaMessage
import org.session.libsession.messaging.messages.signal.OutgoingTextMessage
import org.session.libsession.messaging.messages.visible.Attachment
import org.session.libsession.messaging.messages.visible.VisibleMessage
import org.session.libsession.messaging.open_groups.OpenGroupV2
import org.session.libsession.messaging.open_groups.GroupMember
import org.session.libsession.messaging.open_groups.OpenGroup
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentId
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
import org.session.libsession.messaging.sending_receiving.data_extraction.DataExtractionNotificationInfoMessage
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel
import org.session.libsession.messaging.utilities.SessionId
import org.session.libsession.messaging.utilities.SodiumUtilities
import org.session.libsession.messaging.utilities.UpdateMessageData
import org.session.libsession.snode.OnionRequestAPI
import org.session.libsession.utilities.Address
@@ -40,6 +44,7 @@ import org.session.libsession.utilities.recipients.Recipient
import org.session.libsignal.crypto.ecc.ECKeyPair
import org.session.libsignal.messages.SignalServiceAttachmentPointer
import org.session.libsignal.messages.SignalServiceGroup
import org.session.libsignal.utilities.IdPrefix
import org.session.libsignal.utilities.KeyHelper
import org.session.libsignal.utilities.guava.Optional
import org.thoughtcrime.securesms.ApplicationContext
@@ -73,7 +78,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
}
override fun setUserProfilePictureURL(newValue: String) {
val ourRecipient = Address.fromSerialized(getUserPublicKey()!!).let {
val ourRecipient = fromSerialized(getUserPublicKey()!!).let {
Recipient.from(context, it, false)
}
TextSecurePreferences.setProfilePictureURL(context, newValue)
@@ -125,8 +130,10 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
runIncrement: Boolean,
runThreadUpdate: Boolean): Long? {
var messageID: Long? = null
val senderAddress = Address.fromSerialized(message.sender!!)
val senderAddress = fromSerialized(message.sender!!)
val isUserSender = (message.sender!! == getUserPublicKey())
val isUserBlindedSender = message.threadID?.takeIf { it >= 0 }?.let { getOpenGroup(it)?.publicKey }
?.let { SodiumUtilities.sessionId(getUserPublicKey()!!, message.sender!!, it) } ?: false
val group: Optional<SignalServiceGroup> = when {
openGroupID != null -> Optional.of(SignalServiceGroup(openGroupID.toByteArray(), SignalServiceGroup.GroupType.PUBLIC_CHAT))
groupPublicKey != null -> {
@@ -138,17 +145,17 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
val pointers = attachments.mapNotNull {
it.toSignalAttachment()
}
val targetAddress = if (isUserSender && !message.syncTarget.isNullOrEmpty()) {
Address.fromSerialized(message.syncTarget!!)
val targetAddress = if ((isUserSender || isUserBlindedSender) && !message.syncTarget.isNullOrEmpty()) {
fromSerialized(message.syncTarget!!)
} else if (group.isPresent) {
Address.fromSerialized(GroupUtil.getEncodedId(group.get()))
fromSerialized(GroupUtil.getEncodedId(group.get()))
} else {
senderAddress
}
val targetRecipient = Recipient.from(context, targetAddress, false)
if (!targetRecipient.isGroupRecipient) {
val recipientDb = DatabaseComponent.get(context).recipientDatabase()
if (isUserSender) {
if (isUserSender || isUserBlindedSender) {
recipientDb.setApproved(targetRecipient, true)
} else {
recipientDb.setApprovedMe(targetRecipient, true)
@@ -158,7 +165,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
val quote: Optional<QuoteModel> = if (quotes != null) Optional.of(quotes) else Optional.absent()
val linkPreviews: Optional<List<LinkPreview>> = if (linkPreview.isEmpty()) Optional.absent() else Optional.of(linkPreview.mapNotNull { it!! })
val mmsDatabase = DatabaseComponent.get(context).mmsDatabase()
val insertResult = if (message.sender == getUserPublicKey()) {
val insertResult = if (isUserSender || isUserBlindedSender) {
val mediaMessage = OutgoingMediaMessage.from(message, targetRecipient, pointers, quote.orNull(), linkPreviews.orNull()?.firstOrNull())
mmsDatabase.insertSecureDecryptedMessageOutbox(mediaMessage, message.threadID ?: -1, message.sentTimestamp!!, runThreadUpdate)
} else {
@@ -176,7 +183,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
val smsDatabase = DatabaseComponent.get(context).smsDatabase()
val isOpenGroupInvitation = (message.openGroupInvitation != null)
val insertResult = if (message.sender == getUserPublicKey()) {
val insertResult = if (isUserSender || isUserBlindedSender) {
val textMessage = if (isOpenGroupInvitation) OutgoingTextMessage.fromOpenGroupInvitation(message.openGroupInvitation, targetRecipient, message.sentTimestamp)
else OutgoingTextMessage.from(message, targetRecipient)
smsDatabase.insertMessageOutbox(message.threadID ?: -1, textMessage, message.sentTimestamp!!, runThreadUpdate)
@@ -259,12 +266,12 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
DatabaseComponent.get(context).lokiAPIDatabase().setAuthToken(id, null)
}
override fun getV2OpenGroup(threadId: Long): OpenGroupV2? {
override fun getOpenGroup(threadId: Long): OpenGroup? {
if (threadId.toInt() < 0) { return null }
val database = databaseHelper.readableDatabase
return database.get(LokiThreadDatabase.publicChatTable, "${LokiThreadDatabase.threadID} = ?", arrayOf( threadId.toString() )) { cursor ->
val publicChatAsJson = cursor.getString(LokiThreadDatabase.publicChat)
OpenGroupV2.fromJSON(publicChatAsJson)
OpenGroup.fromJSON(publicChatAsJson)
}
}
@@ -309,6 +316,14 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
DatabaseComponent.get(context).lokiMessageDatabase().setOriginalThreadID(messageID, serverID, threadID)
}
override fun getOpenGroup(room: String, server: String): OpenGroup? {
return getAllOpenGroups().values.firstOrNull { it.server == server && it.room == room }
}
override fun addGroupMember(member: GroupMember) {
DatabaseComponent.get(context).groupMemberDatabase().addGroupMember(member)
}
override fun isDuplicateMessage(timestamp: Long): Boolean {
return getReceivedMessageTimestamps().contains(timestamp)
}
@@ -335,7 +350,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
override fun getMessageIdInDatabase(timestamp: Long, author: String): Long? {
val database = DatabaseComponent.get(context).mmsSmsDatabase()
val address = Address.fromSerialized(author)
val address = fromSerialized(author)
return database.getMessageFor(timestamp, address)?.getId()
}
@@ -453,7 +468,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
override fun insertIncomingInfoMessage(context: Context, senderPublicKey: String, groupID: String, type: SignalServiceGroup.Type, name: String, members: Collection<String>, admins: Collection<String>, sentTimestamp: Long) {
val group = SignalServiceGroup(type, GroupUtil.getDecodedGroupIDAsData(groupID), SignalServiceGroup.GroupType.SIGNAL, name, members.toList(), null, admins.toList())
val m = IncomingTextMessage(Address.fromSerialized(senderPublicKey), 1, sentTimestamp, "", Optional.of(group), 0, true)
val m = IncomingTextMessage(fromSerialized(senderPublicKey), 1, sentTimestamp, "", Optional.of(group), 0, true)
val updateData = UpdateMessageData.buildGroupUpdate(type, name, members)?.toJSON()
val infoMessage = IncomingGroupMessage(m, groupID, updateData, true)
val smsDB = DatabaseComponent.get(context).smsDatabase()
@@ -462,7 +477,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
override fun insertOutgoingInfoMessage(context: Context, groupID: String, type: SignalServiceGroup.Type, name: String, members: Collection<String>, admins: Collection<String>, threadID: Long, sentTimestamp: Long) {
val userPublicKey = getUserPublicKey()
val recipient = Recipient.from(context, Address.fromSerialized(groupID), false)
val recipient = Recipient.from(context, fromSerialized(groupID), false)
val updateData = UpdateMessageData.buildGroupUpdate(type, name, members)?.toJSON() ?: ""
val infoMessage = OutgoingGroupMediaMessage(recipient, updateData, groupID, null, sentTimestamp, 0, true, null, listOf(), listOf())
@@ -475,7 +490,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
override fun isClosedGroup(publicKey: String): Boolean {
val isClosedGroup = DatabaseComponent.get(context).lokiAPIDatabase().isClosedGroup(publicKey)
val address = Address.fromSerialized(publicKey)
val address = fromSerialized(publicKey)
return address.isClosedGroup || isClosedGroup
}
@@ -528,8 +543,20 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
DatabaseComponent.get(context).recipientDatabase().setExpireMessages(recipient, duration);
}
override fun getAllV2OpenGroups(): Map<Long, OpenGroupV2> {
return DatabaseComponent.get(context).lokiThreadDatabase().getAllV2OpenGroups()
override fun setServerCapabilities(server: String, capabilities: List<String>) {
return DatabaseComponent.get(context).lokiAPIDatabase().setServerCapabilities(server, capabilities)
}
override fun getServerCapabilities(server: String): List<String> {
return DatabaseComponent.get(context).lokiAPIDatabase().getServerCapabilities(server)
}
override fun getAllOpenGroups(): Map<Long, OpenGroup> {
return DatabaseComponent.get(context).lokiThreadDatabase().getAllOpenGroups()
}
override fun updateOpenGroup(openGroup: OpenGroup) {
OpenGroupManager.updateOpenGroup(openGroup, context)
}
override fun getAllGroups(): List<GroupRecord> {
@@ -541,7 +568,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
}
override fun onOpenGroupAdded(urlAsString: String) {
val server = OpenGroupV2.getServer(urlAsString)
val server = OpenGroup.getServer(urlAsString)
OpenGroupManager.restartPollerForServer(server.toString().removeSuffix("/"))
}
@@ -562,20 +589,20 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
override fun getOrCreateThreadIdFor(publicKey: String, groupPublicKey: String?, openGroupID: String?): Long {
val database = DatabaseComponent.get(context).threadDatabase()
if (!openGroupID.isNullOrEmpty()) {
val recipient = Recipient.from(context, Address.fromSerialized(GroupUtil.getEncodedOpenGroupID(openGroupID.toByteArray())), false)
return database.getThreadIdIfExistsFor(recipient)
return if (!openGroupID.isNullOrEmpty()) {
val recipient = Recipient.from(context, fromSerialized(GroupUtil.getEncodedOpenGroupID(openGroupID.toByteArray())), false)
database.getThreadIdIfExistsFor(recipient)
} else if (!groupPublicKey.isNullOrEmpty()) {
val recipient = Recipient.from(context, Address.fromSerialized(GroupUtil.doubleEncodeGroupID(groupPublicKey)), false)
return database.getOrCreateThreadIdFor(recipient)
val recipient = Recipient.from(context, fromSerialized(GroupUtil.doubleEncodeGroupID(groupPublicKey)), false)
database.getOrCreateThreadIdFor(recipient)
} else {
val recipient = Recipient.from(context, Address.fromSerialized(publicKey), false)
return database.getOrCreateThreadIdFor(recipient)
val recipient = Recipient.from(context, fromSerialized(publicKey), false)
database.getOrCreateThreadIdFor(recipient)
}
}
override fun getThreadId(publicKeyOrOpenGroupID: String): Long? {
val address = Address.fromSerialized(publicKeyOrOpenGroupID)
val address = fromSerialized(publicKeyOrOpenGroupID)
return getThreadId(address)
}
@@ -622,8 +649,13 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
override fun addContacts(contacts: List<ConfigurationMessage.Contact>) {
val recipientDatabase = DatabaseComponent.get(context).recipientDatabase()
val threadDatabase = DatabaseComponent.get(context).threadDatabase()
for (contact in contacts) {
val address = Address.fromSerialized(contact.publicKey)
val mappingDb = DatabaseComponent.get(context).blindedIdMappingDatabase()
val moreContacts = contacts.filter { contact ->
val id = SessionId(contact.publicKey)
id.prefix != IdPrefix.BLINDED || mappingDb.getBlindedIdMapping(contact.publicKey).none { it.sessionId != null }
}
for (contact in moreContacts) {
val address = fromSerialized(contact.publicKey)
val recipient = Recipient.from(context, address, true)
if (!contact.profilePicture.isNullOrEmpty()) {
recipientDatabase.setProfileAvatar(recipient, contact.profilePicture)
@@ -715,12 +747,48 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
threadDB.setHasSent(threadId, true)
} else {
val mmsDb = DatabaseComponent.get(context).mmsDatabase()
val senderAddress = fromSerialized(senderPublicKey)
val requestSender = Recipient.from(context, senderAddress, false)
recipientDb.setApprovedMe(requestSender, true)
val smsDb = DatabaseComponent.get(context).smsDatabase()
val sender = Recipient.from(context, fromSerialized(senderPublicKey), false)
val threadId = threadDB.getOrCreateThreadIdFor(sender)
threadDB.setHasSent(threadId, true)
val mappingDb = DatabaseComponent.get(context).blindedIdMappingDatabase()
val mappings = mutableMapOf<String, BlindedIdMapping>()
threadDB.readerFor(threadDB.conversationList).use { reader ->
while (reader.next != null) {
val recipient = reader.current.recipient
val address = recipient.address.serialize()
val blindedId = when {
recipient.isGroupRecipient -> null
recipient.isOpenGroupInboxRecipient -> {
GroupUtil.getDecodedOpenGroupInbox(address)
}
else -> {
if (SessionId(address).prefix == IdPrefix.BLINDED) {
address
} else null
}
} ?: continue
mappingDb.getBlindedIdMapping(blindedId).firstOrNull()?.let {
mappings[address] = it
}
}
}
for (mapping in mappings) {
if (!SodiumUtilities.sessionId(senderPublicKey, mapping.value.blindedId, mapping.value.serverId)) {
continue
}
mappingDb.addBlindedIdMapping(mapping.value.copy(sessionId = senderPublicKey))
val blindedThreadId = threadDB.getOrCreateThreadIdFor(Recipient.from(context, fromSerialized(mapping.key), false))
mmsDb.updateThreadId(blindedThreadId, threadId)
smsDb.updateThreadId(blindedThreadId, threadId)
threadDB.deleteConversation(blindedThreadId)
}
recipientDb.setApproved(sender, true)
recipientDb.setApprovedMe(sender, true)
val message = IncomingMediaMessage(
senderAddress,
sender.address,
response.sentTimestamp!!,
-1,
0,
@@ -735,7 +803,6 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
Optional.absent(),
Optional.absent()
)
val threadId = getOrCreateThreadIdFor(senderAddress)
mmsDb.insertSecureDecryptedMessageInbox(message, threadId, runIncrement = true, runThreadUpdate = true)
}
}
@@ -748,7 +815,6 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
DatabaseComponent.get(context).recipientDatabase().setApprovedMe(recipient, approvedMe)
}
override fun insertCallMessage(senderPublicKey: String, callMessageType: CallMessageType, sentTimestamp: Long) {
val database = DatabaseComponent.get(context).smsDatabase()
val address = fromSerialized(senderPublicKey)
@@ -764,4 +830,62 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
return database.getLastSeenAndHasSent(threadId).second() ?: false
}
override fun getLastInboxMessageId(server: String): Long? {
return DatabaseComponent.get(context).lokiAPIDatabase().getLastInboxMessageId(server)
}
override fun setLastInboxMessageId(server: String, messageId: Long) {
DatabaseComponent.get(context).lokiAPIDatabase().setLastInboxMessageId(server, messageId)
}
override fun removeLastInboxMessageId(server: String) {
DatabaseComponent.get(context).lokiAPIDatabase().removeLastInboxMessageId(server)
}
override fun getLastOutboxMessageId(server: String): Long? {
return DatabaseComponent.get(context).lokiAPIDatabase().getLastOutboxMessageId(server)
}
override fun setLastOutboxMessageId(server: String, messageId: Long) {
DatabaseComponent.get(context).lokiAPIDatabase().setLastOutboxMessageId(server, messageId)
}
override fun removeLastOutboxMessageId(server: String) {
DatabaseComponent.get(context).lokiAPIDatabase().removeLastOutboxMessageId(server)
}
override fun getOrCreateBlindedIdMapping(
blindedId: String,
server: String,
serverPublicKey: String,
fromOutbox: Boolean
): BlindedIdMapping {
val db = DatabaseComponent.get(context).blindedIdMappingDatabase()
val mapping = db.getBlindedIdMapping(blindedId).firstOrNull() ?: BlindedIdMapping(blindedId, null, server, serverPublicKey)
if (mapping.sessionId != null) {
return mapping
}
val threadDb = DatabaseComponent.get(context).threadDatabase()
threadDb.readerFor(threadDb.conversationList).use { reader ->
while (reader.next != null) {
val recipient = reader.current.recipient
val sessionId = recipient.address.serialize()
if (!recipient.isGroupRecipient && SodiumUtilities.sessionId(sessionId, blindedId, serverPublicKey)) {
val contactMapping = mapping.copy(sessionId = sessionId)
db.addBlindedIdMapping(contactMapping)
return contactMapping
}
}
}
db.getBlindedIdMappingsExceptFor(server).forEach {
if (SodiumUtilities.sessionId(it.sessionId!!, blindedId, serverPublicKey)) {
val otherMapping = mapping.copy(sessionId = it.sessionId)
db.addBlindedIdMapping(otherMapping)
return otherMapping
}
}
db.addBlindedIdMapping(mapping)
return mapping
}
}

View File

@@ -43,6 +43,7 @@ import org.session.libsession.utilities.TextSecurePreferences;
import org.session.libsession.utilities.Util;
import org.session.libsession.utilities.recipients.Recipient;
import org.session.libsession.utilities.recipients.Recipient.RecipientSettings;
import org.session.libsignal.utilities.IdPrefix;
import org.session.libsignal.utilities.Log;
import org.session.libsignal.utilities.Pair;
import org.session.libsignal.utilities.guava.Optional;
@@ -447,6 +448,11 @@ public class ThreadDatabase extends Database {
return getConversationList(where);
}
public Cursor getBlindedConversationList() {
String where = TABLE_NAME + "." + ADDRESS + " LIKE '" + IdPrefix.BLINDED.getValue() + "%' ";
return getConversationList(where);
}
public Cursor getApprovedConversationList() {
String where = "((" + MESSAGE_COUNT + " != 0 AND (" + HAS_SENT + " = 1 OR " + RecipientDatabase.APPROVED + " = 1 OR "+ GroupDatabase.TABLE_NAME +"."+GROUP_ID+" LIKE '"+CLOSED_GROUP_PREFIX+"%')) OR " + GroupDatabase.TABLE_NAME + "." + GROUP_ID + " LIKE '" + OPEN_GROUP_PREFIX + "%') " +
"AND " + ARCHIVED + " = 0 ";

View File

@@ -14,8 +14,10 @@ import org.session.libsession.utilities.TextSecurePreferences;
import org.session.libsignal.utilities.Log;
import org.thoughtcrime.securesms.crypto.DatabaseSecret;
import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.database.BlindedIdMappingDatabase;
import org.thoughtcrime.securesms.database.DraftDatabase;
import org.thoughtcrime.securesms.database.GroupDatabase;
import org.thoughtcrime.securesms.database.GroupMemberDatabase;
import org.thoughtcrime.securesms.database.GroupReceiptDatabase;
import org.thoughtcrime.securesms.database.JobDatabase;
import org.thoughtcrime.securesms.database.LokiAPIDatabase;
@@ -67,9 +69,10 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
private static final int lokiV32 = 53;
private static final int lokiV33 = 54;
private static final int lokiV34 = 55;
private static final int lokiV35 = 55;
// Loki - onUpgrade(...) must be updated to use Loki version numbers if Signal makes any database changes
private static final int DATABASE_VERSION = lokiV34;
private static final int DATABASE_VERSION = lokiV35;
private static final String DATABASE_NAME = "signal.db";
private final Context context;
@@ -135,6 +138,9 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
db.execSQL(LokiAPIDatabase.getCreateOpenGroupProfilePictureTableCommand());
db.execSQL(LokiAPIDatabase.getCreateClosedGroupEncryptionKeyPairsTable());
db.execSQL(LokiAPIDatabase.getCreateClosedGroupPublicKeysTable());
db.execSQL(LokiAPIDatabase.getCreateServerCapabilitiesCommand());
db.execSQL(LokiAPIDatabase.getCreateLastInboxMessageServerIdCommand());
db.execSQL(LokiAPIDatabase.getCreateLastOutboxMessageServerIdCommand());
db.execSQL(LokiMessageDatabase.getCreateMessageIDTableCommand());
db.execSQL(LokiMessageDatabase.getCreateMessageToThreadMappingTableCommand());
db.execSQL(LokiMessageDatabase.getCreateErrorMessageTableCommand());
@@ -161,6 +167,8 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
db.execSQL(LokiAPIDatabase.DROP_LEGACY_LAST_HASH);
db.execSQL(LokiAPIDatabase.INSERT_RECEIVED_HASHES_DATA);
db.execSQL(LokiAPIDatabase.DROP_LEGACY_RECEIVED_HASHES);
db.execSQL(BlindedIdMappingDatabase.CREATE_BLINDED_ID_MAPPING_TABLE_COMMAND);
db.execSQL(GroupMemberDatabase.CREATE_GROUP_MEMBER_TABLE_COMMAND);
executeStatements(db, SmsDatabase.CREATE_INDEXS);
executeStatements(db, MmsDatabase.CREATE_INDEXS);
@@ -369,6 +377,14 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
db.execSQL(LokiAPIDatabase.DROP_LEGACY_RECEIVED_HASHES);
}
if (oldVersion < lokiV35) {
db.execSQL(LokiAPIDatabase.getCreateServerCapabilitiesCommand());
db.execSQL(LokiAPIDatabase.getCreateLastInboxMessageServerIdCommand());
db.execSQL(LokiAPIDatabase.getCreateLastOutboxMessageServerIdCommand());
db.execSQL(BlindedIdMappingDatabase.CREATE_BLINDED_ID_MAPPING_TABLE_COMMAND);
db.execSQL(GroupMemberDatabase.CREATE_GROUP_MEMBER_TABLE_COMMAND);
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();

View File

@@ -115,6 +115,8 @@ public class ThreadRecord extends DisplayRecord {
return emphasisAdded(context.getString(R.string.ThreadRecord_you_marked_verified));
} else if (SmsDatabase.Types.isIdentityDefault(type)) {
return emphasisAdded(context.getString(R.string.ThreadRecord_you_marked_unverified));
} else if (MmsSmsColumns.Types.isMessageRequestResponse(type)) {
return emphasisAdded(context.getString(R.string.message_requests_accepted));
} else if (getCount() == 0) {
return new SpannableString(context.getString(R.string.ThreadRecord_empty_message));
} else {

View File

@@ -42,4 +42,6 @@ interface DatabaseComponent {
fun sessionContactDatabase(): SessionContactDatabase
fun storage(): Storage
fun attachmentProvider(): MessageDataProvider
fun blindedIdMappingDatabase(): BlindedIdMappingDatabase
fun groupMemberDatabase(): GroupMemberDatabase
}

View File

@@ -117,6 +117,14 @@ object DatabaseModule {
@Singleton
fun provideSessionContactDatabase(@ApplicationContext context: Context, openHelper: SQLCipherOpenHelper) = SessionContactDatabase(context,openHelper)
@Provides
@Singleton
fun provideBlindedIdMappingDatabase(@ApplicationContext context: Context, openHelper: SQLCipherOpenHelper) = BlindedIdMappingDatabase(context, openHelper)
@Provides
@Singleton
fun provideGroupMemberDatabase(@ApplicationContext context: Context, openHelper: SQLCipherOpenHelper) = GroupMemberDatabase(context, openHelper)
@Provides
@Singleton
fun provideStorage(@ApplicationContext context: Context, openHelper: SQLCipherOpenHelper) = Storage(context,openHelper)

View File

@@ -4,19 +4,22 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.asLiveData
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
import nl.komponents.kovenant.functional.map
import org.session.libsession.messaging.open_groups.OpenGroupApi
import org.thoughtcrime.securesms.util.State
typealias DefaultGroups = List<OpenGroupAPIV2.DefaultGroup>
typealias DefaultGroups = List<OpenGroupApi.DefaultGroup>
typealias GroupState = State<DefaultGroups>
class DefaultGroupsViewModel : ViewModel() {
init {
OpenGroupAPIV2.getDefaultRoomsIfNeeded()
OpenGroupApi.getDefaultServerCapabilities().map {
OpenGroupApi.getDefaultRoomsIfNeeded()
}
}
val defaultRooms = OpenGroupAPIV2.defaultRooms.map<DefaultGroups, GroupState> {
val defaultRooms = OpenGroupApi.defaultRooms.map<DefaultGroups, GroupState> {
State.Success(it)
}.onStart {
emit(State.Loading)

View File

@@ -26,7 +26,7 @@ import network.loki.messenger.databinding.ActivityJoinPublicChatBinding
import network.loki.messenger.databinding.FragmentEnterChatUrlBinding
import okhttp3.HttpUrl
import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2.DefaultGroup
import org.session.libsession.messaging.open_groups.OpenGroupApi.DefaultGroup
import org.session.libsession.utilities.Address
import org.session.libsession.utilities.GroupUtil
import org.session.libsession.utilities.recipients.Recipient

View File

@@ -4,16 +4,17 @@ import android.content.Context
import androidx.annotation.WorkerThread
import okhttp3.HttpUrl
import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
import org.session.libsession.messaging.open_groups.OpenGroupV2
import org.session.libsession.messaging.sending_receiving.pollers.OpenGroupPollerV2
import org.session.libsession.messaging.open_groups.GroupMemberRole
import org.session.libsession.messaging.open_groups.OpenGroup
import org.session.libsession.messaging.open_groups.OpenGroupApi
import org.session.libsession.messaging.sending_receiving.pollers.OpenGroupPoller
import org.session.libsignal.utilities.ThreadUtils
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
import java.util.concurrent.Executors
object OpenGroupManager {
private val executorService = Executors.newScheduledThreadPool(4)
private var pollers = mutableMapOf<String, OpenGroupPollerV2>() // One for each server
private var pollers = mutableMapOf<String, OpenGroupPoller>() // One for each server
private var isPolling = false
private val pollUpdaterLock = Any()
@@ -38,10 +39,10 @@ object OpenGroupManager {
if (isPolling) { return }
isPolling = true
val storage = MessagingModuleConfiguration.shared.storage
val servers = storage.getAllV2OpenGroups().values.map { it.server }.toSet()
val servers = storage.getAllOpenGroups().values.map { it.server }.toSet()
servers.forEach { server ->
pollers[server]?.stop() // Shouldn't be necessary
val poller = OpenGroupPollerV2(server, executorService)
val poller = OpenGroupPoller(server, executorService)
poller.startIfNeeded()
pollers[server] = poller
}
@@ -67,17 +68,21 @@ object OpenGroupManager {
// Clear any existing data if needed
storage.removeLastDeletionServerID(room, server)
storage.removeLastMessageServerID(room, server)
storage.removeLastInboxMessageId(server)
storage.removeLastOutboxMessageId(server)
// Store the public key
storage.setOpenGroupPublicKey(server,publicKey)
// Get group info
OpenGroupAPIV2.getAuthToken(room, server).get()
// Get group info
val info = OpenGroupAPIV2.getInfo(room, server).get()
// Get capabilities
val capabilities = OpenGroupApi.getCapabilities(server).get()
storage.setServerCapabilities(server, capabilities.capabilities)
// Get room info
val info = OpenGroupApi.getRoomInfo(room, server).get()
storage.setUserCount(room, server, info.activeUsers)
// Create the group locally if not available already
if (threadID < 0) {
threadID = GroupManager.createOpenGroup(openGroupID, context, null, info.name).threadId
}
val openGroup = OpenGroupV2(server, room, info.name, publicKey)
val openGroup = OpenGroup(server, room, info.name, info.infoUpdates, publicKey)
threadDB.setOpenGroupChat(openGroup, threadID)
}
@@ -86,7 +91,7 @@ object OpenGroupManager {
synchronized(pollUpdaterLock) {
pollers[server]?.stop()
pollers[server]?.startIfNeeded() ?: run {
val poller = OpenGroupPollerV2(server, executorService)
val poller = OpenGroupPoller(server, executorService)
pollers[server] = poller
poller.startIfNeeded()
}
@@ -102,7 +107,7 @@ object OpenGroupManager {
threadDB.setThreadArchived(threadID)
val groupID = recipient.address.serialize()
// Stop the poller if needed
val openGroups = storage.getAllV2OpenGroups().filter { it.value.server == server }
val openGroups = storage.getAllOpenGroups().filter { it.value.server == server }
if (openGroups.count() == 1) {
synchronized(pollUpdaterLock) {
val poller = pollers[server]
@@ -113,6 +118,8 @@ object OpenGroupManager {
// Delete
storage.removeLastDeletionServerID(room, server)
storage.removeLastMessageServerID(room, server)
storage.removeLastInboxMessageId(server)
storage.removeLastOutboxMessageId(server)
val lokiThreadDB = DatabaseComponent.get(context).lokiThreadDatabase()
lokiThreadDB.removeOpenGroupChat(threadID)
ThreadUtils.queue {
@@ -123,9 +130,26 @@ object OpenGroupManager {
fun addOpenGroup(urlAsString: String, context: Context) {
val url = HttpUrl.parse(urlAsString) ?: return
val server = OpenGroupV2.getServer(urlAsString)
val server = OpenGroup.getServer(urlAsString)
val room = url.pathSegments().firstOrNull() ?: return
val publicKey = url.queryParameter("public_key") ?: return
add(server.toString().removeSuffix("/"), room, publicKey, context)
}
fun updateOpenGroup(openGroup: OpenGroup, context: Context) {
val threadDB = DatabaseComponent.get(context).lokiThreadDatabase()
val openGroupID = "${openGroup.server}.${openGroup.room}"
val threadID = GroupManager.getOpenGroupThreadID(openGroupID, context)
threadDB.setOpenGroupChat(openGroup, threadID)
}
fun isUserModerator(context: Context, groupId: String, standardPublicKey: String, blindedPublicKey: String? = null): Boolean {
val memberDatabase = DatabaseComponent.get(context).groupMemberDatabase()
val standardRoles = memberDatabase.getGroupMemberRoles(groupId, standardPublicKey)
val blindedRoles = blindedPublicKey?.let { memberDatabase.getGroupMemberRoles(groupId, it) } ?: emptyList()
return GroupMemberRole.ADMIN in standardRoles || GroupMemberRole.MODERATOR in standardRoles ||
GroupMemberRole.ADMIN in blindedRoles || GroupMemberRole.MODERATOR in blindedRoles
}
}

View File

@@ -3,7 +3,7 @@ package org.thoughtcrime.securesms.groups
import android.content.Context
import androidx.annotation.WorkerThread
import org.greenrobot.eventbus.EventBus
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
import org.session.libsession.messaging.open_groups.OpenGroupApi
import org.session.libsession.utilities.GroupUtil
import org.session.libsession.utilities.recipients.Recipient
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
@@ -29,8 +29,7 @@ object OpenGroupUtilities {
throw IllegalStateException("Attempt to update open group info for non-existent DB record: $groupId")
}
val info = OpenGroupAPIV2.getInfo(room, server).get() // store info again?
OpenGroupAPIV2.getMemberCount(room, server).get()
val info = OpenGroupApi.getRoomInfo(room, server).get() // store info again?
EventBus.getDefault().post(GroupInfoUpdatedEvent(server, room = room))
}

View File

@@ -82,7 +82,7 @@ class ConversationView : LinearLayout {
}
binding.muteIndicatorImageView.setImageResource(drawableRes)
val rawSnippet = thread.getDisplayBody(context)
val snippet = highlightMentions(rawSnippet, recipient.isOpenGroupRecipient, context)
val snippet = highlightMentions(rawSnippet, thread.threadId, context)
binding.snippetTextView.text = snippet
binding.snippetTextView.typeface = if (unreadCount > 0 && !thread.isRead) Typeface.DEFAULT_BOLD else Typeface.DEFAULT
binding.snippetTextView.visibility = if (isTyping) View.GONE else View.VISIBLE

View File

@@ -21,6 +21,7 @@ import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.contacts.Contact
import org.session.libsession.utilities.Address
import org.session.libsession.utilities.recipients.Recipient
import org.session.libsignal.utilities.IdPrefix
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
import org.thoughtcrime.securesms.database.ThreadDatabase
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
@@ -83,8 +84,8 @@ class UserDetailsBottomSheet : BottomSheetDialogFragment() {
}
nameTextView.text = recipient.name ?: publicKey // Uses the Contact API internally
publicKeyTextView.isVisible = !threadRecipient.isOpenGroupRecipient
messageButton.isVisible = !threadRecipient.isOpenGroupRecipient
publicKeyTextView.isVisible = !threadRecipient.isOpenGroupRecipient && !threadRecipient.isOpenGroupInboxRecipient
messageButton.isVisible = !threadRecipient.isOpenGroupRecipient || IdPrefix.fromValue(publicKey) == IdPrefix.BLINDED
publicKeyTextView.text = publicKey
publicKeyTextView.setOnLongClickListener {
val clipboard =
@@ -103,6 +104,7 @@ class UserDetailsBottomSheet : BottomSheetDialogFragment() {
)
intent.putExtra(ConversationActivityV2.ADDRESS, recipient.address)
intent.putExtra(ConversationActivityV2.THREAD_ID, threadId ?: -1)
intent.putExtra(ConversationActivityV2.FROM_GROUP_THREAD_ID, threadID)
startActivity(intent)
dismiss()
}

View File

@@ -2,7 +2,7 @@ package org.thoughtcrime.securesms.mms;
import android.content.Context;
import org.session.libsession.messaging.file_server.FileServerAPIV2;
import org.session.libsession.messaging.file_server.FileServerApi;
public class PushMediaConstraints extends MediaConstraints {
@@ -21,26 +21,26 @@ public class PushMediaConstraints extends MediaConstraints {
@Override
public int getImageMaxSize(Context context) {
return (int) (((double) FileServerAPIV2.maxFileSize) / FileServerAPIV2.fileSizeORMultiplier);
return (int) (((double) FileServerApi.maxFileSize) / FileServerApi.fileSizeORMultiplier);
}
@Override
public int getGifMaxSize(Context context) {
return (int) (((double) FileServerAPIV2.maxFileSize) / FileServerAPIV2.fileSizeORMultiplier);
return (int) (((double) FileServerApi.maxFileSize) / FileServerApi.fileSizeORMultiplier);
}
@Override
public int getVideoMaxSize(Context context) {
return (int) (((double) FileServerAPIV2.maxFileSize) / FileServerAPIV2.fileSizeORMultiplier);
return (int) (((double) FileServerApi.maxFileSize) / FileServerApi.fileSizeORMultiplier);
}
@Override
public int getAudioMaxSize(Context context) {
return (int) (((double) FileServerAPIV2.maxFileSize) / FileServerAPIV2.fileSizeORMultiplier);
return (int) (((double) FileServerApi.maxFileSize) / FileServerApi.fileSizeORMultiplier);
}
@Override
public int getDocumentMaxSize(Context context) {
return (int) (((double) FileServerAPIV2.maxFileSize) / FileServerAPIV2.fileSizeORMultiplier);
return (int) (((double) FileServerApi.maxFileSize) / FileServerApi.fileSizeORMultiplier);
}
}

View File

@@ -17,7 +17,7 @@ import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.jobs.BatchMessageReceiveJob
import org.session.libsession.messaging.jobs.MessageReceiveParameters
import org.session.libsession.messaging.sending_receiving.pollers.ClosedGroupPollerV2
import org.session.libsession.messaging.sending_receiving.pollers.OpenGroupPollerV2
import org.session.libsession.messaging.sending_receiving.pollers.OpenGroupPoller
import org.session.libsession.snode.SnodeAPI
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsignal.utilities.Log
@@ -72,13 +72,13 @@ class BackgroundPollWorker(val context: Context, params: WorkerParameters) : Wor
// Open Groups
val threadDB = DatabaseComponent.get(context).lokiThreadDatabase()
val v2OpenGroups = threadDB.getAllV2OpenGroups()
val v2OpenGroupServers = v2OpenGroups.map { it.value.server }.toSet()
val openGroups = threadDB.getAllOpenGroups()
val openGroupServers = openGroups.map { it.value.server }.toSet()
for (server in v2OpenGroupServers) {
val poller = OpenGroupPollerV2(server, null)
for (server in openGroupServers) {
val poller = OpenGroupPoller(server, null)
poller.hasStarted = true
promises.add(poller.poll(true))
promises.add(poller.poll())
}
// Wait until all the promises are resolved

View File

@@ -36,15 +36,22 @@ import android.service.notification.StatusBarNotification;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
import com.goterl.lazysodium.utils.KeyPair;
import org.session.libsession.messaging.open_groups.OpenGroup;
import org.session.libsession.messaging.sending_receiving.notifications.MessageNotifier;
import org.session.libsession.messaging.utilities.SessionId;
import org.session.libsession.messaging.utilities.SodiumUtilities;
import org.session.libsession.utilities.Address;
import org.session.libsession.utilities.Contact;
import org.session.libsession.utilities.ServiceUtil;
import org.session.libsession.utilities.TextSecurePreferences;
import org.session.libsession.utilities.recipients.Recipient;
import org.session.libsignal.utilities.IdPrefix;
import org.session.libsignal.utilities.Log;
import org.session.libsignal.utilities.Util;
import org.thoughtcrime.securesms.ApplicationContext;
@@ -52,6 +59,8 @@ import org.thoughtcrime.securesms.contactshare.ContactUtil;
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2;
import org.thoughtcrime.securesms.conversation.v2.utilities.MentionManagerUtilities;
import org.thoughtcrime.securesms.conversation.v2.utilities.MentionUtilities;
import org.thoughtcrime.securesms.crypto.KeyPairUtilities;
import org.thoughtcrime.securesms.database.LokiThreadDatabase;
import org.thoughtcrime.securesms.database.MessagingDatabase.MarkedMessageInfo;
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
import org.thoughtcrime.securesms.database.RecipientDatabase;
@@ -66,9 +75,11 @@ import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.util.SessionMetaProtocol;
import org.thoughtcrime.securesms.util.SpanUtil;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.Executor;
@@ -491,6 +502,7 @@ public class DefaultMessageNotifier implements MessageNotifier {
MmsSmsDatabase.Reader reader = DatabaseComponent.get(context).mmsSmsDatabase().readerFor(cursor);
ThreadDatabase threadDatabase = DatabaseComponent.get(context).threadDatabase();
MessageRecord record;
Map<Long, String> cache = new HashMap<Long, String>();
while ((record = reader.getNext()) != null) {
long id = record.getId();
@@ -534,16 +546,22 @@ public class DefaultMessageNotifier implements MessageNotifier {
if (threadRecipients == null || !threadRecipients.isMuted()) {
if (threadRecipients != null && threadRecipients.notifyType == RecipientDatabase.NOTIFY_TYPE_MENTIONS) {
String userPublicKey = TextSecurePreferences.getLocalNumber(context);
String blindedPublicKey = cache.get(threadId);
if (blindedPublicKey == null) {
blindedPublicKey = generateBlindedId(threadId, context);
cache.put(threadId, blindedPublicKey);
}
// check if mentioned here
boolean isQuoteMentioned = false;
if (record instanceof MmsMessageRecord) {
Quote quote = ((MmsMessageRecord) record).getQuote();
Address quoteAddress = quote != null ? quote.getAuthor() : null;
String serializedAddress = quoteAddress != null ? quoteAddress.serialize() : null;
isQuoteMentioned = serializedAddress != null && Objects.equals(TextSecurePreferences.getLocalNumber(context), serializedAddress);
isQuoteMentioned = (serializedAddress!= null && Objects.equals(userPublicKey, serializedAddress)) ||
(blindedPublicKey != null && Objects.equals(userPublicKey, blindedPublicKey));
}
if (body.toString().contains("@"+TextSecurePreferences.getLocalNumber(context))
|| isQuoteMentioned) {
if (body.toString().contains("@"+userPublicKey) || body.toString().contains("@"+blindedPublicKey) || isQuoteMentioned) {
notificationState.addNotification(new NotificationItem(id, mms, recipient, conversationRecipient, threadRecipients, threadId, body, timestamp, slideDeck));
}
} else if (threadRecipients != null && threadRecipients.notifyType == RecipientDatabase.NOTIFY_TYPE_NONE) {
@@ -558,6 +576,19 @@ public class DefaultMessageNotifier implements MessageNotifier {
return notificationState;
}
private @Nullable String generateBlindedId(long threadId, Context context) {
LokiThreadDatabase lokiThreadDatabase = DatabaseComponent.get(context).lokiThreadDatabase();
OpenGroup openGroup = lokiThreadDatabase.getOpenGroupChat(threadId);
KeyPair edKeyPair = KeyPairUtilities.INSTANCE.getUserED25519KeyPair(context);
if (openGroup != null && edKeyPair != null) {
KeyPair blindedKeyPair = SodiumUtilities.INSTANCE.blindedKeyPair(openGroup.getPublicKey(), edKeyPair);
if (blindedKeyPair != null) {
return new SessionId(IdPrefix.BLINDED, blindedKeyPair.getPublicKey().getAsBytes()).getHexString();
}
}
return null;
}
private void updateBadge(Context context, int count) {
try {
if (count == 0) ShortcutBadger.removeCount(context);

View File

@@ -1,12 +1,14 @@
package org.thoughtcrime.securesms.notifications
import android.content.Context
import nl.komponents.kovenant.Promise
import nl.komponents.kovenant.functional.map
import okhttp3.MediaType
import okhttp3.Request
import okhttp3.RequestBody
import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI
import org.session.libsession.snode.OnionRequestAPI
import org.session.libsession.snode.Version
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsignal.utilities.JsonUtil
import org.session.libsignal.utilities.Log
@@ -43,7 +45,7 @@ object LokiPushNotificationManager {
val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters))
val request = Request.Builder().url(url).post(body)
retryIfNeeded(maxRetryCount) {
OnionRequestAPI.sendOnionRequest(request.build(), server, pnServerPublicKey, "/loki/v2/lsrpc").map { json ->
getResponseBody(request.build()).map { json ->
val code = json["code"] as? Int
if (code != null && code != 0) {
TextSecurePreferences.setIsUsingFCM(context, false)
@@ -72,7 +74,7 @@ object LokiPushNotificationManager {
val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters))
val request = Request.Builder().url(url).post(body)
retryIfNeeded(maxRetryCount) {
OnionRequestAPI.sendOnionRequest(request.build(), server, pnServerPublicKey, "/loki/v2/lsrpc").map { json ->
getResponseBody(request.build()).map { json ->
val code = json["code"] as? Int
if (code != null && code != 0) {
TextSecurePreferences.setIsUsingFCM(context, true)
@@ -100,7 +102,7 @@ object LokiPushNotificationManager {
val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters))
val request = Request.Builder().url(url).post(body)
retryIfNeeded(maxRetryCount) {
OnionRequestAPI.sendOnionRequest(request.build(), server, pnServerPublicKey, "/loki/v2/lsrpc").map { json ->
getResponseBody(request.build()).map { json ->
val code = json["code"] as? Int
if (code == null || code == 0) {
Log.d("Loki", "Couldn't subscribe/unsubscribe closed group: $closedGroupPublicKey due to error: ${json["message"] as? String ?: "null"}.")
@@ -110,4 +112,10 @@ object LokiPushNotificationManager {
}
}
}
private fun getResponseBody(request: Request): Promise<Map<*, *>, Exception> {
return OnionRequestAPI.sendOnionRequest(request, server, pnServerPublicKey, Version.V2).map { response ->
JsonUtil.fromJson(response.body, Map::class.java)
}
}
}

View File

@@ -7,7 +7,7 @@ import org.session.libsession.messaging.messages.control.UnsendRequest
import org.session.libsession.messaging.messages.signal.OutgoingTextMessage
import org.session.libsession.messaging.messages.visible.OpenGroupInvitation
import org.session.libsession.messaging.messages.visible.VisibleMessage
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
import org.session.libsession.messaging.open_groups.OpenGroupApi
import org.session.libsession.messaging.sending_receiving.MessageSender
import org.session.libsession.snode.SnodeAPI
import org.session.libsession.utilities.Address
@@ -86,7 +86,7 @@ class DefaultConversationRepository @Inject constructor(
override fun isOxenHostedOpenGroup(threadId: Long): Boolean {
val openGroup = lokiThreadDb.getOpenGroupChat(threadId)
return openGroup?.publicKey == OpenGroupAPIV2.defaultServerPublicKey
return openGroup?.publicKey == OpenGroupApi.defaultServerPublicKey
}
override fun maybeGetRecipientForThreadId(threadId: Long): Recipient? {
@@ -153,7 +153,7 @@ class DefaultConversationRepository @Inject constructor(
val openGroup = lokiThreadDb.getOpenGroupChat(threadId)
if (openGroup != null) {
lokiMessageDb.getServerID(message.id, !message.isMms)?.let { messageServerID ->
OpenGroupAPIV2.deleteMessage(messageServerID, openGroup.room, openGroup.server)
OpenGroupApi.deleteMessage(messageServerID, openGroup.room, openGroup.server)
.success {
messageDataProvider.deleteMessage(message.id, !message.isMms)
continuation.resume(ResultOf.Success(Unit))
@@ -205,7 +205,7 @@ class DefaultConversationRepository @Inject constructor(
messageServerIDs[messageServerID] = message
}
for ((messageServerID, message) in messageServerIDs) {
OpenGroupAPIV2.deleteMessage(messageServerID, openGroup.room, openGroup.server)
OpenGroupApi.deleteMessage(messageServerID, openGroup.room, openGroup.server)
.success {
messageDataProvider.deleteMessage(message.id, !message.isMms)
}.fail { error ->
@@ -228,7 +228,7 @@ class DefaultConversationRepository @Inject constructor(
suspendCoroutine { continuation ->
val sessionID = recipient.address.toString()
val openGroup = lokiThreadDb.getOpenGroupChat(threadId)!!
OpenGroupAPIV2.ban(sessionID, openGroup.room, openGroup.server)
OpenGroupApi.ban(sessionID, openGroup.room, openGroup.server)
.success {
continuation.resume(ResultOf.Success(Unit))
}.fail { error ->
@@ -240,7 +240,7 @@ class DefaultConversationRepository @Inject constructor(
suspendCoroutine { continuation ->
val sessionID = recipient.address.toString()
val openGroup = lokiThreadDb.getOpenGroupChat(threadId)!!
OpenGroupAPIV2.banAndDeleteAll(sessionID, openGroup.room, openGroup.server)
OpenGroupApi.banAndDeleteAll(sessionID, openGroup.room, openGroup.server)
.success {
continuation.resume(ResultOf.Success(Unit))
}.fail { error ->

View File

@@ -12,6 +12,7 @@ import android.graphics.drawable.BitmapDrawable
import android.text.TextPaint
import android.text.TextUtils
import network.loki.messenger.R
import org.session.libsignal.utilities.IdPrefix
import java.math.BigInteger
import java.security.MessageDigest
import java.util.Locale
@@ -66,7 +67,7 @@ object AvatarPlaceholderGenerator {
fun extractLabel(content: String): String {
val trimmedContent = content.trim()
if (trimmedContent.isEmpty()) return EMPTY_LABEL
return if (trimmedContent.length > 2 && trimmedContent.startsWith("05")) {
return if (trimmedContent.length > 2 && IdPrefix.fromValue(trimmedContent) != null) {
trimmedContent[2].toString()
} else {
val splitWords = trimmedContent.split(Regex("\\W"))

View File

@@ -20,4 +20,5 @@ object ContactUtilities {
}
return result
}
}