diff --git a/app/src/main/java/org/session/libsession/messaging/messages/signal/IncomingTextMessage.kt b/app/src/main/java/org/session/libsession/messaging/messages/signal/IncomingTextMessage.kt index 3d3be4641e..199db32ae0 100644 --- a/app/src/main/java/org/session/libsession/messaging/messages/signal/IncomingTextMessage.kt +++ b/app/src/main/java/org/session/libsession/messaging/messages/signal/IncomingTextMessage.kt @@ -1,5 +1,6 @@ package org.session.libsession.messaging.messages.signal +import kotlinx.serialization.json.Json import network.loki.messenger.libsession_util.protocol.ProFeature import org.session.libsession.messaging.calls.CallMessageType import org.session.libsession.messaging.messages.visible.OpenGroupInvitation @@ -83,6 +84,7 @@ data class IncomingTextMessage( companion object { fun fromOpenGroupInvitation( + json: Json, invitation: OpenGroupInvitation, sender: Address, sentTimestampMillis: Long, @@ -92,7 +94,7 @@ data class IncomingTextMessage( val body = UpdateMessageData.buildOpenGroupInvitation( url = invitation.url ?: return null, name = invitation.name ?: return null, - ).toJSON() + ).toJSON(json) return IncomingTextMessage( message = body, diff --git a/app/src/main/java/org/session/libsession/messaging/messages/signal/OutgoingTextMessage.kt b/app/src/main/java/org/session/libsession/messaging/messages/signal/OutgoingTextMessage.kt index eab343b176..97a3079fbe 100644 --- a/app/src/main/java/org/session/libsession/messaging/messages/signal/OutgoingTextMessage.kt +++ b/app/src/main/java/org/session/libsession/messaging/messages/signal/OutgoingTextMessage.kt @@ -1,5 +1,6 @@ package org.session.libsession.messaging.messages.signal +import kotlinx.serialization.json.Json import network.loki.messenger.libsession_util.protocol.ProFeature import org.session.libsession.messaging.messages.visible.OpenGroupInvitation import org.session.libsession.messaging.messages.visible.VisibleMessage @@ -32,6 +33,7 @@ data class OutgoingTextMessage private constructor( companion object { fun fromOpenGroupInvitation( + json: Json, invitation: OpenGroupInvitation, recipient: Address, sentTimestampMillis: Long, @@ -44,7 +46,7 @@ data class OutgoingTextMessage private constructor( message = UpdateMessageData.buildOpenGroupInvitation( url = invitation.url ?: return null, name = invitation.name ?: return null, - ).toJSON(), + ).toJSON(json), expiresInMillis = expiresInMillis, expireStartedAtMillis = expireStartedAtMillis, sentTimestampMillis = sentTimestampMillis, diff --git a/app/src/main/java/org/session/libsession/messaging/utilities/UpdateMessageData.kt b/app/src/main/java/org/session/libsession/messaging/utilities/UpdateMessageData.kt index 5d41f02917..66f9906604 100644 --- a/app/src/main/java/org/session/libsession/messaging/utilities/UpdateMessageData.kt +++ b/app/src/main/java/org/session/libsession/messaging/utilities/UpdateMessageData.kt @@ -1,95 +1,111 @@ package org.session.libsession.messaging.utilities -import com.fasterxml.jackson.annotation.JsonSubTypes -import com.fasterxml.jackson.annotation.JsonTypeInfo -import com.fasterxml.jackson.core.JsonParseException +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonClassDiscriminator import org.session.libsession.messaging.messages.control.GroupUpdated +import org.session.libsignal.utilities.Log import org.session.protos.SessionProtos.GroupUpdateInfoChangeMessage import org.session.protos.SessionProtos.GroupUpdateMemberChangeMessage.Type -import org.session.libsignal.utilities.JsonUtil -import org.session.libsignal.utilities.Log -import java.util.Collections - -// class used to save update messages details -class UpdateMessageData () { - - var kind: Kind? = null - - //the annotations below are required for serialization. Any new Kind class MUST be declared as JsonSubTypes as well - @JsonTypeInfo(use = JsonTypeInfo.Id.NAME) - @JsonSubTypes( - JsonSubTypes.Type(Kind.GroupCreation::class, name = "GroupCreation"), - JsonSubTypes.Type(Kind.GroupNameChange::class, name = "GroupNameChange"), - JsonSubTypes.Type(Kind.GroupMemberAdded::class, name = "GroupMemberAdded"), - JsonSubTypes.Type(Kind.GroupMemberRemoved::class, name = "GroupMemberRemoved"), - JsonSubTypes.Type(Kind.GroupMemberLeft::class, name = "GroupMemberLeft"), - JsonSubTypes.Type(Kind.OpenGroupInvitation::class, name = "OpenGroupInvitation"), - JsonSubTypes.Type(Kind.GroupAvatarUpdated::class, name = "GroupAvatarUpdated"), - JsonSubTypes.Type(Kind.GroupMemberUpdated::class, name = "GroupMemberUpdated"), - JsonSubTypes.Type(Kind.GroupExpirationUpdated::class, name = "GroupExpirationUpdated"), - JsonSubTypes.Type(Kind.GroupInvitation::class, name = "GroupInvitation"), - JsonSubTypes.Type(Kind.GroupLeaving::class, name = "GroupLeaving"), - JsonSubTypes.Type(Kind.GroupErrorQuit::class, name = "GroupErrorQuit"), - ) - sealed class Kind { - data object GroupCreation: Kind() - class GroupNameChange(val name: String): Kind() { - constructor(): this("") //default constructor required for json serialization - } - class GroupMemberAdded(val updatedMembers: Collection, val groupName: String): Kind() { - constructor(): this(Collections.emptyList(), "") - } - class GroupMemberRemoved(val updatedMembers: Collection, val groupName:String): Kind() { - constructor(): this(Collections.emptyList(), "") - } - class GroupMemberLeft(val updatedMembers: Collection, val groupName:String): Kind() { - constructor(): this(Collections.emptyList(), "") - } + +/** + * Represents certain type of message. + * + * This class is an afterthought to save "rich message" into a message's body as JSON text. + * We've since moved away from this setup, a dedicated + * [org.thoughtcrime.securesms.database.model.content.MessageContent] is now used for rich + * message types. + * + * If you want to store a new message type, you should use the new setup instead. + * + * We'll look into migrating this class into the new setup in the future. + */ +@Serializable +class UpdateMessageData(val kind: Kind) { + + @OptIn(ExperimentalSerializationApi::class) + @Serializable + @JsonClassDiscriminator("@type") + sealed interface Kind { + @Serializable + @SerialName("GroupCreation") + data object GroupCreation: Kind + + @Serializable + @SerialName("GroupNameChange") + class GroupNameChange(val name: String): Kind + + @Serializable + @SerialName("GroupMemberAdded") + class GroupMemberAdded(val updatedMembers: Collection, val groupName: String): Kind + + @Serializable + @SerialName("GroupMemberRemoved") + class GroupMemberRemoved(val updatedMembers: Collection, val groupName:String): Kind + + @Serializable + @SerialName("GroupMemberLeft") + class GroupMemberLeft(val updatedMembers: Collection, val groupName:String): Kind + + @Serializable + @SerialName("GroupMemberUpdated") class GroupMemberUpdated( val sessionIds: List, val type: MemberUpdateType?, val groupName: String, val historyShared: Boolean - ): Kind() { - constructor(): this(emptyList(), null, "", false) - } - data object GroupAvatarUpdated: Kind() - class GroupExpirationUpdated(val updatedExpiration: Long, val updatingAdmin: String): Kind() { - constructor(): this(0L, "") - } - class OpenGroupInvitation(val groupUrl: String, val groupName: String): Kind() { - constructor(): this("", "") - } - data object GroupLeaving: Kind() - data class GroupErrorQuit(val groupName: String): Kind() { - constructor(): this("") - } + ): Kind + + @Serializable + @SerialName("GroupAvatarUpdated") + data object GroupAvatarUpdated: Kind + + @Serializable + @SerialName("GroupExpirationUpdated") + class GroupExpirationUpdated(val updatedExpiration: Long, val updatingAdmin: String): Kind + + @Serializable + @SerialName("OpenGroupInvitation") + class OpenGroupInvitation(val groupUrl: String, val groupName: String): Kind + + @Serializable + @SerialName("GroupLeaving") + data object GroupLeaving: Kind + + @Serializable + @SerialName("GroupErrorQuit") + data class GroupErrorQuit(val groupName: String): Kind + + @Serializable + @SerialName("GroupInvitation") class GroupInvitation( val groupAccountId: String, val invitingAdminId: String, val invitingAdminName: String?, val groupName: String - ) : Kind() { - constructor(): this("", "", null, "") - } + ) : Kind } - @JsonTypeInfo(use = JsonTypeInfo.Id.NAME) - @JsonSubTypes( - JsonSubTypes.Type(MemberUpdateType.ADDED::class, name = "ADDED"), - JsonSubTypes.Type(MemberUpdateType.REMOVED::class, name = "REMOVED"), - JsonSubTypes.Type(MemberUpdateType.PROMOTED::class, name = "PROMOTED"), - ) - sealed class MemberUpdateType { - data object ADDED: MemberUpdateType() - data object REMOVED: MemberUpdateType() - data object PROMOTED: MemberUpdateType() - } + @Serializable + @JsonClassDiscriminator("@type") + sealed interface MemberUpdateType { + @Serializable + @SerialName("ADDED") + data object ADDED: MemberUpdateType + + @Serializable + @SerialName("REMOVED") + data object REMOVED: MemberUpdateType + + @Serializable + @SerialName("PROMOTED") + data object PROMOTED: MemberUpdateType - constructor(kind: Kind): this() { - this.kind = kind } + companion object { val TAG = UpdateMessageData::class.simpleName @@ -137,19 +153,17 @@ class UpdateMessageData () { } @JvmStatic - fun fromJSON(json: String): UpdateMessageData? { - return try { - JsonUtil.fromJson(json, UpdateMessageData::class.java) - } catch (e: JsonParseException) { - Log.e(TAG, "${e.message}") - null - } + fun fromJSON(json: Json, value: String): UpdateMessageData? { + return runCatching { + json.decodeFromString(value) + }.onFailure { Log.e(TAG, "Error decoding updateMessageData", it) } + .getOrNull() } } - fun toJSON(): String { - return JsonUtil.toJson(this) + fun toJSON(json: Json): String { + return json.encodeToString(this) } fun isGroupLeavingKind(): Boolean { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/OpenGroupInvitationView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/OpenGroupInvitationView.kt index 62329b2cda..09d5f8bc0e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/OpenGroupInvitationView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/OpenGroupInvitationView.kt @@ -8,6 +8,7 @@ import androidx.annotation.ColorInt import androidx.core.content.ContextCompat import network.loki.messenger.R import network.loki.messenger.databinding.ViewOpenGroupInvitationBinding +import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.utilities.UpdateMessageData import org.session.libsession.utilities.OpenGroupUrlParser import org.thoughtcrime.securesms.database.model.MessageRecord @@ -23,7 +24,7 @@ class OpenGroupInvitationView : LinearLayout { fun bind(message: MessageRecord, @ColorInt textColor: Int) { // FIXME: This is a really weird approach... - val umd = UpdateMessageData.fromJSON(message.body)!! + val umd = UpdateMessageData.fromJSON(MessagingModuleConfiguration.shared.json, message.body)!! val data = umd.kind as UpdateMessageData.Kind.OpenGroupInvitation this.data = data val iconID = if (message.isOutgoing) R.drawable.ic_globe else R.drawable.ic_plus diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/ResendMessageUtilities.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/ResendMessageUtilities.kt index baeffda3f9..2975ee7431 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/ResendMessageUtilities.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/ResendMessageUtilities.kt @@ -1,5 +1,6 @@ package org.thoughtcrime.securesms.conversation.v2.utilities +import kotlinx.serialization.json.Json import org.session.libsession.database.StorageProtocol import org.session.libsession.messaging.messages.Destination import org.session.libsession.messaging.messages.visible.LinkPreview @@ -17,6 +18,7 @@ import javax.inject.Inject class ResendMessageUtilities @Inject constructor( private val messageSender: MessageSender, private val storage: StorageProtocol, + private val json: Json, ) { suspend fun resend(accountId: String?, messageRecord: MessageRecord, userBlindedKey: String?, isResync: Boolean = false) { @@ -25,7 +27,7 @@ class ResendMessageUtilities @Inject constructor( message.id = messageRecord.messageId if (messageRecord.isOpenGroupInvitation) { val openGroupInvitation = OpenGroupInvitation() - UpdateMessageData.fromJSON(messageRecord.body)?.let { updateMessageData -> + UpdateMessageData.fromJSON(json, messageRecord.body)?.let { updateMessageData -> val kind = updateMessageData.kind if (kind is UpdateMessageData.Kind.OpenGroupInvitation) { openGroupInvitation.name = kind.groupName 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 4104419edd..8c8e52cbef 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt @@ -6,6 +6,7 @@ import dagger.Lazy import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +import kotlinx.serialization.json.Json import network.loki.messenger.libsession_util.MutableConversationVolatileConfig import network.loki.messenger.libsession_util.PRIORITY_PINNED import network.loki.messenger.libsession_util.PRIORITY_VISIBLE @@ -108,6 +109,7 @@ open class Storage @Inject constructor( private val openGroupManager: Lazy, private val recipientRepository: RecipientRepository, private val loginStateRepository: LoginStateRepository, + private val json: Json, ) : Database(context, helper), StorageProtocol { override fun getUserPublicKey(): String? { return loginStateRepository.peekLoginState()?.accountId?.hexString } @@ -368,6 +370,7 @@ open class Storage @Inject constructor( val insertResult = if (isUserSender || isUserBlindedSender) { val textMessage = if (isOpenGroupInvitation) OutgoingTextMessage.fromOpenGroupInvitation( + json = json, invitation = message.openGroupInvitation!!, recipient = targetAddress, sentTimestampMillis = message.sentTimestamp!!, @@ -385,6 +388,7 @@ open class Storage @Inject constructor( smsDatabase.insertMessageOutbox(message.threadID ?: -1, textMessage, message.sentTimestamp!!, runThreadUpdate) } else { val textMessage = if (isOpenGroupInvitation) IncomingTextMessage.fromOpenGroupInvitation( + json = json, invitation = message.openGroupInvitation!!, sender = senderAddress, sentTimestampMillis = message.sentTimestamp!!, @@ -797,7 +801,7 @@ open class Storage @Inject constructor( val expiryMode = recipient.expiryMode val expiresInMillis = expiryMode.expiryMillis val expireStartedAt = if (expiryMode is ExpiryMode.AfterSend) sentTimestamp else 0 - val inviteJson = updateData.toJSON() + val inviteJson = updateData.toJSON(json) if (senderPublicKey == null || senderPublicKey == userPublicKey) { val infoMessage = OutgoingMediaMessage( diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java index 577f6dad6b..9f45ac1317 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java @@ -19,6 +19,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import org.session.libsession.messaging.MessagingModuleConfiguration; import org.session.libsession.messaging.utilities.UpdateMessageData; import org.session.libsession.utilities.recipients.Recipient; import org.thoughtcrime.securesms.database.model.content.MessageContent; @@ -106,19 +107,19 @@ public boolean isMediaPending() { @Nullable public UpdateMessageData getGroupUpdateMessage() { if (isGroupUpdateMessage()) { - groupUpdateMessage = UpdateMessageData.Companion.fromJSON(getBody()); + groupUpdateMessage = UpdateMessageData.Companion.fromJSON( + MessagingModuleConfiguration.getShared().getJson(), + getBody() + ); } return groupUpdateMessage; } public boolean isGroupExpirationTimerUpdate() { - if (!isGroupUpdateMessage()) { - return false; - } - - UpdateMessageData updateMessageData = UpdateMessageData.Companion.fromJSON(getBody()); - return updateMessageData != null && updateMessageData.getKind() instanceof UpdateMessageData.Kind.GroupExpirationUpdated; + UpdateMessageData message = getGroupUpdateMessage(); + return message != null && + message.getKind() instanceof UpdateMessageData.Kind.GroupExpirationUpdated; } @Override diff --git a/app/src/main/java/org/thoughtcrime/securesms/repository/DefaultConversationRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/repository/DefaultConversationRepository.kt index fc373ac95d..d772ade0d5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/repository/DefaultConversationRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/repository/DefaultConversationRepository.kt @@ -11,6 +11,7 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.withContext +import kotlinx.serialization.json.Json import network.loki.messenger.libsession_util.util.ExpiryMode import network.loki.messenger.libsession_util.util.GroupInfo import org.session.libsession.database.MessageDataProvider @@ -95,6 +96,7 @@ class DefaultConversationRepository @Inject constructor( private val banUserApiFactory: BanUserApi.Factory, private val unbanUserApiFactory: UnbanUserApi.Factory, private val deleteUserMessageApiFactory: DeleteUserMessagesApi.Factory, + private val json: Json, ) : ConversationRepository { override val conversationListAddressesFlow get() = loginStateRepository.flowWithLoggedInState { @@ -227,6 +229,7 @@ class DefaultConversationRepository @Inject constructor( val expirationConfig = recipientRepository.getRecipientSync(contact).expiryMode val expireStartedAt = if (expirationConfig is ExpiryMode.AfterSend) message.sentTimestamp!! else 0 val outgoingTextMessage = OutgoingTextMessage.Companion.fromOpenGroupInvitation( + json, openGroupInvitation, contact, message.sentTimestamp!!,