feat: add message processing

This commit is contained in:
0x330a 2023-11-24 16:28:29 +11:00
parent 9dd8eef781
commit 56e9a42086
7 changed files with 232 additions and 14 deletions

View File

@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.database
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import com.google.protobuf.ByteString
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import network.loki.messenger.libsession_util.Config import network.loki.messenger.libsession_util.Config
import network.loki.messenger.libsession_util.ConfigBase.Companion.PRIORITY_HIDDEN 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.ExpiryMode
import network.loki.messenger.libsession_util.util.GroupDisplayInfo import network.loki.messenger.libsession_util.util.GroupDisplayInfo
import network.loki.messenger.libsession_util.util.GroupInfo 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 network.loki.messenger.libsession_util.util.UserPic
import nl.komponents.kovenant.functional.bind import nl.komponents.kovenant.functional.bind
import org.session.libsession.avatars.AvatarHelper 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.messages.SignalServiceGroup
import org.session.libsignal.protos.SignalServiceProtos.DataMessage import org.session.libsignal.protos.SignalServiceProtos.DataMessage
import org.session.libsignal.protos.SignalServiceProtos.DataMessage.GroupUpdateInviteResponseMessage 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.Base64
import org.session.libsignal.utilities.Hex import org.session.libsignal.utilities.Hex
import org.session.libsignal.utilities.IdPrefix import org.session.libsignal.utilities.IdPrefix
@ -1351,6 +1354,20 @@ open class Storage(
val job = InviteContactsJob(groupSessionId, filteredMembers.toTypedArray()) val job = InviteContactsJob(groupSessionId, filteredMembers.toTypedArray())
JobQueue.shared.add(job) 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() infoConfig.free()
membersConfig.free() membersConfig.free()
keysConfig.free() keysConfig.free()
@ -1368,12 +1385,99 @@ open class Storage(
override fun insertGroupInfoChange(message: GroupUpdated, closedGroup: SessionId) { override fun insertGroupInfoChange(message: GroupUpdated, closedGroup: SessionId) {
val sentTimestamp = message.sentTimestamp ?: return val sentTimestamp = message.sentTimestamp ?: return
val senderPublicKey = message.sender ?: return val senderPublicKey = message.sender ?: return
val group = SignalServiceGroup(Hex.fromStringCondensed(closedGroup.hexString()), SignalServiceGroup.GroupType.SIGNAL) val userPublicKey = getUserPublicKey()!!
val m = IncomingTextMessage(fromSerialized(senderPublicKey), 1, sentTimestamp, "", Optional.of(group), 0, true, false)
val updateData = UpdateMessageData.buildGroupUpdate(message)?.toJSON() ?: return val updateData = UpdateMessageData.buildGroupUpdate(message)?.toJSON() ?: return
val infoMessage = IncomingGroupMessage(m, updateData, true)
val smsDB = DatabaseComponent.get(context).smsDatabase() if (senderPublicKey == userPublicKey) {
smsDB.insertMessageInbox(infoMessage, true) 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<String>) {
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<String>) { override fun setServerCapabilities(server: String, capabilities: List<String>) {

View File

@ -94,6 +94,9 @@ fun EditClosedGroupScreen(
onReinvite = { contact -> onReinvite = { contact ->
eventSink(EditGroupEvent.ReInviteContact(contact)) eventSink(EditGroupEvent.ReInviteContact(contact))
}, },
onPromote = { contact ->
eventSink(EditGroupEvent.PromoteContact(contact))
},
viewState = viewState viewState = viewState
) )
} }
@ -182,8 +185,13 @@ class EditGroupViewModel @AssistedInject constructor(
).show() ).show()
} }
is EditGroupEvent.ReInviteContact -> { is EditGroupEvent.ReInviteContact -> {
// do a buffer
JobQueue.shared.add(InviteContactsJob(groupSessionId, arrayOf(event.contactSessionId))) 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, onBack: ()->Unit,
onInvite: ()->Unit, onInvite: ()->Unit,
onReinvite: (String)->Unit, onReinvite: (String)->Unit,
onPromote: (String)->Unit,
viewState: EditGroupViewState, viewState: EditGroupViewState,
) { ) {
val scaffoldState = rememberScaffoldState() val scaffoldState = rememberScaffoldState()
@ -339,6 +348,27 @@ fun EditGroupView(
color = MaterialTheme.colors.onPrimary 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, data class InviteContacts(val context: Context,
val contacts: ContactList): EditGroupEvent() val contacts: ContactList): EditGroupEvent()
data class ReInviteContact(val contactSessionId: String): EditGroupEvent() data class ReInviteContact(val contactSessionId: String): EditGroupEvent()
data class PromoteContact(val contactSessionId: String): EditGroupEvent()
} }
data class EditGroupInviteViewState( data class EditGroupInviteViewState(
@ -430,6 +461,7 @@ fun PreviewList() {
onBack = {}, onBack = {},
onInvite = {}, onInvite = {},
onReinvite = {}, onReinvite = {},
onPromote = {},
viewState = viewState viewState = viewState
) )
} }

View File

@ -5,6 +5,7 @@ import android.net.Uri
import network.loki.messenger.libsession_util.Config import network.loki.messenger.libsession_util.Config
import network.loki.messenger.libsession_util.util.GroupDisplayInfo import network.loki.messenger.libsession_util.util.GroupDisplayInfo
import network.loki.messenger.libsession_util.util.GroupInfo 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.BlindedIdMapping
import org.session.libsession.messaging.calls.CallMessageType import org.session.libsession.messaging.calls.CallMessageType
import org.session.libsession.messaging.contacts.Contact import org.session.libsession.messaging.contacts.Contact
@ -168,6 +169,8 @@ interface StorageProtocol {
fun getClosedGroupDisplayInfo(groupSessionId: String): GroupDisplayInfo? fun getClosedGroupDisplayInfo(groupSessionId: String): GroupDisplayInfo?
fun inviteClosedGroupMembers(groupSessionId: String, invitees: List<String>) fun inviteClosedGroupMembers(groupSessionId: String, invitees: List<String>)
fun insertGroupInfoChange(message: GroupUpdated, closedGroup: SessionId) fun insertGroupInfoChange(message: GroupUpdated, closedGroup: SessionId)
fun promoteMember(groupSessionId: String, promotions: Array<String>)
fun handlePromoted(keyPair: KeyPair)
// Groups // Groups
fun getAllGroups(includeInactive: Boolean): List<GroupRecord> fun getAllGroups(includeInactive: Boolean): List<GroupRecord>
@ -175,7 +178,6 @@ interface StorageProtocol {
// Settings // Settings
fun setProfileSharing(address: Address, value: Boolean) fun setProfileSharing(address: Address, value: Boolean)
// Thread // Thread
fun getOrCreateThreadIdFor(address: Address): Long fun getOrCreateThreadIdFor(address: Address): Long
fun getThreadIdFor(publicKey: String, groupPublicKey: String?, openGroupID: String?, createThread: Boolean): Long? fun getThreadIdFor(publicKey: String, groupPublicKey: String?, openGroupID: String?, createThread: Boolean): Long?

View File

@ -2,6 +2,7 @@ package org.session.libsession.messaging.sending_receiving
import android.text.TextUtils import android.text.TextUtils
import network.loki.messenger.libsession_util.ConfigBase 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.avatars.AvatarHelper
import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.jobs.AttachmentDownloadJob import org.session.libsession.messaging.jobs.AttachmentDownloadJob
@ -572,10 +573,14 @@ private fun handleGroupInfoChange(message: GroupUpdated, closedGroup: SessionId)
} }
private fun handlePromotionMessage(message: GroupUpdated) { private fun handlePromotionMessage(message: GroupUpdated) {
val sender = message.sender!!
val storage = MessagingModuleConfiguration.shared.storage val storage = MessagingModuleConfiguration.shared.storage
val inner = message.inner val promotion = message.inner.promoteMessage
// TODO: set ourselves as admin and overwrite the auth data for group 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) { private fun MessageReceiver.handleInviteResponse(message: GroupUpdated, closedGroup: SessionId) {

View File

@ -27,7 +27,7 @@ object UpdateMessageBuilder {
else getSenderName(senderId!!) else getSenderName(senderId!!)
return when (updateData) { return when (updateData) {
is UpdateMessageData.Kind.GroupCreation -> if (isOutgoing) { UpdateMessageData.Kind.GroupCreation -> if (isOutgoing) {
context.getString(R.string.MessageRecord_you_created_a_new_group) context.getString(R.string.MessageRecord_you_created_a_new_group)
} else { } else {
context.getString(R.string.MessageRecord_s_added_you_to_the_group, senderName) 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) context.getString(R.string.MessageRecord_left_group)
} else { } else {
context.getString(R.string.ConversationItem_group_action_left, senderName) 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()
} }
} }

View File

@ -21,7 +21,6 @@ class UpdateMessageData () {
@JsonSubTypes( @JsonSubTypes(
JsonSubTypes.Type(Kind.GroupCreation::class, name = "GroupCreation"), JsonSubTypes.Type(Kind.GroupCreation::class, name = "GroupCreation"),
JsonSubTypes.Type(Kind.GroupNameChange::class, name = "GroupNameChange"), JsonSubTypes.Type(Kind.GroupNameChange::class, name = "GroupNameChange"),
JsonSubTypes.Type(Kind.GroupDescriptionChange::class, name = "GroupDescriptionChange"),
JsonSubTypes.Type(Kind.GroupMemberAdded::class, name = "GroupMemberAdded"), JsonSubTypes.Type(Kind.GroupMemberAdded::class, name = "GroupMemberAdded"),
JsonSubTypes.Type(Kind.GroupMemberRemoved::class, name = "GroupMemberRemoved"), JsonSubTypes.Type(Kind.GroupMemberRemoved::class, name = "GroupMemberRemoved"),
JsonSubTypes.Type(Kind.GroupMemberLeft::class, name = "GroupMemberLeft"), JsonSubTypes.Type(Kind.GroupMemberLeft::class, name = "GroupMemberLeft"),
@ -35,7 +34,6 @@ class UpdateMessageData () {
class GroupNameChange(val name: String): Kind() { class GroupNameChange(val name: String): Kind() {
constructor(): this("") //default constructor required for json serialization constructor(): this("") //default constructor required for json serialization
} }
data class GroupDescriptionChange @JvmOverloads constructor(val description: String = ""): Kind()
class GroupMemberAdded(val updatedMembers: Collection<String>): Kind() { class GroupMemberAdded(val updatedMembers: Collection<String>): Kind() {
constructor(): this(Collections.emptyList()) 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 { sealed class MemberUpdateType {
data object ADDED: MemberUpdateType() data object ADDED: MemberUpdateType()
data object REMOVED: MemberUpdateType() data object REMOVED: MemberUpdateType()

View File

@ -61,6 +61,19 @@
<string name="expiration_weeks_abbreviated">%dw</string> <string name="expiration_weeks_abbreviated">%dw</string>
<string name="ConversationItem_group_action_left">%1$s has left the group.</string> <string name="ConversationItem_group_action_left">%1$s has left the group.</string>
<string name="ConversationItem_group_action_avatar_updated">Group display picture updated.</string>
<string name="ConversationItem_group_name_updated">Group name is now %1$s.</string>
<string name="ConversationItem_group_name_updated_fallback">Group name updated.</string>
<string name="ConversationItem_group_member_added_single">%1$s was invited to join the group.</string>
<string name="ConversationItem_group_member_added_two">%1$s and %2$s were invited to join the group.</string>
<string name="ConversationItem_group_member_added_multiple">%1$s and %2$d others were invited to join the group.</string>
<string name="ConversationItem_group_member_removed_single">%1$s was removed from the group.</string>
<string name="ConversationItem_group_member_removed_two">%1$s and %2$s were removed from the group.</string>
<string name="ConversationItem_group_member_removed_multiple">%1$s and %2$d others were removed from the group.</string>
<string name="ConversationItem_group_member_promoted_single">%1$s was promoted to Admin.</string>
<string name="ConversationItem_group_member_promoted_two">%1$s and %2$s were promoted to Admin.</string>
<string name="ConversationItem_group_member_promoted_multiple">%1$s and %2$d others were promoted to Admin.</string>
<!-- RecipientProvider --> <!-- RecipientProvider -->
<string name="RecipientProvider_unnamed_group">Unnamed group</string> <string name="RecipientProvider_unnamed_group">Unnamed group</string>
</resources> </resources>