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.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<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>) {

View File

@ -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
)
}

View File

@ -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<String>)
fun insertGroupInfoChange(message: GroupUpdated, closedGroup: SessionId)
fun promoteMember(groupSessionId: String, promotions: Array<String>)
fun handlePromoted(keyPair: KeyPair)
// Groups
fun getAllGroups(includeInactive: Boolean): List<GroupRecord>
@ -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?

View File

@ -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) {

View File

@ -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()
}
}

View File

@ -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<String>): 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()

View File

@ -61,6 +61,19 @@
<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_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 -->
<string name="RecipientProvider_unnamed_group">Unnamed group</string>
</resources>