mirror of
https://github.com/oxen-io/session-android.git
synced 2024-11-24 18:45:19 +00:00
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:
parent
b1e954084c
commit
bee287bb7e
@ -104,8 +104,8 @@ dependencies {
|
|||||||
implementation project(":libsignal")
|
implementation project(":libsignal")
|
||||||
implementation project(":libsession")
|
implementation project(":libsession")
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinxJsonVersion"
|
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinxJsonVersion"
|
||||||
implementation "org.whispersystems:curve25519-java:$curve25519Version"
|
implementation "com.github.oxen-io.session-android-curve-25519:curve25519-java:$curve25519Version"
|
||||||
implementation 'com.goterl:lazysodium-android:5.0.2@aar'
|
implementation project(":liblazysodium")
|
||||||
implementation "net.java.dev.jna:jna:5.8.0@aar"
|
implementation "net.java.dev.jna:jna:5.8.0@aar"
|
||||||
implementation "com.google.protobuf:protobuf-java:$protobufVersion"
|
implementation "com.google.protobuf:protobuf-java:$protobufVersion"
|
||||||
implementation "com.fasterxml.jackson.core:jackson-databind:$jacksonDatabindVersion"
|
implementation "com.fasterxml.jackson.core:jackson-databind:$jacksonDatabindVersion"
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -15,6 +15,7 @@ import org.session.libsession.avatars.ProfileContactPhoto
|
|||||||
import org.session.libsession.avatars.ResourceContactPhoto
|
import org.session.libsession.avatars.ResourceContactPhoto
|
||||||
import org.session.libsession.messaging.contacts.Contact
|
import org.session.libsession.messaging.contacts.Contact
|
||||||
import org.session.libsession.utilities.Address
|
import org.session.libsession.utilities.Address
|
||||||
|
import org.session.libsession.utilities.GroupUtil
|
||||||
import org.session.libsession.utilities.recipients.Recipient
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||||
@ -57,6 +58,11 @@ class ProfilePictureView @JvmOverloads constructor(
|
|||||||
val apk = members.getOrNull(1)?.serialize() ?: ""
|
val apk = members.getOrNull(1)?.serialize() ?: ""
|
||||||
additionalPublicKey = apk
|
additionalPublicKey = apk
|
||||||
additionalDisplayName = getUserDisplayName(apk)
|
additionalDisplayName = getUserDisplayName(apk)
|
||||||
|
} else if(recipient.isOpenGroupInboxRecipient) {
|
||||||
|
val publicKey = GroupUtil.getDecodedOpenGroupInbox(recipient.address.serialize())
|
||||||
|
this.publicKey = publicKey
|
||||||
|
displayName = getUserDisplayName(publicKey)
|
||||||
|
additionalPublicKey = null
|
||||||
} else {
|
} else {
|
||||||
val publicKey = recipient.address.toString()
|
val publicKey = recipient.address.toString()
|
||||||
this.publicKey = publicKey
|
this.publicKey = publicKey
|
||||||
|
@ -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.OutgoingMediaMessage
|
||||||
import org.session.libsession.messaging.messages.signal.OutgoingTextMessage
|
import org.session.libsession.messaging.messages.signal.OutgoingTextMessage
|
||||||
import org.session.libsession.messaging.messages.visible.VisibleMessage
|
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.MessageSender
|
||||||
import org.session.libsession.messaging.sending_receiving.attachments.Attachment
|
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.link_preview.LinkPreview
|
||||||
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel
|
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
|
||||||
import org.session.libsession.utilities.Address.Companion.fromSerialized
|
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.MediaTypes
|
||||||
import org.session.libsession.utilities.TextSecurePreferences
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
import org.session.libsession.utilities.concurrent.SimpleTask
|
import org.session.libsession.utilities.concurrent.SimpleTask
|
||||||
import org.session.libsession.utilities.recipients.Recipient
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
import org.session.libsession.utilities.recipients.RecipientModifiedListener
|
import org.session.libsession.utilities.recipients.RecipientModifiedListener
|
||||||
import org.session.libsignal.crypto.MnemonicCodec
|
import org.session.libsignal.crypto.MnemonicCodec
|
||||||
|
import org.session.libsignal.utilities.IdPrefix
|
||||||
import org.session.libsignal.utilities.ListenableFuture
|
import org.session.libsignal.utilities.ListenableFuture
|
||||||
import org.session.libsignal.utilities.guava.Optional
|
import org.session.libsignal.utilities.guava.Optional
|
||||||
import org.session.libsignal.utilities.hexEncodedPrivateKey
|
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.MmsSmsDatabase
|
||||||
import org.thoughtcrime.securesms.database.SessionContactDatabase
|
import org.thoughtcrime.securesms.database.SessionContactDatabase
|
||||||
import org.thoughtcrime.securesms.database.SmsDatabase
|
import org.thoughtcrime.securesms.database.SmsDatabase
|
||||||
|
import org.thoughtcrime.securesms.database.Storage
|
||||||
import org.thoughtcrime.securesms.database.ThreadDatabase
|
import org.thoughtcrime.securesms.database.ThreadDatabase
|
||||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
||||||
@ -167,6 +171,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
@Inject lateinit var smsDb: SmsDatabase
|
@Inject lateinit var smsDb: SmsDatabase
|
||||||
@Inject lateinit var mmsDb: MmsDatabase
|
@Inject lateinit var mmsDb: MmsDatabase
|
||||||
@Inject lateinit var lokiMessageDb: LokiMessageDatabase
|
@Inject lateinit var lokiMessageDb: LokiMessageDatabase
|
||||||
|
@Inject lateinit var storage: Storage
|
||||||
@Inject lateinit var viewModelFactory: ConversationViewModel.AssistedFactory
|
@Inject lateinit var viewModelFactory: ConversationViewModel.AssistedFactory
|
||||||
|
|
||||||
private val screenWidth = Resources.getSystem().displayMetrics.widthPixels
|
private val screenWidth = Resources.getSystem().displayMetrics.widthPixels
|
||||||
@ -177,9 +182,25 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
private val viewModel: ConversationViewModel by viewModels {
|
private val viewModel: ConversationViewModel by viewModels {
|
||||||
var threadId = intent.getLongExtra(THREAD_ID, -1L)
|
var threadId = intent.getLongExtra(THREAD_ID, -1L)
|
||||||
if (threadId == -1L) {
|
if (threadId == -1L) {
|
||||||
intent.getParcelableExtra<Address>(ADDRESS)?.let { address ->
|
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)
|
val recipient = Recipient.from(this, address, false)
|
||||||
threadId = threadDb.getOrCreateThreadIdFor(recipient)
|
threadId = threadDb.getOrCreateThreadIdFor(recipient)
|
||||||
|
}
|
||||||
} ?: finish()
|
} ?: finish()
|
||||||
}
|
}
|
||||||
viewModelFactory.create(threadId)
|
viewModelFactory.create(threadId)
|
||||||
@ -263,6 +284,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
// Extras
|
// Extras
|
||||||
const val THREAD_ID = "thread_id"
|
const val THREAD_ID = "thread_id"
|
||||||
const val ADDRESS = "address"
|
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_ID = "scroll_message_id"
|
||||||
const val SCROLL_MESSAGE_AUTHOR = "scroll_message_author"
|
const val SCROLL_MESSAGE_AUTHOR = "scroll_message_author"
|
||||||
// Request codes
|
// Request codes
|
||||||
@ -508,7 +530,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
|
|
||||||
private fun getLatestOpenGroupInfoIfNeeded() {
|
private fun getLatestOpenGroupInfoIfNeeded() {
|
||||||
val openGroup = lokiThreadDb.getOpenGroupChat(viewModel.threadId) ?: return
|
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
|
// called from onCreate
|
||||||
|
@ -7,7 +7,7 @@ import android.view.View
|
|||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import network.loki.messenger.databinding.ViewMentionCandidateBinding
|
import network.loki.messenger.databinding.ViewMentionCandidateBinding
|
||||||
import org.session.libsession.messaging.mentions.Mention
|
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
|
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||||
|
|
||||||
class MentionCandidateView : LinearLayout {
|
class MentionCandidateView : LinearLayout {
|
||||||
@ -34,7 +34,7 @@ class MentionCandidateView : LinearLayout {
|
|||||||
profilePictureView.root.glide = glide!!
|
profilePictureView.root.glide = glide!!
|
||||||
profilePictureView.root.update()
|
profilePictureView.root.update()
|
||||||
if (openGroupServer != null && openGroupRoom != null) {
|
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
|
moderatorIconImageView.visibility = if (isUserModerator) View.VISIBLE else View.GONE
|
||||||
} else {
|
} else {
|
||||||
moderatorIconImageView.visibility = View.GONE
|
moderatorIconImageView.visibility = View.GONE
|
||||||
|
@ -7,7 +7,7 @@ import android.view.View
|
|||||||
import android.widget.RelativeLayout
|
import android.widget.RelativeLayout
|
||||||
import network.loki.messenger.databinding.ViewMentionCandidateV2Binding
|
import network.loki.messenger.databinding.ViewMentionCandidateV2Binding
|
||||||
import org.session.libsession.messaging.mentions.Mention
|
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
|
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||||
|
|
||||||
class MentionCandidateView : RelativeLayout {
|
class MentionCandidateView : RelativeLayout {
|
||||||
@ -34,7 +34,7 @@ class MentionCandidateView : RelativeLayout {
|
|||||||
profilePictureView.root.glide = glide!!
|
profilePictureView.root.glide = glide!!
|
||||||
profilePictureView.root.update()
|
profilePictureView.root.update()
|
||||||
if (openGroupServer != null && openGroupRoom != null) {
|
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
|
moderatorIconImageView.visibility = if (isUserModerator) View.VISIBLE else View.GONE
|
||||||
} else {
|
} else {
|
||||||
moderatorIconImageView.visibility = View.GONE
|
moderatorIconImageView.visibility = View.GONE
|
||||||
|
@ -5,13 +5,17 @@ import android.view.ActionMode
|
|||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import network.loki.messenger.R
|
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.libsession.utilities.TextSecurePreferences
|
||||||
|
import org.session.libsignal.utilities.IdPrefix
|
||||||
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
|
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
|
||||||
import org.thoughtcrime.securesms.conversation.v2.ConversationAdapter
|
import org.thoughtcrime.securesms.conversation.v2.ConversationAdapter
|
||||||
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord
|
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord
|
||||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
||||||
|
import org.thoughtcrime.securesms.groups.OpenGroupManager
|
||||||
|
|
||||||
class ConversationActionModeCallback(private val adapter: ConversationAdapter, private val threadID: Long,
|
class ConversationActionModeCallback(private val adapter: ConversationAdapter, private val threadID: Long,
|
||||||
private val context: Context) : ActionMode.Callback {
|
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 openGroup = DatabaseComponent.get(context).lokiThreadDatabase().getOpenGroupChat(threadID)
|
||||||
val thread = DatabaseComponent.get(context).threadDatabase().getRecipientForThreadId(threadID)!!
|
val thread = DatabaseComponent.get(context).threadDatabase().getRecipientForThreadId(threadID)!!
|
||||||
val userPublicKey = TextSecurePreferences.getLocalNumber(context)!!
|
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 {
|
fun userCanDeleteSelectedItems(): Boolean {
|
||||||
val allSentByCurrentUser = selectedItems.all { it.isOutgoing }
|
val allSentByCurrentUser = selectedItems.all { it.isOutgoing }
|
||||||
|
|
||||||
@ -41,13 +48,13 @@ class ConversationActionModeCallback(private val adapter: ConversationAdapter, p
|
|||||||
if (!ConversationActivityV2.IS_UNSEND_REQUESTS_ENABLED) {
|
if (!ConversationActivityV2.IS_UNSEND_REQUESTS_ENABLED) {
|
||||||
if (openGroup == null) { return true }
|
if (openGroup == null) { return true }
|
||||||
if (allSentByCurrentUser) { 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 }
|
val allReceivedByCurrentUser = selectedItems.all { !it.isOutgoing }
|
||||||
if (openGroup == null) { return allSentByCurrentUser || allReceivedByCurrentUser }
|
if (openGroup == null) { return allSentByCurrentUser || allReceivedByCurrentUser }
|
||||||
if (allSentByCurrentUser) { return true }
|
if (allSentByCurrentUser) { return true }
|
||||||
return OpenGroupAPIV2.isUserModerator(userPublicKey, openGroup.room, openGroup.server)
|
return OpenGroupManager.isUserModerator(context, openGroup.groupId, userPublicKey, blindedPublicKey)
|
||||||
}
|
}
|
||||||
fun userCanBanSelectedUsers(): Boolean {
|
fun userCanBanSelectedUsers(): Boolean {
|
||||||
if (openGroup == null) { return false }
|
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
|
if (anySentByCurrentUser) { return false } // Users can't ban themselves
|
||||||
val selectedUsers = selectedItems.map { it.recipient.address.toString() }.toSet()
|
val selectedUsers = selectedItems.map { it.recipient.address.toString() }.toSet()
|
||||||
if (selectedUsers.size > 1) { return false }
|
if (selectedUsers.size > 1) { return false }
|
||||||
return OpenGroupAPIV2.isUserModerator(userPublicKey, openGroup.room, openGroup.server)
|
return OpenGroupManager.isUserModerator(context, openGroup.groupId, userPublicKey, blindedPublicKey)
|
||||||
}
|
}
|
||||||
// Delete message
|
// Delete message
|
||||||
menu.findItem(R.id.menu_context_delete_message).isVisible = userCanDeleteSelectedItems()
|
menu.findItem(R.id.menu_context_delete_message).isVisible = userCanDeleteSelectedItems()
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package org.thoughtcrime.securesms.conversation.v2.messages
|
package org.thoughtcrime.securesms.conversation.v2.messages
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
import android.graphics.Canvas
|
import android.graphics.Canvas
|
||||||
import android.graphics.Rect
|
import android.graphics.Rect
|
||||||
@ -23,16 +24,19 @@ import network.loki.messenger.R
|
|||||||
import network.loki.messenger.databinding.ViewVisibleMessageBinding
|
import network.loki.messenger.databinding.ViewVisibleMessageBinding
|
||||||
import org.session.libsession.messaging.contacts.Contact
|
import org.session.libsession.messaging.contacts.Contact
|
||||||
import org.session.libsession.messaging.contacts.Contact.ContactContext
|
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.libsession.utilities.ViewUtil
|
||||||
|
import org.session.libsignal.utilities.IdPrefix
|
||||||
import org.session.libsignal.utilities.ThreadUtils
|
import org.session.libsignal.utilities.ThreadUtils
|
||||||
import org.thoughtcrime.securesms.ApplicationContext
|
import org.thoughtcrime.securesms.ApplicationContext
|
||||||
|
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
|
||||||
import org.thoughtcrime.securesms.database.LokiThreadDatabase
|
import org.thoughtcrime.securesms.database.LokiThreadDatabase
|
||||||
import org.thoughtcrime.securesms.database.MmsDatabase
|
import org.thoughtcrime.securesms.database.MmsDatabase
|
||||||
import org.thoughtcrime.securesms.database.MmsSmsDatabase
|
import org.thoughtcrime.securesms.database.MmsSmsDatabase
|
||||||
import org.thoughtcrime.securesms.database.SmsDatabase
|
import org.thoughtcrime.securesms.database.SmsDatabase
|
||||||
import org.thoughtcrime.securesms.database.ThreadDatabase
|
import org.thoughtcrime.securesms.database.ThreadDatabase
|
||||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||||
|
import org.thoughtcrime.securesms.groups.OpenGroupManager
|
||||||
import org.thoughtcrime.securesms.home.UserDetailsBottomSheet
|
import org.thoughtcrime.securesms.home.UserDetailsBottomSheet
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||||
import org.thoughtcrime.securesms.util.DateUtils
|
import org.thoughtcrime.securesms.util.DateUtils
|
||||||
@ -140,15 +144,27 @@ class VisibleMessageView : LinearLayout {
|
|||||||
binding.profilePictureView.root.glide = glide
|
binding.profilePictureView.root.glide = glide
|
||||||
binding.profilePictureView.root.update(message.individualRecipient)
|
binding.profilePictureView.root.update(message.individualRecipient)
|
||||||
binding.profilePictureView.root.setOnClickListener {
|
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) {
|
if (thread.isOpenGroupRecipient) {
|
||||||
val openGroup = lokiThreadDb.getOpenGroupChat(threadID) ?: return
|
val openGroup = lokiThreadDb.getOpenGroupChat(threadID) ?: return
|
||||||
val isModerator = OpenGroupAPIV2.isUserModerator(
|
var standardPublicKey = ""
|
||||||
senderSessionID,
|
var blindedPublicKey: String? = null
|
||||||
openGroup.room,
|
if (IdPrefix.fromValue(senderSessionID) == IdPrefix.BLINDED) {
|
||||||
openGroup.server
|
blindedPublicKey = senderSessionID
|
||||||
)
|
} else {
|
||||||
|
standardPublicKey = senderSessionID
|
||||||
|
}
|
||||||
|
val isModerator = OpenGroupManager.isUserModerator(context, openGroup.groupId, standardPublicKey, blindedPublicKey)
|
||||||
binding.moderatorIconImageView.isVisible = !message.isOutgoing && isModerator
|
binding.moderatorIconImageView.isVisible = !message.isOutgoing && isModerator
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -403,7 +419,7 @@ class VisibleMessageView : LinearLayout {
|
|||||||
pressCallback = null
|
pressCallback = null
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showUserDetails(publicKey: String, threadID: Long) {
|
private fun maybeShowUserDetails(publicKey: String, threadID: Long) {
|
||||||
val userDetailsBottomSheet = UserDetailsBottomSheet()
|
val userDetailsBottomSheet = UserDetailsBottomSheet()
|
||||||
val bundle = bundleOf(
|
val bundle = bundleOf(
|
||||||
UserDetailsBottomSheet.ARGUMENT_PUBLIC_KEY to publicKey,
|
UserDetailsBottomSheet.ARGUMENT_PUBLIC_KEY to publicKey,
|
||||||
|
@ -11,6 +11,7 @@ import androidx.core.content.res.ResourcesCompat
|
|||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import nl.komponents.kovenant.combine.Tuple2
|
import nl.komponents.kovenant.combine.Tuple2
|
||||||
import org.session.libsession.messaging.contacts.Contact
|
import org.session.libsession.messaging.contacts.Contact
|
||||||
|
import org.session.libsession.messaging.utilities.SodiumUtilities
|
||||||
import org.session.libsession.utilities.TextSecurePreferences
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
||||||
import org.thoughtcrime.securesms.util.UiModeUtilities
|
import org.thoughtcrime.securesms.util.UiModeUtilities
|
||||||
@ -20,39 +21,27 @@ object MentionUtilities {
|
|||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun highlightMentions(text: CharSequence, threadID: Long, context: Context): String {
|
fun highlightMentions(text: CharSequence, threadID: Long, context: Context): String {
|
||||||
val threadDB = DatabaseComponent.get(context).threadDatabase()
|
return highlightMentions(text, false, threadID, context).toString() // isOutgoingMessage is irrelevant
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun highlightMentions(text: CharSequence, isOutgoingMessage: Boolean, threadID: Long, context: Context): SpannableString {
|
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
|
@Suppress("NAME_SHADOWING") var text = text
|
||||||
val pattern = Pattern.compile("@[0-9a-fA-F]*")
|
val pattern = Pattern.compile("@[0-9a-fA-F]*")
|
||||||
var matcher = pattern.matcher(text)
|
var matcher = pattern.matcher(text)
|
||||||
val mentions = mutableListOf<Tuple2<Range<Int>, String>>()
|
val mentions = mutableListOf<Tuple2<Range<Int>, String>>()
|
||||||
var startIndex = 0
|
var startIndex = 0
|
||||||
val userPublicKey = TextSecurePreferences.getLocalNumber(context)!!
|
val userPublicKey = TextSecurePreferences.getLocalNumber(context)!!
|
||||||
|
val openGroup = DatabaseComponent.get(context).storage().getOpenGroup(threadID)
|
||||||
if (matcher.find(startIndex)) {
|
if (matcher.find(startIndex)) {
|
||||||
while (true) {
|
while (true) {
|
||||||
val publicKey = text.subSequence(matcher.start() + 1, matcher.end()).toString() // +1 to get rid of the @
|
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)) {
|
val isUserBlindedPublicKey = openGroup?.let { SodiumUtilities.sessionId(userPublicKey, publicKey, it.publicKey) } ?: false
|
||||||
TextSecurePreferences.getProfileName(context)
|
val userDisplayName: String? = if (publicKey.equals(userPublicKey, ignoreCase = true) || isUserBlindedPublicKey) {
|
||||||
|
context.getString(R.string.MessageRecord_you)
|
||||||
} else {
|
} else {
|
||||||
val contact = DatabaseComponent.get(context).sessionContactDatabase().getContactWithSessionID(publicKey)
|
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)
|
contact?.displayName(context)
|
||||||
}
|
}
|
||||||
if (userDisplayName != null) {
|
if (userDisplayName != null) {
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -23,6 +23,8 @@ import android.database.Cursor;
|
|||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import net.sqlcipher.database.SQLiteDatabase;
|
||||||
|
|
||||||
import org.session.libsession.utilities.WindowDebouncer;
|
import org.session.libsession.utilities.WindowDebouncer;
|
||||||
import org.thoughtcrime.securesms.ApplicationContext;
|
import org.thoughtcrime.securesms.ApplicationContext;
|
||||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||||
@ -92,4 +94,12 @@ public abstract class Database {
|
|||||||
this.databaseHelper = databaseHelper;
|
this.databaseHelper = databaseHelper;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected SQLiteDatabase getReadableDatabase() {
|
||||||
|
return databaseHelper.getReadableDatabase();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected SQLiteDatabase getWritableDatabase() {
|
||||||
|
return databaseHelper.getWritableDatabase();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -12,7 +12,7 @@ import org.session.libsignal.utilities.Hex
|
|||||||
import org.session.libsignal.utilities.Log
|
import org.session.libsignal.utilities.Log
|
||||||
import org.session.libsignal.utilities.PublicKeyValidation
|
import org.session.libsignal.utilities.PublicKeyValidation
|
||||||
import org.session.libsignal.utilities.Snode
|
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.session.libsignal.utilities.toHexString
|
||||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
|
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
|
||||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
|
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 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;"
|
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
|
// region Deprecated
|
||||||
private val deviceLinkCache = "loki_pairing_authorisation_cache"
|
private val deviceLinkCache = "loki_pairing_authorisation_cache"
|
||||||
@ -423,14 +438,14 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
|
|||||||
|
|
||||||
override fun getUserX25519KeyPair(): ECKeyPair {
|
override fun getUserX25519KeyPair(): ECKeyPair {
|
||||||
val keyPair = IdentityKeyUtil.getIdentityKeyPair(context)
|
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) {
|
fun addClosedGroupEncryptionKeyPair(encryptionKeyPair: ECKeyPair, groupPublicKey: String) {
|
||||||
val database = databaseHelper.writableDatabase
|
val database = databaseHelper.writableDatabase
|
||||||
val timestamp = Date().time.toString()
|
val timestamp = Date().time.toString()
|
||||||
val index = "$groupPublicKey-$timestamp"
|
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 encryptionKeyPairPrivateKey = encryptionKeyPair.privateKey.serialize().toHexString()
|
||||||
val row = wrap(mapOf(closedGroupsEncryptionKeyPairIndex to index, Companion.encryptionKeyPairPublicKey to encryptionKeyPairPublicKey,
|
val row = wrap(mapOf(closedGroupsEncryptionKeyPairIndex to index, Companion.encryptionKeyPairPublicKey to encryptionKeyPairPublicKey,
|
||||||
Companion.encryptionKeyPairPrivateKey to encryptionKeyPairPrivateKey ))
|
Companion.encryptionKeyPairPrivateKey to encryptionKeyPairPrivateKey ))
|
||||||
@ -481,6 +496,53 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
|
|||||||
database.delete(closedGroupPublicKeysTable, "${Companion.groupPublicKey} = ?", wrap(groupPublicKey))
|
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 {
|
override fun getForkInfo(): ForkInfo {
|
||||||
val database = databaseHelper.readableDatabase
|
val database = databaseHelper.readableDatabase
|
||||||
val queryCursor = database.query(FORK_INFO_TABLE, arrayOf(HF_VALUE, SF_VALUE), "$DUMMY_KEY = $DUMMY_VALUE", null, null, null, null)
|
val queryCursor = database.query(FORK_INFO_TABLE, arrayOf(HF_VALUE, SF_VALUE), "$DUMMY_KEY = $DUMMY_VALUE", null, null, null, null)
|
||||||
|
@ -3,7 +3,7 @@ package org.thoughtcrime.securesms.database
|
|||||||
import android.content.ContentValues
|
import android.content.ContentValues
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.database.Cursor
|
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.Address
|
||||||
import org.session.libsession.utilities.recipients.Recipient
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
import org.session.libsignal.utilities.JsonUtil
|
import org.session.libsignal.utilities.JsonUtil
|
||||||
@ -30,16 +30,16 @@ class LokiThreadDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa
|
|||||||
return DatabaseComponent.get(context).threadDatabase().getOrCreateThreadIdFor(recipient)
|
return DatabaseComponent.get(context).threadDatabase().getOrCreateThreadIdFor(recipient)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getAllV2OpenGroups(): Map<Long, OpenGroupV2> {
|
fun getAllOpenGroups(): Map<Long, OpenGroup> {
|
||||||
val database = databaseHelper.readableDatabase
|
val database = databaseHelper.readableDatabase
|
||||||
var cursor: Cursor? = null
|
var cursor: Cursor? = null
|
||||||
val result = mutableMapOf<Long, OpenGroupV2>()
|
val result = mutableMapOf<Long, OpenGroup>()
|
||||||
try {
|
try {
|
||||||
cursor = database.rawQuery("select * from $publicChatTable", null)
|
cursor = database.rawQuery("select * from $publicChatTable", null)
|
||||||
while (cursor != null && cursor.moveToNext()) {
|
while (cursor != null && cursor.moveToNext()) {
|
||||||
val threadID = cursor.getLong(threadID)
|
val threadID = cursor.getLong(threadID)
|
||||||
val string = cursor.getString(publicChat)
|
val string = cursor.getString(publicChat)
|
||||||
val openGroup = OpenGroupV2.fromJSON(string)
|
val openGroup = OpenGroup.fromJSON(string)
|
||||||
if (openGroup != null) result[threadID] = openGroup
|
if (openGroup != null) result[threadID] = openGroup
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@ -50,25 +50,25 @@ class LokiThreadDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getOpenGroupChat(threadID: Long): OpenGroupV2? {
|
fun getOpenGroupChat(threadID: Long): OpenGroup? {
|
||||||
if (threadID < 0) {
|
if (threadID < 0) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
val database = databaseHelper.readableDatabase
|
val database = databaseHelper.readableDatabase
|
||||||
return database.get(publicChatTable, "${Companion.threadID} = ?", arrayOf(threadID.toString())) { cursor ->
|
return database.get(publicChatTable, "${Companion.threadID} = ?", arrayOf(threadID.toString())) { cursor ->
|
||||||
val json = cursor.getString(publicChat)
|
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) {
|
if (threadID < 0) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
val database = databaseHelper.writableDatabase
|
val database = databaseHelper.writableDatabase
|
||||||
val contentValues = ContentValues(2)
|
val contentValues = ContentValues(2)
|
||||||
contentValues.put(Companion.threadID, threadID)
|
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()))
|
database.insertOrUpdate(publicChatTable, contentValues, "${Companion.threadID} = ?", arrayOf(threadID.toString()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,6 +42,8 @@ public abstract class MessagingDatabase extends Database implements MmsSmsColumn
|
|||||||
|
|
||||||
public abstract boolean deleteMessage(long messageId);
|
public abstract boolean deleteMessage(long messageId);
|
||||||
|
|
||||||
|
public abstract void updateThreadId(long fromId, long toId);
|
||||||
|
|
||||||
public void addMismatchedIdentity(long messageId, Address address, IdentityKey identityKey) {
|
public void addMismatchedIdentity(long messageId, Address address, IdentityKey identityKey) {
|
||||||
try {
|
try {
|
||||||
addToDocument(messageId, MISMATCHED_IDENTITIES,
|
addToDocument(messageId, MISMATCHED_IDENTITIES,
|
||||||
|
@ -1031,6 +1031,16 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
|
|||||||
return threadDeleted
|
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) {
|
fun deleteThread(threadId: Long) {
|
||||||
deleteThreads(setOf(threadId))
|
deleteThreads(setOf(threadId))
|
||||||
}
|
}
|
||||||
|
@ -93,12 +93,11 @@ public class MmsSmsDatabase extends Database {
|
|||||||
MmsSmsDatabase.Reader reader = readerFor(cursor);
|
MmsSmsDatabase.Reader reader = readerFor(cursor);
|
||||||
|
|
||||||
MessageRecord messageRecord;
|
MessageRecord messageRecord;
|
||||||
|
boolean isOwnNumber = Util.isOwnNumber(context, serializedAuthor);
|
||||||
|
|
||||||
while ((messageRecord = reader.getNext()) != null) {
|
while ((messageRecord = reader.getNext()) != null) {
|
||||||
if ((Util.isOwnNumber(context, serializedAuthor) && messageRecord.isOutgoing()) ||
|
if ((isOwnNumber && messageRecord.isOutgoing()) ||
|
||||||
(!Util.isOwnNumber(context, serializedAuthor)
|
(!isOwnNumber && messageRecord.getIndividualRecipient().getAddress().serialize().equals(serializedAuthor)))
|
||||||
&& messageRecord.getIndividualRecipient().getAddress().serialize().equals(serializedAuthor)
|
|
||||||
))
|
|
||||||
{
|
{
|
||||||
return messageRecord;
|
return messageRecord;
|
||||||
}
|
}
|
||||||
|
@ -255,10 +255,6 @@ public class RecipientDatabase extends Database {
|
|||||||
recipient.resolve().setApproved(approved);
|
recipient.resolve().setApproved(approved);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setAllApproved(List<String> addresses) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setApprovedMe(@NonNull Recipient recipient, boolean approvedMe) {
|
public void setApprovedMe(@NonNull Recipient recipient, boolean approvedMe) {
|
||||||
ContentValues values = new ContentValues();
|
ContentValues values = new ContentValues();
|
||||||
values.put(APPROVED_ME, approvedMe ? 1 : 0);
|
values.put(APPROVED_ME, approvedMe ? 1 : 0);
|
||||||
|
@ -575,6 +575,17 @@ public class SmsDatabase extends MessagingDatabase {
|
|||||||
return threadDeleted;
|
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) {
|
private boolean isDuplicate(IncomingTextMessage message, long threadId) {
|
||||||
SQLiteDatabase database = databaseHelper.getReadableDatabase();
|
SQLiteDatabase database = databaseHelper.getReadableDatabase();
|
||||||
Cursor cursor = database.query(TABLE_NAME, null, DATE_SENT + " = ? AND " + ADDRESS + " = ? AND " + THREAD_ID + " = ?",
|
Cursor cursor = database.query(TABLE_NAME, null, DATE_SENT + " = ? AND " + ADDRESS + " = ? AND " + THREAD_ID + " = ?",
|
||||||
|
@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.database
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import org.session.libsession.database.StorageProtocol
|
import org.session.libsession.database.StorageProtocol
|
||||||
|
import org.session.libsession.messaging.BlindedIdMapping
|
||||||
import org.session.libsession.messaging.calls.CallMessageType
|
import org.session.libsession.messaging.calls.CallMessageType
|
||||||
import org.session.libsession.messaging.contacts.Contact
|
import org.session.libsession.messaging.contacts.Contact
|
||||||
import org.session.libsession.messaging.jobs.AttachmentUploadJob
|
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.signal.OutgoingTextMessage
|
||||||
import org.session.libsession.messaging.messages.visible.Attachment
|
import org.session.libsession.messaging.messages.visible.Attachment
|
||||||
import org.session.libsession.messaging.messages.visible.VisibleMessage
|
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.AttachmentId
|
||||||
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
|
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.data_extraction.DataExtractionNotificationInfoMessage
|
||||||
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview
|
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview
|
||||||
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel
|
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.messaging.utilities.UpdateMessageData
|
||||||
import org.session.libsession.snode.OnionRequestAPI
|
import org.session.libsession.snode.OnionRequestAPI
|
||||||
import org.session.libsession.utilities.Address
|
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.crypto.ecc.ECKeyPair
|
||||||
import org.session.libsignal.messages.SignalServiceAttachmentPointer
|
import org.session.libsignal.messages.SignalServiceAttachmentPointer
|
||||||
import org.session.libsignal.messages.SignalServiceGroup
|
import org.session.libsignal.messages.SignalServiceGroup
|
||||||
|
import org.session.libsignal.utilities.IdPrefix
|
||||||
import org.session.libsignal.utilities.KeyHelper
|
import org.session.libsignal.utilities.KeyHelper
|
||||||
import org.session.libsignal.utilities.guava.Optional
|
import org.session.libsignal.utilities.guava.Optional
|
||||||
import org.thoughtcrime.securesms.ApplicationContext
|
import org.thoughtcrime.securesms.ApplicationContext
|
||||||
@ -73,7 +78,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun setUserProfilePictureURL(newValue: String) {
|
override fun setUserProfilePictureURL(newValue: String) {
|
||||||
val ourRecipient = Address.fromSerialized(getUserPublicKey()!!).let {
|
val ourRecipient = fromSerialized(getUserPublicKey()!!).let {
|
||||||
Recipient.from(context, it, false)
|
Recipient.from(context, it, false)
|
||||||
}
|
}
|
||||||
TextSecurePreferences.setProfilePictureURL(context, newValue)
|
TextSecurePreferences.setProfilePictureURL(context, newValue)
|
||||||
@ -125,8 +130,10 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
|||||||
runIncrement: Boolean,
|
runIncrement: Boolean,
|
||||||
runThreadUpdate: Boolean): Long? {
|
runThreadUpdate: Boolean): Long? {
|
||||||
var messageID: Long? = null
|
var messageID: Long? = null
|
||||||
val senderAddress = Address.fromSerialized(message.sender!!)
|
val senderAddress = fromSerialized(message.sender!!)
|
||||||
val isUserSender = (message.sender!! == getUserPublicKey())
|
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 {
|
val group: Optional<SignalServiceGroup> = when {
|
||||||
openGroupID != null -> Optional.of(SignalServiceGroup(openGroupID.toByteArray(), SignalServiceGroup.GroupType.PUBLIC_CHAT))
|
openGroupID != null -> Optional.of(SignalServiceGroup(openGroupID.toByteArray(), SignalServiceGroup.GroupType.PUBLIC_CHAT))
|
||||||
groupPublicKey != null -> {
|
groupPublicKey != null -> {
|
||||||
@ -138,17 +145,17 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
|||||||
val pointers = attachments.mapNotNull {
|
val pointers = attachments.mapNotNull {
|
||||||
it.toSignalAttachment()
|
it.toSignalAttachment()
|
||||||
}
|
}
|
||||||
val targetAddress = if (isUserSender && !message.syncTarget.isNullOrEmpty()) {
|
val targetAddress = if ((isUserSender || isUserBlindedSender) && !message.syncTarget.isNullOrEmpty()) {
|
||||||
Address.fromSerialized(message.syncTarget!!)
|
fromSerialized(message.syncTarget!!)
|
||||||
} else if (group.isPresent) {
|
} else if (group.isPresent) {
|
||||||
Address.fromSerialized(GroupUtil.getEncodedId(group.get()))
|
fromSerialized(GroupUtil.getEncodedId(group.get()))
|
||||||
} else {
|
} else {
|
||||||
senderAddress
|
senderAddress
|
||||||
}
|
}
|
||||||
val targetRecipient = Recipient.from(context, targetAddress, false)
|
val targetRecipient = Recipient.from(context, targetAddress, false)
|
||||||
if (!targetRecipient.isGroupRecipient) {
|
if (!targetRecipient.isGroupRecipient) {
|
||||||
val recipientDb = DatabaseComponent.get(context).recipientDatabase()
|
val recipientDb = DatabaseComponent.get(context).recipientDatabase()
|
||||||
if (isUserSender) {
|
if (isUserSender || isUserBlindedSender) {
|
||||||
recipientDb.setApproved(targetRecipient, true)
|
recipientDb.setApproved(targetRecipient, true)
|
||||||
} else {
|
} else {
|
||||||
recipientDb.setApprovedMe(targetRecipient, true)
|
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 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 linkPreviews: Optional<List<LinkPreview>> = if (linkPreview.isEmpty()) Optional.absent() else Optional.of(linkPreview.mapNotNull { it!! })
|
||||||
val mmsDatabase = DatabaseComponent.get(context).mmsDatabase()
|
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())
|
val mediaMessage = OutgoingMediaMessage.from(message, targetRecipient, pointers, quote.orNull(), linkPreviews.orNull()?.firstOrNull())
|
||||||
mmsDatabase.insertSecureDecryptedMessageOutbox(mediaMessage, message.threadID ?: -1, message.sentTimestamp!!, runThreadUpdate)
|
mmsDatabase.insertSecureDecryptedMessageOutbox(mediaMessage, message.threadID ?: -1, message.sentTimestamp!!, runThreadUpdate)
|
||||||
} else {
|
} else {
|
||||||
@ -176,7 +183,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
|||||||
val smsDatabase = DatabaseComponent.get(context).smsDatabase()
|
val smsDatabase = DatabaseComponent.get(context).smsDatabase()
|
||||||
val isOpenGroupInvitation = (message.openGroupInvitation != null)
|
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)
|
val textMessage = if (isOpenGroupInvitation) OutgoingTextMessage.fromOpenGroupInvitation(message.openGroupInvitation, targetRecipient, message.sentTimestamp)
|
||||||
else OutgoingTextMessage.from(message, targetRecipient)
|
else OutgoingTextMessage.from(message, targetRecipient)
|
||||||
smsDatabase.insertMessageOutbox(message.threadID ?: -1, textMessage, message.sentTimestamp!!, runThreadUpdate)
|
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)
|
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 }
|
if (threadId.toInt() < 0) { return null }
|
||||||
val database = databaseHelper.readableDatabase
|
val database = databaseHelper.readableDatabase
|
||||||
return database.get(LokiThreadDatabase.publicChatTable, "${LokiThreadDatabase.threadID} = ?", arrayOf( threadId.toString() )) { cursor ->
|
return database.get(LokiThreadDatabase.publicChatTable, "${LokiThreadDatabase.threadID} = ?", arrayOf( threadId.toString() )) { cursor ->
|
||||||
val publicChatAsJson = cursor.getString(LokiThreadDatabase.publicChat)
|
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)
|
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 {
|
override fun isDuplicateMessage(timestamp: Long): Boolean {
|
||||||
return getReceivedMessageTimestamps().contains(timestamp)
|
return getReceivedMessageTimestamps().contains(timestamp)
|
||||||
}
|
}
|
||||||
@ -335,7 +350,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
|||||||
|
|
||||||
override fun getMessageIdInDatabase(timestamp: Long, author: String): Long? {
|
override fun getMessageIdInDatabase(timestamp: Long, author: String): Long? {
|
||||||
val database = DatabaseComponent.get(context).mmsSmsDatabase()
|
val database = DatabaseComponent.get(context).mmsSmsDatabase()
|
||||||
val address = Address.fromSerialized(author)
|
val address = fromSerialized(author)
|
||||||
return database.getMessageFor(timestamp, address)?.getId()
|
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) {
|
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 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 updateData = UpdateMessageData.buildGroupUpdate(type, name, members)?.toJSON()
|
||||||
val infoMessage = IncomingGroupMessage(m, groupID, updateData, true)
|
val infoMessage = IncomingGroupMessage(m, groupID, updateData, true)
|
||||||
val smsDB = DatabaseComponent.get(context).smsDatabase()
|
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) {
|
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 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 updateData = UpdateMessageData.buildGroupUpdate(type, name, members)?.toJSON() ?: ""
|
||||||
val infoMessage = OutgoingGroupMediaMessage(recipient, updateData, groupID, null, sentTimestamp, 0, true, null, listOf(), listOf())
|
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 {
|
override fun isClosedGroup(publicKey: String): Boolean {
|
||||||
val isClosedGroup = DatabaseComponent.get(context).lokiAPIDatabase().isClosedGroup(publicKey)
|
val isClosedGroup = DatabaseComponent.get(context).lokiAPIDatabase().isClosedGroup(publicKey)
|
||||||
val address = Address.fromSerialized(publicKey)
|
val address = fromSerialized(publicKey)
|
||||||
return address.isClosedGroup || isClosedGroup
|
return address.isClosedGroup || isClosedGroup
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -528,8 +543,20 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
|||||||
DatabaseComponent.get(context).recipientDatabase().setExpireMessages(recipient, duration);
|
DatabaseComponent.get(context).recipientDatabase().setExpireMessages(recipient, duration);
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getAllV2OpenGroups(): Map<Long, OpenGroupV2> {
|
override fun setServerCapabilities(server: String, capabilities: List<String>) {
|
||||||
return DatabaseComponent.get(context).lokiThreadDatabase().getAllV2OpenGroups()
|
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> {
|
override fun getAllGroups(): List<GroupRecord> {
|
||||||
@ -541,7 +568,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onOpenGroupAdded(urlAsString: String) {
|
override fun onOpenGroupAdded(urlAsString: String) {
|
||||||
val server = OpenGroupV2.getServer(urlAsString)
|
val server = OpenGroup.getServer(urlAsString)
|
||||||
OpenGroupManager.restartPollerForServer(server.toString().removeSuffix("/"))
|
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 {
|
override fun getOrCreateThreadIdFor(publicKey: String, groupPublicKey: String?, openGroupID: String?): Long {
|
||||||
val database = DatabaseComponent.get(context).threadDatabase()
|
val database = DatabaseComponent.get(context).threadDatabase()
|
||||||
if (!openGroupID.isNullOrEmpty()) {
|
return if (!openGroupID.isNullOrEmpty()) {
|
||||||
val recipient = Recipient.from(context, Address.fromSerialized(GroupUtil.getEncodedOpenGroupID(openGroupID.toByteArray())), false)
|
val recipient = Recipient.from(context, fromSerialized(GroupUtil.getEncodedOpenGroupID(openGroupID.toByteArray())), false)
|
||||||
return database.getThreadIdIfExistsFor(recipient)
|
database.getThreadIdIfExistsFor(recipient)
|
||||||
} else if (!groupPublicKey.isNullOrEmpty()) {
|
} else if (!groupPublicKey.isNullOrEmpty()) {
|
||||||
val recipient = Recipient.from(context, Address.fromSerialized(GroupUtil.doubleEncodeGroupID(groupPublicKey)), false)
|
val recipient = Recipient.from(context, fromSerialized(GroupUtil.doubleEncodeGroupID(groupPublicKey)), false)
|
||||||
return database.getOrCreateThreadIdFor(recipient)
|
database.getOrCreateThreadIdFor(recipient)
|
||||||
} else {
|
} else {
|
||||||
val recipient = Recipient.from(context, Address.fromSerialized(publicKey), false)
|
val recipient = Recipient.from(context, fromSerialized(publicKey), false)
|
||||||
return database.getOrCreateThreadIdFor(recipient)
|
database.getOrCreateThreadIdFor(recipient)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getThreadId(publicKeyOrOpenGroupID: String): Long? {
|
override fun getThreadId(publicKeyOrOpenGroupID: String): Long? {
|
||||||
val address = Address.fromSerialized(publicKeyOrOpenGroupID)
|
val address = fromSerialized(publicKeyOrOpenGroupID)
|
||||||
return getThreadId(address)
|
return getThreadId(address)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -622,8 +649,13 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
|||||||
override fun addContacts(contacts: List<ConfigurationMessage.Contact>) {
|
override fun addContacts(contacts: List<ConfigurationMessage.Contact>) {
|
||||||
val recipientDatabase = DatabaseComponent.get(context).recipientDatabase()
|
val recipientDatabase = DatabaseComponent.get(context).recipientDatabase()
|
||||||
val threadDatabase = DatabaseComponent.get(context).threadDatabase()
|
val threadDatabase = DatabaseComponent.get(context).threadDatabase()
|
||||||
for (contact in contacts) {
|
val mappingDb = DatabaseComponent.get(context).blindedIdMappingDatabase()
|
||||||
val address = Address.fromSerialized(contact.publicKey)
|
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)
|
val recipient = Recipient.from(context, address, true)
|
||||||
if (!contact.profilePicture.isNullOrEmpty()) {
|
if (!contact.profilePicture.isNullOrEmpty()) {
|
||||||
recipientDatabase.setProfileAvatar(recipient, contact.profilePicture)
|
recipientDatabase.setProfileAvatar(recipient, contact.profilePicture)
|
||||||
@ -715,12 +747,48 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
|||||||
threadDB.setHasSent(threadId, true)
|
threadDB.setHasSent(threadId, true)
|
||||||
} else {
|
} else {
|
||||||
val mmsDb = DatabaseComponent.get(context).mmsDatabase()
|
val mmsDb = DatabaseComponent.get(context).mmsDatabase()
|
||||||
val senderAddress = fromSerialized(senderPublicKey)
|
val smsDb = DatabaseComponent.get(context).smsDatabase()
|
||||||
val requestSender = Recipient.from(context, senderAddress, false)
|
val sender = Recipient.from(context, fromSerialized(senderPublicKey), false)
|
||||||
recipientDb.setApprovedMe(requestSender, true)
|
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(
|
val message = IncomingMediaMessage(
|
||||||
senderAddress,
|
sender.address,
|
||||||
response.sentTimestamp!!,
|
response.sentTimestamp!!,
|
||||||
-1,
|
-1,
|
||||||
0,
|
0,
|
||||||
@ -735,7 +803,6 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
|||||||
Optional.absent(),
|
Optional.absent(),
|
||||||
Optional.absent()
|
Optional.absent()
|
||||||
)
|
)
|
||||||
val threadId = getOrCreateThreadIdFor(senderAddress)
|
|
||||||
mmsDb.insertSecureDecryptedMessageInbox(message, threadId, runIncrement = true, runThreadUpdate = true)
|
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)
|
DatabaseComponent.get(context).recipientDatabase().setApprovedMe(recipient, approvedMe)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
override fun insertCallMessage(senderPublicKey: String, callMessageType: CallMessageType, sentTimestamp: Long) {
|
override fun insertCallMessage(senderPublicKey: String, callMessageType: CallMessageType, sentTimestamp: Long) {
|
||||||
val database = DatabaseComponent.get(context).smsDatabase()
|
val database = DatabaseComponent.get(context).smsDatabase()
|
||||||
val address = fromSerialized(senderPublicKey)
|
val address = fromSerialized(senderPublicKey)
|
||||||
@ -764,4 +830,62 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
|||||||
|
|
||||||
return database.getLastSeenAndHasSent(threadId).second() ?: false
|
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
|
||||||
|
}
|
||||||
}
|
}
|
@ -43,6 +43,7 @@ import org.session.libsession.utilities.TextSecurePreferences;
|
|||||||
import org.session.libsession.utilities.Util;
|
import org.session.libsession.utilities.Util;
|
||||||
import org.session.libsession.utilities.recipients.Recipient;
|
import org.session.libsession.utilities.recipients.Recipient;
|
||||||
import org.session.libsession.utilities.recipients.Recipient.RecipientSettings;
|
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.Log;
|
||||||
import org.session.libsignal.utilities.Pair;
|
import org.session.libsignal.utilities.Pair;
|
||||||
import org.session.libsignal.utilities.guava.Optional;
|
import org.session.libsignal.utilities.guava.Optional;
|
||||||
@ -447,6 +448,11 @@ public class ThreadDatabase extends Database {
|
|||||||
return getConversationList(where);
|
return getConversationList(where);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Cursor getBlindedConversationList() {
|
||||||
|
String where = TABLE_NAME + "." + ADDRESS + " LIKE '" + IdPrefix.BLINDED.getValue() + "%' ";
|
||||||
|
return getConversationList(where);
|
||||||
|
}
|
||||||
|
|
||||||
public Cursor getApprovedConversationList() {
|
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 + "%') " +
|
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 ";
|
"AND " + ARCHIVED + " = 0 ";
|
||||||
|
@ -14,8 +14,10 @@ import org.session.libsession.utilities.TextSecurePreferences;
|
|||||||
import org.session.libsignal.utilities.Log;
|
import org.session.libsignal.utilities.Log;
|
||||||
import org.thoughtcrime.securesms.crypto.DatabaseSecret;
|
import org.thoughtcrime.securesms.crypto.DatabaseSecret;
|
||||||
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||||
|
import org.thoughtcrime.securesms.database.BlindedIdMappingDatabase;
|
||||||
import org.thoughtcrime.securesms.database.DraftDatabase;
|
import org.thoughtcrime.securesms.database.DraftDatabase;
|
||||||
import org.thoughtcrime.securesms.database.GroupDatabase;
|
import org.thoughtcrime.securesms.database.GroupDatabase;
|
||||||
|
import org.thoughtcrime.securesms.database.GroupMemberDatabase;
|
||||||
import org.thoughtcrime.securesms.database.GroupReceiptDatabase;
|
import org.thoughtcrime.securesms.database.GroupReceiptDatabase;
|
||||||
import org.thoughtcrime.securesms.database.JobDatabase;
|
import org.thoughtcrime.securesms.database.JobDatabase;
|
||||||
import org.thoughtcrime.securesms.database.LokiAPIDatabase;
|
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 lokiV32 = 53;
|
||||||
private static final int lokiV33 = 54;
|
private static final int lokiV33 = 54;
|
||||||
private static final int lokiV34 = 55;
|
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
|
// 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 static final String DATABASE_NAME = "signal.db";
|
||||||
|
|
||||||
private final Context context;
|
private final Context context;
|
||||||
@ -135,6 +138,9 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
|||||||
db.execSQL(LokiAPIDatabase.getCreateOpenGroupProfilePictureTableCommand());
|
db.execSQL(LokiAPIDatabase.getCreateOpenGroupProfilePictureTableCommand());
|
||||||
db.execSQL(LokiAPIDatabase.getCreateClosedGroupEncryptionKeyPairsTable());
|
db.execSQL(LokiAPIDatabase.getCreateClosedGroupEncryptionKeyPairsTable());
|
||||||
db.execSQL(LokiAPIDatabase.getCreateClosedGroupPublicKeysTable());
|
db.execSQL(LokiAPIDatabase.getCreateClosedGroupPublicKeysTable());
|
||||||
|
db.execSQL(LokiAPIDatabase.getCreateServerCapabilitiesCommand());
|
||||||
|
db.execSQL(LokiAPIDatabase.getCreateLastInboxMessageServerIdCommand());
|
||||||
|
db.execSQL(LokiAPIDatabase.getCreateLastOutboxMessageServerIdCommand());
|
||||||
db.execSQL(LokiMessageDatabase.getCreateMessageIDTableCommand());
|
db.execSQL(LokiMessageDatabase.getCreateMessageIDTableCommand());
|
||||||
db.execSQL(LokiMessageDatabase.getCreateMessageToThreadMappingTableCommand());
|
db.execSQL(LokiMessageDatabase.getCreateMessageToThreadMappingTableCommand());
|
||||||
db.execSQL(LokiMessageDatabase.getCreateErrorMessageTableCommand());
|
db.execSQL(LokiMessageDatabase.getCreateErrorMessageTableCommand());
|
||||||
@ -161,6 +167,8 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
|||||||
db.execSQL(LokiAPIDatabase.DROP_LEGACY_LAST_HASH);
|
db.execSQL(LokiAPIDatabase.DROP_LEGACY_LAST_HASH);
|
||||||
db.execSQL(LokiAPIDatabase.INSERT_RECEIVED_HASHES_DATA);
|
db.execSQL(LokiAPIDatabase.INSERT_RECEIVED_HASHES_DATA);
|
||||||
db.execSQL(LokiAPIDatabase.DROP_LEGACY_RECEIVED_HASHES);
|
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, SmsDatabase.CREATE_INDEXS);
|
||||||
executeStatements(db, MmsDatabase.CREATE_INDEXS);
|
executeStatements(db, MmsDatabase.CREATE_INDEXS);
|
||||||
@ -369,6 +377,14 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
|||||||
db.execSQL(LokiAPIDatabase.DROP_LEGACY_RECEIVED_HASHES);
|
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();
|
db.setTransactionSuccessful();
|
||||||
} finally {
|
} finally {
|
||||||
db.endTransaction();
|
db.endTransaction();
|
||||||
|
@ -115,6 +115,8 @@ public class ThreadRecord extends DisplayRecord {
|
|||||||
return emphasisAdded(context.getString(R.string.ThreadRecord_you_marked_verified));
|
return emphasisAdded(context.getString(R.string.ThreadRecord_you_marked_verified));
|
||||||
} else if (SmsDatabase.Types.isIdentityDefault(type)) {
|
} else if (SmsDatabase.Types.isIdentityDefault(type)) {
|
||||||
return emphasisAdded(context.getString(R.string.ThreadRecord_you_marked_unverified));
|
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) {
|
} else if (getCount() == 0) {
|
||||||
return new SpannableString(context.getString(R.string.ThreadRecord_empty_message));
|
return new SpannableString(context.getString(R.string.ThreadRecord_empty_message));
|
||||||
} else {
|
} else {
|
||||||
|
@ -42,4 +42,6 @@ interface DatabaseComponent {
|
|||||||
fun sessionContactDatabase(): SessionContactDatabase
|
fun sessionContactDatabase(): SessionContactDatabase
|
||||||
fun storage(): Storage
|
fun storage(): Storage
|
||||||
fun attachmentProvider(): MessageDataProvider
|
fun attachmentProvider(): MessageDataProvider
|
||||||
|
fun blindedIdMappingDatabase(): BlindedIdMappingDatabase
|
||||||
|
fun groupMemberDatabase(): GroupMemberDatabase
|
||||||
}
|
}
|
@ -117,6 +117,14 @@ object DatabaseModule {
|
|||||||
@Singleton
|
@Singleton
|
||||||
fun provideSessionContactDatabase(@ApplicationContext context: Context, openHelper: SQLCipherOpenHelper) = SessionContactDatabase(context,openHelper)
|
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
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideStorage(@ApplicationContext context: Context, openHelper: SQLCipherOpenHelper) = Storage(context,openHelper)
|
fun provideStorage(@ApplicationContext context: Context, openHelper: SQLCipherOpenHelper) = Storage(context,openHelper)
|
||||||
|
@ -4,19 +4,22 @@ import androidx.lifecycle.ViewModel
|
|||||||
import androidx.lifecycle.asLiveData
|
import androidx.lifecycle.asLiveData
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.onStart
|
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
|
import org.thoughtcrime.securesms.util.State
|
||||||
|
|
||||||
typealias DefaultGroups = List<OpenGroupAPIV2.DefaultGroup>
|
typealias DefaultGroups = List<OpenGroupApi.DefaultGroup>
|
||||||
typealias GroupState = State<DefaultGroups>
|
typealias GroupState = State<DefaultGroups>
|
||||||
|
|
||||||
class DefaultGroupsViewModel : ViewModel() {
|
class DefaultGroupsViewModel : ViewModel() {
|
||||||
|
|
||||||
init {
|
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)
|
State.Success(it)
|
||||||
}.onStart {
|
}.onStart {
|
||||||
emit(State.Loading)
|
emit(State.Loading)
|
||||||
|
@ -26,7 +26,7 @@ import network.loki.messenger.databinding.ActivityJoinPublicChatBinding
|
|||||||
import network.loki.messenger.databinding.FragmentEnterChatUrlBinding
|
import network.loki.messenger.databinding.FragmentEnterChatUrlBinding
|
||||||
import okhttp3.HttpUrl
|
import okhttp3.HttpUrl
|
||||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
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.Address
|
||||||
import org.session.libsession.utilities.GroupUtil
|
import org.session.libsession.utilities.GroupUtil
|
||||||
import org.session.libsession.utilities.recipients.Recipient
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
|
@ -4,16 +4,17 @@ import android.content.Context
|
|||||||
import androidx.annotation.WorkerThread
|
import androidx.annotation.WorkerThread
|
||||||
import okhttp3.HttpUrl
|
import okhttp3.HttpUrl
|
||||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
|
import org.session.libsession.messaging.open_groups.GroupMemberRole
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroupV2
|
import org.session.libsession.messaging.open_groups.OpenGroup
|
||||||
import org.session.libsession.messaging.sending_receiving.pollers.OpenGroupPollerV2
|
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.session.libsignal.utilities.ThreadUtils
|
||||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
|
|
||||||
object OpenGroupManager {
|
object OpenGroupManager {
|
||||||
private val executorService = Executors.newScheduledThreadPool(4)
|
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 var isPolling = false
|
||||||
private val pollUpdaterLock = Any()
|
private val pollUpdaterLock = Any()
|
||||||
|
|
||||||
@ -38,10 +39,10 @@ object OpenGroupManager {
|
|||||||
if (isPolling) { return }
|
if (isPolling) { return }
|
||||||
isPolling = true
|
isPolling = true
|
||||||
val storage = MessagingModuleConfiguration.shared.storage
|
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 ->
|
servers.forEach { server ->
|
||||||
pollers[server]?.stop() // Shouldn't be necessary
|
pollers[server]?.stop() // Shouldn't be necessary
|
||||||
val poller = OpenGroupPollerV2(server, executorService)
|
val poller = OpenGroupPoller(server, executorService)
|
||||||
poller.startIfNeeded()
|
poller.startIfNeeded()
|
||||||
pollers[server] = poller
|
pollers[server] = poller
|
||||||
}
|
}
|
||||||
@ -67,17 +68,21 @@ object OpenGroupManager {
|
|||||||
// Clear any existing data if needed
|
// Clear any existing data if needed
|
||||||
storage.removeLastDeletionServerID(room, server)
|
storage.removeLastDeletionServerID(room, server)
|
||||||
storage.removeLastMessageServerID(room, server)
|
storage.removeLastMessageServerID(room, server)
|
||||||
|
storage.removeLastInboxMessageId(server)
|
||||||
|
storage.removeLastOutboxMessageId(server)
|
||||||
// Store the public key
|
// Store the public key
|
||||||
storage.setOpenGroupPublicKey(server,publicKey)
|
storage.setOpenGroupPublicKey(server,publicKey)
|
||||||
// Get group info
|
// Get capabilities
|
||||||
OpenGroupAPIV2.getAuthToken(room, server).get()
|
val capabilities = OpenGroupApi.getCapabilities(server).get()
|
||||||
// Get group info
|
storage.setServerCapabilities(server, capabilities.capabilities)
|
||||||
val info = OpenGroupAPIV2.getInfo(room, server).get()
|
// Get room info
|
||||||
|
val info = OpenGroupApi.getRoomInfo(room, server).get()
|
||||||
|
storage.setUserCount(room, server, info.activeUsers)
|
||||||
// Create the group locally if not available already
|
// Create the group locally if not available already
|
||||||
if (threadID < 0) {
|
if (threadID < 0) {
|
||||||
threadID = GroupManager.createOpenGroup(openGroupID, context, null, info.name).threadId
|
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)
|
threadDB.setOpenGroupChat(openGroup, threadID)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -86,7 +91,7 @@ object OpenGroupManager {
|
|||||||
synchronized(pollUpdaterLock) {
|
synchronized(pollUpdaterLock) {
|
||||||
pollers[server]?.stop()
|
pollers[server]?.stop()
|
||||||
pollers[server]?.startIfNeeded() ?: run {
|
pollers[server]?.startIfNeeded() ?: run {
|
||||||
val poller = OpenGroupPollerV2(server, executorService)
|
val poller = OpenGroupPoller(server, executorService)
|
||||||
pollers[server] = poller
|
pollers[server] = poller
|
||||||
poller.startIfNeeded()
|
poller.startIfNeeded()
|
||||||
}
|
}
|
||||||
@ -102,7 +107,7 @@ object OpenGroupManager {
|
|||||||
threadDB.setThreadArchived(threadID)
|
threadDB.setThreadArchived(threadID)
|
||||||
val groupID = recipient.address.serialize()
|
val groupID = recipient.address.serialize()
|
||||||
// Stop the poller if needed
|
// 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) {
|
if (openGroups.count() == 1) {
|
||||||
synchronized(pollUpdaterLock) {
|
synchronized(pollUpdaterLock) {
|
||||||
val poller = pollers[server]
|
val poller = pollers[server]
|
||||||
@ -113,6 +118,8 @@ object OpenGroupManager {
|
|||||||
// Delete
|
// Delete
|
||||||
storage.removeLastDeletionServerID(room, server)
|
storage.removeLastDeletionServerID(room, server)
|
||||||
storage.removeLastMessageServerID(room, server)
|
storage.removeLastMessageServerID(room, server)
|
||||||
|
storage.removeLastInboxMessageId(server)
|
||||||
|
storage.removeLastOutboxMessageId(server)
|
||||||
val lokiThreadDB = DatabaseComponent.get(context).lokiThreadDatabase()
|
val lokiThreadDB = DatabaseComponent.get(context).lokiThreadDatabase()
|
||||||
lokiThreadDB.removeOpenGroupChat(threadID)
|
lokiThreadDB.removeOpenGroupChat(threadID)
|
||||||
ThreadUtils.queue {
|
ThreadUtils.queue {
|
||||||
@ -123,9 +130,26 @@ object OpenGroupManager {
|
|||||||
|
|
||||||
fun addOpenGroup(urlAsString: String, context: Context) {
|
fun addOpenGroup(urlAsString: String, context: Context) {
|
||||||
val url = HttpUrl.parse(urlAsString) ?: return
|
val url = HttpUrl.parse(urlAsString) ?: return
|
||||||
val server = OpenGroupV2.getServer(urlAsString)
|
val server = OpenGroup.getServer(urlAsString)
|
||||||
val room = url.pathSegments().firstOrNull() ?: return
|
val room = url.pathSegments().firstOrNull() ?: return
|
||||||
val publicKey = url.queryParameter("public_key") ?: return
|
val publicKey = url.queryParameter("public_key") ?: return
|
||||||
add(server.toString().removeSuffix("/"), room, publicKey, context)
|
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
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -3,7 +3,7 @@ package org.thoughtcrime.securesms.groups
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.annotation.WorkerThread
|
import androidx.annotation.WorkerThread
|
||||||
import org.greenrobot.eventbus.EventBus
|
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.GroupUtil
|
||||||
import org.session.libsession.utilities.recipients.Recipient
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
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")
|
throw IllegalStateException("Attempt to update open group info for non-existent DB record: $groupId")
|
||||||
}
|
}
|
||||||
|
|
||||||
val info = OpenGroupAPIV2.getInfo(room, server).get() // store info again?
|
val info = OpenGroupApi.getRoomInfo(room, server).get() // store info again?
|
||||||
OpenGroupAPIV2.getMemberCount(room, server).get()
|
|
||||||
|
|
||||||
EventBus.getDefault().post(GroupInfoUpdatedEvent(server, room = room))
|
EventBus.getDefault().post(GroupInfoUpdatedEvent(server, room = room))
|
||||||
}
|
}
|
||||||
|
@ -82,7 +82,7 @@ class ConversationView : LinearLayout {
|
|||||||
}
|
}
|
||||||
binding.muteIndicatorImageView.setImageResource(drawableRes)
|
binding.muteIndicatorImageView.setImageResource(drawableRes)
|
||||||
val rawSnippet = thread.getDisplayBody(context)
|
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.text = snippet
|
||||||
binding.snippetTextView.typeface = if (unreadCount > 0 && !thread.isRead) Typeface.DEFAULT_BOLD else Typeface.DEFAULT
|
binding.snippetTextView.typeface = if (unreadCount > 0 && !thread.isRead) Typeface.DEFAULT_BOLD else Typeface.DEFAULT
|
||||||
binding.snippetTextView.visibility = if (isTyping) View.GONE else View.VISIBLE
|
binding.snippetTextView.visibility = if (isTyping) View.GONE else View.VISIBLE
|
||||||
|
@ -21,6 +21,7 @@ import org.session.libsession.messaging.MessagingModuleConfiguration
|
|||||||
import org.session.libsession.messaging.contacts.Contact
|
import org.session.libsession.messaging.contacts.Contact
|
||||||
import org.session.libsession.utilities.Address
|
import org.session.libsession.utilities.Address
|
||||||
import org.session.libsession.utilities.recipients.Recipient
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
|
import org.session.libsignal.utilities.IdPrefix
|
||||||
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
|
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
|
||||||
import org.thoughtcrime.securesms.database.ThreadDatabase
|
import org.thoughtcrime.securesms.database.ThreadDatabase
|
||||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
||||||
@ -83,8 +84,8 @@ class UserDetailsBottomSheet : BottomSheetDialogFragment() {
|
|||||||
}
|
}
|
||||||
nameTextView.text = recipient.name ?: publicKey // Uses the Contact API internally
|
nameTextView.text = recipient.name ?: publicKey // Uses the Contact API internally
|
||||||
|
|
||||||
publicKeyTextView.isVisible = !threadRecipient.isOpenGroupRecipient
|
publicKeyTextView.isVisible = !threadRecipient.isOpenGroupRecipient && !threadRecipient.isOpenGroupInboxRecipient
|
||||||
messageButton.isVisible = !threadRecipient.isOpenGroupRecipient
|
messageButton.isVisible = !threadRecipient.isOpenGroupRecipient || IdPrefix.fromValue(publicKey) == IdPrefix.BLINDED
|
||||||
publicKeyTextView.text = publicKey
|
publicKeyTextView.text = publicKey
|
||||||
publicKeyTextView.setOnLongClickListener {
|
publicKeyTextView.setOnLongClickListener {
|
||||||
val clipboard =
|
val clipboard =
|
||||||
@ -103,6 +104,7 @@ class UserDetailsBottomSheet : BottomSheetDialogFragment() {
|
|||||||
)
|
)
|
||||||
intent.putExtra(ConversationActivityV2.ADDRESS, recipient.address)
|
intent.putExtra(ConversationActivityV2.ADDRESS, recipient.address)
|
||||||
intent.putExtra(ConversationActivityV2.THREAD_ID, threadId ?: -1)
|
intent.putExtra(ConversationActivityV2.THREAD_ID, threadId ?: -1)
|
||||||
|
intent.putExtra(ConversationActivityV2.FROM_GROUP_THREAD_ID, threadID)
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@ package org.thoughtcrime.securesms.mms;
|
|||||||
|
|
||||||
import android.content.Context;
|
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 {
|
public class PushMediaConstraints extends MediaConstraints {
|
||||||
|
|
||||||
@ -21,26 +21,26 @@ public class PushMediaConstraints extends MediaConstraints {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getImageMaxSize(Context context) {
|
public int getImageMaxSize(Context context) {
|
||||||
return (int) (((double) FileServerAPIV2.maxFileSize) / FileServerAPIV2.fileSizeORMultiplier);
|
return (int) (((double) FileServerApi.maxFileSize) / FileServerApi.fileSizeORMultiplier);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getGifMaxSize(Context context) {
|
public int getGifMaxSize(Context context) {
|
||||||
return (int) (((double) FileServerAPIV2.maxFileSize) / FileServerAPIV2.fileSizeORMultiplier);
|
return (int) (((double) FileServerApi.maxFileSize) / FileServerApi.fileSizeORMultiplier);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getVideoMaxSize(Context context) {
|
public int getVideoMaxSize(Context context) {
|
||||||
return (int) (((double) FileServerAPIV2.maxFileSize) / FileServerAPIV2.fileSizeORMultiplier);
|
return (int) (((double) FileServerApi.maxFileSize) / FileServerApi.fileSizeORMultiplier);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getAudioMaxSize(Context context) {
|
public int getAudioMaxSize(Context context) {
|
||||||
return (int) (((double) FileServerAPIV2.maxFileSize) / FileServerAPIV2.fileSizeORMultiplier);
|
return (int) (((double) FileServerApi.maxFileSize) / FileServerApi.fileSizeORMultiplier);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getDocumentMaxSize(Context context) {
|
public int getDocumentMaxSize(Context context) {
|
||||||
return (int) (((double) FileServerAPIV2.maxFileSize) / FileServerAPIV2.fileSizeORMultiplier);
|
return (int) (((double) FileServerApi.maxFileSize) / FileServerApi.fileSizeORMultiplier);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,7 @@ import org.session.libsession.messaging.MessagingModuleConfiguration
|
|||||||
import org.session.libsession.messaging.jobs.BatchMessageReceiveJob
|
import org.session.libsession.messaging.jobs.BatchMessageReceiveJob
|
||||||
import org.session.libsession.messaging.jobs.MessageReceiveParameters
|
import org.session.libsession.messaging.jobs.MessageReceiveParameters
|
||||||
import org.session.libsession.messaging.sending_receiving.pollers.ClosedGroupPollerV2
|
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.snode.SnodeAPI
|
||||||
import org.session.libsession.utilities.TextSecurePreferences
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
import org.session.libsignal.utilities.Log
|
import org.session.libsignal.utilities.Log
|
||||||
@ -72,13 +72,13 @@ class BackgroundPollWorker(val context: Context, params: WorkerParameters) : Wor
|
|||||||
|
|
||||||
// Open Groups
|
// Open Groups
|
||||||
val threadDB = DatabaseComponent.get(context).lokiThreadDatabase()
|
val threadDB = DatabaseComponent.get(context).lokiThreadDatabase()
|
||||||
val v2OpenGroups = threadDB.getAllV2OpenGroups()
|
val openGroups = threadDB.getAllOpenGroups()
|
||||||
val v2OpenGroupServers = v2OpenGroups.map { it.value.server }.toSet()
|
val openGroupServers = openGroups.map { it.value.server }.toSet()
|
||||||
|
|
||||||
for (server in v2OpenGroupServers) {
|
for (server in openGroupServers) {
|
||||||
val poller = OpenGroupPollerV2(server, null)
|
val poller = OpenGroupPoller(server, null)
|
||||||
poller.hasStarted = true
|
poller.hasStarted = true
|
||||||
promises.add(poller.poll(true))
|
promises.add(poller.poll())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait until all the promises are resolved
|
// Wait until all the promises are resolved
|
||||||
|
@ -36,15 +36,22 @@ import android.service.notification.StatusBarNotification;
|
|||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
import androidx.core.app.NotificationCompat;
|
import androidx.core.app.NotificationCompat;
|
||||||
import androidx.core.app.NotificationManagerCompat;
|
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.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.Address;
|
||||||
import org.session.libsession.utilities.Contact;
|
import org.session.libsession.utilities.Contact;
|
||||||
import org.session.libsession.utilities.ServiceUtil;
|
import org.session.libsession.utilities.ServiceUtil;
|
||||||
import org.session.libsession.utilities.TextSecurePreferences;
|
import org.session.libsession.utilities.TextSecurePreferences;
|
||||||
import org.session.libsession.utilities.recipients.Recipient;
|
import org.session.libsession.utilities.recipients.Recipient;
|
||||||
|
import org.session.libsignal.utilities.IdPrefix;
|
||||||
import org.session.libsignal.utilities.Log;
|
import org.session.libsignal.utilities.Log;
|
||||||
import org.session.libsignal.utilities.Util;
|
import org.session.libsignal.utilities.Util;
|
||||||
import org.thoughtcrime.securesms.ApplicationContext;
|
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.ConversationActivityV2;
|
||||||
import org.thoughtcrime.securesms.conversation.v2.utilities.MentionManagerUtilities;
|
import org.thoughtcrime.securesms.conversation.v2.utilities.MentionManagerUtilities;
|
||||||
import org.thoughtcrime.securesms.conversation.v2.utilities.MentionUtilities;
|
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.MessagingDatabase.MarkedMessageInfo;
|
||||||
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
|
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
|
||||||
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
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.SessionMetaProtocol;
|
||||||
import org.thoughtcrime.securesms.util.SpanUtil;
|
import org.thoughtcrime.securesms.util.SpanUtil;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.ListIterator;
|
import java.util.ListIterator;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.Executor;
|
import java.util.concurrent.Executor;
|
||||||
@ -491,6 +502,7 @@ public class DefaultMessageNotifier implements MessageNotifier {
|
|||||||
MmsSmsDatabase.Reader reader = DatabaseComponent.get(context).mmsSmsDatabase().readerFor(cursor);
|
MmsSmsDatabase.Reader reader = DatabaseComponent.get(context).mmsSmsDatabase().readerFor(cursor);
|
||||||
ThreadDatabase threadDatabase = DatabaseComponent.get(context).threadDatabase();
|
ThreadDatabase threadDatabase = DatabaseComponent.get(context).threadDatabase();
|
||||||
MessageRecord record;
|
MessageRecord record;
|
||||||
|
Map<Long, String> cache = new HashMap<Long, String>();
|
||||||
|
|
||||||
while ((record = reader.getNext()) != null) {
|
while ((record = reader.getNext()) != null) {
|
||||||
long id = record.getId();
|
long id = record.getId();
|
||||||
@ -534,16 +546,22 @@ public class DefaultMessageNotifier implements MessageNotifier {
|
|||||||
|
|
||||||
if (threadRecipients == null || !threadRecipients.isMuted()) {
|
if (threadRecipients == null || !threadRecipients.isMuted()) {
|
||||||
if (threadRecipients != null && threadRecipients.notifyType == RecipientDatabase.NOTIFY_TYPE_MENTIONS) {
|
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
|
// check if mentioned here
|
||||||
boolean isQuoteMentioned = false;
|
boolean isQuoteMentioned = false;
|
||||||
if (record instanceof MmsMessageRecord) {
|
if (record instanceof MmsMessageRecord) {
|
||||||
Quote quote = ((MmsMessageRecord) record).getQuote();
|
Quote quote = ((MmsMessageRecord) record).getQuote();
|
||||||
Address quoteAddress = quote != null ? quote.getAuthor() : null;
|
Address quoteAddress = quote != null ? quote.getAuthor() : null;
|
||||||
String serializedAddress = quoteAddress != null ? quoteAddress.serialize() : 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))
|
if (body.toString().contains("@"+userPublicKey) || body.toString().contains("@"+blindedPublicKey) || isQuoteMentioned) {
|
||||||
|| isQuoteMentioned) {
|
|
||||||
notificationState.addNotification(new NotificationItem(id, mms, recipient, conversationRecipient, threadRecipients, threadId, body, timestamp, slideDeck));
|
notificationState.addNotification(new NotificationItem(id, mms, recipient, conversationRecipient, threadRecipients, threadId, body, timestamp, slideDeck));
|
||||||
}
|
}
|
||||||
} else if (threadRecipients != null && threadRecipients.notifyType == RecipientDatabase.NOTIFY_TYPE_NONE) {
|
} else if (threadRecipients != null && threadRecipients.notifyType == RecipientDatabase.NOTIFY_TYPE_NONE) {
|
||||||
@ -558,6 +576,19 @@ public class DefaultMessageNotifier implements MessageNotifier {
|
|||||||
return notificationState;
|
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) {
|
private void updateBadge(Context context, int count) {
|
||||||
try {
|
try {
|
||||||
if (count == 0) ShortcutBadger.removeCount(context);
|
if (count == 0) ShortcutBadger.removeCount(context);
|
||||||
|
@ -1,12 +1,14 @@
|
|||||||
package org.thoughtcrime.securesms.notifications
|
package org.thoughtcrime.securesms.notifications
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import nl.komponents.kovenant.Promise
|
||||||
import nl.komponents.kovenant.functional.map
|
import nl.komponents.kovenant.functional.map
|
||||||
import okhttp3.MediaType
|
import okhttp3.MediaType
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.RequestBody
|
import okhttp3.RequestBody
|
||||||
import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI
|
import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI
|
||||||
import org.session.libsession.snode.OnionRequestAPI
|
import org.session.libsession.snode.OnionRequestAPI
|
||||||
|
import org.session.libsession.snode.Version
|
||||||
import org.session.libsession.utilities.TextSecurePreferences
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
import org.session.libsignal.utilities.JsonUtil
|
import org.session.libsignal.utilities.JsonUtil
|
||||||
import org.session.libsignal.utilities.Log
|
import org.session.libsignal.utilities.Log
|
||||||
@ -43,7 +45,7 @@ object LokiPushNotificationManager {
|
|||||||
val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters))
|
val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters))
|
||||||
val request = Request.Builder().url(url).post(body)
|
val request = Request.Builder().url(url).post(body)
|
||||||
retryIfNeeded(maxRetryCount) {
|
retryIfNeeded(maxRetryCount) {
|
||||||
OnionRequestAPI.sendOnionRequest(request.build(), server, pnServerPublicKey, "/loki/v2/lsrpc").map { json ->
|
getResponseBody(request.build()).map { json ->
|
||||||
val code = json["code"] as? Int
|
val code = json["code"] as? Int
|
||||||
if (code != null && code != 0) {
|
if (code != null && code != 0) {
|
||||||
TextSecurePreferences.setIsUsingFCM(context, false)
|
TextSecurePreferences.setIsUsingFCM(context, false)
|
||||||
@ -72,7 +74,7 @@ object LokiPushNotificationManager {
|
|||||||
val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters))
|
val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters))
|
||||||
val request = Request.Builder().url(url).post(body)
|
val request = Request.Builder().url(url).post(body)
|
||||||
retryIfNeeded(maxRetryCount) {
|
retryIfNeeded(maxRetryCount) {
|
||||||
OnionRequestAPI.sendOnionRequest(request.build(), server, pnServerPublicKey, "/loki/v2/lsrpc").map { json ->
|
getResponseBody(request.build()).map { json ->
|
||||||
val code = json["code"] as? Int
|
val code = json["code"] as? Int
|
||||||
if (code != null && code != 0) {
|
if (code != null && code != 0) {
|
||||||
TextSecurePreferences.setIsUsingFCM(context, true)
|
TextSecurePreferences.setIsUsingFCM(context, true)
|
||||||
@ -100,7 +102,7 @@ object LokiPushNotificationManager {
|
|||||||
val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters))
|
val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters))
|
||||||
val request = Request.Builder().url(url).post(body)
|
val request = Request.Builder().url(url).post(body)
|
||||||
retryIfNeeded(maxRetryCount) {
|
retryIfNeeded(maxRetryCount) {
|
||||||
OnionRequestAPI.sendOnionRequest(request.build(), server, pnServerPublicKey, "/loki/v2/lsrpc").map { json ->
|
getResponseBody(request.build()).map { json ->
|
||||||
val code = json["code"] as? Int
|
val code = json["code"] as? Int
|
||||||
if (code == null || code == 0) {
|
if (code == null || code == 0) {
|
||||||
Log.d("Loki", "Couldn't subscribe/unsubscribe closed group: $closedGroupPublicKey due to error: ${json["message"] as? String ?: "null"}.")
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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.signal.OutgoingTextMessage
|
||||||
import org.session.libsession.messaging.messages.visible.OpenGroupInvitation
|
import org.session.libsession.messaging.messages.visible.OpenGroupInvitation
|
||||||
import org.session.libsession.messaging.messages.visible.VisibleMessage
|
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.MessageSender
|
||||||
import org.session.libsession.snode.SnodeAPI
|
import org.session.libsession.snode.SnodeAPI
|
||||||
import org.session.libsession.utilities.Address
|
import org.session.libsession.utilities.Address
|
||||||
@ -86,7 +86,7 @@ class DefaultConversationRepository @Inject constructor(
|
|||||||
|
|
||||||
override fun isOxenHostedOpenGroup(threadId: Long): Boolean {
|
override fun isOxenHostedOpenGroup(threadId: Long): Boolean {
|
||||||
val openGroup = lokiThreadDb.getOpenGroupChat(threadId)
|
val openGroup = lokiThreadDb.getOpenGroupChat(threadId)
|
||||||
return openGroup?.publicKey == OpenGroupAPIV2.defaultServerPublicKey
|
return openGroup?.publicKey == OpenGroupApi.defaultServerPublicKey
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun maybeGetRecipientForThreadId(threadId: Long): Recipient? {
|
override fun maybeGetRecipientForThreadId(threadId: Long): Recipient? {
|
||||||
@ -153,7 +153,7 @@ class DefaultConversationRepository @Inject constructor(
|
|||||||
val openGroup = lokiThreadDb.getOpenGroupChat(threadId)
|
val openGroup = lokiThreadDb.getOpenGroupChat(threadId)
|
||||||
if (openGroup != null) {
|
if (openGroup != null) {
|
||||||
lokiMessageDb.getServerID(message.id, !message.isMms)?.let { messageServerID ->
|
lokiMessageDb.getServerID(message.id, !message.isMms)?.let { messageServerID ->
|
||||||
OpenGroupAPIV2.deleteMessage(messageServerID, openGroup.room, openGroup.server)
|
OpenGroupApi.deleteMessage(messageServerID, openGroup.room, openGroup.server)
|
||||||
.success {
|
.success {
|
||||||
messageDataProvider.deleteMessage(message.id, !message.isMms)
|
messageDataProvider.deleteMessage(message.id, !message.isMms)
|
||||||
continuation.resume(ResultOf.Success(Unit))
|
continuation.resume(ResultOf.Success(Unit))
|
||||||
@ -205,7 +205,7 @@ class DefaultConversationRepository @Inject constructor(
|
|||||||
messageServerIDs[messageServerID] = message
|
messageServerIDs[messageServerID] = message
|
||||||
}
|
}
|
||||||
for ((messageServerID, message) in messageServerIDs) {
|
for ((messageServerID, message) in messageServerIDs) {
|
||||||
OpenGroupAPIV2.deleteMessage(messageServerID, openGroup.room, openGroup.server)
|
OpenGroupApi.deleteMessage(messageServerID, openGroup.room, openGroup.server)
|
||||||
.success {
|
.success {
|
||||||
messageDataProvider.deleteMessage(message.id, !message.isMms)
|
messageDataProvider.deleteMessage(message.id, !message.isMms)
|
||||||
}.fail { error ->
|
}.fail { error ->
|
||||||
@ -228,7 +228,7 @@ class DefaultConversationRepository @Inject constructor(
|
|||||||
suspendCoroutine { continuation ->
|
suspendCoroutine { continuation ->
|
||||||
val sessionID = recipient.address.toString()
|
val sessionID = recipient.address.toString()
|
||||||
val openGroup = lokiThreadDb.getOpenGroupChat(threadId)!!
|
val openGroup = lokiThreadDb.getOpenGroupChat(threadId)!!
|
||||||
OpenGroupAPIV2.ban(sessionID, openGroup.room, openGroup.server)
|
OpenGroupApi.ban(sessionID, openGroup.room, openGroup.server)
|
||||||
.success {
|
.success {
|
||||||
continuation.resume(ResultOf.Success(Unit))
|
continuation.resume(ResultOf.Success(Unit))
|
||||||
}.fail { error ->
|
}.fail { error ->
|
||||||
@ -240,7 +240,7 @@ class DefaultConversationRepository @Inject constructor(
|
|||||||
suspendCoroutine { continuation ->
|
suspendCoroutine { continuation ->
|
||||||
val sessionID = recipient.address.toString()
|
val sessionID = recipient.address.toString()
|
||||||
val openGroup = lokiThreadDb.getOpenGroupChat(threadId)!!
|
val openGroup = lokiThreadDb.getOpenGroupChat(threadId)!!
|
||||||
OpenGroupAPIV2.banAndDeleteAll(sessionID, openGroup.room, openGroup.server)
|
OpenGroupApi.banAndDeleteAll(sessionID, openGroup.room, openGroup.server)
|
||||||
.success {
|
.success {
|
||||||
continuation.resume(ResultOf.Success(Unit))
|
continuation.resume(ResultOf.Success(Unit))
|
||||||
}.fail { error ->
|
}.fail { error ->
|
||||||
|
@ -12,6 +12,7 @@ import android.graphics.drawable.BitmapDrawable
|
|||||||
import android.text.TextPaint
|
import android.text.TextPaint
|
||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
|
import org.session.libsignal.utilities.IdPrefix
|
||||||
import java.math.BigInteger
|
import java.math.BigInteger
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
@ -66,7 +67,7 @@ object AvatarPlaceholderGenerator {
|
|||||||
fun extractLabel(content: String): String {
|
fun extractLabel(content: String): String {
|
||||||
val trimmedContent = content.trim()
|
val trimmedContent = content.trim()
|
||||||
if (trimmedContent.isEmpty()) return EMPTY_LABEL
|
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()
|
trimmedContent[2].toString()
|
||||||
} else {
|
} else {
|
||||||
val splitWords = trimmedContent.split(Regex("\\W"))
|
val splitWords = trimmedContent.split(Regex("\\W"))
|
||||||
|
@ -20,4 +20,5 @@ object ContactUtilities {
|
|||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -9,7 +9,7 @@ lifecycleVersion=2.3.1
|
|||||||
daggerVersion=2.40.1
|
daggerVersion=2.40.1
|
||||||
glideVersion=4.11.0
|
glideVersion=4.11.0
|
||||||
kovenantVersion=3.3.0
|
kovenantVersion=3.3.0
|
||||||
curve25519Version=0.5.0
|
curve25519Version=0.6.0
|
||||||
protobufVersion=2.5.0
|
protobufVersion=2.5.0
|
||||||
okhttpVersion=3.12.1
|
okhttpVersion=3.12.1
|
||||||
jacksonDatabindVersion=2.9.8
|
jacksonDatabindVersion=2.9.8
|
||||||
|
2
liblazysodium/build.gradle
Normal file
2
liblazysodium/build.gradle
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
configurations.maybeCreate("default")
|
||||||
|
artifacts.add("default", file('lazysodium.aar'))
|
BIN
liblazysodium/lazysodium.aar
Normal file
BIN
liblazysodium/lazysodium.aar
Normal file
Binary file not shown.
@ -18,7 +18,8 @@ android {
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation project(":libsignal")
|
implementation project(":libsignal")
|
||||||
implementation 'com.goterl:lazysodium-android:5.0.2@aar'
|
implementation project(":liblazysodium")
|
||||||
|
// implementation 'com.goterl:lazysodium-android:5.0.2@aar'
|
||||||
implementation "net.java.dev.jna:jna:5.8.0@aar"
|
implementation "net.java.dev.jna:jna:5.8.0@aar"
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
|
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
|
||||||
implementation 'androidx.core:core-ktx:1.3.2'
|
implementation 'androidx.core:core-ktx:1.3.2'
|
||||||
@ -36,7 +37,7 @@ dependencies {
|
|||||||
implementation 'com.esotericsoftware:kryo:5.1.1'
|
implementation 'com.esotericsoftware:kryo:5.1.1'
|
||||||
implementation "com.google.protobuf:protobuf-java:$protobufVersion"
|
implementation "com.google.protobuf:protobuf-java:$protobufVersion"
|
||||||
implementation "com.fasterxml.jackson.core:jackson-databind:$jacksonDatabindVersion"
|
implementation "com.fasterxml.jackson.core:jackson-databind:$jacksonDatabindVersion"
|
||||||
implementation "org.whispersystems:curve25519-java:$curve25519Version"
|
implementation "com.github.oxen-io.session-android-curve-25519:curve25519-java:$curve25519Version"
|
||||||
implementation "com.squareup.okhttp3:okhttp:$okhttpVersion"
|
implementation "com.squareup.okhttp3:okhttp:$okhttpVersion"
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
|
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
|
||||||
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion"
|
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion"
|
||||||
|
@ -11,12 +11,14 @@ import org.session.libsession.messaging.messages.control.ConfigurationMessage
|
|||||||
import org.session.libsession.messaging.messages.control.MessageRequestResponse
|
import org.session.libsession.messaging.messages.control.MessageRequestResponse
|
||||||
import org.session.libsession.messaging.messages.visible.Attachment
|
import org.session.libsession.messaging.messages.visible.Attachment
|
||||||
import org.session.libsession.messaging.messages.visible.VisibleMessage
|
import org.session.libsession.messaging.messages.visible.VisibleMessage
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroupV2
|
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.AttachmentId
|
||||||
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
|
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.data_extraction.DataExtractionNotificationInfoMessage
|
||||||
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview
|
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview
|
||||||
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel
|
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel
|
||||||
|
import org.session.libsession.messaging.BlindedIdMapping
|
||||||
|
import org.session.libsession.messaging.open_groups.GroupMember
|
||||||
import org.session.libsession.utilities.Address
|
import org.session.libsession.utilities.Address
|
||||||
import org.session.libsession.utilities.GroupRecord
|
import org.session.libsession.utilities.GroupRecord
|
||||||
import org.session.libsession.utilities.recipients.Recipient
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
@ -54,13 +56,20 @@ interface StorageProtocol {
|
|||||||
fun setAuthToken(room: String, server: String, newValue: String)
|
fun setAuthToken(room: String, server: String, newValue: String)
|
||||||
fun removeAuthToken(room: String, server: String)
|
fun removeAuthToken(room: String, server: String)
|
||||||
|
|
||||||
|
// Servers
|
||||||
|
fun setServerCapabilities(server: String, capabilities: List<String>)
|
||||||
|
fun getServerCapabilities(server: String): List<String>
|
||||||
|
|
||||||
// Open Groups
|
// Open Groups
|
||||||
fun getAllV2OpenGroups(): Map<Long, OpenGroupV2>
|
fun getAllOpenGroups(): Map<Long, OpenGroup>
|
||||||
fun getV2OpenGroup(threadId: Long): OpenGroupV2?
|
fun updateOpenGroup(openGroup: OpenGroup)
|
||||||
|
fun getOpenGroup(threadId: Long): OpenGroup?
|
||||||
fun addOpenGroup(urlAsString: String)
|
fun addOpenGroup(urlAsString: String)
|
||||||
fun onOpenGroupAdded(urlAsString: String)
|
fun onOpenGroupAdded(urlAsString: String)
|
||||||
fun hasBackgroundGroupAddJob(groupJoinUrl: String): Boolean
|
fun hasBackgroundGroupAddJob(groupJoinUrl: String): Boolean
|
||||||
fun setOpenGroupServerMessageID(messageID: Long, serverID: Long, threadID: Long, isSms: Boolean)
|
fun setOpenGroupServerMessageID(messageID: Long, serverID: Long, threadID: Long, isSms: Boolean)
|
||||||
|
fun getOpenGroup(room: String, server: String): OpenGroup?
|
||||||
|
fun addGroupMember(member: GroupMember)
|
||||||
|
|
||||||
// Open Group Public Keys
|
// Open Group Public Keys
|
||||||
fun getOpenGroupPublicKey(server: String): String?
|
fun getOpenGroupPublicKey(server: String): String?
|
||||||
@ -167,4 +176,16 @@ interface StorageProtocol {
|
|||||||
fun setRecipientApprovedMe(recipient: Recipient, approvedMe: Boolean)
|
fun setRecipientApprovedMe(recipient: Recipient, approvedMe: Boolean)
|
||||||
fun insertCallMessage(senderPublicKey: String, callMessageType: CallMessageType, sentTimestamp: Long)
|
fun insertCallMessage(senderPublicKey: String, callMessageType: CallMessageType, sentTimestamp: Long)
|
||||||
fun conversationHasOutgoing(userPublicKey: String): Boolean
|
fun conversationHasOutgoing(userPublicKey: String): Boolean
|
||||||
|
|
||||||
|
// Last Inbox Message Id
|
||||||
|
fun getLastInboxMessageId(server: String): Long?
|
||||||
|
fun setLastInboxMessageId(server: String, messageId: Long)
|
||||||
|
fun removeLastInboxMessageId(server: String)
|
||||||
|
|
||||||
|
// Last Outbox Message Id
|
||||||
|
fun getLastOutboxMessageId(server: String): Long?
|
||||||
|
fun setLastOutboxMessageId(server: String, messageId: Long)
|
||||||
|
fun removeLastOutboxMessageId(server: String)
|
||||||
|
fun getOrCreateBlindedIdMapping(blindedId: String, server: String, serverPublicKey: String, fromOutbox: Boolean = false): BlindedIdMapping
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,8 @@
|
|||||||
|
package org.session.libsession.messaging
|
||||||
|
|
||||||
|
data class BlindedIdMapping(
|
||||||
|
val blindedId: String,
|
||||||
|
val sessionId: String?,
|
||||||
|
val serverUrl: String,
|
||||||
|
val serverId: String
|
||||||
|
)
|
@ -44,7 +44,7 @@ class Contact(val sessionID: String) {
|
|||||||
// In open groups, where it's more likely that multiple users have the same name,
|
// In open groups, where it's more likely that multiple users have the same name,
|
||||||
// we display a bit of the Session ID after a user's display name for added context.
|
// we display a bit of the Session ID after a user's display name for added context.
|
||||||
name?.let {
|
name?.let {
|
||||||
return "$name (...${sessionID.takeLast(8)})"
|
return "$name (${sessionID.take(4)}...${sessionID.takeLast(4)})"
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
@ -6,14 +6,13 @@ import okhttp3.Headers
|
|||||||
import okhttp3.HttpUrl
|
import okhttp3.HttpUrl
|
||||||
import okhttp3.MediaType
|
import okhttp3.MediaType
|
||||||
import okhttp3.RequestBody
|
import okhttp3.RequestBody
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
|
|
||||||
import org.session.libsession.snode.OnionRequestAPI
|
import org.session.libsession.snode.OnionRequestAPI
|
||||||
import org.session.libsignal.utilities.HTTP
|
|
||||||
import org.session.libsignal.utilities.Base64
|
import org.session.libsignal.utilities.Base64
|
||||||
|
import org.session.libsignal.utilities.HTTP
|
||||||
import org.session.libsignal.utilities.JsonUtil
|
import org.session.libsignal.utilities.JsonUtil
|
||||||
import org.session.libsignal.utilities.Log
|
import org.session.libsignal.utilities.Log
|
||||||
|
|
||||||
object FileServerAPIV2 {
|
object FileServerApi {
|
||||||
|
|
||||||
private const val serverPublicKey = "da21e1d886c6fbaea313f75298bd64aab03a97ce985b46bb2dad9f2089c8ee59"
|
private const val serverPublicKey = "da21e1d886c6fbaea313f75298bd64aab03a97ce985b46bb2dad9f2089c8ee59"
|
||||||
const val server = "http://filev2.getsession.org"
|
const val server = "http://filev2.getsession.org"
|
||||||
@ -52,8 +51,8 @@ object FileServerAPIV2 {
|
|||||||
return RequestBody.create(MediaType.get("application/json"), parametersAsJSON)
|
return RequestBody.create(MediaType.get("application/json"), parametersAsJSON)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun send(request: Request): Promise<Map<*, *>, Exception> {
|
private fun send(request: Request): Promise<ByteArray, Exception> {
|
||||||
val url = HttpUrl.parse(server) ?: return Promise.ofFail(OpenGroupAPIV2.Error.InvalidURL)
|
val url = HttpUrl.parse(server) ?: return Promise.ofFail(Error.InvalidURL)
|
||||||
val urlBuilder = HttpUrl.Builder()
|
val urlBuilder = HttpUrl.Builder()
|
||||||
.scheme(url.scheme())
|
.scheme(url.scheme())
|
||||||
.host(url.host())
|
.host(url.host())
|
||||||
@ -73,29 +72,37 @@ object FileServerAPIV2 {
|
|||||||
HTTP.Verb.POST -> requestBuilder.post(createBody(request.parameters)!!)
|
HTTP.Verb.POST -> requestBuilder.post(createBody(request.parameters)!!)
|
||||||
HTTP.Verb.DELETE -> requestBuilder.delete(createBody(request.parameters))
|
HTTP.Verb.DELETE -> requestBuilder.delete(createBody(request.parameters))
|
||||||
}
|
}
|
||||||
if (request.useOnionRouting) {
|
return if (request.useOnionRouting) {
|
||||||
return OnionRequestAPI.sendOnionRequest(requestBuilder.build(), server, serverPublicKey).fail { e ->
|
OnionRequestAPI.sendOnionRequest(requestBuilder.build(), server, serverPublicKey).map {
|
||||||
|
it.body ?: throw Error.ParsingFailed
|
||||||
|
}.fail { e ->
|
||||||
Log.e("Loki", "File server request failed.", e)
|
Log.e("Loki", "File server request failed.", e)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return Promise.ofFail(IllegalStateException("It's currently not allowed to send non onion routed requests."))
|
Promise.ofFail(IllegalStateException("It's currently not allowed to send non onion routed requests."))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun upload(file: ByteArray): Promise<Long, Exception> {
|
fun upload(file: ByteArray): Promise<Long, Exception> {
|
||||||
val base64EncodedFile = Base64.encodeBytes(file)
|
val base64EncodedFile = Base64.encodeBytes(file)
|
||||||
val parameters = mapOf( "file" to base64EncodedFile )
|
val parameters = mapOf( "file" to base64EncodedFile )
|
||||||
val request = Request(verb = HTTP.Verb.POST, endpoint = "files", parameters = parameters)
|
val request = Request(
|
||||||
return send(request).map { json ->
|
verb = HTTP.Verb.POST,
|
||||||
json["result"] as? Long ?: throw OpenGroupAPIV2.Error.ParsingFailed
|
endpoint = "file",
|
||||||
|
parameters = parameters,
|
||||||
|
headers = mapOf(
|
||||||
|
"Content-Disposition" to "attachment",
|
||||||
|
"Content-Type" to "application/octet-stream"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return send(request).map { response ->
|
||||||
|
val json = JsonUtil.fromJson(response, Map::class.java)
|
||||||
|
json["result"] as? Long ?: throw Error.ParsingFailed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun download(file: Long): Promise<ByteArray, Exception> {
|
fun download(file: String): Promise<ByteArray, Exception> {
|
||||||
val request = Request(verb = HTTP.Verb.GET, endpoint = "files/$file")
|
val request = Request(verb = HTTP.Verb.GET, endpoint = "file/$file")
|
||||||
return send(request).map { json ->
|
return send(request)
|
||||||
val base64EncodedFile = json["result"] as? String ?: throw Error.ParsingFailed
|
|
||||||
Base64.decode(base64EncodedFile) ?: throw Error.ParsingFailed
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -2,7 +2,7 @@ package org.session.libsession.messaging.jobs
|
|||||||
|
|
||||||
import okhttp3.HttpUrl
|
import okhttp3.HttpUrl
|
||||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
|
import org.session.libsession.messaging.open_groups.OpenGroupApi
|
||||||
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentId
|
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentId
|
||||||
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentState
|
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentState
|
||||||
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
|
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
|
||||||
@ -106,15 +106,15 @@ class AttachmentDownloadJob(val attachmentID: Long, val databaseMessageID: Long)
|
|||||||
}
|
}
|
||||||
messageDataProvider.setAttachmentState(AttachmentState.STARTED, attachment.attachmentId, this.databaseMessageID)
|
messageDataProvider.setAttachmentState(AttachmentState.STARTED, attachment.attachmentId, this.databaseMessageID)
|
||||||
tempFile = createTempFile()
|
tempFile = createTempFile()
|
||||||
val openGroupV2 = storage.getV2OpenGroup(threadID)
|
val openGroup = storage.getOpenGroup(threadID)
|
||||||
if (openGroupV2 == null) {
|
if (openGroup == null) {
|
||||||
Log.d("AttachmentDownloadJob", "downloading normal attachment")
|
Log.d("AttachmentDownloadJob", "downloading normal attachment")
|
||||||
DownloadUtilities.downloadFile(tempFile, attachment.url)
|
DownloadUtilities.downloadFile(tempFile, attachment.url)
|
||||||
} else {
|
} else {
|
||||||
Log.d("AttachmentDownloadJob", "downloading open group attachment")
|
Log.d("AttachmentDownloadJob", "downloading open group attachment")
|
||||||
val url = HttpUrl.parse(attachment.url)!!
|
val url = HttpUrl.parse(attachment.url)!!
|
||||||
val fileID = url.pathSegments().last()
|
val fileID = url.pathSegments().last()
|
||||||
OpenGroupAPIV2.download(fileID.toLong(), openGroupV2.room, openGroupV2.server).get().let {
|
OpenGroupApi.download(fileID, openGroup.room, openGroup.server).get().let {
|
||||||
tempFile.writeBytes(it)
|
tempFile.writeBytes(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,9 +6,10 @@ import com.esotericsoftware.kryo.io.Output
|
|||||||
import nl.komponents.kovenant.Promise
|
import nl.komponents.kovenant.Promise
|
||||||
import okio.Buffer
|
import okio.Buffer
|
||||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||||
import org.session.libsession.messaging.file_server.FileServerAPIV2
|
import org.session.libsession.messaging.file_server.FileServerApi
|
||||||
|
import org.session.libsession.messaging.messages.Destination
|
||||||
import org.session.libsession.messaging.messages.Message
|
import org.session.libsession.messaging.messages.Message
|
||||||
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.MessageSender
|
||||||
import org.session.libsession.messaging.utilities.Data
|
import org.session.libsession.messaging.utilities.Data
|
||||||
import org.session.libsession.utilities.DecodedAudio
|
import org.session.libsession.utilities.DecodedAudio
|
||||||
@ -50,15 +51,15 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess
|
|||||||
val messageDataProvider = MessagingModuleConfiguration.shared.messageDataProvider
|
val messageDataProvider = MessagingModuleConfiguration.shared.messageDataProvider
|
||||||
val attachment = messageDataProvider.getScaledSignalAttachmentStream(attachmentID)
|
val attachment = messageDataProvider.getScaledSignalAttachmentStream(attachmentID)
|
||||||
?: return handleFailure(Error.NoAttachment)
|
?: return handleFailure(Error.NoAttachment)
|
||||||
val v2OpenGroup = storage.getV2OpenGroup(threadID.toLong())
|
val openGroup = storage.getOpenGroup(threadID.toLong())
|
||||||
if (v2OpenGroup != null) {
|
if (openGroup != null) {
|
||||||
val keyAndResult = upload(attachment, v2OpenGroup.server, false) {
|
val keyAndResult = upload(attachment, openGroup.server, false) {
|
||||||
OpenGroupAPIV2.upload(it, v2OpenGroup.room, v2OpenGroup.server)
|
OpenGroupApi.upload(it, openGroup.room, openGroup.server)
|
||||||
}
|
}
|
||||||
handleSuccess(attachment, keyAndResult.first, keyAndResult.second)
|
handleSuccess(attachment, keyAndResult.first, keyAndResult.second)
|
||||||
} else {
|
} else {
|
||||||
val keyAndResult = upload(attachment, FileServerAPIV2.server, true) {
|
val keyAndResult = upload(attachment, FileServerApi.server, true) {
|
||||||
FileServerAPIV2.upload(it)
|
FileServerApi.upload(it)
|
||||||
}
|
}
|
||||||
handleSuccess(attachment, keyAndResult.first, keyAndResult.second)
|
handleSuccess(attachment, keyAndResult.first, keyAndResult.second)
|
||||||
}
|
}
|
||||||
@ -100,7 +101,7 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess
|
|||||||
val id = upload(data).get()
|
val id = upload(data).get()
|
||||||
val digest = drb.transmittedDigest
|
val digest = drb.transmittedDigest
|
||||||
// Return
|
// Return
|
||||||
return Pair(key, UploadResult(id, "${server}/files/$id", digest))
|
return Pair(key, UploadResult(id, "${server}/file/$id", digest))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleSuccess(attachment: SignalServiceAttachmentStream, attachmentKey: ByteArray, uploadResult: UploadResult) {
|
private fun handleSuccess(attachment: SignalServiceAttachmentStream, attachmentKey: ByteArray, uploadResult: UploadResult) {
|
||||||
@ -122,7 +123,25 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess
|
|||||||
Log.e("Loki", "Couldn't process audio attachment", e)
|
Log.e("Loki", "Couldn't process audio attachment", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
MessagingModuleConfiguration.shared.storage.resumeMessageSendJobIfNeeded(messageSendJobID)
|
val storage = MessagingModuleConfiguration.shared.storage
|
||||||
|
storage.getMessageSendJob(messageSendJobID)?.let {
|
||||||
|
val destination = it.destination as? Destination.OpenGroup ?: return@let
|
||||||
|
val updatedJob = MessageSendJob(
|
||||||
|
message = it.message,
|
||||||
|
destination = Destination.OpenGroup(
|
||||||
|
destination.roomToken,
|
||||||
|
destination.server,
|
||||||
|
destination.whisperTo,
|
||||||
|
destination.whisperMods,
|
||||||
|
destination.fileIds + uploadResult.id.toString()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
updatedJob.id = it.id
|
||||||
|
updatedJob.delegate = it.delegate
|
||||||
|
updatedJob.failureCount = it.failureCount
|
||||||
|
storage.persistJob(updatedJob)
|
||||||
|
}
|
||||||
|
storage.resumeMessageSendJobIfNeeded(messageSendJobID)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handlePermanentFailure(e: Exception) {
|
private fun handlePermanentFailure(e: Exception) {
|
||||||
|
@ -2,8 +2,8 @@ package org.session.libsession.messaging.jobs
|
|||||||
|
|
||||||
import okhttp3.HttpUrl
|
import okhttp3.HttpUrl
|
||||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
|
import org.session.libsession.messaging.open_groups.OpenGroup
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroupV2
|
import org.session.libsession.messaging.open_groups.OpenGroupApi
|
||||||
import org.session.libsession.messaging.utilities.Data
|
import org.session.libsession.messaging.utilities.Data
|
||||||
import org.session.libsession.utilities.GroupUtil
|
import org.session.libsession.utilities.GroupUtil
|
||||||
import org.session.libsignal.utilities.Log
|
import org.session.libsignal.utilities.Log
|
||||||
@ -23,7 +23,7 @@ class BackgroundGroupAddJob(val joinUrl: String): Job {
|
|||||||
|
|
||||||
val openGroupId: String? get() {
|
val openGroupId: String? get() {
|
||||||
val url = HttpUrl.parse(joinUrl) ?: return null
|
val url = HttpUrl.parse(joinUrl) ?: return null
|
||||||
val server = OpenGroupV2.getServer(joinUrl)?.toString()?.removeSuffix("/") ?: return null
|
val server = OpenGroup.getServer(joinUrl)?.toString()?.removeSuffix("/") ?: return null
|
||||||
val room = url.pathSegments().firstOrNull() ?: return null
|
val room = url.pathSegments().firstOrNull() ?: return null
|
||||||
return "$server.$room"
|
return "$server.$room"
|
||||||
}
|
}
|
||||||
@ -31,25 +31,29 @@ class BackgroundGroupAddJob(val joinUrl: String): Job {
|
|||||||
override fun execute() {
|
override fun execute() {
|
||||||
try {
|
try {
|
||||||
val storage = MessagingModuleConfiguration.shared.storage
|
val storage = MessagingModuleConfiguration.shared.storage
|
||||||
val allV2OpenGroups = storage.getAllV2OpenGroups().map { it.value.joinURL }
|
val allOpenGroups = storage.getAllOpenGroups().map { it.value.joinURL }
|
||||||
if (allV2OpenGroups.contains(joinUrl)) {
|
if (allOpenGroups.contains(joinUrl)) {
|
||||||
Log.e("OpenGroupDispatcher", "Failed to add group because",DuplicateGroupException())
|
Log.e("OpenGroupDispatcher", "Failed to add group because", DuplicateGroupException())
|
||||||
delegate?.handleJobFailed(this, DuplicateGroupException())
|
delegate?.handleJobFailed(this, DuplicateGroupException())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// get image
|
// get image
|
||||||
val url = HttpUrl.parse(joinUrl) ?: throw Exception("Group joinUrl isn't valid")
|
val url = HttpUrl.parse(joinUrl) ?: throw Exception("Group joinUrl isn't valid")
|
||||||
val server = OpenGroupV2.getServer(joinUrl)
|
val server = OpenGroup.getServer(joinUrl)
|
||||||
val serverString = server.toString().removeSuffix("/")
|
val serverString = server.toString().removeSuffix("/")
|
||||||
val publicKey = url.queryParameter("public_key") ?: throw Exception("Group public key isn't valid")
|
val publicKey = url.queryParameter("public_key") ?: throw Exception("Group public key isn't valid")
|
||||||
val room = url.pathSegments().firstOrNull() ?: throw Exception("Group room isn't valid")
|
val room = url.pathSegments().firstOrNull() ?: throw Exception("Group room isn't valid")
|
||||||
storage.setOpenGroupPublicKey(serverString,publicKey)
|
storage.setOpenGroupPublicKey(serverString, publicKey)
|
||||||
val bytes = OpenGroupAPIV2.downloadOpenGroupProfilePicture(url.pathSegments().firstOrNull()!!, serverString).get()
|
|
||||||
val groupId = GroupUtil.getEncodedOpenGroupID("$server.$room".toByteArray())
|
|
||||||
// get info and auth token
|
// get info and auth token
|
||||||
storage.addOpenGroup(joinUrl)
|
storage.addOpenGroup(joinUrl)
|
||||||
|
val info = OpenGroupApi.getRoomInfo(room, serverString).get()
|
||||||
|
val imageId = info.imageId
|
||||||
|
if (imageId != null) {
|
||||||
|
val bytes = OpenGroupApi.downloadOpenGroupProfilePicture(serverString, room, imageId).get()
|
||||||
|
val groupId = GroupUtil.getEncodedOpenGroupID("$server.$room".toByteArray())
|
||||||
storage.updateProfilePicture(groupId, bytes)
|
storage.updateProfilePicture(groupId, bytes)
|
||||||
storage.updateTimestampUpdated(groupId, System.currentTimeMillis())
|
storage.updateTimestampUpdated(groupId, System.currentTimeMillis())
|
||||||
|
}
|
||||||
storage.onOpenGroupAdded(joinUrl)
|
storage.onOpenGroupAdded(joinUrl)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("OpenGroupDispatcher", "Failed to add group because",e)
|
Log.e("OpenGroupDispatcher", "Failed to add group because",e)
|
||||||
|
@ -17,8 +17,11 @@ import org.session.libsession.messaging.sending_receiving.MessageReceiver
|
|||||||
import org.session.libsession.messaging.sending_receiving.handle
|
import org.session.libsession.messaging.sending_receiving.handle
|
||||||
import org.session.libsession.messaging.sending_receiving.handleVisibleMessage
|
import org.session.libsession.messaging.sending_receiving.handleVisibleMessage
|
||||||
import org.session.libsession.messaging.utilities.Data
|
import org.session.libsession.messaging.utilities.Data
|
||||||
|
import org.session.libsession.messaging.utilities.SessionId
|
||||||
|
import org.session.libsession.messaging.utilities.SodiumUtilities
|
||||||
import org.session.libsession.utilities.SSKEnvironment
|
import org.session.libsession.utilities.SSKEnvironment
|
||||||
import org.session.libsignal.protos.UtilProtos
|
import org.session.libsignal.protos.UtilProtos
|
||||||
|
import org.session.libsignal.utilities.IdPrefix
|
||||||
import org.session.libsignal.utilities.Log
|
import org.session.libsignal.utilities.Log
|
||||||
|
|
||||||
data class MessageReceiveParameters(
|
data class MessageReceiveParameters(
|
||||||
@ -72,12 +75,13 @@ class BatchMessageReceiveJob(
|
|||||||
val storage = MessagingModuleConfiguration.shared.storage
|
val storage = MessagingModuleConfiguration.shared.storage
|
||||||
val context = MessagingModuleConfiguration.shared.context
|
val context = MessagingModuleConfiguration.shared.context
|
||||||
val localUserPublicKey = storage.getUserPublicKey()
|
val localUserPublicKey = storage.getUserPublicKey()
|
||||||
|
val serverPublicKey = openGroupID?.let { storage.getOpenGroupPublicKey(it.split(".").dropLast(1).joinToString(".")) }
|
||||||
|
|
||||||
// parse and collect IDs
|
// parse and collect IDs
|
||||||
messages.forEach { messageParameters ->
|
messages.forEach { messageParameters ->
|
||||||
val (data, serverHash, openGroupMessageServerID) = messageParameters
|
val (data, serverHash, openGroupMessageServerID) = messageParameters
|
||||||
try {
|
try {
|
||||||
val (message, proto) = MessageReceiver.parse(data, openGroupMessageServerID)
|
val (message, proto) = MessageReceiver.parse(data, openGroupMessageServerID, openGroupPublicKey = serverPublicKey)
|
||||||
message.serverHash = serverHash
|
message.serverHash = serverHash
|
||||||
val threadID = getThreadId(message, storage)
|
val threadID = getThreadId(message, storage)
|
||||||
val parsedParams = ParsedMessage(messageParameters, message, proto)
|
val parsedParams = ParsedMessage(messageParameters, message, proto)
|
||||||
@ -111,7 +115,9 @@ class BatchMessageReceiveJob(
|
|||||||
runProfileUpdate = true
|
runProfileUpdate = true
|
||||||
)
|
)
|
||||||
if (messageId != null) {
|
if (messageId != null) {
|
||||||
messageIds += messageId to (message.sender == localUserPublicKey)
|
val isUserBlindedSender = message.sender == serverPublicKey?.let { SodiumUtilities.blindedKeyPair(it, MessagingModuleConfiguration.shared.getUserED25519KeyPair()!!) }?.let { SessionId(
|
||||||
|
IdPrefix.BLINDED, it.publicKey.asBytes).hexString }
|
||||||
|
messageIds += messageId to (message.sender == localUserPublicKey || isUserBlindedSender)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
MessageReceiver.handle(message, proto, openGroupID)
|
MessageReceiver.handle(message, proto, openGroupID)
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
package org.session.libsession.messaging.jobs
|
package org.session.libsession.messaging.jobs
|
||||||
|
|
||||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
|
import org.session.libsession.messaging.open_groups.OpenGroupApi
|
||||||
import org.session.libsession.messaging.utilities.Data
|
import org.session.libsession.messaging.utilities.Data
|
||||||
import org.session.libsession.utilities.GroupUtil
|
import org.session.libsession.utilities.GroupUtil
|
||||||
|
|
||||||
@ -15,8 +15,9 @@ class GroupAvatarDownloadJob(val room: String, val server: String) : Job {
|
|||||||
override fun execute() {
|
override fun execute() {
|
||||||
val storage = MessagingModuleConfiguration.shared.storage
|
val storage = MessagingModuleConfiguration.shared.storage
|
||||||
try {
|
try {
|
||||||
val info = OpenGroupAPIV2.getInfo(room, server).get()
|
val info = OpenGroupApi.getRoomInfo(room, server).get()
|
||||||
val bytes = OpenGroupAPIV2.downloadOpenGroupProfilePicture(info.id, server).get()
|
val imageId = info.imageId ?: return
|
||||||
|
val bytes = OpenGroupApi.downloadOpenGroupProfilePicture(server, info.token, imageId).get()
|
||||||
val groupId = GroupUtil.getEncodedOpenGroupID("$server.$room".toByteArray())
|
val groupId = GroupUtil.getEncodedOpenGroupID("$server.$room".toByteArray())
|
||||||
storage.updateProfilePicture(groupId, bytes)
|
storage.updateProfilePicture(groupId, bytes)
|
||||||
storage.updateTimestampUpdated(groupId, System.currentTimeMillis())
|
storage.updateTimestampUpdated(groupId, System.currentTimeMillis())
|
||||||
|
@ -2,6 +2,7 @@ package org.session.libsession.messaging.jobs
|
|||||||
|
|
||||||
import nl.komponents.kovenant.Promise
|
import nl.komponents.kovenant.Promise
|
||||||
import nl.komponents.kovenant.deferred
|
import nl.komponents.kovenant.deferred
|
||||||
|
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||||
import org.session.libsession.messaging.sending_receiving.MessageReceiver
|
import org.session.libsession.messaging.sending_receiving.MessageReceiver
|
||||||
import org.session.libsession.messaging.sending_receiving.handle
|
import org.session.libsession.messaging.sending_receiving.handle
|
||||||
import org.session.libsession.messaging.utilities.Data
|
import org.session.libsession.messaging.utilities.Data
|
||||||
@ -32,7 +33,10 @@ class MessageReceiveJob(val data: ByteArray, val serverHash: String? = null, val
|
|||||||
val deferred = deferred<Unit, Exception>()
|
val deferred = deferred<Unit, Exception>()
|
||||||
try {
|
try {
|
||||||
val isRetry: Boolean = failureCount != 0
|
val isRetry: Boolean = failureCount != 0
|
||||||
val (message, proto) = MessageReceiver.parse(this.data, this.openGroupMessageServerID)
|
val serverPublicKey = openGroupID?.let {
|
||||||
|
MessagingModuleConfiguration.shared.storage.getOpenGroupPublicKey(it.split(".").dropLast(1).joinToString("."))
|
||||||
|
}
|
||||||
|
val (message, proto) = MessageReceiver.parse(this.data, this.openGroupMessageServerID, openGroupPublicKey = serverPublicKey)
|
||||||
message.serverHash = serverHash
|
message.serverHash = serverHash
|
||||||
MessageReceiver.handle(message, proto, this.openGroupID)
|
MessageReceiver.handle(message, proto, this.openGroupID)
|
||||||
this.handleSuccess()
|
this.handleSuccess()
|
||||||
|
@ -3,8 +3,6 @@ package org.session.libsession.messaging.jobs
|
|||||||
import com.esotericsoftware.kryo.Kryo
|
import com.esotericsoftware.kryo.Kryo
|
||||||
import com.esotericsoftware.kryo.io.Input
|
import com.esotericsoftware.kryo.io.Input
|
||||||
import com.esotericsoftware.kryo.io.Output
|
import com.esotericsoftware.kryo.io.Output
|
||||||
import nl.komponents.kovenant.FailedException
|
|
||||||
import nl.komponents.kovenant.Promise
|
|
||||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||||
import org.session.libsession.messaging.jobs.Job.Companion.MAX_BUFFER_SIZE
|
import org.session.libsession.messaging.jobs.Job.Companion.MAX_BUFFER_SIZE
|
||||||
import org.session.libsession.messaging.messages.Destination
|
import org.session.libsession.messaging.messages.Destination
|
||||||
|
@ -13,6 +13,7 @@ import org.session.libsession.messaging.sending_receiving.notifications.PushNoti
|
|||||||
import org.session.libsession.messaging.utilities.Data
|
import org.session.libsession.messaging.utilities.Data
|
||||||
import org.session.libsession.snode.SnodeMessage
|
import org.session.libsession.snode.SnodeMessage
|
||||||
import org.session.libsession.snode.OnionRequestAPI
|
import org.session.libsession.snode.OnionRequestAPI
|
||||||
|
import org.session.libsession.snode.Version
|
||||||
|
|
||||||
import org.session.libsignal.utilities.Log
|
import org.session.libsignal.utilities.Log
|
||||||
import org.session.libsignal.utilities.JsonUtil
|
import org.session.libsignal.utilities.JsonUtil
|
||||||
@ -38,10 +39,10 @@ class NotifyPNServerJob(val message: SnodeMessage) : Job {
|
|||||||
val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters))
|
val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters))
|
||||||
val request = Request.Builder().url(url).post(body)
|
val request = Request.Builder().url(url).post(body)
|
||||||
retryIfNeeded(4) {
|
retryIfNeeded(4) {
|
||||||
OnionRequestAPI.sendOnionRequest(request.build(), server, PushNotificationAPI.serverPublicKey, "/loki/v2/lsrpc").map { json ->
|
OnionRequestAPI.sendOnionRequest(request.build(), server, PushNotificationAPI.serverPublicKey, Version.V2).map { response ->
|
||||||
val code = json["code"] as? Int
|
val code = response.info["code"] as? Int
|
||||||
if (code == null || code == 0) {
|
if (code == null || code == 0) {
|
||||||
Log.d("Loki", "Couldn't notify PN server due to error: ${json["message"] as? String ?: "null"}.")
|
Log.d("Loki", "Couldn't notify PN server due to error: ${response.info["message"] as? String ?: "null"}.")
|
||||||
}
|
}
|
||||||
}.fail { exception ->
|
}.fail { exception ->
|
||||||
Log.d("Loki", "Couldn't notify PN server due to error: $exception.")
|
Log.d("Loki", "Couldn't notify PN server due to error: $exception.")
|
||||||
|
@ -23,11 +23,13 @@ class OpenGroupDeleteJob(private val messageServerIds: LongArray, private val th
|
|||||||
val dataProvider = MessagingModuleConfiguration.shared.messageDataProvider
|
val dataProvider = MessagingModuleConfiguration.shared.messageDataProvider
|
||||||
val numberToDelete = messageServerIds.size
|
val numberToDelete = messageServerIds.size
|
||||||
Log.d(TAG, "Deleting $numberToDelete messages")
|
Log.d(TAG, "Deleting $numberToDelete messages")
|
||||||
|
var numberDeleted = 0
|
||||||
messageServerIds.forEach { serverId ->
|
messageServerIds.forEach { serverId ->
|
||||||
val (messageId, isSms) = dataProvider.getMessageID(serverId, threadId) ?: return@forEach
|
val (messageId, isSms) = dataProvider.getMessageID(serverId, threadId) ?: return@forEach
|
||||||
dataProvider.deleteMessage(messageId, isSms)
|
dataProvider.deleteMessage(messageId, isSms)
|
||||||
|
numberDeleted++
|
||||||
}
|
}
|
||||||
Log.d(TAG, "Deleted $numberToDelete messages successfully")
|
Log.d(TAG, "Deleted $numberDeleted messages successfully")
|
||||||
delegate?.handleJobSucceeded(this)
|
delegate?.handleJobSucceeded(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
package org.session.libsession.messaging.messages
|
package org.session.libsession.messaging.messages
|
||||||
|
|
||||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroupV2
|
|
||||||
import org.session.libsession.utilities.Address
|
import org.session.libsession.utilities.Address
|
||||||
import org.session.libsession.utilities.GroupUtil
|
import org.session.libsession.utilities.GroupUtil
|
||||||
import org.session.libsignal.utilities.toHexString
|
import org.session.libsignal.utilities.toHexString
|
||||||
@ -14,13 +13,27 @@ sealed class Destination {
|
|||||||
class ClosedGroup(var groupPublicKey: String) : Destination() {
|
class ClosedGroup(var groupPublicKey: String) : Destination() {
|
||||||
internal constructor(): this("")
|
internal constructor(): this("")
|
||||||
}
|
}
|
||||||
class OpenGroupV2(var room: String, var server: String) : Destination() {
|
class LegacyOpenGroup(var roomToken: String, var server: String) : Destination() {
|
||||||
internal constructor(): this("", "")
|
internal constructor(): this("", "")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class OpenGroup(
|
||||||
|
var roomToken: String = "",
|
||||||
|
var server: String = "",
|
||||||
|
var whisperTo: List<String> = emptyList(),
|
||||||
|
var whisperMods: Boolean = false,
|
||||||
|
var fileIds: List<String> = emptyList()
|
||||||
|
) : Destination()
|
||||||
|
|
||||||
|
class OpenGroupInbox(
|
||||||
|
var server: String,
|
||||||
|
var serverPublicKey: String,
|
||||||
|
var blindedPublicKey: String
|
||||||
|
) : Destination()
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
fun from(address: Address): Destination {
|
fun from(address: Address, fileIds: List<String> = emptyList()): Destination {
|
||||||
return when {
|
return when {
|
||||||
address.isContact -> {
|
address.isContact -> {
|
||||||
Contact(address.contactIdentifier())
|
Contact(address.contactIdentifier())
|
||||||
@ -33,11 +46,17 @@ sealed class Destination {
|
|||||||
address.isOpenGroup -> {
|
address.isOpenGroup -> {
|
||||||
val storage = MessagingModuleConfiguration.shared.storage
|
val storage = MessagingModuleConfiguration.shared.storage
|
||||||
val threadID = storage.getThreadId(address)!!
|
val threadID = storage.getThreadId(address)!!
|
||||||
when (val openGroup = storage.getV2OpenGroup(threadID)) {
|
storage.getOpenGroup(threadID)?.let {
|
||||||
is org.session.libsession.messaging.open_groups.OpenGroupV2
|
OpenGroup(roomToken = it.room, server = it.server, fileIds = fileIds)
|
||||||
-> Destination.OpenGroupV2(openGroup.room, openGroup.server)
|
} ?: throw Exception("Missing open group for thread with ID: $threadID.")
|
||||||
else -> throw Exception("Missing open group for thread with ID: $threadID.")
|
|
||||||
}
|
}
|
||||||
|
address.isOpenGroupInbox -> {
|
||||||
|
val groupInboxId = GroupUtil.getDecodedGroupID(address.serialize()).split("!")
|
||||||
|
OpenGroupInbox(
|
||||||
|
groupInboxId.dropLast(2).joinToString("!"),
|
||||||
|
groupInboxId.dropLast(1).last(),
|
||||||
|
groupInboxId.last()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
throw Exception("TODO: Handle legacy closed groups.")
|
throw Exception("TODO: Handle legacy closed groups.")
|
||||||
|
@ -1,16 +1,12 @@
|
|||||||
package org.session.libsession.messaging.messages.control
|
package org.session.libsession.messaging.messages.control
|
||||||
|
|
||||||
import com.google.protobuf.ByteString
|
import com.google.protobuf.ByteString
|
||||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
|
||||||
import org.session.libsession.utilities.Address
|
|
||||||
import org.session.libsession.utilities.recipients.Recipient
|
|
||||||
import org.session.libsession.utilities.GroupUtil
|
|
||||||
import org.session.libsignal.crypto.ecc.DjbECPrivateKey
|
import org.session.libsignal.crypto.ecc.DjbECPrivateKey
|
||||||
import org.session.libsignal.crypto.ecc.DjbECPublicKey
|
import org.session.libsignal.crypto.ecc.DjbECPublicKey
|
||||||
import org.session.libsignal.crypto.ecc.ECKeyPair
|
import org.session.libsignal.crypto.ecc.ECKeyPair
|
||||||
import org.session.libsignal.protos.SignalServiceProtos
|
import org.session.libsignal.protos.SignalServiceProtos
|
||||||
import org.session.libsignal.protos.SignalServiceProtos.DataMessage
|
import org.session.libsignal.protos.SignalServiceProtos.DataMessage
|
||||||
import org.session.libsignal.utilities.removing05PrefixIfNeeded
|
import org.session.libsignal.utilities.removingIdPrefixIfNeeded
|
||||||
import org.session.libsignal.utilities.toHexString
|
import org.session.libsignal.utilities.toHexString
|
||||||
import org.session.libsignal.utilities.Hex
|
import org.session.libsignal.utilities.Hex
|
||||||
import org.session.libsignal.utilities.Log
|
import org.session.libsignal.utilities.Log
|
||||||
@ -140,7 +136,7 @@ class ClosedGroupControlMessage() : ControlMessage() {
|
|||||||
closedGroupControlMessage.publicKey = kind.publicKey
|
closedGroupControlMessage.publicKey = kind.publicKey
|
||||||
closedGroupControlMessage.name = kind.name
|
closedGroupControlMessage.name = kind.name
|
||||||
val encryptionKeyPair = SignalServiceProtos.KeyPair.newBuilder()
|
val encryptionKeyPair = SignalServiceProtos.KeyPair.newBuilder()
|
||||||
encryptionKeyPair.publicKey = ByteString.copyFrom(kind.encryptionKeyPair!!.publicKey.serialize().removing05PrefixIfNeeded())
|
encryptionKeyPair.publicKey = ByteString.copyFrom(kind.encryptionKeyPair!!.publicKey.serialize().removingIdPrefixIfNeeded())
|
||||||
encryptionKeyPair.privateKey = ByteString.copyFrom(kind.encryptionKeyPair!!.privateKey.serialize())
|
encryptionKeyPair.privateKey = ByteString.copyFrom(kind.encryptionKeyPair!!.privateKey.serialize())
|
||||||
closedGroupControlMessage.encryptionKeyPair = encryptionKeyPair.build()
|
closedGroupControlMessage.encryptionKeyPair = encryptionKeyPair.build()
|
||||||
closedGroupControlMessage.addAllMembers(kind.members)
|
closedGroupControlMessage.addAllMembers(kind.members)
|
||||||
|
@ -11,7 +11,7 @@ import org.session.libsignal.crypto.ecc.DjbECPrivateKey
|
|||||||
import org.session.libsignal.crypto.ecc.DjbECPublicKey
|
import org.session.libsignal.crypto.ecc.DjbECPublicKey
|
||||||
import org.session.libsignal.crypto.ecc.ECKeyPair
|
import org.session.libsignal.crypto.ecc.ECKeyPair
|
||||||
import org.session.libsignal.protos.SignalServiceProtos
|
import org.session.libsignal.protos.SignalServiceProtos
|
||||||
import org.session.libsignal.utilities.removing05PrefixIfNeeded
|
import org.session.libsignal.utilities.removingIdPrefixIfNeeded
|
||||||
import org.session.libsignal.utilities.toHexString
|
import org.session.libsignal.utilities.toHexString
|
||||||
import org.session.libsignal.utilities.Hex
|
import org.session.libsignal.utilities.Hex
|
||||||
|
|
||||||
@ -36,7 +36,7 @@ class ConfigurationMessage(var closedGroups: List<ClosedGroup>, var openGroups:
|
|||||||
val publicKey = proto.publicKey.toByteArray().toHexString()
|
val publicKey = proto.publicKey.toByteArray().toHexString()
|
||||||
val name = proto.name
|
val name = proto.name
|
||||||
val encryptionKeyPairAsProto = proto.encryptionKeyPair
|
val encryptionKeyPairAsProto = proto.encryptionKeyPair
|
||||||
val encryptionKeyPair = ECKeyPair(DjbECPublicKey(encryptionKeyPairAsProto.publicKey.toByteArray().removing05PrefixIfNeeded()),
|
val encryptionKeyPair = ECKeyPair(DjbECPublicKey(encryptionKeyPairAsProto.publicKey.toByteArray().removingIdPrefixIfNeeded()),
|
||||||
DjbECPrivateKey(encryptionKeyPairAsProto.privateKey.toByteArray()))
|
DjbECPrivateKey(encryptionKeyPairAsProto.privateKey.toByteArray()))
|
||||||
val members = proto.membersList.map { it.toByteArray().toHexString() }
|
val members = proto.membersList.map { it.toByteArray().toHexString() }
|
||||||
val admins = proto.adminsList.map { it.toByteArray().toHexString() }
|
val admins = proto.adminsList.map { it.toByteArray().toHexString() }
|
||||||
@ -50,7 +50,7 @@ class ConfigurationMessage(var closedGroups: List<ClosedGroup>, var openGroups:
|
|||||||
result.publicKey = ByteString.copyFrom(Hex.fromStringCondensed(publicKey))
|
result.publicKey = ByteString.copyFrom(Hex.fromStringCondensed(publicKey))
|
||||||
result.name = name
|
result.name = name
|
||||||
val encryptionKeyPairAsProto = SignalServiceProtos.KeyPair.newBuilder()
|
val encryptionKeyPairAsProto = SignalServiceProtos.KeyPair.newBuilder()
|
||||||
encryptionKeyPairAsProto.publicKey = ByteString.copyFrom(encryptionKeyPair!!.publicKey.serialize().removing05PrefixIfNeeded())
|
encryptionKeyPairAsProto.publicKey = ByteString.copyFrom(encryptionKeyPair!!.publicKey.serialize().removingIdPrefixIfNeeded())
|
||||||
encryptionKeyPairAsProto.privateKey = ByteString.copyFrom(encryptionKeyPair!!.privateKey.serialize())
|
encryptionKeyPairAsProto.privateKey = ByteString.copyFrom(encryptionKeyPair!!.privateKey.serialize())
|
||||||
result.encryptionKeyPair = encryptionKeyPairAsProto.build()
|
result.encryptionKeyPair = encryptionKeyPairAsProto.build()
|
||||||
result.addAllMembers(members.map { ByteString.copyFrom(Hex.fromStringCondensed(it)) })
|
result.addAllMembers(members.map { ByteString.copyFrom(Hex.fromStringCondensed(it)) })
|
||||||
@ -134,8 +134,8 @@ class ConfigurationMessage(var closedGroups: List<ClosedGroup>, var openGroups:
|
|||||||
}
|
}
|
||||||
if (group.isOpenGroup) {
|
if (group.isOpenGroup) {
|
||||||
val threadID = storage.getThreadId(group.encodedId) ?: continue
|
val threadID = storage.getThreadId(group.encodedId) ?: continue
|
||||||
val openGroupV2 = storage.getV2OpenGroup(threadID)
|
val openGroup = storage.getOpenGroup(threadID)
|
||||||
val shareUrl = openGroupV2?.joinURL ?: continue
|
val shareUrl = openGroup?.joinURL ?: continue
|
||||||
openGroups.add(shareUrl)
|
openGroups.add(shareUrl)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,72 @@
|
|||||||
|
package org.session.libsession.messaging.open_groups
|
||||||
|
|
||||||
|
sealed class Endpoint(val value: String) {
|
||||||
|
|
||||||
|
object Onion : Endpoint("oxen/v4/lsrpc")
|
||||||
|
object Batch : Endpoint("batch")
|
||||||
|
object Sequence : Endpoint("sequence")
|
||||||
|
object Capabilities : Endpoint("capabilities")
|
||||||
|
|
||||||
|
// Rooms
|
||||||
|
|
||||||
|
object Rooms : Endpoint("rooms")
|
||||||
|
data class Room(val roomToken: String) : Endpoint("room/$roomToken")
|
||||||
|
data class RoomPollInfo(val roomToken: String, val infoUpdated: Int) :
|
||||||
|
Endpoint("room/$roomToken/pollInfo/$infoUpdated")
|
||||||
|
|
||||||
|
// Messages
|
||||||
|
|
||||||
|
data class RoomMessage(val roomToken: String) : Endpoint("room/$roomToken/message")
|
||||||
|
|
||||||
|
data class RoomMessageIndividual(val roomToken: String, val messageId: Long) :
|
||||||
|
Endpoint("room/$roomToken/message/$messageId")
|
||||||
|
|
||||||
|
data class RoomMessagesRecent(val roomToken: String) :
|
||||||
|
Endpoint("room/$roomToken/messages/recent")
|
||||||
|
|
||||||
|
data class RoomMessagesBefore(val roomToken: String, val messageId: Long) :
|
||||||
|
Endpoint("room/$roomToken/messages/before/$messageId")
|
||||||
|
|
||||||
|
data class RoomMessagesSince(val roomToken: String, val seqNo: Long) :
|
||||||
|
Endpoint("room/$roomToken/messages/since/$seqNo")
|
||||||
|
|
||||||
|
data class RoomDeleteMessages(val roomToken: String, val sessionId: String) :
|
||||||
|
Endpoint("room/$roomToken/all/$sessionId")
|
||||||
|
|
||||||
|
// Pinning
|
||||||
|
|
||||||
|
data class RoomPinMessage(val roomToken: String, val messageId: Long) :
|
||||||
|
Endpoint("room/$roomToken/pin/$messageId")
|
||||||
|
|
||||||
|
data class RoomUnpinMessage(val roomToken: String, val messageId: Long) :
|
||||||
|
Endpoint("room/$roomToken/unpin/$messageId")
|
||||||
|
|
||||||
|
data class RoomUnpinAll(val roomToken: String) : Endpoint("room/$roomToken/unpin/all")
|
||||||
|
|
||||||
|
// Files
|
||||||
|
|
||||||
|
object File: Endpoint("file")
|
||||||
|
data class FileIndividual(val fileId: Long): Endpoint("file/$fileId")
|
||||||
|
|
||||||
|
data class RoomFile(val roomToken: String) : Endpoint("room/$roomToken/file")
|
||||||
|
data class RoomFileIndividual(
|
||||||
|
val roomToken: String,
|
||||||
|
val fileId: String
|
||||||
|
) : Endpoint("room/$roomToken/file/$fileId")
|
||||||
|
|
||||||
|
// Inbox/Outbox (Message Requests)
|
||||||
|
|
||||||
|
object Inbox : Endpoint("inbox")
|
||||||
|
data class InboxSince(val id: Long) : Endpoint("inbox/since/$id")
|
||||||
|
data class InboxFor(val sessionId: String) : Endpoint("inbox/$sessionId")
|
||||||
|
|
||||||
|
object Outbox : Endpoint("outbox")
|
||||||
|
data class OutboxSince(val id: Long) : Endpoint("outbox/since/$id")
|
||||||
|
|
||||||
|
// Users
|
||||||
|
|
||||||
|
data class UserBan(val sessionId: String) : Endpoint("user/$sessionId/ban")
|
||||||
|
data class UserUnban(val sessionId: String) : Endpoint("user/$sessionId/unban")
|
||||||
|
data class UserModerator(val sessionId: String) : Endpoint("user/$sessionId/moderator")
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
package org.session.libsession.messaging.open_groups
|
||||||
|
|
||||||
|
data class GroupMember(
|
||||||
|
val groupId: String,
|
||||||
|
val profileId: String,
|
||||||
|
val role: GroupMemberRole
|
||||||
|
)
|
||||||
|
|
||||||
|
enum class GroupMemberRole {
|
||||||
|
STANDARD, ZOOMBIE, MODERATOR, ADMIN
|
||||||
|
}
|
@ -5,25 +5,27 @@ import org.session.libsignal.utilities.JsonUtil
|
|||||||
import org.session.libsignal.utilities.Log
|
import org.session.libsignal.utilities.Log
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
data class OpenGroupV2(
|
data class OpenGroup(
|
||||||
val server: String,
|
val server: String,
|
||||||
val room: String,
|
val room: String,
|
||||||
val id: String,
|
val id: String,
|
||||||
val name: String,
|
val name: String,
|
||||||
val publicKey: String
|
val publicKey: String,
|
||||||
|
val infoUpdates: Int,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
constructor(server: String, room: String, name: String, publicKey: String) : this(
|
constructor(server: String, room: String, name: String, infoUpdates: Int, publicKey: String) : this(
|
||||||
server = server,
|
server = server,
|
||||||
room = room,
|
room = room,
|
||||||
id = "$server.$room",
|
id = "$server.$room",
|
||||||
name = name,
|
name = name,
|
||||||
publicKey = publicKey,
|
publicKey = publicKey,
|
||||||
|
infoUpdates = infoUpdates,
|
||||||
)
|
)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
fun fromJSON(jsonAsString: String): OpenGroupV2? {
|
fun fromJSON(jsonAsString: String): OpenGroup? {
|
||||||
return try {
|
return try {
|
||||||
val json = JsonUtil.fromJson(jsonAsString)
|
val json = JsonUtil.fromJson(jsonAsString)
|
||||||
if (!json.has("room")) return null
|
if (!json.has("room")) return null
|
||||||
@ -31,7 +33,9 @@ data class OpenGroupV2(
|
|||||||
val server = json.get("server").asText().toLowerCase(Locale.US)
|
val server = json.get("server").asText().toLowerCase(Locale.US)
|
||||||
val displayName = json.get("displayName").asText()
|
val displayName = json.get("displayName").asText()
|
||||||
val publicKey = json.get("publicKey").asText()
|
val publicKey = json.get("publicKey").asText()
|
||||||
OpenGroupV2(server, room, displayName, publicKey)
|
val infoUpdates = json.get("infoUpdates")?.asText()?.toIntOrNull() ?: 0
|
||||||
|
val capabilities = json.get("capabilities")?.asText()?.split(",") ?: emptyList()
|
||||||
|
OpenGroup(server, room, displayName, infoUpdates, publicKey)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.w("Loki", "Couldn't parse open group from JSON: $jsonAsString.", e);
|
Log.w("Loki", "Couldn't parse open group from JSON: $jsonAsString.", e);
|
||||||
null
|
null
|
||||||
@ -54,7 +58,10 @@ data class OpenGroupV2(
|
|||||||
"server" to server,
|
"server" to server,
|
||||||
"displayName" to name,
|
"displayName" to name,
|
||||||
"publicKey" to publicKey,
|
"publicKey" to publicKey,
|
||||||
|
"infoUpdates" to infoUpdates.toString(),
|
||||||
)
|
)
|
||||||
|
|
||||||
val joinURL: String get() = "$server/$room?public_key=$publicKey"
|
val joinURL: String get() = "$server/$room?public_key=$publicKey"
|
||||||
|
|
||||||
|
val groupId: String get() = "$server.$room"
|
||||||
}
|
}
|
@ -1,499 +0,0 @@
|
|||||||
package org.session.libsession.messaging.open_groups
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty
|
|
||||||
import com.fasterxml.jackson.databind.PropertyNamingStrategy
|
|
||||||
import com.fasterxml.jackson.databind.annotation.JsonNaming
|
|
||||||
import com.fasterxml.jackson.databind.type.TypeFactory
|
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
|
||||||
import nl.komponents.kovenant.Promise
|
|
||||||
import nl.komponents.kovenant.functional.bind
|
|
||||||
import nl.komponents.kovenant.functional.map
|
|
||||||
import okhttp3.Headers
|
|
||||||
import okhttp3.HttpUrl
|
|
||||||
import okhttp3.MediaType
|
|
||||||
import okhttp3.RequestBody
|
|
||||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
|
||||||
import org.session.libsession.messaging.sending_receiving.pollers.OpenGroupPollerV2
|
|
||||||
import org.session.libsession.snode.OnionRequestAPI
|
|
||||||
import org.session.libsession.utilities.AESGCM
|
|
||||||
import org.session.libsession.utilities.TextSecurePreferences
|
|
||||||
import org.session.libsignal.utilities.Base64.decode
|
|
||||||
import org.session.libsignal.utilities.Base64.encodeBytes
|
|
||||||
import org.session.libsignal.utilities.HTTP
|
|
||||||
import org.session.libsignal.utilities.HTTP.Verb.DELETE
|
|
||||||
import org.session.libsignal.utilities.HTTP.Verb.GET
|
|
||||||
import org.session.libsignal.utilities.HTTP.Verb.POST
|
|
||||||
import org.session.libsignal.utilities.HTTP.Verb.PUT
|
|
||||||
import org.session.libsignal.utilities.Hex
|
|
||||||
import org.session.libsignal.utilities.JsonUtil
|
|
||||||
import org.session.libsignal.utilities.Log
|
|
||||||
import org.session.libsignal.utilities.removing05PrefixIfNeeded
|
|
||||||
import org.session.libsignal.utilities.toHexString
|
|
||||||
import org.whispersystems.curve25519.Curve25519
|
|
||||||
import kotlin.collections.component1
|
|
||||||
import kotlin.collections.component2
|
|
||||||
import kotlin.collections.set
|
|
||||||
|
|
||||||
object OpenGroupAPIV2 {
|
|
||||||
private val moderators: HashMap<String, Set<String>> = hashMapOf() // Server URL to (channel ID to set of moderator IDs)
|
|
||||||
private val curve = Curve25519.getInstance(Curve25519.BEST)
|
|
||||||
val defaultRooms = MutableSharedFlow<List<DefaultGroup>>(replay = 1)
|
|
||||||
private val hasPerformedInitialPoll = mutableMapOf<String, Boolean>()
|
|
||||||
private var hasUpdatedLastOpenDate = false
|
|
||||||
|
|
||||||
private val timeSinceLastOpen by lazy {
|
|
||||||
val context = MessagingModuleConfiguration.shared.context
|
|
||||||
val lastOpenDate = TextSecurePreferences.getLastOpenTimeDate(context)
|
|
||||||
val now = System.currentTimeMillis()
|
|
||||||
now - lastOpenDate
|
|
||||||
}
|
|
||||||
|
|
||||||
const val defaultServerPublicKey = "a03c383cf63c3c4efe67acc52112a6dd734b3a946b9545f488aaa93da7991238"
|
|
||||||
const val defaultServer = "http://116.203.70.33"
|
|
||||||
|
|
||||||
sealed class Error(message: String) : Exception(message) {
|
|
||||||
object Generic : Error("An error occurred.")
|
|
||||||
object ParsingFailed : Error("Invalid response.")
|
|
||||||
object DecryptionFailed : Error("Couldn't decrypt response.")
|
|
||||||
object SigningFailed : Error("Couldn't sign message.")
|
|
||||||
object InvalidURL : Error("Invalid URL.")
|
|
||||||
object NoPublicKey : Error("Couldn't find server public key.")
|
|
||||||
}
|
|
||||||
|
|
||||||
data class DefaultGroup(val id: String, val name: String, val image: ByteArray?) {
|
|
||||||
|
|
||||||
val joinURL: String get() = "$defaultServer/$id?public_key=$defaultServerPublicKey"
|
|
||||||
}
|
|
||||||
|
|
||||||
data class Info(val id: String, val name: String, val imageID: String?)
|
|
||||||
|
|
||||||
@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy::class)
|
|
||||||
data class CompactPollRequest(val roomID: String, val authToken: String, val fromDeletionServerID: Long?, val fromMessageServerID: Long?)
|
|
||||||
data class CompactPollResult(val messages: List<OpenGroupMessageV2>, val deletions: List<MessageDeletion>, val moderators: List<String>)
|
|
||||||
|
|
||||||
data class MessageDeletion(
|
|
||||||
@JsonProperty("id")
|
|
||||||
val id: Long = 0,
|
|
||||||
@JsonProperty("deleted_message_id")
|
|
||||||
val deletedMessageServerID: Long = 0
|
|
||||||
) {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
val empty = MessageDeletion()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data class Request(
|
|
||||||
val verb: HTTP.Verb,
|
|
||||||
val room: String?,
|
|
||||||
val server: String,
|
|
||||||
val endpoint: String,
|
|
||||||
val queryParameters: Map<String, String> = mapOf(),
|
|
||||||
val parameters: Any? = null,
|
|
||||||
val headers: Map<String, String> = mapOf(),
|
|
||||||
val isAuthRequired: Boolean = true,
|
|
||||||
/**
|
|
||||||
* Always `true` under normal circumstances. You might want to disable
|
|
||||||
* this when running over Lokinet.
|
|
||||||
*/
|
|
||||||
val useOnionRouting: Boolean = true
|
|
||||||
)
|
|
||||||
|
|
||||||
private fun createBody(parameters: Any?): RequestBody? {
|
|
||||||
if (parameters == null) return null
|
|
||||||
val parametersAsJSON = JsonUtil.toJson(parameters)
|
|
||||||
return RequestBody.create(MediaType.get("application/json"), parametersAsJSON)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun send(request: Request): Promise<Map<*, *>, Exception> {
|
|
||||||
val url = HttpUrl.parse(request.server) ?: return Promise.ofFail(Error.InvalidURL)
|
|
||||||
val urlBuilder = HttpUrl.Builder()
|
|
||||||
.scheme(url.scheme())
|
|
||||||
.host(url.host())
|
|
||||||
.port(url.port())
|
|
||||||
.addPathSegments(request.endpoint)
|
|
||||||
if (request.verb == GET) {
|
|
||||||
for ((key, value) in request.queryParameters) {
|
|
||||||
urlBuilder.addQueryParameter(key, value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fun execute(token: String?): Promise<Map<*, *>, Exception> {
|
|
||||||
val requestBuilder = okhttp3.Request.Builder()
|
|
||||||
.url(urlBuilder.build())
|
|
||||||
.headers(Headers.of(request.headers))
|
|
||||||
if (request.isAuthRequired) {
|
|
||||||
if (token.isNullOrEmpty()) throw IllegalStateException("No auth token for request.")
|
|
||||||
requestBuilder.header("Authorization", token)
|
|
||||||
}
|
|
||||||
when (request.verb) {
|
|
||||||
GET -> requestBuilder.get()
|
|
||||||
PUT -> requestBuilder.put(createBody(request.parameters)!!)
|
|
||||||
POST -> requestBuilder.post(createBody(request.parameters)!!)
|
|
||||||
DELETE -> requestBuilder.delete(createBody(request.parameters))
|
|
||||||
}
|
|
||||||
if (!request.room.isNullOrEmpty()) {
|
|
||||||
requestBuilder.header("Room", request.room)
|
|
||||||
}
|
|
||||||
if (request.useOnionRouting) {
|
|
||||||
val publicKey = MessagingModuleConfiguration.shared.storage.getOpenGroupPublicKey(request.server)
|
|
||||||
?: return Promise.ofFail(Error.NoPublicKey)
|
|
||||||
return OnionRequestAPI.sendOnionRequest(requestBuilder.build(), request.server, publicKey).fail { e ->
|
|
||||||
// A 401 means that we didn't provide a (valid) auth token for a route that required one. We use this as an
|
|
||||||
// indication that the token we're using has expired. Note that a 403 has a different meaning; it means that
|
|
||||||
// we provided a valid token but it doesn't have a high enough permission level for the route in question.
|
|
||||||
if (e is OnionRequestAPI.HTTPRequestFailedAtDestinationException && e.statusCode == 401) {
|
|
||||||
val storage = MessagingModuleConfiguration.shared.storage
|
|
||||||
if (request.room != null) {
|
|
||||||
storage.removeAuthToken(request.room, request.server)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return Promise.ofFail(IllegalStateException("It's currently not allowed to send non onion routed requests."))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return if (request.isAuthRequired) {
|
|
||||||
getAuthToken(request.room!!, request.server).bind { execute(it) }
|
|
||||||
} else {
|
|
||||||
execute(null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun downloadOpenGroupProfilePicture(roomID: String, server: String): Promise<ByteArray, Exception> {
|
|
||||||
val request = Request(verb = GET, room = roomID, server = server, endpoint = "rooms/$roomID/image", isAuthRequired = false)
|
|
||||||
return send(request).map { json ->
|
|
||||||
val result = json["result"] as? String ?: throw Error.ParsingFailed
|
|
||||||
decode(result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// region Authorization
|
|
||||||
fun getAuthToken(room: String, server: String): Promise<String, Exception> {
|
|
||||||
val storage = MessagingModuleConfiguration.shared.storage
|
|
||||||
return storage.getAuthToken(room, server)?.let {
|
|
||||||
Promise.of(it)
|
|
||||||
} ?: run {
|
|
||||||
requestNewAuthToken(room, server)
|
|
||||||
.bind { claimAuthToken(it, room, server) }
|
|
||||||
.success { authToken ->
|
|
||||||
storage.setAuthToken(room, server, authToken)
|
|
||||||
}
|
|
||||||
.fail { exception ->
|
|
||||||
Log.e("Loki", "Failed to get auth token", exception)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun requestNewAuthToken(room: String, server: String): Promise<String, Exception> {
|
|
||||||
val (publicKey, privateKey) = MessagingModuleConfiguration.shared.storage.getUserX25519KeyPair().let { it.publicKey.serialize() to it.privateKey.serialize() }
|
|
||||||
?: return Promise.ofFail(Error.Generic)
|
|
||||||
val queryParameters = mutableMapOf( "public_key" to publicKey.toHexString() )
|
|
||||||
val request = Request(GET, room, server, "auth_token_challenge", queryParameters, isAuthRequired = false, parameters = null)
|
|
||||||
return send(request).map { json ->
|
|
||||||
val challenge = json["challenge"] as? Map<*, *> ?: throw Error.ParsingFailed
|
|
||||||
val base64EncodedCiphertext = challenge["ciphertext"] as? String ?: throw Error.ParsingFailed
|
|
||||||
val base64EncodedEphemeralPublicKey = challenge["ephemeral_public_key"] as? String ?: throw Error.ParsingFailed
|
|
||||||
val ciphertext = decode(base64EncodedCiphertext)
|
|
||||||
val ephemeralPublicKey = decode(base64EncodedEphemeralPublicKey)
|
|
||||||
val symmetricKey = AESGCM.generateSymmetricKey(ephemeralPublicKey, privateKey)
|
|
||||||
val tokenAsData = try {
|
|
||||||
AESGCM.decrypt(ciphertext, symmetricKey)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
throw Error.DecryptionFailed
|
|
||||||
}
|
|
||||||
tokenAsData.toHexString()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun claimAuthToken(authToken: String, room: String, server: String): Promise<String, Exception> {
|
|
||||||
val parameters = mapOf( "public_key" to MessagingModuleConfiguration.shared.storage.getUserPublicKey()!! )
|
|
||||||
val headers = mapOf( "Authorization" to authToken )
|
|
||||||
val request = Request(verb = POST, room = room, server = server, endpoint = "claim_auth_token",
|
|
||||||
parameters = parameters, headers = headers, isAuthRequired = false)
|
|
||||||
return send(request).map { authToken }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun deleteAuthToken(room: String, server: String): Promise<Unit, Exception> {
|
|
||||||
val request = Request(verb = DELETE, room = room, server = server, endpoint = "auth_token")
|
|
||||||
return send(request).map {
|
|
||||||
MessagingModuleConfiguration.shared.storage.removeAuthToken(room, server)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// endregion
|
|
||||||
|
|
||||||
// region Upload/Download
|
|
||||||
fun upload(file: ByteArray, room: String, server: String): Promise<Long, Exception> {
|
|
||||||
val base64EncodedFile = encodeBytes(file)
|
|
||||||
val parameters = mapOf( "file" to base64EncodedFile )
|
|
||||||
val request = Request(verb = POST, room = room, server = server, endpoint = "files", parameters = parameters)
|
|
||||||
return send(request).map { json ->
|
|
||||||
(json["result"] as? Number)?.toLong() ?: throw Error.ParsingFailed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun download(file: Long, room: String, server: String): Promise<ByteArray, Exception> {
|
|
||||||
val request = Request(verb = GET, room = room, server = server, endpoint = "files/$file")
|
|
||||||
return send(request).map { json ->
|
|
||||||
val base64EncodedFile = json["result"] as? String ?: throw Error.ParsingFailed
|
|
||||||
decode(base64EncodedFile) ?: throw Error.ParsingFailed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// endregion
|
|
||||||
|
|
||||||
// region Sending
|
|
||||||
fun send(message: OpenGroupMessageV2, room: String, server: String): Promise<OpenGroupMessageV2, Exception> {
|
|
||||||
val signedMessage = message.sign() ?: return Promise.ofFail(Error.SigningFailed)
|
|
||||||
val jsonMessage = signedMessage.toJSON()
|
|
||||||
val request = Request(verb = POST, room = room, server = server, endpoint = "messages", parameters = jsonMessage)
|
|
||||||
return send(request).map { json ->
|
|
||||||
@Suppress("UNCHECKED_CAST") val rawMessage = json["message"] as? Map<String, Any>
|
|
||||||
?: throw Error.ParsingFailed
|
|
||||||
val result = OpenGroupMessageV2.fromJSON(rawMessage) ?: throw Error.ParsingFailed
|
|
||||||
val storage = MessagingModuleConfiguration.shared.storage
|
|
||||||
storage.addReceivedMessageTimestamp(result.sentTimestamp)
|
|
||||||
result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// endregion
|
|
||||||
|
|
||||||
// region Messages
|
|
||||||
fun getMessages(room: String, server: String): Promise<List<OpenGroupMessageV2>, Exception> {
|
|
||||||
val storage = MessagingModuleConfiguration.shared.storage
|
|
||||||
val queryParameters = mutableMapOf<String, String>()
|
|
||||||
storage.getLastMessageServerID(room, server)?.let { lastId ->
|
|
||||||
queryParameters += "from_server_id" to lastId.toString()
|
|
||||||
}
|
|
||||||
val request = Request(verb = GET, room = room, server = server, endpoint = "messages", queryParameters = queryParameters)
|
|
||||||
return send(request).map { json ->
|
|
||||||
@Suppress("UNCHECKED_CAST") val rawMessages = json["messages"] as? List<Map<String, Any>>
|
|
||||||
?: throw Error.ParsingFailed
|
|
||||||
parseMessages(room, server, rawMessages)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseMessages(room: String, server: String, rawMessages: List<Map<*, *>>): List<OpenGroupMessageV2> {
|
|
||||||
val messages = rawMessages.mapNotNull { json ->
|
|
||||||
json as Map<String, Any>
|
|
||||||
try {
|
|
||||||
val message = OpenGroupMessageV2.fromJSON(json) ?: return@mapNotNull null
|
|
||||||
if (message.serverID == null || message.sender.isNullOrEmpty()) return@mapNotNull null
|
|
||||||
val sender = message.sender
|
|
||||||
val data = decode(message.base64EncodedData)
|
|
||||||
val signature = decode(message.base64EncodedSignature)
|
|
||||||
val publicKey = Hex.fromStringCondensed(sender.removing05PrefixIfNeeded())
|
|
||||||
val isValid = curve.verifySignature(publicKey, data, signature)
|
|
||||||
if (!isValid) {
|
|
||||||
Log.d("Loki", "Ignoring message with invalid signature.")
|
|
||||||
return@mapNotNull null
|
|
||||||
}
|
|
||||||
message
|
|
||||||
} catch (e: Exception) {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return messages
|
|
||||||
}
|
|
||||||
// endregion
|
|
||||||
|
|
||||||
// region Message Deletion
|
|
||||||
@JvmStatic
|
|
||||||
fun deleteMessage(serverID: Long, room: String, server: String): Promise<Unit, Exception> {
|
|
||||||
val request = Request(verb = DELETE, room = room, server = server, endpoint = "messages/$serverID")
|
|
||||||
return send(request).map {
|
|
||||||
Log.d("Loki", "Message deletion successful.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getDeletedMessages(room: String, server: String): Promise<List<MessageDeletion>, Exception> {
|
|
||||||
val storage = MessagingModuleConfiguration.shared.storage
|
|
||||||
val queryParameters = mutableMapOf<String, String>()
|
|
||||||
storage.getLastDeletionServerID(room, server)?.let { last ->
|
|
||||||
queryParameters["from_server_id"] = last.toString()
|
|
||||||
}
|
|
||||||
val request = Request(verb = GET, room = room, server = server, endpoint = "deleted_messages", queryParameters = queryParameters)
|
|
||||||
return send(request).map { json ->
|
|
||||||
val type = TypeFactory.defaultInstance().constructCollectionType(List::class.java, MessageDeletion::class.java)
|
|
||||||
val idsAsString = JsonUtil.toJson(json["ids"])
|
|
||||||
val serverIDs = JsonUtil.fromJson<List<MessageDeletion>>(idsAsString, type) ?: throw Error.ParsingFailed
|
|
||||||
val lastMessageServerId = storage.getLastDeletionServerID(room, server) ?: 0
|
|
||||||
val serverID = serverIDs.maxByOrNull {it.id } ?: MessageDeletion.empty
|
|
||||||
if (serverID.id > lastMessageServerId) {
|
|
||||||
storage.setLastDeletionServerID(room, server, serverID.id)
|
|
||||||
}
|
|
||||||
serverIDs
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// endregion
|
|
||||||
|
|
||||||
// region Moderation
|
|
||||||
private fun handleModerators(serverRoomId: String, moderatorList: List<String>) {
|
|
||||||
moderators[serverRoomId] = moderatorList.toMutableSet()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getModerators(room: String, server: String): Promise<List<String>, Exception> {
|
|
||||||
val request = Request(verb = GET, room = room, server = server, endpoint = "moderators")
|
|
||||||
return send(request).map { json ->
|
|
||||||
@Suppress("UNCHECKED_CAST") val moderatorsJson = json["moderators"] as? List<String>
|
|
||||||
?: throw Error.ParsingFailed
|
|
||||||
val id = "$server.$room"
|
|
||||||
handleModerators(id, moderatorsJson)
|
|
||||||
moderatorsJson
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
fun ban(publicKey: String, room: String, server: String): Promise<Unit, Exception> {
|
|
||||||
val parameters = mapOf( "public_key" to publicKey )
|
|
||||||
val request = Request(verb = POST, room = room, server = server, endpoint = "block_list", parameters = parameters)
|
|
||||||
return send(request).map {
|
|
||||||
Log.d("Loki", "Banned user: $publicKey from: $server.$room.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun banAndDeleteAll(publicKey: String, room: String, server: String): Promise<Unit, Exception> {
|
|
||||||
val parameters = mapOf( "public_key" to publicKey )
|
|
||||||
val request = Request(verb = POST, room = room, server = server, endpoint = "ban_and_delete_all", parameters = parameters)
|
|
||||||
return send(request).map {
|
|
||||||
Log.d("Loki", "Banned user: $publicKey from: $server.$room.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun unban(publicKey: String, room: String, server: String): Promise<Unit, Exception> {
|
|
||||||
val request = Request(verb = DELETE, room = room, server = server, endpoint = "block_list/$publicKey")
|
|
||||||
return send(request).map {
|
|
||||||
Log.d("Loki", "Unbanned user: $publicKey from: $server.$room")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
fun isUserModerator(publicKey: String, room: String, server: String): Boolean =
|
|
||||||
moderators["$server.$room"]?.contains(publicKey) ?: false
|
|
||||||
// endregion
|
|
||||||
|
|
||||||
// region General
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
fun compactPoll(rooms: List<String>, server: String): Promise<Map<String, CompactPollResult>, Exception> {
|
|
||||||
val authTokenRequests = rooms.associateWith { room -> getAuthToken(room, server) }
|
|
||||||
val storage = MessagingModuleConfiguration.shared.storage
|
|
||||||
val context = MessagingModuleConfiguration.shared.context
|
|
||||||
val timeSinceLastOpen = this.timeSinceLastOpen
|
|
||||||
val useMessageLimit = (hasPerformedInitialPoll[server] != true
|
|
||||||
&& timeSinceLastOpen > OpenGroupPollerV2.maxInactivityPeriod)
|
|
||||||
hasPerformedInitialPoll[server] = true
|
|
||||||
if (!hasUpdatedLastOpenDate) {
|
|
||||||
hasUpdatedLastOpenDate = true
|
|
||||||
TextSecurePreferences.setLastOpenDate(context)
|
|
||||||
}
|
|
||||||
val requests = rooms.mapNotNull { room ->
|
|
||||||
val authToken = try {
|
|
||||||
authTokenRequests[room]?.get()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e("Loki", "Failed to get auth token for $room.", e)
|
|
||||||
null
|
|
||||||
} ?: return@mapNotNull null
|
|
||||||
CompactPollRequest(
|
|
||||||
roomID = room,
|
|
||||||
authToken = authToken,
|
|
||||||
fromDeletionServerID = if (useMessageLimit) null else storage.getLastDeletionServerID(room, server),
|
|
||||||
fromMessageServerID = if (useMessageLimit) null else storage.getLastMessageServerID(room, server)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
val request = Request(verb = POST, room = null, server = server, endpoint = "compact_poll", isAuthRequired = false, parameters = mapOf( "requests" to requests ))
|
|
||||||
return send(request = request).map { json ->
|
|
||||||
val results = json["results"] as? List<*> ?: throw Error.ParsingFailed
|
|
||||||
results.mapNotNull { json ->
|
|
||||||
if (json !is Map<*,*>) return@mapNotNull null
|
|
||||||
val roomID = json["room_id"] as? String ?: return@mapNotNull null
|
|
||||||
// A 401 means that we didn't provide a (valid) auth token for a route that required one. We use this as an
|
|
||||||
// indication that the token we're using has expired. Note that a 403 has a different meaning; it means that
|
|
||||||
// we provided a valid token but it doesn't have a high enough permission level for the route in question.
|
|
||||||
val statusCode = json["status_code"] as? Int ?: return@mapNotNull null
|
|
||||||
if (statusCode == 401) {
|
|
||||||
// delete auth token and return null
|
|
||||||
storage.removeAuthToken(roomID, server)
|
|
||||||
}
|
|
||||||
// Moderators
|
|
||||||
val moderators = json["moderators"] as? List<String> ?: return@mapNotNull null
|
|
||||||
handleModerators("$server.$roomID", moderators)
|
|
||||||
// Deletions
|
|
||||||
val type = TypeFactory.defaultInstance().constructCollectionType(List::class.java, MessageDeletion::class.java)
|
|
||||||
val idsAsString = JsonUtil.toJson(json["deletions"])
|
|
||||||
val deletions = JsonUtil.fromJson<List<MessageDeletion>>(idsAsString, type) ?: throw Error.ParsingFailed
|
|
||||||
// Messages
|
|
||||||
val rawMessages = json["messages"] as? List<Map<String, Any>> ?: return@mapNotNull null
|
|
||||||
val messages = parseMessages(roomID, server, rawMessages)
|
|
||||||
roomID to CompactPollResult(
|
|
||||||
messages = messages,
|
|
||||||
deletions = deletions,
|
|
||||||
moderators = moderators
|
|
||||||
)
|
|
||||||
}.toMap()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getDefaultRoomsIfNeeded(): Promise<List<DefaultGroup>, Exception> {
|
|
||||||
val storage = MessagingModuleConfiguration.shared.storage
|
|
||||||
storage.setOpenGroupPublicKey(defaultServer, defaultServerPublicKey)
|
|
||||||
return getAllRooms(defaultServer).map { groups ->
|
|
||||||
val earlyGroups = groups.map { group ->
|
|
||||||
DefaultGroup(group.id, group.name, null)
|
|
||||||
}
|
|
||||||
// See if we have any cached rooms, and if they already have images don't overwrite them with early non-image results
|
|
||||||
defaultRooms.replayCache.firstOrNull()?.let { replayed ->
|
|
||||||
if (replayed.none { it.image?.isNotEmpty() == true}) {
|
|
||||||
defaultRooms.tryEmit(earlyGroups)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val images = groups.map { group ->
|
|
||||||
group.id to downloadOpenGroupProfilePicture(group.id, defaultServer)
|
|
||||||
}.toMap()
|
|
||||||
groups.map { group ->
|
|
||||||
val image = try {
|
|
||||||
images[group.id]!!.get()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
// No image or image failed to download
|
|
||||||
null
|
|
||||||
}
|
|
||||||
DefaultGroup(group.id, group.name, image)
|
|
||||||
}
|
|
||||||
}.success { new ->
|
|
||||||
defaultRooms.tryEmit(new)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getInfo(room: String, server: String): Promise<Info, Exception> {
|
|
||||||
val request = Request(verb = GET, room = null, server = server, endpoint = "rooms/$room", isAuthRequired = false)
|
|
||||||
return send(request).map { json ->
|
|
||||||
val rawRoom = json["room"] as? Map<*, *> ?: throw Error.ParsingFailed
|
|
||||||
val id = rawRoom["id"] as? String ?: throw Error.ParsingFailed
|
|
||||||
val name = rawRoom["name"] as? String ?: throw Error.ParsingFailed
|
|
||||||
val imageID = rawRoom["image_id"] as? String
|
|
||||||
Info(id = id, name = name, imageID = imageID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getAllRooms(server: String): Promise<List<Info>, Exception> {
|
|
||||||
val request = Request(verb = GET, room = null, server = server, endpoint = "rooms", isAuthRequired = false)
|
|
||||||
return send(request).map { json ->
|
|
||||||
val rawRooms = json["rooms"] as? List<Map<*, *>> ?: throw Error.ParsingFailed
|
|
||||||
rawRooms.mapNotNull {
|
|
||||||
val roomJson = it as? Map<*, *> ?: return@mapNotNull null
|
|
||||||
val id = roomJson["id"] as? String ?: return@mapNotNull null
|
|
||||||
val name = roomJson["name"] as? String ?: return@mapNotNull null
|
|
||||||
val imageID = roomJson["image_id"] as? String
|
|
||||||
Info(id, name, imageID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getMemberCount(room: String, server: String): Promise<Int, Exception> {
|
|
||||||
val request = Request(verb = GET, room = room, server = server, endpoint = "member_count")
|
|
||||||
return send(request).map { json ->
|
|
||||||
val memberCount = json["member_count"] as? Int ?: throw Error.ParsingFailed
|
|
||||||
val storage = MessagingModuleConfiguration.shared.storage
|
|
||||||
storage.setUserCount(room, server, memberCount)
|
|
||||||
memberCount
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// endregion
|
|
||||||
}
|
|
@ -0,0 +1,830 @@
|
|||||||
|
package org.session.libsession.messaging.open_groups
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonInclude
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
|
import com.fasterxml.jackson.core.type.TypeReference
|
||||||
|
import com.fasterxml.jackson.databind.PropertyNamingStrategy
|
||||||
|
import com.fasterxml.jackson.databind.annotation.JsonNaming
|
||||||
|
import com.fasterxml.jackson.databind.type.TypeFactory
|
||||||
|
import com.goterl.lazysodium.LazySodiumAndroid
|
||||||
|
import com.goterl.lazysodium.SodiumAndroid
|
||||||
|
import com.goterl.lazysodium.interfaces.GenericHash
|
||||||
|
import com.goterl.lazysodium.interfaces.Sign
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
import nl.komponents.kovenant.Promise
|
||||||
|
import nl.komponents.kovenant.functional.map
|
||||||
|
import okhttp3.Headers
|
||||||
|
import okhttp3.HttpUrl
|
||||||
|
import okhttp3.MediaType
|
||||||
|
import okhttp3.RequestBody
|
||||||
|
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||||
|
import org.session.libsession.messaging.sending_receiving.pollers.OpenGroupPoller.Companion.maxInactivityPeriod
|
||||||
|
import org.session.libsession.messaging.utilities.SessionId
|
||||||
|
import org.session.libsession.messaging.utilities.SodiumUtilities
|
||||||
|
import org.session.libsession.snode.OnionRequestAPI
|
||||||
|
import org.session.libsession.snode.OnionResponse
|
||||||
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
|
import org.session.libsignal.utilities.Base64.decode
|
||||||
|
import org.session.libsignal.utilities.Base64.encodeBytes
|
||||||
|
import org.session.libsignal.utilities.HTTP
|
||||||
|
import org.session.libsignal.utilities.HTTP.Verb.DELETE
|
||||||
|
import org.session.libsignal.utilities.HTTP.Verb.GET
|
||||||
|
import org.session.libsignal.utilities.HTTP.Verb.POST
|
||||||
|
import org.session.libsignal.utilities.HTTP.Verb.PUT
|
||||||
|
import org.session.libsignal.utilities.Hex
|
||||||
|
import org.session.libsignal.utilities.IdPrefix
|
||||||
|
import org.session.libsignal.utilities.JsonUtil
|
||||||
|
import org.session.libsignal.utilities.Log
|
||||||
|
import org.session.libsignal.utilities.removingIdPrefixIfNeeded
|
||||||
|
import org.whispersystems.curve25519.Curve25519
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
import kotlin.collections.component1
|
||||||
|
import kotlin.collections.component2
|
||||||
|
import kotlin.collections.set
|
||||||
|
|
||||||
|
object OpenGroupApi {
|
||||||
|
private val curve = Curve25519.getInstance(Curve25519.BEST)
|
||||||
|
val defaultRooms = MutableSharedFlow<List<DefaultGroup>>(replay = 1)
|
||||||
|
private val hasPerformedInitialPoll = mutableMapOf<String, Boolean>()
|
||||||
|
private var hasUpdatedLastOpenDate = false
|
||||||
|
private val sodium by lazy { LazySodiumAndroid(SodiumAndroid()) }
|
||||||
|
private val timeSinceLastOpen by lazy {
|
||||||
|
val context = MessagingModuleConfiguration.shared.context
|
||||||
|
val lastOpenDate = TextSecurePreferences.getLastOpenTimeDate(context)
|
||||||
|
val now = System.currentTimeMillis()
|
||||||
|
now - lastOpenDate
|
||||||
|
}
|
||||||
|
|
||||||
|
const val defaultServerPublicKey =
|
||||||
|
"a03c383cf63c3c4efe67acc52112a6dd734b3a946b9545f488aaa93da7991238"
|
||||||
|
const val defaultServer = "http://116.203.70.33"
|
||||||
|
|
||||||
|
sealed class Error(message: String) : Exception(message) {
|
||||||
|
object Generic : Error("An error occurred.")
|
||||||
|
object ParsingFailed : Error("Invalid response.")
|
||||||
|
object DecryptionFailed : Error("Couldn't decrypt response.")
|
||||||
|
object SigningFailed : Error("Couldn't sign message.")
|
||||||
|
object InvalidURL : Error("Invalid URL.")
|
||||||
|
object NoPublicKey : Error("Couldn't find server public key.")
|
||||||
|
object NoEd25519KeyPair : Error("Couldn't find ed25519 key pair.")
|
||||||
|
}
|
||||||
|
|
||||||
|
data class DefaultGroup(val id: String, val name: String, val image: ByteArray?) {
|
||||||
|
|
||||||
|
val joinURL: String get() = "$defaultServer/$id?public_key=$defaultServerPublicKey"
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy::class)
|
||||||
|
data class RoomInfo(
|
||||||
|
val token: String = "",
|
||||||
|
val name: String = "",
|
||||||
|
val description: String = "",
|
||||||
|
val infoUpdates: Int = 0,
|
||||||
|
val messageSequence: Long = 0,
|
||||||
|
val created: Long = 0,
|
||||||
|
val activeUsers: Int = 0,
|
||||||
|
val activeUsersCutoff: Int = 0,
|
||||||
|
val imageId: Long? = null,
|
||||||
|
val pinnedMessages: List<PinnedMessage> = emptyList(),
|
||||||
|
val admin: Boolean = false,
|
||||||
|
val globalAdmin: Boolean = false,
|
||||||
|
val admins: List<String> = emptyList(),
|
||||||
|
val hiddenAdmins: List<String> = emptyList(),
|
||||||
|
val moderator: Boolean = false,
|
||||||
|
val globalModerator: Boolean = false,
|
||||||
|
val moderators: List<String> = emptyList(),
|
||||||
|
val hiddenModerators: List<String> = emptyList(),
|
||||||
|
val read: Boolean = false,
|
||||||
|
val defaultRead: Boolean = false,
|
||||||
|
val defaultAccessible: Boolean = false,
|
||||||
|
val write: Boolean = false,
|
||||||
|
val defaultWrite: Boolean = false,
|
||||||
|
val upload: Boolean = false,
|
||||||
|
val defaultUpload: Boolean = false,
|
||||||
|
)
|
||||||
|
|
||||||
|
@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy::class)
|
||||||
|
data class PinnedMessage(
|
||||||
|
val id: Long = 0,
|
||||||
|
val pinnedAt: Long = 0,
|
||||||
|
val pinnedBy: String = ""
|
||||||
|
)
|
||||||
|
|
||||||
|
@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy::class)
|
||||||
|
data class BatchRequestInfo<T>(
|
||||||
|
val request: BatchRequest,
|
||||||
|
val endpoint: Endpoint,
|
||||||
|
val responseType: TypeReference<T>
|
||||||
|
)
|
||||||
|
|
||||||
|
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||||
|
data class BatchRequest(
|
||||||
|
val method: HTTP.Verb,
|
||||||
|
val path: String,
|
||||||
|
val headers: Map<String, String> = emptyMap(),
|
||||||
|
val json: Map<String, Any>? = null,
|
||||||
|
val b64: String? = null,
|
||||||
|
val bytes: ByteArray? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class BatchResponse<T>(
|
||||||
|
val endpoint: Endpoint,
|
||||||
|
val code: Int,
|
||||||
|
val headers: Map<String, String>,
|
||||||
|
val body: T?
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Capabilities(
|
||||||
|
val capabilities: List<String> = emptyList(),
|
||||||
|
val missing: List<String> = emptyList()
|
||||||
|
)
|
||||||
|
|
||||||
|
@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy::class)
|
||||||
|
data class RoomPollInfo(
|
||||||
|
val token: String = "",
|
||||||
|
val activeUsers: Int = 0,
|
||||||
|
val admin: Boolean = false,
|
||||||
|
val globalAdmin: Boolean = false,
|
||||||
|
val moderator: Boolean = false,
|
||||||
|
val globalModerator: Boolean = false,
|
||||||
|
val read: Boolean = false,
|
||||||
|
val defaultRead: Boolean = false,
|
||||||
|
val defaultAccessible: Boolean = false,
|
||||||
|
val write: Boolean = false,
|
||||||
|
val defaultWrite: Boolean = false,
|
||||||
|
val upload: Boolean = false,
|
||||||
|
val defaultUpload: Boolean = false,
|
||||||
|
val details: RoomInfo? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy::class)
|
||||||
|
data class DirectMessage(
|
||||||
|
val id: Long = 0,
|
||||||
|
val sender: String = "",
|
||||||
|
val recipient: String = "",
|
||||||
|
val postedAt: Long = 0,
|
||||||
|
val expiresAt: Long = 0,
|
||||||
|
val message: String = "",
|
||||||
|
)
|
||||||
|
|
||||||
|
@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy::class)
|
||||||
|
data class Message(
|
||||||
|
val id : Long = 0,
|
||||||
|
val sessionId: String = "",
|
||||||
|
val posted: Double = 0.0,
|
||||||
|
val edited: Long = 0,
|
||||||
|
val seqno: Long = 0,
|
||||||
|
val deleted: Boolean = false,
|
||||||
|
val whisper: Boolean = false,
|
||||||
|
val whisperMods: String = "",
|
||||||
|
val whisperTo: String = "",
|
||||||
|
val data: String? = null,
|
||||||
|
val signature: String? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy::class)
|
||||||
|
data class SendMessageRequest(
|
||||||
|
val data: String? = null,
|
||||||
|
val signature: String? = null,
|
||||||
|
val whisperTo: List<String>? = null,
|
||||||
|
val whisperMods: Boolean? = null,
|
||||||
|
val files: List<String>? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
data class MessageDeletion(
|
||||||
|
@JsonProperty("id")
|
||||||
|
val id: Long = 0,
|
||||||
|
@JsonProperty("deleted_message_id")
|
||||||
|
val deletedMessageServerID: Long = 0
|
||||||
|
) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val empty = MessageDeletion()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Request(
|
||||||
|
val verb: HTTP.Verb,
|
||||||
|
val room: String?,
|
||||||
|
val server: String,
|
||||||
|
val endpoint: Endpoint,
|
||||||
|
val queryParameters: Map<String, String> = mapOf(),
|
||||||
|
val parameters: Any? = null,
|
||||||
|
val headers: Map<String, String> = mapOf(),
|
||||||
|
val isAuthRequired: Boolean = true,
|
||||||
|
/**
|
||||||
|
* Always `true` under normal circumstances. You might want to disable
|
||||||
|
* this when running over Lokinet.
|
||||||
|
*/
|
||||||
|
val useOnionRouting: Boolean = true
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun createBody(parameters: Any?): RequestBody? {
|
||||||
|
if (parameters == null) return null
|
||||||
|
val parametersAsJSON = JsonUtil.toJson(parameters)
|
||||||
|
return RequestBody.create(MediaType.get("application/json"), parametersAsJSON)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getResponseBody(request: Request): Promise<ByteArray, Exception> {
|
||||||
|
return send(request).map { response ->
|
||||||
|
response.body ?: throw Error.ParsingFailed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getResponseBodyJson(request: Request): Promise<Map<*, *>, Exception> {
|
||||||
|
return send(request).map {
|
||||||
|
JsonUtil.fromJson(it.body, Map::class.java)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun send(request: Request): Promise<OnionResponse, Exception> {
|
||||||
|
val url = HttpUrl.parse(request.server) ?: return Promise.ofFail(Error.InvalidURL)
|
||||||
|
val urlBuilder = HttpUrl.Builder()
|
||||||
|
.scheme(url.scheme())
|
||||||
|
.host(url.host())
|
||||||
|
.port(url.port())
|
||||||
|
.addPathSegments(request.endpoint.value)
|
||||||
|
if (request.verb == GET) {
|
||||||
|
for ((key, value) in request.queryParameters) {
|
||||||
|
urlBuilder.addQueryParameter(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fun execute(): Promise<OnionResponse, Exception> {
|
||||||
|
val serverCapabilities = MessagingModuleConfiguration.shared.storage.getServerCapabilities(request.server)
|
||||||
|
val publicKey =
|
||||||
|
MessagingModuleConfiguration.shared.storage.getOpenGroupPublicKey(request.server)
|
||||||
|
?: return Promise.ofFail(Error.NoPublicKey)
|
||||||
|
val ed25519KeyPair = MessagingModuleConfiguration.shared.getUserED25519KeyPair()
|
||||||
|
?: return Promise.ofFail(Error.NoEd25519KeyPair)
|
||||||
|
val urlRequest = urlBuilder.build()
|
||||||
|
val headers = request.headers.toMutableMap()
|
||||||
|
if (request.isAuthRequired) {
|
||||||
|
val nonce = sodium.nonce(16)
|
||||||
|
val timestamp = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis())
|
||||||
|
var pubKey = ""
|
||||||
|
var signature = ByteArray(Sign.BYTES)
|
||||||
|
var bodyHash = ByteArray(0)
|
||||||
|
if (request.parameters != null) {
|
||||||
|
val parameterBytes = JsonUtil.toJson(request.parameters).toByteArray()
|
||||||
|
val parameterHash = ByteArray(GenericHash.BYTES_MAX)
|
||||||
|
if (sodium.cryptoGenericHash(
|
||||||
|
parameterHash,
|
||||||
|
parameterHash.size,
|
||||||
|
parameterBytes,
|
||||||
|
parameterBytes.size.toLong()
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
bodyHash = parameterHash
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val messageBytes = Hex.fromStringCondensed(publicKey)
|
||||||
|
.plus(nonce)
|
||||||
|
.plus("$timestamp".toByteArray(Charsets.US_ASCII))
|
||||||
|
.plus(request.verb.rawValue.toByteArray())
|
||||||
|
.plus(urlRequest.encodedPath().toByteArray())
|
||||||
|
.plus(bodyHash)
|
||||||
|
if (serverCapabilities.contains("blind")) {
|
||||||
|
SodiumUtilities.blindedKeyPair(publicKey, ed25519KeyPair)?.let { keyPair ->
|
||||||
|
pubKey = SessionId(
|
||||||
|
IdPrefix.BLINDED,
|
||||||
|
keyPair.publicKey.asBytes
|
||||||
|
).hexString
|
||||||
|
|
||||||
|
signature = SodiumUtilities.sogsSignature(
|
||||||
|
messageBytes,
|
||||||
|
ed25519KeyPair.secretKey.asBytes,
|
||||||
|
keyPair.secretKey.asBytes,
|
||||||
|
keyPair.publicKey.asBytes
|
||||||
|
) ?: return Promise.ofFail(Error.SigningFailed)
|
||||||
|
} ?: return Promise.ofFail(Error.SigningFailed)
|
||||||
|
} else {
|
||||||
|
pubKey = SessionId(
|
||||||
|
IdPrefix.UN_BLINDED,
|
||||||
|
ed25519KeyPair.publicKey.asBytes
|
||||||
|
).hexString
|
||||||
|
sodium.cryptoSignDetached(
|
||||||
|
signature,
|
||||||
|
messageBytes,
|
||||||
|
messageBytes.size.toLong(),
|
||||||
|
ed25519KeyPair.secretKey.asBytes
|
||||||
|
)
|
||||||
|
}
|
||||||
|
headers["X-SOGS-Nonce"] = encodeBytes(nonce)
|
||||||
|
headers["X-SOGS-Timestamp"] = "$timestamp"
|
||||||
|
headers["X-SOGS-Pubkey"] = pubKey
|
||||||
|
headers["X-SOGS-Signature"] = encodeBytes(signature)
|
||||||
|
}
|
||||||
|
|
||||||
|
val requestBuilder = okhttp3.Request.Builder()
|
||||||
|
.url(urlRequest)
|
||||||
|
.headers(Headers.of(headers))
|
||||||
|
when (request.verb) {
|
||||||
|
GET -> requestBuilder.get()
|
||||||
|
PUT -> requestBuilder.put(createBody(request.parameters)!!)
|
||||||
|
POST -> requestBuilder.post(createBody(request.parameters)!!)
|
||||||
|
DELETE -> requestBuilder.delete(createBody(request.parameters))
|
||||||
|
}
|
||||||
|
if (!request.room.isNullOrEmpty()) {
|
||||||
|
requestBuilder.header("Room", request.room)
|
||||||
|
}
|
||||||
|
return if (request.useOnionRouting) {
|
||||||
|
OnionRequestAPI.sendOnionRequest(requestBuilder.build(), request.server, publicKey).fail { e ->
|
||||||
|
Log.e("SOGS", "Failed onion request", e)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Promise.ofFail(IllegalStateException("It's currently not allowed to send non onion routed requests."))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return execute()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun downloadOpenGroupProfilePicture(
|
||||||
|
server: String,
|
||||||
|
roomID: String,
|
||||||
|
imageId: Long
|
||||||
|
): Promise<ByteArray, Exception> {
|
||||||
|
val request = Request(
|
||||||
|
verb = GET,
|
||||||
|
room = roomID,
|
||||||
|
server = server,
|
||||||
|
endpoint = Endpoint.RoomFileIndividual(roomID, imageId.toString())
|
||||||
|
)
|
||||||
|
return getResponseBody(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
// region Upload/Download
|
||||||
|
fun upload(file: ByteArray, room: String, server: String): Promise<Long, Exception> {
|
||||||
|
val parameters = mapOf("file" to file)
|
||||||
|
val request = Request(
|
||||||
|
verb = POST,
|
||||||
|
room = room,
|
||||||
|
server = server,
|
||||||
|
endpoint = Endpoint.RoomFile(room),
|
||||||
|
parameters = parameters
|
||||||
|
)
|
||||||
|
return getResponseBodyJson(request).map { json ->
|
||||||
|
(json["id"] as? Number)?.toLong() ?: throw Error.ParsingFailed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun download(fileId: String, room: String, server: String): Promise<ByteArray, Exception> {
|
||||||
|
val request = Request(
|
||||||
|
verb = GET,
|
||||||
|
room = room,
|
||||||
|
server = server,
|
||||||
|
endpoint = Endpoint.RoomFileIndividual(room, fileId)
|
||||||
|
)
|
||||||
|
return getResponseBody(request)
|
||||||
|
}
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// region Sending
|
||||||
|
fun sendMessage(
|
||||||
|
message: OpenGroupMessage,
|
||||||
|
room: String,
|
||||||
|
server: String,
|
||||||
|
whisperTo: List<String>? = null,
|
||||||
|
whisperMods: Boolean? = null,
|
||||||
|
fileIds: List<String>? = null
|
||||||
|
): Promise<OpenGroupMessage, Exception> {
|
||||||
|
val signedMessage = message.sign(room, server, fallbackSigningType = IdPrefix.STANDARD) ?: return Promise.ofFail(Error.SigningFailed)
|
||||||
|
val request = Request(
|
||||||
|
verb = POST,
|
||||||
|
room = room,
|
||||||
|
server = server,
|
||||||
|
endpoint = Endpoint.RoomMessage(room),
|
||||||
|
parameters = signedMessage.toJSON()
|
||||||
|
)
|
||||||
|
return getResponseBodyJson(request).map { json ->
|
||||||
|
@Suppress("UNCHECKED_CAST") val rawMessage = json as? Map<String, Any>
|
||||||
|
?: throw Error.ParsingFailed
|
||||||
|
val result = OpenGroupMessage.fromJSON(rawMessage) ?: throw Error.ParsingFailed
|
||||||
|
val storage = MessagingModuleConfiguration.shared.storage
|
||||||
|
storage.addReceivedMessageTimestamp(result.sentTimestamp)
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// region Messages
|
||||||
|
fun getMessages(room: String, server: String): Promise<List<OpenGroupMessage>, Exception> {
|
||||||
|
val storage = MessagingModuleConfiguration.shared.storage
|
||||||
|
val queryParameters = mutableMapOf<String, String>()
|
||||||
|
storage.getLastMessageServerID(room, server)?.let { lastId ->
|
||||||
|
queryParameters += "from_server_id" to lastId.toString()
|
||||||
|
}
|
||||||
|
val request = Request(
|
||||||
|
verb = GET,
|
||||||
|
room = room,
|
||||||
|
server = server,
|
||||||
|
endpoint = Endpoint.RoomMessage(room),
|
||||||
|
queryParameters = queryParameters
|
||||||
|
)
|
||||||
|
return getResponseBodyJson(request).map { json ->
|
||||||
|
@Suppress("UNCHECKED_CAST") val rawMessages =
|
||||||
|
json["messages"] as? List<Map<String, Any>>
|
||||||
|
?: throw Error.ParsingFailed
|
||||||
|
parseMessages(room, server, rawMessages)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseMessages(
|
||||||
|
room: String,
|
||||||
|
server: String,
|
||||||
|
rawMessages: List<Map<*, *>>
|
||||||
|
): List<OpenGroupMessage> {
|
||||||
|
val messages = rawMessages.mapNotNull { json ->
|
||||||
|
json as Map<String, Any>
|
||||||
|
try {
|
||||||
|
val message = OpenGroupMessage.fromJSON(json) ?: return@mapNotNull null
|
||||||
|
if (message.serverID == null || message.sender.isNullOrEmpty()) return@mapNotNull null
|
||||||
|
val sender = message.sender
|
||||||
|
val data = decode(message.base64EncodedData)
|
||||||
|
val signature = decode(message.base64EncodedSignature)
|
||||||
|
val publicKey = Hex.fromStringCondensed(sender.removingIdPrefixIfNeeded())
|
||||||
|
val isValid = curve.verifySignature(publicKey, data, signature)
|
||||||
|
if (!isValid) {
|
||||||
|
Log.d("Loki", "Ignoring message with invalid signature.")
|
||||||
|
return@mapNotNull null
|
||||||
|
}
|
||||||
|
message
|
||||||
|
} catch (e: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return messages
|
||||||
|
}
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// region Message Deletion
|
||||||
|
@JvmStatic
|
||||||
|
fun deleteMessage(serverID: Long, room: String, server: String): Promise<Unit, Exception> {
|
||||||
|
val request =
|
||||||
|
Request(verb = DELETE, room = room, server = server, endpoint = Endpoint.RoomMessageIndividual(room, serverID))
|
||||||
|
return send(request).map {
|
||||||
|
Log.d("Loki", "Message deletion successful.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getDeletedMessages(
|
||||||
|
room: String,
|
||||||
|
server: String
|
||||||
|
): Promise<List<MessageDeletion>, Exception> {
|
||||||
|
val storage = MessagingModuleConfiguration.shared.storage
|
||||||
|
val queryParameters = mutableMapOf<String, String>()
|
||||||
|
storage.getLastDeletionServerID(room, server)?.let { last ->
|
||||||
|
queryParameters["from_server_id"] = last.toString()
|
||||||
|
}
|
||||||
|
val request = Request(
|
||||||
|
verb = GET,
|
||||||
|
room = room,
|
||||||
|
server = server,
|
||||||
|
endpoint = Endpoint.RoomDeleteMessages(room, storage.getUserPublicKey() ?: ""),
|
||||||
|
queryParameters = queryParameters
|
||||||
|
)
|
||||||
|
return getResponseBody(request).map { response ->
|
||||||
|
val json = JsonUtil.fromJson(response, Map::class.java)
|
||||||
|
val type = TypeFactory.defaultInstance()
|
||||||
|
.constructCollectionType(List::class.java, MessageDeletion::class.java)
|
||||||
|
val idsAsString = JsonUtil.toJson(json["ids"])
|
||||||
|
val serverIDs = JsonUtil.fromJson<List<MessageDeletion>>(idsAsString, type)
|
||||||
|
?: throw Error.ParsingFailed
|
||||||
|
val lastMessageServerId = storage.getLastDeletionServerID(room, server) ?: 0
|
||||||
|
val serverID = serverIDs.maxByOrNull { it.id } ?: MessageDeletion.empty
|
||||||
|
if (serverID.id > lastMessageServerId) {
|
||||||
|
storage.setLastDeletionServerID(room, server, serverID.id)
|
||||||
|
}
|
||||||
|
serverIDs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// region Moderation
|
||||||
|
@JvmStatic
|
||||||
|
fun ban(publicKey: String, room: String, server: String): Promise<Unit, Exception> {
|
||||||
|
val parameters = mapOf("rooms" to listOf(room))
|
||||||
|
val request = Request(
|
||||||
|
verb = POST,
|
||||||
|
room = room,
|
||||||
|
server = server,
|
||||||
|
endpoint = Endpoint.UserBan(publicKey),
|
||||||
|
parameters = parameters
|
||||||
|
)
|
||||||
|
return send(request).map {
|
||||||
|
Log.d("Loki", "Banned user: $publicKey from: $server.$room.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun banAndDeleteAll(publicKey: String, room: String, server: String): Promise<Unit, Exception> {
|
||||||
|
val requests = mutableListOf<BatchRequestInfo<*>>(
|
||||||
|
BatchRequestInfo(
|
||||||
|
request = BatchRequest(
|
||||||
|
method = POST,
|
||||||
|
path = "/user/$publicKey/ban",
|
||||||
|
json = mapOf("rooms" to listOf(room))
|
||||||
|
),
|
||||||
|
endpoint = Endpoint.UserBan(publicKey),
|
||||||
|
responseType = object: TypeReference<Any>(){}
|
||||||
|
),
|
||||||
|
BatchRequestInfo(
|
||||||
|
request = BatchRequest(DELETE, "/room/$room/all/$publicKey"),
|
||||||
|
endpoint = Endpoint.RoomDeleteMessages(room, publicKey),
|
||||||
|
responseType = object: TypeReference<Any>(){}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return sequentialBatch(server, requests).map {
|
||||||
|
Log.d("Loki", "Banned user: $publicKey from: $server.$room.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun unban(publicKey: String, room: String, server: String): Promise<Unit, Exception> {
|
||||||
|
val request =
|
||||||
|
Request(verb = DELETE, room = room, server = server, endpoint = Endpoint.UserUnban(publicKey))
|
||||||
|
return send(request).map {
|
||||||
|
Log.d("Loki", "Unbanned user: $publicKey from: $server.$room")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// region General
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
fun poll(
|
||||||
|
rooms: List<String>,
|
||||||
|
server: String
|
||||||
|
): Promise<List<BatchResponse<*>>, Exception> {
|
||||||
|
val storage = MessagingModuleConfiguration.shared.storage
|
||||||
|
val context = MessagingModuleConfiguration.shared.context
|
||||||
|
val timeSinceLastOpen = this.timeSinceLastOpen
|
||||||
|
val shouldRetrieveRecentMessages = (hasPerformedInitialPoll[server] != true
|
||||||
|
&& timeSinceLastOpen > maxInactivityPeriod)
|
||||||
|
hasPerformedInitialPoll[server] = true
|
||||||
|
if (!hasUpdatedLastOpenDate) {
|
||||||
|
hasUpdatedLastOpenDate = true
|
||||||
|
TextSecurePreferences.setLastOpenDate(context)
|
||||||
|
}
|
||||||
|
val lastInboxMessageId = storage.getLastInboxMessageId(server)
|
||||||
|
val lastOutboxMessageId = storage.getLastOutboxMessageId(server)
|
||||||
|
val requests = mutableListOf<BatchRequestInfo<*>>(
|
||||||
|
BatchRequestInfo(
|
||||||
|
request = BatchRequest(
|
||||||
|
method = GET,
|
||||||
|
path = "/capabilities"
|
||||||
|
),
|
||||||
|
endpoint = Endpoint.Capabilities,
|
||||||
|
responseType = object : TypeReference<Capabilities>(){}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
rooms.forEach { room ->
|
||||||
|
val infoUpdates = storage.getOpenGroup(room, server)?.infoUpdates ?: 0
|
||||||
|
requests.add(
|
||||||
|
BatchRequestInfo(
|
||||||
|
request = BatchRequest(
|
||||||
|
method = GET,
|
||||||
|
path = "/room/$room/pollInfo/$infoUpdates"
|
||||||
|
),
|
||||||
|
endpoint = Endpoint.RoomPollInfo(room, infoUpdates),
|
||||||
|
responseType = object : TypeReference<RoomPollInfo>(){}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
requests.add(
|
||||||
|
if (shouldRetrieveRecentMessages) {
|
||||||
|
BatchRequestInfo(
|
||||||
|
request = BatchRequest(
|
||||||
|
method = GET,
|
||||||
|
path = "/room/$room/messages/recent"
|
||||||
|
),
|
||||||
|
endpoint = Endpoint.RoomMessagesRecent(room),
|
||||||
|
responseType = object : TypeReference<List<Message>>(){}
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
val lastMessageServerId = storage.getLastMessageServerID(room, server) ?: 0L
|
||||||
|
BatchRequestInfo(
|
||||||
|
request = BatchRequest(
|
||||||
|
method = GET,
|
||||||
|
path = "/room/$room/messages/since/$lastMessageServerId"
|
||||||
|
),
|
||||||
|
endpoint = Endpoint.RoomMessagesSince(room, lastMessageServerId),
|
||||||
|
responseType = object : TypeReference<List<Message>>(){}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val serverCapabilities = storage.getServerCapabilities(server)
|
||||||
|
if (serverCapabilities.contains("blind")) {
|
||||||
|
requests.add(
|
||||||
|
if (lastInboxMessageId == null) {
|
||||||
|
BatchRequestInfo(
|
||||||
|
request = BatchRequest(
|
||||||
|
method = GET,
|
||||||
|
path = "/inbox"
|
||||||
|
),
|
||||||
|
endpoint = Endpoint.Inbox,
|
||||||
|
responseType = object : TypeReference<List<DirectMessage>>() {}
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
BatchRequestInfo(
|
||||||
|
request = BatchRequest(
|
||||||
|
method = GET,
|
||||||
|
path = "/inbox/since/$lastInboxMessageId"
|
||||||
|
),
|
||||||
|
endpoint = Endpoint.InboxSince(lastInboxMessageId),
|
||||||
|
responseType = object : TypeReference<List<DirectMessage>>() {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
requests.add(
|
||||||
|
if (lastOutboxMessageId == null) {
|
||||||
|
BatchRequestInfo(
|
||||||
|
request = BatchRequest(
|
||||||
|
method = GET,
|
||||||
|
path = "/outbox"
|
||||||
|
),
|
||||||
|
endpoint = Endpoint.Outbox,
|
||||||
|
responseType = object : TypeReference<List<DirectMessage>>() {}
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
BatchRequestInfo(
|
||||||
|
request = BatchRequest(
|
||||||
|
method = GET,
|
||||||
|
path = "/outbox/since/$lastOutboxMessageId"
|
||||||
|
),
|
||||||
|
endpoint = Endpoint.OutboxSince(lastOutboxMessageId),
|
||||||
|
responseType = object : TypeReference<List<DirectMessage>>() {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return parallelBatch(server, requests)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parallelBatch(
|
||||||
|
server: String,
|
||||||
|
requests: MutableList<BatchRequestInfo<*>>
|
||||||
|
): Promise<List<BatchResponse<*>>, Exception> {
|
||||||
|
val request = Request(
|
||||||
|
verb = POST,
|
||||||
|
room = null,
|
||||||
|
server = server,
|
||||||
|
endpoint = Endpoint.Batch,
|
||||||
|
parameters = requests.map { it.request }
|
||||||
|
)
|
||||||
|
return getBatchResponseJson(request, requests)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun sequentialBatch(
|
||||||
|
server: String,
|
||||||
|
requests: MutableList<BatchRequestInfo<*>>
|
||||||
|
): Promise<List<BatchResponse<*>>, Exception> {
|
||||||
|
val request = Request(
|
||||||
|
verb = POST,
|
||||||
|
room = null,
|
||||||
|
server = server,
|
||||||
|
endpoint = Endpoint.Sequence,
|
||||||
|
parameters = requests.map { it.request }
|
||||||
|
)
|
||||||
|
return getBatchResponseJson(request, requests)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getBatchResponseJson(
|
||||||
|
request: Request,
|
||||||
|
requests: MutableList<BatchRequestInfo<*>>
|
||||||
|
): Promise<List<BatchResponse<*>>, Exception> {
|
||||||
|
return getResponseBody(request).map { batch ->
|
||||||
|
val results = JsonUtil.fromJson(batch, List::class.java) ?: throw Error.ParsingFailed
|
||||||
|
results.mapIndexed { idx, result ->
|
||||||
|
val response = result as? Map<*, *> ?: throw Error.ParsingFailed
|
||||||
|
val code = response["code"] as Int
|
||||||
|
BatchResponse(
|
||||||
|
endpoint = requests[idx].endpoint,
|
||||||
|
code = code,
|
||||||
|
headers = response["headers"] as Map<String, String>,
|
||||||
|
body = if (code in 200..299) {
|
||||||
|
JsonUtil.toJson(response["body"]).takeIf { it != "[]" }?.let {
|
||||||
|
JsonUtil.fromJson(it, requests[idx].responseType)
|
||||||
|
}
|
||||||
|
} else null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getDefaultServerCapabilities(): Promise<Capabilities, Exception> {
|
||||||
|
val storage = MessagingModuleConfiguration.shared.storage
|
||||||
|
storage.setOpenGroupPublicKey(defaultServer, defaultServerPublicKey)
|
||||||
|
return getCapabilities(defaultServer).map { capabilities ->
|
||||||
|
storage.setServerCapabilities(defaultServer, capabilities.capabilities)
|
||||||
|
capabilities
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getDefaultRoomsIfNeeded(): Promise<List<DefaultGroup>, Exception> {
|
||||||
|
return getAllRooms().map { groups ->
|
||||||
|
val earlyGroups = groups.map { group ->
|
||||||
|
DefaultGroup(group.token, group.name, null)
|
||||||
|
}
|
||||||
|
// See if we have any cached rooms, and if they already have images don't overwrite them with early non-image results
|
||||||
|
defaultRooms.replayCache.firstOrNull()?.let { replayed ->
|
||||||
|
if (replayed.none { it.image?.isNotEmpty() == true }) {
|
||||||
|
defaultRooms.tryEmit(earlyGroups)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val images = groups.associate { group ->
|
||||||
|
group.token to group.imageId?.let { downloadOpenGroupProfilePicture(defaultServer, group.token, it) }
|
||||||
|
}
|
||||||
|
groups.map { group ->
|
||||||
|
val image = try {
|
||||||
|
images[group.token]!!.get()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// No image or image failed to download
|
||||||
|
null
|
||||||
|
}
|
||||||
|
DefaultGroup(group.token, group.name, image)
|
||||||
|
}
|
||||||
|
}.success { new ->
|
||||||
|
defaultRooms.tryEmit(new)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getRoomInfo(roomToken: String, server: String): Promise<RoomInfo, Exception> {
|
||||||
|
val request = Request(
|
||||||
|
verb = GET,
|
||||||
|
room = null,
|
||||||
|
server = server,
|
||||||
|
endpoint = Endpoint.Room(roomToken)
|
||||||
|
)
|
||||||
|
return getResponseBody(request).map { response ->
|
||||||
|
JsonUtil.fromJson(response, RoomInfo::class.java)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getAllRooms(): Promise<List<RoomInfo>, Exception> {
|
||||||
|
val request = Request(
|
||||||
|
verb = GET,
|
||||||
|
room = null,
|
||||||
|
server = defaultServer,
|
||||||
|
endpoint = Endpoint.Rooms
|
||||||
|
)
|
||||||
|
return getResponseBody(request).map { response ->
|
||||||
|
val rawRooms = JsonUtil.fromJson(response, List::class.java) ?: throw Error.ParsingFailed
|
||||||
|
rawRooms.mapNotNull {
|
||||||
|
JsonUtil.fromJson(JsonUtil.toJson(it), RoomInfo::class.java)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getMemberCount(room: String, server: String): Promise<Int, Exception> {
|
||||||
|
return getRoomInfo(room, server).map { info ->
|
||||||
|
val storage = MessagingModuleConfiguration.shared.storage
|
||||||
|
storage.setUserCount(room, server, info.activeUsers)
|
||||||
|
info.activeUsers
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getCapabilities(server: String): Promise<Capabilities, Exception> {
|
||||||
|
val request = Request(verb = GET, room = null, server = server, endpoint = Endpoint.Capabilities, isAuthRequired = false)
|
||||||
|
return getResponseBody(request).map { response ->
|
||||||
|
JsonUtil.fromJson(response, Capabilities::class.java)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getCapabilitiesAndRoomInfo(room: String, server: String): Promise<Pair<Capabilities, RoomInfo>, Exception> {
|
||||||
|
val requests = mutableListOf<BatchRequestInfo<*>>(
|
||||||
|
BatchRequestInfo(
|
||||||
|
request = BatchRequest(
|
||||||
|
method = GET,
|
||||||
|
path = "/capabilities"
|
||||||
|
),
|
||||||
|
endpoint = Endpoint.Capabilities,
|
||||||
|
responseType = object : TypeReference<Capabilities>(){}
|
||||||
|
),
|
||||||
|
BatchRequestInfo(
|
||||||
|
request = BatchRequest(
|
||||||
|
method = GET,
|
||||||
|
path = "/room/$room"
|
||||||
|
),
|
||||||
|
endpoint = Endpoint.Room(room),
|
||||||
|
responseType = object : TypeReference<RoomInfo>(){}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return sequentialBatch(server, requests).map {
|
||||||
|
val capabilities = it.firstOrNull()?.body as? Capabilities ?: throw Error.ParsingFailed
|
||||||
|
val roomInfo = it.lastOrNull()?.body as? RoomInfo ?: throw Error.ParsingFailed
|
||||||
|
capabilities to roomInfo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun sendDirectMessage(message: String, blindedSessionId: String, server: String): Promise<DirectMessage, Exception> {
|
||||||
|
val request = Request(
|
||||||
|
verb = POST,
|
||||||
|
room = null,
|
||||||
|
server = server,
|
||||||
|
endpoint = Endpoint.InboxFor(blindedSessionId),
|
||||||
|
parameters = mapOf("message" to message)
|
||||||
|
)
|
||||||
|
return getResponseBody(request).map { response ->
|
||||||
|
JsonUtil.fromJson(response, DirectMessage::class.java)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// endregion
|
||||||
|
}
|
@ -0,0 +1,93 @@
|
|||||||
|
package org.session.libsession.messaging.open_groups
|
||||||
|
|
||||||
|
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||||
|
import org.session.libsession.messaging.utilities.SodiumUtilities
|
||||||
|
import org.session.libsignal.crypto.PushTransportDetails
|
||||||
|
import org.session.libsignal.protos.SignalServiceProtos
|
||||||
|
import org.session.libsignal.utilities.Base64
|
||||||
|
import org.session.libsignal.utilities.Base64.decode
|
||||||
|
import org.session.libsignal.utilities.IdPrefix
|
||||||
|
import org.session.libsignal.utilities.Log
|
||||||
|
import org.session.libsignal.utilities.removingIdPrefixIfNeeded
|
||||||
|
import org.session.libsignal.utilities.toHexString
|
||||||
|
import org.whispersystems.curve25519.Curve25519
|
||||||
|
|
||||||
|
data class OpenGroupMessage(
|
||||||
|
val serverID: Long? = null,
|
||||||
|
val sender: String?,
|
||||||
|
val sentTimestamp: Long,
|
||||||
|
/**
|
||||||
|
* The serialized protobuf in base64 encoding.
|
||||||
|
*/
|
||||||
|
val base64EncodedData: String,
|
||||||
|
/**
|
||||||
|
* When sending a message, the sender signs the serialized protobuf with their private key so that
|
||||||
|
* a receiving user can verify that the message wasn't tampered with.
|
||||||
|
*/
|
||||||
|
val base64EncodedSignature: String? = null
|
||||||
|
) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val curve = Curve25519.getInstance(Curve25519.BEST)
|
||||||
|
|
||||||
|
fun fromJSON(json: Map<String, Any>): OpenGroupMessage? {
|
||||||
|
val base64EncodedData = json["data"] as? String ?: return null
|
||||||
|
val sentTimestamp = json["posted"] as? Double ?: return null
|
||||||
|
val serverID = json["id"] as? Int
|
||||||
|
val sender = json["session_id"] as? String
|
||||||
|
val base64EncodedSignature = json["signature"] as? String
|
||||||
|
return OpenGroupMessage(
|
||||||
|
serverID = serverID?.toLong(),
|
||||||
|
sender = sender,
|
||||||
|
sentTimestamp = (sentTimestamp * 1000).toLong(),
|
||||||
|
base64EncodedData = base64EncodedData,
|
||||||
|
base64EncodedSignature = base64EncodedSignature
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun sign(room: String, server: String, fallbackSigningType: IdPrefix): OpenGroupMessage? {
|
||||||
|
if (base64EncodedData.isEmpty()) return null
|
||||||
|
val userEdKeyPair = MessagingModuleConfiguration.shared.getUserED25519KeyPair() ?: return null
|
||||||
|
val openGroup = MessagingModuleConfiguration.shared.storage.getOpenGroup(room, server) ?: return null
|
||||||
|
val serverCapabilities = MessagingModuleConfiguration.shared.storage.getServerCapabilities(server)
|
||||||
|
val signature = when {
|
||||||
|
serverCapabilities.contains("blind") -> {
|
||||||
|
val blindedKeyPair = SodiumUtilities.blindedKeyPair(openGroup.publicKey, userEdKeyPair) ?: return null
|
||||||
|
SodiumUtilities.sogsSignature(
|
||||||
|
decode(base64EncodedData),
|
||||||
|
userEdKeyPair.secretKey.asBytes,
|
||||||
|
blindedKeyPair.secretKey.asBytes,
|
||||||
|
blindedKeyPair.publicKey.asBytes
|
||||||
|
) ?: return null
|
||||||
|
}
|
||||||
|
fallbackSigningType == IdPrefix.UN_BLINDED -> {
|
||||||
|
curve.calculateSignature(userEdKeyPair.secretKey.asBytes, decode(base64EncodedData))
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
val (publicKey, privateKey) = MessagingModuleConfiguration.shared.storage.getUserX25519KeyPair().let { it.publicKey.serialize() to it.privateKey.serialize() }
|
||||||
|
if (sender != publicKey.toHexString() && !userEdKeyPair.publicKey.asHexString.equals(sender?.removingIdPrefixIfNeeded(), true)) return null
|
||||||
|
try {
|
||||||
|
curve.calculateSignature(privateKey, decode(base64EncodedData))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w("Loki", "Couldn't sign open group message.", e)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return copy(base64EncodedSignature = Base64.encodeBytes(signature))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toJSON(): Map<String, Any> {
|
||||||
|
val json = mutableMapOf( "data" to base64EncodedData, "timestamp" to sentTimestamp )
|
||||||
|
serverID?.let { json["server_id"] = it }
|
||||||
|
sender?.let { json["public_key"] = it }
|
||||||
|
base64EncodedSignature?.let { json["signature"] = it }
|
||||||
|
return json
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toProto(): SignalServiceProtos.Content {
|
||||||
|
val data = decode(base64EncodedData).let(PushTransportDetails::getStrippedPaddingMessageBody)
|
||||||
|
return SignalServiceProtos.Content.parseFrom(data)
|
||||||
|
}
|
||||||
|
}
|
@ -1,72 +0,0 @@
|
|||||||
package org.session.libsession.messaging.open_groups
|
|
||||||
|
|
||||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
|
||||||
import org.session.libsignal.crypto.PushTransportDetails
|
|
||||||
import org.session.libsignal.protos.SignalServiceProtos
|
|
||||||
import org.session.libsignal.utilities.Base64
|
|
||||||
import org.session.libsignal.utilities.Base64.decode
|
|
||||||
import org.session.libsignal.utilities.Log
|
|
||||||
import org.session.libsignal.utilities.toHexString
|
|
||||||
import org.whispersystems.curve25519.Curve25519
|
|
||||||
|
|
||||||
data class OpenGroupMessageV2(
|
|
||||||
val serverID: Long? = null,
|
|
||||||
val sender: String?,
|
|
||||||
val sentTimestamp: Long,
|
|
||||||
/**
|
|
||||||
* The serialized protobuf in base64 encoding.
|
|
||||||
*/
|
|
||||||
val base64EncodedData: String,
|
|
||||||
/**
|
|
||||||
* When sending a message, the sender signs the serialized protobuf with their private key so that
|
|
||||||
* a receiving user can verify that the message wasn't tampered with.
|
|
||||||
*/
|
|
||||||
val base64EncodedSignature: String? = null
|
|
||||||
) {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private val curve = Curve25519.getInstance(Curve25519.BEST)
|
|
||||||
|
|
||||||
fun fromJSON(json: Map<String, Any>): OpenGroupMessageV2? {
|
|
||||||
val base64EncodedData = json["data"] as? String ?: return null
|
|
||||||
val sentTimestamp = json["timestamp"] as? Long ?: return null
|
|
||||||
val serverID = json["server_id"] as? Int
|
|
||||||
val sender = json["public_key"] as? String
|
|
||||||
val base64EncodedSignature = json["signature"] as? String
|
|
||||||
return OpenGroupMessageV2(
|
|
||||||
serverID = serverID?.toLong(),
|
|
||||||
sender = sender,
|
|
||||||
sentTimestamp = sentTimestamp,
|
|
||||||
base64EncodedData = base64EncodedData,
|
|
||||||
base64EncodedSignature = base64EncodedSignature
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
fun sign(): OpenGroupMessageV2? {
|
|
||||||
if (base64EncodedData.isEmpty()) return null
|
|
||||||
val (publicKey, privateKey) = MessagingModuleConfiguration.shared.storage.getUserX25519KeyPair().let { it.publicKey to it.privateKey }
|
|
||||||
if (sender != publicKey.serialize().toHexString()) return null
|
|
||||||
val signature = try {
|
|
||||||
curve.calculateSignature(privateKey.serialize(), decode(base64EncodedData))
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.w("Loki", "Couldn't sign open group message.", e)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
return copy(base64EncodedSignature = Base64.encodeBytes(signature))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun toJSON(): Map<String, Any> {
|
|
||||||
val json = mutableMapOf( "data" to base64EncodedData, "timestamp" to sentTimestamp )
|
|
||||||
serverID?.let { json["server_id"] = it }
|
|
||||||
sender?.let { json["public_key"] = it }
|
|
||||||
base64EncodedSignature?.let { json["signature"] = it }
|
|
||||||
return json
|
|
||||||
}
|
|
||||||
|
|
||||||
fun toProto(): SignalServiceProtos.Content {
|
|
||||||
val data = decode(base64EncodedData).let(PushTransportDetails::getStrippedPaddingMessageBody)
|
|
||||||
return SignalServiceProtos.Content.parseFrom(data)
|
|
||||||
}
|
|
||||||
}
|
|
@ -5,11 +5,15 @@ import com.goterl.lazysodium.LazySodiumAndroid
|
|||||||
import com.goterl.lazysodium.SodiumAndroid
|
import com.goterl.lazysodium.SodiumAndroid
|
||||||
import com.goterl.lazysodium.interfaces.Box
|
import com.goterl.lazysodium.interfaces.Box
|
||||||
import com.goterl.lazysodium.interfaces.Sign
|
import com.goterl.lazysodium.interfaces.Sign
|
||||||
|
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||||
|
import org.session.libsession.messaging.sending_receiving.MessageReceiver.Error
|
||||||
|
import org.session.libsession.messaging.utilities.SessionId
|
||||||
|
import org.session.libsession.messaging.utilities.SodiumUtilities
|
||||||
import org.session.libsignal.crypto.ecc.ECKeyPair
|
import org.session.libsignal.crypto.ecc.ECKeyPair
|
||||||
import org.session.libsignal.utilities.Hex
|
import org.session.libsignal.utilities.Hex
|
||||||
|
import org.session.libsignal.utilities.IdPrefix
|
||||||
import org.session.libsignal.utilities.hexEncodedPublicKey
|
import org.session.libsignal.utilities.hexEncodedPublicKey
|
||||||
import org.session.libsignal.utilities.removing05PrefixIfNeeded
|
import org.session.libsignal.utilities.removingIdPrefixIfNeeded
|
||||||
import org.session.libsignal.utilities.toHexString
|
|
||||||
|
|
||||||
object MessageDecrypter {
|
object MessageDecrypter {
|
||||||
|
|
||||||
@ -25,7 +29,7 @@ object MessageDecrypter {
|
|||||||
*/
|
*/
|
||||||
public fun decrypt(ciphertext: ByteArray, x25519KeyPair: ECKeyPair): Pair<ByteArray, String> {
|
public fun decrypt(ciphertext: ByteArray, x25519KeyPair: ECKeyPair): Pair<ByteArray, String> {
|
||||||
val recipientX25519PrivateKey = x25519KeyPair.privateKey.serialize()
|
val recipientX25519PrivateKey = x25519KeyPair.privateKey.serialize()
|
||||||
val recipientX25519PublicKey = Hex.fromStringCondensed(x25519KeyPair.hexEncodedPublicKey.removing05PrefixIfNeeded())
|
val recipientX25519PublicKey = Hex.fromStringCondensed(x25519KeyPair.hexEncodedPublicKey.removingIdPrefixIfNeeded())
|
||||||
val signatureSize = Sign.BYTES
|
val signatureSize = Sign.BYTES
|
||||||
val ed25519PublicKeySize = Sign.PUBLICKEYBYTES
|
val ed25519PublicKeySize = Sign.PUBLICKEYBYTES
|
||||||
|
|
||||||
@ -35,9 +39,9 @@ object MessageDecrypter {
|
|||||||
sodium.cryptoBoxSealOpen(plaintextWithMetadata, ciphertext, ciphertext.size.toLong(), recipientX25519PublicKey, recipientX25519PrivateKey)
|
sodium.cryptoBoxSealOpen(plaintextWithMetadata, ciphertext, ciphertext.size.toLong(), recipientX25519PublicKey, recipientX25519PrivateKey)
|
||||||
} catch (exception: Exception) {
|
} catch (exception: Exception) {
|
||||||
Log.d("Loki", "Couldn't decrypt message due to error: $exception.")
|
Log.d("Loki", "Couldn't decrypt message due to error: $exception.")
|
||||||
throw MessageReceiver.Error.DecryptionFailed
|
throw Error.DecryptionFailed
|
||||||
}
|
}
|
||||||
if (plaintextWithMetadata.size <= (signatureSize + ed25519PublicKeySize)) { throw MessageReceiver.Error.DecryptionFailed }
|
if (plaintextWithMetadata.size <= (signatureSize + ed25519PublicKeySize)) { throw Error.DecryptionFailed }
|
||||||
// 2. ) Get the message parts
|
// 2. ) Get the message parts
|
||||||
val signature = plaintextWithMetadata.sliceArray(plaintextWithMetadata.size - signatureSize until plaintextWithMetadata.size)
|
val signature = plaintextWithMetadata.sliceArray(plaintextWithMetadata.size - signatureSize until plaintextWithMetadata.size)
|
||||||
val senderED25519PublicKey = plaintextWithMetadata.sliceArray(plaintextWithMetadata.size - (signatureSize + ed25519PublicKeySize) until plaintextWithMetadata.size - signatureSize)
|
val senderED25519PublicKey = plaintextWithMetadata.sliceArray(plaintextWithMetadata.size - (signatureSize + ed25519PublicKeySize) until plaintextWithMetadata.size - signatureSize)
|
||||||
@ -46,15 +50,62 @@ object MessageDecrypter {
|
|||||||
val verificationData = (plaintext + senderED25519PublicKey + recipientX25519PublicKey)
|
val verificationData = (plaintext + senderED25519PublicKey + recipientX25519PublicKey)
|
||||||
try {
|
try {
|
||||||
val isValid = sodium.cryptoSignVerifyDetached(signature, verificationData, verificationData.size, senderED25519PublicKey)
|
val isValid = sodium.cryptoSignVerifyDetached(signature, verificationData, verificationData.size, senderED25519PublicKey)
|
||||||
if (!isValid) { throw MessageReceiver.Error.InvalidSignature }
|
if (!isValid) { throw Error.InvalidSignature }
|
||||||
} catch (exception: Exception) {
|
} catch (exception: Exception) {
|
||||||
Log.d("Loki", "Couldn't verify message signature due to error: $exception.")
|
Log.d("Loki", "Couldn't verify message signature due to error: $exception.")
|
||||||
throw MessageReceiver.Error.InvalidSignature
|
throw Error.InvalidSignature
|
||||||
}
|
}
|
||||||
// 4. ) Get the sender's X25519 public key
|
// 4. ) Get the sender's X25519 public key
|
||||||
val senderX25519PublicKey = ByteArray(Sign.CURVE25519_PUBLICKEYBYTES)
|
val senderX25519PublicKey = ByteArray(Sign.CURVE25519_PUBLICKEYBYTES)
|
||||||
sodium.convertPublicKeyEd25519ToCurve25519(senderX25519PublicKey, senderED25519PublicKey)
|
sodium.convertPublicKeyEd25519ToCurve25519(senderX25519PublicKey, senderED25519PublicKey)
|
||||||
|
|
||||||
return Pair(plaintext, "05" + senderX25519PublicKey.toHexString())
|
val id = SessionId(IdPrefix.STANDARD, senderX25519PublicKey)
|
||||||
|
return Pair(plaintext, id.hexString)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun decryptBlinded(
|
||||||
|
message: ByteArray,
|
||||||
|
isOutgoing: Boolean,
|
||||||
|
otherBlindedPublicKey: String,
|
||||||
|
serverPublicKey: String
|
||||||
|
): Pair<ByteArray, String> {
|
||||||
|
if (message.size < Box.NONCEBYTES + 2) throw Error.DecryptionFailed
|
||||||
|
val userEdKeyPair = MessagingModuleConfiguration.shared.getUserED25519KeyPair() ?: throw Error.NoUserED25519KeyPair
|
||||||
|
val blindedKeyPair = SodiumUtilities.blindedKeyPair(serverPublicKey, userEdKeyPair) ?: throw Error.DecryptionFailed
|
||||||
|
// Calculate the shared encryption key, receiving from A to B
|
||||||
|
val otherKeyBytes = Hex.fromStringCondensed(otherBlindedPublicKey.removingIdPrefixIfNeeded())
|
||||||
|
val kA = if (isOutgoing) blindedKeyPair.publicKey.asBytes else otherKeyBytes
|
||||||
|
val decryptionKey = SodiumUtilities.sharedBlindedEncryptionKey(
|
||||||
|
userEdKeyPair.secretKey.asBytes,
|
||||||
|
otherKeyBytes,
|
||||||
|
kA,
|
||||||
|
if (isOutgoing) otherKeyBytes else blindedKeyPair.publicKey.asBytes
|
||||||
|
) ?: throw Error.DecryptionFailed
|
||||||
|
|
||||||
|
// v, ct, nc = data[0], data[1:-24], data[-24:size]
|
||||||
|
val version = message.first().toInt()
|
||||||
|
if (version != 0) throw Error.DecryptionFailed
|
||||||
|
val ciphertext = message.drop(1).dropLast(Box.NONCEBYTES).toByteArray()
|
||||||
|
val nonce = message.takeLast(Box.NONCEBYTES).toByteArray()
|
||||||
|
|
||||||
|
// Decrypt the message
|
||||||
|
val innerBytes = SodiumUtilities.decrypt(ciphertext, decryptionKey, nonce) ?: throw Error.DecryptionFailed
|
||||||
|
if (innerBytes.size < Sign.PUBLICKEYBYTES) throw Error.DecryptionFailed
|
||||||
|
|
||||||
|
// Split up: the last 32 bytes are the sender's *unblinded* ed25519 key
|
||||||
|
val plaintextEndIndex = innerBytes.size - Sign.PUBLICKEYBYTES
|
||||||
|
val plaintext = innerBytes.slice(0 until plaintextEndIndex).toByteArray()
|
||||||
|
val senderEdPublicKey = innerBytes.slice((plaintextEndIndex until innerBytes.size)).toByteArray()
|
||||||
|
|
||||||
|
// Verify that the inner senderEdPublicKey (A) yields the same outer kA we got with the message
|
||||||
|
val blindingFactor = SodiumUtilities.generateBlindingFactor(serverPublicKey) ?: throw Error.DecryptionFailed
|
||||||
|
val sharedSecret = SodiumUtilities.combineKeys(blindingFactor, senderEdPublicKey) ?: throw Error.DecryptionFailed
|
||||||
|
if (!kA.contentEquals(sharedSecret)) throw Error.InvalidSignature
|
||||||
|
|
||||||
|
// Get the sender's X25519 public key
|
||||||
|
val senderX25519PublicKey = SodiumUtilities.toX25519(senderEdPublicKey) ?: throw Error.InvalidSignature
|
||||||
|
|
||||||
|
val id = SessionId(IdPrefix.STANDARD, senderX25519PublicKey)
|
||||||
|
return Pair(plaintext, id.hexString)
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -6,9 +6,11 @@ import com.goterl.lazysodium.interfaces.Box
|
|||||||
import com.goterl.lazysodium.interfaces.Sign
|
import com.goterl.lazysodium.interfaces.Sign
|
||||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||||
import org.session.libsession.messaging.sending_receiving.MessageSender.Error
|
import org.session.libsession.messaging.sending_receiving.MessageSender.Error
|
||||||
|
import org.session.libsession.messaging.utilities.SodiumUtilities
|
||||||
import org.session.libsignal.utilities.Hex
|
import org.session.libsignal.utilities.Hex
|
||||||
|
import org.session.libsignal.utilities.IdPrefix
|
||||||
import org.session.libsignal.utilities.Log
|
import org.session.libsignal.utilities.Log
|
||||||
import org.session.libsignal.utilities.removing05PrefixIfNeeded
|
import org.session.libsignal.utilities.removingIdPrefixIfNeeded
|
||||||
|
|
||||||
object MessageEncrypter {
|
object MessageEncrypter {
|
||||||
|
|
||||||
@ -24,7 +26,7 @@ object MessageEncrypter {
|
|||||||
*/
|
*/
|
||||||
internal fun encrypt(plaintext: ByteArray, recipientHexEncodedX25519PublicKey: String): ByteArray {
|
internal fun encrypt(plaintext: ByteArray, recipientHexEncodedX25519PublicKey: String): ByteArray {
|
||||||
val userED25519KeyPair = MessagingModuleConfiguration.shared.getUserED25519KeyPair() ?: throw Error.NoUserED25519KeyPair
|
val userED25519KeyPair = MessagingModuleConfiguration.shared.getUserED25519KeyPair() ?: throw Error.NoUserED25519KeyPair
|
||||||
val recipientX25519PublicKey = Hex.fromStringCondensed(recipientHexEncodedX25519PublicKey.removing05PrefixIfNeeded())
|
val recipientX25519PublicKey = Hex.fromStringCondensed(recipientHexEncodedX25519PublicKey.removingIdPrefixIfNeeded())
|
||||||
|
|
||||||
val verificationData = plaintext + userED25519KeyPair.publicKey.asBytes + recipientX25519PublicKey
|
val verificationData = plaintext + userED25519KeyPair.publicKey.asBytes + recipientX25519PublicKey
|
||||||
val signature = ByteArray(Sign.BYTES)
|
val signature = ByteArray(Sign.BYTES)
|
||||||
@ -46,4 +48,33 @@ object MessageEncrypter {
|
|||||||
return ciphertext
|
return ciphertext
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal fun encryptBlinded(
|
||||||
|
plaintext: ByteArray,
|
||||||
|
recipientBlindedId: String,
|
||||||
|
serverPublicKey: String
|
||||||
|
): ByteArray {
|
||||||
|
if (IdPrefix.fromValue(recipientBlindedId) != IdPrefix.BLINDED) throw Error.SigningFailed
|
||||||
|
val userEdKeyPair =
|
||||||
|
MessagingModuleConfiguration.shared.getUserED25519KeyPair() ?: throw Error.NoUserED25519KeyPair
|
||||||
|
val blindedKeyPair = SodiumUtilities.blindedKeyPair(serverPublicKey, userEdKeyPair) ?: throw Error.SigningFailed
|
||||||
|
val recipientBlindedPublicKey = Hex.fromStringCondensed(recipientBlindedId.removingIdPrefixIfNeeded())
|
||||||
|
|
||||||
|
// Calculate the shared encryption key, sending from A to B
|
||||||
|
val encryptionKey = SodiumUtilities.sharedBlindedEncryptionKey(
|
||||||
|
userEdKeyPair.secretKey.asBytes,
|
||||||
|
recipientBlindedPublicKey,
|
||||||
|
blindedKeyPair.publicKey.asBytes,
|
||||||
|
recipientBlindedPublicKey
|
||||||
|
) ?: throw Error.SigningFailed
|
||||||
|
|
||||||
|
// Inner data: msg || A (i.e. the sender's ed25519 master pubkey, *not* kA blinded pubkey)
|
||||||
|
val message = plaintext + userEdKeyPair.publicKey.asBytes
|
||||||
|
|
||||||
|
// Encrypt using xchacha20-poly1305
|
||||||
|
val nonce = sodium.nonce(24)
|
||||||
|
val ciphertext = SodiumUtilities.encrypt(message, encryptionKey, nonce) ?: throw Error.EncryptionFailed
|
||||||
|
// data = b'\x00' + ciphertext + nonce
|
||||||
|
return byteArrayOf(0.toByte()) + ciphertext + nonce
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -12,8 +12,11 @@ import org.session.libsession.messaging.messages.control.ReadReceipt
|
|||||||
import org.session.libsession.messaging.messages.control.TypingIndicator
|
import org.session.libsession.messaging.messages.control.TypingIndicator
|
||||||
import org.session.libsession.messaging.messages.control.UnsendRequest
|
import org.session.libsession.messaging.messages.control.UnsendRequest
|
||||||
import org.session.libsession.messaging.messages.visible.VisibleMessage
|
import org.session.libsession.messaging.messages.visible.VisibleMessage
|
||||||
|
import org.session.libsession.messaging.utilities.SessionId
|
||||||
|
import org.session.libsession.messaging.utilities.SodiumUtilities
|
||||||
import org.session.libsignal.crypto.PushTransportDetails
|
import org.session.libsignal.crypto.PushTransportDetails
|
||||||
import org.session.libsignal.protos.SignalServiceProtos
|
import org.session.libsignal.protos.SignalServiceProtos
|
||||||
|
import org.session.libsignal.utilities.IdPrefix
|
||||||
import org.session.libsignal.utilities.Log
|
import org.session.libsignal.utilities.Log
|
||||||
|
|
||||||
object MessageReceiver {
|
object MessageReceiver {
|
||||||
@ -31,6 +34,7 @@ object MessageReceiver {
|
|||||||
object SelfSend: Error("Message addressed at self.")
|
object SelfSend: Error("Message addressed at self.")
|
||||||
object InvalidGroupPublicKey: Error("Invalid group public key.")
|
object InvalidGroupPublicKey: Error("Invalid group public key.")
|
||||||
object NoGroupKeyPair: Error("Missing group key pair.")
|
object NoGroupKeyPair: Error("Missing group key pair.")
|
||||||
|
object NoUserED25519KeyPair : Error("Couldn't find user ED25519 key pair.")
|
||||||
|
|
||||||
internal val isRetryable: Boolean = when (this) {
|
internal val isRetryable: Boolean = when (this) {
|
||||||
is DuplicateMessage, is InvalidMessage, is UnknownMessage,
|
is DuplicateMessage, is InvalidMessage, is UnknownMessage,
|
||||||
@ -40,7 +44,13 @@ object MessageReceiver {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun parse(data: ByteArray, openGroupServerID: Long?): Pair<Message, SignalServiceProtos.Content> {
|
internal fun parse(
|
||||||
|
data: ByteArray,
|
||||||
|
openGroupServerID: Long?,
|
||||||
|
isOutgoing: Boolean? = null,
|
||||||
|
otherBlindedPublicKey: String? = null,
|
||||||
|
openGroupPublicKey: String? = null,
|
||||||
|
): Pair<Message, SignalServiceProtos.Content> {
|
||||||
val storage = MessagingModuleConfiguration.shared.storage
|
val storage = MessagingModuleConfiguration.shared.storage
|
||||||
val userPublicKey = storage.getUserPublicKey()
|
val userPublicKey = storage.getUserPublicKey()
|
||||||
val isOpenGroupMessage = (openGroupServerID != null)
|
val isOpenGroupMessage = (openGroupServerID != null)
|
||||||
@ -59,11 +69,24 @@ object MessageReceiver {
|
|||||||
} else {
|
} else {
|
||||||
when (envelope.type) {
|
when (envelope.type) {
|
||||||
SignalServiceProtos.Envelope.Type.SESSION_MESSAGE -> {
|
SignalServiceProtos.Envelope.Type.SESSION_MESSAGE -> {
|
||||||
|
if (IdPrefix.fromValue(envelope.source) == IdPrefix.BLINDED) {
|
||||||
|
openGroupPublicKey ?: throw Error.InvalidGroupPublicKey
|
||||||
|
otherBlindedPublicKey ?: throw Error.DecryptionFailed
|
||||||
|
val decryptionResult = MessageDecrypter.decryptBlinded(
|
||||||
|
ciphertext.toByteArray(),
|
||||||
|
isOutgoing ?: false,
|
||||||
|
otherBlindedPublicKey,
|
||||||
|
openGroupPublicKey
|
||||||
|
)
|
||||||
|
plaintext = decryptionResult.first
|
||||||
|
sender = decryptionResult.second
|
||||||
|
} else {
|
||||||
val userX25519KeyPair = MessagingModuleConfiguration.shared.storage.getUserX25519KeyPair()
|
val userX25519KeyPair = MessagingModuleConfiguration.shared.storage.getUserX25519KeyPair()
|
||||||
val decryptionResult = MessageDecrypter.decrypt(ciphertext.toByteArray(), userX25519KeyPair)
|
val decryptionResult = MessageDecrypter.decrypt(ciphertext.toByteArray(), userX25519KeyPair)
|
||||||
plaintext = decryptionResult.first
|
plaintext = decryptionResult.first
|
||||||
sender = decryptionResult.second
|
sender = decryptionResult.second
|
||||||
}
|
}
|
||||||
|
}
|
||||||
SignalServiceProtos.Envelope.Type.CLOSED_GROUP_MESSAGE -> {
|
SignalServiceProtos.Envelope.Type.CLOSED_GROUP_MESSAGE -> {
|
||||||
val hexEncodedGroupPublicKey = envelope.source
|
val hexEncodedGroupPublicKey = envelope.source
|
||||||
if (hexEncodedGroupPublicKey == null || !MessagingModuleConfiguration.shared.storage.isClosedGroup(hexEncodedGroupPublicKey)) {
|
if (hexEncodedGroupPublicKey == null || !MessagingModuleConfiguration.shared.storage.isClosedGroup(hexEncodedGroupPublicKey)) {
|
||||||
@ -118,8 +141,9 @@ object MessageReceiver {
|
|||||||
VisibleMessage.fromProto(proto) ?: run {
|
VisibleMessage.fromProto(proto) ?: run {
|
||||||
throw Error.UnknownMessage
|
throw Error.UnknownMessage
|
||||||
}
|
}
|
||||||
|
val isUserBlindedSender = sender == openGroupPublicKey?.let { SodiumUtilities.blindedKeyPair(it, MessagingModuleConfiguration.shared.getUserED25519KeyPair()!!) }?.let { SessionId(IdPrefix.BLINDED, it.publicKey.asBytes).hexString }
|
||||||
// Ignore self send if needed
|
// Ignore self send if needed
|
||||||
if (!message.isSelfSendValid && sender == userPublicKey) {
|
if (!message.isSelfSendValid && (sender == userPublicKey || isUserBlindedSender)) {
|
||||||
throw Error.SelfSend
|
throw Error.SelfSend
|
||||||
}
|
}
|
||||||
// Guard against control messages in open groups
|
// Guard against control messages in open groups
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package org.session.libsession.messaging.sending_receiving
|
package org.session.libsession.messaging.sending_receiving
|
||||||
|
|
||||||
|
import com.goterl.lazysodium.utils.KeyPair
|
||||||
import nl.komponents.kovenant.Promise
|
import nl.komponents.kovenant.Promise
|
||||||
import nl.komponents.kovenant.deferred
|
import nl.komponents.kovenant.deferred
|
||||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||||
@ -17,9 +18,11 @@ import org.session.libsession.messaging.messages.visible.LinkPreview
|
|||||||
import org.session.libsession.messaging.messages.visible.Profile
|
import org.session.libsession.messaging.messages.visible.Profile
|
||||||
import org.session.libsession.messaging.messages.visible.Quote
|
import org.session.libsession.messaging.messages.visible.Quote
|
||||||
import org.session.libsession.messaging.messages.visible.VisibleMessage
|
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.open_groups.OpenGroupMessageV2
|
import org.session.libsession.messaging.open_groups.OpenGroupMessage
|
||||||
import org.session.libsession.messaging.utilities.MessageWrapper
|
import org.session.libsession.messaging.utilities.MessageWrapper
|
||||||
|
import org.session.libsession.messaging.utilities.SessionId
|
||||||
|
import org.session.libsession.messaging.utilities.SodiumUtilities
|
||||||
import org.session.libsession.snode.RawResponsePromise
|
import org.session.libsession.snode.RawResponsePromise
|
||||||
import org.session.libsession.snode.SnodeAPI
|
import org.session.libsession.snode.SnodeAPI
|
||||||
import org.session.libsession.snode.SnodeMessage
|
import org.session.libsession.snode.SnodeMessage
|
||||||
@ -30,10 +33,12 @@ import org.session.libsession.utilities.SSKEnvironment
|
|||||||
import org.session.libsignal.crypto.PushTransportDetails
|
import org.session.libsignal.crypto.PushTransportDetails
|
||||||
import org.session.libsignal.protos.SignalServiceProtos
|
import org.session.libsignal.protos.SignalServiceProtos
|
||||||
import org.session.libsignal.utilities.Base64
|
import org.session.libsignal.utilities.Base64
|
||||||
|
import org.session.libsignal.utilities.IdPrefix
|
||||||
import org.session.libsignal.utilities.Namespace
|
import org.session.libsignal.utilities.Namespace
|
||||||
import org.session.libsignal.utilities.defaultRequiresAuth
|
import org.session.libsignal.utilities.defaultRequiresAuth
|
||||||
import org.session.libsignal.utilities.hasNamespaces
|
import org.session.libsignal.utilities.hasNamespaces
|
||||||
import org.session.libsignal.utilities.hexEncodedPublicKey
|
import org.session.libsignal.utilities.hexEncodedPublicKey
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
import java.util.concurrent.atomic.AtomicInteger
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
import org.session.libsession.messaging.sending_receiving.attachments.Attachment as SignalAttachment
|
import org.session.libsession.messaging.sending_receiving.attachments.Attachment as SignalAttachment
|
||||||
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview as SignalLinkPreview
|
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview as SignalLinkPreview
|
||||||
@ -62,10 +67,10 @@ object MessageSender {
|
|||||||
|
|
||||||
// Convenience
|
// Convenience
|
||||||
fun send(message: Message, destination: Destination): Promise<Unit, Exception> {
|
fun send(message: Message, destination: Destination): Promise<Unit, Exception> {
|
||||||
if (destination is Destination.OpenGroupV2) {
|
return if (destination is Destination.LegacyOpenGroup || destination is Destination.OpenGroup || destination is Destination.OpenGroupInbox) {
|
||||||
return sendToOpenGroupDestination(destination, message)
|
sendToOpenGroupDestination(destination, message)
|
||||||
} else {
|
} else {
|
||||||
return sendToSnodeDestination(destination, message)
|
sendToSnodeDestination(destination, message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -96,7 +101,7 @@ object MessageSender {
|
|||||||
when (destination) {
|
when (destination) {
|
||||||
is Destination.Contact -> message.recipient = destination.publicKey
|
is Destination.Contact -> message.recipient = destination.publicKey
|
||||||
is Destination.ClosedGroup -> message.recipient = destination.groupPublicKey
|
is Destination.ClosedGroup -> message.recipient = destination.groupPublicKey
|
||||||
is Destination.OpenGroupV2 -> throw IllegalStateException("Destination should not be an open group.")
|
else -> throw IllegalStateException("Destination should not be an open group.")
|
||||||
}
|
}
|
||||||
// Validate the message
|
// Validate the message
|
||||||
if (!message.isValid()) { throw Error.InvalidMessage }
|
if (!message.isValid()) { throw Error.InvalidMessage }
|
||||||
@ -127,14 +132,13 @@ object MessageSender {
|
|||||||
// Serialize the protobuf
|
// Serialize the protobuf
|
||||||
val plaintext = PushTransportDetails.getPaddedMessageBody(proto.toByteArray())
|
val plaintext = PushTransportDetails.getPaddedMessageBody(proto.toByteArray())
|
||||||
// Encrypt the serialized protobuf
|
// Encrypt the serialized protobuf
|
||||||
val ciphertext: ByteArray
|
val ciphertext = when (destination) {
|
||||||
when (destination) {
|
is Destination.Contact -> MessageEncrypter.encrypt(plaintext, destination.publicKey)
|
||||||
is Destination.Contact -> ciphertext = MessageEncrypter.encrypt(plaintext, destination.publicKey)
|
|
||||||
is Destination.ClosedGroup -> {
|
is Destination.ClosedGroup -> {
|
||||||
val encryptionKeyPair = MessagingModuleConfiguration.shared.storage.getLatestClosedGroupEncryptionKeyPair(destination.groupPublicKey)!!
|
val encryptionKeyPair = MessagingModuleConfiguration.shared.storage.getLatestClosedGroupEncryptionKeyPair(destination.groupPublicKey)!!
|
||||||
ciphertext = MessageEncrypter.encrypt(plaintext, encryptionKeyPair.hexEncodedPublicKey)
|
MessageEncrypter.encrypt(plaintext, encryptionKeyPair.hexEncodedPublicKey)
|
||||||
}
|
}
|
||||||
is Destination.OpenGroupV2 -> throw IllegalStateException("Destination should not be open group.")
|
else -> throw IllegalStateException("Destination should not be open group.")
|
||||||
}
|
}
|
||||||
// Wrap the result
|
// Wrap the result
|
||||||
val kind: SignalServiceProtos.Envelope.Type
|
val kind: SignalServiceProtos.Envelope.Type
|
||||||
@ -157,7 +161,7 @@ object MessageSender {
|
|||||||
kind = SignalServiceProtos.Envelope.Type.CLOSED_GROUP_MESSAGE
|
kind = SignalServiceProtos.Envelope.Type.CLOSED_GROUP_MESSAGE
|
||||||
senderPublicKey = destination.groupPublicKey
|
senderPublicKey = destination.groupPublicKey
|
||||||
}
|
}
|
||||||
is Destination.OpenGroupV2 -> throw IllegalStateException("Destination should not be open group.")
|
else -> throw IllegalStateException("Destination should not be open group.")
|
||||||
}
|
}
|
||||||
val wrappedMessage = MessageWrapper.wrap(kind, message.sentTimestamp!!, senderPublicKey, ciphertext)
|
val wrappedMessage = MessageWrapper.wrap(kind, message.sentTimestamp!!, senderPublicKey, ciphertext)
|
||||||
// Send the result
|
// Send the result
|
||||||
@ -174,7 +178,7 @@ object MessageSender {
|
|||||||
namespaces.map { namespace -> SnodeAPI.sendMessage(snodeMessage, requiresAuth = false, namespace = namespace) }.let { promises ->
|
namespaces.map { namespace -> SnodeAPI.sendMessage(snodeMessage, requiresAuth = false, namespace = namespace) }.let { promises ->
|
||||||
var isSuccess = false
|
var isSuccess = false
|
||||||
val promiseCount = promises.size
|
val promiseCount = promises.size
|
||||||
var errorCount = AtomicInteger(0)
|
val errorCount = AtomicInteger(0)
|
||||||
promises.forEach { promise: RawResponsePromise ->
|
promises.forEach { promise: RawResponsePromise ->
|
||||||
promise.success {
|
promise.success {
|
||||||
if (isSuccess) { return@success } // Succeed as soon as the first promise succeeds
|
if (isSuccess) { return@success } // Succeed as soon as the first promise succeeds
|
||||||
@ -217,19 +221,40 @@ object MessageSender {
|
|||||||
if (message.sentTimestamp == null) {
|
if (message.sentTimestamp == null) {
|
||||||
message.sentTimestamp = System.currentTimeMillis()
|
message.sentTimestamp = System.currentTimeMillis()
|
||||||
}
|
}
|
||||||
message.sender = storage.getUserPublicKey()
|
val userEdKeyPair = MessagingModuleConfiguration.shared.getUserED25519KeyPair()!!
|
||||||
|
var serverCapabilities = listOf<String>()
|
||||||
|
var blindedPublicKey: ByteArray? = null
|
||||||
|
when(destination) {
|
||||||
|
is Destination.OpenGroup -> {
|
||||||
|
serverCapabilities = storage.getServerCapabilities(destination.server)
|
||||||
|
storage.getOpenGroup(destination.roomToken, destination.server)?.let {
|
||||||
|
blindedPublicKey = SodiumUtilities.blindedKeyPair(it.publicKey, userEdKeyPair)?.publicKey?.asBytes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is Destination.OpenGroupInbox -> {
|
||||||
|
serverCapabilities = storage.getServerCapabilities(destination.server)
|
||||||
|
blindedPublicKey = SodiumUtilities.blindedKeyPair(destination.serverPublicKey, userEdKeyPair)?.publicKey?.asBytes
|
||||||
|
}
|
||||||
|
is Destination.LegacyOpenGroup -> {
|
||||||
|
serverCapabilities = storage.getServerCapabilities(destination.server)
|
||||||
|
storage.getOpenGroup(destination.roomToken, destination.server)?.let {
|
||||||
|
blindedPublicKey = SodiumUtilities.blindedKeyPair(it.publicKey, userEdKeyPair)?.publicKey?.asBytes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
val messageSender = if (serverCapabilities.contains("blind") && blindedPublicKey != null) {
|
||||||
|
SessionId(IdPrefix.BLINDED, blindedPublicKey!!).hexString
|
||||||
|
} else {
|
||||||
|
SessionId(IdPrefix.UN_BLINDED, userEdKeyPair.publicKey.asBytes).hexString
|
||||||
|
}
|
||||||
|
message.sender = messageSender
|
||||||
// Set the failure handler (need it here already for precondition failure handling)
|
// Set the failure handler (need it here already for precondition failure handling)
|
||||||
fun handleFailure(error: Exception) {
|
fun handleFailure(error: Exception) {
|
||||||
handleFailedMessageSend(message, error)
|
handleFailedMessageSend(message, error)
|
||||||
deferred.reject(error)
|
deferred.reject(error)
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
when (destination) {
|
|
||||||
is Destination.Contact, is Destination.ClosedGroup -> throw IllegalStateException("Invalid destination.")
|
|
||||||
is Destination.OpenGroupV2 -> {
|
|
||||||
message.recipient = "${destination.server}.${destination.room}"
|
|
||||||
val server = destination.server
|
|
||||||
val room = destination.room
|
|
||||||
// Attach the user's profile if needed
|
// Attach the user's profile if needed
|
||||||
if (message is VisibleMessage) {
|
if (message is VisibleMessage) {
|
||||||
val displayName = storage.getUserDisplayName()!!
|
val displayName = storage.getUserDisplayName()!!
|
||||||
@ -241,18 +266,22 @@ object MessageSender {
|
|||||||
message.profile = Profile(displayName)
|
message.profile = Profile(displayName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
when (destination) {
|
||||||
|
is Destination.OpenGroup -> {
|
||||||
|
val whisperMods = if (destination.whisperTo.isNullOrEmpty() && destination.whisperMods) "mods" else null
|
||||||
|
message.recipient = "${destination.server}.${destination.roomToken}.${destination.whisperTo}.$whisperMods"
|
||||||
// Validate the message
|
// Validate the message
|
||||||
if (message !is VisibleMessage || !message.isValid()) {
|
if (message !is VisibleMessage || !message.isValid()) {
|
||||||
throw Error.InvalidMessage
|
throw Error.InvalidMessage
|
||||||
}
|
}
|
||||||
val proto = message.toProto()!!
|
val messageBody = message.toProto()?.toByteArray()!!
|
||||||
val plaintext = PushTransportDetails.getPaddedMessageBody(proto.toByteArray())
|
val plaintext = PushTransportDetails.getPaddedMessageBody(messageBody)
|
||||||
val openGroupMessage = OpenGroupMessageV2(
|
val openGroupMessage = OpenGroupMessage(
|
||||||
sender = message.sender,
|
sender = message.sender,
|
||||||
sentTimestamp = message.sentTimestamp!!,
|
sentTimestamp = message.sentTimestamp!!,
|
||||||
base64EncodedData = Base64.encodeBytes(plaintext),
|
base64EncodedData = Base64.encodeBytes(plaintext),
|
||||||
)
|
)
|
||||||
OpenGroupAPIV2.send(openGroupMessage,room,server).success {
|
OpenGroupApi.sendMessage(openGroupMessage, destination.roomToken, destination.server, destination.whisperTo, destination.whisperMods, destination.fileIds).success {
|
||||||
message.openGroupServerMessageID = it.serverID
|
message.openGroupServerMessageID = it.serverID
|
||||||
handleSuccessfulMessageSend(message, destination, openGroupSentTimestamp = it.sentTimestamp)
|
handleSuccessfulMessageSend(message, destination, openGroupSentTimestamp = it.sentTimestamp)
|
||||||
deferred.resolve(Unit)
|
deferred.resolve(Unit)
|
||||||
@ -260,6 +289,29 @@ object MessageSender {
|
|||||||
handleFailure(it)
|
handleFailure(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
is Destination.OpenGroupInbox -> {
|
||||||
|
message.recipient = destination.blindedPublicKey
|
||||||
|
// Validate the message
|
||||||
|
if (message !is VisibleMessage || !message.isValid()) {
|
||||||
|
throw Error.InvalidMessage
|
||||||
|
}
|
||||||
|
val messageBody = message.toProto()?.toByteArray()!!
|
||||||
|
val plaintext = PushTransportDetails.getPaddedMessageBody(messageBody)
|
||||||
|
val ciphertext = MessageEncrypter.encryptBlinded(
|
||||||
|
plaintext,
|
||||||
|
destination.blindedPublicKey,
|
||||||
|
destination.serverPublicKey
|
||||||
|
)
|
||||||
|
val base64EncodedData = Base64.encodeBytes(ciphertext)
|
||||||
|
OpenGroupApi.sendDirectMessage(base64EncodedData, destination.blindedPublicKey, destination.server).success {
|
||||||
|
message.openGroupServerMessageID = it.id
|
||||||
|
handleSuccessfulMessageSend(message, destination, openGroupSentTimestamp = TimeUnit.SECONDS.toMillis(it.postedAt))
|
||||||
|
deferred.resolve(Unit)
|
||||||
|
}.fail {
|
||||||
|
handleFailure(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> throw IllegalStateException("Invalid destination.")
|
||||||
}
|
}
|
||||||
} catch (exception: Exception) {
|
} catch (exception: Exception) {
|
||||||
handleFailure(exception)
|
handleFailure(exception)
|
||||||
@ -273,7 +325,7 @@ object MessageSender {
|
|||||||
val userPublicKey = storage.getUserPublicKey()!!
|
val userPublicKey = storage.getUserPublicKey()!!
|
||||||
// Ignore future self-sends
|
// Ignore future self-sends
|
||||||
storage.addReceivedMessageTimestamp(message.sentTimestamp!!)
|
storage.addReceivedMessageTimestamp(message.sentTimestamp!!)
|
||||||
storage.getMessageIdInDatabase(message.sentTimestamp!!, message.sender?:userPublicKey)?.let { messageID ->
|
storage.getMessageIdInDatabase(message.sentTimestamp!!, userPublicKey)?.let { messageID ->
|
||||||
if (openGroupSentTimestamp != -1L && message is VisibleMessage) {
|
if (openGroupSentTimestamp != -1L && message is VisibleMessage) {
|
||||||
storage.addReceivedMessageTimestamp(openGroupSentTimestamp)
|
storage.addReceivedMessageTimestamp(openGroupSentTimestamp)
|
||||||
storage.updateSentTimestamp(messageID, message.isMediaMessage(), openGroupSentTimestamp, message.threadID!!)
|
storage.updateSentTimestamp(messageID, message.isMediaMessage(), openGroupSentTimestamp, message.threadID!!)
|
||||||
@ -286,19 +338,19 @@ object MessageSender {
|
|||||||
storage.setMessageServerHash(messageID, it)
|
storage.setMessageServerHash(messageID, it)
|
||||||
}
|
}
|
||||||
// Track the open group server message ID
|
// Track the open group server message ID
|
||||||
if (message.openGroupServerMessageID != null && destination is Destination.OpenGroupV2) {
|
if (message.openGroupServerMessageID != null && destination is Destination.LegacyOpenGroup) {
|
||||||
val encoded = GroupUtil.getEncodedOpenGroupID("${destination.server}.${destination.room}".toByteArray())
|
val encoded = GroupUtil.getEncodedOpenGroupID("${destination.server}.${destination.roomToken}".toByteArray())
|
||||||
val threadID = storage.getThreadId(Address.fromSerialized(encoded))
|
val threadID = storage.getThreadId(Address.fromSerialized(encoded))
|
||||||
if (threadID != null && threadID >= 0) {
|
if (threadID != null && threadID >= 0) {
|
||||||
storage.setOpenGroupServerMessageID(messageID, message.openGroupServerMessageID!!, threadID, !(message as VisibleMessage).isMediaMessage())
|
storage.setOpenGroupServerMessageID(messageID, message.openGroupServerMessageID!!, threadID, !(message as VisibleMessage).isMediaMessage())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Mark the message as sent
|
// Mark the message as sent
|
||||||
storage.markAsSent(message.sentTimestamp!!, message.sender?:userPublicKey)
|
storage.markAsSent(message.sentTimestamp!!, userPublicKey)
|
||||||
storage.markUnidentified(message.sentTimestamp!!, message.sender?:userPublicKey)
|
storage.markUnidentified(message.sentTimestamp!!, userPublicKey)
|
||||||
// Start the disappearing messages timer if needed
|
// Start the disappearing messages timer if needed
|
||||||
if (message is VisibleMessage && !isSyncMessage) {
|
if (message is VisibleMessage && !isSyncMessage) {
|
||||||
SSKEnvironment.shared.messageExpirationManager.startAnyExpiration(message.sentTimestamp!!, message.sender?:userPublicKey)
|
SSKEnvironment.shared.messageExpirationManager.startAnyExpiration(message.sentTimestamp!!, userPublicKey)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Sync the message if:
|
// Sync the message if:
|
||||||
|
@ -21,7 +21,7 @@ import org.session.libsignal.utilities.guava.Optional
|
|||||||
import org.session.libsignal.messages.SignalServiceGroup
|
import org.session.libsignal.messages.SignalServiceGroup
|
||||||
import org.session.libsignal.protos.SignalServiceProtos
|
import org.session.libsignal.protos.SignalServiceProtos
|
||||||
import org.session.libsignal.utilities.hexEncodedPublicKey
|
import org.session.libsignal.utilities.hexEncodedPublicKey
|
||||||
import org.session.libsignal.utilities.removing05PrefixIfNeeded
|
import org.session.libsignal.utilities.removingIdPrefixIfNeeded
|
||||||
import org.session.libsignal.utilities.Hex
|
import org.session.libsignal.utilities.Hex
|
||||||
import org.session.libsignal.utilities.ThreadUtils
|
import org.session.libsignal.utilities.ThreadUtils
|
||||||
import org.session.libsignal.utilities.Log
|
import org.session.libsignal.utilities.Log
|
||||||
@ -290,7 +290,7 @@ fun MessageSender.generateAndSendNewEncryptionKeyPair(groupPublicKey: String, ta
|
|||||||
fun MessageSender.sendEncryptionKeyPair(groupPublicKey: String, newKeyPair: ECKeyPair, targetMembers: Collection<String>, targetUser: String? = null, force: Boolean = true): Promise<Unit, Exception>? {
|
fun MessageSender.sendEncryptionKeyPair(groupPublicKey: String, newKeyPair: ECKeyPair, targetMembers: Collection<String>, targetUser: String? = null, force: Boolean = true): Promise<Unit, Exception>? {
|
||||||
val destination = targetUser ?: GroupUtil.doubleEncodeGroupID(groupPublicKey)
|
val destination = targetUser ?: GroupUtil.doubleEncodeGroupID(groupPublicKey)
|
||||||
val proto = SignalServiceProtos.KeyPair.newBuilder()
|
val proto = SignalServiceProtos.KeyPair.newBuilder()
|
||||||
proto.publicKey = ByteString.copyFrom(newKeyPair.publicKey.serialize().removing05PrefixIfNeeded())
|
proto.publicKey = ByteString.copyFrom(newKeyPair.publicKey.serialize().removingIdPrefixIfNeeded())
|
||||||
proto.privateKey = ByteString.copyFrom(newKeyPair.privateKey.serialize())
|
proto.privateKey = ByteString.copyFrom(newKeyPair.privateKey.serialize())
|
||||||
val plaintext = proto.build().toByteArray()
|
val plaintext = proto.build().toByteArray()
|
||||||
val wrappers = targetMembers.map { publicKey ->
|
val wrappers = targetMembers.map { publicKey ->
|
||||||
@ -326,7 +326,7 @@ fun MessageSender.sendLatestEncryptionKeyPair(publicKey: String, groupPublicKey:
|
|||||||
?: storage.getLatestClosedGroupEncryptionKeyPair(groupPublicKey) ?: return
|
?: storage.getLatestClosedGroupEncryptionKeyPair(groupPublicKey) ?: return
|
||||||
// Send it
|
// Send it
|
||||||
val proto = SignalServiceProtos.KeyPair.newBuilder()
|
val proto = SignalServiceProtos.KeyPair.newBuilder()
|
||||||
proto.publicKey = ByteString.copyFrom(encryptionKeyPair.publicKey.serialize().removing05PrefixIfNeeded())
|
proto.publicKey = ByteString.copyFrom(encryptionKeyPair.publicKey.serialize().removingIdPrefixIfNeeded())
|
||||||
proto.privateKey = ByteString.copyFrom(encryptionKeyPair.privateKey.serialize())
|
proto.privateKey = ByteString.copyFrom(encryptionKeyPair.privateKey.serialize())
|
||||||
val plaintext = proto.build().toByteArray()
|
val plaintext = proto.build().toByteArray()
|
||||||
val ciphertext = MessageEncrypter.encrypt(plaintext, publicKey)
|
val ciphertext = MessageEncrypter.encrypt(plaintext, publicKey)
|
||||||
|
@ -23,6 +23,8 @@ import org.session.libsession.messaging.sending_receiving.link_preview.LinkPrevi
|
|||||||
import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI
|
import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI
|
||||||
import org.session.libsession.messaging.sending_receiving.pollers.ClosedGroupPollerV2
|
import org.session.libsession.messaging.sending_receiving.pollers.ClosedGroupPollerV2
|
||||||
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel
|
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.WebRtcUtils
|
import org.session.libsession.messaging.utilities.WebRtcUtils
|
||||||
import org.session.libsession.snode.SnodeAPI
|
import org.session.libsession.snode.SnodeAPI
|
||||||
import org.session.libsession.utilities.Address
|
import org.session.libsession.utilities.Address
|
||||||
@ -38,9 +40,10 @@ import org.session.libsignal.crypto.ecc.ECKeyPair
|
|||||||
import org.session.libsignal.messages.SignalServiceGroup
|
import org.session.libsignal.messages.SignalServiceGroup
|
||||||
import org.session.libsignal.protos.SignalServiceProtos
|
import org.session.libsignal.protos.SignalServiceProtos
|
||||||
import org.session.libsignal.utilities.Base64
|
import org.session.libsignal.utilities.Base64
|
||||||
|
import org.session.libsignal.utilities.IdPrefix
|
||||||
import org.session.libsignal.utilities.Log
|
import org.session.libsignal.utilities.Log
|
||||||
import org.session.libsignal.utilities.guava.Optional
|
import org.session.libsignal.utilities.guava.Optional
|
||||||
import org.session.libsignal.utilities.removing05PrefixIfNeeded
|
import org.session.libsignal.utilities.removingIdPrefixIfNeeded
|
||||||
import org.session.libsignal.utilities.toHexString
|
import org.session.libsignal.utilities.toHexString
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
import java.util.LinkedList
|
import java.util.LinkedList
|
||||||
@ -153,7 +156,7 @@ private fun handleConfigurationMessage(message: ConfigurationMessage) {
|
|||||||
closedGroup.encryptionKeyPair!!, closedGroup.members, closedGroup.admins, message.sentTimestamp!!, closedGroup.expirationTimer)
|
closedGroup.encryptionKeyPair!!, closedGroup.members, closedGroup.admins, message.sentTimestamp!!, closedGroup.expirationTimer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val allV2OpenGroups = storage.getAllV2OpenGroups().map { it.value.joinURL }
|
val allV2OpenGroups = storage.getAllOpenGroups().map { it.value.joinURL }
|
||||||
for (openGroup in message.openGroups) {
|
for (openGroup in message.openGroups) {
|
||||||
if (allV2OpenGroups.contains(openGroup)) continue
|
if (allV2OpenGroups.contains(openGroup)) continue
|
||||||
Log.d("OpenGroup", "All open groups doesn't contain $openGroup")
|
Log.d("OpenGroup", "All open groups doesn't contain $openGroup")
|
||||||
@ -216,8 +219,7 @@ fun MessageReceiver.handleVisibleMessage(message: VisibleMessage,
|
|||||||
// Get or create thread
|
// Get or create thread
|
||||||
// FIXME: In case this is an open group this actually * doesn't * create the thread if it doesn't yet
|
// 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.
|
// exist. This is intentional, but it's very non-obvious.
|
||||||
val threadID = storage.getOrCreateThreadIdFor(message.syncTarget
|
val threadID = storage.getOrCreateThreadIdFor(message.syncTarget ?: messageSender!!, message.groupPublicKey, openGroupID)
|
||||||
?: messageSender!!, message.groupPublicKey, openGroupID)
|
|
||||||
if (threadID < 0) {
|
if (threadID < 0) {
|
||||||
// Thread doesn't exist; should only be reached in a case where we are processing open group messages for a no longer existent thread
|
// 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
|
||||||
@ -226,7 +228,9 @@ fun MessageReceiver.handleVisibleMessage(message: VisibleMessage,
|
|||||||
val recipient = Recipient.from(context, Address.fromSerialized(messageSender!!), false)
|
val recipient = Recipient.from(context, Address.fromSerialized(messageSender!!), false)
|
||||||
if (runProfileUpdate) {
|
if (runProfileUpdate) {
|
||||||
val profile = message.profile
|
val profile = message.profile
|
||||||
if (profile != null && userPublicKey != messageSender) {
|
val isUserBlindedSender = messageSender == storage.getOpenGroup(threadID)?.publicKey?.let { SodiumUtilities.blindedKeyPair(it, MessagingModuleConfiguration.shared.getUserED25519KeyPair()!!) }?.let { SessionId(
|
||||||
|
IdPrefix.BLINDED, it.publicKey.asBytes).hexString }
|
||||||
|
if (profile != null && userPublicKey != messageSender && !isUserBlindedSender) {
|
||||||
val profileManager = SSKEnvironment.shared.profileManager
|
val profileManager = SSKEnvironment.shared.profileManager
|
||||||
val name = profile.displayName!!
|
val name = profile.displayName!!
|
||||||
if (name.isNotEmpty()) {
|
if (name.isNotEmpty()) {
|
||||||
@ -395,7 +399,7 @@ private fun MessageReceiver.handleClosedGroupEncryptionKeyPair(message: ClosedGr
|
|||||||
val plaintext = MessageDecrypter.decrypt(encryptedKeyPair, userKeyPair).first
|
val plaintext = MessageDecrypter.decrypt(encryptedKeyPair, userKeyPair).first
|
||||||
// Parse it
|
// Parse it
|
||||||
val proto = SignalServiceProtos.KeyPair.parseFrom(plaintext)
|
val proto = SignalServiceProtos.KeyPair.parseFrom(plaintext)
|
||||||
val keyPair = ECKeyPair(DjbECPublicKey(proto.publicKey.toByteArray().removing05PrefixIfNeeded()), DjbECPrivateKey(proto.privateKey.toByteArray()))
|
val keyPair = ECKeyPair(DjbECPublicKey(proto.publicKey.toByteArray().removingIdPrefixIfNeeded()), DjbECPrivateKey(proto.privateKey.toByteArray()))
|
||||||
// Store it if needed
|
// Store it if needed
|
||||||
val closedGroupEncryptionKeyPairs = storage.getClosedGroupEncryptionKeyPairs(groupPublicKey)
|
val closedGroupEncryptionKeyPairs = storage.getClosedGroupEncryptionKeyPairs(groupPublicKey)
|
||||||
if (closedGroupEncryptionKeyPairs.contains(keyPair)) {
|
if (closedGroupEncryptionKeyPairs.contains(keyPair)) {
|
||||||
|
@ -7,6 +7,7 @@ import okhttp3.Request
|
|||||||
import okhttp3.RequestBody
|
import okhttp3.RequestBody
|
||||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||||
import org.session.libsession.snode.OnionRequestAPI
|
import org.session.libsession.snode.OnionRequestAPI
|
||||||
|
import org.session.libsession.snode.Version
|
||||||
import org.session.libsession.utilities.TextSecurePreferences
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
import org.session.libsignal.utilities.retryIfNeeded
|
import org.session.libsignal.utilities.retryIfNeeded
|
||||||
import org.session.libsignal.utilities.JsonUtil
|
import org.session.libsignal.utilities.JsonUtil
|
||||||
@ -38,12 +39,12 @@ object PushNotificationAPI {
|
|||||||
val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters))
|
val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters))
|
||||||
val request = Request.Builder().url(url).post(body)
|
val request = Request.Builder().url(url).post(body)
|
||||||
retryIfNeeded(maxRetryCount) {
|
retryIfNeeded(maxRetryCount) {
|
||||||
OnionRequestAPI.sendOnionRequest(request.build(), server, serverPublicKey, "/loki/v2/lsrpc").map { json ->
|
OnionRequestAPI.sendOnionRequest(request.build(), server, serverPublicKey, Version.V2).map { response ->
|
||||||
val code = json["code"] as? Int
|
val code = response.info["code"] as? Int
|
||||||
if (code != null && code != 0) {
|
if (code != null && code != 0) {
|
||||||
TextSecurePreferences.setIsUsingFCM(context, false)
|
TextSecurePreferences.setIsUsingFCM(context, false)
|
||||||
} else {
|
} else {
|
||||||
Log.d("Loki", "Couldn't disable FCM due to error: ${json["message"] as? String ?: "null"}.")
|
Log.d("Loki", "Couldn't disable FCM due to error: ${response.info["message"] as? String ?: "null"}.")
|
||||||
}
|
}
|
||||||
}.fail { exception ->
|
}.fail { exception ->
|
||||||
Log.d("Loki", "Couldn't disable FCM due to error: ${exception}.")
|
Log.d("Loki", "Couldn't disable FCM due to error: ${exception}.")
|
||||||
@ -66,14 +67,14 @@ object PushNotificationAPI {
|
|||||||
val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters))
|
val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters))
|
||||||
val request = Request.Builder().url(url).post(body)
|
val request = Request.Builder().url(url).post(body)
|
||||||
retryIfNeeded(maxRetryCount) {
|
retryIfNeeded(maxRetryCount) {
|
||||||
OnionRequestAPI.sendOnionRequest(request.build(), server, serverPublicKey, "/loki/v2/lsrpc").map { json ->
|
OnionRequestAPI.sendOnionRequest(request.build(), server, serverPublicKey, Version.V2).map { response ->
|
||||||
val code = json["code"] as? Int
|
val code = response.info["code"] as? Int
|
||||||
if (code != null && code != 0) {
|
if (code != null && code != 0) {
|
||||||
TextSecurePreferences.setIsUsingFCM(context, true)
|
TextSecurePreferences.setIsUsingFCM(context, true)
|
||||||
TextSecurePreferences.setFCMToken(context, token)
|
TextSecurePreferences.setFCMToken(context, token)
|
||||||
TextSecurePreferences.setLastFCMUploadTime(context, System.currentTimeMillis())
|
TextSecurePreferences.setLastFCMUploadTime(context, System.currentTimeMillis())
|
||||||
} else {
|
} else {
|
||||||
Log.d("Loki", "Couldn't register for FCM due to error: ${json["message"] as? String ?: "null"}.")
|
Log.d("Loki", "Couldn't register for FCM due to error: ${response.info["message"] as? String ?: "null"}.")
|
||||||
}
|
}
|
||||||
}.fail { exception ->
|
}.fail { exception ->
|
||||||
Log.d("Loki", "Couldn't register for FCM due to error: ${exception}.")
|
Log.d("Loki", "Couldn't register for FCM due to error: ${exception}.")
|
||||||
@ -93,10 +94,10 @@ object PushNotificationAPI {
|
|||||||
val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters))
|
val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters))
|
||||||
val request = Request.Builder().url(url).post(body)
|
val request = Request.Builder().url(url).post(body)
|
||||||
retryIfNeeded(maxRetryCount) {
|
retryIfNeeded(maxRetryCount) {
|
||||||
OnionRequestAPI.sendOnionRequest(request.build(), server, serverPublicKey, "/loki/v2/lsrpc").map { json ->
|
OnionRequestAPI.sendOnionRequest(request.build(), server, serverPublicKey, Version.V2).map { response ->
|
||||||
val code = json["code"] as? Int
|
val code = response.info["code"] as? Int
|
||||||
if (code == null || code == 0) {
|
if (code == null || code == 0) {
|
||||||
Log.d("Loki", "Couldn't subscribe/unsubscribe closed group: $closedGroupPublicKey due to error: ${json["message"] as? String ?: "null"}.")
|
Log.d("Loki", "Couldn't subscribe/unsubscribe closed group: $closedGroupPublicKey due to error: ${response.info["message"] as? String ?: "null"}.")
|
||||||
}
|
}
|
||||||
}.fail { exception ->
|
}.fail { exception ->
|
||||||
Log.d("Loki", "Couldn't subscribe/unsubscribe closed group: $closedGroupPublicKey due to error: ${exception}.")
|
Log.d("Loki", "Couldn't subscribe/unsubscribe closed group: $closedGroupPublicKey due to error: ${exception}.")
|
||||||
|
@ -0,0 +1,274 @@
|
|||||||
|
package org.session.libsession.messaging.sending_receiving.pollers
|
||||||
|
|
||||||
|
import com.google.protobuf.ByteString
|
||||||
|
import nl.komponents.kovenant.Promise
|
||||||
|
import nl.komponents.kovenant.functional.map
|
||||||
|
import org.session.libsession.messaging.BlindedIdMapping
|
||||||
|
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||||
|
import org.session.libsession.messaging.jobs.BatchMessageReceiveJob
|
||||||
|
import org.session.libsession.messaging.jobs.GroupAvatarDownloadJob
|
||||||
|
import org.session.libsession.messaging.jobs.JobQueue
|
||||||
|
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.control.ExpirationTimerUpdate
|
||||||
|
import org.session.libsession.messaging.messages.visible.VisibleMessage
|
||||||
|
import org.session.libsession.messaging.open_groups.Endpoint
|
||||||
|
import org.session.libsession.messaging.open_groups.GroupMember
|
||||||
|
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.open_groups.OpenGroupMessage
|
||||||
|
import org.session.libsession.messaging.sending_receiving.MessageReceiver
|
||||||
|
import org.session.libsession.messaging.sending_receiving.handle
|
||||||
|
import org.session.libsession.snode.OnionRequestAPI
|
||||||
|
import org.session.libsession.utilities.Address
|
||||||
|
import org.session.libsession.utilities.GroupUtil
|
||||||
|
import org.session.libsignal.protos.SignalServiceProtos
|
||||||
|
import org.session.libsignal.utilities.Base64
|
||||||
|
import org.session.libsignal.utilities.Log
|
||||||
|
import org.session.libsignal.utilities.successBackground
|
||||||
|
import java.util.concurrent.ScheduledExecutorService
|
||||||
|
import java.util.concurrent.ScheduledFuture
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
class OpenGroupPoller(private val server: String, private val executorService: ScheduledExecutorService?) {
|
||||||
|
var hasStarted = false
|
||||||
|
var isCaughtUp = false
|
||||||
|
var secondToLastJob: MessageReceiveJob? = null
|
||||||
|
private var future: ScheduledFuture<*>? = null
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val pollInterval: Long = 4000L
|
||||||
|
const val maxInactivityPeriod = 14 * 24 * 60 * 60 * 1000
|
||||||
|
}
|
||||||
|
|
||||||
|
fun startIfNeeded() {
|
||||||
|
if (hasStarted) { return }
|
||||||
|
hasStarted = true
|
||||||
|
future = executorService?.schedule(::poll, 0, TimeUnit.MILLISECONDS)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun stop() {
|
||||||
|
future?.cancel(false)
|
||||||
|
hasStarted = false
|
||||||
|
}
|
||||||
|
|
||||||
|
fun poll(isPostCapabilitiesRetry: Boolean = false): Promise<Unit, Exception> {
|
||||||
|
val storage = MessagingModuleConfiguration.shared.storage
|
||||||
|
val rooms = storage.getAllOpenGroups().values.filter { it.server == server }.map { it.room }
|
||||||
|
rooms.forEach { downloadGroupAvatarIfNeeded(it) }
|
||||||
|
return OpenGroupApi.poll(rooms, server).successBackground { responses ->
|
||||||
|
responses.filterNot { it.body == null }.forEach { response ->
|
||||||
|
when (response.endpoint) {
|
||||||
|
is Endpoint.Capabilities -> {
|
||||||
|
handleCapabilities(server, response.body as OpenGroupApi.Capabilities)
|
||||||
|
}
|
||||||
|
is Endpoint.RoomPollInfo -> {
|
||||||
|
handleRoomPollInfo(server, response.endpoint.roomToken, response.body as OpenGroupApi.RoomPollInfo)
|
||||||
|
}
|
||||||
|
is Endpoint.RoomMessagesRecent -> {
|
||||||
|
handleMessages(server, response.endpoint.roomToken, response.body as List<OpenGroupApi.Message>)
|
||||||
|
}
|
||||||
|
is Endpoint.RoomMessagesSince -> {
|
||||||
|
handleMessages(server, response.endpoint.roomToken, response.body as List<OpenGroupApi.Message>)
|
||||||
|
}
|
||||||
|
is Endpoint.Inbox, is Endpoint.InboxSince -> {
|
||||||
|
handleDirectMessages(server, false, response.body as List<OpenGroupApi.DirectMessage>)
|
||||||
|
}
|
||||||
|
is Endpoint.Outbox, is Endpoint.OutboxSince -> {
|
||||||
|
handleDirectMessages(server, true, response.body as List<OpenGroupApi.DirectMessage>)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (secondToLastJob == null && !isCaughtUp) {
|
||||||
|
isCaughtUp = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
executorService?.schedule(this@OpenGroupPoller::poll, pollInterval, TimeUnit.MILLISECONDS)
|
||||||
|
}.fail {
|
||||||
|
updateCapabilitiesIfNeeded(isPostCapabilitiesRetry, it)
|
||||||
|
}.map { }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateCapabilitiesIfNeeded(isPostCapabilitiesRetry: Boolean, exception: Exception) {
|
||||||
|
if (exception is OnionRequestAPI.HTTPRequestFailedBlindingRequiredException) {
|
||||||
|
if (!isPostCapabilitiesRetry) {
|
||||||
|
OpenGroupApi.getCapabilities(server).map {
|
||||||
|
handleCapabilities(server, it)
|
||||||
|
}
|
||||||
|
executorService?.schedule({ poll(isPostCapabilitiesRetry = true) }, pollInterval, TimeUnit.MILLISECONDS)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
executorService?.schedule(this@OpenGroupPoller::poll, pollInterval, TimeUnit.MILLISECONDS)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleCapabilities(server: String, capabilities: OpenGroupApi.Capabilities) {
|
||||||
|
val storage = MessagingModuleConfiguration.shared.storage
|
||||||
|
storage.setServerCapabilities(server, capabilities.capabilities)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleRoomPollInfo(
|
||||||
|
server: String,
|
||||||
|
roomToken: String,
|
||||||
|
pollInfo: OpenGroupApi.RoomPollInfo
|
||||||
|
) {
|
||||||
|
val storage = MessagingModuleConfiguration.shared.storage
|
||||||
|
val groupId = "$server.$roomToken"
|
||||||
|
|
||||||
|
val existingOpenGroup = storage.getOpenGroup(roomToken, server)
|
||||||
|
val publicKey = existingOpenGroup?.publicKey ?: return
|
||||||
|
val openGroup = OpenGroup(
|
||||||
|
server = server,
|
||||||
|
room = pollInfo.token,
|
||||||
|
name = pollInfo.details?.name ?: "",
|
||||||
|
infoUpdates = pollInfo.details?.infoUpdates ?: 0,
|
||||||
|
publicKey = publicKey,
|
||||||
|
)
|
||||||
|
// - Open Group changes
|
||||||
|
storage.updateOpenGroup(openGroup)
|
||||||
|
|
||||||
|
// - User Count
|
||||||
|
storage.setUserCount(roomToken, server, pollInfo.activeUsers)
|
||||||
|
|
||||||
|
// - Moderators
|
||||||
|
pollInfo.details?.moderators?.forEach {
|
||||||
|
storage.addGroupMember(GroupMember(groupId, it, GroupMemberRole.MODERATOR))
|
||||||
|
}
|
||||||
|
// - Admins
|
||||||
|
pollInfo.details?.admins?.forEach {
|
||||||
|
storage.addGroupMember(GroupMember(groupId, it, GroupMemberRole.ADMIN))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleMessages(
|
||||||
|
server: String,
|
||||||
|
roomToken: String,
|
||||||
|
messages: List<OpenGroupApi.Message>
|
||||||
|
) {
|
||||||
|
val openGroupId = "$server.$roomToken"
|
||||||
|
val sortedMessages = messages.sortedBy { it.seqno }
|
||||||
|
sortedMessages.maxOfOrNull { it.seqno }?.let {
|
||||||
|
MessagingModuleConfiguration.shared.storage.setLastMessageServerID(roomToken, server, it)
|
||||||
|
}
|
||||||
|
val (deletions, additions) = sortedMessages.partition { it.deleted || it.data.isNullOrBlank() }
|
||||||
|
handleNewMessages(openGroupId, additions.map {
|
||||||
|
OpenGroupMessage(
|
||||||
|
serverID = it.id,
|
||||||
|
sender = it.sessionId,
|
||||||
|
sentTimestamp = (it.posted * 1000).toLong(),
|
||||||
|
base64EncodedData = it.data!!,
|
||||||
|
base64EncodedSignature = it.signature
|
||||||
|
)
|
||||||
|
})
|
||||||
|
handleDeletedMessages(openGroupId, deletions.map { it.id })
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleDirectMessages(
|
||||||
|
server: String,
|
||||||
|
fromOutbox: Boolean,
|
||||||
|
messages: List<OpenGroupApi.DirectMessage>
|
||||||
|
) {
|
||||||
|
if (messages.isEmpty()) return
|
||||||
|
val storage = MessagingModuleConfiguration.shared.storage
|
||||||
|
val serverPublicKey = storage.getOpenGroupPublicKey(server)!!
|
||||||
|
val sortedMessages = messages.sortedBy { it.id }
|
||||||
|
val lastMessageId = sortedMessages.last().id
|
||||||
|
val mappingCache = mutableMapOf<String, BlindedIdMapping>()
|
||||||
|
if (fromOutbox) {
|
||||||
|
storage.setLastOutboxMessageId(server, lastMessageId)
|
||||||
|
} else {
|
||||||
|
storage.setLastInboxMessageId(server, lastMessageId)
|
||||||
|
}
|
||||||
|
sortedMessages.forEach {
|
||||||
|
val encodedMessage = Base64.decode(it.message)
|
||||||
|
val envelope = SignalServiceProtos.Envelope.newBuilder()
|
||||||
|
.setTimestamp(TimeUnit.SECONDS.toMillis(it.postedAt))
|
||||||
|
.setType(SignalServiceProtos.Envelope.Type.SESSION_MESSAGE)
|
||||||
|
.setContent(ByteString.copyFrom(encodedMessage))
|
||||||
|
.setSource(it.sender)
|
||||||
|
.build()
|
||||||
|
try {
|
||||||
|
val (message, proto) = MessageReceiver.parse(
|
||||||
|
envelope.toByteArray(),
|
||||||
|
null,
|
||||||
|
fromOutbox,
|
||||||
|
if (fromOutbox) it.recipient else it.sender,
|
||||||
|
serverPublicKey
|
||||||
|
)
|
||||||
|
if (fromOutbox) {
|
||||||
|
val mapping = mappingCache[it.recipient] ?: storage.getOrCreateBlindedIdMapping(
|
||||||
|
it.recipient,
|
||||||
|
server,
|
||||||
|
serverPublicKey,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
val syncTarget = mapping.sessionId ?: it.recipient
|
||||||
|
if (message is VisibleMessage) {
|
||||||
|
message.syncTarget = syncTarget
|
||||||
|
} else if (message is ExpirationTimerUpdate) {
|
||||||
|
message.syncTarget = syncTarget
|
||||||
|
}
|
||||||
|
mappingCache[it.recipient] = mapping
|
||||||
|
}
|
||||||
|
MessageReceiver.handle(message, proto, null)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("Loki", "Couldn't handle direct message", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleNewMessages(openGroupID: String, messages: List<OpenGroupMessage>) {
|
||||||
|
val storage = MessagingModuleConfiguration.shared.storage
|
||||||
|
val groupID = GroupUtil.getEncodedOpenGroupID(openGroupID.toByteArray())
|
||||||
|
// check thread still exists
|
||||||
|
val threadId = storage.getThreadId(Address.fromSerialized(groupID)) ?: -1
|
||||||
|
val threadExists = threadId >= 0
|
||||||
|
if (!hasStarted || !threadExists) { return }
|
||||||
|
val envelopes = messages.sortedBy { it.serverID!! }.map { message ->
|
||||||
|
val senderPublicKey = message.sender!!
|
||||||
|
val builder = SignalServiceProtos.Envelope.newBuilder()
|
||||||
|
builder.type = SignalServiceProtos.Envelope.Type.SESSION_MESSAGE
|
||||||
|
builder.source = senderPublicKey
|
||||||
|
builder.sourceDevice = 1
|
||||||
|
builder.content = message.toProto().toByteString()
|
||||||
|
builder.timestamp = message.sentTimestamp
|
||||||
|
builder.build() to message.serverID
|
||||||
|
}
|
||||||
|
|
||||||
|
envelopes.chunked(BatchMessageReceiveJob.BATCH_DEFAULT_NUMBER).forEach { list ->
|
||||||
|
val parameters = list.map { (message, serverId) ->
|
||||||
|
MessageReceiveParameters(message.toByteArray(), openGroupMessageServerID = serverId)
|
||||||
|
}
|
||||||
|
JobQueue.shared.add(BatchMessageReceiveJob(parameters, openGroupID))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (envelopes.isNotEmpty()) {
|
||||||
|
JobQueue.shared.add(TrimThreadJob(threadId,openGroupID))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleDeletedMessages(openGroupID: String, serverIds: List<Long>) {
|
||||||
|
val storage = MessagingModuleConfiguration.shared.storage
|
||||||
|
val groupID = GroupUtil.getEncodedOpenGroupID(openGroupID.toByteArray())
|
||||||
|
val threadID = storage.getThreadId(Address.fromSerialized(groupID)) ?: return
|
||||||
|
|
||||||
|
if (serverIds.isNotEmpty()) {
|
||||||
|
val deleteJob = OpenGroupDeleteJob(serverIds.toLongArray(), threadID, openGroupID)
|
||||||
|
JobQueue.shared.add(deleteJob)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun downloadGroupAvatarIfNeeded(room: String) {
|
||||||
|
val storage = MessagingModuleConfiguration.shared.storage
|
||||||
|
if (storage.getGroupAvatarDownloadJob(server, room) != null) return
|
||||||
|
val groupId = GroupUtil.getEncodedOpenGroupID("$server.$room".toByteArray())
|
||||||
|
storage.getGroup(groupId)?.let {
|
||||||
|
if (System.currentTimeMillis() > it.updatedTimestamp + TimeUnit.DAYS.toMillis(7)) {
|
||||||
|
JobQueue.shared.add(GroupAvatarDownloadJob(room, server))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,132 +0,0 @@
|
|||||||
package org.session.libsession.messaging.sending_receiving.pollers
|
|
||||||
|
|
||||||
import nl.komponents.kovenant.Promise
|
|
||||||
import nl.komponents.kovenant.functional.map
|
|
||||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
|
||||||
import org.session.libsession.messaging.jobs.BatchMessageReceiveJob
|
|
||||||
import org.session.libsession.messaging.jobs.GroupAvatarDownloadJob
|
|
||||||
import org.session.libsession.messaging.jobs.JobQueue
|
|
||||||
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.open_groups.OpenGroupAPIV2
|
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroupMessageV2
|
|
||||||
import org.session.libsession.utilities.Address
|
|
||||||
import org.session.libsession.utilities.GroupUtil
|
|
||||||
import org.session.libsignal.protos.SignalServiceProtos
|
|
||||||
import org.session.libsignal.utilities.successBackground
|
|
||||||
import java.util.concurrent.ScheduledExecutorService
|
|
||||||
import java.util.concurrent.ScheduledFuture
|
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
import kotlin.math.max
|
|
||||||
|
|
||||||
class OpenGroupPollerV2(private val server: String, private val executorService: ScheduledExecutorService?) {
|
|
||||||
var hasStarted = false
|
|
||||||
var isCaughtUp = false
|
|
||||||
var secondToLastJob: MessageReceiveJob? = null
|
|
||||||
private var future: ScheduledFuture<*>? = null
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val pollInterval: Long = 4000L
|
|
||||||
const val maxInactivityPeriod = 14 * 24 * 60 * 60 * 1000
|
|
||||||
}
|
|
||||||
|
|
||||||
fun startIfNeeded() {
|
|
||||||
if (hasStarted) { return }
|
|
||||||
hasStarted = true
|
|
||||||
future = executorService?.schedule(::poll, 0, TimeUnit.MILLISECONDS)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun stop() {
|
|
||||||
future?.cancel(false)
|
|
||||||
hasStarted = false
|
|
||||||
}
|
|
||||||
|
|
||||||
fun poll(isBackgroundPoll: Boolean = false): Promise<Unit, Exception> {
|
|
||||||
val storage = MessagingModuleConfiguration.shared.storage
|
|
||||||
val rooms = storage.getAllV2OpenGroups().values.filter { it.server == server }.map { it.room }
|
|
||||||
rooms.forEach { downloadGroupAvatarIfNeeded(it) }
|
|
||||||
return OpenGroupAPIV2.compactPoll(rooms, server).successBackground { responses ->
|
|
||||||
responses.forEach { (room, response) ->
|
|
||||||
val openGroupID = "$server.$room"
|
|
||||||
handleNewMessages(room, openGroupID, response.messages, isBackgroundPoll)
|
|
||||||
handleDeletedMessages(room, openGroupID, response.deletions)
|
|
||||||
if (secondToLastJob == null && !isCaughtUp) {
|
|
||||||
isCaughtUp = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.always {
|
|
||||||
executorService?.schedule(this@OpenGroupPollerV2::poll, pollInterval, TimeUnit.MILLISECONDS)
|
|
||||||
}.map { }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handleNewMessages(room: String, openGroupID: String, messages: List<OpenGroupMessageV2>, isBackgroundPoll: Boolean) {
|
|
||||||
val storage = MessagingModuleConfiguration.shared.storage
|
|
||||||
val groupID = GroupUtil.getEncodedOpenGroupID(openGroupID.toByteArray())
|
|
||||||
// check thread still exists
|
|
||||||
val threadId = storage.getThreadId(Address.fromSerialized(groupID)) ?: -1
|
|
||||||
val threadExists = threadId >= 0
|
|
||||||
if (!hasStarted || !threadExists) { return }
|
|
||||||
val envelopes = messages.sortedBy { it.serverID!! }.map { message ->
|
|
||||||
val senderPublicKey = message.sender!!
|
|
||||||
val builder = SignalServiceProtos.Envelope.newBuilder()
|
|
||||||
builder.type = SignalServiceProtos.Envelope.Type.SESSION_MESSAGE
|
|
||||||
builder.source = senderPublicKey
|
|
||||||
builder.sourceDevice = 1
|
|
||||||
builder.content = message.toProto().toByteString()
|
|
||||||
builder.timestamp = message.sentTimestamp
|
|
||||||
builder.build() to message.serverID
|
|
||||||
}
|
|
||||||
|
|
||||||
envelopes.chunked(BatchMessageReceiveJob.BATCH_DEFAULT_NUMBER).forEach { list ->
|
|
||||||
val parameters = list.map { (message, serverId) ->
|
|
||||||
MessageReceiveParameters(message.toByteArray(), openGroupMessageServerID = serverId)
|
|
||||||
}
|
|
||||||
JobQueue.shared.add(BatchMessageReceiveJob(parameters, openGroupID))
|
|
||||||
}
|
|
||||||
|
|
||||||
if (envelopes.isNotEmpty()) {
|
|
||||||
JobQueue.shared.add(TrimThreadJob(threadId,openGroupID))
|
|
||||||
}
|
|
||||||
|
|
||||||
val indicatedMax = messages.mapNotNull { it.serverID }.maxOrNull() ?: 0
|
|
||||||
val currentLastMessageServerID = storage.getLastMessageServerID(room, server) ?: 0
|
|
||||||
val actualMax = max(indicatedMax, currentLastMessageServerID)
|
|
||||||
if (actualMax > 0 && indicatedMax > currentLastMessageServerID) {
|
|
||||||
storage.setLastMessageServerID(room, server, actualMax)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handleDeletedMessages(room: String, openGroupID: String, deletions: List<OpenGroupAPIV2.MessageDeletion>) {
|
|
||||||
val storage = MessagingModuleConfiguration.shared.storage
|
|
||||||
val groupID = GroupUtil.getEncodedOpenGroupID(openGroupID.toByteArray())
|
|
||||||
val threadID = storage.getThreadId(Address.fromSerialized(groupID)) ?: return
|
|
||||||
|
|
||||||
val serverIds = deletions.map { deletion ->
|
|
||||||
deletion.deletedMessageServerID
|
|
||||||
}
|
|
||||||
|
|
||||||
if (serverIds.isNotEmpty()) {
|
|
||||||
val deleteJob = OpenGroupDeleteJob(serverIds.toLongArray(), threadID, openGroupID)
|
|
||||||
JobQueue.shared.add(deleteJob)
|
|
||||||
}
|
|
||||||
|
|
||||||
val currentMax = storage.getLastDeletionServerID(room, server) ?: 0L
|
|
||||||
val latestMax = deletions.map { it.id }.maxOrNull() ?: 0L
|
|
||||||
if (latestMax > currentMax && latestMax != 0L) {
|
|
||||||
storage.setLastDeletionServerID(room, server, latestMax)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun downloadGroupAvatarIfNeeded(room: String) {
|
|
||||||
val storage = MessagingModuleConfiguration.shared.storage
|
|
||||||
if (storage.getGroupAvatarDownloadJob(server, room) != null) return
|
|
||||||
val groupId = GroupUtil.getEncodedOpenGroupID("$server.$room".toByteArray())
|
|
||||||
storage.getGroup(groupId)?.let {
|
|
||||||
if (System.currentTimeMillis() > it.updatedTimestamp + TimeUnit.DAYS.toMillis(7)) {
|
|
||||||
JobQueue.shared.add(GroupAvatarDownloadJob(room, server))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,251 @@
|
|||||||
|
package org.session.libsession.messaging.utilities
|
||||||
|
|
||||||
|
import com.goterl.lazysodium.LazySodiumAndroid
|
||||||
|
import com.goterl.lazysodium.SodiumAndroid
|
||||||
|
import com.goterl.lazysodium.interfaces.AEAD
|
||||||
|
import com.goterl.lazysodium.interfaces.GenericHash
|
||||||
|
import com.goterl.lazysodium.interfaces.Hash
|
||||||
|
import com.goterl.lazysodium.utils.Key
|
||||||
|
import com.goterl.lazysodium.utils.KeyPair
|
||||||
|
import org.session.libsignal.utilities.Hex
|
||||||
|
import org.session.libsignal.utilities.IdPrefix
|
||||||
|
import org.session.libsignal.utilities.toHexString
|
||||||
|
import org.whispersystems.curve25519.Curve25519
|
||||||
|
import kotlin.experimental.xor
|
||||||
|
|
||||||
|
object SodiumUtilities {
|
||||||
|
private val sodium by lazy { LazySodiumAndroid(SodiumAndroid()) }
|
||||||
|
private val curve by lazy { Curve25519.getInstance(Curve25519.BEST) }
|
||||||
|
|
||||||
|
private const val SCALAR_LENGTH: Int = 32 // crypto_core_ed25519_scalarbytes
|
||||||
|
private const val NO_CLAMP_LENGTH: Int = 32 // crypto_scalarmult_ed25519_bytes
|
||||||
|
private const val SCALAR_MULT_LENGTH: Int = 32 // crypto_scalarmult_bytes
|
||||||
|
private const val PUBLIC_KEY_LENGTH: Int = 32 // crypto_scalarmult_bytes
|
||||||
|
private const val SECRET_KEY_LENGTH: Int = 64 //crypto_sign_secretkeybytes
|
||||||
|
|
||||||
|
/* 64-byte blake2b hash then reduce to get the blinding factor */
|
||||||
|
fun generateBlindingFactor(serverPublicKey: String): ByteArray? {
|
||||||
|
// k = salt.crypto_core_ed25519_scalar_reduce(blake2b(server_pk, digest_size=64).digest())
|
||||||
|
val serverPubKeyData = Hex.fromStringCondensed(serverPublicKey)
|
||||||
|
if (serverPubKeyData.size != PUBLIC_KEY_LENGTH) return null
|
||||||
|
val serverPubKeyHash = ByteArray(GenericHash.BLAKE2B_BYTES_MAX)
|
||||||
|
if (!sodium.cryptoGenericHash(serverPubKeyHash, serverPubKeyHash.size, serverPubKeyData, serverPubKeyData.size.toLong())) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
// Reduce the server public key into an ed25519 scalar (`k`)
|
||||||
|
val x25519PublicKey = ByteArray(SCALAR_LENGTH)
|
||||||
|
sodium.cryptoCoreEd25519ScalarReduce(x25519PublicKey, serverPubKeyHash)
|
||||||
|
return if (x25519PublicKey.any { it.toInt() != 0 }) {
|
||||||
|
x25519PublicKey
|
||||||
|
} else null
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Calculate k*a. To get 'a' (the Ed25519 private key scalar) we call the sodium function to
|
||||||
|
convert to an *x* secret key, which seems wrong--but isn't because converted keys use the
|
||||||
|
same secret scalar secret (and so this is just the most convenient way to get 'a' out of
|
||||||
|
a sodium Ed25519 secret key)
|
||||||
|
*/
|
||||||
|
fun generatePrivateKeyScalar(secretKey: ByteArray): ByteArray? {
|
||||||
|
// a = s.to_curve25519_private_key().encode()
|
||||||
|
val aBytes = ByteArray(SCALAR_MULT_LENGTH)
|
||||||
|
return if (sodium.convertSecretKeyEd25519ToCurve25519(aBytes, secretKey)) {
|
||||||
|
aBytes
|
||||||
|
} else null
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Constructs a "blinded" key pair (`ka, kA`) based on an open group server `publicKey` and an ed25519 `keyPair` */
|
||||||
|
fun blindedKeyPair(serverPublicKey: String, edKeyPair: KeyPair): KeyPair? {
|
||||||
|
if (edKeyPair.publicKey.asBytes.size != PUBLIC_KEY_LENGTH || edKeyPair.secretKey.asBytes.size != SECRET_KEY_LENGTH) return null
|
||||||
|
val kBytes = generateBlindingFactor(serverPublicKey) ?: return null
|
||||||
|
val aBytes = generatePrivateKeyScalar(edKeyPair.secretKey.asBytes) ?: return null
|
||||||
|
// Generate the blinded key pair `ka`, `kA`
|
||||||
|
val kaBytes = ByteArray(SECRET_KEY_LENGTH)
|
||||||
|
sodium.cryptoCoreEd25519ScalarMul(kaBytes, kBytes, aBytes)
|
||||||
|
if (kaBytes.all { it.toInt() == 0 }) return null
|
||||||
|
|
||||||
|
val kABytes = ByteArray(PUBLIC_KEY_LENGTH)
|
||||||
|
return if (sodium.cryptoScalarMultEd25519BaseNoClamp(kABytes, kaBytes)) {
|
||||||
|
KeyPair(Key.fromBytes(kABytes), Key.fromBytes(kaBytes))
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Constructs an Ed25519 signature from a root Ed25519 key and a blinded scalar/pubkey pair, with one tweak to the
|
||||||
|
construction: we add kA into the hashed value that yields r so that we have domain separation for different blinded
|
||||||
|
pubkeys (this doesn't affect verification at all)
|
||||||
|
*/
|
||||||
|
fun sogsSignature(
|
||||||
|
message: ByteArray,
|
||||||
|
secretKey: ByteArray,
|
||||||
|
blindedSecretKey: ByteArray, /*ka*/
|
||||||
|
blindedPublicKey: ByteArray /*kA*/
|
||||||
|
): ByteArray? {
|
||||||
|
// H_rh = sha512(s.encode()).digest()[32:]
|
||||||
|
val digest = ByteArray(Hash.SHA512_BYTES)
|
||||||
|
val h_rh = if (sodium.cryptoHashSha512(digest, secretKey, secretKey.size.toLong())) {
|
||||||
|
digest.takeLast(32).toByteArray()
|
||||||
|
} else return null
|
||||||
|
|
||||||
|
// r = salt.crypto_core_ed25519_scalar_reduce(sha512_multipart(H_rh, kA, message_parts))
|
||||||
|
val rHash = sha512Multipart(listOf(h_rh, blindedPublicKey, message)) ?: return null
|
||||||
|
val r = ByteArray(SCALAR_LENGTH)
|
||||||
|
sodium.cryptoCoreEd25519ScalarReduce(r, rHash)
|
||||||
|
if (r.all { it.toInt() == 0 }) return null
|
||||||
|
|
||||||
|
// sig_R = salt.crypto_scalarmult_ed25519_base_noclamp(r)
|
||||||
|
val sig_R = ByteArray(NO_CLAMP_LENGTH)
|
||||||
|
if (!sodium.cryptoScalarMultEd25519BaseNoClamp(sig_R, r)) return null
|
||||||
|
|
||||||
|
// HRAM = salt.crypto_core_ed25519_scalar_reduce(sha512_multipart(sig_R, kA, message_parts))
|
||||||
|
val hRamHash = sha512Multipart(listOf(sig_R, blindedPublicKey, message)) ?: return null
|
||||||
|
val hRam = ByteArray(SCALAR_LENGTH)
|
||||||
|
sodium.cryptoCoreEd25519ScalarReduce(hRam, hRamHash)
|
||||||
|
if (hRam.all { it.toInt() == 0 }) return null
|
||||||
|
|
||||||
|
// sig_s = salt.crypto_core_ed25519_scalar_add(r, salt.crypto_core_ed25519_scalar_mul(HRAM, ka))
|
||||||
|
val sig_sMul = ByteArray(SCALAR_LENGTH)
|
||||||
|
val sig_s = ByteArray(SCALAR_LENGTH)
|
||||||
|
sodium.cryptoCoreEd25519ScalarMul(sig_sMul, hRam, blindedSecretKey)
|
||||||
|
if (sig_sMul.any { it.toInt() != 0 }) {
|
||||||
|
sodium.cryptoCoreEd25519ScalarAdd(sig_s, r, sig_sMul)
|
||||||
|
if (sig_s.all { it.toInt() == 0 }) return null
|
||||||
|
} else return null
|
||||||
|
|
||||||
|
return sig_R + sig_s
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun sha512Multipart(parts: List<ByteArray>): ByteArray? {
|
||||||
|
val state = Hash.State512()
|
||||||
|
sodium.cryptoHashSha512Init(state)
|
||||||
|
parts.forEach {
|
||||||
|
sodium.cryptoHashSha512Update(state, it, it.size.toLong())
|
||||||
|
}
|
||||||
|
val finalHash = ByteArray(Hash.SHA512_BYTES)
|
||||||
|
return if (sodium.cryptoHashSha512Final(state, finalHash)) {
|
||||||
|
finalHash
|
||||||
|
} else null
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Combines two keys (`kA`) */
|
||||||
|
fun combineKeys(lhsKey: ByteArray, rhsKey: ByteArray): ByteArray? {
|
||||||
|
val kA = ByteArray(NO_CLAMP_LENGTH)
|
||||||
|
return if (sodium.cryptoScalarMultEd25519NoClamp(kA, lhsKey, rhsKey)) {
|
||||||
|
kA
|
||||||
|
} else null
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Calculate a shared secret for a message from A to B:
|
||||||
|
BLAKE2b(a kB || kA || kB)
|
||||||
|
The receiver can calculate the same value via:
|
||||||
|
BLAKE2b(b kA || kA || kB)
|
||||||
|
*/
|
||||||
|
fun sharedBlindedEncryptionKey(
|
||||||
|
secretKey: ByteArray,
|
||||||
|
otherBlindedPublicKey: ByteArray,
|
||||||
|
kA: ByteArray, /*fromBlindedPublicKey*/
|
||||||
|
kB: ByteArray /*toBlindedPublicKey*/
|
||||||
|
): ByteArray? {
|
||||||
|
val aBytes = generatePrivateKeyScalar(secretKey) ?: return null
|
||||||
|
val combinedKeyBytes = combineKeys(aBytes, otherBlindedPublicKey) ?: return null
|
||||||
|
val outputHash = ByteArray(GenericHash.KEYBYTES)
|
||||||
|
val inputBytes = combinedKeyBytes + kA + kB
|
||||||
|
return if (sodium.cryptoGenericHash(outputHash, outputHash.size, inputBytes, inputBytes.size.toLong())) {
|
||||||
|
outputHash
|
||||||
|
} else null
|
||||||
|
}
|
||||||
|
|
||||||
|
/* This method should be used to check if a users standard sessionId matches a blinded one */
|
||||||
|
fun sessionId(
|
||||||
|
standardSessionId: String,
|
||||||
|
blindedSessionId: String,
|
||||||
|
serverPublicKey: String
|
||||||
|
): Boolean {
|
||||||
|
// Only support generating blinded keys for standard session ids
|
||||||
|
val sessionId = SessionId(standardSessionId)
|
||||||
|
if (sessionId.prefix != IdPrefix.STANDARD) return false
|
||||||
|
val blindedId = SessionId(blindedSessionId)
|
||||||
|
if (blindedId.prefix != IdPrefix.BLINDED) return false
|
||||||
|
val k = generateBlindingFactor(serverPublicKey) ?: return false
|
||||||
|
|
||||||
|
// From the session id (ignoring 05 prefix) we have two possible ed25519 pubkeys;
|
||||||
|
// the first is the positive (which is what Signal's XEd25519 conversion always uses)
|
||||||
|
val xEd25519Key = curve.convertToEd25519PublicKey(Key.fromHexString(sessionId.publicKey).asBytes)
|
||||||
|
|
||||||
|
// Blind the positive public key
|
||||||
|
val pk1 = combineKeys(k, xEd25519Key) ?: return false
|
||||||
|
|
||||||
|
// For the negative, what we're going to get out of the above is simply the negative of pk1, so flip the sign bit to get pk2
|
||||||
|
// pk2 = pk1[0:31] + bytes([pk1[31] ^ 0b1000_0000])
|
||||||
|
val pk2 = pk1.take(31).toByteArray() + listOf(pk1.last().xor(128.toByte())).toByteArray()
|
||||||
|
return SessionId(IdPrefix.BLINDED, pk1).publicKey == blindedId.publicKey ||
|
||||||
|
SessionId(IdPrefix.BLINDED, pk2).publicKey == blindedId.publicKey
|
||||||
|
}
|
||||||
|
|
||||||
|
fun encrypt(message: ByteArray, secretKey: ByteArray, nonce: ByteArray, additionalData: ByteArray? = null): ByteArray? {
|
||||||
|
val authenticatedCipherText = ByteArray(message.size + AEAD.CHACHA20POLY1305_ABYTES)
|
||||||
|
return if (sodium.cryptoAeadXChaCha20Poly1305IetfEncrypt(
|
||||||
|
authenticatedCipherText,
|
||||||
|
longArrayOf(0),
|
||||||
|
message,
|
||||||
|
message.size.toLong(),
|
||||||
|
additionalData,
|
||||||
|
(additionalData?.size ?: 0).toLong(),
|
||||||
|
null,
|
||||||
|
nonce,
|
||||||
|
secretKey
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
authenticatedCipherText
|
||||||
|
} else null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun decrypt(ciphertext: ByteArray, decryptionKey: ByteArray, nonce: ByteArray): ByteArray? {
|
||||||
|
val plaintextSize = ciphertext.size - AEAD.CHACHA20POLY1305_ABYTES
|
||||||
|
val plaintext = ByteArray(plaintextSize)
|
||||||
|
return if (sodium.cryptoAeadXChaCha20Poly1305IetfDecrypt(
|
||||||
|
plaintext,
|
||||||
|
longArrayOf(plaintextSize.toLong()),
|
||||||
|
null,
|
||||||
|
ciphertext,
|
||||||
|
ciphertext.size.toLong(),
|
||||||
|
null,
|
||||||
|
0L,
|
||||||
|
nonce,
|
||||||
|
decryptionKey
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
plaintext
|
||||||
|
} else null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toX25519(ed25519PublicKey: ByteArray): ByteArray? {
|
||||||
|
val x25519PublicKey = ByteArray(PUBLIC_KEY_LENGTH)
|
||||||
|
return if (sodium.convertPublicKeyEd25519ToCurve25519(x25519PublicKey, ed25519PublicKey)) {
|
||||||
|
x25519PublicKey
|
||||||
|
} else null
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class SessionId {
|
||||||
|
var prefix: IdPrefix?
|
||||||
|
var publicKey: String
|
||||||
|
|
||||||
|
constructor(id: String) {
|
||||||
|
prefix = IdPrefix.fromValue(id)
|
||||||
|
publicKey = id.drop(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(prefix: IdPrefix, publicKey: ByteArray) {
|
||||||
|
this.prefix = prefix
|
||||||
|
this.publicKey = publicKey.toHexString()
|
||||||
|
}
|
||||||
|
|
||||||
|
val hexString
|
||||||
|
get() = prefix?.value + publicKey
|
||||||
|
}
|
||||||
|
|
@ -1,12 +1,13 @@
|
|||||||
package org.session.libsession.snode
|
package org.session.libsession.snode
|
||||||
|
|
||||||
|
import nl.komponents.kovenant.Deferred
|
||||||
import nl.komponents.kovenant.Promise
|
import nl.komponents.kovenant.Promise
|
||||||
import nl.komponents.kovenant.all
|
import nl.komponents.kovenant.all
|
||||||
import nl.komponents.kovenant.deferred
|
import nl.komponents.kovenant.deferred
|
||||||
import nl.komponents.kovenant.functional.bind
|
import nl.komponents.kovenant.functional.bind
|
||||||
import nl.komponents.kovenant.functional.map
|
import nl.komponents.kovenant.functional.map
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import org.session.libsession.messaging.file_server.FileServerAPIV2
|
import org.session.libsession.messaging.file_server.FileServerApi
|
||||||
import org.session.libsession.utilities.AESGCM
|
import org.session.libsession.utilities.AESGCM
|
||||||
import org.session.libsession.utilities.AESGCM.EncryptionResult
|
import org.session.libsession.utilities.AESGCM.EncryptionResult
|
||||||
import org.session.libsession.utilities.getBodyForOnionRequest
|
import org.session.libsession.utilities.getBodyForOnionRequest
|
||||||
@ -76,7 +77,8 @@ object OnionRequestAPI {
|
|||||||
const val targetPathCount = 2 // A main path and a backup path for the case where the target snode is in the main path
|
const val targetPathCount = 2 // A main path and a backup path for the case where the target snode is in the main path
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
class HTTPRequestFailedAtDestinationException(val statusCode: Int, val json: Map<*, *>, val destination: String)
|
class HTTPRequestFailedBlindingRequiredException(statusCode: Int, json: Map<*, *>, destination: String): HTTPRequestFailedAtDestinationException(statusCode, json, destination)
|
||||||
|
open class HTTPRequestFailedAtDestinationException(val statusCode: Int, val json: Map<*, *>, val destination: String)
|
||||||
: Exception("HTTP request failed at destination ($destination) with status code $statusCode.")
|
: Exception("HTTP request failed at destination ($destination) with status code $statusCode.")
|
||||||
class InsufficientSnodesException : Exception("Couldn't find enough snodes to build a path.")
|
class InsufficientSnodesException : Exception("Couldn't find enough snodes to build a path.")
|
||||||
|
|
||||||
@ -100,7 +102,8 @@ object OnionRequestAPI {
|
|||||||
ThreadUtils.queue { // No need to block the shared context for this
|
ThreadUtils.queue { // No need to block the shared context for this
|
||||||
val url = "${snode.address}:${snode.port}/get_stats/v1"
|
val url = "${snode.address}:${snode.port}/get_stats/v1"
|
||||||
try {
|
try {
|
||||||
val json = HTTP.execute(HTTP.Verb.GET, url, 3)
|
val response = HTTP.execute(HTTP.Verb.GET, url, 3).decodeToString()
|
||||||
|
val json = JsonUtil.fromJson(response, Map::class.java)
|
||||||
val version = json["version"] as? String
|
val version = json["version"] as? String
|
||||||
if (version == null) { deferred.reject(Exception("Missing snode version.")); return@queue }
|
if (version == null) { deferred.reject(Exception("Missing snode version.")); return@queue }
|
||||||
if (version >= "2.0.7") {
|
if (version >= "2.0.7") {
|
||||||
@ -207,29 +210,33 @@ object OnionRequestAPI {
|
|||||||
}
|
}
|
||||||
OnionRequestAPI.guardSnodes = guardSnodes
|
OnionRequestAPI.guardSnodes = guardSnodes
|
||||||
fun getPath(paths: List<Path>): Path {
|
fun getPath(paths: List<Path>): Path {
|
||||||
if (snodeToExclude != null) {
|
return if (snodeToExclude != null) {
|
||||||
return paths.filter { !it.contains(snodeToExclude) }.getRandomElement()
|
paths.filter { !it.contains(snodeToExclude) }.getRandomElement()
|
||||||
} else {
|
} else {
|
||||||
return paths.getRandomElement()
|
paths.getRandomElement()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (paths.count() >= targetPathCount) {
|
when {
|
||||||
|
paths.count() >= targetPathCount -> {
|
||||||
return Promise.of(getPath(paths))
|
return Promise.of(getPath(paths))
|
||||||
} else if (paths.isNotEmpty()) {
|
}
|
||||||
if (paths.any { !it.contains(snodeToExclude) }) {
|
paths.isNotEmpty() -> {
|
||||||
|
return if (paths.any { !it.contains(snodeToExclude) }) {
|
||||||
buildPaths(paths) // Re-build paths in the background
|
buildPaths(paths) // Re-build paths in the background
|
||||||
return Promise.of(getPath(paths))
|
Promise.of(getPath(paths))
|
||||||
} else {
|
} else {
|
||||||
return buildPaths(paths).map { newPaths ->
|
buildPaths(paths).map { newPaths ->
|
||||||
getPath(newPaths)
|
getPath(newPaths)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
|
else -> {
|
||||||
return buildPaths(listOf()).map { newPaths ->
|
return buildPaths(listOf()).map { newPaths ->
|
||||||
getPath(newPaths)
|
getPath(newPaths)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun dropGuardSnode(snode: Snode) {
|
private fun dropGuardSnode(snode: Snode) {
|
||||||
guardSnodes = guardSnodes.filter { it != snode }.toSet()
|
guardSnodes = guardSnodes.filter { it != snode }.toSet()
|
||||||
@ -268,7 +275,11 @@ object OnionRequestAPI {
|
|||||||
/**
|
/**
|
||||||
* Builds an onion around `payload` and returns the result.
|
* Builds an onion around `payload` and returns the result.
|
||||||
*/
|
*/
|
||||||
private fun buildOnionForDestination(payload: Map<*, *>, destination: Destination): Promise<OnionBuildingResult, Exception> {
|
private fun buildOnionForDestination(
|
||||||
|
payload: ByteArray,
|
||||||
|
destination: Destination,
|
||||||
|
version: Version
|
||||||
|
): Promise<OnionBuildingResult, Exception> {
|
||||||
lateinit var guardSnode: Snode
|
lateinit var guardSnode: Snode
|
||||||
lateinit var destinationSymmetricKey: ByteArray // Needed by LokiAPI to decrypt the response sent back by the destination
|
lateinit var destinationSymmetricKey: ByteArray // Needed by LokiAPI to decrypt the response sent back by the destination
|
||||||
lateinit var encryptionResult: EncryptionResult
|
lateinit var encryptionResult: EncryptionResult
|
||||||
@ -279,19 +290,19 @@ object OnionRequestAPI {
|
|||||||
return getPath(snodeToExclude).bind { path ->
|
return getPath(snodeToExclude).bind { path ->
|
||||||
guardSnode = path.first()
|
guardSnode = path.first()
|
||||||
// Encrypt in reverse order, i.e. the destination first
|
// Encrypt in reverse order, i.e. the destination first
|
||||||
OnionRequestEncryption.encryptPayloadForDestination(payload, destination).bind { r ->
|
OnionRequestEncryption.encryptPayloadForDestination(payload, destination, version).bind { r ->
|
||||||
destinationSymmetricKey = r.symmetricKey
|
destinationSymmetricKey = r.symmetricKey
|
||||||
// Recursively encrypt the layers of the onion (again in reverse order)
|
// Recursively encrypt the layers of the onion (again in reverse order)
|
||||||
encryptionResult = r
|
encryptionResult = r
|
||||||
@Suppress("NAME_SHADOWING") var path = path
|
@Suppress("NAME_SHADOWING") var path = path
|
||||||
var rhs = destination
|
var rhs = destination
|
||||||
fun addLayer(): Promise<EncryptionResult, Exception> {
|
fun addLayer(): Promise<EncryptionResult, Exception> {
|
||||||
if (path.isEmpty()) {
|
return if (path.isEmpty()) {
|
||||||
return Promise.of(encryptionResult)
|
Promise.of(encryptionResult)
|
||||||
} else {
|
} else {
|
||||||
val lhs = Destination.Snode(path.last())
|
val lhs = Destination.Snode(path.last())
|
||||||
path = path.dropLast(1)
|
path = path.dropLast(1)
|
||||||
return OnionRequestEncryption.encryptHop(lhs, rhs, encryptionResult).bind { r ->
|
OnionRequestEncryption.encryptHop(lhs, rhs, encryptionResult).bind { r ->
|
||||||
encryptionResult = r
|
encryptionResult = r
|
||||||
rhs = lhs
|
rhs = lhs
|
||||||
addLayer()
|
addLayer()
|
||||||
@ -306,16 +317,20 @@ object OnionRequestAPI {
|
|||||||
/**
|
/**
|
||||||
* Sends an onion request to `destination`. Builds new paths as needed.
|
* Sends an onion request to `destination`. Builds new paths as needed.
|
||||||
*/
|
*/
|
||||||
private fun sendOnionRequest(destination: Destination, payload: Map<*, *>): Promise<Map<*, *>, Exception> {
|
private fun sendOnionRequest(
|
||||||
val deferred = deferred<Map<*, *>, Exception>()
|
destination: Destination,
|
||||||
|
payload: ByteArray,
|
||||||
|
version: Version
|
||||||
|
): Promise<OnionResponse, Exception> {
|
||||||
|
val deferred = deferred<OnionResponse, Exception>()
|
||||||
var guardSnode: Snode? = null
|
var guardSnode: Snode? = null
|
||||||
buildOnionForDestination(payload, destination).success { result ->
|
buildOnionForDestination(payload, destination, version).success { result ->
|
||||||
guardSnode = result.guardSnode
|
guardSnode = result.guardSnode
|
||||||
val nonNullGuardSnode = result.guardSnode
|
val nonNullGuardSnode = result.guardSnode
|
||||||
val url = "${nonNullGuardSnode.address}:${nonNullGuardSnode.port}/onion_req/v2"
|
val url = "${nonNullGuardSnode.address}:${nonNullGuardSnode.port}/onion_req/v2"
|
||||||
val finalEncryptionResult = result.finalEncryptionResult
|
val finalEncryptionResult = result.finalEncryptionResult
|
||||||
val onion = finalEncryptionResult.ciphertext
|
val onion = finalEncryptionResult.ciphertext
|
||||||
if (destination is Destination.Server && onion.count().toDouble() > 0.75 * FileServerAPIV2.maxFileSize.toDouble()) {
|
if (destination is Destination.Server && onion.count().toDouble() > 0.75 * FileServerApi.maxFileSize.toDouble()) {
|
||||||
Log.d("Loki", "Approaching request size limit: ~${onion.count()} bytes.")
|
Log.d("Loki", "Approaching request size limit: ~${onion.count()} bytes.")
|
||||||
}
|
}
|
||||||
@Suppress("NAME_SHADOWING") val parameters = mapOf(
|
@Suppress("NAME_SHADOWING") val parameters = mapOf(
|
||||||
@ -330,65 +345,8 @@ object OnionRequestAPI {
|
|||||||
val destinationSymmetricKey = result.destinationSymmetricKey
|
val destinationSymmetricKey = result.destinationSymmetricKey
|
||||||
ThreadUtils.queue {
|
ThreadUtils.queue {
|
||||||
try {
|
try {
|
||||||
val json = HTTP.execute(HTTP.Verb.POST, url, body)
|
val response = HTTP.execute(HTTP.Verb.POST, url, body)
|
||||||
val base64EncodedIVAndCiphertext = json["result"] as? String ?: return@queue deferred.reject(Exception("Invalid JSON"))
|
handleResponse(response, destinationSymmetricKey, destination, version, deferred)
|
||||||
val ivAndCiphertext = Base64.decode(base64EncodedIVAndCiphertext)
|
|
||||||
try {
|
|
||||||
val plaintext = AESGCM.decrypt(ivAndCiphertext, destinationSymmetricKey)
|
|
||||||
try {
|
|
||||||
@Suppress("NAME_SHADOWING") val json = JsonUtil.fromJson(plaintext.toString(Charsets.UTF_8), Map::class.java)
|
|
||||||
val statusCode = json["status_code"] as? Int ?: json["status"] as Int
|
|
||||||
if (statusCode == 406) {
|
|
||||||
@Suppress("NAME_SHADOWING") val body = mapOf( "result" to "Your clock is out of sync with the service node network." )
|
|
||||||
val exception = HTTPRequestFailedAtDestinationException(statusCode, body, destination.description)
|
|
||||||
return@queue deferred.reject(exception)
|
|
||||||
} else if (json["body"] != null) {
|
|
||||||
@Suppress("NAME_SHADOWING") val body: Map<*, *>
|
|
||||||
if (json["body"] is Map<*, *>) {
|
|
||||||
body = json["body"] as Map<*, *>
|
|
||||||
} else {
|
|
||||||
val bodyAsString = json["body"] as String
|
|
||||||
body = JsonUtil.fromJson(bodyAsString, Map::class.java)
|
|
||||||
}
|
|
||||||
if (body["t"] != null) {
|
|
||||||
val timestamp = body["t"] as Long
|
|
||||||
val offset = timestamp - Date().time
|
|
||||||
SnodeAPI.clockOffset = offset
|
|
||||||
}
|
|
||||||
if (body.containsKey("hf")) {
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
val currentHf = body["hf"] as List<Int>
|
|
||||||
if (currentHf.size < 2) {
|
|
||||||
Log.e("Loki", "Response contains fork information but doesn't have a hard and soft number")
|
|
||||||
} else {
|
|
||||||
val hf = currentHf[0]
|
|
||||||
val sf = currentHf[1]
|
|
||||||
val newForkInfo = ForkInfo(hf, sf)
|
|
||||||
if (newForkInfo > SnodeAPI.forkInfo) {
|
|
||||||
SnodeAPI.forkInfo = ForkInfo(hf,sf)
|
|
||||||
} else if (newForkInfo < SnodeAPI.forkInfo) {
|
|
||||||
Log.w("Loki", "Got a new snode info fork version that was $newForkInfo, less than current known ${SnodeAPI.forkInfo}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (statusCode != 200) {
|
|
||||||
val exception = HTTPRequestFailedAtDestinationException(statusCode, body, destination.description)
|
|
||||||
return@queue deferred.reject(exception)
|
|
||||||
}
|
|
||||||
deferred.resolve(body)
|
|
||||||
} else {
|
|
||||||
if (statusCode != 200) {
|
|
||||||
val exception = HTTPRequestFailedAtDestinationException(statusCode, json, destination.description)
|
|
||||||
return@queue deferred.reject(exception)
|
|
||||||
}
|
|
||||||
deferred.resolve(json)
|
|
||||||
}
|
|
||||||
} catch (exception: Exception) {
|
|
||||||
deferred.reject(Exception("Invalid JSON: ${plaintext.toString(Charsets.UTF_8)}."))
|
|
||||||
}
|
|
||||||
} catch (exception: Exception) {
|
|
||||||
deferred.reject(exception)
|
|
||||||
}
|
|
||||||
} catch (exception: Exception) {
|
} catch (exception: Exception) {
|
||||||
deferred.reject(exception)
|
deferred.reject(exception)
|
||||||
}
|
}
|
||||||
@ -459,9 +417,19 @@ object OnionRequestAPI {
|
|||||||
/**
|
/**
|
||||||
* Sends an onion request to `snode`. Builds new paths as needed.
|
* Sends an onion request to `snode`. Builds new paths as needed.
|
||||||
*/
|
*/
|
||||||
internal fun sendOnionRequest(method: Snode.Method, parameters: Map<*, *>, snode: Snode, publicKey: String? = null): Promise<Map<*, *>, Exception> {
|
internal fun sendOnionRequest(
|
||||||
val payload = mapOf( "method" to method.rawValue, "params" to parameters )
|
method: Snode.Method,
|
||||||
return sendOnionRequest(Destination.Snode(snode), payload).recover { exception ->
|
parameters: Map<*, *>,
|
||||||
|
snode: Snode,
|
||||||
|
version: Version,
|
||||||
|
publicKey: String? = null
|
||||||
|
): Promise<OnionResponse, Exception> {
|
||||||
|
val payload = mapOf(
|
||||||
|
"method" to method.rawValue,
|
||||||
|
"params" to parameters
|
||||||
|
)
|
||||||
|
val payloadData = JsonUtil.toJson(payload).toByteArray()
|
||||||
|
return sendOnionRequest(Destination.Snode(snode), payloadData, version).recover { exception ->
|
||||||
val error = when (exception) {
|
val error = when (exception) {
|
||||||
is HTTP.HTTPRequestFailedException -> SnodeAPI.handleSnodeError(exception.statusCode, exception.json, snode, publicKey)
|
is HTTP.HTTPRequestFailedException -> SnodeAPI.handleSnodeError(exception.statusCode, exception.json, snode, publicKey)
|
||||||
is HTTPRequestFailedAtDestinationException -> SnodeAPI.handleSnodeError(exception.statusCode, exception.json, snode, publicKey)
|
is HTTPRequestFailedAtDestinationException -> SnodeAPI.handleSnodeError(exception.statusCode, exception.json, snode, publicKey)
|
||||||
@ -477,27 +445,228 @@ object OnionRequestAPI {
|
|||||||
*
|
*
|
||||||
* `publicKey` is the hex encoded public key of the user the call is associated with. This is needed for swarm cache maintenance.
|
* `publicKey` is the hex encoded public key of the user the call is associated with. This is needed for swarm cache maintenance.
|
||||||
*/
|
*/
|
||||||
fun sendOnionRequest(request: Request, server: String, x25519PublicKey: String, target: String = "/loki/v3/lsrpc"): Promise<Map<*, *>, Exception> {
|
fun sendOnionRequest(
|
||||||
val headers = request.getHeadersForOnionRequest()
|
request: Request,
|
||||||
|
server: String,
|
||||||
|
x25519PublicKey: String,
|
||||||
|
version: Version = Version.V4
|
||||||
|
): Promise<OnionResponse, Exception> {
|
||||||
|
val url = request.url()
|
||||||
|
val payload = generatePayload(request, server, version)
|
||||||
|
val destination = Destination.Server(url.host(), version.value, x25519PublicKey, url.scheme(), url.port())
|
||||||
|
return sendOnionRequest(destination, payload, version).recover { exception ->
|
||||||
|
Log.d("Loki", "Couldn't reach server: $url due to error: $exception.")
|
||||||
|
throw exception
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun generatePayload(request: Request, server: String, version: Version): ByteArray {
|
||||||
|
val headers = request.getHeadersForOnionRequest().toMutableMap()
|
||||||
val url = request.url()
|
val url = request.url()
|
||||||
val urlAsString = url.toString()
|
val urlAsString = url.toString()
|
||||||
val host = url.host()
|
val body = request.getBodyForOnionRequest() ?: "null"
|
||||||
val endpoint = when {
|
val endpoint = when {
|
||||||
server.count() < urlAsString.count() -> urlAsString.substringAfter(server).removePrefix("/")
|
server.count() < urlAsString.count() -> urlAsString.substringAfter(server)
|
||||||
else -> ""
|
else -> ""
|
||||||
}
|
}
|
||||||
val body = request.getBodyForOnionRequest() ?: "null"
|
return if (version == Version.V4) {
|
||||||
val payload = mapOf(
|
if (request.body() != null &&
|
||||||
"body" to body,
|
headers.keys.find { it.equals("Content-Type", true) } == null) {
|
||||||
|
headers["Content-Type"] = "application/json"
|
||||||
|
}
|
||||||
|
val requestPayload = mapOf(
|
||||||
"endpoint" to endpoint,
|
"endpoint" to endpoint,
|
||||||
"method" to request.method(),
|
"method" to request.method(),
|
||||||
"headers" to headers
|
"headers" to headers
|
||||||
)
|
)
|
||||||
val destination = Destination.Server(host, target, x25519PublicKey, url.scheme(), url.port())
|
val requestData = JsonUtil.toJson(requestPayload).toByteArray()
|
||||||
return sendOnionRequest(destination, payload).recover { exception ->
|
val prefixData = "l${requestData.size}:".toByteArray(Charsets.US_ASCII)
|
||||||
Log.d("Loki", "Couldn't reach server: $urlAsString due to error: $exception.")
|
val suffixData = "e".toByteArray(Charsets.US_ASCII)
|
||||||
throw exception
|
if (request.body() != null) {
|
||||||
|
val bodyData = body.toString().toByteArray()
|
||||||
|
val bodyLengthData = "${bodyData.size}:".toByteArray(Charsets.US_ASCII)
|
||||||
|
prefixData + requestData + bodyLengthData + bodyData + suffixData
|
||||||
|
} else {
|
||||||
|
prefixData + requestData + suffixData
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val payload = mapOf(
|
||||||
|
"body" to body,
|
||||||
|
"endpoint" to endpoint.removePrefix("/"),
|
||||||
|
"method" to request.method(),
|
||||||
|
"headers" to headers
|
||||||
|
)
|
||||||
|
JsonUtil.toJson(payload).toByteArray()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleResponse(
|
||||||
|
response: ByteArray,
|
||||||
|
destinationSymmetricKey: ByteArray,
|
||||||
|
destination: Destination,
|
||||||
|
version: Version,
|
||||||
|
deferred: Deferred<OnionResponse, Exception>
|
||||||
|
) {
|
||||||
|
if (version == Version.V4) {
|
||||||
|
try {
|
||||||
|
if (response.size <= AESGCM.ivSize) return deferred.reject(Exception("Invalid response"))
|
||||||
|
// The data will be in the form of `l123:jsone` or `l123:json456:bodye` so we need to break the data into
|
||||||
|
// parts to properly process it
|
||||||
|
val plaintext = AESGCM.decrypt(response, destinationSymmetricKey)
|
||||||
|
if (!byteArrayOf(plaintext.first()).contentEquals("l".toByteArray())) return deferred.reject(Exception("Invalid response"))
|
||||||
|
val infoSepIdx = plaintext.indexOfFirst { byteArrayOf(it).contentEquals(":".toByteArray()) }
|
||||||
|
val infoLenSlice = plaintext.slice(1 until infoSepIdx)
|
||||||
|
val infoLength = infoLenSlice.toByteArray().toString(Charsets.US_ASCII).toIntOrNull()
|
||||||
|
if (infoLenSlice.size <= 1 || infoLength == null) return deferred.reject(Exception("Invalid response"))
|
||||||
|
val infoStartIndex = "l$infoLength".length + 1
|
||||||
|
val infoEndIndex = infoStartIndex + infoLength
|
||||||
|
val info = plaintext.slice(infoStartIndex until infoEndIndex)
|
||||||
|
val responseInfo = JsonUtil.fromJson(info.toByteArray(), Map::class.java)
|
||||||
|
when (val statusCode = responseInfo["code"].toString().toInt()) {
|
||||||
|
// Custom handle a clock out of sync error (v4 returns '425' but included the '406' just in case)
|
||||||
|
406, 425 -> {
|
||||||
|
@Suppress("NAME_SHADOWING")
|
||||||
|
val exception = HTTPRequestFailedAtDestinationException(
|
||||||
|
statusCode,
|
||||||
|
mapOf("result" to "Your clock is out of sync with the service node network."),
|
||||||
|
destination.description
|
||||||
|
)
|
||||||
|
return deferred.reject(exception)
|
||||||
|
}
|
||||||
|
// Handle error status codes
|
||||||
|
!in 200..299 -> {
|
||||||
|
val responseBody = if (destination is Destination.Server && statusCode == 400) plaintext.getBody(infoLength, infoEndIndex) else null
|
||||||
|
val requireBlinding = "Invalid authentication: this server requires the use of blinded ids"
|
||||||
|
val exception = if (responseBody != null && responseBody.decodeToString() == requireBlinding) {
|
||||||
|
HTTPRequestFailedBlindingRequiredException(400, responseInfo, destination.description)
|
||||||
|
} else HTTPRequestFailedAtDestinationException(
|
||||||
|
statusCode,
|
||||||
|
responseInfo,
|
||||||
|
destination.description
|
||||||
|
)
|
||||||
|
return deferred.reject(exception)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val responseBody = plaintext.getBody(infoLength, infoEndIndex)
|
||||||
|
|
||||||
|
// If there is no data in the response, i.e. only `l123:jsone`, then just return the ResponseInfo
|
||||||
|
if (responseBody.isEmpty()) {
|
||||||
|
return deferred.resolve(OnionResponse(responseInfo, null))
|
||||||
|
}
|
||||||
|
return deferred.resolve(OnionResponse(responseInfo, responseBody))
|
||||||
|
} catch (exception: Exception) {
|
||||||
|
deferred.reject(exception)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val json = try {
|
||||||
|
JsonUtil.fromJson(response, Map::class.java)
|
||||||
|
} catch (exception: Exception) {
|
||||||
|
mapOf( "result" to response.decodeToString())
|
||||||
|
}
|
||||||
|
val base64EncodedIVAndCiphertext = json["result"] as? String ?: return deferred.reject(Exception("Invalid JSON"))
|
||||||
|
val ivAndCiphertext = Base64.decode(base64EncodedIVAndCiphertext)
|
||||||
|
try {
|
||||||
|
val plaintext = AESGCM.decrypt(ivAndCiphertext, destinationSymmetricKey)
|
||||||
|
try {
|
||||||
|
@Suppress("NAME_SHADOWING") val json =
|
||||||
|
JsonUtil.fromJson(plaintext.toString(Charsets.UTF_8), Map::class.java)
|
||||||
|
val statusCode = json["status_code"] as? Int ?: json["status"] as Int
|
||||||
|
when {
|
||||||
|
statusCode == 406 -> {
|
||||||
|
@Suppress("NAME_SHADOWING")
|
||||||
|
val body =
|
||||||
|
mapOf("result" to "Your clock is out of sync with the service node network.")
|
||||||
|
val exception = HTTPRequestFailedAtDestinationException(
|
||||||
|
statusCode,
|
||||||
|
body,
|
||||||
|
destination.description
|
||||||
|
)
|
||||||
|
return deferred.reject(exception)
|
||||||
|
}
|
||||||
|
json["body"] != null -> {
|
||||||
|
@Suppress("NAME_SHADOWING")
|
||||||
|
val body = if (json["body"] is Map<*, *>) {
|
||||||
|
json["body"] as Map<*, *>
|
||||||
|
} else {
|
||||||
|
val bodyAsString = json["body"] as String
|
||||||
|
JsonUtil.fromJson(bodyAsString, Map::class.java)
|
||||||
|
}
|
||||||
|
if (body["t"] != null) {
|
||||||
|
val timestamp = body["t"] as Long
|
||||||
|
val offset = timestamp - Date().time
|
||||||
|
SnodeAPI.clockOffset = offset
|
||||||
|
}
|
||||||
|
if (body.containsKey("hf")) {
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
val currentHf = body["hf"] as List<Int>
|
||||||
|
if (currentHf.size < 2) {
|
||||||
|
Log.e("Loki", "Response contains fork information but doesn't have a hard and soft number")
|
||||||
|
} else {
|
||||||
|
val hf = currentHf[0]
|
||||||
|
val sf = currentHf[1]
|
||||||
|
val newForkInfo = ForkInfo(hf, sf)
|
||||||
|
if (newForkInfo > SnodeAPI.forkInfo) {
|
||||||
|
SnodeAPI.forkInfo = ForkInfo(hf,sf)
|
||||||
|
} else if (newForkInfo < SnodeAPI.forkInfo) {
|
||||||
|
Log.w("Loki", "Got a new snode info fork version that was $newForkInfo, less than current known ${SnodeAPI.forkInfo}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (statusCode != 200) {
|
||||||
|
val exception = HTTPRequestFailedAtDestinationException(
|
||||||
|
statusCode,
|
||||||
|
body,
|
||||||
|
destination.description
|
||||||
|
)
|
||||||
|
return deferred.reject(exception)
|
||||||
|
}
|
||||||
|
deferred.resolve(OnionResponse(body, JsonUtil.toJson(body).toByteArray()))
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
if (statusCode != 200) {
|
||||||
|
val exception = HTTPRequestFailedAtDestinationException(
|
||||||
|
statusCode,
|
||||||
|
json,
|
||||||
|
destination.description
|
||||||
|
)
|
||||||
|
return deferred.reject(exception)
|
||||||
|
}
|
||||||
|
deferred.resolve(OnionResponse(json, JsonUtil.toJson(json).toByteArray()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (exception: Exception) {
|
||||||
|
deferred.reject(Exception("Invalid JSON: ${plaintext.toString(Charsets.UTF_8)}."))
|
||||||
|
}
|
||||||
|
} catch (exception: Exception) {
|
||||||
|
deferred.reject(exception)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun ByteArray.getBody(infoLength: Int, infoEndIndex: Int): ByteArray {
|
||||||
|
// If there is no data in the response, i.e. only `l123:jsone`, then just return the ResponseInfo
|
||||||
|
val infoLengthStringLength = infoLength.toString().length
|
||||||
|
if (size <= infoLength + infoLengthStringLength + 2/*l and e bytes*/) {
|
||||||
|
return byteArrayOf()
|
||||||
|
}
|
||||||
|
// Extract the response data as well
|
||||||
|
val dataSlice = slice(infoEndIndex + 1 until size - 1)
|
||||||
|
val dataSepIdx = dataSlice.indexOfFirst { byteArrayOf(it).contentEquals(":".toByteArray()) }
|
||||||
|
val responseBody = dataSlice.slice(dataSepIdx + 1 until dataSlice.size)
|
||||||
|
return responseBody.toByteArray()
|
||||||
|
}
|
||||||
|
|
||||||
// endregion
|
// endregion
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum class Version(val value: String) {
|
||||||
|
V2("/loki/v2/lsrpc"),
|
||||||
|
V3("/loki/v3/lsrpc"),
|
||||||
|
V4("/oxen/v4/lsrpc");
|
||||||
|
}
|
||||||
|
|
||||||
|
data class OnionResponse(
|
||||||
|
val info: Map<*, *>,
|
||||||
|
val body: ByteArray? = null
|
||||||
|
)
|
||||||
|
@ -2,6 +2,7 @@ package org.session.libsession.snode
|
|||||||
|
|
||||||
import nl.komponents.kovenant.Promise
|
import nl.komponents.kovenant.Promise
|
||||||
import nl.komponents.kovenant.deferred
|
import nl.komponents.kovenant.deferred
|
||||||
|
import org.session.libsession.snode.OnionRequestAPI.Destination
|
||||||
import org.session.libsession.utilities.AESGCM
|
import org.session.libsession.utilities.AESGCM
|
||||||
import org.session.libsession.utilities.AESGCM.EncryptionResult
|
import org.session.libsession.utilities.AESGCM.EncryptionResult
|
||||||
import org.session.libsignal.utilities.toHexString
|
import org.session.libsignal.utilities.toHexString
|
||||||
@ -31,25 +32,29 @@ object OnionRequestEncryption {
|
|||||||
/**
|
/**
|
||||||
* Encrypts `payload` for `destination` and returns the result. Use this to build the core of an onion request.
|
* Encrypts `payload` for `destination` and returns the result. Use this to build the core of an onion request.
|
||||||
*/
|
*/
|
||||||
internal fun encryptPayloadForDestination(payload: Map<*, *>, destination: OnionRequestAPI.Destination): Promise<EncryptionResult, Exception> {
|
internal fun encryptPayloadForDestination(
|
||||||
|
payload: ByteArray,
|
||||||
|
destination: Destination,
|
||||||
|
version: Version
|
||||||
|
): Promise<EncryptionResult, Exception> {
|
||||||
val deferred = deferred<EncryptionResult, Exception>()
|
val deferred = deferred<EncryptionResult, Exception>()
|
||||||
ThreadUtils.queue {
|
ThreadUtils.queue {
|
||||||
try {
|
try {
|
||||||
|
val plaintext = if (version == Version.V4) {
|
||||||
|
payload
|
||||||
|
} else {
|
||||||
// Wrapping isn't needed for file server or open group onion requests
|
// Wrapping isn't needed for file server or open group onion requests
|
||||||
when (destination) {
|
when (destination) {
|
||||||
is OnionRequestAPI.Destination.Snode -> {
|
is Destination.Snode -> encode(payload, mapOf("headers" to ""))
|
||||||
val snodeX25519PublicKey = destination.snode.publicKeySet!!.x25519Key
|
is Destination.Server -> payload
|
||||||
val payloadAsData = JsonUtil.toJson(payload).toByteArray()
|
}
|
||||||
val plaintext = encode(payloadAsData, mapOf( "headers" to "" ))
|
}
|
||||||
val result = AESGCM.encrypt(plaintext, snodeX25519PublicKey)
|
val x25519PublicKey = when (destination) {
|
||||||
|
is Destination.Snode -> destination.snode.publicKeySet!!.x25519Key
|
||||||
|
is Destination.Server -> destination.x25519PublicKey
|
||||||
|
}
|
||||||
|
val result = AESGCM.encrypt(plaintext, x25519PublicKey)
|
||||||
deferred.resolve(result)
|
deferred.resolve(result)
|
||||||
}
|
|
||||||
is OnionRequestAPI.Destination.Server -> {
|
|
||||||
val plaintext = JsonUtil.toJson(payload).toByteArray()
|
|
||||||
val result = AESGCM.encrypt(plaintext, destination.x25519PublicKey)
|
|
||||||
deferred.resolve(result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (exception: Exception) {
|
} catch (exception: Exception) {
|
||||||
deferred.reject(exception)
|
deferred.reject(exception)
|
||||||
}
|
}
|
||||||
@ -60,17 +65,16 @@ object OnionRequestEncryption {
|
|||||||
/**
|
/**
|
||||||
* Encrypts the previous encryption result (i.e. that of the hop after this one) for this hop. Use this to build the layers of an onion request.
|
* Encrypts the previous encryption result (i.e. that of the hop after this one) for this hop. Use this to build the layers of an onion request.
|
||||||
*/
|
*/
|
||||||
internal fun encryptHop(lhs: OnionRequestAPI.Destination, rhs: OnionRequestAPI.Destination, previousEncryptionResult: EncryptionResult): Promise<EncryptionResult, Exception> {
|
internal fun encryptHop(lhs: Destination, rhs: Destination, previousEncryptionResult: EncryptionResult): Promise<EncryptionResult, Exception> {
|
||||||
val deferred = deferred<EncryptionResult, Exception>()
|
val deferred = deferred<EncryptionResult, Exception>()
|
||||||
ThreadUtils.queue {
|
ThreadUtils.queue {
|
||||||
try {
|
try {
|
||||||
val payload: MutableMap<String, Any>
|
val payload: MutableMap<String, Any> = when (rhs) {
|
||||||
when (rhs) {
|
is Destination.Snode -> {
|
||||||
is OnionRequestAPI.Destination.Snode -> {
|
mutableMapOf( "destination" to rhs.snode.publicKeySet!!.ed25519Key )
|
||||||
payload = mutableMapOf( "destination" to rhs.snode.publicKeySet!!.ed25519Key )
|
|
||||||
}
|
}
|
||||||
is OnionRequestAPI.Destination.Server -> {
|
is Destination.Server -> {
|
||||||
payload = mutableMapOf(
|
mutableMapOf(
|
||||||
"host" to rhs.host,
|
"host" to rhs.host,
|
||||||
"target" to rhs.target,
|
"target" to rhs.target,
|
||||||
"method" to "POST",
|
"method" to "POST",
|
||||||
@ -80,13 +84,12 @@ object OnionRequestEncryption {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
payload["ephemeral_key"] = previousEncryptionResult.ephemeralPublicKey.toHexString()
|
payload["ephemeral_key"] = previousEncryptionResult.ephemeralPublicKey.toHexString()
|
||||||
val x25519PublicKey: String
|
val x25519PublicKey = when (lhs) {
|
||||||
when (lhs) {
|
is Destination.Snode -> {
|
||||||
is OnionRequestAPI.Destination.Snode -> {
|
lhs.snode.publicKeySet!!.x25519Key
|
||||||
x25519PublicKey = lhs.snode.publicKeySet!!.x25519Key
|
|
||||||
}
|
}
|
||||||
is OnionRequestAPI.Destination.Server -> {
|
is Destination.Server -> {
|
||||||
x25519PublicKey = lhs.x25519PublicKey
|
lhs.x25519PublicKey
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val plaintext = encode(previousEncryptionResult.ciphertext, payload)
|
val plaintext = encode(previousEncryptionResult.ciphertext, payload)
|
||||||
|
@ -26,6 +26,7 @@ import org.session.libsignal.utilities.Base64
|
|||||||
import org.session.libsignal.utilities.Broadcaster
|
import org.session.libsignal.utilities.Broadcaster
|
||||||
import org.session.libsignal.utilities.HTTP
|
import org.session.libsignal.utilities.HTTP
|
||||||
import org.session.libsignal.utilities.Hex
|
import org.session.libsignal.utilities.Hex
|
||||||
|
import org.session.libsignal.utilities.JsonUtil
|
||||||
import org.session.libsignal.utilities.Log
|
import org.session.libsignal.utilities.Log
|
||||||
import org.session.libsignal.utilities.Snode
|
import org.session.libsignal.utilities.Snode
|
||||||
import org.session.libsignal.utilities.ThreadUtils
|
import org.session.libsignal.utilities.ThreadUtils
|
||||||
@ -93,16 +94,26 @@ object SnodeAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Internal API
|
// Internal API
|
||||||
internal fun invoke(method: Snode.Method, snode: Snode, publicKey: String? = null, parameters: Map<String, Any>): RawResponsePromise {
|
internal fun invoke(
|
||||||
|
method: Snode.Method,
|
||||||
|
snode: Snode,
|
||||||
|
parameters: Map<String, Any>,
|
||||||
|
publicKey: String? = null,
|
||||||
|
version: Version = Version.V3
|
||||||
|
): RawResponsePromise {
|
||||||
val url = "${snode.address}:${snode.port}/storage_rpc/v1"
|
val url = "${snode.address}:${snode.port}/storage_rpc/v1"
|
||||||
if (useOnionRequests) {
|
|
||||||
return OnionRequestAPI.sendOnionRequest(method, parameters, snode, publicKey)
|
|
||||||
} else {
|
|
||||||
val deferred = deferred<Map<*, *>, Exception>()
|
val deferred = deferred<Map<*, *>, Exception>()
|
||||||
|
if (useOnionRequests) {
|
||||||
|
OnionRequestAPI.sendOnionRequest(method, parameters, snode, version, publicKey).map {
|
||||||
|
val body = it.body ?: throw Error.Generic
|
||||||
|
deferred.resolve(JsonUtil.fromJson(body, Map::class.java))
|
||||||
|
}.fail { deferred.reject(it) }
|
||||||
|
} else {
|
||||||
ThreadUtils.queue {
|
ThreadUtils.queue {
|
||||||
val payload = mapOf( "method" to method.rawValue, "params" to parameters )
|
val payload = mapOf( "method" to method.rawValue, "params" to parameters )
|
||||||
try {
|
try {
|
||||||
val json = HTTP.execute(HTTP.Verb.POST, url, payload)
|
val response = HTTP.execute(HTTP.Verb.POST, url, payload).toString()
|
||||||
|
val json = JsonUtil.fromJson(response, Map::class.java)
|
||||||
deferred.resolve(json)
|
deferred.resolve(json)
|
||||||
} catch (exception: Exception) {
|
} catch (exception: Exception) {
|
||||||
val httpRequestFailedException = exception as? HTTP.HTTPRequestFailedException
|
val httpRequestFailedException = exception as? HTTP.HTTPRequestFailedException
|
||||||
@ -114,8 +125,8 @@ object SnodeAPI {
|
|||||||
deferred.reject(exception)
|
deferred.reject(exception)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return deferred.promise
|
|
||||||
}
|
}
|
||||||
|
return deferred.promise
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun getRandomSnode(): Promise<Snode, Exception> {
|
internal fun getRandomSnode(): Promise<Snode, Exception> {
|
||||||
@ -136,7 +147,12 @@ object SnodeAPI {
|
|||||||
deferred<Snode, Exception>()
|
deferred<Snode, Exception>()
|
||||||
ThreadUtils.queue {
|
ThreadUtils.queue {
|
||||||
try {
|
try {
|
||||||
val json = HTTP.execute(HTTP.Verb.POST, url, parameters, useSeedNodeConnection = true)
|
val response = HTTP.execute(HTTP.Verb.POST, url, parameters, useSeedNodeConnection = true)
|
||||||
|
val json = try {
|
||||||
|
JsonUtil.fromJson(response, Map::class.java)
|
||||||
|
} catch (exception: Exception) {
|
||||||
|
mapOf( "result" to response.toString())
|
||||||
|
}
|
||||||
val intermediate = json["result"] as? Map<*, *>
|
val intermediate = json["result"] as? Map<*, *>
|
||||||
val rawSnodes = intermediate?.get("service_node_states") as? List<*>
|
val rawSnodes = intermediate?.get("service_node_states") as? List<*>
|
||||||
if (rawSnodes != null) {
|
if (rawSnodes != null) {
|
||||||
@ -211,7 +227,7 @@ object SnodeAPI {
|
|||||||
val promises = (1..validationCount).map {
|
val promises = (1..validationCount).map {
|
||||||
getRandomSnode().bind { snode ->
|
getRandomSnode().bind { snode ->
|
||||||
retryIfNeeded(maxRetryCount) {
|
retryIfNeeded(maxRetryCount) {
|
||||||
invoke(Snode.Method.OxenDaemonRPCCall, snode, null, parameters)
|
invoke(Snode.Method.OxenDaemonRPCCall, snode, parameters)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -275,14 +291,14 @@ object SnodeAPI {
|
|||||||
|
|
||||||
fun getSwarm(publicKey: String): Promise<Set<Snode>, Exception> {
|
fun getSwarm(publicKey: String): Promise<Set<Snode>, Exception> {
|
||||||
val cachedSwarm = database.getSwarm(publicKey)
|
val cachedSwarm = database.getSwarm(publicKey)
|
||||||
if (cachedSwarm != null && cachedSwarm.size >= minimumSwarmSnodeCount) {
|
return if (cachedSwarm != null && cachedSwarm.size >= minimumSwarmSnodeCount) {
|
||||||
val cachedSwarmCopy = mutableSetOf<Snode>() // Workaround for a Kotlin compiler issue
|
val cachedSwarmCopy = mutableSetOf<Snode>() // Workaround for a Kotlin compiler issue
|
||||||
cachedSwarmCopy.addAll(cachedSwarm)
|
cachedSwarmCopy.addAll(cachedSwarm)
|
||||||
return task { cachedSwarmCopy }
|
task { cachedSwarmCopy }
|
||||||
} else {
|
} else {
|
||||||
val parameters = mapOf( "pubKey" to publicKey )
|
val parameters = mapOf( "pubKey" to publicKey )
|
||||||
return getRandomSnode().bind {
|
getRandomSnode().bind {
|
||||||
invoke(Snode.Method.GetSwarm, it, publicKey, parameters)
|
invoke(Snode.Method.GetSwarm, it, parameters, publicKey)
|
||||||
}.map {
|
}.map {
|
||||||
parseSnodes(it).toSet()
|
parseSnodes(it).toSet()
|
||||||
}.success {
|
}.success {
|
||||||
@ -329,7 +345,7 @@ object SnodeAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Make the request
|
// Make the request
|
||||||
return invoke(Snode.Method.GetMessages, snode, publicKey, parameters)
|
return invoke(Snode.Method.GetMessages, snode, parameters, publicKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getMessages(publicKey: String): MessageListPromise {
|
fun getMessages(publicKey: String): MessageListPromise {
|
||||||
@ -341,7 +357,7 @@ object SnodeAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun getNetworkTime(snode: Snode): Promise<Pair<Snode,Long>, Exception> {
|
private fun getNetworkTime(snode: Snode): Promise<Pair<Snode,Long>, Exception> {
|
||||||
return invoke(Snode.Method.Info, snode, null, emptyMap()).map { rawResponse ->
|
return invoke(Snode.Method.Info, snode, emptyMap()).map { rawResponse ->
|
||||||
val timestamp = rawResponse["timestamp"] as? Long ?: -1
|
val timestamp = rawResponse["timestamp"] as? Long ?: -1
|
||||||
snode to timestamp
|
snode to timestamp
|
||||||
}
|
}
|
||||||
@ -375,7 +391,7 @@ object SnodeAPI {
|
|||||||
parameters["namespace"] = namespace
|
parameters["namespace"] = namespace
|
||||||
}
|
}
|
||||||
getSingleTargetSnode(destination).bind { snode ->
|
getSingleTargetSnode(destination).bind { snode ->
|
||||||
invoke(Snode.Method.SendMessage, snode, destination, parameters)
|
invoke(Snode.Method.SendMessage, snode, parameters, destination)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -396,7 +412,7 @@ object SnodeAPI {
|
|||||||
"messages" to serverHashes,
|
"messages" to serverHashes,
|
||||||
"signature" to Base64.encodeBytes(signature)
|
"signature" to Base64.encodeBytes(signature)
|
||||||
)
|
)
|
||||||
invoke(Snode.Method.DeleteMessage, snode, publicKey, deleteMessageParams).map { rawResponse ->
|
invoke(Snode.Method.DeleteMessage, snode, deleteMessageParams, publicKey).map { rawResponse ->
|
||||||
val swarms = rawResponse["swarm"] as? Map<String, Any> ?: return@map mapOf()
|
val swarms = rawResponse["swarm"] as? Map<String, Any> ?: return@map mapOf()
|
||||||
val result = swarms.mapNotNull { (hexSnodePublicKey, rawJSON) ->
|
val result = swarms.mapNotNull { (hexSnodePublicKey, rawJSON) ->
|
||||||
val json = rawJSON as? Map<String, Any> ?: return@mapNotNull null
|
val json = rawJSON as? Map<String, Any> ?: return@mapNotNull null
|
||||||
@ -466,7 +482,7 @@ object SnodeAPI {
|
|||||||
"timestamp" to timestamp,
|
"timestamp" to timestamp,
|
||||||
"signature" to Base64.encodeBytes(signature)
|
"signature" to Base64.encodeBytes(signature)
|
||||||
)
|
)
|
||||||
invoke(Snode.Method.DeleteAll, snode, userPublicKey, deleteMessageParams).map {
|
invoke(Snode.Method.DeleteAll, snode, deleteMessageParams, userPublicKey).map {
|
||||||
rawResponse -> parseDeletions(userPublicKey, timestamp, rawResponse)
|
rawResponse -> parseDeletions(userPublicKey, timestamp, rawResponse)
|
||||||
}.fail { e ->
|
}.fail { e ->
|
||||||
Log.e("Loki", "Failed to clear data", e)
|
Log.e("Loki", "Failed to clear data", e)
|
||||||
|
@ -25,8 +25,10 @@ class Address private constructor(address: String) : Parcelable, Comparable<Addr
|
|||||||
get() = GroupUtil.isClosedGroup(address)
|
get() = GroupUtil.isClosedGroup(address)
|
||||||
val isOpenGroup: Boolean
|
val isOpenGroup: Boolean
|
||||||
get() = GroupUtil.isOpenGroup(address)
|
get() = GroupUtil.isOpenGroup(address)
|
||||||
|
val isOpenGroupInbox: Boolean
|
||||||
|
get() = GroupUtil.isOpenGroupInbox(address)
|
||||||
val isContact: Boolean
|
val isContact: Boolean
|
||||||
get() = !isGroup
|
get() = !(isGroup || isOpenGroupInbox)
|
||||||
|
|
||||||
fun contactIdentifier(): String {
|
fun contactIdentifier(): String {
|
||||||
if (!isContact && !isOpenGroup) {
|
if (!isContact && !isOpenGroup) {
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
package org.session.libsession.utilities
|
package org.session.libsession.utilities
|
||||||
|
|
||||||
import okhttp3.HttpUrl
|
import okhttp3.HttpUrl
|
||||||
import org.session.libsession.messaging.file_server.FileServerAPIV2
|
import org.session.libsession.messaging.file_server.FileServerApi
|
||||||
import org.session.libsignal.utilities.Log
|
import org.session.libsignal.utilities.Log
|
||||||
import org.session.libsignal.messages.SignalServiceAttachment
|
|
||||||
import java.io.*
|
import java.io.*
|
||||||
|
|
||||||
object DownloadUtilities {
|
object DownloadUtilities {
|
||||||
@ -37,7 +36,7 @@ object DownloadUtilities {
|
|||||||
val url = HttpUrl.parse(urlAsString)!!
|
val url = HttpUrl.parse(urlAsString)!!
|
||||||
val fileID = url.pathSegments().last()
|
val fileID = url.pathSegments().last()
|
||||||
try {
|
try {
|
||||||
FileServerAPIV2.download(fileID.toLong()).get().let {
|
FileServerApi.download(fileID).get().let {
|
||||||
outputStream.write(it)
|
outputStream.write(it)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
@ -8,12 +8,18 @@ import kotlin.jvm.Throws
|
|||||||
object GroupUtil {
|
object GroupUtil {
|
||||||
const val CLOSED_GROUP_PREFIX = "__textsecure_group__!"
|
const val CLOSED_GROUP_PREFIX = "__textsecure_group__!"
|
||||||
const val OPEN_GROUP_PREFIX = "__loki_public_chat_group__!"
|
const val OPEN_GROUP_PREFIX = "__loki_public_chat_group__!"
|
||||||
|
const val OPEN_GROUP_INBOX_PREFIX = "__open_group_inbox__!"
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun getEncodedOpenGroupID(groupID: ByteArray): String {
|
fun getEncodedOpenGroupID(groupID: ByteArray): String {
|
||||||
return OPEN_GROUP_PREFIX + Hex.toStringCondensed(groupID)
|
return OPEN_GROUP_PREFIX + Hex.toStringCondensed(groupID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun getEncodedOpenGroupInboxID(groupInboxID: ByteArray): String {
|
||||||
|
return OPEN_GROUP_INBOX_PREFIX + Hex.toStringCondensed(groupInboxID)
|
||||||
|
}
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun getEncodedClosedGroupID(groupID: ByteArray): String {
|
fun getEncodedClosedGroupID(groupID: ByteArray): String {
|
||||||
return CLOSED_GROUP_PREFIX + Hex.toStringCondensed(groupID)
|
return CLOSED_GROUP_PREFIX + Hex.toStringCondensed(groupID)
|
||||||
@ -45,6 +51,15 @@ object GroupUtil {
|
|||||||
return Hex.fromStringCondensed(splitEncodedGroupID(groupID))
|
return Hex.fromStringCondensed(splitEncodedGroupID(groupID))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun getDecodedOpenGroupInbox(groupID: String): String {
|
||||||
|
val decodedGroupId = getDecodedGroupID(groupID)
|
||||||
|
if (decodedGroupId.split("!").count() > 2) {
|
||||||
|
return decodedGroupId.split("!", limit = 3)[2]
|
||||||
|
}
|
||||||
|
return decodedGroupId
|
||||||
|
}
|
||||||
|
|
||||||
fun isEncodedGroup(groupId: String): Boolean {
|
fun isEncodedGroup(groupId: String): Boolean {
|
||||||
return groupId.startsWith(CLOSED_GROUP_PREFIX) || groupId.startsWith(OPEN_GROUP_PREFIX)
|
return groupId.startsWith(CLOSED_GROUP_PREFIX) || groupId.startsWith(OPEN_GROUP_PREFIX)
|
||||||
}
|
}
|
||||||
@ -54,6 +69,11 @@ object GroupUtil {
|
|||||||
return groupId.startsWith(OPEN_GROUP_PREFIX)
|
return groupId.startsWith(OPEN_GROUP_PREFIX)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun isOpenGroupInbox(groupId: String): Boolean {
|
||||||
|
return groupId.startsWith(OPEN_GROUP_INBOX_PREFIX)
|
||||||
|
}
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun isClosedGroup(groupId: String): Boolean {
|
fun isClosedGroup(groupId: String): Boolean {
|
||||||
return groupId.startsWith(CLOSED_GROUP_PREFIX)
|
return groupId.startsWith(CLOSED_GROUP_PREFIX)
|
||||||
|
@ -4,7 +4,7 @@ import android.content.Context
|
|||||||
import nl.komponents.kovenant.Promise
|
import nl.komponents.kovenant.Promise
|
||||||
import nl.komponents.kovenant.deferred
|
import nl.komponents.kovenant.deferred
|
||||||
import okio.Buffer
|
import okio.Buffer
|
||||||
import org.session.libsession.messaging.file_server.FileServerAPIV2
|
import org.session.libsession.messaging.file_server.FileServerApi
|
||||||
import org.session.libsignal.streams.ProfileCipherOutputStream
|
import org.session.libsignal.streams.ProfileCipherOutputStream
|
||||||
import org.session.libsignal.utilities.ProfileAvatarData
|
import org.session.libsignal.utilities.ProfileAvatarData
|
||||||
import org.session.libsignal.streams.DigestingRequestBody
|
import org.session.libsignal.streams.DigestingRequestBody
|
||||||
@ -30,13 +30,13 @@ object ProfilePictureUtilities {
|
|||||||
var id: Long = 0
|
var id: Long = 0
|
||||||
try {
|
try {
|
||||||
id = retryIfNeeded(4) {
|
id = retryIfNeeded(4) {
|
||||||
FileServerAPIV2.upload(data)
|
FileServerApi.upload(data)
|
||||||
}.get()
|
}.get()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
deferred.reject(e)
|
deferred.reject(e)
|
||||||
}
|
}
|
||||||
TextSecurePreferences.setLastProfilePictureUpload(context, Date().time)
|
TextSecurePreferences.setLastProfilePictureUpload(context, Date().time)
|
||||||
val url = "${FileServerAPIV2.server}/files/$id"
|
val url = "${FileServerApi.server}/file/$id"
|
||||||
TextSecurePreferences.setProfilePictureURL(context, url)
|
TextSecurePreferences.setProfilePictureURL(context, url)
|
||||||
deferred.resolve(Unit)
|
deferred.resolve(Unit)
|
||||||
}
|
}
|
||||||
|
@ -40,6 +40,7 @@ import org.session.libsession.messaging.contacts.Contact;
|
|||||||
import org.session.libsession.utilities.Address;
|
import org.session.libsession.utilities.Address;
|
||||||
import org.session.libsession.utilities.FutureTaskListener;
|
import org.session.libsession.utilities.FutureTaskListener;
|
||||||
import org.session.libsession.utilities.GroupRecord;
|
import org.session.libsession.utilities.GroupRecord;
|
||||||
|
import org.session.libsession.utilities.GroupUtil;
|
||||||
import org.session.libsession.utilities.ListenableFutureTask;
|
import org.session.libsession.utilities.ListenableFutureTask;
|
||||||
import org.session.libsession.utilities.MaterialColor;
|
import org.session.libsession.utilities.MaterialColor;
|
||||||
import org.session.libsession.utilities.ProfilePictureModifiedEvent;
|
import org.session.libsession.utilities.ProfilePictureModifiedEvent;
|
||||||
@ -314,6 +315,11 @@ public class Recipient implements RecipientModifiedListener {
|
|||||||
} else {
|
} else {
|
||||||
return this.name;
|
return this.name;
|
||||||
}
|
}
|
||||||
|
} else if (isOpenGroupInboxRecipient()){
|
||||||
|
String inboxID = GroupUtil.getDecodedOpenGroupInbox(sessionID);
|
||||||
|
Contact contact = storage.getContactWithSessionID(inboxID);
|
||||||
|
if (contact == null) { return sessionID; }
|
||||||
|
return contact.displayName(Contact.ContactContext.REGULAR);
|
||||||
} else {
|
} else {
|
||||||
Contact contact = storage.getContactWithSessionID(sessionID);
|
Contact contact = storage.getContactWithSessionID(sessionID);
|
||||||
if (contact == null) { return sessionID; }
|
if (contact == null) { return sessionID; }
|
||||||
@ -431,6 +437,10 @@ public class Recipient implements RecipientModifiedListener {
|
|||||||
return address.isOpenGroup();
|
return address.isOpenGroup();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isOpenGroupInboxRecipient() {
|
||||||
|
return address.isOpenGroupInbox();
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isClosedGroupRecipient() {
|
public boolean isClosedGroupRecipient() {
|
||||||
return address.isClosedGroup();
|
return address.isClosedGroup();
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,7 @@ dependencies {
|
|||||||
implementation "androidx.annotation:annotation:1.2.0"
|
implementation "androidx.annotation:annotation:1.2.0"
|
||||||
implementation "com.google.protobuf:protobuf-java:$protobufVersion"
|
implementation "com.google.protobuf:protobuf-java:$protobufVersion"
|
||||||
implementation "com.fasterxml.jackson.core:jackson-databind:$jacksonDatabindVersion"
|
implementation "com.fasterxml.jackson.core:jackson-databind:$jacksonDatabindVersion"
|
||||||
implementation "org.whispersystems:curve25519-java:$curve25519Version"
|
implementation "com.github.oxen-io.session-android-curve-25519:curve25519-java:$curve25519Version"
|
||||||
implementation "com.squareup.okhttp3:okhttp:$okhttpVersion"
|
implementation "com.squareup.okhttp3:okhttp:$okhttpVersion"
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
|
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
|
||||||
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion"
|
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion"
|
||||||
|
@ -74,26 +74,26 @@ object HTTP {
|
|||||||
/**
|
/**
|
||||||
* Sync. Don't call from the main thread.
|
* Sync. Don't call from the main thread.
|
||||||
*/
|
*/
|
||||||
fun execute(verb: Verb, url: String, timeout: Long = HTTP.timeout, useSeedNodeConnection: Boolean = false): Map<*, *> {
|
fun execute(verb: Verb, url: String, timeout: Long = HTTP.timeout, useSeedNodeConnection: Boolean = false): ByteArray {
|
||||||
return execute(verb = verb, url = url, body = null, timeout = timeout, useSeedNodeConnection = useSeedNodeConnection)
|
return execute(verb = verb, url = url, body = null, timeout = timeout, useSeedNodeConnection = useSeedNodeConnection)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sync. Don't call from the main thread.
|
* Sync. Don't call from the main thread.
|
||||||
*/
|
*/
|
||||||
fun execute(verb: Verb, url: String, parameters: Map<String, Any>?, timeout: Long = HTTP.timeout, useSeedNodeConnection: Boolean = false): Map<*, *> {
|
fun execute(verb: Verb, url: String, parameters: Map<String, Any>?, timeout: Long = HTTP.timeout, useSeedNodeConnection: Boolean = false): ByteArray {
|
||||||
if (parameters != null) {
|
return if (parameters != null) {
|
||||||
val body = JsonUtil.toJson(parameters).toByteArray()
|
val body = JsonUtil.toJson(parameters).toByteArray()
|
||||||
return execute(verb = verb, url = url, body = body, timeout = timeout, useSeedNodeConnection = useSeedNodeConnection)
|
execute(verb = verb, url = url, body = body, timeout = timeout, useSeedNodeConnection = useSeedNodeConnection)
|
||||||
} else {
|
} else {
|
||||||
return execute(verb = verb, url = url, body = null, timeout = timeout, useSeedNodeConnection = useSeedNodeConnection)
|
execute(verb = verb, url = url, body = null, timeout = timeout, useSeedNodeConnection = useSeedNodeConnection)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sync. Don't call from the main thread.
|
* Sync. Don't call from the main thread.
|
||||||
*/
|
*/
|
||||||
fun execute(verb: Verb, url: String, body: ByteArray?, timeout: Long = HTTP.timeout, useSeedNodeConnection: Boolean = false): Map<*, *> {
|
fun execute(verb: Verb, url: String, body: ByteArray?, timeout: Long = HTTP.timeout, useSeedNodeConnection: Boolean = false): ByteArray {
|
||||||
val request = Request.Builder().url(url)
|
val request = Request.Builder().url(url)
|
||||||
.removeHeader("User-Agent").addHeader("User-Agent", "WhatsApp") // Set a fake value
|
.removeHeader("User-Agent").addHeader("User-Agent", "WhatsApp") // Set a fake value
|
||||||
.removeHeader("Accept-Language").addHeader("Accept-Language", "en-us") // Set a fake value
|
.removeHeader("Accept-Language").addHeader("Accept-Language", "en-us") // Set a fake value
|
||||||
@ -109,14 +109,13 @@ object HTTP {
|
|||||||
}
|
}
|
||||||
lateinit var response: Response
|
lateinit var response: Response
|
||||||
try {
|
try {
|
||||||
val connection: OkHttpClient
|
val connection = if (timeout != HTTP.timeout) { // Custom timeout
|
||||||
if (timeout != HTTP.timeout) { // Custom timeout
|
|
||||||
if (useSeedNodeConnection) {
|
if (useSeedNodeConnection) {
|
||||||
throw IllegalStateException("Setting a custom timeout is only allowed for requests to snodes.")
|
throw IllegalStateException("Setting a custom timeout is only allowed for requests to snodes.")
|
||||||
}
|
}
|
||||||
connection = getDefaultConnection(timeout)
|
getDefaultConnection(timeout)
|
||||||
} else {
|
} else {
|
||||||
connection = if (useSeedNodeConnection) seedNodeConnection else defaultConnection
|
if (useSeedNodeConnection) seedNodeConnection else defaultConnection
|
||||||
}
|
}
|
||||||
response = connection.newCall(request.build()).execute()
|
response = connection.newCall(request.build()).execute()
|
||||||
} catch (exception: Exception) {
|
} catch (exception: Exception) {
|
||||||
@ -124,14 +123,9 @@ object HTTP {
|
|||||||
// Override the actual error so that we can correctly catch failed requests in OnionRequestAPI
|
// Override the actual error so that we can correctly catch failed requests in OnionRequestAPI
|
||||||
throw HTTPRequestFailedException(0, null)
|
throw HTTPRequestFailedException(0, null)
|
||||||
}
|
}
|
||||||
when (val statusCode = response.code()) {
|
return when (val statusCode = response.code()) {
|
||||||
200 -> {
|
200 -> {
|
||||||
val bodyAsString = response.body()?.string() ?: throw Exception("An error occurred.")
|
response.body()?.bytes() ?: throw Exception("An error occurred.")
|
||||||
try {
|
|
||||||
return JsonUtil.fromJson(bodyAsString, Map::class.java)
|
|
||||||
} catch (exception: Exception) {
|
|
||||||
return mapOf( "result" to bodyAsString)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
Log.d("Loki", "${verb.rawValue} request to $url failed with status code: $statusCode.")
|
Log.d("Loki", "${verb.rawValue} request to $url failed with status code: $statusCode.")
|
||||||
|
@ -0,0 +1,15 @@
|
|||||||
|
package org.session.libsignal.utilities
|
||||||
|
|
||||||
|
enum class IdPrefix(val value: String) {
|
||||||
|
STANDARD("05"), BLINDED("15"), UN_BLINDED("00");
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromValue(rawValue: String): IdPrefix? = when(rawValue.take(2)) {
|
||||||
|
STANDARD.value -> STANDARD
|
||||||
|
BLINDED.value -> BLINDED
|
||||||
|
UN_BLINDED.value -> UN_BLINDED
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,6 +1,7 @@
|
|||||||
package org.session.libsignal.utilities;
|
package org.session.libsignal.utilities;
|
||||||
|
|
||||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||||
import com.fasterxml.jackson.databind.JavaType;
|
import com.fasterxml.jackson.databind.JavaType;
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
@ -30,6 +31,10 @@ public class JsonUtil {
|
|||||||
return fromJson(new String(serialized), clazz);
|
return fromJson(new String(serialized), clazz);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static <T> T fromJson(String serialized, TypeReference<T> typeReference) throws IOException {
|
||||||
|
return objectMapper.readValue(serialized, typeReference);
|
||||||
|
}
|
||||||
|
|
||||||
public static <T> T fromJson(String serialized, Class<T> clazz) throws IOException {
|
public static <T> T fromJson(String serialized, Class<T> clazz) throws IOException {
|
||||||
return objectMapper.readValue(serialized, clazz);
|
return objectMapper.readValue(serialized, clazz);
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,10 @@
|
|||||||
package org.session.libsignal.utilities
|
package org.session.libsignal.utilities
|
||||||
|
|
||||||
import org.session.libsignal.utilities.Hex
|
fun String.removingIdPrefixIfNeeded(): String {
|
||||||
|
return if (length == 66 && IdPrefix.fromValue(this) != null) removeRange(0..1) else this
|
||||||
fun String.removing05PrefixIfNeeded(): String {
|
|
||||||
return if (length == 66) removePrefix("05") else this
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun ByteArray.removing05PrefixIfNeeded(): ByteArray {
|
fun ByteArray.removingIdPrefixIfNeeded(): ByteArray {
|
||||||
val string = Hex.toStringCondensed(this).removing05PrefixIfNeeded()
|
val string = Hex.toStringCondensed(this).removingIdPrefixIfNeeded()
|
||||||
return Hex.fromStringCondensed(string)
|
return Hex.fromStringCondensed(string)
|
||||||
}
|
}
|
||||||
|
@ -10,9 +10,9 @@ object PublicKeyValidation {
|
|||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun isValid(candidate: String, expectedLength: Int, isPrefixRequired: Boolean): Boolean {
|
fun isValid(candidate: String, expectedLength: Int, isPrefixRequired: Boolean): Boolean {
|
||||||
val hexCharacters = "0123456789ABCDEF".toSet()
|
val hexCharacters = "0123456789ABCDEF".toSet()
|
||||||
val isValidHexEncoding = hexCharacters.containsAll(candidate.toUpperCase().toSet())
|
val isValidHexEncoding = hexCharacters.containsAll(candidate.uppercase().toSet())
|
||||||
val hasValidLength = candidate.length == expectedLength
|
val hasValidLength = candidate.length == expectedLength
|
||||||
val hasValidPrefix = if (isPrefixRequired) candidate.startsWith("05") else true
|
val hasValidPrefix = if (isPrefixRequired) IdPrefix.fromValue(candidate) != null else true
|
||||||
return isValidHexEncoding && hasValidLength && hasValidPrefix
|
return isValidHexEncoding && hasValidLength && hasValidPrefix
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
rootProject.name = "session-android"
|
rootProject.name = "session-android"
|
||||||
|
|
||||||
include ':app'
|
include ':app'
|
||||||
|
include ':liblazysodium'
|
||||||
include ':libsession'
|
include ':libsession'
|
||||||
include ':libsignal'
|
include ':libsignal'
|
Loading…
Reference in New Issue
Block a user