From 56e9a42086deb2bda107eadc3ae877ae4c76f1c3 Mon Sep 17 00:00:00 2001 From: 0x330a <92654767+0x330a@users.noreply.github.com> Date: Fri, 24 Nov 2023 16:28:29 +1100 Subject: [PATCH] feat: add message processing --- .../securesms/database/Storage.kt | 114 +++++++++++++++++- .../securesms/groups/compose/EditGroup.kt | 32 +++++ .../libsession/database/StorageProtocol.kt | 4 +- .../ReceivedMessageHandler.kt | 11 +- .../utilities/UpdateMessageBuilder.kt | 64 +++++++++- .../messaging/utilities/UpdateMessageData.kt | 8 +- libsession/src/main/res/values/strings.xml | 13 ++ 7 files changed, 232 insertions(+), 14 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 5d5d14f23..6e5672008 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt @@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.database import android.content.Context import android.net.Uri +import com.google.protobuf.ByteString import kotlinx.coroutines.runBlocking import network.loki.messenger.libsession_util.Config import network.loki.messenger.libsession_util.ConfigBase.Companion.PRIORITY_HIDDEN @@ -19,6 +20,7 @@ import network.loki.messenger.libsession_util.util.Conversation import network.loki.messenger.libsession_util.util.ExpiryMode import network.loki.messenger.libsession_util.util.GroupDisplayInfo import network.loki.messenger.libsession_util.util.GroupInfo +import network.loki.messenger.libsession_util.util.KeyPair import network.loki.messenger.libsession_util.util.UserPic import nl.komponents.kovenant.functional.bind import org.session.libsession.avatars.AvatarHelper @@ -84,6 +86,7 @@ import org.session.libsignal.messages.SignalServiceAttachmentPointer import org.session.libsignal.messages.SignalServiceGroup import org.session.libsignal.protos.SignalServiceProtos.DataMessage import org.session.libsignal.protos.SignalServiceProtos.DataMessage.GroupUpdateInviteResponseMessage +import org.session.libsignal.protos.SignalServiceProtos.DataMessage.GroupUpdateMemberChangeMessage import org.session.libsignal.utilities.Base64 import org.session.libsignal.utilities.Hex import org.session.libsignal.utilities.IdPrefix @@ -1351,6 +1354,20 @@ open class Storage( val job = InviteContactsJob(groupSessionId, filteredMembers.toTypedArray()) JobQueue.shared.add(job) + val timestamp = SnodeAPI.nowWithOffset + val messageToSign = "MEMBER_CHANGE${GroupUpdateMemberChangeMessage.Type.ADDED.name}$timestamp" + val signature = SodiumUtilities.sign(messageToSign.toByteArray(), adminKey) + val updatedMessage = GroupUpdated( + DataMessage.GroupUpdateMessage.newBuilder() + .setMemberChangeMessage( + GroupUpdateMemberChangeMessage.newBuilder() + .addAllMemberSessionIds(filteredMembers) + .setType(GroupUpdateMemberChangeMessage.Type.ADDED) + .setAdminSignature(ByteString.copyFrom(signature)) + ) + .build() + ) + MessageSender.send(updatedMessage, fromSerialized(groupSessionId)) infoConfig.free() membersConfig.free() keysConfig.free() @@ -1368,12 +1385,99 @@ open class Storage( override fun insertGroupInfoChange(message: GroupUpdated, closedGroup: SessionId) { val sentTimestamp = message.sentTimestamp ?: return val senderPublicKey = message.sender ?: return - val group = SignalServiceGroup(Hex.fromStringCondensed(closedGroup.hexString()), SignalServiceGroup.GroupType.SIGNAL) - val m = IncomingTextMessage(fromSerialized(senderPublicKey), 1, sentTimestamp, "", Optional.of(group), 0, true, false) + val userPublicKey = getUserPublicKey()!! val updateData = UpdateMessageData.buildGroupUpdate(message)?.toJSON() ?: return - val infoMessage = IncomingGroupMessage(m, updateData, true) - val smsDB = DatabaseComponent.get(context).smsDatabase() - smsDB.insertMessageInbox(infoMessage, true) + + if (senderPublicKey == userPublicKey) { + val recipient = Recipient.from(context, fromSerialized(closedGroup.hexString()), false) + val infoMessage = OutgoingGroupMediaMessage(recipient, updateData, closedGroup.hexString(), null, sentTimestamp, 0, true, null, listOf(), listOf()) + val mmsDB = DatabaseComponent.get(context).mmsDatabase() + val mmsSmsDB = DatabaseComponent.get(context).mmsSmsDatabase() + if (mmsSmsDB.getMessageFor(sentTimestamp, userPublicKey) != null) return + val threadDb = DatabaseComponent.get(context).threadDatabase() + val threadID = threadDb.getThreadIdIfExistsFor(recipient) + val infoMessageID = mmsDB.insertMessageOutbox(infoMessage, threadID, false, null, runThreadUpdate = true) + mmsDB.markAsSent(infoMessageID, true) + } else { + val group = SignalServiceGroup(Hex.fromStringCondensed(closedGroup.hexString()), SignalServiceGroup.GroupType.SIGNAL) + val m = IncomingTextMessage(fromSerialized(senderPublicKey), 1, sentTimestamp, "", Optional.of(group), 0, true, false) + val infoMessage = IncomingGroupMessage(m, updateData, true) + val smsDB = DatabaseComponent.get(context).smsDatabase() + smsDB.insertMessageInbox(infoMessage, true) + } + } + + override fun promoteMember(groupSessionId: String, promotions: Array) { + val closedGroupId = SessionId.from(groupSessionId) + val adminKey = configFactory.userGroups?.getClosedGroup(groupSessionId)?.adminKey ?: return + if (adminKey.isEmpty()) { + return Log.e("ClosedGroup", "No admin key for group") + } + val info = configFactory.getGroupInfoConfig(closedGroupId) ?: return + val members = configFactory.getGroupMemberConfig(closedGroupId) ?: return + val keys = configFactory.getGroupKeysConfig(closedGroupId, info, members, free = false) ?: return + + promotions.forEach { sessionId -> + val promoted = members.get(sessionId)?.copy( + promotionPending = true, + ) ?: return@forEach + members.set(promoted) + + val message = GroupUpdated( + DataMessage.GroupUpdateMessage.newBuilder() + .setPromoteMessage( + DataMessage.GroupUpdatePromoteMessage.newBuilder() + .setGroupIdentitySeed(ByteString.copyFrom(adminKey)) + ) + .build() + ) + MessageSender.send(message, fromSerialized(sessionId)) + } + configFactory.saveGroupConfigs(keys, info, members) + info.free() + members.free() + keys.free() + val groupDestination = Destination.ClosedGroup(groupSessionId) + ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(groupDestination) + val timestamp = SnodeAPI.nowWithOffset + val messageToSign = "MEMBER_CHANGE${GroupUpdateMemberChangeMessage.Type.PROMOTED.name}$timestamp" + val signature = SodiumUtilities.sign(messageToSign.toByteArray(), adminKey) + val message = GroupUpdated( + DataMessage.GroupUpdateMessage.newBuilder() + .setMemberChangeMessage( + GroupUpdateMemberChangeMessage.newBuilder() + .addAllMemberSessionIds(promotions.toList()) + .setType(GroupUpdateMemberChangeMessage.Type.PROMOTED) + .setAdminSignature(ByteString.copyFrom(signature)) + ) + .build() + ).apply { sentTimestamp = timestamp } + MessageSender.send(message, fromSerialized(groupSessionId)) + } + + override fun handlePromoted(keyPair: KeyPair) { + val closedGroupId = SessionId(IdPrefix.GROUP, keyPair.pubKey) + val ourSessionId = getUserPublicKey()!! + val userGroups = configFactory.userGroups ?: return + val closedGroup = userGroups.getClosedGroup(closedGroupId.hexString()) + ?: return Log.w("ClosedGroup", "No closed group in user groups matching promoted message") + + val modified = closedGroup.copy(adminKey = keyPair.secretKey, authData = byteArrayOf()) + userGroups.set(modified) + val info = configFactory.getGroupInfoConfig(closedGroupId) ?: return + val members = configFactory.getGroupMemberConfig(closedGroupId) ?: return + val keys = configFactory.getGroupKeysConfig(closedGroupId, info, members, free = false) ?: return + val ourMember = members.get(ourSessionId)?.copy( + admin = true, + promotionPending = false, + promotionFailed = false + ) ?: return Log.e("ClosedGroup", "We aren't a member in the closed group") + members.set(ourMember) + configFactory.saveGroupConfigs(keys, info, members) + ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(Destination.ClosedGroup(closedGroupId.hexString())) + info.free() + members.free() + keys.free() } override fun setServerCapabilities(server: String, capabilities: List) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/compose/EditGroup.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/compose/EditGroup.kt index e5270a4f6..2279847ac 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/compose/EditGroup.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/compose/EditGroup.kt @@ -94,6 +94,9 @@ fun EditClosedGroupScreen( onReinvite = { contact -> eventSink(EditGroupEvent.ReInviteContact(contact)) }, + onPromote = { contact -> + eventSink(EditGroupEvent.PromoteContact(contact)) + }, viewState = viewState ) } @@ -182,8 +185,13 @@ class EditGroupViewModel @AssistedInject constructor( ).show() } is EditGroupEvent.ReInviteContact -> { + // do a buffer JobQueue.shared.add(InviteContactsJob(groupSessionId, arrayOf(event.contactSessionId))) } + is EditGroupEvent.PromoteContact -> { + // do a buffer + storage.promoteMember(groupSessionId, arrayOf(event.contactSessionId)) + } } } } @@ -238,6 +246,7 @@ fun EditGroupView( onBack: ()->Unit, onInvite: ()->Unit, onReinvite: (String)->Unit, + onPromote: (String)->Unit, viewState: EditGroupViewState, ) { val scaffoldState = rememberScaffoldState() @@ -339,6 +348,27 @@ fun EditGroupView( color = MaterialTheme.colors.onPrimary ) } + } else if (viewState.admin && member.memberState == MemberState.Member) { + TextButton( + onClick = { + onPromote(member.memberSessionId) + }, + modifier = Modifier + .clip(CircleShape) + .background( + Color( + MaterialColors.getColor(LocalContext.current, + R.attr.colorControlHighlight, + MaterialTheme.colors.onPrimary.toArgb()) + ) + ) + ) { + Text( + "Promote", + color = MaterialTheme.colors.onPrimary + ) + } + } } } @@ -394,6 +424,7 @@ sealed class EditGroupEvent { data class InviteContacts(val context: Context, val contacts: ContactList): EditGroupEvent() data class ReInviteContact(val contactSessionId: String): EditGroupEvent() + data class PromoteContact(val contactSessionId: String): EditGroupEvent() } data class EditGroupInviteViewState( @@ -430,6 +461,7 @@ fun PreviewList() { onBack = {}, onInvite = {}, onReinvite = {}, + onPromote = {}, viewState = viewState ) } diff --git a/libsession/src/main/java/org/session/libsession/database/StorageProtocol.kt b/libsession/src/main/java/org/session/libsession/database/StorageProtocol.kt index fcc8d7d26..cc53427de 100644 --- a/libsession/src/main/java/org/session/libsession/database/StorageProtocol.kt +++ b/libsession/src/main/java/org/session/libsession/database/StorageProtocol.kt @@ -5,6 +5,7 @@ import android.net.Uri import network.loki.messenger.libsession_util.Config import network.loki.messenger.libsession_util.util.GroupDisplayInfo import network.loki.messenger.libsession_util.util.GroupInfo +import network.loki.messenger.libsession_util.util.KeyPair import org.session.libsession.messaging.BlindedIdMapping import org.session.libsession.messaging.calls.CallMessageType import org.session.libsession.messaging.contacts.Contact @@ -168,6 +169,8 @@ interface StorageProtocol { fun getClosedGroupDisplayInfo(groupSessionId: String): GroupDisplayInfo? fun inviteClosedGroupMembers(groupSessionId: String, invitees: List) fun insertGroupInfoChange(message: GroupUpdated, closedGroup: SessionId) + fun promoteMember(groupSessionId: String, promotions: Array) + fun handlePromoted(keyPair: KeyPair) // Groups fun getAllGroups(includeInactive: Boolean): List @@ -175,7 +178,6 @@ interface StorageProtocol { // Settings fun setProfileSharing(address: Address, value: Boolean) - // Thread fun getOrCreateThreadIdFor(address: Address): Long fun getThreadIdFor(publicKey: String, groupPublicKey: String?, openGroupID: String?, createThread: Boolean): Long? 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 160d069ce..0e524bf0b 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 @@ -2,6 +2,7 @@ package org.session.libsession.messaging.sending_receiving import android.text.TextUtils import network.loki.messenger.libsession_util.ConfigBase +import network.loki.messenger.libsession_util.util.Sodium import org.session.libsession.avatars.AvatarHelper import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.jobs.AttachmentDownloadJob @@ -572,10 +573,14 @@ private fun handleGroupInfoChange(message: GroupUpdated, closedGroup: SessionId) } private fun handlePromotionMessage(message: GroupUpdated) { - val sender = message.sender!! val storage = MessagingModuleConfiguration.shared.storage - val inner = message.inner - // TODO: set ourselves as admin and overwrite the auth data for group + val promotion = message.inner.promoteMessage + if (!promotion.hasGroupIdentitySeed()) { + Log.e("GroupUpdated", "") + } + val seed = promotion.groupIdentitySeed.toByteArray() + val keyPair = Sodium.ed25519KeyPair(seed) + storage.handlePromoted(keyPair) } private fun MessageReceiver.handleInviteResponse(message: GroupUpdated, closedGroup: SessionId) { diff --git a/libsession/src/main/java/org/session/libsession/messaging/utilities/UpdateMessageBuilder.kt b/libsession/src/main/java/org/session/libsession/messaging/utilities/UpdateMessageBuilder.kt index 6cf18ba5c..9b3effca8 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/utilities/UpdateMessageBuilder.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/utilities/UpdateMessageBuilder.kt @@ -27,7 +27,7 @@ object UpdateMessageBuilder { else getSenderName(senderId!!) return when (updateData) { - is UpdateMessageData.Kind.GroupCreation -> if (isOutgoing) { + UpdateMessageData.Kind.GroupCreation -> if (isOutgoing) { context.getString(R.string.MessageRecord_you_created_a_new_group) } else { context.getString(R.string.MessageRecord_s_added_you_to_the_group, senderName) @@ -64,12 +64,70 @@ object UpdateMessageBuilder { } } } - is UpdateMessageData.Kind.GroupMemberLeft -> if (isOutgoing) { + UpdateMessageData.Kind.GroupMemberLeft -> if (isOutgoing) { context.getString(R.string.MessageRecord_left_group) } else { context.getString(R.string.ConversationItem_group_action_left, senderName) } - else -> return "" + UpdateMessageData.Kind.GroupAvatarUpdated -> context.getString(R.string.ConversationItem_group_action_avatar_updated) + is UpdateMessageData.Kind.GroupExpirationUpdated -> TODO() + is UpdateMessageData.Kind.GroupMemberUpdated -> { + when (updateData.type) { + UpdateMessageData.MemberUpdateType.ADDED -> { + val number = updateData.sessionIds.size + if (number == 1) context.getString( + R.string.ConversationItem_group_member_added_single, + getSenderName(updateData.sessionIds.first()) + ) + else if (number == 2) context.getString( + R.string.ConversationItem_group_member_added_two, + getSenderName(updateData.sessionIds.first()), + getSenderName(updateData.sessionIds.last()) + ) + else context.getString( + R.string.ConversationItem_group_member_added_multiple, + getSenderName(updateData.sessionIds.first()), + updateData.sessionIds.size - 1 + ) + } + UpdateMessageData.MemberUpdateType.PROMOTED -> { + val number = updateData.sessionIds.size + if (number == 1) context.getString( + R.string.ConversationItem_group_member_promoted_single, + getSenderName(updateData.sessionIds.first()) + ) + else if (number == 2) context.getString( + R.string.ConversationItem_group_member_promoted_two, + getSenderName(updateData.sessionIds.first()), + getSenderName(updateData.sessionIds.last()) + ) + else context.getString( + R.string.ConversationItem_group_member_promoted_multiple, + getSenderName(updateData.sessionIds.first()), + updateData.sessionIds.size - 1 + ) + } + UpdateMessageData.MemberUpdateType.REMOVED -> { + val number = updateData.sessionIds.size + if (number == 1) context.getString( + R.string.ConversationItem_group_member_removed_single, + getSenderName(updateData.sessionIds.first()) + ) + else if (number == 2) context.getString( + R.string.ConversationItem_group_member_removed_two, + getSenderName(updateData.sessionIds.first()), + getSenderName(updateData.sessionIds.last()) + ) + else context.getString( + R.string.ConversationItem_group_member_removed_multiple, + getSenderName(updateData.sessionIds.first()), + updateData.sessionIds.size - 1 + ) + } + null -> "" + } + } + is UpdateMessageData.Kind.OpenGroupInvitation -> TODO() } } diff --git a/libsession/src/main/java/org/session/libsession/messaging/utilities/UpdateMessageData.kt b/libsession/src/main/java/org/session/libsession/messaging/utilities/UpdateMessageData.kt index bd35541e8..ef09f98ea 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/utilities/UpdateMessageData.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/utilities/UpdateMessageData.kt @@ -21,7 +21,6 @@ class UpdateMessageData () { @JsonSubTypes( JsonSubTypes.Type(Kind.GroupCreation::class, name = "GroupCreation"), JsonSubTypes.Type(Kind.GroupNameChange::class, name = "GroupNameChange"), - JsonSubTypes.Type(Kind.GroupDescriptionChange::class, name = "GroupDescriptionChange"), JsonSubTypes.Type(Kind.GroupMemberAdded::class, name = "GroupMemberAdded"), JsonSubTypes.Type(Kind.GroupMemberRemoved::class, name = "GroupMemberRemoved"), JsonSubTypes.Type(Kind.GroupMemberLeft::class, name = "GroupMemberLeft"), @@ -35,7 +34,6 @@ class UpdateMessageData () { class GroupNameChange(val name: String): Kind() { constructor(): this("") //default constructor required for json serialization } - data class GroupDescriptionChange @JvmOverloads constructor(val description: String = ""): Kind() class GroupMemberAdded(val updatedMembers: Collection): Kind() { constructor(): this(Collections.emptyList()) } @@ -53,6 +51,12 @@ class UpdateMessageData () { } } + @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() diff --git a/libsession/src/main/res/values/strings.xml b/libsession/src/main/res/values/strings.xml index c9904920f..b2efc87ae 100644 --- a/libsession/src/main/res/values/strings.xml +++ b/libsession/src/main/res/values/strings.xml @@ -61,6 +61,19 @@ %dw %1$s has left the group. + Group display picture updated. + Group name is now %1$s. + Group name updated. + %1$s was invited to join the group. + %1$s and %2$s were invited to join the group. + %1$s and %2$d others were invited to join the group. + %1$s was removed from the group. + %1$s and %2$s were removed from the group. + %1$s and %2$d others were removed from the group. + %1$s was promoted to Admin. + %1$s and %2$s were promoted to Admin. + %1$s and %2$d others were promoted to Admin. + Unnamed group