mirror of
https://github.com/oxen-io/session-android.git
synced 2025-08-11 20:47:42 +00:00
Merge branch 'dev' of https://github.com/loki-project/session-android into data-extraction
This commit is contained in:
@@ -1,10 +1,8 @@
|
||||
package org.session.libsession.database
|
||||
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.Attachment
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentState
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.SessionServiceAttachmentPointer
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.SessionServiceAttachmentStream
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.*
|
||||
import org.session.libsession.messaging.threads.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 java.io.InputStream
|
||||
@@ -14,10 +12,13 @@ interface MessageDataProvider {
|
||||
fun getMessageID(serverID: Long): Long?
|
||||
fun deleteMessage(messageID: Long)
|
||||
|
||||
fun getDatabaseAttachment(attachmentId: Long): DatabaseAttachment?
|
||||
|
||||
fun getAttachmentStream(attachmentId: Long): SessionServiceAttachmentStream?
|
||||
fun getAttachmentPointer(attachmentId: Long): SessionServiceAttachmentPointer?
|
||||
|
||||
fun getSignalAttachmentStream(attachmentId: Long): SignalServiceAttachmentStream?
|
||||
fun getScaledSignalAttachmentStream(attachmentId: Long): SignalServiceAttachmentStream?
|
||||
fun getSignalAttachmentPointer(attachmentId: Long): SignalServiceAttachmentPointer?
|
||||
|
||||
fun setAttachmentState(attachmentState: AttachmentState, attachmentId: Long, messageID: Long)
|
||||
@@ -26,12 +27,15 @@ interface MessageDataProvider {
|
||||
|
||||
fun isOutgoingMessage(timestamp: Long): Boolean
|
||||
|
||||
@Throws(Exception::class)
|
||||
fun uploadAttachment(attachmentId: Long)
|
||||
fun updateAttachmentAfterUploadSucceeded(attachmentId: Long, attachmentStream: SignalServiceAttachmentStream, attachmentKey: ByteArray, uploadResult: DotNetAPI.UploadResult)
|
||||
fun updateAttachmentAfterUploadFailed(attachmentId: Long)
|
||||
|
||||
// Quotes
|
||||
fun getMessageForQuote(timestamp: Long, author: Address): Long?
|
||||
fun getAttachmentsAndLinkPreviewFor(messageID: Long): List<Attachment>
|
||||
fun getMessageBodyFor(messageID: Long): String
|
||||
|
||||
fun getAttachmentIDsFor(messageID: Long): List<Long>
|
||||
fun getLinkPreviewAttachmentIDFor(messageID: Long): Long?
|
||||
|
||||
}
|
@@ -0,0 +1,10 @@
|
||||
package org.session.libsession.database.documents;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface Document<T> {
|
||||
|
||||
public int size();
|
||||
public List<T> getList();
|
||||
|
||||
}
|
@@ -0,0 +1,88 @@
|
||||
package org.session.libsession.database.documents;
|
||||
|
||||
import org.session.libsignal.utilities.logging.Log;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
||||
import com.fasterxml.jackson.databind.DeserializationContext;
|
||||
import com.fasterxml.jackson.databind.JsonDeserializer;
|
||||
import com.fasterxml.jackson.databind.JsonSerializer;
|
||||
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.libsignal.utilities.Base64;
|
||||
import org.session.libsignal.libsignal.IdentityKey;
|
||||
import org.session.libsignal.libsignal.InvalidKeyException;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class IdentityKeyMismatch {
|
||||
|
||||
private static final String TAG = IdentityKeyMismatch.class.getSimpleName();
|
||||
|
||||
@JsonProperty(value = "a")
|
||||
private String address;
|
||||
|
||||
@JsonProperty(value = "k")
|
||||
@JsonSerialize(using = IdentityKeySerializer.class)
|
||||
@JsonDeserialize(using = IdentityKeyDeserializer.class)
|
||||
private IdentityKey identityKey;
|
||||
|
||||
public IdentityKeyMismatch() {}
|
||||
|
||||
public IdentityKeyMismatch(Address address, IdentityKey identityKey) {
|
||||
this.address = address.serialize();
|
||||
this.identityKey = identityKey;
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
public Address getAddress() {
|
||||
return Address.fromSerialized(address);
|
||||
}
|
||||
|
||||
public IdentityKey getIdentityKey() {
|
||||
return identityKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
if (other == null || !(other instanceof IdentityKeyMismatch)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
IdentityKeyMismatch that = (IdentityKeyMismatch)other;
|
||||
return that.address.equals(this.address) && that.identityKey.equals(this.identityKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return address.hashCode() ^ identityKey.hashCode();
|
||||
}
|
||||
|
||||
private static class IdentityKeySerializer extends JsonSerializer<IdentityKey> {
|
||||
@Override
|
||||
public void serialize(IdentityKey value, JsonGenerator jsonGenerator, SerializerProvider serializers)
|
||||
throws IOException
|
||||
{
|
||||
jsonGenerator.writeString(Base64.encodeBytes(value.serialize()));
|
||||
}
|
||||
}
|
||||
|
||||
private static class IdentityKeyDeserializer extends JsonDeserializer<IdentityKey> {
|
||||
@Override
|
||||
public IdentityKey deserialize(JsonParser jsonParser, DeserializationContext ctxt)
|
||||
throws IOException
|
||||
{
|
||||
try {
|
||||
return new IdentityKey(Base64.decode(jsonParser.getValueAsString()), 0);
|
||||
} catch (InvalidKeyException e) {
|
||||
Log.w(TAG, e);
|
||||
throw new IOException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,33 @@
|
||||
package org.session.libsession.database.documents;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
public class IdentityKeyMismatchList implements Document<IdentityKeyMismatch> {
|
||||
|
||||
@JsonProperty(value = "m")
|
||||
private List<IdentityKeyMismatch> mismatches;
|
||||
|
||||
public IdentityKeyMismatchList() {
|
||||
this.mismatches = new LinkedList<>();
|
||||
}
|
||||
|
||||
public IdentityKeyMismatchList(List<IdentityKeyMismatch> mismatches) {
|
||||
this.mismatches = mismatches;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
if (mismatches == null) return 0;
|
||||
else return mismatches.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
@JsonIgnore
|
||||
public List<IdentityKeyMismatch> getList() {
|
||||
return mismatches;
|
||||
}
|
||||
}
|
@@ -0,0 +1,36 @@
|
||||
package org.session.libsession.database.documents;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import org.session.libsession.messaging.threads.Address;
|
||||
|
||||
public class NetworkFailure {
|
||||
|
||||
@JsonProperty(value = "a")
|
||||
private String address;
|
||||
|
||||
public NetworkFailure(Address address) {
|
||||
this.address = address.serialize();
|
||||
}
|
||||
|
||||
public NetworkFailure() {}
|
||||
|
||||
@JsonIgnore
|
||||
public Address getAddress() {
|
||||
return Address.fromSerialized(address);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
if (other == null || !(other instanceof NetworkFailure)) return false;
|
||||
|
||||
NetworkFailure that = (NetworkFailure)other;
|
||||
return this.address.equals(that.address);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return address.hashCode();
|
||||
}
|
||||
}
|
@@ -0,0 +1,33 @@
|
||||
package org.session.libsession.database.documents;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
public class NetworkFailureList implements Document<NetworkFailure> {
|
||||
|
||||
@JsonProperty(value = "l")
|
||||
private List<NetworkFailure> failures;
|
||||
|
||||
public NetworkFailureList() {
|
||||
this.failures = new LinkedList<>();
|
||||
}
|
||||
|
||||
public NetworkFailureList(List<NetworkFailure> failures) {
|
||||
this.failures = failures;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
if (failures == null) return 0;
|
||||
else return failures.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
@JsonIgnore
|
||||
public List<NetworkFailure> getList() {
|
||||
return failures;
|
||||
}
|
||||
}
|
@@ -59,6 +59,8 @@ interface StorageProtocol {
|
||||
fun getThreadID(openGroupID: String): String?
|
||||
fun getAllOpenGroups(): Map<Long, PublicChat>
|
||||
fun addOpenGroup(server: String, channel: Long)
|
||||
fun setOpenGroupServerMessageID(messageID: Long, serverID: Long)
|
||||
fun getQuoteServerID(quoteID: Long, publicKey: String): Long?
|
||||
|
||||
// Open Group Public Keys
|
||||
fun getOpenGroupPublicKey(server: String): String?
|
||||
@@ -94,10 +96,9 @@ interface StorageProtocol {
|
||||
fun persistAttachments(messageId: Long, attachments: List<Attachment>): List<Long>
|
||||
|
||||
fun getMessageIdInDatabase(timestamp: Long, author: String): Long?
|
||||
fun setOpenGroupServerMessageID(messageID: Long, serverID: Long)
|
||||
fun markAsSent(messageID: Long)
|
||||
fun markUnidentified(messageID: Long)
|
||||
fun setErrorMessage(messageID: Long, error: Exception)
|
||||
fun markAsSent(timestamp: Long, author: String)
|
||||
fun markUnidentified(timestamp: Long, author: String)
|
||||
fun setErrorMessage(timestamp: Long, author: String, error: Exception)
|
||||
|
||||
// Closed Groups
|
||||
fun getGroup(groupID: String): GroupRecord?
|
||||
@@ -112,9 +113,9 @@ interface StorageProtocol {
|
||||
fun addClosedGroupEncryptionKeyPair(encryptionKeyPair: ECKeyPair, groupPublicKey: String)
|
||||
fun removeAllClosedGroupEncryptionKeyPairs(groupPublicKey: String)
|
||||
fun insertIncomingInfoMessage(context: Context, senderPublicKey: String, groupID: String, type0: SignalServiceProtos.GroupContext.Type, type1: SignalServiceGroup.Type,
|
||||
name: String, members: Collection<String>, admins: Collection<String>)
|
||||
name: String, members: Collection<String>, admins: Collection<String>, sentTimestamp: Long)
|
||||
fun insertOutgoingInfoMessage(context: Context, groupID: String, type: SignalServiceProtos.GroupContext.Type, name: String,
|
||||
members: Collection<String>, admins: Collection<String>, threadID: Long)
|
||||
members: Collection<String>, admins: Collection<String>, threadID: Long, sentTimestamp: Long)
|
||||
fun isClosedGroup(publicKey: String): Boolean
|
||||
fun getClosedGroupEncryptionKeyPairs(groupPublicKey: String): MutableList<ECKeyPair>
|
||||
fun getLatestClosedGroupEncryptionKeyPair(groupPublicKey: String): ECKeyPair?
|
||||
|
@@ -1,40 +0,0 @@
|
||||
package org.session.libsession.messaging.avatars;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.session.libsession.utilities.color.MaterialColor;
|
||||
|
||||
|
||||
/**
|
||||
* Used for migrating legacy colors to modern colors. For normal color generation, use
|
||||
* {@link ContactColors}.
|
||||
*/
|
||||
public class ContactColorsLegacy {
|
||||
|
||||
private static final String[] LEGACY_PALETTE = new String[] {
|
||||
"red",
|
||||
"pink",
|
||||
"purple",
|
||||
"deep_purple",
|
||||
"indigo",
|
||||
"blue",
|
||||
"light_blue",
|
||||
"cyan",
|
||||
"teal",
|
||||
"green",
|
||||
"light_green",
|
||||
"orange",
|
||||
"deep_orange",
|
||||
"amber",
|
||||
"blue_grey"
|
||||
};
|
||||
|
||||
public static MaterialColor generateFor(@NonNull String name) {
|
||||
String serialized = LEGACY_PALETTE[Math.abs(name.hashCode()) % LEGACY_PALETTE.length];
|
||||
try {
|
||||
return MaterialColor.fromSerialized(serialized);
|
||||
} catch (MaterialColor.UnknownColorException e) {
|
||||
return ContactColors.generateFor(name);
|
||||
}
|
||||
}
|
||||
}
|
@@ -29,7 +29,6 @@ class FileServerAPI(public val server: String, userPublicKey: String, userPrivat
|
||||
* possible after proof of work has been calculated and the onion request encryption has happened, which takes several seconds.
|
||||
*/
|
||||
public val fileSizeORMultiplier = 2 // TODO: It should be possible to set this to 1.5?
|
||||
val server = "https://file.getsession.org"
|
||||
public val fileStorageBucketURL = "https://file-static.lokinet.org"
|
||||
// endregion
|
||||
|
||||
|
@@ -17,7 +17,7 @@ class AttachmentDownloadJob(val attachmentID: Long, val databaseMessageID: Long)
|
||||
private val MAX_ATTACHMENT_SIZE = 10 * 1024 * 1024
|
||||
|
||||
// Error
|
||||
internal sealed class Error(val description: String) : Exception() {
|
||||
internal sealed class Error(val description: String) : Exception(description) {
|
||||
object NoAttachment : Error("No such attachment.")
|
||||
}
|
||||
|
||||
|
@@ -8,11 +8,15 @@ import org.session.libsession.messaging.fileserver.FileServerAPI
|
||||
import org.session.libsession.messaging.messages.Message
|
||||
import org.session.libsession.messaging.sending_receiving.MessageSender
|
||||
import org.session.libsession.messaging.utilities.DotNetAPI
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
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.utilities.PlaintextOutputStreamFactory
|
||||
import org.session.libsignal.utilities.ThreadUtils
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
|
||||
class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val message: Message, val messageSendJobID: String) : Job {
|
||||
|
||||
@@ -21,14 +25,14 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess
|
||||
override var failureCount: Int = 0
|
||||
|
||||
// Error
|
||||
internal sealed class Error(val description: String) : Exception() {
|
||||
internal sealed class Error(val description: String) : Exception(description) {
|
||||
object NoAttachment : Error("No such attachment.")
|
||||
}
|
||||
|
||||
// Settings
|
||||
override val maxFailureCount: Int = 20
|
||||
companion object {
|
||||
val TAG = AttachmentUploadJob::class.qualifiedName
|
||||
val TAG = AttachmentUploadJob::class.simpleName
|
||||
val KEY: String = "AttachmentUploadJob"
|
||||
|
||||
val maxFailureCount: Int = 20
|
||||
@@ -41,46 +45,54 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess
|
||||
}
|
||||
|
||||
override fun execute() {
|
||||
try {
|
||||
val attachmentStream = MessagingConfiguration.shared.messageDataProvider.getAttachmentStream(attachmentID)
|
||||
?: return handleFailure(Error.NoAttachment)
|
||||
ThreadUtils.queue {
|
||||
try {
|
||||
val attachment = MessagingConfiguration.shared.messageDataProvider.getScaledSignalAttachmentStream(attachmentID)
|
||||
?: return@queue handleFailure(Error.NoAttachment)
|
||||
|
||||
val openGroup = MessagingConfiguration.shared.storage.getOpenGroup(threadID)
|
||||
val server = openGroup?.server ?: FileServerAPI.server
|
||||
var server = FileServerAPI.shared.server
|
||||
var shouldEncrypt = true
|
||||
val usePadding = false
|
||||
val openGroup = MessagingConfiguration.shared.storage.getOpenGroup(threadID)
|
||||
openGroup?.let {
|
||||
server = it.server
|
||||
shouldEncrypt = false
|
||||
}
|
||||
|
||||
//TODO add some encryption stuff here
|
||||
val isEncryptionRequired = false
|
||||
//val isEncryptionRequired = (server == FileServerAPI.server)
|
||||
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 attachmentKey = Util.getSecretBytes(64)
|
||||
val outputStreamFactory = if (isEncryptionRequired) AttachmentCipherOutputStreamFactory(attachmentKey) else PlaintextOutputStreamFactory()
|
||||
val ciphertextLength = attachmentStream.length
|
||||
val outputStreamFactory = if (shouldEncrypt) AttachmentCipherOutputStreamFactory(attachmentKey) else PlaintextOutputStreamFactory()
|
||||
val attachmentData = PushAttachmentData(attachment.contentType, dataStream, ciphertextLength, outputStreamFactory, attachment.listener)
|
||||
|
||||
val attachmentData = PushAttachmentData(attachmentStream.contentType, attachmentStream.inputStream, ciphertextLength, outputStreamFactory, attachmentStream.listener)
|
||||
val uploadResult = FileServerAPI.shared.uploadAttachment(server, attachmentData)
|
||||
handleSuccess(attachment, attachmentKey, uploadResult)
|
||||
|
||||
FileServerAPI.shared.uploadAttachment(server, attachmentData)
|
||||
|
||||
} catch (e: java.lang.Exception) {
|
||||
if (e is Error && e == Error.NoAttachment) {
|
||||
this.handlePermanentFailure(e)
|
||||
} else if (e is DotNetAPI.Error && !e.isRetryable) {
|
||||
this.handlePermanentFailure(e)
|
||||
} else {
|
||||
this.handleFailure(e)
|
||||
} catch (e: java.lang.Exception) {
|
||||
if (e is Error && e == Error.NoAttachment) {
|
||||
this.handlePermanentFailure(e)
|
||||
} else if (e is DotNetAPI.Error && !e.isRetryable) {
|
||||
this.handlePermanentFailure(e)
|
||||
} else {
|
||||
this.handleFailure(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleSuccess() {
|
||||
private fun handleSuccess(attachment: SignalServiceAttachmentStream, attachmentKey: ByteArray, uploadResult: DotNetAPI.UploadResult) {
|
||||
Log.w(TAG, "Attachment uploaded successfully.")
|
||||
delegate?.handleJobSucceeded(this)
|
||||
MessagingConfiguration.shared.messageDataProvider.updateAttachmentAfterUploadSucceeded(attachmentID, attachment, attachmentKey, uploadResult)
|
||||
MessagingConfiguration.shared.storage.resumeMessageSendJobIfNeeded(messageSendJobID)
|
||||
//TODO interaction stuff, not sure how to deal with that
|
||||
}
|
||||
|
||||
private fun handlePermanentFailure(e: Exception) {
|
||||
Log.w(TAG, "Attachment upload failed permanently due to error: $this.")
|
||||
delegate?.handleJobFailedPermanently(this, e)
|
||||
MessagingConfiguration.shared.messageDataProvider.updateAttachmentAfterUploadFailed(attachmentID)
|
||||
failAssociatedMessageSendJob(e)
|
||||
}
|
||||
|
||||
@@ -95,7 +107,7 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess
|
||||
private fun failAssociatedMessageSendJob(e: Exception) {
|
||||
val storage = MessagingConfiguration.shared.storage
|
||||
val messageSendJob = storage.getMessageSendJob(messageSendJobID)
|
||||
MessageSender.handleFailedMessageSend(this.message!!, e)
|
||||
MessageSender.handleFailedMessageSend(this.message, e)
|
||||
if (messageSendJob != null) {
|
||||
storage.markJobAsFailed(messageSendJob)
|
||||
}
|
||||
@@ -119,7 +131,7 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess
|
||||
}
|
||||
|
||||
override fun getFactoryKey(): String {
|
||||
return AttachmentDownloadJob.KEY
|
||||
return KEY
|
||||
}
|
||||
|
||||
class Factory: Job.Factory<AttachmentUploadJob> {
|
||||
|
@@ -15,7 +15,8 @@ class MessageReceiveJob(val data: ByteArray, val isBackgroundPoll: Boolean, val
|
||||
// Settings
|
||||
override val maxFailureCount: Int = 10
|
||||
companion object {
|
||||
val TAG = MessageReceiveJob::class.qualifiedName
|
||||
|
||||
val TAG = MessageReceiveJob::class.simpleName
|
||||
val KEY: String = "MessageReceiveJob"
|
||||
|
||||
//keys used for database storage purpose
|
||||
@@ -75,7 +76,7 @@ class MessageReceiveJob(val data: ByteArray, val isBackgroundPoll: Boolean, val
|
||||
}
|
||||
|
||||
override fun getFactoryKey(): String {
|
||||
return AttachmentDownloadJob.KEY
|
||||
return KEY
|
||||
}
|
||||
|
||||
class Factory: Job.Factory<MessageReceiveJob> {
|
||||
|
@@ -19,7 +19,7 @@ class MessageSendJob(val message: Message, val destination: Destination) : Job {
|
||||
// Settings
|
||||
override val maxFailureCount: Int = 10
|
||||
companion object {
|
||||
val TAG = MessageSendJob::class.qualifiedName
|
||||
val TAG = MessageSendJob::class.simpleName
|
||||
val KEY: String = "MessageSendJob"
|
||||
|
||||
//keys used for database storage purpose
|
||||
@@ -32,13 +32,17 @@ class MessageSendJob(val message: Message, val destination: Destination) : Job {
|
||||
val message = message as? VisibleMessage
|
||||
message?.let {
|
||||
if(!messageDataProvider.isOutgoingMessage(message.sentTimestamp!!)) return // The message has been deleted
|
||||
val attachments = message.attachmentIDs.map { messageDataProvider.getAttachmentStream(it) }.filterNotNull()
|
||||
val attachmentsToUpload = attachments.filter { !it.isUploaded }
|
||||
val attachmentIDs = mutableListOf<Long>()
|
||||
attachmentIDs.addAll(message.attachmentIDs)
|
||||
message.quote?.let { it.attachmentID?.let { attachmentID -> attachmentIDs.add(attachmentID) } }
|
||||
message.linkPreview?.let { it.attachmentID?.let { attachmentID -> attachmentIDs.add(attachmentID) } }
|
||||
val attachments = attachmentIDs.mapNotNull { messageDataProvider.getDatabaseAttachment(it) }
|
||||
val attachmentsToUpload = attachments.filter { it.url.isNullOrEmpty() }
|
||||
attachmentsToUpload.forEach {
|
||||
if(MessagingConfiguration.shared.storage.getAttachmentUploadJob(it.attachmentId) != null) {
|
||||
if (MessagingConfiguration.shared.storage.getAttachmentUploadJob(it.attachmentId.rowId) != null) {
|
||||
// Wait for it to finish
|
||||
} else {
|
||||
val job = AttachmentUploadJob(it.attachmentId, message.threadID!!.toString(), message, id!!)
|
||||
val job = AttachmentUploadJob(it.attachmentId.rowId, message.threadID!!.toString(), message, id!!)
|
||||
JobQueue.shared.add(job)
|
||||
}
|
||||
}
|
||||
@@ -79,21 +83,21 @@ class MessageSendJob(val message: Message, val destination: Destination) : Job {
|
||||
//serialize Message and Destination properties
|
||||
val kryo = Kryo()
|
||||
kryo.isRegistrationRequired = false
|
||||
val serializedMessage = ByteArray(4096)
|
||||
val serializedDestination = ByteArray(4096)
|
||||
var output = Output(serializedMessage)
|
||||
kryo.writeObject(output, message)
|
||||
val output = Output(ByteArray(4096), -1) // maxBufferSize '-1' will dynamically grow internally if we run out of room serializing the message
|
||||
kryo.writeClassAndObject(output, message)
|
||||
output.close()
|
||||
output = Output(serializedDestination)
|
||||
kryo.writeObject(output, destination)
|
||||
val serializedMessage = output.toBytes()
|
||||
output.clear()
|
||||
kryo.writeClassAndObject(output, destination)
|
||||
output.close()
|
||||
val serializedDestination = output.toBytes()
|
||||
return Data.Builder().putByteArray(KEY_MESSAGE, serializedMessage)
|
||||
.putByteArray(KEY_DESTINATION, serializedDestination)
|
||||
.build();
|
||||
}
|
||||
|
||||
override fun getFactoryKey(): String {
|
||||
return AttachmentDownloadJob.KEY
|
||||
return KEY
|
||||
}
|
||||
|
||||
class Factory: Job.Factory<MessageSendJob> {
|
||||
@@ -103,10 +107,10 @@ class MessageSendJob(val message: Message, val destination: Destination) : Job {
|
||||
//deserialize Message and Destination properties
|
||||
val kryo = Kryo()
|
||||
var input = Input(serializedMessage)
|
||||
val message: Message = kryo.readObject(input, Message::class.java)
|
||||
val message = kryo.readClassAndObject(input) as Message
|
||||
input.close()
|
||||
input = Input(serializedDestination)
|
||||
val destination: Destination = kryo.readObject(input, Destination::class.java)
|
||||
val destination = kryo.readClassAndObject(input) as Destination
|
||||
input.close()
|
||||
return MessageSendJob(message, destination)
|
||||
}
|
||||
|
@@ -76,7 +76,7 @@ class NotifyPNServerJob(val message: SnodeMessage) : Job {
|
||||
}
|
||||
|
||||
override fun getFactoryKey(): String {
|
||||
return AttachmentDownloadJob.KEY
|
||||
return KEY
|
||||
}
|
||||
|
||||
class Factory: Job.Factory<NotifyPNServerJob> {
|
||||
|
@@ -3,26 +3,39 @@ package org.session.libsession.messaging.messages
|
||||
import org.session.libsession.messaging.MessagingConfiguration
|
||||
import org.session.libsession.messaging.threads.Address
|
||||
import org.session.libsession.utilities.GroupUtil
|
||||
import org.session.libsignal.service.loki.utilities.toHexString
|
||||
|
||||
sealed class Destination {
|
||||
|
||||
class Contact(val publicKey: String) : Destination()
|
||||
class ClosedGroup(val groupPublicKey: String) : Destination()
|
||||
class OpenGroup(val channel: Long, val server: String) : Destination()
|
||||
class Contact(var publicKey: String) : Destination() {
|
||||
internal constructor(): this("")
|
||||
}
|
||||
class ClosedGroup(var groupPublicKey: String) : Destination() {
|
||||
internal constructor(): this("")
|
||||
}
|
||||
class OpenGroup(var channel: Long, var server: String) : Destination() {
|
||||
internal constructor(): this(0, "")
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun from(address: Address): Destination {
|
||||
if (address.isContact) {
|
||||
return Contact(address.contactIdentifier())
|
||||
} else if (address.isClosedGroup) {
|
||||
val groupID = address.contactIdentifier()
|
||||
val groupPublicKey = GroupUtil.getDecodedGroupID(groupID)
|
||||
return ClosedGroup(groupPublicKey)
|
||||
} else if (address.isOpenGroup) {
|
||||
val openGroup = MessagingConfiguration.shared.storage.getOpenGroup(address.contactIdentifier())!!
|
||||
return OpenGroup(openGroup.channel, openGroup.server)
|
||||
} else {
|
||||
throw Exception("TODO: Handle legacy closed groups.")
|
||||
return when {
|
||||
address.isContact -> {
|
||||
Contact(address.contactIdentifier())
|
||||
}
|
||||
address.isClosedGroup -> {
|
||||
val groupID = address.toGroupString()
|
||||
val groupPublicKey = GroupUtil.doubleDecodeGroupID(groupID).toHexString()
|
||||
ClosedGroup(groupPublicKey)
|
||||
}
|
||||
address.isOpenGroup -> {
|
||||
val threadID = MessagingConfiguration.shared.storage.getThreadID(address.contactIdentifier())!!
|
||||
val openGroup = MessagingConfiguration.shared.storage.getOpenGroup(threadID)!!
|
||||
OpenGroup(openGroup.channel, openGroup.server)
|
||||
}
|
||||
else -> {
|
||||
throw Exception("TODO: Handle legacy closed groups.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,5 +1,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
|
||||
|
||||
abstract class Message {
|
||||
@@ -18,11 +20,23 @@ abstract class Message {
|
||||
|
||||
// validation
|
||||
open fun isValid(): Boolean {
|
||||
sentTimestamp = if (sentTimestamp!! > 0) sentTimestamp else return false
|
||||
receivedTimestamp = if (receivedTimestamp!! > 0) receivedTimestamp else return false
|
||||
sentTimestamp?.let {
|
||||
if (it <= 0) return false
|
||||
}
|
||||
receivedTimestamp?.let {
|
||||
if (it <= 0) return false
|
||||
}
|
||||
return sender != null && recipient != null
|
||||
}
|
||||
|
||||
abstract fun toProto(): SignalServiceProtos.Content?
|
||||
|
||||
fun setGroupContext(dataMessage: SignalServiceProtos.DataMessage.Builder) {
|
||||
val groupProto = SignalServiceProtos.GroupContext.newBuilder()
|
||||
val groupID = GroupUtil.doubleEncodeGroupID(recipient!!)
|
||||
groupProto.id = ByteString.copyFrom(GroupUtil.getDecodedGroupIDAsData(groupID))
|
||||
groupProto.type = SignalServiceProtos.GroupContext.Type.DELIVER
|
||||
dataMessage.group = groupProto.build()
|
||||
}
|
||||
|
||||
}
|
@@ -1,12 +1,17 @@
|
||||
package org.session.libsession.messaging.messages.control
|
||||
|
||||
import com.google.protobuf.ByteString
|
||||
import org.session.libsession.messaging.MessagingConfiguration
|
||||
import org.session.libsession.messaging.threads.Address
|
||||
import org.session.libsession.messaging.threads.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.utilities.logging.Log
|
||||
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.utilities.Hex
|
||||
|
||||
@@ -25,18 +30,30 @@ class ClosedGroupControlMessage() : ControlMessage() {
|
||||
|
||||
// Kind enum
|
||||
sealed class Kind {
|
||||
class New(val publicKey: ByteString, val name: String, val encryptionKeyPair: ECKeyPair, val members: List<ByteString>, val admins: List<ByteString>) : Kind()
|
||||
class New(var publicKey: ByteString, var name: String, var encryptionKeyPair: ECKeyPair?, var members: List<ByteString>, var admins: List<ByteString>) : Kind() {
|
||||
internal constructor(): this(ByteString.EMPTY, "", null, listOf(), listOf())
|
||||
}
|
||||
/// - Note: Deprecated in favor of more explicit group updates.
|
||||
class Update(val name: String, val members: List<ByteString>) : Kind()
|
||||
class Update(var name: String, var members: List<ByteString>) : Kind() {
|
||||
internal constructor(): this("", listOf())
|
||||
}
|
||||
/// An encryption key pair encrypted for each member individually.
|
||||
///
|
||||
/// - Note: `publicKey` is only set when an encryption key pair is sent in a one-to-one context (i.e. not in a group).
|
||||
class EncryptionKeyPair(val publicKey: ByteString?, val wrappers: Collection<KeyPairWrapper>) : Kind()
|
||||
class NameChange(val name: String) : Kind()
|
||||
class MembersAdded(val members: List<ByteString>) : Kind()
|
||||
class MembersRemoved( val members: List<ByteString>) : Kind()
|
||||
object MemberLeft : Kind()
|
||||
object EncryptionKeyPairRequest: Kind()
|
||||
class EncryptionKeyPair(var publicKey: ByteString?, var wrappers: Collection<KeyPairWrapper>) : Kind() {
|
||||
internal constructor(): this(null, listOf())
|
||||
}
|
||||
class NameChange(var name: String) : Kind() {
|
||||
internal constructor(): this("")
|
||||
}
|
||||
class MembersAdded(var members: List<ByteString>) : Kind() {
|
||||
internal constructor(): this(listOf())
|
||||
}
|
||||
class MembersRemoved(var members: List<ByteString>) : Kind() {
|
||||
internal constructor(): this(listOf())
|
||||
}
|
||||
class MemberLeft() : Kind()
|
||||
class EncryptionKeyPairRequest(): Kind()
|
||||
|
||||
val description: String =
|
||||
when(this) {
|
||||
@@ -46,8 +63,8 @@ class ClosedGroupControlMessage() : ControlMessage() {
|
||||
is NameChange -> "nameChange"
|
||||
is MembersAdded -> "membersAdded"
|
||||
is MembersRemoved -> "membersRemoved"
|
||||
MemberLeft -> "memberLeft"
|
||||
EncryptionKeyPairRequest -> "encryptionKeyPairRequest"
|
||||
is MemberLeft -> "memberLeft"
|
||||
is EncryptionKeyPairRequest -> "encryptionKeyPairRequest"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,10 +108,10 @@ class ClosedGroupControlMessage() : ControlMessage() {
|
||||
kind = Kind.MembersRemoved(closedGroupControlMessageProto.membersList)
|
||||
}
|
||||
DataMessage.ClosedGroupControlMessage.Type.MEMBER_LEFT -> {
|
||||
kind = Kind.MemberLeft
|
||||
kind = Kind.MemberLeft()
|
||||
}
|
||||
DataMessage.ClosedGroupControlMessage.Type.ENCRYPTION_KEY_PAIR_REQUEST -> {
|
||||
kind = Kind.EncryptionKeyPairRequest
|
||||
kind = Kind.EncryptionKeyPairRequest()
|
||||
}
|
||||
}
|
||||
return ClosedGroupControlMessage(kind)
|
||||
@@ -112,8 +129,8 @@ class ClosedGroupControlMessage() : ControlMessage() {
|
||||
val kind = kind ?: return false
|
||||
return when(kind) {
|
||||
is Kind.New -> {
|
||||
!kind.publicKey.isEmpty && kind.name.isNotEmpty() && kind.encryptionKeyPair.publicKey != null
|
||||
&& kind.encryptionKeyPair.privateKey != null && kind.members.isNotEmpty() && kind.admins.isNotEmpty()
|
||||
!kind.publicKey.isEmpty && kind.name.isNotEmpty() && kind.encryptionKeyPair!!.publicKey != null
|
||||
&& kind.encryptionKeyPair!!.privateKey != null && kind.members.isNotEmpty() && kind.admins.isNotEmpty()
|
||||
}
|
||||
is Kind.Update -> kind.name.isNotEmpty()
|
||||
is Kind.EncryptionKeyPair -> true
|
||||
@@ -138,16 +155,10 @@ class ClosedGroupControlMessage() : ControlMessage() {
|
||||
closedGroupControlMessage.type = DataMessage.ClosedGroupControlMessage.Type.NEW
|
||||
closedGroupControlMessage.publicKey = kind.publicKey
|
||||
closedGroupControlMessage.name = kind.name
|
||||
val encryptionKeyPairAsProto = SignalServiceProtos.KeyPair.newBuilder()
|
||||
encryptionKeyPairAsProto.publicKey = ByteString.copyFrom(kind.encryptionKeyPair.publicKey.serialize())
|
||||
encryptionKeyPairAsProto.privateKey = ByteString.copyFrom(kind.encryptionKeyPair.privateKey.serialize())
|
||||
|
||||
try {
|
||||
closedGroupControlMessage.encryptionKeyPair = encryptionKeyPairAsProto.build()
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Couldn't construct closed group update proto from: $this")
|
||||
return null
|
||||
}
|
||||
val encryptionKeyPair = SignalServiceProtos.KeyPair.newBuilder()
|
||||
encryptionKeyPair.publicKey = ByteString.copyFrom(kind.encryptionKeyPair!!.publicKey.serialize().removing05PrefixIfNeeded())
|
||||
encryptionKeyPair.privateKey = ByteString.copyFrom(kind.encryptionKeyPair!!.privateKey.serialize())
|
||||
closedGroupControlMessage.encryptionKeyPair = encryptionKeyPair.build()
|
||||
closedGroupControlMessage.addAllMembers(kind.members)
|
||||
closedGroupControlMessage.addAllAdmins(kind.admins)
|
||||
}
|
||||
@@ -184,6 +195,11 @@ class ClosedGroupControlMessage() : ControlMessage() {
|
||||
val dataMessageProto = DataMessage.newBuilder()
|
||||
dataMessageProto.closedGroupControlMessage = closedGroupControlMessage.build()
|
||||
// Group context
|
||||
setGroupContext(dataMessageProto)
|
||||
// Expiration timer
|
||||
// TODO: We * want * expiration timer updates to be explicit. But currently Android will disable the expiration timer for a conversation
|
||||
// if it receives a message without the current expiration timer value attached to it...
|
||||
dataMessageProto.expireTimer = Recipient.from(MessagingConfiguration.shared.context, Address.fromSerialized(GroupUtil.doubleEncodeGroupID(recipient!!)), false).expireMessages
|
||||
contentProto.dataMessage = dataMessageProto.build()
|
||||
return contentProto.build()
|
||||
} catch (e: Exception) {
|
||||
|
@@ -14,11 +14,13 @@ import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded
|
||||
import org.session.libsignal.service.loki.utilities.toHexString
|
||||
import org.session.libsignal.utilities.Hex
|
||||
|
||||
class ConfigurationMessage(val closedGroups: List<ClosedGroup>, val openGroups: List<String>, val contacts: List<Contact>, val displayName: String, val profilePicture: String?, val profileKey: ByteArray): ControlMessage() {
|
||||
class ConfigurationMessage(var closedGroups: List<ClosedGroup>, var openGroups: List<String>, var contacts: List<Contact>, var displayName: String, var profilePicture: String?, var profileKey: ByteArray): ControlMessage() {
|
||||
|
||||
class ClosedGroup(val publicKey: String, val name: String, val encryptionKeyPair: ECKeyPair, val members: List<String>, val admins: List<String>) {
|
||||
class ClosedGroup(var publicKey: String, var name: String, var encryptionKeyPair: ECKeyPair?, var members: List<String>, var admins: List<String>) {
|
||||
val isValid: Boolean get() = members.isNotEmpty() && admins.isNotEmpty()
|
||||
|
||||
internal constructor(): this("", "", null, listOf(), listOf())
|
||||
|
||||
override fun toString(): String {
|
||||
return name
|
||||
}
|
||||
@@ -30,7 +32,7 @@ class ConfigurationMessage(val closedGroups: List<ClosedGroup>, val openGroups:
|
||||
val name = proto.name
|
||||
val encryptionKeyPairAsProto = proto.encryptionKeyPair
|
||||
val encryptionKeyPair = ECKeyPair(DjbECPublicKey(encryptionKeyPairAsProto.publicKey.toByteArray().removing05PrefixIfNeeded()),
|
||||
DjbECPrivateKey(encryptionKeyPairAsProto.privateKey.toByteArray()))
|
||||
DjbECPrivateKey(encryptionKeyPairAsProto.privateKey.toByteArray()))
|
||||
val members = proto.membersList.map { it.toByteArray().toHexString() }
|
||||
val admins = proto.adminsList.map { it.toByteArray().toHexString() }
|
||||
return ClosedGroup(publicKey, name, encryptionKeyPair, members, admins)
|
||||
@@ -42,8 +44,8 @@ class ConfigurationMessage(val closedGroups: List<ClosedGroup>, val openGroups:
|
||||
result.publicKey = ByteString.copyFrom(Hex.fromStringCondensed(publicKey))
|
||||
result.name = name
|
||||
val encryptionKeyPairAsProto = SignalServiceProtos.KeyPair.newBuilder()
|
||||
encryptionKeyPairAsProto.publicKey = ByteString.copyFrom(encryptionKeyPair.publicKey.serialize().removing05PrefixIfNeeded())
|
||||
encryptionKeyPairAsProto.privateKey = ByteString.copyFrom(encryptionKeyPair.privateKey.serialize())
|
||||
encryptionKeyPairAsProto.publicKey = ByteString.copyFrom(encryptionKeyPair!!.publicKey.serialize().removing05PrefixIfNeeded())
|
||||
encryptionKeyPairAsProto.privateKey = ByteString.copyFrom(encryptionKeyPair!!.privateKey.serialize())
|
||||
result.encryptionKeyPair = encryptionKeyPairAsProto.build()
|
||||
result.addAllMembers(members.map { ByteString.copyFrom(Hex.fromStringCondensed(it)) })
|
||||
result.addAllAdmins(admins.map { ByteString.copyFrom(Hex.fromStringCondensed(it)) })
|
||||
@@ -51,7 +53,10 @@ class ConfigurationMessage(val closedGroups: List<ClosedGroup>, val openGroups:
|
||||
}
|
||||
}
|
||||
|
||||
class Contact(val publicKey: String, val name: String, val profilePicture: String?, val profileKey: ByteArray?) {
|
||||
class Contact(var publicKey: String, var name: String, var profilePicture: String?, var profileKey: ByteArray?) {
|
||||
|
||||
internal constructor(): this("", "", null, null)
|
||||
|
||||
companion object {
|
||||
fun fromProto(proto: SignalServiceProtos.ConfigurationMessage.Contact): Contact? {
|
||||
if (!proto.hasName() || !proto.hasProfileKey()) return null
|
||||
@@ -128,6 +133,8 @@ class ConfigurationMessage(val closedGroups: List<ClosedGroup>, val openGroups:
|
||||
}
|
||||
}
|
||||
|
||||
internal constructor(): this(listOf(), listOf(), listOf(), "", null, byteArrayOf())
|
||||
|
||||
override fun toProto(): SignalServiceProtos.Content? {
|
||||
val configurationProto = SignalServiceProtos.ConfigurationMessage.newBuilder()
|
||||
configurationProto.addAllClosedGroups(closedGroups.mapNotNull { it.toProto() })
|
||||
|
@@ -1,10 +1,13 @@
|
||||
package org.session.libsession.messaging.messages.control
|
||||
|
||||
import org.session.libsession.messaging.MessagingConfiguration
|
||||
import org.session.libsession.messaging.messages.visible.VisibleMessage
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos
|
||||
|
||||
class ExpirationTimerUpdate() : ControlMessage() {
|
||||
|
||||
var syncTarget: String? = null
|
||||
var duration: Int? = 0
|
||||
|
||||
companion object {
|
||||
@@ -12,7 +15,7 @@ class ExpirationTimerUpdate() : ControlMessage() {
|
||||
|
||||
fun fromProto(proto: SignalServiceProtos.Content): ExpirationTimerUpdate? {
|
||||
val dataMessageProto = proto.dataMessage ?: return null
|
||||
val isExpirationTimerUpdate = (dataMessageProto.flags and SignalServiceProtos.DataMessage.Flags.EXPIRATION_TIMER_UPDATE_VALUE) != 0 //TODO validate that 'and' operator equivalent to Swift '&'
|
||||
val isExpirationTimerUpdate = dataMessageProto.flags.and(SignalServiceProtos.DataMessage.Flags.EXPIRATION_TIMER_UPDATE_VALUE) != 0
|
||||
if (!isExpirationTimerUpdate) return null
|
||||
val duration = dataMessageProto.expireTimer
|
||||
return ExpirationTimerUpdate(duration)
|
||||
@@ -39,6 +42,16 @@ class ExpirationTimerUpdate() : ControlMessage() {
|
||||
val dataMessageProto = SignalServiceProtos.DataMessage.newBuilder()
|
||||
dataMessageProto.flags = SignalServiceProtos.DataMessage.Flags.EXPIRATION_TIMER_UPDATE_VALUE
|
||||
dataMessageProto.expireTimer = duration
|
||||
syncTarget?.let { dataMessageProto.syncTarget = it }
|
||||
// Group context
|
||||
if (MessagingConfiguration.shared.storage.isClosedGroup(recipient!!)) {
|
||||
try {
|
||||
setGroupContext(dataMessageProto)
|
||||
} catch(e: Exception) {
|
||||
Log.w(VisibleMessage.TAG, "Couldn't construct visible message proto from: $this")
|
||||
return null
|
||||
}
|
||||
}
|
||||
val contentProto = SignalServiceProtos.Content.newBuilder()
|
||||
try {
|
||||
contentProto.dataMessage = dataMessageProto.build()
|
||||
|
@@ -5,7 +5,7 @@ import org.session.libsignal.service.internal.push.SignalServiceProtos
|
||||
|
||||
class ReadReceipt() : ControlMessage() {
|
||||
|
||||
var timestamps: LongArray? = null
|
||||
var timestamps: List<Long>? = null
|
||||
|
||||
companion object {
|
||||
const val TAG = "ReadReceipt"
|
||||
@@ -15,12 +15,12 @@ class ReadReceipt() : ControlMessage() {
|
||||
if (receiptProto.type != SignalServiceProtos.ReceiptMessage.Type.READ) return null
|
||||
val timestamps = receiptProto.timestampList
|
||||
if (timestamps.isEmpty()) return null
|
||||
return ReadReceipt(timestamps = timestamps.toLongArray())
|
||||
return ReadReceipt(timestamps = timestamps)
|
||||
}
|
||||
}
|
||||
|
||||
//constructor
|
||||
internal constructor(timestamps: LongArray?) : this() {
|
||||
internal constructor(timestamps: List<Long>?) : this() {
|
||||
this.timestamps = timestamps
|
||||
}
|
||||
|
||||
|
@@ -0,0 +1,13 @@
|
||||
package org.session.libsession.messaging.messages.signal;
|
||||
|
||||
public class IncomingEncryptedMessage extends IncomingTextMessage {
|
||||
|
||||
public IncomingEncryptedMessage(IncomingTextMessage base, String newBody) {
|
||||
super(base, newBody);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSecureMessage() {
|
||||
return true;
|
||||
}
|
||||
}
|
@@ -0,0 +1,27 @@
|
||||
package org.session.libsession.messaging.messages.signal;
|
||||
|
||||
import static org.session.libsignal.service.internal.push.SignalServiceProtos.GroupContext;
|
||||
|
||||
public class IncomingGroupMessage extends IncomingTextMessage {
|
||||
|
||||
private final GroupContext groupContext;
|
||||
|
||||
public IncomingGroupMessage(IncomingTextMessage base, GroupContext groupContext, String body) {
|
||||
super(base, body);
|
||||
this.groupContext = groupContext;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isGroup() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean isUpdate() {
|
||||
return groupContext.getType().getNumber() == GroupContext.Type.UPDATE_VALUE;
|
||||
}
|
||||
|
||||
public boolean isQuit() {
|
||||
return groupContext.getType().getNumber() == GroupContext.Type.QUIT_VALUE;
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,134 @@
|
||||
package org.session.libsession.messaging.messages.signal;
|
||||
|
||||
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.sharecontacts.Contact;
|
||||
import org.session.libsession.messaging.threads.Address;
|
||||
import org.session.libsession.messaging.sending_receiving.linkpreview.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 java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
public class IncomingMediaMessage {
|
||||
|
||||
private final Address from;
|
||||
private final Address groupId;
|
||||
private final String body;
|
||||
private final boolean push;
|
||||
private final long sentTimeMillis;
|
||||
private final int subscriptionId;
|
||||
private final long expiresIn;
|
||||
private final boolean expirationUpdate;
|
||||
private final QuoteModel quote;
|
||||
private final boolean unidentified;
|
||||
|
||||
private final List<Attachment> attachments = new LinkedList<>();
|
||||
private final List<Contact> sharedContacts = new LinkedList<>();
|
||||
private final List<LinkPreview> linkPreviews = new LinkedList<>();
|
||||
|
||||
public IncomingMediaMessage(Address from,
|
||||
long sentTimeMillis,
|
||||
int subscriptionId,
|
||||
long expiresIn,
|
||||
boolean expirationUpdate,
|
||||
boolean unidentified,
|
||||
Optional<String> body,
|
||||
Optional<SignalServiceGroup> group,
|
||||
Optional<List<SignalServiceAttachment>> attachments,
|
||||
Optional<QuoteModel> quote,
|
||||
Optional<List<Contact>> sharedContacts,
|
||||
Optional<List<LinkPreview>> linkPreviews)
|
||||
{
|
||||
this.push = true;
|
||||
this.from = from;
|
||||
this.sentTimeMillis = sentTimeMillis;
|
||||
this.body = body.orNull();
|
||||
this.subscriptionId = subscriptionId;
|
||||
this.expiresIn = expiresIn;
|
||||
this.expirationUpdate = expirationUpdate;
|
||||
this.quote = quote.orNull();
|
||||
this.unidentified = unidentified;
|
||||
|
||||
if (group.isPresent()) this.groupId = Address.fromSerialized(GroupUtil.INSTANCE.getEncodedId(group.get()));
|
||||
else this.groupId = null;
|
||||
|
||||
this.attachments.addAll(PointerAttachment.forPointers(attachments));
|
||||
this.sharedContacts.addAll(sharedContacts.or(Collections.emptyList()));
|
||||
this.linkPreviews.addAll(linkPreviews.or(Collections.emptyList()));
|
||||
}
|
||||
|
||||
public static IncomingMediaMessage from(VisibleMessage message,
|
||||
Address from,
|
||||
long expiresIn,
|
||||
Optional<SignalServiceGroup> group,
|
||||
Optional<List<SignalServiceAttachment>> attachments,
|
||||
Optional<QuoteModel> quote,
|
||||
Optional<List<LinkPreview>> linkPreviews)
|
||||
{
|
||||
return new IncomingMediaMessage(from, message.getReceivedTimestamp(), -1, expiresIn, false,
|
||||
false, Optional.fromNullable(message.getText()), group, attachments, quote, Optional.absent(), linkPreviews);
|
||||
}
|
||||
|
||||
public int getSubscriptionId() {
|
||||
return subscriptionId;
|
||||
}
|
||||
|
||||
public String getBody() {
|
||||
return body;
|
||||
}
|
||||
|
||||
public List<Attachment> getAttachments() {
|
||||
return attachments;
|
||||
}
|
||||
|
||||
public Address getFrom() {
|
||||
return from;
|
||||
}
|
||||
|
||||
public Address getGroupId() {
|
||||
return groupId;
|
||||
}
|
||||
|
||||
public boolean isPushMessage() {
|
||||
return push;
|
||||
}
|
||||
|
||||
public boolean isExpirationUpdate() {
|
||||
return expirationUpdate;
|
||||
}
|
||||
|
||||
public long getSentTimeMillis() {
|
||||
return sentTimeMillis;
|
||||
}
|
||||
|
||||
public long getExpiresIn() {
|
||||
return expiresIn;
|
||||
}
|
||||
|
||||
public boolean isGroupMessage() {
|
||||
return groupId != null;
|
||||
}
|
||||
|
||||
public QuoteModel getQuote() {
|
||||
return quote;
|
||||
}
|
||||
|
||||
public List<Contact> getSharedContacts() {
|
||||
return sharedContacts;
|
||||
}
|
||||
|
||||
public List<LinkPreview> getLinkPreviews() {
|
||||
return linkPreviews;
|
||||
}
|
||||
|
||||
public boolean isUnidentified() {
|
||||
return unidentified;
|
||||
}
|
||||
}
|
@@ -0,0 +1,185 @@
|
||||
package org.session.libsession.messaging.messages.signal;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.session.libsession.messaging.messages.visible.VisibleMessage;
|
||||
import org.session.libsession.messaging.threads.Address;
|
||||
import org.session.libsession.utilities.GroupUtil;
|
||||
import org.session.libsignal.libsignal.util.guava.Optional;
|
||||
import org.session.libsignal.service.api.messages.SignalServiceGroup;
|
||||
|
||||
public class IncomingTextMessage implements Parcelable {
|
||||
|
||||
public static final Parcelable.Creator<IncomingTextMessage> CREATOR = new Parcelable.Creator<IncomingTextMessage>() {
|
||||
@Override
|
||||
public IncomingTextMessage createFromParcel(Parcel in) {
|
||||
return new IncomingTextMessage(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public IncomingTextMessage[] newArray(int size) {
|
||||
return new IncomingTextMessage[size];
|
||||
}
|
||||
};
|
||||
private static final String TAG = IncomingTextMessage.class.getSimpleName();
|
||||
|
||||
private final String message;
|
||||
private Address sender;
|
||||
private final int senderDeviceId;
|
||||
private final int protocol;
|
||||
private final String serviceCenterAddress;
|
||||
private final boolean replyPathPresent;
|
||||
private final String pseudoSubject;
|
||||
private final long sentTimestampMillis;
|
||||
private final Address groupId;
|
||||
private final boolean push;
|
||||
private final int subscriptionId;
|
||||
private final long expiresInMillis;
|
||||
private final boolean unidentified;
|
||||
|
||||
public IncomingTextMessage(Address sender, int senderDeviceId, long sentTimestampMillis,
|
||||
String encodedBody, Optional<SignalServiceGroup> group,
|
||||
long expiresInMillis, boolean unidentified)
|
||||
{
|
||||
this.message = encodedBody;
|
||||
this.sender = sender;
|
||||
this.senderDeviceId = senderDeviceId;
|
||||
this.protocol = 31337;
|
||||
this.serviceCenterAddress = "GCM";
|
||||
this.replyPathPresent = true;
|
||||
this.pseudoSubject = "";
|
||||
this.sentTimestampMillis = sentTimestampMillis;
|
||||
this.push = true;
|
||||
this.subscriptionId = -1;
|
||||
this.expiresInMillis = expiresInMillis;
|
||||
this.unidentified = unidentified;
|
||||
|
||||
if (group.isPresent()) {
|
||||
this.groupId = Address.fromSerialized(GroupUtil.getEncodedId(group.get()));
|
||||
} else {
|
||||
this.groupId = null;
|
||||
}
|
||||
}
|
||||
|
||||
public IncomingTextMessage(Parcel in) {
|
||||
this.message = in.readString();
|
||||
this.sender = in.readParcelable(IncomingTextMessage.class.getClassLoader());
|
||||
this.senderDeviceId = in.readInt();
|
||||
this.protocol = in.readInt();
|
||||
this.serviceCenterAddress = in.readString();
|
||||
this.replyPathPresent = (in.readInt() == 1);
|
||||
this.pseudoSubject = in.readString();
|
||||
this.sentTimestampMillis = in.readLong();
|
||||
this.groupId = in.readParcelable(IncomingTextMessage.class.getClassLoader());
|
||||
this.push = (in.readInt() == 1);
|
||||
this.subscriptionId = in.readInt();
|
||||
this.expiresInMillis = in.readLong();
|
||||
this.unidentified = in.readInt() == 1;
|
||||
}
|
||||
|
||||
public IncomingTextMessage(IncomingTextMessage base, String newBody) {
|
||||
this.message = newBody;
|
||||
this.sender = base.getSender();
|
||||
this.senderDeviceId = base.getSenderDeviceId();
|
||||
this.protocol = base.getProtocol();
|
||||
this.serviceCenterAddress = base.getServiceCenterAddress();
|
||||
this.replyPathPresent = base.isReplyPathPresent();
|
||||
this.pseudoSubject = base.getPseudoSubject();
|
||||
this.sentTimestampMillis = base.getSentTimestampMillis();
|
||||
this.groupId = base.getGroupId();
|
||||
this.push = base.isPush();
|
||||
this.subscriptionId = base.getSubscriptionId();
|
||||
this.expiresInMillis = base.getExpiresIn();
|
||||
this.unidentified = base.isUnidentified();
|
||||
}
|
||||
|
||||
public static IncomingTextMessage from(VisibleMessage message,
|
||||
Address sender,
|
||||
Optional<SignalServiceGroup> group,
|
||||
long expiresInMillis)
|
||||
{
|
||||
return new IncomingTextMessage(sender, 1, message.getReceivedTimestamp(), message.getText(), group, expiresInMillis, false);
|
||||
}
|
||||
|
||||
public int getSubscriptionId() {
|
||||
return subscriptionId;
|
||||
}
|
||||
|
||||
public long getExpiresIn() {
|
||||
return expiresInMillis;
|
||||
}
|
||||
|
||||
public long getSentTimestampMillis() {
|
||||
return sentTimestampMillis;
|
||||
}
|
||||
|
||||
public String getPseudoSubject() {
|
||||
return pseudoSubject;
|
||||
}
|
||||
|
||||
public String getMessageBody() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public Address getSender() {
|
||||
return sender;
|
||||
}
|
||||
|
||||
public int getSenderDeviceId() {
|
||||
return senderDeviceId;
|
||||
}
|
||||
|
||||
public int getProtocol() {
|
||||
return protocol;
|
||||
}
|
||||
|
||||
public String getServiceCenterAddress() {
|
||||
return serviceCenterAddress;
|
||||
}
|
||||
|
||||
public boolean isReplyPathPresent() {
|
||||
return replyPathPresent;
|
||||
}
|
||||
|
||||
public boolean isSecureMessage() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean isPush() {
|
||||
return push;
|
||||
}
|
||||
|
||||
public @Nullable Address getGroupId() {
|
||||
return groupId;
|
||||
}
|
||||
|
||||
public boolean isGroup() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean isUnidentified() {
|
||||
return unidentified;
|
||||
}
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel out, int flags) {
|
||||
out.writeString(message);
|
||||
out.writeParcelable(sender, flags);
|
||||
out.writeInt(senderDeviceId);
|
||||
out.writeInt(protocol);
|
||||
out.writeString(serviceCenterAddress);
|
||||
out.writeInt(replyPathPresent ? 1 : 0);
|
||||
out.writeString(pseudoSubject);
|
||||
out.writeLong(sentTimestampMillis);
|
||||
out.writeParcelable(groupId, flags);
|
||||
out.writeInt(push ? 1 : 0);
|
||||
out.writeInt(subscriptionId);
|
||||
out.writeInt(unidentified ? 1 : 0);
|
||||
}
|
||||
}
|
@@ -0,0 +1,29 @@
|
||||
package org.session.libsession.messaging.messages.signal;
|
||||
|
||||
import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate;
|
||||
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 java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
|
||||
public class OutgoingExpirationUpdateMessage extends OutgoingSecureMediaMessage {
|
||||
|
||||
public OutgoingExpirationUpdateMessage(Recipient recipient, long sentTimeMillis, long expiresIn) {
|
||||
super(recipient, "", new LinkedList<Attachment>(), sentTimeMillis,
|
||||
DistributionTypes.CONVERSATION, expiresIn, null, Collections.emptyList(),
|
||||
Collections.emptyList());
|
||||
}
|
||||
|
||||
public static OutgoingExpirationUpdateMessage from(ExpirationTimerUpdate message,
|
||||
Recipient recipient) {
|
||||
return new OutgoingExpirationUpdateMessage(recipient, message.getSentTimestamp(), message.getDuration() * 1000);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isExpirationUpdate() {
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,89 @@
|
||||
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.messaging.sending_receiving.attachments.Attachment;
|
||||
import org.session.libsession.messaging.sending_receiving.sharecontacts.Contact;
|
||||
import org.session.libsession.messaging.sending_receiving.linkpreview.LinkPreview;
|
||||
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel;
|
||||
import org.session.libsession.messaging.threads.recipients.Recipient;
|
||||
import org.session.libsignal.utilities.Base64;
|
||||
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos.GroupContext;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
public class OutgoingGroupMediaMessage extends OutgoingSecureMediaMessage {
|
||||
|
||||
private final GroupContext group;
|
||||
|
||||
public OutgoingGroupMediaMessage(@NonNull Recipient recipient,
|
||||
@NonNull String encodedGroupContext,
|
||||
@NonNull List<Attachment> avatar,
|
||||
long sentTimeMillis,
|
||||
long expiresIn,
|
||||
@Nullable QuoteModel quote,
|
||||
@NonNull List<Contact> contacts,
|
||||
@NonNull List<LinkPreview> previews)
|
||||
throws IOException
|
||||
{
|
||||
super(recipient, encodedGroupContext, avatar, sentTimeMillis,
|
||||
DistributionTypes.CONVERSATION, expiresIn, quote, contacts, previews);
|
||||
|
||||
this.group = GroupContext.parseFrom(Base64.decode(encodedGroupContext));
|
||||
}
|
||||
|
||||
public OutgoingGroupMediaMessage(@NonNull Recipient recipient,
|
||||
@NonNull GroupContext group,
|
||||
@Nullable final Attachment avatar,
|
||||
long expireIn,
|
||||
@Nullable QuoteModel quote,
|
||||
@NonNull List<Contact> contacts,
|
||||
@NonNull List<LinkPreview> previews)
|
||||
{
|
||||
super(recipient, Base64.encodeBytes(group.toByteArray()),
|
||||
new LinkedList<Attachment>() {{if (avatar != null) add(avatar);}},
|
||||
System.currentTimeMillis(),
|
||||
DistributionTypes.CONVERSATION, expireIn, quote, contacts, previews);
|
||||
|
||||
this.group = group;
|
||||
}
|
||||
|
||||
public OutgoingGroupMediaMessage(@NonNull Recipient recipient,
|
||||
@NonNull GroupContext group,
|
||||
@Nullable final Attachment avatar,
|
||||
long sentTime,
|
||||
long expireIn,
|
||||
@Nullable QuoteModel quote,
|
||||
@NonNull List<Contact> contacts,
|
||||
@NonNull List<LinkPreview> previews)
|
||||
{
|
||||
super(recipient, Base64.encodeBytes(group.toByteArray()),
|
||||
new LinkedList<Attachment>() {{if (avatar != null) add(avatar);}},
|
||||
sentTime,
|
||||
DistributionTypes.CONVERSATION, expireIn, quote, contacts, previews);
|
||||
|
||||
this.group = group;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isGroup() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean isGroupUpdate() {
|
||||
return group.getType().getNumber() == GroupContext.Type.UPDATE_VALUE;
|
||||
}
|
||||
|
||||
public boolean isGroupQuit() {
|
||||
return group.getType().getNumber() == GroupContext.Type.QUIT_VALUE;
|
||||
}
|
||||
|
||||
public GroupContext getGroupContext() {
|
||||
return group;
|
||||
}
|
||||
}
|
@@ -0,0 +1,140 @@
|
||||
package org.session.libsession.messaging.messages.signal;
|
||||
|
||||
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.messaging.sending_receiving.attachments.Attachment;
|
||||
import org.session.libsession.messaging.sending_receiving.sharecontacts.Contact;
|
||||
import org.session.libsession.messaging.sending_receiving.linkpreview.LinkPreview;
|
||||
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel;
|
||||
import org.session.libsession.messaging.threads.recipients.Recipient;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
public class OutgoingMediaMessage {
|
||||
|
||||
private final Recipient recipient;
|
||||
protected final String body;
|
||||
protected final List<Attachment> attachments;
|
||||
private final long sentTimeMillis;
|
||||
private final int distributionType;
|
||||
private final int subscriptionId;
|
||||
private final long expiresIn;
|
||||
private final QuoteModel outgoingQuote;
|
||||
|
||||
private final List<NetworkFailure> networkFailures = new LinkedList<>();
|
||||
private final List<IdentityKeyMismatch> identityKeyMismatches = new LinkedList<>();
|
||||
private final List<Contact> contacts = new LinkedList<>();
|
||||
private final List<LinkPreview> linkPreviews = new LinkedList<>();
|
||||
|
||||
public OutgoingMediaMessage(Recipient recipient, String message,
|
||||
List<Attachment> attachments, long sentTimeMillis,
|
||||
int subscriptionId, long expiresIn,
|
||||
int distributionType,
|
||||
@Nullable QuoteModel outgoingQuote,
|
||||
@NonNull List<Contact> contacts,
|
||||
@NonNull List<LinkPreview> linkPreviews,
|
||||
@NonNull List<NetworkFailure> networkFailures,
|
||||
@NonNull List<IdentityKeyMismatch> identityKeyMismatches)
|
||||
{
|
||||
this.recipient = recipient;
|
||||
this.body = message;
|
||||
this.sentTimeMillis = sentTimeMillis;
|
||||
this.distributionType = distributionType;
|
||||
this.attachments = attachments;
|
||||
this.subscriptionId = subscriptionId;
|
||||
this.expiresIn = expiresIn;
|
||||
this.outgoingQuote = outgoingQuote;
|
||||
|
||||
this.contacts.addAll(contacts);
|
||||
this.linkPreviews.addAll(linkPreviews);
|
||||
this.networkFailures.addAll(networkFailures);
|
||||
this.identityKeyMismatches.addAll(identityKeyMismatches);
|
||||
}
|
||||
|
||||
public OutgoingMediaMessage(OutgoingMediaMessage that) {
|
||||
this.recipient = that.getRecipient();
|
||||
this.body = that.body;
|
||||
this.distributionType = that.distributionType;
|
||||
this.attachments = that.attachments;
|
||||
this.sentTimeMillis = that.sentTimeMillis;
|
||||
this.subscriptionId = that.subscriptionId;
|
||||
this.expiresIn = that.expiresIn;
|
||||
this.outgoingQuote = that.outgoingQuote;
|
||||
|
||||
this.identityKeyMismatches.addAll(that.identityKeyMismatches);
|
||||
this.networkFailures.addAll(that.networkFailures);
|
||||
this.contacts.addAll(that.contacts);
|
||||
this.linkPreviews.addAll(that.linkPreviews);
|
||||
}
|
||||
|
||||
public static OutgoingMediaMessage from(VisibleMessage message,
|
||||
Recipient recipient,
|
||||
List<Attachment> attachments,
|
||||
@Nullable QuoteModel outgoingQuote,
|
||||
@Nullable LinkPreview linkPreview)
|
||||
{
|
||||
List<LinkPreview> previews = Collections.emptyList();
|
||||
if (linkPreview != null) {
|
||||
previews = Collections.singletonList(linkPreview);
|
||||
}
|
||||
return new OutgoingMediaMessage(recipient, message.getText(), attachments, message.getSentTimestamp(), -1,
|
||||
recipient.getExpireMessages() * 1000, DistributionTypes.DEFAULT, outgoingQuote, Collections.emptyList(),
|
||||
previews, Collections.emptyList(), Collections.emptyList());
|
||||
}
|
||||
|
||||
public Recipient getRecipient() {
|
||||
return recipient;
|
||||
}
|
||||
|
||||
public String getBody() {
|
||||
return body;
|
||||
}
|
||||
|
||||
public List<Attachment> getAttachments() {
|
||||
return attachments;
|
||||
}
|
||||
|
||||
public boolean isSecure() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean isGroup() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean isExpirationUpdate() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public long getSentTimeMillis() {
|
||||
return sentTimeMillis;
|
||||
}
|
||||
|
||||
public int getSubscriptionId() {
|
||||
return subscriptionId;
|
||||
}
|
||||
|
||||
public long getExpiresIn() {
|
||||
return expiresIn;
|
||||
}
|
||||
|
||||
public @Nullable QuoteModel getOutgoingQuote() {
|
||||
return outgoingQuote;
|
||||
}
|
||||
|
||||
public @NonNull List<Contact> getSharedContacts() {
|
||||
return contacts;
|
||||
}
|
||||
|
||||
public @NonNull List<LinkPreview> getLinkPreviews() {
|
||||
return linkPreviews;
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,37 @@
|
||||
package org.session.libsession.messaging.messages.signal;
|
||||
|
||||
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.messaging.sending_receiving.linkpreview.LinkPreview;
|
||||
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel;
|
||||
import org.session.libsession.messaging.threads.recipients.Recipient;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class OutgoingSecureMediaMessage extends OutgoingMediaMessage {
|
||||
|
||||
public OutgoingSecureMediaMessage(Recipient recipient, String body,
|
||||
List<Attachment> attachments,
|
||||
long sentTimeMillis,
|
||||
int distributionType,
|
||||
long expiresIn,
|
||||
@Nullable QuoteModel quote,
|
||||
@NonNull List<Contact> contacts,
|
||||
@NonNull List<LinkPreview> previews)
|
||||
{
|
||||
super(recipient, body, attachments, sentTimeMillis, -1, expiresIn, distributionType, quote, contacts, previews, Collections.emptyList(), Collections.emptyList());
|
||||
}
|
||||
|
||||
public OutgoingSecureMediaMessage(OutgoingMediaMessage base) {
|
||||
super(base);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSecure() {
|
||||
return true;
|
||||
}
|
||||
}
|
@@ -0,0 +1,43 @@
|
||||
package org.session.libsession.messaging.messages.signal;
|
||||
|
||||
import org.session.libsession.messaging.messages.visible.VisibleMessage;
|
||||
import org.session.libsession.messaging.threads.recipients.Recipient;
|
||||
|
||||
public class OutgoingTextMessage {
|
||||
|
||||
private final Recipient recipient;
|
||||
private final String message;
|
||||
private final int subscriptionId;
|
||||
private final long expiresIn;
|
||||
|
||||
public OutgoingTextMessage(Recipient recipient, String message, long expiresIn, int subscriptionId) {
|
||||
this.recipient = recipient;
|
||||
this.message = message;
|
||||
this.expiresIn = expiresIn;
|
||||
this.subscriptionId = subscriptionId;
|
||||
}
|
||||
|
||||
public static OutgoingTextMessage from(VisibleMessage message, Recipient recipient) {
|
||||
return new OutgoingTextMessage(recipient, message.getText(), recipient.getExpireMessages() * 1000, -1);
|
||||
}
|
||||
|
||||
public long getExpiresIn() {
|
||||
return expiresIn;
|
||||
}
|
||||
|
||||
public int getSubscriptionId() {
|
||||
return subscriptionId;
|
||||
}
|
||||
|
||||
public String getMessageBody() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public Recipient getRecipient() {
|
||||
return recipient;
|
||||
}
|
||||
|
||||
public boolean isSecureMessage() {
|
||||
return true;
|
||||
}
|
||||
}
|
@@ -2,8 +2,11 @@ package org.session.libsession.messaging.messages.visible
|
||||
|
||||
import android.util.Size
|
||||
import android.webkit.MimeTypeMap
|
||||
import org.session.libsession.database.MessageDataProvider
|
||||
import com.google.protobuf.ByteString
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
|
||||
import org.session.libsignal.service.api.messages.SignalServiceAttachmentPointer
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.Attachment as SignalAttachment
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos
|
||||
import java.io.File
|
||||
|
||||
@@ -32,7 +35,7 @@ class Attachment {
|
||||
result.contentType = proto.contentType ?: inferContentType()
|
||||
result.key = proto.key.toByteArray()
|
||||
result.digest = proto.digest.toByteArray()
|
||||
val kind: Kind = if (proto.hasFlags() && (proto.flags and SignalServiceProtos.AttachmentPointer.Flags.VOICE_MESSAGE_VALUE) > 0) { //TODO validate that 'and' operator = swift '&'
|
||||
val kind: Kind = if (proto.hasFlags() && proto.flags.and(SignalServiceProtos.AttachmentPointer.Flags.VOICE_MESSAGE_VALUE) > 0) {
|
||||
Kind.VOICE_MESSAGE
|
||||
} else {
|
||||
Kind.GENERIC
|
||||
@@ -42,13 +45,42 @@ class Attachment {
|
||||
val size: Size = if (proto.hasWidth() && proto.width > 0 && proto.hasHeight() && proto.height > 0) {
|
||||
Size(proto.width, proto.height)
|
||||
} else {
|
||||
Size(0,0) //TODO check that it's equivalent to swift: CGSize.zero
|
||||
Size(0,0)
|
||||
}
|
||||
result.size = size
|
||||
result.sizeInBytes = if (proto.size > 0) proto.size else null
|
||||
result. url = proto.url
|
||||
return result
|
||||
}
|
||||
|
||||
fun createAttachmentPointer(attachment: SignalServiceAttachmentPointer): SignalServiceProtos.AttachmentPointer? {
|
||||
val builder = SignalServiceProtos.AttachmentPointer.newBuilder()
|
||||
.setContentType(attachment.contentType)
|
||||
.setId(attachment.id)
|
||||
.setKey(ByteString.copyFrom(attachment.key))
|
||||
.setDigest(ByteString.copyFrom(attachment.digest.get()))
|
||||
.setSize(attachment.size.get())
|
||||
.setUrl(attachment.url)
|
||||
if (attachment.fileName.isPresent) {
|
||||
builder.fileName = attachment.fileName.get()
|
||||
}
|
||||
if (attachment.preview.isPresent) {
|
||||
builder.thumbnail = ByteString.copyFrom(attachment.preview.get())
|
||||
}
|
||||
if (attachment.width > 0) {
|
||||
builder.width = attachment.width
|
||||
}
|
||||
if (attachment.height > 0) {
|
||||
builder.height = attachment.height
|
||||
}
|
||||
if (attachment.voiceNote) {
|
||||
builder.flags = SignalServiceProtos.AttachmentPointer.Flags.VOICE_MESSAGE_VALUE
|
||||
}
|
||||
if (attachment.caption.isPresent) {
|
||||
builder.caption = attachment.caption.get()
|
||||
}
|
||||
return builder.build()
|
||||
}
|
||||
}
|
||||
|
||||
enum class Kind {
|
||||
@@ -66,8 +98,9 @@ class Attachment {
|
||||
TODO("Not implemented")
|
||||
}
|
||||
|
||||
fun toDatabaseAttachment(): org.session.libsession.messaging.sending_receiving.attachments.Attachment {
|
||||
return DatabaseAttachment(null, 0, true, true, contentType, 0,
|
||||
fun toSignalAttachment(): SignalAttachment? {
|
||||
if (!isValid()) return null
|
||||
return DatabaseAttachment(null, 0, false, false, contentType, 0,
|
||||
sizeInBytes?.toLong() ?: 0, fileName, null, key.toString(), null, digest, null, kind == Kind.VOICE_MESSAGE,
|
||||
size?.width ?: 0, size?.height ?: 0, false, caption, url)
|
||||
}
|
||||
|
@@ -1,6 +1,7 @@
|
||||
package org.session.libsession.messaging.messages.visible
|
||||
|
||||
import org.session.libsession.messaging.MessagingConfiguration
|
||||
import org.session.libsession.messaging.sending_receiving.linkpreview.LinkPreview as SignalLinkPreiview
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos
|
||||
|
||||
@@ -18,6 +19,14 @@ class LinkPreview() {
|
||||
val url = proto.url
|
||||
return LinkPreview(title, url, null)
|
||||
}
|
||||
|
||||
fun from(signalLinkPreview: SignalLinkPreiview?): LinkPreview? {
|
||||
return if (signalLinkPreview == null) {
|
||||
null
|
||||
} else {
|
||||
LinkPreview(signalLinkPreview.title, signalLinkPreview.url, signalLinkPreview.attachmentId?.rowId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//constructor
|
||||
@@ -44,8 +53,10 @@ class LinkPreview() {
|
||||
title?.let { linkPreviewProto.title = title }
|
||||
val attachmentID = attachmentID
|
||||
attachmentID?.let {
|
||||
val attachmentProto = MessagingConfiguration.shared.messageDataProvider.getAttachmentStream(attachmentID)
|
||||
attachmentProto?.let { linkPreviewProto.image = attachmentProto.toProto() }
|
||||
MessagingConfiguration.shared.messageDataProvider.getSignalAttachmentPointer(attachmentID)?.let {
|
||||
val attachmentProto = Attachment.createAttachmentPointer(it)
|
||||
linkPreviewProto.image = attachmentProto
|
||||
}
|
||||
}
|
||||
// Build
|
||||
try {
|
||||
|
@@ -2,6 +2,8 @@ package org.session.libsession.messaging.messages.visible
|
||||
|
||||
import com.goterl.lazycode.lazysodium.BuildConfig
|
||||
import org.session.libsession.messaging.MessagingConfiguration
|
||||
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
|
||||
|
||||
@@ -21,6 +23,15 @@ class Quote() {
|
||||
val text = proto.text
|
||||
return Quote(timestamp, publicKey, text, null)
|
||||
}
|
||||
|
||||
fun from(signalQuote: SignalQuote?): Quote? {
|
||||
return if (signalQuote == null) {
|
||||
null
|
||||
} else {
|
||||
val attachmentID = (signalQuote.attachments?.firstOrNull() as? DatabaseAttachment)?.attachmentId?.rowId
|
||||
Quote(signalQuote.id, signalQuote.author.serialize(), signalQuote.text, attachmentID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//constructor
|
||||
@@ -58,13 +69,13 @@ class Quote() {
|
||||
}
|
||||
|
||||
private fun addAttachmentsIfNeeded(quoteProto: SignalServiceProtos.DataMessage.Quote.Builder) {
|
||||
val attachmentID = attachmentID ?: return
|
||||
val attachmentProto = MessagingConfiguration.shared.messageDataProvider.getAttachmentStream(attachmentID)
|
||||
if (attachmentProto == null) {
|
||||
if (attachmentID == null) return
|
||||
val attachment = MessagingConfiguration.shared.messageDataProvider.getSignalAttachmentPointer(attachmentID!!)
|
||||
if (attachment == null) {
|
||||
Log.w(TAG, "Ignoring invalid attachment for quoted message.")
|
||||
return
|
||||
}
|
||||
if (!attachmentProto.isUploaded) {
|
||||
if (attachment.url.isNullOrEmpty()) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
//TODO equivalent to iOS's preconditionFailure
|
||||
Log.d(TAG,"Sending a message before all associated attachments have been uploaded.")
|
||||
@@ -72,10 +83,9 @@ class Quote() {
|
||||
}
|
||||
}
|
||||
val quotedAttachmentProto = SignalServiceProtos.DataMessage.Quote.QuotedAttachment.newBuilder()
|
||||
quotedAttachmentProto.contentType = attachmentProto.contentType
|
||||
val fileName = attachmentProto.fileName?.get()
|
||||
fileName?.let { quotedAttachmentProto.fileName = fileName }
|
||||
quotedAttachmentProto.thumbnail = attachmentProto.toProto()
|
||||
quotedAttachmentProto.contentType = attachment.contentType
|
||||
if (attachment.fileName.isPresent) quotedAttachmentProto.fileName = attachment.fileName.get()
|
||||
quotedAttachmentProto.thumbnail = Attachment.createAttachmentPointer(attachment)
|
||||
try {
|
||||
quoteProto.addAttachments(quotedAttachmentProto.build())
|
||||
} catch (e: Exception) {
|
||||
|
@@ -1,12 +1,15 @@
|
||||
package org.session.libsession.messaging.messages.visible
|
||||
|
||||
import com.goterl.lazycode.lazysodium.BuildConfig
|
||||
|
||||
import org.session.libsession.messaging.MessagingConfiguration
|
||||
import org.session.libsession.messaging.messages.Message
|
||||
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
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.GroupUtil
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.Attachment as SignalAttachment
|
||||
|
||||
class VisibleMessage : Message() {
|
||||
|
||||
@@ -46,6 +49,14 @@ class VisibleMessage : Message() {
|
||||
}
|
||||
}
|
||||
|
||||
fun addSignalAttachments(signalAttachments: List<SignalAttachment>) {
|
||||
val attachmentIDs = signalAttachments.map {
|
||||
val databaseAttachment = it as DatabaseAttachment
|
||||
databaseAttachment.attachmentId.rowId
|
||||
}
|
||||
this.attachmentIDs = attachmentIDs as ArrayList<Long>
|
||||
}
|
||||
|
||||
fun isMediaMessage(): Boolean {
|
||||
return attachmentIDs.isNotEmpty() || quote != null || linkPreview != null || contact != null
|
||||
}
|
||||
@@ -61,7 +72,6 @@ class VisibleMessage : Message() {
|
||||
|
||||
override fun toProto(): SignalServiceProtos.Content? {
|
||||
val proto = SignalServiceProtos.Content.newBuilder()
|
||||
var attachmentIDs = this.attachmentIDs
|
||||
val dataMessage: SignalServiceProtos.DataMessage.Builder
|
||||
// Profile
|
||||
val profile = profile
|
||||
@@ -74,44 +84,49 @@ class VisibleMessage : Message() {
|
||||
// Text
|
||||
text?.let { dataMessage.body = text }
|
||||
// Quote
|
||||
val quotedAttachmentID = quote?.attachmentID
|
||||
quotedAttachmentID?.let {
|
||||
val index = attachmentIDs.indexOf(quotedAttachmentID)
|
||||
if (index >= 0) { attachmentIDs.removeAt(index) }
|
||||
}
|
||||
val quote = quote
|
||||
quote?.let {
|
||||
val quoteProto = quote.toProto()
|
||||
val quoteProto = it.toProto()
|
||||
if (quoteProto != null) dataMessage.quote = quoteProto
|
||||
}
|
||||
//Link preview
|
||||
val linkPreviewAttachmentID = linkPreview?.attachmentID
|
||||
linkPreviewAttachmentID?.let {
|
||||
val index = attachmentIDs.indexOf(quotedAttachmentID)
|
||||
if (index >= 0) { attachmentIDs.removeAt(index) }
|
||||
}
|
||||
val linkPreview = linkPreview
|
||||
linkPreview?.let {
|
||||
val linkPreviewProto = linkPreview.toProto()
|
||||
val linkPreviewProto = it.toProto()
|
||||
linkPreviewProto?.let {
|
||||
dataMessage.addAllPreview(listOf(linkPreviewProto))
|
||||
}
|
||||
}
|
||||
//Attachments
|
||||
val attachments = attachmentIDs.mapNotNull { MessagingConfiguration.shared.messageDataProvider.getAttachmentStream(it) }
|
||||
if (!attachments.all { it.isUploaded }) {
|
||||
val attachments = attachmentIDs.mapNotNull { MessagingConfiguration.shared.messageDataProvider.getSignalAttachmentPointer(it) }
|
||||
if (!attachments.all { !it.url.isNullOrEmpty() }) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
//TODO equivalent to iOS's preconditionFailure
|
||||
Log.d(TAG,"Sending a message before all associated attachments have been uploaded.")
|
||||
Log.d(TAG, "Sending a message before all associated attachments have been uploaded.")
|
||||
}
|
||||
}
|
||||
val attachmentPointers = attachments.mapNotNull { Attachment.createAttachmentPointer(it) }
|
||||
dataMessage.addAllAttachments(attachmentPointers)
|
||||
// TODO Contact
|
||||
// Expiration timer
|
||||
// TODO: We * want * expiration timer updates to be explicit. But currently Android will disable the expiration timer for a conversation
|
||||
// if it receives a message without the current expiration timer value attached to it...
|
||||
val storage = MessagingConfiguration.shared.storage
|
||||
val context = MessagingConfiguration.shared.context
|
||||
val expiration = if (storage.isClosedGroup(recipient!!)) Recipient.from(context, Address.fromSerialized(GroupUtil.doubleEncodeGroupID(recipient!!)), false).expireMessages
|
||||
else Recipient.from(context, Address.fromSerialized(recipient!!), false).expireMessages
|
||||
dataMessage.expireTimer = expiration
|
||||
// Group context
|
||||
if (storage.isClosedGroup(recipient!!)) {
|
||||
try {
|
||||
setGroupContext(dataMessage)
|
||||
} catch(e: Exception) {
|
||||
Log.w(TAG, "Couldn't construct visible message proto from: $this")
|
||||
return null
|
||||
}
|
||||
}
|
||||
val attachmentProtos = attachments.mapNotNull { it.toProto() }
|
||||
dataMessage.addAllAttachments(attachmentProtos)
|
||||
// Sync target
|
||||
if (syncTarget != null) {
|
||||
dataMessage.syncTarget = syncTarget
|
||||
}
|
||||
// TODO Contact
|
||||
// Build
|
||||
try {
|
||||
proto.dataMessage = dataMessage.build()
|
||||
|
@@ -41,6 +41,7 @@ object OpenGroupAPI: DotNetAPI() {
|
||||
return listOf() // Don't auto-join any open groups right now
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun isUserModerator(hexEncodedPublicKey: String, channel: Long, server: String): Boolean {
|
||||
if (moderators[server] != null && moderators[server]!![channel] != null) {
|
||||
return moderators[server]!![channel]!!.contains(hexEncodedPublicKey)
|
||||
@@ -113,7 +114,7 @@ object OpenGroupAPI: DotNetAPI() {
|
||||
val id = attachmentAsJSON["id"] as? Long ?: (attachmentAsJSON["id"] as? Int)?.toLong() ?: (attachmentAsJSON["id"] as String).toLong()
|
||||
val contentType = attachmentAsJSON["contentType"] as String
|
||||
val size = attachmentAsJSON["size"] as? Int ?: (attachmentAsJSON["size"] as? Long)?.toInt() ?: (attachmentAsJSON["size"] as String).toInt()
|
||||
val fileName = attachmentAsJSON["fileName"] as String
|
||||
val fileName = attachmentAsJSON["fileName"] as? String
|
||||
val flags = 0
|
||||
val url = attachmentAsJSON["url"] as String
|
||||
val caption = attachmentAsJSON["caption"] as? String
|
||||
@@ -209,8 +210,7 @@ object OpenGroupAPI: DotNetAPI() {
|
||||
format.timeZone = TimeZone.getTimeZone("GMT")
|
||||
val dateAsString = data["created_at"] as String
|
||||
val timestamp = format.parse(dateAsString).time
|
||||
@Suppress("NAME_SHADOWING") val message = OpenGroupMessage(serverID, userKeyPair.first, userDisplayName, text, timestamp, openGroupMessageType, message.quote, message.attachments, null, signedMessage.signature, timestamp)
|
||||
message
|
||||
OpenGroupMessage(serverID, userKeyPair.first, userDisplayName, text, timestamp, openGroupMessageType, message.quote, message.attachments, null, signedMessage.signature, timestamp)
|
||||
} catch (exception: Exception) {
|
||||
Log.d("Loki", "Couldn't parse message for open group with ID: $channel on server: $server.")
|
||||
throw exception
|
||||
@@ -238,6 +238,7 @@ object OpenGroupAPI: DotNetAPI() {
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun deleteMessages(messageServerIDs: List<Long>, channel: Long, server: String, isSentByUser: Boolean): Promise<List<Long>, Exception> {
|
||||
return retryIfNeeded(maxRetryCount) {
|
||||
val isModerationRequest = !isSentByUser
|
||||
@@ -338,6 +339,7 @@ object OpenGroupAPI: DotNetAPI() {
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun ban(publicKey: String, server: String): Promise<Unit,Exception> {
|
||||
return retryIfNeeded(maxRetryCount) {
|
||||
execute(HTTPVerb.POST, server, "/loki/v1/moderation/blacklist/@$publicKey").then {
|
||||
|
@@ -26,7 +26,6 @@ data class OpenGroupMessage(
|
||||
fun from(message: VisibleMessage, server: String): OpenGroupMessage? {
|
||||
val storage = MessagingConfiguration.shared.storage
|
||||
val userPublicKey = storage.getUserPublicKey() ?: return null
|
||||
var attachmentIDs = message.attachmentIDs
|
||||
// Validation
|
||||
if (!message.isValid()) { return null } // Should be valid at this point
|
||||
// Quote
|
||||
@@ -34,10 +33,8 @@ data class OpenGroupMessage(
|
||||
val quote = message.quote
|
||||
if (quote != null && quote.isValid()) {
|
||||
val quotedMessageBody = quote.text ?: quote.timestamp!!.toString()
|
||||
val quotedAttachmentID = quote.attachmentID
|
||||
if (quotedAttachmentID != null) { attachmentIDs.remove(quotedAttachmentID) }
|
||||
// FIXME: For some reason the server always returns a 500 if quotedMessageServerID is set...
|
||||
Quote(quote.timestamp!!, quote.publicKey!!, quotedMessageBody, null)
|
||||
val serverID = storage.getQuoteServerID(quote.timestamp!!, quote.publicKey!!)
|
||||
Quote(quote.timestamp!!, quote.publicKey!!, quotedMessageBody, serverID)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
@@ -51,18 +48,18 @@ data class OpenGroupMessage(
|
||||
linkPreview?.let {
|
||||
if (!linkPreview.isValid()) { return@let }
|
||||
val attachmentID = linkPreview.attachmentID ?: return@let
|
||||
val attachment = MessagingConfiguration.shared.messageDataProvider.getAttachmentPointer(attachmentID) ?: return@let
|
||||
val attachment = MessagingConfiguration.shared.messageDataProvider.getSignalAttachmentPointer(attachmentID) ?: return@let
|
||||
val openGroupLinkPreview = Attachment(
|
||||
Attachment.Kind.LinkPreview,
|
||||
server,
|
||||
attachment.id,
|
||||
attachment.contentType!!,
|
||||
attachment.size.get(),
|
||||
attachment.fileName.get(),
|
||||
attachment.fileName.orNull(),
|
||||
0,
|
||||
attachment.width,
|
||||
attachment.height,
|
||||
attachment.caption.get(),
|
||||
attachment.caption.orNull(),
|
||||
attachment.url,
|
||||
linkPreview.url,
|
||||
linkPreview.title)
|
||||
@@ -70,18 +67,18 @@ data class OpenGroupMessage(
|
||||
}
|
||||
// Attachments
|
||||
val attachments = message.attachmentIDs.mapNotNull {
|
||||
val attachment = MessagingConfiguration.shared.messageDataProvider.getAttachmentPointer(it) ?: return@mapNotNull null
|
||||
val attachment = MessagingConfiguration.shared.messageDataProvider.getSignalAttachmentPointer(it) ?: return@mapNotNull null
|
||||
return@mapNotNull Attachment(
|
||||
Attachment.Kind.Attachment,
|
||||
server,
|
||||
attachment.id,
|
||||
attachment.contentType!!,
|
||||
attachment.size.get(),
|
||||
attachment.fileName.get(),
|
||||
attachment.size.orNull(),
|
||||
attachment.fileName.orNull() ?: "",
|
||||
0,
|
||||
attachment.width,
|
||||
attachment.height,
|
||||
attachment.caption.get(),
|
||||
attachment.caption.orNull(),
|
||||
attachment.url,
|
||||
null,
|
||||
null)
|
||||
@@ -121,7 +118,7 @@ data class OpenGroupMessage(
|
||||
val serverID: Long,
|
||||
val contentType: String,
|
||||
val size: Int,
|
||||
val fileName: String,
|
||||
val fileName: String?,
|
||||
val flags: Int,
|
||||
val width: Int,
|
||||
val height: Int,
|
||||
|
@@ -11,7 +11,7 @@ object MessageReceiver {
|
||||
|
||||
private val lastEncryptionKeyPairRequest = mutableMapOf<String, Long>()
|
||||
|
||||
internal sealed class Error(val description: String) : Exception() {
|
||||
internal sealed class Error(val description: String) : Exception(description) {
|
||||
object DuplicateMessage: Error("Duplicate message.")
|
||||
object InvalidMessage: Error("Invalid message.")
|
||||
object UnknownMessage: Error("Unknown message type.")
|
||||
|
@@ -50,7 +50,7 @@ fun MessageReceiver.handle(message: Message, proto: SignalServiceProtos.Content,
|
||||
|
||||
private fun MessageReceiver.handleReadReceipt(message: ReadReceipt) {
|
||||
val context = MessagingConfiguration.shared.context
|
||||
SSKEnvironment.shared.readReceiptManager.processReadReceipts(context, message.sender!!, message.timestamps!!.asList(), message.receivedTimestamp!!)
|
||||
SSKEnvironment.shared.readReceiptManager.processReadReceipts(context, message.sender!!, message.timestamps!!, message.receivedTimestamp!!)
|
||||
}
|
||||
|
||||
private fun MessageReceiver.handleTypingIndicator(message: TypingIndicator) {
|
||||
@@ -110,7 +110,7 @@ private fun MessageReceiver.handleConfigurationMessage(message: ConfigurationMes
|
||||
val allClosedGroupPublicKeys = storage.getAllClosedGroupPublicKeys()
|
||||
for (closeGroup in message.closedGroups) {
|
||||
if (allClosedGroupPublicKeys.contains(closeGroup.publicKey)) continue
|
||||
handleNewClosedGroup(message.sender!!, closeGroup.publicKey, closeGroup.name, closeGroup.encryptionKeyPair, closeGroup.members, closeGroup.admins, message.sentTimestamp!!)
|
||||
handleNewClosedGroup(message.sender!!, message.sentTimestamp!!, closeGroup.publicKey, closeGroup.name, closeGroup.encryptionKeyPair!!, closeGroup.members, closeGroup.admins, message.sentTimestamp!!)
|
||||
}
|
||||
val allOpenGroups = storage.getAllOpenGroups().map { it.value.server }
|
||||
for (openGroup in message.openGroups) {
|
||||
@@ -212,8 +212,8 @@ private fun MessageReceiver.handleClosedGroupControlMessage(message: ClosedGroup
|
||||
is ClosedGroupControlMessage.Kind.NameChange -> handleClosedGroupNameChanged(message)
|
||||
is ClosedGroupControlMessage.Kind.MembersAdded -> handleClosedGroupMembersAdded(message)
|
||||
is ClosedGroupControlMessage.Kind.MembersRemoved -> handleClosedGroupMembersRemoved(message)
|
||||
ClosedGroupControlMessage.Kind.MemberLeft -> handleClosedGroupMemberLeft(message)
|
||||
ClosedGroupControlMessage.Kind.EncryptionKeyPairRequest -> handleClosedGroupEncryptionKeyPairRequest(message)
|
||||
is ClosedGroupControlMessage.Kind.MemberLeft -> handleClosedGroupMemberLeft(message)
|
||||
is ClosedGroupControlMessage.Kind.EncryptionKeyPairRequest -> handleClosedGroupEncryptionKeyPairRequest(message)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,11 +222,11 @@ private fun MessageReceiver.handleNewClosedGroup(message: ClosedGroupControlMess
|
||||
val groupPublicKey = kind.publicKey.toByteArray().toHexString()
|
||||
val members = kind.members.map { it.toByteArray().toHexString() }
|
||||
val admins = kind.admins.map { it.toByteArray().toHexString() }
|
||||
handleNewClosedGroup(message.sender!!, groupPublicKey, kind.name, kind.encryptionKeyPair, members, admins, message.sentTimestamp!!)
|
||||
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, groupPublicKey: String, name: String, encryptionKeyPair: ECKeyPair, members: List<String>, admins: List<String>, formationTimestamp: Long) {
|
||||
private fun handleNewClosedGroup(sender: String, sentTimestamp: Long, groupPublicKey: String, name: String, encryptionKeyPair: ECKeyPair, members: List<String>, admins: List<String>, formationTimestamp: Long) {
|
||||
val context = MessagingConfiguration.shared.context
|
||||
val storage = MessagingConfiguration.shared.storage
|
||||
// Create the group
|
||||
@@ -239,7 +239,7 @@ private fun handleNewClosedGroup(sender: String, groupPublicKey: String, name: S
|
||||
storage.createGroup(groupID, name, LinkedList(members.map { Address.fromSerialized(it) }),
|
||||
null, null, LinkedList(admins.map { Address.fromSerialized(it) }), formationTimestamp)
|
||||
// Notify the user
|
||||
storage.insertIncomingInfoMessage(context, sender, groupID, SignalServiceProtos.GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins)
|
||||
storage.insertIncomingInfoMessage(context, sender, groupID, SignalServiceProtos.GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins, sentTimestamp)
|
||||
}
|
||||
storage.setProfileSharing(Address.fromSerialized(groupID), true)
|
||||
// Add the group to the user's set of public keys to poll for
|
||||
@@ -297,7 +297,7 @@ private fun MessageReceiver.handleClosedGroupUpdated(message: ClosedGroupControl
|
||||
val wasSenderRemoved = !members.contains(senderPublicKey)
|
||||
val type0 = if (wasSenderRemoved) SignalServiceProtos.GroupContext.Type.QUIT else SignalServiceProtos.GroupContext.Type.UPDATE
|
||||
val type1 = if (wasSenderRemoved) SignalServiceGroup.Type.QUIT else SignalServiceGroup.Type.UPDATE
|
||||
storage.insertIncomingInfoMessage(context, senderPublicKey, groupID, type0, type1, name, members, group.admins.map { it.toString() })
|
||||
storage.insertIncomingInfoMessage(context, senderPublicKey, groupID, type0, type1, name, members, group.admins.map { it.toString() }, message.sentTimestamp!!)
|
||||
}
|
||||
|
||||
private fun MessageReceiver.handleClosedGroupEncryptionKeyPair(message: ClosedGroupControlMessage) {
|
||||
@@ -315,7 +315,7 @@ private fun MessageReceiver.handleClosedGroupEncryptionKeyPair(message: ClosedGr
|
||||
return
|
||||
}
|
||||
if (!group.members.map { it.toString() }.contains(senderPublicKey)) {
|
||||
android.util.Log.d("Loki", "Ignoring closed group encryption key pair from non-member.")
|
||||
Log.d("Loki", "Ignoring closed group encryption key pair from non-member.")
|
||||
return
|
||||
}
|
||||
// Find our wrapper and decrypt it if possible
|
||||
@@ -356,12 +356,13 @@ private fun MessageReceiver.handleClosedGroupNameChanged(message: ClosedGroupCon
|
||||
val name = kind.name
|
||||
storage.updateTitle(groupID, name)
|
||||
|
||||
storage.insertIncomingInfoMessage(context, senderPublicKey, groupID, SignalServiceProtos.GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins)
|
||||
storage.insertIncomingInfoMessage(context, senderPublicKey, groupID, SignalServiceProtos.GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins, message.sentTimestamp!!)
|
||||
}
|
||||
|
||||
private fun MessageReceiver.handleClosedGroupMembersAdded(message: ClosedGroupControlMessage) {
|
||||
val context = MessagingConfiguration.shared.context
|
||||
val storage = MessagingConfiguration.shared.storage
|
||||
val userPublicKey = storage.getUserPublicKey()!!
|
||||
val senderPublicKey = message.sender ?: return
|
||||
val kind = message.kind!! as? ClosedGroupControlMessage.Kind.MembersAdded ?: return
|
||||
val groupPublicKey = message.groupPublicKey ?: return
|
||||
@@ -370,9 +371,7 @@ private fun MessageReceiver.handleClosedGroupMembersAdded(message: ClosedGroupCo
|
||||
Log.d("Loki", "Ignoring closed group info message for nonexistent group.")
|
||||
return
|
||||
}
|
||||
if (!isValidGroupUpdate(group, message.sentTimestamp!!, senderPublicKey)) {
|
||||
return
|
||||
}
|
||||
if (!isValidGroupUpdate(group, message.sentTimestamp!!, senderPublicKey)) { return }
|
||||
val name = group.title
|
||||
// Check common group update logic
|
||||
val members = group.members.map { it.serialize() }
|
||||
@@ -381,14 +380,25 @@ 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) })
|
||||
// Send the latest encryption key pair to the added members if the current user is the admin of the group
|
||||
val isCurrentUserAdmin = admins.contains(storage.getUserPublicKey()!!)
|
||||
if (isCurrentUserAdmin) {
|
||||
for (member in updateMembers) {
|
||||
MessageSender.sendLatestEncryptionKeyPair(member, groupPublicKey)
|
||||
if (userPublicKey == senderPublicKey) {
|
||||
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
|
||||
storage.insertOutgoingInfoMessage(context, groupID, SignalServiceProtos.GroupContext.Type.UPDATE, name, members, admins, threadID, message.sentTimestamp!!)
|
||||
} else {
|
||||
storage.insertIncomingInfoMessage(context, senderPublicKey, groupID, SignalServiceProtos.GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins, message.sentTimestamp!!)
|
||||
}
|
||||
if (userPublicKey in admins) {
|
||||
// send current encryption key to the latest added members
|
||||
val encryptionKeyPair = pendingKeyPair[groupPublicKey]?.orNull()
|
||||
?: storage.getLatestClosedGroupEncryptionKeyPair(groupPublicKey)
|
||||
if (encryptionKeyPair == null) {
|
||||
android.util.Log.d("Loki", "Couldn't get encryption key pair for closed group.")
|
||||
} else {
|
||||
for (user in updateMembers) {
|
||||
MessageSender.sendEncryptionKeyPair(groupPublicKey, encryptionKeyPair, setOf(user), targetUser = user, force = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
storage.insertIncomingInfoMessage(context, senderPublicKey, groupID, SignalServiceProtos.GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins)
|
||||
storage.insertIncomingInfoMessage(context, senderPublicKey, groupID, SignalServiceProtos.GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins, message.sentTimestamp!!)
|
||||
}
|
||||
|
||||
private fun MessageReceiver.handleClosedGroupMembersRemoved(message: ClosedGroupControlMessage) {
|
||||
@@ -437,7 +447,7 @@ private fun MessageReceiver.handleClosedGroupMembersRemoved(message: ClosedGroup
|
||||
if (senderLeft) SignalServiceProtos.GroupContext.Type.QUIT to SignalServiceGroup.Type.QUIT
|
||||
else SignalServiceProtos.GroupContext.Type.UPDATE to SignalServiceGroup.Type.UPDATE
|
||||
|
||||
storage.insertIncomingInfoMessage(context, senderPublicKey, groupID, contextType, signalType, name, members, admins)
|
||||
storage.insertIncomingInfoMessage(context, senderPublicKey, groupID, contextType, signalType, name, members, admins, message.sentTimestamp!!)
|
||||
}
|
||||
|
||||
private fun MessageReceiver.handleClosedGroupMemberLeft(message: ClosedGroupControlMessage) {
|
||||
@@ -472,7 +482,7 @@ private fun MessageReceiver.handleClosedGroupMemberLeft(message: ClosedGroupCont
|
||||
MessageSender.generateAndSendNewEncryptionKeyPair(groupPublicKey, updatedMemberList)
|
||||
}
|
||||
}
|
||||
storage.insertIncomingInfoMessage(context, senderPublicKey, groupID, SignalServiceProtos.GroupContext.Type.QUIT, SignalServiceGroup.Type.QUIT, name, members, admins)
|
||||
storage.insertIncomingInfoMessage(context, senderPublicKey, groupID, SignalServiceProtos.GroupContext.Type.QUIT, SignalServiceGroup.Type.QUIT, name, members, admins, message.sentTimestamp!!)
|
||||
}
|
||||
|
||||
private fun MessageReceiver.handleClosedGroupEncryptionKeyPairRequest(message: ClosedGroupControlMessage) {
|
||||
@@ -491,7 +501,13 @@ private fun MessageReceiver.handleClosedGroupEncryptionKeyPairRequest(message: C
|
||||
return
|
||||
}
|
||||
if (!isValidGroupUpdate(group, message.sentTimestamp!!, senderPublicKey)) { return }
|
||||
MessageSender.sendLatestEncryptionKeyPair(senderPublicKey, groupPublicKey)
|
||||
val encryptionKeyPair = pendingKeyPair[groupPublicKey]?.orNull()
|
||||
?: storage.getLatestClosedGroupEncryptionKeyPair(groupPublicKey)
|
||||
if (encryptionKeyPair == null) {
|
||||
Log.d("Loki", "Couldn't get encryption key pair for closed group.")
|
||||
} else {
|
||||
MessageSender.sendEncryptionKeyPair(groupPublicKey, encryptionKeyPair, setOf(senderPublicKey), targetUser = senderPublicKey, force = false)
|
||||
}
|
||||
}
|
||||
|
||||
private fun isValidGroupUpdate(group: GroupRecord,
|
||||
|
@@ -1,37 +1,41 @@
|
||||
package org.session.libsession.messaging.sending_receiving
|
||||
|
||||
import android.util.Size
|
||||
import nl.komponents.kovenant.Promise
|
||||
import nl.komponents.kovenant.deferred
|
||||
import org.session.libsession.messaging.MessagingConfiguration
|
||||
import org.session.libsession.messaging.jobs.JobQueue
|
||||
import org.session.libsession.messaging.jobs.MessageSendJob
|
||||
import org.session.libsession.messaging.jobs.NotifyPNServerJob
|
||||
import org.session.libsession.messaging.messages.Destination
|
||||
import org.session.libsession.messaging.messages.Message
|
||||
import org.session.libsession.messaging.messages.control.ClosedGroupControlMessage
|
||||
import org.session.libsession.messaging.messages.control.ConfigurationMessage
|
||||
import org.session.libsession.messaging.messages.visible.Attachment
|
||||
import org.session.libsession.messaging.messages.visible.Profile
|
||||
import org.session.libsession.messaging.messages.visible.VisibleMessage
|
||||
import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate
|
||||
import org.session.libsession.messaging.messages.visible.*
|
||||
import org.session.libsession.messaging.opengroups.OpenGroupAPI
|
||||
import org.session.libsession.messaging.opengroups.OpenGroupMessage
|
||||
import org.session.libsession.messaging.threads.Address
|
||||
import org.session.libsession.messaging.utilities.MessageWrapper
|
||||
import org.session.libsession.snode.RawResponsePromise
|
||||
import org.session.libsession.snode.SnodeAPI
|
||||
import org.session.libsession.snode.SnodeConfiguration
|
||||
import org.session.libsession.snode.SnodeMessage
|
||||
import org.session.libsession.utilities.SSKEnvironment
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
import org.session.libsignal.service.api.messages.SignalServiceAttachment
|
||||
import org.session.libsignal.service.internal.push.PushTransportDetails
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos
|
||||
import org.session.libsignal.utilities.Base64
|
||||
import org.session.libsignal.service.loki.api.crypto.ProofOfWork
|
||||
import org.session.libsignal.service.loki.utilities.hexEncodedPublicKey
|
||||
import org.session.libsignal.utilities.Base64
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.Attachment as SignalAttachment
|
||||
import org.session.libsession.messaging.sending_receiving.linkpreview.LinkPreview as SignalLinkPreview
|
||||
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel as SignalQuote
|
||||
|
||||
|
||||
object MessageSender {
|
||||
|
||||
// Error
|
||||
internal sealed class Error(val description: String) : Exception() {
|
||||
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.")
|
||||
@@ -46,6 +50,9 @@ object MessageSender {
|
||||
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
|
||||
@@ -55,29 +62,6 @@ object MessageSender {
|
||||
}
|
||||
}
|
||||
|
||||
// Preparation
|
||||
fun prep(signalAttachments: List<SignalServiceAttachment>, message: VisibleMessage) {
|
||||
// TODO: Deal with SignalServiceAttachmentStream
|
||||
val attachments = mutableListOf<Attachment>()
|
||||
for (signalAttachment in signalAttachments) {
|
||||
val attachment = Attachment()
|
||||
if (signalAttachment.isPointer) {
|
||||
val signalAttachmentPointer = signalAttachment.asPointer()
|
||||
attachment.fileName = signalAttachmentPointer.fileName.orNull()
|
||||
attachment.caption = signalAttachmentPointer.caption.orNull()
|
||||
attachment.contentType = signalAttachmentPointer.contentType
|
||||
attachment.digest = signalAttachmentPointer.digest.orNull()
|
||||
attachment.key = signalAttachmentPointer.key
|
||||
attachment.sizeInBytes = signalAttachmentPointer.size.orNull()
|
||||
attachment.url = signalAttachmentPointer.url
|
||||
attachment.size = Size(signalAttachmentPointer.width, signalAttachmentPointer.height)
|
||||
attachments.add(attachment)
|
||||
}
|
||||
}
|
||||
val attachmentIDs = MessagingConfiguration.shared.storage.persistAttachments(message.id ?: 0, attachments)
|
||||
message.attachmentIDs.addAll(attachmentIDs)
|
||||
}
|
||||
|
||||
// Convenience
|
||||
fun send(message: Message, destination: Destination): Promise<Unit, Exception> {
|
||||
if (destination is Destination.OpenGroup) {
|
||||
@@ -87,30 +71,28 @@ object MessageSender {
|
||||
}
|
||||
|
||||
// One-on-One Chats & Closed Groups
|
||||
fun sendToSnodeDestination(destination: Destination, message: Message, isSyncMessage: Boolean = false): Promise<Unit, Exception> {
|
||||
private fun sendToSnodeDestination(destination: Destination, message: Message, isSyncMessage: Boolean = false): Promise<Unit, Exception> {
|
||||
val deferred = deferred<Unit, Exception>()
|
||||
val promise = deferred.promise
|
||||
val storage = MessagingConfiguration.shared.storage
|
||||
val userPublicKey = storage.getUserPublicKey()
|
||||
val preconditionFailure = Exception("Destination should not be open groups!")
|
||||
var snodeMessage: SnodeMessage? = null
|
||||
// Set the timestamp, sender and recipient
|
||||
message.sentTimestamp ?: run { 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)
|
||||
fun handleFailure(error: Exception) {
|
||||
handleFailedMessageSend(message, error)
|
||||
if (destination is Destination.Contact && message is VisibleMessage && !isSelfSend) {
|
||||
SnodeConfiguration.shared.broadcaster.broadcast("messageFailed", message.sentTimestamp!!)
|
||||
}
|
||||
deferred.reject(error)
|
||||
}
|
||||
try {
|
||||
when (destination) {
|
||||
is Destination.Contact -> message.recipient = destination.publicKey
|
||||
is Destination.ClosedGroup -> message.recipient = destination.groupPublicKey
|
||||
is Destination.OpenGroup -> throw preconditionFailure
|
||||
}
|
||||
val isSelfSend = (message.recipient == userPublicKey)
|
||||
// Set the failure handler (need it here already for precondition failure handling)
|
||||
fun handleFailure(error: Exception) {
|
||||
handleFailedMessageSend(message, error)
|
||||
if (destination is Destination.Contact && message is VisibleMessage && !isSelfSend) {
|
||||
//TODO Notify user for send failure
|
||||
}
|
||||
deferred.reject(error)
|
||||
is Destination.OpenGroup -> throw Error.PreconditionFailure("Destination should not be open groups!")
|
||||
}
|
||||
// Validate the message
|
||||
if (!message.isValid()) { throw Error.InvalidMessage }
|
||||
@@ -139,11 +121,8 @@ object MessageSender {
|
||||
// Convert it to protobuf
|
||||
val proto = message.toProto() ?: throw Error.ProtoConversionFailed
|
||||
// Serialize the protobuf
|
||||
val plaintext = proto.toByteArray()
|
||||
val plaintext = PushTransportDetails.getPaddedMessageBody(proto.toByteArray())
|
||||
// Encrypt the serialized protobuf
|
||||
if (destination is Destination.Contact && message is VisibleMessage && !isSelfSend) {
|
||||
//TODO Notify user for encrypting message
|
||||
}
|
||||
val ciphertext: ByteArray
|
||||
when (destination) {
|
||||
is Destination.Contact -> ciphertext = MessageSenderEncryption.encryptWithSessionProtocol(plaintext, destination.publicKey)
|
||||
@@ -151,7 +130,7 @@ object MessageSender {
|
||||
val encryptionKeyPair = MessagingConfiguration.shared.storage.getLatestClosedGroupEncryptionKeyPair(destination.groupPublicKey)!!
|
||||
ciphertext = MessageSenderEncryption.encryptWithSessionProtocol(plaintext, encryptionKeyPair.hexEncodedPublicKey)
|
||||
}
|
||||
is Destination.OpenGroup -> throw preconditionFailure
|
||||
is Destination.OpenGroup -> throw Error.PreconditionFailure("Destination should not be open groups!")
|
||||
}
|
||||
// Wrap the result
|
||||
val kind: SignalServiceProtos.Envelope.Type
|
||||
@@ -165,19 +144,22 @@ object MessageSender {
|
||||
kind = SignalServiceProtos.Envelope.Type.CLOSED_GROUP_CIPHERTEXT
|
||||
senderPublicKey = destination.groupPublicKey
|
||||
}
|
||||
is Destination.OpenGroup -> throw preconditionFailure
|
||||
is Destination.OpenGroup -> throw Error.PreconditionFailure("Destination should not be open groups!")
|
||||
}
|
||||
val wrappedMessage = MessageWrapper.wrap(kind, message.sentTimestamp!!, senderPublicKey, ciphertext)
|
||||
// Calculate proof of work
|
||||
if (destination is Destination.Contact && message is VisibleMessage && !isSelfSend) {
|
||||
//TODO Notify user for proof of work calculating
|
||||
SnodeConfiguration.shared.broadcaster.broadcast("calculatingPoW", message.sentTimestamp!!)
|
||||
}
|
||||
val recipient = message.recipient!!
|
||||
val base64EncodedData = Base64.encodeBytes(wrappedMessage)
|
||||
val timestamp = System.currentTimeMillis()
|
||||
val nonce = ProofOfWork.calculate(base64EncodedData, recipient, timestamp, message.ttl.toInt()) ?: throw Error.ProofOfWorkCalculationFailed
|
||||
// Send the result
|
||||
snodeMessage = SnodeMessage(recipient, base64EncodedData, message.ttl, timestamp, nonce)
|
||||
val snodeMessage = SnodeMessage(recipient, base64EncodedData, message.ttl, timestamp, nonce)
|
||||
if (destination is Destination.Contact && message is VisibleMessage && !isSelfSend) {
|
||||
SnodeConfiguration.shared.broadcaster.broadcast("sendingMessage", message.sentTimestamp!!)
|
||||
}
|
||||
SnodeAPI.sendMessage(snodeMessage).success { promises: Set<RawResponsePromise> ->
|
||||
var isSuccess = false
|
||||
val promiseCount = promises.size
|
||||
@@ -187,7 +169,7 @@ object MessageSender {
|
||||
if (isSuccess) { return@success } // Succeed as soon as the first promise succeeds
|
||||
isSuccess = true
|
||||
if (destination is Destination.Contact && message is VisibleMessage && !isSelfSend) {
|
||||
//TODO Notify user for message sent
|
||||
SnodeConfiguration.shared.broadcaster.broadcast("messageSent", message.sentTimestamp!!)
|
||||
}
|
||||
handleSuccessfulMessageSend(message, destination, isSyncMessage)
|
||||
var shouldNotify = (message is VisibleMessage && !isSyncMessage)
|
||||
@@ -199,57 +181,52 @@ object MessageSender {
|
||||
JobQueue.shared.add(notifyPNServerJob)
|
||||
deferred.resolve(Unit)
|
||||
}
|
||||
|
||||
}
|
||||
promise.fail {
|
||||
errorCount += 1
|
||||
if (errorCount != promiseCount) { return@fail } // Only error out if all promises failed
|
||||
handleFailure(it)
|
||||
deferred.reject(it)
|
||||
}
|
||||
}
|
||||
}.fail {
|
||||
Log.d("Loki", "Couldn't send message due to error: $it.")
|
||||
deferred.reject(it)
|
||||
handleFailure(it)
|
||||
}
|
||||
} catch (exception: Exception) {
|
||||
deferred.reject(exception)
|
||||
handleFailure(exception)
|
||||
}
|
||||
return promise
|
||||
}
|
||||
|
||||
// Open Groups
|
||||
fun sendToOpenGroupDestination(destination: Destination, message: Message): Promise<Unit, Exception> {
|
||||
private fun sendToOpenGroupDestination(destination: Destination, message: Message): Promise<Unit, Exception> {
|
||||
val deferred = deferred<Unit, Exception>()
|
||||
val storage = MessagingConfiguration.shared.storage
|
||||
val preconditionFailure = Exception("Destination should not be contacts or closed groups!")
|
||||
message.sentTimestamp ?: run { message.sentTimestamp = System.currentTimeMillis() }
|
||||
message.sender = storage.getUserPublicKey()
|
||||
// Set the failure handler (need it here already for precondition failure handling)
|
||||
fun handleFailure(error: Exception) {
|
||||
handleFailedMessageSend(message, error)
|
||||
deferred.reject(error)
|
||||
}
|
||||
try {
|
||||
val server: String
|
||||
val channel: Long
|
||||
when (destination) {
|
||||
is Destination.Contact -> throw preconditionFailure
|
||||
is Destination.ClosedGroup -> throw preconditionFailure
|
||||
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.OpenGroup -> {
|
||||
message.recipient = "${destination.server}.${destination.channel}"
|
||||
server = destination.server
|
||||
channel = destination.channel
|
||||
}
|
||||
}
|
||||
// Set the failure handler (need it here already for precondition failure handling)
|
||||
fun handleFailure(error: Exception) {
|
||||
handleFailedMessageSend(message, error)
|
||||
deferred.reject(error)
|
||||
}
|
||||
// Validate the message
|
||||
if (message !is VisibleMessage || !message.isValid()) {
|
||||
handleFailure(Error.InvalidMessage)
|
||||
throw Error.InvalidMessage
|
||||
}
|
||||
// Convert the message to an open group message
|
||||
val openGroupMessage = OpenGroupMessage.from(message, server) ?: kotlin.run {
|
||||
handleFailure(Error.InvalidMessage)
|
||||
throw Error.InvalidMessage
|
||||
}
|
||||
// Send the result
|
||||
@@ -261,7 +238,7 @@ object MessageSender {
|
||||
handleFailure(it)
|
||||
}
|
||||
} catch (exception: Exception) {
|
||||
deferred.reject(exception)
|
||||
handleFailure(exception)
|
||||
}
|
||||
return deferred.promise
|
||||
}
|
||||
@@ -269,7 +246,8 @@ object MessageSender {
|
||||
// Result Handling
|
||||
fun handleSuccessfulMessageSend(message: Message, destination: Destination, isSyncMessage: Boolean = false) {
|
||||
val storage = MessagingConfiguration.shared.storage
|
||||
val messageId = storage.getMessageIdInDatabase(message.sentTimestamp!!, message.sender!!) ?: return
|
||||
val userPublicKey = storage.getUserPublicKey()!!
|
||||
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
|
||||
@@ -277,23 +255,89 @@ object MessageSender {
|
||||
storage.setOpenGroupServerMessageID(messageId, message.openGroupServerMessageID!!)
|
||||
}
|
||||
// Mark the message as sent
|
||||
storage.markAsSent(messageId)
|
||||
storage.markUnidentified(messageId)
|
||||
storage.markAsSent(message.sentTimestamp!!, message.sender?:userPublicKey)
|
||||
storage.markUnidentified(message.sentTimestamp!!, message.sender?:userPublicKey)
|
||||
// Start the disappearing messages timer if needed
|
||||
SSKEnvironment.shared.messageExpirationManager.startAnyExpiration(messageId)
|
||||
if (message is VisibleMessage && !isSyncMessage) {
|
||||
SSKEnvironment.shared.messageExpirationManager.startAnyExpiration(message.sentTimestamp!!, message.sender?:userPublicKey)
|
||||
}
|
||||
// Sync the message if:
|
||||
// • it's a visible message
|
||||
// • the destination was a contact
|
||||
// • we didn't sync it already
|
||||
val userPublicKey = storage.getUserPublicKey()!!
|
||||
if (destination is Destination.Contact && !isSyncMessage && message is VisibleMessage) {
|
||||
sendToSnodeDestination(Destination.Contact(userPublicKey), message, true).get()
|
||||
if (destination is Destination.Contact && !isSyncMessage) {
|
||||
if (message is VisibleMessage) { message.syncTarget = destination.publicKey }
|
||||
if (message is ExpirationTimerUpdate) { message.syncTarget = destination.publicKey }
|
||||
sendToSnodeDestination(Destination.Contact(userPublicKey), message, true)
|
||||
}
|
||||
}
|
||||
|
||||
fun handleFailedMessageSend(message: Message, error: Exception) {
|
||||
val storage = MessagingConfiguration.shared.storage
|
||||
val messageId = storage.getMessageIdInDatabase(message.sentTimestamp!!, message.sender!!) ?: return
|
||||
storage.setErrorMessage(messageId, error)
|
||||
val userPublicKey = storage.getUserPublicKey()!!
|
||||
storage.setErrorMessage(message.sentTimestamp!!, message.sender?:userPublicKey, error)
|
||||
}
|
||||
|
||||
// Convenience
|
||||
@JvmStatic
|
||||
fun send(message: VisibleMessage, address: Address, attachments: List<SignalAttachment>, quote: SignalQuote?, linkPreview: SignalLinkPreview?) {
|
||||
val dataProvider = MessagingConfiguration.shared.messageDataProvider
|
||||
val attachmentIDs = dataProvider.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)
|
||||
}
|
||||
}
|
||||
}
|
||||
send(message, address)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun send(message: Message, address: Address) {
|
||||
val threadID = MessagingConfiguration.shared.storage.getOrCreateThreadIdFor(address)
|
||||
message.threadID = threadID
|
||||
val destination = Destination.from(address)
|
||||
val job = MessageSendJob(message, destination)
|
||||
JobQueue.shared.add(job)
|
||||
}
|
||||
|
||||
fun sendNonDurably(message: VisibleMessage, attachments: List<SignalAttachment>, address: Address): Promise<Unit, Exception> {
|
||||
val attachmentIDs = MessagingConfiguration.shared.messageDataProvider.getAttachmentIDsFor(message.id!!)
|
||||
message.attachmentIDs.addAll(attachmentIDs)
|
||||
return sendNonDurably(message, address)
|
||||
}
|
||||
|
||||
fun sendNonDurably(message: Message, address: Address): Promise<Unit, Exception> {
|
||||
val threadID = MessagingConfiguration.shared.storage.getOrCreateThreadIdFor(address)
|
||||
message.threadID = threadID
|
||||
val destination = Destination.from(address)
|
||||
return send(message, destination)
|
||||
}
|
||||
|
||||
// Closed groups
|
||||
fun createClosedGroup(name: String, members: Collection<String>): Promise<String, Exception> {
|
||||
return create(name, members)
|
||||
}
|
||||
|
||||
fun explicitNameChange(groupPublicKey: String, newName: String) {
|
||||
return setName(groupPublicKey, newName)
|
||||
}
|
||||
|
||||
fun explicitAddMembers(groupPublicKey: String, membersToAdd: List<String>) {
|
||||
return addMembers(groupPublicKey, membersToAdd)
|
||||
}
|
||||
|
||||
fun explicitRemoveMembers(groupPublicKey: String, membersToRemove: List<String>) {
|
||||
return removeMembers(groupPublicKey, membersToRemove)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun explicitLeave(groupPublicKey: String): Promise<Unit, Exception> {
|
||||
return leave(groupPublicKey)
|
||||
}
|
||||
}
|
@@ -12,6 +12,7 @@ import org.session.libsession.messaging.sending_receiving.notifications.PushNoti
|
||||
import org.session.libsession.messaging.sending_receiving.MessageSender.Error
|
||||
import org.session.libsession.messaging.threads.Address
|
||||
import org.session.libsession.utilities.GroupUtil
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsignal.utilities.Hex
|
||||
|
||||
import org.session.libsignal.libsignal.ecc.Curve
|
||||
@@ -25,9 +26,10 @@ import org.session.libsignal.utilities.logging.Log
|
||||
import java.util.*
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
private val pendingKeyPair = ConcurrentHashMap<String, Optional<ECKeyPair>>()
|
||||
const val groupSizeLimit = 100
|
||||
val pendingKeyPair = ConcurrentHashMap<String, Optional<ECKeyPair>>()
|
||||
|
||||
fun MessageSender.createClosedGroup(name: String, members: Collection<String>): Promise<String, Exception> {
|
||||
fun MessageSender.create(name: String, members: Collection<String>): Promise<String, Exception> {
|
||||
val deferred = deferred<String, Exception>()
|
||||
ThreadUtils.queue {
|
||||
// Prepare
|
||||
@@ -48,9 +50,11 @@ fun MessageSender.createClosedGroup(name: String, members: Collection<String>):
|
||||
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)
|
||||
val sentTime = System.currentTimeMillis()
|
||||
for (member in members) {
|
||||
val closedGroupControlMessage = ClosedGroupControlMessage(closedGroupUpdateKind)
|
||||
sendNonDurably(closedGroupControlMessage, Address.fromSerialized(groupID)).get()
|
||||
closedGroupControlMessage.sentTimestamp = sentTime
|
||||
sendNonDurably(closedGroupControlMessage, Address.fromSerialized(member)).get()
|
||||
}
|
||||
// Add the group to the user's set of public keys to poll for
|
||||
storage.addClosedGroupPublicKey(groupPublicKey)
|
||||
@@ -58,7 +62,7 @@ fun MessageSender.createClosedGroup(name: String, members: Collection<String>):
|
||||
storage.addClosedGroupEncryptionKeyPair(encryptionKeyPair, groupPublicKey)
|
||||
// Notify the user
|
||||
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
|
||||
storage.insertOutgoingInfoMessage(context, groupID, SignalServiceProtos.GroupContext.Type.UPDATE, name, members, admins, threadID)
|
||||
storage.insertOutgoingInfoMessage(context, groupID, SignalServiceProtos.GroupContext.Type.UPDATE, name, members, admins, threadID, sentTime)
|
||||
// Notify the PN server
|
||||
PushNotificationAPI.performOperation(PushNotificationAPI.ClosedGroupOperation.Subscribe, groupPublicKey, userPublicKey)
|
||||
// Fulfill the promise
|
||||
@@ -68,7 +72,7 @@ fun MessageSender.createClosedGroup(name: String, members: Collection<String>):
|
||||
return deferred.promise
|
||||
}
|
||||
|
||||
fun MessageSender.v2_update(groupPublicKey: String, members: List<String>, name: String) {
|
||||
fun MessageSender.update(groupPublicKey: String, members: List<String>, name: String) {
|
||||
val context = MessagingConfiguration.shared.context
|
||||
val storage = MessagingConfiguration.shared.storage
|
||||
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
|
||||
@@ -98,14 +102,16 @@ fun MessageSender.setName(groupPublicKey: String, newName: String) {
|
||||
val admins = group.admins.map { it.serialize() }
|
||||
// Send the update to the group
|
||||
val kind = ClosedGroupControlMessage.Kind.NameChange(newName)
|
||||
val sentTime = System.currentTimeMillis()
|
||||
val closedGroupControlMessage = ClosedGroupControlMessage(kind)
|
||||
closedGroupControlMessage.sentTimestamp = sentTime
|
||||
send(closedGroupControlMessage, Address.fromSerialized(groupID))
|
||||
// Update the group
|
||||
storage.updateTitle(groupID, newName)
|
||||
// Notify the user
|
||||
val infoType = SignalServiceProtos.GroupContext.Type.UPDATE
|
||||
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
|
||||
storage.insertOutgoingInfoMessage(context, groupID, infoType, newName, members, admins, threadID)
|
||||
storage.insertOutgoingInfoMessage(context, groupID, infoType, newName, members, admins, threadID, sentTime)
|
||||
}
|
||||
|
||||
fun MessageSender.addMembers(groupPublicKey: String, membersToAdd: List<String>) {
|
||||
@@ -134,18 +140,21 @@ fun MessageSender.addMembers(groupPublicKey: String, membersToAdd: List<String>)
|
||||
val name = group.title
|
||||
// Send the update to the group
|
||||
val memberUpdateKind = ClosedGroupControlMessage.Kind.MembersAdded(newMembersAsData)
|
||||
val sentTime = System.currentTimeMillis()
|
||||
val closedGroupControlMessage = ClosedGroupControlMessage(memberUpdateKind)
|
||||
closedGroupControlMessage.sentTimestamp = sentTime
|
||||
send(closedGroupControlMessage, Address.fromSerialized(groupID))
|
||||
// Send closed group update messages to any new members individually
|
||||
for (member in membersToAdd) {
|
||||
val closedGroupNewKind = ClosedGroupControlMessage.Kind.New(ByteString.copyFrom(Hex.fromStringCondensed(groupPublicKey)), name, encryptionKeyPair, membersAsData, adminsAsData)
|
||||
val closedGroupControlMessage = ClosedGroupControlMessage(closedGroupNewKind)
|
||||
closedGroupControlMessage.sentTimestamp = sentTime
|
||||
send(closedGroupControlMessage, Address.fromSerialized(member))
|
||||
}
|
||||
// Notify the user
|
||||
val infoType = SignalServiceProtos.GroupContext.Type.UPDATE
|
||||
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
|
||||
storage.insertOutgoingInfoMessage(context, groupID, infoType, name, updatedMembers, admins, threadID)
|
||||
storage.insertOutgoingInfoMessage(context, groupID, infoType, name, updatedMembers, admins, threadID, sentTime)
|
||||
}
|
||||
|
||||
fun MessageSender.removeMembers(groupPublicKey: String, membersToRemove: List<String>) {
|
||||
@@ -173,7 +182,9 @@ fun MessageSender.removeMembers(groupPublicKey: String, membersToRemove: List<St
|
||||
val name = group.title
|
||||
// Send the update to the group
|
||||
val memberUpdateKind = ClosedGroupControlMessage.Kind.MembersRemoved(removeMembersAsData)
|
||||
val sentTime = System.currentTimeMillis()
|
||||
val closedGroupControlMessage = ClosedGroupControlMessage(memberUpdateKind)
|
||||
closedGroupControlMessage.sentTimestamp = sentTime
|
||||
send(closedGroupControlMessage, Address.fromSerialized(groupID))
|
||||
val isCurrentUserAdmin = admins.contains(userPublicKey)
|
||||
if (isCurrentUserAdmin) {
|
||||
@@ -182,31 +193,37 @@ fun MessageSender.removeMembers(groupPublicKey: String, membersToRemove: List<St
|
||||
// Notify the user
|
||||
val infoType = SignalServiceProtos.GroupContext.Type.UPDATE
|
||||
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
|
||||
storage.insertOutgoingInfoMessage(context, groupID, infoType, name, updatedMembers, admins, threadID)
|
||||
storage.insertOutgoingInfoMessage(context, groupID, infoType, name, updatedMembers, admins, threadID, sentTime)
|
||||
}
|
||||
|
||||
fun MessageSender.v2_leave(groupPublicKey: String) {
|
||||
val context = MessagingConfiguration.shared.context
|
||||
val storage = MessagingConfiguration.shared.storage
|
||||
val userPublicKey = storage.getUserPublicKey()!!
|
||||
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
|
||||
val group = storage.getGroup(groupID) ?: run {
|
||||
Log.d("Loki", "Can't leave nonexistent closed group.")
|
||||
throw Error.NoThread
|
||||
fun MessageSender.leave(groupPublicKey: String, notifyUser: Boolean = true): Promise<Unit, Exception> {
|
||||
val deferred = deferred<Unit, Exception>()
|
||||
ThreadUtils.queue {
|
||||
val context = MessagingConfiguration.shared.context
|
||||
val storage = MessagingConfiguration.shared.storage
|
||||
val userPublicKey = TextSecurePreferences.getLocalNumber(context)!!
|
||||
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
|
||||
val group = storage.getGroup(groupID) ?: return@queue deferred.reject(Error.NoThread)
|
||||
val updatedMembers = group.members.map { it.serialize() }.toSet() - userPublicKey
|
||||
val admins = group.admins.map { it.serialize() }
|
||||
val name = group.title
|
||||
// Send the update to the group
|
||||
val closedGroupControlMessage = ClosedGroupControlMessage(ClosedGroupControlMessage.Kind.MemberLeft())
|
||||
val sentTime = System.currentTimeMillis()
|
||||
closedGroupControlMessage.sentTimestamp = sentTime
|
||||
sendNonDurably(closedGroupControlMessage, Address.fromSerialized(groupID)).success {
|
||||
// Notify the user
|
||||
val infoType = SignalServiceProtos.GroupContext.Type.QUIT
|
||||
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
|
||||
if (notifyUser) {
|
||||
storage.insertOutgoingInfoMessage(context, groupID, infoType, name, updatedMembers, admins, threadID, sentTime)
|
||||
}
|
||||
// Remove the group private key and unsubscribe from PNs
|
||||
MessageReceiver.disableLocalGroupAndUnsubscribe(groupPublicKey, groupID, userPublicKey)
|
||||
deferred.resolve(Unit)
|
||||
}
|
||||
}
|
||||
val updatedMembers = group.members.map { it.serialize() }.toSet() - userPublicKey
|
||||
val admins = group.admins.map { it.serialize() }
|
||||
val name = group.title
|
||||
// Send the update to the group
|
||||
val closedGroupControlMessage = ClosedGroupControlMessage(ClosedGroupControlMessage.Kind.MemberLeft)
|
||||
sendNonDurably(closedGroupControlMessage, Address.fromSerialized(groupID)).success {
|
||||
// Remove the group private key and unsubscribe from PNs
|
||||
MessageReceiver.disableLocalGroupAndUnsubscribe(groupPublicKey, groupID, userPublicKey)
|
||||
}
|
||||
// Notify the user
|
||||
val infoType = SignalServiceProtos.GroupContext.Type.QUIT
|
||||
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
|
||||
storage.insertOutgoingInfoMessage(context, groupID, infoType, name, updatedMembers, admins, threadID)
|
||||
return deferred.promise
|
||||
}
|
||||
|
||||
fun MessageSender.generateAndSendNewEncryptionKeyPair(groupPublicKey: String, targetMembers: Collection<String>) {
|
||||
@@ -230,6 +247,15 @@ fun MessageSender.generateAndSendNewEncryptionKeyPair(groupPublicKey: String, ta
|
||||
// make sure we set the pendingKeyPair or wait until it is not null
|
||||
} while (!pendingKeyPair.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()
|
||||
}
|
||||
}
|
||||
|
||||
fun MessageSender.sendEncryptionKeyPair(groupPublicKey: String, newKeyPair: ECKeyPair, targetMembers: Collection<String>, targetUser: String? = null, force: Boolean = true): Promise<Unit, Exception>? {
|
||||
val destination = targetUser ?: GroupUtil.doubleEncodeGroupID(groupPublicKey)
|
||||
val proto = SignalServiceProtos.KeyPair.newBuilder()
|
||||
proto.publicKey = ByteString.copyFrom(newKeyPair.publicKey.serialize().removing05PrefixIfNeeded())
|
||||
proto.privateKey = ByteString.copyFrom(newKeyPair.privateKey.serialize())
|
||||
@@ -238,12 +264,15 @@ fun MessageSender.generateAndSendNewEncryptionKeyPair(groupPublicKey: String, ta
|
||||
val ciphertext = MessageSenderEncryption.encryptWithSessionProtocol(plaintext, publicKey)
|
||||
ClosedGroupControlMessage.KeyPairWrapper(publicKey, ByteString.copyFrom(ciphertext))
|
||||
}
|
||||
val kind = ClosedGroupControlMessage.Kind.EncryptionKeyPair(null, wrappers)
|
||||
val kind = ClosedGroupControlMessage.Kind.EncryptionKeyPair(ByteString.copyFrom(Hex.fromStringCondensed(groupPublicKey)), wrappers)
|
||||
val sentTime = System.currentTimeMillis()
|
||||
val closedGroupControlMessage = ClosedGroupControlMessage(kind)
|
||||
sendNonDurably(closedGroupControlMessage, Address.fromSerialized(groupID)).success {
|
||||
// Store it * after * having sent out the message to the group
|
||||
storage.addClosedGroupEncryptionKeyPair(newKeyPair, groupPublicKey)
|
||||
pendingKeyPair[groupPublicKey] = Optional.absent()
|
||||
closedGroupControlMessage.sentTimestamp = sentTime
|
||||
return if (force) {
|
||||
MessageSender.sendNonDurably(closedGroupControlMessage, Address.fromSerialized(destination))
|
||||
} else {
|
||||
MessageSender.send(closedGroupControlMessage, Address.fromSerialized(destination))
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -258,34 +287,8 @@ fun MessageSender.requestEncryptionKeyPair(groupPublicKey: String) {
|
||||
val members = group.members.map { it.serialize() }.toSet()
|
||||
if (!members.contains(storage.getUserPublicKey()!!)) return
|
||||
// Send the request to the group
|
||||
val closedGroupControlMessage = ClosedGroupControlMessage(ClosedGroupControlMessage.Kind.EncryptionKeyPairRequest)
|
||||
val sentTime = System.currentTimeMillis()
|
||||
val closedGroupControlMessage = ClosedGroupControlMessage(ClosedGroupControlMessage.Kind.EncryptionKeyPairRequest())
|
||||
closedGroupControlMessage.sentTimestamp = sentTime
|
||||
send(closedGroupControlMessage, Address.fromSerialized(groupID))
|
||||
}
|
||||
|
||||
fun MessageSender.sendLatestEncryptionKeyPair(publicKey: String, groupPublicKey: String) {
|
||||
val storage = MessagingConfiguration.shared.storage
|
||||
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
|
||||
val group = storage.getGroup(groupID) ?: run {
|
||||
Log.d("Loki", "Can't send encryption key pair for nonexistent closed group.")
|
||||
throw Error.NoThread
|
||||
}
|
||||
val members = group.members.map { it.serialize() }
|
||||
if (!members.contains(publicKey)) {
|
||||
Log.d("Loki", "Refusing to send latest encryption key pair to non-member.")
|
||||
return
|
||||
}
|
||||
// Get the latest encryption key pair
|
||||
val encryptionKeyPair = pendingKeyPair[groupPublicKey]?.orNull()
|
||||
?: storage.getLatestClosedGroupEncryptionKeyPair(groupPublicKey) ?: return
|
||||
// Send it
|
||||
val proto = SignalServiceProtos.KeyPair.newBuilder()
|
||||
proto.publicKey = ByteString.copyFrom(encryptionKeyPair.publicKey.serialize())
|
||||
proto.privateKey = ByteString.copyFrom(encryptionKeyPair.privateKey.serialize())
|
||||
val plaintext = proto.build().toByteArray()
|
||||
val ciphertext = MessageSenderEncryption.encryptWithSessionProtocol(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))
|
||||
val closedGroupControlMessage = ClosedGroupControlMessage(kind)
|
||||
MessageSender.send(closedGroupControlMessage, Address.fromSerialized(publicKey))
|
||||
}
|
@@ -1,38 +0,0 @@
|
||||
package org.session.libsession.messaging.sending_receiving
|
||||
|
||||
import nl.komponents.kovenant.Promise
|
||||
import org.session.libsession.messaging.MessagingConfiguration
|
||||
|
||||
import org.session.libsession.messaging.jobs.JobQueue
|
||||
import org.session.libsession.messaging.jobs.MessageSendJob
|
||||
import org.session.libsession.messaging.messages.Destination
|
||||
import org.session.libsession.messaging.messages.Message
|
||||
import org.session.libsession.messaging.messages.visible.VisibleMessage
|
||||
import org.session.libsession.messaging.threads.Address
|
||||
|
||||
import org.session.libsignal.service.api.messages.SignalServiceAttachment
|
||||
|
||||
fun MessageSender.send(message: VisibleMessage, attachments: List<SignalServiceAttachment>, address: Address) {
|
||||
prep(attachments, message)
|
||||
send(message, address)
|
||||
}
|
||||
|
||||
fun MessageSender.send(message: Message, address: Address) {
|
||||
val threadID = MessagingConfiguration.shared.storage.getOrCreateThreadIdFor(address)
|
||||
message.threadID = threadID
|
||||
val destination = Destination.from(address)
|
||||
val job = MessageSendJob(message, destination)
|
||||
JobQueue.shared.add(job)
|
||||
}
|
||||
|
||||
fun MessageSender.sendNonDurably(message: VisibleMessage, attachments: List<SignalServiceAttachment>, address: Address): Promise<Unit, Exception> {
|
||||
prep(attachments, message)
|
||||
return sendNonDurably(message, address)
|
||||
}
|
||||
|
||||
fun MessageSender.sendNonDurably(message: Message, address: Address): Promise<Unit, Exception> {
|
||||
val threadID = MessagingConfiguration.shared.storage.getOrCreateThreadIdFor(address)
|
||||
message.threadID = threadID
|
||||
val destination = Destination.from(address)
|
||||
return MessageSender.send(message, destination)
|
||||
}
|
@@ -1,11 +1,53 @@
|
||||
package org.session.libsession.messaging.sending_receiving
|
||||
|
||||
import com.goterl.lazycode.lazysodium.LazySodiumAndroid
|
||||
import com.goterl.lazycode.lazysodium.SodiumAndroid
|
||||
import com.goterl.lazycode.lazysodium.interfaces.Box
|
||||
import com.goterl.lazycode.lazysodium.interfaces.Sign
|
||||
|
||||
import org.session.libsession.messaging.MessagingConfiguration
|
||||
import org.session.libsession.messaging.sending_receiving.MessageSender.Error
|
||||
import org.session.libsession.utilities.KeyPairUtilities
|
||||
|
||||
import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded
|
||||
import org.session.libsignal.utilities.Hex
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
|
||||
object MessageSenderEncryption {
|
||||
|
||||
internal fun encryptWithSessionProtocol(plaintext: ByteArray, recipientPublicKey: String): ByteArray{
|
||||
return MessagingConfiguration.shared.sessionProtocol.encrypt(plaintext, recipientPublicKey)
|
||||
private val sodium by lazy { LazySodiumAndroid(SodiumAndroid()) }
|
||||
|
||||
/**
|
||||
* Encrypts `plaintext` using the Session protocol for `hexEncodedX25519PublicKey`.
|
||||
*
|
||||
* @param plaintext the plaintext to encrypt. Must already be padded.
|
||||
* @param recipientHexEncodedX25519PublicKey the X25519 public key to encrypt for. Could be the Session ID of a user, or the public key of a closed group.
|
||||
*
|
||||
* @return the encrypted message.
|
||||
*/
|
||||
internal fun encryptWithSessionProtocol(plaintext: ByteArray, recipientHexEncodedX25519PublicKey: String): ByteArray{
|
||||
val context = MessagingConfiguration.shared.context
|
||||
val userED25519KeyPair = KeyPairUtilities.getUserED25519KeyPair(context) ?: throw Error.NoUserED25519KeyPair
|
||||
val recipientX25519PublicKey = Hex.fromStringCondensed(recipientHexEncodedX25519PublicKey.removing05PrefixIfNeeded())
|
||||
|
||||
val verificationData = plaintext + userED25519KeyPair.publicKey.asBytes + recipientX25519PublicKey
|
||||
val signature = ByteArray(Sign.BYTES)
|
||||
try {
|
||||
sodium.cryptoSignDetached(signature, verificationData, verificationData.size.toLong(), userED25519KeyPair.secretKey.asBytes)
|
||||
} catch (exception: Exception) {
|
||||
Log.d("Loki", "Couldn't sign message due to error: $exception.")
|
||||
throw Error.SigningFailed
|
||||
}
|
||||
val plaintextWithMetadata = plaintext + userED25519KeyPair.publicKey.asBytes + signature
|
||||
val ciphertext = ByteArray(plaintextWithMetadata.size + Box.SEALBYTES)
|
||||
try {
|
||||
sodium.cryptoBoxSeal(ciphertext, plaintextWithMetadata, plaintextWithMetadata.size.toLong(), recipientX25519PublicKey)
|
||||
} catch (exception: Exception) {
|
||||
Log.d("Loki", "Couldn't encrypt message due to error: $exception.")
|
||||
throw Error.EncryptionFailed
|
||||
}
|
||||
|
||||
return ciphertext
|
||||
}
|
||||
|
||||
}
|
@@ -12,10 +12,10 @@ import org.session.libsession.utilities.Util;
|
||||
public class AttachmentId implements Parcelable {
|
||||
|
||||
@JsonProperty
|
||||
private final long rowId;
|
||||
private final long rowId; // This is the field id in the database
|
||||
|
||||
@JsonProperty
|
||||
private final long uniqueId;
|
||||
private final long uniqueId; // This is the timestamp when the attachment is written into the database
|
||||
|
||||
public AttachmentId(@JsonProperty("rowId") long rowId, @JsonProperty("uniqueId") long uniqueId) {
|
||||
this.rowId = rowId;
|
||||
|
@@ -7,10 +7,10 @@ import org.session.libsession.messaging.MessagingConfiguration
|
||||
import org.session.libsession.messaging.jobs.JobQueue
|
||||
import org.session.libsession.messaging.jobs.MessageReceiveJob
|
||||
import org.session.libsession.messaging.utilities.MessageWrapper
|
||||
import org.session.libsession.snode.Snode
|
||||
import org.session.libsession.snode.SnodeAPI
|
||||
import org.session.libsession.snode.SnodeConfiguration
|
||||
|
||||
import org.session.libsignal.service.loki.api.Snode
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
import org.session.libsignal.utilities.Base64
|
||||
|
||||
|
@@ -0,0 +1,9 @@
|
||||
package org.session.libsession.messaging.threads
|
||||
|
||||
object DistributionTypes {
|
||||
const val DEFAULT = 2
|
||||
const val BROADCAST = 1
|
||||
const val CONVERSATION = 2
|
||||
const val ARCHIVE = 3
|
||||
const val INBOX_ZERO = 4
|
||||
}
|
@@ -17,19 +17,7 @@
|
||||
package org.session.libsession.messaging.threads.recipients;
|
||||
|
||||
public class RecipientFormattingException extends Exception {
|
||||
public RecipientFormattingException() {
|
||||
super();
|
||||
}
|
||||
|
||||
public RecipientFormattingException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public RecipientFormattingException(String message, Throwable nested) {
|
||||
super(message, nested);
|
||||
}
|
||||
|
||||
public RecipientFormattingException(Throwable nested) {
|
||||
super(nested);
|
||||
}
|
||||
}
|
||||
|
@@ -1,76 +0,0 @@
|
||||
/**
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.session.libsession.messaging.threads.recipients;
|
||||
|
||||
import android.telephony.PhoneNumberUtils;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.StringTokenizer;
|
||||
|
||||
public class RecipientsFormatter {
|
||||
|
||||
private static String parseBracketedNumber(String recipient) throws RecipientFormattingException {
|
||||
int begin = recipient.indexOf('<');
|
||||
int end = recipient.indexOf('>');
|
||||
String value = recipient.substring(begin + 1, end);
|
||||
|
||||
if (PhoneNumberUtils.isWellFormedSmsAddress(value))
|
||||
return value;
|
||||
else
|
||||
throw new RecipientFormattingException("Bracketed value: " + value + " is not valid.");
|
||||
}
|
||||
|
||||
private static String parseRecipient(String recipient) throws RecipientFormattingException {
|
||||
recipient = recipient.trim();
|
||||
|
||||
if ((recipient.indexOf('<') != -1) && (recipient.indexOf('>') != -1))
|
||||
return parseBracketedNumber(recipient);
|
||||
|
||||
if (PhoneNumberUtils.isWellFormedSmsAddress(recipient))
|
||||
return recipient;
|
||||
|
||||
throw new RecipientFormattingException("Recipient: " + recipient + " is badly formatted.");
|
||||
}
|
||||
|
||||
public static List<String> getRecipients(String rawText) throws RecipientFormattingException {
|
||||
ArrayList<String> results = new ArrayList<String>();
|
||||
StringTokenizer tokenizer = new StringTokenizer(rawText, ",");
|
||||
|
||||
while (tokenizer.hasMoreTokens()) {
|
||||
results.add(parseRecipient(tokenizer.nextToken()));
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
public static String formatNameAndNumber(String name, String number) {
|
||||
// Format like this: Mike Cleron <(650) 555-1234>
|
||||
// Erick Tseng <(650) 555-1212>
|
||||
// Tutankhamun <tutank1341@gmail.com>
|
||||
// (408) 555-1289
|
||||
String formattedNumber = PhoneNumberUtils.formatNumber(number);
|
||||
if (!TextUtils.isEmpty(name) && !name.equals(number)) {
|
||||
return name + " <" + formattedNumber + ">";
|
||||
} else {
|
||||
return formattedNumber;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
@@ -42,7 +42,7 @@ open class DotNetAPI {
|
||||
internal enum class HTTPVerb { GET, PUT, POST, DELETE, PATCH }
|
||||
|
||||
// Error
|
||||
internal sealed class Error(val description: String) : Exception() {
|
||||
internal sealed class Error(val description: String) : Exception(description) {
|
||||
object Generic : Error("An error occurred.")
|
||||
object InvalidURL : Error("Invalid URL.")
|
||||
object ParsingFailed : Error("Invalid file server response.")
|
||||
|
@@ -10,7 +10,7 @@ import java.security.SecureRandom
|
||||
object MessageWrapper {
|
||||
|
||||
// region Types
|
||||
sealed class Error(val description: String) : Exception() {
|
||||
sealed class Error(val description: String) : Exception(description) {
|
||||
object FailedToWrapData : Error("Failed to wrap data.")
|
||||
object FailedToWrapMessageInEnvelope : Error("Failed to wrap message in envelope.")
|
||||
object FailedToWrapEnvelopeInWebSocketMessage : Error("Failed to wrap envelope in web socket message.")
|
||||
|
@@ -1,63 +0,0 @@
|
||||
package org.session.libsession.messaging.utilities
|
||||
|
||||
import android.content.Context
|
||||
import com.goterl.lazycode.lazysodium.LazySodiumAndroid
|
||||
import com.goterl.lazycode.lazysodium.SodiumAndroid
|
||||
import org.session.libsession.messaging.MessagingConfiguration
|
||||
import org.session.libsession.utilities.TextSecurePreferences.isUniversalUnidentifiedAccess
|
||||
import org.session.libsession.utilities.Util.getSecretBytes
|
||||
import org.session.libsignal.metadata.SignalProtos
|
||||
import org.session.libsignal.service.api.crypto.UnidentifiedAccess
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
|
||||
object UnidentifiedAccessUtil {
|
||||
private val TAG = UnidentifiedAccessUtil::class.simpleName
|
||||
private val sodium by lazy { LazySodiumAndroid(SodiumAndroid()) }
|
||||
|
||||
fun getAccessFor(recipientPublicKey: String): UnidentifiedAccess? {
|
||||
val theirUnidentifiedAccessKey = getTargetUnidentifiedAccessKey(recipientPublicKey)
|
||||
val ourUnidentifiedAccessKey = getSelfUnidentifiedAccessKey()
|
||||
val ourUnidentifiedAccessCertificate = getUnidentifiedAccessCertificate()
|
||||
|
||||
Log.i(TAG, "Their access key present? " + (theirUnidentifiedAccessKey != null) +
|
||||
" | Our access key present? " + (ourUnidentifiedAccessKey != null) +
|
||||
" | Our certificate present? " + (ourUnidentifiedAccessCertificate != null))
|
||||
|
||||
return if (theirUnidentifiedAccessKey != null && ourUnidentifiedAccessKey != null && ourUnidentifiedAccessCertificate != null) {
|
||||
UnidentifiedAccess(theirUnidentifiedAccessKey)
|
||||
} else null
|
||||
}
|
||||
|
||||
fun getAccessForSync(context: Context): UnidentifiedAccess? {
|
||||
var ourUnidentifiedAccessKey = getSelfUnidentifiedAccessKey()
|
||||
val ourUnidentifiedAccessCertificate = getUnidentifiedAccessCertificate()
|
||||
if (isUniversalUnidentifiedAccess(context)) {
|
||||
ourUnidentifiedAccessKey = getSecretBytes(16)
|
||||
}
|
||||
return if (ourUnidentifiedAccessKey != null && ourUnidentifiedAccessCertificate != null) {
|
||||
UnidentifiedAccess(ourUnidentifiedAccessKey)
|
||||
} else null
|
||||
}
|
||||
|
||||
private fun getTargetUnidentifiedAccessKey(recipientPublicKey: String): ByteArray? {
|
||||
val theirProfileKey = MessagingConfiguration.shared.storage.getProfileKeyForRecipient(recipientPublicKey) ?: return sodium.randomBytesBuf(16)
|
||||
return UnidentifiedAccess.deriveAccessKeyFrom(theirProfileKey)
|
||||
}
|
||||
|
||||
private fun getSelfUnidentifiedAccessKey(): ByteArray? {
|
||||
val userPublicKey = MessagingConfiguration.shared.storage.getUserPublicKey()
|
||||
if (userPublicKey != null) {
|
||||
return sodium.randomBytesBuf(16)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun getUnidentifiedAccessCertificate(): ByteArray? {
|
||||
val userPublicKey = MessagingConfiguration.shared.storage.getUserPublicKey()
|
||||
if (userPublicKey != null) {
|
||||
val certificate = SignalProtos.SenderCertificate.newBuilder().setSender(userPublicKey).setSenderDevice(1).build()
|
||||
return certificate.toByteArray()
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
@@ -10,7 +10,7 @@ import org.session.libsession.utilities.AESGCM
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
import org.session.libsignal.utilities.Base64
|
||||
import org.session.libsignal.utilities.*
|
||||
import org.session.libsignal.service.loki.api.*
|
||||
import org.session.libsignal.service.loki.api.Snode
|
||||
import org.session.libsignal.service.loki.api.fileserver.FileServerAPI
|
||||
import org.session.libsignal.service.loki.api.utilities.*
|
||||
import org.session.libsession.utilities.AESGCM.EncryptionResult
|
||||
@@ -74,7 +74,7 @@ object OnionRequestAPI {
|
||||
)
|
||||
|
||||
internal sealed class Destination {
|
||||
class Snode(val snode: org.session.libsession.snode.Snode) : Destination()
|
||||
class Snode(val snode: org.session.libsignal.service.loki.api.Snode) : Destination()
|
||||
class Server(val host: String, val target: String, val x25519PublicKey: String) : Destination()
|
||||
}
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
package org.session.libsession.snode
|
||||
|
||||
public class Snode(val address: String, val port: Int, val publicKeySet: KeySet?) {
|
||||
class Snode(val address: String, val port: Int, val publicKeySet: KeySet?) {
|
||||
|
||||
val ip: String get() = address.removePrefix("https://")
|
||||
|
||||
|
@@ -10,6 +10,9 @@ import org.session.libsession.snode.utilities.getRandomElement
|
||||
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
import org.session.libsignal.service.loki.api.utilities.HTTP
|
||||
import org.session.libsignal.service.loki.api.Snode
|
||||
import org.session.libsignal.service.loki.database.LokiAPIDatabaseProtocol
|
||||
import org.session.libsignal.service.loki.utilities.Broadcaster
|
||||
import org.session.libsignal.service.loki.utilities.prettifiedDescription
|
||||
import org.session.libsignal.service.loki.utilities.retryIfNeeded
|
||||
import org.session.libsignal.utilities.*
|
||||
@@ -17,10 +20,11 @@ import org.session.libsignal.utilities.*
|
||||
import java.security.SecureRandom
|
||||
|
||||
object SnodeAPI {
|
||||
val database = SnodeConfiguration.shared.storage
|
||||
val broadcaster = SnodeConfiguration.shared.broadcaster
|
||||
val database: LokiAPIDatabaseProtocol
|
||||
get() = SnodeConfiguration.shared.storage
|
||||
val broadcaster: Broadcaster
|
||||
get() = SnodeConfiguration.shared.broadcaster
|
||||
val sharedContext = Kovenant.createContext()
|
||||
val messageSendingContext = Kovenant.createContext()
|
||||
val messagePollingContext = Kovenant.createContext()
|
||||
|
||||
internal var snodeFailureCount: MutableMap<Snode, Int> = mutableMapOf()
|
||||
@@ -41,7 +45,7 @@ object SnodeAPI {
|
||||
internal var powDifficulty = 1
|
||||
|
||||
// Error
|
||||
internal sealed class Error(val description: String) : Exception() {
|
||||
internal sealed class Error(val description: String) : Exception(description) {
|
||||
object Generic : Error("An error occurred.")
|
||||
object ClockOutOfSync : Error("The user's clock is out of sync with the service node network.")
|
||||
object RandomSnodePoolUpdatingFailed : Error("Failed to update random service node pool.")
|
||||
@@ -158,7 +162,7 @@ object SnodeAPI {
|
||||
val parameters = mapOf( "pubKey" to publicKey )
|
||||
return getRandomSnode().bind {
|
||||
invoke(Snode.Method.GetSwarm, it, publicKey, parameters)
|
||||
}.map(SnodeAPI.sharedContext) {
|
||||
}.map(sharedContext) {
|
||||
parseSnodes(it).toSet()
|
||||
}.success {
|
||||
database.setSwarm(publicKey, it)
|
||||
@@ -182,19 +186,12 @@ object SnodeAPI {
|
||||
|
||||
fun sendMessage(message: SnodeMessage): Promise<Set<RawResponsePromise>, Exception> {
|
||||
val destination = message.recipient
|
||||
fun broadcast(event: String) {
|
||||
val dayInMs: Long = 86400000
|
||||
if (message.ttl != dayInMs && message.ttl != 4 * dayInMs) { return }
|
||||
broadcaster.broadcast(event, message.timestamp)
|
||||
}
|
||||
broadcast("calculatingPoW")
|
||||
return retryIfNeeded(maxRetryCount) {
|
||||
getTargetSnodes(destination).map(messageSendingContext) { swarm ->
|
||||
getTargetSnodes(destination).map { swarm ->
|
||||
swarm.map { snode ->
|
||||
broadcast("sendingMessage")
|
||||
val parameters = message.toJSON()
|
||||
retryIfNeeded(maxRetryCount) {
|
||||
invoke(Snode.Method.SendMessage, snode, destination, parameters).map(messageSendingContext) { rawResponse ->
|
||||
invoke(Snode.Method.SendMessage, snode, destination, parameters).map { rawResponse ->
|
||||
val json = rawResponse as? Map<*, *>
|
||||
val powDifficulty = json?.get("difficulty") as? Int
|
||||
if (powDifficulty != null) {
|
||||
|
@@ -1,12 +1,13 @@
|
||||
package org.session.libsession.snode
|
||||
|
||||
import org.session.libsignal.service.loki.database.LokiAPIDatabaseProtocol
|
||||
import org.session.libsignal.service.loki.utilities.Broadcaster
|
||||
|
||||
class SnodeConfiguration(val storage: SnodeStorageProtocol, val broadcaster: Broadcaster) {
|
||||
class SnodeConfiguration(val storage: LokiAPIDatabaseProtocol, val broadcaster: Broadcaster) {
|
||||
companion object {
|
||||
lateinit var shared: SnodeConfiguration
|
||||
|
||||
fun configure(storage: SnodeStorageProtocol, broadcaster: Broadcaster) {
|
||||
fun configure(storage: LokiAPIDatabaseProtocol, broadcaster: Broadcaster) {
|
||||
if (Companion::shared.isInitialized) { return }
|
||||
shared = SnodeConfiguration(storage, broadcaster)
|
||||
}
|
||||
|
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
* Copyright (C) 2013 Open Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.session.libsession.utilities;
|
||||
|
||||
import android.content.Context;
|
||||
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.utilities.Base64;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Utility class for working with identity keys.
|
||||
*
|
||||
* @author Moxie Marlinspike
|
||||
*/
|
||||
public class IdentityKeyUtil {
|
||||
|
||||
private static final String MASTER_SECRET_UTIL_PREFERENCES_NAME = "SecureSMS-Preferences";
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static final String TAG = IdentityKeyUtil.class.getSimpleName();
|
||||
|
||||
public static final String IDENTITY_PUBLIC_KEY_PREF = "pref_identity_public_v3";
|
||||
public static final String IDENTITY_PRIVATE_KEY_PREF = "pref_identity_private_v3";
|
||||
public static final String ED25519_PUBLIC_KEY = "pref_ed25519_public_key";
|
||||
public static final String ED25519_SECRET_KEY = "pref_ed25519_secret_key";
|
||||
public static final String LOKI_SEED = "loki_seed";
|
||||
|
||||
public static boolean hasIdentityKey(Context context) {
|
||||
SharedPreferences preferences = context.getSharedPreferences(MASTER_SECRET_UTIL_PREFERENCES_NAME, 0);
|
||||
|
||||
return
|
||||
preferences.contains(IDENTITY_PUBLIC_KEY_PREF) &&
|
||||
preferences.contains(IDENTITY_PRIVATE_KEY_PREF);
|
||||
}
|
||||
|
||||
public static @NonNull IdentityKey getIdentityKey(@NonNull Context context) {
|
||||
if (!hasIdentityKey(context)) throw new AssertionError("There isn't one!");
|
||||
|
||||
try {
|
||||
byte[] publicKeyBytes = Base64.decode(retrieve(context, IDENTITY_PUBLIC_KEY_PREF));
|
||||
return new IdentityKey(publicKeyBytes, 0);
|
||||
} catch (IOException | InvalidKeyException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static @NonNull IdentityKeyPair getIdentityKeyPair(@NonNull Context context) {
|
||||
if (!hasIdentityKey(context)) throw new AssertionError("There isn't one!");
|
||||
|
||||
try {
|
||||
IdentityKey publicKey = getIdentityKey(context);
|
||||
ECPrivateKey privateKey = Curve.decodePrivatePoint(Base64.decode(retrieve(context, IDENTITY_PRIVATE_KEY_PREF)));
|
||||
|
||||
return new IdentityKeyPair(publicKey, privateKey);
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void generateIdentityKeyPair(@NonNull Context context) {
|
||||
ECKeyPair keyPair = Curve.generateKeyPair();
|
||||
ECPublicKey publicKey = keyPair.getPublicKey();
|
||||
ECPrivateKey privateKey = keyPair.getPrivateKey();
|
||||
save(context, IDENTITY_PUBLIC_KEY_PREF, Base64.encodeBytes(publicKey.serialize()));
|
||||
save(context, IDENTITY_PRIVATE_KEY_PREF, Base64.encodeBytes(privateKey.serialize()));
|
||||
}
|
||||
|
||||
public static String retrieve(Context context, String key) {
|
||||
SharedPreferences preferences = context.getSharedPreferences(MASTER_SECRET_UTIL_PREFERENCES_NAME, 0);
|
||||
return preferences.getString(key, null);
|
||||
}
|
||||
|
||||
public static void save(Context context, String key, String value) {
|
||||
SharedPreferences preferences = context.getSharedPreferences(MASTER_SECRET_UTIL_PREFERENCES_NAME, 0);
|
||||
Editor preferencesEditor = preferences.edit();
|
||||
|
||||
preferencesEditor.putString(key, value);
|
||||
if (!preferencesEditor.commit()) throw new AssertionError("failed to save identity key/value to shared preferences");
|
||||
}
|
||||
|
||||
public static void delete(Context context, String key) {
|
||||
context.getSharedPreferences(MASTER_SECRET_UTIL_PREFERENCES_NAME, 0).edit().remove(key).commit();
|
||||
}
|
||||
}
|
@@ -0,0 +1,60 @@
|
||||
package org.session.libsession.utilities
|
||||
|
||||
import android.content.Context
|
||||
import com.goterl.lazycode.lazysodium.LazySodiumAndroid
|
||||
import com.goterl.lazycode.lazysodium.SodiumAndroid
|
||||
import com.goterl.lazycode.lazysodium.utils.Key
|
||||
import com.goterl.lazycode.lazysodium.utils.KeyPair
|
||||
import org.session.libsignal.utilities.Base64
|
||||
import org.session.libsignal.utilities.Hex
|
||||
import org.session.libsignal.libsignal.ecc.DjbECPrivateKey
|
||||
import org.session.libsignal.libsignal.ecc.DjbECPublicKey
|
||||
import org.session.libsignal.libsignal.ecc.ECKeyPair
|
||||
|
||||
object KeyPairUtilities {
|
||||
|
||||
private val sodium by lazy { LazySodiumAndroid(SodiumAndroid()) }
|
||||
|
||||
fun generate(): KeyPairGenerationResult {
|
||||
val seed = sodium.randomBytesBuf(16)
|
||||
try {
|
||||
return generate(seed)
|
||||
} catch (exception: Exception) {
|
||||
return generate()
|
||||
}
|
||||
}
|
||||
|
||||
fun generate(seed: ByteArray): KeyPairGenerationResult {
|
||||
val padding = ByteArray(16) { 0 }
|
||||
val ed25519KeyPair = sodium.cryptoSignSeedKeypair(seed + padding)
|
||||
val sodiumX25519KeyPair = sodium.convertKeyPairEd25519ToCurve25519(ed25519KeyPair)
|
||||
val x25519KeyPair = ECKeyPair(DjbECPublicKey(sodiumX25519KeyPair.publicKey.asBytes), DjbECPrivateKey(sodiumX25519KeyPair.secretKey.asBytes))
|
||||
return KeyPairGenerationResult(seed, ed25519KeyPair, x25519KeyPair)
|
||||
}
|
||||
|
||||
fun store(context: Context, seed: ByteArray, ed25519KeyPair: KeyPair, x25519KeyPair: ECKeyPair) {
|
||||
IdentityKeyUtil.save(context, IdentityKeyUtil.LOKI_SEED, Hex.toStringCondensed(seed))
|
||||
IdentityKeyUtil.save(context, IdentityKeyUtil.IDENTITY_PUBLIC_KEY_PREF, Base64.encodeBytes(x25519KeyPair.publicKey.serialize()))
|
||||
IdentityKeyUtil.save(context, IdentityKeyUtil.IDENTITY_PRIVATE_KEY_PREF, Base64.encodeBytes(x25519KeyPair.privateKey.serialize()))
|
||||
IdentityKeyUtil.save(context, IdentityKeyUtil.ED25519_PUBLIC_KEY, Base64.encodeBytes(ed25519KeyPair.publicKey.asBytes))
|
||||
IdentityKeyUtil.save(context, IdentityKeyUtil.ED25519_SECRET_KEY, Base64.encodeBytes(ed25519KeyPair.secretKey.asBytes))
|
||||
}
|
||||
|
||||
fun hasV2KeyPair(context: Context): Boolean {
|
||||
return (IdentityKeyUtil.retrieve(context, IdentityKeyUtil.ED25519_SECRET_KEY) != null)
|
||||
}
|
||||
|
||||
fun getUserED25519KeyPair(context: Context): KeyPair? {
|
||||
val base64EncodedED25519PublicKey = IdentityKeyUtil.retrieve(context, IdentityKeyUtil.ED25519_PUBLIC_KEY) ?: return null
|
||||
val base64EncodedED25519SecretKey = IdentityKeyUtil.retrieve(context, IdentityKeyUtil.ED25519_SECRET_KEY) ?: return null
|
||||
val ed25519PublicKey = Key.fromBytes(Base64.decode(base64EncodedED25519PublicKey))
|
||||
val ed25519SecretKey = Key.fromBytes(Base64.decode(base64EncodedED25519SecretKey))
|
||||
return KeyPair(ed25519PublicKey, ed25519SecretKey)
|
||||
}
|
||||
|
||||
data class KeyPairGenerationResult(
|
||||
val seed: ByteArray,
|
||||
val ed25519KeyPair: KeyPair,
|
||||
val x25519KeyPair: ECKeyPair
|
||||
)
|
||||
}
|
@@ -38,7 +38,7 @@ class SSKEnvironment(
|
||||
interface MessageExpirationManagerProtocol {
|
||||
fun setExpirationTimer(messageID: Long?, duration: Int, senderPublicKey: String, content: SignalServiceProtos.Content)
|
||||
fun disableExpirationTimer(messageID: Long?, senderPublicKey: String, content: SignalServiceProtos.Content)
|
||||
fun startAnyExpiration(messageID: Long)
|
||||
fun startAnyExpiration(timestamp: Long, author: String)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
Reference in New Issue
Block a user