diff --git a/libsession/build.gradle b/libsession/build.gradle index aea50a3003..47fc374def 100644 --- a/libsession/build.gradle +++ b/libsession/build.gradle @@ -38,7 +38,10 @@ dependencies { implementation 'androidx.core:core-ktx:1.3.2' implementation 'androidx.appcompat:appcompat:1.2.0' implementation 'com.google.android.material:material:1.2.1' + implementation "com.google.protobuf:protobuf-java:$protobufVersion" testImplementation 'junit:junit:4.+' androidTestImplementation 'androidx.test.ext:junit:1.1.2' androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' + + implementation project(":libsignal") } \ No newline at end of file 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 bd39ccbbce..a0573bd19c 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 @@ -1,5 +1,12 @@ -package org.session.messaging.messages +package org.session.libsession.messaging.messages -enum class Destination { +sealed class Destination { + class Contact(val publicKey: String) + class ClosedGroup(val groupPublicKey: String) + class OpenGroup(val channel: Long, val server: String) + + companion object { + //TODO need to implement the equivalent to TSThread and then implement from(...) + } } \ 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 b1ca1ce0ca..3e92077689 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 @@ -1,5 +1,30 @@ -package org.session.messaging.messages +package org.session.libsession.messaging.messages -open class Message { +import org.session.libsignal.service.internal.push.SignalServiceProtos + +abstract class Message { + + var id: String? = null + var threadID: String? = null + var sentTimestamp: Long? = null + var receivedTimestamp: Long? = null + var recipient: String? = null + var sender: String? = null + var groupPublicKey: String? = null + var openGroupServerMessageID: Long? = null + + companion object { + @JvmStatic + val ttl = 2 * 24 * 60 * 60 * 1000 //TODO not sure about that declaration + } + + // validation + open fun isValid(): Boolean { + sentTimestamp = if (sentTimestamp!! > 0) sentTimestamp else return false + receivedTimestamp = if (receivedTimestamp!! > 0) receivedTimestamp else return false + return sender != null && recipient != null + } + + abstract fun toProto(): SignalServiceProtos.Content? } \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/control/ClosedGroupUpdate.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/control/ClosedGroupUpdate.kt index e6c673c7ee..75486b6557 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/control/ClosedGroupUpdate.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/control/ClosedGroupUpdate.kt @@ -1,4 +1,154 @@ -package org.session.messaging.messages.control +package org.session.libsession.messaging.messages.control -class ClosedGroupUpdate : ControlMessage() { +import com.google.protobuf.ByteString +import org.session.libsignal.libsignal.logging.Log +import org.session.libsignal.service.internal.push.SignalServiceProtos +import org.session.libsignal.service.loki.protocol.closedgroups.ClosedGroupSenderKey + +class ClosedGroupUpdate() : ControlMessage() { + + var kind: Kind? = null + + // Kind enum + sealed class Kind { + class New(val groupPublicKey: ByteArray, val name: String, val groupPrivateKey: ByteArray, val senderKeys: Collection, val members: Collection, val admins: Collection) : Kind() + class Info(val groupPublicKey: ByteArray, val name: String, val senderKeys: Collection, val members: Collection, val admins: Collection) : Kind() + class SenderKeyRequest(val groupPublicKey: ByteArray) : Kind() + class SenderKey(val groupPublicKey: ByteArray, val senderKey: org.session.libsignal.service.loki.protocol.closedgroups.ClosedGroupSenderKey) : Kind() + } + + companion object { + const val TAG = "ClosedGroupUpdate" + + fun fromProto(proto: SignalServiceProtos.Content): ClosedGroupUpdate? { + val closedGroupUpdateProto = proto.dataMessage?.closedGroupUpdate ?: return null + val groupPublicKey = closedGroupUpdateProto.groupPublicKey + var kind: Kind + when(closedGroupUpdateProto.type) { + SignalServiceProtos.ClosedGroupUpdate.Type.NEW -> { + val name = closedGroupUpdateProto.name ?: return null + val groupPrivateKey = closedGroupUpdateProto.groupPrivateKey ?: return null + val senderKeys = closedGroupUpdateProto.senderKeysList.map { ClosedGroupSenderKey.fromProto(it) } + kind = Kind.New( + groupPublicKey = groupPublicKey.toByteArray(), + name = name, + groupPrivateKey = groupPrivateKey.toByteArray(), + senderKeys = senderKeys, + members = closedGroupUpdateProto.membersList.map { it.toByteArray() }, + admins = closedGroupUpdateProto.adminsList.map { it.toByteArray() } + ) + } + SignalServiceProtos.ClosedGroupUpdate.Type.INFO -> { + val name = closedGroupUpdateProto.name ?: return null + val senderKeys = closedGroupUpdateProto.senderKeysList.map { ClosedGroupSenderKey.fromProto(it) } + kind = Kind.Info( + groupPublicKey = groupPublicKey.toByteArray(), + name = name, + senderKeys = senderKeys, + members = closedGroupUpdateProto.membersList.map { it.toByteArray() }, + admins = closedGroupUpdateProto.adminsList.map { it.toByteArray() } + ) + } + SignalServiceProtos.ClosedGroupUpdate.Type.SENDER_KEY_REQUEST -> { + kind = Kind.SenderKeyRequest(groupPublicKey = groupPublicKey.toByteArray()) + } + SignalServiceProtos.ClosedGroupUpdate.Type.SENDER_KEY -> { + val senderKeyProto = closedGroupUpdateProto.senderKeysList?.first() ?: return null + kind = Kind.SenderKey( + groupPublicKey = groupPublicKey.toByteArray(), + senderKey = ClosedGroupSenderKey.fromProto(senderKeyProto) + ) + } + } + return ClosedGroupUpdate(kind) + } + } + + // constructor + internal constructor(kind: Kind?) : this() { + this.kind = kind + } + + // validation + override fun isValid(): Boolean { + if (!super.isValid()) return false + val kind = kind ?: return false + when(kind) { + is Kind.New -> { + return !kind.groupPublicKey.isEmpty() && !kind.name.isEmpty() && !kind.groupPrivateKey.isEmpty() && !kind.members.isEmpty() && !kind.admins.isEmpty() + } + is Kind.Info -> { + return !kind.groupPublicKey.isEmpty() && !kind.name.isEmpty() && !kind.members.isEmpty() && !kind.admins.isEmpty() + } + is Kind.SenderKeyRequest -> { + return !kind.groupPublicKey.isEmpty() + } + is Kind.SenderKey -> { + return !kind.groupPublicKey.isEmpty() + } + } + } + + override fun toProto(): SignalServiceProtos.Content? { + val kind = kind + if (kind == null) { + Log.w(TAG, "Couldn't construct closed group update proto from: $this") + return null + } + try { + val closedGroupUpdate: SignalServiceProtos.ClosedGroupUpdate.Builder = SignalServiceProtos.ClosedGroupUpdate.newBuilder() + when (kind) { + is Kind.New -> { + closedGroupUpdate.groupPublicKey = ByteString.copyFrom(kind.groupPublicKey) + closedGroupUpdate.type = SignalServiceProtos.ClosedGroupUpdate.Type.NEW + closedGroupUpdate.name = kind.name + closedGroupUpdate.groupPrivateKey = ByteString.copyFrom(kind.groupPrivateKey) + closedGroupUpdate.addAllSenderKeys(kind.senderKeys.map { it.toProto() }) + closedGroupUpdate.addAllMembers(kind.members.map { ByteString.copyFrom(it) }) + closedGroupUpdate.addAllAdmins(kind.admins.map { ByteString.copyFrom(it) }) + } + is Kind.Info -> { + closedGroupUpdate.groupPublicKey = ByteString.copyFrom(kind.groupPublicKey) + closedGroupUpdate.type = SignalServiceProtos.ClosedGroupUpdate.Type.INFO + closedGroupUpdate.name = kind.name + closedGroupUpdate.addAllSenderKeys(kind.senderKeys.map { it.toProto() }) + closedGroupUpdate.addAllMembers(kind.members.map { ByteString.copyFrom(it) }) + closedGroupUpdate.addAllAdmins(kind.admins.map { ByteString.copyFrom(it) }) + } + is Kind.SenderKeyRequest -> { + closedGroupUpdate.groupPublicKey = ByteString.copyFrom(kind.groupPublicKey) + closedGroupUpdate.type = SignalServiceProtos.ClosedGroupUpdate.Type.SENDER_KEY_REQUEST + } + is Kind.SenderKey -> { + closedGroupUpdate.groupPublicKey = ByteString.copyFrom(kind.groupPublicKey) + closedGroupUpdate.type = SignalServiceProtos.ClosedGroupUpdate.Type.SENDER_KEY + closedGroupUpdate.addAllSenderKeys(listOf( kind.senderKey.toProto() )) + } + } + val contentProto = SignalServiceProtos.Content.newBuilder() + val dataMessageProto = SignalServiceProtos.DataMessage.newBuilder() + dataMessageProto.closedGroupUpdate = closedGroupUpdate.build() + contentProto.dataMessage = dataMessageProto.build() + return contentProto.build() + } catch (e: Exception) { + Log.w(TAG, "Couldn't construct closed group update proto from: $this") + return null + } + return null + } + +} + +// extension functions to class ClosedGroupSenderKey + +private fun ClosedGroupSenderKey.Companion.fromProto(proto: SignalServiceProtos.ClosedGroupUpdate.SenderKey): ClosedGroupSenderKey { + return ClosedGroupSenderKey(chainKey = proto.chainKey.toByteArray(), keyIndex = proto.keyIndex, publicKey = proto.publicKey.toByteArray()) +} + +private fun ClosedGroupSenderKey.toProto(): SignalServiceProtos.ClosedGroupUpdate.SenderKey { + val proto = SignalServiceProtos.ClosedGroupUpdate.SenderKey.newBuilder() + proto.chainKey = ByteString.copyFrom(chainKey) + proto.keyIndex = keyIndex + proto.publicKey = ByteString.copyFrom(publicKey) + return proto.build() } \ 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 670553854c..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,6 +1,6 @@ -package org.session.messaging.messages.control +package org.session.libsession.messaging.messages.control -import org.session.messaging.messages.Message +import org.session.libsession.messaging.messages.Message -open 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/ExpirationTimerUpdate.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/control/ExpirationTimerUpdate.kt index 24d9140792..9f2a38c86c 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/control/ExpirationTimerUpdate.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/control/ExpirationTimerUpdate.kt @@ -1,4 +1,51 @@ -package org.session.messaging.messages.control +package org.session.libsession.messaging.messages.control -class ExpirationTimerUpdate : ControlMessage() { +import org.session.libsignal.libsignal.logging.Log +import org.session.libsignal.service.internal.push.SignalServiceProtos + +class ExpirationTimerUpdate() : ControlMessage() { + + var duration: Int? = 0 + + companion object { + const val TAG = "ExpirationTimerUpdate" + + 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 '&' + if (!isExpirationTimerUpdate) return null + val duration = dataMessageProto.expireTimer + return ExpirationTimerUpdate(duration) + } + } + + //constructor + internal constructor(duration: Int) : this() { + this.duration = duration + } + + // validation + override fun isValid(): Boolean { + if (!super.isValid()) return false + return duration != null + } + + override fun toProto(): SignalServiceProtos.Content? { + val duration = duration + if (duration == null) { + Log.w(TAG, "Couldn't construct expiration timer update proto from: $this") + return null + } + val dataMessageProto = SignalServiceProtos.DataMessage.newBuilder() + dataMessageProto.flags = SignalServiceProtos.DataMessage.Flags.EXPIRATION_TIMER_UPDATE_VALUE + dataMessageProto.expireTimer = duration + val contentProto = SignalServiceProtos.Content.newBuilder() + try { + contentProto.dataMessage = dataMessageProto.build() + return contentProto.build() + } catch (e: Exception) { + Log.w(TAG, "Couldn't construct expiration timer update proto from: $this") + return null + } + } } \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/control/ReadReceipt.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/control/ReadReceipt.kt index 74f96aec81..960b5aeaf9 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/control/ReadReceipt.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/control/ReadReceipt.kt @@ -1,4 +1,53 @@ -package org.session.messaging.messages.control +package org.session.libsession.messaging.messages.control -class ReadReceipt : ControlMessage() { +import org.session.libsignal.libsignal.logging.Log +import org.session.libsignal.service.internal.push.SignalServiceProtos + +class ReadReceipt() : ControlMessage() { + + var timestamps: LongArray? = null + + companion object { + const val TAG = "ReadReceipt" + + fun fromProto(proto: SignalServiceProtos.Content): ReadReceipt? { + val receiptProto = proto.receiptMessage ?: return null + if (receiptProto.type != SignalServiceProtos.ReceiptMessage.Type.READ) return null + val timestamps = receiptProto.timestampList + if (timestamps.isEmpty()) return null + return ReadReceipt(timestamps = timestamps.toLongArray()) + } + } + + //constructor + internal constructor(timestamps: LongArray?) : this() { + this.timestamps = timestamps + } + + // validation + override fun isValid(): Boolean { + if (!super.isValid()) return false + val timestamps = timestamps ?: return false + if (timestamps.isNotEmpty()) { return true } + return false + } + + override fun toProto(): SignalServiceProtos.Content? { + val timestamps = timestamps + if (timestamps == null) { + Log.w(ExpirationTimerUpdate.TAG, "Couldn't construct read receipt proto from: $this") + return null + } + val receiptProto = SignalServiceProtos.ReceiptMessage.newBuilder() + receiptProto.type = SignalServiceProtos.ReceiptMessage.Type.READ + receiptProto.addAllTimestamp(timestamps.asIterable()) + val contentProto = SignalServiceProtos.Content.newBuilder() + try { + contentProto.receiptMessage = receiptProto.build() + return contentProto.build() + } catch (e: Exception) { + Log.w(ExpirationTimerUpdate.TAG, "Couldn't construct read receipt proto from: $this") + return null + } + } } \ 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 9610e0b88d..ec9ae23358 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 @@ -1,4 +1,75 @@ -package org.session.messaging.messages.control +package org.session.libsession.messaging.messages.control -class TypingIndicator : ControlMessage() { +import org.session.libsignal.libsignal.logging.Log +import org.session.libsignal.service.internal.push.SignalServiceProtos + +class TypingIndicator() : ControlMessage() { + + 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) + return TypingIndicator(kind = kind) + } + } + + // Kind enum + enum class Kind { + STARTED, + STOPPED, + ; + + companion object { + @JvmStatic + fun fromProto(proto: SignalServiceProtos.TypingMessage.Action): Kind = + when (proto) { + SignalServiceProtos.TypingMessage.Action.STARTED -> STARTED + SignalServiceProtos.TypingMessage.Action.STOPPED -> STOPPED + } + } + + fun toProto(): SignalServiceProtos.TypingMessage.Action { + when (this) { + STARTED -> return SignalServiceProtos.TypingMessage.Action.STARTED + STOPPED -> return SignalServiceProtos.TypingMessage.Action.STOPPED + } + } + } + + var kind: Kind? = null + + //constructor + internal constructor(kind: Kind) : this() { + this.kind = kind + } + + // validation + override fun isValid(): Boolean { + if (!super.isValid()) return false + return kind != null + } + + override fun toProto(): SignalServiceProtos.Content? { + val timestamp = sentTimestamp + val kind = kind + if (timestamp == null || kind == null) { + Log.w(TAG, "Couldn't construct typing indicator proto from: $this") + return null + } + val typingIndicatorProto = SignalServiceProtos.TypingMessage.newBuilder() + typingIndicatorProto.timestamp = timestamp + typingIndicatorProto.action = kind.toProto() + val contentProto = SignalServiceProtos.Content.newBuilder() + try { + contentProto.typingMessage = typingIndicatorProto.build() + return contentProto.build() + } catch (e: Exception) { + Log.w(TAG, "Couldn't construct typing indicator proto from: $this") + return null + } + } } \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/control/unused/NullMessage.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/control/unused/NullMessage.kt index a791c32ca3..b46bbc0231 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/control/unused/NullMessage.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/control/unused/NullMessage.kt @@ -1,6 +1,37 @@ -package org.session.messaging.messages.control.unused +package org.session.libsession.messaging.messages.control.unused -import org.session.messaging.messages.control.ControlMessage +import com.google.protobuf.ByteString +import org.session.libsession.messaging.messages.control.ControlMessage +import org.session.libsession.messaging.messages.control.TypingIndicator +import org.session.libsignal.libsignal.logging.Log +import org.session.libsignal.service.internal.push.SignalServiceProtos +import java.security.SecureRandom -class NullMessage : ControlMessage() { +class NullMessage() : ControlMessage() { + + + companion object { + const val TAG = "NullMessage" + + fun fromProto(proto: SignalServiceProtos.Content): NullMessage? { + if (proto.nullMessage == null) return null + return NullMessage() + } + } + + override fun toProto(): SignalServiceProtos.Content? { + val nullMessageProto = SignalServiceProtos.NullMessage.newBuilder() + val sr = SecureRandom() + val paddingSize = sr.nextInt(512) + val padding = ByteArray(paddingSize) + nullMessageProto.padding = ByteString.copyFrom(padding) + val contentProto = SignalServiceProtos.Content.newBuilder() + try { + contentProto.nullMessage = nullMessageProto.build() + return contentProto.build() + } catch (e: Exception) { + Log.w(TAG, "Couldn't construct null message proto from: $this") + return null + } + } } \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/control/unused/SessionRequest.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/control/unused/SessionRequest.kt index 51f99a7c16..b078ce5545 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/control/unused/SessionRequest.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/control/unused/SessionRequest.kt @@ -1,6 +1,81 @@ -package org.session.messaging.messages.control.unused +package org.session.libsession.messaging.messages.control.unused -import org.session.messaging.messages.control.ControlMessage +import com.google.protobuf.ByteString +import org.session.libsession.messaging.messages.control.ControlMessage +import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate +import org.session.libsession.messaging.messages.control.TypingIndicator +import org.session.libsignal.libsignal.logging.Log +import org.session.libsignal.libsignal.state.PreKeyBundle +import org.session.libsignal.service.internal.push.SignalServiceProtos +import java.security.SecureRandom -class SessionRequest : ControlMessage() { +class SessionRequest() : ControlMessage() { + + var preKeyBundle: PreKeyBundle? = null + + companion object { + const val TAG = "SessionRequest" + + fun fromProto(proto: SignalServiceProtos.Content): SessionRequest? { + if (proto.nullMessage == null) return null + val preKeyBundleProto = proto.preKeyBundleMessage ?: return null + val registrationID: Int = 0 + //TODO looks like database stuff here + /*iOS code: Configuration.shared.storage.with { transaction in + registrationID = Configuration.shared.storage.getOrGenerateRegistrationID(using: transaction) + }*/ + val preKeyBundle = PreKeyBundle( + registrationID, + 1, + preKeyBundleProto.preKeyId, + null, //TODO preKeyBundleProto.preKey, + 0, //TODO preKeyBundleProto.signedKey, + null, //TODO preKeyBundleProto.signedKeyId, + preKeyBundleProto.signature.toByteArray(), + null, //TODO preKeyBundleProto.identityKey + ) + return SessionRequest(preKeyBundle) + } + } + + //constructor + internal constructor(preKeyBundle: PreKeyBundle) : this() { + this.preKeyBundle = preKeyBundle + } + + // validation + override fun isValid(): Boolean { + if (!super.isValid()) return false + return preKeyBundle != null + } + + override fun toProto(): SignalServiceProtos.Content? { + val preKeyBundle = preKeyBundle + if (preKeyBundle == null) { + Log.w(TAG, "Couldn't construct session request proto from: $this") + return null + } + val nullMessageProto = SignalServiceProtos.NullMessage.newBuilder() + val sr = SecureRandom() + val paddingSize = sr.nextInt(512) + val padding = ByteArray(paddingSize) + nullMessageProto.padding = ByteString.copyFrom(padding) + val preKeyBundleProto = SignalServiceProtos.PreKeyBundleMessage.newBuilder() + //TODO preKeyBundleProto.identityKey = preKeyBundle.identityKey + preKeyBundleProto.deviceId = preKeyBundle.deviceId + preKeyBundleProto.preKeyId = preKeyBundle.preKeyId + //TODO preKeyBundleProto.preKey = preKeyBundle.preKeyPublic + preKeyBundleProto.signedKeyId = preKeyBundle.signedPreKeyId + //TODO preKeyBundleProto.signedKey = preKeyBundle.signedPreKeyPublic + preKeyBundleProto.signature = ByteString.copyFrom(preKeyBundle.signedPreKeySignature) + val contentProto = SignalServiceProtos.Content.newBuilder() + try { + contentProto.nullMessage = nullMessageProto.build() + contentProto.preKeyBundleMessage = preKeyBundleProto.build() + return contentProto.build() + } catch (e: Exception) { + Log.w(TAG, "Couldn't construct session request proto from: $this") + return null + } + } } \ No newline at end of file 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 new file mode 100644 index 0000000000..7a37ef4849 --- /dev/null +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Attachment.kt @@ -0,0 +1,71 @@ +package org.session.libsession.messaging.messages.visible + +import android.util.Size +import android.webkit.MimeTypeMap +import org.session.libsignal.service.internal.push.SignalServiceProtos +import java.io.File +import java.net.URL +import kotlin.math.absoluteValue + +class Attachment : VisibleMessage() { + + var fileName: String? = null + var contentType: String? = null + var key: ByteArray? = null + var digest: ByteArray? = null + var kind: Kind? = null + var caption: String? = null + var size: Size? = null + var sizeInBytes: Int? = 0 + var url: String? = null + + companion object { + fun fromProto(proto: SignalServiceProtos.AttachmentPointer): Attachment? { + val result = Attachment() + result.fileName = proto.fileName + fun inferContentType(): String { + val fileName = result.fileName ?: return "application/octet-stream" //TODO find equivalent to OWSMimeTypeApplicationOctetStream + val fileExtension = File(fileName).extension + val mimeTypeMap = MimeTypeMap.getSingleton() + return mimeTypeMap.getMimeTypeFromExtension(fileExtension) ?: "application/octet-stream" //TODO check that it's correct + } + 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 '&' + kind = Kind.VOICEMESSAGE + } else { + kind = Kind.GENERIC + } + result.kind = kind + result.caption = if (proto.hasCaption()) proto.caption else null + val size: Size + if (proto.hasWidth() && proto.width > 0 && proto.hasHeight() && proto.height > 0) { + size = Size(proto.width, proto.height) + } else { + size = Size(0,0) //TODO check that it's equivalent to swift: CGSize.zero + } + result.size = size + result.sizeInBytes = if (proto.size > 0) proto.size else null + result. url = proto.url + return result + } + } + + enum class Kind { + VOICEMESSAGE, + GENERIC + } + + // validation + override fun isValid(): Boolean { + if (!super.isValid()) return false + // key and digest can be nil for open group attachments + return (contentType != null && kind != null && size != null && sizeInBytes != null && url != null) + } + + override fun toProto(transaction: String): SignalServiceProtos.AttachmentPointer? { + TODO("Not implemented") + } +} \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/BaseVisibleMessage.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/BaseVisibleMessage.kt new file mode 100644 index 0000000000..5e3fab2d1c --- /dev/null +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/BaseVisibleMessage.kt @@ -0,0 +1,102 @@ +package org.session.libsession.messaging.messages.visible + +import org.session.libsignal.libsignal.logging.Log +import org.session.libsignal.service.internal.push.SignalServiceProtos + +class BaseVisibleMessage() : VisibleMessage() { + + var text: String? = null + var attachmentIDs = ArrayList() + var quote: Quote? = null + var linkPreview: LinkPreview? = null + var contact: Contact? = null + var profile: Profile? = null + + companion object { + const val TAG = "BaseVisibleMessage" + + fun fromProto(proto: SignalServiceProtos.Content): BaseVisibleMessage? { + val dataMessage = proto.dataMessage ?: return null + val result = BaseVisibleMessage() + result.text = dataMessage.body + // Attachments are handled in MessageReceiver + val quoteProto = dataMessage.quote + quoteProto?.let { + val quote = Quote.fromProto(quoteProto) + quote?.let { result.quote = quote } + } + val linkPreviewProto = dataMessage.previewList.first() + linkPreviewProto?.let { + val linkPreview = LinkPreview.fromProto(linkPreviewProto) + linkPreview?.let { result.linkPreview = linkPreview } + } + // TODO Contact + val profile = Profile.fromProto(dataMessage) + profile?.let { result.profile = profile } + return result + } + } + + // validation + override fun isValid(): Boolean { + if (!super.isValid()) return false + if (attachmentIDs.isNotEmpty()) return true + val text = text?.trim() ?: return false + if (text.isNotEmpty()) return true + return false + } + + override fun toProto(transaction: String): SignalServiceProtos.Content? { + val proto = SignalServiceProtos.Content.newBuilder() + var attachmentIDs = this.attachmentIDs + val dataMessage: SignalServiceProtos.DataMessage.Builder + // Profile + val profile = profile + val profileProto = profile?.toSSProto() + if (profileProto != null) { + dataMessage = profileProto.toBuilder() + } else { + dataMessage = SignalServiceProtos.DataMessage.newBuilder() + } + // 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(transaction) + 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(transaction) + linkPreviewProto?.let { + dataMessage.addAllPreview(listOf(linkPreviewProto)) + } + } + //Attachments + // TODO I'm blocking on that one... + //swift: let attachments = attachmentIDs.compactMap { TSAttachmentStream.fetch(uniqueId: $0, transaction: transaction) } + + // TODO Contact + // Build + try { + proto.dataMessage = dataMessage.build() + return proto.build() + } catch (e: Exception) { + Log.w(TAG, "Couldn't construct visible message proto from: $this") + return null + } + } + +} \ 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 7efb865119..cbca11cd2d 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,4 +1,16 @@ -package org.session.messaging.messages.visible +package org.session.libsession.messaging.messages.visible -internal class Contact { +import org.session.libsignal.service.internal.push.SignalServiceProtos + +class Contact : VisibleMessage() { + + companion object { + fun fromProto(proto: SignalServiceProtos.Content): Contact? { + TODO("Not yet implemented") + } + } + + override fun toProto(transaction: String): 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 a385545b51..7806e9fbe0 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,4 +1,58 @@ -package org.session.messaging.messages.visible +package org.session.libsession.messaging.messages.visible -internal class LinkPreview { +import org.session.libsession.messaging.messages.control.TypingIndicator +import org.session.libsignal.libsignal.logging.Log +import org.session.libsignal.service.internal.push.SignalServiceProtos + +class LinkPreview() : VisibleMessage(){ + + var title: String? = null + var url: String? = null + var attachmentID: String? = null + + companion object { + const val TAG = "LinkPreview" + + fun fromProto(proto: SignalServiceProtos.DataMessage.Preview): LinkPreview? { + val title = proto.title + val url = proto.url + return LinkPreview(title, url, null) + } + } + + //constructor + internal constructor(title: String?, url: String, attachmentID: String?) : this() { + this.title = title + this.url = url + this.attachmentID = attachmentID + } + + + // validation + override fun isValid(): Boolean { + if (!super.isValid()) return false + return (title != null && url != null && attachmentID != null) + } + + override fun toProto(transaction: String): SignalServiceProtos.DataMessage.Preview? { + val url = url + if (url == null) { + Log.w(TAG, "Couldn't construct link preview proto from: $this") + return null + } + val linkPreviewProto = SignalServiceProtos.DataMessage.Preview.newBuilder() + linkPreviewProto.url = url + title?.let { linkPreviewProto.title = title } + val attachmentID = attachmentID + attachmentID?.let { + //TODO database stuff + } + // Build + try { + return linkPreviewProto.build() + } catch (e: Exception) { + Log.w(TAG, "Couldn't construct link preview proto from: $this") + return null + } + } } \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Profile.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Profile.kt index 22740911ea..5a2590b56d 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Profile.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Profile.kt @@ -1,4 +1,64 @@ -package org.session.messaging.messages.visible +package org.session.libsession.messaging.messages.visible -internal class Profile { +import com.google.protobuf.ByteString +import org.session.libsignal.libsignal.logging.Log +import org.session.libsignal.service.internal.push.SignalServiceProtos + +class Profile() : VisibleMessage() { + + var displayName: String? = null + var profileKey: ByteArray? = null + var profilePictureURL: String? = null + + companion object { + const val TAG = "Profile" + + fun fromProto(proto: SignalServiceProtos.DataMessage): Profile? { + val profileProto = proto.profile ?: return null + val displayName = profileProto.displayName ?: return null + val profileKey = proto.profileKey + val profilePictureURL = profileProto.profilePictureURL + profileKey?.let { + val profilePictureURL = profilePictureURL + profilePictureURL?.let { + return Profile(displayName = displayName, profileKey = profileKey.toByteArray(), profilePictureURL = profilePictureURL) + } + } + return Profile(displayName) + } + } + + //constructor + internal constructor(displayName: String, profileKey: ByteArray? = null, profilePictureURL: String? = null) : this() { + this.displayName = displayName + this.profileKey = profileKey + this.profilePictureURL = profilePictureURL + } + + fun toSSProto(): SignalServiceProtos.DataMessage? { + return this.toProto("") + } + + override fun toProto(transaction: String): SignalServiceProtos.DataMessage? { + val displayName = displayName + if (displayName == null) { + Log.w(TAG, "Couldn't construct link preview proto from: $this") + return null + } + val dataMessageProto = SignalServiceProtos.DataMessage.newBuilder() + val profileProto = SignalServiceProtos.LokiUserProfile.newBuilder() + profileProto.displayName = displayName + val profileKey = profileKey + profileKey?.let { dataMessageProto.profileKey = ByteString.copyFrom(profileKey) } + val profilePictureURL = profilePictureURL + profilePictureURL?.let { profileProto.profilePictureURL = profilePictureURL } + // Build + try { + dataMessageProto.profile = profileProto.build() + return dataMessageProto.build() + } catch (e: Exception) { + Log.w(TAG, "Couldn't construct profile proto from: $this") + return null + } + } } \ No newline at end of file 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 90e2c287c5..0d07821e40 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,4 +1,72 @@ -package org.session.messaging.messages.visible +package org.session.libsession.messaging.messages.visible -internal class Quote { +import org.session.libsignal.libsignal.logging.Log +import org.session.libsignal.service.internal.push.SignalServiceProtos + +class Quote() : VisibleMessage() { + + var timestamp: Long? = 0 + var publicKey: String? = null + var text: String? = null + var attachmentID: String? = null + + companion object { + const val TAG = "Quote" + + fun fromProto(proto: SignalServiceProtos.DataMessage.Quote): Quote? { + val timestamp = proto.id + val publicKey = proto.author + val text = proto.text + return Quote(timestamp, publicKey, text, null) + } + } + + //constructor + internal constructor(timestamp: Long, publicKey: String, text: String?, attachmentID: String?) : this() { + this.timestamp = timestamp + this.publicKey = publicKey + this.text = text + this.attachmentID = attachmentID + } + + + // validation + override fun isValid(): Boolean { + if (!super.isValid()) return false + return (timestamp != null && publicKey != null) + } + + override fun toProto(transaction: String): SignalServiceProtos.DataMessage.Quote? { + val timestamp = timestamp + val publicKey = publicKey + if (timestamp == null || publicKey == null) { + Log.w(TAG, "Couldn't construct quote proto from: $this") + return null + } + val quoteProto = SignalServiceProtos.DataMessage.Quote.newBuilder() + quoteProto.id = timestamp + quoteProto.author = publicKey + text?.let { quoteProto.text = text } + addAttachmentsIfNeeded(quoteProto, transaction) + // Build + try { + return quoteProto.build() + } catch (e: Exception) { + Log.w(TAG, "Couldn't construct quote proto from: $this") + return null + } + } + + private fun addAttachmentsIfNeeded(quoteProto: SignalServiceProtos.DataMessage.Quote.Builder, transaction: String) { + val attachmentID = attachmentID ?: return + //TODO databas stuff + val quotedAttachmentProto = SignalServiceProtos.DataMessage.Quote.QuotedAttachment.newBuilder() + //TODO more database related stuff + //quotedAttachmentProto.contentType = + try { + quoteProto.addAttachments(quotedAttachmentProto.build()) + } catch (e: Exception) { + Log.w(TAG, "Couldn't construct quoted attachment proto from: $this") + } + } } \ No newline at end of file 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 0b332aaaca..dd1068cc45 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,6 +1,15 @@ -package org.session.messaging.messages.visible +package org.session.libsession.messaging.messages.visible -import org.session.messaging.messages.Message +import org.session.libsession.messaging.messages.Message +import org.session.libsignal.service.internal.push.SignalServiceProtos -class VisibleMessage : Message() { +abstract class VisibleMessage : Message() { + + abstract fun toProto(transaction: String): T + + final override fun toProto(): SignalServiceProtos.Content? { + //we don't need to implement this method in subclasses + //TODO it just needs an equivalent to swift: preconditionFailure("Use toProto(using:) if that exists... + TODO("Not implemented") + } } \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/attachments/Attachment.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/attachments/Attachment.kt deleted file mode 100644 index fa94a1808e..0000000000 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/attachments/Attachment.kt +++ /dev/null @@ -1,4 +0,0 @@ -package org.session.messaging.messages.visible.attachments - -internal class Attachment { -} \ No newline at end of file