diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml
index e9d3c6ab2f..b8bd208960 100644
--- a/.github/workflows/build_and_test.yml
+++ b/.github/workflows/build_and_test.yml
@@ -30,7 +30,7 @@ jobs:
submodules: 'recursive'
- name: Cache Gradle
- uses: actions/cache@v4
+ uses: actions/cache@v5
with:
path: |
~/.gradle/caches
@@ -54,14 +54,14 @@ jobs:
- name: Upload build reports regardless
if: always()
- uses: actions/upload-artifact@v5
+ uses: actions/upload-artifact@v6
with:
name: build-reports-${{ matrix.variant }}-${{ matrix.build_type }}
path: app/build/reports
if-no-files-found: ignore
- name: Upload artifacts
- uses: actions/upload-artifact@v5
+ uses: actions/upload-artifact@v6
with:
name: session-${{ matrix.variant }}-${{ matrix.build_type }}
path: app/build/outputs/apk/${{ matrix.variant }}/${{ matrix.build_type }}/*-universal*apk
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index a7b9f32f6b..b7a9a074b5 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -12,9 +12,9 @@ plugins {
alias(libs.plugins.hilt.android)
alias(libs.plugins.dependency.analysis)
alias(libs.plugins.google.services)
- alias(libs.plugins.protobuf.compiler)
id("generate-ip-country-data")
+ id("local-snode-pool")
id("rename-apk")
id("witness")
}
@@ -26,8 +26,8 @@ configurations.configureEach {
exclude(module = "commons-logging")
}
-val canonicalVersionCode = 436
-val canonicalVersionName = "1.30.3"
+val canonicalVersionCode = 437
+val canonicalVersionName = "1.31.0"
val postFixSize = 10
val abiPostFix = mapOf(
@@ -86,22 +86,7 @@ kotlin {
}
}
-protobuf {
- protoc {
- artifact = libs.protoc.get().toString()
- }
-
- plugins {
- generateProtoTasks {
- all().forEach {
- it.builtins {
- create("java") {
- }
- }
- }
- }
- }
-}
+val testJvmAgent = configurations.create("mockitoAgent")
android {
namespace = "network.loki.messenger"
@@ -151,6 +136,13 @@ android {
buildConfigField("String", "USER_AGENT", "\"OWA\"")
buildConfigField("int", "CANONICAL_VERSION_CODE", "$canonicalVersionCode")
+ buildConfigField("org.thoughtcrime.securesms.pro.ProBackendConfig", "PRO_BACKEND_DEV", """
+ new org.thoughtcrime.securesms.pro.ProBackendConfig(
+ "https://pro-backend-dev.getsession.org",
+ "fc947730f49eb01427a66e050733294d9e520e545c7a27125a780634e0860a27"
+ )
+ """.trimIndent())
+
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
testInstrumentationRunnerArguments["clearPackageData"] = "true"
testOptions {
@@ -303,6 +295,9 @@ android {
testOptions {
unitTests.isIncludeAndroidResources = true
+ unitTests.all {
+ it.jvmArgs("-javaagent:${testJvmAgent.asPath}")
+ }
}
lint {
@@ -332,6 +327,8 @@ android {
testNamespace = "network.loki.messenger.test"
}
+
+
dependencies {
implementation(project(":content-descriptions"))
@@ -394,28 +391,19 @@ dependencies {
implementation(libs.androidx.sqlite.ktx)
implementation(libs.sqlcipher.android)
implementation(libs.kotlinx.serialization.json)
- implementation(libs.protobuf.java)
implementation(libs.jackson.databind)
implementation(libs.okhttp)
implementation(libs.phrase)
implementation(libs.copper.flow)
implementation(libs.kotlinx.coroutines.android)
implementation(libs.kotlinx.coroutines.guava)
- implementation(libs.kovenant)
- implementation(libs.kovenant.android)
implementation(libs.opencsv)
implementation(libs.androidx.work.runtime.ktx)
implementation(libs.rxbinding)
- if (hasIncludedLibSessionUtilProject) {
- implementation(
- group = libs.libsession.util.android.get().group,
- name = libs.libsession.util.android.get().name,
- version = "dev-snapshot"
- )
- } else {
- implementation(libs.libsession.util.android)
- }
+ // If libsession_util project is included into the build, use that, otherwise use the published version
+ findProject(":libsession-util-android")?.let(::implementation)
+ ?: implementation(libs.libsession.util.android)
implementation(libs.kryo)
testImplementation(libs.junit)
@@ -434,8 +422,14 @@ dependencies {
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.truth)
testImplementation(libs.truth)
+ testImplementation(libs.androidx.sqlite.framework)
androidTestImplementation(libs.truth)
testRuntimeOnly(libs.mockito.core)
+ testImplementation(libs.mockk)
+ testImplementation(libs.kotlin.test)
+
+ // Pull in appropriate JVM agents for unit test
+ testJvmAgent(libs.mockito.core) { isTransitive = false }
androidTestImplementation(libs.androidx.espresso.core)
androidTestImplementation(libs.androidx.espresso.contrib)
@@ -449,7 +443,6 @@ dependencies {
androidTestUtil(libs.androidx.orchestrator)
testImplementation(libs.robolectric)
- testImplementation(libs.robolectric.shadows.multidex)
testImplementation(libs.conscrypt.openjdk.uber)
testImplementation(libs.turbine)
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index cce3a226fb..3babe8fe18 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -218,6 +218,17 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/java/org/session/libsession/database/StorageProtocol.kt b/app/src/main/java/org/session/libsession/database/StorageProtocol.kt
index 1d99e66c52..030b5071c9 100644
--- a/app/src/main/java/org/session/libsession/database/StorageProtocol.kt
+++ b/app/src/main/java/org/session/libsession/database/StorageProtocol.kt
@@ -57,6 +57,7 @@ interface StorageProtocol {
// Servers
fun setServerCapabilities(server: String, capabilities: List)
fun getServerCapabilities(server: String): List?
+ fun clearServerCapabilities(server: String)
// Open Groups
suspend fun addOpenGroup(urlAsString: String)
diff --git a/app/src/main/java/org/session/libsession/messaging/MessagingModuleConfiguration.kt b/app/src/main/java/org/session/libsession/messaging/MessagingModuleConfiguration.kt
index 7c3565f501..46fa5e6f12 100644
--- a/app/src/main/java/org/session/libsession/messaging/MessagingModuleConfiguration.kt
+++ b/app/src/main/java/org/session/libsession/messaging/MessagingModuleConfiguration.kt
@@ -7,9 +7,9 @@ import org.session.libsession.database.MessageDataProvider
import org.session.libsession.database.StorageProtocol
import org.session.libsession.messaging.groups.GroupManagerV2
import org.session.libsession.messaging.groups.LegacyGroupDeprecationManager
-import org.session.libsession.messaging.jobs.MessageSendJob
import org.session.libsession.messaging.notifications.TokenFetcher
-import org.session.libsession.snode.SnodeClock
+import org.session.libsession.network.SnodeClock
+import org.session.libsession.network.onion.PathManager
import org.session.libsession.utilities.ConfigFactoryProtocol
import org.session.libsession.utilities.Device
import org.session.libsession.utilities.TextSecurePreferences
@@ -28,14 +28,14 @@ class MessagingModuleConfiguration @Inject constructor(
val configFactory: ConfigFactoryProtocol,
val tokenFetcher: TokenFetcher,
val groupManagerV2: GroupManagerV2,
- val clock: SnodeClock,
val preferences: TextSecurePreferences,
val deprecationManager: LegacyGroupDeprecationManager,
val recipientRepository: RecipientRepository,
val avatarUtils: AvatarUtils,
val proStatusManager: ProStatusManager,
- val messageSendJobFactory: MessageSendJob.Factory,
val json: Json,
+ val snodeClock: SnodeClock,
+ val pathManager: PathManager
) {
companion object {
diff --git a/app/src/main/java/org/session/libsession/messaging/file_server/FileDownloadApi.kt b/app/src/main/java/org/session/libsession/messaging/file_server/FileDownloadApi.kt
new file mode 100644
index 0000000000..b7f1b26ab3
--- /dev/null
+++ b/app/src/main/java/org/session/libsession/messaging/file_server/FileDownloadApi.kt
@@ -0,0 +1,60 @@
+package org.session.libsession.messaging.file_server
+
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import okhttp3.HttpUrl.Companion.toHttpUrl
+import org.thoughtcrime.securesms.api.server.ServerApiErrorManager
+import org.thoughtcrime.securesms.api.ApiExecutorContext
+import org.thoughtcrime.securesms.api.http.HttpBody
+import org.thoughtcrime.securesms.api.http.HttpRequest
+import org.thoughtcrime.securesms.api.http.HttpResponse
+import org.thoughtcrime.securesms.api.server.ServerApi
+import java.time.ZonedDateTime
+import java.time.format.DateTimeFormatter
+
+class FileDownloadApi @AssistedInject constructor(
+ @Assisted private val fileId: String,
+ errorManager: ServerApiErrorManager,
+) : ServerApi(errorManager) {
+ override fun buildRequest(
+ baseUrl: String,
+ x25519PubKeyHex: String
+ ): HttpRequest {
+ return HttpRequest(
+ url = "$baseUrl/file/$fileId".toHttpUrl(),
+ method = "GET",
+ headers = emptyMap(),
+ body = null
+ )
+ }
+
+ override suspend fun handleSuccessResponse(
+ executorContext: ApiExecutorContext,
+ baseUrl: String,
+ response: HttpResponse
+ ): Response {
+ return Response(
+ data = response.body,
+ expires = response.parseFileServerExpiresHeader()
+ )
+ }
+
+ class Response(
+ val data: HttpBody,
+ val expires: ZonedDateTime?
+ )
+
+ @AssistedFactory
+ interface Factory {
+ fun create(fileId: String): FileDownloadApi
+ }
+
+ companion object {
+ fun HttpResponse.parseFileServerExpiresHeader(): ZonedDateTime? {
+ return headers["expires"]?.let {
+ ZonedDateTime.parse(it, DateTimeFormatter.RFC_1123_DATE_TIME)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/session/libsession/messaging/file_server/FileRenewApi.kt b/app/src/main/java/org/session/libsession/messaging/file_server/FileRenewApi.kt
new file mode 100644
index 0000000000..143b6e5686
--- /dev/null
+++ b/app/src/main/java/org/session/libsession/messaging/file_server/FileRenewApi.kt
@@ -0,0 +1,40 @@
+package org.session.libsession.messaging.file_server
+
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import okhttp3.HttpUrl.Companion.toHttpUrl
+import org.thoughtcrime.securesms.api.server.ServerApiErrorManager
+import org.thoughtcrime.securesms.api.ApiExecutorContext
+import org.thoughtcrime.securesms.api.http.HttpRequest
+import org.thoughtcrime.securesms.api.http.HttpResponse
+import org.thoughtcrime.securesms.api.server.ServerApi
+
+class FileRenewApi @AssistedInject constructor(
+ @Assisted private val fileId: String,
+ errorManager: ServerApiErrorManager,
+) : ServerApi(errorManager) {
+
+ override fun buildRequest(
+ baseUrl: String,
+ x25519PubKeyHex: String
+ ): HttpRequest {
+ return HttpRequest(
+ url = "$baseUrl/file/$fileId/extend".toHttpUrl(),
+ method = "POST",
+ headers = emptyMap(),
+ body = null,
+ )
+ }
+
+ override suspend fun handleSuccessResponse(
+ executorContext: ApiExecutorContext,
+ baseUrl: String,
+ response: HttpResponse
+ ) = Unit
+
+ @AssistedFactory
+ interface Factory {
+ fun create(fileId: String): FileRenewApi
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/session/libsession/messaging/file_server/FileServer.kt b/app/src/main/java/org/session/libsession/messaging/file_server/FileServer.kt
index 1d376ea8c9..cbdf01b8ac 100644
--- a/app/src/main/java/org/session/libsession/messaging/file_server/FileServer.kt
+++ b/app/src/main/java/org/session/libsession/messaging/file_server/FileServer.kt
@@ -1,9 +1,11 @@
package org.session.libsession.messaging.file_server
import kotlinx.serialization.Serializable
+import network.loki.messenger.libsession_util.Curve25519
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl
import org.session.libsession.utilities.serializable.HttpUrlSerializer
+import org.session.libsignal.utilities.Hex
@Serializable
data class FileServer(
@@ -12,6 +14,12 @@ data class FileServer(
val ed25519PublicKeyHex: String
) {
constructor(url: String, ed25519PublicKeyHex: String) : this(url.toHttpUrl(), ed25519PublicKeyHex)
+
+ val x25519PubKeyHex: String by lazy {
+ Hex.toStringCondensed(
+ Curve25519.pubKeyFromED25519(Hex.fromStringCondensed(ed25519PublicKeyHex))
+ )
+ }
}
val HttpUrl.isOfficial: Boolean
diff --git a/app/src/main/java/org/session/libsession/messaging/file_server/FileServerApi.kt b/app/src/main/java/org/session/libsession/messaging/file_server/FileServerApi.kt
deleted file mode 100644
index e5128232f7..0000000000
--- a/app/src/main/java/org/session/libsession/messaging/file_server/FileServerApi.kt
+++ /dev/null
@@ -1,361 +0,0 @@
-package org.session.libsession.messaging.file_server
-
-import android.util.Base64
-import kotlinx.coroutines.CancellationException
-import network.loki.messenger.libsession_util.Curve25519
-import network.loki.messenger.libsession_util.ED25519
-import network.loki.messenger.libsession_util.util.BlindKeyAPI
-import okhttp3.Headers.Companion.toHeaders
-import okhttp3.HttpUrl
-import okhttp3.MediaType.Companion.toMediaType
-import okhttp3.RequestBody
-import okhttp3.RequestBody.Companion.toRequestBody
-import org.session.libsession.database.StorageProtocol
-import org.session.libsession.snode.OnionRequestAPI
-import org.session.libsession.snode.utilities.await
-import org.session.libsignal.utilities.ByteArraySlice
-import org.session.libsignal.utilities.HTTP
-import org.session.libsignal.utilities.Hex
-import org.session.libsignal.utilities.JsonUtil
-import org.session.libsignal.utilities.Log
-import org.session.libsignal.utilities.toHexString
-import org.thoughtcrime.securesms.util.DateUtils.Companion.asEpochSeconds
-import java.time.ZonedDateTime
-import java.time.format.DateTimeFormatter
-import javax.inject.Inject
-import javax.inject.Singleton
-import kotlin.time.Duration
-import kotlin.time.Duration.Companion.milliseconds
-
-@Singleton
-class FileServerApi @Inject constructor(
- private val storage: StorageProtocol,
-) {
-
- companion object {
- const val MAX_FILE_SIZE = 10_000_000 // 10 MB
-
- val DEFAULT_FILE_SERVER: FileServer = FileServer(
- url = "http://filev2.getsession.org",
- ed25519PublicKeyHex = "b8eef9821445ae16e2e97ef8aa6fe782fd11ad5253cd6723b281341dba22e371"
- )
- }
-
- sealed class Error(message: String) : Exception(message) {
- object ParsingFailed : Error("Invalid response.")
- object InvalidURL : Error("Invalid URL.")
- object NoEd25519KeyPair : Error("Couldn't find ed25519 key pair.")
- }
-
- private data class Request(
- val fileServer: FileServer,
- val verb: HTTP.Verb,
- val endpoint: String,
- val queryParameters: Map = mapOf(),
- val parameters: Any? = null,
- val headers: Map = mapOf(),
- val body: ByteArray? = null,
- /**
- * Always `true` under normal circumstances. You might want to disable
- * this when running over Lokinet.
- */
- val useOnionRouting: Boolean = true
- )
-
- private fun createBody(body: ByteArray?, parameters: Any?): RequestBody? {
- if (body != null) return body.toRequestBody(
- "application/octet-stream".toMediaType(),
- 0,
- body.size
- )
-
- if (parameters == null) return null
- val parametersAsJSON = JsonUtil.toJson(parameters)
- return parametersAsJSON.toRequestBody("application/json".toMediaType())
- }
-
-
- private suspend fun send(request: Request): SendResponse {
- val urlBuilder = request.fileServer.url
- .newBuilder()
- .addPathSegments(request.endpoint)
- if (request.verb == HTTP.Verb.GET) {
- for ((key, value) in request.queryParameters) {
- urlBuilder.addQueryParameter(key, value)
- }
- }
- val requestBuilder = okhttp3.Request.Builder()
- .url(urlBuilder.build())
- .headers(request.headers.toHeaders())
- when (request.verb) {
- HTTP.Verb.GET -> requestBuilder.get()
- HTTP.Verb.PUT -> requestBuilder.put(createBody(request.body, request.parameters) ?: RequestBody.EMPTY)
- HTTP.Verb.POST -> requestBuilder.post(createBody(request.body, request.parameters) ?: RequestBody.EMPTY)
- HTTP.Verb.DELETE -> requestBuilder.delete(createBody(request.body, request.parameters))
- }
- return if (request.useOnionRouting) {
- try {
- val response = OnionRequestAPI.sendOnionRequest(
- request = requestBuilder.build(),
- server = request.fileServer.url.host,
- x25519PublicKey =
- Hex.toStringCondensed(
- Curve25519.pubKeyFromED25519(Hex.fromStringCondensed(request.fileServer.ed25519PublicKeyHex))
- )
- ).await()
-
- check(response.code in 200..299) {
- "Error response from file server: ${response.code}"
- }
-
- val body = response.body ?: throw Error.ParsingFailed
-
- SendResponse(
- body = body,
- headers = response.info["headers"] as? Map
- )
- } catch (e: CancellationException) {
- throw e
- } catch (e: Exception) {
- Log.e("Loki", "File server request failed", e)
- throw e
- }
- } else {
- error("It's currently not allowed to send non onion routed requests.")
- }
- }
-
- suspend fun upload(
- file: ByteArray,
- usedDeterministicEncryption: Boolean,
- fileServer: FileServer,
- customExpiresDuration: Duration? = null
- ): UploadResult {
- val request = Request(
- fileServer = fileServer,
- verb = HTTP.Verb.POST,
- endpoint = "file",
- body = file,
- headers = buildMap {
- put("Content-Disposition", "attachment")
- put("Content-Type", "application/octet-stream")
- if (customExpiresDuration != null) {
- put("X-FS-TTL", customExpiresDuration.inWholeSeconds.toString())
- }
- }
- )
- val response = send(request)
- val json = JsonUtil.fromJson(response.body, Map::class.java)
- val id = json["id"]!!.toString()
- val expiresEpochSeconds = (json.getOrDefault("expires", null) as? Number)?.toLong()
-
- return UploadResult(
- fileId = id,
- fileUrl = buildAttachmentUrl(
- fileId = id,
- fileServer = fileServer,
- usesDeterministicEncryption = usedDeterministicEncryption
- ).toString(),
- expires = expiresEpochSeconds?.asEpochSeconds()
- )
- }
-
- suspend fun download(
- fileId: String,
- fileServer: FileServer = DEFAULT_FILE_SERVER
- ): SendResponse {
- val request = Request(
- fileServer = fileServer,
- verb = HTTP.Verb.GET,
- endpoint = "file/$fileId"
- )
- return send(request)
- }
-
- suspend fun renew(fileId: String,
- customTtl: Duration? = null,
- fileServer: FileServer = DEFAULT_FILE_SERVER) {
- val resp = send(Request(
- fileServer = fileServer,
- verb = HTTP.Verb.POST,
- endpoint = "file/$fileId/extend",
- headers = customTtl?.let {
- buildMap {
- "X-FS-TTL" to it.inWholeSeconds.toString()
- }
- } ?: mapOf()
- ))
-
- resp.expires
- }
-
- fun buildAttachmentUrl(
- fileId: String,
- fileServer: FileServer,
- usesDeterministicEncryption: Boolean
- ): HttpUrl {
- val urlFragment = sequenceOf(
- "d".takeIf { usesDeterministicEncryption },
- if (!fileServer.url.isOfficial || fileServer.ed25519PublicKeyHex != DEFAULT_FILE_SERVER.ed25519PublicKeyHex) {
- "p=${fileServer.ed25519PublicKeyHex}"
- } else {
- null
- }
- ).filterNotNull()
- .joinToString(separator = "&")
-
- return fileServer.url
- .newBuilder()
- .addPathSegment("file")
- .addPathSegment(fileId)
- .fragment(urlFragment.takeIf { it.isNotBlank() })
- .build()
- }
-
- data class URLParseResult(
- val fileId: String,
- val fileServer: FileServer,
- val usesDeterministicEncryption: Boolean
- )
-
- fun parseAttachmentUrl(url: HttpUrl): URLParseResult {
- check(url.pathSegments.size == 2) {
- "Invalid URL: requiring exactly 2 path segments"
- }
-
- check(url.pathSegments[0] == "file") {
- "Invalid URL: first path segment must be 'file'"
- }
-
- val id = url.pathSegments[1]
- check(id.isNotBlank()) {
- "Invalid URL: id must not be blank"
- }
-
- var deterministicEncryption = false
- var fileServerPubKeyHex: String? = null
-
- url.fragment
- .orEmpty()
- .splitToSequence('&')
- .forEach { fragment ->
- when {
- fragment == "d" || fragment == "d=" -> deterministicEncryption = true
- fragment.startsWith("p=", ignoreCase = true) -> {
- fileServerPubKeyHex = fragment.substringAfter("p=").takeIf { it.isNotBlank() }
- }
- }
- }
-
- val fileServerUrl = url.newBuilder()
- .removePathSegment(0) // remove "file"
- .removePathSegment(0) // remove id
- .fragment(null) // remove fragment
- .build()
-
- when {
- !fileServerPubKeyHex.isNullOrEmpty() -> {
- // We'll use the public key we get from the URL
- return URLParseResult(
- fileId = id,
- fileServer = FileServer(url = fileServerUrl, ed25519PublicKeyHex = fileServerPubKeyHex),
- usesDeterministicEncryption = deterministicEncryption
- )
- }
-
- fileServerUrl == DEFAULT_FILE_SERVER.url -> {
- // We'll use the default file server
- return URLParseResult(
- fileId = id,
- fileServer = DEFAULT_FILE_SERVER,
- usesDeterministicEncryption = deterministicEncryption
- )
- }
-
- fileServerUrl.isOfficial -> {
- // We don't have a public key, but given it's an official file server,
- // we can use the default public key
- return URLParseResult(
- fileId = id,
- fileServer = FileServer(
- url = fileServerUrl,
- ed25519PublicKeyHex = DEFAULT_FILE_SERVER.ed25519PublicKeyHex
- ),
- usesDeterministicEncryption = deterministicEncryption
- )
- }
-
- else -> {
- // We don't have a public key, and it's not the default file server
- throw Error.InvalidURL
- }
- }
- }
-
- /**
- * Returns the current version of session
- * This is effectively proxying (and caching) the response from the github release
- * page.
- *
- * Note that the value is cached and can be up to 30 minutes out of date normally, and up to 24
- * hours out of date if we cannot reach the Github API for some reason.
- *
- * https://github.com/session-foundation/session-file-server/blob/dev/doc/api.yaml#L119
- */
- suspend fun getClientVersion(fileServer: FileServer = DEFAULT_FILE_SERVER): VersionData {
- // Generate the auth signature
- val secretKey = storage.getUserED25519KeyPair()?.secretKey?.data
- ?: throw (Error.NoEd25519KeyPair)
-
- val blindedKeys = BlindKeyAPI.blindVersionKeyPair(secretKey)
- val timestamp = System.currentTimeMillis().milliseconds.inWholeSeconds // The current timestamp in seconds
- val signature = BlindKeyAPI.blindVersionSign(secretKey, timestamp)
-
- // The hex encoded version-blinded public key with a 07 prefix
- val blindedPkHex = "07" + blindedKeys.pubKey.data.toHexString()
-
- val request = Request(
- fileServer = fileServer,
- verb = HTTP.Verb.GET,
- endpoint = "session_version",
- queryParameters = mapOf("platform" to "android"),
- headers = mapOf(
- "X-FS-Pubkey" to blindedPkHex,
- "X-FS-Timestamp" to timestamp.toString(),
- "X-FS-Signature" to Base64.encodeToString(signature, Base64.NO_WRAP)
- )
- )
-
- // transform the promise into a coroutine
- val result = send(request)
-
- // map out the result
- return JsonUtil.fromJson(result.body, Map::class.java).let {
- VersionData(
- statusCode = it["status_code"] as? Int ?: 0,
- version = it["result"] as? String ?: "",
- updated = it["updated"] as? Double ?: 0.0
- )
- }
- }
-
- data class UploadResult(
- val fileId: String,
- val fileUrl: String,
- val expires: ZonedDateTime?
- )
-
- data class SendResponse(
- val body: ByteArraySlice,
- val headers: Map?
- ) {
- /**
- * The "expires" header's value if any
- */
- val expires: ZonedDateTime? by lazy {
- headers?.get("expires")?.let {
- ZonedDateTime.parse(it, DateTimeFormatter.RFC_1123_DATE_TIME)
- }
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/org/session/libsession/messaging/file_server/FileServerApis.kt b/app/src/main/java/org/session/libsession/messaging/file_server/FileServerApis.kt
new file mode 100644
index 0000000000..8989e70cd2
--- /dev/null
+++ b/app/src/main/java/org/session/libsession/messaging/file_server/FileServerApis.kt
@@ -0,0 +1,132 @@
+package org.session.libsession.messaging.file_server
+
+import okhttp3.HttpUrl
+import java.time.ZonedDateTime
+
+object FileServerApis {
+ const val MAX_FILE_SIZE = 10_000_000 // 10 MB
+
+ val DEFAULT_FILE_SERVER: FileServer = FileServer(
+ url = "http://filev2.getsession.org",
+ ed25519PublicKeyHex = "b8eef9821445ae16e2e97ef8aa6fe782fd11ad5253cd6723b281341dba22e371"
+ )
+
+
+ fun buildAttachmentUrl(
+ fileId: String,
+ fileServer: FileServer,
+ usesDeterministicEncryption: Boolean
+ ): HttpUrl {
+ val urlFragment = sequenceOf(
+ "d".takeIf { usesDeterministicEncryption },
+ if (!fileServer.url.isOfficial || fileServer.ed25519PublicKeyHex != DEFAULT_FILE_SERVER.ed25519PublicKeyHex) {
+ "p=${fileServer.ed25519PublicKeyHex}"
+ } else {
+ null
+ }
+ ).filterNotNull()
+ .joinToString(separator = "&")
+
+ return fileServer.url
+ .newBuilder()
+ .addPathSegment("file")
+ .addPathSegment(fileId)
+ .fragment(urlFragment.takeIf { it.isNotBlank() })
+ .build()
+ }
+
+ fun parseAttachmentUrl(url: HttpUrl): URLParseResult {
+ check(url.pathSegments.size == 2) {
+ "Invalid URL: requiring exactly 2 path segments"
+ }
+
+ check(url.pathSegments[0] == "file") {
+ "Invalid URL: first path segment must be 'file'"
+ }
+
+ val id = url.pathSegments[1]
+ check(id.isNotBlank()) {
+ "Invalid URL: id must not be blank"
+ }
+
+ var deterministicEncryption = false
+ var fileServerPubKeyHex: String? = null
+
+ url.fragment
+ .orEmpty()
+ .splitToSequence('&')
+ .forEach { fragment ->
+ when {
+ fragment == "d" || fragment == "d=" -> deterministicEncryption = true
+ fragment.startsWith("p=", ignoreCase = true) -> {
+ fileServerPubKeyHex = fragment.substringAfter("p=").takeIf { it.isNotBlank() }
+ }
+ }
+ }
+
+ val fileServerUrl = url.newBuilder()
+ .removePathSegment(0) // remove "file"
+ .removePathSegment(0) // remove id
+ .fragment(null) // remove fragment
+ .build()
+
+ when {
+ !fileServerPubKeyHex.isNullOrEmpty() -> {
+ // We'll use the public key we get from the URL
+ return URLParseResult(
+ fileId = id,
+ fileServer = FileServer(url = fileServerUrl, ed25519PublicKeyHex = fileServerPubKeyHex),
+ usesDeterministicEncryption = deterministicEncryption
+ )
+ }
+
+ fileServerUrl == DEFAULT_FILE_SERVER.url -> {
+ // We'll use the default file server
+ return URLParseResult(
+ fileId = id,
+ fileServer = DEFAULT_FILE_SERVER,
+ usesDeterministicEncryption = deterministicEncryption
+ )
+ }
+
+ fileServerUrl.isOfficial -> {
+ // We don't have a public key, but given it's an official file server,
+ // we can use the default public key
+ return URLParseResult(
+ fileId = id,
+ fileServer = FileServer(
+ url = fileServerUrl,
+ ed25519PublicKeyHex = DEFAULT_FILE_SERVER.ed25519PublicKeyHex
+ ),
+ usesDeterministicEncryption = deterministicEncryption
+ )
+ }
+
+ else -> {
+ // We don't have a public key, and it's not the default file server
+ throw Error.InvalidURL
+ }
+ }
+ }
+
+ sealed class Error(message: String) : Exception(message) {
+ object ParsingFailed : Error("Invalid response.")
+ object InvalidURL : Error("Invalid URL.")
+ object NoEd25519KeyPair : Error("Couldn't find ed25519 key pair.")
+ }
+
+ data class URLParseResult(
+ val fileId: String,
+ val fileServer: FileServer,
+ val usesDeterministicEncryption: Boolean
+ )
+
+
+
+ data class UploadResult(
+ val fileId: String,
+ val fileUrl: String,
+ val expires: ZonedDateTime?
+ )
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/session/libsession/messaging/file_server/FileUploadApi.kt b/app/src/main/java/org/session/libsession/messaging/file_server/FileUploadApi.kt
new file mode 100644
index 0000000000..9a96245641
--- /dev/null
+++ b/app/src/main/java/org/session/libsession/messaging/file_server/FileUploadApi.kt
@@ -0,0 +1,86 @@
+package org.session.libsession.messaging.file_server
+
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.json.Json
+import kotlinx.serialization.json.decodeFromStream
+import okhttp3.HttpUrl.Companion.toHttpUrl
+import org.thoughtcrime.securesms.api.server.ServerApiErrorManager
+import org.thoughtcrime.securesms.api.ApiExecutorContext
+import org.thoughtcrime.securesms.api.http.HttpBody
+import org.thoughtcrime.securesms.api.http.HttpRequest
+import org.thoughtcrime.securesms.api.http.HttpResponse
+import org.thoughtcrime.securesms.api.server.ServerApi
+import org.thoughtcrime.securesms.util.DateUtils.Companion.asEpochSeconds
+
+class FileUploadApi @AssistedInject constructor(
+ @Assisted private val fileServer: FileServer,
+ @Assisted private val data: ByteArray,
+ @Assisted private val usedDeterministicEncryption: Boolean,
+ @Assisted private val customExpiresSeconds: Long?,
+ errorManager: ServerApiErrorManager,
+ private val json: Json,
+) : ServerApi(
+ errorManager
+) {
+ override fun buildRequest(
+ baseUrl: String,
+ x25519PubKeyHex: String
+ ): HttpRequest {
+ check(fileServer.url.toString().startsWith(baseUrl)) {
+ "FileServer URL ${fileServer.url} does not match base URL $baseUrl"
+ }
+
+ return HttpRequest(
+ url = "$baseUrl/file".toHttpUrl(),
+ method = "POST",
+ headers = buildMap {
+ put("Content-Disposition", "attachment")
+ put("Content-Type", "application/octet-stream")
+ if (customExpiresSeconds != null) {
+ put("X-FS-TTL", customExpiresSeconds.toString())
+ }
+ },
+ body = HttpBody.Bytes(data),
+ )
+ }
+
+ override suspend fun handleSuccessResponse(
+ executorContext: ApiExecutorContext,
+ baseUrl: String,
+ response: HttpResponse
+ ): FileServerApis.UploadResult {
+ @Suppress("OPT_IN_USAGE")
+ val response = json.decodeFromStream(response.body.asInputStream())
+ return FileServerApis.UploadResult(
+ fileId = response.id,
+ fileUrl = FileServerApis.buildAttachmentUrl(
+ fileId = response.id,
+ fileServer = fileServer,
+ usesDeterministicEncryption = usedDeterministicEncryption
+ ).toString(),
+ expires = response.expiresEpochSeconds?.toLong()?.asEpochSeconds()
+ )
+ }
+
+ @Serializable
+ private class Response(
+ val id: String,
+
+ @SerialName("expires")
+ val expiresEpochSeconds: Double?,
+ )
+
+ @AssistedFactory
+ interface Factory {
+ fun create(
+ fileServer: FileServer,
+ data: ByteArray,
+ usedDeterministicEncryption: Boolean,
+ customExpiresSeconds: Long? = null,
+ ): FileUploadApi
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/session/libsession/messaging/file_server/GetClientVersionApi.kt b/app/src/main/java/org/session/libsession/messaging/file_server/GetClientVersionApi.kt
new file mode 100644
index 0000000000..d1e34cae05
--- /dev/null
+++ b/app/src/main/java/org/session/libsession/messaging/file_server/GetClientVersionApi.kt
@@ -0,0 +1,74 @@
+package org.session.libsession.messaging.file_server
+
+import android.util.Base64
+import kotlinx.serialization.ExperimentalSerializationApi
+import kotlinx.serialization.json.Json
+import kotlinx.serialization.json.decodeFromStream
+import network.loki.messenger.libsession_util.util.BlindKeyAPI
+import okhttp3.HttpUrl.Companion.toHttpUrl
+import org.thoughtcrime.securesms.api.server.ServerApiErrorManager
+import org.session.libsession.network.SnodeClock
+import org.session.libsignal.utilities.toHexString
+import org.thoughtcrime.securesms.api.ApiExecutorContext
+import org.thoughtcrime.securesms.api.http.HttpRequest
+import org.thoughtcrime.securesms.api.http.HttpResponse
+import org.thoughtcrime.securesms.api.server.ServerApi
+import org.thoughtcrime.securesms.auth.LoginStateRepository
+import javax.inject.Inject
+
+/**
+ * Returns the current version of session
+ * This is effectively proxying (and caching) the response from the github release
+ * page.
+ *
+ * Note that the value is cached and can be up to 30 minutes out of date normally, and up to 24
+ * hours out of date if we cannot reach the Github API for some reason.
+ *
+ * https://github.com/session-foundation/session-file-server/blob/dev/doc/api.yaml#L119
+ */
+class GetClientVersionApi @Inject constructor(
+ errorManager: ServerApiErrorManager,
+ private val loginStateRepository: LoginStateRepository,
+ private val snodeClock: SnodeClock,
+ private val json: Json,
+) : ServerApi(errorManager) {
+ override fun buildRequest(
+ baseUrl: String,
+ x25519PubKeyHex: String
+ ): HttpRequest {
+ val secretKey = loginStateRepository
+ .requireLoggedInState()
+ .accountEd25519KeyPair
+ .secretKey
+ .data
+
+ val blindedKeys = BlindKeyAPI.blindVersionKeyPair(secretKey)
+
+ // The hex encoded version-blinded public key with a 07 prefix
+ val blindedPkHex = "07" + blindedKeys.pubKey.data.toHexString()
+
+ val timestamp = snodeClock.currentTimeSeconds()
+ val signature = BlindKeyAPI.blindVersionSign(secretKey, timestamp)
+
+ return HttpRequest(
+ url = "$baseUrl/session_version?platform=android".toHttpUrl(),
+ method = "GET",
+ headers = mapOf(
+ "X-FS-Pubkey" to blindedPkHex,
+ "X-FS-Timestamp" to timestamp.toString(),
+ "X-FS-Signature" to Base64.encodeToString(signature, Base64.NO_WRAP),
+ ),
+ body = null
+ )
+ }
+
+ @OptIn(ExperimentalSerializationApi::class)
+ override suspend fun handleSuccessResponse(
+ executorContext: ApiExecutorContext,
+ baseUrl: String,
+ response: HttpResponse
+ ): VersionData {
+ return response.body.asInputStream()
+ .use(json::decodeFromStream)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/session/libsession/messaging/file_server/VersionData.kt b/app/src/main/java/org/session/libsession/messaging/file_server/VersionData.kt
index b7c28020e7..e79e5b2f52 100644
--- a/app/src/main/java/org/session/libsession/messaging/file_server/VersionData.kt
+++ b/app/src/main/java/org/session/libsession/messaging/file_server/VersionData.kt
@@ -1,7 +1,16 @@
package org.session.libsession.messaging.file_server
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+@Serializable
data class VersionData(
+ @SerialName("status_code")
val statusCode: Int, // The value 200. Included for backwards compatibility, and may be removed someday.
+
+ @SerialName("result")
val version: String, // The Session version.
+
+ @SerialName("updated")
val updated: Double // The unix timestamp when this version was retrieved from Github; this can be up to 24 hours ago in case of consistent fetch errors, though normally will be within the last 30 minutes.
)
\ No newline at end of file
diff --git a/app/src/main/java/org/session/libsession/messaging/groups/GroupInviteException.kt b/app/src/main/java/org/session/libsession/messaging/groups/GroupInviteException.kt
index a4c2bc8f49..7e5cd7a7d1 100644
--- a/app/src/main/java/org/session/libsession/messaging/groups/GroupInviteException.kt
+++ b/app/src/main/java/org/session/libsession/messaging/groups/GroupInviteException.kt
@@ -23,6 +23,7 @@ class GroupInviteException(
val isPromotion: Boolean,
val inviteeAccountIds: List,
val groupName: String,
+ val isReinvite: Boolean,
underlying: Throwable
) : RuntimeException(underlying) {
init {
@@ -41,19 +42,26 @@ class GroupInviteException(
val third = inviteeAccountIds.getOrNull(2)?.let(getInviteeName)
if (second != null && third != null) {
- return Phrase.from(context, if (isPromotion) R.string.adminPromotionFailedDescriptionMultiple else R.string.groupInviteFailedMultiple)
+ val errorString =
+ if (isPromotion) if (isReinvite) R.string.failedResendPromotionMultiple else R.string.adminPromotionFailedDescriptionMultiple else
+ if (isReinvite) R.string.failedResendInviteMultiple else R.string.groupInviteFailedMultiple
+ return Phrase.from(context, errorString)
.put(NAME_KEY, first)
.put(COUNT_KEY, inviteeAccountIds.size - 1)
.put(GROUP_NAME_KEY, groupName)
.format()
} else if (second != null) {
- return Phrase.from(context, if (isPromotion) R.string.adminPromotionFailedDescriptionTwo else R.string.groupInviteFailedTwo)
+ val errorString = if (isPromotion) if (isReinvite) R.string.failedResendPromotionTwo else R.string.adminPromotionFailedDescriptionTwo else
+ if (isReinvite) R.string.failedResendInviteTwo else R.string.groupInviteFailedTwo
+ return Phrase.from(context, errorString)
.put(NAME_KEY, first)
.put(OTHER_NAME_KEY, second)
.put(GROUP_NAME_KEY, groupName)
.format()
} else {
- return Phrase.from(context, if (isPromotion) R.string.adminPromotionFailedDescription else R.string.groupInviteFailedUser)
+ val errorString = if (isPromotion) if (isReinvite) R.string.failedResendPromotion else R.string.adminPromotionFailedDescription else
+ if (isReinvite) R.string.failedResendInvite else R.string.groupInviteFailedUser
+ return Phrase.from(context, errorString)
.put(NAME_KEY, first)
.put(GROUP_NAME_KEY, groupName)
.format()
diff --git a/app/src/main/java/org/session/libsession/messaging/groups/GroupManagerV2.kt b/app/src/main/java/org/session/libsession/messaging/groups/GroupManagerV2.kt
index b8ea9990ec..c9abda96d0 100644
--- a/app/src/main/java/org/session/libsession/messaging/groups/GroupManagerV2.kt
+++ b/app/src/main/java/org/session/libsession/messaging/groups/GroupManagerV2.kt
@@ -4,8 +4,9 @@ import androidx.annotation.StringRes
import network.loki.messenger.libsession_util.util.ExpiryMode
import org.session.libsession.messaging.messages.control.GroupUpdated
import org.session.libsession.utilities.recipients.Recipient
-import org.session.libsignal.protos.SignalServiceProtos.DataMessage.GroupUpdateDeleteMemberContentMessage
+import org.session.protos.SessionProtos.GroupUpdateDeleteMemberContentMessage
import org.session.libsignal.utilities.AccountId
+import org.thoughtcrime.securesms.groups.MemberInvite
/**
* Business logic handling group v2 operations like inviting members,
@@ -25,6 +26,11 @@ interface GroupManagerV2 {
isReinvite: Boolean, // Whether this comes from a re-invite
)
+ suspend fun reinviteMembers(
+ group: AccountId,
+ invites: List
+ )
+
suspend fun removeMembers(
groupAccountId: AccountId,
removedMembers: List,
@@ -52,7 +58,7 @@ interface GroupManagerV2 {
)
suspend fun handleMemberLeftMessage(memberId: AccountId, group: AccountId)
- suspend fun leaveGroup(groupId: AccountId)
+ suspend fun leaveGroup(groupId: AccountId, deleteGroup : Boolean = false)
suspend fun promoteMember(group: AccountId, members: List, isRepromote: Boolean)
suspend fun handleInvitation(
@@ -119,6 +125,10 @@ interface GroupManagerV2 {
fun getLeaveGroupConfirmationDialogData(groupId: AccountId, name: String): ConfirmDialogData?
+ fun getDeleteGroupConfirmationDialogData(groupId : AccountId, name : String) : ConfirmDialogData?
+
+ fun isCurrentUserLastAdmin(groupId : AccountId) : Boolean
+
data class ConfirmDialogData(
val title: String,
val message: CharSequence,
@@ -126,5 +136,6 @@ interface GroupManagerV2 {
@StringRes val negativeText: Int,
@StringRes val positiveQaTag: Int?,
@StringRes val negativeQaTag: Int?,
+ val showCloseButton: Boolean = false
)
}
\ No newline at end of file
diff --git a/app/src/main/java/org/session/libsession/messaging/jobs/AttachmentDownloadJob.kt b/app/src/main/java/org/session/libsession/messaging/jobs/AttachmentDownloadJob.kt
index 46f473e210..7eb6300236 100644
--- a/app/src/main/java/org/session/libsession/messaging/jobs/AttachmentDownloadJob.kt
+++ b/app/src/main/java/org/session/libsession/messaging/jobs/AttachmentDownloadJob.kt
@@ -7,22 +7,29 @@ import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import org.session.libsession.database.MessageDataProvider
import org.session.libsession.database.StorageProtocol
-import org.session.libsession.messaging.file_server.FileServerApi
-import org.session.libsession.messaging.open_groups.OpenGroupApi
+import org.session.libsession.messaging.file_server.FileDownloadApi
+import org.session.libsession.messaging.file_server.FileServerApis
+import org.session.libsession.messaging.open_groups.api.CommunityApiExecutor
+import org.session.libsession.messaging.open_groups.api.CommunityApiRequest
+import org.session.libsession.messaging.open_groups.api.CommunityFileDownloadApi
+import org.session.libsession.messaging.open_groups.api.execute
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.DatabaseAttachment
import org.session.libsession.messaging.utilities.Data
-import org.session.libsession.snode.OnionRequestAPI
import org.session.libsession.utilities.Address
import org.session.libsession.utilities.DecodedAudio
import org.session.libsession.utilities.InputStreamMediaDataSource
import org.session.libsignal.exceptions.NonRetryableException
import org.session.libsignal.utilities.Base64
-import org.session.libsignal.utilities.HTTP
import org.session.libsignal.utilities.Log
+import org.thoughtcrime.securesms.api.error.UnhandledStatusCodeException
+import org.thoughtcrime.securesms.api.server.ServerApiExecutor
+import org.thoughtcrime.securesms.api.server.ServerApiRequest
+import org.thoughtcrime.securesms.api.server.execute
import org.thoughtcrime.securesms.attachments.AttachmentProcessor
import org.thoughtcrime.securesms.database.model.MessageId
+import org.thoughtcrime.securesms.util.findCause
class AttachmentDownloadJob @AssistedInject constructor(
@Assisted("attachmentID") val attachmentID: Long,
@@ -30,7 +37,10 @@ class AttachmentDownloadJob @AssistedInject constructor(
private val storage: StorageProtocol,
private val messageDataProvider: MessageDataProvider,
private val attachmentProcessor: AttachmentProcessor,
- private val fileServerApi: FileServerApi,
+ private val serverApiExecutor: ServerApiExecutor,
+ private val fileDownloadApiFactory: FileDownloadApi.Factory,
+ private val communityApiExecutor: CommunityApiExecutor,
+ private val communityFileDownloadApiFactory: CommunityFileDownloadApi.Factory,
) : Job {
override var delegate: JobDelegate? = null
override var id: String? = null
@@ -83,7 +93,7 @@ class AttachmentDownloadJob @AssistedInject constructor(
val threadID = storage.getThreadIdForMms(mmsMessageId)
val handleFailure: (java.lang.Exception, attachmentId: AttachmentId?) -> Unit = { exception, attachment ->
- if(exception is HTTP.HTTPRequestFailedException && exception.statusCode == 404){
+ if (exception.findCause()?.code == 404){
attachment?.let { id ->
Log.d("AttachmentDownloadJob", "Setting attachment state = failed, have attachment")
messageDataProvider.setAttachmentState(AttachmentState.EXPIRED, id, mmsMessageId)
@@ -94,7 +104,7 @@ class AttachmentDownloadJob @AssistedInject constructor(
} else if (exception == Error.NoAttachment
|| exception == Error.NoThread
|| exception == Error.NoSender
- || (exception is OnionRequestAPI.HTTPRequestFailedAtDestinationException && exception.statusCode == 400)
+ || (exception.findCause()?.code == 400)
|| exception is NonRetryableException) {
attachment?.let { id ->
Log.d("AttachmentDownloadJob", "Setting attachment state = failed, have attachment")
@@ -157,7 +167,7 @@ class AttachmentDownloadJob @AssistedInject constructor(
val decrypted = if (threadRecipient?.address !is Address.Community) {
Log.d("AttachmentDownloadJob", "downloading normal attachment")
- val r = runCatching { fileServerApi.parseAttachmentUrl(attachment.url.toHttpUrl()) }
+ val r = runCatching { FileServerApis.parseAttachmentUrl(attachment.url.toHttpUrl()) }
.recover { throw NonRetryableException("Invalid file server URL", it) }
.getOrThrow()
@@ -165,10 +175,12 @@ class AttachmentDownloadJob @AssistedInject constructor(
throw NonRetryableException("Missing attachment key")
}.let(Base64::decode)
- val cipherText = fileServerApi.download(
- fileId = r.fileId,
- fileServer = r.fileServer
- ).body
+ val cipherText = serverApiExecutor.execute(
+ ServerApiRequest(
+ fileServer = r.fileServer,
+ api = fileDownloadApiFactory.create(r.fileId)
+ )
+ ).data.toByteArraySlice()
runCatching {
if (r.usesDeterministicEncryption) {
@@ -189,7 +201,17 @@ class AttachmentDownloadJob @AssistedInject constructor(
Log.d("AttachmentDownloadJob", "downloading open group attachment")
val url = attachment.url.toHttpUrlOrNull()!!
val fileID = url.pathSegments.last()
- OpenGroupApi.download(fileID, room = threadRecipient.address.room, server = threadRecipient.address.serverUrl)
+
+ communityApiExecutor.execute(
+ CommunityApiRequest(
+ serverBaseUrl = threadRecipient.address.serverUrl,
+ api = communityFileDownloadApiFactory.create(
+ room = threadRecipient.address.room,
+ fileId = fileID,
+ requiresSigning = true,
+ )
+ )
+ ).toByteArraySlice()
}
Log.d("AttachmentDownloadJob", "getting input stream")
diff --git a/app/src/main/java/org/session/libsession/messaging/jobs/AttachmentUploadJob.kt b/app/src/main/java/org/session/libsession/messaging/jobs/AttachmentUploadJob.kt
index 86ce01eec7..8c8413eb40 100644
--- a/app/src/main/java/org/session/libsession/messaging/jobs/AttachmentUploadJob.kt
+++ b/app/src/main/java/org/session/libsession/messaging/jobs/AttachmentUploadJob.kt
@@ -9,10 +9,14 @@ import dagger.assisted.AssistedInject
import network.loki.messenger.libsession_util.encrypt.Attachments
import org.session.libsession.database.MessageDataProvider
import org.session.libsession.database.StorageProtocol
-import org.session.libsession.messaging.file_server.FileServerApi
+import org.session.libsession.messaging.file_server.FileServerApis
+import org.session.libsession.messaging.file_server.FileUploadApi
import org.session.libsession.messaging.messages.Destination
import org.session.libsession.messaging.messages.Message
-import org.session.libsession.messaging.open_groups.OpenGroupApi
+import org.session.libsession.messaging.open_groups.api.CommunityApiExecutor
+import org.session.libsession.messaging.open_groups.api.CommunityApiRequest
+import org.session.libsession.messaging.open_groups.api.CommunityFileUploadApi
+import org.session.libsession.messaging.open_groups.api.execute
import org.session.libsession.messaging.sending_receiving.MessageSender
import org.session.libsession.messaging.utilities.Data
import org.session.libsession.utilities.Address
@@ -22,6 +26,10 @@ import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.UploadResult
import org.session.libsignal.messages.SignalServiceAttachmentStream
import org.session.libsignal.utilities.Log
+import org.thoughtcrime.securesms.api.http.HttpBody
+import org.thoughtcrime.securesms.api.server.ServerApiExecutor
+import org.thoughtcrime.securesms.api.server.ServerApiRequest
+import org.thoughtcrime.securesms.api.server.execute
import org.thoughtcrime.securesms.attachments.AttachmentProcessor
import org.thoughtcrime.securesms.database.ThreadDatabase
@@ -36,8 +44,11 @@ class AttachmentUploadJob @AssistedInject constructor(
private val threadDatabase: ThreadDatabase,
private val attachmentProcessor: AttachmentProcessor,
private val preferences: TextSecurePreferences,
- private val fileServerApi: FileServerApi,
private val messageSender: MessageSender,
+ private val serverApiExecutor: ServerApiExecutor,
+ private val fileUploadApiFactory: FileUploadApi.Factory,
+ private val communityApiExecutor: CommunityApiExecutor,
+ private val communityFileUploadApiFactory: CommunityFileUploadApi.Factory,
) : Job {
override var delegate: JobDelegate? = null
override var id: String? = null
@@ -75,20 +86,33 @@ class AttachmentUploadJob @AssistedInject constructor(
attachment = attachment,
encrypt = false
) { data, _ ->
- val id = OpenGroupApi.upload(data, threadAddress.room, threadAddress.serverUrl)
+ val id = communityApiExecutor.execute(
+ CommunityApiRequest(
+ serverBaseUrl = threadAddress.serverUrl,
+ api = communityFileUploadApiFactory.create(
+ file = HttpBody.Bytes(data),
+ room = threadAddress.room,
+ )
+ )
+ )
id to "${threadAddress.serverUrl}/file/$id"
}
handleSuccess(dispatcherName, attachment, keyAndResult.first, keyAndResult.second)
} else {
- val fileServer = preferences.alternativeFileServer ?: FileServerApi.DEFAULT_FILE_SERVER
+ val fileServer = preferences.alternativeFileServer ?: FileServerApis.DEFAULT_FILE_SERVER
val keyAndResult = upload(
attachment = attachment,
encrypt = true
) { data, isDeterministicallyEncrypted ->
- val result = fileServerApi.upload(
- file = data,
- usedDeterministicEncryption = isDeterministicallyEncrypted,
- fileServer = fileServer
+ val result = serverApiExecutor.execute(
+ ServerApiRequest(
+ fileServer = fileServer,
+ api = fileUploadApiFactory.create(
+ data = data,
+ usedDeterministicEncryption = isDeterministicallyEncrypted,
+ fileServer = fileServer,
+ )
+ )
)
result.fileId to result.fileUrl
diff --git a/app/src/main/java/org/session/libsession/messaging/jobs/BatchMessageReceiveJob.kt b/app/src/main/java/org/session/libsession/messaging/jobs/BatchMessageReceiveJob.kt
deleted file mode 100644
index 410acddafc..0000000000
--- a/app/src/main/java/org/session/libsession/messaging/jobs/BatchMessageReceiveJob.kt
+++ /dev/null
@@ -1,415 +0,0 @@
-package org.session.libsession.messaging.jobs
-
-import android.content.Context
-import com.google.protobuf.ByteString
-import dagger.assisted.Assisted
-import dagger.assisted.AssistedFactory
-import dagger.assisted.AssistedInject
-import dagger.hilt.android.qualifiers.ApplicationContext
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.async
-import kotlinx.coroutines.awaitAll
-import kotlinx.coroutines.coroutineScope
-import network.loki.messenger.libsession_util.PRIORITY_HIDDEN
-import org.session.libsession.database.StorageProtocol
-import org.session.libsession.messaging.messages.Destination
-import org.session.libsession.messaging.messages.Message
-import org.session.libsession.messaging.messages.Message.Companion.senderOrSync
-import org.session.libsession.messaging.messages.control.CallMessage
-import org.session.libsession.messaging.messages.control.DataExtractionNotification
-import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate
-import org.session.libsession.messaging.messages.control.MessageRequestResponse
-import org.session.libsession.messaging.messages.control.ReadReceipt
-import org.session.libsession.messaging.messages.control.TypingIndicator
-import org.session.libsession.messaging.messages.control.UnsendRequest
-import org.session.libsession.messaging.messages.visible.ParsedMessage
-import org.session.libsession.messaging.messages.visible.VisibleMessage
-import org.session.libsession.messaging.open_groups.OpenGroupApi
-import org.session.libsession.messaging.sending_receiving.MessageReceiver
-import org.session.libsession.messaging.sending_receiving.ReceivedMessageHandler
-import org.session.libsession.messaging.sending_receiving.VisibleMessageHandlerContext
-import org.session.libsession.messaging.sending_receiving.constructReactionRecords
-import org.session.libsession.messaging.sending_receiving.notifications.MessageNotifier
-import org.session.libsession.messaging.utilities.Data
-import org.session.libsession.utilities.Address
-import org.session.libsession.utilities.Address.Companion.toAddress
-import org.session.libsession.utilities.ConfigFactoryProtocol
-import org.session.libsession.utilities.UserConfigType
-import org.session.libsignal.protos.UtilProtos
-import org.session.libsignal.utilities.Log
-import org.thoughtcrime.securesms.database.RecipientRepository
-import org.thoughtcrime.securesms.database.ThreadDatabase
-import org.thoughtcrime.securesms.database.model.MessageId
-import org.thoughtcrime.securesms.database.model.ReactionRecord
-import kotlin.math.max
-
-data class MessageReceiveParameters(
- val data: ByteArray,
- val serverHash: String? = null,
- val openGroupMessageServerID: Long? = null,
- val reactions: Map? = null,
- val closedGroup: Destination.ClosedGroup? = null
-)
-
-@Deprecated("BatchMessageReceiveJob is now only here so that existing persisted jobs can be processed.")
-class BatchMessageReceiveJob @AssistedInject constructor(
- @Assisted private val messages: List,
- @Assisted val fromCommunity: Address.Community?, // The community the messages are received in, if any
- private val configFactory: ConfigFactoryProtocol,
- private val storage: StorageProtocol,
- @param:ApplicationContext private val context: Context,
- private val receivedMessageHandler: ReceivedMessageHandler,
- private val visibleMessageHandlerContextFactory: VisibleMessageHandlerContext.Factory,
- private val messageNotifier: MessageNotifier,
- private val threadDatabase: ThreadDatabase,
- private val recipientRepository: RecipientRepository,
- private val messageReceiver: MessageReceiver,
-) : Job {
-
- override var delegate: JobDelegate? = null
- override var id: String? = null
- override var failureCount: Int = 0
- override val maxFailureCount: Int = 1 // handled in JobQueue onJobFailed
- // Failure Exceptions must be retryable if they're a MessageReceiver.Error
- val failures = mutableListOf()
-
- companion object {
- const val TAG = "BatchMessageReceiveJob"
- const val KEY = "BatchMessageReceiveJob"
-
- const val BATCH_DEFAULT_NUMBER = 512
-
- // used for processing messages that don't have a thread and shouldn't create one
- const val NO_THREAD_MAPPING = -1L
-
- // Keys used for database storage
- private val NUM_MESSAGES_KEY = "numMessages"
- private val DATA_KEY = "data"
- private val SERVER_HASH_KEY = "serverHash"
- private val OPEN_GROUP_MESSAGE_SERVER_ID_KEY = "openGroupMessageServerID"
- @Deprecated("No longer used, keep for backwards compatibility")
- private val OPEN_GROUP_ID_KEY = "open_group_id"
- private val CLOSED_GROUP_DESTINATION_KEY = "closed_group_destination"
- private val FROM_COMMUNITY_KEY = "from_community"
- }
-
- fun recreateWithNewMessages(
- newMessages: List,
- ): BatchMessageReceiveJob {
- return BatchMessageReceiveJob(
- messages = newMessages,
- configFactory = configFactory,
- storage = storage,
- context = context,
- receivedMessageHandler = receivedMessageHandler,
- visibleMessageHandlerContextFactory = visibleMessageHandlerContextFactory,
- messageNotifier = messageNotifier,
- fromCommunity = fromCommunity,
- threadDatabase = threadDatabase,
- recipientRepository = recipientRepository,
- messageReceiver = messageReceiver,
- )
- }
-
- private fun shouldCreateThread(parsedMessage: ParsedMessage): Boolean {
- val message = parsedMessage.message
- if (message is VisibleMessage) return true
- else { // message is control message otherwise
- return when(message) {
- is DataExtractionNotification -> false
- is MessageRequestResponse -> false
- is ExpirationTimerUpdate -> false
- is TypingIndicator -> false
- is UnsendRequest -> false
- is ReadReceipt -> false
- is CallMessage -> false // TODO: maybe
- else -> false // shouldn't happen, or I guess would be Visible
- }
- }
- }
-
- override suspend fun execute(dispatcherName: String) {
- executeAsync(dispatcherName)
- }
-
- private fun isHidden(message: Message): Boolean {
- // if the contact is marked as hidden for 1on1 messages
- // and the message's sentTimestamp is earlier than the sentTimestamp of the last config
- val publicKey = storage.getUserPublicKey()
- if (message.sentTimestamp == null || publicKey == null) return false
-
- val contactConfigTimestamp = configFactory.getConfigTimestamp(UserConfigType.CONTACTS, publicKey)
-
- return configFactory.withUserConfigs { configs ->
- message.groupPublicKey == null && // not a group
- message.openGroupServerMessageID == null && // not a community
- // not marked as hidden
- configs.contacts.get(message.senderOrSync)?.priority == PRIORITY_HIDDEN &&
- // the message's sentTimestamp is earlier than the sentTimestamp of the last config
- message.sentTimestamp!! < contactConfigTimestamp
- }
- }
-
- suspend fun executeAsync(dispatcherName: String) {
- val threadMap = mutableMapOf>>()
- val localUserPublicKey = storage.getUserPublicKey()
- val serverPublicKey = fromCommunity?.let { storage.getOpenGroupPublicKey(it.serverUrl) }
- val currentClosedGroups = storage.getAllActiveClosedGroupPublicKeys()
-
- // parse and collect IDs
- messages.forEach { messageParameters ->
- val (data, serverHash, openGroupMessageServerID) = messageParameters
- try {
- val (message, proto) = messageReceiver.parse(
- data,
- openGroupMessageServerID,
- openGroupPublicKey = serverPublicKey,
- currentClosedGroups = currentClosedGroups,
- closedGroupSessionId = messageParameters.closedGroup?.publicKey
- )
- message.serverHash = serverHash
- val parsedParams = ParsedMessage(messageParameters, message, proto)
-
- if(isHidden(message)) return@forEach
-
- val threadAddress = when {
- fromCommunity != null -> fromCommunity
- message.groupPublicKey != null -> message.groupPublicKey!!.toAddress()
- else -> message.senderOrSync.toAddress()
- } as Address.Conversable
-
- val threadID = if (shouldCreateThread(parsedParams)) {
- threadDatabase.getOrCreateThreadIdFor(threadAddress)
- } else {
- threadDatabase.getThreadIdIfExistsFor(threadAddress)
- }
- threadMap.getOrPut(threadID) { threadAddress to mutableListOf() }.second += parsedParams
- } catch (e: Exception) {
- when (e) {
- is MessageReceiver.Error.DuplicateMessage, MessageReceiver.Error.SelfSend -> {
- Log.i(TAG, "Couldn't receive message, failed with error: ${e.message} (id: $id)")
- }
- is MessageReceiver.Error -> {
- if (!e.isRetryable) {
- Log.e(TAG, "Couldn't receive message, failed permanently (id: $id)", e)
- }
- else {
- Log.e(TAG, "Couldn't receive message, failed (id: $id)", e)
- failures += messageParameters
- }
- }
- else -> {
- Log.e(TAG, "Couldn't receive message, failed (id: $id)", e)
- failures += messageParameters
- }
- }
- }
- }
-
- // iterate over threads and persist them (persistence is the longest constant in the batch process operation)
- suspend fun processMessages(threadId: Long, threadAddress: Address.Conversable, messages: List) {
- // The LinkedHashMap should preserve insertion order
- val messageIds = linkedMapOf>()
- val myLastSeen = storage.getLastSeen(threadId)
- var updatedLastSeen = myLastSeen.takeUnless { it == -1L } ?: 0
- val handlerContext = visibleMessageHandlerContextFactory.create(
- threadId = threadId,
- threadAddress,
- )
-
- val communityReactions = mutableMapOf>()
-
- messages.forEach { (parameters, message, proto) ->
- try {
- when (message) {
- is VisibleMessage -> {
- val isUserBlindedSender =
- message.sender == handlerContext.userBlindedKey
-
- if (message.sender == localUserPublicKey || isUserBlindedSender) {
- // use sent timestamp here since that is technically the last one we have
- updatedLastSeen = max(updatedLastSeen, message.sentTimestamp!!)
- }
- val messageId = receivedMessageHandler.handleVisibleMessage(
- message = message,
- proto = proto,
- context = handlerContext,
- runThreadUpdate = false,
- runProfileUpdate = true
- )
-
- if (messageId != null && message.reaction == null) {
- messageIds[messageId] = Pair(
- (message.sender == localUserPublicKey || isUserBlindedSender),
- message.hasMention
- )
- }
-
- parameters.openGroupMessageServerID?.let {
- constructReactionRecords(
- openGroupMessageServerID = it,
- context = handlerContext,
- reactions = parameters.reactions,
- out = communityReactions
- )
- }
- }
-
- is UnsendRequest -> {
- val deletedMessage = receivedMessageHandler.handleUnsendRequest(message)
-
- // If we removed a message then ensure it isn't in the 'messageIds'
- if (deletedMessage != null) {
- messageIds.remove(deletedMessage)
- }
- }
-
- else -> receivedMessageHandler.handle(
- message = message,
- proto = proto,
- threadId = threadId,
- threadAddress = threadAddress
- )
- }
- } catch (e: Exception) {
- Log.e(TAG, "Couldn't process message (id: $id)", e)
- if (e is MessageReceiver.Error && !e.isRetryable) {
- Log.e(TAG, "Message failed permanently (id: $id)", e)
- } else {
- Log.e(TAG, "Message failed (id: $id)", e)
- failures += parameters
- }
- }
- }
- // increment unreads, notify, and update thread
- // last seen will be the current last seen if not changed (re-computes the read counts for thread record)
- // might have been updated from a different thread at this point
- val storedLastSeen = storage.getLastSeen(threadId).let { if (it == -1L) 0 else it }
- updatedLastSeen = max(updatedLastSeen, storedLastSeen)
- // Only call markConversationAsRead() when lastSeen actually advanced (we sent a message).
- // For incoming-only batches (like reactions), skip this to preserve REACTIONS_UNREAD flags
- // so the notification system can detect them. Thread updates happen separately below.
- if (updatedLastSeen > 0 || storedLastSeen == 0L) {
- storage.markConversationAsRead(threadId, updatedLastSeen, force = true)
- }
- storage.updateThread(threadId, true)
- messageNotifier.updateNotification(context, threadId)
-
- if (communityReactions.isNotEmpty()) {
- storage.addReactions(communityReactions, replaceAll = true, notifyUnread = false)
- }
- }
-
- coroutineScope {
- val withoutDefault = threadMap.entries.filter { it.key != NO_THREAD_MAPPING }
- val deferredThreadMap = withoutDefault.map { (threadId, data) ->
- val (threadAddress, messages) = data
- async(Dispatchers.Default) {
- processMessages(
- threadId = threadId,
- threadAddress = threadAddress,
- messages = messages
- )
- }
- }
- // await all thread processing
- deferredThreadMap.awaitAll()
- }
-
- val noThreadMessages = threadMap[NO_THREAD_MAPPING]
- if (noThreadMessages != null && noThreadMessages.second.isNotEmpty()) {
- processMessages(NO_THREAD_MAPPING, noThreadMessages.first, noThreadMessages.second)
- }
-
- if (failures.isEmpty()) {
- handleSuccess(dispatcherName)
- } else {
- handleFailure(dispatcherName)
- }
- }
-
- private fun handleSuccess(dispatcherName: String) {
- Log.i(TAG, "Completed processing of ${messages.size} messages (id: $id)")
- delegate?.handleJobSucceeded(this, dispatcherName)
- }
-
- private fun handleFailure(dispatcherName: String) {
- Log.i(TAG, "Handling failure of ${failures.size} messages (${messages.size - failures.size} processed successfully) (id: $id)")
- delegate?.handleJobFailed(this, dispatcherName, Exception("One or more jobs resulted in failure"))
- }
-
- override fun serialize(): Data {
- val arraySize = messages.size
- val dataArrays = UtilProtos.ByteArrayList.newBuilder()
- .addAllContent(messages.map(MessageReceiveParameters::data).map(ByteString::copyFrom))
- .build()
- val serverHashes = messages.map { it.serverHash.orEmpty() }
- val openGroupServerIds = messages.map { it.openGroupMessageServerID ?: -1L }
- val closedGroups = messages.map { it.closedGroup?.publicKey.orEmpty() }
- return Data.Builder()
- .putInt(NUM_MESSAGES_KEY, arraySize)
- .putByteArray(DATA_KEY, dataArrays.toByteArray())
- .putLongArray(OPEN_GROUP_MESSAGE_SERVER_ID_KEY, openGroupServerIds.toLongArray())
- .putStringArray(SERVER_HASH_KEY, serverHashes.toTypedArray())
- .putStringArray(CLOSED_GROUP_DESTINATION_KEY, closedGroups.toTypedArray())
- .putString(FROM_COMMUNITY_KEY, fromCommunity?.address)
- .build()
- }
-
- override fun getFactoryKey(): String = KEY
-
-
- @AssistedFactory
- abstract class Factory : Job.DeserializeFactory {
- @Deprecated("New code should try to handle message directly instead of creating this job")
- protected abstract fun create(
- messages: List,
- fromCommunity: Address.Community?,
- ): BatchMessageReceiveJob
-
- override fun create(data: Data): BatchMessageReceiveJob {
- val numMessages = data.getInt(NUM_MESSAGES_KEY)
- val dataArrays = data.getByteArray(DATA_KEY)
- val contents =
- UtilProtos.ByteArrayList.parseFrom(dataArrays).contentList.map(ByteString::toByteArray)
- val serverHashes =
- if (data.hasStringArray(SERVER_HASH_KEY)) data.getStringArray(SERVER_HASH_KEY) else arrayOf()
- val openGroupMessageServerIDs = data.getLongArray(OPEN_GROUP_MESSAGE_SERVER_ID_KEY)
- val openGroupID = data.getStringOrDefault(OPEN_GROUP_ID_KEY, null)
- val closedGroups =
- if (data.hasStringArray(CLOSED_GROUP_DESTINATION_KEY)) data.getStringArray(CLOSED_GROUP_DESTINATION_KEY)
- else arrayOf()
-
- val parameters = (0 until numMessages).map { index ->
- val serverHash = serverHashes[index].let { if (it.isEmpty()) null else it }
- val serverId = openGroupMessageServerIDs[index].let { if (it == -1L) null else it }
- val closedGroup = closedGroups.getOrNull(index)?.let {
- if (it.isEmpty()) null else Destination.ClosedGroup(it)
- }
- MessageReceiveParameters(
- data = contents[index],
- serverHash = serverHash,
- openGroupMessageServerID = serverId,
- closedGroup = closedGroup
- )
- }
- var fromCommunity = data.getStringOrDefault(FROM_COMMUNITY_KEY, null)
- ?.toAddress() as? Address.Community
-
- if (fromCommunity == null && openGroupID != null) {
- // This is the old "server.room" format, which we no longer use but will have
- // to support for a bit for the persisted data to run through the system.
- val split = openGroupID.lastIndexOf(".")
- .takeIf { it >= 0 && it < openGroupID.length - 1 }
- ?.let { openGroupID.substring(0, it) to openGroupID.substring(it + 1) }
-
- if (split != null) {
- fromCommunity = Address.Community(serverUrl = split.first, room = split.second)
- }
- }
-
- return create(messages = parameters, fromCommunity = fromCommunity)
- }
- }
-
-}
\ No newline at end of file
diff --git a/app/src/main/java/org/session/libsession/messaging/jobs/InviteContactsJob.kt b/app/src/main/java/org/session/libsession/messaging/jobs/InviteContactsJob.kt
index 70157d8ef1..03d546c69a 100644
--- a/app/src/main/java/org/session/libsession/messaging/jobs/InviteContactsJob.kt
+++ b/app/src/main/java/org/session/libsession/messaging/jobs/InviteContactsJob.kt
@@ -18,25 +18,31 @@ import org.session.libsession.messaging.messages.control.GroupUpdated
import org.session.libsession.messaging.sending_receiving.MessageSender
import org.session.libsession.messaging.utilities.Data
import org.session.libsession.messaging.utilities.MessageAuthentication.buildGroupInviteSignature
-import org.session.libsession.snode.SnodeAPI
+import org.session.libsession.network.SnodeClock
import org.session.libsession.utilities.ConfigFactoryProtocol
import org.session.libsession.utilities.getGroup
-import org.session.libsignal.protos.SignalServiceProtos.DataMessage.GroupUpdateInviteMessage
-import org.session.libsignal.protos.SignalServiceProtos.DataMessage.GroupUpdateMessage
+import org.session.libsession.utilities.withGroupConfigs
+import org.session.libsession.utilities.withMutableGroupConfigs
import org.session.libsignal.utilities.AccountId
import org.session.libsignal.utilities.Log
+import org.session.protos.SessionProtos.GroupUpdateInviteMessage
+import org.session.protos.SessionProtos.GroupUpdateMessage
class InviteContactsJob @AssistedInject constructor(
@Assisted val groupSessionId: String,
@Assisted val memberSessionIds: Array,
+ @Assisted val isReinvite: Boolean,
private val configFactory: ConfigFactoryProtocol,
private val messageSender: MessageSender,
+ private val snodeClock: SnodeClock
+
) : Job {
companion object {
const val KEY = "InviteContactJob"
private const val GROUP = "group"
private const val MEMBER = "member"
+ private const val REINVITE = "reinvite"
}
@@ -66,7 +72,7 @@ class InviteContactsJob @AssistedInject constructor(
configs.groupInfo.getName() to configs.groupKeys.makeSubAccount(memberSessionId)
}
- val timestamp = SnodeAPI.nowWithOffset
+ val timestamp = snodeClock.currentTimeMillis()
val signature = ED25519.sign(
ed25519PrivateKey = adminKey.data,
message = buildGroupInviteSignature(memberId, timestamp),
@@ -130,10 +136,17 @@ class InviteContactsJob @AssistedInject constructor(
inviteeAccountIds = failures.map { it.first },
groupName = groupName.orEmpty(),
underlying = firstError,
- ).format(MessagingModuleConfiguration.shared.context,
- MessagingModuleConfiguration.shared.recipientRepository).let {
+ isReinvite = isReinvite
+ ).format(
+ MessagingModuleConfiguration.shared.context,
+ MessagingModuleConfiguration.shared.recipientRepository
+ ).let {
withContext(Dispatchers.Main) {
- Toast.makeText(MessagingModuleConfiguration.shared.context, it, Toast.LENGTH_LONG).show()
+ Toast.makeText(
+ MessagingModuleConfiguration.shared.context,
+ it,
+ Toast.LENGTH_LONG
+ ).show()
}
}
}
@@ -144,6 +157,7 @@ class InviteContactsJob @AssistedInject constructor(
Data.Builder()
.putString(GROUP, groupSessionId)
.putStringArray(MEMBER, memberSessionIds)
+ .putBoolean(REINVITE, isReinvite)
.build()
override fun getFactoryKey(): String = KEY
@@ -153,14 +167,17 @@ class InviteContactsJob @AssistedInject constructor(
abstract fun create(
groupSessionId: String,
memberSessionIds: Array,
+ isReinvite: Boolean
): InviteContactsJob
override fun create(data: Data): InviteContactsJob? {
val groupSessionId = data.getString(GROUP) ?: return null
val memberSessionIds = data.getStringArray(MEMBER) ?: return null
+ val reinvite = data.getBooleanOrDefault(REINVITE, false)
return create(
groupSessionId = groupSessionId,
memberSessionIds = memberSessionIds,
+ isReinvite = reinvite
)
}
}
diff --git a/app/src/main/java/org/session/libsession/messaging/jobs/JobQueue.kt b/app/src/main/java/org/session/libsession/messaging/jobs/JobQueue.kt
index 38cda9a392..ea60427a04 100644
--- a/app/src/main/java/org/session/libsession/messaging/jobs/JobQueue.kt
+++ b/app/src/main/java/org/session/libsession/messaging/jobs/JobQueue.kt
@@ -38,7 +38,6 @@ class JobQueue : JobDelegate {
for (job in channel) {
if (!isActive) break
val communityAddress = when (job) {
- is BatchMessageReceiveJob -> job.fromCommunity?.address
is OpenGroupDeleteJob -> job.address?.address
is TrimThreadJob -> job.communityAddress?.address
else -> null
@@ -121,7 +120,6 @@ class JobQueue : JobDelegate {
while (isActive) {
when (val job = queue.receive()) {
is InviteContactsJob,
- is NotifyPNServerJob,
is AttachmentUploadJob,
is MessageSendJob -> {
txQueue.send(job)
@@ -132,10 +130,8 @@ class JobQueue : JobDelegate {
is OpenGroupDeleteJob -> {
openGroupQueue.send(job)
}
- is TrimThreadJob,
- is BatchMessageReceiveJob -> {
- if ((job is BatchMessageReceiveJob && job.fromCommunity != null)
- || (job is TrimThreadJob && job.communityAddress != null)) {
+ is TrimThreadJob -> {
+ if (job.communityAddress != null) {
openGroupQueue.send(job)
} else {
rxQueue.send(job)
@@ -222,8 +218,6 @@ class JobQueue : JobDelegate {
AttachmentUploadJob.KEY,
AttachmentDownloadJob.KEY,
MessageSendJob.KEY,
- NotifyPNServerJob.KEY,
- BatchMessageReceiveJob.KEY,
OpenGroupDeleteJob.KEY,
InviteContactsJob.KEY,
)
@@ -249,16 +243,6 @@ class JobQueue : JobDelegate {
return
}
- // Batch message receive job, re-queue non-permanently failed jobs
- if (job is BatchMessageReceiveJob && job.failureCount <= 0) {
- val replacementParameters = job.failures.toList()
- if (replacementParameters.isNotEmpty()) {
- val newJob = job.recreateWithNewMessages(replacementParameters)
- newJob.failureCount = job.failureCount + 1
- add(newJob)
- }
- }
-
// Regular job failure
job.failureCount += 1
diff --git a/app/src/main/java/org/session/libsession/messaging/jobs/MessageSendJob.kt b/app/src/main/java/org/session/libsession/messaging/jobs/MessageSendJob.kt
index 1cb9e88bfa..eeec1748ac 100644
--- a/app/src/main/java/org/session/libsession/messaging/jobs/MessageSendJob.kt
+++ b/app/src/main/java/org/session/libsession/messaging/jobs/MessageSendJob.kt
@@ -22,9 +22,10 @@ import org.session.libsession.messaging.sending_receiving.MessageSender
import org.session.libsession.messaging.utilities.Data
import org.session.libsession.utilities.ConfigFactoryProtocol
import org.session.libsession.utilities.ConfigUpdateNotification
+import org.session.libsession.utilities.withGroupConfigs
import org.session.libsignal.utilities.AccountId
-import org.session.libsignal.utilities.HTTP
import org.session.libsignal.utilities.Log
+import org.thoughtcrime.securesms.api.error.UnhandledStatusCodeException
class MessageSendJob @AssistedInject constructor(
@Assisted val message: Message,
@@ -102,8 +103,8 @@ class MessageSendJob @AssistedInject constructor(
this.handleSuccess(dispatcherName)
statusCallback?.trySend(Result.success(Unit))
- } catch (e: HTTP.HTTPRequestFailedException) {
- if (e.statusCode == 429) { this.handlePermanentFailure(dispatcherName, e) }
+ } catch (e: UnhandledStatusCodeException) {
+ if (e.code == 429) { this.handlePermanentFailure(dispatcherName, e) }
else { this.handleFailure(dispatcherName, e) }
statusCallback?.trySend(Result.failure(e))
diff --git a/app/src/main/java/org/session/libsession/messaging/jobs/NotifyPNServerJob.kt b/app/src/main/java/org/session/libsession/messaging/jobs/NotifyPNServerJob.kt
deleted file mode 100644
index 62e2fb94bb..0000000000
--- a/app/src/main/java/org/session/libsession/messaging/jobs/NotifyPNServerJob.kt
+++ /dev/null
@@ -1,94 +0,0 @@
-package org.session.libsession.messaging.jobs
-
-import com.esotericsoftware.kryo.Kryo
-import com.esotericsoftware.kryo.io.Input
-import com.esotericsoftware.kryo.io.Output
-import okhttp3.MediaType.Companion.toMediaType
-import okhttp3.Request
-import okhttp3.RequestBody
-import org.session.libsession.messaging.jobs.Job.Companion.MAX_BUFFER_SIZE_BYTES
-import org.session.libsession.messaging.sending_receiving.notifications.Server
-import org.session.libsession.messaging.utilities.Data
-import org.session.libsession.snode.OnionRequestAPI
-import org.session.libsession.snode.SnodeMessage
-import org.session.libsession.snode.Version
-import org.session.libsignal.utilities.JsonUtil
-import org.session.libsignal.utilities.Log
-import org.session.libsignal.utilities.retryIfNeeded
-
-class NotifyPNServerJob(val message: SnodeMessage) : Job {
- override var delegate: JobDelegate? = null
- override var id: String? = null
- override var failureCount: Int = 0
-
- override val maxFailureCount: Int = 20
- companion object {
- val KEY: String = "NotifyPNServerJob"
-
- // Keys used for database storage
- private val MESSAGE_KEY = "message"
- }
-
- override suspend fun execute(dispatcherName: String) {
- val server = Server.LEGACY
- val parameters = mapOf( "data" to message.data, "send_to" to message.recipient )
- val url = "${server.url}/notify"
- val body = RequestBody.create("application/json".toMediaType(), JsonUtil.toJson(parameters))
- val request = Request.Builder().url(url).post(body).build()
- retryIfNeeded(4) {
- OnionRequestAPI.sendOnionRequest(
- request,
- server.url,
- server.publicKey,
- Version.V2
- ) success { response ->
- when (response.code) {
- null, 0 -> Log.d("NotifyPNServerJob", "Couldn't notify PN server due to error: ${response.message}.")
- }
- } fail { exception ->
- Log.d("NotifyPNServerJob", "Couldn't notify PN server due to error: $exception.")
- }
- } success {
- handleSuccess(dispatcherName)
- } fail {
- handleFailure(dispatcherName, it)
- }
- }
-
- private fun handleSuccess(dispatcherName: String) {
- delegate?.handleJobSucceeded(this, dispatcherName)
- }
-
- private fun handleFailure(dispatcherName: String, error: Exception) {
- delegate?.handleJobFailed(this, dispatcherName, error)
- }
-
- override fun serialize(): Data {
- val kryo = Kryo()
- kryo.isRegistrationRequired = false
- val serializedMessage = ByteArray(4096)
- val output = Output(serializedMessage, MAX_BUFFER_SIZE_BYTES)
- kryo.writeObject(output, message)
- output.close()
- return Data.Builder()
- .putByteArray(MESSAGE_KEY, serializedMessage)
- .build();
- }
-
- override fun getFactoryKey(): String {
- return KEY
- }
-
- class DeserializeFactory : Job.DeserializeFactory {
-
- override fun create(data: Data): NotifyPNServerJob {
- val serializedMessage = data.getByteArray(MESSAGE_KEY)
- val kryo = Kryo()
- kryo.isRegistrationRequired = false
- val input = Input(serializedMessage)
- val message = kryo.readObject(input, SnodeMessage::class.java)
- input.close()
- return NotifyPNServerJob(message)
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/org/session/libsession/messaging/jobs/SessionJobManagerFactories.kt b/app/src/main/java/org/session/libsession/messaging/jobs/SessionJobManagerFactories.kt
index e1780ce19f..663ee8079e 100644
--- a/app/src/main/java/org/session/libsession/messaging/jobs/SessionJobManagerFactories.kt
+++ b/app/src/main/java/org/session/libsession/messaging/jobs/SessionJobManagerFactories.kt
@@ -5,7 +5,6 @@ import javax.inject.Inject
class SessionJobManagerFactories @Inject constructor(
private val attachmentDownloadJobFactory: AttachmentDownloadJob.Factory,
private val attachmentUploadJobFactory: AttachmentUploadJob.Factory,
- private val batchFactory: BatchMessageReceiveJob.Factory,
private val trimThreadFactory: TrimThreadJob.Factory,
private val messageSendJobFactory: MessageSendJob.Factory,
private val deleteJobFactory: OpenGroupDeleteJob.Factory,
@@ -17,9 +16,7 @@ class SessionJobManagerFactories @Inject constructor(
AttachmentDownloadJob.KEY to attachmentDownloadJobFactory,
AttachmentUploadJob.KEY to attachmentUploadJobFactory,
MessageSendJob.KEY to messageSendJobFactory,
- NotifyPNServerJob.KEY to NotifyPNServerJob.DeserializeFactory(),
TrimThreadJob.KEY to trimThreadFactory,
- BatchMessageReceiveJob.KEY to batchFactory,
OpenGroupDeleteJob.KEY to deleteJobFactory,
InviteContactsJob.KEY to inviteContactsJobFactory,
)
diff --git a/app/src/main/java/org/session/libsession/messaging/messages/Destination.kt b/app/src/main/java/org/session/libsession/messaging/messages/Destination.kt
index 5ddbad9fa1..3b6c5f3f86 100644
--- a/app/src/main/java/org/session/libsession/messaging/messages/Destination.kt
+++ b/app/src/main/java/org/session/libsession/messaging/messages/Destination.kt
@@ -2,6 +2,7 @@ package org.session.libsession.messaging.messages
import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.utilities.Address
+import org.session.libsession.utilities.withUserConfigs
sealed class Destination {
diff --git a/app/src/main/java/org/session/libsession/messaging/messages/Message.kt b/app/src/main/java/org/session/libsession/messaging/messages/Message.kt
index 38d0c2eedc..311b638b13 100644
--- a/app/src/main/java/org/session/libsession/messaging/messages/Message.kt
+++ b/app/src/main/java/org/session/libsession/messaging/messages/Message.kt
@@ -1,5 +1,8 @@
package org.session.libsession.messaging.messages
+import network.loki.messenger.libsession_util.protocol.ProFeature
+import network.loki.messenger.libsession_util.protocol.ProMessageFeature
+import network.loki.messenger.libsession_util.protocol.ProProfileFeature
import network.loki.messenger.libsession_util.util.ExpiryMode
import org.session.libsession.database.MessageDataProvider
import org.session.libsession.messaging.MessagingModuleConfiguration
@@ -7,9 +10,11 @@ import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate
import org.session.libsession.messaging.messages.visible.VisibleMessage
import org.session.libsession.snode.SnodeMessage
import org.session.libsession.utilities.Address
-import org.session.libsignal.protos.SignalServiceProtos
-import org.session.libsignal.protos.SignalServiceProtos.Content.ExpirationType
+import org.session.protos.SessionProtos
+import org.session.protos.SessionProtos.Content.ExpirationType
import org.thoughtcrime.securesms.database.model.MessageId
+import org.thoughtcrime.securesms.pro.toProMessageBitSetValue
+import org.thoughtcrime.securesms.pro.toProProfileBitSetValue
abstract class Message {
var id: MessageId? = null // Message ID in the database. Not all messages will be saved to db.
@@ -27,6 +32,20 @@ abstract class Message {
var expiryMode: ExpiryMode = ExpiryMode.NONE
+ /**
+ * The pro features enabled for this message.
+ *
+ * Note:
+ * * When this message is an incoming message, the pro features will only be populated
+ * if we can prove that the sender has an active pro subscription.
+ *
+ * * When this message represents an outgoing message, this property can be populated by
+ * application code at their wishes but the actual translating to protobuf onto the wired will
+ * be checked against the current user's pro proof, if no active pro subscription is found,
+ * the pro features will not be sent in the protobuf messages.
+ */
+ var proFeatures: Set = emptySet()
+
open val coerceDisappearAfterSendToRead = false
open val defaultTtl: Long = SnodeMessage.DEFAULT_TTL
@@ -49,17 +68,17 @@ abstract class Message {
&& recipient != null
protected abstract fun buildProto(
- builder: SignalServiceProtos.Content.Builder,
+ builder: SessionProtos.Content.Builder,
messageDataProvider: MessageDataProvider
)
fun toProto(
- builder: SignalServiceProtos.Content.Builder,
+ builder: SessionProtos.Content.Builder,
messageDataProvider: MessageDataProvider
) {
// First apply common message data
// * Expiry mode
- builder.expirationTimerSeconds = expiryMode.expirySeconds.toInt()
+ builder.expirationTimer = expiryMode.expirySeconds.toInt()
builder.expirationType = when (expiryMode) {
is ExpiryMode.AfterSend -> ExpirationType.DELETE_AFTER_SEND
is ExpiryMode.AfterRead -> ExpirationType.DELETE_AFTER_READ
@@ -67,7 +86,20 @@ abstract class Message {
}
// * Timestamps
- builder.setSigTimestampMs(sentTimestamp!!)
+ builder.setSigTimestamp(sentTimestamp!!)
+
+ // Pro features
+ if (proFeatures.any { it is ProMessageFeature }) {
+ builder.proMessageBuilder.setMsgBitset(
+ proFeatures.toProMessageBitSetValue()
+ )
+ }
+
+ if (proFeatures.any { it is ProProfileFeature }) {
+ builder.proMessageBuilder.setProfileBitset(
+ proFeatures.toProProfileBitSetValue()
+ )
+ }
// Then ask the subclasses to build their specific proto
buildProto(builder, messageDataProvider)
@@ -76,8 +108,8 @@ abstract class Message {
abstract fun shouldDiscardIfBlocked(): Boolean
}
-inline fun M.copyExpiration(proto: SignalServiceProtos.Content): M = apply {
- (proto.takeIf { it.hasExpirationTimerSeconds() }?.expirationTimerSeconds ?: proto.dataMessage?.expireTimerSeconds)?.let { duration ->
+inline fun M.copyExpiration(proto: SessionProtos.Content): M = apply {
+ proto.takeIf { it.hasExpirationTimer() }?.expirationTimer?.let { duration ->
expiryMode = when (proto.expirationType.takeIf { duration > 0 }) {
ExpirationType.DELETE_AFTER_SEND -> ExpiryMode.AfterSend(duration.toLong())
ExpirationType.DELETE_AFTER_READ -> ExpiryMode.AfterRead(duration.toLong())
diff --git a/app/src/main/java/org/session/libsession/messaging/messages/ProfileUpdateHandler.kt b/app/src/main/java/org/session/libsession/messaging/messages/ProfileUpdateHandler.kt
index 86a6b8ec96..75dc0fab54 100644
--- a/app/src/main/java/org/session/libsession/messaging/messages/ProfileUpdateHandler.kt
+++ b/app/src/main/java/org/session/libsession/messaging/messages/ProfileUpdateHandler.kt
@@ -1,6 +1,8 @@
package org.session.libsession.messaging.messages
import com.google.protobuf.ByteString
+import network.loki.messenger.libsession_util.pro.ProProof
+import network.loki.messenger.libsession_util.protocol.DecodedPro
import network.loki.messenger.libsession_util.protocol.ProProfileFeature
import network.loki.messenger.libsession_util.util.BaseCommunityInfo
import network.loki.messenger.libsession_util.util.BitSet
@@ -10,9 +12,10 @@ import org.session.libsession.utilities.Address
import org.session.libsession.utilities.Address.Companion.toAddress
import org.session.libsession.utilities.ConfigFactoryProtocol
import org.session.libsession.utilities.updateContact
-import org.session.libsignal.protos.SignalServiceProtos
+import org.session.libsession.utilities.withMutableUserConfigs
import org.session.libsignal.utilities.AccountId
import org.session.libsignal.utilities.Log
+import org.session.protos.SessionProtos
import org.thoughtcrime.securesms.database.BlindMappingRepository
import org.thoughtcrime.securesms.database.RecipientRepository
import org.thoughtcrime.securesms.database.RecipientSettingsDatabase
@@ -77,9 +80,7 @@ class ProfileUpdateHandler @Inject constructor(
profilePicture = updates.pic
}
- if (updates.proFeatures != null) {
- proFeatures = updates.proFeatures
- }
+ proFeatures = updates.proFeatures
if (updates.profileUpdateTime != null) {
profileUpdatedEpochSeconds = updates.profileUpdateTime.toEpochSeconds()
@@ -115,9 +116,7 @@ class ProfileUpdateHandler @Inject constructor(
c.name = updates.name
}
- if (updates.proFeatures != null) {
- c.proFeatures = updates.proFeatures
- }
+ c.proFeatures = updates.proFeatures
if (updates.profileUpdateTime != null) {
c.profileUpdatedEpochSeconds = updates.profileUpdateTime.toEpochSeconds()
@@ -154,16 +153,15 @@ class ProfileUpdateHandler @Inject constructor(
r.copy(
name = updates.name ?: r.name,
profilePic = updates.pic ?: r.profilePic,
- blocksCommunityMessagesRequests = updates.blocksCommunityMessageRequests ?: r.blocksCommunityMessagesRequests,
+ blocksCommunityMessagesRequests = updates.blocksCommunityMessageRequests,
proData = updates.proProof?.let {
RecipientSettings.ProData(
info = it,
- features = updates.proFeatures ?: BitSet()
+ features = updates.proFeatures,
)
},
)
- } else if (updates.blocksCommunityMessageRequests != null &&
- r.blocksCommunityMessagesRequests != updates.blocksCommunityMessageRequests) {
+ } else if (r.blocksCommunityMessagesRequests != updates.blocksCommunityMessageRequests) {
r.copy(blocksCommunityMessagesRequests = updates.blocksCommunityMessageRequests)
} else {
r
@@ -190,23 +188,18 @@ class ProfileUpdateHandler @Inject constructor(
}
class Updates private constructor(
- // Name to update, must be non-blank if provided.
- val name: String? = null,
- val pic: UserPic? = null,
- val proProof: Conversation.ProProofInfo? = null,
- val proFeatures: BitSet? = null,
- val blocksCommunityMessageRequests: Boolean? = null,
+ val name: String?,
+ val pic: UserPic?,
+ val proProof: Conversation.ProProofInfo?,
+ val proFeatures: BitSet,
+ val blocksCommunityMessageRequests: Boolean,
val profileUpdateTime: Instant?,
) {
- init {
- check(name == null || name.isNotBlank()) {
- "Name must be non-blank if provided"
- }
- }
-
companion object {
- fun create(content: SignalServiceProtos.Content): Updates? {
- val profile: SignalServiceProtos.DataMessage.LokiProfile
+ fun create(content: SessionProtos.Content,
+ nowMills: Long,
+ pro: DecodedPro?): Updates? {
+ val profile: SessionProtos.LokiProfile
val profilePicKey: ByteString?
when {
@@ -248,20 +241,33 @@ class ProfileUpdateHandler @Inject constructor(
content.dataMessage.hasBlocksCommunityMessageRequests()) {
content.dataMessage.blocksCommunityMessageRequests
} else {
- null
+ true
}
- if (name == null && pic == null && blocksCommunityMessageRequests == null) {
- // Nothing is updated..
- return null
+ val proProofInfo: Conversation.ProProofInfo?
+ val proFeatures: BitSet
+
+ if (pro?.status == ProProof.STATUS_VALID &&
+ pro.proof != null &&
+ pro.proof!!.expiryMs > nowMills) {
+ proProofInfo = Conversation.ProProofInfo(
+ genIndexHash = pro.proof!!.genIndexHashHex.hexToByteArray(),
+ expiryMs = pro.proof!!.expiryMs,
+ )
+ proFeatures = pro.proProfileFeatures
+ } else {
+ proProofInfo = null
+ proFeatures = BitSet()
}
return Updates(
name = name,
pic = pic,
blocksCommunityMessageRequests = blocksCommunityMessageRequests,
- profileUpdateTime = if (profile.hasLastProfileUpdateSeconds()) {
- Instant.ofEpochSecond(profile.lastProfileUpdateSeconds)
+ proProof = proProofInfo,
+ proFeatures = proFeatures,
+ profileUpdateTime = if (profile.hasLastUpdateSeconds()) {
+ Instant.ofEpochSecond(profile.lastUpdateSeconds)
} else {
null
}
diff --git a/app/src/main/java/org/session/libsession/messaging/messages/control/CallMessage.kt b/app/src/main/java/org/session/libsession/messaging/messages/control/CallMessage.kt
index f4c9781730..a6c84e2425 100644
--- a/app/src/main/java/org/session/libsession/messaging/messages/control/CallMessage.kt
+++ b/app/src/main/java/org/session/libsession/messaging/messages/control/CallMessage.kt
@@ -2,15 +2,15 @@ package org.session.libsession.messaging.messages.control
import org.session.libsession.database.MessageDataProvider
import org.session.libsession.messaging.messages.copyExpiration
-import org.session.libsignal.protos.SignalServiceProtos
-import org.session.libsignal.protos.SignalServiceProtos.CallMessage.Type.ANSWER
-import org.session.libsignal.protos.SignalServiceProtos.CallMessage.Type.END_CALL
-import org.session.libsignal.protos.SignalServiceProtos.CallMessage.Type.OFFER
-import org.session.libsignal.protos.SignalServiceProtos.CallMessage.Type.PRE_OFFER
+import org.session.protos.SessionProtos
+import org.session.protos.SessionProtos.CallMessage.Type.ANSWER
+import org.session.protos.SessionProtos.CallMessage.Type.END_CALL
+import org.session.protos.SessionProtos.CallMessage.Type.OFFER
+import org.session.protos.SessionProtos.CallMessage.Type.PRE_OFFER
import java.util.UUID
class CallMessage(): ControlMessage() {
- var type: SignalServiceProtos.CallMessage.Type? = null
+ var type: SessionProtos.CallMessage.Type? = null
var sdps: List = listOf()
var sdpMLineIndexes: List = listOf()
var sdpMids: List = listOf()
@@ -26,7 +26,7 @@ class CallMessage(): ControlMessage() {
override fun isValid(): Boolean = super.isValid() && type != null && callId != null
&& (sdps.isNotEmpty() || type in listOf(END_CALL, PRE_OFFER))
- constructor(type: SignalServiceProtos.CallMessage.Type,
+ constructor(type: SessionProtos.CallMessage.Type,
sdps: List,
sdpMLineIndexes: List,
sdpMids: List,
@@ -64,7 +64,7 @@ class CallMessage(): ControlMessage() {
fun endCall(callId: UUID) = CallMessage(END_CALL, emptyList(), emptyList(), emptyList(), callId)
- fun fromProto(proto: SignalServiceProtos.Content): CallMessage? {
+ fun fromProto(proto: SessionProtos.Content): CallMessage? {
val callMessageProto = if (proto.hasCallMessage()) proto.callMessage else return null
val type = callMessageProto.type
val sdps = callMessageProto.sdpsList
@@ -77,7 +77,7 @@ class CallMessage(): ControlMessage() {
}
protected override fun buildProto(
- builder: SignalServiceProtos.Content.Builder,
+ builder: SessionProtos.Content.Builder,
messageDataProvider: MessageDataProvider
) {
builder.callMessageBuilder
diff --git a/app/src/main/java/org/session/libsession/messaging/messages/control/DataExtractionNotification.kt b/app/src/main/java/org/session/libsession/messaging/messages/control/DataExtractionNotification.kt
index 79872cc635..2ee5e1a2f6 100644
--- a/app/src/main/java/org/session/libsession/messaging/messages/control/DataExtractionNotification.kt
+++ b/app/src/main/java/org/session/libsession/messaging/messages/control/DataExtractionNotification.kt
@@ -2,7 +2,7 @@ package org.session.libsession.messaging.messages.control
import org.session.libsession.database.MessageDataProvider
import org.session.libsession.messaging.messages.copyExpiration
-import org.session.libsignal.protos.SignalServiceProtos
+import org.session.protos.SessionProtos
class DataExtractionNotification() : ControlMessage() {
var kind: Kind? = null
@@ -25,13 +25,13 @@ class DataExtractionNotification() : ControlMessage() {
companion object {
const val TAG = "DataExtractionNotification"
- fun fromProto(proto: SignalServiceProtos.Content): DataExtractionNotification? {
+ fun fromProto(proto: SessionProtos.Content): DataExtractionNotification? {
if (!proto.hasDataExtractionNotification()) return null
val dataExtractionNotification = proto.dataExtractionNotification!!
val kind: Kind = when(dataExtractionNotification.type) {
- SignalServiceProtos.DataExtractionNotification.Type.SCREENSHOT -> Kind.Screenshot()
- SignalServiceProtos.DataExtractionNotification.Type.MEDIA_SAVED -> {
- val timestamp = if (dataExtractionNotification.hasTimestampMs()) dataExtractionNotification.timestampMs else return null
+ SessionProtos.DataExtractionNotification.Type.SCREENSHOT -> Kind.Screenshot()
+ SessionProtos.DataExtractionNotification.Type.MEDIA_SAVED -> {
+ val timestamp = if (dataExtractionNotification.hasTimestamp()) dataExtractionNotification.timestamp else return null
Kind.MediaSaved(timestamp)
}
}
@@ -53,16 +53,16 @@ class DataExtractionNotification() : ControlMessage() {
}
}
- protected override fun buildProto(
- builder: SignalServiceProtos.Content.Builder,
+ override fun buildProto(
+ builder: SessionProtos.Content.Builder,
messageDataProvider: MessageDataProvider
) {
val dataExtractionNotification = builder.dataExtractionNotificationBuilder
when (val kind = kind!!) {
- is Kind.Screenshot -> dataExtractionNotification.type = SignalServiceProtos.DataExtractionNotification.Type.SCREENSHOT
+ is Kind.Screenshot -> dataExtractionNotification.type = SessionProtos.DataExtractionNotification.Type.SCREENSHOT
is Kind.MediaSaved -> {
- dataExtractionNotification.type = SignalServiceProtos.DataExtractionNotification.Type.MEDIA_SAVED
- dataExtractionNotification.timestampMs = kind.timestamp
+ dataExtractionNotification.type = SessionProtos.DataExtractionNotification.Type.MEDIA_SAVED
+ dataExtractionNotification.timestamp = kind.timestamp
}
}
}
diff --git a/app/src/main/java/org/session/libsession/messaging/messages/control/ExpirationTimerUpdate.kt b/app/src/main/java/org/session/libsession/messaging/messages/control/ExpirationTimerUpdate.kt
index c7d1f21903..10a7bc5487 100644
--- a/app/src/main/java/org/session/libsession/messaging/messages/control/ExpirationTimerUpdate.kt
+++ b/app/src/main/java/org/session/libsession/messaging/messages/control/ExpirationTimerUpdate.kt
@@ -3,8 +3,8 @@ package org.session.libsession.messaging.messages.control
import org.session.libsession.database.MessageDataProvider
import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.messages.copyExpiration
-import org.session.libsignal.protos.SignalServiceProtos
-import org.session.libsignal.protos.SignalServiceProtos.DataMessage.Flags.EXPIRATION_TIMER_UPDATE_VALUE
+import org.session.protos.SessionProtos
+import org.session.protos.SessionProtos.DataMessage.Flags.EXPIRATION_TIMER_UPDATE_VALUE
/** In the case of a sync message, the public key of the person the message was targeted at.
*
@@ -19,19 +19,18 @@ data class ExpirationTimerUpdate(var syncTarget: String? = null, val isGroup: Bo
const val TAG = "ExpirationTimerUpdate"
private val storage = MessagingModuleConfiguration.shared.storage
- fun fromProto(proto: SignalServiceProtos.Content, isGroup: Boolean): ExpirationTimerUpdate? =
+ fun fromProto(proto: SessionProtos.Content, isGroup: Boolean): ExpirationTimerUpdate? =
proto.dataMessage?.takeIf { it.flags and EXPIRATION_TIMER_UPDATE_VALUE != 0 }?.run {
ExpirationTimerUpdate(takeIf { hasSyncTarget() }?.syncTarget, isGroup).copyExpiration(proto)
}
}
- protected override fun buildProto(
- builder: SignalServiceProtos.Content.Builder,
+ override fun buildProto(
+ builder: SessionProtos.Content.Builder,
messageDataProvider: MessageDataProvider
) {
builder.dataMessageBuilder
.setFlags(EXPIRATION_TIMER_UPDATE_VALUE)
- .setExpireTimerSeconds(expiryMode.expirySeconds.toInt())
.also { builder ->
// Sync target
syncTarget?.let { builder.syncTarget = it }
diff --git a/app/src/main/java/org/session/libsession/messaging/messages/control/GroupUpdated.kt b/app/src/main/java/org/session/libsession/messaging/messages/control/GroupUpdated.kt
index 16fa0cba23..ef4f4e848c 100644
--- a/app/src/main/java/org/session/libsession/messaging/messages/control/GroupUpdated.kt
+++ b/app/src/main/java/org/session/libsession/messaging/messages/control/GroupUpdated.kt
@@ -1,11 +1,10 @@
package org.session.libsession.messaging.messages.control
import org.session.libsession.database.MessageDataProvider
-import org.session.libsignal.protos.SignalServiceProtos.Content
-import org.session.libsignal.protos.SignalServiceProtos.DataMessage.GroupUpdateMessage
+import org.session.protos.SessionProtos
class GroupUpdated @JvmOverloads constructor(
- val inner: GroupUpdateMessage = GroupUpdateMessage.getDefaultInstance(),
+ val inner: SessionProtos.GroupUpdateMessage = SessionProtos.GroupUpdateMessage.getDefaultInstance(),
): ControlMessage() {
override fun isValid(): Boolean {
@@ -20,7 +19,7 @@ class GroupUpdated @JvmOverloads constructor(
&& !inner.hasInviteResponse() && !inner.hasDeleteMemberContent()
companion object {
- fun fromProto(message: Content): GroupUpdated? =
+ fun fromProto(message: SessionProtos.Content): GroupUpdated? =
if (message.hasDataMessage() && message.dataMessage.hasGroupUpdateMessage())
GroupUpdated(
inner = message.dataMessage.groupUpdateMessage,
@@ -28,7 +27,7 @@ class GroupUpdated @JvmOverloads constructor(
else null
}
- override fun buildProto(builder: Content.Builder, messageDataProvider: MessageDataProvider) {
+ override fun buildProto(builder: SessionProtos.Content.Builder, messageDataProvider: MessageDataProvider) {
builder.dataMessageBuilder
.setGroupUpdateMessage(inner)
.apply { profile?.let(this::setProfile) }
diff --git a/app/src/main/java/org/session/libsession/messaging/messages/control/MessageRequestResponse.kt b/app/src/main/java/org/session/libsession/messaging/messages/control/MessageRequestResponse.kt
index bb7057b17e..cd122710d4 100644
--- a/app/src/main/java/org/session/libsession/messaging/messages/control/MessageRequestResponse.kt
+++ b/app/src/main/java/org/session/libsession/messaging/messages/control/MessageRequestResponse.kt
@@ -2,7 +2,7 @@ package org.session.libsession.messaging.messages.control
import org.session.libsession.database.MessageDataProvider
import org.session.libsession.messaging.messages.copyExpiration
-import org.session.libsignal.protos.SignalServiceProtos
+import org.session.protos.SessionProtos
class MessageRequestResponse(val isApproved: Boolean) : ControlMessage() {
@@ -11,7 +11,7 @@ class MessageRequestResponse(val isApproved: Boolean) : ControlMessage() {
override fun shouldDiscardIfBlocked(): Boolean = true
override fun buildProto(
- builder: SignalServiceProtos.Content.Builder,
+ builder: SessionProtos.Content.Builder,
messageDataProvider: MessageDataProvider
) {
builder.messageRequestResponseBuilder
@@ -21,7 +21,7 @@ class MessageRequestResponse(val isApproved: Boolean) : ControlMessage() {
companion object {
const val TAG = "MessageRequestResponse"
- fun fromProto(proto: SignalServiceProtos.Content): MessageRequestResponse? {
+ fun fromProto(proto: SessionProtos.Content): MessageRequestResponse? {
val messageRequestResponseProto = if (proto.hasMessageRequestResponse()) proto.messageRequestResponse else return null
val isApproved = messageRequestResponseProto.isApproved
return MessageRequestResponse(isApproved)
diff --git a/app/src/main/java/org/session/libsession/messaging/messages/control/ReadReceipt.kt b/app/src/main/java/org/session/libsession/messaging/messages/control/ReadReceipt.kt
index 4c8021e416..3fb6350d22 100644
--- a/app/src/main/java/org/session/libsession/messaging/messages/control/ReadReceipt.kt
+++ b/app/src/main/java/org/session/libsession/messaging/messages/control/ReadReceipt.kt
@@ -2,7 +2,7 @@ package org.session.libsession.messaging.messages.control
import org.session.libsession.database.MessageDataProvider
import org.session.libsession.messaging.messages.copyExpiration
-import org.session.libsignal.protos.SignalServiceProtos
+import org.session.protos.SessionProtos
class ReadReceipt() : ControlMessage() {
var timestamps: List? = null
@@ -19,10 +19,10 @@ class ReadReceipt() : ControlMessage() {
companion object {
const val TAG = "ReadReceipt"
- fun fromProto(proto: SignalServiceProtos.Content): ReadReceipt? {
+ fun fromProto(proto: SessionProtos.Content): ReadReceipt? {
val receiptProto = if (proto.hasReceiptMessage()) proto.receiptMessage else return null
- if (receiptProto.type != SignalServiceProtos.ReceiptMessage.Type.READ) return null
- val timestamps = receiptProto.timestampMsList
+ if (receiptProto.type != SessionProtos.ReceiptMessage.Type.READ) return null
+ val timestamps = receiptProto.timestampList
if (timestamps.isEmpty()) return null
return ReadReceipt(timestamps = timestamps)
.copyExpiration(proto)
@@ -33,14 +33,14 @@ class ReadReceipt() : ControlMessage() {
this.timestamps = timestamps
}
- protected override fun buildProto(
- builder: SignalServiceProtos.Content.Builder,
+ override fun buildProto(
+ builder: SessionProtos.Content.Builder,
messageDataProvider: MessageDataProvider
) {
builder
.receiptMessageBuilder
- .setType(SignalServiceProtos.ReceiptMessage.Type.READ)
- .addAllTimestampMs(requireNotNull(timestamps) {
+ .setType(SessionProtos.ReceiptMessage.Type.READ)
+ .addAllTimestamp(requireNotNull(timestamps) {
"Timestamps is null"
})
}
diff --git a/app/src/main/java/org/session/libsession/messaging/messages/control/TypingIndicator.kt b/app/src/main/java/org/session/libsession/messaging/messages/control/TypingIndicator.kt
index ab2c17a1f2..ed9c2baf13 100644
--- a/app/src/main/java/org/session/libsession/messaging/messages/control/TypingIndicator.kt
+++ b/app/src/main/java/org/session/libsession/messaging/messages/control/TypingIndicator.kt
@@ -2,7 +2,7 @@ package org.session.libsession.messaging.messages.control
import org.session.libsession.database.MessageDataProvider
import org.session.libsession.messaging.messages.copyExpiration
-import org.session.libsignal.protos.SignalServiceProtos
+import org.session.protos.SessionProtos
class TypingIndicator() : ControlMessage() {
var kind: Kind? = null
@@ -19,7 +19,7 @@ class TypingIndicator() : ControlMessage() {
companion object {
const val TAG = "TypingIndicator"
- fun fromProto(proto: SignalServiceProtos.Content): TypingIndicator? {
+ fun fromProto(proto: SessionProtos.Content): TypingIndicator? {
val typingIndicatorProto = if (proto.hasTypingMessage()) proto.typingMessage else return null
val kind = Kind.fromProto(typingIndicatorProto.action)
return TypingIndicator(kind = kind)
@@ -32,17 +32,17 @@ class TypingIndicator() : ControlMessage() {
companion object {
@JvmStatic
- fun fromProto(proto: SignalServiceProtos.TypingMessage.Action): Kind =
+ fun fromProto(proto: SessionProtos.TypingMessage.Action): Kind =
when (proto) {
- SignalServiceProtos.TypingMessage.Action.STARTED -> STARTED
- SignalServiceProtos.TypingMessage.Action.STOPPED -> STOPPED
+ SessionProtos.TypingMessage.Action.STARTED -> STARTED
+ SessionProtos.TypingMessage.Action.STOPPED -> STOPPED
}
}
- fun toProto(): SignalServiceProtos.TypingMessage.Action {
+ fun toProto(): SessionProtos.TypingMessage.Action {
when (this) {
- STARTED -> return SignalServiceProtos.TypingMessage.Action.STARTED
- STOPPED -> return SignalServiceProtos.TypingMessage.Action.STOPPED
+ STARTED -> return SessionProtos.TypingMessage.Action.STARTED
+ STOPPED -> return SessionProtos.TypingMessage.Action.STOPPED
}
}
}
@@ -51,12 +51,12 @@ class TypingIndicator() : ControlMessage() {
this.kind = kind
}
- protected override fun buildProto(
- builder: SignalServiceProtos.Content.Builder,
+ override fun buildProto(
+ builder: SessionProtos.Content.Builder,
messageDataProvider: MessageDataProvider
) {
builder.typingMessageBuilder
- .setTimestampMs(sentTimestamp!!)
+ .setTimestamp(sentTimestamp!!)
.setAction(kind!!.toProto())
}
}
\ No newline at end of file
diff --git a/app/src/main/java/org/session/libsession/messaging/messages/control/UnsendRequest.kt b/app/src/main/java/org/session/libsession/messaging/messages/control/UnsendRequest.kt
index 38f5d7d34b..81dddb8467 100644
--- a/app/src/main/java/org/session/libsession/messaging/messages/control/UnsendRequest.kt
+++ b/app/src/main/java/org/session/libsession/messaging/messages/control/UnsendRequest.kt
@@ -2,7 +2,7 @@ package org.session.libsession.messaging.messages.control
import org.session.libsession.database.MessageDataProvider
import org.session.libsession.messaging.messages.copyExpiration
-import org.session.libsignal.protos.SignalServiceProtos
+import org.session.protos.SessionProtos
class UnsendRequest(var timestamp: Long? = null, var author: String? = null): ControlMessage() {
@@ -20,16 +20,16 @@ class UnsendRequest(var timestamp: Long? = null, var author: String? = null): Co
companion object {
const val TAG = "UnsendRequest"
- fun fromProto(proto: SignalServiceProtos.Content): UnsendRequest? =
- proto.takeIf { it.hasUnsendRequest() }?.unsendRequest?.run { UnsendRequest(timestampMs, author) }?.copyExpiration(proto)
+ fun fromProto(proto: SessionProtos.Content): UnsendRequest? =
+ proto.takeIf { it.hasUnsendRequest() }?.unsendRequest?.run { UnsendRequest(timestamp, author) }?.copyExpiration(proto)
}
- protected override fun buildProto(
- builder: SignalServiceProtos.Content.Builder,
+ override fun buildProto(
+ builder: SessionProtos.Content.Builder,
messageDataProvider: MessageDataProvider
) {
builder.unsendRequestBuilder
- .setTimestampMs(timestamp!!)
+ .setTimestamp(timestamp!!)
.setAuthor(author!!)
}
diff --git a/app/src/main/java/org/session/libsession/messaging/messages/signal/OutgoingMediaMessage.kt b/app/src/main/java/org/session/libsession/messaging/messages/signal/OutgoingMediaMessage.kt
index b7183cd8c8..830a656058 100644
--- a/app/src/main/java/org/session/libsession/messaging/messages/signal/OutgoingMediaMessage.kt
+++ b/app/src/main/java/org/session/libsession/messaging/messages/signal/OutgoingMediaMessage.kt
@@ -48,6 +48,7 @@ class OutgoingMediaMessage(
linkPreviews = linkPreview?.let { listOf(it) } ?: emptyList(),
group = null,
isGroupUpdateMessage = false,
+ proFeatures = message.proFeatures
)
constructor(
diff --git a/app/src/main/java/org/session/libsession/messaging/messages/signal/OutgoingTextMessage.kt b/app/src/main/java/org/session/libsession/messaging/messages/signal/OutgoingTextMessage.kt
index 912da3d5c7..eab343b176 100644
--- a/app/src/main/java/org/session/libsession/messaging/messages/signal/OutgoingTextMessage.kt
+++ b/app/src/main/java/org/session/libsession/messaging/messages/signal/OutgoingTextMessage.kt
@@ -6,14 +6,14 @@ import org.session.libsession.messaging.messages.visible.VisibleMessage
import org.session.libsession.messaging.utilities.UpdateMessageData
import org.session.libsession.utilities.Address
-data class OutgoingTextMessage(
+data class OutgoingTextMessage private constructor(
val recipient: Address,
val message: String?,
val expiresInMillis: Long,
val expireStartedAtMillis: Long,
val sentTimestampMillis: Long,
val isOpenGroupInvitation: Boolean,
- val proFeatures: Set = emptySet()
+ val proFeatures: Set
) {
constructor(
message: VisibleMessage,
@@ -27,6 +27,7 @@ data class OutgoingTextMessage(
expireStartedAtMillis = expireStartedAtMillis,
sentTimestampMillis = message.sentTimestamp!!,
isOpenGroupInvitation = false,
+ proFeatures = message.proFeatures
)
companion object {
@@ -36,7 +37,8 @@ data class OutgoingTextMessage(
sentTimestampMillis: Long,
expiresInMillis: Long,
expireStartedAtMillis: Long,
- ): OutgoingTextMessage? {
+ proFeatures: Set,
+ ): OutgoingTextMessage? {
return OutgoingTextMessage(
recipient = recipient,
message = UpdateMessageData.buildOpenGroupInvitation(
@@ -47,6 +49,7 @@ data class OutgoingTextMessage(
expireStartedAtMillis = expireStartedAtMillis,
sentTimestampMillis = sentTimestampMillis,
isOpenGroupInvitation = true,
+ proFeatures = proFeatures
)
}
}
diff --git a/app/src/main/java/org/session/libsession/messaging/messages/visible/Attachment.kt b/app/src/main/java/org/session/libsession/messaging/messages/visible/Attachment.kt
index d7a2cb82dc..cfb1c2f1b2 100644
--- a/app/src/main/java/org/session/libsession/messaging/messages/visible/Attachment.kt
+++ b/app/src/main/java/org/session/libsession/messaging/messages/visible/Attachment.kt
@@ -6,7 +6,7 @@ import android.webkit.MimeTypeMap
import com.google.protobuf.ByteString
import org.session.libsession.messaging.sending_receiving.attachments.PointerAttachment
import org.session.libsignal.messages.SignalServiceAttachmentPointer
-import org.session.libsignal.protos.SignalServiceProtos
+import org.session.protos.SessionProtos
import org.session.libsignal.utilities.guava.Optional
import java.io.File
import org.session.libsession.messaging.sending_receiving.attachments.Attachment as SignalAttachment
@@ -24,7 +24,7 @@ class Attachment {
companion object {
- fun fromProto(proto: SignalServiceProtos.AttachmentPointer): Attachment {
+ fun fromProto(proto: SessionProtos.AttachmentPointer): Attachment {
val result = Attachment()
// Note: For legacy Session Android clients this filename will be null and we'll synthesise an appropriate filename
@@ -49,7 +49,7 @@ class Attachment {
result.key = proto.key.toByteArray()
result.digest = proto.digest.toByteArray()
- val kind: Kind = if (proto.hasFlags() && proto.flags.and(SignalServiceProtos.AttachmentPointer.Flags.VOICE_MESSAGE_VALUE) > 0) {
+ val kind: Kind = if (proto.hasFlags() && proto.flags.and(SessionProtos.AttachmentPointer.Flags.VOICE_MESSAGE_VALUE) > 0) {
Kind.VOICE_MESSAGE
} else {
Kind.GENERIC
@@ -68,8 +68,8 @@ class Attachment {
return result
}
- fun createAttachmentPointer(attachment: SignalServiceAttachmentPointer): SignalServiceProtos.AttachmentPointer? {
- val builder = SignalServiceProtos.AttachmentPointer.newBuilder()
+ fun createAttachmentPointer(attachment: SignalServiceAttachmentPointer): SessionProtos.AttachmentPointer? {
+ val builder = SessionProtos.AttachmentPointer.newBuilder()
.setContentType(attachment.contentType)
.setId(attachment.id.toString().toLongOrNull() ?: 0L)
.setKey(ByteString.copyFrom(attachment.key))
@@ -89,7 +89,7 @@ class Attachment {
if (attachment.preview.isPresent) { builder.thumbnail = ByteString.copyFrom(attachment.preview.get()) }
if (attachment.width > 0) { builder.width = attachment.width }
if (attachment.height > 0) { builder.height = attachment.height }
- if (attachment.voiceNote) { builder.flags = SignalServiceProtos.AttachmentPointer.Flags.VOICE_MESSAGE_VALUE }
+ if (attachment.voiceNote) { builder.flags = SessionProtos.AttachmentPointer.Flags.VOICE_MESSAGE_VALUE }
if (attachment.caption.isPresent) { builder.caption = attachment.caption.get() }
return builder.build()
@@ -106,7 +106,7 @@ class Attachment {
return (contentType != null && kind != null && size != null && sizeInBytes != null && url != null)
}
- fun toProto(): SignalServiceProtos.AttachmentPointer? {
+ fun toProto(): SessionProtos.AttachmentPointer? {
TODO("Not implemented")
}
diff --git a/app/src/main/java/org/session/libsession/messaging/messages/visible/LinkPreview.kt b/app/src/main/java/org/session/libsession/messaging/messages/visible/LinkPreview.kt
index 6187c9a9e3..263266e267 100644
--- a/app/src/main/java/org/session/libsession/messaging/messages/visible/LinkPreview.kt
+++ b/app/src/main/java/org/session/libsession/messaging/messages/visible/LinkPreview.kt
@@ -3,7 +3,7 @@ package org.session.libsession.messaging.messages.visible
import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview as SignalLinkPreiview
import org.session.libsignal.utilities.Log
-import org.session.libsignal.protos.SignalServiceProtos
+import org.session.protos.SessionProtos
class LinkPreview() {
var title: String? = null
@@ -17,7 +17,7 @@ class LinkPreview() {
companion object {
const val TAG = "LinkPreview"
- fun fromProto(proto: SignalServiceProtos.DataMessage.Preview): LinkPreview? {
+ fun fromProto(proto: SessionProtos.DataMessage.Preview): LinkPreview? {
val title = proto.title
val url = proto.url
return LinkPreview(title, url, null)
@@ -35,13 +35,13 @@ class LinkPreview() {
this.attachmentID = attachmentID
}
- fun toProto(): SignalServiceProtos.DataMessage.Preview? {
+ fun toProto(): SessionProtos.DataMessage.Preview? {
val url = url
if (url == null) {
Log.w(TAG, "Couldn't construct link preview proto from: $this")
return null
}
- val linkPreviewProto = SignalServiceProtos.DataMessage.Preview.newBuilder()
+ val linkPreviewProto = SessionProtos.DataMessage.Preview.newBuilder()
linkPreviewProto.url = url
title?.let { linkPreviewProto.title = it }
val database = MessagingModuleConfiguration.shared.messageDataProvider
diff --git a/app/src/main/java/org/session/libsession/messaging/messages/visible/OpenGroupInvitation.kt b/app/src/main/java/org/session/libsession/messaging/messages/visible/OpenGroupInvitation.kt
index 71faa53769..2f86f2d51d 100644
--- a/app/src/main/java/org/session/libsession/messaging/messages/visible/OpenGroupInvitation.kt
+++ b/app/src/main/java/org/session/libsession/messaging/messages/visible/OpenGroupInvitation.kt
@@ -1,6 +1,6 @@
package org.session.libsession.messaging.messages.visible
-import org.session.libsignal.protos.SignalServiceProtos
+import org.session.protos.SessionProtos
import org.session.libsignal.utilities.Log
class OpenGroupInvitation() {
@@ -14,7 +14,7 @@ class OpenGroupInvitation() {
companion object {
const val TAG = "OpenGroupInvitation"
- fun fromProto(proto: SignalServiceProtos.DataMessage.OpenGroupInvitation): OpenGroupInvitation {
+ fun fromProto(proto: SessionProtos.DataMessage.OpenGroupInvitation): OpenGroupInvitation {
return OpenGroupInvitation(proto.url, proto.name)
}
}
@@ -24,8 +24,8 @@ class OpenGroupInvitation() {
this.name = serverName
}
- fun toProto(): SignalServiceProtos.DataMessage.OpenGroupInvitation? {
- val openGroupInvitationProto = SignalServiceProtos.DataMessage.OpenGroupInvitation.newBuilder()
+ fun toProto(): SessionProtos.DataMessage.OpenGroupInvitation? {
+ val openGroupInvitationProto = SessionProtos.DataMessage.OpenGroupInvitation.newBuilder()
openGroupInvitationProto.url = url
openGroupInvitationProto.name = name
return try {
diff --git a/app/src/main/java/org/session/libsession/messaging/messages/visible/ParsedMessage.kt b/app/src/main/java/org/session/libsession/messaging/messages/visible/ParsedMessage.kt
deleted file mode 100644
index b0c5ced904..0000000000
--- a/app/src/main/java/org/session/libsession/messaging/messages/visible/ParsedMessage.kt
+++ /dev/null
@@ -1,11 +0,0 @@
-package org.session.libsession.messaging.messages.visible
-
-import org.session.libsession.messaging.jobs.MessageReceiveParameters
-import org.session.libsession.messaging.messages.Message
-import org.session.libsignal.protos.SignalServiceProtos
-
-data class ParsedMessage(
- val parameters: MessageReceiveParameters,
- val message: Message,
- val proto: SignalServiceProtos.Content
-)
\ No newline at end of file
diff --git a/app/src/main/java/org/session/libsession/messaging/messages/visible/Quote.kt b/app/src/main/java/org/session/libsession/messaging/messages/visible/Quote.kt
index 0b247c59cf..8b26ebc25e 100644
--- a/app/src/main/java/org/session/libsession/messaging/messages/visible/Quote.kt
+++ b/app/src/main/java/org/session/libsession/messaging/messages/visible/Quote.kt
@@ -3,7 +3,7 @@ package org.session.libsession.messaging.messages.visible
import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel as SignalQuote
-import org.session.libsignal.protos.SignalServiceProtos
+import org.session.protos.SessionProtos
import org.session.libsignal.utilities.Log
class Quote() {
@@ -17,7 +17,7 @@ class Quote() {
companion object {
const val TAG = "Quote"
- fun fromProto(proto: SignalServiceProtos.DataMessage.Quote): Quote? {
+ fun fromProto(proto: SessionProtos.DataMessage.Quote): Quote? {
val timestamp = proto.id
val publicKey = proto.author
val text = proto.text
@@ -38,14 +38,14 @@ class Quote() {
this.attachmentID = attachmentID
}
- fun toProto(): SignalServiceProtos.DataMessage.Quote? {
+ fun toProto(): SessionProtos.DataMessage.Quote? {
val timestamp = timestamp
val publicKey = publicKey
if (timestamp == null || publicKey == null) {
Log.w(TAG, "Couldn't construct quote proto from: $this")
return null
}
- val quoteProto = SignalServiceProtos.DataMessage.Quote.newBuilder()
+ val quoteProto = SessionProtos.DataMessage.Quote.newBuilder()
quoteProto.id = timestamp
quoteProto.author = publicKey
text?.let { quoteProto.text = it }
@@ -60,7 +60,7 @@ class Quote() {
}
}
- private fun addAttachmentsIfNeeded(quoteProto: SignalServiceProtos.DataMessage.Quote.Builder) {
+ private fun addAttachmentsIfNeeded(quoteProto: SessionProtos.DataMessage.Quote.Builder) {
val attachmentID = attachmentID ?: return Log.w(TAG, "Cannot add attachment with null attachmentID - bailing.")
val database = MessagingModuleConfiguration.shared.messageDataProvider
@@ -72,7 +72,7 @@ class Quote() {
return Log.w(TAG,"Cannot send a message before all associated attachments have been uploaded - bailing.")
}
- val quotedAttachmentProto = SignalServiceProtos.DataMessage.Quote.QuotedAttachment.newBuilder()
+ val quotedAttachmentProto = SessionProtos.DataMessage.Quote.QuotedAttachment.newBuilder()
quotedAttachmentProto.contentType = pointer.contentType
quotedAttachmentProto.fileName = pointer.filename
quotedAttachmentProto.thumbnail = Attachment.createAttachmentPointer(pointer)
diff --git a/app/src/main/java/org/session/libsession/messaging/messages/visible/Reaction.kt b/app/src/main/java/org/session/libsession/messaging/messages/visible/Reaction.kt
index 6e6327b248..2277fe8b1b 100644
--- a/app/src/main/java/org/session/libsession/messaging/messages/visible/Reaction.kt
+++ b/app/src/main/java/org/session/libsession/messaging/messages/visible/Reaction.kt
@@ -1,7 +1,7 @@
package org.session.libsession.messaging.messages.visible
-import org.session.libsignal.protos.SignalServiceProtos
-import org.session.libsignal.protos.SignalServiceProtos.DataMessage.Reaction.Action
+import org.session.protos.SessionProtos
+import org.session.protos.SessionProtos.DataMessage.Reaction.Action
import org.session.libsignal.utilities.Log
class Reaction() {
@@ -22,7 +22,7 @@ class Reaction() {
companion object {
const val TAG = "Quote"
- fun fromProto(proto: SignalServiceProtos.DataMessage.Reaction): Reaction {
+ fun fromProto(proto: SessionProtos.DataMessage.Reaction): Reaction {
val react = proto.action == Action.REACT
return Reaction(publicKey = proto.author, emoji = proto.emoji, react = react, timestamp = proto.id, count = 1)
}
@@ -42,7 +42,7 @@ class Reaction() {
this.index = index
}
- fun toProto(): SignalServiceProtos.DataMessage.Reaction? {
+ fun toProto(): SessionProtos.DataMessage.Reaction? {
val timestamp = timestamp
val publicKey = publicKey
val emoji = emoji
@@ -51,7 +51,7 @@ class Reaction() {
Log.w(TAG, "Couldn't construct reaction proto from: $this")
return null
}
- val reactionProto = SignalServiceProtos.DataMessage.Reaction.newBuilder()
+ val reactionProto = SessionProtos.DataMessage.Reaction.newBuilder()
reactionProto.id = timestamp
reactionProto.author = publicKey
reactionProto.emoji = emoji
diff --git a/app/src/main/java/org/session/libsession/messaging/messages/visible/VisibleMessage.kt b/app/src/main/java/org/session/libsession/messaging/messages/visible/VisibleMessage.kt
index 410d244885..26fe22bc9c 100644
--- a/app/src/main/java/org/session/libsession/messaging/messages/visible/VisibleMessage.kt
+++ b/app/src/main/java/org/session/libsession/messaging/messages/visible/VisibleMessage.kt
@@ -9,7 +9,7 @@ import org.session.libsession.database.MessageDataProvider
import org.session.libsession.messaging.messages.Message
import org.session.libsession.messaging.messages.copyExpiration
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
-import org.session.libsignal.protos.SignalServiceProtos
+import org.session.protos.SessionProtos
import org.session.libsignal.utilities.Log
import org.thoughtcrime.securesms.pro.toProMessageBitSetValue
import org.thoughtcrime.securesms.pro.toProProfileBitSetValue
@@ -30,12 +30,11 @@ data class VisibleMessage(
var reaction: Reaction? = null,
var hasMention: Boolean = false,
var blocksMessageRequests: Boolean = false,
- var proFeatures: Set = emptySet()
) : Message() {
// This empty constructor is needed for kryo serialization
@Keep
- constructor(): this(proFeatures = emptySet())
+ constructor(): this(text = null)
override val isSelfSendValid: Boolean = true
@@ -56,7 +55,7 @@ data class VisibleMessage(
companion object {
const val TAG = "VisibleMessage"
- fun fromProto(proto: SignalServiceProtos.Content): VisibleMessage? =
+ fun fromProto(proto: SessionProtos.Content): VisibleMessage? =
proto.dataMessage?.let { VisibleMessage().apply {
if (it.hasSyncTarget()) syncTarget = it.syncTarget
text = it.body
@@ -71,7 +70,7 @@ data class VisibleMessage(
}
protected override fun buildProto(
- builder: SignalServiceProtos.Content.Builder,
+ builder: SessionProtos.Content.Builder,
messageDataProvider: MessageDataProvider
) {
val dataMessage = builder.dataMessageBuilder
@@ -115,19 +114,6 @@ data class VisibleMessage(
if (syncTarget != null) {
dataMessage.syncTarget = syncTarget
}
-
- // Pro features
- if (proFeatures.any { it is ProMessageFeature }) {
- builder.proMessageBuilder.setMsgBitset(
- proFeatures.toProMessageBitSetValue()
- )
- }
-
- if (proFeatures.any { it is ProProfileFeature }) {
- builder.proMessageBuilder.setProfileBitset(
- proFeatures.toProProfileBitSetValue()
- )
- }
}
// endregion
diff --git a/app/src/main/java/org/session/libsession/messaging/open_groups/CommunityModule.kt b/app/src/main/java/org/session/libsession/messaging/open_groups/CommunityModule.kt
new file mode 100644
index 0000000000..0dedf4115f
--- /dev/null
+++ b/app/src/main/java/org/session/libsession/messaging/open_groups/CommunityModule.kt
@@ -0,0 +1,36 @@
+package org.session.libsession.messaging.open_groups
+
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import kotlinx.coroutines.CoroutineScope
+import org.session.libsession.messaging.open_groups.api.CommunityApiBatcher
+import org.session.libsession.messaging.open_groups.api.CommunityApiExecutor
+import org.session.libsession.messaging.open_groups.api.CommunityApiExecutorImpl
+import org.thoughtcrime.securesms.api.AutoRetryApiExecutor
+import org.thoughtcrime.securesms.api.batch.BatchApiExecutor
+import org.thoughtcrime.securesms.api.server.ServerApiExecutor
+import org.thoughtcrime.securesms.dependencies.ManagerScope
+import javax.inject.Named
+import javax.inject.Singleton
+
+@Module
+@InstallIn(SingletonComponent::class)
+class CommunityModule {
+ @Provides
+ @Singleton
+ fun provideCommunityApiExecutor(
+ executor: CommunityApiExecutorImpl,
+ batcher: CommunityApiBatcher,
+ @ManagerScope scope: CoroutineScope,
+ ): CommunityApiExecutor {
+ return AutoRetryApiExecutor(
+ actualExecutor = BatchApiExecutor(
+ actualExecutor = executor,
+ batcher = batcher,
+ scope = scope
+ )
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/session/libsession/messaging/open_groups/Endpoint.kt b/app/src/main/java/org/session/libsession/messaging/open_groups/Endpoint.kt
deleted file mode 100644
index e50fe293e1..0000000000
--- a/app/src/main/java/org/session/libsession/messaging/open_groups/Endpoint.kt
+++ /dev/null
@@ -1,81 +0,0 @@
-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 accountId: String) :
- Endpoint("room/$roomToken/all/$accountId")
-
- data class Reactors(val roomToken: String, val messageId: Long, val emoji: String):
- Endpoint("room/$roomToken/reactors/$messageId/$emoji")
-
- data class Reaction(val roomToken: String, val messageId: Long, val emoji: String):
- Endpoint("room/$roomToken/reaction/$messageId/$emoji")
-
- data class ReactionDelete(val roomToken: String, val messageId: Long, val emoji: String):
- Endpoint("room/$roomToken/reactions/$messageId/$emoji")
-
- // 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 accountId: String) : Endpoint("inbox/$accountId")
-
- object Outbox : Endpoint("outbox")
- data class OutboxSince(val id: Long) : Endpoint("outbox/since/$id")
-
- // Users
-
- data class UserBan(val accountId: String) : Endpoint("user/$accountId/ban")
- data class UserUnban(val accountId: String) : Endpoint("user/$accountId/unban")
- data class UserModerator(val accountId: String) : Endpoint("user/$accountId/moderator")
-
-}
diff --git a/app/src/main/java/org/session/libsession/messaging/open_groups/OfficialCommunityRepository.kt b/app/src/main/java/org/session/libsession/messaging/open_groups/OfficialCommunityRepository.kt
new file mode 100644
index 0000000000..8f17a7a2e1
--- /dev/null
+++ b/app/src/main/java/org/session/libsession/messaging/open_groups/OfficialCommunityRepository.kt
@@ -0,0 +1,123 @@
+package org.session.libsession.messaging.open_groups
+
+import android.util.Log
+import kotlinx.coroutines.CancellationException
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.async
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.flow.shareIn
+import org.session.libsession.database.StorageProtocol
+import org.session.libsession.messaging.open_groups.api.CommunityApiExecutor
+import org.session.libsession.messaging.open_groups.api.CommunityApiRequest
+import org.session.libsession.messaging.open_groups.api.CommunityFileDownloadApi
+import org.session.libsession.messaging.open_groups.api.GetCapsApi
+import org.session.libsession.messaging.open_groups.api.GetRoomsApi
+import org.session.libsession.messaging.open_groups.api.execute
+import org.thoughtcrime.securesms.dependencies.ManagerScope
+import javax.inject.Inject
+import javax.inject.Provider
+import javax.inject.Singleton
+
+@Singleton
+class OfficialCommunityRepository @Inject constructor(
+ storage: StorageProtocol,
+ communityApiExecutor: CommunityApiExecutor,
+ getRoomsApiFactory: GetRoomsApi.Factory,
+ getCapsApi: Provider,
+ communityFileDownloadApiFactory: CommunityFileDownloadApi.Factory,
+ @ManagerScope scope: CoroutineScope,
+) {
+ private val refreshTrigger = MutableSharedFlow()
+
+ @Suppress("OPT_IN_USAGE")
+ private val officialCommunitiesCache =
+ refreshTrigger
+ .onStart { emit(Unit) }
+ .flatMapLatest {
+ flow {
+ emit(runCatching {
+ coroutineScope {
+ val roomsDeferred = async {
+ communityApiExecutor.execute(
+ CommunityApiRequest(
+ serverBaseUrl = OFFICIAL_COMMUNITY_URL,
+ serverPubKey = OFFICIAL_COMMUNITY_X25519_PUB_KEY_HEX,
+ api = getRoomsApiFactory.create(requiresSigning = false)
+ )
+ )
+ }
+
+ val capsDeferred = async {
+ communityApiExecutor.execute(
+ CommunityApiRequest(
+ serverBaseUrl = OFFICIAL_COMMUNITY_URL,
+ serverPubKey = OFFICIAL_COMMUNITY_X25519_PUB_KEY_HEX,
+ api = getCapsApi.get(),
+ )
+ )
+ }
+
+
+ val rooms = roomsDeferred.await()
+ val roomAvatars = rooms.associate { room ->
+ room.token to room.imageId?.let { fileId ->
+ try {
+ communityApiExecutor.execute(
+ CommunityApiRequest(
+ serverBaseUrl = OFFICIAL_COMMUNITY_URL,
+ serverPubKey = OFFICIAL_COMMUNITY_X25519_PUB_KEY_HEX,
+ api = communityFileDownloadApiFactory.create(
+ room = room.token,
+ fileId = fileId,
+ requiresSigning = false,
+ )
+ )
+ ).toByteArraySlice()
+ } catch (e: CancellationException) {
+ throw e
+ } catch (e: Exception) {
+ Log.e(TAG, "Failed to download official community room avatar for room ${room.token}", e)
+ null
+ }
+ }
+ }
+
+ storage.setServerCapabilities(OFFICIAL_COMMUNITY_URL, capsDeferred.await().capabilities)
+
+ rooms.map { room ->
+ OpenGroupApi.DefaultGroup(
+ serverUrl = OFFICIAL_COMMUNITY_URL,
+ id = room.token,
+ name = room.name,
+ image = roomAvatars[room.token],
+ publicKey = OFFICIAL_COMMUNITY_X25519_PUB_KEY_HEX
+ )
+ }
+ }
+ })
+ }
+ }
+ .shareIn(scope, SharingStarted.Lazily)
+
+ suspend fun fetchOfficialCommunities(): List {
+ if (officialCommunitiesCache.replayCache.firstOrNull()?.isFailure == true) {
+ refreshTrigger.emit(Unit)
+ }
+
+ return officialCommunitiesCache.first().getOrThrow()
+ }
+
+ companion object {
+ const val OFFICIAL_COMMUNITY_URL = "https://open.getsession.org"
+ const val OFFICIAL_COMMUNITY_URL_INSECURE = "http://open.getsession.org"
+ private const val OFFICIAL_COMMUNITY_X25519_PUB_KEY_HEX = "a03c383cf63c3c4efe67acc52112a6dd734b3a946b9545f488aaa93da7991238"
+
+ private const val TAG = "OfficialCommunityRepo"
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupApi.kt b/app/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupApi.kt
index 3d650db223..2627546900 100644
--- a/app/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupApi.kt
+++ b/app/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupApi.kt
@@ -1,68 +1,19 @@
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 kotlinx.coroutines.flow.MutableSharedFlow
-import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
-import kotlinx.serialization.json.decodeFromStream
-import network.loki.messenger.libsession_util.ED25519
-import network.loki.messenger.libsession_util.Hash
-import network.loki.messenger.libsession_util.util.BlindKeyAPI
-import okhttp3.Headers.Companion.toHeaders
-import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
-import okhttp3.MediaType.Companion.toMediaType
-import okhttp3.RequestBody
-import org.session.libsession.messaging.MessagingModuleConfiguration
-import org.session.libsession.snode.OnionRequestAPI
-import org.session.libsession.snode.OnionResponse
-import org.session.libsession.snode.SnodeAPI
-import org.session.libsession.snode.utilities.await
-import org.session.libsignal.utilities.AccountId
-import org.session.libsignal.utilities.Base64.encodeBytes
+import org.session.libsession.utilities.serializable.InstantAsSecondDoubleSerializer
import org.session.libsignal.utilities.ByteArraySlice
-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 java.security.SecureRandom
-import java.util.concurrent.TimeUnit
+import java.time.Instant
object OpenGroupApi {
- val defaultRooms = MutableSharedFlow>(replay = 1)
- const val defaultServerPublicKey = "a03c383cf63c3c4efe67acc52112a6dd734b3a946b9545f488aaa93da7991238"
const val legacyServerIP = "116.203.70.33"
- const val legacyDefaultServer = "http://116.203.70.33" // TODO: migrate all references to use new value
- /** For migration purposes only, don't use this value in joining groups */
- const val httpDefaultServer = "http://open.getsession.org"
+ data class DefaultGroup(val serverUrl: String, val publicKey: String, val id: String, val name: String, val image: ByteArraySlice?) {
- const val defaultServer = "https://open.getsession.org"
-
- val pendingReactions = mutableListOf()
-
- 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: ByteArraySlice?) {
-
- val joinURL: String get() = "$defaultServer/$id?public_key=$defaultServerPublicKey"
+ val joinURL: String get() = "$serverUrl/$id?public_key=$publicKey"
}
@Serializable
@@ -74,7 +25,8 @@ object OpenGroupApi {
val infoUpdates: Int = 0,
@SerialName("message_sequence")
val messageSequence: Long = 0,
- val created: Double = 0.0,
+ @Serializable(with = InstantAsSecondDoubleSerializer::class)
+ val created: Instant? = null,
@SerialName("active_users")
val activeUsers: Int = 0,
@SerialName("active_users_cutoff")
@@ -117,30 +69,7 @@ object OpenGroupApi {
val pinnedBy: String = ""
)
- data class BatchRequestInfo(
- val request: BatchRequest,
- val endpoint: Endpoint,
- val queryParameters: Map = mapOf(),
- val responseType: TypeReference?
- )
-
- @JsonInclude(JsonInclude.Include.NON_NULL)
- data class BatchRequest(
- val method: HTTP.Verb,
- val path: String,
- val headers: Map = emptyMap(),
- val json: Map? = null,
- val b64: String? = null,
- val bytes: ByteArray? = null,
- )
-
- data class BatchResponse(
- val endpoint: Endpoint,
- val code: Int,
- val headers: Map,
- val body: T?
- )
-
+ @Serializable
data class Capabilities(
val capabilities: List = emptyList(),
val missing: List = emptyList()
@@ -176,31 +105,44 @@ object OpenGroupApi {
)
@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy::class)
+ @Serializable
data class DirectMessage(
val id: Long = 0,
val sender: String = "",
val recipient: String = "",
- val postedAt: Long = 0,
- val expiresAt: Long = 0,
+ @SerialName("posted_at")
+ @Serializable(with = InstantAsSecondDoubleSerializer::class)
+ val postedAt: Instant? = null,
+ @SerialName("expires_at")
+ @Serializable(with = InstantAsSecondDoubleSerializer::class)
+ val expiresAt: Instant? = null,
val message: String = "",
)
@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy::class)
+ @Serializable
data class Message(
val id : Long = 0,
+ @SerialName("session_id")
val sessionId: String = "",
- val posted: Double = 0.0,
- val edited: Long = 0,
+ @Serializable(with = InstantAsSecondDoubleSerializer::class)
+ val posted: Instant? = null,
+ @Serializable(with = InstantAsSecondDoubleSerializer::class)
+ val edited: Instant? = null,
val seqno: Long = 0,
val deleted: Boolean = false,
val whisper: Boolean = false,
+ @SerialName("whisper_mods")
val whisperMods: String = "",
+
+ @SerialName("whisper_to")
val whisperTo: String = "",
val data: String? = null,
val signature: String? = null,
val reactions: Map? = null,
)
+ @Serializable
data class Reaction(
val count: Long = 0,
val reactors: List = emptyList(),
@@ -215,527 +157,4 @@ object OpenGroupApi {
val added: Boolean
)
- @Serializable
- data class DeleteReactionResponse(
- @SerialName("seqno")
- val seqNo: Long,
- val removed: Boolean
- )
-
- data class DeleteAllReactionsResponse(
- val seqNo: Long,
- val removed: Boolean
- )
-
- data class PendingReaction(
- val server: String,
- val room: String,
- val messageId: Long,
- val emoji: String,
- val add: Boolean,
- var seqNo: Long? = null
- )
-
- @JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy::class)
- data class SendMessageRequest(
- val data: String? = null,
- val signature: String? = null,
- val whisperTo: List? = null,
- val whisperMods: Boolean? = null,
- val files: List? = 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 = mapOf(),
- val parameters: Any? = null,
- val headers: Map = mapOf(),
- val body: ByteArray? = null,
- /**
- * Always `true` under normal circumstances. You might want to disable
- * this when running over Lokinet.
- */
- val useOnionRouting: Boolean = true
- )
-
- private fun createBody(body: ByteArray?, parameters: Any?): RequestBody? {
- if (body != null) return RequestBody.create("application/octet-stream".toMediaType(), body)
- if (parameters == null) return null
- val parametersAsJSON = JsonUtil.toJson(parameters)
- return RequestBody.create("application/json".toMediaType(), parametersAsJSON)
- }
-
- private suspend fun getResponseBody(
- request: Request,
- signRequest: Boolean = true,
- serverPubKeyHex: String? = null
- ): ByteArraySlice {
- val response = send(request, signRequest = signRequest, serverPubKeyHex = serverPubKeyHex)
-
- return response.body ?: throw Error.ParsingFailed
- }
-
- private suspend fun getResponseBodyJson(
- request: Request,
- signRequest: Boolean = true,
- serverPubKeyHex: String? = null
- ): Map<*, *> {
- val response = send(request, signRequest = signRequest, serverPubKeyHex = serverPubKeyHex)
- return JsonUtil.fromJson(response.body, Map::class.java)
- }
-
- suspend fun getOrFetchServerCapabilities(server: String): List {
- val storage = MessagingModuleConfiguration.shared.storage
- val caps = storage.getServerCapabilities(server)
-
- if (caps != null) {
- return caps
- }
-
- val fetched = getCapabilities(server,
- serverPubKeyHex = defaultServerPublicKey.takeIf { server == defaultServer }
- )
-
- storage.setServerCapabilities(server, fetched.capabilities)
- return fetched.capabilities
- }
-
- private suspend fun send(request: Request, signRequest: Boolean, serverPubKeyHex: String? = null): OnionResponse {
- request.server.toHttpUrlOrNull() ?: throw(Error.InvalidURL)
-
- val urlBuilder = StringBuilder("${request.server}/${request.endpoint.value}")
- if (request.verb == GET && request.queryParameters.isNotEmpty()) {
- urlBuilder.append("?")
- for ((key, value) in request.queryParameters) {
- urlBuilder.append("$key=$value")
- }
- }
-
- val serverPublicKey = serverPubKeyHex
- ?: MessagingModuleConfiguration.shared.storage.getOpenGroupPublicKey(request.server)
- ?: throw Error.NoPublicKey
- val urlRequest = urlBuilder.toString()
-
- val headers = if (signRequest) {
- val serverCapabilities = getOrFetchServerCapabilities(request.server)
-
- val ed25519KeyPair = MessagingModuleConfiguration.shared.storage.getUserED25519KeyPair()
- ?: throw Error.NoEd25519KeyPair
-
- val headers = request.headers.toMutableMap()
- val nonce = ByteArray(16).also { SecureRandom().nextBytes(it) }
- val timestamp = TimeUnit.MILLISECONDS.toSeconds(SnodeAPI.nowWithOffset)
- val bodyHash = if (request.parameters != null) {
- val parameterBytes = JsonUtil.toJson(request.parameters).toByteArray()
- Hash.hash64(parameterBytes)
- } else if (request.body != null) {
- Hash.hash64(request.body)
- } else {
- byteArrayOf()
- }
-
- val messageBytes = Hex.fromStringCondensed(serverPublicKey)
- .plus(nonce)
- .plus("$timestamp".toByteArray(Charsets.US_ASCII))
- .plus(request.verb.rawValue.toByteArray())
- .plus("/${request.endpoint.value}".toByteArray())
- .plus(bodyHash)
-
- val signature: ByteArray
- val pubKey: String
-
- if (serverCapabilities.isEmpty() || serverCapabilities.contains(Capability.BLIND.name.lowercase())) {
- pubKey = AccountId(
- IdPrefix.BLINDED,
- BlindKeyAPI.blind15KeyPair(
- ed25519SecretKey = ed25519KeyPair.secretKey.data,
- serverPubKey = Hex.fromStringCondensed(serverPublicKey)
- ).pubKey.data
- ).hexString
-
- try {
- signature = BlindKeyAPI.blind15Sign(
- ed25519SecretKey = ed25519KeyPair.secretKey.data,
- serverPubKey = serverPublicKey,
- message = messageBytes
- )
- } catch (e: Exception) {
- throw Error.SigningFailed
- }
- } else {
- pubKey = AccountId(
- IdPrefix.UN_BLINDED,
- ed25519KeyPair.pubKey.data
- ).hexString
-
- signature = ED25519.sign(
- ed25519PrivateKey = ed25519KeyPair.secretKey.data,
- message = messageBytes
- )
- }
- headers["X-SOGS-Nonce"] = encodeBytes(nonce)
- headers["X-SOGS-Timestamp"] = "$timestamp"
- headers["X-SOGS-Pubkey"] = pubKey
- headers["X-SOGS-Signature"] = encodeBytes(signature)
- headers
- } else {
- request.headers
- }
-
- val requestBuilder = okhttp3.Request.Builder()
- .url(urlRequest)
- .headers(headers.toHeaders())
- when (request.verb) {
- GET -> requestBuilder.get()
- PUT -> requestBuilder.put(createBody(request.body, request.parameters)!!)
- POST -> requestBuilder.post(createBody(request.body, request.parameters)!!)
- DELETE -> requestBuilder.delete(createBody(request.body, request.parameters))
- }
- if (!request.room.isNullOrEmpty()) {
- requestBuilder.header("Room", request.room)
- }
- return if (request.useOnionRouting) {
- OnionRequestAPI.sendOnionRequest(requestBuilder.build(), request.server, serverPublicKey).fail { e ->
- when (e) {
- // No need for the stack trace for HTTP errors
- is HTTP.HTTPRequestFailedException -> Log.e("SOGS", "Failed onion request: ${e.message}")
- else -> Log.e("SOGS", "Failed onion request", e)
- }
- }.await()
- } else {
- throw IllegalStateException("It's currently not allowed to send non onion routed requests.")
- }
- }
-
- suspend fun downloadOpenGroupProfilePicture(
- server: String,
- roomID: String,
- imageId: String,
- signRequest: Boolean = true,
- serverPubKeyHex: String? = null,
- ): ByteArraySlice {
- val request = Request(
- verb = GET,
- room = roomID,
- server = server,
- endpoint = Endpoint.RoomFileIndividual(roomID, imageId)
- )
- return getResponseBody(request, signRequest = signRequest, serverPubKeyHex = serverPubKeyHex)
- }
-
- // region Upload/Download
- suspend fun upload(file: ByteArray, room: String, server: String): String {
- val request = Request(
- verb = POST,
- room = room,
- server = server,
- endpoint = Endpoint.RoomFile(room),
- body = file,
- headers = mapOf(
- "Content-Disposition" to "attachment",
- "Content-Type" to "application/octet-stream"
- )
- )
- val json = getResponseBodyJson(request, signRequest = true)
- return json["id"]?.toString() ?: throw Error.ParsingFailed
- }
-
- suspend fun download(fileId: String, room: String, server: String): ByteArraySlice {
- val request = Request(
- verb = GET,
- room = room,
- server = server,
- endpoint = Endpoint.RoomFileIndividual(room, fileId)
- )
- return getResponseBody(request, signRequest = true)
- }
- // endregion
-
- // region Sending
- suspend fun sendMessage(
- message: OpenGroupMessage,
- room: String,
- server: String,
- whisperTo: List? = null,
- whisperMods: Boolean? = null,
- fileIds: List? = null
- ): OpenGroupMessage {
- val signedMessage = message.sign(server) ?:throw Error.SigningFailed
- val parameters = signedMessage.toJSON().toMutableMap()
-
- // add file IDs if there are any (from attachments)
- if (!fileIds.isNullOrEmpty()) {
- parameters += "files" to fileIds
- }
-
- val request = Request(
- verb = POST,
- room = room,
- server = server,
- endpoint = Endpoint.RoomMessage(room),
- parameters = parameters
- )
- val json = getResponseBodyJson(request, signRequest = true)
- @Suppress("UNCHECKED_CAST") val rawMessage = json as? Map
- ?: throw Error.ParsingFailed
- val result = OpenGroupMessage.fromJSON(rawMessage) ?: throw Error.ParsingFailed
- val storage = MessagingModuleConfiguration.shared.storage
- storage.addReceivedMessageTimestamp(result.sentTimestamp)
- return result
- }
- // endregion
-
- @OptIn(ExperimentalSerializationApi::class)
- suspend fun addReaction(room: String, server: String, messageId: Long, emoji: String): AddReactionResponse {
- val request = Request(
- verb = PUT,
- room = room,
- server = server,
- endpoint = Endpoint.Reaction(room, messageId, emoji),
- parameters = emptyMap()
- )
-
- val response = getResponseBody(request, signRequest = true)
- val reaction: AddReactionResponse = response.inputStream().use( MessagingModuleConfiguration.shared.json::decodeFromStream)
-
- return reaction
- }
-
- @OptIn(ExperimentalSerializationApi::class)
- suspend fun deleteReaction(room: String, server: String, messageId: Long, emoji: String): DeleteReactionResponse {
- val request = Request(
- verb = DELETE,
- room = room,
- server = server,
- endpoint = Endpoint.Reaction(room, messageId, emoji)
- )
-
- val response = getResponseBody(request, signRequest = true)
- val reaction: DeleteReactionResponse = MessagingModuleConfiguration.shared.json.decodeFromStream(response.inputStream())
-
- return reaction
- }
-
- suspend fun deleteAllReactions(room: String, server: String, messageId: Long, emoji: String): DeleteAllReactionsResponse {
- val request = Request(
- verb = DELETE,
- room = room,
- server = server,
- endpoint = Endpoint.ReactionDelete(room, messageId, emoji)
- )
- val response = getResponseBody(request, signRequest = true)
- return JsonUtil.fromJson(response, DeleteAllReactionsResponse::class.java)
- }
- // endregion
-
- // region Message Deletion
- suspend fun deleteMessage(serverID: Long, room: String, server: String) {
- val request = Request(verb = DELETE, room = room, server = server, endpoint = Endpoint.RoomMessageIndividual(room, serverID))
- send(request, signRequest = true)
- Log.d("Loki", "Message deletion successful.")
- }
-
- // endregion
-
- // region Moderation
- suspend fun ban(publicKey: String, room: String, server: String) {
- val parameters = mapOf("rooms" to listOf(room))
- val request = Request(
- verb = POST,
- room = room,
- server = server,
- endpoint = Endpoint.UserBan(publicKey),
- parameters = parameters
- )
-
- send(request, signRequest = true)
- Log.d("Loki", "Banned user: $publicKey from: $server.$room.")
- }
-
- suspend fun banAndDeleteAll(publicKey: String, room: String, server: String) {
-
- val requests = mutableListOf>(
- // Ban request
- BatchRequestInfo(
- request = BatchRequest(
- method = POST,
- path = "/user/$publicKey/ban",
- json = mapOf("rooms" to listOf(room))
- ),
- endpoint = Endpoint.UserBan(publicKey),
- responseType = object: TypeReference(){}
- ),
- // Delete request
- BatchRequestInfo(
- request = BatchRequest(DELETE, "/room/$room/all/$publicKey"),
- endpoint = Endpoint.RoomDeleteMessages(room, publicKey),
- responseType = object: TypeReference(){}
- )
- )
- sequentialBatch(server, requests)
- Log.d("Loki", "Banned user: $publicKey from: $server.$room.")
- }
-
- suspend fun unban(publicKey: String, room: String, server: String) {
- val request = Request(verb = DELETE, room = room, server = server, endpoint = Endpoint.UserUnban(publicKey))
- send(request, signRequest = true)
- Log.d("Loki", "Unbanned user: $publicKey from: $server.$room")
- }
- // endregion
-
- // region General
-
- suspend fun parallelBatch(
- server: String,
- requests: MutableList>
- ): List> {
- val request = Request(
- verb = POST,
- room = null,
- server = server,
- endpoint = Endpoint.Batch,
- parameters = requests.map { it.request }
- )
- return getBatchResponseJson(request, requests)
- }
-
- private suspend fun sequentialBatch(
- server: String,
- requests: MutableList>
- ): List> {
- val request = Request(
- verb = POST,
- room = null,
- server = server,
- endpoint = Endpoint.Sequence,
- parameters = requests.map { it.request }
- )
- return getBatchResponseJson(request, requests)
- }
-
- private suspend fun getBatchResponseJson(
- request: Request,
- requests: MutableList>,
- signRequest: Boolean = true
- ): List> {
- val batch = getResponseBody(request, signRequest = signRequest)
- val results = JsonUtil.fromJson(batch, List::class.java) ?: throw Error.ParsingFailed
- return 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,
- body = if (code in 200..299) {
- requests[idx].responseType?.let { respType ->
- JsonUtil.toJson(response["body"]).takeIf { it != "[]" }?.let {
- JsonUtil.fromJson(it, respType)
- } ?: response["body"]
- }
-
- } else null
- )
- }
- }
-
- suspend fun getDefaultServerCapabilities(): List {
- return getOrFetchServerCapabilities(defaultServer)
- }
-
- suspend fun getDefaultRoomsIfNeeded(): List {
- val groups = getAllRooms()
-
- 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(
- server = defaultServer,
- roomID = group.token,
- imageId = it,
- signRequest = false,
- serverPubKeyHex = defaultServerPublicKey,
- ) }
- }
-
- return groups.map { group ->
- val image = try {
- images[group.token]!!
- } catch (e: Exception) {
- // No image or image failed to download
- null
- }
- DefaultGroup(group.token, group.name, image)
- }.also(defaultRooms::tryEmit)
- }
-
- private suspend fun getAllRooms(): List {
- val request = Request(
- verb = GET,
- room = null,
- server = defaultServer,
- endpoint = Endpoint.Rooms
- )
- val response = getResponseBody(
- request = request,
- signRequest = false,
- serverPubKeyHex = defaultServerPublicKey
- )
-
- return MessagingModuleConfiguration.shared.json
- .decodeFromStream>(response.inputStream()).toList()
- }
-
- suspend fun getCapabilities(server: String, serverPubKeyHex: String? = null): Capabilities {
- val request = Request(verb = GET, room = null, server = server, endpoint = Endpoint.Capabilities)
- val response = getResponseBody(request, signRequest = false, serverPubKeyHex)
- return JsonUtil.fromJson(response, Capabilities::class.java)
- }
-
- suspend fun sendDirectMessage(message: String, blindedAccountId: String, server: String): DirectMessage {
- val request = Request(
- verb = POST,
- room = null,
- server = server,
- endpoint = Endpoint.InboxFor(blindedAccountId),
- parameters = mapOf("message" to message)
- )
- val response = getResponseBody(request)
- return JsonUtil.fromJson(response, DirectMessage::class.java)
- }
-
- suspend fun deleteAllInboxMessages(server: String): Map<*, *> {
- val request = Request(
- verb = DELETE,
- room = null,
- server = server,
- endpoint = Endpoint.Inbox
- )
- val response = getResponseBody(request)
- return JsonUtil.fromJson(response, Map::class.java)
- }
-
- // endregion
}
\ No newline at end of file
diff --git a/app/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupMessage.kt b/app/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupMessage.kt
index 93fac63e91..880fd34591 100644
--- a/app/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupMessage.kt
+++ b/app/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupMessage.kt
@@ -1,16 +1,8 @@
package org.session.libsession.messaging.open_groups
-import network.loki.messenger.libsession_util.ED25519
-import network.loki.messenger.libsession_util.util.BlindKeyAPI
-import org.session.libsession.messaging.MessagingModuleConfiguration
-import org.session.libsession.messaging.open_groups.OpenGroupApi.Capability
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.removingIdPrefixIfNeeded
-import org.session.libsignal.utilities.toHexString
+import org.session.protos.SessionProtos
data class OpenGroupMessage(
val serverID: Long? = null,
@@ -27,66 +19,8 @@ data class OpenGroupMessage(
val base64EncodedSignature: String? = null,
val reactions: Map? = null
) {
-
- companion object {
- fun fromJSON(json: Map): 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(server: String): OpenGroupMessage? {
- if (base64EncodedData.isNullOrEmpty()) return null
- val userEdKeyPair = MessagingModuleConfiguration.shared.storage.getUserED25519KeyPair() ?: return null
- val communityServerPubKey = MessagingModuleConfiguration.shared.storage.getOpenGroupPublicKey(server) ?: return null
- val serverCapabilities = MessagingModuleConfiguration.shared.storage.getServerCapabilities(server)
- val signature = if (serverCapabilities?.contains(Capability.BLIND.name.lowercase()) == true) {
- runCatching {
- BlindKeyAPI.blind15Sign(
- ed25519SecretKey = userEdKeyPair.secretKey.data,
- serverPubKey = communityServerPubKey,
- message = decode(base64EncodedData)
- )
- }.onFailure {
- Log.e("OpenGroupMessage", "Failed to sign message with blind key", it)
- }.getOrNull() ?: return null
- }
- else {
- val x25519PublicKey = MessagingModuleConfiguration.shared.storage.getUserX25519KeyPair().pubKey.data
- if (sender != x25519PublicKey.toHexString() && !userEdKeyPair.pubKey.data.toHexString().equals(sender?.removingIdPrefixIfNeeded(), true)) return null
- try {
- ED25519.sign(
- ed25519PrivateKey = userEdKeyPair.secretKey.data,
- message = 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 {
- 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 {
+ fun toProto(): SessionProtos.Content {
val data = decode(base64EncodedData).let(PushTransportDetails::getStrippedPaddingMessageBody)
- return SignalServiceProtos.Content.parseFrom(data)
+ return SessionProtos.Content.parseFrom(data)
}
}
\ No newline at end of file
diff --git a/app/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupUtils.kt b/app/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupUtils.kt
index ebf2965b1a..53f2a90ef8 100644
--- a/app/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupUtils.kt
+++ b/app/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupUtils.kt
@@ -1,9 +1,9 @@
package org.session.libsession.messaging.open_groups
fun String.migrateLegacyServerUrl() = if (contains(OpenGroupApi.legacyServerIP)) {
- OpenGroupApi.defaultServer
-} else if (contains(OpenGroupApi.httpDefaultServer)) {
- OpenGroupApi.defaultServer
+ OfficialCommunityRepository.OFFICIAL_COMMUNITY_URL
+} else if (contains(OfficialCommunityRepository.OFFICIAL_COMMUNITY_URL_INSECURE)) {
+ OfficialCommunityRepository.OFFICIAL_COMMUNITY_URL
} else {
this
}
\ No newline at end of file
diff --git a/app/src/main/java/org/session/libsession/messaging/open_groups/api/AddReactionApi.kt b/app/src/main/java/org/session/libsession/messaging/open_groups/api/AddReactionApi.kt
new file mode 100644
index 0000000000..355b3627b2
--- /dev/null
+++ b/app/src/main/java/org/session/libsession/messaging/open_groups/api/AddReactionApi.kt
@@ -0,0 +1,57 @@
+package org.session.libsession.messaging.open_groups.api
+
+import android.net.Uri
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import kotlinx.serialization.json.JsonObject
+import kotlinx.serialization.json.decodeFromStream
+import okhttp3.MediaType
+import org.session.libsession.messaging.open_groups.OpenGroupApi
+import org.thoughtcrime.securesms.api.ApiExecutorContext
+import org.thoughtcrime.securesms.api.http.HttpBody
+import org.thoughtcrime.securesms.api.http.HttpResponse
+
+class AddReactionApi @AssistedInject constructor(
+ @Assisted("room") override val room: String,
+ @Assisted val messageId: Long,
+ @Assisted val emoji: String,
+ deps: CommunityApiDependencies,
+) : CommunityApi(deps) {
+ override val requiresSigning: Boolean get() = true
+ override val httpMethod: String get() = "PUT"
+ override val httpEndpoint: String = buildString {
+ append("/room/")
+ append(Uri.encode(room))
+ append("/reaction/")
+ append(messageId)
+ append("/")
+ append(Uri.encode(emoji))
+ }
+
+ override fun buildRequestBody(
+ serverBaseUrl: String,
+ x25519PubKeyHex: String
+ ): Pair {
+ return buildJsonRequestBody(JsonObject(emptyMap()))
+ }
+
+ override suspend fun handleSuccessResponse(
+ executorContext: ApiExecutorContext,
+ baseUrl: String,
+ response: HttpResponse
+ ): OpenGroupApi.AddReactionResponse {
+ @Suppress("OPT_IN_USAGE")
+ return response.body.asInputStream()
+ .use(json::decodeFromStream)
+ }
+
+ @AssistedFactory
+ interface Factory {
+ fun create(
+ @Assisted("room") room: String,
+ messageId: Long,
+ emoji: String
+ ): AddReactionApi
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/session/libsession/messaging/open_groups/api/BanUserApi.kt b/app/src/main/java/org/session/libsession/messaging/open_groups/api/BanUserApi.kt
new file mode 100644
index 0000000000..ab5e7b9394
--- /dev/null
+++ b/app/src/main/java/org/session/libsession/messaging/open_groups/api/BanUserApi.kt
@@ -0,0 +1,47 @@
+package org.session.libsession.messaging.open_groups.api
+
+import android.net.Uri
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import kotlinx.serialization.Serializable
+import org.thoughtcrime.securesms.api.ApiExecutorContext
+import org.thoughtcrime.securesms.api.http.HttpBody
+import org.thoughtcrime.securesms.api.http.HttpResponse
+import okhttp3.MediaType
+
+class BanUserApi @AssistedInject constructor(
+ @Assisted("user") private val userToBan: String,
+ @Assisted override val room: String,
+ deps: CommunityApiDependencies,
+) : CommunityApi(deps) {
+ override val requiresSigning: Boolean get() = true
+ override val httpMethod: String get() = "POST"
+ override val httpEndpoint: String =
+ "/user/${Uri.encode(userToBan)}/ban"
+
+ @Serializable
+ private data class BanBody(val rooms: List)
+
+ override fun buildRequestBody(
+ serverBaseUrl: String,
+ x25519PubKeyHex: String
+ ): Pair {
+ // JSON body {"rooms":[room]}
+ return buildJsonRequestBody(BanBody(rooms = listOf(room)))
+ }
+
+ override suspend fun handleSuccessResponse(
+ executorContext: ApiExecutorContext,
+ baseUrl: String,
+ response: HttpResponse
+ ) = Unit
+
+ @AssistedFactory
+ interface Factory {
+ fun create(
+ @Assisted("user") userToBan: String,
+ room: String
+ ): BanUserApi
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/session/libsession/messaging/open_groups/api/BatchApi.kt b/app/src/main/java/org/session/libsession/messaging/open_groups/api/BatchApi.kt
new file mode 100644
index 0000000000..73f480b96e
--- /dev/null
+++ b/app/src/main/java/org/session/libsession/messaging/open_groups/api/BatchApi.kt
@@ -0,0 +1,111 @@
+package org.session.libsession.messaging.open_groups.api
+
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import kotlinx.serialization.ExperimentalSerializationApi
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.json.Json
+import kotlinx.serialization.json.JsonElement
+import kotlinx.serialization.json.decodeFromStream
+import okhttp3.MediaType
+import org.session.libsignal.utilities.Base64
+import org.thoughtcrime.securesms.api.ApiExecutorContext
+import org.thoughtcrime.securesms.api.http.HttpBody
+import org.thoughtcrime.securesms.api.http.HttpRequest
+import org.thoughtcrime.securesms.api.http.HttpResponse
+
+class BatchApi @AssistedInject constructor(
+ @Assisted private val items: List,
+ deps: CommunityApiDependencies,
+) : CommunityApi>(deps) {
+ override val room: String? get() = null
+ override val requiresSigning: Boolean get() = true
+ override val httpMethod: String get() = "POST"
+
+ @OptIn(ExperimentalSerializationApi::class)
+ override suspend fun handleSuccessResponse(
+ executorContext: ApiExecutorContext,
+ baseUrl: String,
+ response: HttpResponse
+ ): List {
+ return response.body.asInputStream().use(json::decodeFromStream)
+ }
+
+ override val httpEndpoint: String
+ get() = "/batch"
+
+ override fun buildRequestBody(
+ serverBaseUrl: String,
+ x25519PubKeyHex: String
+ ): Pair {
+ return buildJsonRequestBody(items)
+ }
+
+ @Serializable
+ class BatchRequestItem(
+ val method: String,
+ val path: String,
+ val headers: Map,
+ val json: JsonElement? = null,
+ val b64: String? = null
+ ) {
+ init {
+ check(json == null || b64 == null) { "Only one of 'json' or 'b64' can be set" }
+ }
+
+ constructor(httpRequest: HttpRequest, json: Json) : this(
+ method = httpRequest.method,
+ // Path includes query parameters
+ path = buildString {
+ append(httpRequest.url.encodedPath)
+ if (httpRequest.url.encodedQuery != null) {
+ append("?")
+ append(httpRequest.url.encodedQuery)
+ }
+ },
+ headers = httpRequest.headers.toMap(),
+ json = if (httpRequest.isJsonBody) {
+ @Suppress("OPT_IN_USAGE")
+ httpRequest.body?.asInputStream()?.use(json::decodeFromStream)
+ } else {
+ null
+ },
+ b64 = if (!httpRequest.isJsonBody) {
+ httpRequest.body?.toBytes()?.let(Base64::encodeBytes)
+ } else {
+ null
+ }
+ )
+ }
+
+
+ @Serializable
+ class BatchResponseItem(
+ val code: Int,
+ val headers: Map,
+ val body: JsonElement?
+ ) {
+ fun toHttpResponse(json: Json): HttpResponse {
+ return HttpResponse(
+ statusCode = code,
+ headers = headers,
+ body = body?.let {
+ HttpBody.Text(json.encodeToString(it))
+ } ?: HttpBody.empty()
+ )
+ }
+ }
+
+ @AssistedFactory
+ interface Factory {
+ fun create(items: List): BatchApi
+ }
+
+ companion object {
+ private val HttpRequest.isJsonBody: Boolean
+ get() {
+ return getHeader("Content-Type")?.startsWith("application/json", ignoreCase = true) == true
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/session/libsession/messaging/open_groups/api/CommunityApi.kt b/app/src/main/java/org/session/libsession/messaging/open_groups/api/CommunityApi.kt
new file mode 100644
index 0000000000..ec515c763a
--- /dev/null
+++ b/app/src/main/java/org/session/libsession/messaging/open_groups/api/CommunityApi.kt
@@ -0,0 +1,173 @@
+package org.session.libsession.messaging.open_groups.api
+
+import androidx.collection.arrayMapOf
+import kotlinx.serialization.json.Json
+import network.loki.messenger.libsession_util.ED25519
+import network.loki.messenger.libsession_util.Hash
+import network.loki.messenger.libsession_util.util.BlindKeyAPI
+import okhttp3.HttpUrl.Companion.toHttpUrl
+import okhttp3.MediaType
+import okhttp3.MediaType.Companion.toMediaType
+import org.session.libsession.database.StorageProtocol
+import org.session.libsession.messaging.MessagingModuleConfiguration
+import org.session.libsession.messaging.open_groups.OpenGroupApi
+import org.thoughtcrime.securesms.api.server.ServerApiErrorManager
+import org.session.libsession.network.SnodeClock
+import org.session.libsession.network.model.FailureDecision
+import org.session.libsession.utilities.ConfigFactoryProtocol
+import org.session.libsignal.utilities.AccountId
+import org.session.libsignal.utilities.Base64
+import org.session.libsignal.utilities.Hex
+import org.session.libsignal.utilities.IdPrefix
+import org.thoughtcrime.securesms.api.ApiExecutorContext
+import org.thoughtcrime.securesms.api.error.ErrorWithFailureDecision
+import org.thoughtcrime.securesms.api.http.HttpBody
+import org.thoughtcrime.securesms.api.http.HttpRequest
+import org.thoughtcrime.securesms.api.http.HttpResponse
+import org.thoughtcrime.securesms.api.server.ServerApi
+import org.thoughtcrime.securesms.auth.LoginStateRepository
+import java.io.ByteArrayOutputStream
+import java.security.SecureRandom
+import javax.inject.Inject
+import javax.inject.Provider
+
+abstract class CommunityApi(
+ errorManager: ServerApiErrorManager,
+ protected val json: Json,
+ protected val loginStateRepository: Provider,
+ protected val configFactory: ConfigFactoryProtocol,
+ protected val storage: StorageProtocol,
+ protected val clock: SnodeClock,
+) : ServerApi(
+ errorManager = errorManager,
+) {
+ constructor(deps: CommunityApiDependencies) : this(
+ errorManager = deps.errorManager,
+ json = deps.json,
+ loginStateRepository = deps.loginStateRepository,
+ configFactory = deps.configFactory,
+ storage = deps.storage,
+ clock = deps.clock,
+ )
+
+
+ // The room ID associated with this API call
+ abstract val room: String?
+
+ abstract val requiresSigning: Boolean
+
+ abstract val httpMethod: String
+ abstract val httpEndpoint: String
+
+ open fun buildRequestBody(serverBaseUrl: String, x25519PubKeyHex: String): Pair? = null
+
+ protected inline fun buildJsonRequestBody(obj: T): Pair {
+ return "application/json".toMediaType() to HttpBody.Text(json.encodeToString(obj))
+ }
+
+ override fun buildRequest(baseUrl: String, x25519PubKeyHex: String): HttpRequest {
+ val builtBody = buildRequestBody(baseUrl, x25519PubKeyHex)
+ val headers = arrayMapOf()
+
+ val httpBody = if (builtBody != null) {
+ headers["Content-Type"] = builtBody.first.toString()
+ headers["Content-Length"] = builtBody.second.byteLength.toString()
+ builtBody.second
+ } else {
+ null
+ }
+
+ room?.let {
+ headers["Room"] = it
+ }
+
+ if (requiresSigning) {
+ val loggedInState = loginStateRepository.get().requireLoggedInState()
+ val bodyHash = builtBody?.let { Hash.hash64(it.second.toBytes()) } ?: byteArrayOf()
+ val nonce = ByteArray(16).also(SecureRandom()::nextBytes)
+ val timestamp = clock.currentTimeSeconds().toString()
+
+ val messageToSign = ByteArrayOutputStream().use { stream ->
+ stream.write(Hex.fromStringCondensed(x25519PubKeyHex))
+ stream.write(nonce)
+
+ stream.writer().use { w ->
+ w.write(timestamp)
+ w.write(httpMethod)
+ w.write(httpEndpoint)
+ }
+
+ stream.write(bodyHash)
+ stream.toByteArray()
+ }
+
+ val pubKeyHexUsedToSign: String
+ val signature: ByteArray
+
+ if (storage.getServerCapabilities(baseUrl)
+ ?.contains(OpenGroupApi.Capability.BLIND.name.lowercase()) == true) {
+ pubKeyHexUsedToSign = AccountId(
+ IdPrefix.BLINDED,
+ loggedInState.getBlindedKeyPair(baseUrl, x25519PubKeyHex).pubKey.data
+ ).hexString
+
+ signature = BlindKeyAPI.blind15Sign(
+ ed25519SecretKey = loggedInState.accountEd25519KeyPair.secretKey.data,
+ serverPubKey = x25519PubKeyHex,
+ message = messageToSign
+ )
+ } else {
+ pubKeyHexUsedToSign = AccountId(
+ IdPrefix.UN_BLINDED,
+ loggedInState.accountEd25519KeyPair.pubKey.data
+ ).hexString
+
+ signature = ED25519.sign(
+ ed25519PrivateKey = loggedInState.accountEd25519KeyPair.secretKey.data,
+ message = messageToSign
+ )
+ }
+
+ headers["X-SOGS-Nonce"] = Base64.encodeBytes(nonce)
+ headers["X-SOGS-Timestamp"] = timestamp
+ headers["X-SOGS-PubKey"] = pubKeyHexUsedToSign
+ headers["X-SOGS-Signature"] = Base64.encodeBytes(signature)
+ }
+
+ return HttpRequest(
+ url = requireNotNull(baseUrl.toHttpUrl().resolve(httpEndpoint)) {
+ "Could not resolve URL for endpoint $httpEndpoint with base $baseUrl"
+ },
+ method = httpMethod,
+ headers = headers,
+ body = httpBody,
+ )
+ }
+
+
+ override suspend fun handleErrorResponse(
+ executorContext: ApiExecutorContext,
+ baseUrl: String,
+ response: HttpResponse
+ ): Nothing {
+ if (response.statusCode == 400 &&
+ response.body.toText()?.contains("Invalid authentication: this server requires the use of blinded ids", ignoreCase = true) == true) {
+ storage.clearServerCapabilities(baseUrl)
+ throw ErrorWithFailureDecision(
+ cause = RuntimeException("Server requires blinded ids"),
+ failureDecision = FailureDecision.Retry,
+ )
+ }
+
+ super.handleErrorResponse(executorContext, baseUrl, response)
+ }
+
+ class CommunityApiDependencies @Inject constructor(
+ val errorManager: ServerApiErrorManager,
+ val json: Json,
+ val loginStateRepository: Provider,
+ val configFactory: ConfigFactoryProtocol,
+ val storage: StorageProtocol,
+ val clock: SnodeClock,
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/session/libsession/messaging/open_groups/api/CommunityApiBatcher.kt b/app/src/main/java/org/session/libsession/messaging/open_groups/api/CommunityApiBatcher.kt
new file mode 100644
index 0000000000..523be59bc3
--- /dev/null
+++ b/app/src/main/java/org/session/libsession/messaging/open_groups/api/CommunityApiBatcher.kt
@@ -0,0 +1,82 @@
+package org.session.libsession.messaging.open_groups.api
+
+import kotlinx.serialization.json.Json
+import org.session.libsession.database.StorageProtocol
+import org.thoughtcrime.securesms.api.ApiExecutorContext
+import org.thoughtcrime.securesms.api.batch.Batcher
+import javax.inject.Inject
+
+class CommunityApiBatcher @Inject constructor(
+ private val batchApiFactory: BatchApi.Factory,
+ private val json: Json,
+ private val storage: StorageProtocol,
+) : Batcher, Any, BatchApi.BatchRequestItem> {
+ override fun transformRequestForBatching(
+ ctx: ApiExecutorContext,
+ req: CommunityApiRequest<*>
+ ): BatchApi.BatchRequestItem {
+ val pubKey = requireNotNull(req.serverPubKey ?: storage.getOpenGroupPublicKey(req.serverBaseUrl)) {
+ "No stored x25519 public key for server ${req.serverBaseUrl}"
+ }
+
+ return BatchApi.BatchRequestItem(
+ httpRequest = req.api.buildRequest(
+ baseUrl = req.serverBaseUrl,
+ x25519PubKeyHex = pubKey
+ ),
+ json = json
+ )
+ }
+
+ override fun constructBatchRequest(
+ firstRequest: CommunityApiRequest<*>,
+ intermediateRequests: List
+ ): CommunityApiRequest<*> {
+ return CommunityApiRequest(
+ serverBaseUrl = firstRequest.serverBaseUrl,
+ serverPubKey = firstRequest.serverPubKey,
+ api = batchApiFactory.create(intermediateRequests)
+ )
+ }
+
+ override fun batchKey(req: CommunityApiRequest<*>): Any? {
+ // Shouldn't batch the batch requests themselves
+ if (req.api is BatchApi) {
+ return null
+ }
+
+ // Shouldn't batch APIs that return large amount of data
+ if (req.api is CommunityFileDownloadApi) {
+ return null
+ }
+
+ // Only batch requests that require signing
+ if (!req.api.requiresSigning) {
+ return null
+ }
+
+ return req.serverBaseUrl
+ }
+
+ @Suppress("UNCHECKED_CAST")
+ override suspend fun deconstructBatchResponse(
+ requests: List>>,
+ response: Any
+ ): List> {
+ response as List
+
+ check(requests.size == response.size) {
+ "Mismatched batch response size: expected=${requests.size}, actual=${response.size}"
+ }
+
+ return requests.mapIndexed { index, (ctx, req) ->
+ runCatching {
+ req.api.processResponse(
+ executorContext = ctx,
+ response = response[index].toHttpResponse(json),
+ baseUrl = req.serverBaseUrl,
+ )
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/session/libsession/messaging/open_groups/api/CommunityApiExecutor.kt b/app/src/main/java/org/session/libsession/messaging/open_groups/api/CommunityApiExecutor.kt
new file mode 100644
index 0000000000..2f890ad891
--- /dev/null
+++ b/app/src/main/java/org/session/libsession/messaging/open_groups/api/CommunityApiExecutor.kt
@@ -0,0 +1,51 @@
+package org.session.libsession.messaging.open_groups.api
+
+import org.session.libsession.database.StorageProtocol
+import org.thoughtcrime.securesms.api.ApiExecutor
+import org.thoughtcrime.securesms.api.ApiExecutorContext
+import org.thoughtcrime.securesms.api.server.ServerApi
+import org.thoughtcrime.securesms.api.server.ServerApiExecutor
+import org.thoughtcrime.securesms.api.server.ServerApiRequest
+import org.thoughtcrime.securesms.api.server.ServerApiResponse
+import javax.inject.Inject
+
+data class CommunityApiRequest>(
+ val serverBaseUrl: String,
+ val api: Api,
+ val serverPubKey: String? = null, // Null to let executor fill in from config data
+) {
+ override fun toString(): String {
+ return "CommunityApiRequest(${api::class.java.simpleName}, serverBaseUrl=$serverBaseUrl)"
+ }
+}
+
+typealias CommunityApiExecutor = ApiExecutor, ServerApiResponse>
+
+class CommunityApiExecutorImpl @Inject constructor(
+ private val serverApiExecutor: ServerApiExecutor,
+ private val storage: StorageProtocol
+) : CommunityApiExecutor {
+ override suspend fun send(
+ ctx: ApiExecutorContext,
+ req: CommunityApiRequest<*>
+ ): ServerApiResponse {
+ val x25519PubKey = checkNotNull(
+ req.serverPubKey ?: storage.getOpenGroupPublicKey(req.serverBaseUrl)) {
+ "No stored x25519 public key for server ${req.serverBaseUrl}"
+ }
+
+ @Suppress("UNCHECKED_CAST")
+ return serverApiExecutor.send(ctx, ServerApiRequest(
+ serverBaseUrl = req.serverBaseUrl,
+ serverX25519PubKeyHex = x25519PubKey,
+ api = req.api as ServerApi
+ ))
+ }
+}
+
+suspend inline fun > CommunityApiExecutor.execute(
+ req: CommunityApiRequest,
+ ctx: ApiExecutorContext = ApiExecutorContext()
+): Resp {
+ return send(ctx, req) as Resp
+}
diff --git a/app/src/main/java/org/session/libsession/messaging/open_groups/api/CommunityFileDownloadApi.kt b/app/src/main/java/org/session/libsession/messaging/open_groups/api/CommunityFileDownloadApi.kt
new file mode 100644
index 0000000000..f9266915f3
--- /dev/null
+++ b/app/src/main/java/org/session/libsession/messaging/open_groups/api/CommunityFileDownloadApi.kt
@@ -0,0 +1,54 @@
+package org.session.libsession.messaging.open_groups.api
+
+import android.net.Uri
+import android.util.Base64
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import org.thoughtcrime.securesms.api.ApiExecutorContext
+import org.thoughtcrime.securesms.api.http.HttpBody
+import org.thoughtcrime.securesms.api.http.HttpResponse
+
+class CommunityFileDownloadApi @AssistedInject constructor(
+ @Assisted override val requiresSigning: Boolean,
+ @Assisted("room") override val room: String?,
+ @Assisted val fileId: String,
+ deps: CommunityApiDependencies,
+) : CommunityApi(deps) {
+ override val httpMethod: String get() = "GET"
+ override val httpEndpoint: String = if (room != null) {
+ "/room/${Uri.encode(room)}/file/${Uri.encode(fileId)}"
+ } else {
+ "/file/${Uri.encode(fileId)}"
+ }
+
+ override suspend fun handleSuccessResponse(
+ executorContext: ApiExecutorContext,
+ baseUrl: String,
+ response: HttpResponse
+ ): HttpBody {
+ // If the response body is text, it's very likely they were base64 encoded
+ // before being sent over the network. Try to decode it.
+ if (response.body is HttpBody.Text) {
+ val bytes = runCatching {
+ Base64.decode(response.body.text, Base64.DEFAULT)
+ }.getOrNull()
+
+ if (bytes != null) {
+ return HttpBody.Bytes(bytes)
+ }
+ }
+
+ return response.body
+ }
+
+ @AssistedFactory
+ interface Factory {
+ fun create(
+ @Assisted("room")
+ room: String?,
+ fileId: String,
+ requiresSigning: Boolean,
+ ): CommunityFileDownloadApi
+ }
+}
diff --git a/app/src/main/java/org/session/libsession/messaging/open_groups/api/CommunityFileUploadApi.kt b/app/src/main/java/org/session/libsession/messaging/open_groups/api/CommunityFileUploadApi.kt
new file mode 100644
index 0000000000..1eb9762d81
--- /dev/null
+++ b/app/src/main/java/org/session/libsession/messaging/open_groups/api/CommunityFileUploadApi.kt
@@ -0,0 +1,57 @@
+package org.session.libsession.messaging.open_groups.api
+
+import android.net.Uri
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.json.decodeFromStream
+import org.thoughtcrime.securesms.api.ApiExecutorContext
+import org.thoughtcrime.securesms.api.http.HttpBody
+import org.thoughtcrime.securesms.api.http.HttpRequest
+import org.thoughtcrime.securesms.api.http.HttpResponse
+
+class CommunityFileUploadApi @AssistedInject constructor(
+ @Assisted private val file: HttpBody,
+ @Assisted override val room: String,
+ deps: CommunityApiDependencies
+) : CommunityApi(deps) {
+ override val requiresSigning: Boolean get() = true
+ override val httpMethod: String get() = "POST"
+ override val httpEndpoint: String = "/room/${Uri.encode(room)}/file"
+
+ override fun buildRequest(baseUrl: String, x25519PubKeyHex: String): HttpRequest {
+ val request = super.buildRequest(baseUrl, x25519PubKeyHex)
+ return request.copy(
+ headers = request.headers.toMutableMap().apply {
+ this["Content-Type"] = "application/octet-stream"
+ this["Content-Disposition"] = "attachment"
+ },
+ body = file,
+ )
+ }
+
+ override suspend fun handleSuccessResponse(
+ executorContext: ApiExecutorContext,
+ baseUrl: String,
+ response: HttpResponse
+ ): String {
+ @Suppress("OPT_IN_USAGE")
+ return response.body.asInputStream()
+ .use { json.decodeFromStream(it) }
+ .id
+ }
+
+ @Serializable
+ private class UploadResult(
+ val id: String
+ )
+
+ @AssistedFactory
+ interface Factory {
+ fun create(
+ file: HttpBody,
+ room: String
+ ): CommunityFileUploadApi
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/session/libsession/messaging/open_groups/api/DeleteAllInboxMessagesApi.kt b/app/src/main/java/org/session/libsession/messaging/open_groups/api/DeleteAllInboxMessagesApi.kt
new file mode 100644
index 0000000000..0426b4f9d5
--- /dev/null
+++ b/app/src/main/java/org/session/libsession/messaging/open_groups/api/DeleteAllInboxMessagesApi.kt
@@ -0,0 +1,20 @@
+package org.session.libsession.messaging.open_groups.api
+
+import org.thoughtcrime.securesms.api.ApiExecutorContext
+import org.thoughtcrime.securesms.api.http.HttpResponse
+import javax.inject.Inject
+
+class DeleteAllInboxMessagesApi @Inject constructor(
+ deps: CommunityApiDependencies,
+): CommunityApi(deps) {
+ override val room: String? get() = null
+ override val requiresSigning: Boolean get() = true
+ override val httpMethod: String get() = "DELETE"
+ override val httpEndpoint: String get() = "/inbox"
+
+ override suspend fun handleSuccessResponse(
+ executorContext: ApiExecutorContext,
+ baseUrl: String,
+ response: HttpResponse
+ ) = Unit
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/session/libsession/messaging/open_groups/api/DeleteAllReactionsApi.kt b/app/src/main/java/org/session/libsession/messaging/open_groups/api/DeleteAllReactionsApi.kt
new file mode 100644
index 0000000000..a4c437ae15
--- /dev/null
+++ b/app/src/main/java/org/session/libsession/messaging/open_groups/api/DeleteAllReactionsApi.kt
@@ -0,0 +1,43 @@
+package org.session.libsession.messaging.open_groups.api
+
+import android.net.Uri
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import kotlinx.serialization.json.decodeFromStream
+import org.session.libsession.messaging.open_groups.OpenGroupApi
+import org.thoughtcrime.securesms.api.ApiExecutorContext
+import org.thoughtcrime.securesms.api.http.HttpResponse
+
+class DeleteAllReactionsApi @AssistedInject constructor(
+ @Assisted("room") override val room: String,
+ @Assisted val messageId: Long,
+ @Assisted val emoji: String,
+ deps: CommunityApiDependencies,
+) : CommunityApi(deps) {
+ override val requiresSigning: Boolean get() = true
+ override val httpMethod: String get() = "DELETE"
+ override val httpEndpoint: String = buildString {
+ append("/room/")
+ append(Uri.encode(room))
+ append("/reactions/")
+ append(messageId)
+ append("/")
+ append(Uri.encode(emoji))
+ }
+
+ override suspend fun handleSuccessResponse(
+ executorContext: ApiExecutorContext,
+ baseUrl: String,
+ response: HttpResponse
+ ) = Unit
+
+ @AssistedFactory
+ interface Factory {
+ fun create(
+ @Assisted("room") room: String,
+ messageId: Long,
+ emoji: String
+ ): DeleteAllReactionsApi
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/session/libsession/messaging/open_groups/api/DeleteMessageApi.kt b/app/src/main/java/org/session/libsession/messaging/open_groups/api/DeleteMessageApi.kt
new file mode 100644
index 0000000000..85316f6934
--- /dev/null
+++ b/app/src/main/java/org/session/libsession/messaging/open_groups/api/DeleteMessageApi.kt
@@ -0,0 +1,31 @@
+package org.session.libsession.messaging.open_groups.api
+
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import org.thoughtcrime.securesms.api.ApiExecutorContext
+import org.thoughtcrime.securesms.api.http.HttpResponse
+
+class DeleteMessageApi @AssistedInject constructor(
+ @Assisted override val room: String,
+ @Assisted private val messageId: Long,
+ deps: CommunityApiDependencies
+) : CommunityApi(deps) {
+ override val requiresSigning: Boolean get() = true
+ override val httpMethod: String get() = "DELETE"
+ override val httpEndpoint: String = "/room/$room/message/$messageId"
+
+ override suspend fun handleSuccessResponse(
+ executorContext: ApiExecutorContext,
+ baseUrl: String,
+ response: HttpResponse
+ ) = Unit
+
+ @AssistedFactory
+ interface Factory {
+ fun create(
+ room: String,
+ messageId: Long
+ ): DeleteMessageApi
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/session/libsession/messaging/open_groups/api/DeleteReactionApi.kt b/app/src/main/java/org/session/libsession/messaging/open_groups/api/DeleteReactionApi.kt
new file mode 100644
index 0000000000..07793f6533
--- /dev/null
+++ b/app/src/main/java/org/session/libsession/messaging/open_groups/api/DeleteReactionApi.kt
@@ -0,0 +1,41 @@
+package org.session.libsession.messaging.open_groups.api
+
+import android.net.Uri
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import org.thoughtcrime.securesms.api.ApiExecutorContext
+import org.thoughtcrime.securesms.api.http.HttpResponse
+
+class DeleteReactionApi @AssistedInject constructor(
+ @Assisted("room") override val room: String,
+ @Assisted val messageId: Long,
+ @Assisted val emoji: String,
+ deps: CommunityApiDependencies,
+) : CommunityApi(deps) {
+ override val requiresSigning: Boolean get() = true
+ override val httpMethod: String get() = "DELETE"
+ override val httpEndpoint: String = buildString {
+ append("/room/")
+ append(Uri.encode(room))
+ append("/reaction/")
+ append(messageId)
+ append("/")
+ append(Uri.encode(emoji))
+ }
+
+ override suspend fun handleSuccessResponse(
+ executorContext: ApiExecutorContext,
+ baseUrl: String,
+ response: HttpResponse
+ ) = Unit
+
+ @AssistedFactory
+ interface Factory {
+ fun create(
+ @Assisted("room") room: String,
+ messageId: Long,
+ emoji: String
+ ): DeleteReactionApi
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/session/libsession/messaging/open_groups/api/DeleteUserMessagesApi.kt b/app/src/main/java/org/session/libsession/messaging/open_groups/api/DeleteUserMessagesApi.kt
new file mode 100644
index 0000000000..edd04ef1a8
--- /dev/null
+++ b/app/src/main/java/org/session/libsession/messaging/open_groups/api/DeleteUserMessagesApi.kt
@@ -0,0 +1,33 @@
+package org.session.libsession.messaging.open_groups.api
+
+import android.net.Uri
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import org.thoughtcrime.securesms.api.ApiExecutorContext
+import org.thoughtcrime.securesms.api.http.HttpResponse
+
+class DeleteUserMessagesApi @AssistedInject constructor(
+ @Assisted("user") private val userToDelete: String,
+ @Assisted override val room: String,
+ deps: CommunityApiDependencies,
+) : CommunityApi(deps) {
+ override val requiresSigning: Boolean get() = true
+ override val httpMethod: String get() = "DELETE"
+ override val httpEndpoint: String =
+ "/room/${Uri.encode(room)}/all/${Uri.encode(userToDelete)}"
+
+ override suspend fun handleSuccessResponse(
+ executorContext: ApiExecutorContext,
+ baseUrl: String,
+ response: HttpResponse
+ ) = Unit
+
+ @AssistedFactory
+ interface Factory {
+ fun create(
+ @Assisted("user") userToDelete: String,
+ room: String
+ ): DeleteUserMessagesApi
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/session/libsession/messaging/open_groups/api/GetCapsApi.kt b/app/src/main/java/org/session/libsession/messaging/open_groups/api/GetCapsApi.kt
new file mode 100644
index 0000000000..81546d48e2
--- /dev/null
+++ b/app/src/main/java/org/session/libsession/messaging/open_groups/api/GetCapsApi.kt
@@ -0,0 +1,25 @@
+package org.session.libsession.messaging.open_groups.api
+
+import kotlinx.serialization.json.decodeFromStream
+import org.session.libsession.messaging.open_groups.OpenGroupApi.Capabilities
+import org.thoughtcrime.securesms.api.ApiExecutorContext
+import org.thoughtcrime.securesms.api.http.HttpResponse
+import javax.inject.Inject
+
+class GetCapsApi @Inject constructor(
+ deps: CommunityApiDependencies,
+) : CommunityApi(deps) {
+ override val room: String? get() = null
+ override val requiresSigning: Boolean get() = false
+ override val httpMethod: String get() = "GET"
+ override val httpEndpoint: String get() = "/capabilities"
+
+ override suspend fun handleSuccessResponse(
+ executorContext: ApiExecutorContext,
+ baseUrl: String,
+ response: HttpResponse
+ ): Capabilities {
+ @Suppress("OPT_IN_USAGE")
+ return response.body.asInputStream().use(json::decodeFromStream)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/session/libsession/messaging/open_groups/api/GetDirectMessagesApi.kt b/app/src/main/java/org/session/libsession/messaging/open_groups/api/GetDirectMessagesApi.kt
new file mode 100644
index 0000000000..8e01fd7976
--- /dev/null
+++ b/app/src/main/java/org/session/libsession/messaging/open_groups/api/GetDirectMessagesApi.kt
@@ -0,0 +1,57 @@
+package org.session.libsession.messaging.open_groups.api
+
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import kotlinx.serialization.json.decodeFromStream
+import org.session.libsession.messaging.open_groups.OpenGroupApi
+import org.thoughtcrime.securesms.api.ApiExecutorContext
+import org.thoughtcrime.securesms.api.http.HttpResponse
+
+class GetDirectMessagesApi @AssistedInject constructor(
+ @Assisted inboxOrOutbox: Boolean,
+ @Assisted sinceLastId: Long?,
+ deps: CommunityApiDependencies,
+) : CommunityApi>(deps) {
+ override val room: String? get() = null
+ override val requiresSigning: Boolean get() = true
+ override val httpMethod: String get() = "GET"
+ override val httpEndpoint: String = when {
+ inboxOrOutbox && sinceLastId == null -> "/inbox"
+ inboxOrOutbox && sinceLastId != null -> "/inbox/since/$sinceLastId"
+ !inboxOrOutbox && sinceLastId == null -> "/outbox"
+ else /* !isInboxOrOutbox && sinceSeqNo != null */ -> "/outbox/since/$sinceLastId"
+ }
+
+ override suspend fun processResponse(
+ executorContext: ApiExecutorContext,
+ baseUrl: String,
+ response: HttpResponse
+ ): List {
+ if (response.statusCode == 304) {
+ // This "Not Modified" error is specific to the direct messages API to indicate there
+ // are no new messages, this is not an error for our purposes, so we won't go through
+ // the usual error handling path.
+ return emptyList()
+ }
+
+ return super.processResponse(executorContext, baseUrl, response)
+ }
+
+ override suspend fun handleSuccessResponse(
+ executorContext: ApiExecutorContext,
+ baseUrl: String,
+ response: HttpResponse
+ ): List {
+ @Suppress("OPT_IN_USAGE")
+ return response.body.asInputStream().use(json::decodeFromStream)
+ }
+
+ @AssistedFactory
+ interface Factory {
+ fun create(
+ inboxOrOutbox: Boolean,
+ sinceLastId: Long?
+ ): GetDirectMessagesApi
+ }
+}
diff --git a/app/src/main/java/org/session/libsession/messaging/open_groups/api/GetRoomMessagesApi.kt b/app/src/main/java/org/session/libsession/messaging/open_groups/api/GetRoomMessagesApi.kt
new file mode 100644
index 0000000000..add2d383c9
--- /dev/null
+++ b/app/src/main/java/org/session/libsession/messaging/open_groups/api/GetRoomMessagesApi.kt
@@ -0,0 +1,62 @@
+package org.session.libsession.messaging.open_groups.api
+
+import android.net.Uri
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import kotlinx.serialization.json.decodeFromStream
+import org.session.libsession.messaging.open_groups.OpenGroupApi
+import org.thoughtcrime.securesms.api.ApiExecutorContext
+import org.thoughtcrime.securesms.api.http.HttpRequest
+import org.thoughtcrime.securesms.api.http.HttpResponse
+
+class GetRoomMessagesApi @AssistedInject constructor(
+ @Assisted override val room: String,
+ @Assisted sinceSeqNo: Long?,
+ @Assisted reactors: Int?,
+ deps: CommunityApiDependencies,
+) : CommunityApi>(deps) {
+ override val requiresSigning: Boolean get() = true
+ override val httpMethod: String get() = "GET"
+ override val httpEndpoint: String = buildString {
+ append("/room/")
+ append(Uri.encode(room))
+ append("/messages")
+
+ if (sinceSeqNo != null) {
+ append("/since/")
+ append(sinceSeqNo)
+ } else {
+ append("/recent")
+ }
+
+ append("?t=r") // Not sure what 't=r' means, but it's in the original code
+ if (reactors != null) {
+ append("&reactors=")
+ append(reactors)
+ }
+ }
+
+ override fun buildRequest(baseUrl: String, x25519PubKeyHex: String): HttpRequest {
+ val request = super.buildRequest(baseUrl, x25519PubKeyHex)
+ return request
+ }
+
+ override suspend fun handleSuccessResponse(
+ executorContext: ApiExecutorContext,
+ baseUrl: String,
+ response: HttpResponse
+ ): List {
+ @Suppress("OPT_IN_USAGE")
+ return response.body.asInputStream().use(json::decodeFromStream)
+ }
+
+ @AssistedFactory
+ interface Factory {
+ fun create(
+ room: String,
+ sinceSeqNo: Long?,
+ reactors: Int? = 5
+ ): GetRoomMessagesApi
+ }
+}
diff --git a/app/src/main/java/org/session/libsession/messaging/open_groups/api/GetRoomsApi.kt b/app/src/main/java/org/session/libsession/messaging/open_groups/api/GetRoomsApi.kt
new file mode 100644
index 0000000000..bf52b3faef
--- /dev/null
+++ b/app/src/main/java/org/session/libsession/messaging/open_groups/api/GetRoomsApi.kt
@@ -0,0 +1,32 @@
+package org.session.libsession.messaging.open_groups.api
+
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import kotlinx.serialization.json.decodeFromStream
+import org.session.libsession.messaging.open_groups.OpenGroupApi
+import org.thoughtcrime.securesms.api.ApiExecutorContext
+import org.thoughtcrime.securesms.api.http.HttpResponse
+
+class GetRoomsApi @AssistedInject constructor(
+ @Assisted override val requiresSigning: Boolean,
+ deps: CommunityApiDependencies,
+) : CommunityApi>(deps) {
+ override val room: String? get() = null
+ override val httpMethod: String get() = "GET"
+ override val httpEndpoint: String get() = "/rooms"
+
+ override suspend fun handleSuccessResponse(
+ executorContext: ApiExecutorContext,
+ baseUrl: String,
+ response: HttpResponse
+ ): List {
+ @Suppress("OPT_IN_USAGE")
+ return json.decodeFromStream(response.body.asInputStream())
+ }
+
+ @AssistedFactory
+ interface Factory {
+ fun create(requiresSigning: Boolean): GetRoomsApi
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/session/libsession/messaging/open_groups/api/PollRoomApi.kt b/app/src/main/java/org/session/libsession/messaging/open_groups/api/PollRoomApi.kt
new file mode 100644
index 0000000000..7f02e6bd36
--- /dev/null
+++ b/app/src/main/java/org/session/libsession/messaging/open_groups/api/PollRoomApi.kt
@@ -0,0 +1,31 @@
+package org.session.libsession.messaging.open_groups.api
+
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import kotlinx.serialization.json.JsonObject
+import kotlinx.serialization.json.decodeFromStream
+import org.thoughtcrime.securesms.api.ApiExecutorContext
+import org.thoughtcrime.securesms.api.http.HttpResponse
+
+class PollRoomApi @AssistedInject constructor(
+ @Assisted override val room: String,
+ @Assisted infoUpdates: Int,
+ deps: CommunityApiDependencies,
+) : CommunityApi(deps) {
+ override val httpMethod: String get() = "GET"
+ override val httpEndpoint: String = "room/$room/pollInfo/$infoUpdates"
+ override val requiresSigning: Boolean get() = true
+
+ @Suppress("OPT_IN_USAGE")
+ override suspend fun handleSuccessResponse(
+ executorContext: ApiExecutorContext,
+ baseUrl: String,
+ response: HttpResponse
+ ): JsonObject = response.body.asInputStream().use(json::decodeFromStream)
+
+ @AssistedFactory
+ interface Factory {
+ fun create(room: String, infoUpdates: Int): PollRoomApi
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/session/libsession/messaging/open_groups/api/SendDirectMessageApi.kt b/app/src/main/java/org/session/libsession/messaging/open_groups/api/SendDirectMessageApi.kt
new file mode 100644
index 0000000000..67ac5f06d9
--- /dev/null
+++ b/app/src/main/java/org/session/libsession/messaging/open_groups/api/SendDirectMessageApi.kt
@@ -0,0 +1,56 @@
+package org.session.libsession.messaging.open_groups.api
+
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.json.JsonObject
+import kotlinx.serialization.json.JsonPrimitive
+import kotlinx.serialization.json.decodeFromStream
+import okhttp3.MediaType
+import okhttp3.MediaType.Companion.toMediaType
+import org.session.libsession.messaging.open_groups.OpenGroupApi
+import org.session.libsession.utilities.Address
+import org.thoughtcrime.securesms.api.ApiExecutorContext
+import org.thoughtcrime.securesms.api.http.HttpBody
+import org.thoughtcrime.securesms.api.http.HttpResponse
+
+class SendDirectMessageApi @AssistedInject constructor(
+ @Assisted private val recipient: Address.Blinded,
+ @Assisted private val messageContent: String,
+ deps: CommunityApiDependencies,
+) : CommunityApi(deps) {
+ override val room: String? get() = null
+ override val requiresSigning: Boolean get() = true
+ override val httpMethod: String get() = "POST"
+ override val httpEndpoint: String = "/inbox/${recipient.blindedId.hexString}"
+
+ override suspend fun handleSuccessResponse(
+ executorContext: ApiExecutorContext,
+ baseUrl: String,
+ response: HttpResponse
+ ): OpenGroupApi.DirectMessage {
+ @Suppress("OPT_IN_USAGE")
+ return response.body.asInputStream().use(json::decodeFromStream)
+ }
+
+ override fun buildRequestBody(
+ serverBaseUrl: String,
+ x25519PubKeyHex: String
+ ): Pair {
+ return buildJsonRequestBody(Request(messageContent))
+ }
+
+ @Serializable
+ private class Request(
+ val message: String
+ )
+
+ @AssistedFactory
+ interface Factory {
+ fun create(
+ recipient: Address.Blinded,
+ messageContent: String
+ ): SendDirectMessageApi
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/session/libsession/messaging/open_groups/api/SendMessageApi.kt b/app/src/main/java/org/session/libsession/messaging/open_groups/api/SendMessageApi.kt
new file mode 100644
index 0000000000..73860bcd94
--- /dev/null
+++ b/app/src/main/java/org/session/libsession/messaging/open_groups/api/SendMessageApi.kt
@@ -0,0 +1,100 @@
+package org.session.libsession.messaging.open_groups.api
+
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.json.decodeFromStream
+import network.loki.messenger.libsession_util.ED25519
+import network.loki.messenger.libsession_util.util.BlindKeyAPI
+import okhttp3.MediaType
+import okhttp3.MediaType.Companion.toMediaType
+import org.session.libsession.messaging.open_groups.OpenGroupApi
+import org.session.libsession.messaging.open_groups.OpenGroupMessage
+import org.session.libsignal.utilities.Base64
+import org.thoughtcrime.securesms.api.ApiExecutorContext
+import org.thoughtcrime.securesms.api.http.HttpBody
+import org.thoughtcrime.securesms.api.http.HttpResponse
+
+class SendMessageApi @AssistedInject constructor(
+ @Assisted override val room: String,
+ @Assisted val message: OpenGroupMessage,
+ @Assisted val fileIds: List,
+ deps: CommunityApiDependencies,
+) : CommunityApi(deps) {
+ override val requiresSigning: Boolean get() = true
+ override val httpMethod: String get() = "POST"
+ override val httpEndpoint: String = "room/$room/message"
+
+ override fun buildRequestBody(
+ serverBaseUrl: String,
+ x25519PubKeyHex: String
+ ): Pair {
+ val caps = storage.getServerCapabilities(serverBaseUrl)
+ val ed25519SecretKey = loginStateRepository.get()
+ .requireLoggedInState().accountEd25519KeyPair.secretKey.data
+ val signature = if (caps?.contains(OpenGroupApi.Capability.BLIND.name.lowercase()) == true) {
+ BlindKeyAPI.blind15Sign(
+ ed25519SecretKey = ed25519SecretKey,
+ serverPubKey = x25519PubKeyHex,
+ message = Base64.decode(message.base64EncodedData.orEmpty())
+ )
+ } else {
+ ED25519.sign(
+ ed25519PrivateKey = ed25519SecretKey,
+ message = Base64.decode(message.base64EncodedData.orEmpty())
+ )
+ }
+
+ return buildJsonRequestBody(Message(
+ data = message.base64EncodedData.orEmpty(),
+ timestampSeconds = message.sentTimestamp / 1000.0,
+ signature = Base64.encodeBytes(signature),
+ sender = message.sender.orEmpty(),
+ files = fileIds.takeIf { it.isNotEmpty() }
+ ))
+ }
+
+ override suspend fun handleSuccessResponse(
+ executorContext: ApiExecutorContext,
+ baseUrl: String,
+ response: HttpResponse
+ ): Response {
+ @Suppress("OPT_IN_USAGE")
+ val response: Response = response.body.asInputStream().use(json::decodeFromStream)
+ storage.addReceivedMessageTimestamp(response.postedMills)
+ return response
+ }
+
+ @Serializable
+ private class Message(
+ val data: String,
+ @SerialName("timestamp")
+ val timestampSeconds: Double,
+ val signature: String,
+ @SerialName("public_key")
+ val sender: String,
+ val files: List? = null,
+ )
+
+ @Serializable
+ class Response(
+ val id: Long,
+
+ @SerialName("posted")
+ val postedSeconds: Double,
+ ) {
+ val postedMills: Long
+ get() = (postedSeconds * 1000.0).toLong()
+ }
+
+ @AssistedFactory
+ interface Factory {
+ fun create(
+ room: String,
+ message: OpenGroupMessage,
+ fileIds: List
+ ): SendMessageApi
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/session/libsession/messaging/open_groups/api/UnbanUserApi.kt b/app/src/main/java/org/session/libsession/messaging/open_groups/api/UnbanUserApi.kt
new file mode 100644
index 0000000000..598944c6c9
--- /dev/null
+++ b/app/src/main/java/org/session/libsession/messaging/open_groups/api/UnbanUserApi.kt
@@ -0,0 +1,47 @@
+package org.session.libsession.messaging.open_groups.api
+
+import android.net.Uri
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import kotlinx.serialization.Serializable
+import org.thoughtcrime.securesms.api.ApiExecutorContext
+import org.thoughtcrime.securesms.api.http.HttpBody
+import org.thoughtcrime.securesms.api.http.HttpResponse
+import okhttp3.MediaType
+
+class UnbanUserApi @AssistedInject constructor(
+ @Assisted("user") private val userToBan: String,
+ @Assisted override val room: String,
+ deps: CommunityApiDependencies,
+) : CommunityApi(deps) {
+ override val requiresSigning: Boolean get() = true
+ override val httpMethod: String get() = "POST"
+ override val httpEndpoint: String =
+ "/user/${Uri.encode(userToBan)}/unban"
+
+ @Serializable
+ private data class BanBody(val rooms: List)
+
+ override fun buildRequestBody(
+ serverBaseUrl: String,
+ x25519PubKeyHex: String
+ ): Pair {
+ // JSON body {"rooms":[room]}
+ return buildJsonRequestBody(BanBody(rooms = listOf(room)))
+ }
+
+ override suspend fun handleSuccessResponse(
+ executorContext: ApiExecutorContext,
+ baseUrl: String,
+ response: HttpResponse
+ ) = Unit
+
+ @AssistedFactory
+ interface Factory {
+ fun create(
+ @Assisted("user") userToBan: String,
+ room: String
+ ): UnbanUserApi
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/session/libsession/messaging/sending_receiving/GroupMessageHandler.kt b/app/src/main/java/org/session/libsession/messaging/sending_receiving/GroupMessageHandler.kt
index a16f8d24e8..611ce42722 100644
--- a/app/src/main/java/org/session/libsession/messaging/sending_receiving/GroupMessageHandler.kt
+++ b/app/src/main/java/org/session/libsession/messaging/sending_receiving/GroupMessageHandler.kt
@@ -3,6 +3,7 @@ package org.session.libsession.messaging.sending_receiving
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import network.loki.messenger.libsession_util.ED25519
+import network.loki.messenger.libsession_util.protocol.DecodedPro
import org.session.libsession.database.StorageProtocol
import org.session.libsession.messaging.groups.GroupManagerV2
import org.session.libsession.messaging.messages.ProfileUpdateHandler
@@ -11,7 +12,8 @@ import org.session.libsession.messaging.utilities.MessageAuthentication.buildDel
import org.session.libsession.messaging.utilities.MessageAuthentication.buildGroupInviteSignature
import org.session.libsession.messaging.utilities.MessageAuthentication.buildInfoChangeSignature
import org.session.libsession.messaging.utilities.MessageAuthentication.buildMemberChangeSignature
-import org.session.libsignal.protos.SignalServiceProtos
+import org.session.libsession.network.SnodeClock
+import org.session.protos.SessionProtos
import org.session.libsignal.utilities.AccountId
import org.session.libsignal.utilities.IdPrefix
import org.session.libsignal.utilities.Log
@@ -26,8 +28,14 @@ class GroupMessageHandler @Inject constructor(
private val storage: StorageProtocol,
private val groupManagerV2: GroupManagerV2,
@param:ManagerScope private val scope: CoroutineScope,
+ private val clock: SnodeClock,
) {
- fun handleGroupUpdated(message: GroupUpdated, groupId: AccountId?, proto: SignalServiceProtos.Content) {
+ fun handleGroupUpdated(
+ message: GroupUpdated,
+ groupId: AccountId?,
+ proto: SessionProtos.Content,
+ pro: DecodedPro?
+ ) {
val inner = message.inner
if (groupId == null &&
!inner.hasInviteMessage() && !inner.hasPromoteMessage()) {
@@ -35,7 +43,7 @@ class GroupMessageHandler @Inject constructor(
}
// Update profile if needed
- ProfileUpdateHandler.Updates.create(proto)?.let { updates ->
+ ProfileUpdateHandler.Updates.create(proto, clock.currentTimeMillis(), pro)?.let { updates ->
profileUpdateHandler.handleProfileUpdate(
senderId = AccountId(message.sender!!),
updates = updates,
@@ -55,7 +63,7 @@ class GroupMessageHandler @Inject constructor(
}
}
- private fun handleNewLibSessionClosedGroupMessage(message: GroupUpdated, proto: SignalServiceProtos.Content) {
+ private fun handleNewLibSessionClosedGroupMessage(message: GroupUpdated, proto: SessionProtos.Content) {
val storage = storage
val ourUserId = storage.getUserPublicKey()!!
val invite = message.inner.inviteMessage
@@ -117,7 +125,7 @@ class GroupMessageHandler @Inject constructor(
}
- private fun handlePromotionMessage(message: GroupUpdated, proto: SignalServiceProtos.Content) {
+ private fun handlePromotionMessage(message: GroupUpdated, proto: SessionProtos.Content) {
val promotion = message.inner.promoteMessage
val seed = promotion.groupIdentitySeed.toByteArray()
val sender = message.sender!!
diff --git a/app/src/main/java/org/session/libsession/messaging/sending_receiving/MessageDecrypter.kt b/app/src/main/java/org/session/libsession/messaging/sending_receiving/MessageDecrypter.kt
deleted file mode 100644
index af8e0c4d2c..0000000000
--- a/app/src/main/java/org/session/libsession/messaging/sending_receiving/MessageDecrypter.kt
+++ /dev/null
@@ -1,76 +0,0 @@
-package org.session.libsession.messaging.sending_receiving
-
-import network.loki.messenger.libsession_util.SessionEncrypt
-import network.loki.messenger.libsession_util.util.BlindKeyAPI
-import network.loki.messenger.libsession_util.util.KeyPair
-import org.session.libsession.messaging.MessagingModuleConfiguration
-import org.session.libsession.messaging.sending_receiving.MessageReceiver.Error
-import org.session.libsignal.utilities.Hex
-import org.session.libsignal.utilities.Log
-import org.session.libsignal.utilities.removingIdPrefixIfNeeded
-
-@Deprecated("This class is deprecated and new code should try to decrypt/decode message using SessionProtocol API")
-object MessageDecrypter {
-
- /**
- * Decrypts `ciphertext` using the Session protocol and `x25519KeyPair`.
- *
- * @param ciphertext the data to decrypt.
- * @param x25519KeyPair the key pair to use for decryption. This could be the current user's key pair, or the key pair of a closed group.
- *
- * @return the padded plaintext.
- */
- fun decrypt(ciphertext: ByteArray, x25519KeyPair: KeyPair): Pair {
- val recipientX25519PrivateKey = x25519KeyPair.secretKey.data
- val recipientX25519PublicKey = x25519KeyPair.pubKey.data.removingIdPrefixIfNeeded()
- val (id, data) = SessionEncrypt.decryptIncoming(
- x25519PubKey = recipientX25519PublicKey,
- x25519PrivKey = recipientX25519PrivateKey,
- ciphertext = ciphertext
- )
-
- return data.data to id
- }
-
- fun decryptBlinded(
- message: ByteArray,
- isOutgoing: Boolean,
- otherBlindedPublicKey: String,
- serverPublicKey: String
- ): Pair {
- val userEdKeyPair = MessagingModuleConfiguration.shared.storage.getUserED25519KeyPair()
- ?: throw Error.NoUserED25519KeyPair
- val blindedKeyPair = BlindKeyAPI.blind15KeyPairOrNull(
- ed25519SecretKey = userEdKeyPair.secretKey.data,
- serverPubKey = Hex.fromStringCondensed(serverPublicKey),
- ) ?: throw Error.DecryptionFailed
- val otherKeyBytes =
- Hex.fromStringCondensed(otherBlindedPublicKey.removingIdPrefixIfNeeded())
-
- val senderKeyBytes: ByteArray
- val recipientKeyBytes: ByteArray
-
- if (isOutgoing) {
- senderKeyBytes = blindedKeyPair.pubKey.data
- recipientKeyBytes = otherKeyBytes
- } else {
- senderKeyBytes = otherKeyBytes
- recipientKeyBytes = blindedKeyPair.pubKey.data
- }
-
- try {
- val (sessionId, plainText) = SessionEncrypt.decryptForBlindedRecipient(
- ciphertext = message,
- myEd25519Privkey = userEdKeyPair.secretKey.data,
- openGroupPubkey = Hex.fromStringCondensed(serverPublicKey),
- senderBlindedId = byteArrayOf(0x15) + senderKeyBytes,
- recipientBlindId = byteArrayOf(0x15) + recipientKeyBytes,
- )
-
- return plainText.data to sessionId
- } catch (e: Exception) {
- Log.e("MessageDecrypter", "Failed to decrypt blinded message", e)
- throw Error.DecryptionFailed
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/org/session/libsession/messaging/sending_receiving/MessageEncrypter.kt b/app/src/main/java/org/session/libsession/messaging/sending_receiving/MessageEncrypter.kt
deleted file mode 100644
index d026b4fb26..0000000000
--- a/app/src/main/java/org/session/libsession/messaging/sending_receiving/MessageEncrypter.kt
+++ /dev/null
@@ -1,37 +0,0 @@
-package org.session.libsession.messaging.sending_receiving
-
-import network.loki.messenger.libsession_util.SessionEncrypt
-import org.session.libsession.messaging.MessagingModuleConfiguration
-import org.session.libsession.messaging.sending_receiving.MessageSender.Error
-import org.session.libsignal.utilities.Hex
-import org.session.libsignal.utilities.Log
-import org.session.libsignal.utilities.removingIdPrefixIfNeeded
-
-@Deprecated("This class is deprecated and new code should try to encrypt/encode message using SessionProtocol API")
-object MessageEncrypter {
-
- /**
- * Encrypts `plaintext` using the Session protocol for `hexEncodedX25519PublicKey`.
- *
- * @param plaintext the plaintext to encrypt. Must already be padded.
- * @param recipientHexEncodedX25519PublicKey the X25519 public key to encrypt for. Could be the Account ID of a user, or the public key of a closed group.
- *
- * @return the encrypted message.
- */
- internal fun encrypt(plaintext: ByteArray, recipientHexEncodedX25519PublicKey: String): ByteArray {
- val userED25519KeyPair = MessagingModuleConfiguration.shared.storage.getUserED25519KeyPair() ?: throw Error.NoUserED25519KeyPair()
- val recipientX25519PublicKey = Hex.fromStringCondensed(recipientHexEncodedX25519PublicKey.removingIdPrefixIfNeeded())
-
- try {
- return SessionEncrypt.encryptForRecipient(
- userED25519KeyPair.secretKey.data,
- recipientX25519PublicKey,
- plaintext
- ).data
- } catch (exception: Exception) {
- Log.d("Loki", "Couldn't encrypt message due to error: $exception.")
- throw Error.EncryptionFailed()
- }
- }
-
-}
\ No newline at end of file
diff --git a/app/src/main/java/org/session/libsession/messaging/sending_receiving/MessageParser.kt b/app/src/main/java/org/session/libsession/messaging/sending_receiving/MessageParser.kt
index 73eed4bb7b..ee64c19de6 100644
--- a/app/src/main/java/org/session/libsession/messaging/sending_receiving/MessageParser.kt
+++ b/app/src/main/java/org/session/libsession/messaging/sending_receiving/MessageParser.kt
@@ -1,11 +1,10 @@
package org.session.libsession.messaging.sending_receiving
-import dagger.Lazy
import network.loki.messenger.libsession_util.SessionEncrypt
+import network.loki.messenger.libsession_util.pro.ProProof
import network.loki.messenger.libsession_util.protocol.DecodedEnvelope
import network.loki.messenger.libsession_util.protocol.DecodedPro
import network.loki.messenger.libsession_util.protocol.SessionProtocol
-import network.loki.messenger.libsession_util.util.BitSet
import network.loki.messenger.libsession_util.util.asSequence
import org.session.libsession.database.StorageProtocol
import org.session.libsession.messaging.messages.Message
@@ -19,19 +18,22 @@ import org.session.libsession.messaging.messages.control.TypingIndicator
import org.session.libsession.messaging.messages.control.UnsendRequest
import org.session.libsession.messaging.messages.visible.VisibleMessage
import org.session.libsession.messaging.open_groups.OpenGroupApi
-import org.session.libsession.snode.SnodeClock
+import org.session.libsession.network.SnodeClock
import org.session.libsession.utilities.Address
import org.session.libsession.utilities.ConfigFactoryProtocol
import org.session.libsession.utilities.TextSecurePreferences
+import org.session.libsession.utilities.withGroupConfigs
+import org.session.libsession.utilities.withUserConfigs
import org.session.libsignal.exceptions.NonRetryableException
-import org.session.libsignal.protos.SignalServiceProtos
import org.session.libsignal.utilities.AccountId
import org.session.libsignal.utilities.Base64
import org.session.libsignal.utilities.Hex
import org.session.libsignal.utilities.IdPrefix
-import org.thoughtcrime.securesms.pro.ProStatusManager
+import org.session.protos.SessionProtos
+import org.thoughtcrime.securesms.pro.ProBackendConfig
import java.util.concurrent.TimeUnit
import javax.inject.Inject
+import javax.inject.Provider
import javax.inject.Singleton
import kotlin.math.abs
@@ -41,19 +43,23 @@ class MessageParser @Inject constructor(
private val storage: StorageProtocol,
private val snodeClock: SnodeClock,
private val prefs: TextSecurePreferences,
+ private val proBackendConfig: Provider,
) {
- //TODO: Obtain proBackendKey from somewhere
- private val proBackendKey = ByteArray(32)
-
// A faster way to check if the user is blocked than to go through RecipientRepository
private fun isUserBlocked(accountId: AccountId): Boolean {
return configFactory.withUserConfigs { it.contacts.get(accountId.hexString) }
?.blocked == true
}
+ class ParseResult(
+ val message: Message,
+ val proto: SessionProtos.Content,
+ val pro: DecodedPro?
+ )
+
- private fun createMessageFromProto(proto: SignalServiceProtos.Content, isGroupMessage: Boolean): Message {
+ private fun createMessageFromProto(proto: SessionProtos.Content, isGroupMessage: Boolean): Message {
val message = ReadReceipt.fromProto(proto) ?:
TypingIndicator.fromProto(proto) ?:
DataExtractionNotification.fromProto(proto) ?:
@@ -79,7 +85,7 @@ class MessageParser @Inject constructor(
currentUserId: AccountId,
currentUserBlindedIDs: List,
senderIdPrefix: IdPrefix
- ): Pair {
+ ): ParseResult {
return parseMessage(
sender = AccountId(senderIdPrefix, decodedEnvelope.senderX25519PubKey.data),
contentPlaintext = decodedEnvelope.contentPlainText.data,
@@ -103,12 +109,12 @@ class MessageParser @Inject constructor(
isForGroup: Boolean,
currentUserId: AccountId,
currentUserBlindedIDs: List,
- ): Pair {
- val proto = SignalServiceProtos.Content.parseFrom(contentPlaintext)
+ ): ParseResult {
+ val proto = SessionProtos.Content.parseFrom(contentPlaintext)
// Check signature
- if (proto.hasSigTimestampMs()) {
- val diff = abs(proto.sigTimestampMs - messageTimestampMs)
+ if (proto.hasSigTimestamp()) {
+ val diff = abs(proto.sigTimestamp - messageTimestampMs)
if (
(!relaxSignatureCheck && diff != 0L ) ||
(relaxSignatureCheck && diff > TimeUnit.HOURS.toMillis(6))) {
@@ -133,14 +139,16 @@ class MessageParser @Inject constructor(
message.sender = sender.hexString
message.recipient = currentUserId.hexString
message.sentTimestamp = messageTimestampMs
- message.receivedTimestamp = snodeClock.currentTimeMills()
+ message.receivedTimestamp = snodeClock.currentTimeMillis()
message.isSenderSelf = isSenderSelf
// Only process pro features post pro launch
if (prefs.forcePostPro()) {
- (message as? VisibleMessage)?.proFeatures = buildSet {
- pro?.proMessageFeatures?.asSequence()?.let(::addAll)
- pro?.proProfileFeatures?.asSequence()?.let(::addAll)
+ if (pro?.status == ProProof.STATUS_VALID) {
+ (message as? VisibleMessage)?.proFeatures = buildSet {
+ addAll(pro.proMessageFeatures.asSequence())
+ addAll(pro.proProfileFeatures.asSequence())
+ }
}
}
@@ -164,7 +172,11 @@ class MessageParser @Inject constructor(
}
storage.addReceivedMessageTimestamp(messageTimestampMs)
- return message to proto
+ return ParseResult(
+ message = message,
+ proto = proto,
+ pro = pro
+ )
}
@@ -173,11 +185,11 @@ class MessageParser @Inject constructor(
serverHash: String?,
currentUserEd25519PrivKey: ByteArray,
currentUserId: AccountId,
- ): Pair {
+ ): ParseResult {
val envelop = SessionProtocol.decodeFor1o1(
myEd25519PrivKey = currentUserEd25519PrivKey,
payload = data,
- proBackendPubKey = proBackendKey,
+ proBackendPubKey = proBackendConfig.get().ed25519PubKey,
)
return parseMessage(
@@ -188,8 +200,8 @@ class MessageParser @Inject constructor(
senderIdPrefix = IdPrefix.STANDARD,
currentUserId = currentUserId,
currentUserBlindedIDs = emptyList(),
- ).also { (message, _) ->
- message.serverHash = serverHash
+ ).also { result ->
+ result.message.serverHash = serverHash
}
}
@@ -199,7 +211,7 @@ class MessageParser @Inject constructor(
groupId: AccountId,
currentUserEd25519PrivKey: ByteArray,
currentUserId: AccountId,
- ): Pair {
+ ): ParseResult {
val keys = configFactory.withGroupConfigs(groupId) {
it.groupKeys.groupKeys()
}
@@ -209,7 +221,7 @@ class MessageParser @Inject constructor(
myEd25519PrivKey = currentUserEd25519PrivKey,
groupEd25519PublicKey = groupId.pubKeyBytes,
groupEd25519PrivateKeys = keys.toTypedArray(),
- proBackendPubKey = proBackendKey
+ proBackendPubKey = proBackendConfig.get().ed25519PubKey,
)
return parseMessage(
@@ -220,8 +232,8 @@ class MessageParser @Inject constructor(
senderIdPrefix = IdPrefix.STANDARD,
currentUserId = currentUserId,
currentUserBlindedIDs = emptyList(),
- ).also { (message, _) ->
- message.serverHash = serverHash
+ ).also { result ->
+ result.message.serverHash = serverHash
}
}
@@ -229,15 +241,15 @@ class MessageParser @Inject constructor(
msg: OpenGroupApi.Message,
currentUserId: AccountId,
currentUserBlindedIDs: List,
- ): Pair? {
+ ): ParseResult? {
if (msg.data.isNullOrBlank()) {
return null
}
val decoded = SessionProtocol.decodeForCommunity(
payload = Base64.decode(msg.data),
- timestampMs = snodeClock.currentTimeMills(),
- proBackendPubKey = proBackendKey,
+ timestampMs = msg.posted?.toEpochMilli() ?: 0L,
+ proBackendPubKey = proBackendConfig.get().ed25519PubKey,
)
val sender = AccountId(msg.sessionId)
@@ -250,10 +262,10 @@ class MessageParser @Inject constructor(
isForGroup = false,
currentUserId = currentUserId,
sender = sender,
- messageTimestampMs = (msg.posted * 1000).toLong(),
+ messageTimestampMs = msg.posted?.toEpochMilli() ?: 0L,
currentUserBlindedIDs = currentUserBlindedIDs,
- ).also { (message, _) ->
- message.openGroupServerMessageID = msg.id
+ ).also { result ->
+ result.message.openGroupServerMessageID = msg.id
}
}
@@ -263,7 +275,7 @@ class MessageParser @Inject constructor(
currentUserEd25519PrivKey: ByteArray,
currentUserId: AccountId,
currentUserBlindedIDs: List,
- ): Pair {
+ ): ParseResult {
val (senderId, plaintext) = SessionEncrypt.decryptForBlindedRecipient(
ciphertext = Base64.decode(msg.message),
myEd25519Privkey = currentUserEd25519PrivKey,
@@ -274,8 +286,8 @@ class MessageParser @Inject constructor(
val decoded = SessionProtocol.decodeForCommunity(
payload = plaintext.data,
- timestampMs = snodeClock.currentTimeMills(),
- proBackendPubKey = proBackendKey,
+ timestampMs = msg.postedAt?.toEpochMilli() ?: 0L,
+ proBackendPubKey = proBackendConfig.get().ed25519PubKey,
)
val sender = Address.Standard(AccountId(senderId))
@@ -288,7 +300,7 @@ class MessageParser @Inject constructor(
isForGroup = false,
currentUserId = currentUserId,
sender = sender.accountId,
- messageTimestampMs = (msg.postedAt * 1000),
+ messageTimestampMs = msg.postedAt?.toEpochMilli() ?: 0L,
currentUserBlindedIDs = currentUserBlindedIDs,
)
}
diff --git a/app/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiver.kt b/app/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiver.kt
deleted file mode 100644
index e2b42f3976..0000000000
--- a/app/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiver.kt
+++ /dev/null
@@ -1,225 +0,0 @@
-package org.session.libsession.messaging.sending_receiving
-
-import network.loki.messenger.libsession_util.util.BlindKeyAPI
-import network.loki.messenger.libsession_util.util.KeyPair
-import org.session.libsession.database.StorageProtocol
-import org.session.libsession.messaging.messages.Message
-import org.session.libsession.messaging.messages.control.CallMessage
-import org.session.libsession.messaging.messages.control.DataExtractionNotification
-import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate
-import org.session.libsession.messaging.messages.control.GroupUpdated
-import org.session.libsession.messaging.messages.control.MessageRequestResponse
-import org.session.libsession.messaging.messages.control.ReadReceipt
-import org.session.libsession.messaging.messages.control.TypingIndicator
-import org.session.libsession.messaging.messages.control.UnsendRequest
-import org.session.libsession.messaging.messages.visible.VisibleMessage
-import org.session.libsession.snode.SnodeAPI
-import org.session.libsignal.crypto.PushTransportDetails
-import org.session.libsignal.protos.SignalServiceProtos
-import org.session.libsignal.protos.SignalServiceProtos.Envelope
-import org.session.libsignal.utilities.AccountId
-import org.session.libsignal.utilities.Hex
-import org.session.libsignal.utilities.IdPrefix
-import org.session.libsignal.utilities.Log
-import java.util.concurrent.TimeUnit
-import javax.inject.Inject
-import javax.inject.Singleton
-import kotlin.math.abs
-
-@Deprecated("This class only exists so the old BatchMessageReceiver can function. New code should use MessageHandler directly.")
-@Singleton
-class MessageReceiver @Inject constructor(
- private val storage: StorageProtocol,
-) {
-
- internal sealed class Error(message: String) : Exception(message) {
- object DuplicateMessage: Error("Duplicate message.")
- object InvalidMessage: Error("Invalid message.")
- object UnknownMessage: Error("Unknown message type.")
- object UnknownEnvelopeType: Error("Unknown envelope type.")
- object DecryptionFailed : Exception("Couldn't decrypt message.")
- object InvalidSignature: Error("Invalid message signature.")
- object NoData: Error("Received an empty envelope.")
- object SenderBlocked: Error("Received a message from a blocked user.")
- object NoThread: Error("Couldn't find thread for message.")
- object SelfSend: Error("Message addressed at self.")
- object InvalidGroupPublicKey: Error("Invalid group public key.")
- object NoGroupThread: Error("No thread exists for this group.")
- object NoGroupKeyPair: Error("Missing group key pair.")
- object NoUserED25519KeyPair : Error("Couldn't find user ED25519 key pair.")
- object ExpiredMessage: Error("Message has already expired, prevent adding")
-
- internal val isRetryable: Boolean = when (this) {
- is DuplicateMessage, is InvalidMessage, is UnknownMessage,
- is UnknownEnvelopeType, is InvalidSignature, is NoData,
- is SenderBlocked, is SelfSend,
- is ExpiredMessage, is NoGroupThread -> false
- else -> true
- }
- }
-
- internal fun parse(
- data: ByteArray,
- openGroupServerID: Long?,
- isOutgoing: Boolean? = null,
- otherBlindedPublicKey: String? = null,
- openGroupPublicKey: String? = null,
- currentClosedGroups: Set?,
- closedGroupSessionId: String? = null,
- ): Pair {
- val userPublicKey = storage.getUserPublicKey()
- val isOpenGroupMessage = (openGroupServerID != null)
- var plaintext: ByteArray? = null
- var sender: String? = null
- var groupPublicKey: String? = null
- // Parse the envelope
- val envelope = Envelope.parseFrom(data) ?: throw Error.InvalidMessage
- // Decrypt the contents
- val envelopeContent = envelope.content ?: run {
- throw Error.NoData
- }
-
- if (isOpenGroupMessage) {
- plaintext = envelopeContent.toByteArray()
- sender = envelope.source
- } else {
- when (envelope.type) {
- SignalServiceProtos.Envelope.Type.SESSION_MESSAGE -> {
- if (IdPrefix.fromValue(envelope.source)?.isBlinded() == true) {
- openGroupPublicKey ?: throw Error.InvalidGroupPublicKey
- otherBlindedPublicKey ?: throw Error.DecryptionFailed
- val decryptionResult = MessageDecrypter.decryptBlinded(
- envelopeContent.toByteArray(),
- isOutgoing ?: false,
- otherBlindedPublicKey,
- openGroupPublicKey
- )
- plaintext = decryptionResult.first
- sender = decryptionResult.second
- } else {
- val userX25519KeyPair = storage.getUserX25519KeyPair()
- val decryptionResult = MessageDecrypter.decrypt(envelopeContent.toByteArray(), userX25519KeyPair)
- plaintext = decryptionResult.first
- sender = decryptionResult.second
- }
- }
- SignalServiceProtos.Envelope.Type.CLOSED_GROUP_MESSAGE -> {
- val hexEncodedGroupPublicKey = closedGroupSessionId ?: envelope.source
- val sessionId = AccountId(hexEncodedGroupPublicKey)
- if (sessionId.prefix == IdPrefix.GROUP) {
- plaintext = envelopeContent.toByteArray()
- sender = envelope.source
- groupPublicKey = hexEncodedGroupPublicKey
- } else {
- if (!storage.isLegacyClosedGroup(hexEncodedGroupPublicKey)) {
- throw Error.InvalidGroupPublicKey
- }
- val encryptionKeyPairs = storage.getClosedGroupEncryptionKeyPairs(hexEncodedGroupPublicKey)
- if (encryptionKeyPairs.isEmpty()) {
- throw Error.NoGroupKeyPair
- }
- // Loop through all known group key pairs in reverse order (i.e. try the latest key pair first (which'll more than
- // likely be the one we want) but try older ones in case that didn't work)
- var encryptionKeyPair = encryptionKeyPairs.removeAt(encryptionKeyPairs.lastIndex)
- fun decrypt() {
- try {
- val decryptionResult = MessageDecrypter.decrypt(envelopeContent.toByteArray(),
- KeyPair(
- pubKey = encryptionKeyPair.publicKey.serialize(),
- secretKey = encryptionKeyPair.privateKey.serialize()
- ))
- plaintext = decryptionResult.first
- sender = decryptionResult.second
- } catch (e: Exception) {
- if (encryptionKeyPairs.isNotEmpty()) {
- encryptionKeyPair = encryptionKeyPairs.removeAt(encryptionKeyPairs.lastIndex)
- decrypt()
- } else {
- Log.e("Loki", "Failed to decrypt group message", e)
- throw e
- }
- }
- }
- groupPublicKey = hexEncodedGroupPublicKey
- decrypt()
- }
- }
- else -> {
- throw Error.UnknownEnvelopeType
- }
- }
- }
- // Parse the proto
- val proto = SignalServiceProtos.Content.parseFrom(PushTransportDetails.getStrippedPaddingMessageBody(plaintext))
-
- // Verify the signature timestamp inside the content is the same as in envelope.
- // If the message is from an open group, 6 hours of difference is allowed.
- if (proto.hasSigTimestampMs()) {
- val isCommunityOrCommunityInbox = openGroupServerID != null || otherBlindedPublicKey != null
-
- if (
- (isCommunityOrCommunityInbox && abs(proto.sigTimestampMs - envelope.timestampMs) > TimeUnit.HOURS.toMillis(6)) ||
- (!isCommunityOrCommunityInbox && proto.sigTimestampMs != envelope.timestampMs)
- ) {
- throw Error.InvalidSignature
- }
- }
-
- // Parse the message
- val message: Message = ReadReceipt.fromProto(proto) ?:
- TypingIndicator.fromProto(proto) ?:
- DataExtractionNotification.fromProto(proto) ?:
- ExpirationTimerUpdate.fromProto(proto, closedGroupSessionId != null) ?:
- UnsendRequest.fromProto(proto) ?:
- MessageRequestResponse.fromProto(proto) ?:
- CallMessage.fromProto(proto) ?:
- GroupUpdated.fromProto(proto) ?:
- VisibleMessage.fromProto(proto) ?: throw Error.UnknownMessage
-
- // Don't process the envelope any further if the sender is blocked (still visible in community chats)
- if (!isOpenGroupMessage && isBlocked(sender!!) && message.shouldDiscardIfBlocked()) {
- throw Error.SenderBlocked
- }
- val isUserBlindedSender = sender == openGroupPublicKey?.let {
- BlindKeyAPI.blind15KeyPairOrNull(
- ed25519SecretKey = storage.getUserED25519KeyPair()!!.secretKey.data,
- serverPubKey = Hex.fromStringCondensed(it),
- )
- }?.let { AccountId(IdPrefix.BLINDED, it.pubKey.data).hexString }
- val isUserSender = sender == userPublicKey
-
- if (isUserSender || isUserBlindedSender) {
- // Ignore self send if needed
- if (!message.isSelfSendValid) throw Error.SelfSend
- message.isSenderSelf = true
- }
- // Guard against control messages in open groups
- if (isOpenGroupMessage && message !is VisibleMessage) {
- throw Error.InvalidMessage
- }
- // Finish parsing
- message.sender = sender
- message.recipient = userPublicKey
- message.sentTimestamp = envelope.timestampMs
- message.receivedTimestamp = if (envelope.hasServerTimestampMs()) envelope.serverTimestampMs else SnodeAPI.nowWithOffset
- message.groupPublicKey = groupPublicKey
- message.openGroupServerMessageID = openGroupServerID
- // Validate
- var isValid = message.isValid()
- if (message is VisibleMessage && !isValid && proto.dataMessage.attachmentsCount != 0) { isValid = true }
- if (!isValid) {
- throw Error.InvalidMessage
- }
- // If the message failed to process the first time around we retry it later (if the error is retryable). In this case the timestamp
- // will already be in the database but we don't want to treat the message as a duplicate. The isRetry flag is a simple workaround
- // for this issue.
- if (groupPublicKey != null && groupPublicKey !in (currentClosedGroups ?: emptySet()) && IdPrefix.fromValue(groupPublicKey) != IdPrefix.GROUP) {
- throw Error.NoGroupThread
- }
- if (storage.isDuplicateMessage(envelope.timestampMs)) { throw Error.DuplicateMessage }
- storage.addReceivedMessageTimestamp(envelope.timestampMs)
- // Return
- return Pair(message, proto)
- }
-
-}
\ No newline at end of file
diff --git a/app/src/main/java/org/session/libsession/messaging/sending_receiving/MessageRequestResponseHandler.kt b/app/src/main/java/org/session/libsession/messaging/sending_receiving/MessageRequestResponseHandler.kt
index a1db7543b2..99e517673e 100644
--- a/app/src/main/java/org/session/libsession/messaging/sending_receiving/MessageRequestResponseHandler.kt
+++ b/app/src/main/java/org/session/libsession/messaging/sending_receiving/MessageRequestResponseHandler.kt
@@ -1,19 +1,22 @@
package org.session.libsession.messaging.sending_receiving
-import network.loki.messenger.libsession_util.util.BitSet
+import network.loki.messenger.libsession_util.protocol.DecodedPro
import org.session.libsession.messaging.messages.Message
import org.session.libsession.messaging.messages.ProfileUpdateHandler
import org.session.libsession.messaging.messages.control.MessageRequestResponse
import org.session.libsession.messaging.messages.signal.IncomingMediaMessage
import org.session.libsession.messaging.messages.visible.VisibleMessage
+import org.session.libsession.network.SnodeClock
import org.session.libsession.utilities.Address
import org.session.libsession.utilities.Address.Companion.toAddress
import org.session.libsession.utilities.ConfigFactoryProtocol
import org.session.libsession.utilities.recipients.Recipient
import org.session.libsession.utilities.updateContact
import org.session.libsession.utilities.upsertContact
-import org.session.libsignal.protos.SignalServiceProtos
+import org.session.libsession.utilities.withMutableUserConfigs
+import org.session.libsession.utilities.withUserConfigs
import org.session.libsignal.utilities.Log
+import org.session.protos.SessionProtos
import org.thoughtcrime.securesms.database.BlindMappingRepository
import org.thoughtcrime.securesms.database.MmsDatabase
import org.thoughtcrime.securesms.database.RecipientRepository
@@ -31,6 +34,7 @@ class MessageRequestResponseHandler @Inject constructor(
private val smsDatabase: SmsDatabase,
private val threadDatabase: ThreadDatabase,
private val blindMappingRepository: BlindMappingRepository,
+ private val clock: SnodeClock,
) {
fun handleVisibleMessage(
@@ -69,7 +73,8 @@ class MessageRequestResponseHandler @Inject constructor(
fun handleExplicitRequestResponseMessage(
ctx: ReceivedMessageProcessor.MessageProcessingContext?,
message: MessageRequestResponse,
- proto: SignalServiceProtos.Content,
+ proto: SessionProtos.Content,
+ pro: DecodedPro?,
) {
val (sender, receiver) = fetchSenderAndReceiver(message) ?: return
// Always handle explicit request response
@@ -82,7 +87,7 @@ class MessageRequestResponseHandler @Inject constructor(
// Always process the profile update if any. We don't need
// to process profile for other kind of messages as they should be handled elsewhere
- ProfileUpdateHandler.Updates.create(proto)?.let { updates ->
+ ProfileUpdateHandler.Updates.create(proto, clock.currentTimeMillis(), pro)?.let { updates ->
profileUpdateHandler.get().handleProfileUpdate(
senderId = (sender.address as Address.Standard).accountId,
updates = updates,
diff --git a/app/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt b/app/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt
index 590dec0032..2777700af3 100644
--- a/app/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt
+++ b/app/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt
@@ -6,12 +6,10 @@ import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.SendChannel
import kotlinx.coroutines.launch
import kotlinx.coroutines.supervisorScope
+import network.loki.messenger.libsession_util.Namespace
import network.loki.messenger.libsession_util.PRIORITY_HIDDEN
import network.loki.messenger.libsession_util.PRIORITY_VISIBLE
-import network.loki.messenger.libsession_util.Namespace
-import network.loki.messenger.libsession_util.ReadableUserProfile
import network.loki.messenger.libsession_util.protocol.SessionProtocol
-import network.loki.messenger.libsession_util.util.BlindKeyAPI
import network.loki.messenger.libsession_util.util.ExpiryMode
import org.session.libsession.database.MessageDataProvider
import org.session.libsession.database.StorageProtocol
@@ -22,31 +20,40 @@ import org.session.libsession.messaging.messages.Message
import org.session.libsession.messaging.messages.applyExpiryMode
import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate
import org.session.libsession.messaging.messages.control.GroupUpdated
-import org.session.libsession.messaging.messages.control.MessageRequestResponse
import org.session.libsession.messaging.messages.control.UnsendRequest
import org.session.libsession.messaging.messages.visible.LinkPreview
import org.session.libsession.messaging.messages.visible.Quote
import org.session.libsession.messaging.messages.visible.VisibleMessage
-import org.session.libsession.messaging.open_groups.OpenGroupApi
import org.session.libsession.messaging.open_groups.OpenGroupApi.Capability
import org.session.libsession.messaging.open_groups.OpenGroupMessage
-import org.session.libsession.snode.SnodeAPI
-import org.session.libsession.snode.SnodeClock
+import org.session.libsession.messaging.open_groups.api.CommunityApiExecutor
+import org.session.libsession.messaging.open_groups.api.CommunityApiRequest
+import org.session.libsession.messaging.open_groups.api.SendDirectMessageApi
+import org.session.libsession.messaging.open_groups.api.SendMessageApi
+import org.session.libsession.messaging.open_groups.api.execute
+import org.session.libsession.network.SnodeClock
import org.session.libsession.snode.SnodeMessage
import org.session.libsession.utilities.Address
+import org.session.libsession.utilities.Address.Companion.toAddress
import org.session.libsession.utilities.ConfigFactoryProtocol
-import org.session.libsignal.protos.SignalServiceProtos
+import org.session.libsession.utilities.withGroupConfigs
+import org.session.libsession.utilities.withMutableUserConfigs
+import org.session.libsession.utilities.withUserConfigs
import org.session.libsignal.utilities.AccountId
import org.session.libsignal.utilities.Base64
import org.session.libsignal.utilities.Hex
import org.session.libsignal.utilities.IdPrefix
import org.session.libsignal.utilities.Log
+import org.session.protos.SessionProtos
+import org.thoughtcrime.securesms.api.snode.StoreMessageApi
+import org.thoughtcrime.securesms.api.swarm.SwarmApiExecutor
+import org.thoughtcrime.securesms.api.swarm.SwarmApiRequest
+import org.thoughtcrime.securesms.api.swarm.execute
+import org.thoughtcrime.securesms.auth.LoginStateRepository
import org.thoughtcrime.securesms.database.RecipientRepository
import org.thoughtcrime.securesms.dependencies.ManagerScope
import org.thoughtcrime.securesms.pro.copyFromLibSession
-import org.thoughtcrime.securesms.pro.db.ProDatabase
import org.thoughtcrime.securesms.service.ExpiringMessageManager
-import java.util.concurrent.TimeUnit
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.coroutines.cancellation.CancellationException
@@ -61,9 +68,14 @@ class MessageSender @Inject constructor(
private val messageDataProvider: MessageDataProvider,
private val messageSendJobFactory: MessageSendJob.Factory,
private val messageExpirationManager: ExpiringMessageManager,
- private val proDatabase: ProDatabase,
private val snodeClock: SnodeClock,
+ private val communityApiExecutor: CommunityApiExecutor,
+ private val sendCommunityMessageApiFactory: SendMessageApi.Factory,
+ private val sendCommunityDirectMessageApiFactory: SendDirectMessageApi.Factory,
+ private val swarmApiExecutor: SwarmApiExecutor,
+ private val storeSnodeMessageApiFactory: StoreMessageApi.Factory,
@param:ManagerScope private val scope: CoroutineScope,
+ private val loginStateRepository: LoginStateRepository,
) {
// Error
@@ -84,25 +96,25 @@ class MessageSender @Inject constructor(
}
- private fun SignalServiceProtos.DataMessage.Builder.copyProfileFromConfig() {
+ private fun SessionProtos.DataMessage.Builder.copyProfileFromConfig() {
configFactory.withUserConfigs {
val pic = it.userProfile.getPic()
profileBuilder.setDisplayName(it.userProfile.getName().orEmpty())
.setProfilePicture(pic.url)
- .setLastProfileUpdateSeconds(it.userProfile.getProfileUpdatedSeconds())
+ .setLastUpdateSeconds(it.userProfile.getProfileUpdatedSeconds())
setProfileKey(ByteString.copyFrom(pic.keyAsByteArray))
}
}
- private fun SignalServiceProtos.MessageRequestResponse.Builder.copyProfileFromConfig() {
+ private fun SessionProtos.MessageRequestResponse.Builder.copyProfileFromConfig() {
configFactory.withUserConfigs {
val pic = it.userProfile.getPic()
profileBuilder.setDisplayName(it.userProfile.getName().orEmpty())
.setProfilePicture(pic.url)
- .setLastProfileUpdateSeconds(it.userProfile.getProfileUpdatedSeconds())
+ .setLastUpdateSeconds(it.userProfile.getProfileUpdatedSeconds())
setProfileKey(ByteString.copyFrom(pic.keyAsByteArray))
}
@@ -117,15 +129,19 @@ class MessageSender @Inject constructor(
}
}
- private fun buildProto(msg: Message): SignalServiceProtos.Content {
+ private fun buildProto(msg: Message): SessionProtos.Content {
try {
- val builder = SignalServiceProtos.Content.newBuilder()
+ val builder = SessionProtos.Content.newBuilder()
msg.toProto(builder, messageDataProvider)
// Attach pro proof
- configFactory.withUserConfigs { it.userProfile.getProConfig() }?.proProof?.let { proof ->
- builder.proMessageBuilder.proofBuilder.copyFromLibSession(proof)
+ val proProof = configFactory.withUserConfigs { it.userProfile.getProConfig() }?.proProof
+ if (proProof != null && proProof.expiryMs > snodeClock.currentTimeMillis()) {
+ builder.proMessageBuilder.proofBuilder.copyFromLibSession(proProof)
+ } else {
+ // If we don't have any valid pro proof, clear the pro message
+ builder.clearProMessage()
}
// Attach the user's profile if needed
@@ -152,7 +168,7 @@ class MessageSender @Inject constructor(
"Missing user key"
}
// Set the timestamp, sender and recipient
- val messageSendTime = snodeClock.currentTimeMills()
+ val messageSendTime = snodeClock.currentTimeMillis()
if (message.sentTimestamp == null) {
message.sentTimestamp =
messageSendTime // Visible messages will already have their sent timestamp set
@@ -183,27 +199,34 @@ class MessageSender @Inject constructor(
throw Error.InvalidMessage()
}
+ val proRotatingEd25519PrivKey = configFactory.withUserConfigs { configs ->
+ configs.userProfile.getProConfig()
+ }?.rotatingPrivateKey?.data
+
+ val messagePlaintext = buildProto(message).toByteArray()
+
+
val messageContent = when (destination) {
is Destination.Contact -> {
SessionProtocol.encodeFor1o1(
- plaintext = buildProto(message).toByteArray(),
+ plaintext = messagePlaintext,
myEd25519PrivKey = userEd25519PrivKey,
timestampMs = message.sentTimestamp!!,
recipientPubKey = Hex.fromStringCondensed(destination.publicKey),
- proRotatingEd25519PrivKey = null,
+ proRotatingEd25519PrivKey = proRotatingEd25519PrivKey,
)
}
is Destination.ClosedGroup -> {
SessionProtocol.encodeForGroup(
- plaintext = buildProto(message).toByteArray(),
+ plaintext = messagePlaintext,
myEd25519PrivKey = userEd25519PrivKey,
timestampMs = message.sentTimestamp!!,
groupEd25519PublicKey = Hex.fromStringCondensed(destination.publicKey),
groupEd25519PrivateKey = configFactory.withGroupConfigs(AccountId(destination.publicKey)) {
it.groupKeys.groupEncKey()
},
- proRotatingEd25519PrivKey = null
+ proRotatingEd25519PrivKey = proRotatingEd25519PrivKey,
)
}
@@ -236,14 +259,28 @@ class MessageSender @Inject constructor(
"Unable to authorize group message send"
}
- SnodeAPI.sendMessage(
- auth = groupAuth,
- message = snodeMessage,
- namespace = Namespace.GROUP_MESSAGES(),
+ swarmApiExecutor.execute(
+ SwarmApiRequest(
+ swarmPubKeyHex = destination.publicKey,
+ api = storeSnodeMessageApiFactory.create(
+ message = snodeMessage,
+ auth = groupAuth,
+ namespace = Namespace.GROUP_MESSAGES(),
+ )
+ )
)
}
is Destination.Contact -> {
- SnodeAPI.sendMessage(snodeMessage, auth = null, namespace = Namespace.DEFAULT())
+ swarmApiExecutor.execute(
+ SwarmApiRequest(
+ swarmPubKeyHex = destination.publicKey,
+ api = storeSnodeMessageApiFactory.create(
+ message = snodeMessage,
+ auth = null,
+ namespace = Namespace.DEFAULT()
+ )
+ )
+ )
}
is Destination.OpenGroup,
is Destination.OpenGroupInbox -> throw IllegalStateException("Destination should not be an open group.")
@@ -295,7 +332,7 @@ class MessageSender @Inject constructor(
// Open Groups
private suspend fun sendToOpenGroupDestination(destination: Destination, message: Message) {
if (message.sentTimestamp == null) {
- message.sentTimestamp = snodeClock.currentTimeMills()
+ message.sentTimestamp = snodeClock.currentTimeMillis()
}
// Attach the blocks message requests info
configFactory.withUserConfigs { configs ->
@@ -303,25 +340,24 @@ class MessageSender @Inject constructor(
message.blocksMessageRequests = !configs.userProfile.getCommunityMessageRequests()
}
}
- val userEdKeyPair = storage.getUserED25519KeyPair()!!
var serverCapabilities: List
var blindedPublicKey: ByteArray? = null
+ val loggedInState = loginStateRepository.requireLoggedInState()
when (destination) {
is Destination.OpenGroup -> {
serverCapabilities = storage.getServerCapabilities(destination.server).orEmpty()
storage.getOpenGroupPublicKey(destination.server)?.let {
- blindedPublicKey = BlindKeyAPI.blind15KeyPairOrNull(
- ed25519SecretKey = userEdKeyPair.secretKey.data,
- serverPubKey = Hex.fromStringCondensed(it),
- )?.pubKey?.data
+ blindedPublicKey = loggedInState
+ .getBlindedKeyPair(serverUrl = destination.server, serverPubKeyHex = it)
+ .pubKey.data
}
}
is Destination.OpenGroupInbox -> {
serverCapabilities = storage.getServerCapabilities(destination.server).orEmpty()
- blindedPublicKey = BlindKeyAPI.blind15KeyPairOrNull(
- ed25519SecretKey = userEdKeyPair.secretKey.data,
- serverPubKey = Hex.fromStringCondensed(destination.serverPublicKey),
- )?.pubKey?.data
+ blindedPublicKey = loggedInState
+ .getBlindedKeyPair(serverUrl = destination.server,
+ serverPubKeyHex = destination.serverPublicKey)
+ .pubKey.data
}
is Destination.ClosedGroup,
@@ -330,7 +366,7 @@ class MessageSender @Inject constructor(
val messageSender = if (serverCapabilities.contains(Capability.BLIND.name.lowercase()) && blindedPublicKey != null) {
AccountId(IdPrefix.BLINDED, blindedPublicKey).hexString
} else {
- AccountId(IdPrefix.UN_BLINDED, userEdKeyPair.pubKey.data).hexString
+ AccountId(IdPrefix.UN_BLINDED, loggedInState.accountEd25519KeyPair.pubKey.data).hexString
}
message.sender = messageSender
@@ -347,7 +383,9 @@ class MessageSender @Inject constructor(
}
val plaintext = SessionProtocol.encodeForCommunity(
plaintext = content.toByteArray(),
- proRotatingEd25519PrivKey = null
+ proRotatingEd25519PrivKey = configFactory.withUserConfigs { configs ->
+ configs.userProfile.getProConfig()
+ }?.rotatingPrivateKey?.data,
)
val openGroupMessage = OpenGroupMessage(
@@ -356,17 +394,19 @@ class MessageSender @Inject constructor(
base64EncodedData = Base64.encodeBytes(plaintext),
)
- val response = OpenGroupApi.sendMessage(
- openGroupMessage,
- destination.roomToken,
- destination.server,
- destination.whisperTo,
- destination.whisperMods,
- destination.fileIds
+ val response = communityApiExecutor.execute(
+ CommunityApiRequest(
+ serverBaseUrl = destination.server,
+ api = sendCommunityMessageApiFactory.create(
+ room = destination.roomToken,
+ message = openGroupMessage,
+ fileIds = destination.fileIds
+ )
+ ),
)
- message.openGroupServerMessageID = response.serverID
- handleSuccessfulMessageSend(message, destination, openGroupSentTimestamp = response.sentTimestamp)
+ message.openGroupServerMessageID = response.id
+ handleSuccessfulMessageSend(message, destination, openGroupSentTimestamp = response.postedMills)
return
}
is Destination.OpenGroupInbox -> {
@@ -377,22 +417,24 @@ class MessageSender @Inject constructor(
}
val ciphertext = SessionProtocol.encodeForCommunityInbox(
plaintext = content.toByteArray(),
- myEd25519PrivKey = userEdKeyPair.secretKey.data,
+ myEd25519PrivKey = loggedInState.accountEd25519KeyPair.secretKey.data,
timestampMs = message.sentTimestamp!!,
recipientPubKey = Hex.fromStringCondensed(destination.blindedPublicKey),
communityServerPubKey = Hex.fromStringCondensed(destination.serverPublicKey),
proRotatingEd25519PrivKey = null,
)
- val base64EncodedData = Base64.encodeBytes(ciphertext)
- val response = OpenGroupApi.sendDirectMessage(
- base64EncodedData,
- destination.blindedPublicKey,
- destination.server
- )
+ val response = communityApiExecutor.execute(CommunityApiRequest(
+ serverBaseUrl = destination.server,
+ api = sendCommunityDirectMessageApiFactory.create(
+ recipient = destination.blindedPublicKey.toAddress() as Address.Blinded,
+ messageContent = Base64.encodeBytes(ciphertext)
+ )
+ ))
message.openGroupServerMessageID = response.id
- handleSuccessfulMessageSend(message, destination, openGroupSentTimestamp = TimeUnit.SECONDS.toMillis(response.postedAt))
+ handleSuccessfulMessageSend(message, destination,
+ openGroupSentTimestamp = response.postedAt?.toEpochMilli() ?: 0L)
return
}
else -> throw IllegalStateException("Invalid destination.")
diff --git a/app/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt b/app/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt
deleted file mode 100644
index 979e83c8cb..0000000000
--- a/app/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt
+++ /dev/null
@@ -1,829 +0,0 @@
-package org.session.libsession.messaging.sending_receiving
-
-import android.content.Context
-import android.text.TextUtils
-import dagger.assisted.Assisted
-import dagger.assisted.AssistedFactory
-import dagger.assisted.AssistedInject
-import dagger.hilt.android.qualifiers.ApplicationContext
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.launch
-import network.loki.messenger.R
-import network.loki.messenger.libsession_util.PRIORITY_HIDDEN
-import network.loki.messenger.libsession_util.PRIORITY_VISIBLE
-import network.loki.messenger.libsession_util.ED25519
-import network.loki.messenger.libsession_util.util.BaseCommunityInfo
-import network.loki.messenger.libsession_util.util.BlindKeyAPI
-import network.loki.messenger.libsession_util.util.ExpiryMode
-import network.loki.messenger.libsession_util.util.Util
-import org.session.libsession.database.MessageDataProvider
-import org.session.libsession.database.StorageProtocol
-import org.session.libsession.database.userAuth
-import org.session.libsession.messaging.MessagingModuleConfiguration
-import org.session.libsession.messaging.groups.GroupManagerV2
-import org.session.libsession.messaging.jobs.AttachmentDownloadJob
-import org.session.libsession.messaging.jobs.JobQueue
-import org.session.libsession.messaging.messages.Message
-import org.session.libsession.messaging.messages.ProfileUpdateHandler
-import org.session.libsession.messaging.messages.control.CallMessage
-import org.session.libsession.messaging.messages.control.DataExtractionNotification
-import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate
-import org.session.libsession.messaging.messages.control.GroupUpdated
-import org.session.libsession.messaging.messages.control.MessageRequestResponse
-import org.session.libsession.messaging.messages.control.ReadReceipt
-import org.session.libsession.messaging.messages.control.TypingIndicator
-import org.session.libsession.messaging.messages.control.UnsendRequest
-import org.session.libsession.messaging.messages.visible.Attachment
-import org.session.libsession.messaging.messages.visible.VisibleMessage
-import org.session.libsession.messaging.open_groups.OpenGroupApi
-import org.session.libsession.messaging.sending_receiving.attachments.PointerAttachment
-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.notifications.MessageNotifier
-import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel
-import org.session.libsession.messaging.utilities.MessageAuthentication.buildDeleteMemberContentSignature
-import org.session.libsession.messaging.utilities.MessageAuthentication.buildGroupInviteSignature
-import org.session.libsession.messaging.utilities.MessageAuthentication.buildInfoChangeSignature
-import org.session.libsession.messaging.utilities.MessageAuthentication.buildMemberChangeSignature
-import org.session.libsession.messaging.utilities.WebRtcUtils
-import org.session.libsession.snode.SnodeAPI
-import org.session.libsession.utilities.Address
-import org.session.libsession.utilities.Address.Companion.toAddress
-import org.session.libsession.utilities.ConfigFactoryProtocol
-import org.session.libsession.utilities.GroupRecord
-import org.session.libsession.utilities.GroupUtil.doubleEncodeGroupID
-import org.session.libsession.utilities.SSKEnvironment
-import org.session.libsession.utilities.TextSecurePreferences
-import org.session.libsession.utilities.recipients.MessageType
-import org.session.libsession.utilities.recipients.Recipient
-import org.session.libsession.utilities.recipients.RecipientData
-import org.session.libsession.utilities.recipients.getType
-import org.session.libsession.utilities.updateContact
-import org.session.libsession.utilities.upsertContact
-import org.session.libsignal.protos.SignalServiceProtos
-import org.session.libsignal.utilities.AccountId
-import org.session.libsignal.utilities.Hex
-import org.session.libsignal.utilities.IdPrefix
-import org.session.libsignal.utilities.Log
-import org.session.libsignal.utilities.guava.Optional
-import org.thoughtcrime.securesms.database.ConfigDatabase
-import org.thoughtcrime.securesms.database.RecipientRepository
-import org.thoughtcrime.securesms.database.model.MessageId
-import org.thoughtcrime.securesms.database.model.ReactionRecord
-import org.thoughtcrime.securesms.dependencies.ManagerScope
-import org.thoughtcrime.securesms.pro.ProStatusManager
-import org.thoughtcrime.securesms.sskenvironment.ReadReceiptManager
-import java.security.SignatureException
-import javax.inject.Inject
-import javax.inject.Provider
-import javax.inject.Singleton
-import kotlin.math.min
-
-internal fun MessageReceiver.isBlocked(publicKey: String): Boolean {
- val recipient = MessagingModuleConfiguration.shared.recipientRepository.getRecipientSync(Address.fromSerialized(publicKey))
- return recipient?.blocked == true
-}
-
-@Deprecated(replaceWith = ReplaceWith("ReceivedMessageProcessor"), message = "Use ReceivedMessageProcessor instead")
-@Singleton
-class ReceivedMessageHandler @Inject constructor(
- @param:ApplicationContext private val context: Context,
- private val storage: StorageProtocol,
- private val readReceiptManager: ReadReceiptManager,
- private val typingIndicators: SSKEnvironment.TypingIndicatorsProtocol,
- private val messageDataProvider: MessageDataProvider,
- private val messageExpirationManager: SSKEnvironment.MessageExpirationManagerProtocol,
- private val notificationManager: MessageNotifier,
- private val groupManagerV2: GroupManagerV2,
- private val proStatusManager: ProStatusManager,
- private val visibleMessageContextFactory: VisibleMessageHandlerContext.Factory,
- private val attachmentDownloadJobFactory: AttachmentDownloadJob.Factory,
- private val profileUpdateHandler: Provider,
- @param:ManagerScope private val scope: CoroutineScope,
- private val configFactory: ConfigFactoryProtocol,
- private val messageRequestResponseHandler: Provider,
- private val prefs: TextSecurePreferences,
-) {
-
- suspend fun handle(
- message: Message,
- proto: SignalServiceProtos.Content,
- threadId: Long,
- threadAddress: Address.Conversable,
- ) {
- // Do nothing if the message was outdated
- if (messageIsOutdated(message, threadId)) { return }
-
- when (message) {
- is ReadReceipt -> handleReadReceipt(message)
- is TypingIndicator -> handleTypingIndicator(message)
- is GroupUpdated -> handleGroupUpdated(
- message = message,
- closedGroup = (threadAddress as? Address.Group)?.accountId,
- proto = proto
- )
- is ExpirationTimerUpdate -> {
- // For groupsv2, there are dedicated mechanisms for handling expiration timers, and
- // we want to avoid the 1-to-1 message format which is unauthenticated in a group settings.
- if (threadAddress is Address.Group) {
- Log.d("MessageReceiver", "Ignoring expiration timer update for closed group")
- } // also ignore it for communities since they do not support disappearing messages
- else if (threadAddress is Address.Community) {
- Log.d("MessageReceiver", "Ignoring expiration timer update for communities")
- } else {
- handleExpirationTimerUpdate(message)
- }
- }
- is DataExtractionNotification -> handleDataExtractionNotification(message)
- is UnsendRequest -> handleUnsendRequest(message)
- is MessageRequestResponse -> messageRequestResponseHandler.get().handleExplicitRequestResponseMessage(null, message, proto)
- is VisibleMessage -> handleVisibleMessage(
- message = message,
- proto = proto,
- context = visibleMessageContextFactory.create(threadId, threadAddress),
- runThreadUpdate = true,
- runProfileUpdate = true
- )
- is CallMessage -> handleCallMessage(message)
- }
- }
-
- private fun messageIsOutdated(message: Message, threadId: Long): Boolean {
- when (message) {
- is ReadReceipt -> return false // No visible artifact created so better to keep for more reliable read states
- is UnsendRequest -> return false // We should always process the removal of messages just in case
- }
-
- // Determine the state of the conversation and the validity of the message
- val userPublicKey = storage.getUserPublicKey()!!
- val threadRecipient = storage.getRecipientForThread(threadId)
- val conversationExists = threadRecipient != null
- val canPerformChange = storage.canPerformConfigChange(
- if (threadRecipient?.address?.toString() == userPublicKey) ConfigDatabase.USER_PROFILE_VARIANT else ConfigDatabase.CONTACTS_VARIANT,
- userPublicKey,
- message.sentTimestamp!!
- )
-
- // If the thread is visible or the message was sent more recently than the last config message (minus
- // buffer period) then we should process the message, if not then the message is outdated
- return (!conversationExists && !canPerformChange)
- }
-
- private fun handleReadReceipt(message: ReadReceipt) {
- readReceiptManager.processReadReceipts(
- message.sender!!,
- message.timestamps!!,
- message.receivedTimestamp!!
- )
- }
-
- private fun handleCallMessage(message: CallMessage) {
- // TODO: refactor this out to persistence, just to help debug the flow and send/receive in synchronous testing
- WebRtcUtils.SIGNAL_QUEUE.trySend(message)
- }
-
- private fun handleTypingIndicator(message: TypingIndicator) {
- when (message.kind!!) {
- TypingIndicator.Kind.STARTED -> showTypingIndicatorIfNeeded(message.sender!!)
- TypingIndicator.Kind.STOPPED -> hideTypingIndicatorIfNeeded(message.sender!!)
- }
- }
-
- private fun showTypingIndicatorIfNeeded(senderPublicKey: String) {
- // We don't want to show other people's indicators if the toggle is off
- if(!prefs.isTypingIndicatorsEnabled()) return
-
- val address = Address.fromSerialized(senderPublicKey)
- val threadID = storage.getThreadId(address) ?: return
- typingIndicators.didReceiveTypingStartedMessage(threadID, address, 1)
- }
-
- private fun hideTypingIndicatorIfNeeded(senderPublicKey: String) {
- val address = Address.fromSerialized(senderPublicKey)
- val threadID = storage.getThreadId(address) ?: return
- typingIndicators.didReceiveTypingStoppedMessage(threadID, address, 1, false)
- }
-
- private fun cancelTypingIndicatorsIfNeeded(senderPublicKey: String) {
- val address = Address.fromSerialized(senderPublicKey)
- val threadID = storage.getThreadId(address) ?: return
- typingIndicators.didReceiveIncomingMessage(threadID, address, 1)
- }
-
- private fun handleExpirationTimerUpdate(message: ExpirationTimerUpdate) {
- messageExpirationManager.run {
- insertExpirationTimerMessage(message)
- onMessageReceived(message)
- }
- }
-
- private fun handleDataExtractionNotification(message: DataExtractionNotification) {
- // We don't handle data extraction messages for groups (they shouldn't be sent, but just in case we filter them here too)
- if (message.groupPublicKey != null) return
- val senderPublicKey = message.sender!!
-
- val notification: DataExtractionNotificationInfoMessage = when(message.kind) {
- is DataExtractionNotification.Kind.MediaSaved -> DataExtractionNotificationInfoMessage(DataExtractionNotificationInfoMessage.Kind.MEDIA_SAVED)
- else -> return
- }
- storage.insertDataExtractionNotificationMessage(senderPublicKey, notification, message.sentTimestamp!!)
- }
-
-
- fun handleUnsendRequest(message: UnsendRequest): MessageId? {
- val userPublicKey = storage.getUserPublicKey()
- val userAuth = storage.userAuth ?: return null
- val isLegacyGroupAdmin: Boolean = message.groupPublicKey?.let { key ->
- var admin = false
- val groupID = doubleEncodeGroupID(key)
- val group = storage.getGroup(groupID)
- if(group != null) {
- admin = group.admins.map { it.toString() }.contains(message.sender)
- }
- admin
- } ?: false
-
- // First we need to determine the validity of the UnsendRequest
- // It is valid if:
- val requestIsValid = message.sender == message.author || // the sender is the author of the message
- message.author == userPublicKey || // the sender is the current user
- isLegacyGroupAdmin // sender is an admin of legacy group
-
- if (!requestIsValid) { return null }
-
- val timestamp = message.timestamp ?: return null
- val author = message.author ?: return null
- val messageToDelete = storage.getMessageByTimestamp(timestamp, author, false) ?: return null
- val messageIdToDelete = messageToDelete.messageId
- val messageType = messageToDelete.individualRecipient?.getType()
-
- // send a /delete rquest for 1on1 messages
- if (messageType == MessageType.ONE_ON_ONE) {
- messageDataProvider.getServerHashForMessage(messageIdToDelete)?.let { serverHash ->
- scope.launch(Dispatchers.IO) { // using scope as we are slowly migrating to coroutines but we can't migrate everything at once
- try {
- SnodeAPI.deleteMessage(author, userAuth, listOf(serverHash))
- } catch (e: Exception) {
- Log.e("Loki", "Failed to delete message", e)
- }
- }
- }
- }
-
- // the message is marked as deleted locally
- // except for 'note to self' where the message is completely deleted
- if (messageType == MessageType.NOTE_TO_SELF){
- messageDataProvider.deleteMessage(messageIdToDelete)
- } else {
- messageDataProvider.markMessageAsDeleted(
- messageIdToDelete,
- displayedMessage = context.getString(R.string.deleteMessageDeletedGlobally)
- )
- }
-
- // delete reactions
- storage.deleteReactions(messageToDelete.messageId)
-
- // update notification
- if (!messageToDelete.isOutgoing) {
- notificationManager.updateNotification(context)
- }
-
- return messageIdToDelete
- }
-
- suspend fun handleVisibleMessage(
- message: VisibleMessage,
- proto: SignalServiceProtos.Content,
- context: VisibleMessageHandlerContext,
- runThreadUpdate: Boolean,
- runProfileUpdate: Boolean
- ): MessageId? {
- val userPublicKey = context.storage.getUserPublicKey()
- val senderAddress = message.sender!!.toAddress()
-
- // Do nothing if the message was outdated
- if (messageIsOutdated(message, context.threadId)) { return null }
-
- messageRequestResponseHandler.get().handleVisibleMessage(null, message)
-
- // Handle group invite response if new closed group
- val threadRecipientAddress = context.threadAddress
- if (threadRecipientAddress is Address.Group && senderAddress is Address.Standard) {
- scope.launch {
- try {
- groupManagerV2
- .handleInviteResponse(
- threadRecipientAddress.accountId,
- senderAddress.accountId,
- approved = true
- )
- } catch (e: Exception) {
- Log.e("Loki", "Failed to handle invite response", e)
- }
- }
- }
- // Parse quote if needed
- var quoteModel: QuoteModel? = null
- var quoteMessageBody: String? = null
- if (message.quote != null && proto.dataMessage.hasQuote()) {
- val quote = proto.dataMessage.quote
-
- val author = if (quote.author == context.userBlindedKey) {
- Address.fromSerialized(userPublicKey!!)
- } else {
- Address.fromSerialized(quote.author)
- }
-
- val messageInfo = messageDataProvider.getMessageForQuote(context.threadId, quote.id, author)
- quoteMessageBody = messageInfo?.third
- quoteModel = if (messageInfo != null) {
- val attachments = if (messageInfo.second) messageDataProvider.getAttachmentsAndLinkPreviewFor(messageInfo.first) else ArrayList()
- QuoteModel(quote.id, author,null,false, attachments)
- } else {
- QuoteModel(quote.id, author,null, true, PointerAttachment.forPointers(proto.dataMessage.quote.attachmentsList))
- }
- }
- // Parse link preview if needed
- val linkPreviews: MutableList = mutableListOf()
- if (message.linkPreview != null && proto.dataMessage.previewCount > 0) {
- for (preview in proto.dataMessage.previewList) {
- val thumbnail = PointerAttachment.forPointer(preview.image)
- val url = Optional.fromNullable(preview.url)
- val title = Optional.fromNullable(preview.title)
- val hasContent = !TextUtils.isEmpty(title.or("")) || thumbnail.isPresent
- if (hasContent) {
- val linkPreview = LinkPreview(url.get(), title.or(""), thumbnail)
- linkPreviews.add(linkPreview)
- } else {
- Log.w("Loki", "Discarding an invalid link preview. hasContent: $hasContent")
- }
- }
- }
- // Parse attachments if needed
- val attachments = proto.dataMessage.attachmentsList.map(Attachment::fromProto).filter { it.isValid() }
-
- // Cancel any typing indicators if needed
- cancelTypingIndicatorsIfNeeded(message.sender!!)
-
- // Parse reaction if needed
- val threadIsGroup = context.threadRecipient.isGroupOrCommunityRecipient
- message.reaction?.let { reaction ->
- if (reaction.react == true) {
- reaction.serverId = message.openGroupServerMessageID?.toString() ?: message.serverHash.orEmpty()
- reaction.dateSent = message.sentTimestamp ?: 0
- reaction.dateReceived = message.receivedTimestamp ?: 0
- context.storage.addReaction(
- threadId = context.threadId,
- reaction = reaction,
- messageSender = senderAddress.address,
- notifyUnread = !threadIsGroup
- )
- } else {
- context.storage.removeReaction(
- emoji = reaction.emoji!!,
- messageTimestamp = reaction.timestamp!!,
- threadId = context.threadId,
- author = senderAddress.address,
- notifyUnread = threadIsGroup
- )
- }
- } ?: run {
- // A user is mentioned if their public key is in the body of a message or one of their messages
- // was quoted
-
- // Verify the incoming message length and truncate it if needed, before saving it to the db
- val maxChars = proStatusManager.getIncomingMessageMaxLength(message)
- val messageText = message.text?.let { Util.truncateCodepoints(it, maxChars) } // truncate to max char limit for this message
- message.text = messageText
- message.hasMention = listOfNotNull(userPublicKey, context.userBlindedKey)
- .any { key ->
- messageText?.contains("@$key") == true || key == (quoteModel?.author?.toString() ?: "")
- }
-
- // Persist the message
- message.threadID = context.threadId
-
- // clean up the message - For example we do not want any expiration data on messages for communities
- if(message.openGroupServerMessageID != null){
- message.expiryMode = ExpiryMode.NONE
- }
-
- val messageID = context.storage.persist(
- threadRecipient = context.threadRecipient,
- message = message,
- quotes = quoteModel,
- linkPreview = linkPreviews,
- attachments = attachments,
- runThreadUpdate = runThreadUpdate
- ) ?: return null
-
- // If we have previously "hidden" the sender, we should flip the flag back to visible,
- // and this should only be done only for 1:1 messages
- if (senderAddress is Address.Standard && senderAddress.address != userPublicKey
- && context.threadAddress is Address.Standard) {
- val existingContact =
- configFactory.withUserConfigs { it.contacts.get(senderAddress.accountId.hexString) }
-
- if (existingContact != null && existingContact.priority == PRIORITY_HIDDEN) {
- Log.d(TAG, "Flipping thread for ${senderAddress.debugString} to visible")
- configFactory.withMutableUserConfigs { configs ->
- configs.contacts.updateContact(senderAddress) {
- priority = PRIORITY_VISIBLE
- }
- }
- } else if (existingContact == null || !existingContact.approvedMe) {
- // If we don't have the contact, create a new one with approvedMe = true
- Log.d(TAG, "Creating new contact for ${senderAddress.debugString} with approvedMe = true")
- configFactory.withMutableUserConfigs { configs ->
- configs.contacts.upsertContact(senderAddress) {
- approvedMe = true
- }
- }
- }
- }
-
- // Update profile if needed:
- // - must be done after the message is persisted)
- // - must be done after neccessary contact is created
- if (runProfileUpdate && senderAddress is Address.WithAccountId) {
- val updates = ProfileUpdateHandler.Updates.create(proto)
-
- if (updates != null) {
- profileUpdateHandler.get().handleProfileUpdate(
- senderId = senderAddress.accountId,
- updates = updates,
- fromCommunity = (context.threadRecipient.data as? RecipientData.Community)?.let { data ->
- BaseCommunityInfo(baseUrl = data.serverUrl, room = data.room, pubKeyHex = data.serverPubKey)
- },
- )
- }
- }
-
- // Parse & persist attachments
- // Start attachment downloads if needed
- if (messageID.mms && (context.threadRecipient.autoDownloadAttachments == true || senderAddress.address == userPublicKey)) {
- context.storage.getAttachmentsForMessage(messageID.id).iterator().forEach { attachment ->
- attachment.attachmentId?.let { id ->
- JobQueue.shared.add(attachmentDownloadJobFactory.create(
- attachmentID = id.rowId,
- mmsMessageId = messageID.id
- ))
- }
- }
- }
- message.openGroupServerMessageID?.let {
- context.storage.setOpenGroupServerMessageID(
- messageID = messageID,
- serverID = it,
- threadID = context.threadId
- )
- }
- message.id = messageID
- context.messageExpirationManager.onMessageReceived(message)
- return messageID
- }
- return null
- }
-
- private fun handleGroupUpdated(message: GroupUpdated, closedGroup: AccountId?, proto: SignalServiceProtos.Content) {
- val inner = message.inner
- if (closedGroup == null &&
- !inner.hasInviteMessage() && !inner.hasPromoteMessage()) {
- throw NullPointerException("Message wasn't polled from a closed group!")
- }
-
- // Update profile if needed
- ProfileUpdateHandler.Updates.create(proto)?.let { updates ->
- profileUpdateHandler.get().handleProfileUpdate(
- senderId = AccountId(message.sender!!),
- updates = updates,
- fromCommunity = null // Groupv2 is not a community
- )
- }
-
- when {
- inner.hasInviteMessage() -> handleNewLibSessionClosedGroupMessage(message, proto)
- inner.hasInviteResponse() -> handleInviteResponse(message, closedGroup!!)
- inner.hasPromoteMessage() -> handlePromotionMessage(message, proto)
- inner.hasInfoChangeMessage() -> handleGroupInfoChange(message, closedGroup!!)
- inner.hasMemberChangeMessage() -> handleMemberChange(message, closedGroup!!)
- inner.hasMemberLeftMessage() -> handleMemberLeft(message, closedGroup!!)
- inner.hasMemberLeftNotificationMessage() -> handleMemberLeftNotification(message, closedGroup!!)
- inner.hasDeleteMemberContent() -> handleDeleteMemberContent(message, closedGroup!!)
- }
- }
-
- private fun handleDeleteMemberContent(message: GroupUpdated, closedGroup: AccountId) {
- val deleteMemberContent = message.inner.deleteMemberContent
- val adminSig = if (deleteMemberContent.hasAdminSignature()) deleteMemberContent.adminSignature.toByteArray()!! else byteArrayOf()
-
- val hasValidAdminSignature = adminSig.isNotEmpty() && runCatching {
- verifyAdminSignature(
- closedGroup,
- adminSig,
- buildDeleteMemberContentSignature(
- memberIds = deleteMemberContent.memberSessionIdsList.asSequence().map(::AccountId).asIterable(),
- messageHashes = deleteMemberContent.messageHashesList,
- timestamp = message.sentTimestamp!!,
- )
- )
- }.isSuccess
-
- scope.launch {
- try {
- groupManagerV2.handleDeleteMemberContent(
- groupId = closedGroup,
- deleteMemberContent = deleteMemberContent,
- timestamp = message.sentTimestamp!!,
- sender = AccountId(message.sender!!),
- senderIsVerifiedAdmin = hasValidAdminSignature
- )
- } catch (e: Exception) {
- Log.e("GroupUpdated", "Failed to handle delete member content", e)
- }
- }
- }
-
- private fun handleMemberChange(message: GroupUpdated, closedGroup: AccountId) {
- val memberChange = message.inner.memberChangeMessage
- val type = memberChange.type
- val timestamp = message.sentTimestamp!!
- verifyAdminSignature(closedGroup,
- memberChange.adminSignature.toByteArray(),
- buildMemberChangeSignature(type, timestamp)
- )
- storage.insertGroupInfoChange(message, closedGroup)
- }
-
- private fun handleMemberLeft(message: GroupUpdated, closedGroup: AccountId) {
- scope.launch(Dispatchers.Default) {
- try {
- groupManagerV2.handleMemberLeftMessage(
- AccountId(message.sender!!), closedGroup
- )
- } catch (e: Exception) {
- Log.e("GroupUpdated", "Failed to handle member left message", e)
- }
- }
- }
-
- private fun handleMemberLeftNotification(message: GroupUpdated, closedGroup: AccountId) {
- storage.insertGroupInfoChange(message, closedGroup)
- }
-
- private fun handleGroupInfoChange(message: GroupUpdated, closedGroup: AccountId) {
- val inner = message.inner
- val infoChanged = inner.infoChangeMessage ?: return
- if (!infoChanged.hasAdminSignature()) return Log.e("GroupUpdated", "Info changed message doesn't contain admin signature")
- val adminSignature = infoChanged.adminSignature
- val type = infoChanged.type
- val timestamp = message.sentTimestamp!!
- verifyAdminSignature(closedGroup, adminSignature.toByteArray(), buildInfoChangeSignature(type, timestamp))
-
- groupManagerV2.handleGroupInfoChange(message, closedGroup)
- }
-
- private fun handlePromotionMessage(message: GroupUpdated, proto: SignalServiceProtos.Content) {
- val promotion = message.inner.promoteMessage
- val seed = promotion.groupIdentitySeed.toByteArray()
- val sender = message.sender!!
- val adminId = AccountId(sender)
- scope.launch {
- try {
- groupManagerV2
- .handlePromotion(
- groupId = AccountId(IdPrefix.GROUP, ED25519.generate(seed).pubKey.data),
- groupName = promotion.name,
- adminKeySeed = seed,
- promoter = adminId,
- promoterName = if (proto.hasDataMessage() && proto.dataMessage.hasProfile() && proto.dataMessage.profile.hasDisplayName())
- proto.dataMessage.profile.displayName
- else null,
- promoteMessageHash = message.serverHash!!,
- promoteMessageTimestamp = message.sentTimestamp!!,
- )
- } catch (e: Exception) {
- Log.e("GroupUpdated", "Failed to handle promotion message", e)
- }
- }
- }
-
- private fun handleInviteResponse(message: GroupUpdated, closedGroup: AccountId) {
- val sender = message.sender!!
- // val profile = message // maybe we do need data to be the inner so we can access profile
- val approved = message.inner.inviteResponse.isApproved
- scope.launch {
- try {
- groupManagerV2.handleInviteResponse(closedGroup, AccountId(sender), approved)
- } catch (e: Exception) {
- Log.e("GroupUpdated", "Failed to handle invite response", e)
- }
- }
- }
-
- private fun handleNewLibSessionClosedGroupMessage(message: GroupUpdated, proto: SignalServiceProtos.Content) {
- val storage = storage
- val ourUserId = storage.getUserPublicKey()!!
- val invite = message.inner.inviteMessage
- val groupId = AccountId(invite.groupSessionId)
- verifyAdminSignature(
- groupSessionId = groupId,
- signatureData = invite.adminSignature.toByteArray(),
- messageToValidate = buildGroupInviteSignature(AccountId(ourUserId), message.sentTimestamp!!)
- )
-
- val sender = message.sender!!
- val adminId = AccountId(sender)
- scope.launch {
- try {
- groupManagerV2
- .handleInvitation(
- groupId = groupId,
- groupName = invite.name,
- authData = invite.memberAuthData.toByteArray(),
- inviter = adminId,
- inviterName = if (proto.hasDataMessage() && proto.dataMessage.hasProfile() && proto.dataMessage.profile.hasDisplayName())
- proto.dataMessage.profile.displayName
- else null,
- inviteMessageHash = message.serverHash!!,
- inviteMessageTimestamp = message.sentTimestamp!!,
- )
- } catch (e: Exception) {
- Log.e("GroupUpdated", "Failed to handle invite message", e)
- }
- }
- }
-
-
- /**
- * Does nothing on successful signature verification, throws otherwise.
- * Assumes the signer is using the ed25519 group key signing key
- * @param groupSessionId the AccountId of the group to check the signature against
- * @param signatureData the byte array supplied to us through a protobuf message from the admin
- * @param messageToValidate the expected values used for this signature generation, often something like `INVITE||{inviteeSessionId}||{timestamp}`
- * @throws SignatureException if signature cannot be verified with given parameters
- */
- private fun verifyAdminSignature(groupSessionId: AccountId, signatureData: ByteArray, messageToValidate: ByteArray) {
- val groupPubKey = groupSessionId.pubKeyBytes
- if (!ED25519.verify(signature = signatureData, ed25519PublicKey = groupPubKey, message = messageToValidate)) {
- throw SignatureException("Verification failed for signature data")
- }
- }
-
- private fun isValidGroupUpdate(group: GroupRecord, sentTimestamp: Long, senderPublicKey: String): Boolean {
- val oldMembers = group.members.map { it.toString() }
- // Check that the message isn't from before the group was created
- if (group.formationTimestamp > sentTimestamp) {
- Log.d("Loki", "Ignoring closed group update from before thread was created.")
- return false
- }
- // Check that the sender is a member of the group (before the update)
- if (senderPublicKey !in oldMembers) {
- Log.d("Loki", "Ignoring closed group info message from non-member.")
- return false
- }
- return true
- }
-
- companion object {
- private const val TAG = "ReceivedMessageHandler"
- }
-
-}
-
-
-
-
-// region Control Messages
-
-
-//endregion
-
-private fun SignalServiceProtos.Content.ExpirationType.expiryMode(durationSeconds: Long) = takeIf { durationSeconds > 0 }?.let {
- when (it) {
- SignalServiceProtos.Content.ExpirationType.DELETE_AFTER_READ -> ExpiryMode.AfterRead(durationSeconds)
- SignalServiceProtos.Content.ExpirationType.DELETE_AFTER_SEND, SignalServiceProtos.Content.ExpirationType.UNKNOWN -> ExpiryMode.AfterSend(durationSeconds)
- else -> ExpiryMode.NONE
- }
-} ?: ExpiryMode.NONE
-
-
-class VisibleMessageHandlerContext @AssistedInject constructor(
- @param:ApplicationContext val context: Context,
- @Assisted val threadAddress: Address.Conversable,
- @Assisted val threadId: Long,
- val storage: StorageProtocol,
- val groupManagerV2: GroupManagerV2,
- val messageExpirationManager: SSKEnvironment.MessageExpirationManagerProtocol,
- val messageDataProvider: MessageDataProvider,
- val recipientRepository: RecipientRepository,
-) {
- val userBlindedKey: String? by lazy {
- (threadRecipient.data as? RecipientData.Community)?.let {
- val blindedKey = BlindKeyAPI.blind15KeyPairOrNull(
- ed25519SecretKey = storage.getUserED25519KeyPair()!!.secretKey.data,
- serverPubKey = Hex.fromStringCondensed(it.serverPubKey),
- ) ?: return@let null
-
- AccountId(
- IdPrefix.BLINDED, blindedKey.pubKey.data
- ).hexString
- }
- }
-
- val threadRecipient: Recipient by lazy {
- recipientRepository.getRecipientSync(threadAddress)
- }
-
- val userPublicKey: String? by lazy {
- storage.getUserPublicKey()
- }
-
-
- @AssistedFactory
- interface Factory {
- fun create(threadId: Long, threadAddress: Address.Conversable): VisibleMessageHandlerContext
- }
-}
-
-
-/**
- * Constructs reaction records for a given open group message.
- *
- * If the open group message exists in our database, we'll construct a list of reaction records
- * that is specified in the [reactions].
- *
- * Note that this function does not know or check if the local message has any reactions,
- * you'll be responsible for that. In simpler words, [out] only contains reactions that are given
- * to this function, it will not include any existing reactions in the database.
- *
- * @param openGroupMessageServerID The server ID of this message
- * @param context The context containing necessary data for processing reactions
- * @param reactions A map of emoji to [OpenGroupApi.Reaction] objects, representing the reactions for the message
- * @param out A mutable map that will be populated with [ReactionRecord]s, keyed by [MessageId]
- */
-fun constructReactionRecords(
- openGroupMessageServerID: Long,
- context: VisibleMessageHandlerContext,
- reactions: Map?,
- out: MutableMap>
-) {
- if (reactions.isNullOrEmpty()) return
- if (context.threadAddress !is Address.Community) return
- val messageId = context.messageDataProvider.getMessageID(openGroupMessageServerID, context.threadId) ?: return
-
- val outList = out.getOrPut(messageId) { arrayListOf() }
-
- for ((emoji, reaction) in reactions) {
- val shouldAddUserReaction = reaction.you || reaction.reactors.contains(context.userPublicKey)
- val reactorIds = reaction.reactors.filter { it != context.userBlindedKey && it != context.userPublicKey }
- val count = if (reaction.you) reaction.count - 1 else reaction.count
- // Add the first reaction (with the count)
- reactorIds.firstOrNull()?.let { reactor ->
- outList += ReactionRecord(
- messageId = messageId,
- author = reactor,
- emoji = emoji,
- serverId = openGroupMessageServerID.toString(),
- count = count,
- sortId = reaction.index,
- )
- }
-
- // Add all other reactions
- val maxAllowed = if (shouldAddUserReaction) 4 else 5
- val lastIndex = min(maxAllowed, reactorIds.size)
- reactorIds.slice(1 until lastIndex).map { reactor ->
- outList += ReactionRecord(
- messageId = messageId,
- author = reactor,
- emoji = emoji,
- serverId = openGroupMessageServerID.toString(),
- count = 0, // Only want this on the first reaction
- sortId = reaction.index,
- )
- }
-
- // Add the current user reaction (if applicable and not already included)
- if (shouldAddUserReaction) {
- outList += ReactionRecord(
- messageId = messageId,
- author = context.userPublicKey!!,
- emoji = emoji,
- serverId = openGroupMessageServerID.toString(),
- count = 1,
- sortId = reaction.index,
- )
- }
- }
-}
-
-//endregion
-
-// region Closed Groups
-
-
-
-// endregion
diff --git a/app/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageProcessor.kt b/app/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageProcessor.kt
index cdb269e165..4f88ad205a 100644
--- a/app/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageProcessor.kt
+++ b/app/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageProcessor.kt
@@ -3,10 +3,10 @@ package org.session.libsession.messaging.sending_receiving
import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import network.loki.messenger.R
import network.loki.messenger.libsession_util.PRIORITY_HIDDEN
+import network.loki.messenger.libsession_util.protocol.DecodedPro
import network.loki.messenger.libsession_util.util.BaseCommunityInfo
import network.loki.messenger.libsession_util.util.BlindKeyAPI
import network.loki.messenger.libsession_util.util.KeyPair
@@ -28,7 +28,6 @@ import org.session.libsession.messaging.open_groups.OpenGroupApi
import org.session.libsession.messaging.sending_receiving.data_extraction.DataExtractionNotificationInfoMessage
import org.session.libsession.messaging.sending_receiving.notifications.MessageNotifier
import org.session.libsession.messaging.utilities.WebRtcUtils
-import org.session.libsession.snode.SnodeAPI
import org.session.libsession.utilities.Address
import org.session.libsession.utilities.Address.Companion.toAddress
import org.session.libsession.utilities.ConfigFactoryProtocol
@@ -39,9 +38,14 @@ import org.session.libsession.utilities.UserConfigType
import org.session.libsession.utilities.recipients.MessageType
import org.session.libsession.utilities.recipients.Recipient
import org.session.libsession.utilities.recipients.getType
-import org.session.libsignal.protos.SignalServiceProtos
+import org.session.libsession.utilities.withUserConfigs
import org.session.libsignal.utilities.AccountId
import org.session.libsignal.utilities.Log
+import org.session.protos.SessionProtos
+import org.thoughtcrime.securesms.api.snode.DeleteMessageApi
+import org.thoughtcrime.securesms.api.swarm.SwarmApiExecutor
+import org.thoughtcrime.securesms.api.swarm.SwarmApiRequest
+import org.thoughtcrime.securesms.api.swarm.execute
import org.thoughtcrime.securesms.database.BlindMappingRepository
import org.thoughtcrime.securesms.database.RecipientRepository
import org.thoughtcrime.securesms.database.Storage
@@ -75,6 +79,8 @@ class ReceivedMessageProcessor @Inject constructor(
private val visibleMessageHandler: Provider,
private val blindMappingRepository: BlindMappingRepository,
private val messageParser: MessageParser,
+ private val swarmApiExecutor: SwarmApiExecutor,
+ private val deleteMessageApiFactory: DeleteMessageApi.Factory
) {
private val threadMutexes = ConcurrentHashMap()
@@ -129,7 +135,8 @@ class ReceivedMessageProcessor @Inject constructor(
context: MessageProcessingContext,
threadAddress: Address.Conversable,
message: Message,
- proto: SignalServiceProtos.Content,
+ proto: SessionProtos.Content,
+ pro: DecodedPro?,
) = withThreadLock(threadAddress) {
// The logic to check if the message should be discarded due to being from a hidden contact.
if (threadAddress is Address.Standard &&
@@ -149,9 +156,9 @@ class ReceivedMessageProcessor @Inject constructor(
threadDatabase.getOrCreateThreadIdFor(threadAddress)
.also { context.threadIDs[threadAddress] = it }
} else {
- threadDatabase.getThreadIdIfExistsFor(threadAddress)
+ storage.getThreadId(threadAddress)
.also { id ->
- if (id == -1L) {
+ if (id == null) {
log { "Dropping message for non-existing thread ${threadAddress.debugString}" }
return@withThreadLock
} else {
@@ -166,7 +173,8 @@ class ReceivedMessageProcessor @Inject constructor(
is GroupUpdated -> groupMessageHandler.get().handleGroupUpdated(
message = message,
groupId = (threadAddress as? Address.Group)?.accountId,
- proto = proto
+ proto = proto,
+ pro = pro,
)
is ExpirationTimerUpdate -> {
@@ -185,7 +193,7 @@ class ReceivedMessageProcessor @Inject constructor(
is DataExtractionNotification -> handleDataExtractionNotification(message)
is UnsendRequest -> handleUnsendRequest(message)
is MessageRequestResponse -> messageRequestResponseHandler.get()
- .handleExplicitRequestResponseMessage(context, message, proto)
+ .handleExplicitRequestResponseMessage(context, message, proto, pro)
is VisibleMessage -> {
if (message.isSenderSelf &&
@@ -195,15 +203,18 @@ class ReceivedMessageProcessor @Inject constructor(
context.maxOutgoingMessageTimestamp = message.sentTimestamp!!
}
- visibleMessageHandler.get().handleVisibleMessage(
- ctx = context,
- message = message,
- threadId = threadId,
- threadAddress = threadAddress,
- proto = proto,
- runThreadUpdate = false,
- runProfileUpdate = true,
- )
+ threadId?.let {
+ visibleMessageHandler.get().handleVisibleMessage(
+ ctx = context,
+ message = message,
+ threadId = it,
+ threadAddress = threadAddress,
+ proto = proto,
+ runThreadUpdate = false,
+ runProfileUpdate = true,
+ pro = pro,
+ )
+ }
}
is CallMessage -> handleCallMessage(message)
@@ -217,7 +228,7 @@ class ReceivedMessageProcessor @Inject constructor(
communityServerPubKeyHex: String,
message: OpenGroupApi.DirectMessage
) {
- val (message, proto) = messageParser.parseCommunityDirectMessage(
+ val parseResult = messageParser.parseCommunityDirectMessage(
msg = message,
currentUserId = context.currentUserId,
currentUserEd25519PrivKey = context.currentUserEd25519KeyPair.secretKey.data,
@@ -225,14 +236,15 @@ class ReceivedMessageProcessor @Inject constructor(
communityServerPubKeyHex = communityServerPubKeyHex,
)
- val threadAddress = message.senderOrSync.toAddress() as Address.Conversable
+ val threadAddress = parseResult.message.senderOrSync.toAddress() as Address.Conversable
withThreadLock(threadAddress) {
processSwarmMessage(
context = context,
threadAddress = threadAddress,
- message = message,
- proto = proto
+ message = parseResult.message,
+ proto = parseResult.proto,
+ pro = parseResult.pro
)
}
}
@@ -243,7 +255,7 @@ class ReceivedMessageProcessor @Inject constructor(
communityServerPubKeyHex: String,
msg: OpenGroupApi.DirectMessage
) {
- val (message, proto) = messageParser.parseCommunityDirectMessage(
+ val parseResult = messageParser.parseCommunityDirectMessage(
msg = msg,
currentUserId = context.currentUserId,
currentUserEd25519PrivKey = context.currentUserEd25519KeyPair.secretKey.data,
@@ -260,8 +272,9 @@ class ReceivedMessageProcessor @Inject constructor(
processSwarmMessage(
context = context,
threadAddress = threadAddress,
- message = message,
- proto = proto
+ message = parseResult.message,
+ proto = parseResult.proto,
+ pro = parseResult.pro
)
}
}
@@ -275,15 +288,16 @@ class ReceivedMessageProcessor @Inject constructor(
msg = message,
currentUserId = context.currentUserId,
currentUserBlindedIDs = context.getCurrentUserBlindedIDsByThread(threadAddress)
- )?.let { (msg, proto) ->
+ )?.let { parseResult ->
processSwarmMessage(
context = context,
threadAddress = threadAddress,
- message = msg,
- proto = proto
+ message = parseResult.message,
+ proto = parseResult.proto,
+ pro = parseResult.pro
)
- msg.id
+ parseResult.message.id
}
// For community, we have a different way of handling reaction, this is outside of
@@ -381,7 +395,7 @@ class ReceivedMessageProcessor @Inject constructor(
* Return true if this message should result in the creation of a thread.
*/
private fun shouldCreateThread(message: Message): Boolean {
- return message is VisibleMessage
+ return message is VisibleMessage || message is GroupUpdated
}
private fun handleExpirationTimerUpdate(message: ExpirationTimerUpdate) {
@@ -443,9 +457,17 @@ class ReceivedMessageProcessor @Inject constructor(
// send a /delete rquest for 1on1 messages
if (messageType == MessageType.ONE_ON_ONE) {
messageDataProvider.getServerHashForMessage(messageIdToDelete)?.let { serverHash ->
- scope.launch(Dispatchers.IO) { // using scope as we are slowly migrating to coroutines but we can't migrate everything at once
+ scope.launch { // using scope as we are slowly migrating to coroutines but we can't migrate everything at once
try {
- SnodeAPI.deleteMessage(author, userAuth, listOf(serverHash))
+ swarmApiExecutor.execute(
+ SwarmApiRequest(
+ swarmPubKeyHex = userAuth.accountId.hexString,
+ api = deleteMessageApiFactory.create(
+ messageHashes = listOf(serverHash),
+ swarmAuth = userAuth
+ )
+ )
+ )
} catch (e: Exception) {
Log.e("Loki", "Failed to delete message", e)
}
diff --git a/app/src/main/java/org/session/libsession/messaging/sending_receiving/VisibleMessageHandler.kt b/app/src/main/java/org/session/libsession/messaging/sending_receiving/VisibleMessageHandler.kt
index 57ea0d6182..eea91d02d9 100644
--- a/app/src/main/java/org/session/libsession/messaging/sending_receiving/VisibleMessageHandler.kt
+++ b/app/src/main/java/org/session/libsession/messaging/sending_receiving/VisibleMessageHandler.kt
@@ -5,6 +5,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import network.loki.messenger.libsession_util.PRIORITY_HIDDEN
import network.loki.messenger.libsession_util.PRIORITY_VISIBLE
+import network.loki.messenger.libsession_util.protocol.DecodedPro
import network.loki.messenger.libsession_util.util.BaseCommunityInfo
import network.loki.messenger.libsession_util.util.ExpiryMode
import network.loki.messenger.libsession_util.util.Util
@@ -18,6 +19,7 @@ import org.session.libsession.messaging.messages.visible.VisibleMessage
import org.session.libsession.messaging.sending_receiving.attachments.PointerAttachment
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel
+import org.session.libsession.network.SnodeClock
import org.session.libsession.utilities.Address
import org.session.libsession.utilities.Address.Companion.toAddress
import org.session.libsession.utilities.SSKEnvironment
@@ -25,9 +27,11 @@ import org.session.libsession.utilities.isGroupOrCommunity
import org.session.libsession.utilities.recipients.RecipientData
import org.session.libsession.utilities.updateContact
import org.session.libsession.utilities.upsertContact
-import org.session.libsignal.protos.SignalServiceProtos
+import org.session.libsession.utilities.withMutableUserConfigs
+import org.session.libsession.utilities.withUserConfigs
import org.session.libsignal.utilities.Log
import org.session.libsignal.utilities.guava.Optional
+import org.session.protos.SessionProtos
import org.thoughtcrime.securesms.database.Storage
import org.thoughtcrime.securesms.database.model.MessageId
import org.thoughtcrime.securesms.dependencies.ConfigFactory
@@ -48,13 +52,15 @@ class VisibleMessageHandler @Inject constructor(
private val attachmentDownloadJobFactory: AttachmentDownloadJob.Factory,
private val messageExpirationManager: SSKEnvironment.MessageExpirationManagerProtocol,
private val typingIndicators: SSKEnvironment.TypingIndicatorsProtocol,
+ private val clock: SnodeClock,
){
fun handleVisibleMessage(
ctx: ReceivedMessageProcessor.MessageProcessingContext,
message: VisibleMessage,
+ pro: DecodedPro?,
threadId: Long,
threadAddress: Address.Conversable,
- proto: SignalServiceProtos.Content,
+ proto: SessionProtos.Content,
runThreadUpdate: Boolean,
runProfileUpdate: Boolean,
): MessageId? {
@@ -203,7 +209,11 @@ class VisibleMessageHandler @Inject constructor(
// - must be done after the message is persisted)
// - must be done after neccessary contact is created
if (runProfileUpdate && senderAddress is Address.WithAccountId) {
- val updates = ProfileUpdateHandler.Updates.create(proto)
+ val updates = ProfileUpdateHandler.Updates.create(
+ content = proto,
+ nowMills = clock.currentTimeMillis(),
+ pro = pro
+ )
if (updates != null) {
profileUpdateHandler.get().handleProfileUpdate(
diff --git a/app/src/main/java/org/session/libsession/messaging/sending_receiving/attachments/PointerAttachment.java b/app/src/main/java/org/session/libsession/messaging/sending_receiving/attachments/PointerAttachment.java
index 8d0d5b50e0..d14f2bd748 100644
--- a/app/src/main/java/org/session/libsession/messaging/sending_receiving/attachments/PointerAttachment.java
+++ b/app/src/main/java/org/session/libsession/messaging/sending_receiving/attachments/PointerAttachment.java
@@ -8,7 +8,7 @@
import org.session.libsignal.utilities.guava.Optional;
import org.session.libsignal.messages.SignalServiceAttachment;
import org.session.libsignal.utilities.Base64;
-import org.session.libsignal.protos.SignalServiceProtos;
+import org.session.protos.SessionProtos;
import java.util.LinkedList;
import java.util.List;
@@ -53,11 +53,11 @@ public static List forPointers(Optional forPointers(List pointers) {
+ public static List forPointers(List pointers) {
List results = new LinkedList<>();
if (pointers != null) {
- for (SignalServiceProtos.DataMessage.Quote.QuotedAttachment pointer : pointers) {
+ for (SessionProtos.DataMessage.Quote.QuotedAttachment pointer : pointers) {
Optional result = forPointer(pointer);
if (result.isPresent()) {
@@ -98,7 +98,7 @@ public static Optional forPointer(Optional
}
- public static Optional forPointer(SignalServiceProtos.AttachmentPointer pointer) {
+ public static Optional forPointer(SessionProtos.AttachmentPointer pointer) {
return Optional.of(new PointerAttachment(pointer.getContentType(),
AttachmentState.PENDING.getValue(),
(long)pointer.getSize(),
@@ -115,8 +115,8 @@ public static Optional forPointer(SignalServiceProtos.AttachmentPoin
pointer.getUrl()));
}
- public static Optional forPointer(SignalServiceProtos.DataMessage.Quote.QuotedAttachment pointer) {
- SignalServiceProtos.AttachmentPointer thumbnail = pointer.getThumbnail();
+ public static Optional forPointer(SessionProtos.DataMessage.Quote.QuotedAttachment pointer) {
+ SessionProtos.AttachmentPointer thumbnail = pointer.getThumbnail();
return Optional.of(new PointerAttachment(pointer.getContentType(),
AttachmentState.PENDING.getValue(),
diff --git a/app/src/main/java/org/session/libsession/messaging/sending_receiving/attachments/SessionServiceAttachmentStream.kt b/app/src/main/java/org/session/libsession/messaging/sending_receiving/attachments/SessionServiceAttachmentStream.kt
index 4af1d16acc..e2beca19b8 100644
--- a/app/src/main/java/org/session/libsession/messaging/sending_receiving/attachments/SessionServiceAttachmentStream.kt
+++ b/app/src/main/java/org/session/libsession/messaging/sending_receiving/attachments/SessionServiceAttachmentStream.kt
@@ -8,7 +8,7 @@ package org.session.libsession.messaging.sending_receiving.attachments
import android.util.Size
import com.google.protobuf.ByteString
import org.session.libsignal.utilities.guava.Optional
-import org.session.libsignal.protos.SignalServiceProtos
+import org.session.protos.SessionProtos
import org.session.libsignal.messages.SignalServiceAttachment as SAttachment
import java.io.InputStream
import kotlin.math.round
@@ -35,8 +35,8 @@ class SessionServiceAttachmentStream(val inputStream: InputStream?, contentType:
return false
}
- fun toProto(): SignalServiceProtos.AttachmentPointer? {
- val builder = SignalServiceProtos.AttachmentPointer.newBuilder()
+ fun toProto(): SessionProtos.AttachmentPointer? {
+ val builder = SessionProtos.AttachmentPointer.newBuilder()
builder.contentType = this.contentType
builder.fileName = this.filename
@@ -47,7 +47,7 @@ class SessionServiceAttachmentStream(val inputStream: InputStream?, contentType:
builder.size = this.length.toInt()
builder.key = this.key
builder.digest = ByteString.copyFrom(this.digest.get())
- builder.flags = if (this.voiceNote) SignalServiceProtos.AttachmentPointer.Flags.VOICE_MESSAGE.number else 0
+ builder.flags = if (this.voiceNote) SessionProtos.AttachmentPointer.Flags.VOICE_MESSAGE.number else 0
//TODO I did copy the behavior of iOS below, not sure if that's relevant here...
if (this.shouldHaveImageSize()) {
diff --git a/app/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/Server.kt b/app/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/NotificationServer.kt
similarity index 80%
rename from app/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/Server.kt
rename to app/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/NotificationServer.kt
index 0497fe2220..3d5ab2a326 100644
--- a/app/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/Server.kt
+++ b/app/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/NotificationServer.kt
@@ -1,6 +1,6 @@
package org.session.libsession.messaging.sending_receiving.notifications
-enum class Server(val url: String, val publicKey: String) {
+enum class NotificationServer(val url: String, val publicKey: String) {
LATEST("https://push.getsession.org", "d7557fe563e2610de876c0ac7341b62f3c82d5eea4b62c702392ea4368f51b3b"),
LEGACY("https://live.apns.getsession.org", "642a6585919742e5a2d4dc51244964fbcd8bcab2b75612407de58b810740d049")
}
diff --git a/app/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushRegistryV1.kt b/app/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushRegistryV1.kt
deleted file mode 100644
index 4a0509049f..0000000000
--- a/app/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushRegistryV1.kt
+++ /dev/null
@@ -1,78 +0,0 @@
-package org.session.libsession.messaging.sending_receiving.notifications
-
-import android.annotation.SuppressLint
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.GlobalScope
-import nl.komponents.kovenant.Promise
-import okhttp3.MediaType.Companion.toMediaType
-import okhttp3.Request
-import okhttp3.RequestBody.Companion.toRequestBody
-import org.session.libsession.messaging.MessagingModuleConfiguration
-import org.session.libsession.snode.OnionRequestAPI
-import org.session.libsession.snode.OnionResponse
-import org.session.libsession.snode.Version
-import org.session.libsession.snode.utilities.asyncPromise
-import org.session.libsession.snode.utilities.await
-import org.session.libsession.utilities.TextSecurePreferences
-import org.session.libsignal.utilities.JsonUtil
-import org.session.libsignal.utilities.emptyPromise
-import org.session.libsignal.utilities.retryWithUniformInterval
-
-@SuppressLint("StaticFieldLeak")
-object PushRegistryV1 {
- val context = MessagingModuleConfiguration.shared.context
- private const val MAX_RETRY_COUNT = 4
-
- private val server = Server.LEGACY
-
- @Suppress("OPT_IN_USAGE")
- private val scope: CoroutineScope = GlobalScope
-
- // Legacy Closed Groups
-
- fun subscribeGroup(
- closedGroupSessionId: String,
- isPushEnabled: Boolean = TextSecurePreferences.isPushEnabled(context),
- publicKey: String = MessagingModuleConfiguration.shared.storage.getUserPublicKey()!!
- ) = if (isPushEnabled) {
- performGroupOperation("subscribe_closed_group", closedGroupSessionId, publicKey)
- } else emptyPromise()
-
- fun unsubscribeGroup(
- closedGroupPublicKey: String,
- isPushEnabled: Boolean = TextSecurePreferences.isPushEnabled(context),
- publicKey: String = MessagingModuleConfiguration.shared.storage.getUserPublicKey()!!
- ) = if (isPushEnabled) {
- performGroupOperation("unsubscribe_closed_group", closedGroupPublicKey, publicKey)
- } else emptyPromise()
-
- private fun performGroupOperation(
- operation: String,
- closedGroupPublicKey: String,
- publicKey: String
- ): Promise<*, Exception> = scope.asyncPromise {
- val parameters = mapOf("closedGroupPublicKey" to closedGroupPublicKey, "pubKey" to publicKey)
- val url = "${server.url}/$operation"
- val body = JsonUtil.toJson(parameters).toRequestBody("application/json".toMediaType())
- val request = Request.Builder().url(url).post(body).build()
-
- retryWithUniformInterval(MAX_RETRY_COUNT) {
- sendOnionRequest(request)
- .await()
- .checkError()
- }
- }
-
- private fun OnionResponse.checkError() {
- check(code != null && code != 0) {
- "error: $message."
- }
- }
-
- private fun sendOnionRequest(request: Request): Promise = OnionRequestAPI.sendOnionRequest(
- request,
- server.url,
- server.publicKey,
- Version.V2
- )
-}
diff --git a/app/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/BasePoller.kt b/app/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/BasePoller.kt
new file mode 100644
index 0000000000..424ce51a43
--- /dev/null
+++ b/app/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/BasePoller.kt
@@ -0,0 +1,179 @@
+package org.session.libsession.messaging.sending_receiving.pollers
+
+import kotlinx.coroutines.CancellationException
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
+import org.session.libsignal.utilities.Log
+import org.thoughtcrime.securesms.util.AppVisibilityManager
+import org.thoughtcrime.securesms.util.NetworkConnectivity
+import kotlin.time.Clock
+import kotlin.time.Instant
+
+/**
+ * Base class for pollers that perform periodic polling operations. These poller will:
+ *
+ * 1. Run periodically when the app is in the foreground and there is network.
+ * 2. Adjust the polling interval based on success/failure of previous polls.
+ * 3. Expose the current polling state via [pollState]
+ * 4. Allow manual polling via [manualPollOnce]
+ *
+ * @param T The type of the result returned by the single polling.
+ */
+abstract class BasePoller(
+ private val networkConnectivity: NetworkConnectivity,
+ appVisibilityManager: AppVisibilityManager,
+ private val scope: CoroutineScope,
+) {
+ protected val logTag: String = this::class.java.simpleName
+ private val pollMutex = Mutex()
+
+ private val mutablePollState = MutableStateFlow>(PollState.Idle)
+
+ /**
+ * The current state of the poller.
+ */
+ val pollState: StateFlow> get() = mutablePollState
+
+ init {
+ scope.launch {
+ var numConsecutiveFailures = 0
+
+ while (true) {
+ // Wait until the app is in the foreground and we have network connectivity
+ combine(
+ appVisibilityManager.isAppVisible.filter { visible ->
+ if (visible) {
+ true
+ } else {
+ Log.d(logTag, "Polling paused - app in background")
+ false
+ }
+ },
+ networkConnectivity.networkAvailable.filter { hasNetwork ->
+ if (hasNetwork) {
+ true
+ } else {
+ Log.d(logTag, "Polling paused - no network connectivity")
+ false
+ }
+ },
+ transform = { _, _ -> }
+ ).first()
+
+ try {
+ pollOnce("routine")
+ numConsecutiveFailures = 0
+ } catch (e: CancellationException) {
+ throw e
+ } catch (_: Throwable) {
+ numConsecutiveFailures += 1
+ }
+
+ val nextPollSeconds = nextPollDelaySeconds(numConsecutiveFailures)
+ Log.d(logTag, "Next poll in ${nextPollSeconds}s")
+ delay(nextPollSeconds * 1000L)
+ }
+ }
+ }
+
+ protected open val successfulPollIntervalSeconds: Int get() = 2
+ protected open val maxRetryIntervalSeconds: Int get() = 10
+
+ /**
+ * Returns the delay until the next poll should be performed.
+ *
+ * @param numConsecutiveFailures The number of consecutive polling failures that have occurred.
+ * 0 indicates the last poll was successful.
+ */
+ private fun nextPollDelaySeconds(
+ numConsecutiveFailures: Int,
+ ): Int {
+ val delay = successfulPollIntervalSeconds * (numConsecutiveFailures + 1)
+ return delay.coerceAtMost(maxRetryIntervalSeconds)
+ }
+
+ /**
+ * Performs a single polling operation. A failed poll should throw an exception.
+ *
+ * @param isFirstPollSinceApoStarted True if this is the first poll since the app started.
+ * @return The result of the polling operation.
+ */
+ protected abstract suspend fun doPollOnce(isFirstPollSinceApoStarted: Boolean): T
+
+ private suspend fun pollOnce(reason: String): T {
+ pollMutex.withLock {
+ val lastState = mutablePollState.value
+ mutablePollState.value =
+ PollState.Polling(reason, lastPolledResult = lastState.lastPolledResult)
+ Log.d(logTag, "Start $reason polling")
+ val result = runCatching {
+ doPollOnce(isFirstPollSinceApoStarted = lastState is PollState.Idle)
+ }
+
+ if (result.isSuccess) {
+ Log.d(logTag, "$reason polling succeeded")
+ } else if (result.exceptionOrNull() !is CancellationException) {
+ Log.e(logTag, "$reason polling failed", result.exceptionOrNull())
+ }
+
+ mutablePollState.value = PollState.Polled(
+ at = Clock.System.now(),
+ result = result,
+ )
+
+ return result.getOrThrow()
+ }
+ }
+
+ /**
+ * Manually triggers a single polling operation.
+ *
+ * Note:
+ * * If a polling operation is already in progress, this will wait for it to complete first.
+ * * This method does not check for app foreground/background state or network connectivity.
+ * * This method will throw if the polling operation fails.
+ */
+ suspend fun manualPollOnce(): T {
+ val resultChannel = Channel>()
+
+ scope.launch {
+ resultChannel.trySend(runCatching {
+ pollOnce("manual")
+ })
+ }
+
+ return resultChannel.receive().getOrThrow()
+ }
+
+
+ sealed interface PollState {
+ val lastPolledResult: Result?
+
+ object Idle : PollState {
+ override val lastPolledResult: Result?
+ get() = null
+ }
+
+ data class Polled(
+ val at: Instant,
+ val result: Result,
+ ) : PollState {
+ override val lastPolledResult: Result
+ get() = result
+ }
+
+ data class Polling(
+ val reason: String,
+ override val lastPolledResult: Result?,
+ ) : PollState
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/OpenGroupPoller.kt b/app/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/OpenGroupPoller.kt
index e83caa4d04..dbaa4e6e32 100644
--- a/app/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/OpenGroupPoller.kt
+++ b/app/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/OpenGroupPoller.kt
@@ -1,48 +1,37 @@
package org.session.libsession.messaging.sending_receiving.pollers
-import com.fasterxml.jackson.core.type.TypeReference
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.channels.Channel
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.collectLatest
-import kotlinx.coroutines.flow.flow
-import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit
+import kotlinx.serialization.json.Json
import org.session.libsession.database.StorageProtocol
import org.session.libsession.messaging.jobs.JobQueue
import org.session.libsession.messaging.jobs.OpenGroupDeleteJob
import org.session.libsession.messaging.jobs.TrimThreadJob
-import org.session.libsession.messaging.messages.Message.Companion.senderOrSync
-import org.session.libsession.messaging.open_groups.Endpoint
import org.session.libsession.messaging.open_groups.OpenGroupApi
-import org.session.libsession.messaging.open_groups.OpenGroupApi.BatchRequest
-import org.session.libsession.messaging.open_groups.OpenGroupApi.BatchRequestInfo
-import org.session.libsession.messaging.open_groups.OpenGroupApi.BatchResponse
import org.session.libsession.messaging.open_groups.OpenGroupApi.Capability
import org.session.libsession.messaging.open_groups.OpenGroupApi.DirectMessage
-import org.session.libsession.messaging.open_groups.OpenGroupApi.getOrFetchServerCapabilities
-import org.session.libsession.messaging.open_groups.OpenGroupApi.parallelBatch
-import org.session.libsession.messaging.sending_receiving.MessageParser
+import org.session.libsession.messaging.open_groups.api.CommunityApiExecutor
+import org.session.libsession.messaging.open_groups.api.CommunityApiRequest
+import org.session.libsession.messaging.open_groups.api.GetCapsApi
+import org.session.libsession.messaging.open_groups.api.GetDirectMessagesApi
+import org.session.libsession.messaging.open_groups.api.GetRoomMessagesApi
+import org.session.libsession.messaging.open_groups.api.PollRoomApi
+import org.session.libsession.messaging.open_groups.api.execute
import org.session.libsession.messaging.sending_receiving.ReceivedMessageProcessor
import org.session.libsession.utilities.Address
-import org.session.libsession.utilities.Address.Companion.toAddress
import org.session.libsession.utilities.ConfigFactoryProtocol
-import org.session.libsignal.utilities.Base64
-import org.session.libsignal.utilities.HTTP.Verb.GET
-import org.session.libsignal.utilities.JsonUtil
+import org.session.libsession.utilities.withUserConfigs
import org.session.libsignal.utilities.Log
import org.thoughtcrime.securesms.database.CommunityDatabase
import org.thoughtcrime.securesms.util.AppVisibilityManager
-
-private typealias PollRequestToken = Channel>>
+import org.thoughtcrime.securesms.util.NetworkConnectivity
+import javax.inject.Provider
/**
* A [OpenGroupPoller] is responsible for polling all communities on a particular server.
@@ -54,88 +43,38 @@ private typealias PollRequestToken = Channel>>
*/
class OpenGroupPoller @AssistedInject constructor(
private val storage: StorageProtocol,
- private val appVisibilityManager: AppVisibilityManager,
private val configFactory: ConfigFactoryProtocol,
private val trimThreadJobFactory: TrimThreadJob.Factory,
private val openGroupDeleteJobFactory: OpenGroupDeleteJob.Factory,
private val communityDatabase: CommunityDatabase,
private val receivedMessageProcessor: ReceivedMessageProcessor,
- private val messageParser: MessageParser,
+ private val communityApiExecutor: CommunityApiExecutor,
+ private val getRoomMessagesFactory: GetRoomMessagesApi.Factory,
+ private val getDirectMessageFactory: GetDirectMessagesApi.Factory,
+ private val pollRoomInfoFactory: PollRoomApi.Factory,
+ private val getCapsApi: Provider,
+ networkConnectivity: NetworkConnectivity,
+ appVisibilityManager: AppVisibilityManager,
+ private val json: Json,
@Assisted private val server: String,
@Assisted private val scope: CoroutineScope,
@Assisted private val pollerSemaphore: Semaphore,
+): BasePoller(
+ networkConnectivity = networkConnectivity,
+ scope = scope,
+ appVisibilityManager = appVisibilityManager
) {
- companion object {
- private const val POLL_INTERVAL_MILLS: Long = 4000L
- const val MAX_INACTIVITIY_PERIOD_MILLS = 14 * 24 * 60 * 60 * 1000L // 14 days
-
- private const val TAG = "OpenGroupPoller"
- }
-
- private val pendingPollRequest = Channel()
-
- @OptIn(ExperimentalCoroutinesApi::class)
- val pollState: StateFlow = flow {
- val tokens = arrayListOf()
-
- while (true) {
- // Wait for next request(s) to come in
- tokens.clear()
- tokens.add(pendingPollRequest.receive())
- tokens.addAll(generateSequence { pendingPollRequest.tryReceive().getOrNull() })
-
- Log.d(TAG, "Polling open group messages for server: $server")
- emit(PollState.Polling)
- val pollResult = runCatching {
- pollerSemaphore.withPermit {
- pollOnce()
- }
- }
- tokens.forEach { it.trySend(pollResult) }
- emit(PollState.Idle(pollResult))
-
- pollResult.exceptionOrNull()?.let {
- Log.e(TAG, "Error while polling open groups for $server", it)
- }
-
- }
- }.stateIn(scope, SharingStarted.Eagerly, PollState.Idle(null))
-
- init {
- // Start a periodic polling request when the app becomes visible
- scope.launch {
- appVisibilityManager.isAppVisible
- .collectLatest { visible ->
- if (visible) {
- while (true) {
- val r = requestPollAndAwait()
- if (r.isSuccess) {
- delay(POLL_INTERVAL_MILLS)
- } else {
- delay(2000L)
- }
- }
- }
- }
- }
- }
+ override val successfulPollIntervalSeconds: Int
+ get() = 4
- /**
- * Requests a poll and await for the result.
- *
- * The result will be a list of room tokens that were polled.
- */
- suspend fun requestPollAndAwait(): Result> {
- val token: PollRequestToken = Channel()
- pendingPollRequest.send(token)
- return token.receive()
- }
+ override val maxRetryIntervalSeconds: Int
+ get() = 30
private fun handleRoomPollInfo(
address: Address.Community,
- pollInfoJson: Map<*, *>,
+ pollInfoJsonText: String,
) {
- communityDatabase.patchRoomInfo(address, JsonUtil.toJson(pollInfoJson))
+ communityDatabase.patchRoomInfo(address, pollInfoJsonText)
}
@@ -144,136 +83,114 @@ class OpenGroupPoller @AssistedInject constructor(
*
* @return A list of rooms that were polled.
*/
- private suspend fun pollOnce(): List {
+ override suspend fun doPollOnce(isFirstPollSinceApoStarted: Boolean): Unit = pollerSemaphore.withPermit {
val allCommunities = configFactory.withUserConfigs { it.userGroups.allCommunityInfo() }
val rooms = allCommunities
.mapNotNull { c -> c.community.takeIf { it.baseUrl == server }?.room }
- if (rooms.isEmpty()) {
- return emptyList()
+ val serverKey = allCommunities.firstOrNull {
+ it.community.baseUrl == server
+ }?.community?.pubKeyHex
+
+ if (rooms.isEmpty() || serverKey.isNullOrBlank()) {
+ return
}
- poll(rooms)
- .asSequence()
- .filterNot { it.body == null }
- .forEach { response ->
- when (response.endpoint) {
- is Endpoint.RoomPollInfo -> {
- handleRoomPollInfo(Address.Community(server, response.endpoint.roomToken), response.body as Map<*, *>)
- }
- is Endpoint.RoomMessagesRecent -> {
- handleMessages(response.endpoint.roomToken, response.body as List)
- }
- is Endpoint.RoomMessagesSince -> {
- handleMessages(response.endpoint.roomToken, response.body as List)
- }
- is Endpoint.Inbox, is Endpoint.InboxSince -> {
- handleInboxMessages( response.body as List)
- }
- is Endpoint.Outbox, is Endpoint.OutboxSince -> {
- handleOutboxMessages( response.body as List)
- }
- else -> { /* We don't care about the result of any other calls (won't be polled for) */}
- }
+ coroutineScope {
+ var caps = storage.getServerCapabilities(server)
+ if (caps == null) {
+ val fetched = communityApiExecutor.execute(
+ CommunityApiRequest(
+ serverBaseUrl = server,
+ serverPubKey = serverKey,
+ api = getCapsApi.get(),
+ )
+ )
+ storage.setServerCapabilities(server, fetched.capabilities)
+ caps = fetched.capabilities
}
- return rooms
- }
-
- @Suppress("UNCHECKED_CAST")
- suspend fun poll(rooms: List): List> {
- val lastInboxMessageId = storage.getLastInboxMessageId(server)
- val lastOutboxMessageId = storage.getLastOutboxMessageId(server)
- val requests = mutableListOf>()
-
- val serverCapabilities = getOrFetchServerCapabilities(server)
-
- rooms.forEach { room ->
- val address = Address.Community(serverUrl = server, room = room)
- val latestRoomPollInfo = communityDatabase.getRoomInfo(address)
- val infoUpdates = latestRoomPollInfo?.details?.infoUpdates ?: 0
- val lastMessageServerId = storage.getLastMessageServerID(room, server) ?: 0L
- requests.add(
- BatchRequestInfo(
- request = BatchRequest(
- method = GET,
- path = "/room/$room/pollInfo/$infoUpdates"
- ),
- endpoint = Endpoint.RoomPollInfo(room, infoUpdates),
- responseType = object : TypeReference