From fa5edcefd504987431d9a1fdf0a6818e86d7b97d Mon Sep 17 00:00:00 2001 From: nielsandriesse Date: Wed, 12 May 2021 14:01:57 +1000 Subject: [PATCH 1/4] Minor message type refactoring --- .../messaging/messages/Destination.kt | 16 +-- .../libsession/messaging/messages/Message.kt | 10 +- .../control/ClosedGroupControlMessage.kt | 71 +++++----- .../messages/control/ConfigurationMessage.kt | 49 +++---- .../messages/control/ControlMessage.kt | 3 +- .../control/DataExtractionNotification.kt | 6 +- .../messages/control/ExpirationTimerUpdate.kt | 23 ++- .../messaging/messages/control/ReadReceipt.kt | 14 +- .../messages/control/TypingIndicator.kt | 13 +- .../messaging/messages/visible/LinkPreview.kt | 21 ++- .../messaging/messages/visible/Profile.kt | 17 +-- .../messaging/messages/visible/Quote.kt | 37 +++-- .../messages/visible/VisibleMessage.kt | 134 +++++++++--------- 13 files changed, 204 insertions(+), 210 deletions(-) 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 4a2d99da84..212e110b25 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 @@ -7,9 +7,6 @@ import org.session.libsession.messaging.threads.Address import org.session.libsession.utilities.GroupUtil import org.session.libsignal.service.loki.utilities.toHexString -typealias OpenGroupModel = OpenGroup -typealias OpenGroupV2Model = OpenGroupV2 - sealed class Destination { class Contact(var publicKey: String) : Destination() { @@ -21,11 +18,12 @@ sealed class Destination { class OpenGroup(var channel: Long, var server: String) : Destination() { internal constructor(): this(0, "") } - class OpenGroupV2(var room: String, var server: String): Destination() { + class OpenGroupV2(var room: String, var server: String) : Destination() { internal constructor(): this("", "") } companion object { + fun from(address: Address): Destination { return when { address.isContact -> { @@ -39,10 +37,12 @@ sealed class Destination { address.isOpenGroup -> { val storage = MessagingModuleConfiguration.shared.storage val threadID = storage.getThreadID(address.contactIdentifier())!! - when (val openGroup = storage.getOpenGroup(threadID) ?: storage.getV2OpenGroup(threadID)) { - is OpenGroupModel -> OpenGroup(openGroup.channel, openGroup.server) - is OpenGroupV2Model -> OpenGroupV2(openGroup.room, openGroup.server) - else -> throw Exception("Invalid OpenGroup $openGroup") + when (val openGroup = storage.getV2OpenGroup(threadID) ?: storage.getOpenGroup(threadID)) { + is org.session.libsession.messaging.open_groups.OpenGroup + -> Destination.OpenGroup(openGroup.channel, openGroup.server) + is org.session.libsession.messaging.open_groups.OpenGroupV2 + -> Destination.OpenGroupV2(openGroup.room, openGroup.server) + else -> throw Exception("Missing open group for thread with ID: $threadID.") } } else -> { 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 d6204dc123..323c3fd263 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 @@ -18,12 +18,10 @@ abstract class Message { open val isSelfSendValid: Boolean = false open fun isValid(): Boolean { - sentTimestamp?.let { - if (it <= 0) return false - } - receivedTimestamp?.let { - if (it <= 0) return false - } + val sentTimestamp = sentTimestamp + if (sentTimestamp != null && sentTimestamp <= 0) { return false } + val receivedTimestamp = receivedTimestamp + if (receivedTimestamp != null && receivedTimestamp <= 0) { return false } return sender != null && recipient != null } diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/control/ClosedGroupControlMessage.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/control/ClosedGroupControlMessage.kt index 78275ca3b2..0d2403cd24 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/control/ClosedGroupControlMessage.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/control/ClosedGroupControlMessage.kt @@ -16,9 +16,10 @@ import org.session.libsignal.utilities.Hex import org.session.libsignal.utilities.logging.Log class ClosedGroupControlMessage() : ControlMessage() { + var kind: Kind? = null - override val ttl: Long = run { - when (kind) { + override val ttl: Long get() { + return when (kind) { is Kind.EncryptionKeyPair -> 14 * 24 * 60 * 60 * 1000 else -> 14 * 24 * 60 * 60 * 1000 } @@ -26,31 +27,46 @@ class ClosedGroupControlMessage() : ControlMessage() { override val isSelfSendValid: Boolean = true - var kind: Kind? = null + override fun isValid(): Boolean { + val kind = kind + if (!super.isValid() || kind == null) return false + return when (kind) { + is Kind.New -> { + !kind.publicKey.isEmpty && kind.name.isNotEmpty() && kind.encryptionKeyPair?.publicKey != null + && kind.encryptionKeyPair?.privateKey != null && kind.members.isNotEmpty() && kind.admins.isNotEmpty() + } + is Kind.EncryptionKeyPair -> true + is Kind.NameChange -> kind.name.isNotEmpty() + is Kind.MembersAdded -> kind.members.isNotEmpty() + is Kind.MembersRemoved -> kind.members.isNotEmpty() + is Kind.MemberLeft -> true + } + } sealed class Kind { class New(var publicKey: ByteString, var name: String, var encryptionKeyPair: ECKeyPair?, var members: List, var admins: List) : Kind() { - internal constructor(): this(ByteString.EMPTY, "", null, listOf(), listOf()) + internal constructor() : this(ByteString.EMPTY, "", null, listOf(), listOf()) } - /// An encryption key pair encrypted for each member individually. - /// - /// - Note: `publicKey` is only set when an encryption key pair is sent in a one-to-one context (i.e. not in a group). + /** An encryption key pair encrypted for each member individually. + * + * **Note:** `publicKey` is only set when an encryption key pair is sent in a one-to-one context (i.e. not in a group). + */ class EncryptionKeyPair(var publicKey: ByteString?, var wrappers: Collection) : Kind() { - internal constructor(): this(null, listOf()) + internal constructor() : this(null, listOf()) } class NameChange(var name: String) : Kind() { - internal constructor(): this("") + internal constructor() : this("") } class MembersAdded(var members: List) : Kind() { - internal constructor(): this(listOf()) + internal constructor() : this(listOf()) } class MembersRemoved(var members: List) : Kind() { - internal constructor(): this(listOf()) + internal constructor() : this(listOf()) } class MemberLeft() : Kind() val description: String = - when(this) { + when (this) { is New -> "new" is EncryptionKeyPair -> "encryptionKeyPair" is NameChange -> "nameChange" @@ -65,18 +81,19 @@ class ClosedGroupControlMessage() : ControlMessage() { fun fromProto(proto: SignalServiceProtos.Content): ClosedGroupControlMessage? { if (!proto.hasDataMessage() || !proto.dataMessage.hasClosedGroupControlMessage()) return null - val closedGroupControlMessageProto = proto.dataMessage?.closedGroupControlMessage!! + val closedGroupControlMessageProto = proto.dataMessage!!.closedGroupControlMessage!! val kind: Kind - when (closedGroupControlMessageProto.type) { + when (closedGroupControlMessageProto.type!!) { DataMessage.ClosedGroupControlMessage.Type.NEW -> { val publicKey = closedGroupControlMessageProto.publicKey ?: return null val name = closedGroupControlMessageProto.name ?: return null val encryptionKeyPairAsProto = closedGroupControlMessageProto.encryptionKeyPair ?: return null try { - val encryptionKeyPair = ECKeyPair(DjbECPublicKey(encryptionKeyPairAsProto.publicKey.toByteArray()), DjbECPrivateKey(encryptionKeyPairAsProto.privateKey.toByteArray())) + val encryptionKeyPair = ECKeyPair(DjbECPublicKey(encryptionKeyPairAsProto.publicKey.toByteArray()), + DjbECPrivateKey(encryptionKeyPairAsProto.privateKey.toByteArray())) kind = Kind.New(publicKey, name, encryptionKeyPair, closedGroupControlMessageProto.membersList, closedGroupControlMessageProto.adminsList) } catch (e: Exception) { - Log.w(TAG, "Couldn't parse key pair") + Log.w(TAG, "Couldn't parse key pair from proto: $encryptionKeyPairAsProto.") return null } } @@ -107,26 +124,10 @@ class ClosedGroupControlMessage() : ControlMessage() { this.kind = kind } - override fun isValid(): Boolean { - if (!super.isValid()) return false - val kind = kind ?: return false - return when(kind) { - is Kind.New -> { - !kind.publicKey.isEmpty && kind.name.isNotEmpty() && kind.encryptionKeyPair!!.publicKey != null - && kind.encryptionKeyPair!!.privateKey != null && kind.members.isNotEmpty() && kind.admins.isNotEmpty() - } - is Kind.EncryptionKeyPair -> true - is Kind.NameChange -> kind.name.isNotEmpty() - is Kind.MembersAdded -> kind.members.isNotEmpty() - is Kind.MembersRemoved -> kind.members.isNotEmpty() - is Kind.MemberLeft -> true - } - } - override fun toProto(): SignalServiceProtos.Content? { val kind = kind if (kind == null) { - Log.w(TAG, "Couldn't construct closed group update proto from: $this") + Log.w(TAG, "Couldn't construct closed group control message proto from: $this.") return null } try { @@ -176,7 +177,7 @@ class ClosedGroupControlMessage() : ControlMessage() { contentProto.dataMessage = dataMessageProto.build() return contentProto.build() } catch (e: Exception) { - Log.w(TAG, "Couldn't construct closed group update proto from: $this") + Log.w(TAG, "Couldn't construct closed group control message proto from: $this.") return null } } @@ -188,6 +189,7 @@ class ClosedGroupControlMessage() : ControlMessage() { } companion object { + fun fromProto(proto: DataMessage.ClosedGroupControlMessage.KeyPairWrapper): KeyPairWrapper { return KeyPairWrapper(proto.publicKey.toByteArray().toHexString(), proto.encryptedKeyPair) } @@ -199,7 +201,6 @@ class ClosedGroupControlMessage() : ControlMessage() { val result = DataMessage.ClosedGroupControlMessage.KeyPairWrapper.newBuilder() result.publicKey = ByteString.copyFrom(Hex.fromStringCondensed(publicKey)) result.encryptedKeyPair = encryptedKeyPair - return try { result.build() } catch (e: Exception) { diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/control/ConfigurationMessage.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/control/ConfigurationMessage.kt index 310ec0c019..85f1d8a1b0 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/control/ConfigurationMessage.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/control/ConfigurationMessage.kt @@ -14,12 +14,15 @@ import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded import org.session.libsignal.service.loki.utilities.toHexString import org.session.libsignal.utilities.Hex -class ConfigurationMessage(var closedGroups: List, var openGroups: List, var contacts: List, var displayName: String, var profilePicture: String?, var profileKey: ByteArray): ControlMessage() { +class ConfigurationMessage(var closedGroups: List, var openGroups: List, var contacts: List, + var displayName: String, var profilePicture: String?, var profileKey: ByteArray) : ControlMessage() { + + override val isSelfSendValid: Boolean = true class ClosedGroup(var publicKey: String, var name: String, var encryptionKeyPair: ECKeyPair?, var members: List, var admins: List) { val isValid: Boolean get() = members.isNotEmpty() && admins.isNotEmpty() - internal constructor(): this("", "", null, listOf(), listOf()) + internal constructor() : this("", "", null, listOf(), listOf()) override fun toString(): String { return name @@ -56,7 +59,7 @@ class ConfigurationMessage(var closedGroups: List, var openGroups: class Contact(var publicKey: String, var name: String, var profilePicture: String?, var profileKey: ByteArray?) { - internal constructor(): this("", "", null, null) + internal constructor() : this("", "", null, null) companion object { @@ -66,8 +69,7 @@ class ConfigurationMessage(var closedGroups: List, var openGroups: val name = proto.name val profilePicture = if (proto.hasProfilePicture()) proto.profilePicture else null val profileKey = if (proto.hasProfileKey()) proto.profileKey.toByteArray() else null - - return Contact(publicKey,name,profilePicture,profileKey) + return Contact(publicKey, name, profilePicture, profileKey) } } @@ -79,18 +81,18 @@ class ConfigurationMessage(var closedGroups: List, var openGroups: } catch (e: Exception) { return null } - if (!this.profilePicture.isNullOrEmpty()) { - result.profilePicture = this.profilePicture + val profilePicture = profilePicture + if (!profilePicture.isNullOrEmpty()) { + result.profilePicture = profilePicture } - if (this.profileKey != null) { - result.profileKey = ByteString.copyFrom(this.profileKey) + val profileKey = profileKey + if (profileKey != null) { + result.profileKey = ByteString.copyFrom(profileKey) } return result.build() } } - override val isSelfSendValid: Boolean = true - companion object { fun getCurrent(contacts: List): ConfigurationMessage? { @@ -103,24 +105,22 @@ class ConfigurationMessage(var closedGroups: List, var openGroups: val profilePicture = TextSecurePreferences.getProfilePictureURL(context) val profileKey = ProfileKeyUtil.getProfileKey(context) val groups = storage.getAllGroups() - for (groupRecord in groups) { - if (groupRecord.isClosedGroup) { - if (!groupRecord.members.contains(Address.fromSerialized(storage.getUserPublicKey()!!))) continue - val groupPublicKey = GroupUtil.doubleDecodeGroupID(groupRecord.encodedId).toHexString() + for (group in groups) { + if (group.isClosedGroup) { + if (!group.members.contains(Address.fromSerialized(storage.getUserPublicKey()!!))) continue + val groupPublicKey = GroupUtil.doubleDecodeGroupID(group.encodedId).toHexString() val encryptionKeyPair = storage.getLatestClosedGroupEncryptionKeyPair(groupPublicKey) ?: continue - val closedGroup = ClosedGroup(groupPublicKey, groupRecord.title, encryptionKeyPair, groupRecord.members.map { it.serialize() }, groupRecord.admins.map { it.serialize() }) + val closedGroup = ClosedGroup(groupPublicKey, group.title, encryptionKeyPair, group.members.map { it.serialize() }, group.admins.map { it.serialize() }) closedGroups.add(closedGroup) } - if (groupRecord.isOpenGroup) { - val threadID = storage.getThreadID(groupRecord.encodedId) ?: continue + if (group.isOpenGroup) { + val threadID = storage.getThreadID(group.encodedId) ?: continue val openGroup = storage.getOpenGroup(threadID) val openGroupV2 = storage.getV2OpenGroup(threadID) - val shareUrl = openGroup?.server ?: openGroupV2?.toJoinUrl() ?: continue openGroups.add(shareUrl) } } - return ConfigurationMessage(closedGroups, openGroups, contacts, displayName, profilePicture, profileKey) } @@ -145,6 +145,7 @@ class ConfigurationMessage(var closedGroups: List, var openGroups: configurationProto.addAllOpenGroups(openGroups) configurationProto.addAllContacts(this.contacts.mapNotNull { it.toProto() }) configurationProto.displayName = displayName + val profilePicture = profilePicture if (!profilePicture.isNullOrEmpty()) { configurationProto.profilePicture = profilePicture } @@ -157,10 +158,10 @@ class ConfigurationMessage(var closedGroups: List, var openGroups: override fun toString(): String { return """ ConfigurationMessage( - closedGroups: ${(closedGroups)} - openGroups: ${(openGroups)} - displayName: $displayName - profilePicture: $profilePicture + closedGroups: ${(closedGroups)}, + openGroups: ${(openGroups)}, + displayName: $displayName, + profilePicture: $profilePicture, profileKey: $profileKey ) """.trimIndent() 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 44cd7ee4d8..fbc013d73e 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 @@ -2,5 +2,4 @@ package org.session.libsession.messaging.messages.control import org.session.libsession.messaging.messages.Message -abstract class ControlMessage : Message() { -} \ No newline at end of file +abstract class ControlMessage : Message() \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/control/DataExtractionNotification.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/control/DataExtractionNotification.kt index 5aec11827b..90cc803713 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/control/DataExtractionNotification.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/control/DataExtractionNotification.kt @@ -3,7 +3,7 @@ package org.session.libsession.messaging.messages.control import org.session.libsignal.service.internal.push.SignalServiceProtos import org.session.libsignal.utilities.logging.Log -class DataExtractionNotification(): ControlMessage() { +class DataExtractionNotification() : ControlMessage() { var kind: Kind? = null sealed class Kind { @@ -39,8 +39,8 @@ class DataExtractionNotification(): ControlMessage() { } override fun isValid(): Boolean { - if (!super.isValid()) return false - val kind = kind ?: return false + val kind = kind + if (!super.isValid() || kind == null) return false return when(kind) { is Kind.Screenshot -> true is Kind.MediaSaved -> kind.timestamp > 0 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 5d1854e815..266de5eb92 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 @@ -6,13 +6,20 @@ import org.session.libsignal.utilities.logging.Log import org.session.libsignal.service.internal.push.SignalServiceProtos class ExpirationTimerUpdate() : ControlMessage() { - /// In the case of a sync message, the public key of the person the message was targeted at. - /// - Note: `nil` if this isn't a sync message. + /** In the case of a sync message, the public key of the person the message was targeted at. + * + * **Note:** `nil` if this isn't a sync message. + */ var syncTarget: String? = null var duration: Int? = 0 override val isSelfSendValid: Boolean = true + override fun isValid(): Boolean { + if (!super.isValid()) return false + return duration != null + } + companion object { const val TAG = "ExpirationTimerUpdate" @@ -26,21 +33,11 @@ class ExpirationTimerUpdate() : ControlMessage() { } } - internal constructor(syncTarget: String?, duration: Int) : this() { + internal constructor(syncTarget: String? = null, duration: Int) : this() { this.syncTarget = syncTarget this.duration = duration } - internal constructor(duration: Int) : this() { - this.syncTarget = null - this.duration = duration - } - - override fun isValid(): Boolean { - if (!super.isValid()) return false - return duration != null - } - override fun toProto(): SignalServiceProtos.Content? { val duration = duration if (duration == null) { 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 a912740da0..1f4bc84e3e 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 @@ -6,6 +6,13 @@ import org.session.libsignal.utilities.logging.Log class ReadReceipt() : ControlMessage() { var timestamps: List? = null + override fun isValid(): Boolean { + if (!super.isValid()) return false + val timestamps = timestamps ?: return false + if (timestamps.isNotEmpty()) { return true } + return false + } + companion object { const val TAG = "ReadReceipt" @@ -22,13 +29,6 @@ class ReadReceipt() : ControlMessage() { this.timestamps = timestamps } - override fun isValid(): Boolean { - if (!super.isValid()) return false - val timestamps = timestamps ?: return false - if (timestamps.isNotEmpty()) { return true } - return false - } - override fun toProto(): SignalServiceProtos.Content? { val timestamps = timestamps if (timestamps == null) { 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 dd26ae7031..a06f821cb8 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 @@ -4,9 +4,15 @@ import org.session.libsignal.service.internal.push.SignalServiceProtos import org.session.libsignal.utilities.logging.Log class TypingIndicator() : ControlMessage() { - override val ttl: Long = 30 * 1000 var kind: Kind? = null + override val ttl: Long = 20 * 1000 + + override fun isValid(): Boolean { + if (!super.isValid()) return false + return kind != null + } + companion object { const val TAG = "TypingIndicator" @@ -41,11 +47,6 @@ class TypingIndicator() : ControlMessage() { this.kind = kind } - override fun isValid(): Boolean { - if (!super.isValid()) return false - return kind != null - } - override fun toProto(): SignalServiceProtos.Content? { val timestamp = sentTimestamp val kind = kind 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 a292bf7c6a..10c41b18b6 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 @@ -10,6 +10,10 @@ class LinkPreview() { var url: String? = null var attachmentID: Long? = 0 + fun isValid(): Boolean { + return (title != null && url != null && attachmentID != null) + } + companion object { const val TAG = "LinkPreview" @@ -20,11 +24,8 @@ class LinkPreview() { } fun from(signalLinkPreview: SignalLinkPreiview?): LinkPreview? { - return if (signalLinkPreview == null) { - null - } else { - LinkPreview(signalLinkPreview.title, signalLinkPreview.url, signalLinkPreview.attachmentId?.rowId) - } + if (signalLinkPreview == null) { return null } + return LinkPreview(signalLinkPreview.title, signalLinkPreview.url, signalLinkPreview.attachmentId?.rowId) } } @@ -34,10 +35,6 @@ class LinkPreview() { this.attachmentID = attachmentID } - fun isValid(): Boolean { - return (title != null && url != null && attachmentID != null) - } - fun toProto(): SignalServiceProtos.DataMessage.Preview? { val url = url if (url == null) { @@ -46,10 +43,10 @@ class LinkPreview() { } val linkPreviewProto = SignalServiceProtos.DataMessage.Preview.newBuilder() linkPreviewProto.url = url - title?.let { linkPreviewProto.title = title } - val attachmentID = attachmentID + title?.let { linkPreviewProto.title = it } + val database = MessagingModuleConfiguration.shared.messageDataProvider attachmentID?.let { - MessagingModuleConfiguration.shared.messageDataProvider.getSignalAttachmentPointer(attachmentID)?.let { + database.getSignalAttachmentPointer(it)?.let { val attachmentProto = Attachment.createAttachmentPointer(it) linkPreviewProto.image = attachmentProto } 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 7464a4be5d..98cb5ecafb 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 @@ -17,12 +17,11 @@ class Profile() { val displayName = profileProto.displayName ?: return null val profileKey = proto.profileKey val profilePictureURL = profileProto.profilePicture - profileKey?.let { - profilePictureURL?.let { - return Profile(displayName = displayName, profileKey = profileKey.toByteArray(), profilePictureURL = profilePictureURL) - } + if (profileKey != null && profilePictureURL != null) { + return Profile(displayName, profileKey.toByteArray(), profilePictureURL) + } else { + return Profile(displayName) } - return Profile(displayName) } } @@ -35,16 +34,14 @@ class Profile() { fun toProto(): SignalServiceProtos.DataMessage? { val displayName = displayName if (displayName == null) { - Log.w(TAG, "Couldn't construct link preview proto from: $this") + Log.w(TAG, "Couldn't construct profile proto from: $this") return null } val dataMessageProto = SignalServiceProtos.DataMessage.newBuilder() val profileProto = SignalServiceProtos.DataMessage.LokiProfile.newBuilder() profileProto.displayName = displayName - val profileKey = profileKey - profileKey?.let { dataMessageProto.profileKey = ByteString.copyFrom(profileKey) } - val profilePictureURL = profilePictureURL - profilePictureURL?.let { profileProto.profilePicture = profilePictureURL } + profileKey?.let { dataMessageProto.profileKey = ByteString.copyFrom(it) } + profilePictureURL?.let { profileProto.profilePicture = it } // Build try { dataMessageProto.profile = profileProto.build() 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 88bf089a1c..376f52fd25 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 @@ -13,6 +13,10 @@ class Quote() { var text: String? = null var attachmentID: Long? = null + fun isValid(): Boolean { + return (timestamp != null && publicKey != null) + } + companion object { const val TAG = "Quote" @@ -24,12 +28,9 @@ class Quote() { } fun from(signalQuote: SignalQuote?): Quote? { - return if (signalQuote == null) { - null - } else { - val attachmentID = (signalQuote.attachments?.firstOrNull() as? DatabaseAttachment)?.attachmentId?.rowId - Quote(signalQuote.id, signalQuote.author.serialize(), signalQuote.text, attachmentID) - } + if (signalQuote == null) { return null } + val attachmentID = (signalQuote.attachments?.firstOrNull() as? DatabaseAttachment)?.attachmentId?.rowId + return Quote(signalQuote.id, signalQuote.author.serialize(), signalQuote.text, attachmentID) } } @@ -40,10 +41,6 @@ class Quote() { this.attachmentID = attachmentID } - fun isValid(): Boolean { - return (timestamp != null && publicKey != null) - } - fun toProto(): SignalServiceProtos.DataMessage.Quote? { val timestamp = timestamp val publicKey = publicKey @@ -54,7 +51,7 @@ class Quote() { val quoteProto = SignalServiceProtos.DataMessage.Quote.newBuilder() quoteProto.id = timestamp quoteProto.author = publicKey - text?.let { quoteProto.text = text } + text?.let { quoteProto.text = it } addAttachmentsIfNeeded(quoteProto) // Build try { @@ -66,23 +63,23 @@ class Quote() { } private fun addAttachmentsIfNeeded(quoteProto: SignalServiceProtos.DataMessage.Quote.Builder) { - if (attachmentID == null) return - val attachment = MessagingModuleConfiguration.shared.messageDataProvider.getSignalAttachmentPointer(attachmentID!!) - if (attachment == null) { + val attachmentID = attachmentID ?: return + val database = MessagingModuleConfiguration.shared.messageDataProvider + val pointer = database.getSignalAttachmentPointer(attachmentID) + if (pointer == null) { Log.w(TAG, "Ignoring invalid attachment for quoted message.") return } - if (attachment.url.isNullOrEmpty()) { + if (pointer.url.isNullOrEmpty()) { if (BuildConfig.DEBUG) { - //TODO equivalent to iOS's preconditionFailure - Log.d(TAG,"Sending a message before all associated attachments have been uploaded.") + Log.w(TAG,"Sending a message before all associated attachments have been uploaded.") return } } val quotedAttachmentProto = SignalServiceProtos.DataMessage.Quote.QuotedAttachment.newBuilder() - quotedAttachmentProto.contentType = attachment.contentType - if (attachment.fileName.isPresent) quotedAttachmentProto.fileName = attachment.fileName.get() - quotedAttachmentProto.thumbnail = Attachment.createAttachmentPointer(attachment) + quotedAttachmentProto.contentType = pointer.contentType + if (pointer.fileName.isPresent) { quotedAttachmentProto.fileName = pointer.fileName.get() } + quotedAttachmentProto.thumbnail = Attachment.createAttachmentPointer(pointer) try { quoteProto.addAttachments(quotedAttachmentProto.build()) } catch (e: Exception) { 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 63756c0948..8c795d22e8 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 @@ -12,6 +12,10 @@ import org.session.libsignal.utilities.logging.Log import org.session.libsession.messaging.sending_receiving.attachments.Attachment as SignalAttachment class VisibleMessage : Message() { + /** In the case of a sync message, the public key of the person the message was targeted at. + * + * **Note:** `nil` if this isn't a sync message. + */ var syncTarget: String? = null var text: String? = null val attachmentIDs: MutableList = mutableListOf() @@ -21,46 +25,7 @@ class VisibleMessage : Message() { override val isSelfSendValid: Boolean = true - companion object { - const val TAG = "VisibleMessage" - - fun fromProto(proto: SignalServiceProtos.Content): VisibleMessage? { - val dataMessage = if (proto.hasDataMessage()) proto.dataMessage else return null - val result = VisibleMessage() - if (dataMessage.hasSyncTarget()) { - result.syncTarget = dataMessage.syncTarget - } - result.text = dataMessage.body - // Attachments are handled in MessageReceiver - val quoteProto = if (dataMessage.hasQuote()) dataMessage.quote else null - quoteProto?.let { - val quote = Quote.fromProto(quoteProto) - quote?.let { result.quote = quote } - } - val linkPreviewProto = dataMessage.previewList.firstOrNull() - 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 - } - } - - fun addSignalAttachments(signalAttachments: List) { - val attachmentIDs = signalAttachments.map { - val databaseAttachment = it as DatabaseAttachment - databaseAttachment.attachmentId.rowId - } - this.attachmentIDs.addAll(attachmentIDs) - } - - fun isMediaMessage(): Boolean { - return attachmentIDs.isNotEmpty() || quote != null || linkPreview != null - } - + // region Validation override fun isValid(): Boolean { if (!super.isValid()) return false if (attachmentIDs.isNotEmpty()) return true @@ -68,56 +33,84 @@ class VisibleMessage : Message() { if (text.isNotEmpty()) return true return false } + // endregion + + // region Proto Conversion + companion object { + const val TAG = "VisibleMessage" + + fun fromProto(proto: SignalServiceProtos.Content): VisibleMessage? { + val dataMessage = proto.dataMessage ?: return null + val result = VisibleMessage() + if (dataMessage.hasSyncTarget()) { result.syncTarget = dataMessage.syncTarget } + result.text = dataMessage.body + // Attachments are handled in MessageReceiver + val quoteProto = if (dataMessage.hasQuote()) dataMessage.quote else null + if (quoteProto != null) { + val quote = Quote.fromProto(quoteProto) + result.quote = quote + } + val linkPreviewProto = dataMessage.previewList.firstOrNull() + if (linkPreviewProto != null) { + val linkPreview = LinkPreview.fromProto(linkPreviewProto) + result.linkPreview = linkPreview + } + // TODO: Contact + val profile = Profile.fromProto(dataMessage) + if (profile != null) { result.profile = profile } + return result + } + } override fun toProto(): SignalServiceProtos.Content? { val proto = SignalServiceProtos.Content.newBuilder() val dataMessage: SignalServiceProtos.DataMessage.Builder // Profile - val profile = profile - val profileProto = profile?.toProto() + val profileProto = profile?.let { it.toProto() } if (profileProto != null) { dataMessage = profileProto.toBuilder() } else { dataMessage = SignalServiceProtos.DataMessage.newBuilder() } // Text - text?.let { dataMessage.body = text } + if (text != null) { dataMessage.body = text } // Quote - quote?.let { - val quoteProto = it.toProto() - if (quoteProto != null) dataMessage.quote = quoteProto + val quoteProto = quote?.let { it.toProto() } + if (quoteProto != null) { + dataMessage.quote = quoteProto } - //Link preview - linkPreview?.let { - val linkPreviewProto = it.toProto() - linkPreviewProto?.let { - dataMessage.addAllPreview(listOf(linkPreviewProto)) - } + // Link preview + val linkPreviewProto = linkPreview?.let { it.toProto() } + if (linkPreviewProto != null) { + dataMessage.addAllPreview(listOf(linkPreviewProto)) } - //Attachments - val attachments = attachmentIDs.mapNotNull { MessagingModuleConfiguration.shared.messageDataProvider.getSignalAttachmentPointer(it) } - if (!attachments.all { !it.url.isNullOrEmpty() }) { + // Attachments + val database = MessagingModuleConfiguration.shared.messageDataProvider + val attachments = attachmentIDs.mapNotNull { database.getSignalAttachmentPointer(it) } + if (attachments.any { it.url.isNullOrEmpty() }) { if (BuildConfig.DEBUG) { - //TODO equivalent to iOS's preconditionFailure - Log.d(TAG, "Sending a message before all associated attachments have been uploaded.") + Log.w(TAG, "Sending a message before all associated attachments have been uploaded.") } } - val attachmentPointers = attachments.mapNotNull { Attachment.createAttachmentPointer(it) } - dataMessage.addAllAttachments(attachmentPointers) - // TODO Contact + val pointers = attachments.mapNotNull { Attachment.createAttachmentPointer(it) } + dataMessage.addAllAttachments(pointers) + // TODO: Contact // Expiration timer // TODO: We * want * expiration timer updates to be explicit. But currently Android will disable the expiration timer for a conversation - // if it receives a message without the current expiration timer value attached to it... + // if it receives a message without the current expiration timer value attached to it... val storage = MessagingModuleConfiguration.shared.storage val context = MessagingModuleConfiguration.shared.context - val expiration = if (storage.isClosedGroup(recipient!!)) Recipient.from(context, Address.fromSerialized(GroupUtil.doubleEncodeGroupID(recipient!!)), false).expireMessages - else Recipient.from(context, Address.fromSerialized(recipient!!), false).expireMessages + val expiration = if (storage.isClosedGroup(recipient!!)) { + Recipient.from(context, Address.fromSerialized(GroupUtil.doubleEncodeGroupID(recipient!!)), false).expireMessages + } else { + Recipient.from(context, Address.fromSerialized(recipient!!), false).expireMessages + } dataMessage.expireTimer = expiration // Group context if (storage.isClosedGroup(recipient!!)) { try { setGroupContext(dataMessage) - } catch(e: Exception) { + } catch (e: Exception) { Log.w(TAG, "Couldn't construct visible message proto from: $this") return null } @@ -135,4 +128,17 @@ class VisibleMessage : Message() { return null } } + // endregion + + fun addSignalAttachments(signalAttachments: List) { + val attachmentIDs = signalAttachments.map { + val databaseAttachment = it as DatabaseAttachment + databaseAttachment.attachmentId.rowId + } + this.attachmentIDs.addAll(attachmentIDs) + } + + fun isMediaMessage(): Boolean { + return attachmentIDs.isNotEmpty() || quote != null || linkPreview != null + } } \ No newline at end of file From 21698fcba51625137ca25d5622e8e6f57d654002 Mon Sep 17 00:00:00 2001 From: nielsandriesse Date: Wed, 12 May 2021 14:02:07 +1000 Subject: [PATCH 2/4] Update version number --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 2888a2ba07..74738b8801 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -158,8 +158,8 @@ dependencies { testImplementation 'org.robolectric:shadows-multidex:4.2' } -def canonicalVersionCode = 158 -def canonicalVersionName = "1.10.1" +def canonicalVersionCode = 159 +def canonicalVersionName = "1.10.2" def postFixSize = 10 def abiPostFix = ['armeabi-v7a' : 1, From d8932416f145696607bed624223cf8d2f0201bac Mon Sep 17 00:00:00 2001 From: nielsandriesse Date: Wed, 12 May 2021 14:48:13 +1000 Subject: [PATCH 3/4] Minor V2 open group refactoring --- .../securesms/database/Storage.kt | 2 +- .../loki/activities/JoinPublicChatActivity.kt | 2 +- .../loki/database/LokiThreadDatabase.kt | 4 +- .../messaging/file_server/FileServerAPIV2.kt | 7 +- .../messages/control/ConfigurationMessage.kt | 2 +- .../messages/control/ExpirationTimerUpdate.kt | 7 +- .../messaging/open_groups/OpenGroupAPIV2.kt | 292 ++++++++---------- .../open_groups/OpenGroupMessageV2.kt | 54 ++-- .../messaging/open_groups/OpenGroupV2.kt | 41 ++- .../ReceivedMessageHandler.kt | 2 +- 10 files changed, 197 insertions(+), 216 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt index 0ab4824343..6a2ff6e8df 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt @@ -257,7 +257,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, val database = databaseHelper.readableDatabase return database.get(LokiThreadDatabase.publicChatTable, "${LokiThreadDatabase.threadID} = ?", arrayOf(threadId)) { cursor -> val publicChatAsJson = cursor.getString(LokiThreadDatabase.publicChat) - OpenGroupV2.fromJson(publicChatAsJson) + OpenGroupV2.fromJSON(publicChatAsJson) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/JoinPublicChatActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/activities/JoinPublicChatActivity.kt index 6686ca8345..f0490d379d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/JoinPublicChatActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/activities/JoinPublicChatActivity.kt @@ -195,7 +195,7 @@ class EnterChatURLFragment : Fragment() { chip.chipIcon = drawable chip.text = defaultGroup.name chip.setOnClickListener { - (requireActivity() as JoinPublicChatActivity).joinPublicChatIfPossible(defaultGroup.toJoinUrl()) + (requireActivity() as JoinPublicChatActivity).joinPublicChatIfPossible(defaultGroup.joinURL) } defaultRoomsGridLayout.addView(chip) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/database/LokiThreadDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/database/LokiThreadDatabase.kt index ba9c0ff477..68ca31cea4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/database/LokiThreadDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/database/LokiThreadDatabase.kt @@ -68,7 +68,7 @@ class LokiThreadDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa while (cursor != null && cursor.moveToNext()) { val threadID = cursor.getLong(threadID) val string = cursor.getString(publicChat) - val openGroup = OpenGroupV2.fromJson(string) + val openGroup = OpenGroupV2.fromJSON(string) if (openGroup != null) result[threadID] = openGroup } } catch (e: Exception) { @@ -100,7 +100,7 @@ class LokiThreadDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa val database = databaseHelper.readableDatabase return database.get(publicChatTable, "${Companion.threadID} = ?", arrayOf(threadID.toString())) { cursor -> val json = cursor.getString(publicChat) - OpenGroupV2.fromJson(json) + OpenGroupV2.fromJSON(json) } } diff --git a/libsession/src/main/java/org/session/libsession/messaging/file_server/FileServerAPIV2.kt b/libsession/src/main/java/org/session/libsession/messaging/file_server/FileServerAPIV2.kt index 9469514871..1762a797d9 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/file_server/FileServerAPIV2.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/file_server/FileServerAPIV2.kt @@ -1,17 +1,14 @@ package org.session.libsession.messaging.file_server import nl.komponents.kovenant.Promise -import nl.komponents.kovenant.functional.bind import nl.komponents.kovenant.functional.map import okhttp3.Headers import okhttp3.HttpUrl import okhttp3.MediaType import okhttp3.RequestBody -import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.open_groups.OpenGroupAPIV2 import org.session.libsession.snode.OnionRequestAPI import org.session.libsignal.service.loki.HTTP -import org.session.libsignal.service.loki.utilities.retryIfNeeded import org.session.libsignal.utilities.Base64 import org.session.libsignal.utilities.JsonUtil import org.session.libsignal.utilities.logging.Log @@ -51,7 +48,7 @@ object FileServerAPIV2 { } private fun send(request: Request): Promise, Exception> { - val parsed = HttpUrl.parse(DEFAULT_SERVER) ?: return Promise.ofFail(OpenGroupAPIV2.Error.INVALID_URL) + val parsed = HttpUrl.parse(DEFAULT_SERVER) ?: return Promise.ofFail(OpenGroupAPIV2.Error.InvalidURL) val urlBuilder = HttpUrl.Builder() .scheme(parsed.scheme()) .host(parsed.host()) @@ -91,7 +88,7 @@ object FileServerAPIV2 { val parameters = mapOf("file" to base64EncodedFile) val request = Request(verb = HTTP.Verb.POST, endpoint = "files", parameters = parameters) return send(request).map { json -> - json["result"] as? Long ?: throw OpenGroupAPIV2.Error.PARSING_FAILED + json["result"] as? Long ?: throw OpenGroupAPIV2.Error.ParsingFailed } } diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/control/ConfigurationMessage.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/control/ConfigurationMessage.kt index 85f1d8a1b0..1a15d34860 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/control/ConfigurationMessage.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/control/ConfigurationMessage.kt @@ -117,7 +117,7 @@ class ConfigurationMessage(var closedGroups: List, var openGroups: val threadID = storage.getThreadID(group.encodedId) ?: continue val openGroup = storage.getOpenGroup(threadID) val openGroupV2 = storage.getV2OpenGroup(threadID) - val shareUrl = openGroup?.server ?: openGroupV2?.toJoinUrl() ?: continue + val shareUrl = openGroup?.server ?: openGroupV2?.joinURL ?: continue openGroups.add(shareUrl) } } 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 266de5eb92..9aa777782a 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 @@ -33,7 +33,12 @@ class ExpirationTimerUpdate() : ControlMessage() { } } - internal constructor(syncTarget: String? = null, duration: Int) : this() { + internal constructor(duration: Int) : this() { + this.syncTarget = null + this.duration = duration + } + + internal constructor(syncTarget: String, duration: Int) : this() { this.syncTarget = syncTarget this.duration = duration } diff --git a/libsession/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupAPIV2.kt b/libsession/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupAPIV2.kt index 7714a66e5d..7f88cd3bc8 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupAPIV2.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupAPIV2.kt @@ -5,7 +5,6 @@ import com.fasterxml.jackson.databind.annotation.JsonNaming import com.fasterxml.jackson.databind.type.TypeFactory import kotlinx.coroutines.* import kotlinx.coroutines.flow.MutableSharedFlow -import nl.komponents.kovenant.Kovenant import nl.komponents.kovenant.Promise import nl.komponents.kovenant.functional.bind import nl.komponents.kovenant.functional.map @@ -14,7 +13,6 @@ import okhttp3.HttpUrl import okhttp3.MediaType import okhttp3.RequestBody import org.session.libsession.messaging.MessagingModuleConfiguration -import org.session.libsession.messaging.open_groups.OpenGroupAPIV2.Error import org.session.libsession.snode.OnionRequestAPI import org.session.libsession.utilities.AESGCM import org.session.libsignal.service.loki.HTTP @@ -29,62 +27,48 @@ import org.whispersystems.curve25519.Curve25519 import java.util.* object OpenGroupAPIV2 { - private val moderators: HashMap> = hashMapOf() // Server URL to (channel ID to set of moderator IDs) - const val DEFAULT_SERVER = "http://116.203.70.33" - private const val DEFAULT_SERVER_PUBLIC_KEY = "a03c383cf63c3c4efe67acc52112a6dd734b3a946b9545f488aaa93da7991238" - + private val curve = Curve25519.getInstance(Curve25519.BEST) val defaultRooms = MutableSharedFlow>(replay = 1) - private val curve = Curve25519.getInstance(Curve25519.BEST) + private const val DEFAULT_SERVER_PUBLIC_KEY = "a03c383cf63c3c4efe67acc52112a6dd734b3a946b9545f488aaa93da7991238" + const val DEFAULT_SERVER = "http://116.203.70.33" sealed class Error : Exception() { - object GENERIC : Error() - object PARSING_FAILED : Error() - object DECRYPTION_FAILED : Error() - object SIGNING_FAILED : Error() - object INVALID_URL : Error() - object NO_PUBLIC_KEY : Error() + object Generic : Error() + object ParsingFailed : Error() + object DecryptionFailed : Error() + object SigningFailed : Error() + object InvalidURL : Error() + object NoPublicKey : Error() fun errorDescription() = when (this) { - Error.GENERIC -> "An error occurred." - Error.PARSING_FAILED -> "Invalid response." - Error.DECRYPTION_FAILED -> "Couldn't decrypt response." - Error.SIGNING_FAILED -> "Couldn't sign message." - Error.INVALID_URL -> "Invalid URL." - Error.NO_PUBLIC_KEY -> "Couldn't find server public key." + Error.Generic -> "An error occurred." + Error.ParsingFailed -> "Invalid response." + Error.DecryptionFailed -> "Couldn't decrypt response." + Error.SigningFailed -> "Couldn't sign message." + Error.InvalidURL -> "Invalid URL." + Error.NoPublicKey -> "Couldn't find server public key." } } - data class DefaultGroup(val id: String, - val name: String, - val image: ByteArray?) { - fun toJoinUrl(): String = "$DEFAULT_SERVER/$id?public_key=$DEFAULT_SERVER_PUBLIC_KEY" + data class DefaultGroup(val id: String, val name: String, val image: ByteArray?) { + + val joinURL: String get() = "$DEFAULT_SERVER/$id?public_key=$DEFAULT_SERVER_PUBLIC_KEY" } - data class Info( - val id: String, - val name: String, - val imageID: String? - ) + data class Info(val id: String, val name: String, val imageID: String?) @JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy::class) - data class CompactPollRequest(val roomId: String, - val authToken: String, - val fromDeletionServerId: Long?, - val fromMessageServerId: Long? - ) - - data class CompactPollResult(val messages: List, - val deletions: List, - val moderators: List - ) + data class CompactPollRequest(val roomID: String, val authToken: String, val fromDeletionServerID: Long?, val fromMessageServerID: Long?) + data class CompactPollResult(val messages: List, val deletions: List, val moderators: List) @JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy::class) - data class MessageDeletion @JvmOverloads constructor(val id: Long = 0, - val deletedMessageId: Long = 0 + data class MessageDeletion + @JvmOverloads constructor(val id: Long = 0, val deletedMessageId: Long = 0 ) { + companion object { val EMPTY = MessageDeletion() } @@ -99,38 +83,37 @@ object OpenGroupAPIV2 { val parameters: Any? = null, val headers: Map = mapOf(), val isAuthRequired: Boolean = true, - // Always `true` under normal circumstances. You might want to disable - // this when running over Lokinet. + /** + * Always `true` under normal circumstances. You might want to disable + * this when running over Lokinet. + */ val useOnionRouting: Boolean = true ) private fun createBody(parameters: Any?): RequestBody? { if (parameters == null) return null - val parametersAsJSON = JsonUtil.toJson(parameters) return RequestBody.create(MediaType.get("application/json"), parametersAsJSON) } private fun send(request: Request, isJsonRequired: Boolean = true): Promise, Exception> { - val parsed = HttpUrl.parse(request.server) ?: return Promise.ofFail(Error.INVALID_URL) + val url = HttpUrl.parse(request.server) ?: return Promise.ofFail(Error.InvalidURL) val urlBuilder = HttpUrl.Builder() - .scheme(parsed.scheme()) - .host(parsed.host()) - .port(parsed.port()) - .addPathSegments(request.endpoint) - + .scheme(url.scheme()) + .host(url.host()) + .port(url.port()) + .addPathSegments(request.endpoint) if (request.verb == GET) { for ((key, value) in request.queryParameters) { urlBuilder.addQueryParameter(key, value) } } - fun execute(token: String?): Promise, Exception> { val requestBuilder = okhttp3.Request.Builder() - .url(urlBuilder.build()) - .headers(Headers.of(request.headers)) + .url(urlBuilder.build()) + .headers(Headers.of(request.headers)) if (request.isAuthRequired) { - if (token.isNullOrEmpty()) throw IllegalStateException("No auth token for request") + if (token.isNullOrEmpty()) throw IllegalStateException("No auth token for request.") requestBuilder.header("Authorization", token) } when (request.verb) { @@ -139,25 +122,25 @@ object OpenGroupAPIV2 { POST -> requestBuilder.post(createBody(request.parameters)!!) DELETE -> requestBuilder.delete(createBody(request.parameters)) } - if (!request.room.isNullOrEmpty()) { requestBuilder.header("Room", request.room) } - if (request.useOnionRouting) { val publicKey = MessagingModuleConfiguration.shared.storage.getOpenGroupPublicKey(request.server) - ?: return Promise.ofFail(Error.NO_PUBLIC_KEY) - return OnionRequestAPI.sendOnionRequest(requestBuilder.build(), request.server, publicKey, isJSONRequired = isJsonRequired) - .fail { e -> - if (e is OnionRequestAPI.HTTPRequestFailedAtDestinationException && e.statusCode == 401) { - val storage = MessagingModuleConfiguration.shared.storage - if (request.room != null) { - storage.removeAuthToken("${request.server}.${request.room}") - } else { - storage.removeAuthToken(request.server) - } - } + ?: return Promise.ofFail(Error.NoPublicKey) + return OnionRequestAPI.sendOnionRequest(requestBuilder.build(), request.server, publicKey, isJSONRequired = isJsonRequired).fail { e -> + // A 401 means that we didn't provide a (valid) auth token for a route that required one. We use this as an + // indication that the token we're using has expired. Note that a 403 has a different meaning; it means that + // we provided a valid token but it doesn't have a high enough permission level for the route in question. + if (e is OnionRequestAPI.HTTPRequestFailedAtDestinationException && e.statusCode == 401) { + val storage = MessagingModuleConfiguration.shared.storage + if (request.room != null) { + storage.removeAuthToken("${request.server}.${request.room}") + } else { + storage.removeAuthToken(request.server) } + } + } } else { return Promise.ofFail(IllegalStateException("It's currently not allowed to send non onion routed requests.")) } @@ -172,52 +155,51 @@ object OpenGroupAPIV2 { fun downloadOpenGroupProfilePicture(roomID: String, server: String): Promise { val request = Request(verb = GET, room = roomID, server = server, endpoint = "rooms/$roomID/image", isAuthRequired = false) return send(request).map { json -> - val result = json["result"] as? String ?: throw Error.PARSING_FAILED + val result = json["result"] as? String ?: throw Error.ParsingFailed decode(result) } } + // region Authorization fun getAuthToken(room: String, server: String): Promise { val storage = MessagingModuleConfiguration.shared.storage return storage.getAuthToken(room, server)?.let { Promise.of(it) } ?: run { requestNewAuthToken(room, server) - .bind { claimAuthToken(it, room, server) } - .success { authToken -> - storage.setAuthToken(room, server, authToken) - } + .bind { claimAuthToken(it, room, server) } + .success { authToken -> + storage.setAuthToken(room, server, authToken) + } } } fun requestNewAuthToken(room: String, server: String): Promise { val (publicKey, privateKey) = MessagingModuleConfiguration.shared.storage.getUserKeyPair() - ?: return Promise.ofFail(Error.GENERIC) - val queryParameters = mutableMapOf("public_key" to publicKey) + ?: return Promise.ofFail(Error.Generic) + val queryParameters = mutableMapOf( "public_key" to publicKey ) val request = Request(GET, room, server, "auth_token_challenge", queryParameters, isAuthRequired = false, parameters = null) return send(request).map { json -> - val challenge = json["challenge"] as? Map<*, *> ?: throw Error.PARSING_FAILED - val base64EncodedCiphertext = challenge["ciphertext"] as? String - ?: throw Error.PARSING_FAILED - val base64EncodedEphemeralPublicKey = challenge["ephemeral_public_key"] as? String - ?: throw Error.PARSING_FAILED + val challenge = json["challenge"] as? Map<*, *> ?: throw Error.ParsingFailed + val base64EncodedCiphertext = challenge["ciphertext"] as? String ?: throw Error.ParsingFailed + val base64EncodedEphemeralPublicKey = challenge["ephemeral_public_key"] as? String ?: throw Error.ParsingFailed val ciphertext = decode(base64EncodedCiphertext) val ephemeralPublicKey = decode(base64EncodedEphemeralPublicKey) val symmetricKey = AESGCM.generateSymmetricKey(ephemeralPublicKey, privateKey) val tokenAsData = try { AESGCM.decrypt(ciphertext, symmetricKey) } catch (e: Exception) { - throw Error.DECRYPTION_FAILED + throw Error.DecryptionFailed } tokenAsData.toHexString() } } fun claimAuthToken(authToken: String, room: String, server: String): Promise { - val parameters = mapOf("public_key" to MessagingModuleConfiguration.shared.storage.getUserPublicKey()!!) - val headers = mapOf("Authorization" to authToken) + val parameters = mapOf( "public_key" to MessagingModuleConfiguration.shared.storage.getUserPublicKey()!! ) + val headers = mapOf( "Authorization" to authToken ) val request = Request(verb = POST, room = room, server = server, endpoint = "claim_auth_token", - parameters = parameters, headers = headers, isAuthRequired = false) + parameters = parameters, headers = headers, isAuthRequired = false) return send(request).map { authToken } } @@ -227,33 +209,36 @@ object OpenGroupAPIV2 { MessagingModuleConfiguration.shared.storage.removeAuthToken(room, server) } } + // endregion - // region Sending + // region Upload/Download fun upload(file: ByteArray, room: String, server: String): Promise { val base64EncodedFile = encodeBytes(file) - val parameters = mapOf("file" to base64EncodedFile) + val parameters = mapOf( "file" to base64EncodedFile ) val request = Request(verb = POST, room = room, server = server, endpoint = "files", parameters = parameters) return send(request).map { json -> - json["result"] as? Long ?: throw Error.PARSING_FAILED + json["result"] as? Long ?: throw Error.ParsingFailed } } fun download(file: Long, room: String, server: String): Promise { val request = Request(verb = GET, room = room, server = server, endpoint = "files/$file") return send(request).map { json -> - val base64EncodedFile = json["result"] as? String ?: throw Error.PARSING_FAILED - decode(base64EncodedFile) ?: throw Error.PARSING_FAILED + val base64EncodedFile = json["result"] as? String ?: throw Error.ParsingFailed + decode(base64EncodedFile) ?: throw Error.ParsingFailed } } + // endregion + // region Sending fun send(message: OpenGroupMessageV2, room: String, server: String): Promise { - val signedMessage = message.sign() ?: return Promise.ofFail(Error.SIGNING_FAILED) + val signedMessage = message.sign() ?: return Promise.ofFail(Error.SigningFailed) val jsonMessage = signedMessage.toJSON() val request = Request(verb = POST, room = room, server = server, endpoint = "messages", parameters = jsonMessage) return send(request).map { json -> @Suppress("UNCHECKED_CAST") val rawMessage = json["message"] as? Map - ?: throw Error.PARSING_FAILED - OpenGroupMessageV2.fromJSON(rawMessage) ?: throw Error.PARSING_FAILED + ?: throw Error.ParsingFailed + OpenGroupMessageV2.fromJSON(rawMessage) ?: throw Error.ParsingFailed } } // endregion @@ -268,10 +253,9 @@ object OpenGroupAPIV2 { val request = Request(verb = GET, room = room, server = server, endpoint = "messages", queryParameters = queryParameters) return send(request).map { jsonList -> @Suppress("UNCHECKED_CAST") val rawMessages = jsonList["messages"] as? List> - ?: throw Error.PARSING_FAILED - val lastMessageServerId = storage.getLastMessageServerId(room, server) ?: 0 - - var currentMax = lastMessageServerId + ?: throw Error.ParsingFailed + val lastMessageServerID = storage.getLastMessageServerId(room, server) ?: 0 + var currentLastMessageServerID = lastMessageServerID val messages = rawMessages.mapNotNull { json -> try { val message = OpenGroupMessageV2.fromJSON(json) ?: return@mapNotNull null @@ -285,15 +269,15 @@ object OpenGroupAPIV2 { Log.d("Loki", "Ignoring message with invalid signature") return@mapNotNull null } - if (message.serverID > lastMessageServerId) { - currentMax = message.serverID + if (message.serverID > lastMessageServerID) { + currentLastMessageServerID = message.serverID } message } catch (e: Exception) { null } } - storage.setLastMessageServerId(room, server, currentMax) + storage.setLastMessageServerId(room, server, currentLastMessageServerID) messages } } @@ -304,7 +288,7 @@ object OpenGroupAPIV2 { fun deleteMessage(serverID: Long, room: String, server: String): Promise { val request = Request(verb = DELETE, room = room, server = server, endpoint = "messages/$serverID") return send(request).map { - Log.d("Loki", "Deleted server message") + Log.d("Loki", "Message deletion successful.") } } @@ -318,7 +302,7 @@ object OpenGroupAPIV2 { return send(request).map { json -> val type = TypeFactory.defaultInstance().constructCollectionType(List::class.java, MessageDeletion::class.java) val idsAsString = JsonUtil.toJson(json["ids"]) - val serverIDs = JsonUtil.fromJson>(idsAsString, type) ?: throw Error.PARSING_FAILED + val serverIDs = JsonUtil.fromJson>(idsAsString, type) ?: throw Error.ParsingFailed val lastMessageServerId = storage.getLastDeletionServerId(room, server) ?: 0 val serverID = serverIDs.maxByOrNull {it.id } ?: MessageDeletion.EMPTY if (serverID.id > lastMessageServerId) { @@ -338,7 +322,7 @@ object OpenGroupAPIV2 { val request = Request(verb = GET, room = room, server = server, endpoint = "moderators") return send(request).map { json -> @Suppress("UNCHECKED_CAST") val moderatorsJson = json["moderators"] as? List - ?: throw Error.PARSING_FAILED + ?: throw Error.ParsingFailed val id = "$server.$room" handleModerators(id, moderatorsJson) moderatorsJson @@ -347,90 +331,86 @@ object OpenGroupAPIV2 { @JvmStatic fun ban(publicKey: String, room: String, server: String): Promise { - val parameters = mapOf("public_key" to publicKey) + val parameters = mapOf( "public_key" to publicKey ) val request = Request(verb = POST, room = room, server = server, endpoint = "block_list", parameters = parameters) return send(request).map { - Log.d("Loki", "Banned user $publicKey from $server.$room") + Log.d("Loki", "Banned user: $publicKey from: $server.$room.") } } fun unban(publicKey: String, room: String, server: String): Promise { val request = Request(verb = DELETE, room = room, server = server, endpoint = "block_list/$publicKey") return send(request).map { - Log.d("Loki", "Unbanned user $publicKey from $server.$room") + Log.d("Loki", "Unbanned user: $publicKey from: $server.$room") } } @JvmStatic fun isUserModerator(publicKey: String, room: String, server: String): Boolean = - moderators["$server.$room"]?.contains(publicKey) ?: false + moderators["$server.$room"]?.contains(publicKey) ?: false // endregion // region General @Suppress("UNCHECKED_CAST") fun getCompactPoll(rooms: List, server: String): Promise, Exception> { - val requestAuths = rooms.associateWith { room -> getAuthToken(room, server) } + val authTokenRequests = rooms.associateWith { room -> getAuthToken(room, server) } val storage = MessagingModuleConfiguration.shared.storage val requests = rooms.mapNotNull { room -> val authToken = try { - requestAuths[room]?.get() + authTokenRequests[room]?.get() } catch (e: Exception) { - Log.e("Loki", "Failed to get auth token for $room", e) + Log.e("Loki", "Failed to get auth token for $room.", e) null } ?: return@mapNotNull null - - CompactPollRequest(roomId = room, - authToken = authToken, - fromDeletionServerId = storage.getLastDeletionServerId(room, server), - fromMessageServerId = storage.getLastMessageServerId(room, server) + CompactPollRequest( + roomID = room, + authToken = authToken, + fromDeletionServerID = storage.getLastDeletionServerId(room, server), + fromMessageServerID = storage.getLastMessageServerId(room, server) ) } - val request = Request(verb = POST, room = null, server = server, endpoint = "compact_poll", isAuthRequired = false, parameters = mapOf("requests" to requests)) - // build a request for all rooms + val request = Request(verb = POST, room = null, server = server, endpoint = "compact_poll", isAuthRequired = false, parameters = mapOf( "requests" to requests )) return send(request = request).map { json -> - val results = json["results"] as? List<*> ?: throw Error.PARSING_FAILED - - results.mapNotNull { roomJson -> - if (roomJson !is Map<*,*>) return@mapNotNull null - val roomId = roomJson["room_id"] as? String ?: return@mapNotNull null - - // check the status was fine - val statusCode = roomJson["status_code"] as? Int ?: return@mapNotNull null + val results = json["results"] as? List<*> ?: throw Error.ParsingFailed + results.mapNotNull { json -> + if (json !is Map<*,*>) return@mapNotNull null + val roomID = json["room_id"] as? String ?: return@mapNotNull null + // A 401 means that we didn't provide a (valid) auth token for a route that required one. We use this as an + // indication that the token we're using has expired. Note that a 403 has a different meaning; it means that + // we provided a valid token but it doesn't have a high enough permission level for the route in question. + val statusCode = json["status_code"] as? Int ?: return@mapNotNull null if (statusCode == 401) { // delete auth token and return null - storage.removeAuthToken(roomId, server) + storage.removeAuthToken(roomID, server) } - - // check and store mods - val moderators = roomJson["moderators"] as? List ?: return@mapNotNull null - handleModerators("$server.$roomId", moderators) - - // get deletions + // Moderators + val moderators = json["moderators"] as? List ?: return@mapNotNull null + handleModerators("$server.$roomID", moderators) + // Deletions val type = TypeFactory.defaultInstance().constructCollectionType(List::class.java, MessageDeletion::class.java) - val idsAsString = JsonUtil.toJson(roomJson["deletions"]) - val deletedServerIDs = JsonUtil.fromJson>(idsAsString, type) ?: throw Error.PARSING_FAILED - val lastDeletionServerId = storage.getLastDeletionServerId(roomId, server) ?: 0 + val idsAsString = JsonUtil.toJson(json["deletions"]) + val deletedServerIDs = JsonUtil.fromJson>(idsAsString, type) ?: throw Error.ParsingFailed + val lastDeletionServerID = storage.getLastDeletionServerId(roomID, server) ?: 0 val serverID = deletedServerIDs.maxByOrNull {it.id } ?: MessageDeletion.EMPTY - if (serverID.id > lastDeletionServerId) { - storage.setLastDeletionServerId(roomId, server, serverID.id) + if (serverID.id > lastDeletionServerID) { + storage.setLastDeletionServerId(roomID, server, serverID.id) } - - // get messages - val rawMessages = roomJson["messages"] as? List> ?: return@mapNotNull null // parsing failed - - val lastMessageServerId = storage.getLastMessageServerId(roomId, server) ?: 0 - var currentMax = lastMessageServerId + // Messages + val rawMessages = json["messages"] as? List> ?: return@mapNotNull null + val lastMessageServerID = storage.getLastMessageServerId(roomID, server) ?: 0 + var currentLastMessageServerID = lastMessageServerID val messages = rawMessages.mapNotNull { rawMessage -> val message = OpenGroupMessageV2.fromJSON(rawMessage)?.apply { - currentMax = maxOf(currentMax,this.serverID ?: 0) + currentLastMessageServerID = maxOf(currentLastMessageServerID,this.serverID ?: 0) } + // TODO: We need to check the signature here... message } - storage.setLastMessageServerId(roomId, server, currentMax) - roomId to CompactPollResult( - messages = messages, - deletions = deletedServerIDs.map { it.deletedMessageId }, - moderators = moderators + storage.setLastMessageServerId(roomID, server, currentLastMessageServerID) + roomID to CompactPollResult( + messages = messages, + deletions = deletedServerIDs.map { it.deletedMessageId }, + moderators = moderators ) }.toMap() } @@ -443,7 +423,7 @@ object OpenGroupAPIV2 { val earlyGroups = groups.map { group -> DefaultGroup(group.id, group.name, null) } - // see if we have any cached rooms, and if they already have images, don't overwrite with early non-image results + // See if we have any cached rooms, and if they already have images don't overwrite them with early non-image results defaultRooms.replayCache.firstOrNull()?.let { replayed -> if (replayed.none { it.image?.isNotEmpty() == true}) { defaultRooms.tryEmit(earlyGroups) @@ -452,12 +432,11 @@ object OpenGroupAPIV2 { val images = groups.map { group -> group.id to downloadOpenGroupProfilePicture(group.id, DEFAULT_SERVER) }.toMap() - groups.map { group -> val image = try { images[group.id]!!.get() } catch (e: Exception) { - // no image or image failed to download + // No image or image failed to download null } DefaultGroup(group.id, group.name, image) @@ -470,9 +449,9 @@ object OpenGroupAPIV2 { fun getInfo(room: String, server: String): Promise { val request = Request(verb = GET, room = null, server = server, endpoint = "rooms/$room", isAuthRequired = false) return send(request).map { json -> - val rawRoom = json["room"] as? Map<*, *> ?: throw Error.PARSING_FAILED - val id = rawRoom["id"] as? String ?: throw Error.PARSING_FAILED - val name = rawRoom["name"] as? String ?: throw Error.PARSING_FAILED + val rawRoom = json["room"] as? Map<*, *> ?: throw Error.ParsingFailed + val id = rawRoom["id"] as? String ?: throw Error.ParsingFailed + val name = rawRoom["name"] as? String ?: throw Error.ParsingFailed val imageID = rawRoom["image_id"] as? String Info(id = id, name = name, imageID = imageID) } @@ -481,13 +460,13 @@ object OpenGroupAPIV2 { fun getAllRooms(server: String): Promise, Exception> { val request = Request(verb = GET, room = null, server = server, endpoint = "rooms", isAuthRequired = false) return send(request).map { json -> - val rawRooms = json["rooms"] as? List> ?: throw Error.PARSING_FAILED + val rawRooms = json["rooms"] as? List> ?: throw Error.ParsingFailed rawRooms.mapNotNull { val roomJson = it as? Map<*, *> ?: return@mapNotNull null val id = roomJson["id"] as? String ?: return@mapNotNull null val name = roomJson["name"] as? String ?: return@mapNotNull null - val imageId = roomJson["image_id"] as? String - Info(id, name, imageId) + val imageID = roomJson["image_id"] as? String + Info(id, name, imageID) } } } @@ -495,12 +474,11 @@ object OpenGroupAPIV2 { fun getMemberCount(room: String, server: String): Promise { val request = Request(verb = GET, room = room, server = server, endpoint = "member_count") return send(request).map { json -> - val memberCount = json["member_count"] as? Int ?: throw Error.PARSING_FAILED + val memberCount = json["member_count"] as? Int ?: throw Error.ParsingFailed val storage = MessagingModuleConfiguration.shared.storage storage.setUserCount(room, server, memberCount) memberCount } } // endregion - } \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupMessageV2.kt b/libsession/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupMessageV2.kt index 262c3d2a7b..1b75c1224b 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupMessageV2.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupMessageV2.kt @@ -9,14 +9,18 @@ import org.session.libsignal.utilities.logging.Log import org.whispersystems.curve25519.Curve25519 data class OpenGroupMessageV2( - val serverID: Long? = null, - val sender: String?, - val sentTimestamp: Long, - // The serialized protobuf in base64 encoding - val base64EncodedData: String, - // When sending a message, the sender signs the serialized protobuf with their private key so that - // a receiving user can verify that the message wasn't tampered with. - val base64EncodedSignature: String? = null + val serverID: Long? = null, + val sender: String?, + val sentTimestamp: Long, + /** + * The serialized protobuf in base64 encoding. + */ + val base64EncodedData: String, + /** + * When sending a message, the sender signs the serialized protobuf with their private key so that + * a receiving user can verify that the message wasn't tampered with. + */ + val base64EncodedSignature: String? = null ) { companion object { @@ -28,11 +32,12 @@ data class OpenGroupMessageV2( val serverID = json["server_id"] as? Int val sender = json["public_key"] as? String val base64EncodedSignature = json["signature"] as? String - return OpenGroupMessageV2(serverID = serverID?.toLong(), - sender = sender, - sentTimestamp = sentTimestamp, - base64EncodedData = base64EncodedData, - base64EncodedSignature = base64EncodedSignature + return OpenGroupMessageV2( + serverID = serverID?.toLong(), + sender = sender, + sentTimestamp = sentTimestamp, + base64EncodedData = base64EncodedData, + base64EncodedSignature = base64EncodedSignature ) } @@ -41,29 +46,26 @@ data class OpenGroupMessageV2( fun sign(): OpenGroupMessageV2? { if (base64EncodedData.isEmpty()) return null val (publicKey, privateKey) = MessagingModuleConfiguration.shared.storage.getUserKeyPair() ?: return null - - if (sender != publicKey) return null // only sign our own messages? - + if (sender != publicKey) return null val signature = try { curve.calculateSignature(privateKey, decode(base64EncodedData)) } catch (e: Exception) { - Log.e("Loki", "Couldn't sign OpenGroupV2Message", e) + Log.w("Loki", "Couldn't sign open group message.", e) return null } - return copy(base64EncodedSignature = Base64.encodeBytes(signature)) } fun toJSON(): Map { - val jsonMap = mutableMapOf("data" to base64EncodedData, "timestamp" to sentTimestamp) - serverID?.let { jsonMap["server_id"] = serverID } - sender?.let { jsonMap["public_key"] = sender } - base64EncodedSignature?.let { jsonMap["signature"] = base64EncodedSignature } - return jsonMap + val json = mutableMapOf( "data" to base64EncodedData, "timestamp" to sentTimestamp ) + serverID?.let { json["server_id"] = it } + sender?.let { json["public_key"] = it } + base64EncodedSignature?.let { json["signature"] = it } + return json } - fun toProto(): SignalServiceProtos.Content = decode(base64EncodedData).let(PushTransportDetails::getStrippedPaddingMessageBody).let { bytes -> - SignalServiceProtos.Content.parseFrom(bytes) + fun toProto(): SignalServiceProtos.Content { + val data = decode(base64EncodedData).let(PushTransportDetails::getStrippedPaddingMessageBody) + return SignalServiceProtos.Content.parseFrom(data) } - } \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupV2.kt b/libsession/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupV2.kt index 29965079cf..1e766a42ed 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupV2.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupV2.kt @@ -1,51 +1,50 @@ package org.session.libsession.messaging.open_groups import org.session.libsignal.utilities.JsonUtil +import org.session.libsignal.utilities.logging.Log import java.util.* data class OpenGroupV2( - val server: String, - val room: String, - val id: String, - val name: String, - val publicKey: String + val server: String, + val room: String, + val id: String, + val name: String, + val publicKey: String ) { constructor(server: String, room: String, name: String, publicKey: String) : this( - server = server, - room = room, - id = "$server.$room", - name = name, - publicKey = publicKey, + server = server, + room = room, + id = "$server.$room", + name = name, + publicKey = publicKey, ) companion object { - fun fromJson(jsonAsString: String): OpenGroupV2? { + fun fromJSON(jsonAsString: String): OpenGroupV2? { return try { val json = JsonUtil.fromJson(jsonAsString) if (!json.has("room")) return null - - val room = json.get("room").asText().toLowerCase(Locale.getDefault()) - val server = json.get("server").asText().toLowerCase(Locale.getDefault()) + val room = json.get("room").asText().toLowerCase(Locale.US) + val server = json.get("server").asText().toLowerCase(Locale.US) val displayName = json.get("displayName").asText() val publicKey = json.get("publicKey").asText() - OpenGroupV2(server, room, displayName, publicKey) } catch (e: Exception) { + Log.w("Loki", "Couldn't parse open group from JSON: $jsonAsString.", e); null } } } - fun toJoinUrl(): String = "$server/$room?public_key=$publicKey" - fun toJson(): Map = mapOf( - "room" to room, - "server" to server, - "displayName" to name, - "publicKey" to publicKey, + "room" to room, + "server" to server, + "displayName" to name, + "publicKey" to publicKey, ) + val joinURL: String get() = "$server/$room?public_key=$publicKey" } \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt index 2a0b13ae3e..1f8782311e 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt @@ -126,7 +126,7 @@ private fun handleConfigurationMessage(message: ConfigurationMessage) { handleNewClosedGroup(message.sender!!, message.sentTimestamp!!, closeGroup.publicKey, closeGroup.name, closeGroup.encryptionKeyPair!!, closeGroup.members, closeGroup.admins, message.sentTimestamp!!) } val allOpenGroups = storage.getAllOpenGroups().map { it.value.server } - val allV2OpenGroups = storage.getAllV2OpenGroups().map { it.value.toJoinUrl() } + val allV2OpenGroups = storage.getAllV2OpenGroups().map { it.value.joinURL } for (openGroup in message.openGroups) { if (allOpenGroups.contains(openGroup) || allV2OpenGroups.contains(openGroup)) continue storage.addOpenGroup(openGroup, 1) From c8cf5ebfa0f7187cc968810a8595ae4ecba03c78 Mon Sep 17 00:00:00 2001 From: nielsandriesse Date: Wed, 12 May 2021 14:52:24 +1000 Subject: [PATCH 4/4] Make custom error messages actually work --- .../messaging/file_server/FileServerAPIV2.kt | 66 ++++++++----------- .../messaging/open_groups/OpenGroupAPIV2.kt | 24 ++----- 2 files changed, 34 insertions(+), 56 deletions(-) diff --git a/libsession/src/main/java/org/session/libsession/messaging/file_server/FileServerAPIV2.kt b/libsession/src/main/java/org/session/libsession/messaging/file_server/FileServerAPIV2.kt index 1762a797d9..c8db066692 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/file_server/FileServerAPIV2.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/file_server/FileServerAPIV2.kt @@ -15,77 +15,66 @@ import org.session.libsignal.utilities.logging.Log object FileServerAPIV2 { - const val DEFAULT_SERVER = "http://88.99.175.227" private const val DEFAULT_SERVER_PUBLIC_KEY = "7cb31905b55cd5580c686911debf672577b3fb0bff81df4ce2d5c4cb3a7aaa69" + const val DEFAULT_SERVER = "http://88.99.175.227" - sealed class Error : Exception() { - object PARSING_FAILED : Error() - object INVALID_URL : Error() - - fun errorDescription() = when (this) { - PARSING_FAILED -> "Invalid response." - INVALID_URL -> "Invalid URL." - } - + sealed class Error(message: String) : Exception(message) { + object ParsingFailed : Error("Invalid response.") + object InvalidURL : Error("Invalid URL.") } data class Request( - val verb: HTTP.Verb, - val endpoint: String, - val queryParameters: Map = mapOf(), - val parameters: Any? = null, - val headers: Map = mapOf(), - // Always `true` under normal circumstances. You might want to disable - // this when running over Lokinet. - val useOnionRouting: Boolean = true + val verb: HTTP.Verb, + val endpoint: String, + val queryParameters: Map = mapOf(), + val parameters: Any? = null, + val headers: Map = mapOf(), + /** + * Always `true` under normal circumstances. You might want to disable + * this when running over Lokinet. + */ + val useOnionRouting: Boolean = true ) private fun createBody(parameters: Any?): RequestBody? { if (parameters == null) return null - val parametersAsJSON = JsonUtil.toJson(parameters) return RequestBody.create(MediaType.get("application/json"), parametersAsJSON) } private fun send(request: Request): Promise, Exception> { - val parsed = HttpUrl.parse(DEFAULT_SERVER) ?: return Promise.ofFail(OpenGroupAPIV2.Error.InvalidURL) + val url = HttpUrl.parse(DEFAULT_SERVER) ?: return Promise.ofFail(OpenGroupAPIV2.Error.InvalidURL) val urlBuilder = HttpUrl.Builder() - .scheme(parsed.scheme()) - .host(parsed.host()) - .port(parsed.port()) - .addPathSegments(request.endpoint) - + .scheme(url.scheme()) + .host(url.host()) + .port(url.port()) + .addPathSegments(request.endpoint) if (request.verb == HTTP.Verb.GET) { for ((key, value) in request.queryParameters) { urlBuilder.addQueryParameter(key, value) } } - val requestBuilder = okhttp3.Request.Builder() - .url(urlBuilder.build()) - .headers(Headers.of(request.headers)) + .url(urlBuilder.build()) + .headers(Headers.of(request.headers)) when (request.verb) { HTTP.Verb.GET -> requestBuilder.get() HTTP.Verb.PUT -> requestBuilder.put(createBody(request.parameters)!!) HTTP.Verb.POST -> requestBuilder.post(createBody(request.parameters)!!) HTTP.Verb.DELETE -> requestBuilder.delete(createBody(request.parameters)) } - if (request.useOnionRouting) { - return OnionRequestAPI.sendOnionRequest(requestBuilder.build(), DEFAULT_SERVER, DEFAULT_SERVER_PUBLIC_KEY) - .fail { e -> - Log.e("Loki", "FileServerV2 failed with error",e) - } + return OnionRequestAPI.sendOnionRequest(requestBuilder.build(), DEFAULT_SERVER, DEFAULT_SERVER_PUBLIC_KEY).fail { e -> + Log.e("Loki", "File server request failed.", e) + } } else { return Promise.ofFail(IllegalStateException("It's currently not allowed to send non onion routed requests.")) } - } - // region Sending fun upload(file: ByteArray): Promise { val base64EncodedFile = Base64.encodeBytes(file) - val parameters = mapOf("file" to base64EncodedFile) + val parameters = mapOf( "file" to base64EncodedFile ) val request = Request(verb = HTTP.Verb.POST, endpoint = "files", parameters = parameters) return send(request).map { json -> json["result"] as? Long ?: throw OpenGroupAPIV2.Error.ParsingFailed @@ -95,9 +84,8 @@ object FileServerAPIV2 { fun download(file: Long): Promise { val request = Request(verb = HTTP.Verb.GET, endpoint = "files/$file") return send(request).map { json -> - val base64EncodedFile = json["result"] as? String ?: throw Error.PARSING_FAILED - Base64.decode(base64EncodedFile) ?: throw Error.PARSING_FAILED + val base64EncodedFile = json["result"] as? String ?: throw Error.ParsingFailed + Base64.decode(base64EncodedFile) ?: throw Error.ParsingFailed } } - } \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupAPIV2.kt b/libsession/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupAPIV2.kt index 7f88cd3bc8..33f9a9613a 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupAPIV2.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupAPIV2.kt @@ -34,23 +34,13 @@ object OpenGroupAPIV2 { private const val DEFAULT_SERVER_PUBLIC_KEY = "a03c383cf63c3c4efe67acc52112a6dd734b3a946b9545f488aaa93da7991238" const val DEFAULT_SERVER = "http://116.203.70.33" - sealed class Error : Exception() { - object Generic : Error() - object ParsingFailed : Error() - object DecryptionFailed : Error() - object SigningFailed : Error() - object InvalidURL : Error() - object NoPublicKey : Error() - - fun errorDescription() = when (this) { - Error.Generic -> "An error occurred." - Error.ParsingFailed -> "Invalid response." - Error.DecryptionFailed -> "Couldn't decrypt response." - Error.SigningFailed -> "Couldn't sign message." - Error.InvalidURL -> "Invalid URL." - Error.NoPublicKey -> "Couldn't find server public key." - } - + sealed class Error(message: String) : Exception(message) { + object Generic : Error("An error occurred.") + object ParsingFailed : Error("Invalid response.") + object DecryptionFailed : Error("Couldn't decrypt response.") + object SigningFailed : Error("Couldn't sign message.") + object InvalidURL : Error("Invalid URL.") + object NoPublicKey : Error("Couldn't find server public key.") } data class DefaultGroup(val id: String, val name: String, val image: ByteArray?) {