Convert and store new group changes in MessageExtras.

This commit is contained in:
Clark
2024-02-26 10:43:51 -05:00
committed by Alex Hart
parent cc25f0685c
commit 1ade8b502f
17 changed files with 933 additions and 56 deletions

View File

@@ -2305,6 +2305,8 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
val parentStoryId = ParentStoryId.deserialize(cursor.requireLong(PARENT_STORY_ID))
val messageRangesData = cursor.requireBlob(MESSAGE_RANGES)
val scheduledDate = cursor.requireLong(SCHEDULED_DATE)
val messageExtrasBytes = cursor.requireBlob(MESSAGE_EXTRAS)
val messageExtras = if (messageExtrasBytes != null) MessageExtras.ADAPTER.decode(messageExtrasBytes) else null
val quoteId = cursor.requireLong(QUOTE_ID)
val quoteAuthor = cursor.requireLong(QUOTE_AUTHOR)
@@ -2357,7 +2359,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
if (body != null && (MessageTypes.isGroupQuit(outboxType) || MessageTypes.isGroupUpdate(outboxType))) {
OutgoingMessage.groupUpdateMessage(
threadRecipient = threadRecipient,
groupContext = MessageGroupContext(body, MessageTypes.isGroupV2(outboxType)),
groupContext = if (messageExtras != null) MessageGroupContext(messageExtras, MessageTypes.isGroupV2(outboxType)) else MessageGroupContext(body, MessageTypes.isGroupV2(outboxType)),
avatar = attachments,
sentTimeMillis = timestamp,
expiresIn = 0,
@@ -2859,6 +2861,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
contentValues.put(PARENT_STORY_ID, if (message.parentStoryId != null) message.parentStoryId.serialize() else 0)
contentValues.put(SCHEDULED_DATE, message.scheduledDate)
contentValues.putNull(LATEST_REVISION_ID)
contentValues.put(MESSAGE_EXTRAS, message.messageExtras?.encode())
if (editedMessage != null) {
contentValues.put(ORIGINAL_MESSAGE_ID, editedMessage.getOriginalOrOwnMessageId().id)
@@ -5062,7 +5065,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
val editCount = cursor.requireInt(REVISION_NUMBER)
val isRead = cursor.requireBoolean(READ)
val messageExtraBytes = cursor.requireBlob(MESSAGE_EXTRAS)
val messageExtras = if (messageExtraBytes != null) MessageExtras.ADAPTER.decode(messageExtraBytes) else null
val messageExtras = messageExtraBytes?.let { MessageExtras.ADAPTER.decode(it) }
if (!TextSecurePreferences.isReadReceiptsEnabled(context)) {
hasReadReceipt = false

View File

@@ -1929,7 +1929,8 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa
val hasReadReceipt = TextSecurePreferences.isReadReceiptsEnabled(context) && cursor.requireBoolean(HAS_READ_RECEIPT)
val extraString = cursor.getString(cursor.getColumnIndexOrThrow(SNIPPET_EXTRAS))
val messageExtras = cursor.getBlob(cursor.getColumnIndexOrThrow(SNIPPET_MESSAGE_EXTRAS))
val messageExtraBytes = cursor.getBlob(cursor.getColumnIndexOrThrow(SNIPPET_MESSAGE_EXTRAS))
val messageExtras = if (messageExtraBytes != null) MessageExtras.ADAPTER.decode(messageExtraBytes) else null
val extra: Extra? = if (extraString != null) {
try {
val jsonObject = SaneJSONObject(JSONObject(extraString))
@@ -1974,6 +1975,7 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa
.setPinned(cursor.requireBoolean(PINNED))
.setUnreadSelfMentionsCount(cursor.requireInt(UNREAD_SELF_MENTION_COUNT))
.setExtra(extra)
.setSnippetMessageExtras(messageExtras)
.build()
}

View File

@@ -0,0 +1,681 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.database.model
import ProtoUtil.isNullOrEmpty
import okio.ByteString
import org.signal.core.util.StringUtil
import org.signal.storageservice.protos.groups.AccessControl
import org.signal.storageservice.protos.groups.AccessControl.AccessRequired
import org.signal.storageservice.protos.groups.Member
import org.signal.storageservice.protos.groups.local.DecryptedGroup
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange
import org.signal.storageservice.protos.groups.local.DecryptedPendingMember
import org.signal.storageservice.protos.groups.local.DecryptedRequestingMember
import org.signal.storageservice.protos.groups.local.EnabledState
import org.thoughtcrime.securesms.backup.v2.proto.GenericGroupUpdate
import org.thoughtcrime.securesms.backup.v2.proto.GroupAdminStatusUpdate
import org.thoughtcrime.securesms.backup.v2.proto.GroupAnnouncementOnlyChangeUpdate
import org.thoughtcrime.securesms.backup.v2.proto.GroupAttributesAccessLevelChangeUpdate
import org.thoughtcrime.securesms.backup.v2.proto.GroupAvatarUpdate
import org.thoughtcrime.securesms.backup.v2.proto.GroupChangeChatUpdate
import org.thoughtcrime.securesms.backup.v2.proto.GroupCreationUpdate
import org.thoughtcrime.securesms.backup.v2.proto.GroupDescriptionUpdate
import org.thoughtcrime.securesms.backup.v2.proto.GroupExpirationTimerUpdate
import org.thoughtcrime.securesms.backup.v2.proto.GroupInvitationAcceptedUpdate
import org.thoughtcrime.securesms.backup.v2.proto.GroupInvitationDeclinedUpdate
import org.thoughtcrime.securesms.backup.v2.proto.GroupInvitationRevokedUpdate
import org.thoughtcrime.securesms.backup.v2.proto.GroupInviteLinkAdminApprovalUpdate
import org.thoughtcrime.securesms.backup.v2.proto.GroupInviteLinkDisabledUpdate
import org.thoughtcrime.securesms.backup.v2.proto.GroupInviteLinkEnabledUpdate
import org.thoughtcrime.securesms.backup.v2.proto.GroupInviteLinkResetUpdate
import org.thoughtcrime.securesms.backup.v2.proto.GroupJoinRequestApprovalUpdate
import org.thoughtcrime.securesms.backup.v2.proto.GroupJoinRequestCanceledUpdate
import org.thoughtcrime.securesms.backup.v2.proto.GroupJoinRequestUpdate
import org.thoughtcrime.securesms.backup.v2.proto.GroupMemberAddedUpdate
import org.thoughtcrime.securesms.backup.v2.proto.GroupMemberJoinedByLinkUpdate
import org.thoughtcrime.securesms.backup.v2.proto.GroupMemberJoinedUpdate
import org.thoughtcrime.securesms.backup.v2.proto.GroupMemberLeftUpdate
import org.thoughtcrime.securesms.backup.v2.proto.GroupMemberRemovedUpdate
import org.thoughtcrime.securesms.backup.v2.proto.GroupMembershipAccessLevelChangeUpdate
import org.thoughtcrime.securesms.backup.v2.proto.GroupNameUpdate
import org.thoughtcrime.securesms.backup.v2.proto.GroupSelfInvitationRevokedUpdate
import org.thoughtcrime.securesms.backup.v2.proto.GroupSequenceOfRequestsAndCancelsUpdate
import org.thoughtcrime.securesms.backup.v2.proto.GroupUnknownInviteeUpdate
import org.thoughtcrime.securesms.backup.v2.proto.GroupV2AccessLevel
import org.thoughtcrime.securesms.backup.v2.proto.SelfInvitedOtherUserToGroupUpdate
import org.thoughtcrime.securesms.backup.v2.proto.SelfInvitedToGroupUpdate
import org.thoughtcrime.securesms.database.model.databaseprotos.DecryptedGroupV2Context
import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil
import org.whispersystems.signalservice.api.push.ServiceId.Companion.parseOrNull
import org.whispersystems.signalservice.api.push.ServiceIds
import org.whispersystems.signalservice.api.util.UuidUtil
import java.util.LinkedList
import java.util.Optional
import java.util.stream.Collectors
/**
* Object to help with the translation between DecryptedGroupV2Context group updates
* and GroupChangeChatUpdates, which store the update messages as distinct messages rather
* than diffs of the group state.
*/
object GroupsV2UpdateMessageConverter {
@JvmStatic
fun translateDecryptedChange(selfIds: ServiceIds, groupContext: DecryptedGroupV2Context): GroupChangeChatUpdate {
if (groupContext.change != null && ((groupContext.groupState != null && groupContext.groupState.revision != 0) || groupContext.previousGroupState != null)) {
return translateDecryptedChangeUpdate(selfIds, groupContext)
} else {
return translateDecryptedChangeNewGroup(selfIds, groupContext)
}
}
@JvmStatic
fun translateDecryptedChangeNewGroup(selfIds: ServiceIds, groupContext: DecryptedGroupV2Context): GroupChangeChatUpdate {
var selfPending = Optional.empty<DecryptedPendingMember>()
val decryptedGroupChange = groupContext.change
val group = groupContext.groupState
val updates: MutableList<GroupChangeChatUpdate.Update> = LinkedList()
if (group != null) {
selfPending = DecryptedGroupUtil.findPendingByServiceId(group.pendingMembers, selfIds.aci)
if (selfPending.isEmpty() && selfIds.pni != null) {
selfPending = DecryptedGroupUtil.findPendingByServiceId(group.pendingMembers, selfIds.pni)
}
}
if (selfPending.isPresent) {
updates.add(
GroupChangeChatUpdate.Update(
selfInvitedToGroupUpdate = SelfInvitedToGroupUpdate(inviterAci = selfPending.get().addedByAci)
)
)
return GroupChangeChatUpdate(updates = updates)
}
if (decryptedGroupChange != null) {
val foundingMemberUuid: ByteString = decryptedGroupChange.editorServiceIdBytes
if (foundingMemberUuid.size > 0) {
if (selfIds.matches(foundingMemberUuid)) {
updates.add(
GroupChangeChatUpdate.Update(
groupCreationUpdate = GroupCreationUpdate(updaterAci = foundingMemberUuid)
)
)
} else {
updates.add(
GroupChangeChatUpdate.Update(
groupMemberAddedUpdate = GroupMemberAddedUpdate(updaterAci = foundingMemberUuid, newMemberAci = selfIds.aci.toByteString())
)
)
}
return GroupChangeChatUpdate(updates = updates)
}
}
if (group != null && DecryptedGroupUtil.findMemberByAci(group.members, selfIds.aci).isPresent) {
updates.add(GroupChangeChatUpdate.Update(groupMemberJoinedUpdate = GroupMemberJoinedUpdate(newMemberAci = selfIds.aci.toByteString())))
}
return GroupChangeChatUpdate(updates = updates)
}
@JvmStatic
fun translateDecryptedChangeUpdate(selfIds: ServiceIds, groupContext: DecryptedGroupV2Context): GroupChangeChatUpdate {
var previousGroupState = groupContext.previousGroupState
val change = groupContext.change!!
if (DecryptedGroup().equals(previousGroupState)) {
previousGroupState = null
}
val updates: MutableList<GroupChangeChatUpdate.Update> = LinkedList()
var editorUnknown = change.editorServiceIdBytes.size == 0
val editorServiceId = if (editorUnknown) null else parseOrNull(change.editorServiceIdBytes)
if (editorServiceId == null || editorServiceId.isUnknown) {
editorUnknown = true
}
translateMemberAdditions(change, editorUnknown, updates)
translateModifyMemberRoles(change, editorUnknown, updates)
translateInvitations(selfIds, change, editorUnknown, updates)
translateRevokedInvitations(selfIds, change, editorUnknown, updates)
translatePromotePending(selfIds, change, editorUnknown, updates)
translateNewTitle(change, editorUnknown, updates)
translateNewDescription(change, editorUnknown, updates)
translateNewAvatar(change, editorUnknown, updates)
translateNewTimer(change, editorUnknown, updates)
translateNewAttributeAccess(change, editorUnknown, updates)
translateNewMembershipAccess(change, editorUnknown, updates)
translateNewGroupInviteLinkAccess(previousGroupState, change, editorUnknown, updates)
translateRequestingMembers(selfIds, change, editorUnknown, updates)
translateRequestingMemberApprovals(selfIds, change, editorUnknown, updates)
translateRequestingMemberDeletes(selfIds, change, editorUnknown, updates)
translateAnnouncementGroupChange(change, editorUnknown, updates)
translatePromotePendingPniAci(selfIds, change, editorUnknown, updates)
translateMemberRemovals(selfIds, change, editorUnknown, updates)
if (updates.isEmpty()) {
translateUnknownChange(change, editorUnknown, updates)
}
return GroupChangeChatUpdate(updates = updates)
}
@JvmStatic
fun translateMemberAdditions(change: DecryptedGroupChange, editorUnknown: Boolean, updates: MutableList<GroupChangeChatUpdate.Update>) {
for (member in change.newMembers) {
if (!editorUnknown && member.aciBytes == change.editorServiceIdBytes) {
updates.add(
GroupChangeChatUpdate.Update(
groupMemberJoinedByLinkUpdate = GroupMemberJoinedByLinkUpdate(newMemberAci = member.aciBytes)
)
)
} else {
updates.add(
GroupChangeChatUpdate.Update(
groupMemberAddedUpdate = GroupMemberAddedUpdate(
updaterAci = if (editorUnknown) null else change.editorServiceIdBytes,
newMemberAci = member.aciBytes,
hadOpenInvitation = false
)
)
)
}
}
}
@JvmStatic
fun translateModifyMemberRoles(change: DecryptedGroupChange, editorUnknown: Boolean, updates: MutableList<GroupChangeChatUpdate.Update>) {
for (roleChange in change.modifyMemberRoles) {
updates.add(
GroupChangeChatUpdate.Update(
groupAdminStatusUpdate = GroupAdminStatusUpdate(
updaterAci = if (editorUnknown) null else change.editorServiceIdBytes,
memberAci = roleChange.aciBytes,
wasAdminStatusGranted = roleChange.role == Member.Role.ADMINISTRATOR
)
)
)
}
}
@JvmStatic
fun translateInvitations(selfIds: ServiceIds, change: DecryptedGroupChange, editorUnknown: Boolean, updates: MutableList<GroupChangeChatUpdate.Update>) {
val editorIsYou = selfIds.matches(change.editorServiceIdBytes)
var notYouInviteCount = 0
for (invitee in change.newPendingMembers) {
val newMemberIsYou = selfIds.matches(invitee.serviceIdBytes)
if (newMemberIsYou) {
updates.add(
GroupChangeChatUpdate.Update(
selfInvitedToGroupUpdate = SelfInvitedToGroupUpdate(
inviterAci = if (editorUnknown) convertUnknownUUIDtoNull(invitee.addedByAci) else change.editorServiceIdBytes
)
)
)
} else {
if (editorIsYou) {
updates.add(GroupChangeChatUpdate.Update(selfInvitedOtherUserToGroupUpdate = SelfInvitedOtherUserToGroupUpdate(inviteeServiceId = invitee.serviceIdBytes)))
} else {
notYouInviteCount++
}
}
}
if (notYouInviteCount > 0) {
updates.add(
GroupChangeChatUpdate.Update(
groupUnknownInviteeUpdate = GroupUnknownInviteeUpdate(
inviterAci = if (editorUnknown) null else change.editorServiceIdBytes,
inviteeCount = notYouInviteCount
)
)
)
}
}
@JvmStatic
fun translateRevokedInvitations(selfIds: ServiceIds, change: DecryptedGroupChange, editorUnknown: Boolean, updates: MutableList<GroupChangeChatUpdate.Update>) {
val editorAci = if (editorUnknown) null else change.editorServiceIdBytes
val revokedInvitees = LinkedList<GroupInvitationRevokedUpdate.Invitee>()
for (invitee in change.deletePendingMembers) {
val decline = invitee.serviceIdBytes == editorAci
if (decline) {
updates.add(
GroupChangeChatUpdate.Update(
groupInvitationDeclinedUpdate = GroupInvitationDeclinedUpdate(inviteeAci = invitee.serviceIdBytes)
)
)
} else if (selfIds.matches(invitee.serviceIdBytes)) {
updates.add(
GroupChangeChatUpdate.Update(
groupSelfInvitationRevokedUpdate = GroupSelfInvitationRevokedUpdate(revokerAci = editorAci)
)
)
} else {
revokedInvitees.add(
GroupInvitationRevokedUpdate.Invitee(
inviteeAci = invitee.serviceIdBytes
)
)
}
}
if (revokedInvitees.isNotEmpty()) {
updates.add(
GroupChangeChatUpdate.Update(
groupInvitationRevokedUpdate = GroupInvitationRevokedUpdate(
updaterAci = editorAci,
invitees = revokedInvitees
)
)
)
}
}
@JvmStatic
fun translatePromotePending(selfIds: ServiceIds, change: DecryptedGroupChange, editorUnknown: Boolean, updates: MutableList<GroupChangeChatUpdate.Update>) {
val editorAci = if (editorUnknown) null else change.editorServiceIdBytes
val editorIsYou = if (editorUnknown) false else selfIds.matches(editorAci)
for (member in change.promotePendingMembers) {
val newMemberIsYou: Boolean = selfIds.matches(member.aciBytes)
if (editorIsYou) {
if (newMemberIsYou) {
updates.add(
GroupChangeChatUpdate.Update(
groupInvitationAcceptedUpdate = GroupInvitationAcceptedUpdate(
inviterAci = null,
newMemberAci = member.aciBytes
)
)
)
} else {
updates.add(
GroupChangeChatUpdate.Update(
groupMemberAddedUpdate = GroupMemberAddedUpdate(
updaterAci = editorAci,
newMemberAci = member.aciBytes,
hadOpenInvitation = true
)
)
)
}
} else if (editorUnknown) {
updates.add(
GroupChangeChatUpdate.Update(
groupMemberJoinedUpdate = GroupMemberJoinedUpdate(
newMemberAci = member.aciBytes
)
)
)
} else if (member.aciBytes == change.editorServiceIdBytes) {
updates.add(
GroupChangeChatUpdate.Update(
groupInvitationAcceptedUpdate = GroupInvitationAcceptedUpdate(
inviterAci = null,
newMemberAci = member.aciBytes
)
)
)
} else {
updates.add(
GroupChangeChatUpdate.Update(
groupMemberAddedUpdate = GroupMemberAddedUpdate(
updaterAci = editorAci,
newMemberAci = member.aciBytes,
hadOpenInvitation = true
)
)
)
}
}
}
@JvmStatic
fun translateNewTitle(change: DecryptedGroupChange, editorUnknown: Boolean, updates: MutableList<GroupChangeChatUpdate.Update>) {
if (change.newTitle != null) {
val editorAci = if (editorUnknown) null else change.editorServiceIdBytes
val newTitle = StringUtil.isolateBidi(change.newTitle?.value_)
updates.add(
GroupChangeChatUpdate.Update(
groupNameUpdate = GroupNameUpdate(
updaterAci = editorAci,
newGroupName = newTitle
)
)
)
}
}
@JvmStatic
fun translateNewDescription(change: DecryptedGroupChange, editorUnknown: Boolean, updates: MutableList<GroupChangeChatUpdate.Update>) {
if (change.newDescription != null) {
val editorAci = if (editorUnknown) null else change.editorServiceIdBytes
updates.add(
GroupChangeChatUpdate.Update(
groupDescriptionUpdate = GroupDescriptionUpdate(
updaterAci = editorAci,
newDescription = change.newDescription?.value_
)
)
)
}
}
@JvmStatic
fun translateNewAvatar(change: DecryptedGroupChange, editorUnknown: Boolean, updates: MutableList<GroupChangeChatUpdate.Update>) {
if (change.newAvatar != null) {
val editorAci = if (editorUnknown) null else change.editorServiceIdBytes
updates.add(
GroupChangeChatUpdate.Update(
groupAvatarUpdate = GroupAvatarUpdate(
updaterAci = editorAci,
wasRemoved = change.newAvatar?.value_.isNullOrEmpty()
)
)
)
}
}
@JvmStatic
fun translateNewTimer(change: DecryptedGroupChange, editorUnknown: Boolean, updates: MutableList<GroupChangeChatUpdate.Update>) {
if (change.newTimer != null) {
updates.add(
GroupChangeChatUpdate.Update(
groupExpirationTimerUpdate = GroupExpirationTimerUpdate(
expiresInMs = change.newTimer!!.duration * 1000,
updaterAci = if (editorUnknown) null else change.editorServiceIdBytes
)
)
)
}
}
@JvmStatic
fun translateNewAttributeAccess(change: DecryptedGroupChange, editorUnknown: Boolean, updates: MutableList<GroupChangeChatUpdate.Update>) {
if (change.newAttributeAccess != AccessControl.AccessRequired.UNKNOWN) {
val editorAci = if (editorUnknown) null else change.editorServiceIdBytes
updates.add(
GroupChangeChatUpdate.Update(
groupAttributesAccessLevelChangeUpdate = GroupAttributesAccessLevelChangeUpdate(
updaterAci = editorAci,
accessLevel = translateGv2AccessLevel(change.newAttributeAccess)
)
)
)
}
}
private fun translateGv2AccessLevel(accessRequired: AccessRequired): GroupV2AccessLevel {
return when (accessRequired) {
AccessRequired.ANY -> GroupV2AccessLevel.ANY
AccessRequired.MEMBER -> GroupV2AccessLevel.MEMBER
AccessRequired.ADMINISTRATOR -> GroupV2AccessLevel.ADMINISTRATOR
AccessRequired.UNSATISFIABLE -> GroupV2AccessLevel.UNSATISFIABLE
AccessRequired.UNKNOWN -> GroupV2AccessLevel.UNKNOWN
else -> GroupV2AccessLevel.UNKNOWN
}
}
@JvmStatic
fun translateNewMembershipAccess(change: DecryptedGroupChange, editorUnknown: Boolean, updates: MutableList<GroupChangeChatUpdate.Update>) {
if (change.newMemberAccess !== AccessRequired.UNKNOWN) {
val editorAci = if (editorUnknown) null else change.editorServiceIdBytes
updates.add(
GroupChangeChatUpdate.Update(
groupMembershipAccessLevelChangeUpdate = GroupMembershipAccessLevelChangeUpdate(
updaterAci = editorAci,
accessLevel = translateGv2AccessLevel(change.newMemberAccess)
)
)
)
}
}
@JvmStatic
fun translateNewGroupInviteLinkAccess(previousGroupState: DecryptedGroup?, change: DecryptedGroupChange, editorUnknown: Boolean, updates: MutableList<GroupChangeChatUpdate.Update>) {
var previousAccessControl: AccessRequired? = null
if (previousGroupState?.accessControl != null) {
previousAccessControl = previousGroupState.accessControl!!.addFromInviteLink
}
var groupLinkEnabled = false
val editorAci = if (editorUnknown) null else change.editorServiceIdBytes
when (change.newInviteLinkAccess) {
AccessRequired.ANY -> {
groupLinkEnabled = true
updates.add(
if (previousAccessControl == AccessControl.AccessRequired.ADMINISTRATOR) {
GroupChangeChatUpdate.Update(
groupInviteLinkAdminApprovalUpdate = GroupInviteLinkAdminApprovalUpdate(
updaterAci = editorAci,
linkRequiresAdminApproval = false
)
)
} else {
GroupChangeChatUpdate.Update(
groupInviteLinkEnabledUpdate = GroupInviteLinkEnabledUpdate(
updaterAci = editorAci,
linkRequiresAdminApproval = false
)
)
}
)
}
AccessRequired.ADMINISTRATOR -> {
groupLinkEnabled = true
updates.add(
if (previousAccessControl == AccessControl.AccessRequired.ANY) {
GroupChangeChatUpdate.Update(
groupInviteLinkAdminApprovalUpdate = GroupInviteLinkAdminApprovalUpdate(
updaterAci = editorAci,
linkRequiresAdminApproval = true
)
)
} else {
GroupChangeChatUpdate.Update(
groupInviteLinkEnabledUpdate = GroupInviteLinkEnabledUpdate(
updaterAci = editorAci,
linkRequiresAdminApproval = true
)
)
}
)
}
AccessRequired.UNSATISFIABLE -> {
updates.add(
GroupChangeChatUpdate.Update(
groupInviteLinkDisabledUpdate = GroupInviteLinkDisabledUpdate(
updaterAci = editorAci
)
)
)
}
else -> {}
}
if (!groupLinkEnabled && change.newInviteLinkPassword.size > 0) {
updates.add(
GroupChangeChatUpdate.Update(
groupInviteLinkResetUpdate = GroupInviteLinkResetUpdate(
updaterAci = editorAci
)
)
)
}
}
@JvmStatic
fun translateRequestingMembers(selfIds: ServiceIds, change: DecryptedGroupChange, editorUnknown: Boolean, updates: MutableList<GroupChangeChatUpdate.Update>) {
val deleteRequestingUuids: Set<ByteString> = HashSet(change.deleteRequestingMembers)
for (member in change.newRequestingMembers) {
val requestingMemberIsYou = selfIds.matches(member.aciBytes)
if (!requestingMemberIsYou && deleteRequestingUuids.contains(member.aciBytes)) {
updates.add(
GroupChangeChatUpdate.Update(
groupSequenceOfRequestsAndCancelsUpdate = GroupSequenceOfRequestsAndCancelsUpdate(
requestorAci = member.aciBytes,
count = change.deleteRequestingMembers.size
)
)
)
} else {
updates.add(
GroupChangeChatUpdate.Update(
groupJoinRequestUpdate = GroupJoinRequestUpdate(
requestorAci = member.aciBytes
)
)
)
}
}
}
@JvmStatic
fun translateRequestingMemberApprovals(selfIds: ServiceIds, change: DecryptedGroupChange, editorUnknown: Boolean, updates: MutableList<GroupChangeChatUpdate.Update>) {
val editorAci = if (editorUnknown) null else change.editorServiceIdBytes
for (requestingMember in change.promoteRequestingMembers) {
updates.add(
GroupChangeChatUpdate.Update(
groupJoinRequestApprovalUpdate = GroupJoinRequestApprovalUpdate(
updaterAci = editorAci,
requestorAci = requestingMember.aciBytes,
wasApproved = true
)
)
)
}
}
@JvmStatic
fun translateRequestingMemberDeletes(selfIds: ServiceIds, change: DecryptedGroupChange, editorUnknown: Boolean, updates: MutableList<GroupChangeChatUpdate.Update>) {
val newRequestingUuids = change.newRequestingMembers.stream().map { m: DecryptedRequestingMember -> m.aciBytes }.collect(Collectors.toSet())
val editorIsYou = selfIds.matches(change.editorServiceIdBytes)
val editorAci = if (editorUnknown) null else change.editorServiceIdBytes
for (requestingMember in change.deleteRequestingMembers) {
if (newRequestingUuids.contains(requestingMember)) {
continue
}
val requestingMemberIsYou = selfIds.matches(requestingMember)
if ((requestingMemberIsYou && editorIsYou) || (change.editorServiceIdBytes == requestingMember)) {
updates.add(
GroupChangeChatUpdate.Update(
groupJoinRequestCanceledUpdate = GroupJoinRequestCanceledUpdate(
requestorAci = requestingMember
)
)
)
} else {
updates.add(
GroupChangeChatUpdate.Update(
groupJoinRequestApprovalUpdate = GroupJoinRequestApprovalUpdate(
requestorAci = requestingMember,
updaterAci = editorAci,
wasApproved = false
)
)
)
}
}
}
@JvmStatic
fun translateAnnouncementGroupChange(change: DecryptedGroupChange, editorUnknown: Boolean, updates: MutableList<GroupChangeChatUpdate.Update>) {
if (change.newIsAnnouncementGroup == EnabledState.ENABLED || change.newIsAnnouncementGroup == EnabledState.DISABLED) {
val editorAci = if (editorUnknown) null else change.editorServiceIdBytes
updates.add(
GroupChangeChatUpdate.Update(
groupAnnouncementOnlyChangeUpdate = GroupAnnouncementOnlyChangeUpdate(
updaterAci = editorAci,
isAnnouncementOnly = change.newIsAnnouncementGroup == EnabledState.ENABLED
)
)
)
}
}
@JvmStatic
fun translatePromotePendingPniAci(selfIds: ServiceIds, change: DecryptedGroupChange, editorUnknown: Boolean, updates: MutableList<GroupChangeChatUpdate.Update>) {
val editorIsYou = selfIds.matches(change.editorServiceIdBytes)
for (newMember in change.promotePendingPniAciMembers) {
if (editorUnknown) {
updates.add(
GroupChangeChatUpdate.Update(
groupMemberJoinedUpdate = GroupMemberJoinedUpdate(
newMemberAci = newMember.aciBytes
)
)
)
} else {
if ((selfIds.matches(newMember.aciBytes) && editorIsYou) || newMember.aciBytes == change.editorServiceIdBytes) {
updates.add(
GroupChangeChatUpdate.Update(
groupInvitationAcceptedUpdate = GroupInvitationAcceptedUpdate(
inviterAci = null,
newMemberAci = newMember.aciBytes
)
)
)
} else {
updates.add(
GroupChangeChatUpdate.Update(
groupMemberAddedUpdate = GroupMemberAddedUpdate(
newMemberAci = newMember.aciBytes,
updaterAci = change.editorServiceIdBytes,
hadOpenInvitation = true,
inviterAci = null
)
)
)
}
}
}
}
@JvmStatic
fun translateMemberRemovals(selfIds: ServiceIds, change: DecryptedGroupChange, editorUnknown: Boolean, updates: MutableList<GroupChangeChatUpdate.Update>) {
val editorIsYou: Boolean = selfIds.matches(change.editorServiceIdBytes)
for (member in change.deleteMembers) {
val removedMemberIsYou: Boolean = selfIds.matches(member)
if ((editorIsYou && removedMemberIsYou) || member == change.editorServiceIdBytes) {
updates.add(
GroupChangeChatUpdate.Update(
groupMemberLeftUpdate = GroupMemberLeftUpdate(aci = member)
)
)
} else {
updates.add(
GroupChangeChatUpdate.Update(
groupMemberRemovedUpdate = GroupMemberRemovedUpdate(
removerAci = if (editorUnknown) null else change.editorServiceIdBytes,
removedAci = member
)
)
)
}
}
}
@JvmStatic
fun translateUnknownChange(change: DecryptedGroupChange, editorUnknown: Boolean, updates: MutableList<GroupChangeChatUpdate.Update>) {
updates.add(
GroupChangeChatUpdate.Update(
genericGroupUpdate = GenericGroupUpdate(
updaterAci = if (editorUnknown) null else change.editorServiceIdBytes
)
)
)
}
private fun convertUnknownUUIDtoNull(id: ByteString?): ByteString? {
if (id.isNullOrEmpty()) return null
val uuid = UuidUtil.fromByteStringOrUnknown(id)
if (UuidUtil.UNKNOWN_UUID == uuid) return null
return id
}
}

View File

@@ -33,6 +33,7 @@ import org.thoughtcrime.securesms.backup.v2.proto.GroupAvatarUpdate;
import org.thoughtcrime.securesms.backup.v2.proto.GroupChangeChatUpdate;
import org.thoughtcrime.securesms.backup.v2.proto.GroupCreationUpdate;
import org.thoughtcrime.securesms.backup.v2.proto.GroupDescriptionUpdate;
import org.thoughtcrime.securesms.backup.v2.proto.GroupExpirationTimerUpdate;
import org.thoughtcrime.securesms.backup.v2.proto.GroupInvitationAcceptedUpdate;
import org.thoughtcrime.securesms.backup.v2.proto.GroupInvitationDeclinedUpdate;
import org.thoughtcrime.securesms.backup.v2.proto.GroupInvitationRevokedUpdate;
@@ -44,11 +45,13 @@ import org.thoughtcrime.securesms.backup.v2.proto.GroupJoinRequestApprovalUpdate
import org.thoughtcrime.securesms.backup.v2.proto.GroupJoinRequestCanceledUpdate;
import org.thoughtcrime.securesms.backup.v2.proto.GroupJoinRequestUpdate;
import org.thoughtcrime.securesms.backup.v2.proto.GroupMemberAddedUpdate;
import org.thoughtcrime.securesms.backup.v2.proto.GroupMemberJoinedByLinkUpdate;
import org.thoughtcrime.securesms.backup.v2.proto.GroupMemberJoinedUpdate;
import org.thoughtcrime.securesms.backup.v2.proto.GroupMemberLeftUpdate;
import org.thoughtcrime.securesms.backup.v2.proto.GroupMemberRemovedUpdate;
import org.thoughtcrime.securesms.backup.v2.proto.GroupMembershipAccessLevelChangeUpdate;
import org.thoughtcrime.securesms.backup.v2.proto.GroupNameUpdate;
import org.thoughtcrime.securesms.backup.v2.proto.GroupSelfInvitationRevokedUpdate;
import org.thoughtcrime.securesms.backup.v2.proto.GroupUnknownInviteeUpdate;
import org.thoughtcrime.securesms.backup.v2.proto.GroupV2AccessLevel;
import org.thoughtcrime.securesms.backup.v2.proto.GroupV2MigrationDroppedMembersUpdate;
@@ -57,6 +60,7 @@ import org.thoughtcrime.securesms.backup.v2.proto.GroupV2MigrationSelfInvitedUpd
import org.thoughtcrime.securesms.backup.v2.proto.GroupV2MigrationUpdate;
import org.thoughtcrime.securesms.backup.v2.proto.SelfInvitedOtherUserToGroupUpdate;
import org.thoughtcrime.securesms.backup.v2.proto.SelfInvitedToGroupUpdate;
import org.thoughtcrime.securesms.database.model.databaseprotos.DecryptedGroupV2Context;
import org.thoughtcrime.securesms.database.model.databaseprotos.GV2UpdateDescription;
import org.thoughtcrime.securesms.groups.GV2AccessLevelUtil;
import org.thoughtcrime.securesms.groups.GroupMigrationMembershipChange;
@@ -145,6 +149,9 @@ final class GroupsV2UpdateMessageProducer {
for (GroupChangeChatUpdate.Update update : groupUpdates) {
describeUpdate(update, updates);
}
if (updates.isEmpty()) {
updates.add(updateDescription(context.getString(R.string.MessageRecord_group_updated), R.drawable.ic_update_group_16));
}
return updates;
}
@@ -210,6 +217,41 @@ final class GroupsV2UpdateMessageProducer {
describeGroupV2MigrationInvitedMembersUpdate(update.groupV2MigrationInvitedMembersUpdate, updates);
} else if (update.groupV2MigrationSelfInvitedUpdate != null) {
describeGroupV2MigrationSelfInvitedUpdate(update.groupV2MigrationSelfInvitedUpdate, updates);
} else if (update.groupMemberJoinedByLinkUpdate != null) {
describeGroupMemberJoinedByLinkUpdate(update.groupMemberJoinedByLinkUpdate, updates);
} else if (update.groupExpirationTimerUpdate != null) {
describeGroupExpirationTimerUpdate(update.groupExpirationTimerUpdate, updates);
} else if (update.groupSelfInvitationRevokedUpdate != null) {
describeGroupSelfInvitationRevokedUpdate(update.groupSelfInvitationRevokedUpdate, updates);
}
}
private void describeGroupSelfInvitationRevokedUpdate(@NonNull GroupSelfInvitationRevokedUpdate update, @NonNull List<UpdateDescription> updates) {
if (update.revokerAci == null) {
updates.add(updateDescription(context.getString(R.string.MessageRecord_an_admin_revoked_your_invitation_to_the_group), R.drawable.ic_update_group_decline_16));
} else {
updates.add(updateDescription(R.string.MessageRecord_s_revoked_your_invitation_to_the_group, update.revokerAci, R.drawable.ic_update_group_decline_16));
}
}
private void describeGroupExpirationTimerUpdate(@NonNull GroupExpirationTimerUpdate update, @NonNull List<UpdateDescription> updates) {
String time = ExpirationUtil.getExpirationDisplayValue(context, update.expiresInMs / 1000);
if (update.updaterAci == null) {
updates.add(updateDescription(context.getString(R.string.MessageRecord_disappearing_message_time_set_to_s, time), R.drawable.ic_update_timer_16));
} else {
boolean editorIsYou = selfIds.matches(update.updaterAci);
if (editorIsYou) {
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_set_disappearing_message_time_to_s, time), R.drawable.ic_update_timer_16));
} else {
updates.add(updateDescription(R.string.MessageRecord_s_set_disappearing_message_time_to_s, update.updaterAci, time, R.drawable.ic_update_timer_16));
}
}
}
private void describeGroupMemberJoinedByLinkUpdate(@NonNull GroupMemberJoinedByLinkUpdate update, @NonNull List<UpdateDescription> updates) {
if (selfIds.matches(update.newMemberAci)) {
updates.add(0, updateDescription(context.getString(R.string.MessageRecord_you_joined_the_group_via_the_group_link), R.drawable.ic_update_group_accept_16));
} else {
updates.add(updateDescription(R.string.MessageRecord_s_joined_the_group_via_the_group_link, update.newMemberAci, R.drawable.ic_update_group_accept_16));
}
}
@@ -254,12 +296,10 @@ final class GroupsV2UpdateMessageProducer {
}
private void describeInviteLinkDisabledUpdate(@NonNull GroupInviteLinkDisabledUpdate update, @NonNull List<UpdateDescription> updates) {
boolean editorIsYou = selfIds.matches(update.updaterAci);
if (update.updaterAci == null) {
updates.add(updateDescription(context.getString(R.string.MessageRecord_the_group_link_has_been_turned_off), R.drawable.ic_update_group_role_16));
} else {
if (editorIsYou) {
if (selfIds.matches(update.updaterAci)) {
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_turned_off_the_group_link), R.drawable.ic_update_group_role_16));
} else {
updates.add(updateDescription(R.string.MessageRecord_s_turned_off_the_group_link, update.updaterAci, R.drawable.ic_update_group_role_16));
@@ -268,7 +308,6 @@ final class GroupsV2UpdateMessageProducer {
}
private void describeInviteLinkEnabledUpdate(@NonNull GroupInviteLinkEnabledUpdate update, @NonNull List<UpdateDescription> updates) {
boolean editorIsYou = selfIds.matches(update.updaterAci);
if (update.updaterAci == null) {
if (update.linkRequiresAdminApproval) {
@@ -277,7 +316,7 @@ final class GroupsV2UpdateMessageProducer {
updates.add(updateDescription(context.getString(R.string.MessageRecord_the_group_link_has_been_turned_on_with_admin_approval_off), R.drawable.ic_update_group_role_16));
}
} else {
if (editorIsYou) {
if (selfIds.matches(update.updaterAci)) {
if (update.linkRequiresAdminApproval) {
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_turned_on_the_group_link_with_admin_approval_on), R.drawable.ic_update_group_role_16));
} else {
@@ -366,7 +405,7 @@ final class GroupsV2UpdateMessageProducer {
private void describeGroupInvitationRevokedUpdate(@NonNull GroupInvitationRevokedUpdate update, @NonNull List<UpdateDescription> updates) {
int revokedMeCount = 0;
for (GroupInvitationRevokedUpdate.Invitee invitee : update.invitees) {
if (selfIds.matches(invitee.inviteeAci) || selfIds.matches(invitee.inviteePni)) {
if ((invitee.inviteeAci != null && selfIds.matches(invitee.inviteeAci)) || (invitee.inviteePni != null && selfIds.matches(invitee.inviteePni))) {
revokedMeCount++;
}
}
@@ -409,17 +448,21 @@ final class GroupsV2UpdateMessageProducer {
} else {
updates.add(updateDescription(R.string.MessageRecord_s_joined_the_group, update.newMemberAci, R.drawable.ic_update_group_add_16));
}
} else if (update.hadOpenInvitation) {
if (selfIds.matches(update.updaterAci)) {
updates.add(updateDescription(R.string.MessageRecord_you_added_invited_member_s, update.newMemberAci, R.drawable.ic_update_group_add_16));
} else {
updates.add(updateDescription(R.string.MessageRecord_s_added_invited_member_s, update.updaterAci, update.newMemberAci, R.drawable.ic_update_group_add_16));
}
} else {
if (newMemberIsYou) {
updates.add(0, updateDescription(R.string.MessageRecord_s_added_you, update.updaterAci, R.drawable.ic_update_group_add_16));
} else if (selfIds.matches(update.updaterAci)) {
if (update.hadOpenInvitation) {
updates.add(updateDescription(R.string.MessageRecord_you_added_invited_member_s, update.newMemberAci, R.drawable.ic_update_group_add_16));
} else {
updates.add(updateDescription(R.string.MessageRecord_you_added_s, update.newMemberAci, R.drawable.ic_update_group_add_16));
}
} else {
updates.add(updateDescription(R.string.MessageRecord_s_added_s, update.updaterAci, update.newMemberAci, R.drawable.ic_update_group_add_16));
if (update.hadOpenInvitation) {
updates.add(updateDescription(R.string.MessageRecord_s_added_invited_member_s, update.updaterAci, update.newMemberAci, R.drawable.ic_update_group_add_16));
} else {
updates.add(updateDescription(R.string.MessageRecord_s_added_s, update.updaterAci, update.newMemberAci, R.drawable.ic_update_group_add_16));
}
}
}
}

View File

@@ -180,7 +180,11 @@ public abstract class MessageRecord extends DisplayRecord {
public @Nullable UpdateDescription getUpdateDisplayBody(@NonNull Context context, @Nullable Consumer<RecipientId> recipientClickHandler) {
if (isGroupUpdate() && isGroupV2()) {
return getGv2ChangeDescription(context, getBody(), recipientClickHandler);
if (messageExtras != null) {
return getGv2ChangeDescription(context, messageExtras, recipientClickHandler);
} else {
return getGv2ChangeDescription(context, getBody(), recipientClickHandler);
}
} else if (isGroupUpdate() && isOutgoing()) {
return staticUpdateDescription(context.getString(R.string.MessageRecord_you_updated_group), R.drawable.ic_update_group_16);
} else if (isGroupUpdate()) {