diff --git a/app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachment.java b/app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachment.java index afa4d615d0..7762d18f9f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachment.java @@ -12,6 +12,7 @@ public class DatabaseAttachment extends Attachment { private final long mmsId; private final boolean hasData; private final boolean hasThumbnail; + private boolean isUploaded = false; public DatabaseAttachment(AttachmentId attachmentId, long mmsId, boolean hasData, boolean hasThumbnail, @@ -75,4 +76,12 @@ public class DatabaseAttachment extends Attachment { public boolean hasThumbnail() { return hasThumbnail; } + + public boolean isUploaded() { + return isUploaded; + } + + public void setUploaded(boolean uploaded) { + isUploaded = uploaded; + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachmentProvider.kt b/app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachmentProvider.kt new file mode 100644 index 0000000000..671f0a508b --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachmentProvider.kt @@ -0,0 +1,52 @@ +package org.thoughtcrime.securesms.attachments + +import android.content.Context +import com.google.protobuf.ByteString +import org.session.libsession.database.dto.DatabaseAttachmentDTO +import org.session.libsession.database.MessageDataProvider +import org.session.libsignal.service.internal.push.SignalServiceProtos +import org.thoughtcrime.securesms.database.Database +import org.thoughtcrime.securesms.database.DatabaseFactory +import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper +import org.thoughtcrime.securesms.util.MediaUtil + +class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper), MessageDataProvider { + override fun getAttachment(uniqueID: String): DatabaseAttachmentDTO? { + + val attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context) + val uniqueID = uniqueID.toLongOrNull() ?: return null + val attachmentID = AttachmentId(0, uniqueID) + val databaseAttachment = attachmentDatabase.getAttachment(attachmentID) ?: return null + + return databaseAttachment.toDTO() + } + +} + +// Extension to DatabaseAttachment class + +fun DatabaseAttachment.toDTO(): DatabaseAttachmentDTO { + var databaseAttachmentDTO = DatabaseAttachmentDTO() + databaseAttachmentDTO.contentType = this.contentType + databaseAttachmentDTO.fileName = this.fileName + databaseAttachmentDTO.caption = this.caption + + databaseAttachmentDTO.size = this.size.toInt() + databaseAttachmentDTO.key = ByteString.copyFrom(this.key?.toByteArray()) + databaseAttachmentDTO.digest = ByteString.copyFrom(this.digest) + databaseAttachmentDTO.flags = if (this.isVoiceNote) SignalServiceProtos.AttachmentPointer.Flags.VOICE_MESSAGE.number else 0 + + databaseAttachmentDTO.url = this.url + + if (this.shouldHaveImageSize()) { + databaseAttachmentDTO.shouldHaveImageSize = true + databaseAttachmentDTO.width = this.width + databaseAttachmentDTO.height = this.height + } + + return databaseAttachmentDTO +} + +fun DatabaseAttachment.shouldHaveImageSize(): Boolean { + return (MediaUtil.isVideo(this) || MediaUtil.isImage(this) || MediaUtil.isGif(this)); +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentDatabase.java index 2e124f1579..0e82b96422 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentDatabase.java @@ -497,6 +497,7 @@ public class AttachmentDatabase extends Database { database.update(TABLE_NAME, values, PART_ID_WHERE, ((DatabaseAttachment)attachment).getAttachmentId().toStrings()); notifyConversationListeners(DatabaseFactory.getMmsDatabase(context).getThreadIdForMessage(messageId)); + ((DatabaseAttachment) attachment).setUploaded(true); } public void setTransferState(long messageId, @NonNull Attachment attachment, int transferState) { diff --git a/libsession/src/main/java/org/session/libsession/database/MessageDataProvider.kt b/libsession/src/main/java/org/session/libsession/database/MessageDataProvider.kt new file mode 100644 index 0000000000..2f089ff4ff --- /dev/null +++ b/libsession/src/main/java/org/session/libsession/database/MessageDataProvider.kt @@ -0,0 +1,9 @@ +package org.session.libsession.database + +import org.session.libsession.database.dto.DatabaseAttachmentDTO + +interface MessageDataProvider { + + fun getAttachment(uniqueID: String): DatabaseAttachmentDTO? + +} \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/database/dto/DatabaseAttachmentDTO.kt b/libsession/src/main/java/org/session/libsession/database/dto/DatabaseAttachmentDTO.kt new file mode 100644 index 0000000000..3ad5a23399 --- /dev/null +++ b/libsession/src/main/java/org/session/libsession/database/dto/DatabaseAttachmentDTO.kt @@ -0,0 +1,73 @@ +package org.session.libsession.database.dto + +import android.util.Size +import com.google.protobuf.ByteString +import org.session.libsignal.service.internal.push.SignalServiceProtos +import kotlin.math.round + +class DatabaseAttachmentDTO { + var contentType: String? = null + + var fileName: String? = null + + var url: String? = null + + var caption: String? = null + + var size: Int = 0 + + var key: ByteString? = null + + var digest: ByteString? = null + + var flags: Int = 0 + + var width: Int = 0 + + var height: Int = 0 + + val isVoiceNote: Boolean = false + + var shouldHaveImageSize: Boolean = false + + val isUploaded: Boolean = false + + fun toProto(): SignalServiceProtos.AttachmentPointer? { + val builder = org.session.libsignal.service.internal.push.SignalServiceProtos.AttachmentPointer.newBuilder() + builder.contentType = this.contentType + + if (!this.fileName.isNullOrEmpty()) { + builder.fileName = this.fileName + } + if (!this.caption.isNullOrEmpty()) { + builder.caption = this.caption + } + + builder.size = this.size + builder.key = this.key + builder.digest = this.digest + builder.flags = if (this.isVoiceNote) org.session.libsignal.service.internal.push.SignalServiceProtos.AttachmentPointer.Flags.VOICE_MESSAGE.number else 0 + + //TODO I did copy the behavior of iOS below, not sure if that's relevant here... + if (this.shouldHaveImageSize) { + if (this.width < kotlin.Int.MAX_VALUE && this.height < kotlin.Int.MAX_VALUE) { + val imageSize: Size = Size(this.width, this.height) + val imageWidth = round(imageSize.width.toDouble()) + val imageHeight = round(imageSize.height.toDouble()) + if (imageWidth > 0 && imageHeight > 0) { + builder.width = imageWidth.toInt() + builder.height = imageHeight.toInt() + } + } + } + + builder.url = this.url + + try { + return builder.build() + } catch (e: Exception) { + return null + } + } + +} \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/Configuration.kt b/libsession/src/main/java/org/session/libsession/messaging/Configuration.kt index e95bcebfd4..c544a972c7 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/Configuration.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/Configuration.kt @@ -1,22 +1,31 @@ package org.session.libsession.messaging +import org.session.libsession.database.MessageDataProvider import org.session.libsignal.libsignal.loki.SessionResetProtocol import org.session.libsignal.libsignal.state.* import org.session.libsignal.metadata.certificate.CertificateValidator import org.session.libsignal.service.loki.protocol.closedgroups.SharedSenderKeysDatabaseProtocol -class Configuration(val storage: StorageProtocol, val signalStorage: SignalProtocolStore, val sskDatabase: SharedSenderKeysDatabaseProtocol, val sessionResetImp: SessionResetProtocol, val certificateValidator: CertificateValidator) { +class Configuration( + val storage: StorageProtocol, + val signalStorage: SignalProtocolStore, + val sskDatabase: SharedSenderKeysDatabaseProtocol, + val messageDataProvider: MessageDataProvider, + val sessionResetImp: SessionResetProtocol, + val certificateValidator: CertificateValidator) +{ companion object { lateinit var shared: Configuration fun configure(storage: StorageProtocol, signalStorage: SignalProtocolStore, sskDatabase: SharedSenderKeysDatabaseProtocol, + messageDataProvider: MessageDataProvider, sessionResetImp: SessionResetProtocol, certificateValidator: CertificateValidator ) { if (Companion::shared.isInitialized) { return } - shared = Configuration(storage, signalStorage, sskDatabase, sessionResetImp, certificateValidator) + shared = Configuration(storage, signalStorage, sskDatabase, messageDataProvider, sessionResetImp, certificateValidator) } } } \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/StorageProtocol.kt b/libsession/src/main/java/org/session/libsession/messaging/StorageProtocol.kt index 75702b6191..f9aa62107b 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/StorageProtocol.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/StorageProtocol.kt @@ -17,6 +17,8 @@ interface StorageProtocol { fun getUserProfileKey(): ByteArray? fun getUserProfilePictureURL(): String? + fun getProfileKeyForRecipient(recipientPublicKey: String): ByteArray? + // Signal Protocol fun getOrGenerateRegistrationID(): Int //TODO needs impl @@ -65,6 +67,10 @@ interface StorageProtocol { fun updateTitle(groupID: String, newValue: String) fun updateProfilePicture(groupID: String, newValue: ByteArray) + // Message Handling + fun getReceivedMessageTimestamps(): Set + fun addReceivedMessageTimestamp(timestamp: Long) + diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/MessageSendJob.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/MessageSendJob.kt index 9e32623fde..71cab56c30 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/MessageSendJob.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/MessageSendJob.kt @@ -1,6 +1,9 @@ package org.session.libsession.messaging.jobs -class MessageSendJob : Job { +import org.session.libsession.messaging.messages.Destination +import org.session.libsession.messaging.messages.Message + +class MessageSendJob(val message: Message, val destination: Destination) : Job { override var delegate: JobDelegate? = null override var id: String? = null override var failureCount: Int = 0 diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/Destination.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/Destination.kt index 07c2e726af..d213c08ad4 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/Destination.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/Destination.kt @@ -8,5 +8,8 @@ sealed class Destination { companion object { //TODO need to implement the equivalent to TSThread and then implement from(...) + fun from(threadID: String): Destination { + return Contact(threadID) // Fake for dev + } } } \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/Message.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/Message.kt index 7fd3aa8773..4e9814fcc2 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/Message.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/Message.kt @@ -2,7 +2,7 @@ package org.session.libsession.messaging.messages import org.session.libsignal.service.internal.push.SignalServiceProtos -abstract class Message { +abstract class Message { var id: String? = null var threadID: String? = null @@ -12,7 +12,7 @@ abstract class Message { var sender: String? = null var groupPublicKey: String? = null var openGroupServerMessageID: Long? = null - val ttl: Long = 2 * 24 * 60 * 60 * 1000 + open val ttl: Long = 2 * 24 * 60 * 60 * 1000 // validation open fun isValid(): Boolean { @@ -21,6 +21,6 @@ abstract class Message { return sender != null && recipient != null } - abstract fun toProto(): T + abstract fun toProto(): SignalServiceProtos.Content? } \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/control/ControlMessage.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/control/ControlMessage.kt index 74164054bd..44cd7ee4d8 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/control/ControlMessage.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/control/ControlMessage.kt @@ -1,7 +1,6 @@ package org.session.libsession.messaging.messages.control import org.session.libsession.messaging.messages.Message -import org.session.libsignal.service.internal.push.SignalServiceProtos -abstract class ControlMessage : Message() { +abstract class ControlMessage : Message() { } \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/control/TypingIndicator.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/control/TypingIndicator.kt index ec9ae23358..755d0fc1c8 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/control/TypingIndicator.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/control/TypingIndicator.kt @@ -5,11 +5,11 @@ import org.session.libsignal.service.internal.push.SignalServiceProtos class TypingIndicator() : ControlMessage() { + override val ttl: Long = 30 * 1000 + companion object { const val TAG = "TypingIndicator" - //val ttl: 30 * 1000 //TODO - fun fromProto(proto: SignalServiceProtos.Content): TypingIndicator? { val typingIndicatorProto = proto.typingMessage ?: return null val kind = Kind.fromProto(typingIndicatorProto.action) diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Attachment.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Attachment.kt index 8ff6e476f4..6bf441230f 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Attachment.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Attachment.kt @@ -2,10 +2,11 @@ package org.session.libsession.messaging.messages.visible import android.util.Size import android.webkit.MimeTypeMap +import org.session.libsession.database.MessageDataProvider import org.session.libsignal.service.internal.push.SignalServiceProtos import java.io.File -class Attachment : VisibleMessageProto() { +class Attachment { var fileName: String? = null var contentType: String? = null @@ -32,7 +33,7 @@ class Attachment : VisibleMessageProto() 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 '&' - kind = Kind.VOICEMESSAGE + kind = Kind.VOICE_MESSAGE } else { kind = Kind.GENERIC } @@ -52,18 +53,17 @@ class Attachment : VisibleMessageProto() } enum class Kind { - VOICEMESSAGE, + VOICE_MESSAGE, GENERIC } // validation - override fun isValid(): Boolean { - if (!super.isValid()) return false + fun isValid(): Boolean { // key and digest can be nil for open group attachments return (contentType != null && kind != null && size != null && sizeInBytes != null && url != null) } - override fun toProto(): SignalServiceProtos.AttachmentPointer? { + fun toProto(): SignalServiceProtos.AttachmentPointer? { TODO("Not implemented") } } \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Contact.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Contact.kt index 0c223a3896..1b959e5e03 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Contact.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Contact.kt @@ -1,8 +1,9 @@ package org.session.libsession.messaging.messages.visible +import org.session.libsession.database.MessageDataProvider import org.session.libsignal.service.internal.push.SignalServiceProtos -class Contact : VisibleMessageProto() { +class Contact() { companion object { fun fromProto(proto: SignalServiceProtos.Content): Contact? { @@ -10,7 +11,7 @@ class Contact : VisibleMessageProto() } } - override fun toProto(): SignalServiceProtos.DataMessage.Contact? { + fun toProto(): SignalServiceProtos.DataMessage.Contact? { TODO("Not yet implemented") } } \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/LinkPreview.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/LinkPreview.kt index 3e570f9e05..14a7735ac2 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/LinkPreview.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/LinkPreview.kt @@ -1,9 +1,12 @@ package org.session.libsession.messaging.messages.visible +import android.content.Context +import org.session.libsession.database.MessageDataProvider +import org.session.libsession.messaging.Configuration import org.session.libsignal.libsignal.logging.Log import org.session.libsignal.service.internal.push.SignalServiceProtos -class LinkPreview() : VisibleMessageProto(){ +class LinkPreview() { var title: String? = null var url: String? = null @@ -28,12 +31,11 @@ class LinkPreview() : VisibleMessageProto() { +class Profile() { var displayName: String? = null var profileKey: ByteArray? = null @@ -19,7 +20,6 @@ class Profile() : VisibleMessageProto() { val profileKey = proto.profileKey val profilePictureURL = profileProto.profilePictureURL profileKey?.let { - val profilePictureURL = profilePictureURL profilePictureURL?.let { return Profile(displayName = displayName, profileKey = profileKey.toByteArray(), profilePictureURL = profilePictureURL) } @@ -35,7 +35,7 @@ class Profile() : VisibleMessageProto() { this.profilePictureURL = profilePictureURL } - override fun toProto(): SignalServiceProtos.DataMessage? { + fun toProto(): SignalServiceProtos.DataMessage? { val displayName = displayName if (displayName == null) { Log.w(TAG, "Couldn't construct link preview proto from: $this") diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Quote.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Quote.kt index 165899c044..b1d67c398b 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Quote.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Quote.kt @@ -1,9 +1,12 @@ package org.session.libsession.messaging.messages.visible +import com.goterl.lazycode.lazysodium.BuildConfig +import org.session.libsession.database.MessageDataProvider +import org.session.libsession.messaging.Configuration import org.session.libsignal.libsignal.logging.Log import org.session.libsignal.service.internal.push.SignalServiceProtos -class Quote() : VisibleMessageProto() { +class Quote() { var timestamp: Long? = 0 var publicKey: String? = null @@ -29,14 +32,12 @@ class Quote() : VisibleMessageProto() { this.attachmentID = attachmentID } - // validation - override fun isValid(): Boolean { - if (!super.isValid()) return false + fun isValid(): Boolean { return (timestamp != null && publicKey != null) } - override fun toProto(): SignalServiceProtos.DataMessage.Quote? { + fun toProto(): SignalServiceProtos.DataMessage.Quote? { val timestamp = timestamp val publicKey = publicKey if (timestamp == null || publicKey == null) { @@ -47,7 +48,7 @@ class Quote() : VisibleMessageProto() { quoteProto.id = timestamp quoteProto.author = publicKey text?.let { quoteProto.text = text } - addAttachmentsIfNeeded(quoteProto) + addAttachmentsIfNeeded(quoteProto, Configuration.shared.messageDataProvider) // Build try { return quoteProto.build() @@ -57,12 +58,25 @@ class Quote() : VisibleMessageProto() { } } - private fun addAttachmentsIfNeeded(quoteProto: SignalServiceProtos.DataMessage.Quote.Builder) { + private fun addAttachmentsIfNeeded(quoteProto: SignalServiceProtos.DataMessage.Quote.Builder, messageDataProvider: MessageDataProvider) { val attachmentID = attachmentID ?: return - //TODO databas stuff + val attachmentProto = messageDataProvider.getAttachment(attachmentID) + if (attachmentProto == null) { + Log.w(TAG, "Ignoring invalid attachment for quoted message.") + return + } + if (!attachmentProto.isUploaded) { + if (BuildConfig.DEBUG) { + //TODO equivalent to iOS's preconditionFailure + Log.d(TAG,"Sending a message before all associated attachments have been uploaded.") + return + } + } val quotedAttachmentProto = SignalServiceProtos.DataMessage.Quote.QuotedAttachment.newBuilder() - //TODO more database related stuff - //quotedAttachmentProto.contentType = + quotedAttachmentProto.contentType = attachmentProto.contentType + val fileName = attachmentProto.fileName + fileName?.let { quotedAttachmentProto.fileName = fileName } + quotedAttachmentProto.thumbnail = attachmentProto.toProto() try { quoteProto.addAttachments(quotedAttachmentProto.build()) } catch (e: Exception) { diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/VisibleMessage.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/VisibleMessage.kt index dbdd8ffbf5..be5a0d3061 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/VisibleMessage.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/VisibleMessage.kt @@ -1,9 +1,15 @@ package org.session.libsession.messaging.messages.visible +import com.goterl.lazycode.lazysodium.BuildConfig + +import org.session.libsession.database.MessageDataProvider +import org.session.libsession.messaging.Configuration +import org.session.libsession.messaging.messages.Message + import org.session.libsignal.libsignal.logging.Log import org.session.libsignal.service.internal.push.SignalServiceProtos -class VisibleMessage() : VisibleMessageProto() { +class VisibleMessage : Message() { var text: String? = null var attachmentIDs = ArrayList() @@ -46,7 +52,7 @@ class VisibleMessage() : VisibleMessageProto() { return false } - override fun toProto(): SignalServiceProtos.Content? { + fun toProto(): SignalServiceProtos.Content? { val proto = SignalServiceProtos.Content.newBuilder() var attachmentIDs = this.attachmentIDs val dataMessage: SignalServiceProtos.DataMessage.Builder @@ -85,9 +91,15 @@ class VisibleMessage() : VisibleMessageProto() { } } //Attachments - // TODO I'm blocking on that one... - //swift: let attachments = attachmentIDs.compactMap { TSAttachmentStream.fetch(uniqueId: $0, transaction: transaction) } - + val attachments = attachmentIDs.mapNotNull { Configuration.shared.messageDataProvider.getAttachment(it) } + if (!attachments.all { it.isUploaded }) { + if (BuildConfig.DEBUG) { + //TODO equivalent to iOS's preconditionFailure + Log.d(TAG,"Sending a message before all associated attachments have been uploaded.") + } + } + val attachmentProtos = attachments.mapNotNull { it.toProto() } + dataMessage.addAllAttachments(attachmentProtos) // TODO Contact // Build try { @@ -98,5 +110,4 @@ class VisibleMessage() : VisibleMessageProto() { return null } } - } \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/VisibleMessageProto.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/VisibleMessageProto.kt deleted file mode 100644 index 97b1506d6c..0000000000 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/VisibleMessageProto.kt +++ /dev/null @@ -1,6 +0,0 @@ -package org.session.libsession.messaging.messages.visible - -import org.session.libsession.messaging.messages.Message - -abstract class VisibleMessageProto : Message() { -} \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiver.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiver.kt index 2b50dfd62d..43dfa79187 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiver.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiver.kt @@ -1,7 +1,18 @@ package org.session.libsession.messaging.sending_receiving +import org.session.libsession.messaging.Configuration +import org.session.libsession.messaging.messages.Message +import org.session.libsession.messaging.messages.control.ClosedGroupUpdate +import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate +import org.session.libsession.messaging.messages.control.ReadReceipt +import org.session.libsession.messaging.messages.control.TypingIndicator +import org.session.libsession.messaging.messages.visible.VisibleMessage + +import org.session.libsignal.service.internal.push.SignalServiceProtos + object MessageReceiver { internal sealed class Error(val description: String) : Exception() { + object DuplicateMessage: Error("Duplicate message.") object InvalidMessage: Error("Invalid message.") object UnknownMessage: Error("Unknown message type.") object UnknownEnvelopeType: Error("Unknown envelope type.") @@ -26,4 +37,59 @@ object MessageReceiver { else -> true } } + + internal fun parse(data: ByteArray, openGroupServerID: Long?): Pair { + val storage = Configuration.shared.storage + val userPublicKey = storage.getUserPublicKey() + val isOpenGroupMessage = openGroupServerID != null + // Parse the envelope + val envelope = SignalServiceProtos.Envelope.parseFrom(data) + if (storage.getReceivedMessageTimestamps().contains(envelope.timestamp)) throw Error.DuplicateMessage + storage.addReceivedMessageTimestamp(envelope.timestamp) + // Decrypt the contents + val plaintext: ByteArray + val sender: String + var groupPublicKey: String? = null + if (isOpenGroupMessage) { + plaintext = envelope.content.toByteArray() + sender = envelope.source + } else { + when (envelope.type) { + SignalServiceProtos.Envelope.Type.UNIDENTIFIED_SENDER -> { + val decryptionResult = MessageReceiverDecryption.decryptWithSignalProtocol(envelope) + plaintext = decryptionResult.first() + sender = decryptionResult.second() + } + SignalServiceProtos.Envelope.Type.CLOSED_GROUP_CIPHERTEXT -> { + val decryptionResult = MessageReceiverDecryption.decryptWithSharedSenderKeys(envelope) + plaintext = decryptionResult.first() + sender = decryptionResult.second() + } + else -> throw Error.UnknownEnvelopeType + } + } + // Don't process the envelope any further if the sender is blocked + if (isBlock(sender)) throw Error.SenderBlocked + // Ignore self sends + if (sender == userPublicKey) throw Error.SelfSend + // Parse the proto + val proto = SignalServiceProtos.Content.parseFrom(plaintext) + // Parse the message + val message: Message = ReadReceipt.fromProto(proto) ?: + TypingIndicator.fromProto(proto) ?: + ClosedGroupUpdate.fromProto(proto) ?: + ExpirationTimerUpdate.fromProto(proto) ?: + VisibleMessage.fromProto(proto) ?: throw Error.UnknownMessage + if (isOpenGroupMessage && message !is VisibleMessage) throw Error.InvalidMessage + message.sender = sender + message.recipient = userPublicKey + message.sentTimestamp = envelope.timestamp + message.receivedTimestamp = System.currentTimeMillis() + message.groupPublicKey = groupPublicKey + message.openGroupServerMessageID = openGroupServerID + var isValid = message.isValid() + if (message is VisibleMessage && !isValid && proto.dataMessage.attachmentsCount == 0) { isValid = true } + if (!isValid) { throw Error.InvalidMessage } + return Pair(message, proto) + } } \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiverDecryption.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiverDecryption.kt index 529d7d5272..264d8c4cc2 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiverDecryption.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiverDecryption.kt @@ -1,35 +1,40 @@ package org.session.libsession.messaging.sending_receiving import org.session.libsession.messaging.Configuration -import org.session.libsignal.service.api.push.SignalServiceAddress -import org.session.libsignal.service.loki.crypto.LokiServiceCipher import org.session.libsession.messaging.sending_receiving.MessageReceiver.Error import org.session.libsession.utilities.AESGCM + import org.whispersystems.curve25519.Curve25519 + import org.session.libsignal.libsignal.loki.ClosedGroupCiphertextMessage import org.session.libsignal.libsignal.util.Pair +import org.session.libsignal.service.api.crypto.SignalServiceCipher import org.session.libsignal.service.api.messages.SignalServiceEnvelope +import org.session.libsignal.service.api.push.SignalServiceAddress import org.session.libsignal.service.internal.push.SignalServiceProtos import org.session.libsignal.service.loki.protocol.closedgroups.SharedSenderKeysImplementation import org.session.libsignal.service.loki.utilities.toHexString + import javax.crypto.Mac import javax.crypto.spec.SecretKeySpec object MessageReceiverDecryption { - internal fun decryptWithSignalProtocol(envelope: SignalServiceEnvelope): Pair { + internal fun decryptWithSignalProtocol(envelope: SignalServiceProtos.Envelope): Pair { val storage = Configuration.shared.signalStorage - val certificateValidator = Configuration.shared.certificateValidator val sskDatabase = Configuration.shared.sskDatabase val sessionResetImp = Configuration.shared.sessionResetImp + val certificateValidator = Configuration.shared.certificateValidator val data = envelope.content if (data.count() == 0) { throw Error.NoData } val userPublicKey = Configuration.shared.storage.getUserPublicKey() ?: throw Error.NoUserPublicKey - val cipher = LokiServiceCipher(SignalServiceAddress(userPublicKey), storage, sskDatabase, sessionResetImp, certificateValidator) - val result = cipher.decrypt(envelope) + val localAddress = SignalServiceAddress(userPublicKey) + val cipher = SignalServiceCipher(localAddress, storage, sskDatabase, sessionResetImp, certificateValidator) + val result = cipher.decrypt(SignalServiceEnvelope(envelope)) + return Pair(result, result.sender) } - internal fun decryptWithSharedSenderKeys(envelope: SignalServiceEnvelope): Pair { + internal fun decryptWithSharedSenderKeys(envelope: SignalServiceProtos.Envelope): Pair { // 1. ) Check preconditions val groupPublicKey = envelope.source if (!Configuration.shared.storage.isClosedGroup(groupPublicKey)) { throw Error.InvalidGroupPublicKey } diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiverDelegate.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiverDelegate.kt deleted file mode 100644 index 8312f2c674..0000000000 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiverDelegate.kt +++ /dev/null @@ -1,4 +0,0 @@ -package org.session.libsession.messaging.sending_receiving - -interface MessageReceiverDelegate { -} \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiverHandler.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiverHandler.kt new file mode 100644 index 0000000000..54f70d8590 --- /dev/null +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiverHandler.kt @@ -0,0 +1,158 @@ +package org.session.libsession.messaging.sending_receiving + +import org.session.libsession.messaging.Configuration +import org.session.libsession.messaging.messages.Destination +import org.session.libsession.messaging.messages.Message +import org.session.libsession.messaging.messages.control.ClosedGroupUpdate +import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate +import org.session.libsession.messaging.messages.control.ReadReceipt +import org.session.libsession.messaging.messages.control.TypingIndicator +import org.session.libsession.messaging.messages.visible.VisibleMessage +import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI +import org.session.libsession.utilities.LKGroupUtilities +import org.session.libsignal.libsignal.util.Hex +import org.session.libsignal.service.api.messages.SignalServiceGroup + +import org.session.libsignal.service.internal.push.SignalServiceProtos +import org.session.libsignal.service.loki.protocol.closedgroups.ClosedGroupRatchet +import org.session.libsignal.service.loki.protocol.closedgroups.ClosedGroupRatchetCollectionType +import org.session.libsignal.service.loki.protocol.closedgroups.ClosedGroupSenderKey +import org.session.libsignal.service.loki.protocol.closedgroups.SharedSenderKeysImplementation +import org.session.libsignal.service.loki.utilities.toHexString +import java.util.* + +internal fun MessageReceiver.isBlock(publicKey: String): Boolean { + // TODO: move isBlocked from Recipient to BlockManager + return false +} + +fun MessageReceiver.handle(message: Message, proto: SignalServiceProtos.Content, openGroupID: String?) { + when (message) { + is ReadReceipt -> handleReadReceipt(message) + is TypingIndicator -> handleTypingIndicator(message) + is ClosedGroupUpdate -> handleClosedGroupUpdate(message) + is ExpirationTimerUpdate -> handleExpirationTimerUpdate(message) + is VisibleMessage -> handleVisibleMessage(message, proto, openGroupID) + } +} + +private fun MessageReceiver.handleReadReceipt(message: ReadReceipt) { + +} + +private fun MessageReceiver.handleTypingIndicator(message: TypingIndicator) { + when (message.kind!!) { + TypingIndicator.Kind.STARTED -> showTypingIndicatorIfNeeded(message.sender!!) + TypingIndicator.Kind.STOPPED -> hideTypingIndicatorIfNeeded(message.sender!!) + } +} + +fun MessageReceiver.showTypingIndicatorIfNeeded(senderPublicKey: String) { + +} + +fun MessageReceiver.hideTypingIndicatorIfNeeded(senderPublicKey: String) { + +} + +fun MessageReceiver.cancelTypingIndicatorsIfNeeded(senderPublicKey: String) { + +} + +private fun MessageReceiver.handleExpirationTimerUpdate(message: ExpirationTimerUpdate) { + if (message.duration!! > 0) { + setExpirationTimer(message.duration!!, message.sender!!, message.groupPublicKey) + } else { + disableExpirationTimer(message.sender!!, message.groupPublicKey) + } +} + +fun MessageReceiver.setExpirationTimer(duration: Int, senderPublicKey: String, groupPublicKey: String?) { + +} + +fun MessageReceiver.disableExpirationTimer(senderPublicKey: String, groupPublicKey: String?) { + +} + +fun MessageReceiver.handleVisibleMessage(message: VisibleMessage, proto: SignalServiceProtos.Content, openGroupID: String?) { + +} + +private fun MessageReceiver.handleClosedGroupUpdate(message: ClosedGroupUpdate) { + when (message.kind!!) { + is ClosedGroupUpdate.Kind.New -> handleNewGroup(message) + is ClosedGroupUpdate.Kind.Info -> handleGroupUpdate(message) + is ClosedGroupUpdate.Kind.SenderKeyRequest -> handleSenderKeyRequest(message) + is ClosedGroupUpdate.Kind.SenderKey -> handleSenderKey(message) + } +} + +private fun MessageReceiver.handleNewGroup(message: ClosedGroupUpdate) { + val storage = Configuration.shared.storage + val sskDatabase = Configuration.shared.sskDatabase + val kind = message.kind!! as ClosedGroupUpdate.Kind.New + val groupPublicKey = kind.groupPublicKey.toHexString() + val name = kind.name + val groupPrivateKey = kind.groupPrivateKey + val senderKeys = kind.senderKeys + val members = kind.members.map { it.toHexString() } + val admins = kind.admins.map { it.toHexString() } + // Persist the ratchets + senderKeys.forEach { senderKey -> + if (!members.contains(senderKey.publicKey.toHexString())) { return@forEach } + val ratchet = ClosedGroupRatchet(senderKey.chainKey.toHexString(), senderKey.keyIndex, listOf()) + sskDatabase.setClosedGroupRatchet(groupPublicKey, senderKey.publicKey.toHexString(), ratchet, ClosedGroupRatchetCollectionType.Current) + } + // Sort out any discrepancies between the provided sender keys and what's required + val missingSenderKeys = members.toSet().subtract(senderKeys.map { Hex.toStringCondensed(it.publicKey) }) + val userPublicKey = storage.getUserPublicKey()!! + if (missingSenderKeys.contains(userPublicKey)) { + val userRatchet = SharedSenderKeysImplementation.shared.generateRatchet(groupPublicKey, userPublicKey) + val userSenderKey = ClosedGroupSenderKey(Hex.fromStringCondensed(userRatchet.chainKey), userRatchet.keyIndex, Hex.fromStringCondensed(userPublicKey)) + members.forEach { member -> + if (member == userPublicKey) return@forEach + val closedGroupUpdateKind = ClosedGroupUpdate.Kind.SenderKey(groupPublicKey.toByteArray(), userSenderKey) + val closedGroupUpdate = ClosedGroupUpdate() + closedGroupUpdate.kind = closedGroupUpdateKind + MessageSender.send(closedGroupUpdate, Destination.ClosedGroup(groupPublicKey)) + } + } + missingSenderKeys.minus(userPublicKey).forEach { publicKey -> + MessageSender.requestSenderKey(groupPublicKey, publicKey) + } + // Create the group + val groupID = LKGroupUtilities.getEncodedClosedGroupIDAsData(groupPublicKey) + val groupDB = DatabaseFactory.getGroupDatabase(context) + if (groupDB.getGroup(groupID).orNull() != null) { + // Update the group + groupDB.updateTitle(groupID, name) + groupDB.updateMembers(groupID, members.map { Address.fromSerialized(it) }) + } else { + groupDB.create(groupID, name, LinkedList
(members.map { Address.fromSerialized(it) }), + null, null, LinkedList
(admins.map { Address.fromSerialized(it) })) + } + DatabaseFactory.getRecipientDatabase(context).setProfileSharing(Recipient.from(context, Address.fromSerialized(groupID), false), true) + // Add the group to the user's set of public keys to poll for + sskDatabase.setClosedGroupPrivateKey(groupPublicKey, groupPrivateKey.toHexString()) + // Notify the PN server + PushNotificationAPI.performOperation(context, ClosedGroupOperation.Subscribe, groupPublicKey, userPublicKey) + // Notify the user + insertIncomingInfoMessage(context, senderPublicKey, groupID, SignalServiceProtos.GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins) + // Establish sessions if needed + establishSessionsWithMembersIfNeeded(context, members) + + +} + +private fun MessageReceiver.handleGroupUpdate(message: ClosedGroupUpdate) { + +} + +private fun MessageReceiver.handleSenderKeyRequest(message: ClosedGroupUpdate) { + +} + +private fun MessageReceiver.handleSenderKey(message: ClosedGroupUpdate) { + +} \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt index 773cb50dfc..1d18f3e9df 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt @@ -1,5 +1,6 @@ package org.session.libsession.messaging.sending_receiving +import com.google.protobuf.MessageOrBuilder import nl.komponents.kovenant.Promise import nl.komponents.kovenant.deferred @@ -17,6 +18,7 @@ import org.session.libsession.snode.SnodeAPI import org.session.libsession.snode.SnodeMessage import org.session.libsignal.libsignal.logging.Log +import org.session.libsignal.service.api.messages.SignalServiceAttachment import org.session.libsignal.service.internal.push.SignalServiceProtos import org.session.libsignal.service.internal.util.Base64 import org.session.libsignal.service.loki.api.crypto.ProofOfWork @@ -45,6 +47,11 @@ object MessageSender { } } + // Preparation + fun prep(signalAttachments: List, message: VisibleMessage) { + // TODO: Deal with attachments + } + // Convenience fun send(message: Message, destination: Destination): Promise { if (destination is Destination.OpenGroup) { diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderClosedGroup.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderClosedGroup.kt new file mode 100644 index 0000000000..3aa4cbb0fd --- /dev/null +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderClosedGroup.kt @@ -0,0 +1,209 @@ +@file:Suppress("NAME_SHADOWING") + +package org.session.libsession.messaging.sending_receiving + +import android.content.Context +import android.util.Log +import nl.komponents.kovenant.Promise +import nl.komponents.kovenant.deferred + +import org.session.libsession.messaging.Configuration +import org.session.libsession.messaging.messages.Destination +import org.session.libsession.messaging.messages.control.ClosedGroupUpdate +import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI +import org.session.libsession.utilities.LKGroupUtilities + +import org.session.libsignal.libsignal.ecc.Curve +import org.session.libsignal.libsignal.util.Hex +import org.session.libsignal.service.internal.push.SignalServiceProtos +import org.session.libsignal.service.loki.protocol.closedgroups.ClosedGroupRatchetCollectionType +import org.session.libsignal.service.loki.protocol.closedgroups.ClosedGroupSenderKey +import org.session.libsignal.service.loki.protocol.closedgroups.SharedSenderKeysImplementation +import org.session.libsignal.service.loki.utilities.hexEncodedPrivateKey +import org.session.libsignal.service.loki.utilities.hexEncodedPublicKey +import java.util.* + +fun MessageSender.createClosedGroup(name: String, members: Collection): Promise { + val deferred = deferred() + // Prepare + val members = members + val userPublicKey = Configuration.shared.storage.getUserPublicKey()!! + // Generate a key pair for the group + val groupKeyPair = Curve.generateKeyPair() + val groupPublicKey = groupKeyPair.hexEncodedPublicKey // Includes the "05" prefix + members.plus(userPublicKey) + val membersAsData = members.map { Hex.fromStringCondensed(it) } + // Create ratchets for all members + val senderKeys: List = members.map { publicKey -> + val ratchet = SharedSenderKeysImplementation.shared.generateRatchet(groupPublicKey, publicKey) + ClosedGroupSenderKey(Hex.fromStringCondensed(ratchet.chainKey), ratchet.keyIndex, Hex.fromStringCondensed(publicKey)) + } + // Create the group + val admins = setOf( userPublicKey ) + val adminsAsData = admins.map { Hex.fromStringCondensed(it) } + val groupID = LKGroupUtilities.getEncodedClosedGroupIDAsData(groupPublicKey) + /* TODO: + DatabaseFactory.getGroupDatabase(context).create(groupID, name, LinkedList
(members.map { Address.fromSerialized(it) }), + null, null, LinkedList
(admins.map { Address.fromSerialized(it) })) + DatabaseFactory.getRecipientDatabase(context).setProfileSharing(Recipient.from(context, Address.fromSerialized(groupID), false), true) + */ + // Send a closed group update message to all members using established channels + val promises = mutableListOf>() + for (member in members) { + if (member == userPublicKey) { continue } + val closedGroupUpdateKind = ClosedGroupUpdate.Kind.New(Hex.fromStringCondensed(groupPublicKey), name, groupKeyPair.privateKey.serialize(), + senderKeys, membersAsData, adminsAsData) + val closedGroupUpdate = ClosedGroupUpdate() + closedGroupUpdate.kind = closedGroupUpdateKind + val promise = MessageSender.sendNonDurably(closedGroupUpdate, threadID) + promises.add(promise) + } + // Add the group to the user's set of public keys to poll for + Configuration.shared.sskDatabase.setClosedGroupPrivateKey(groupPublicKey, groupKeyPair.hexEncodedPrivateKey) + // Notify the PN server + PushNotificationAPI.performOperation(PushNotificationAPI.ClosedGroupOperation.Subscribe, groupPublicKey, userPublicKey) + // Notify the user + /* TODO + val threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false)) + insertOutgoingInfoMessage(context, groupID, GroupContext.Type.UPDATE, name, members, admins, threadID) + */ + // Fulfill the promise + deferred.resolve(groupPublicKey) + // Return + return deferred.promise +} + +fun MessageSender.update(groupPublicKey: String, members: Collection, name: String): Promise { + val deferred = deferred() + val userPublicKey = Configuration.shared.storage.getUserPublicKey()!! + val sskDatabase = Configuration.shared.sskDatabase + val groupDB = DatabaseFactory.getGroupDatabase(context) + val groupID = LKGroupUtilities.getEncodedClosedGroupIDAsData(groupPublicKey) + val group = groupDB.getGroup(groupID).orNull() + if (group == null) { + Log.d("Loki", "Can't update nonexistent closed group.") + return deferred.reject(Error.NoThread) + } + val oldMembers = group.members.map { it.serialize() }.toSet() + val newMembers = members.minus(oldMembers) + val membersAsData = members.map { Hex.fromStringCondensed(it) } + val admins = group.admins.map { it.serialize() } + val adminsAsData = admins.map { Hex.fromStringCondensed(it) } + val groupPrivateKey = DatabaseFactory.getSSKDatabase(context).getClosedGroupPrivateKey(groupPublicKey) + if (groupPrivateKey == null) { + Log.d("Loki", "Couldn't get private key for closed group.") + return@Thread deferred.reject(Error.NoPrivateKey) + } + val wasAnyUserRemoved = members.toSet().intersect(oldMembers) != oldMembers.toSet() + val removedMembers = oldMembers.minus(members) + val isUserLeaving = removedMembers.contains(userPublicKey) + var newSenderKeys = listOf() + if (wasAnyUserRemoved) { + if (isUserLeaving && removedMembers.count() != 1) { + Log.d("Loki", "Can't remove self and others simultaneously.") + return@Thread deferred.reject(Error.InvalidUpdate) + } + // Establish sessions if needed + establishSessionsWithMembersIfNeeded(context, members) + // Send the update to the existing members using established channels (don't include new ratchets as everyone should regenerate new ratchets individually) + for (member in oldMembers) { + @Suppress("NAME_SHADOWING") + val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.Info(Hex.fromStringCondensed(groupPublicKey), + name, setOf(), membersAsData, adminsAsData) + @Suppress("NAME_SHADOWING") + val job = ClosedGroupUpdateMessageSendJob(member, closedGroupUpdateKind) + job.setContext(context) + job.onRun() // Run the job immediately + } + val allOldRatchets = sskDatabase.getAllClosedGroupRatchets(groupPublicKey, ClosedGroupRatchetCollectionType.Current) + for (pair in allOldRatchets) { + val senderPublicKey = pair.first + val ratchet = pair.second + val collection = ClosedGroupRatchetCollectionType.Old + sskDatabase.setClosedGroupRatchet(groupPublicKey, senderPublicKey, ratchet, collection) + } + // Delete all ratchets (it's important that this happens * after * sending out the update) + sskDatabase.removeAllClosedGroupRatchets(groupPublicKey, ClosedGroupRatchetCollectionType.Current) + // Remove the group from the user's set of public keys to poll for if the user is leaving. Otherwise generate a new ratchet and + // send it out to all members (minus the removed ones) using established channels. + if (isUserLeaving) { + sskDatabase.removeClosedGroupPrivateKey(groupPublicKey) + groupDB.setActive(groupID, false) + groupDB.removeMember(groupID, Address.fromSerialized(userPublicKey)) + // Notify the PN server + LokiPushNotificationManager.performOperation(context, ClosedGroupOperation.Unsubscribe, groupPublicKey, userPublicKey) + } else { + // Send closed group update messages to any new members using established channels + for (member in newMembers) { + @Suppress("NAME_SHADOWING") + val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.New(Hex.fromStringCondensed(groupPublicKey), name, + Hex.fromStringCondensed(groupPrivateKey), listOf(), membersAsData, adminsAsData) + @Suppress("NAME_SHADOWING") + val job = ClosedGroupUpdateMessageSendJob(member, closedGroupUpdateKind) + ApplicationContext.getInstance(context).jobManager.add(job) + } + // Send out the user's new ratchet to all members (minus the removed ones) using established channels + val userRatchet = SharedSenderKeysImplementation.shared.generateRatchet(groupPublicKey, userPublicKey) + val userSenderKey = ClosedGroupSenderKey(Hex.fromStringCondensed(userRatchet.chainKey), userRatchet.keyIndex, Hex.fromStringCondensed(userPublicKey)) + for (member in members) { + if (member == userPublicKey) { continue } + @Suppress("NAME_SHADOWING") + val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.SenderKey(Hex.fromStringCondensed(groupPublicKey), userSenderKey) + @Suppress("NAME_SHADOWING") + val job = ClosedGroupUpdateMessageSendJob(member, closedGroupUpdateKind) + ApplicationContext.getInstance(context).jobManager.add(job) + } + } + } else if (newMembers.isNotEmpty()) { + // Generate ratchets for any new members + newSenderKeys = newMembers.map { publicKey -> + val ratchet = SharedSenderKeysImplementation.shared.generateRatchet(groupPublicKey, publicKey) + ClosedGroupSenderKey(Hex.fromStringCondensed(ratchet.chainKey), ratchet.keyIndex, Hex.fromStringCondensed(publicKey)) + } + // Send a closed group update message to the existing members with the new members' ratchets (this message is aimed at the group) + val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.Info(Hex.fromStringCondensed(groupPublicKey), name, + newSenderKeys, membersAsData, adminsAsData) + val job = ClosedGroupUpdateMessageSendJob(groupPublicKey, closedGroupUpdateKind) + ApplicationContext.getInstance(context).jobManager.add(job) + // Establish sessions if needed + establishSessionsWithMembersIfNeeded(context, newMembers) + // Send closed group update messages to the new members using established channels + var allSenderKeys = sskDatabase.getAllClosedGroupSenderKeys(groupPublicKey, ClosedGroupRatchetCollectionType.Current) + allSenderKeys = allSenderKeys.union(newSenderKeys) + for (member in newMembers) { + @Suppress("NAME_SHADOWING") + val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.New(Hex.fromStringCondensed(groupPublicKey), name, + Hex.fromStringCondensed(groupPrivateKey), allSenderKeys, membersAsData, adminsAsData) + @Suppress("NAME_SHADOWING") + val job = ClosedGroupUpdateMessageSendJob(member, closedGroupUpdateKind) + ApplicationContext.getInstance(context).jobManager.add(job) + } + } else { + val allSenderKeys = sskDatabase.getAllClosedGroupSenderKeys(groupPublicKey, ClosedGroupRatchetCollectionType.Current) + val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.Info(Hex.fromStringCondensed(groupPublicKey), name, + allSenderKeys, membersAsData, adminsAsData) + val job = ClosedGroupUpdateMessageSendJob(groupPublicKey, closedGroupUpdateKind) + ApplicationContext.getInstance(context).jobManager.add(job) + } + // Update the group + groupDB.updateTitle(groupID, name) + if (!isUserLeaving) { + // The call below sets isActive to true, so if the user is leaving we have to use groupDB.remove(...) instead + groupDB.updateMembers(groupID, members.map { Address.fromSerialized(it) }) + } + // Notify the user + val infoType = if (isUserLeaving) SignalServiceProtos.GroupContext.Type.QUIT else SignalServiceProtos.GroupContext.Type.UPDATE + val threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false)) + insertOutgoingInfoMessage(context, groupID, infoType, name, members, admins, threadID) + deferred.resolve(Unit) + return deferred.promise +} + +fun MessageSender.requestSenderKey(groupPublicKey: String, senderPublicKey: String) { + Log.d("Loki", "Requesting sender key for group public key: $groupPublicKey, sender public key: $senderPublicKey.") + // Send the request + val closedGroupUpdateKind = ClosedGroupUpdate.Kind.SenderKeyRequest(Hex.fromStringCondensed(groupPublicKey)) + val closedGroupUpdate = ClosedGroupUpdate() + closedGroupUpdate.kind = closedGroupUpdateKind + MessageSender.send(closedGroupUpdate, Destination.ClosedGroup(groupPublicKey)) +} \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderConvenience.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderConvenience.kt new file mode 100644 index 0000000000..de5b6fa89e --- /dev/null +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderConvenience.kt @@ -0,0 +1,35 @@ +package org.session.libsession.messaging.sending_receiving + +import nl.komponents.kovenant.Promise + +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.libsignal.service.api.messages.SignalServiceAttachment + +fun MessageSender.send(message: VisibleMessage, attachments: List, threadID: String) { + prep(attachments, message) + send(message, threadID) +} + +fun MessageSender.send(message: Message, threadID: String) { + message.threadID = threadID + val destination = Destination.from(threadID) + val job = MessageSendJob(message, destination) + JobQueue.shared.add(job) +} + +fun MessageSender.sendNonDurably(message: VisibleMessage, attachments: List, threadID: String): Promise { + prep(attachments, message) + // TODO: Deal with attachments + return sendNonDurably(message, threadID) +} + +fun MessageSender.sendNonDurably(message: Message, threadID: String): Promise { + message.threadID = threadID + val destination = Destination.from(threadID) + return MessageSender.send(message, destination) +} \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderDelegate.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderDelegate.kt deleted file mode 100644 index 2cdcb4d206..0000000000 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderDelegate.kt +++ /dev/null @@ -1,4 +0,0 @@ -package org.session.libsession.messaging.sending_receiving - -interface MessageSenderDelegate { -} \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushNotificationAPI.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushNotificationAPI.kt index 80d925a081..457b83ad88 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushNotificationAPI.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushNotificationAPI.kt @@ -3,6 +3,7 @@ package org.session.libsession.messaging.sending_receiving.notifications import android.content.Context import nl.komponents.kovenant.functional.map import okhttp3.* +import org.session.libsession.messaging.Configuration import org.session.libsignal.libsignal.logging.Log import org.session.libsignal.service.internal.util.JsonUtil import org.session.libsignal.service.loki.api.onionrequests.OnionRequestAPI @@ -27,7 +28,7 @@ object PushNotificationAPI { } } - fun unregister(token: String, context: Context) { + fun unregister(token: String) { val parameters = mapOf( "token" to token ) val url = "$server/unregister" val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters)) @@ -45,14 +46,14 @@ object PushNotificationAPI { } } // Unsubscribe from all closed groups - val allClosedGroupPublicKeys = DatabaseFactory.getSSKDatabase(context).getAllClosedGroupPublicKeys() - val userPublicKey = TextSecurePreferences.getLocalNumber(context) + val allClosedGroupPublicKeys = Configuration.shared.sskDatabase.getAllClosedGroupPublicKeys() + val userPublicKey = Configuration.shared.storage.getUserPublicKey()!! allClosedGroupPublicKeys.forEach { closedGroup -> - performOperation(context, ClosedGroupOperation.Unsubscribe, closedGroup, userPublicKey) + performOperation(ClosedGroupOperation.Unsubscribe, closedGroup, userPublicKey) } } - fun register(token: String, publicKey: String, context: Context, force: Boolean) { + fun register(token: String, publicKey: String, force: Boolean) { val oldToken = TextSecurePreferences.getFCMToken(context) val lastUploadDate = TextSecurePreferences.getLastFCMUploadTime(context) if (!force && token == oldToken && System.currentTimeMillis() - lastUploadDate < tokenExpirationInterval) { return } @@ -75,13 +76,13 @@ object PushNotificationAPI { } } // Subscribe to all closed groups - val allClosedGroupPublicKeys = DatabaseFactory.getSSKDatabase(context).getAllClosedGroupPublicKeys() + val allClosedGroupPublicKeys = Configuration.shared.sskDatabase.getAllClosedGroupPublicKeys() allClosedGroupPublicKeys.forEach { closedGroup -> - performOperation(context, ClosedGroupOperation.Subscribe, closedGroup, publicKey) + performOperation(ClosedGroupOperation.Subscribe, closedGroup, publicKey) } } - fun performOperation(context: Context, operation: ClosedGroupOperation, closedGroupPublicKey: String, publicKey: String) { + fun performOperation(operation: ClosedGroupOperation, closedGroupPublicKey: String, publicKey: String) { if (!TextSecurePreferences.isUsingFCM(context)) { return } val parameters = mapOf( "closedGroupPublicKey" to closedGroupPublicKey, "pubKey" to publicKey ) val url = "$server/${operation.rawValue}" diff --git a/libsession/src/main/java/org/session/libsession/messaging/utilities/MessageWrapper.kt b/libsession/src/main/java/org/session/libsession/messaging/utilities/MessageWrapper.kt index 8af6ddf5bc..06f6711abc 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/utilities/MessageWrapper.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/utilities/MessageWrapper.kt @@ -71,11 +71,11 @@ object MessageWrapper { /** * `data` shouldn't be base 64 encoded. */ - fun unwrap(data: ByteArray): Envelope { + fun unwrap(data: ByteArray): ByteArray { try { val webSocketMessage = WebSocketMessage.parseFrom(data) val envelopeAsData = webSocketMessage.request.body - return Envelope.parseFrom(envelopeAsData) + return envelopeAsData.toByteArray() } catch (e: Exception) { Log.d("Loki", "Failed to unwrap data: ${e.message}.") throw Error.FailedToUnwrapData diff --git a/libsession/src/main/java/org/session/libsession/utilities/LKGroupUtilities.kt b/libsession/src/main/java/org/session/libsession/utilities/LKGroupUtilities.kt new file mode 100644 index 0000000000..5c47e0b53f --- /dev/null +++ b/libsession/src/main/java/org/session/libsession/utilities/LKGroupUtilities.kt @@ -0,0 +1,47 @@ +package org.session.libsession.utilities + +object LKGroupUtilities { + const val CLOSED_GROUP_PREFIX = "__textsecure_group__!" + const val MMS_GROUP_PREFIX = "__signal_mms_group__!" + const val OPEN_GROUP_PREFIX = "__loki_public_chat_group__!" + + fun getEncodedOpenGroupID(groupID: String): String { + return OPEN_GROUP_PREFIX + groupID + } + + fun getEncodedOpenGroupIDAsData(groupID: String): ByteArray { + return (OPEN_GROUP_PREFIX + groupID).toByteArray() + } + + fun getEncodedClosedGroupID(groupID: String): String { + return CLOSED_GROUP_PREFIX + groupID + } + + fun getEncodedClosedGroupIDAsData(groupID: String): ByteArray { + return (CLOSED_GROUP_PREFIX + groupID).toByteArray() + } + + fun getEncodedMMSGroupID(groupID: String): String { + return MMS_GROUP_PREFIX + groupID + } + + fun getEncodedMMSGroupIDAsData(groupID: String): ByteArray { + return (MMS_GROUP_PREFIX + groupID).toByteArray() + } + + fun getEncodedGroupID(groupID: ByteArray): String { + return groupID.toString() + } + + fun getDecodedGroupID(groupID: ByteArray): String { + val encodedGroupID = groupID.toString() + if (encodedGroupID.split("!").count() > 1) { + return encodedGroupID.split("!")[1] + } + return encodedGroupID.split("!")[0] + } + + fun getDecodedGroupIDAsData(groupID: ByteArray): ByteArray { + return getDecodedGroupID(groupID).toByteArray() + } +} \ No newline at end of file