Merge branch 'dev' of https://github.com/oxen-io/session-android into client-side-nickname

This commit is contained in:
Ryan ZHAO
2021-05-20 11:15:24 +10:00
1536 changed files with 48410 additions and 85442 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
@@ -17,12 +17,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 {
@@ -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

@@ -8,15 +8,17 @@ import okhttp3.MediaType
import okhttp3.RequestBody
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.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 {
private const val DEFAULT_SERVER_PUBLIC_KEY = "7cb31905b55cd5580c686911debf672577b3fb0bff81df4ce2d5c4cb3a7aaa69"
const val DEFAULT_SERVER = "http://88.99.175.227"
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.")
@@ -24,12 +26,12 @@ object FileServerAPIV2 {
}
data class Request(
val verb: HTTP.Verb,
val endpoint: String,
val queryParameters: Map<String, String> = mapOf(),
val parameters: Any? = null,
val headers: Map<String, String> = mapOf(),
/**
val verb: HTTP.Verb,
val endpoint: String,
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.
*/
@@ -42,8 +44,10 @@ object FileServerAPIV2 {
return RequestBody.create(MediaType.get("application/json"), parametersAsJSON)
}
private fun send(request: Request): Promise<Map<*, *>, Exception> {
val url = HttpUrl.parse(DEFAULT_SERVER) ?: return Promise.ofFail(OpenGroupAPIV2.Error.InvalidURL)
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(url.scheme())
.host(url.host())
@@ -64,7 +68,7 @@ object FileServerAPIV2 {
HTTP.Verb.DELETE -> requestBuilder.delete(createBody(request.parameters))
}
if (request.useOnionRouting) {
return OnionRequestAPI.sendOnionRequest(requestBuilder.build(), DEFAULT_SERVER, DEFAULT_SERVER_PUBLIC_KEY).fail { e ->
return OnionRequestAPI.sendOnionRequest(requestBuilder.build(), server, serverPublicKey).fail { e ->
Log.e("Loki", "File server request failed.", e)
}
} else {
@@ -76,14 +80,14 @@ object FileServerAPIV2 {
val base64EncodedFile = Base64.encodeBytes(file)
val parameters = mapOf( "file" to base64EncodedFile )
val request = Request(verb = HTTP.Verb.POST, endpoint = "files", parameters = parameters)
return send(request).map { json ->
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 ->
return send(request, useOldServer).map { json ->
val base64EncodedFile = json["result"] as? String ?: throw Error.ParsingFailed
Base64.decode(base64EncodedFile) ?: throw Error.ParsingFailed
}

View File

@@ -8,9 +8,9 @@ import org.session.libsession.messaging.sending_receiving.attachments.Attachment
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
@@ -36,28 +36,29 @@ class AttachmentDownloadJob(val attachmentID: Long, val databaseMessageID: Long)
}
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)
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()) {
@@ -67,13 +68,13 @@ class AttachmentDownloadJob(val attachmentID: Long, val databaseMessageID: Long)
}
} 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) {

View File

@@ -3,21 +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
@@ -45,27 +49,29 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess
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 = openGroupV2?.server ?: openGroup?.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) {
val dataBytes = attachmentData.data.readBytes()
val result = OpenGroupAPIV2.upload(dataBytes, openGroupV2.room, openGroupV2.server).get()
DotNetAPI.UploadResult(result, "${openGroupV2.server}/files/$result", byteArrayOf())
} else {
FileServerAPI.shared.uploadAttachment(server, attachmentData)
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)
@@ -77,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.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)
}

View File

@@ -4,7 +4,7 @@ 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
@@ -130,12 +130,7 @@ class JobQueue : JobDelegate {
}
// Message send jobs waiting for the attachment to upload
if (job is MessageSendJob && error is MessageSendJob.AwaitingAttachmentUploadException) {
val retryInterval: Long = 1000 * 4
Log.i("Loki", "Message send job waiting for attachment upload to finish.")
timer.schedule(delay = retryInterval) {
Log.i("Loki", "Retrying ${job::class.simpleName}.")
queue.offer(job)
}
return
}
// Regular job failure

View File

@@ -5,9 +5,9 @@ import nl.komponents.kovenant.deferred
import org.session.libsession.messaging.sending_receiving.MessageReceiver
import org.session.libsession.messaging.sending_receiving.handle
import org.session.libsession.messaging.utilities.Data
import org.session.libsignal.utilities.logging.Log
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
@@ -69,7 +69,6 @@ class MessageReceiveJob(val data: ByteArray, val isBackgroundPoll: Boolean, val
override fun serialize(): Data {
val builder = Data.Builder().putByteArray(DATA_KEY, data)
.putBoolean(IS_BACKGROUND_POLL_KEY, isBackgroundPoll)
openGroupMessageServerID?.let { builder.putLong(OPEN_GROUP_MESSAGE_SERVER_ID_KEY, it) }
openGroupID?.let { builder.putString(OPEN_GROUP_ID_KEY, it) }
return builder.build();
@@ -84,7 +83,6 @@ class MessageReceiveJob(val data: ByteArray, val isBackgroundPoll: Boolean, val
override fun create(data: Data): MessageReceiveJob {
return MessageReceiveJob(
data.getByteArray(DATA_KEY),
data.getBoolean(IS_BACKGROUND_POLL_KEY),
data.getLong(OPEN_GROUP_MESSAGE_SERVER_ID_KEY),
data.getString(OPEN_GROUP_ID_KEY)
)

View File

@@ -10,7 +10,7 @@ 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.libsession.messaging.utilities.Data
import org.session.libsignal.utilities.logging.Log
import org.session.libsignal.utilities.Log
class MessageSendJob(val message: Message, val destination: Destination) : Job {

View File

@@ -13,9 +13,9 @@ 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

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,9 +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
import org.session.libsignal.utilities.toHexString
sealed class Destination {

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

View File

@@ -2,18 +2,18 @@ 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

View File

@@ -2,16 +2,16 @@ 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>,

View File

@@ -1,7 +1,7 @@
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() {
var kind: Kind? = null

View File

@@ -2,8 +2,8 @@ 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.

View File

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

View File

@@ -1,7 +1,7 @@
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() {
var kind: Kind? = null

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,8 +2,8 @@ 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

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

View File

@@ -4,8 +4,8 @@ import com.goterl.lazycode.lazysodium.BuildConfig
import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel as SignalQuote
import org.session.libsignal.utilities.logging.Log
import org.session.libsignal.service.internal.push.SignalServiceProtos
import org.session.libsignal.utilities.Log
import org.session.libsignal.protos.SignalServiceProtos
class Quote() {
var timestamp: Long? = 0

View File

@@ -4,11 +4,11 @@ import com.goterl.lazycode.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.recipients.Recipient
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.libsession.messaging.sending_receiving.attachments.Attachment as SignalAttachment
class VisibleMessage : Message() {
@@ -22,6 +22,7 @@ class VisibleMessage : Message() {
var quote: Quote? = null
var linkPreview: LinkPreview? = null
var profile: Profile? = null
var openGroupInvitation: OpenGroupInvitation? = null
override val isSelfSendValid: Boolean = true
@@ -29,6 +30,7 @@ class VisibleMessage : Message() {
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
@@ -55,7 +57,12 @@ class VisibleMessage : Message() {
val linkPreview = LinkPreview.fromProto(linkPreviewProto)
result.linkPreview = linkPreview
}
// TODO: Contact
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)
if (profile != null) { result.profile = profile }
return result
@@ -66,7 +73,7 @@ class VisibleMessage : Message() {
val proto = SignalServiceProtos.Content.newBuilder()
val dataMessage: SignalServiceProtos.DataMessage.Builder
// Profile
val profileProto = profile?.let { it.toProto() }
val profileProto = profile?.toProto()
if (profileProto != null) {
dataMessage = profileProto.toBuilder()
} else {
@@ -75,15 +82,20 @@ class VisibleMessage : Message() {
// Text
if (text != null) { dataMessage.body = text }
// Quote
val quoteProto = quote?.let { it.toProto() }
val quoteProto = quote?.toProto()
if (quoteProto != null) {
dataMessage.quote = quoteProto
}
// Link preview
val linkPreviewProto = linkPreview?.let { it.toProto() }
val linkPreviewProto = linkPreview?.toProto()
if (linkPreviewProto != null) {
dataMessage.addAllPreview(listOf(linkPreviewProto))
}
// 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) }

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

@@ -15,14 +15,14 @@ import okhttp3.RequestBody
import org.session.libsession.messaging.MessagingModuleConfiguration
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.*
@@ -65,15 +65,15 @@ object OpenGroupAPIV2 {
}
data class Request(
val verb: HTTP.Verb,
val room: String?,
val server: String,
val endpoint: String,
val queryParameters: Map<String, String> = mapOf(),
val parameters: Any? = null,
val headers: Map<String, String> = mapOf(),
val isAuthRequired: Boolean = true,
/**
val verb: HTTP.Verb,
val room: String?,
val server: String,
val endpoint: String,
val queryParameters: Map<String, String> = mapOf(),
val parameters: Any? = null,
val headers: Map<String, String> = mapOf(),
val isAuthRequired: Boolean = true,
/**
* Always `true` under normal circumstances. You might want to disable
* this when running over Lokinet.
*/
@@ -348,7 +348,7 @@ object OpenGroupAPIV2 {
// region General
@Suppress("UNCHECKED_CAST")
fun getCompactPoll(rooms: List<String>, server: String): Promise<Map<String, CompactPollResult>, Exception> {
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 ->

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,11 +1,11 @@
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(

View File

@@ -1,7 +1,7 @@
package org.session.libsession.messaging.open_groups
import org.session.libsignal.utilities.JsonUtil
import org.session.libsignal.utilities.logging.Log
import org.session.libsignal.utilities.Log
import java.util.*
data class OpenGroupV2(

View File

@@ -0,0 +1,60 @@
package org.session.libsession.messaging.sending_receiving
import android.util.Log
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 org.session.libsignal.crypto.ecc.ECKeyPair
import org.session.libsignal.utilities.hexEncodedPublicKey
import org.session.libsignal.utilities.removing05PrefixIfNeeded
import org.session.libsignal.utilities.toHexString
import org.session.libsignal.utilities.Hex
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

@@ -9,11 +9,11 @@ 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.removing05PrefixIfNeeded
import org.session.libsignal.utilities.Hex
import org.session.libsignal.utilities.logging.Log
import org.session.libsignal.utilities.Log
object MessageSenderEncryption {
object MessageEncrypter {
private val sodium by lazy { LazySodiumAndroid(SodiumAndroid()) }
@@ -25,7 +25,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(plaintext),
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

@@ -13,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
@@ -52,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!!)
@@ -93,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) {
@@ -109,20 +107,20 @@ 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.joinURL }
@@ -136,7 +134,8 @@ private fun handleConfigurationMessage(message: ConfigurationMessage) {
TextSecurePreferences.setProfileName(context, message.displayName)
profileManager.setProfileName(context, recipient, 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)
profileManager.setProfileKey(context, recipient, message.profileKey)
@@ -147,46 +146,39 @@ private fun handleConfigurationMessage(message: ConfigurationMessage) {
}
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)
if (threadID < 0) {
// thread doesn't exist, should only be reached in a case where we are processing open group messages for no longer existent thread
// Thread doesn't exist; should only be reached in a case where we are processing open group messages for a no longer existent thread
throw MessageReceiver.Error.NoThread
}
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.setProfileName(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
@@ -194,10 +186,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))
}
@@ -218,6 +211,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()) {
@@ -226,7 +220,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
@@ -240,14 +233,17 @@ fun MessageReceiver.handleVisibleMessage(message: VisibleMessage, proto: SignalS
}
val openGroupServerID = message.openGroupServerMessageID
if (openGroupServerID != null) {
storage.setOpenGroupServerMessageID(messageID, openGroupServerID, threadID, !message.isMediaMessage())
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)
@@ -267,7 +263,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
@@ -279,11 +274,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 {
@@ -311,11 +305,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)) {
@@ -325,7 +319,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()))
@@ -336,7 +330,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) {
@@ -349,11 +343,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
@@ -364,10 +358,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 {
@@ -384,11 +376,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 }
@@ -400,19 +392,28 @@ private fun MessageReceiver.handleClosedGroupMembersAdded(message: ClosedGroupCo
val updateMembers = kind.members.map { it.toByteArray().toHexString() }
val newMembers = members + updateMembers
storage.updateMembers(groupID, newMembers.map { Address.fromSerialized(it) })
// 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 {
@@ -437,65 +438,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 {
@@ -517,11 +507,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
@@ -535,19 +525,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 {
@@ -555,9 +542,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) {
@@ -582,4 +567,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

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

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

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 {

View File

@@ -1,6 +1,6 @@
package org.session.libsession.snode
import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded
import org.session.libsignal.utilities.removing05PrefixIfNeeded
data class SnodeMessage(
/**

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,19 +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? {
if (isEmpty()) return null
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

@@ -7,9 +7,9 @@ import com.goterl.lazycode.lazysodium.utils.Key
import com.goterl.lazycode.lazysodium.utils.KeyPair
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
import org.session.libsignal.crypto.ecc.DjbECPrivateKey
import org.session.libsignal.crypto.ecc.DjbECPublicKey
import org.session.libsignal.crypto.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() {

View File

@@ -1,9 +1,9 @@
package org.session.libsession.database.documents;
package org.session.libsession.utilities;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.session.libsession.messaging.threads.Address;
import org.session.libsession.utilities.Address;
public class NetworkFailure {

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

@@ -1,4 +1,4 @@
package org.session.libsession.utilities.preferences;
package org.session.libsession.utilities;
public class NotificationPrivacyPreference {

View File

@@ -1,19 +0,0 @@
package org.session.libsession.utilities
import android.telephony.PhoneNumberUtils
import android.util.Patterns
object NumberUtil {
private val emailPattern = Patterns.EMAIL_ADDRESS
@JvmStatic
fun isValidEmail(number: String): Boolean {
val matcher = emailPattern.matcher(number)
return matcher.matches()
}
@JvmStatic
fun isValidSmsOrEmail(number: String): Boolean {
return PhoneNumberUtils.isWellFormedSmsAddress(number) || isValidEmail(number)
}
}

View File

@@ -0,0 +1,36 @@
package org.session.libsession.utilities
import okhttp3.HttpUrl
object OpenGroupUrlParser {
sealed class Error(val description: String) : Exception(description) {
object MalformedURL : Error("Malformed URL.")
object NoRoom : Error("No room specified in the URL.")
object NoPublicKey : Error("No public key specified in the URL.")
object InvalidPublicKey : Error("Invalid public key provided.")
}
private const val suffix = "/"
private const val queryPrefix = "public_key"
fun parseUrl(string: String): V2OpenGroupInfo {
// URL has to start with 'http://'
val urlWithPrefix = if (!string.startsWith("http")) "http://$string" else string
// If the URL is malformed, throw an exception
val url = HttpUrl.parse(urlWithPrefix) ?: throw Error.MalformedURL
// Parse components
val server = HttpUrl.Builder().scheme(url.scheme()).host(url.host()).port(url.port()).build().toString().removeSuffix(suffix)
val room = url.pathSegments().firstOrNull { !it.isNullOrEmpty() } ?: throw Error.NoRoom
val publicKey = url.queryParameter(queryPrefix) ?: throw Error.NoPublicKey
if (publicKey.length != 64) throw Error.InvalidPublicKey
// Return
return V2OpenGroupInfo(server,room,publicKey)
}
fun trimQueryParameter(string: String): String {
return string.substringBefore("?$queryPrefix")
}
}
class V2OpenGroupInfo(val server: String, val room: String, val serverPublicKey: String)

Some files were not shown because too many files have changed in this diff Show More