Merge branch 'dev' of https://github.com/loki-project/session-android into zombie-handling-update

This commit is contained in:
Brice-W
2021-05-20 16:16:35 +10:00
1542 changed files with 48493 additions and 86248 deletions

View File

@@ -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" />

View File

@@ -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;

View File

@@ -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;

View File

@@ -1,4 +1,4 @@
package org.session.libsession.messaging.avatars;
package org.session.libsession.avatars;
import android.content.Context;
import android.net.Uri;

View File

@@ -1,4 +1,4 @@
package org.session.libsession.messaging.avatars;
package org.session.libsession.avatars;
import android.content.Context;
import android.graphics.drawable.Drawable;

View File

@@ -1,4 +1,4 @@
package org.session.libsession.messaging.avatars;
package org.session.libsession.avatars;
import android.content.Context;
import android.graphics.Color;

View File

@@ -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;

View File

@@ -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;

View File

@@ -1,4 +1,4 @@
package org.session.libsession.messaging.avatars;
package org.session.libsession.avatars;
import android.content.Context;
import android.graphics.Color;

View File

@@ -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;

View File

@@ -1,4 +1,4 @@
package org.session.libsession.messaging.avatars;
package org.session.libsession.avatars;
import android.content.Context;
import android.graphics.drawable.Drawable;

View File

@@ -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>

View File

@@ -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>)

View File

@@ -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)
}
}
}

View File

@@ -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() {

View File

@@ -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
}
}
}

View File

@@ -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))
}
}
}

View File

@@ -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)!!
)
}
}
}

View File

@@ -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?
}
}

View File

@@ -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 {

View File

@@ -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)
)
}
}
}

View File

@@ -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)
}
}

View File

@@ -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()

View File

@@ -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
}
}
}

View File

@@ -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(),

View File

@@ -0,0 +1,3 @@
package org.session.libsession.messaging.mentions
data class Mention(val publicKey: String, val displayName: String)

View File

@@ -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

View File

@@ -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 -> {

View File

@@ -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
}

View File

@@ -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) {

View File

@@ -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()

View File

@@ -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()

View File

@@ -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

View File

@@ -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? {

View File

@@ -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) {

View File

@@ -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

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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; }
}

View File

@@ -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 {

View File

@@ -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
}

View File

@@ -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
}
}
}

View File

@@ -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()

View File

@@ -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) {

View File

@@ -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
}
}

View File

@@ -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.*

View File

@@ -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
}

View File

@@ -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(

View File

@@ -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)
}
}

View File

@@ -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"
}

View File

@@ -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())
}
}

View File

@@ -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())

View File

@@ -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)
}

View File

@@ -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)
}
}

View File

@@ -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)
}
}
}

View File

@@ -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))

View File

@@ -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

View File

@@ -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;

View File

@@ -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?) {

View File

@@ -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
*/

View File

@@ -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

View File

@@ -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;

View File

@@ -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)

View File

@@ -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 {

View File

@@ -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

View File

@@ -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) }

View File

@@ -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)
}
}
}

View File

@@ -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
}

View File

@@ -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)

View File

@@ -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,

View File

@@ -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);
}
}
}

View File

@@ -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")

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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

View File

@@ -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.")

View File

@@ -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
)
}
}

View File

@@ -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.")
}

View File

@@ -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 ""
)
}
}

View File

@@ -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) {

View File

@@ -1,6 +1,6 @@
package org.session.libsession.snode
import org.session.libsignal.service.loki.Snode
import org.session.libsignal.utilities.Snode
interface SnodeStorageProtocol {

View File

@@ -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()!!
}

View File

@@ -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.
*/

View File

@@ -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

View File

@@ -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) {

View File

@@ -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;

View File

@@ -1,4 +1,4 @@
package org.session.libsession.messaging.threads
package org.session.libsession.utilities
object DistributionTypes {
const val DEFAULT = 2

View File

@@ -1,4 +1,4 @@
package org.session.libsession.database.documents;
package org.session.libsession.utilities;
import java.util.List;

View File

@@ -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) {

View File

@@ -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>()

View File

@@ -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

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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
)
}

View File

@@ -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