mirror of
https://github.com/oxen-io/session-android.git
synced 2025-08-12 13:37:56 +00:00
Merge branch 'dev' of https://github.com/loki-project/session-android into zombie-handling-update
This commit is contained in:
@@ -1,5 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="org.session.libsession">
|
||||
|
||||
</manifest>
|
||||
<manifest package="org.session.libsession" />
|
@@ -1,4 +1,4 @@
|
||||
package org.session.libsession.messaging.avatars;
|
||||
package org.session.libsession.avatars;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
@@ -7,7 +7,7 @@ import androidx.annotation.Nullable;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.session.libsession.messaging.threads.Address;
|
||||
import org.session.libsession.utilities.Address;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
@@ -1,8 +1,8 @@
|
||||
package org.session.libsession.messaging.avatars;
|
||||
package org.session.libsession.avatars;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.session.libsession.utilities.color.MaterialColor;
|
||||
import org.session.libsession.utilities.MaterialColor;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
@@ -1,4 +1,4 @@
|
||||
package org.session.libsession.messaging.avatars;
|
||||
package org.session.libsession.avatars;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
@@ -1,4 +1,4 @@
|
||||
package org.session.libsession.messaging.avatars;
|
||||
package org.session.libsession.avatars;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.drawable.Drawable;
|
@@ -1,4 +1,4 @@
|
||||
package org.session.libsession.messaging.avatars;
|
||||
package org.session.libsession.avatars;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Color;
|
@@ -1,4 +1,4 @@
|
||||
package org.session.libsession.messaging.avatars;
|
||||
package org.session.libsession.avatars;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
@@ -7,11 +7,11 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration;
|
||||
import org.session.libsession.messaging.StorageProtocol;
|
||||
import org.session.libsession.messaging.threads.Address;
|
||||
import org.session.libsession.messaging.threads.GroupRecord;
|
||||
import org.session.libsession.database.StorageProtocol;
|
||||
import org.session.libsession.utilities.Address;
|
||||
import org.session.libsession.utilities.GroupRecord;
|
||||
import org.session.libsession.utilities.Conversions;
|
||||
import org.session.libsignal.libsignal.util.guava.Optional;
|
||||
import org.session.libsignal.utilities.guava.Optional;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
@@ -1,4 +1,4 @@
|
||||
package org.session.libsession.messaging.avatars;
|
||||
package org.session.libsession.avatars;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
@@ -6,7 +6,7 @@ import android.net.Uri;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.session.libsession.messaging.threads.Address;
|
||||
import org.session.libsession.utilities.Address;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
@@ -1,4 +1,4 @@
|
||||
package org.session.libsession.messaging.avatars;
|
||||
package org.session.libsession.avatars;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Color;
|
@@ -1,4 +1,4 @@
|
||||
package org.session.libsession.messaging.avatars;
|
||||
package org.session.libsession.avatars;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
@@ -7,7 +7,7 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
|
||||
import org.session.libsession.messaging.threads.Address;
|
||||
import org.session.libsession.utilities.Address;
|
||||
import org.session.libsession.utilities.Conversions;
|
||||
|
||||
import java.io.FileNotFoundException;
|
@@ -1,4 +1,4 @@
|
||||
package org.session.libsession.messaging.avatars;
|
||||
package org.session.libsession.avatars;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.drawable.Drawable;
|
@@ -2,10 +2,10 @@ package org.session.libsession.database
|
||||
|
||||
import org.session.libsession.messaging.open_groups.OpenGroup
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.*
|
||||
import org.session.libsession.messaging.threads.Address
|
||||
import org.session.libsession.utilities.Address
|
||||
import org.session.libsession.messaging.utilities.DotNetAPI
|
||||
import org.session.libsignal.service.api.messages.SignalServiceAttachmentPointer
|
||||
import org.session.libsignal.service.api.messages.SignalServiceAttachmentStream
|
||||
import org.session.libsignal.messages.SignalServiceAttachmentPointer
|
||||
import org.session.libsignal.messages.SignalServiceAttachmentStream
|
||||
import java.io.InputStream
|
||||
|
||||
interface MessageDataProvider {
|
||||
@@ -29,8 +29,8 @@ interface MessageDataProvider {
|
||||
|
||||
fun isOutgoingMessage(timestamp: Long): Boolean
|
||||
|
||||
fun updateAttachmentAfterUploadSucceeded(attachmentId: Long, attachmentStream: SignalServiceAttachmentStream, attachmentKey: ByteArray, uploadResult: DotNetAPI.UploadResult)
|
||||
fun updateAttachmentAfterUploadFailed(attachmentId: Long)
|
||||
fun handleSuccessfulAttachmentUpload(attachmentId: Long, attachmentStream: SignalServiceAttachmentStream, attachmentKey: ByteArray, uploadResult: DotNetAPI.UploadResult)
|
||||
fun handleFailedAttachmentUpload(attachmentId: Long)
|
||||
|
||||
fun getMessageForQuote(timestamp: Long, author: Address): Pair<Long, Boolean>?
|
||||
fun getAttachmentsAndLinkPreviewFor(mmsId: Long): List<Attachment>
|
||||
|
@@ -1,4 +1,4 @@
|
||||
package org.session.libsession.messaging
|
||||
package org.session.libsession.database
|
||||
|
||||
|
||||
import android.content.Context
|
||||
@@ -16,12 +16,12 @@ import org.session.libsession.messaging.sending_receiving.data_extraction.DataEx
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
|
||||
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview
|
||||
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel
|
||||
import org.session.libsession.messaging.threads.Address
|
||||
import org.session.libsession.messaging.threads.GroupRecord
|
||||
import org.session.libsession.messaging.threads.recipients.Recipient.RecipientSettings
|
||||
import org.session.libsignal.libsignal.ecc.ECKeyPair
|
||||
import org.session.libsignal.service.api.messages.SignalServiceAttachmentPointer
|
||||
import org.session.libsignal.service.api.messages.SignalServiceGroup
|
||||
import org.session.libsession.utilities.Address
|
||||
import org.session.libsession.utilities.GroupRecord
|
||||
import org.session.libsession.utilities.recipients.Recipient.RecipientSettings
|
||||
import org.session.libsignal.crypto.ecc.ECKeyPair
|
||||
import org.session.libsignal.messages.SignalServiceAttachmentPointer
|
||||
import org.session.libsignal.messages.SignalServiceGroup
|
||||
|
||||
interface StorageProtocol {
|
||||
|
||||
@@ -44,9 +44,9 @@ interface StorageProtocol {
|
||||
|
||||
// Jobs
|
||||
fun persistJob(job: Job)
|
||||
fun markJobAsSucceeded(job: Job)
|
||||
fun markJobAsFailed(job: Job)
|
||||
fun getAllPendingJobs(type: String): List<Job>
|
||||
fun markJobAsSucceeded(jobId: String)
|
||||
fun markJobAsFailedPermanently(jobId: String)
|
||||
fun getAllPendingJobs(type: String): Map<String,Job?>
|
||||
fun getAttachmentUploadJob(attachmentID: Long): AttachmentUploadJob?
|
||||
fun getMessageSendJob(messageSendJobID: String): MessageSendJob?
|
||||
fun resumeMessageSendJobIfNeeded(messageSendJobID: String)
|
||||
@@ -92,7 +92,7 @@ interface StorageProtocol {
|
||||
fun removeLastDeletionServerId(room: String, server: String)
|
||||
|
||||
// Message Handling
|
||||
fun isMessageDuplicated(timestamp: Long, sender: String): Boolean
|
||||
fun isDuplicateMessage(timestamp: Long): Boolean
|
||||
fun getReceivedMessageTimestamps(): Set<Long>
|
||||
fun addReceivedMessageTimestamp(timestamp: Long)
|
||||
fun removeReceivedMessageTimestamps(timestamps: Set<Long>)
|
@@ -2,25 +2,23 @@ package org.session.libsession.messaging
|
||||
|
||||
import android.content.Context
|
||||
import org.session.libsession.database.MessageDataProvider
|
||||
import org.session.libsignal.service.loki.api.crypto.SessionProtocol
|
||||
import org.session.libsession.database.StorageProtocol
|
||||
|
||||
class MessagingModuleConfiguration(
|
||||
val context: Context,
|
||||
val storage: StorageProtocol,
|
||||
val messageDataProvider: MessageDataProvider,
|
||||
val sessionProtocol: SessionProtocol)
|
||||
{
|
||||
val context: Context,
|
||||
val storage: StorageProtocol,
|
||||
val messageDataProvider: MessageDataProvider
|
||||
) {
|
||||
|
||||
companion object {
|
||||
lateinit var shared: MessagingModuleConfiguration
|
||||
|
||||
fun configure(context: Context,
|
||||
storage: StorageProtocol,
|
||||
messageDataProvider: MessageDataProvider,
|
||||
sessionProtocol: SessionProtocol
|
||||
storage: StorageProtocol,
|
||||
messageDataProvider: MessageDataProvider
|
||||
) {
|
||||
if (Companion::shared.isInitialized) { return }
|
||||
shared = MessagingModuleConfiguration(context, storage, messageDataProvider, sessionProtocol)
|
||||
shared = MessagingModuleConfiguration(context, storage, messageDataProvider)
|
||||
}
|
||||
}
|
||||
}
|
@@ -5,11 +5,11 @@ import nl.komponents.kovenant.functional.map
|
||||
import okhttp3.Request
|
||||
import org.session.libsession.messaging.utilities.DotNetAPI
|
||||
import org.session.libsession.snode.OnionRequestAPI
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.session.libsignal.utilities.Base64
|
||||
import org.session.libsignal.utilities.JsonUtil
|
||||
import org.session.libsignal.service.loki.LokiAPIDatabaseProtocol
|
||||
import org.session.libsignal.service.loki.utilities.*
|
||||
import org.session.libsignal.database.LokiAPIDatabaseProtocol
|
||||
import org.session.libsignal.utilities.*
|
||||
import java.net.URL
|
||||
|
||||
class FileServerAPI(public val server: String, userPublicKey: String, userPrivateKey: ByteArray, private val database: LokiAPIDatabaseProtocol) : DotNetAPI() {
|
||||
|
@@ -1,35 +1,28 @@
|
||||
package org.session.libsession.messaging.file_server
|
||||
|
||||
import nl.komponents.kovenant.Promise
|
||||
import nl.komponents.kovenant.functional.bind
|
||||
import nl.komponents.kovenant.functional.map
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.MediaType
|
||||
import okhttp3.RequestBody
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
|
||||
import org.session.libsession.snode.OnionRequestAPI
|
||||
import org.session.libsignal.service.loki.HTTP
|
||||
import org.session.libsignal.service.loki.utilities.retryIfNeeded
|
||||
import org.session.libsignal.utilities.HTTP
|
||||
import org.session.libsignal.utilities.Base64
|
||||
import org.session.libsignal.utilities.JsonUtil
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
import org.session.libsignal.utilities.Log
|
||||
|
||||
object FileServerAPIV2 {
|
||||
|
||||
const val DEFAULT_SERVER = "http://88.99.175.227"
|
||||
private const val DEFAULT_SERVER_PUBLIC_KEY = "7cb31905b55cd5580c686911debf672577b3fb0bff81df4ce2d5c4cb3a7aaa69"
|
||||
|
||||
sealed class Error : Exception() {
|
||||
object PARSING_FAILED : Error()
|
||||
object INVALID_URL : Error()
|
||||
|
||||
fun errorDescription() = when (this) {
|
||||
PARSING_FAILED -> "Invalid response."
|
||||
INVALID_URL -> "Invalid URL."
|
||||
}
|
||||
private const val OLD_SERVER_PUBLIC_KEY = "7cb31905b55cd5580c686911debf672577b3fb0bff81df4ce2d5c4cb3a7aaa69"
|
||||
const val OLD_SERVER = "http://88.99.175.227"
|
||||
private const val SERVER_PUBLIC_KEY = "da21e1d886c6fbaea313f75298bd64aab03a97ce985b46bb2dad9f2089c8ee59"
|
||||
const val SERVER = "http://filev2.getsession.org"
|
||||
|
||||
sealed class Error(message: String) : Exception(message) {
|
||||
object ParsingFailed : Error("Invalid response.")
|
||||
object InvalidURL : Error("Invalid URL.")
|
||||
}
|
||||
|
||||
data class Request(
|
||||
@@ -38,69 +31,65 @@ object FileServerAPIV2 {
|
||||
val queryParameters: Map<String, String> = mapOf(),
|
||||
val parameters: Any? = null,
|
||||
val headers: Map<String, String> = mapOf(),
|
||||
// Always `true` under normal circumstances. You might want to disable
|
||||
// this when running over Lokinet.
|
||||
val useOnionRouting: Boolean = true
|
||||
/**
|
||||
* Always `true` under normal circumstances. You might want to disable
|
||||
* this when running over Lokinet.
|
||||
*/
|
||||
val useOnionRouting: Boolean = true
|
||||
)
|
||||
|
||||
private fun createBody(parameters: Any?): RequestBody? {
|
||||
if (parameters == null) return null
|
||||
|
||||
val parametersAsJSON = JsonUtil.toJson(parameters)
|
||||
return RequestBody.create(MediaType.get("application/json"), parametersAsJSON)
|
||||
}
|
||||
|
||||
private fun send(request: Request): Promise<Map<*, *>, Exception> {
|
||||
val parsed = HttpUrl.parse(DEFAULT_SERVER) ?: return Promise.ofFail(OpenGroupAPIV2.Error.INVALID_URL)
|
||||
private fun send(request: Request, useOldServer: Boolean): Promise<Map<*, *>, Exception> {
|
||||
val server = if (useOldServer) OLD_SERVER else SERVER
|
||||
val serverPublicKey = if (useOldServer) OLD_SERVER_PUBLIC_KEY else SERVER_PUBLIC_KEY
|
||||
val url = HttpUrl.parse(server) ?: return Promise.ofFail(OpenGroupAPIV2.Error.InvalidURL)
|
||||
val urlBuilder = HttpUrl.Builder()
|
||||
.scheme(parsed.scheme())
|
||||
.host(parsed.host())
|
||||
.port(parsed.port())
|
||||
.addPathSegments(request.endpoint)
|
||||
|
||||
.scheme(url.scheme())
|
||||
.host(url.host())
|
||||
.port(url.port())
|
||||
.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(Headers.of(request.headers))
|
||||
.url(urlBuilder.build())
|
||||
.headers(Headers.of(request.headers))
|
||||
when (request.verb) {
|
||||
HTTP.Verb.GET -> requestBuilder.get()
|
||||
HTTP.Verb.PUT -> requestBuilder.put(createBody(request.parameters)!!)
|
||||
HTTP.Verb.POST -> requestBuilder.post(createBody(request.parameters)!!)
|
||||
HTTP.Verb.DELETE -> requestBuilder.delete(createBody(request.parameters))
|
||||
}
|
||||
|
||||
if (request.useOnionRouting) {
|
||||
return OnionRequestAPI.sendOnionRequest(requestBuilder.build(), DEFAULT_SERVER, DEFAULT_SERVER_PUBLIC_KEY)
|
||||
.fail { e ->
|
||||
Log.e("Loki", "FileServerV2 failed with error",e)
|
||||
}
|
||||
return OnionRequestAPI.sendOnionRequest(requestBuilder.build(), server, serverPublicKey).fail { e ->
|
||||
Log.e("Loki", "File server request failed.", e)
|
||||
}
|
||||
} else {
|
||||
return Promise.ofFail(IllegalStateException("It's currently not allowed to send non onion routed requests."))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// region Sending
|
||||
fun upload(file: ByteArray): Promise<Long, Exception> {
|
||||
val base64EncodedFile = Base64.encodeBytes(file)
|
||||
val parameters = mapOf("file" to base64EncodedFile)
|
||||
val parameters = mapOf( "file" to base64EncodedFile )
|
||||
val request = Request(verb = HTTP.Verb.POST, endpoint = "files", parameters = parameters)
|
||||
return send(request).map { json ->
|
||||
json["result"] as? Long ?: throw OpenGroupAPIV2.Error.PARSING_FAILED
|
||||
return send(request, false).map { json ->
|
||||
json["result"] as? Long ?: throw OpenGroupAPIV2.Error.ParsingFailed
|
||||
}
|
||||
}
|
||||
|
||||
fun download(file: Long): Promise<ByteArray, Exception> {
|
||||
fun download(file: Long, useOldServer: Boolean): Promise<ByteArray, Exception> {
|
||||
val request = Request(verb = HTTP.Verb.GET, endpoint = "files/$file")
|
||||
return send(request).map { json ->
|
||||
val base64EncodedFile = json["result"] as? String ?: throw Error.PARSING_FAILED
|
||||
Base64.decode(base64EncodedFile) ?: throw Error.PARSING_FAILED
|
||||
return send(request, useOldServer).map { json ->
|
||||
val base64EncodedFile = json["result"] as? String ?: throw Error.ParsingFailed
|
||||
Base64.decode(base64EncodedFile) ?: throw Error.ParsingFailed
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -3,14 +3,14 @@ package org.session.libsession.messaging.jobs
|
||||
import okhttp3.HttpUrl
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.messaging.file_server.FileServerAPI
|
||||
import org.session.libsession.messaging.file_server.FileServerAPIV2
|
||||
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentState
|
||||
import org.session.libsession.messaging.utilities.Data
|
||||
import org.session.libsession.messaging.utilities.DotNetAPI
|
||||
import org.session.libsession.utilities.DownloadUtilities
|
||||
import org.session.libsignal.service.api.crypto.AttachmentCipherInputStream
|
||||
import org.session.libsignal.streams.AttachmentCipherInputStream
|
||||
import org.session.libsignal.utilities.Base64
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
import org.session.libsignal.utilities.Log
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
|
||||
@@ -31,48 +31,50 @@ class AttachmentDownloadJob(val attachmentID: Long, val databaseMessageID: Long)
|
||||
val KEY: String = "AttachmentDownloadJob"
|
||||
|
||||
// Keys used for database storage
|
||||
private val KEY_ATTACHMENT_ID = "attachment_id"
|
||||
private val KEY_TS_INCOMING_MESSAGE_ID = "tsIncoming_message_id"
|
||||
private val ATTACHMENT_ID_KEY = "attachment_id"
|
||||
private val TS_INCOMING_MESSAGE_ID_KEY = "tsIncoming_message_id"
|
||||
}
|
||||
|
||||
override fun execute() {
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val messageDataProvider = MessagingModuleConfiguration.shared.messageDataProvider
|
||||
val handleFailure: (java.lang.Exception) -> Unit = { exception ->
|
||||
if (exception == Error.NoAttachment) {
|
||||
MessagingModuleConfiguration.shared.messageDataProvider.setAttachmentState(AttachmentState.FAILED, attachmentID, databaseMessageID)
|
||||
messageDataProvider.setAttachmentState(AttachmentState.FAILED, attachmentID, databaseMessageID)
|
||||
this.handlePermanentFailure(exception)
|
||||
} else if (exception == DotNetAPI.Error.ParsingFailed) {
|
||||
// No need to retry if the response is invalid. Most likely this means we (incorrectly)
|
||||
// got a "Cannot GET ..." error from the file server.
|
||||
MessagingModuleConfiguration.shared.messageDataProvider.setAttachmentState(AttachmentState.FAILED, attachmentID, databaseMessageID)
|
||||
messageDataProvider.setAttachmentState(AttachmentState.FAILED, attachmentID, databaseMessageID)
|
||||
this.handlePermanentFailure(exception)
|
||||
} else {
|
||||
this.handleFailure(exception)
|
||||
}
|
||||
}
|
||||
try {
|
||||
val messageDataProvider = MessagingModuleConfiguration.shared.messageDataProvider
|
||||
val attachment = messageDataProvider.getDatabaseAttachment(attachmentID)
|
||||
?: return handleFailure(Error.NoAttachment)
|
||||
?: return handleFailure(Error.NoAttachment)
|
||||
messageDataProvider.setAttachmentState(AttachmentState.STARTED, attachmentID, this.databaseMessageID)
|
||||
val tempFile = createTempFile()
|
||||
|
||||
val threadId = MessagingModuleConfiguration.shared.storage.getThreadIdForMms(databaseMessageID)
|
||||
val openGroupV2 = MessagingModuleConfiguration.shared.storage.getV2OpenGroup(threadId.toString())
|
||||
|
||||
val stream = if (openGroupV2 == null) {
|
||||
val threadID = storage.getThreadIdForMms(databaseMessageID)
|
||||
val openGroupV2 = storage.getV2OpenGroup(threadID.toString())
|
||||
val inputStream = if (openGroupV2 == null) {
|
||||
DownloadUtilities.downloadFile(tempFile, attachment.url, FileServerAPI.maxFileSize, null)
|
||||
// Assume we're retrieving an attachment for an open group server if the digest is not set
|
||||
if (attachment.digest?.size ?: 0 == 0 || attachment.key.isNullOrEmpty()) FileInputStream(tempFile)
|
||||
else AttachmentCipherInputStream.createForAttachment(tempFile, attachment.size, Base64.decode(attachment.key), attachment.digest)
|
||||
if (attachment.digest?.size ?: 0 == 0 || attachment.key.isNullOrEmpty()) {
|
||||
FileInputStream(tempFile)
|
||||
} else {
|
||||
AttachmentCipherInputStream.createForAttachment(tempFile, attachment.size, Base64.decode(attachment.key), attachment.digest)
|
||||
}
|
||||
} else {
|
||||
val url = HttpUrl.parse(attachment.url)!!
|
||||
val fileId = url.pathSegments().last()
|
||||
OpenGroupAPIV2.download(fileId.toLong(), openGroupV2.room, openGroupV2.server).get().let {
|
||||
val fileID = url.pathSegments().last()
|
||||
OpenGroupAPIV2.download(fileID.toLong(), openGroupV2.room, openGroupV2.server).get().let {
|
||||
tempFile.writeBytes(it)
|
||||
}
|
||||
FileInputStream(tempFile)
|
||||
}
|
||||
messageDataProvider.insertAttachment(databaseMessageID, attachment.attachmentId, stream)
|
||||
messageDataProvider.insertAttachment(databaseMessageID, attachment.attachmentId, inputStream)
|
||||
tempFile.delete()
|
||||
handleSuccess()
|
||||
} catch (e: Exception) {
|
||||
@@ -100,8 +102,9 @@ class AttachmentDownloadJob(val attachmentID: Long, val databaseMessageID: Long)
|
||||
}
|
||||
|
||||
override fun serialize(): Data {
|
||||
return Data.Builder().putLong(KEY_ATTACHMENT_ID, attachmentID)
|
||||
.putLong(KEY_TS_INCOMING_MESSAGE_ID, databaseMessageID)
|
||||
return Data.Builder()
|
||||
.putLong(ATTACHMENT_ID_KEY, attachmentID)
|
||||
.putLong(TS_INCOMING_MESSAGE_ID_KEY, databaseMessageID)
|
||||
.build();
|
||||
}
|
||||
|
||||
@@ -110,8 +113,9 @@ class AttachmentDownloadJob(val attachmentID: Long, val databaseMessageID: Long)
|
||||
}
|
||||
|
||||
class Factory : Job.Factory<AttachmentDownloadJob> {
|
||||
|
||||
override fun create(data: Data): AttachmentDownloadJob {
|
||||
return AttachmentDownloadJob(data.getLong(KEY_ATTACHMENT_ID), data.getLong(KEY_TS_INCOMING_MESSAGE_ID))
|
||||
return AttachmentDownloadJob(data.getLong(ATTACHMENT_ID_KEY), data.getLong(TS_INCOMING_MESSAGE_ID_KEY))
|
||||
}
|
||||
}
|
||||
}
|
@@ -3,20 +3,25 @@ package org.session.libsession.messaging.jobs
|
||||
import com.esotericsoftware.kryo.Kryo
|
||||
import com.esotericsoftware.kryo.io.Input
|
||||
import com.esotericsoftware.kryo.io.Output
|
||||
import nl.komponents.kovenant.Promise
|
||||
import okio.Buffer
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.messaging.file_server.FileServerAPI
|
||||
import org.session.libsession.messaging.file_server.FileServerAPIV2
|
||||
import org.session.libsession.messaging.messages.Message
|
||||
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
|
||||
import org.session.libsession.messaging.sending_receiving.MessageSender
|
||||
import org.session.libsession.messaging.utilities.Data
|
||||
import org.session.libsession.messaging.utilities.DotNetAPI
|
||||
import org.session.libsignal.service.api.crypto.AttachmentCipherOutputStream
|
||||
import org.session.libsignal.service.api.messages.SignalServiceAttachmentStream
|
||||
import org.session.libsignal.service.internal.crypto.PaddingInputStream
|
||||
import org.session.libsignal.service.internal.push.PushAttachmentData
|
||||
import org.session.libsignal.service.internal.push.http.AttachmentCipherOutputStreamFactory
|
||||
import org.session.libsignal.service.internal.util.Util
|
||||
import org.session.libsignal.service.loki.PlaintextOutputStreamFactory
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
import org.session.libsignal.streams.AttachmentCipherOutputStream
|
||||
import org.session.libsignal.messages.SignalServiceAttachmentStream
|
||||
import org.session.libsignal.streams.PaddingInputStream
|
||||
import org.session.libsignal.utilities.PushAttachmentData
|
||||
import org.session.libsignal.streams.AttachmentCipherOutputStreamFactory
|
||||
import org.session.libsignal.streams.DigestingRequestBody
|
||||
import org.session.libsignal.utilities.Util
|
||||
import org.session.libsignal.streams.PlaintextOutputStreamFactory
|
||||
import org.session.libsignal.utilities.Log
|
||||
|
||||
class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val message: Message, val messageSendJobID: String) : Job {
|
||||
override var delegate: JobDelegate? = null
|
||||
@@ -30,46 +35,43 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess
|
||||
|
||||
// Settings
|
||||
override val maxFailureCount: Int = 20
|
||||
|
||||
companion object {
|
||||
val TAG = AttachmentUploadJob::class.simpleName
|
||||
val KEY: String = "AttachmentUploadJob"
|
||||
|
||||
// Keys used for database storage
|
||||
private val KEY_ATTACHMENT_ID = "attachment_id"
|
||||
private val KEY_THREAD_ID = "thread_id"
|
||||
private val KEY_MESSAGE = "message"
|
||||
private val KEY_MESSAGE_SEND_JOB_ID = "message_send_job_id"
|
||||
private val ATTACHMENT_ID_KEY = "attachment_id"
|
||||
private val THREAD_ID_KEY = "thread_id"
|
||||
private val MESSAGE_KEY = "message"
|
||||
private val MESSAGE_SEND_JOB_ID_KEY = "message_send_job_id"
|
||||
}
|
||||
|
||||
override fun execute() {
|
||||
try {
|
||||
val attachment = MessagingModuleConfiguration.shared.messageDataProvider.getScaledSignalAttachmentStream(attachmentID)
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val messageDataProvider = MessagingModuleConfiguration.shared.messageDataProvider
|
||||
val attachment = messageDataProvider.getScaledSignalAttachmentStream(attachmentID)
|
||||
?: return handleFailure(Error.NoAttachment)
|
||||
|
||||
val usePadding = false
|
||||
val openGroupV2 = MessagingModuleConfiguration.shared.storage.getV2OpenGroup(threadID)
|
||||
val openGroup = MessagingModuleConfiguration.shared.storage.getOpenGroup(threadID)
|
||||
val server = openGroup?.let {
|
||||
it.server
|
||||
} ?: openGroupV2?.let {
|
||||
it.server
|
||||
} ?: FileServerAPI.shared.server
|
||||
val shouldEncrypt = (openGroup == null && openGroupV2 == null) // Encrypt if this isn't an open group
|
||||
|
||||
val attachmentKey = Util.getSecretBytes(64)
|
||||
val paddedLength = if (usePadding) PaddingInputStream.getPaddedSize(attachment.length) else attachment.length
|
||||
val dataStream = if (usePadding) PaddingInputStream(attachment.inputStream, attachment.length) else attachment.inputStream
|
||||
val ciphertextLength = if (shouldEncrypt) AttachmentCipherOutputStream.getCiphertextLength(paddedLength) else attachment.length
|
||||
|
||||
val outputStreamFactory = if (shouldEncrypt) AttachmentCipherOutputStreamFactory(attachmentKey) else PlaintextOutputStreamFactory()
|
||||
val attachmentData = PushAttachmentData(attachment.contentType, dataStream, ciphertextLength, outputStreamFactory, attachment.listener)
|
||||
|
||||
val uploadResult = if (openGroupV2 == null) FileServerAPI.shared.uploadAttachment(server, attachmentData) else {
|
||||
val dataBytes = attachmentData.data.readBytes()
|
||||
val result = OpenGroupAPIV2.upload(dataBytes, openGroupV2.room, openGroupV2.server).get()
|
||||
DotNetAPI.UploadResult(result, "${openGroupV2.server}/files/$result", byteArrayOf())
|
||||
val v2OpenGroup = storage.getV2OpenGroup(threadID)
|
||||
val v1OpenGroup = storage.getOpenGroup(threadID)
|
||||
if (v2OpenGroup != null) {
|
||||
val keyAndResult = upload(attachment, v2OpenGroup.server, false) {
|
||||
OpenGroupAPIV2.upload(it, v2OpenGroup.room, v2OpenGroup.server)
|
||||
}
|
||||
handleSuccess(attachment, keyAndResult.first, keyAndResult.second)
|
||||
} else if (v1OpenGroup == null) {
|
||||
val keyAndResult = upload(attachment, FileServerAPIV2.SERVER, true) {
|
||||
FileServerAPIV2.upload(it)
|
||||
}
|
||||
handleSuccess(attachment, keyAndResult.first, keyAndResult.second)
|
||||
} else { // V1 open group
|
||||
val server = v1OpenGroup.server
|
||||
val pushData = PushAttachmentData(attachment.contentType, attachment.inputStream,
|
||||
attachment.length, PlaintextOutputStreamFactory(), attachment.listener)
|
||||
val result = FileServerAPI.shared.uploadAttachment(server, pushData)
|
||||
handleSuccess(attachment, ByteArray(0), result)
|
||||
}
|
||||
handleSuccess(attachment, attachmentKey, uploadResult)
|
||||
} catch (e: java.lang.Exception) {
|
||||
if (e == Error.NoAttachment) {
|
||||
this.handlePermanentFailure(e)
|
||||
@@ -81,17 +83,49 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess
|
||||
}
|
||||
}
|
||||
|
||||
private fun upload(attachment: SignalServiceAttachmentStream, server: String, encrypt: Boolean, upload: (ByteArray) -> Promise<Long, Exception>): Pair<ByteArray, DotNetAPI.UploadResult> {
|
||||
// Key
|
||||
val key = if (encrypt) Util.getSecretBytes(64) else ByteArray(0)
|
||||
// Length
|
||||
val rawLength = attachment.length
|
||||
val length = if (encrypt) {
|
||||
val paddedLength = PaddingInputStream.getPaddedSize(rawLength)
|
||||
AttachmentCipherOutputStream.getCiphertextLength(paddedLength)
|
||||
} else {
|
||||
attachment.length
|
||||
}
|
||||
// In & out streams
|
||||
// PaddingInputStream adds padding as data is read out from it. AttachmentCipherOutputStream
|
||||
// encrypts as it writes data.
|
||||
val inputStream = if (encrypt) PaddingInputStream(attachment.inputStream, rawLength) else attachment.inputStream
|
||||
val outputStreamFactory = if (encrypt) AttachmentCipherOutputStreamFactory(key) else PlaintextOutputStreamFactory()
|
||||
// Create a digesting request body but immediately read it out to a buffer. Doing this makes
|
||||
// it easier to deal with inputStream and outputStreamFactory.
|
||||
val pad = PushAttachmentData(attachment.contentType, inputStream, length, outputStreamFactory, attachment.listener)
|
||||
val contentType = "application/octet-stream"
|
||||
val drb = DigestingRequestBody(pad.data, pad.outputStreamFactory, contentType, pad.dataSize, pad.listener)
|
||||
Log.d("Loki", "File size: ${length.toDouble() / 1000} kb.")
|
||||
val b = Buffer()
|
||||
drb.writeTo(b)
|
||||
val data = b.readByteArray()
|
||||
// Upload the data
|
||||
val id = upload(data).get()
|
||||
val digest = drb.transmittedDigest
|
||||
// Return
|
||||
return Pair(key, DotNetAPI.UploadResult(id, "${server}/files/$id", digest))
|
||||
}
|
||||
|
||||
private fun handleSuccess(attachment: SignalServiceAttachmentStream, attachmentKey: ByteArray, uploadResult: DotNetAPI.UploadResult) {
|
||||
Log.w(TAG, "Attachment uploaded successfully.")
|
||||
Log.d(TAG, "Attachment uploaded successfully.")
|
||||
delegate?.handleJobSucceeded(this)
|
||||
MessagingModuleConfiguration.shared.messageDataProvider.updateAttachmentAfterUploadSucceeded(attachmentID, attachment, attachmentKey, uploadResult)
|
||||
MessagingModuleConfiguration.shared.messageDataProvider.handleSuccessfulAttachmentUpload(attachmentID, attachment, attachmentKey, uploadResult)
|
||||
MessagingModuleConfiguration.shared.storage.resumeMessageSendJobIfNeeded(messageSendJobID)
|
||||
}
|
||||
|
||||
private fun handlePermanentFailure(e: Exception) {
|
||||
Log.w(TAG, "Attachment upload failed permanently due to error: $this.")
|
||||
delegate?.handleJobFailedPermanently(this, e)
|
||||
MessagingModuleConfiguration.shared.messageDataProvider.updateAttachmentAfterUploadFailed(attachmentID)
|
||||
MessagingModuleConfiguration.shared.messageDataProvider.handleFailedAttachmentUpload(attachmentID)
|
||||
failAssociatedMessageSendJob(e)
|
||||
}
|
||||
|
||||
@@ -108,7 +142,7 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess
|
||||
val messageSendJob = storage.getMessageSendJob(messageSendJobID)
|
||||
MessageSender.handleFailedMessageSend(this.message, e)
|
||||
if (messageSendJob != null) {
|
||||
storage.markJobAsFailed(messageSendJob)
|
||||
storage.markJobAsFailedPermanently(messageSendJobID)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,10 +153,11 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess
|
||||
val output = Output(serializedMessage)
|
||||
kryo.writeObject(output, message)
|
||||
output.close()
|
||||
return Data.Builder().putLong(KEY_ATTACHMENT_ID, attachmentID)
|
||||
.putString(KEY_THREAD_ID, threadID)
|
||||
.putByteArray(KEY_MESSAGE, serializedMessage)
|
||||
.putString(KEY_MESSAGE_SEND_JOB_ID, messageSendJobID)
|
||||
return Data.Builder()
|
||||
.putLong(ATTACHMENT_ID_KEY, attachmentID)
|
||||
.putString(THREAD_ID_KEY, threadID)
|
||||
.putByteArray(MESSAGE_KEY, serializedMessage)
|
||||
.putString(MESSAGE_SEND_JOB_ID_KEY, messageSendJobID)
|
||||
.build();
|
||||
}
|
||||
|
||||
@@ -133,12 +168,18 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess
|
||||
class Factory: Job.Factory<AttachmentUploadJob> {
|
||||
|
||||
override fun create(data: Data): AttachmentUploadJob {
|
||||
val serializedMessage = data.getByteArray(KEY_MESSAGE)
|
||||
val serializedMessage = data.getByteArray(MESSAGE_KEY)
|
||||
val kryo = Kryo()
|
||||
kryo.isRegistrationRequired = false
|
||||
val input = Input(serializedMessage)
|
||||
val message: Message = kryo.readObject(input, Message::class.java)
|
||||
input.close()
|
||||
return AttachmentUploadJob(data.getLong(KEY_ATTACHMENT_ID), data.getString(KEY_THREAD_ID)!!, message, data.getString(KEY_MESSAGE_SEND_JOB_ID)!!)
|
||||
return AttachmentUploadJob(
|
||||
data.getLong(ATTACHMENT_ID_KEY),
|
||||
data.getString(THREAD_ID_KEY)!!,
|
||||
message,
|
||||
data.getString(MESSAGE_SEND_JOB_ID_KEY)!!
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,5 +1,7 @@
|
||||
package org.session.libsession.messaging.jobs
|
||||
|
||||
import org.session.libsession.messaging.utilities.Data
|
||||
|
||||
interface Job {
|
||||
var delegate: JobDelegate?
|
||||
var id: String?
|
||||
@@ -8,21 +10,21 @@ interface Job {
|
||||
val maxFailureCount: Int
|
||||
|
||||
companion object {
|
||||
|
||||
// Keys used for database storage
|
||||
private val KEY_ID = "id"
|
||||
private val KEY_FAILURE_COUNT = "failure_count"
|
||||
private val ID_KEY = "id"
|
||||
private val FAILURE_COUNT_KEY = "failure_count"
|
||||
internal const val MAX_BUFFER_SIZE = 1_000_000 // bytes
|
||||
}
|
||||
|
||||
fun execute()
|
||||
|
||||
fun serialize(): Data
|
||||
|
||||
/**
|
||||
* Returns the key that can be used to find the relevant factory needed to create your job.
|
||||
*/
|
||||
fun getFactoryKey(): String
|
||||
|
||||
interface Factory<T : Job> {
|
||||
fun create(data: Data): T
|
||||
|
||||
fun create(data: Data): T?
|
||||
}
|
||||
}
|
@@ -4,7 +4,8 @@ import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.channels.Channel.Factory.UNLIMITED
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
import org.session.libsignal.utilities.Log
|
||||
import java.lang.IllegalStateException
|
||||
import java.util.*
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.Executors
|
||||
@@ -17,44 +18,58 @@ import kotlin.math.roundToLong
|
||||
class JobQueue : JobDelegate {
|
||||
private var hasResumedPendingJobs = false // Just for debugging
|
||||
private val jobTimestampMap = ConcurrentHashMap<Long, AtomicInteger>()
|
||||
private val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
|
||||
private val multiDispatcher = Executors.newFixedThreadPool(2).asCoroutineDispatcher()
|
||||
private val rxDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
|
||||
private val txDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
|
||||
private val attachmentDispatcher = Executors.newFixedThreadPool(2).asCoroutineDispatcher()
|
||||
private val scope = GlobalScope + SupervisorJob()
|
||||
private val queue = Channel<Job>(UNLIMITED)
|
||||
|
||||
val timer = Timer()
|
||||
|
||||
private fun CoroutineScope.processWithDispatcher(channel: Channel<Job>, dispatcher: CoroutineDispatcher) = launch(dispatcher) {
|
||||
for (job in channel) {
|
||||
if (!isActive) break
|
||||
job.delegate = this@JobQueue
|
||||
job.execute()
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
// Process jobs
|
||||
scope.launch(dispatcher) {
|
||||
scope.launch {
|
||||
val rxQueue = Channel<Job>(capacity = 1024)
|
||||
val txQueue = Channel<Job>(capacity = 1024)
|
||||
val attachmentQueue = Channel<Job>(capacity = 1024)
|
||||
|
||||
val receiveJob = processWithDispatcher(rxQueue, rxDispatcher)
|
||||
val txJob = processWithDispatcher(txQueue, txDispatcher)
|
||||
val attachmentJob = processWithDispatcher(attachmentQueue, attachmentDispatcher)
|
||||
|
||||
while (isActive) {
|
||||
queue.receive().let { job ->
|
||||
if (job.canExecuteParallel()) {
|
||||
launch(multiDispatcher) {
|
||||
job.delegate = this@JobQueue
|
||||
job.execute()
|
||||
}
|
||||
} else {
|
||||
job.delegate = this@JobQueue
|
||||
job.execute()
|
||||
for (job in queue) {
|
||||
when (job) {
|
||||
is NotifyPNServerJob, is AttachmentUploadJob, is MessageSendJob -> txQueue.send(job)
|
||||
is AttachmentDownloadJob -> attachmentQueue.send(job)
|
||||
is MessageReceiveJob -> rxQueue.send(job)
|
||||
else -> throw IllegalStateException("Unexpected job type.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The job has been cancelled
|
||||
receiveJob.cancel()
|
||||
txJob.cancel()
|
||||
attachmentJob.cancel()
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@JvmStatic
|
||||
val shared: JobQueue by lazy { JobQueue() }
|
||||
}
|
||||
|
||||
private fun Job.canExecuteParallel(): Boolean {
|
||||
return this.javaClass in arrayOf(
|
||||
AttachmentUploadJob::class.java,
|
||||
AttachmentDownloadJob::class.java
|
||||
)
|
||||
}
|
||||
|
||||
fun add(job: Job) {
|
||||
addWithoutExecuting(job)
|
||||
queue.offer(job) // offer always called on unlimited capacity
|
||||
@@ -68,7 +83,6 @@ class JobQueue : JobDelegate {
|
||||
val currentTime = System.currentTimeMillis()
|
||||
jobTimestampMap.putIfAbsent(currentTime, AtomicInteger())
|
||||
job.id = currentTime.toString() + jobTimestampMap[currentTime]!!.getAndIncrement().toString()
|
||||
|
||||
MessagingModuleConfiguration.shared.storage.persistJob(job)
|
||||
}
|
||||
|
||||
@@ -78,42 +92,70 @@ class JobQueue : JobDelegate {
|
||||
return
|
||||
}
|
||||
hasResumedPendingJobs = true
|
||||
val allJobTypes = listOf(AttachmentDownloadJob.KEY, AttachmentDownloadJob.KEY, MessageReceiveJob.KEY, MessageSendJob.KEY, NotifyPNServerJob.KEY)
|
||||
val allJobTypes = listOf(
|
||||
AttachmentUploadJob.KEY,
|
||||
AttachmentDownloadJob.KEY,
|
||||
MessageReceiveJob.KEY,
|
||||
MessageSendJob.KEY,
|
||||
NotifyPNServerJob.KEY
|
||||
)
|
||||
allJobTypes.forEach { type ->
|
||||
val allPendingJobs = MessagingModuleConfiguration.shared.storage.getAllPendingJobs(type)
|
||||
allPendingJobs.sortedBy { it.id }.forEach { job ->
|
||||
Log.i("Jobs", "Resuming pending job of type: ${job::class.simpleName}.")
|
||||
val pendingJobs = mutableListOf<Job>()
|
||||
for ((id, job) in allPendingJobs) {
|
||||
if (job == null) {
|
||||
// Job failed to deserialize, remove it from the DB
|
||||
handleJobFailedPermanently(id)
|
||||
} else {
|
||||
pendingJobs.add(job)
|
||||
}
|
||||
}
|
||||
pendingJobs.sortedBy { it.id }.forEach { job ->
|
||||
Log.i("Loki", "Resuming pending job of type: ${job::class.simpleName}.")
|
||||
queue.offer(job) // Offer always called on unlimited capacity
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun handleJobSucceeded(job: Job) {
|
||||
MessagingModuleConfiguration.shared.storage.markJobAsSucceeded(job)
|
||||
val jobId = job.id ?: return
|
||||
MessagingModuleConfiguration.shared.storage.markJobAsSucceeded(jobId)
|
||||
}
|
||||
|
||||
override fun handleJobFailed(job: Job, error: Exception) {
|
||||
job.failureCount += 1
|
||||
// Canceled
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
if (storage.isJobCanceled(job)) { return Log.i("Jobs", "${job::class.simpleName} canceled.")}
|
||||
storage.persistJob(job)
|
||||
if (job.failureCount == job.maxFailureCount) {
|
||||
storage.markJobAsFailed(job)
|
||||
if (storage.isJobCanceled(job)) {
|
||||
return Log.i("Loki", "${job::class.simpleName} canceled.")
|
||||
}
|
||||
// Message send jobs waiting for the attachment to upload
|
||||
if (job is MessageSendJob && error is MessageSendJob.AwaitingAttachmentUploadException) {
|
||||
Log.i("Loki", "Message send job waiting for attachment upload to finish.")
|
||||
return
|
||||
}
|
||||
// Regular job failure
|
||||
job.failureCount += 1
|
||||
if (job.failureCount >= job.maxFailureCount) {
|
||||
handleJobFailedPermanently(job, error)
|
||||
} else {
|
||||
storage.persistJob(job)
|
||||
val retryInterval = getRetryInterval(job)
|
||||
Log.i("Jobs", "${job::class.simpleName} failed; scheduling retry (failure count is ${job.failureCount}).")
|
||||
Log.i("Loki", "${job::class.simpleName} failed; scheduling retry (failure count is ${job.failureCount}).")
|
||||
timer.schedule(delay = retryInterval) {
|
||||
Log.i("Jobs", "Retrying ${job::class.simpleName}.")
|
||||
Log.i("Loki", "Retrying ${job::class.simpleName}.")
|
||||
queue.offer(job)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun handleJobFailedPermanently(job: Job, error: Exception) {
|
||||
job.failureCount += 1
|
||||
val jobId = job.id ?: return
|
||||
handleJobFailedPermanently(jobId)
|
||||
}
|
||||
|
||||
private fun handleJobFailedPermanently(jobId: String) {
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
storage.persistJob(job)
|
||||
storage.markJobAsFailed(job)
|
||||
storage.markJobAsFailedPermanently(jobId)
|
||||
}
|
||||
|
||||
private fun getRetryInterval(job: Job): Long {
|
||||
|
@@ -4,14 +4,14 @@ import nl.komponents.kovenant.Promise
|
||||
import nl.komponents.kovenant.deferred
|
||||
import org.session.libsession.messaging.sending_receiving.MessageReceiver
|
||||
import org.session.libsession.messaging.sending_receiving.handle
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
import org.session.libsession.messaging.utilities.Data
|
||||
import org.session.libsignal.utilities.Log
|
||||
|
||||
class MessageReceiveJob(val data: ByteArray, val isBackgroundPoll: Boolean, val openGroupMessageServerID: Long? = null, val openGroupID: String? = null) : Job {
|
||||
class MessageReceiveJob(val data: ByteArray, val openGroupMessageServerID: Long? = null, val openGroupID: String? = null) : Job {
|
||||
override var delegate: JobDelegate? = null
|
||||
override var id: String? = null
|
||||
override var failureCount: Int = 0
|
||||
|
||||
// Settings
|
||||
override val maxFailureCount: Int = 10
|
||||
companion object {
|
||||
val TAG = MessageReceiveJob::class.simpleName
|
||||
@@ -20,10 +20,11 @@ class MessageReceiveJob(val data: ByteArray, val isBackgroundPoll: Boolean, val
|
||||
private val RECEIVE_LOCK = Object()
|
||||
|
||||
// Keys used for database storage
|
||||
private val KEY_DATA = "data"
|
||||
private val KEY_IS_BACKGROUND_POLL = "is_background_poll"
|
||||
private val KEY_OPEN_GROUP_MESSAGE_SERVER_ID = "openGroupMessageServerID"
|
||||
private val KEY_OPEN_GROUP_ID = "open_group_id"
|
||||
private val DATA_KEY = "data"
|
||||
// FIXME: We probably shouldn't be using this job when background polling
|
||||
private val IS_BACKGROUND_POLL_KEY = "is_background_poll"
|
||||
private val OPEN_GROUP_MESSAGE_SERVER_ID_KEY = "openGroupMessageServerID"
|
||||
private val OPEN_GROUP_ID_KEY = "open_group_id"
|
||||
}
|
||||
|
||||
override fun execute() {
|
||||
@@ -35,19 +36,18 @@ class MessageReceiveJob(val data: ByteArray, val isBackgroundPoll: Boolean, val
|
||||
try {
|
||||
val isRetry: Boolean = failureCount != 0
|
||||
val (message, proto) = MessageReceiver.parse(this.data, this.openGroupMessageServerID, isRetry)
|
||||
synchronized(RECEIVE_LOCK) {
|
||||
synchronized(RECEIVE_LOCK) { // FIXME: Do we need this?
|
||||
MessageReceiver.handle(message, proto, this.openGroupID)
|
||||
}
|
||||
this.handleSuccess()
|
||||
deferred.resolve(Unit)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Couldn't receive message due to error", e)
|
||||
val error = e as? MessageReceiver.Error
|
||||
if (error != null && !error.isRetryable) {
|
||||
Log.e("Loki", "Message receive job permanently failed due to error", e)
|
||||
this.handlePermanentFailure(error)
|
||||
Log.e(TAG, "Couldn't receive message.", e)
|
||||
if (e is MessageReceiver.Error && !e.isRetryable) {
|
||||
Log.e("Loki", "Message receive job permanently failed.", e)
|
||||
this.handlePermanentFailure(e)
|
||||
} else {
|
||||
Log.e("Loki", "Couldn't receive message due to error", e)
|
||||
Log.e("Loki", "Couldn't receive message.", e)
|
||||
this.handleFailure(e)
|
||||
}
|
||||
deferred.resolve(Unit) // The promise is just used to keep track of when we're done
|
||||
@@ -68,10 +68,9 @@ class MessageReceiveJob(val data: ByteArray, val isBackgroundPoll: Boolean, val
|
||||
}
|
||||
|
||||
override fun serialize(): Data {
|
||||
val builder = Data.Builder().putByteArray(KEY_DATA, data)
|
||||
.putBoolean(KEY_IS_BACKGROUND_POLL, isBackgroundPoll)
|
||||
openGroupMessageServerID?.let { builder.putLong(KEY_OPEN_GROUP_MESSAGE_SERVER_ID, openGroupMessageServerID) }
|
||||
openGroupID?.let { builder.putString(KEY_OPEN_GROUP_ID, openGroupID) }
|
||||
val builder = Data.Builder().putByteArray(DATA_KEY, data)
|
||||
openGroupMessageServerID?.let { builder.putLong(OPEN_GROUP_MESSAGE_SERVER_ID_KEY, it) }
|
||||
openGroupID?.let { builder.putString(OPEN_GROUP_ID_KEY, it) }
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
@@ -82,7 +81,11 @@ class MessageReceiveJob(val data: ByteArray, val isBackgroundPoll: Boolean, val
|
||||
class Factory: Job.Factory<MessageReceiveJob> {
|
||||
|
||||
override fun create(data: Data): MessageReceiveJob {
|
||||
return MessageReceiveJob(data.getByteArray(KEY_DATA), data.getBoolean(KEY_IS_BACKGROUND_POLL), data.getLong(KEY_OPEN_GROUP_MESSAGE_SERVER_ID), data.getString(KEY_OPEN_GROUP_ID))
|
||||
return MessageReceiveJob(
|
||||
data.getByteArray(DATA_KEY),
|
||||
data.getLong(OPEN_GROUP_MESSAGE_SERVER_ID_KEY),
|
||||
data.getString(OPEN_GROUP_ID_KEY)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@@ -4,33 +4,38 @@ import com.esotericsoftware.kryo.Kryo
|
||||
import com.esotericsoftware.kryo.io.Input
|
||||
import com.esotericsoftware.kryo.io.Output
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.messaging.jobs.Job.Companion.MAX_BUFFER_SIZE
|
||||
import org.session.libsession.messaging.messages.Destination
|
||||
import org.session.libsession.messaging.messages.Message
|
||||
import org.session.libsession.messaging.messages.visible.VisibleMessage
|
||||
import org.session.libsession.messaging.sending_receiving.MessageSender
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
import org.session.libsession.messaging.utilities.Data
|
||||
import org.session.libsignal.utilities.Log
|
||||
|
||||
class MessageSendJob(val message: Message, val destination: Destination) : Job {
|
||||
|
||||
object AwaitingAttachmentUploadException : Exception("Awaiting attachment upload.")
|
||||
|
||||
override var delegate: JobDelegate? = null
|
||||
override var id: String? = null
|
||||
override var failureCount: Int = 0
|
||||
|
||||
// Settings
|
||||
override val maxFailureCount: Int = 10
|
||||
|
||||
companion object {
|
||||
val TAG = MessageSendJob::class.simpleName
|
||||
val KEY: String = "MessageSendJob"
|
||||
|
||||
// Keys used for database storage
|
||||
private val KEY_MESSAGE = "message"
|
||||
private val KEY_DESTINATION = "destination"
|
||||
private val MESSAGE_KEY = "message"
|
||||
private val DESTINATION_KEY = "destination"
|
||||
}
|
||||
|
||||
override fun execute() {
|
||||
val messageDataProvider = MessagingModuleConfiguration.shared.messageDataProvider
|
||||
val message = message as? VisibleMessage
|
||||
message?.let {
|
||||
if(!messageDataProvider.isOutgoingMessage(message.sentTimestamp!!)) return // The message has been deleted
|
||||
if (message != null) {
|
||||
if (!messageDataProvider.isOutgoingMessage(message.sentTimestamp!!)) return // The message has been deleted
|
||||
val attachmentIDs = mutableListOf<Long>()
|
||||
attachmentIDs.addAll(message.attachmentIDs)
|
||||
message.quote?.let { it.attachmentID?.let { attachmentID -> attachmentIDs.add(attachmentID) } }
|
||||
@@ -45,15 +50,17 @@ class MessageSendJob(val message: Message, val destination: Destination) : Job {
|
||||
JobQueue.shared.add(job)
|
||||
}
|
||||
}
|
||||
if (attachmentsToUpload.isNotEmpty()) return // Wait for all attachments to upload before continuing
|
||||
if (attachmentsToUpload.isNotEmpty()) {
|
||||
this.handleFailure(AwaitingAttachmentUploadException)
|
||||
return
|
||||
} // Wait for all attachments to upload before continuing
|
||||
}
|
||||
MessageSender.send(this.message, this.destination).success {
|
||||
this.handleSuccess()
|
||||
}.fail { exception ->
|
||||
Log.e(TAG, "Couldn't send message due to error: $exception.")
|
||||
val e = exception as? MessageSender.Error
|
||||
e?.let {
|
||||
if (!e.isRetryable) this.handlePermanentFailure(e)
|
||||
if (exception is MessageSender.Error) {
|
||||
if (!exception.isRetryable) { this.handlePermanentFailure(exception) }
|
||||
}
|
||||
this.handleFailure(exception)
|
||||
}
|
||||
@@ -70,8 +77,10 @@ class MessageSendJob(val message: Message, val destination: Destination) : Job {
|
||||
private fun handleFailure(error: Exception) {
|
||||
Log.w(TAG, "Failed to send $message::class.simpleName.")
|
||||
val message = message as? VisibleMessage
|
||||
message?.let {
|
||||
if(!MessagingModuleConfiguration.shared.messageDataProvider.isOutgoingMessage(message.sentTimestamp!!)) return // The message has been deleted
|
||||
if (message != null) {
|
||||
if (!MessagingModuleConfiguration.shared.messageDataProvider.isOutgoingMessage(message.sentTimestamp!!)) {
|
||||
return // The message has been deleted
|
||||
}
|
||||
}
|
||||
delegate?.handleJobFailed(this, error)
|
||||
}
|
||||
@@ -79,35 +88,55 @@ class MessageSendJob(val message: Message, val destination: Destination) : Job {
|
||||
override fun serialize(): Data {
|
||||
val kryo = Kryo()
|
||||
kryo.isRegistrationRequired = false
|
||||
val output = Output(ByteArray(4096), -1) // maxBufferSize '-1' will dynamically grow internally if we run out of room serializing the message
|
||||
val output = Output(ByteArray(4096), MAX_BUFFER_SIZE)
|
||||
// Message
|
||||
kryo.writeClassAndObject(output, message)
|
||||
output.close()
|
||||
val serializedMessage = output.toBytes()
|
||||
output.clear()
|
||||
// Destination
|
||||
kryo.writeClassAndObject(output, destination)
|
||||
output.close()
|
||||
val serializedDestination = output.toBytes()
|
||||
return Data.Builder().putByteArray(KEY_MESSAGE, serializedMessage)
|
||||
.putByteArray(KEY_DESTINATION, serializedDestination)
|
||||
.build();
|
||||
output.clear()
|
||||
// Serialize
|
||||
return Data.Builder()
|
||||
.putByteArray(MESSAGE_KEY, serializedMessage)
|
||||
.putByteArray(DESTINATION_KEY, serializedDestination)
|
||||
.build()
|
||||
}
|
||||
|
||||
override fun getFactoryKey(): String {
|
||||
return KEY
|
||||
}
|
||||
|
||||
class Factory: Job.Factory<MessageSendJob> {
|
||||
class Factory : Job.Factory<MessageSendJob> {
|
||||
|
||||
override fun create(data: Data): MessageSendJob {
|
||||
val serializedMessage = data.getByteArray(KEY_MESSAGE)
|
||||
val serializedDestination = data.getByteArray(KEY_DESTINATION)
|
||||
override fun create(data: Data): MessageSendJob? {
|
||||
val serializedMessage = data.getByteArray(MESSAGE_KEY)
|
||||
val serializedDestination = data.getByteArray(DESTINATION_KEY)
|
||||
val kryo = Kryo()
|
||||
var input = Input(serializedMessage)
|
||||
val message = kryo.readClassAndObject(input) as Message
|
||||
input.close()
|
||||
input = Input(serializedDestination)
|
||||
val destination = kryo.readClassAndObject(input) as Destination
|
||||
input.close()
|
||||
// Message
|
||||
val messageInput = Input(serializedMessage)
|
||||
val message: Message
|
||||
try {
|
||||
message = kryo.readClassAndObject(messageInput) as Message
|
||||
} catch (e: Exception) {
|
||||
Log.e("Loki", "Couldn't deserialize message send job.", e)
|
||||
return null
|
||||
}
|
||||
messageInput.close()
|
||||
// Destination
|
||||
val destinationInput = Input(serializedDestination)
|
||||
val destination: Destination
|
||||
try {
|
||||
destination = kryo.readClassAndObject(destinationInput) as Destination
|
||||
} catch (e: Exception) {
|
||||
Log.e("Loki", "Couldn't deserialize message send job.", e)
|
||||
return null
|
||||
}
|
||||
destinationInput.close()
|
||||
// Return
|
||||
return MessageSendJob(message, destination)
|
||||
}
|
||||
}
|
||||
|
@@ -9,28 +9,27 @@ import okhttp3.Request
|
||||
import okhttp3.RequestBody
|
||||
|
||||
import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI
|
||||
import org.session.libsession.messaging.utilities.Data
|
||||
import org.session.libsession.snode.SnodeMessage
|
||||
import org.session.libsession.snode.OnionRequestAPI
|
||||
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.session.libsignal.utilities.JsonUtil
|
||||
import org.session.libsignal.service.loki.utilities.retryIfNeeded
|
||||
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
|
||||
|
||||
// Settings
|
||||
override val maxFailureCount: Int = 20
|
||||
companion object {
|
||||
val KEY: String = "NotifyPNServerJob"
|
||||
|
||||
// Keys used for database storage
|
||||
private val KEY_MESSAGE = "message"
|
||||
private val MESSAGE_KEY = "message"
|
||||
}
|
||||
|
||||
// Running
|
||||
override fun execute() {
|
||||
val server = PushNotificationAPI.server
|
||||
val parameters = mapOf( "data" to message.data, "send_to" to message.recipient )
|
||||
@@ -41,10 +40,10 @@ class NotifyPNServerJob(val message: SnodeMessage) : Job {
|
||||
OnionRequestAPI.sendOnionRequest(request.build(), server, PushNotificationAPI.serverPublicKey, "/loki/v2/lsrpc").map { json ->
|
||||
val code = json["code"] as? Int
|
||||
if (code == null || code == 0) {
|
||||
Log.d("Loki", "[Loki] Couldn't notify PN server due to error: ${json["message"] as? String ?: "null"}.")
|
||||
Log.d("Loki", "Couldn't notify PN server due to error: ${json["message"] as? String ?: "null"}.")
|
||||
}
|
||||
}.fail { exception ->
|
||||
Log.d("Loki", "[Loki] Couldn't notify PN server due to error: $exception.")
|
||||
Log.d("Loki", "Couldn't notify PN server due to error: $exception.")
|
||||
}
|
||||
}.success {
|
||||
handleSuccess()
|
||||
@@ -68,18 +67,19 @@ class NotifyPNServerJob(val message: SnodeMessage) : Job {
|
||||
val output = Output(serializedMessage)
|
||||
kryo.writeObject(output, message)
|
||||
output.close()
|
||||
return Data.Builder().putByteArray(KEY_MESSAGE, serializedMessage).build();
|
||||
return Data.Builder().putByteArray(MESSAGE_KEY, serializedMessage).build();
|
||||
}
|
||||
|
||||
override fun getFactoryKey(): String {
|
||||
return KEY
|
||||
}
|
||||
|
||||
class Factory: Job.Factory<NotifyPNServerJob> {
|
||||
class Factory : Job.Factory<NotifyPNServerJob> {
|
||||
|
||||
override fun create(data: Data): NotifyPNServerJob {
|
||||
val serializedMessage = data.getByteArray(KEY_MESSAGE)
|
||||
val serializedMessage = data.getByteArray(MESSAGE_KEY)
|
||||
val kryo = Kryo()
|
||||
kryo.isRegistrationRequired = false
|
||||
val input = Input(serializedMessage)
|
||||
val message: SnodeMessage = kryo.readObject(input, SnodeMessage::class.java)
|
||||
input.close()
|
||||
|
@@ -1,12 +1,14 @@
|
||||
package org.session.libsession.messaging.jobs
|
||||
|
||||
import org.session.libsession.messaging.utilities.Data
|
||||
|
||||
class SessionJobInstantiator(private val jobFactories: Map<String, Job.Factory<out Job>>) {
|
||||
|
||||
fun instantiate(jobFactoryKey: String, data: Data): Job {
|
||||
fun instantiate(jobFactoryKey: String, data: Data): Job? {
|
||||
if (jobFactories.containsKey(jobFactoryKey)) {
|
||||
return jobFactories[jobFactoryKey]?.create(data) ?: throw IllegalStateException("Tried to instantiate a job with key '$jobFactoryKey', but no matching factory was found.")
|
||||
return jobFactories[jobFactoryKey]?.create(data)
|
||||
} else {
|
||||
throw IllegalStateException("Tried to instantiate a job with key '$jobFactoryKey', but no matching factory was found.")
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
@@ -3,6 +3,7 @@ package org.session.libsession.messaging.jobs
|
||||
class SessionJobManagerFactories {
|
||||
|
||||
companion object {
|
||||
|
||||
fun getSessionJobFactories(): Map<String, Job.Factory<out Job>> {
|
||||
return mapOf(
|
||||
AttachmentDownloadJob.KEY to AttachmentDownloadJob.Factory(),
|
||||
|
@@ -0,0 +1,3 @@
|
||||
package org.session.libsession.messaging.mentions
|
||||
|
||||
data class Mention(val publicKey: String, val displayName: String)
|
@@ -1,9 +1,8 @@
|
||||
package org.session.libsession.messaging.mentions
|
||||
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsignal.service.loki.Mention
|
||||
|
||||
import org.session.libsignal.service.loki.LokiUserDatabaseProtocol
|
||||
import org.session.libsignal.database.LokiUserDatabaseProtocol
|
||||
|
||||
class MentionsManager(private val userPublicKey: String, private val userDatabase: LokiUserDatabaseProtocol) {
|
||||
var userPublicKeyCache = mutableMapOf<Long, Set<String>>() // Thread ID to set of user hex encoded public keys
|
||||
|
@@ -3,12 +3,9 @@ package org.session.libsession.messaging.messages
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.messaging.open_groups.OpenGroupV2
|
||||
import org.session.libsession.messaging.open_groups.OpenGroup
|
||||
import org.session.libsession.messaging.threads.Address
|
||||
import org.session.libsession.utilities.Address
|
||||
import org.session.libsession.utilities.GroupUtil
|
||||
import org.session.libsignal.service.loki.utilities.toHexString
|
||||
|
||||
typealias OpenGroupModel = OpenGroup
|
||||
typealias OpenGroupV2Model = OpenGroupV2
|
||||
import org.session.libsignal.utilities.toHexString
|
||||
|
||||
sealed class Destination {
|
||||
|
||||
@@ -21,11 +18,12 @@ sealed class Destination {
|
||||
class OpenGroup(var channel: Long, var server: String) : Destination() {
|
||||
internal constructor(): this(0, "")
|
||||
}
|
||||
class OpenGroupV2(var room: String, var server: String): Destination() {
|
||||
class OpenGroupV2(var room: String, var server: String) : Destination() {
|
||||
internal constructor(): this("", "")
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
fun from(address: Address): Destination {
|
||||
return when {
|
||||
address.isContact -> {
|
||||
@@ -39,10 +37,12 @@ sealed class Destination {
|
||||
address.isOpenGroup -> {
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val threadID = storage.getThreadID(address.contactIdentifier())!!
|
||||
when (val openGroup = storage.getOpenGroup(threadID) ?: storage.getV2OpenGroup(threadID)) {
|
||||
is OpenGroupModel -> OpenGroup(openGroup.channel, openGroup.server)
|
||||
is OpenGroupV2Model -> OpenGroupV2(openGroup.room, openGroup.server)
|
||||
else -> throw Exception("Invalid OpenGroup $openGroup")
|
||||
when (val openGroup = storage.getV2OpenGroup(threadID) ?: storage.getOpenGroup(threadID)) {
|
||||
is org.session.libsession.messaging.open_groups.OpenGroup
|
||||
-> Destination.OpenGroup(openGroup.channel, openGroup.server)
|
||||
is org.session.libsession.messaging.open_groups.OpenGroupV2
|
||||
-> Destination.OpenGroupV2(openGroup.room, openGroup.server)
|
||||
else -> throw Exception("Missing open group for thread with ID: $threadID.")
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
|
@@ -2,7 +2,7 @@ package org.session.libsession.messaging.messages
|
||||
|
||||
import com.google.protobuf.ByteString
|
||||
import org.session.libsession.utilities.GroupUtil
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos
|
||||
import org.session.libsignal.protos.SignalServiceProtos
|
||||
|
||||
abstract class Message {
|
||||
var id: Long? = null
|
||||
@@ -18,12 +18,10 @@ abstract class Message {
|
||||
open val isSelfSendValid: Boolean = false
|
||||
|
||||
open fun isValid(): Boolean {
|
||||
sentTimestamp?.let {
|
||||
if (it <= 0) return false
|
||||
}
|
||||
receivedTimestamp?.let {
|
||||
if (it <= 0) return false
|
||||
}
|
||||
val sentTimestamp = sentTimestamp
|
||||
if (sentTimestamp != null && sentTimestamp <= 0) { return false }
|
||||
val receivedTimestamp = receivedTimestamp
|
||||
if (receivedTimestamp != null && receivedTimestamp <= 0) { return false }
|
||||
return sender != null && recipient != null
|
||||
}
|
||||
|
||||
|
@@ -2,23 +2,24 @@ package org.session.libsession.messaging.messages.control
|
||||
|
||||
import com.google.protobuf.ByteString
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.messaging.threads.Address
|
||||
import org.session.libsession.messaging.threads.recipients.Recipient
|
||||
import org.session.libsession.utilities.Address
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.session.libsession.utilities.GroupUtil
|
||||
import org.session.libsignal.libsignal.ecc.DjbECPrivateKey
|
||||
import org.session.libsignal.libsignal.ecc.DjbECPublicKey
|
||||
import org.session.libsignal.libsignal.ecc.ECKeyPair
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos.DataMessage
|
||||
import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded
|
||||
import org.session.libsignal.service.loki.utilities.toHexString
|
||||
import org.session.libsignal.crypto.ecc.DjbECPrivateKey
|
||||
import org.session.libsignal.crypto.ecc.DjbECPublicKey
|
||||
import org.session.libsignal.crypto.ecc.ECKeyPair
|
||||
import org.session.libsignal.protos.SignalServiceProtos
|
||||
import org.session.libsignal.protos.SignalServiceProtos.DataMessage
|
||||
import org.session.libsignal.utilities.removing05PrefixIfNeeded
|
||||
import org.session.libsignal.utilities.toHexString
|
||||
import org.session.libsignal.utilities.Hex
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
import org.session.libsignal.utilities.Log
|
||||
|
||||
class ClosedGroupControlMessage() : ControlMessage() {
|
||||
var kind: Kind? = null
|
||||
|
||||
override val ttl: Long = run {
|
||||
when (kind) {
|
||||
override val ttl: Long get() {
|
||||
return when (kind) {
|
||||
is Kind.EncryptionKeyPair -> 14 * 24 * 60 * 60 * 1000
|
||||
else -> 14 * 24 * 60 * 60 * 1000
|
||||
}
|
||||
@@ -26,31 +27,46 @@ class ClosedGroupControlMessage() : ControlMessage() {
|
||||
|
||||
override val isSelfSendValid: Boolean = true
|
||||
|
||||
var kind: Kind? = null
|
||||
override fun isValid(): Boolean {
|
||||
val kind = kind
|
||||
if (!super.isValid() || kind == null) return false
|
||||
return when (kind) {
|
||||
is Kind.New -> {
|
||||
!kind.publicKey.isEmpty && kind.name.isNotEmpty() && kind.encryptionKeyPair?.publicKey != null
|
||||
&& kind.encryptionKeyPair?.privateKey != null && kind.members.isNotEmpty() && kind.admins.isNotEmpty()
|
||||
}
|
||||
is Kind.EncryptionKeyPair -> true
|
||||
is Kind.NameChange -> kind.name.isNotEmpty()
|
||||
is Kind.MembersAdded -> kind.members.isNotEmpty()
|
||||
is Kind.MembersRemoved -> kind.members.isNotEmpty()
|
||||
is Kind.MemberLeft -> true
|
||||
}
|
||||
}
|
||||
|
||||
sealed class Kind {
|
||||
class New(var publicKey: ByteString, var name: String, var encryptionKeyPair: ECKeyPair?, var members: List<ByteString>, var admins: List<ByteString>) : Kind() {
|
||||
internal constructor(): this(ByteString.EMPTY, "", null, listOf(), listOf())
|
||||
internal constructor() : this(ByteString.EMPTY, "", null, listOf(), listOf())
|
||||
}
|
||||
/// An encryption key pair encrypted for each member individually.
|
||||
///
|
||||
/// - Note: `publicKey` is only set when an encryption key pair is sent in a one-to-one context (i.e. not in a group).
|
||||
/** An encryption key pair encrypted for each member individually.
|
||||
*
|
||||
* **Note:** `publicKey` is only set when an encryption key pair is sent in a one-to-one context (i.e. not in a group).
|
||||
*/
|
||||
class EncryptionKeyPair(var publicKey: ByteString?, var wrappers: Collection<KeyPairWrapper>) : Kind() {
|
||||
internal constructor(): this(null, listOf())
|
||||
internal constructor() : this(null, listOf())
|
||||
}
|
||||
class NameChange(var name: String) : Kind() {
|
||||
internal constructor(): this("")
|
||||
internal constructor() : this("")
|
||||
}
|
||||
class MembersAdded(var members: List<ByteString>) : Kind() {
|
||||
internal constructor(): this(listOf())
|
||||
internal constructor() : this(listOf())
|
||||
}
|
||||
class MembersRemoved(var members: List<ByteString>) : Kind() {
|
||||
internal constructor(): this(listOf())
|
||||
internal constructor() : this(listOf())
|
||||
}
|
||||
class MemberLeft() : Kind()
|
||||
|
||||
val description: String =
|
||||
when(this) {
|
||||
when (this) {
|
||||
is New -> "new"
|
||||
is EncryptionKeyPair -> "encryptionKeyPair"
|
||||
is NameChange -> "nameChange"
|
||||
@@ -65,18 +81,19 @@ class ClosedGroupControlMessage() : ControlMessage() {
|
||||
|
||||
fun fromProto(proto: SignalServiceProtos.Content): ClosedGroupControlMessage? {
|
||||
if (!proto.hasDataMessage() || !proto.dataMessage.hasClosedGroupControlMessage()) return null
|
||||
val closedGroupControlMessageProto = proto.dataMessage?.closedGroupControlMessage!!
|
||||
val closedGroupControlMessageProto = proto.dataMessage!!.closedGroupControlMessage!!
|
||||
val kind: Kind
|
||||
when (closedGroupControlMessageProto.type) {
|
||||
when (closedGroupControlMessageProto.type!!) {
|
||||
DataMessage.ClosedGroupControlMessage.Type.NEW -> {
|
||||
val publicKey = closedGroupControlMessageProto.publicKey ?: return null
|
||||
val name = closedGroupControlMessageProto.name ?: return null
|
||||
val encryptionKeyPairAsProto = closedGroupControlMessageProto.encryptionKeyPair ?: return null
|
||||
try {
|
||||
val encryptionKeyPair = ECKeyPair(DjbECPublicKey(encryptionKeyPairAsProto.publicKey.toByteArray()), DjbECPrivateKey(encryptionKeyPairAsProto.privateKey.toByteArray()))
|
||||
val encryptionKeyPair = ECKeyPair(DjbECPublicKey(encryptionKeyPairAsProto.publicKey.toByteArray()),
|
||||
DjbECPrivateKey(encryptionKeyPairAsProto.privateKey.toByteArray()))
|
||||
kind = Kind.New(publicKey, name, encryptionKeyPair, closedGroupControlMessageProto.membersList, closedGroupControlMessageProto.adminsList)
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Couldn't parse key pair")
|
||||
Log.w(TAG, "Couldn't parse key pair from proto: $encryptionKeyPairAsProto.")
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -107,26 +124,10 @@ class ClosedGroupControlMessage() : ControlMessage() {
|
||||
this.kind = kind
|
||||
}
|
||||
|
||||
override fun isValid(): Boolean {
|
||||
if (!super.isValid()) return false
|
||||
val kind = kind ?: return false
|
||||
return when(kind) {
|
||||
is Kind.New -> {
|
||||
!kind.publicKey.isEmpty && kind.name.isNotEmpty() && kind.encryptionKeyPair!!.publicKey != null
|
||||
&& kind.encryptionKeyPair!!.privateKey != null && kind.members.isNotEmpty() && kind.admins.isNotEmpty()
|
||||
}
|
||||
is Kind.EncryptionKeyPair -> true
|
||||
is Kind.NameChange -> kind.name.isNotEmpty()
|
||||
is Kind.MembersAdded -> kind.members.isNotEmpty()
|
||||
is Kind.MembersRemoved -> kind.members.isNotEmpty()
|
||||
is Kind.MemberLeft -> true
|
||||
}
|
||||
}
|
||||
|
||||
override fun toProto(): SignalServiceProtos.Content? {
|
||||
val kind = kind
|
||||
if (kind == null) {
|
||||
Log.w(TAG, "Couldn't construct closed group update proto from: $this")
|
||||
Log.w(TAG, "Couldn't construct closed group control message proto from: $this.")
|
||||
return null
|
||||
}
|
||||
try {
|
||||
@@ -176,7 +177,7 @@ class ClosedGroupControlMessage() : ControlMessage() {
|
||||
contentProto.dataMessage = dataMessageProto.build()
|
||||
return contentProto.build()
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Couldn't construct closed group update proto from: $this")
|
||||
Log.w(TAG, "Couldn't construct closed group control message proto from: $this.")
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -188,6 +189,7 @@ class ClosedGroupControlMessage() : ControlMessage() {
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
fun fromProto(proto: DataMessage.ClosedGroupControlMessage.KeyPairWrapper): KeyPairWrapper {
|
||||
return KeyPairWrapper(proto.publicKey.toByteArray().toHexString(), proto.encryptedKeyPair)
|
||||
}
|
||||
@@ -199,7 +201,6 @@ class ClosedGroupControlMessage() : ControlMessage() {
|
||||
val result = DataMessage.ClosedGroupControlMessage.KeyPairWrapper.newBuilder()
|
||||
result.publicKey = ByteString.copyFrom(Hex.fromStringCondensed(publicKey))
|
||||
result.encryptedKeyPair = encryptedKeyPair
|
||||
|
||||
return try {
|
||||
result.build()
|
||||
} catch (e: Exception) {
|
||||
|
@@ -2,24 +2,27 @@ package org.session.libsession.messaging.messages.control
|
||||
|
||||
import com.google.protobuf.ByteString
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.messaging.threads.Address
|
||||
import org.session.libsession.utilities.Address
|
||||
import org.session.libsession.utilities.GroupUtil
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsession.utilities.preferences.ProfileKeyUtil
|
||||
import org.session.libsignal.libsignal.ecc.DjbECPrivateKey
|
||||
import org.session.libsignal.libsignal.ecc.DjbECPublicKey
|
||||
import org.session.libsignal.libsignal.ecc.ECKeyPair
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos
|
||||
import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded
|
||||
import org.session.libsignal.service.loki.utilities.toHexString
|
||||
import org.session.libsession.utilities.ProfileKeyUtil
|
||||
import org.session.libsignal.crypto.ecc.DjbECPrivateKey
|
||||
import org.session.libsignal.crypto.ecc.DjbECPublicKey
|
||||
import org.session.libsignal.crypto.ecc.ECKeyPair
|
||||
import org.session.libsignal.protos.SignalServiceProtos
|
||||
import org.session.libsignal.utilities.removing05PrefixIfNeeded
|
||||
import org.session.libsignal.utilities.toHexString
|
||||
import org.session.libsignal.utilities.Hex
|
||||
|
||||
class ConfigurationMessage(var closedGroups: List<ClosedGroup>, var openGroups: List<String>, var contacts: List<Contact>, var displayName: String, var profilePicture: String?, var profileKey: ByteArray): ControlMessage() {
|
||||
class ConfigurationMessage(var closedGroups: List<ClosedGroup>, var openGroups: List<String>, var contacts: List<Contact>,
|
||||
var displayName: String, var profilePicture: String?, var profileKey: ByteArray) : ControlMessage() {
|
||||
|
||||
override val isSelfSendValid: Boolean = true
|
||||
|
||||
class ClosedGroup(var publicKey: String, var name: String, var encryptionKeyPair: ECKeyPair?, var members: List<String>, var admins: List<String>) {
|
||||
val isValid: Boolean get() = members.isNotEmpty() && admins.isNotEmpty()
|
||||
|
||||
internal constructor(): this("", "", null, listOf(), listOf())
|
||||
internal constructor() : this("", "", null, listOf(), listOf())
|
||||
|
||||
override fun toString(): String {
|
||||
return name
|
||||
@@ -56,7 +59,7 @@ class ConfigurationMessage(var closedGroups: List<ClosedGroup>, var openGroups:
|
||||
|
||||
class Contact(var publicKey: String, var name: String, var profilePicture: String?, var profileKey: ByteArray?) {
|
||||
|
||||
internal constructor(): this("", "", null, null)
|
||||
internal constructor() : this("", "", null, null)
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -66,8 +69,7 @@ class ConfigurationMessage(var closedGroups: List<ClosedGroup>, var openGroups:
|
||||
val name = proto.name
|
||||
val profilePicture = if (proto.hasProfilePicture()) proto.profilePicture else null
|
||||
val profileKey = if (proto.hasProfileKey()) proto.profileKey.toByteArray() else null
|
||||
|
||||
return Contact(publicKey,name,profilePicture,profileKey)
|
||||
return Contact(publicKey, name, profilePicture, profileKey)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,18 +81,18 @@ class ConfigurationMessage(var closedGroups: List<ClosedGroup>, var openGroups:
|
||||
} catch (e: Exception) {
|
||||
return null
|
||||
}
|
||||
if (!this.profilePicture.isNullOrEmpty()) {
|
||||
result.profilePicture = this.profilePicture
|
||||
val profilePicture = profilePicture
|
||||
if (!profilePicture.isNullOrEmpty()) {
|
||||
result.profilePicture = profilePicture
|
||||
}
|
||||
if (this.profileKey != null) {
|
||||
result.profileKey = ByteString.copyFrom(this.profileKey)
|
||||
val profileKey = profileKey
|
||||
if (profileKey != null) {
|
||||
result.profileKey = ByteString.copyFrom(profileKey)
|
||||
}
|
||||
return result.build()
|
||||
}
|
||||
}
|
||||
|
||||
override val isSelfSendValid: Boolean = true
|
||||
|
||||
companion object {
|
||||
|
||||
fun getCurrent(contacts: List<Contact>): ConfigurationMessage? {
|
||||
@@ -103,24 +105,22 @@ class ConfigurationMessage(var closedGroups: List<ClosedGroup>, var openGroups:
|
||||
val profilePicture = TextSecurePreferences.getProfilePictureURL(context)
|
||||
val profileKey = ProfileKeyUtil.getProfileKey(context)
|
||||
val groups = storage.getAllGroups()
|
||||
for (groupRecord in groups) {
|
||||
if (groupRecord.isClosedGroup) {
|
||||
if (!groupRecord.members.contains(Address.fromSerialized(storage.getUserPublicKey()!!))) continue
|
||||
val groupPublicKey = GroupUtil.doubleDecodeGroupID(groupRecord.encodedId).toHexString()
|
||||
for (group in groups) {
|
||||
if (group.isClosedGroup) {
|
||||
if (!group.members.contains(Address.fromSerialized(storage.getUserPublicKey()!!))) continue
|
||||
val groupPublicKey = GroupUtil.doubleDecodeGroupID(group.encodedId).toHexString()
|
||||
val encryptionKeyPair = storage.getLatestClosedGroupEncryptionKeyPair(groupPublicKey) ?: continue
|
||||
val closedGroup = ClosedGroup(groupPublicKey, groupRecord.title, encryptionKeyPair, groupRecord.members.map { it.serialize() }, groupRecord.admins.map { it.serialize() })
|
||||
val closedGroup = ClosedGroup(groupPublicKey, group.title, encryptionKeyPair, group.members.map { it.serialize() }, group.admins.map { it.serialize() })
|
||||
closedGroups.add(closedGroup)
|
||||
}
|
||||
if (groupRecord.isOpenGroup) {
|
||||
val threadID = storage.getThreadID(groupRecord.encodedId) ?: continue
|
||||
if (group.isOpenGroup) {
|
||||
val threadID = storage.getThreadID(group.encodedId) ?: continue
|
||||
val openGroup = storage.getOpenGroup(threadID)
|
||||
val openGroupV2 = storage.getV2OpenGroup(threadID)
|
||||
|
||||
val shareUrl = openGroup?.server ?: openGroupV2?.toJoinUrl() ?: continue
|
||||
val shareUrl = openGroup?.server ?: openGroupV2?.joinURL ?: continue
|
||||
openGroups.add(shareUrl)
|
||||
}
|
||||
}
|
||||
|
||||
return ConfigurationMessage(closedGroups, openGroups, contacts, displayName, profilePicture, profileKey)
|
||||
}
|
||||
|
||||
@@ -145,6 +145,7 @@ class ConfigurationMessage(var closedGroups: List<ClosedGroup>, var openGroups:
|
||||
configurationProto.addAllOpenGroups(openGroups)
|
||||
configurationProto.addAllContacts(this.contacts.mapNotNull { it.toProto() })
|
||||
configurationProto.displayName = displayName
|
||||
val profilePicture = profilePicture
|
||||
if (!profilePicture.isNullOrEmpty()) {
|
||||
configurationProto.profilePicture = profilePicture
|
||||
}
|
||||
@@ -157,10 +158,10 @@ class ConfigurationMessage(var closedGroups: List<ClosedGroup>, var openGroups:
|
||||
override fun toString(): String {
|
||||
return """
|
||||
ConfigurationMessage(
|
||||
closedGroups: ${(closedGroups)}
|
||||
openGroups: ${(openGroups)}
|
||||
displayName: $displayName
|
||||
profilePicture: $profilePicture
|
||||
closedGroups: ${(closedGroups)},
|
||||
openGroups: ${(openGroups)},
|
||||
displayName: $displayName,
|
||||
profilePicture: $profilePicture,
|
||||
profileKey: $profileKey
|
||||
)
|
||||
""".trimIndent()
|
||||
|
@@ -2,5 +2,4 @@ package org.session.libsession.messaging.messages.control
|
||||
|
||||
import org.session.libsession.messaging.messages.Message
|
||||
|
||||
abstract class ControlMessage : Message() {
|
||||
}
|
||||
abstract class ControlMessage : Message()
|
@@ -1,9 +1,9 @@
|
||||
package org.session.libsession.messaging.messages.control
|
||||
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
import org.session.libsignal.protos.SignalServiceProtos
|
||||
import org.session.libsignal.utilities.Log
|
||||
|
||||
class DataExtractionNotification(): ControlMessage() {
|
||||
class DataExtractionNotification() : ControlMessage() {
|
||||
var kind: Kind? = null
|
||||
|
||||
sealed class Kind {
|
||||
@@ -39,8 +39,8 @@ class DataExtractionNotification(): ControlMessage() {
|
||||
}
|
||||
|
||||
override fun isValid(): Boolean {
|
||||
if (!super.isValid()) return false
|
||||
val kind = kind ?: return false
|
||||
val kind = kind
|
||||
if (!super.isValid() || kind == null) return false
|
||||
return when(kind) {
|
||||
is Kind.Screenshot -> true
|
||||
is Kind.MediaSaved -> kind.timestamp > 0
|
||||
|
@@ -2,17 +2,24 @@ package org.session.libsession.messaging.messages.control
|
||||
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.messaging.messages.visible.VisibleMessage
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.session.libsignal.protos.SignalServiceProtos
|
||||
|
||||
class ExpirationTimerUpdate() : ControlMessage() {
|
||||
/// In the case of a sync message, the public key of the person the message was targeted at.
|
||||
/// - Note: `nil` if this isn't a sync message.
|
||||
/** In the case of a sync message, the public key of the person the message was targeted at.
|
||||
*
|
||||
* **Note:** `nil` if this isn't a sync message.
|
||||
*/
|
||||
var syncTarget: String? = null
|
||||
var duration: Int? = 0
|
||||
|
||||
override val isSelfSendValid: Boolean = true
|
||||
|
||||
override fun isValid(): Boolean {
|
||||
if (!super.isValid()) return false
|
||||
return duration != null
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TAG = "ExpirationTimerUpdate"
|
||||
|
||||
@@ -26,19 +33,14 @@ class ExpirationTimerUpdate() : ControlMessage() {
|
||||
}
|
||||
}
|
||||
|
||||
internal constructor(syncTarget: String?, duration: Int) : this() {
|
||||
this.syncTarget = syncTarget
|
||||
this.duration = duration
|
||||
}
|
||||
|
||||
internal constructor(duration: Int) : this() {
|
||||
this.syncTarget = null
|
||||
this.duration = duration
|
||||
}
|
||||
|
||||
override fun isValid(): Boolean {
|
||||
if (!super.isValid()) return false
|
||||
return duration != null
|
||||
internal constructor(syncTarget: String, duration: Int) : this() {
|
||||
this.syncTarget = syncTarget
|
||||
this.duration = duration
|
||||
}
|
||||
|
||||
override fun toProto(): SignalServiceProtos.Content? {
|
||||
|
@@ -1,11 +1,18 @@
|
||||
package org.session.libsession.messaging.messages.control
|
||||
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
import org.session.libsignal.protos.SignalServiceProtos
|
||||
import org.session.libsignal.utilities.Log
|
||||
|
||||
class ReadReceipt() : ControlMessage() {
|
||||
var timestamps: List<Long>? = null
|
||||
|
||||
override fun isValid(): Boolean {
|
||||
if (!super.isValid()) return false
|
||||
val timestamps = timestamps ?: return false
|
||||
if (timestamps.isNotEmpty()) { return true }
|
||||
return false
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TAG = "ReadReceipt"
|
||||
|
||||
@@ -22,13 +29,6 @@ class ReadReceipt() : ControlMessage() {
|
||||
this.timestamps = timestamps
|
||||
}
|
||||
|
||||
override fun isValid(): Boolean {
|
||||
if (!super.isValid()) return false
|
||||
val timestamps = timestamps ?: return false
|
||||
if (timestamps.isNotEmpty()) { return true }
|
||||
return false
|
||||
}
|
||||
|
||||
override fun toProto(): SignalServiceProtos.Content? {
|
||||
val timestamps = timestamps
|
||||
if (timestamps == null) {
|
||||
|
@@ -1,12 +1,18 @@
|
||||
package org.session.libsession.messaging.messages.control
|
||||
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
import org.session.libsignal.protos.SignalServiceProtos
|
||||
import org.session.libsignal.utilities.Log
|
||||
|
||||
class TypingIndicator() : ControlMessage() {
|
||||
override val ttl: Long = 30 * 1000
|
||||
var kind: Kind? = null
|
||||
|
||||
override val ttl: Long = 20 * 1000
|
||||
|
||||
override fun isValid(): Boolean {
|
||||
if (!super.isValid()) return false
|
||||
return kind != null
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TAG = "TypingIndicator"
|
||||
|
||||
@@ -41,11 +47,6 @@ class TypingIndicator() : ControlMessage() {
|
||||
this.kind = kind
|
||||
}
|
||||
|
||||
override fun isValid(): Boolean {
|
||||
if (!super.isValid()) return false
|
||||
return kind != null
|
||||
}
|
||||
|
||||
override fun toProto(): SignalServiceProtos.Content? {
|
||||
val timestamp = sentTimestamp
|
||||
val kind = kind
|
||||
|
@@ -4,14 +4,14 @@ import org.session.libsession.messaging.messages.visible.VisibleMessage;
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
|
||||
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.sharecontacts.Contact;
|
||||
import org.session.libsession.messaging.threads.Address;
|
||||
import org.session.libsession.utilities.Contact;
|
||||
import org.session.libsession.utilities.Address;
|
||||
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview;
|
||||
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel;
|
||||
import org.session.libsession.utilities.GroupUtil;
|
||||
import org.session.libsignal.libsignal.util.guava.Optional;
|
||||
import org.session.libsignal.service.api.messages.SignalServiceAttachment;
|
||||
import org.session.libsignal.service.api.messages.SignalServiceGroup;
|
||||
import org.session.libsignal.utilities.guava.Optional;
|
||||
import org.session.libsignal.messages.SignalServiceAttachment;
|
||||
import org.session.libsignal.messages.SignalServiceGroup;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
|
@@ -5,11 +5,13 @@ import android.os.Parcelable;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.session.libsession.messaging.messages.visible.OpenGroupInvitation;
|
||||
import org.session.libsession.messaging.messages.visible.VisibleMessage;
|
||||
import org.session.libsession.messaging.threads.Address;
|
||||
import org.session.libsession.utilities.Address;
|
||||
import org.session.libsession.messaging.utilities.UpdateMessageData;
|
||||
import org.session.libsession.utilities.GroupUtil;
|
||||
import org.session.libsignal.libsignal.util.guava.Optional;
|
||||
import org.session.libsignal.service.api.messages.SignalServiceGroup;
|
||||
import org.session.libsignal.utilities.guava.Optional;
|
||||
import org.session.libsignal.messages.SignalServiceGroup;
|
||||
|
||||
public class IncomingTextMessage implements Parcelable {
|
||||
|
||||
@@ -40,6 +42,8 @@ public class IncomingTextMessage implements Parcelable {
|
||||
private final long expiresInMillis;
|
||||
private final boolean unidentified;
|
||||
|
||||
private boolean isOpenGroupInvitation = false;
|
||||
|
||||
public IncomingTextMessage(Address sender, int senderDeviceId, long sentTimestampMillis,
|
||||
String encodedBody, Optional<SignalServiceGroup> group,
|
||||
long expiresInMillis, boolean unidentified)
|
||||
@@ -94,6 +98,7 @@ public class IncomingTextMessage implements Parcelable {
|
||||
this.subscriptionId = base.getSubscriptionId();
|
||||
this.expiresInMillis = base.getExpiresIn();
|
||||
this.unidentified = base.isUnidentified();
|
||||
this.isOpenGroupInvitation= base.isOpenGroupInvitation();
|
||||
}
|
||||
|
||||
public static IncomingTextMessage from(VisibleMessage message,
|
||||
@@ -104,6 +109,18 @@ public class IncomingTextMessage implements Parcelable {
|
||||
return new IncomingTextMessage(sender, 1, message.getSentTimestamp(), message.getText(), group, expiresInMillis, false);
|
||||
}
|
||||
|
||||
public static IncomingTextMessage fromOpenGroupInvitation(OpenGroupInvitation openGroupInvitation, Address sender, Long sentTimestamp)
|
||||
{
|
||||
String url = openGroupInvitation.getUrl();
|
||||
String name = openGroupInvitation.getName();
|
||||
if (url == null || name == null) { return null; }
|
||||
// FIXME: Doing toJSON() to get the body here is weird
|
||||
String body = UpdateMessageData.Companion.buildOpenGroupInvitation(url, name).toJSON();
|
||||
IncomingTextMessage incomingTextMessage = new IncomingTextMessage(sender, 1, sentTimestamp, body, Optional.absent(), 0, false);
|
||||
incomingTextMessage.isOpenGroupInvitation = true;
|
||||
return incomingTextMessage;
|
||||
}
|
||||
|
||||
public int getSubscriptionId() {
|
||||
return subscriptionId;
|
||||
}
|
||||
@@ -163,6 +180,9 @@ public class IncomingTextMessage implements Parcelable {
|
||||
public boolean isUnidentified() {
|
||||
return unidentified;
|
||||
}
|
||||
|
||||
public boolean isOpenGroupInvitation() { return isOpenGroupInvitation; }
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
|
@@ -1,8 +1,8 @@
|
||||
package org.session.libsession.messaging.messages.signal;
|
||||
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
|
||||
import org.session.libsession.messaging.threads.DistributionTypes;
|
||||
import org.session.libsession.messaging.threads.recipients.Recipient;
|
||||
import org.session.libsession.utilities.DistributionTypes;
|
||||
import org.session.libsession.utilities.recipients.Recipient;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
|
@@ -3,12 +3,12 @@ package org.session.libsession.messaging.messages.signal;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.session.libsession.messaging.threads.DistributionTypes;
|
||||
import org.session.libsession.utilities.DistributionTypes;
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
|
||||
import org.session.libsession.messaging.sending_receiving.sharecontacts.Contact;
|
||||
import org.session.libsession.utilities.Contact;
|
||||
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview;
|
||||
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel;
|
||||
import org.session.libsession.messaging.threads.recipients.Recipient;
|
||||
import org.session.libsession.utilities.recipients.Recipient;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
@@ -4,14 +4,14 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.session.libsession.messaging.messages.visible.VisibleMessage;
|
||||
import org.session.libsession.messaging.threads.DistributionTypes;
|
||||
import org.session.libsession.database.documents.IdentityKeyMismatch;
|
||||
import org.session.libsession.database.documents.NetworkFailure;
|
||||
import org.session.libsession.utilities.DistributionTypes;
|
||||
import org.session.libsession.utilities.IdentityKeyMismatch;
|
||||
import org.session.libsession.utilities.NetworkFailure;
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
|
||||
import org.session.libsession.messaging.sending_receiving.sharecontacts.Contact;
|
||||
import org.session.libsession.utilities.Contact;
|
||||
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview;
|
||||
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel;
|
||||
import org.session.libsession.messaging.threads.recipients.Recipient;
|
||||
import org.session.libsession.utilities.recipients.Recipient;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
|
@@ -4,10 +4,10 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
|
||||
import org.session.libsession.messaging.sending_receiving.sharecontacts.Contact;
|
||||
import org.session.libsession.utilities.Contact;
|
||||
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview;
|
||||
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel;
|
||||
import org.session.libsession.messaging.threads.recipients.Recipient;
|
||||
import org.session.libsession.utilities.recipients.Recipient;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
@@ -1,15 +1,17 @@
|
||||
package org.session.libsession.messaging.messages.signal;
|
||||
|
||||
import org.session.libsession.messaging.messages.visible.OpenGroupInvitation;
|
||||
import org.session.libsession.messaging.messages.visible.VisibleMessage;
|
||||
import org.session.libsession.messaging.threads.recipients.Recipient;
|
||||
import org.session.libsession.utilities.recipients.Recipient;
|
||||
import org.session.libsession.messaging.utilities.UpdateMessageData;
|
||||
|
||||
public class OutgoingTextMessage {
|
||||
|
||||
private final Recipient recipient;
|
||||
private final String message;
|
||||
private final int subscriptionId;
|
||||
private final long expiresIn;
|
||||
private final long sentTimestampMillis;
|
||||
private boolean isOpenGroupInvitation = false;
|
||||
|
||||
public OutgoingTextMessage(Recipient recipient, String message, long expiresIn, int subscriptionId, long sentTimestampMillis) {
|
||||
this.recipient = recipient;
|
||||
@@ -23,6 +25,17 @@ public class OutgoingTextMessage {
|
||||
return new OutgoingTextMessage(recipient, message.getText(), recipient.getExpireMessages() * 1000, -1, message.getSentTimestamp());
|
||||
}
|
||||
|
||||
public static OutgoingTextMessage fromOpenGroupInvitation(OpenGroupInvitation openGroupInvitation, Recipient recipient, Long sentTimestamp) {
|
||||
String url = openGroupInvitation.getUrl();
|
||||
String name = openGroupInvitation.getName();
|
||||
if (url == null || name == null) { return null; }
|
||||
// FIXME: Doing toJSON() to get the body here is weird
|
||||
String body = UpdateMessageData.Companion.buildOpenGroupInvitation(url, name).toJSON();
|
||||
OutgoingTextMessage outgoingTextMessage = new OutgoingTextMessage(recipient, body, 0, -1, sentTimestamp);
|
||||
outgoingTextMessage.isOpenGroupInvitation = true;
|
||||
return outgoingTextMessage;
|
||||
}
|
||||
|
||||
public long getExpiresIn() {
|
||||
return expiresIn;
|
||||
}
|
||||
@@ -46,4 +59,6 @@ public class OutgoingTextMessage {
|
||||
public boolean isSecureMessage() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean isOpenGroupInvitation() { return isOpenGroupInvitation; }
|
||||
}
|
||||
|
@@ -5,9 +5,9 @@ import android.webkit.MimeTypeMap
|
||||
import com.google.protobuf.ByteString
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.Attachment as SignalAttachment
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.PointerAttachment
|
||||
import org.session.libsignal.libsignal.util.guava.Optional
|
||||
import org.session.libsignal.service.api.messages.SignalServiceAttachmentPointer
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos
|
||||
import org.session.libsignal.utilities.guava.Optional
|
||||
import org.session.libsignal.messages.SignalServiceAttachmentPointer
|
||||
import org.session.libsignal.protos.SignalServiceProtos
|
||||
import java.io.File
|
||||
|
||||
class Attachment {
|
||||
|
@@ -2,14 +2,18 @@ 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.logging.Log
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.session.libsignal.protos.SignalServiceProtos
|
||||
|
||||
class LinkPreview() {
|
||||
var title: String? = null
|
||||
var url: String? = null
|
||||
var attachmentID: Long? = 0
|
||||
|
||||
fun isValid(): Boolean {
|
||||
return (title != null && url != null && attachmentID != null)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TAG = "LinkPreview"
|
||||
|
||||
@@ -20,11 +24,8 @@ class LinkPreview() {
|
||||
}
|
||||
|
||||
fun from(signalLinkPreview: SignalLinkPreiview?): LinkPreview? {
|
||||
return if (signalLinkPreview == null) {
|
||||
null
|
||||
} else {
|
||||
LinkPreview(signalLinkPreview.title, signalLinkPreview.url, signalLinkPreview.attachmentId?.rowId)
|
||||
}
|
||||
if (signalLinkPreview == null) { return null }
|
||||
return LinkPreview(signalLinkPreview.title, signalLinkPreview.url, signalLinkPreview.attachmentId?.rowId)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,10 +35,6 @@ class LinkPreview() {
|
||||
this.attachmentID = attachmentID
|
||||
}
|
||||
|
||||
fun isValid(): Boolean {
|
||||
return (title != null && url != null && attachmentID != null)
|
||||
}
|
||||
|
||||
fun toProto(): SignalServiceProtos.DataMessage.Preview? {
|
||||
val url = url
|
||||
if (url == null) {
|
||||
@@ -46,10 +43,10 @@ class LinkPreview() {
|
||||
}
|
||||
val linkPreviewProto = SignalServiceProtos.DataMessage.Preview.newBuilder()
|
||||
linkPreviewProto.url = url
|
||||
title?.let { linkPreviewProto.title = title }
|
||||
val attachmentID = attachmentID
|
||||
title?.let { linkPreviewProto.title = it }
|
||||
val database = MessagingModuleConfiguration.shared.messageDataProvider
|
||||
attachmentID?.let {
|
||||
MessagingModuleConfiguration.shared.messageDataProvider.getSignalAttachmentPointer(attachmentID)?.let {
|
||||
database.getSignalAttachmentPointer(it)?.let {
|
||||
val attachmentProto = Attachment.createAttachmentPointer(it)
|
||||
linkPreviewProto.image = attachmentProto
|
||||
}
|
||||
|
@@ -0,0 +1,38 @@
|
||||
package org.session.libsession.messaging.messages.visible
|
||||
|
||||
import org.session.libsignal.protos.SignalServiceProtos
|
||||
import org.session.libsignal.utilities.Log
|
||||
|
||||
class OpenGroupInvitation() {
|
||||
var url: String? = null
|
||||
var name: String? = null
|
||||
|
||||
fun isValid(): Boolean {
|
||||
return (url != null && name != null)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TAG = "OpenGroupInvitation"
|
||||
|
||||
fun fromProto(proto: SignalServiceProtos.DataMessage.OpenGroupInvitation): OpenGroupInvitation {
|
||||
return OpenGroupInvitation(proto.url, proto.name)
|
||||
}
|
||||
}
|
||||
|
||||
constructor(url: String?, serverName: String?): this() {
|
||||
this.url = url
|
||||
this.name = serverName
|
||||
}
|
||||
|
||||
fun toProto(): SignalServiceProtos.DataMessage.OpenGroupInvitation? {
|
||||
val openGroupInvitationProto = SignalServiceProtos.DataMessage.OpenGroupInvitation.newBuilder()
|
||||
openGroupInvitationProto.url = url
|
||||
openGroupInvitationProto.name = name
|
||||
return try {
|
||||
openGroupInvitationProto.build()
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Couldn't construct open group invitation proto from: $this.")
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,8 +1,8 @@
|
||||
package org.session.libsession.messaging.messages.visible
|
||||
|
||||
import com.google.protobuf.ByteString
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.session.libsignal.protos.SignalServiceProtos
|
||||
|
||||
class Profile() {
|
||||
var displayName: String? = null
|
||||
@@ -17,12 +17,11 @@ class Profile() {
|
||||
val displayName = profileProto.displayName ?: return null
|
||||
val profileKey = proto.profileKey
|
||||
val profilePictureURL = profileProto.profilePicture
|
||||
profileKey?.let {
|
||||
profilePictureURL?.let {
|
||||
return Profile(displayName = displayName, profileKey = profileKey.toByteArray(), profilePictureURL = profilePictureURL)
|
||||
}
|
||||
if (profileKey != null && profilePictureURL != null) {
|
||||
return Profile(displayName, profileKey.toByteArray(), profilePictureURL)
|
||||
} else {
|
||||
return Profile(displayName)
|
||||
}
|
||||
return Profile(displayName)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,16 +34,14 @@ class Profile() {
|
||||
fun toProto(): SignalServiceProtos.DataMessage? {
|
||||
val displayName = displayName
|
||||
if (displayName == null) {
|
||||
Log.w(TAG, "Couldn't construct link preview proto from: $this")
|
||||
Log.w(TAG, "Couldn't construct profile proto from: $this")
|
||||
return null
|
||||
}
|
||||
val dataMessageProto = SignalServiceProtos.DataMessage.newBuilder()
|
||||
val profileProto = SignalServiceProtos.DataMessage.LokiProfile.newBuilder()
|
||||
profileProto.displayName = displayName
|
||||
val profileKey = profileKey
|
||||
profileKey?.let { dataMessageProto.profileKey = ByteString.copyFrom(profileKey) }
|
||||
val profilePictureURL = profilePictureURL
|
||||
profilePictureURL?.let { profileProto.profilePicture = profilePictureURL }
|
||||
profileKey?.let { dataMessageProto.profileKey = ByteString.copyFrom(it) }
|
||||
profilePictureURL?.let { profileProto.profilePicture = it }
|
||||
// Build
|
||||
try {
|
||||
dataMessageProto.profile = profileProto.build()
|
||||
|
@@ -1,11 +1,11 @@
|
||||
package org.session.libsession.messaging.messages.visible
|
||||
|
||||
import com.goterl.lazycode.lazysodium.BuildConfig
|
||||
import com.goterl.lazysodium.BuildConfig
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
|
||||
import org.session.libsignal.protos.SignalServiceProtos
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel as SignalQuote
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos
|
||||
|
||||
class Quote() {
|
||||
var timestamp: Long? = 0
|
||||
@@ -13,6 +13,10 @@ class Quote() {
|
||||
var text: String? = null
|
||||
var attachmentID: Long? = null
|
||||
|
||||
fun isValid(): Boolean {
|
||||
return (timestamp != null && publicKey != null)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TAG = "Quote"
|
||||
|
||||
@@ -24,12 +28,9 @@ class Quote() {
|
||||
}
|
||||
|
||||
fun from(signalQuote: SignalQuote?): Quote? {
|
||||
return if (signalQuote == null) {
|
||||
null
|
||||
} else {
|
||||
val attachmentID = (signalQuote.attachments?.firstOrNull() as? DatabaseAttachment)?.attachmentId?.rowId
|
||||
Quote(signalQuote.id, signalQuote.author.serialize(), signalQuote.text, attachmentID)
|
||||
}
|
||||
if (signalQuote == null) { return null }
|
||||
val attachmentID = (signalQuote.attachments?.firstOrNull() as? DatabaseAttachment)?.attachmentId?.rowId
|
||||
return Quote(signalQuote.id, signalQuote.author.serialize(), signalQuote.text, attachmentID)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,10 +41,6 @@ class Quote() {
|
||||
this.attachmentID = attachmentID
|
||||
}
|
||||
|
||||
fun isValid(): Boolean {
|
||||
return (timestamp != null && publicKey != null)
|
||||
}
|
||||
|
||||
fun toProto(): SignalServiceProtos.DataMessage.Quote? {
|
||||
val timestamp = timestamp
|
||||
val publicKey = publicKey
|
||||
@@ -54,7 +51,7 @@ class Quote() {
|
||||
val quoteProto = SignalServiceProtos.DataMessage.Quote.newBuilder()
|
||||
quoteProto.id = timestamp
|
||||
quoteProto.author = publicKey
|
||||
text?.let { quoteProto.text = text }
|
||||
text?.let { quoteProto.text = it }
|
||||
addAttachmentsIfNeeded(quoteProto)
|
||||
// Build
|
||||
try {
|
||||
@@ -66,23 +63,23 @@ class Quote() {
|
||||
}
|
||||
|
||||
private fun addAttachmentsIfNeeded(quoteProto: SignalServiceProtos.DataMessage.Quote.Builder) {
|
||||
if (attachmentID == null) return
|
||||
val attachment = MessagingModuleConfiguration.shared.messageDataProvider.getSignalAttachmentPointer(attachmentID!!)
|
||||
if (attachment == null) {
|
||||
val attachmentID = attachmentID ?: return
|
||||
val database = MessagingModuleConfiguration.shared.messageDataProvider
|
||||
val pointer = database.getSignalAttachmentPointer(attachmentID)
|
||||
if (pointer == null) {
|
||||
Log.w(TAG, "Ignoring invalid attachment for quoted message.")
|
||||
return
|
||||
}
|
||||
if (attachment.url.isNullOrEmpty()) {
|
||||
if (pointer.url.isNullOrEmpty()) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
//TODO equivalent to iOS's preconditionFailure
|
||||
Log.d(TAG,"Sending a message before all associated attachments have been uploaded.")
|
||||
Log.w(TAG,"Sending a message before all associated attachments have been uploaded.")
|
||||
return
|
||||
}
|
||||
}
|
||||
val quotedAttachmentProto = SignalServiceProtos.DataMessage.Quote.QuotedAttachment.newBuilder()
|
||||
quotedAttachmentProto.contentType = attachment.contentType
|
||||
if (attachment.fileName.isPresent) quotedAttachmentProto.fileName = attachment.fileName.get()
|
||||
quotedAttachmentProto.thumbnail = Attachment.createAttachmentPointer(attachment)
|
||||
quotedAttachmentProto.contentType = pointer.contentType
|
||||
if (pointer.fileName.isPresent) { quotedAttachmentProto.fileName = pointer.fileName.get() }
|
||||
quotedAttachmentProto.thumbnail = Attachment.createAttachmentPointer(pointer)
|
||||
try {
|
||||
quoteProto.addAttachments(quotedAttachmentProto.build())
|
||||
} catch (e: Exception) {
|
||||
|
@@ -1,79 +1,78 @@
|
||||
package org.session.libsession.messaging.messages.visible
|
||||
|
||||
import com.goterl.lazycode.lazysodium.BuildConfig
|
||||
import com.goterl.lazysodium.BuildConfig
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.messaging.messages.Message
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
|
||||
import org.session.libsession.messaging.threads.Address
|
||||
import org.session.libsession.messaging.threads.recipients.Recipient
|
||||
import org.session.libsession.utilities.Address
|
||||
import org.session.libsession.utilities.GroupUtil
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.session.libsignal.protos.SignalServiceProtos
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.Attachment as SignalAttachment
|
||||
|
||||
class VisibleMessage : Message() {
|
||||
/** In the case of a sync message, the public key of the person the message was targeted at.
|
||||
*
|
||||
* **Note:** `nil` if this isn't a sync message.
|
||||
*/
|
||||
var syncTarget: String? = null
|
||||
var text: String? = null
|
||||
val attachmentIDs: MutableList<Long> = mutableListOf()
|
||||
var quote: Quote? = null
|
||||
var linkPreview: LinkPreview? = null
|
||||
var profile: Profile? = null
|
||||
var openGroupInvitation: OpenGroupInvitation? = null
|
||||
|
||||
override val isSelfSendValid: Boolean = true
|
||||
|
||||
// region Validation
|
||||
override fun isValid(): Boolean {
|
||||
if (!super.isValid()) return false
|
||||
if (attachmentIDs.isNotEmpty()) return true
|
||||
if (openGroupInvitation != null) return true
|
||||
val text = text?.trim() ?: return false
|
||||
if (text.isNotEmpty()) return true
|
||||
return false
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Proto Conversion
|
||||
companion object {
|
||||
const val TAG = "VisibleMessage"
|
||||
|
||||
fun fromProto(proto: SignalServiceProtos.Content): VisibleMessage? {
|
||||
val dataMessage = if (proto.hasDataMessage()) proto.dataMessage else return null
|
||||
val dataMessage = proto.dataMessage ?: return null
|
||||
val result = VisibleMessage()
|
||||
if (dataMessage.hasSyncTarget()) {
|
||||
result.syncTarget = dataMessage.syncTarget
|
||||
}
|
||||
if (dataMessage.hasSyncTarget()) { result.syncTarget = dataMessage.syncTarget }
|
||||
result.text = dataMessage.body
|
||||
// Attachments are handled in MessageReceiver
|
||||
val quoteProto = if (dataMessage.hasQuote()) dataMessage.quote else null
|
||||
quoteProto?.let {
|
||||
if (quoteProto != null) {
|
||||
val quote = Quote.fromProto(quoteProto)
|
||||
quote?.let { result.quote = quote }
|
||||
result.quote = quote
|
||||
}
|
||||
val linkPreviewProto = dataMessage.previewList.firstOrNull()
|
||||
linkPreviewProto?.let {
|
||||
if (linkPreviewProto != null) {
|
||||
val linkPreview = LinkPreview.fromProto(linkPreviewProto)
|
||||
linkPreview?.let { result.linkPreview = linkPreview }
|
||||
result.linkPreview = linkPreview
|
||||
}
|
||||
val openGroupInvitationProto = if (dataMessage.hasOpenGroupInvitation()) dataMessage.openGroupInvitation else null
|
||||
if (openGroupInvitationProto != null) {
|
||||
val openGroupInvitation = OpenGroupInvitation.fromProto(openGroupInvitationProto)
|
||||
result.openGroupInvitation = openGroupInvitation
|
||||
}
|
||||
// TODO Contact
|
||||
val profile = Profile.fromProto(dataMessage)
|
||||
profile?.let { result.profile = profile }
|
||||
if (profile != null) { result.profile = profile }
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
fun addSignalAttachments(signalAttachments: List<SignalAttachment>) {
|
||||
val attachmentIDs = signalAttachments.map {
|
||||
val databaseAttachment = it as DatabaseAttachment
|
||||
databaseAttachment.attachmentId.rowId
|
||||
}
|
||||
this.attachmentIDs.addAll(attachmentIDs)
|
||||
}
|
||||
|
||||
fun isMediaMessage(): Boolean {
|
||||
return attachmentIDs.isNotEmpty() || quote != null || linkPreview != null
|
||||
}
|
||||
|
||||
override fun isValid(): Boolean {
|
||||
if (!super.isValid()) return false
|
||||
if (attachmentIDs.isNotEmpty()) return true
|
||||
val text = text?.trim() ?: return false
|
||||
if (text.isNotEmpty()) return true
|
||||
return false
|
||||
}
|
||||
|
||||
override fun toProto(): SignalServiceProtos.Content? {
|
||||
val proto = SignalServiceProtos.Content.newBuilder()
|
||||
val dataMessage: SignalServiceProtos.DataMessage.Builder
|
||||
// Profile
|
||||
val profile = profile
|
||||
val profileProto = profile?.toProto()
|
||||
if (profileProto != null) {
|
||||
dataMessage = profileProto.toBuilder()
|
||||
@@ -81,43 +80,49 @@ class VisibleMessage : Message() {
|
||||
dataMessage = SignalServiceProtos.DataMessage.newBuilder()
|
||||
}
|
||||
// Text
|
||||
text?.let { dataMessage.body = text }
|
||||
if (text != null) { dataMessage.body = text }
|
||||
// Quote
|
||||
quote?.let {
|
||||
val quoteProto = it.toProto()
|
||||
if (quoteProto != null) dataMessage.quote = quoteProto
|
||||
val quoteProto = quote?.toProto()
|
||||
if (quoteProto != null) {
|
||||
dataMessage.quote = quoteProto
|
||||
}
|
||||
//Link preview
|
||||
linkPreview?.let {
|
||||
val linkPreviewProto = it.toProto()
|
||||
linkPreviewProto?.let {
|
||||
dataMessage.addAllPreview(listOf(linkPreviewProto))
|
||||
}
|
||||
// Link preview
|
||||
val linkPreviewProto = linkPreview?.toProto()
|
||||
if (linkPreviewProto != null) {
|
||||
dataMessage.addAllPreview(listOf(linkPreviewProto))
|
||||
}
|
||||
//Attachments
|
||||
val attachments = attachmentIDs.mapNotNull { MessagingModuleConfiguration.shared.messageDataProvider.getSignalAttachmentPointer(it) }
|
||||
if (!attachments.all { !it.url.isNullOrEmpty() }) {
|
||||
// Open group invitation
|
||||
val openGroupInvitationProto = openGroupInvitation?.toProto()
|
||||
if (openGroupInvitationProto != null) {
|
||||
dataMessage.openGroupInvitation = openGroupInvitationProto
|
||||
}
|
||||
// Attachments
|
||||
val database = MessagingModuleConfiguration.shared.messageDataProvider
|
||||
val attachments = attachmentIDs.mapNotNull { database.getSignalAttachmentPointer(it) }
|
||||
if (attachments.any { it.url.isNullOrEmpty() }) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
//TODO equivalent to iOS's preconditionFailure
|
||||
Log.d(TAG, "Sending a message before all associated attachments have been uploaded.")
|
||||
Log.w(TAG, "Sending a message before all associated attachments have been uploaded.")
|
||||
}
|
||||
}
|
||||
val attachmentPointers = attachments.mapNotNull { Attachment.createAttachmentPointer(it) }
|
||||
dataMessage.addAllAttachments(attachmentPointers)
|
||||
// TODO Contact
|
||||
val pointers = attachments.mapNotNull { Attachment.createAttachmentPointer(it) }
|
||||
dataMessage.addAllAttachments(pointers)
|
||||
// TODO: Contact
|
||||
// Expiration timer
|
||||
// TODO: We * want * expiration timer updates to be explicit. But currently Android will disable the expiration timer for a conversation
|
||||
// if it receives a message without the current expiration timer value attached to it...
|
||||
// if it receives a message without the current expiration timer value attached to it...
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val context = MessagingModuleConfiguration.shared.context
|
||||
val expiration = if (storage.isClosedGroup(recipient!!)) Recipient.from(context, Address.fromSerialized(GroupUtil.doubleEncodeGroupID(recipient!!)), false).expireMessages
|
||||
else Recipient.from(context, Address.fromSerialized(recipient!!), false).expireMessages
|
||||
val expiration = if (storage.isClosedGroup(recipient!!)) {
|
||||
Recipient.from(context, Address.fromSerialized(GroupUtil.doubleEncodeGroupID(recipient!!)), false).expireMessages
|
||||
} else {
|
||||
Recipient.from(context, Address.fromSerialized(recipient!!), false).expireMessages
|
||||
}
|
||||
dataMessage.expireTimer = expiration
|
||||
// Group context
|
||||
if (storage.isClosedGroup(recipient!!)) {
|
||||
try {
|
||||
setGroupContext(dataMessage)
|
||||
} catch(e: Exception) {
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Couldn't construct visible message proto from: $this")
|
||||
return null
|
||||
}
|
||||
@@ -135,4 +140,17 @@ class VisibleMessage : Message() {
|
||||
return null
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
|
||||
fun addSignalAttachments(signalAttachments: List<SignalAttachment>) {
|
||||
val attachmentIDs = signalAttachments.map {
|
||||
val databaseAttachment = it as DatabaseAttachment
|
||||
databaseAttachment.attachmentId.rowId
|
||||
}
|
||||
this.attachmentIDs.addAll(attachmentIDs)
|
||||
}
|
||||
|
||||
fun isMediaMessage(): Boolean {
|
||||
return attachmentIDs.isNotEmpty() || quote != null || linkPreview != null
|
||||
}
|
||||
}
|
@@ -1,6 +1,5 @@
|
||||
package org.session.libsession.messaging.open_groups
|
||||
|
||||
import nl.komponents.kovenant.Kovenant
|
||||
import nl.komponents.kovenant.Promise
|
||||
import nl.komponents.kovenant.deferred
|
||||
import nl.komponents.kovenant.functional.map
|
||||
@@ -9,12 +8,11 @@ import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.messaging.file_server.FileServerAPI
|
||||
import org.session.libsession.messaging.utilities.DotNetAPI
|
||||
import org.session.libsession.utilities.DownloadUtilities
|
||||
import org.session.libsignal.service.loki.utilities.retryIfNeeded
|
||||
import org.session.libsignal.utilities.retryIfNeeded
|
||||
import org.session.libsignal.utilities.*
|
||||
import org.session.libsignal.utilities.Base64
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
import org.session.libsignal.utilities.Log
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.IOException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
|
@@ -5,7 +5,6 @@ import com.fasterxml.jackson.databind.annotation.JsonNaming
|
||||
import com.fasterxml.jackson.databind.type.TypeFactory
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import nl.komponents.kovenant.Kovenant
|
||||
import nl.komponents.kovenant.Promise
|
||||
import nl.komponents.kovenant.functional.bind
|
||||
import nl.komponents.kovenant.functional.map
|
||||
@@ -14,77 +13,52 @@ import okhttp3.HttpUrl
|
||||
import okhttp3.MediaType
|
||||
import okhttp3.RequestBody
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2.Error
|
||||
import org.session.libsession.snode.OnionRequestAPI
|
||||
import org.session.libsession.utilities.AESGCM
|
||||
import org.session.libsignal.service.loki.HTTP
|
||||
import org.session.libsignal.service.loki.HTTP.Verb.*
|
||||
import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded
|
||||
import org.session.libsignal.service.loki.utilities.toHexString
|
||||
import org.session.libsignal.utilities.HTTP
|
||||
import org.session.libsignal.utilities.HTTP.Verb.*
|
||||
import org.session.libsignal.utilities.removing05PrefixIfNeeded
|
||||
import org.session.libsignal.utilities.toHexString
|
||||
import org.session.libsignal.utilities.Base64.*
|
||||
import org.session.libsignal.utilities.Hex
|
||||
import org.session.libsignal.utilities.JsonUtil
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.whispersystems.curve25519.Curve25519
|
||||
import java.util.*
|
||||
|
||||
object OpenGroupAPIV2 {
|
||||
|
||||
private val moderators: HashMap<String, Set<String>> = hashMapOf() // Server URL to (channel ID to set of moderator IDs)
|
||||
const val DEFAULT_SERVER = "http://116.203.70.33"
|
||||
private const val DEFAULT_SERVER_PUBLIC_KEY = "a03c383cf63c3c4efe67acc52112a6dd734b3a946b9545f488aaa93da7991238"
|
||||
|
||||
private val curve = Curve25519.getInstance(Curve25519.BEST)
|
||||
val defaultRooms = MutableSharedFlow<List<DefaultGroup>>(replay = 1)
|
||||
|
||||
private val curve = Curve25519.getInstance(Curve25519.BEST)
|
||||
|
||||
sealed class Error : Exception() {
|
||||
object GENERIC : Error()
|
||||
object PARSING_FAILED : Error()
|
||||
object DECRYPTION_FAILED : Error()
|
||||
object SIGNING_FAILED : Error()
|
||||
object INVALID_URL : Error()
|
||||
object NO_PUBLIC_KEY : Error()
|
||||
|
||||
fun errorDescription() = when (this) {
|
||||
Error.GENERIC -> "An error occurred."
|
||||
Error.PARSING_FAILED -> "Invalid response."
|
||||
Error.DECRYPTION_FAILED -> "Couldn't decrypt response."
|
||||
Error.SIGNING_FAILED -> "Couldn't sign message."
|
||||
Error.INVALID_URL -> "Invalid URL."
|
||||
Error.NO_PUBLIC_KEY -> "Couldn't find server public key."
|
||||
}
|
||||
private const val DEFAULT_SERVER_PUBLIC_KEY = "a03c383cf63c3c4efe67acc52112a6dd734b3a946b9545f488aaa93da7991238"
|
||||
const val DEFAULT_SERVER = "http://116.203.70.33"
|
||||
|
||||
sealed class Error(message: String) : Exception(message) {
|
||||
object Generic : Error("An error occurred.")
|
||||
object ParsingFailed : Error("Invalid response.")
|
||||
object DecryptionFailed : Error("Couldn't decrypt response.")
|
||||
object SigningFailed : Error("Couldn't sign message.")
|
||||
object InvalidURL : Error("Invalid URL.")
|
||||
object NoPublicKey : Error("Couldn't find server public key.")
|
||||
}
|
||||
|
||||
data class DefaultGroup(val id: String,
|
||||
val name: String,
|
||||
val image: ByteArray?) {
|
||||
fun toJoinUrl(): String = "$DEFAULT_SERVER/$id?public_key=$DEFAULT_SERVER_PUBLIC_KEY"
|
||||
data class DefaultGroup(val id: String, val name: String, val image: ByteArray?) {
|
||||
|
||||
val joinURL: String get() = "$DEFAULT_SERVER/$id?public_key=$DEFAULT_SERVER_PUBLIC_KEY"
|
||||
}
|
||||
|
||||
data class Info(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val imageID: String?
|
||||
)
|
||||
data class Info(val id: String, val name: String, val imageID: String?)
|
||||
|
||||
@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy::class)
|
||||
data class CompactPollRequest(val roomId: String,
|
||||
val authToken: String,
|
||||
val fromDeletionServerId: Long?,
|
||||
val fromMessageServerId: Long?
|
||||
)
|
||||
|
||||
data class CompactPollResult(val messages: List<OpenGroupMessageV2>,
|
||||
val deletions: List<Long>,
|
||||
val moderators: List<String>
|
||||
)
|
||||
data class CompactPollRequest(val roomID: String, val authToken: String, val fromDeletionServerID: Long?, val fromMessageServerID: Long?)
|
||||
data class CompactPollResult(val messages: List<OpenGroupMessageV2>, val deletions: List<Long>, val moderators: List<String>)
|
||||
|
||||
@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy::class)
|
||||
data class MessageDeletion @JvmOverloads constructor(val id: Long = 0,
|
||||
val deletedMessageId: Long = 0
|
||||
data class MessageDeletion
|
||||
@JvmOverloads constructor(val id: Long = 0, val deletedMessageId: Long = 0
|
||||
) {
|
||||
|
||||
companion object {
|
||||
val EMPTY = MessageDeletion()
|
||||
}
|
||||
@@ -99,38 +73,37 @@ object OpenGroupAPIV2 {
|
||||
val parameters: Any? = null,
|
||||
val headers: Map<String, String> = mapOf(),
|
||||
val isAuthRequired: Boolean = true,
|
||||
// Always `true` under normal circumstances. You might want to disable
|
||||
// this when running over Lokinet.
|
||||
val useOnionRouting: Boolean = true
|
||||
/**
|
||||
* Always `true` under normal circumstances. You might want to disable
|
||||
* this when running over Lokinet.
|
||||
*/
|
||||
val useOnionRouting: Boolean = true
|
||||
)
|
||||
|
||||
private fun createBody(parameters: Any?): RequestBody? {
|
||||
if (parameters == null) return null
|
||||
|
||||
val parametersAsJSON = JsonUtil.toJson(parameters)
|
||||
return RequestBody.create(MediaType.get("application/json"), parametersAsJSON)
|
||||
}
|
||||
|
||||
private fun send(request: Request, isJsonRequired: Boolean = true): Promise<Map<*, *>, Exception> {
|
||||
val parsed = HttpUrl.parse(request.server) ?: return Promise.ofFail(Error.INVALID_URL)
|
||||
val url = HttpUrl.parse(request.server) ?: return Promise.ofFail(Error.InvalidURL)
|
||||
val urlBuilder = HttpUrl.Builder()
|
||||
.scheme(parsed.scheme())
|
||||
.host(parsed.host())
|
||||
.port(parsed.port())
|
||||
.addPathSegments(request.endpoint)
|
||||
|
||||
.scheme(url.scheme())
|
||||
.host(url.host())
|
||||
.port(url.port())
|
||||
.addPathSegments(request.endpoint)
|
||||
if (request.verb == GET) {
|
||||
for ((key, value) in request.queryParameters) {
|
||||
urlBuilder.addQueryParameter(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
fun execute(token: String?): Promise<Map<*, *>, Exception> {
|
||||
val requestBuilder = okhttp3.Request.Builder()
|
||||
.url(urlBuilder.build())
|
||||
.headers(Headers.of(request.headers))
|
||||
.url(urlBuilder.build())
|
||||
.headers(Headers.of(request.headers))
|
||||
if (request.isAuthRequired) {
|
||||
if (token.isNullOrEmpty()) throw IllegalStateException("No auth token for request")
|
||||
if (token.isNullOrEmpty()) throw IllegalStateException("No auth token for request.")
|
||||
requestBuilder.header("Authorization", token)
|
||||
}
|
||||
when (request.verb) {
|
||||
@@ -139,25 +112,25 @@ object OpenGroupAPIV2 {
|
||||
POST -> requestBuilder.post(createBody(request.parameters)!!)
|
||||
DELETE -> requestBuilder.delete(createBody(request.parameters))
|
||||
}
|
||||
|
||||
if (!request.room.isNullOrEmpty()) {
|
||||
requestBuilder.header("Room", request.room)
|
||||
}
|
||||
|
||||
if (request.useOnionRouting) {
|
||||
val publicKey = MessagingModuleConfiguration.shared.storage.getOpenGroupPublicKey(request.server)
|
||||
?: return Promise.ofFail(Error.NO_PUBLIC_KEY)
|
||||
return OnionRequestAPI.sendOnionRequest(requestBuilder.build(), request.server, publicKey, isJSONRequired = isJsonRequired)
|
||||
.fail { e ->
|
||||
if (e is OnionRequestAPI.HTTPRequestFailedAtDestinationException && e.statusCode == 401) {
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
if (request.room != null) {
|
||||
storage.removeAuthToken("${request.server}.${request.room}")
|
||||
} else {
|
||||
storage.removeAuthToken(request.server)
|
||||
}
|
||||
}
|
||||
?: return Promise.ofFail(Error.NoPublicKey)
|
||||
return OnionRequestAPI.sendOnionRequest(requestBuilder.build(), request.server, publicKey, isJSONRequired = isJsonRequired).fail { e ->
|
||||
// A 401 means that we didn't provide a (valid) auth token for a route that required one. We use this as an
|
||||
// indication that the token we're using has expired. Note that a 403 has a different meaning; it means that
|
||||
// we provided a valid token but it doesn't have a high enough permission level for the route in question.
|
||||
if (e is OnionRequestAPI.HTTPRequestFailedAtDestinationException && e.statusCode == 401) {
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
if (request.room != null) {
|
||||
storage.removeAuthToken("${request.server}.${request.room}")
|
||||
} else {
|
||||
storage.removeAuthToken(request.server)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return Promise.ofFail(IllegalStateException("It's currently not allowed to send non onion routed requests."))
|
||||
}
|
||||
@@ -172,52 +145,51 @@ object OpenGroupAPIV2 {
|
||||
fun downloadOpenGroupProfilePicture(roomID: String, server: String): Promise<ByteArray, Exception> {
|
||||
val request = Request(verb = GET, room = roomID, server = server, endpoint = "rooms/$roomID/image", isAuthRequired = false)
|
||||
return send(request).map { json ->
|
||||
val result = json["result"] as? String ?: throw Error.PARSING_FAILED
|
||||
val result = json["result"] as? String ?: throw Error.ParsingFailed
|
||||
decode(result)
|
||||
}
|
||||
}
|
||||
|
||||
// region Authorization
|
||||
fun getAuthToken(room: String, server: String): Promise<String, Exception> {
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
return storage.getAuthToken(room, server)?.let {
|
||||
Promise.of(it)
|
||||
} ?: run {
|
||||
requestNewAuthToken(room, server)
|
||||
.bind { claimAuthToken(it, room, server) }
|
||||
.success { authToken ->
|
||||
storage.setAuthToken(room, server, authToken)
|
||||
}
|
||||
.bind { claimAuthToken(it, room, server) }
|
||||
.success { authToken ->
|
||||
storage.setAuthToken(room, server, authToken)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun requestNewAuthToken(room: String, server: String): Promise<String, Exception> {
|
||||
val (publicKey, privateKey) = MessagingModuleConfiguration.shared.storage.getUserKeyPair()
|
||||
?: return Promise.ofFail(Error.GENERIC)
|
||||
val queryParameters = mutableMapOf("public_key" to publicKey)
|
||||
?: return Promise.ofFail(Error.Generic)
|
||||
val queryParameters = mutableMapOf( "public_key" to publicKey )
|
||||
val request = Request(GET, room, server, "auth_token_challenge", queryParameters, isAuthRequired = false, parameters = null)
|
||||
return send(request).map { json ->
|
||||
val challenge = json["challenge"] as? Map<*, *> ?: throw Error.PARSING_FAILED
|
||||
val base64EncodedCiphertext = challenge["ciphertext"] as? String
|
||||
?: throw Error.PARSING_FAILED
|
||||
val base64EncodedEphemeralPublicKey = challenge["ephemeral_public_key"] as? String
|
||||
?: throw Error.PARSING_FAILED
|
||||
val challenge = json["challenge"] as? Map<*, *> ?: throw Error.ParsingFailed
|
||||
val base64EncodedCiphertext = challenge["ciphertext"] as? String ?: throw Error.ParsingFailed
|
||||
val base64EncodedEphemeralPublicKey = challenge["ephemeral_public_key"] as? String ?: throw Error.ParsingFailed
|
||||
val ciphertext = decode(base64EncodedCiphertext)
|
||||
val ephemeralPublicKey = decode(base64EncodedEphemeralPublicKey)
|
||||
val symmetricKey = AESGCM.generateSymmetricKey(ephemeralPublicKey, privateKey)
|
||||
val tokenAsData = try {
|
||||
AESGCM.decrypt(ciphertext, symmetricKey)
|
||||
} catch (e: Exception) {
|
||||
throw Error.DECRYPTION_FAILED
|
||||
throw Error.DecryptionFailed
|
||||
}
|
||||
tokenAsData.toHexString()
|
||||
}
|
||||
}
|
||||
|
||||
fun claimAuthToken(authToken: String, room: String, server: String): Promise<String, Exception> {
|
||||
val parameters = mapOf("public_key" to MessagingModuleConfiguration.shared.storage.getUserPublicKey()!!)
|
||||
val headers = mapOf("Authorization" to authToken)
|
||||
val parameters = mapOf( "public_key" to MessagingModuleConfiguration.shared.storage.getUserPublicKey()!! )
|
||||
val headers = mapOf( "Authorization" to authToken )
|
||||
val request = Request(verb = POST, room = room, server = server, endpoint = "claim_auth_token",
|
||||
parameters = parameters, headers = headers, isAuthRequired = false)
|
||||
parameters = parameters, headers = headers, isAuthRequired = false)
|
||||
return send(request).map { authToken }
|
||||
}
|
||||
|
||||
@@ -227,33 +199,36 @@ object OpenGroupAPIV2 {
|
||||
MessagingModuleConfiguration.shared.storage.removeAuthToken(room, server)
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Sending
|
||||
// region Upload/Download
|
||||
fun upload(file: ByteArray, room: String, server: String): Promise<Long, Exception> {
|
||||
val base64EncodedFile = encodeBytes(file)
|
||||
val parameters = mapOf("file" to base64EncodedFile)
|
||||
val parameters = mapOf( "file" to base64EncodedFile )
|
||||
val request = Request(verb = POST, room = room, server = server, endpoint = "files", parameters = parameters)
|
||||
return send(request).map { json ->
|
||||
json["result"] as? Long ?: throw Error.PARSING_FAILED
|
||||
json["result"] as? Long ?: throw Error.ParsingFailed
|
||||
}
|
||||
}
|
||||
|
||||
fun download(file: Long, room: String, server: String): Promise<ByteArray, Exception> {
|
||||
val request = Request(verb = GET, room = room, server = server, endpoint = "files/$file")
|
||||
return send(request).map { json ->
|
||||
val base64EncodedFile = json["result"] as? String ?: throw Error.PARSING_FAILED
|
||||
decode(base64EncodedFile) ?: throw Error.PARSING_FAILED
|
||||
val base64EncodedFile = json["result"] as? String ?: throw Error.ParsingFailed
|
||||
decode(base64EncodedFile) ?: throw Error.ParsingFailed
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Sending
|
||||
fun send(message: OpenGroupMessageV2, room: String, server: String): Promise<OpenGroupMessageV2, Exception> {
|
||||
val signedMessage = message.sign() ?: return Promise.ofFail(Error.SIGNING_FAILED)
|
||||
val signedMessage = message.sign() ?: return Promise.ofFail(Error.SigningFailed)
|
||||
val jsonMessage = signedMessage.toJSON()
|
||||
val request = Request(verb = POST, room = room, server = server, endpoint = "messages", parameters = jsonMessage)
|
||||
return send(request).map { json ->
|
||||
@Suppress("UNCHECKED_CAST") val rawMessage = json["message"] as? Map<String, Any>
|
||||
?: throw Error.PARSING_FAILED
|
||||
OpenGroupMessageV2.fromJSON(rawMessage) ?: throw Error.PARSING_FAILED
|
||||
?: throw Error.ParsingFailed
|
||||
OpenGroupMessageV2.fromJSON(rawMessage) ?: throw Error.ParsingFailed
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
@@ -266,37 +241,42 @@ object OpenGroupAPIV2 {
|
||||
queryParameters += "from_server_id" to lastId.toString()
|
||||
}
|
||||
val request = Request(verb = GET, room = room, server = server, endpoint = "messages", queryParameters = queryParameters)
|
||||
return send(request).map { jsonList ->
|
||||
@Suppress("UNCHECKED_CAST") val rawMessages = jsonList["messages"] as? List<Map<String, Any>>
|
||||
?: throw Error.PARSING_FAILED
|
||||
val lastMessageServerId = storage.getLastMessageServerId(room, server) ?: 0
|
||||
|
||||
var currentMax = lastMessageServerId
|
||||
val messages = rawMessages.mapNotNull { json ->
|
||||
try {
|
||||
val message = OpenGroupMessageV2.fromJSON(json) ?: return@mapNotNull null
|
||||
if (message.serverID == null || message.sender.isNullOrEmpty()) return@mapNotNull null
|
||||
val sender = message.sender
|
||||
val data = decode(message.base64EncodedData)
|
||||
val signature = decode(message.base64EncodedSignature)
|
||||
val publicKey = Hex.fromStringCondensed(sender.removing05PrefixIfNeeded())
|
||||
val isValid = curve.verifySignature(publicKey, data, signature)
|
||||
if (!isValid) {
|
||||
Log.d("Loki", "Ignoring message with invalid signature")
|
||||
return@mapNotNull null
|
||||
}
|
||||
if (message.serverID > lastMessageServerId) {
|
||||
currentMax = message.serverID
|
||||
}
|
||||
message
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
storage.setLastMessageServerId(room, server, currentMax)
|
||||
messages
|
||||
return send(request).map { json ->
|
||||
@Suppress("UNCHECKED_CAST") val rawMessages = json["messages"] as? List<Map<String, Any>>
|
||||
?: throw Error.ParsingFailed
|
||||
parseMessages(room, server, rawMessages)
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseMessages(room: String, server: String, rawMessages: List<Map<*, *>>): List<OpenGroupMessageV2> {
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val lastMessageServerID = storage.getLastMessageServerId(room, server) ?: 0
|
||||
var currentLastMessageServerID = lastMessageServerID
|
||||
val messages = rawMessages.mapNotNull { json ->
|
||||
json as Map<String, Any>
|
||||
try {
|
||||
val message = OpenGroupMessageV2.fromJSON(json) ?: return@mapNotNull null
|
||||
if (message.serverID == null || message.sender.isNullOrEmpty()) return@mapNotNull null
|
||||
val sender = message.sender
|
||||
val data = decode(message.base64EncodedData)
|
||||
val signature = decode(message.base64EncodedSignature)
|
||||
val publicKey = Hex.fromStringCondensed(sender.removing05PrefixIfNeeded())
|
||||
val isValid = curve.verifySignature(publicKey, data, signature)
|
||||
if (!isValid) {
|
||||
Log.d("Loki", "Ignoring message with invalid signature.")
|
||||
return@mapNotNull null
|
||||
}
|
||||
if (message.serverID > lastMessageServerID) {
|
||||
currentLastMessageServerID = message.serverID
|
||||
}
|
||||
message
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
storage.setLastMessageServerId(room, server, currentLastMessageServerID)
|
||||
return messages
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Message Deletion
|
||||
@@ -304,7 +284,7 @@ object OpenGroupAPIV2 {
|
||||
fun deleteMessage(serverID: Long, room: String, server: String): Promise<Unit, Exception> {
|
||||
val request = Request(verb = DELETE, room = room, server = server, endpoint = "messages/$serverID")
|
||||
return send(request).map {
|
||||
Log.d("Loki", "Deleted server message")
|
||||
Log.d("Loki", "Message deletion successful.")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -318,7 +298,7 @@ object OpenGroupAPIV2 {
|
||||
return send(request).map { json ->
|
||||
val type = TypeFactory.defaultInstance().constructCollectionType(List::class.java, MessageDeletion::class.java)
|
||||
val idsAsString = JsonUtil.toJson(json["ids"])
|
||||
val serverIDs = JsonUtil.fromJson<List<MessageDeletion>>(idsAsString, type) ?: throw Error.PARSING_FAILED
|
||||
val serverIDs = JsonUtil.fromJson<List<MessageDeletion>>(idsAsString, type) ?: throw Error.ParsingFailed
|
||||
val lastMessageServerId = storage.getLastDeletionServerId(room, server) ?: 0
|
||||
val serverID = serverIDs.maxByOrNull {it.id } ?: MessageDeletion.EMPTY
|
||||
if (serverID.id > lastMessageServerId) {
|
||||
@@ -338,7 +318,7 @@ object OpenGroupAPIV2 {
|
||||
val request = Request(verb = GET, room = room, server = server, endpoint = "moderators")
|
||||
return send(request).map { json ->
|
||||
@Suppress("UNCHECKED_CAST") val moderatorsJson = json["moderators"] as? List<String>
|
||||
?: throw Error.PARSING_FAILED
|
||||
?: throw Error.ParsingFailed
|
||||
val id = "$server.$room"
|
||||
handleModerators(id, moderatorsJson)
|
||||
moderatorsJson
|
||||
@@ -347,90 +327,77 @@ object OpenGroupAPIV2 {
|
||||
|
||||
@JvmStatic
|
||||
fun ban(publicKey: String, room: String, server: String): Promise<Unit, Exception> {
|
||||
val parameters = mapOf("public_key" to publicKey)
|
||||
val parameters = mapOf( "public_key" to publicKey )
|
||||
val request = Request(verb = POST, room = room, server = server, endpoint = "block_list", parameters = parameters)
|
||||
return send(request).map {
|
||||
Log.d("Loki", "Banned user $publicKey from $server.$room")
|
||||
Log.d("Loki", "Banned user: $publicKey from: $server.$room.")
|
||||
}
|
||||
}
|
||||
|
||||
fun unban(publicKey: String, room: String, server: String): Promise<Unit, Exception> {
|
||||
val request = Request(verb = DELETE, room = room, server = server, endpoint = "block_list/$publicKey")
|
||||
return send(request).map {
|
||||
Log.d("Loki", "Unbanned user $publicKey from $server.$room")
|
||||
Log.d("Loki", "Unbanned user: $publicKey from: $server.$room")
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun isUserModerator(publicKey: String, room: String, server: String): Boolean =
|
||||
moderators["$server.$room"]?.contains(publicKey) ?: false
|
||||
moderators["$server.$room"]?.contains(publicKey) ?: false
|
||||
// endregion
|
||||
|
||||
// region General
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun getCompactPoll(rooms: List<String>, server: String): Promise<Map<String, CompactPollResult>, Exception> {
|
||||
val requestAuths = rooms.associateWith { room -> getAuthToken(room, server) }
|
||||
fun compactPoll(rooms: List<String>, server: String): Promise<Map<String, CompactPollResult>, Exception> {
|
||||
val authTokenRequests = rooms.associateWith { room -> getAuthToken(room, server) }
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val requests = rooms.mapNotNull { room ->
|
||||
val authToken = try {
|
||||
requestAuths[room]?.get()
|
||||
authTokenRequests[room]?.get()
|
||||
} catch (e: Exception) {
|
||||
Log.e("Loki", "Failed to get auth token for $room", e)
|
||||
Log.e("Loki", "Failed to get auth token for $room.", e)
|
||||
null
|
||||
} ?: return@mapNotNull null
|
||||
|
||||
CompactPollRequest(roomId = room,
|
||||
authToken = authToken,
|
||||
fromDeletionServerId = storage.getLastDeletionServerId(room, server),
|
||||
fromMessageServerId = storage.getLastMessageServerId(room, server)
|
||||
CompactPollRequest(
|
||||
roomID = room,
|
||||
authToken = authToken,
|
||||
fromDeletionServerID = storage.getLastDeletionServerId(room, server),
|
||||
fromMessageServerID = storage.getLastMessageServerId(room, server)
|
||||
)
|
||||
}
|
||||
val request = Request(verb = POST, room = null, server = server, endpoint = "compact_poll", isAuthRequired = false, parameters = mapOf("requests" to requests))
|
||||
// build a request for all rooms
|
||||
val request = Request(verb = POST, room = null, server = server, endpoint = "compact_poll", isAuthRequired = false, parameters = mapOf( "requests" to requests ))
|
||||
return send(request = request).map { json ->
|
||||
val results = json["results"] as? List<*> ?: throw Error.PARSING_FAILED
|
||||
|
||||
results.mapNotNull { roomJson ->
|
||||
if (roomJson !is Map<*,*>) return@mapNotNull null
|
||||
val roomId = roomJson["room_id"] as? String ?: return@mapNotNull null
|
||||
|
||||
// check the status was fine
|
||||
val statusCode = roomJson["status_code"] as? Int ?: return@mapNotNull null
|
||||
val results = json["results"] as? List<*> ?: throw Error.ParsingFailed
|
||||
results.mapNotNull { json ->
|
||||
if (json !is Map<*,*>) return@mapNotNull null
|
||||
val roomID = json["room_id"] as? String ?: return@mapNotNull null
|
||||
// A 401 means that we didn't provide a (valid) auth token for a route that required one. We use this as an
|
||||
// indication that the token we're using has expired. Note that a 403 has a different meaning; it means that
|
||||
// we provided a valid token but it doesn't have a high enough permission level for the route in question.
|
||||
val statusCode = json["status_code"] as? Int ?: return@mapNotNull null
|
||||
if (statusCode == 401) {
|
||||
// delete auth token and return null
|
||||
storage.removeAuthToken(roomId, server)
|
||||
storage.removeAuthToken(roomID, server)
|
||||
}
|
||||
|
||||
// check and store mods
|
||||
val moderators = roomJson["moderators"] as? List<String> ?: return@mapNotNull null
|
||||
handleModerators("$server.$roomId", moderators)
|
||||
|
||||
// get deletions
|
||||
// Moderators
|
||||
val moderators = json["moderators"] as? List<String> ?: return@mapNotNull null
|
||||
handleModerators("$server.$roomID", moderators)
|
||||
// Deletions
|
||||
val type = TypeFactory.defaultInstance().constructCollectionType(List::class.java, MessageDeletion::class.java)
|
||||
val idsAsString = JsonUtil.toJson(roomJson["deletions"])
|
||||
val deletedServerIDs = JsonUtil.fromJson<List<MessageDeletion>>(idsAsString, type) ?: throw Error.PARSING_FAILED
|
||||
val lastDeletionServerId = storage.getLastDeletionServerId(roomId, server) ?: 0
|
||||
val serverID = deletedServerIDs.maxByOrNull {it.id } ?: MessageDeletion.EMPTY
|
||||
if (serverID.id > lastDeletionServerId) {
|
||||
storage.setLastDeletionServerId(roomId, server, serverID.id)
|
||||
val idsAsString = JsonUtil.toJson(json["deletions"])
|
||||
val deletedServerIDs = JsonUtil.fromJson<List<MessageDeletion>>(idsAsString, type) ?: throw Error.ParsingFailed
|
||||
val lastDeletionServerID = storage.getLastDeletionServerId(roomID, server) ?: 0
|
||||
val serverID = deletedServerIDs.maxByOrNull { it.id } ?: MessageDeletion.EMPTY
|
||||
if (serverID.id > lastDeletionServerID) {
|
||||
storage.setLastDeletionServerId(roomID, server, serverID.id)
|
||||
}
|
||||
|
||||
// get messages
|
||||
val rawMessages = roomJson["messages"] as? List<Map<String, Any>> ?: return@mapNotNull null // parsing failed
|
||||
|
||||
val lastMessageServerId = storage.getLastMessageServerId(roomId, server) ?: 0
|
||||
var currentMax = lastMessageServerId
|
||||
val messages = rawMessages.mapNotNull { rawMessage ->
|
||||
val message = OpenGroupMessageV2.fromJSON(rawMessage)?.apply {
|
||||
currentMax = maxOf(currentMax,this.serverID ?: 0)
|
||||
}
|
||||
message
|
||||
}
|
||||
storage.setLastMessageServerId(roomId, server, currentMax)
|
||||
roomId to CompactPollResult(
|
||||
messages = messages,
|
||||
deletions = deletedServerIDs.map { it.deletedMessageId },
|
||||
moderators = moderators
|
||||
// Messages
|
||||
val rawMessages = json["messages"] as? List<Map<String, Any>> ?: return@mapNotNull null
|
||||
val messages = parseMessages(roomID, server, rawMessages)
|
||||
roomID to CompactPollResult(
|
||||
messages = messages,
|
||||
deletions = deletedServerIDs.map { it.deletedMessageId },
|
||||
moderators = moderators
|
||||
)
|
||||
}.toMap()
|
||||
}
|
||||
@@ -443,7 +410,7 @@ object OpenGroupAPIV2 {
|
||||
val earlyGroups = groups.map { group ->
|
||||
DefaultGroup(group.id, group.name, null)
|
||||
}
|
||||
// see if we have any cached rooms, and if they already have images, don't overwrite with early non-image results
|
||||
// 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)
|
||||
@@ -452,12 +419,11 @@ object OpenGroupAPIV2 {
|
||||
val images = groups.map { group ->
|
||||
group.id to downloadOpenGroupProfilePicture(group.id, DEFAULT_SERVER)
|
||||
}.toMap()
|
||||
|
||||
groups.map { group ->
|
||||
val image = try {
|
||||
images[group.id]!!.get()
|
||||
} catch (e: Exception) {
|
||||
// no image or image failed to download
|
||||
// No image or image failed to download
|
||||
null
|
||||
}
|
||||
DefaultGroup(group.id, group.name, image)
|
||||
@@ -470,9 +436,9 @@ object OpenGroupAPIV2 {
|
||||
fun getInfo(room: String, server: String): Promise<Info, Exception> {
|
||||
val request = Request(verb = GET, room = null, server = server, endpoint = "rooms/$room", isAuthRequired = false)
|
||||
return send(request).map { json ->
|
||||
val rawRoom = json["room"] as? Map<*, *> ?: throw Error.PARSING_FAILED
|
||||
val id = rawRoom["id"] as? String ?: throw Error.PARSING_FAILED
|
||||
val name = rawRoom["name"] as? String ?: throw Error.PARSING_FAILED
|
||||
val rawRoom = json["room"] as? Map<*, *> ?: throw Error.ParsingFailed
|
||||
val id = rawRoom["id"] as? String ?: throw Error.ParsingFailed
|
||||
val name = rawRoom["name"] as? String ?: throw Error.ParsingFailed
|
||||
val imageID = rawRoom["image_id"] as? String
|
||||
Info(id = id, name = name, imageID = imageID)
|
||||
}
|
||||
@@ -481,13 +447,13 @@ object OpenGroupAPIV2 {
|
||||
fun getAllRooms(server: String): Promise<List<Info>, Exception> {
|
||||
val request = Request(verb = GET, room = null, server = server, endpoint = "rooms", isAuthRequired = false)
|
||||
return send(request).map { json ->
|
||||
val rawRooms = json["rooms"] as? List<Map<*, *>> ?: throw Error.PARSING_FAILED
|
||||
val rawRooms = json["rooms"] as? List<Map<*, *>> ?: throw Error.ParsingFailed
|
||||
rawRooms.mapNotNull {
|
||||
val roomJson = it as? Map<*, *> ?: return@mapNotNull null
|
||||
val id = roomJson["id"] as? String ?: return@mapNotNull null
|
||||
val name = roomJson["name"] as? String ?: return@mapNotNull null
|
||||
val imageId = roomJson["image_id"] as? String
|
||||
Info(id, name, imageId)
|
||||
val imageID = roomJson["image_id"] as? String
|
||||
Info(id, name, imageID)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -495,12 +461,11 @@ object OpenGroupAPIV2 {
|
||||
fun getMemberCount(room: String, server: String): Promise<Int, Exception> {
|
||||
val request = Request(verb = GET, room = room, server = server, endpoint = "member_count")
|
||||
return send(request).map { json ->
|
||||
val memberCount = json["member_count"] as? Int ?: throw Error.PARSING_FAILED
|
||||
val memberCount = json["member_count"] as? Int ?: throw Error.ParsingFailed
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
storage.setUserCount(room, server, memberCount)
|
||||
memberCount
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
|
||||
}
|
@@ -2,9 +2,9 @@ package org.session.libsession.messaging.open_groups
|
||||
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.messaging.messages.visible.VisibleMessage
|
||||
import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded
|
||||
import org.session.libsignal.utilities.removing05PrefixIfNeeded
|
||||
import org.session.libsignal.utilities.Hex
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.whispersystems.curve25519.Curve25519
|
||||
|
||||
data class OpenGroupMessage(
|
||||
|
@@ -1,22 +1,26 @@
|
||||
package org.session.libsession.messaging.open_groups
|
||||
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsignal.service.internal.push.PushTransportDetails
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos
|
||||
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.logging.Log
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.whispersystems.curve25519.Curve25519
|
||||
|
||||
data class OpenGroupMessageV2(
|
||||
val serverID: Long? = null,
|
||||
val sender: String?,
|
||||
val sentTimestamp: Long,
|
||||
// The serialized protobuf in base64 encoding
|
||||
val base64EncodedData: String,
|
||||
// When sending a message, the sender signs the serialized protobuf with their private key so that
|
||||
// a receiving user can verify that the message wasn't tampered with.
|
||||
val base64EncodedSignature: String? = null
|
||||
val serverID: Long? = null,
|
||||
val sender: String?,
|
||||
val sentTimestamp: Long,
|
||||
/**
|
||||
* The serialized protobuf in base64 encoding.
|
||||
*/
|
||||
val base64EncodedData: String,
|
||||
/**
|
||||
* When sending a message, the sender signs the serialized protobuf with their private key so that
|
||||
* a receiving user can verify that the message wasn't tampered with.
|
||||
*/
|
||||
val base64EncodedSignature: String? = null
|
||||
) {
|
||||
|
||||
companion object {
|
||||
@@ -28,11 +32,12 @@ data class OpenGroupMessageV2(
|
||||
val serverID = json["server_id"] as? Int
|
||||
val sender = json["public_key"] as? String
|
||||
val base64EncodedSignature = json["signature"] as? String
|
||||
return OpenGroupMessageV2(serverID = serverID?.toLong(),
|
||||
sender = sender,
|
||||
sentTimestamp = sentTimestamp,
|
||||
base64EncodedData = base64EncodedData,
|
||||
base64EncodedSignature = base64EncodedSignature
|
||||
return OpenGroupMessageV2(
|
||||
serverID = serverID?.toLong(),
|
||||
sender = sender,
|
||||
sentTimestamp = sentTimestamp,
|
||||
base64EncodedData = base64EncodedData,
|
||||
base64EncodedSignature = base64EncodedSignature
|
||||
)
|
||||
}
|
||||
|
||||
@@ -41,29 +46,26 @@ data class OpenGroupMessageV2(
|
||||
fun sign(): OpenGroupMessageV2? {
|
||||
if (base64EncodedData.isEmpty()) return null
|
||||
val (publicKey, privateKey) = MessagingModuleConfiguration.shared.storage.getUserKeyPair() ?: return null
|
||||
|
||||
if (sender != publicKey) return null // only sign our own messages?
|
||||
|
||||
if (sender != publicKey) return null
|
||||
val signature = try {
|
||||
curve.calculateSignature(privateKey, decode(base64EncodedData))
|
||||
} catch (e: Exception) {
|
||||
Log.e("Loki", "Couldn't sign OpenGroupV2Message", e)
|
||||
Log.w("Loki", "Couldn't sign open group message.", e)
|
||||
return null
|
||||
}
|
||||
|
||||
return copy(base64EncodedSignature = Base64.encodeBytes(signature))
|
||||
}
|
||||
|
||||
fun toJSON(): Map<String, Any> {
|
||||
val jsonMap = mutableMapOf("data" to base64EncodedData, "timestamp" to sentTimestamp)
|
||||
serverID?.let { jsonMap["server_id"] = serverID }
|
||||
sender?.let { jsonMap["public_key"] = sender }
|
||||
base64EncodedSignature?.let { jsonMap["signature"] = base64EncodedSignature }
|
||||
return jsonMap
|
||||
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 = decode(base64EncodedData).let(PushTransportDetails::getStrippedPaddingMessageBody).let { bytes ->
|
||||
SignalServiceProtos.Content.parseFrom(bytes)
|
||||
fun toProto(): SignalServiceProtos.Content {
|
||||
val data = decode(base64EncodedData).let(PushTransportDetails::getStrippedPaddingMessageBody)
|
||||
return SignalServiceProtos.Content.parseFrom(data)
|
||||
}
|
||||
|
||||
}
|
@@ -1,51 +1,50 @@
|
||||
package org.session.libsession.messaging.open_groups
|
||||
|
||||
import org.session.libsignal.utilities.JsonUtil
|
||||
import org.session.libsignal.utilities.Log
|
||||
import java.util.*
|
||||
|
||||
data class OpenGroupV2(
|
||||
val server: String,
|
||||
val room: String,
|
||||
val id: String,
|
||||
val name: String,
|
||||
val publicKey: String
|
||||
val server: String,
|
||||
val room: String,
|
||||
val id: String,
|
||||
val name: String,
|
||||
val publicKey: String
|
||||
) {
|
||||
|
||||
constructor(server: String, room: String, name: String, publicKey: String) : this(
|
||||
server = server,
|
||||
room = room,
|
||||
id = "$server.$room",
|
||||
name = name,
|
||||
publicKey = publicKey,
|
||||
server = server,
|
||||
room = room,
|
||||
id = "$server.$room",
|
||||
name = name,
|
||||
publicKey = publicKey,
|
||||
)
|
||||
|
||||
companion object {
|
||||
|
||||
fun fromJson(jsonAsString: String): OpenGroupV2? {
|
||||
fun fromJSON(jsonAsString: String): OpenGroupV2? {
|
||||
return try {
|
||||
val json = JsonUtil.fromJson(jsonAsString)
|
||||
if (!json.has("room")) return null
|
||||
|
||||
val room = json.get("room").asText().toLowerCase(Locale.getDefault())
|
||||
val server = json.get("server").asText().toLowerCase(Locale.getDefault())
|
||||
val room = json.get("room").asText().toLowerCase(Locale.US)
|
||||
val server = json.get("server").asText().toLowerCase(Locale.US)
|
||||
val displayName = json.get("displayName").asText()
|
||||
val publicKey = json.get("publicKey").asText()
|
||||
|
||||
OpenGroupV2(server, room, displayName, publicKey)
|
||||
} catch (e: Exception) {
|
||||
Log.w("Loki", "Couldn't parse open group from JSON: $jsonAsString.", e);
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun toJoinUrl(): String = "$server/$room?public_key=$publicKey"
|
||||
|
||||
fun toJson(): Map<String,String> = mapOf(
|
||||
"room" to room,
|
||||
"server" to server,
|
||||
"displayName" to name,
|
||||
"publicKey" to publicKey,
|
||||
"room" to room,
|
||||
"server" to server,
|
||||
"displayName" to name,
|
||||
"publicKey" to publicKey,
|
||||
)
|
||||
|
||||
val joinURL: String get() = "$server/$room?public_key=$publicKey"
|
||||
}
|
@@ -0,0 +1,60 @@
|
||||
package org.session.libsession.messaging.sending_receiving
|
||||
|
||||
import android.util.Log
|
||||
import com.goterl.lazysodium.LazySodiumAndroid
|
||||
import com.goterl.lazysodium.SodiumAndroid
|
||||
import com.goterl.lazysodium.interfaces.Box
|
||||
import com.goterl.lazysodium.interfaces.Sign
|
||||
import org.session.libsignal.crypto.ecc.ECKeyPair
|
||||
import org.session.libsignal.utilities.Hex
|
||||
import org.session.libsignal.utilities.hexEncodedPublicKey
|
||||
import org.session.libsignal.utilities.removing05PrefixIfNeeded
|
||||
import org.session.libsignal.utilities.toHexString
|
||||
|
||||
object MessageDecrypter {
|
||||
|
||||
private val sodium by lazy { LazySodiumAndroid(SodiumAndroid()) }
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
public fun decrypt(ciphertext: ByteArray, x25519KeyPair: ECKeyPair): Pair<ByteArray, String> {
|
||||
val recipientX25519PrivateKey = x25519KeyPair.privateKey.serialize()
|
||||
val recipientX25519PublicKey = Hex.fromStringCondensed(x25519KeyPair.hexEncodedPublicKey.removing05PrefixIfNeeded())
|
||||
val signatureSize = Sign.BYTES
|
||||
val ed25519PublicKeySize = Sign.PUBLICKEYBYTES
|
||||
|
||||
// 1. ) Decrypt the message
|
||||
val plaintextWithMetadata = ByteArray(ciphertext.size - Box.SEALBYTES)
|
||||
try {
|
||||
sodium.cryptoBoxSealOpen(plaintextWithMetadata, ciphertext, ciphertext.size.toLong(), recipientX25519PublicKey, recipientX25519PrivateKey)
|
||||
} catch (exception: Exception) {
|
||||
Log.d("Loki", "Couldn't decrypt message due to error: $exception.")
|
||||
throw MessageReceiver.Error.DecryptionFailed
|
||||
}
|
||||
if (plaintextWithMetadata.size <= (signatureSize + ed25519PublicKeySize)) { throw MessageReceiver.Error.DecryptionFailed }
|
||||
// 2. ) Get the message parts
|
||||
val signature = plaintextWithMetadata.sliceArray(plaintextWithMetadata.size - signatureSize until plaintextWithMetadata.size)
|
||||
val senderED25519PublicKey = plaintextWithMetadata.sliceArray(plaintextWithMetadata.size - (signatureSize + ed25519PublicKeySize) until plaintextWithMetadata.size - signatureSize)
|
||||
val plaintext = plaintextWithMetadata.sliceArray(0 until plaintextWithMetadata.size - (signatureSize + ed25519PublicKeySize))
|
||||
// 3. ) Verify the signature
|
||||
val verificationData = (plaintext + senderED25519PublicKey + recipientX25519PublicKey)
|
||||
try {
|
||||
val isValid = sodium.cryptoSignVerifyDetached(signature, verificationData, verificationData.size, senderED25519PublicKey)
|
||||
if (!isValid) { throw MessageReceiver.Error.InvalidSignature }
|
||||
} catch (exception: Exception) {
|
||||
Log.d("Loki", "Couldn't verify message signature due to error: $exception.")
|
||||
throw MessageReceiver.Error.InvalidSignature
|
||||
}
|
||||
// 4. ) Get the sender's X25519 public key
|
||||
val senderX25519PublicKey = ByteArray(Sign.CURVE25519_PUBLICKEYBYTES)
|
||||
sodium.convertPublicKeyEd25519ToCurve25519(senderX25519PublicKey, senderED25519PublicKey)
|
||||
|
||||
return Pair(plaintext, "05" + senderX25519PublicKey.toHexString())
|
||||
}
|
||||
}
|
@@ -1,19 +1,17 @@
|
||||
package org.session.libsession.messaging.sending_receiving
|
||||
|
||||
import com.goterl.lazycode.lazysodium.LazySodiumAndroid
|
||||
import com.goterl.lazycode.lazysodium.SodiumAndroid
|
||||
import com.goterl.lazycode.lazysodium.interfaces.Box
|
||||
import com.goterl.lazycode.lazysodium.interfaces.Sign
|
||||
|
||||
import com.goterl.lazysodium.LazySodiumAndroid
|
||||
import com.goterl.lazysodium.SodiumAndroid
|
||||
import com.goterl.lazysodium.interfaces.Box
|
||||
import com.goterl.lazysodium.interfaces.Sign
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.messaging.sending_receiving.MessageSender.Error
|
||||
import org.session.libsession.utilities.KeyPairUtilities
|
||||
|
||||
import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded
|
||||
import org.session.libsignal.utilities.Hex
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.session.libsignal.utilities.removing05PrefixIfNeeded
|
||||
|
||||
object MessageSenderEncryption {
|
||||
object MessageEncrypter {
|
||||
|
||||
private val sodium by lazy { LazySodiumAndroid(SodiumAndroid()) }
|
||||
|
||||
@@ -25,7 +23,7 @@ object MessageSenderEncryption {
|
||||
*
|
||||
* @return the encrypted message.
|
||||
*/
|
||||
internal fun encryptWithSessionProtocol(plaintext: ByteArray, recipientHexEncodedX25519PublicKey: String): ByteArray{
|
||||
internal fun encrypt(plaintext: ByteArray, recipientHexEncodedX25519PublicKey: String): ByteArray{
|
||||
val context = MessagingModuleConfiguration.shared.context
|
||||
val userED25519KeyPair = KeyPairUtilities.getUserED25519KeyPair(context) ?: throw Error.NoUserED25519KeyPair
|
||||
val recipientX25519PublicKey = Hex.fromStringCondensed(recipientHexEncodedX25519PublicKey.removing05PrefixIfNeeded())
|
@@ -5,40 +5,29 @@ import org.session.libsession.messaging.messages.Message
|
||||
import org.session.libsession.messaging.messages.control.*
|
||||
import org.session.libsession.messaging.messages.visible.VisibleMessage
|
||||
import org.session.libsession.utilities.GroupUtil
|
||||
import org.session.libsignal.service.internal.push.PushTransportDetails
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos
|
||||
import org.session.libsignal.crypto.PushTransportDetails
|
||||
import org.session.libsignal.protos.SignalServiceProtos
|
||||
|
||||
object MessageReceiver {
|
||||
|
||||
private val lastEncryptionKeyPairRequest = mutableMapOf<String, Long>()
|
||||
|
||||
internal sealed class Error(val description: String) : Exception(description) {
|
||||
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 NoUserX25519KeyPair: Error("Couldn't find user X25519 key pair.")
|
||||
object NoUserED25519KeyPair: Error("Couldn't find user ED25519 key pair.")
|
||||
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 ParsingFailed : Error("Couldn't parse ciphertext message.")
|
||||
// Shared sender keys
|
||||
object InvalidGroupPublicKey: Error("Invalid group public key.")
|
||||
object NoGroupKeyPair: Error("Missing group key pair.")
|
||||
|
||||
internal val isRetryable: Boolean = when (this) {
|
||||
is DuplicateMessage -> false
|
||||
is InvalidMessage -> false
|
||||
is UnknownMessage -> false
|
||||
is UnknownEnvelopeType -> false
|
||||
is InvalidSignature -> false
|
||||
is NoData -> false
|
||||
is NoThread -> false
|
||||
is SenderBlocked -> false
|
||||
is SelfSend -> false
|
||||
is DuplicateMessage, is InvalidMessage, is UnknownMessage,
|
||||
is UnknownEnvelopeType, is InvalidSignature, is NoData,
|
||||
is SenderBlocked, is SelfSend -> false
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
@@ -46,13 +35,9 @@ object MessageReceiver {
|
||||
internal fun parse(data: ByteArray, openGroupServerID: Long?, isRetry: Boolean = false): Pair<Message, SignalServiceProtos.Content> {
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val userPublicKey = storage.getUserPublicKey()
|
||||
val isOpenGroupMessage = openGroupServerID != null
|
||||
val isOpenGroupMessage = (openGroupServerID != null)
|
||||
// Parse the envelope
|
||||
val envelope = SignalServiceProtos.Envelope.parseFrom(data)
|
||||
// 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 (storage.isMessageDuplicated(envelope.timestamp, GroupUtil.doubleEncodeGroupID(envelope.source)) && !isRetry) throw Error.DuplicateMessage
|
||||
// Decrypt the contents
|
||||
val ciphertext = envelope.content ?: throw Error.NoData
|
||||
var plaintext: ByteArray? = null
|
||||
@@ -65,7 +50,7 @@ object MessageReceiver {
|
||||
when (envelope.type) {
|
||||
SignalServiceProtos.Envelope.Type.SESSION_MESSAGE -> {
|
||||
val userX25519KeyPair = MessagingModuleConfiguration.shared.storage.getUserX25519KeyPair()
|
||||
val decryptionResult = MessageReceiverDecryption.decryptWithSessionProtocol(ciphertext.toByteArray(), userX25519KeyPair)
|
||||
val decryptionResult = MessageDecrypter.decrypt(ciphertext.toByteArray(), userX25519KeyPair)
|
||||
plaintext = decryptionResult.first
|
||||
sender = decryptionResult.second
|
||||
}
|
||||
@@ -81,7 +66,7 @@ object MessageReceiver {
|
||||
var encryptionKeyPair = encryptionKeyPairs.removeLast()
|
||||
fun decrypt() {
|
||||
try {
|
||||
val decryptionResult = MessageReceiverDecryption.decryptWithSessionProtocol(ciphertext.toByteArray(), encryptionKeyPair)
|
||||
val decryptionResult = MessageDecrypter.decrypt(ciphertext.toByteArray(), encryptionKeyPair)
|
||||
plaintext = decryptionResult.first
|
||||
sender = decryptionResult.second
|
||||
} catch (e: Exception) {
|
||||
@@ -99,10 +84,8 @@ object MessageReceiver {
|
||||
else -> throw Error.UnknownEnvelopeType
|
||||
}
|
||||
}
|
||||
// Don't process the envelope any further if the message has been handled already
|
||||
if (storage.isMessageDuplicated(envelope.timestamp, sender!!) && !isRetry) throw Error.DuplicateMessage
|
||||
// Don't process the envelope any further if the sender is blocked
|
||||
if (isBlock(sender!!)) throw Error.SenderBlocked
|
||||
if (isBlocked(sender!!)) throw Error.SenderBlocked
|
||||
// Parse the proto
|
||||
val proto = SignalServiceProtos.Content.parseFrom(PushTransportDetails.getStrippedPaddingMessageBody(plaintext))
|
||||
// Parse the message
|
||||
@@ -113,7 +96,7 @@ object MessageReceiver {
|
||||
ExpirationTimerUpdate.fromProto(proto) ?:
|
||||
ConfigurationMessage.fromProto(proto) ?:
|
||||
VisibleMessage.fromProto(proto) ?: throw Error.UnknownMessage
|
||||
// Ignore self sends if needed
|
||||
// Ignore self send if needed
|
||||
if (!message.isSelfSendValid && sender == userPublicKey) throw Error.SelfSend
|
||||
// Guard against control messages in open groups
|
||||
if (isOpenGroupMessage && message !is VisibleMessage) throw Error.InvalidMessage
|
||||
@@ -128,6 +111,19 @@ object MessageReceiver {
|
||||
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 (message is ClosedGroupControlMessage && message.kind is ClosedGroupControlMessage.Kind.New) {
|
||||
// Allow duplicates in this case to avoid the following situation:
|
||||
// • The app performed a background poll or received a push notification
|
||||
// • This method was invoked and the received message timestamps table was updated
|
||||
// • Processing wasn't finished
|
||||
// • The user doesn't see the new closed group
|
||||
} else {
|
||||
if (storage.isDuplicateMessage(envelope.timestamp)) { throw Error.DuplicateMessage }
|
||||
storage.addReceivedMessageTimestamp(envelope.timestamp)
|
||||
}
|
||||
// Return
|
||||
return Pair(message, proto)
|
||||
}
|
||||
|
@@ -1,11 +0,0 @@
|
||||
package org.session.libsession.messaging.sending_receiving
|
||||
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsignal.libsignal.ecc.ECKeyPair
|
||||
|
||||
object MessageReceiverDecryption {
|
||||
|
||||
internal fun decryptWithSessionProtocol(ciphertext: ByteArray, x25519KeyPair: ECKeyPair): Pair<ByteArray, String> {
|
||||
return MessagingModuleConfiguration.shared.sessionProtocol.decrypt(ciphertext, x25519KeyPair)
|
||||
}
|
||||
}
|
@@ -13,8 +13,7 @@ import org.session.libsession.messaging.messages.control.ConfigurationMessage
|
||||
import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate
|
||||
import org.session.libsession.messaging.messages.visible.*
|
||||
import org.session.libsession.messaging.open_groups.*
|
||||
import org.session.libsession.messaging.threads.Address
|
||||
import org.session.libsession.messaging.threads.recipients.Recipient
|
||||
import org.session.libsession.utilities.Address
|
||||
import org.session.libsession.messaging.utilities.MessageWrapper
|
||||
import org.session.libsession.snode.RawResponsePromise
|
||||
import org.session.libsession.snode.SnodeAPI
|
||||
@@ -22,11 +21,12 @@ import org.session.libsession.snode.SnodeModule
|
||||
import org.session.libsession.snode.SnodeMessage
|
||||
import org.session.libsession.utilities.GroupUtil
|
||||
import org.session.libsession.utilities.SSKEnvironment
|
||||
import org.session.libsignal.service.internal.push.PushTransportDetails
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos
|
||||
import org.session.libsignal.service.loki.utilities.hexEncodedPublicKey
|
||||
import org.session.libsignal.crypto.PushTransportDetails
|
||||
import org.session.libsignal.protos.SignalServiceProtos
|
||||
import org.session.libsignal.utilities.hexEncodedPublicKey
|
||||
import org.session.libsignal.utilities.Base64
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
import org.session.libsignal.utilities.Log
|
||||
import java.lang.IllegalStateException
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.Attachment as SignalAttachment
|
||||
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview as SignalLinkPreview
|
||||
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel as SignalQuote
|
||||
@@ -37,8 +37,6 @@ object MessageSender {
|
||||
sealed class Error(val description: String) : Exception(description) {
|
||||
object InvalidMessage : Error("Invalid message.")
|
||||
object ProtoConversionFailed : Error("Couldn't convert message to proto.")
|
||||
object ProofOfWorkCalculationFailed : Error("Proof of work calculation failed.")
|
||||
object NoUserX25519KeyPair : Error("Couldn't find user X25519 key pair.")
|
||||
object NoUserED25519KeyPair : Error("Couldn't find user ED25519 key pair.")
|
||||
object SigningFailed : Error("Couldn't sign message.")
|
||||
object EncryptionFailed : Error("Couldn't encrypt message.")
|
||||
@@ -46,17 +44,10 @@ object MessageSender {
|
||||
// Closed groups
|
||||
object NoThread : Error("Couldn't find a thread associated with the given group public key.")
|
||||
object NoKeyPair: Error("Couldn't find a private key associated with the given group public key.")
|
||||
object NoPrivateKey : Error("Couldn't find a private key associated with the given group public key.")
|
||||
object InvalidClosedGroupUpdate : Error("Invalid group update.")
|
||||
|
||||
// Precondition
|
||||
class PreconditionFailure(val reason: String): Error(reason)
|
||||
|
||||
internal val isRetryable: Boolean = when (this) {
|
||||
is InvalidMessage -> false
|
||||
is ProtoConversionFailed -> false
|
||||
is ProofOfWorkCalculationFailed -> false
|
||||
is InvalidClosedGroupUpdate -> false
|
||||
is InvalidMessage, ProtoConversionFailed, InvalidClosedGroupUpdate -> false
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
@@ -76,7 +67,9 @@ object MessageSender {
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val userPublicKey = storage.getUserPublicKey()
|
||||
// Set the timestamp, sender and recipient
|
||||
message.sentTimestamp ?: run { message.sentTimestamp = System.currentTimeMillis() } /* Visible messages will already have their sent timestamp set */
|
||||
if (message.sentTimestamp == null) {
|
||||
message.sentTimestamp = System.currentTimeMillis() // Visible messages will already have their sent timestamp set
|
||||
}
|
||||
message.sender = userPublicKey
|
||||
val isSelfSend = (message.recipient == userPublicKey)
|
||||
// Set the failure handler (need it here already for precondition failure handling)
|
||||
@@ -91,8 +84,7 @@ object MessageSender {
|
||||
when (destination) {
|
||||
is Destination.Contact -> message.recipient = destination.publicKey
|
||||
is Destination.ClosedGroup -> message.recipient = destination.groupPublicKey
|
||||
is Destination.OpenGroup,
|
||||
is Destination.OpenGroupV2 -> throw Error.PreconditionFailure("Destination should not be open groups!")
|
||||
is Destination.OpenGroup, is Destination.OpenGroupV2 -> throw IllegalStateException("Destination should not be an open group.")
|
||||
}
|
||||
// Validate the message
|
||||
if (!message.isValid()) { throw Error.InvalidMessage }
|
||||
@@ -125,13 +117,12 @@ object MessageSender {
|
||||
// Encrypt the serialized protobuf
|
||||
val ciphertext: ByteArray
|
||||
when (destination) {
|
||||
is Destination.Contact -> ciphertext = MessageSenderEncryption.encryptWithSessionProtocol(plaintext, destination.publicKey)
|
||||
is Destination.Contact -> ciphertext = MessageEncrypter.encrypt(plaintext, destination.publicKey)
|
||||
is Destination.ClosedGroup -> {
|
||||
val encryptionKeyPair = MessagingModuleConfiguration.shared.storage.getLatestClosedGroupEncryptionKeyPair(destination.groupPublicKey)!!
|
||||
ciphertext = MessageSenderEncryption.encryptWithSessionProtocol(plaintext, encryptionKeyPair.hexEncodedPublicKey)
|
||||
ciphertext = MessageEncrypter.encrypt(plaintext, encryptionKeyPair.hexEncodedPublicKey)
|
||||
}
|
||||
is Destination.OpenGroup,
|
||||
is Destination.OpenGroupV2 -> throw Error.PreconditionFailure("Destination should not be open groups!")
|
||||
is Destination.OpenGroup, is Destination.OpenGroupV2 -> throw IllegalStateException("Destination should not be open group.")
|
||||
}
|
||||
// Wrap the result
|
||||
val kind: SignalServiceProtos.Envelope.Type
|
||||
@@ -145,8 +136,7 @@ object MessageSender {
|
||||
kind = SignalServiceProtos.Envelope.Type.CLOSED_GROUP_MESSAGE
|
||||
senderPublicKey = destination.groupPublicKey
|
||||
}
|
||||
is Destination.OpenGroup,
|
||||
is Destination.OpenGroupV2 -> throw Error.PreconditionFailure("Destination should not be open groups!")
|
||||
is Destination.OpenGroup, is Destination.OpenGroupV2 -> throw IllegalStateException("Destination should not be open group.")
|
||||
}
|
||||
val wrappedMessage = MessageWrapper.wrap(kind, message.sentTimestamp!!, senderPublicKey, ciphertext)
|
||||
// Send the result
|
||||
@@ -201,7 +191,9 @@ object MessageSender {
|
||||
private fun sendToOpenGroupDestination(destination: Destination, message: Message): Promise<Unit, Exception> {
|
||||
val deferred = deferred<Unit, Exception>()
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
message.sentTimestamp ?: run { message.sentTimestamp = System.currentTimeMillis() }
|
||||
if (message.sentTimestamp == null) {
|
||||
message.sentTimestamp = System.currentTimeMillis()
|
||||
}
|
||||
message.sender = storage.getUserPublicKey()
|
||||
// Set the failure handler (need it here already for precondition failure handling)
|
||||
fun handleFailure(error: Exception) {
|
||||
@@ -210,18 +202,15 @@ object MessageSender {
|
||||
}
|
||||
try {
|
||||
when (destination) {
|
||||
is Destination.Contact -> throw Error.PreconditionFailure("Destination should not be contacts!")
|
||||
is Destination.ClosedGroup -> throw Error.PreconditionFailure("Destination should not be closed groups!")
|
||||
is Destination.Contact, is Destination.ClosedGroup -> throw IllegalStateException("Invalid destination.")
|
||||
is Destination.OpenGroup -> {
|
||||
message.recipient = "${destination.server}.${destination.channel}"
|
||||
val server = destination.server
|
||||
val channel = destination.channel
|
||||
|
||||
// Validate the message
|
||||
if (message !is VisibleMessage || !message.isValid()) {
|
||||
throw Error.InvalidMessage
|
||||
}
|
||||
|
||||
// Convert the message to an open group message
|
||||
val openGroupMessage = OpenGroupMessage.from(message, server) ?: run {
|
||||
throw Error.InvalidMessage
|
||||
@@ -239,7 +228,6 @@ object MessageSender {
|
||||
message.recipient = "${destination.server}.${destination.room}"
|
||||
val server = destination.server
|
||||
val room = destination.room
|
||||
|
||||
// Attach the user's profile if needed
|
||||
if (message is VisibleMessage) {
|
||||
val displayName = storage.getUserDisplayName()!!
|
||||
@@ -251,20 +239,17 @@ object MessageSender {
|
||||
message.profile = Profile(displayName)
|
||||
}
|
||||
}
|
||||
|
||||
// Validate the message
|
||||
if (message !is VisibleMessage || !message.isValid()) {
|
||||
throw Error.InvalidMessage
|
||||
}
|
||||
|
||||
val proto = message.toProto()!!
|
||||
|
||||
val plaintext = PushTransportDetails.getPaddedMessageBody(proto.toByteArray())
|
||||
val openGroupMessage = OpenGroupMessageV2(
|
||||
sender = message.sender,
|
||||
sentTimestamp = message.sentTimestamp!!,
|
||||
base64EncodedData = Base64.encodeBytes(proto.toByteArray()),
|
||||
sender = message.sender,
|
||||
sentTimestamp = message.sentTimestamp!!,
|
||||
base64EncodedData = Base64.encodeBytes(plaintext),
|
||||
)
|
||||
|
||||
OpenGroupAPIV2.send(openGroupMessage,room,server).success {
|
||||
message.openGroupServerMessageID = it.serverID
|
||||
handleSuccessfulMessageSend(message, destination)
|
||||
@@ -272,7 +257,6 @@ object MessageSender {
|
||||
}.fail {
|
||||
handleFailure(it)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
} catch (exception: Exception) {
|
||||
@@ -285,7 +269,7 @@ object MessageSender {
|
||||
fun handleSuccessfulMessageSend(message: Message, destination: Destination, isSyncMessage: Boolean = false) {
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val userPublicKey = storage.getUserPublicKey()!!
|
||||
val messageId = storage.getMessageIdInDatabase(message.sentTimestamp!!, message.sender?:userPublicKey) ?: return
|
||||
val messageID = storage.getMessageIdInDatabase(message.sentTimestamp!!, message.sender?:userPublicKey) ?: return
|
||||
// Ignore future self-sends
|
||||
storage.addReceivedMessageTimestamp(message.sentTimestamp!!)
|
||||
// Track the open group server message ID
|
||||
@@ -293,7 +277,7 @@ object MessageSender {
|
||||
val encoded = GroupUtil.getEncodedOpenGroupID("${destination.server}.${destination.room}".toByteArray())
|
||||
val threadID = storage.getThreadIdFor(Address.fromSerialized(encoded))
|
||||
if (threadID != null && threadID >= 0) {
|
||||
storage.setOpenGroupServerMessageID(messageId, message.openGroupServerMessageID!!, threadID, !(message as VisibleMessage).isMediaMessage())
|
||||
storage.setOpenGroupServerMessageID(messageID, message.openGroupServerMessageID!!, threadID, !(message as VisibleMessage).isMediaMessage())
|
||||
}
|
||||
}
|
||||
// Mark the message as sent
|
||||
@@ -323,16 +307,16 @@ object MessageSender {
|
||||
// Convenience
|
||||
@JvmStatic
|
||||
fun send(message: VisibleMessage, address: Address, attachments: List<SignalAttachment>, quote: SignalQuote?, linkPreview: SignalLinkPreview?) {
|
||||
val dataProvider = MessagingModuleConfiguration.shared.messageDataProvider
|
||||
val attachmentIDs = dataProvider.getAttachmentIDsFor(message.id!!)
|
||||
val messageDataProvider = MessagingModuleConfiguration.shared.messageDataProvider
|
||||
val attachmentIDs = messageDataProvider.getAttachmentIDsFor(message.id!!)
|
||||
message.attachmentIDs.addAll(attachmentIDs)
|
||||
message.quote = Quote.from(quote)
|
||||
message.linkPreview = LinkPreview.from(linkPreview)
|
||||
message.linkPreview?.let {
|
||||
if (it.attachmentID == null) {
|
||||
dataProvider.getLinkPreviewAttachmentIDFor(message.id!!)?.let {
|
||||
message.linkPreview!!.attachmentID = it
|
||||
message.attachmentIDs.remove(it)
|
||||
message.linkPreview?.let { linkPreview ->
|
||||
if (linkPreview.attachmentID == null) {
|
||||
messageDataProvider.getLinkPreviewAttachmentIDFor(message.id!!)?.let { attachmentID ->
|
||||
message.linkPreview!!.attachmentID = attachmentID
|
||||
message.attachmentIDs.remove(attachmentID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -9,24 +9,25 @@ import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.messaging.messages.control.ClosedGroupControlMessage
|
||||
import org.session.libsession.messaging.sending_receiving.MessageSender.Error
|
||||
import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI
|
||||
import org.session.libsession.messaging.threads.Address
|
||||
import org.session.libsession.utilities.Address
|
||||
import org.session.libsession.utilities.GroupUtil
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsignal.libsignal.ecc.Curve
|
||||
import org.session.libsignal.libsignal.ecc.ECKeyPair
|
||||
import org.session.libsignal.libsignal.util.guava.Optional
|
||||
import org.session.libsignal.service.api.messages.SignalServiceGroup
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos
|
||||
import org.session.libsignal.service.loki.utilities.hexEncodedPublicKey
|
||||
import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded
|
||||
import org.session.libsignal.crypto.ecc.Curve
|
||||
import org.session.libsignal.crypto.ecc.ECKeyPair
|
||||
import org.session.libsignal.utilities.guava.Optional
|
||||
import org.session.libsignal.messages.SignalServiceGroup
|
||||
import org.session.libsignal.protos.SignalServiceProtos
|
||||
import org.session.libsignal.utilities.hexEncodedPublicKey
|
||||
import org.session.libsignal.utilities.removing05PrefixIfNeeded
|
||||
import org.session.libsignal.utilities.Hex
|
||||
import org.session.libsignal.utilities.ThreadUtils
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
import org.session.libsignal.utilities.Log
|
||||
import java.util.*
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
const val groupSizeLimit = 100
|
||||
val pendingKeyPair = ConcurrentHashMap<String, Optional<ECKeyPair>>()
|
||||
|
||||
val pendingKeyPairs = ConcurrentHashMap<String, Optional<ECKeyPair>>()
|
||||
|
||||
fun MessageSender.create(name: String, members: Collection<String>): Promise<String, Exception> {
|
||||
val deferred = deferred<String, Exception>()
|
||||
@@ -45,7 +46,7 @@ fun MessageSender.create(name: String, members: Collection<String>): Promise<Str
|
||||
val admins = setOf( userPublicKey )
|
||||
val adminsAsData = admins.map { ByteString.copyFrom(Hex.fromStringCondensed(it)) }
|
||||
storage.createGroup(groupID, name, LinkedList(members.map { Address.fromSerialized(it) }),
|
||||
null, null, LinkedList(admins.map { Address.fromSerialized(it) }), System.currentTimeMillis())
|
||||
null, null, LinkedList(admins.map { Address.fromSerialized(it) }), System.currentTimeMillis())
|
||||
storage.setProfileSharing(Address.fromSerialized(groupID), true)
|
||||
// Send a closed group update message to all members individually
|
||||
val closedGroupUpdateKind = ClosedGroupControlMessage.Kind.New(ByteString.copyFrom(Hex.fromStringCondensed(groupPublicKey)), name, encryptionKeyPair, membersAsData, adminsAsData)
|
||||
@@ -53,8 +54,14 @@ fun MessageSender.create(name: String, members: Collection<String>): Promise<Str
|
||||
for (member in members) {
|
||||
val closedGroupControlMessage = ClosedGroupControlMessage(closedGroupUpdateKind)
|
||||
closedGroupControlMessage.sentTimestamp = sentTime
|
||||
sendNonDurably(closedGroupControlMessage, Address.fromSerialized(member)).get()
|
||||
try {
|
||||
sendNonDurably(closedGroupControlMessage, Address.fromSerialized(member)).get()
|
||||
} catch (e: Exception) {
|
||||
deferred.reject(e)
|
||||
return@queue
|
||||
}
|
||||
}
|
||||
|
||||
// Add the group to the user's set of public keys to poll for
|
||||
storage.addClosedGroupPublicKey(groupPublicKey)
|
||||
// Store the encryption key pair
|
||||
@@ -179,13 +186,11 @@ fun MessageSender.removeMembers(groupPublicKey: String, membersToRemove: List<St
|
||||
Log.d("Loki", "Can't remove admin from closed group unless the group is destroyed entirely.")
|
||||
throw Error.InvalidClosedGroupUpdate
|
||||
}
|
||||
|
||||
// Save the new group members
|
||||
storage.updateMembers(groupID, updatedMembers.map { Address.fromSerialized(it) })
|
||||
// Update the zombie list
|
||||
val oldZombies = storage.getZombieMember(groupID)
|
||||
storage.updateZombieMembers(groupID, oldZombies.minus(membersToRemove).map { Address.fromSerialized(it) })
|
||||
|
||||
val removeMembersAsData = membersToRemove.map { ByteString.copyFrom(Hex.fromStringCondensed(it)) }
|
||||
val name = group.title
|
||||
// Send the update to the group
|
||||
@@ -194,17 +199,14 @@ fun MessageSender.removeMembers(groupPublicKey: String, membersToRemove: List<St
|
||||
val closedGroupControlMessage = ClosedGroupControlMessage(memberUpdateKind)
|
||||
closedGroupControlMessage.sentTimestamp = sentTime
|
||||
send(closedGroupControlMessage, Address.fromSerialized(groupID))
|
||||
|
||||
// Send the new encryption key pair to the remaining group members
|
||||
// At this stage we know the user is admin, no need to test
|
||||
// Send the new encryption key pair to the remaining group members.
|
||||
// At this stage we know the user is admin, no need to test.
|
||||
generateAndSendNewEncryptionKeyPair(groupPublicKey, updatedMembers)
|
||||
// Notify the user
|
||||
|
||||
// Insert an outgoing notification
|
||||
// we don't display zombie members in the notification as users have already been notified when those members left
|
||||
// We don't display zombie members in the notification as users have already been notified when those members left
|
||||
val notificationMembers = membersToRemove.minus(oldZombies)
|
||||
if (notificationMembers.isNotEmpty()) {
|
||||
// no notification to display when only zombies have been removed
|
||||
// No notification to display when only zombies have been removed
|
||||
val infoType = SignalServiceGroup.Type.MEMBER_REMOVED
|
||||
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
|
||||
storage.insertOutgoingInfoMessage(context, groupID, infoType, name, notificationMembers, admins, threadID, sentTime)
|
||||
@@ -259,16 +261,16 @@ fun MessageSender.generateAndSendNewEncryptionKeyPair(groupPublicKey: String, ta
|
||||
}
|
||||
// Generate the new encryption key pair
|
||||
val newKeyPair = Curve.generateKeyPair()
|
||||
// replace call will not succeed if no value already set
|
||||
pendingKeyPair.putIfAbsent(groupPublicKey,Optional.absent())
|
||||
// Replace call will not succeed if no value already set
|
||||
pendingKeyPairs.putIfAbsent(groupPublicKey,Optional.absent())
|
||||
do {
|
||||
// make sure we set the pendingKeyPair or wait until it is not null
|
||||
} while (!pendingKeyPair.replace(groupPublicKey,Optional.absent(),Optional.fromNullable(newKeyPair)))
|
||||
// Make sure we set the pending key pair or wait until it is not null
|
||||
} while (!pendingKeyPairs.replace(groupPublicKey,Optional.absent(),Optional.fromNullable(newKeyPair)))
|
||||
// Distribute it
|
||||
sendEncryptionKeyPair(groupPublicKey, newKeyPair, targetMembers)?.success {
|
||||
// Store it * after * having sent out the message to the group
|
||||
storage.addClosedGroupEncryptionKeyPair(newKeyPair, groupPublicKey)
|
||||
pendingKeyPair[groupPublicKey] = Optional.absent()
|
||||
pendingKeyPairs[groupPublicKey] = Optional.absent()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -279,7 +281,7 @@ fun MessageSender.sendEncryptionKeyPair(groupPublicKey: String, newKeyPair: ECKe
|
||||
proto.privateKey = ByteString.copyFrom(newKeyPair.privateKey.serialize())
|
||||
val plaintext = proto.build().toByteArray()
|
||||
val wrappers = targetMembers.map { publicKey ->
|
||||
val ciphertext = MessageSenderEncryption.encryptWithSessionProtocol(plaintext, publicKey)
|
||||
val ciphertext = MessageEncrypter.encrypt(plaintext, publicKey)
|
||||
ClosedGroupControlMessage.KeyPairWrapper(publicKey, ByteString.copyFrom(ciphertext))
|
||||
}
|
||||
val kind = ClosedGroupControlMessage.Kind.EncryptionKeyPair(ByteString.copyFrom(Hex.fromStringCondensed(groupPublicKey)), wrappers)
|
||||
@@ -307,14 +309,14 @@ fun MessageSender.sendLatestEncryptionKeyPair(publicKey: String, groupPublicKey:
|
||||
return
|
||||
}
|
||||
// Get the latest encryption key pair
|
||||
val encryptionKeyPair = pendingKeyPair[groupPublicKey]?.orNull()
|
||||
?: storage.getLatestClosedGroupEncryptionKeyPair(groupPublicKey) ?: return
|
||||
val encryptionKeyPair = pendingKeyPairs[groupPublicKey]?.orNull()
|
||||
?: storage.getLatestClosedGroupEncryptionKeyPair(groupPublicKey) ?: return
|
||||
// Send it
|
||||
val proto = SignalServiceProtos.KeyPair.newBuilder()
|
||||
proto.publicKey = ByteString.copyFrom(encryptionKeyPair.publicKey.serialize().removing05PrefixIfNeeded())
|
||||
proto.privateKey = ByteString.copyFrom(encryptionKeyPair.privateKey.serialize())
|
||||
val plaintext = proto.build().toByteArray()
|
||||
val ciphertext = MessageSenderEncryption.encryptWithSessionProtocol(plaintext, publicKey)
|
||||
val ciphertext = MessageEncrypter.encrypt(plaintext, publicKey)
|
||||
Log.d("Loki", "Sending latest encryption key pair to: $publicKey.")
|
||||
val wrapper = ClosedGroupControlMessage.KeyPairWrapper(publicKey, ByteString.copyFrom(ciphertext))
|
||||
val kind = ClosedGroupControlMessage.Kind.EncryptionKeyPair(ByteString.copyFrom(Hex.fromStringCondensed(groupPublicKey)), listOf(wrapper))
|
||||
|
@@ -1,7 +1,6 @@
|
||||
package org.session.libsession.messaging.sending_receiving
|
||||
|
||||
import android.text.TextUtils
|
||||
import okhttp3.HttpUrl
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.messaging.jobs.AttachmentDownloadJob
|
||||
import org.session.libsession.messaging.jobs.JobQueue
|
||||
@@ -14,28 +13,28 @@ import org.session.libsession.messaging.sending_receiving.data_extraction.DataEx
|
||||
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview
|
||||
import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI
|
||||
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel
|
||||
import org.session.libsession.messaging.threads.Address
|
||||
import org.session.libsession.messaging.threads.GroupRecord
|
||||
import org.session.libsession.messaging.threads.recipients.Recipient
|
||||
import org.session.libsession.utilities.Address
|
||||
import org.session.libsession.utilities.GroupRecord
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.session.libsession.utilities.GroupUtil
|
||||
import org.session.libsession.utilities.SSKEnvironment
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsession.utilities.preferences.ProfileKeyUtil
|
||||
import org.session.libsignal.libsignal.ecc.DjbECPrivateKey
|
||||
import org.session.libsignal.libsignal.ecc.DjbECPublicKey
|
||||
import org.session.libsignal.libsignal.ecc.ECKeyPair
|
||||
import org.session.libsignal.libsignal.util.guava.Optional
|
||||
import org.session.libsignal.service.api.messages.SignalServiceGroup
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos
|
||||
import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded
|
||||
import org.session.libsignal.service.loki.utilities.toHexString
|
||||
import org.session.libsession.utilities.ProfileKeyUtil
|
||||
import org.session.libsignal.crypto.ecc.DjbECPrivateKey
|
||||
import org.session.libsignal.crypto.ecc.DjbECPublicKey
|
||||
import org.session.libsignal.crypto.ecc.ECKeyPair
|
||||
import org.session.libsignal.utilities.guava.Optional
|
||||
import org.session.libsignal.messages.SignalServiceGroup
|
||||
import org.session.libsignal.protos.SignalServiceProtos
|
||||
import org.session.libsignal.utilities.removing05PrefixIfNeeded
|
||||
import org.session.libsignal.utilities.toHexString
|
||||
import org.session.libsignal.utilities.Base64
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
import org.session.libsignal.utilities.Log
|
||||
import java.security.MessageDigest
|
||||
import java.util.*
|
||||
import kotlin.collections.ArrayList
|
||||
|
||||
internal fun MessageReceiver.isBlock(publicKey: String): Boolean {
|
||||
internal fun MessageReceiver.isBlocked(publicKey: String): Boolean {
|
||||
val context = MessagingModuleConfiguration.shared.context
|
||||
val recipient = Recipient.from(context, Address.fromSerialized(publicKey), false)
|
||||
return recipient.isBlocked
|
||||
@@ -53,6 +52,7 @@ fun MessageReceiver.handle(message: Message, proto: SignalServiceProtos.Content,
|
||||
}
|
||||
}
|
||||
|
||||
// region Control Messages
|
||||
private fun MessageReceiver.handleReadReceipt(message: ReadReceipt) {
|
||||
val context = MessagingModuleConfiguration.shared.context
|
||||
SSKEnvironment.shared.readReceiptManager.processReadReceipts(context, message.sender!!, message.timestamps!!, message.receivedTimestamp!!)
|
||||
@@ -94,12 +94,9 @@ private fun MessageReceiver.handleExpirationTimerUpdate(message: ExpirationTimer
|
||||
}
|
||||
}
|
||||
|
||||
// Data Extraction Notification handling
|
||||
|
||||
private fun MessageReceiver.handleDataExtractionNotification(message: DataExtractionNotification) {
|
||||
// we don't handle data extraction messages for groups (they shouldn't be sent, but in case we filter them here too)
|
||||
// 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 storage = MessagingModuleConfiguration.shared.storage
|
||||
val senderPublicKey = message.sender!!
|
||||
val notification: DataExtractionNotificationInfoMessage = when(message.kind) {
|
||||
@@ -110,23 +107,23 @@ private fun MessageReceiver.handleDataExtractionNotification(message: DataExtrac
|
||||
storage.insertDataExtractionNotificationMessage(senderPublicKey, notification, message.sentTimestamp!!)
|
||||
}
|
||||
|
||||
// Configuration message handling
|
||||
|
||||
private fun handleConfigurationMessage(message: ConfigurationMessage) {
|
||||
val context = MessagingModuleConfiguration.shared.context
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
if (TextSecurePreferences.getConfigurationMessageSynced(context) && !TextSecurePreferences.shouldUpdateProfile(context, message.sentTimestamp!!)) return
|
||||
if (TextSecurePreferences.getConfigurationMessageSynced(context)
|
||||
&& !TextSecurePreferences.shouldUpdateProfile(context, message.sentTimestamp!!)) return
|
||||
val userPublicKey = storage.getUserPublicKey()
|
||||
if (userPublicKey == null || message.sender != storage.getUserPublicKey()) return
|
||||
TextSecurePreferences.setConfigurationMessageSynced(context, true)
|
||||
TextSecurePreferences.setLastProfileUpdateTime(context, message.sentTimestamp!!)
|
||||
val allClosedGroupPublicKeys = storage.getAllClosedGroupPublicKeys()
|
||||
for (closeGroup in message.closedGroups) {
|
||||
if (allClosedGroupPublicKeys.contains(closeGroup.publicKey)) continue
|
||||
handleNewClosedGroup(message.sender!!, message.sentTimestamp!!, closeGroup.publicKey, closeGroup.name, closeGroup.encryptionKeyPair!!, closeGroup.members, closeGroup.admins, message.sentTimestamp!!)
|
||||
for (closedGroup in message.closedGroups) {
|
||||
if (allClosedGroupPublicKeys.contains(closedGroup.publicKey)) continue
|
||||
handleNewClosedGroup(message.sender!!, message.sentTimestamp!!, closedGroup.publicKey, closedGroup.name,
|
||||
closedGroup.encryptionKeyPair!!, closedGroup.members, closedGroup.admins, message.sentTimestamp!!)
|
||||
}
|
||||
val allOpenGroups = storage.getAllOpenGroups().map { it.value.server }
|
||||
val allV2OpenGroups = storage.getAllV2OpenGroups().map { it.value.toJoinUrl() }
|
||||
val allV2OpenGroups = storage.getAllV2OpenGroups().map { it.value.joinURL }
|
||||
for (openGroup in message.openGroups) {
|
||||
if (allOpenGroups.contains(openGroup) || allV2OpenGroups.contains(openGroup)) continue
|
||||
storage.addOpenGroup(openGroup, 1)
|
||||
@@ -135,52 +132,48 @@ private fun handleConfigurationMessage(message: ConfigurationMessage) {
|
||||
TextSecurePreferences.setProfileName(context, message.displayName)
|
||||
storage.setDisplayName(userPublicKey, message.displayName)
|
||||
}
|
||||
if (message.profileKey.isNotEmpty()) {
|
||||
if (message.profileKey.isNotEmpty() && !message.profilePicture.isNullOrEmpty()
|
||||
&& TextSecurePreferences.getProfilePictureURL(context) != message.profilePicture) {
|
||||
val profileKey = Base64.encodeBytes(message.profileKey)
|
||||
ProfileKeyUtil.setEncodedProfileKey(context, profileKey)
|
||||
storage.setProfileKeyForRecipient(userPublicKey, message.profileKey)
|
||||
// handle profile photo
|
||||
if (!message.profilePicture.isNullOrEmpty() && TextSecurePreferences.getProfilePictureURL(context) != message.profilePicture) {
|
||||
storage.setUserProfilePictureUrl(message.profilePicture!!)
|
||||
}
|
||||
storage.setUserProfilePictureUrl(message.profilePicture!!)
|
||||
}
|
||||
storage.addContacts(message.contacts)
|
||||
}
|
||||
//endregion
|
||||
|
||||
// region Visible Messages
|
||||
fun MessageReceiver.handleVisibleMessage(message: VisibleMessage, proto: SignalServiceProtos.Content, openGroupID: String?) {
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val context = MessagingModuleConfiguration.shared.context
|
||||
val userPublicKey = storage.getUserPublicKey()
|
||||
|
||||
// Get or create thread
|
||||
// FIXME: In case this is an open group this actually * doesn't * create the thread if it doesn't yet
|
||||
// exist. This is intentional, but it's very non-obvious.
|
||||
val threadID = storage.getOrCreateThreadIdFor(message.syncTarget
|
||||
?: message.sender!!, message.groupPublicKey, openGroupID)
|
||||
|
||||
?: message.sender!!, message.groupPublicKey, openGroupID)
|
||||
if (threadID < 0) {
|
||||
// Thread doesn't exist; should only be reached in a case where we are processing open group messages for a no longer existent thread
|
||||
throw MessageReceiver.Error.NoThread
|
||||
}
|
||||
val openGroup = threadID.let {
|
||||
storage.getOpenGroup(it.toString())
|
||||
}
|
||||
|
||||
// Update profile if needed
|
||||
val newProfile = message.profile
|
||||
|
||||
if (newProfile != null && userPublicKey != message.sender && openGroup == null) {
|
||||
val profile = message.profile
|
||||
if (profile != null && userPublicKey != message.sender && openGroup == null) { // Don't do this in V1 open groups
|
||||
val profileManager = SSKEnvironment.shared.profileManager
|
||||
val recipient = Recipient.from(context, Address.fromSerialized(message.sender!!), false)
|
||||
val displayName = newProfile.displayName!!
|
||||
val displayName = profile.displayName!!
|
||||
if (displayName.isNotEmpty()) {
|
||||
profileManager.setDisplayName(context, recipient, displayName)
|
||||
}
|
||||
if (newProfile.profileKey?.isNotEmpty() == true
|
||||
&& (recipient.profileKey == null || !MessageDigest.isEqual(recipient.profileKey, newProfile.profileKey))) {
|
||||
profileManager.setProfileKey(context, recipient, newProfile.profileKey!!)
|
||||
if (profile.profileKey?.isNotEmpty() == true && profile.profilePictureURL?.isNotEmpty() == true
|
||||
&& (recipient.profileKey == null || !MessageDigest.isEqual(recipient.profileKey, profile.profileKey))) {
|
||||
profileManager.setProfileKey(context, recipient, profile.profileKey!!)
|
||||
profileManager.setUnidentifiedAccessMode(context, recipient, Recipient.UnidentifiedAccessMode.UNKNOWN)
|
||||
val newUrl = newProfile.profilePictureURL
|
||||
if (!newUrl.isNullOrEmpty()) {
|
||||
profileManager.setProfilePictureURL(context, recipient, newUrl)
|
||||
if (userPublicKey == message.sender) {
|
||||
profileManager.updateOpenGroupProfilePicturesIfNeeded(context)
|
||||
}
|
||||
}
|
||||
profileManager.setProfilePictureURL(context, recipient, profile.profilePictureURL!!)
|
||||
}
|
||||
}
|
||||
// Parse quote if needed
|
||||
@@ -188,10 +181,11 @@ fun MessageReceiver.handleVisibleMessage(message: VisibleMessage, proto: SignalS
|
||||
if (message.quote != null && proto.dataMessage.hasQuote()) {
|
||||
val quote = proto.dataMessage.quote
|
||||
val author = Address.fromSerialized(quote.author)
|
||||
val messageInfo = MessagingModuleConfiguration.shared.messageDataProvider.getMessageForQuote(quote.id, author)
|
||||
val messageDataProvider = MessagingModuleConfiguration.shared.messageDataProvider
|
||||
val messageInfo = messageDataProvider.getMessageForQuote(quote.id, author)
|
||||
if (messageInfo != null) {
|
||||
val attachments = if (messageInfo.second) MessagingModuleConfiguration.shared.messageDataProvider.getAttachmentsAndLinkPreviewFor(messageInfo.first) else ArrayList()
|
||||
quoteModel = QuoteModel(quote.id, author, MessagingModuleConfiguration.shared.messageDataProvider.getMessageBodyFor(quote.id, quote.author), false, attachments)
|
||||
val attachments = if (messageInfo.second) messageDataProvider.getAttachmentsAndLinkPreviewFor(messageInfo.first) else ArrayList()
|
||||
quoteModel = QuoteModel(quote.id, author, messageDataProvider.getMessageBodyFor(quote.id, quote.author), false, attachments)
|
||||
} else {
|
||||
quoteModel = QuoteModel(quote.id, author, quote.text, true, PointerAttachment.forPointers(proto.dataMessage.quote.attachmentsList))
|
||||
}
|
||||
@@ -212,6 +206,7 @@ fun MessageReceiver.handleVisibleMessage(message: VisibleMessage, proto: SignalS
|
||||
}
|
||||
}
|
||||
}
|
||||
// Parse attachments if needed
|
||||
val attachments = proto.dataMessage.attachmentsList.mapNotNull { proto ->
|
||||
val attachment = Attachment.fromProto(proto)
|
||||
if (!attachment.isValid()) {
|
||||
@@ -220,7 +215,6 @@ fun MessageReceiver.handleVisibleMessage(message: VisibleMessage, proto: SignalS
|
||||
return@mapNotNull attachment
|
||||
}
|
||||
}
|
||||
// Parse stickers if needed
|
||||
// Persist the message
|
||||
message.threadID = threadID
|
||||
val messageID = storage.persist(message, quoteModel, linkPreviews, message.groupPublicKey, openGroupID, attachments) ?: throw MessageReceiver.Error.DuplicateMessage
|
||||
@@ -234,14 +228,17 @@ fun MessageReceiver.handleVisibleMessage(message: VisibleMessage, proto: SignalS
|
||||
}
|
||||
val openGroupServerID = message.openGroupServerMessageID
|
||||
if (openGroupServerID != null) {
|
||||
storage.setOpenGroupServerMessageID(messageID, openGroupServerID, threadID, !(message.isMediaMessage() || attachments.isNotEmpty()))
|
||||
val isSms = !(message.isMediaMessage() || attachments.isNotEmpty())
|
||||
storage.setOpenGroupServerMessageID(messageID, openGroupServerID, threadID, isSms)
|
||||
}
|
||||
// Cancel any typing indicators if needed
|
||||
cancelTypingIndicatorsIfNeeded(message.sender!!)
|
||||
//Notify the user if needed
|
||||
SSKEnvironment.shared.notificationManager.updateNotification(context, threadID)
|
||||
}
|
||||
//endregion
|
||||
|
||||
// region Closed Groups
|
||||
private fun MessageReceiver.handleClosedGroupControlMessage(message: ClosedGroupControlMessage) {
|
||||
when (message.kind!!) {
|
||||
is ClosedGroupControlMessage.Kind.New -> handleNewClosedGroup(message)
|
||||
@@ -261,7 +258,6 @@ private fun MessageReceiver.handleNewClosedGroup(message: ClosedGroupControlMess
|
||||
handleNewClosedGroup(message.sender!!, message.sentTimestamp!!, groupPublicKey, kind.name, kind.encryptionKeyPair!!, members, admins, message.sentTimestamp!!)
|
||||
}
|
||||
|
||||
// Parameter @sender:String is just for inserting incoming info message
|
||||
private fun handleNewClosedGroup(sender: String, sentTimestamp: Long, groupPublicKey: String, name: String, encryptionKeyPair: ECKeyPair, members: List<String>, admins: List<String>, formationTimestamp: Long) {
|
||||
val context = MessagingModuleConfiguration.shared.context
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
@@ -273,11 +269,10 @@ private fun handleNewClosedGroup(sender: String, sentTimestamp: Long, groupPubli
|
||||
storage.updateMembers(groupID, members.map { Address.fromSerialized(it) })
|
||||
} else {
|
||||
storage.createGroup(groupID, name, LinkedList(members.map { Address.fromSerialized(it) }),
|
||||
null, null, LinkedList(admins.map { Address.fromSerialized(it) }), formationTimestamp)
|
||||
null, null, LinkedList(admins.map { Address.fromSerialized(it) }), formationTimestamp)
|
||||
val userPublicKey = TextSecurePreferences.getLocalNumber(context)
|
||||
// Notify the user
|
||||
if (userPublicKey == sender) {
|
||||
// sender is a linked device
|
||||
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
|
||||
storage.insertOutgoingInfoMessage(context, groupID, SignalServiceGroup.Type.CREATION, name, members, admins, threadID, sentTimestamp)
|
||||
} else {
|
||||
@@ -305,11 +300,11 @@ private fun MessageReceiver.handleClosedGroupEncryptionKeyPair(message: ClosedGr
|
||||
// Unwrap the message
|
||||
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
|
||||
val group = storage.getGroup(groupID) ?: run {
|
||||
Log.d("Loki", "Ignoring closed group info message for nonexistent group.")
|
||||
Log.d("Loki", "Ignoring closed group encryption key pair for nonexistent group.")
|
||||
return
|
||||
}
|
||||
if (!group.isActive) {
|
||||
Log.d("Loki", "Ignoring closed group info message for inactive group")
|
||||
Log.d("Loki", "Ignoring closed group encryption key pair for inactive group.")
|
||||
return
|
||||
}
|
||||
if (!group.admins.map { it.toString() }.contains(senderPublicKey)) {
|
||||
@@ -319,7 +314,7 @@ private fun MessageReceiver.handleClosedGroupEncryptionKeyPair(message: ClosedGr
|
||||
// Find our wrapper and decrypt it if possible
|
||||
val wrapper = kind.wrappers.firstOrNull { it.publicKey!! == userPublicKey } ?: return
|
||||
val encryptedKeyPair = wrapper.encryptedKeyPair!!.toByteArray()
|
||||
val plaintext = MessageReceiverDecryption.decryptWithSessionProtocol(encryptedKeyPair, userKeyPair).first
|
||||
val plaintext = MessageDecrypter.decrypt(encryptedKeyPair, userKeyPair).first
|
||||
// Parse it
|
||||
val proto = SignalServiceProtos.KeyPair.parseFrom(plaintext)
|
||||
val keyPair = ECKeyPair(DjbECPublicKey(proto.publicKey.toByteArray().removing05PrefixIfNeeded()), DjbECPrivateKey(proto.privateKey.toByteArray()))
|
||||
@@ -330,7 +325,7 @@ private fun MessageReceiver.handleClosedGroupEncryptionKeyPair(message: ClosedGr
|
||||
return
|
||||
}
|
||||
storage.addClosedGroupEncryptionKeyPair(keyPair, groupPublicKey)
|
||||
Log.d("Loki", "Received a new closed group encryption key pair")
|
||||
Log.d("Loki", "Received a new closed group encryption key pair.")
|
||||
}
|
||||
|
||||
private fun MessageReceiver.handleClosedGroupNameChanged(message: ClosedGroupControlMessage) {
|
||||
@@ -343,11 +338,11 @@ private fun MessageReceiver.handleClosedGroupNameChanged(message: ClosedGroupCon
|
||||
// Check that the sender is a member of the group (before the update)
|
||||
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
|
||||
val group = storage.getGroup(groupID) ?: run {
|
||||
Log.d("Loki", "Ignoring closed group info message for nonexistent group.")
|
||||
Log.d("Loki", "Ignoring closed group update for nonexistent group.")
|
||||
return
|
||||
}
|
||||
if (!group.isActive) {
|
||||
Log.d("Loki", "Ignoring closed group info message for inactive group")
|
||||
Log.d("Loki", "Ignoring closed group update for inactive group.")
|
||||
return
|
||||
}
|
||||
// Check common group update logic
|
||||
@@ -358,10 +353,8 @@ private fun MessageReceiver.handleClosedGroupNameChanged(message: ClosedGroupCon
|
||||
val admins = group.admins.map { it.serialize() }
|
||||
val name = kind.name
|
||||
storage.updateTitle(groupID, name)
|
||||
|
||||
// Notify the user
|
||||
if (userPublicKey == senderPublicKey) {
|
||||
// sender is a linked device
|
||||
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
|
||||
storage.insertOutgoingInfoMessage(context, groupID, SignalServiceGroup.Type.NAME_CHANGE, name, members, admins, threadID, message.sentTimestamp!!)
|
||||
} else {
|
||||
@@ -378,11 +371,11 @@ private fun MessageReceiver.handleClosedGroupMembersAdded(message: ClosedGroupCo
|
||||
val groupPublicKey = message.groupPublicKey ?: return
|
||||
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
|
||||
val group = storage.getGroup(groupID) ?: run {
|
||||
Log.d("Loki", "Ignoring closed group info message for nonexistent group.")
|
||||
Log.d("Loki", "Ignoring closed group update for nonexistent group.")
|
||||
return
|
||||
}
|
||||
if (!group.isActive) {
|
||||
Log.d("Loki", "Ignoring closed group info message for inactive group")
|
||||
Log.d("Loki", "Ignoring closed group update for inactive group.")
|
||||
return
|
||||
}
|
||||
if (!isValidGroupUpdate(group, message.sentTimestamp!!, senderPublicKey)) { return }
|
||||
@@ -402,16 +395,26 @@ private fun MessageReceiver.handleClosedGroupMembersAdded(message: ClosedGroupCo
|
||||
|
||||
// Notify the user
|
||||
if (userPublicKey == senderPublicKey) {
|
||||
// sender is a linked device
|
||||
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
|
||||
storage.insertOutgoingInfoMessage(context, groupID, SignalServiceGroup.Type.MEMBER_ADDED, name, updateMembers, admins, threadID, message.sentTimestamp!!)
|
||||
} else {
|
||||
storage.insertIncomingInfoMessage(context, senderPublicKey, groupID, SignalServiceGroup.Type.MEMBER_ADDED, name, updateMembers, admins, message.sentTimestamp!!)
|
||||
}
|
||||
if (userPublicKey in admins) {
|
||||
// send current encryption key to the latest added members
|
||||
val encryptionKeyPair = pendingKeyPair[groupPublicKey]?.orNull()
|
||||
?: storage.getLatestClosedGroupEncryptionKeyPair(groupPublicKey)
|
||||
// Send the latest encryption key pair to the added members if the current user is the admin of the group
|
||||
//
|
||||
// This fixes a race condition where:
|
||||
// • A member removes another member.
|
||||
// • A member adds someone to the group and sends them the latest group key pair.
|
||||
// • The admin is offline during all of this.
|
||||
// • When the admin comes back online they see the member removed message and generate + distribute a new key pair,
|
||||
// but they don't know about the added member yet.
|
||||
// • Now they see the member added message.
|
||||
//
|
||||
// Without the code below, the added member(s) would never get the key pair that was generated by the admin when they saw
|
||||
// the member removed message.
|
||||
val encryptionKeyPair = pendingKeyPairs[groupPublicKey]?.orNull()
|
||||
?: storage.getLatestClosedGroupEncryptionKeyPair(groupPublicKey)
|
||||
if (encryptionKeyPair == null) {
|
||||
android.util.Log.d("Loki", "Couldn't get encryption key pair for closed group.")
|
||||
} else {
|
||||
@@ -436,65 +439,54 @@ private fun MessageReceiver.handleClosedGroupMembersRemoved(message: ClosedGroup
|
||||
val groupPublicKey = message.groupPublicKey ?: return
|
||||
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
|
||||
val group = storage.getGroup(groupID) ?: run {
|
||||
Log.d("Loki", "Ignoring closed group info message for nonexistent group.")
|
||||
Log.d("Loki", "Ignoring closed group update for nonexistent group.")
|
||||
return
|
||||
}
|
||||
if (!group.isActive) {
|
||||
Log.d("Loki", "Ignoring closed group info message for inactive group.")
|
||||
Log.d("Loki", "Ignoring closed group update for inactive group.")
|
||||
return
|
||||
}
|
||||
val name = group.title
|
||||
// Check common group update logic
|
||||
val members = group.members.map { it.serialize() }
|
||||
val admins = group.admins.map { it.toString() }
|
||||
|
||||
// Users that are part of this remove update
|
||||
val updateMembers = kind.members.map { it.toByteArray().toHexString() }
|
||||
|
||||
val removedMembers = kind.members.map { it.toByteArray().toHexString() }
|
||||
// Check that the admin wasn't removed
|
||||
if (updateMembers.contains(admins.first())) {
|
||||
if (removedMembers.contains(admins.first())) {
|
||||
Log.d("Loki", "Ignoring invalid closed group update.")
|
||||
return
|
||||
}
|
||||
|
||||
// Check that the message was sent by the group admin
|
||||
if (!admins.contains(senderPublicKey)) {
|
||||
Log.d("Loki", "Ignoring invalid closed group update.")
|
||||
return
|
||||
}
|
||||
|
||||
if (!isValidGroupUpdate(group, message.sentTimestamp!!, senderPublicKey)) { return }
|
||||
// If admin leaves the group is disbanded
|
||||
val didAdminLeave = admins.any { it in updateMembers }
|
||||
// newMembers to save is old members minus removed members
|
||||
val newMembers = members - updateMembers
|
||||
// user should be posting MEMBERS_LEFT so this should not be encountered
|
||||
val senderLeft = senderPublicKey in updateMembers
|
||||
// If the admin leaves the group is disbanded
|
||||
val didAdminLeave = admins.any { it in removedMembers }
|
||||
val newMembers = members - removedMembers
|
||||
// A user should be posting a MEMBERS_LEFT in case they leave, so this shouldn't be encountered
|
||||
val senderLeft = senderPublicKey in removedMembers
|
||||
if (senderLeft) {
|
||||
android.util.Log.d("Loki", "Received a MEMBERS_REMOVED instead of a MEMBERS_LEFT from sender $senderPublicKey")
|
||||
Log.d("Loki", "Received a MEMBERS_REMOVED instead of a MEMBERS_LEFT from sender: $senderPublicKey.")
|
||||
}
|
||||
val wasCurrentUserRemoved = userPublicKey in updateMembers
|
||||
|
||||
// admin should send a MEMBERS_LEFT message but handled here in case
|
||||
val wasCurrentUserRemoved = userPublicKey in removedMembers
|
||||
// Admin should send a MEMBERS_LEFT message but handled here just in case
|
||||
if (didAdminLeave || wasCurrentUserRemoved) {
|
||||
disableLocalGroupAndUnsubscribe(groupPublicKey, groupID, userPublicKey)
|
||||
} else {
|
||||
storage.updateMembers(groupID, newMembers.map { Address.fromSerialized(it) })
|
||||
}
|
||||
// update zombie members
|
||||
// Update zombie members
|
||||
val zombies = storage.getZombieMember(groupID)
|
||||
storage.updateZombieMembers(groupID, zombies.minus(updateMembers).map { Address.fromSerialized(it) })
|
||||
|
||||
val type = if (senderLeft) SignalServiceGroup.Type.QUIT
|
||||
else SignalServiceGroup.Type.MEMBER_REMOVED
|
||||
|
||||
storage.updateZombieMembers(groupID, zombies.minus(removedMembers).map { Address.fromSerialized(it) })
|
||||
val type = if (senderLeft) SignalServiceGroup.Type.QUIT else SignalServiceGroup.Type.MEMBER_REMOVED
|
||||
// Notify the user
|
||||
// we don't display zombie members in the notification as users have already been notified when those members left
|
||||
val notificationMembers = updateMembers.minus(zombies)
|
||||
// We don't display zombie members in the notification as users have already been notified when those members left
|
||||
val notificationMembers = removedMembers.minus(zombies)
|
||||
if (notificationMembers.isNotEmpty()) {
|
||||
// no notification to display when only zombies have been removed
|
||||
// No notification to display when only zombies have been removed
|
||||
if (userPublicKey == senderPublicKey) {
|
||||
// sender is a linked device
|
||||
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
|
||||
storage.insertOutgoingInfoMessage(context, groupID, type, name, notificationMembers, admins, threadID, message.sentTimestamp!!)
|
||||
} else {
|
||||
@@ -516,11 +508,11 @@ private fun MessageReceiver.handleClosedGroupMemberLeft(message: ClosedGroupCont
|
||||
val groupPublicKey = message.groupPublicKey ?: return
|
||||
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
|
||||
val group = storage.getGroup(groupID) ?: run {
|
||||
Log.d("Loki", "Ignoring closed group info message for nonexistent group.")
|
||||
Log.d("Loki", "Ignoring closed group update for nonexistent group.")
|
||||
return
|
||||
}
|
||||
if (!group.isActive) {
|
||||
Log.d("Loki", "Ignoring closed group info message for inactive group")
|
||||
Log.d("Loki", "Ignoring closed group update for inactive group.")
|
||||
return
|
||||
}
|
||||
val name = group.title
|
||||
@@ -534,19 +526,16 @@ private fun MessageReceiver.handleClosedGroupMemberLeft(message: ClosedGroupCont
|
||||
val didAdminLeave = admins.contains(senderPublicKey)
|
||||
val updatedMemberList = members - senderPublicKey
|
||||
val userLeft = (userPublicKey == senderPublicKey)
|
||||
|
||||
if (didAdminLeave || userLeft) {
|
||||
// admin left the group of linked device left the group
|
||||
disableLocalGroupAndUnsubscribe(groupPublicKey, groupID, userPublicKey)
|
||||
} else {
|
||||
storage.updateMembers(groupID, updatedMemberList.map { Address.fromSerialized(it) })
|
||||
// update zombie members
|
||||
// Update zombie members
|
||||
val zombies = storage.getZombieMember(groupID)
|
||||
storage.updateZombieMembers(groupID, zombies.plus(senderPublicKey).map { Address.fromSerialized(it) })
|
||||
}
|
||||
// Notify the user
|
||||
if (userLeft) {
|
||||
//sender is a linked device
|
||||
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
|
||||
storage.insertOutgoingInfoMessage(context, groupID, SignalServiceGroup.Type.QUIT, name, members, admins, threadID, message.sentTimestamp!!)
|
||||
} else {
|
||||
@@ -554,9 +543,7 @@ private fun MessageReceiver.handleClosedGroupMemberLeft(message: ClosedGroupCont
|
||||
}
|
||||
}
|
||||
|
||||
private fun isValidGroupUpdate(group: GroupRecord,
|
||||
sentTimestamp: Long,
|
||||
senderPublicKey: String): Boolean {
|
||||
private fun isValidGroupUpdate(group: GroupRecord, sentTimestamp: Long, senderPublicKey: String): Boolean {
|
||||
val oldMembers = group.members.map { it.serialize() }
|
||||
// Check that the message isn't from before the group was created
|
||||
if (group.formationTimestamp > sentTimestamp) {
|
||||
@@ -581,4 +568,5 @@ fun MessageReceiver.disableLocalGroupAndUnsubscribe(groupPublicKey: String, grou
|
||||
storage.removeMember(groupID, Address.fromSerialized(userPublicKey))
|
||||
// Notify the PN server
|
||||
PushNotificationAPI.performOperation(PushNotificationAPI.ClosedGroupOperation.Unsubscribe, groupPublicKey, userPublicKey)
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
|
@@ -5,11 +5,11 @@ import android.net.Uri;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.session.libsignal.libsignal.util.guava.Optional;
|
||||
import org.session.libsignal.service.api.messages.SignalServiceAttachment;
|
||||
import org.session.libsignal.service.api.messages.SignalServiceDataMessage;
|
||||
import org.session.libsignal.utilities.guava.Optional;
|
||||
import org.session.libsignal.messages.SignalServiceAttachment;
|
||||
import org.session.libsignal.messages.SignalServiceDataMessage;
|
||||
import org.session.libsignal.utilities.Base64;
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos;
|
||||
import org.session.libsignal.protos.SignalServiceProtos;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
@@ -1,8 +1,8 @@
|
||||
package org.session.libsession.messaging.sending_receiving.attachments
|
||||
|
||||
import com.google.protobuf.ByteString
|
||||
import org.session.libsignal.libsignal.util.guava.Optional
|
||||
import org.session.libsignal.service.api.messages.SignalServiceAttachment
|
||||
import org.session.libsignal.utilities.guava.Optional
|
||||
import org.session.libsignal.messages.SignalServiceAttachment
|
||||
import java.io.InputStream
|
||||
|
||||
abstract class SessionServiceAttachment protected constructor(val contentType: String?) {
|
||||
|
@@ -5,12 +5,12 @@
|
||||
*/
|
||||
package org.session.libsession.messaging.sending_receiving.attachments
|
||||
|
||||
import org.session.libsignal.libsignal.util.guava.Optional
|
||||
import org.session.libsignal.utilities.guava.Optional
|
||||
|
||||
/**
|
||||
* Represents a received SignalServiceAttachment "handle." This
|
||||
* is a pointer to the actual attachment content, which needs to be
|
||||
* retrieved using [SignalServiceMessageReceiver.retrieveAttachment]
|
||||
* retrieved using SignalServiceMessageReceiver.retrieveAttachment
|
||||
*
|
||||
* @author Moxie Marlinspike
|
||||
*/
|
||||
|
@@ -7,9 +7,9 @@ package org.session.libsession.messaging.sending_receiving.attachments
|
||||
|
||||
import android.util.Size
|
||||
import com.google.protobuf.ByteString
|
||||
import org.session.libsignal.libsignal.util.guava.Optional
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos
|
||||
import org.session.libsignal.service.api.messages.SignalServiceAttachment as SAttachment
|
||||
import org.session.libsignal.utilities.guava.Optional
|
||||
import org.session.libsignal.protos.SignalServiceProtos
|
||||
import org.session.libsignal.messages.SignalServiceAttachment as SAttachment
|
||||
import java.io.InputStream
|
||||
import kotlin.math.round
|
||||
|
||||
|
@@ -10,7 +10,7 @@ import org.session.libsession.messaging.sending_receiving.attachments.Attachment
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentId;
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment;
|
||||
import org.session.libsignal.utilities.JsonUtil;
|
||||
import org.session.libsignal.libsignal.util.guava.Optional;
|
||||
import org.session.libsignal.utilities.guava.Optional;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
|
@@ -1,9 +1,7 @@
|
||||
package org.session.libsession.messaging.sending_receiving.notifications
|
||||
|
||||
import android.content.Context
|
||||
import org.session.libsession.messaging.threads.recipients.Recipient
|
||||
import org.session.libsignal.service.api.messages.SignalServiceGroup
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
|
||||
interface MessageNotifier {
|
||||
fun setVisibleThread(threadId: Long)
|
||||
|
@@ -8,9 +8,9 @@ import okhttp3.RequestBody
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.snode.OnionRequestAPI
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsignal.service.loki.utilities.retryIfNeeded
|
||||
import org.session.libsignal.utilities.retryIfNeeded
|
||||
import org.session.libsignal.utilities.JsonUtil
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
import org.session.libsignal.utilities.Log
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
object PushNotificationAPI {
|
||||
|
@@ -8,8 +8,8 @@ import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.messaging.jobs.JobQueue
|
||||
import org.session.libsession.messaging.jobs.MessageReceiveJob
|
||||
import org.session.libsession.snode.SnodeAPI
|
||||
import org.session.libsignal.service.loki.utilities.getRandomElementOrNull
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
import org.session.libsignal.crypto.getRandomElementOrNull
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.session.libsignal.utilities.successBackground
|
||||
|
||||
class ClosedGroupPoller {
|
||||
@@ -26,7 +26,7 @@ class ClosedGroupPoller {
|
||||
|
||||
// region Settings
|
||||
companion object {
|
||||
private val pollInterval: Long = 2 * 1000
|
||||
private val pollInterval: Long = 6 * 1000
|
||||
}
|
||||
// endregion
|
||||
|
||||
@@ -57,7 +57,8 @@ class ClosedGroupPoller {
|
||||
// region Private API
|
||||
private fun poll(): List<Promise<Unit, Exception>> {
|
||||
if (!isPolling) { return listOf() }
|
||||
val publicKeys = MessagingModuleConfiguration.shared.storage.getAllActiveClosedGroupPublicKeys()
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val publicKeys = storage.getAllActiveClosedGroupPublicKeys()
|
||||
return publicKeys.map { publicKey ->
|
||||
val promise = SnodeAPI.getSwarm(publicKey).bind { swarm ->
|
||||
val snode = swarm.getRandomElementOrNull() ?: throw InsufficientSnodesException() // Should be cryptographically secure
|
||||
@@ -65,19 +66,16 @@ class ClosedGroupPoller {
|
||||
SnodeAPI.getRawMessages(snode, publicKey).map {SnodeAPI.parseRawMessagesResponse(it, snode, publicKey) }
|
||||
}
|
||||
promise.successBackground { messages ->
|
||||
if (!MessagingModuleConfiguration.shared.storage.isGroupActive(publicKey)) {
|
||||
// ignore inactive group's messages
|
||||
return@successBackground
|
||||
}
|
||||
if (!storage.isGroupActive(publicKey)) { return@successBackground }
|
||||
messages.forEach { envelope ->
|
||||
val job = MessageReceiveJob(envelope.toByteArray(), false)
|
||||
val job = MessageReceiveJob(envelope.toByteArray())
|
||||
JobQueue.shared.add(job)
|
||||
}
|
||||
}
|
||||
promise.fail {
|
||||
Log.d("Loki", "Polling failed for closed group with public key: $publicKey due to error: $it.")
|
||||
}
|
||||
promise.map { Unit }
|
||||
promise.map { }
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
|
@@ -9,10 +9,10 @@ import org.session.libsession.messaging.jobs.MessageReceiveJob
|
||||
import org.session.libsession.messaging.open_groups.OpenGroup
|
||||
import org.session.libsession.messaging.open_groups.OpenGroupAPI
|
||||
import org.session.libsession.messaging.open_groups.OpenGroupMessage
|
||||
import org.session.libsession.messaging.threads.Address
|
||||
import org.session.libsession.utilities.Address
|
||||
import org.session.libsession.utilities.GroupUtil
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos.*
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
import org.session.libsignal.protos.SignalServiceProtos.*
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.session.libsignal.utilities.successBackground
|
||||
import java.util.*
|
||||
import java.util.concurrent.ScheduledExecutorService
|
||||
@@ -172,7 +172,7 @@ class OpenGroupPoller(private val openGroup: OpenGroup, private val executorServ
|
||||
builder.timestamp = message.timestamp
|
||||
builder.serverTimestamp = message.serverTimestamp
|
||||
val envelope = builder.build()
|
||||
val job = MessageReceiveJob(envelope.toByteArray(), isBackgroundPoll, messageServerID, openGroup.id)
|
||||
val job = MessageReceiveJob(envelope.toByteArray(), messageServerID, openGroup.id)
|
||||
Log.d("Loki", "Scheduling Job $job")
|
||||
if (isBackgroundPoll) {
|
||||
job.executeAsync().always { deferred.resolve(Unit) }
|
||||
|
@@ -0,0 +1,92 @@
|
||||
package org.session.libsession.messaging.sending_receiving.pollers
|
||||
|
||||
import nl.komponents.kovenant.Promise
|
||||
import nl.komponents.kovenant.functional.map
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.messaging.jobs.JobQueue
|
||||
import org.session.libsession.messaging.jobs.MessageReceiveJob
|
||||
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
|
||||
import org.session.libsession.messaging.open_groups.OpenGroupMessageV2
|
||||
import org.session.libsession.utilities.Address
|
||||
import org.session.libsession.utilities.GroupUtil
|
||||
import org.session.libsignal.protos.SignalServiceProtos
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.session.libsignal.utilities.successBackground
|
||||
import java.util.concurrent.ScheduledExecutorService
|
||||
import java.util.concurrent.ScheduledFuture
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class OpenGroupPollerV2(private val server: String, private val executorService: ScheduledExecutorService?) {
|
||||
var hasStarted = false
|
||||
private var future: ScheduledFuture<*>? = null
|
||||
|
||||
companion object {
|
||||
private val pollInterval: Long = 4 * 1000
|
||||
}
|
||||
|
||||
fun startIfNeeded() {
|
||||
if (hasStarted) { return }
|
||||
hasStarted = true
|
||||
future = executorService?.schedule(::poll, 0, TimeUnit.MILLISECONDS)
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
future?.cancel(false)
|
||||
hasStarted = false
|
||||
}
|
||||
|
||||
fun poll(isBackgroundPoll: Boolean = false): Promise<Unit, Exception> {
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val rooms = storage.getAllV2OpenGroups().values.filter { it.server == server }.map { it.room }
|
||||
return OpenGroupAPIV2.compactPoll(rooms, server).successBackground { responses ->
|
||||
responses.forEach { (room, response) ->
|
||||
val openGroupID = "$server.$room"
|
||||
handleNewMessages(openGroupID, response.messages, isBackgroundPoll)
|
||||
handleDeletedMessages(openGroupID, response.deletions)
|
||||
}
|
||||
}.always {
|
||||
executorService?.schedule(this@OpenGroupPollerV2::poll, OpenGroupPollerV2.pollInterval, TimeUnit.MILLISECONDS)
|
||||
}.map { }
|
||||
}
|
||||
|
||||
private fun handleNewMessages(openGroupID: String, messages: List<OpenGroupMessageV2>, isBackgroundPoll: Boolean) {
|
||||
if (!hasStarted) { return }
|
||||
messages.sortedBy { it.serverID!! }.forEach { message ->
|
||||
try {
|
||||
val senderPublicKey = message.sender!!
|
||||
val builder = SignalServiceProtos.Envelope.newBuilder()
|
||||
builder.type = SignalServiceProtos.Envelope.Type.SESSION_MESSAGE
|
||||
builder.source = senderPublicKey
|
||||
builder.sourceDevice = 1
|
||||
builder.content = message.toProto().toByteString()
|
||||
builder.timestamp = message.sentTimestamp
|
||||
val envelope = builder.build()
|
||||
val job = MessageReceiveJob(envelope.toByteArray(), message.serverID, openGroupID)
|
||||
if (isBackgroundPoll) {
|
||||
job.executeAsync()
|
||||
} else {
|
||||
JobQueue.shared.add(job)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("Loki", "Exception parsing message", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleDeletedMessages(openGroupID: String, deletedMessageServerIDs: List<Long>) {
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val dataProvider = MessagingModuleConfiguration.shared.messageDataProvider
|
||||
val groupID = GroupUtil.getEncodedOpenGroupID(openGroupID.toByteArray())
|
||||
val threadID = storage.getThreadIdFor(Address.fromSerialized(groupID)) ?: return
|
||||
val deletedMessageIDs = deletedMessageServerIDs.mapNotNull { serverID ->
|
||||
val messageID = dataProvider.getMessageID(serverID, threadID)
|
||||
if (messageID == null) {
|
||||
Log.d("Loki", "Couldn't find message ID for message with serverID: $serverID.")
|
||||
}
|
||||
messageID
|
||||
}
|
||||
deletedMessageIDs.forEach { (messageId, isSms) ->
|
||||
MessagingModuleConfiguration.shared.messageDataProvider.deleteMessage(messageId, isSms)
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,130 +0,0 @@
|
||||
package org.session.libsession.messaging.sending_receiving.pollers
|
||||
|
||||
import nl.komponents.kovenant.Promise
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.messaging.jobs.JobQueue
|
||||
import org.session.libsession.messaging.jobs.MessageReceiveJob
|
||||
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
|
||||
import org.session.libsession.messaging.open_groups.OpenGroupMessageV2
|
||||
import org.session.libsession.messaging.open_groups.OpenGroupV2
|
||||
import org.session.libsession.messaging.threads.Address
|
||||
import org.session.libsession.utilities.GroupUtil
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
import org.session.libsignal.utilities.successBackground
|
||||
import java.util.concurrent.ScheduledExecutorService
|
||||
import java.util.concurrent.ScheduledFuture
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class OpenGroupV2Poller(private val openGroups: List<OpenGroupV2>, private val executorService: ScheduledExecutorService? = null) {
|
||||
|
||||
private var hasStarted = false
|
||||
@Volatile private var isPollOngoing = false
|
||||
var isCaughtUp = false
|
||||
|
||||
private val cancellableFutures = mutableListOf<ScheduledFuture<out Any>>()
|
||||
|
||||
// use this as a receive time-based window to calculate re-poll interval
|
||||
private val receivedQueue = ArrayDeque<Long>(50)
|
||||
|
||||
private fun calculatePollInterval(): Long {
|
||||
// sample last default poll time * 2
|
||||
while (receivedQueue.size > 50) {
|
||||
receivedQueue.removeLast()
|
||||
}
|
||||
val sampleWindow = System.currentTimeMillis() - pollForNewMessagesInterval * 2
|
||||
val numberInSample = receivedQueue.toList().filter { it > sampleWindow }.size.coerceAtLeast(1)
|
||||
return ((2 + (50 / numberInSample / 20)*5) * 1000).toLong()
|
||||
}
|
||||
|
||||
// region Settings
|
||||
companion object {
|
||||
private val pollForNewMessagesInterval: Long = 10 * 1000
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Lifecycle
|
||||
fun startIfNeeded() {
|
||||
if (hasStarted || executorService == null) return
|
||||
cancellableFutures += executorService.schedule(::compactPoll, 0, TimeUnit.MILLISECONDS)
|
||||
hasStarted = true
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
cancellableFutures.forEach { future ->
|
||||
future.cancel(false)
|
||||
}
|
||||
cancellableFutures.clear()
|
||||
hasStarted = false
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Polling
|
||||
|
||||
private fun compactPoll(): Promise<Any, Exception> {
|
||||
return compactPoll(false)
|
||||
}
|
||||
|
||||
fun compactPoll(isBackgroundPoll: Boolean): Promise<Any, Exception> {
|
||||
if (isPollOngoing || !hasStarted) return Promise.of(Unit)
|
||||
isPollOngoing = true
|
||||
val server = openGroups.first().server // assume all the same server
|
||||
val rooms = openGroups.map { it.room }
|
||||
return OpenGroupAPIV2.getCompactPoll(rooms = rooms, server).successBackground { results ->
|
||||
results.forEach { (room, results) ->
|
||||
val serverRoomId = "$server.$room"
|
||||
handleDeletedMessages(serverRoomId,results.deletions)
|
||||
handleNewMessages(serverRoomId, results.messages.sortedBy { it.serverID }, isBackgroundPoll)
|
||||
}
|
||||
}.always {
|
||||
isPollOngoing = false
|
||||
if (!isBackgroundPoll) {
|
||||
val delay = calculatePollInterval()
|
||||
executorService?.schedule(this@OpenGroupV2Poller::compactPoll, delay, TimeUnit.MILLISECONDS)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleNewMessages(serverRoomId: String, newMessages: List<OpenGroupMessageV2>, isBackgroundPoll: Boolean) {
|
||||
if (!hasStarted) return
|
||||
newMessages.forEach { message ->
|
||||
try {
|
||||
val senderPublicKey = message.sender!!
|
||||
// Main message
|
||||
// Envelope
|
||||
val builder = SignalServiceProtos.Envelope.newBuilder()
|
||||
builder.type = SignalServiceProtos.Envelope.Type.SESSION_MESSAGE
|
||||
builder.source = senderPublicKey
|
||||
builder.sourceDevice = 1
|
||||
builder.content = message.toProto().toByteString()
|
||||
builder.timestamp = message.sentTimestamp
|
||||
val envelope = builder.build()
|
||||
val job = MessageReceiveJob(envelope.toByteArray(), isBackgroundPoll, message.serverID, serverRoomId)
|
||||
Log.d("Loki", "Scheduling Job $job")
|
||||
if (isBackgroundPoll) {
|
||||
job.executeAsync()
|
||||
// The promise is just used to keep track of when we're done
|
||||
} else {
|
||||
JobQueue.shared.add(job)
|
||||
}
|
||||
receivedQueue.addFirst(message.sentTimestamp)
|
||||
} catch (e: Exception) {
|
||||
Log.e("Loki", "Exception parsing message", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleDeletedMessages(serverRoomId: String, deletedMessageServerIDs: List<Long>) {
|
||||
val messagingModule = MessagingModuleConfiguration.shared
|
||||
val address = GroupUtil.getEncodedOpenGroupID(serverRoomId.toByteArray())
|
||||
val threadId = messagingModule.storage.getThreadIdFor(Address.fromSerialized(address)) ?: return
|
||||
|
||||
val deletedMessageIDs = deletedMessageServerIDs.mapNotNull { serverId ->
|
||||
messagingModule.messageDataProvider.getMessageID(serverId, threadId)
|
||||
}
|
||||
deletedMessageIDs.forEach { (messageId, isSms) ->
|
||||
MessagingModuleConfiguration.shared.messageDataProvider.deleteMessage(messageId, isSms)
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
}
|
@@ -7,8 +7,8 @@ import org.session.libsession.messaging.jobs.JobQueue
|
||||
import org.session.libsession.messaging.jobs.MessageReceiveJob
|
||||
import org.session.libsession.snode.SnodeAPI
|
||||
import org.session.libsession.snode.SnodeModule
|
||||
import org.session.libsignal.service.loki.Snode
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
import org.session.libsignal.utilities.Snode
|
||||
import org.session.libsignal.utilities.Log
|
||||
import java.security.SecureRandom
|
||||
import java.util.*
|
||||
|
||||
@@ -18,7 +18,7 @@ class Poller {
|
||||
var userPublicKey = MessagingModuleConfiguration.shared.storage.getUserPublicKey() ?: ""
|
||||
private var hasStarted: Boolean = false
|
||||
private val usedSnodes: MutableSet<Snode> = mutableSetOf()
|
||||
public var isCaughtUp = false
|
||||
var isCaughtUp = false
|
||||
|
||||
// region Settings
|
||||
companion object {
|
||||
@@ -92,7 +92,7 @@ class Poller {
|
||||
} else {
|
||||
val messages = SnodeAPI.parseRawMessagesResponse(rawResponse, snode, userPublicKey)
|
||||
messages.forEach { envelope ->
|
||||
val job = MessageReceiveJob(envelope.toByteArray(), false)
|
||||
val job = MessageReceiveJob(envelope.toByteArray())
|
||||
JobQueue.shared.add(job)
|
||||
}
|
||||
poll(snode, deferred)
|
||||
|
@@ -1,7 +1,7 @@
|
||||
package org.session.libsession.messaging.sending_receiving.quotes
|
||||
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.Attachment
|
||||
import org.session.libsession.messaging.threads.Address
|
||||
import org.session.libsession.utilities.Address
|
||||
|
||||
class QuoteModel(val id: Long,
|
||||
val author: Address,
|
||||
|
@@ -1,4 +1,4 @@
|
||||
package org.session.libsession.messaging.jobs;
|
||||
package org.session.libsession.messaging.utilities;
|
||||
|
||||
import android.os.Parcelable;
|
||||
|
||||
@@ -12,11 +12,7 @@ import org.session.libsession.utilities.ParcelableUtil;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
// Introduce a dedicated Map<String, byte[]> field specifically for parcelable needs.
|
||||
public class Data {
|
||||
|
||||
public static final Data EMPTY = new Data.Builder().build();
|
||||
|
||||
@JsonProperty private final Map<String, String> strings;
|
||||
@JsonProperty private final Map<String, String[]> stringArrays;
|
||||
@JsonProperty private final Map<String, Integer> integers;
|
||||
@@ -31,20 +27,23 @@ public class Data {
|
||||
@JsonProperty private final Map<String, boolean[]> booleanArrays;
|
||||
@JsonProperty private final Map<String, byte[]> byteArrays;
|
||||
|
||||
public Data(@JsonProperty("strings") @NonNull Map<String, String> strings,
|
||||
@JsonProperty("stringArrays") @NonNull Map<String, String[]> stringArrays,
|
||||
@JsonProperty("integers") @NonNull Map<String, Integer> integers,
|
||||
@JsonProperty("integerArrays") @NonNull Map<String, int[]> integerArrays,
|
||||
@JsonProperty("longs") @NonNull Map<String, Long> longs,
|
||||
@JsonProperty("longArrays") @NonNull Map<String, long[]> longArrays,
|
||||
@JsonProperty("floats") @NonNull Map<String, Float> floats,
|
||||
@JsonProperty("floatArrays") @NonNull Map<String, float[]> floatArrays,
|
||||
@JsonProperty("doubles") @NonNull Map<String, Double> doubles,
|
||||
@JsonProperty("doubleArrays") @NonNull Map<String, double[]> doubleArrays,
|
||||
@JsonProperty("booleans") @NonNull Map<String, Boolean> booleans,
|
||||
@JsonProperty("booleanArrays") @NonNull Map<String, boolean[]> booleanArrays,
|
||||
@JsonProperty("byteArrays") @NonNull Map<String, byte[]> byteArrays)
|
||||
{
|
||||
public static final Data EMPTY = new Data.Builder().build();
|
||||
|
||||
public Data(
|
||||
@JsonProperty("strings") @NonNull Map<String, String> strings,
|
||||
@JsonProperty("stringArrays") @NonNull Map<String, String[]> stringArrays,
|
||||
@JsonProperty("integers") @NonNull Map<String, Integer> integers,
|
||||
@JsonProperty("integerArrays") @NonNull Map<String, int[]> integerArrays,
|
||||
@JsonProperty("longs") @NonNull Map<String, Long> longs,
|
||||
@JsonProperty("longArrays") @NonNull Map<String, long[]> longArrays,
|
||||
@JsonProperty("floats") @NonNull Map<String, Float> floats,
|
||||
@JsonProperty("floatArrays") @NonNull Map<String, float[]> floatArrays,
|
||||
@JsonProperty("doubles") @NonNull Map<String, Double> doubles,
|
||||
@JsonProperty("doubleArrays") @NonNull Map<String, double[]> doubleArrays,
|
||||
@JsonProperty("booleans") @NonNull Map<String, Boolean> booleans,
|
||||
@JsonProperty("booleanArrays") @NonNull Map<String, boolean[]> booleanArrays,
|
||||
@JsonProperty("byteArrays") @NonNull Map<String, byte[]> byteArrays
|
||||
) {
|
||||
this.strings = strings;
|
||||
this.stringArrays = stringArrays;
|
||||
this.integers = integers;
|
||||
@@ -75,6 +74,7 @@ public class Data {
|
||||
}
|
||||
|
||||
|
||||
|
||||
public boolean hasStringArray(@NonNull String key) {
|
||||
return stringArrays.containsKey(key);
|
||||
}
|
||||
@@ -100,6 +100,7 @@ public class Data {
|
||||
}
|
||||
|
||||
|
||||
|
||||
public boolean hasIntegerArray(@NonNull String key) {
|
||||
return integerArrays.containsKey(key);
|
||||
}
|
||||
@@ -110,6 +111,7 @@ public class Data {
|
||||
}
|
||||
|
||||
|
||||
|
||||
public boolean hasLong(@NonNull String key) {
|
||||
return longs.containsKey(key);
|
||||
}
|
||||
@@ -125,6 +127,7 @@ public class Data {
|
||||
}
|
||||
|
||||
|
||||
|
||||
public boolean hasLongArray(@NonNull String key) {
|
||||
return longArrays.containsKey(key);
|
||||
}
|
||||
@@ -135,6 +138,7 @@ public class Data {
|
||||
}
|
||||
|
||||
|
||||
|
||||
public boolean hasFloat(@NonNull String key) {
|
||||
return floats.containsKey(key);
|
||||
}
|
||||
@@ -150,6 +154,7 @@ public class Data {
|
||||
}
|
||||
|
||||
|
||||
|
||||
public boolean hasFloatArray(@NonNull String key) {
|
||||
return floatArrays.containsKey(key);
|
||||
}
|
||||
@@ -160,6 +165,7 @@ public class Data {
|
||||
}
|
||||
|
||||
|
||||
|
||||
public boolean hasDouble(@NonNull String key) {
|
||||
return doubles.containsKey(key);
|
||||
}
|
||||
@@ -175,6 +181,7 @@ public class Data {
|
||||
}
|
||||
|
||||
|
||||
|
||||
public boolean hasDoubleArray(@NonNull String key) {
|
||||
return floatArrays.containsKey(key);
|
||||
}
|
||||
@@ -185,6 +192,7 @@ public class Data {
|
||||
}
|
||||
|
||||
|
||||
|
||||
public boolean hasBoolean(@NonNull String key) {
|
||||
return booleans.containsKey(key);
|
||||
}
|
||||
@@ -200,6 +208,7 @@ public class Data {
|
||||
}
|
||||
|
||||
|
||||
|
||||
public boolean hasBooleanArray(@NonNull String key) {
|
||||
return booleanArrays.containsKey(key);
|
||||
}
|
||||
@@ -209,6 +218,8 @@ public class Data {
|
||||
return booleanArrays.get(key);
|
||||
}
|
||||
|
||||
|
||||
|
||||
public boolean hasByteArray(@NonNull String key) {
|
||||
return byteArrays.containsKey(key);
|
||||
}
|
||||
@@ -218,6 +229,8 @@ public class Data {
|
||||
return byteArrays.get(key);
|
||||
}
|
||||
|
||||
|
||||
|
||||
public boolean hasParcelable(@NonNull String key) {
|
||||
return byteArrays.containsKey(key);
|
||||
}
|
||||
@@ -228,6 +241,8 @@ public class Data {
|
||||
return ParcelableUtil.unmarshall(bytes, creator);
|
||||
}
|
||||
|
||||
|
||||
|
||||
private void throwIfAbsent(@NonNull Map map, @NonNull String key) {
|
||||
if (!map.containsKey(key)) {
|
||||
throw new IllegalStateException("Tried to retrieve a value with key '" + key + "', but it wasn't present.");
|
||||
@@ -236,7 +251,6 @@ public class Data {
|
||||
|
||||
|
||||
public static class Builder {
|
||||
|
||||
private final Map<String, String> strings = new HashMap<>();
|
||||
private final Map<String, String[]> stringArrays = new HashMap<>();
|
||||
private final Map<String, Integer> integers = new HashMap<>();
|
||||
@@ -323,19 +337,21 @@ public class Data {
|
||||
}
|
||||
|
||||
public Data build() {
|
||||
return new Data(strings,
|
||||
stringArrays,
|
||||
integers,
|
||||
integerArrays,
|
||||
longs,
|
||||
longArrays,
|
||||
floats,
|
||||
floatArrays,
|
||||
doubles,
|
||||
doubleArrays,
|
||||
booleans,
|
||||
booleanArrays,
|
||||
byteArrays);
|
||||
return new Data(
|
||||
strings,
|
||||
stringArrays,
|
||||
integers,
|
||||
integerArrays,
|
||||
longs,
|
||||
longArrays,
|
||||
floats,
|
||||
floatArrays,
|
||||
doubles,
|
||||
doubleArrays,
|
||||
booleans,
|
||||
booleanArrays,
|
||||
byteArrays
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -343,5 +359,4 @@ public class Data {
|
||||
@NonNull String serialize(@NonNull Data data);
|
||||
@NonNull Data deserialize(@NonNull String serialized);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -10,26 +10,21 @@ import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.snode.OnionRequestAPI
|
||||
import org.session.libsession.messaging.file_server.FileServerAPI
|
||||
|
||||
import org.session.libsignal.utilities.DiffieHellman
|
||||
import org.session.libsignal.service.api.crypto.ProfileCipherOutputStream
|
||||
import org.session.libsignal.service.api.messages.SignalServiceAttachment
|
||||
import org.session.libsignal.service.api.push.exceptions.NonSuccessfulResponseCodeException
|
||||
import org.session.libsignal.service.api.push.exceptions.PushNetworkException
|
||||
import org.session.libsignal.service.api.util.StreamDetails
|
||||
import org.session.libsignal.service.internal.push.ProfileAvatarData
|
||||
import org.session.libsignal.service.internal.push.PushAttachmentData
|
||||
import org.session.libsignal.service.internal.push.http.DigestingRequestBody
|
||||
import org.session.libsignal.service.internal.push.http.ProfileCipherOutputStreamFactory
|
||||
import org.session.libsignal.crypto.DiffieHellman
|
||||
import org.session.libsignal.streams.ProfileCipherOutputStream
|
||||
import org.session.libsignal.exceptions.NonSuccessfulResponseCodeException
|
||||
import org.session.libsignal.exceptions.PushNetworkException
|
||||
import org.session.libsignal.streams.StreamDetails
|
||||
import org.session.libsignal.utilities.ProfileAvatarData
|
||||
import org.session.libsignal.utilities.PushAttachmentData
|
||||
import org.session.libsignal.streams.DigestingRequestBody
|
||||
import org.session.libsignal.streams.ProfileCipherOutputStreamFactory
|
||||
import org.session.libsignal.utilities.Hex
|
||||
import org.session.libsignal.utilities.JsonUtil
|
||||
import org.session.libsignal.service.loki.HTTP
|
||||
import org.session.libsignal.service.loki.utilities.*
|
||||
import org.session.libsignal.utilities.HTTP
|
||||
import org.session.libsignal.utilities.*
|
||||
import org.session.libsignal.utilities.Base64
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.OutputStream
|
||||
import org.session.libsignal.utilities.Log
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
@@ -214,7 +209,7 @@ open class DotNetAPI {
|
||||
fun uploadProfilePicture(server: String, key: ByteArray, profilePicture: StreamDetails, setLastProfilePictureUpload: () -> Unit): UploadResult {
|
||||
val profilePictureUploadData = ProfileAvatarData(profilePicture.stream, ProfileCipherOutputStream.getCiphertextLength(profilePicture.length), profilePicture.contentType, ProfileCipherOutputStreamFactory(key))
|
||||
val file = DigestingRequestBody(profilePictureUploadData.data, profilePictureUploadData.outputStreamFactory,
|
||||
profilePictureUploadData.contentType, profilePictureUploadData.dataLength, null)
|
||||
profilePictureUploadData.contentType, profilePictureUploadData.dataLength, null)
|
||||
val body = MultipartBody.Builder()
|
||||
.setType(MultipartBody.FORM)
|
||||
.addFormDataPart("type", "network.loki")
|
||||
|
@@ -1,11 +1,10 @@
|
||||
package org.session.libsession.messaging.utilities
|
||||
|
||||
import com.google.protobuf.ByteString
|
||||
import org.session.libsignal.metadata.SignalProtos
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos.Envelope
|
||||
import org.session.libsignal.service.internal.websocket.WebSocketProtos.WebSocketMessage
|
||||
import org.session.libsignal.service.internal.websocket.WebSocketProtos.WebSocketRequestMessage
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.session.libsignal.protos.SignalServiceProtos.Envelope
|
||||
import org.session.libsignal.protos.WebSocketProtos.WebSocketMessage
|
||||
import org.session.libsignal.protos.WebSocketProtos.WebSocketRequestMessage
|
||||
import java.security.SecureRandom
|
||||
|
||||
object MessageWrapper {
|
||||
|
@@ -6,9 +6,9 @@ import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.messaging.sending_receiving.data_extraction.DataExtractionNotificationInfoMessage
|
||||
import org.session.libsession.utilities.ExpirationUtil
|
||||
|
||||
object ClosedGroupUpdateMessageBuilder {
|
||||
object UpdateMessageBuilder {
|
||||
|
||||
fun buildGroupUpdateMessage(context: Context, updateMessageData: ClosedGroupUpdateMessageData, sender: String? = null, isOutgoing: Boolean = false): String {
|
||||
fun buildGroupUpdateMessage(context: Context, updateMessageData: UpdateMessageData, sender: String? = null, isOutgoing: Boolean = false): String {
|
||||
var message = ""
|
||||
val updateData = updateMessageData.kind ?: return message
|
||||
if (!isOutgoing && sender == null) return message
|
||||
@@ -17,21 +17,21 @@ object ClosedGroupUpdateMessageBuilder {
|
||||
} else { context.getString(R.string.MessageRecord_you) }
|
||||
|
||||
when (updateData) {
|
||||
is ClosedGroupUpdateMessageData.Kind.GroupCreation -> {
|
||||
is UpdateMessageData.Kind.GroupCreation -> {
|
||||
message = if (isOutgoing) {
|
||||
context.getString(R.string.MessageRecord_you_created_a_new_group)
|
||||
} else {
|
||||
context.getString(R.string.MessageRecord_s_added_you_to_the_group, senderName)
|
||||
}
|
||||
}
|
||||
is ClosedGroupUpdateMessageData.Kind.GroupNameChange -> {
|
||||
is UpdateMessageData.Kind.GroupNameChange -> {
|
||||
message = if (isOutgoing) {
|
||||
context.getString(R.string.MessageRecord_you_renamed_the_group_to_s, updateData.name)
|
||||
} else {
|
||||
context.getString(R.string.MessageRecord_s_renamed_the_group_to_s, senderName, updateData.name)
|
||||
}
|
||||
}
|
||||
is ClosedGroupUpdateMessageData.Kind.GroupMemberAdded -> {
|
||||
is UpdateMessageData.Kind.GroupMemberAdded -> {
|
||||
val members = updateData.updatedMembers.joinToString(", ") {
|
||||
MessagingModuleConfiguration.shared.storage.getDisplayNameForRecipient(it) ?: it
|
||||
}
|
||||
@@ -41,7 +41,7 @@ object ClosedGroupUpdateMessageBuilder {
|
||||
context.getString(R.string.MessageRecord_s_added_s_to_the_group, senderName, members)
|
||||
}
|
||||
}
|
||||
is ClosedGroupUpdateMessageData.Kind.GroupMemberRemoved -> {
|
||||
is UpdateMessageData.Kind.GroupMemberRemoved -> {
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val userPublicKey = storage.getUserPublicKey()!!
|
||||
// 1st case: you are part of the removed members
|
||||
@@ -63,7 +63,7 @@ object ClosedGroupUpdateMessageBuilder {
|
||||
}
|
||||
}
|
||||
}
|
||||
is ClosedGroupUpdateMessageData.Kind.GroupMemberLeft -> {
|
||||
is UpdateMessageData.Kind.GroupMemberLeft -> {
|
||||
message = if (isOutgoing) {
|
||||
context.getString(R.string.MessageRecord_left_group)
|
||||
} else {
|
@@ -3,13 +3,13 @@ package org.session.libsession.messaging.utilities
|
||||
import com.fasterxml.jackson.annotation.JsonSubTypes
|
||||
import com.fasterxml.jackson.annotation.JsonTypeInfo
|
||||
import com.fasterxml.jackson.core.JsonParseException
|
||||
import org.session.libsignal.service.api.messages.SignalServiceGroup
|
||||
import org.session.libsignal.messages.SignalServiceGroup
|
||||
import org.session.libsignal.utilities.JsonUtil
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
import org.session.libsignal.utilities.Log
|
||||
import java.util.*
|
||||
|
||||
// class used to save update messages details
|
||||
class ClosedGroupUpdateMessageData () {
|
||||
class UpdateMessageData () {
|
||||
|
||||
var kind: Kind? = null
|
||||
|
||||
@@ -20,7 +20,8 @@ class ClosedGroupUpdateMessageData () {
|
||||
JsonSubTypes.Type(Kind.GroupNameChange::class, name = "GroupNameChange"),
|
||||
JsonSubTypes.Type(Kind.GroupMemberAdded::class, name = "GroupMemberAdded"),
|
||||
JsonSubTypes.Type(Kind.GroupMemberRemoved::class, name = "GroupMemberRemoved"),
|
||||
JsonSubTypes.Type(Kind.GroupMemberLeft::class, name = "GroupMemberLeft")
|
||||
JsonSubTypes.Type(Kind.GroupMemberLeft::class, name = "GroupMemberLeft"),
|
||||
JsonSubTypes.Type(Kind.OpenGroupInvitation::class, name = "OpenGroupInvitation")
|
||||
)
|
||||
sealed class Kind() {
|
||||
class GroupCreation(): Kind()
|
||||
@@ -34,6 +35,9 @@ class ClosedGroupUpdateMessageData () {
|
||||
constructor(): this(Collections.emptyList())
|
||||
}
|
||||
class GroupMemberLeft(): Kind()
|
||||
class OpenGroupInvitation(val groupUrl: String, val groupName: String): Kind() {
|
||||
constructor(): this("", "")
|
||||
}
|
||||
}
|
||||
|
||||
constructor(kind: Kind): this() {
|
||||
@@ -41,22 +45,26 @@ class ClosedGroupUpdateMessageData () {
|
||||
}
|
||||
|
||||
companion object {
|
||||
val TAG = ClosedGroupUpdateMessageData::class.simpleName
|
||||
val TAG = UpdateMessageData::class.simpleName
|
||||
|
||||
fun buildGroupUpdate(type: SignalServiceGroup.Type, name: String, members: Collection<String>): ClosedGroupUpdateMessageData? {
|
||||
fun buildGroupUpdate(type: SignalServiceGroup.Type, name: String, members: Collection<String>): UpdateMessageData? {
|
||||
return when(type) {
|
||||
SignalServiceGroup.Type.CREATION -> ClosedGroupUpdateMessageData(Kind.GroupCreation())
|
||||
SignalServiceGroup.Type.NAME_CHANGE -> ClosedGroupUpdateMessageData(Kind.GroupNameChange(name))
|
||||
SignalServiceGroup.Type.MEMBER_ADDED -> ClosedGroupUpdateMessageData(Kind.GroupMemberAdded(members))
|
||||
SignalServiceGroup.Type.MEMBER_REMOVED -> ClosedGroupUpdateMessageData(Kind.GroupMemberRemoved(members))
|
||||
SignalServiceGroup.Type.QUIT -> ClosedGroupUpdateMessageData(Kind.GroupMemberLeft())
|
||||
SignalServiceGroup.Type.CREATION -> UpdateMessageData(Kind.GroupCreation())
|
||||
SignalServiceGroup.Type.NAME_CHANGE -> UpdateMessageData(Kind.GroupNameChange(name))
|
||||
SignalServiceGroup.Type.MEMBER_ADDED -> UpdateMessageData(Kind.GroupMemberAdded(members))
|
||||
SignalServiceGroup.Type.MEMBER_REMOVED -> UpdateMessageData(Kind.GroupMemberRemoved(members))
|
||||
SignalServiceGroup.Type.QUIT -> UpdateMessageData(Kind.GroupMemberLeft())
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
fun fromJSON(json: String): ClosedGroupUpdateMessageData? {
|
||||
fun buildOpenGroupInvitation(url: String, name: String): UpdateMessageData {
|
||||
return UpdateMessageData(Kind.OpenGroupInvitation(url, name))
|
||||
}
|
||||
|
||||
fun fromJSON(json: String): UpdateMessageData? {
|
||||
return try {
|
||||
JsonUtil.fromJson(json, ClosedGroupUpdateMessageData::class.java)
|
||||
JsonUtil.fromJson(json, UpdateMessageData::class.java)
|
||||
} catch (e: JsonParseException) {
|
||||
Log.e(TAG, "${e.message}")
|
||||
null
|
@@ -8,18 +8,18 @@ import nl.komponents.kovenant.functional.map
|
||||
import okhttp3.Request
|
||||
import org.session.libsession.messaging.file_server.FileServerAPI
|
||||
import org.session.libsession.utilities.AESGCM
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.session.libsignal.utilities.Base64
|
||||
import org.session.libsignal.utilities.*
|
||||
import org.session.libsignal.service.loki.Snode
|
||||
import org.session.libsignal.service.loki.*
|
||||
import org.session.libsignal.utilities.Snode
|
||||
import org.session.libsession.utilities.AESGCM.EncryptionResult
|
||||
import org.session.libsession.utilities.getBodyForOnionRequest
|
||||
import org.session.libsession.utilities.getHeadersForOnionRequest
|
||||
import org.session.libsignal.service.loki.Broadcaster
|
||||
import org.session.libsignal.service.loki.HTTP
|
||||
import org.session.libsignal.service.loki.LokiAPIDatabaseProtocol
|
||||
import org.session.libsignal.service.loki.utilities.*
|
||||
import org.session.libsignal.crypto.getRandomElement
|
||||
import org.session.libsignal.crypto.getRandomElementOrNull
|
||||
import org.session.libsignal.utilities.Broadcaster
|
||||
import org.session.libsignal.utilities.HTTP
|
||||
import org.session.libsignal.database.LokiAPIDatabaseProtocol
|
||||
|
||||
private typealias Path = List<Snode>
|
||||
|
||||
@@ -53,11 +53,11 @@ object OnionRequestAPI {
|
||||
/**
|
||||
* The number of times a path can fail before it's replaced.
|
||||
*/
|
||||
private const val pathFailureThreshold = 1
|
||||
private const val pathFailureThreshold = 3
|
||||
/**
|
||||
* The number of times a snode can fail before it's replaced.
|
||||
*/
|
||||
private const val snodeFailureThreshold = 1
|
||||
private const val snodeFailureThreshold = 3
|
||||
/**
|
||||
* The number of guard snodes required to maintain `targetPathCount` paths.
|
||||
*/
|
||||
@@ -74,13 +74,13 @@ object OnionRequestAPI {
|
||||
class InsufficientSnodesException : Exception("Couldn't find enough snodes to build a path.")
|
||||
|
||||
private data class OnionBuildingResult(
|
||||
val guardSnode: Snode,
|
||||
val finalEncryptionResult: EncryptionResult,
|
||||
val destinationSymmetricKey: ByteArray
|
||||
val guardSnode: Snode,
|
||||
val finalEncryptionResult: EncryptionResult,
|
||||
val destinationSymmetricKey: ByteArray
|
||||
)
|
||||
|
||||
internal sealed class Destination {
|
||||
class Snode(val snode: org.session.libsignal.service.loki.Snode) : Destination()
|
||||
class Snode(val snode: org.session.libsignal.utilities.Snode) : Destination()
|
||||
class Server(val host: String, val target: String, val x25519PublicKey: String, val scheme: String, val port: Int) : Destination()
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@ object OnionRequestAPI {
|
||||
ThreadUtils.queue { // No need to block the shared context for this
|
||||
val url = "${snode.address}:${snode.port}/get_stats/v1"
|
||||
try {
|
||||
val json = HTTP.execute(HTTP.Verb.GET, url)
|
||||
val json = HTTP.execute(HTTP.Verb.GET, url, 3)
|
||||
val version = json["version"] as? String
|
||||
if (version == null) { deferred.reject(Exception("Missing snode version.")); return@queue }
|
||||
if (version >= "2.0.7") {
|
||||
@@ -463,7 +463,6 @@ object OnionRequestAPI {
|
||||
"method" to request.method(),
|
||||
"headers" to headers
|
||||
)
|
||||
url.isHttps
|
||||
val destination = Destination.Server(host, target, x25519PublicKey, url.scheme(), url.port())
|
||||
return sendOnionRequest(destination, payload, isJSONRequired).recover { exception ->
|
||||
Log.d("Loki", "Couldn't reach server: $urlAsString due to error: $exception.")
|
||||
|
@@ -4,7 +4,7 @@ import nl.komponents.kovenant.Promise
|
||||
import nl.komponents.kovenant.deferred
|
||||
import org.session.libsession.utilities.AESGCM
|
||||
import org.session.libsession.utilities.AESGCM.EncryptionResult
|
||||
import org.session.libsignal.service.loki.utilities.toHexString
|
||||
import org.session.libsignal.utilities.toHexString
|
||||
import org.session.libsignal.utilities.JsonUtil
|
||||
import org.session.libsignal.utilities.ThreadUtils
|
||||
import java.nio.Buffer
|
||||
@@ -71,11 +71,11 @@ object OnionRequestEncryption {
|
||||
}
|
||||
is OnionRequestAPI.Destination.Server -> {
|
||||
payload = mutableMapOf(
|
||||
"host" to rhs.host,
|
||||
"target" to rhs.target,
|
||||
"method" to "POST",
|
||||
"protocol" to rhs.scheme,
|
||||
"port" to rhs.port
|
||||
"host" to rhs.host,
|
||||
"target" to rhs.target,
|
||||
"method" to "POST",
|
||||
"protocol" to rhs.scheme,
|
||||
"port" to rhs.port
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@@ -7,17 +7,17 @@ import nl.komponents.kovenant.*
|
||||
import nl.komponents.kovenant.functional.bind
|
||||
import nl.komponents.kovenant.functional.map
|
||||
import org.session.libsession.messaging.utilities.MessageWrapper
|
||||
import org.session.libsession.snode.utilities.getRandomElement
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos
|
||||
import org.session.libsignal.service.loki.Snode
|
||||
import org.session.libsignal.service.loki.HTTP
|
||||
import org.session.libsignal.service.loki.LokiAPIDatabaseProtocol
|
||||
import org.session.libsignal.service.loki.Broadcaster
|
||||
import org.session.libsignal.service.loki.utilities.prettifiedDescription
|
||||
import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded
|
||||
import org.session.libsignal.service.loki.utilities.retryIfNeeded
|
||||
import org.session.libsignal.crypto.getRandomElement
|
||||
import org.session.libsignal.protos.SignalServiceProtos
|
||||
import org.session.libsignal.utilities.Snode
|
||||
import org.session.libsignal.utilities.HTTP
|
||||
import org.session.libsignal.database.LokiAPIDatabaseProtocol
|
||||
import org.session.libsignal.utilities.Broadcaster
|
||||
import org.session.libsignal.utilities.prettifiedDescription
|
||||
import org.session.libsignal.utilities.removing05PrefixIfNeeded
|
||||
import org.session.libsignal.utilities.retryIfNeeded
|
||||
import org.session.libsignal.utilities.*
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
import org.session.libsignal.utilities.Log
|
||||
import java.security.SecureRandom
|
||||
|
||||
object SnodeAPI {
|
||||
@@ -33,8 +33,8 @@ object SnodeAPI {
|
||||
|
||||
// Settings
|
||||
private val maxRetryCount = 6
|
||||
private val minimumSnodePoolCount = 24
|
||||
private val minimumSwarmSnodeCount = 2
|
||||
private val minimumSnodePoolCount = 12
|
||||
private val minimumSwarmSnodeCount = 3
|
||||
// Use port 4433 if the API level can handle the network security configuration and enforce pinned certificates
|
||||
private val seedNodePort = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) 443 else 4433
|
||||
private val seedNodePool by lazy {
|
||||
@@ -44,7 +44,7 @@ object SnodeAPI {
|
||||
setOf( "https://storage.seed1.loki.network:$seedNodePort ", "https://storage.seed3.loki.network:$seedNodePort ", "https://public.loki.foundation:$seedNodePort" )
|
||||
}
|
||||
}
|
||||
private val snodeFailureThreshold = 4
|
||||
private val snodeFailureThreshold = 3
|
||||
private val targetSwarmSnodeCount = 2
|
||||
private val useOnionRequests = true
|
||||
|
||||
@@ -92,6 +92,7 @@ object SnodeAPI {
|
||||
"method" to "get_n_service_nodes",
|
||||
"params" to mapOf(
|
||||
"active_only" to true,
|
||||
"limit" to 256,
|
||||
"fields" to mapOf( "public_ip" to true, "storage_port" to true, "pubkey_x25519" to true, "pubkey_ed25519" to true )
|
||||
)
|
||||
)
|
||||
@@ -251,19 +252,20 @@ object SnodeAPI {
|
||||
|
||||
private fun removeDuplicates(publicKey: String, rawMessages: List<*>): List<*> {
|
||||
val receivedMessageHashValues = database.getReceivedMessageHashValues(publicKey)?.toMutableSet() ?: mutableSetOf()
|
||||
return rawMessages.filter { rawMessage ->
|
||||
val result = rawMessages.filter { rawMessage ->
|
||||
val rawMessageAsJSON = rawMessage as? Map<*, *>
|
||||
val hashValue = rawMessageAsJSON?.get("hash") as? String
|
||||
if (hashValue != null) {
|
||||
val isDuplicate = receivedMessageHashValues.contains(hashValue)
|
||||
receivedMessageHashValues.add(hashValue)
|
||||
database.setReceivedMessageHashValues(publicKey, receivedMessageHashValues)
|
||||
!isDuplicate
|
||||
} else {
|
||||
Log.d("Loki", "Missing hash value for message: ${rawMessage?.prettifiedDescription()}.")
|
||||
false
|
||||
}
|
||||
}
|
||||
database.setReceivedMessageHashValues(publicKey, receivedMessageHashValues)
|
||||
return result
|
||||
}
|
||||
|
||||
private fun parseEnvelopes(rawMessages: List<*>): List<SignalServiceProtos.Envelope> {
|
||||
@@ -304,7 +306,7 @@ object SnodeAPI {
|
||||
}
|
||||
}
|
||||
when (statusCode) {
|
||||
400, 500, 503 -> { // Usually indicates that the snode isn't up to date
|
||||
400, 500, 502, 503 -> { // Usually indicates that the snode isn't up to date
|
||||
handleBadSnode()
|
||||
}
|
||||
406 -> {
|
||||
@@ -315,8 +317,20 @@ object SnodeAPI {
|
||||
421 -> {
|
||||
// The snode isn't associated with the given public key anymore
|
||||
if (publicKey != null) {
|
||||
Log.d("Loki", "Invalidating swarm for: $publicKey.")
|
||||
dropSnodeFromSwarmIfNeeded(snode, publicKey)
|
||||
fun invalidateSwarm() {
|
||||
Log.d("Loki", "Invalidating swarm for: $publicKey.")
|
||||
dropSnodeFromSwarmIfNeeded(snode, publicKey)
|
||||
}
|
||||
if (json != null) {
|
||||
val snodes = parseSnodes(json)
|
||||
if (snodes.isNotEmpty()) {
|
||||
database.setSwarm(publicKey, snodes.toSet())
|
||||
} else {
|
||||
invalidateSwarm()
|
||||
}
|
||||
} else {
|
||||
invalidateSwarm()
|
||||
}
|
||||
} else {
|
||||
Log.d("Loki", "Got a 421 without an associated public key.")
|
||||
}
|
||||
|
@@ -1,25 +1,35 @@
|
||||
package org.session.libsession.snode
|
||||
|
||||
import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded
|
||||
import org.session.libsignal.utilities.removing05PrefixIfNeeded
|
||||
|
||||
data class SnodeMessage(
|
||||
// The hex encoded public key of the recipient.
|
||||
/**
|
||||
* The hex encoded public key of the recipient.
|
||||
*/
|
||||
val recipient: String,
|
||||
// The content of the message.
|
||||
/**
|
||||
* The content of the message.
|
||||
*/
|
||||
val data: String,
|
||||
// The time to live for the message in milliseconds.
|
||||
/**
|
||||
* The time to live for the message in milliseconds.
|
||||
*/
|
||||
val ttl: Long,
|
||||
// When the proof of work was calculated.
|
||||
/**
|
||||
* When the proof of work was calculated.
|
||||
*
|
||||
* **Note:** Expressed as milliseconds since 00:00:00 UTC on 1 January 1970.
|
||||
*/
|
||||
val timestamp: Long
|
||||
) {
|
||||
|
||||
internal fun toJSON(): Map<String, String> {
|
||||
return mapOf(
|
||||
"pubKey" to if (SnodeAPI.useTestnet) recipient.removing05PrefixIfNeeded() else recipient,
|
||||
"data" to data,
|
||||
"ttl" to ttl.toString(),
|
||||
"timestamp" to timestamp.toString(),
|
||||
"nonce" to ""
|
||||
"data" to data,
|
||||
"ttl" to ttl.toString(),
|
||||
"timestamp" to timestamp.toString(),
|
||||
"nonce" to ""
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@@ -1,7 +1,7 @@
|
||||
package org.session.libsession.snode
|
||||
|
||||
import org.session.libsignal.service.loki.LokiAPIDatabaseProtocol
|
||||
import org.session.libsignal.service.loki.Broadcaster
|
||||
import org.session.libsignal.database.LokiAPIDatabaseProtocol
|
||||
import org.session.libsignal.utilities.Broadcaster
|
||||
|
||||
class SnodeModule(val storage: LokiAPIDatabaseProtocol, val broadcaster: Broadcaster) {
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
package org.session.libsession.snode
|
||||
|
||||
import org.session.libsignal.service.loki.Snode
|
||||
import org.session.libsignal.utilities.Snode
|
||||
|
||||
interface SnodeStorageProtocol {
|
||||
|
||||
|
@@ -1,18 +0,0 @@
|
||||
package org.session.libsession.snode.utilities
|
||||
|
||||
import java.security.SecureRandom
|
||||
|
||||
/**
|
||||
* Uses `SecureRandom` to pick an element from this collection.
|
||||
*/
|
||||
fun <T> Collection<T>.getRandomElementOrNull(): T? {
|
||||
val index = SecureRandom().nextInt(size) // SecureRandom() should be cryptographically secure
|
||||
return elementAtOrNull(index)
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses `SecureRandom` to pick an element from this collection.
|
||||
*/
|
||||
fun <T> Collection<T>.getRandomElement(): T {
|
||||
return getRandomElementOrNull()!!
|
||||
}
|
@@ -1,8 +1,8 @@
|
||||
package org.session.libsession.utilities
|
||||
|
||||
import androidx.annotation.WorkerThread
|
||||
import org.session.libsignal.libsignal.util.ByteUtil
|
||||
import org.session.libsignal.service.internal.util.Util
|
||||
import org.session.libsignal.utilities.ByteUtil
|
||||
import org.session.libsignal.utilities.Util
|
||||
import org.session.libsignal.utilities.Hex
|
||||
import org.whispersystems.curve25519.Curve25519
|
||||
import javax.crypto.Cipher
|
||||
@@ -12,16 +12,15 @@ import javax.crypto.spec.SecretKeySpec
|
||||
|
||||
@WorkerThread
|
||||
internal object AESGCM {
|
||||
|
||||
internal data class EncryptionResult(
|
||||
internal val ciphertext: ByteArray,
|
||||
internal val symmetricKey: ByteArray,
|
||||
internal val ephemeralPublicKey: ByteArray
|
||||
)
|
||||
|
||||
internal val gcmTagSize = 128
|
||||
internal val ivSize = 12
|
||||
|
||||
internal data class EncryptionResult(
|
||||
internal val ciphertext: ByteArray,
|
||||
internal val symmetricKey: ByteArray,
|
||||
internal val ephemeralPublicKey: ByteArray
|
||||
)
|
||||
|
||||
/**
|
||||
* Sync. Don't call from the main thread.
|
||||
*/
|
||||
|
@@ -1,4 +1,4 @@
|
||||
package org.session.libsession.messaging.threads
|
||||
package org.session.libsession.utilities
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Parcel
|
||||
@@ -7,8 +7,8 @@ import android.util.Pair
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import org.session.libsession.utilities.DelimiterUtil
|
||||
import org.session.libsession.utilities.GroupUtil
|
||||
import org.session.libsignal.libsignal.util.guava.Optional
|
||||
import org.session.libsignal.service.internal.util.Util
|
||||
import org.session.libsignal.utilities.guava.Optional
|
||||
import org.session.libsignal.utilities.Util
|
||||
import java.util.*
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
import java.util.regex.Matcher
|
||||
@@ -25,8 +25,6 @@ class Address private constructor(address: String) : Parcelable, Comparable<Addr
|
||||
get() = GroupUtil.isClosedGroup(address)
|
||||
val isOpenGroup: Boolean
|
||||
get() = GroupUtil.isOpenGroup(address)
|
||||
val isMmsGroup: Boolean
|
||||
get() = GroupUtil.isMmsGroup(address)
|
||||
val isContact: Boolean
|
||||
get() = !isGroup
|
||||
|
@@ -1,12 +1,10 @@
|
||||
package org.session.libsession.utilities.color.spans;
|
||||
|
||||
package org.session.libsession.utilities;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import android.text.TextPaint;
|
||||
import android.text.style.MetricAffectingSpan;
|
||||
|
||||
public class CenterAlignedRelativeSizeSpan extends MetricAffectingSpan {
|
||||
|
||||
private final float relativeSize;
|
||||
|
||||
public CenterAlignedRelativeSizeSpan(float relativeSize) {
|
@@ -1,4 +1,4 @@
|
||||
package org.session.libsession.messaging.sending_receiving.sharecontacts;
|
||||
package org.session.libsession.utilities;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.os.Parcel;
|
||||
@@ -17,7 +17,6 @@ import org.session.libsession.messaging.sending_receiving.attachments.Attachment
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentId;
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.UriAttachment;
|
||||
import org.session.libsignal.utilities.JsonUtil;
|
||||
import org.session.libsession.utilities.MediaTypes;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
@@ -1,4 +1,4 @@
|
||||
package org.session.libsession.messaging.threads
|
||||
package org.session.libsession.utilities
|
||||
|
||||
object DistributionTypes {
|
||||
const val DEFAULT = 2
|
@@ -1,4 +1,4 @@
|
||||
package org.session.libsession.database.documents;
|
||||
package org.session.libsession.utilities;
|
||||
|
||||
import java.util.List;
|
||||
|
@@ -5,11 +5,10 @@ import okhttp3.Request
|
||||
import org.session.libsession.messaging.file_server.FileServerAPI
|
||||
import org.session.libsession.messaging.file_server.FileServerAPIV2
|
||||
import org.session.libsession.snode.OnionRequestAPI
|
||||
import org.session.libsignal.service.api.crypto.AttachmentCipherInputStream
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
import org.session.libsignal.service.api.messages.SignalServiceAttachment
|
||||
import org.session.libsignal.service.api.push.exceptions.NonSuccessfulResponseCodeException
|
||||
import org.session.libsignal.service.api.push.exceptions.PushNetworkException
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.session.libsignal.messages.SignalServiceAttachment
|
||||
import org.session.libsignal.exceptions.NonSuccessfulResponseCodeException
|
||||
import org.session.libsignal.exceptions.PushNetworkException
|
||||
import org.session.libsignal.utilities.Base64
|
||||
import java.io.*
|
||||
|
||||
@@ -42,11 +41,12 @@ object DownloadUtilities {
|
||||
@JvmStatic
|
||||
fun downloadFile(outputStream: OutputStream, url: String, maxSize: Int, listener: SignalServiceAttachment.ProgressListener?) {
|
||||
|
||||
if (url.contains(FileServerAPIV2.DEFAULT_SERVER)) {
|
||||
if (url.contains(FileServerAPIV2.SERVER) || url.contains(FileServerAPIV2.OLD_SERVER)) {
|
||||
val httpUrl = HttpUrl.parse(url)!!
|
||||
val fileId = httpUrl.pathSegments().last()
|
||||
val useOldServer = url.contains(FileServerAPIV2.OLD_SERVER)
|
||||
try {
|
||||
FileServerAPIV2.download(fileId.toLong()).get().let {
|
||||
FileServerAPIV2.download(fileId.toLong(), useOldServer).get().let {
|
||||
outputStream.write(it)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
|
@@ -1,15 +1,15 @@
|
||||
package org.session.libsession.messaging.threads
|
||||
package org.session.libsession.utilities
|
||||
|
||||
import android.text.TextUtils
|
||||
import org.session.libsession.utilities.GroupUtil
|
||||
import org.session.libsession.utilities.Address
|
||||
import java.io.IOException
|
||||
import java.util.*
|
||||
|
||||
class GroupRecord(
|
||||
val encodedId: String, val title: String, members: String?, val avatar: ByteArray?,
|
||||
val avatarId: Long?, val avatarKey: ByteArray?, val avatarContentType: String?,
|
||||
val relay: String?, val isActive: Boolean, val avatarDigest: ByteArray?, val isMms: Boolean,
|
||||
val url: String?, admins: String?, val formationTimestamp: Long
|
||||
val encodedId: String, val title: String, members: String?, val avatar: ByteArray?,
|
||||
val avatarId: Long?, val avatarKey: ByteArray?, val avatarContentType: String?,
|
||||
val relay: String?, val isActive: Boolean, val avatarDigest: ByteArray?, val isMms: Boolean,
|
||||
val url: String?, admins: String?, val formationTimestamp: Long
|
||||
) {
|
||||
var members: List<Address> = LinkedList<Address>()
|
||||
var admins: List<Address> = LinkedList<Address>()
|
@@ -1,13 +1,12 @@
|
||||
package org.session.libsession.utilities
|
||||
|
||||
import org.session.libsignal.service.api.messages.SignalServiceGroup
|
||||
import org.session.libsignal.messages.SignalServiceGroup
|
||||
import org.session.libsignal.utilities.Hex
|
||||
import java.io.IOException
|
||||
import kotlin.jvm.Throws
|
||||
|
||||
object GroupUtil {
|
||||
const val CLOSED_GROUP_PREFIX = "__textsecure_group__!"
|
||||
const val MMS_GROUP_PREFIX = "__signal_mms_group__!"
|
||||
const val OPEN_GROUP_PREFIX = "__loki_public_chat_group__!"
|
||||
|
||||
@JvmStatic
|
||||
@@ -20,11 +19,6 @@ object GroupUtil {
|
||||
return CLOSED_GROUP_PREFIX + Hex.toStringCondensed(groupID)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getEncodedMMSGroupID(groupID: ByteArray): String {
|
||||
return MMS_GROUP_PREFIX + Hex.toStringCondensed(groupID)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getEncodedId(group: SignalServiceGroup): String {
|
||||
val groupId = group.groupId
|
||||
@@ -52,12 +46,7 @@ object GroupUtil {
|
||||
}
|
||||
|
||||
fun isEncodedGroup(groupId: String): Boolean {
|
||||
return groupId.startsWith(CLOSED_GROUP_PREFIX) || groupId.startsWith(MMS_GROUP_PREFIX) || groupId.startsWith(OPEN_GROUP_PREFIX)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun isMmsGroup(groupId: String): Boolean {
|
||||
return groupId.startsWith(MMS_GROUP_PREFIX)
|
||||
return groupId.startsWith(CLOSED_GROUP_PREFIX) || groupId.startsWith(OPEN_GROUP_PREFIX)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
|
@@ -1,6 +1,6 @@
|
||||
package org.session.libsession.database.documents;
|
||||
package org.session.libsession.utilities;
|
||||
|
||||
import org.session.libsignal.utilities.logging.Log;
|
||||
import org.session.libsignal.utilities.Log;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
@@ -13,10 +13,10 @@ import com.fasterxml.jackson.databind.SerializerProvider;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
|
||||
import org.session.libsession.messaging.threads.Address;
|
||||
import org.session.libsession.utilities.Address;
|
||||
import org.session.libsignal.utilities.Base64;
|
||||
import org.session.libsignal.libsignal.IdentityKey;
|
||||
import org.session.libsignal.libsignal.InvalidKeyException;
|
||||
import org.session.libsignal.crypto.IdentityKey;
|
||||
import org.session.libsignal.exceptions.InvalidKeyException;
|
||||
|
||||
import java.io.IOException;
|
||||
|
@@ -1,4 +1,4 @@
|
||||
package org.session.libsession.database.documents;
|
||||
package org.session.libsession.utilities;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
@@ -22,13 +22,13 @@ import android.content.SharedPreferences;
|
||||
import android.content.SharedPreferences.Editor;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.session.libsignal.libsignal.ecc.ECPublicKey;
|
||||
import org.session.libsignal.libsignal.IdentityKey;
|
||||
import org.session.libsignal.libsignal.IdentityKeyPair;
|
||||
import org.session.libsignal.libsignal.InvalidKeyException;
|
||||
import org.session.libsignal.libsignal.ecc.Curve;
|
||||
import org.session.libsignal.libsignal.ecc.ECKeyPair;
|
||||
import org.session.libsignal.libsignal.ecc.ECPrivateKey;
|
||||
import org.session.libsignal.crypto.ecc.ECPublicKey;
|
||||
import org.session.libsignal.crypto.IdentityKey;
|
||||
import org.session.libsignal.crypto.IdentityKeyPair;
|
||||
import org.session.libsignal.exceptions.InvalidKeyException;
|
||||
import org.session.libsignal.crypto.ecc.Curve;
|
||||
import org.session.libsignal.crypto.ecc.ECKeyPair;
|
||||
import org.session.libsignal.crypto.ecc.ECPrivateKey;
|
||||
|
||||
import org.session.libsignal.utilities.Base64;
|
||||
|
||||
|
@@ -1,15 +1,15 @@
|
||||
package org.session.libsession.utilities
|
||||
|
||||
import android.content.Context
|
||||
import com.goterl.lazycode.lazysodium.LazySodiumAndroid
|
||||
import com.goterl.lazycode.lazysodium.SodiumAndroid
|
||||
import com.goterl.lazycode.lazysodium.utils.Key
|
||||
import com.goterl.lazycode.lazysodium.utils.KeyPair
|
||||
import com.goterl.lazysodium.LazySodiumAndroid
|
||||
import com.goterl.lazysodium.SodiumAndroid
|
||||
import com.goterl.lazysodium.utils.Key
|
||||
import com.goterl.lazysodium.utils.KeyPair
|
||||
import org.session.libsignal.crypto.ecc.DjbECPrivateKey
|
||||
import org.session.libsignal.crypto.ecc.DjbECPublicKey
|
||||
import org.session.libsignal.crypto.ecc.ECKeyPair
|
||||
import org.session.libsignal.utilities.Base64
|
||||
import org.session.libsignal.utilities.Hex
|
||||
import org.session.libsignal.libsignal.ecc.DjbECPrivateKey
|
||||
import org.session.libsignal.libsignal.ecc.DjbECPublicKey
|
||||
import org.session.libsignal.libsignal.ecc.ECKeyPair
|
||||
|
||||
object KeyPairUtilities {
|
||||
|
||||
@@ -53,8 +53,8 @@ object KeyPairUtilities {
|
||||
}
|
||||
|
||||
data class KeyPairGenerationResult(
|
||||
val seed: ByteArray,
|
||||
val ed25519KeyPair: KeyPair,
|
||||
val x25519KeyPair: ECKeyPair
|
||||
val seed: ByteArray,
|
||||
val ed25519KeyPair: KeyPair,
|
||||
val x25519KeyPair: ECKeyPair
|
||||
)
|
||||
}
|
@@ -1,4 +1,4 @@
|
||||
package org.session.libsession.utilities.color;
|
||||
package org.session.libsession.utilities;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Color;
|
||||
@@ -58,7 +58,6 @@ public enum MaterialColor {
|
||||
|
||||
private final String serialized;
|
||||
|
||||
|
||||
MaterialColor(@ColorRes int mainColor, @ColorRes int tintColor, @ColorRes int shadeColor, String serialized) {
|
||||
this.mainColor = mainColor;
|
||||
this.tintColor = tintColor;
|
||||
@@ -110,9 +109,9 @@ public enum MaterialColor {
|
||||
}
|
||||
|
||||
public boolean represents(Context context, int colorValue) {
|
||||
return context.getResources().getColor(mainColor) == colorValue ||
|
||||
context.getResources().getColor(tintColor) == colorValue ||
|
||||
context.getResources().getColor(shadeColor) == colorValue;
|
||||
return context.getResources().getColor(mainColor) == colorValue
|
||||
|| context.getResources().getColor(tintColor) == colorValue
|
||||
|| context.getResources().getColor(shadeColor) == colorValue;
|
||||
}
|
||||
|
||||
public String serialize() {
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user