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

@ -9,6 +9,7 @@ import android.database.Cursor
import com.annimon.stream.Stream
import okio.ByteString.Companion.toByteString
import org.signal.core.util.Base64
import org.signal.core.util.Base64.decode
import org.signal.core.util.Base64.decodeOrThrow
import org.signal.core.util.logging.Log
import org.signal.core.util.requireBlob
@ -40,11 +41,15 @@ import org.thoughtcrime.securesms.database.SignalDatabase.Companion.calls
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatchSet
import org.thoughtcrime.securesms.database.documents.NetworkFailureSet
import org.thoughtcrime.securesms.database.model.GroupCallUpdateDetailsUtil
import org.thoughtcrime.securesms.database.model.GroupsV2UpdateMessageConverter
import org.thoughtcrime.securesms.database.model.ReactionRecord
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList
import org.thoughtcrime.securesms.database.model.databaseprotos.DecryptedGroupV2Context
import org.thoughtcrime.securesms.database.model.databaseprotos.MessageExtras
import org.thoughtcrime.securesms.database.model.databaseprotos.ProfileChangeDetails
import org.thoughtcrime.securesms.database.model.databaseprotos.SessionSwitchoverEvent
import org.thoughtcrime.securesms.database.model.databaseprotos.ThreadMergeEvent
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.mms.QuoteModel
import org.thoughtcrime.securesms.util.JsonUtils
import org.whispersystems.signalservice.api.push.ServiceId.ACI
@ -154,6 +159,26 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize:
}
)
}
MessageTypes.isGroupV2(record.type) && MessageTypes.isGroupUpdate(record.type) -> {
val groupChange = record.messageExtras?.gv2UpdateDescription?.groupChangeUpdate
if (groupChange != null) {
builder.updateMessage = ChatUpdateMessage(
groupChange = groupChange
)
} else if (record.body != null) {
try {
val decoded: ByteArray = decode(record.body)
val context = DecryptedGroupV2Context.ADAPTER.decode(decoded)
builder.updateMessage = ChatUpdateMessage(
groupChange = GroupsV2UpdateMessageConverter.translateDecryptedChange(selfIds = SignalStore.account().getServiceIds(), context)
)
} catch (e: IOException) {
continue
}
} else {
continue
}
}
MessageTypes.isCallLog(record.type) -> {
val call = calls.getCallByMessageId(record.id)
if (call != null) {
@ -412,6 +437,17 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize:
}
}
private fun ByteArray?.parseMessageExtras(): MessageExtras? {
if (this == null) {
return null
}
return try {
MessageExtras.ADAPTER.decode(this)
} catch (e: java.lang.Exception) {
null
}
}
private fun Cursor.toBackupMessageRecord(): BackupMessageRecord {
return BackupMessageRecord(
id = this.requireLong(MessageTable.ID),
@ -443,7 +479,8 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize:
receiptTimestamp = this.requireLong(MessageTable.RECEIPT_TIMESTAMP),
networkFailureRecipientIds = this.requireString(MessageTable.NETWORK_FAILURES).parseNetworkFailures(),
identityMismatchRecipientIds = this.requireString(MessageTable.MISMATCHED_IDENTITIES).parseIdentityMismatches(),
baseType = this.requireLong(COLUMN_BASE_TYPE)
baseType = this.requireLong(COLUMN_BASE_TYPE),
messageExtras = this.requireBlob(MessageTable.MESSAGE_EXTRAS).parseMessageExtras()
)
}
@ -477,6 +514,7 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize:
val read: Boolean,
val networkFailureRecipientIds: Set<Long>,
val identityMismatchRecipientIds: Set<Long>,
val baseType: Long
val baseType: Long,
val messageExtras: MessageExtras?
)
}

View file

@ -33,6 +33,8 @@ import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatchSet
import org.thoughtcrime.securesms.database.documents.NetworkFailure
import org.thoughtcrime.securesms.database.documents.NetworkFailureSet
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList
import org.thoughtcrime.securesms.database.model.databaseprotos.GV2UpdateDescription
import org.thoughtcrime.securesms.database.model.databaseprotos.MessageExtras
import org.thoughtcrime.securesms.database.model.databaseprotos.ProfileChangeDetails
import org.thoughtcrime.securesms.database.model.databaseprotos.SessionSwitchoverEvent
import org.thoughtcrime.securesms.database.model.databaseprotos.ThreadMergeEvent
@ -410,6 +412,17 @@ class ChatItemImportInserter(
// Calls don't use the incoming/outgoing flags, so we overwrite the flags here
this.put(MessageTable.TYPE, typeFlags)
}
updateMessage.groupChange != null -> {
put(MessageTable.BODY, "")
put(
MessageTable.MESSAGE_EXTRAS,
MessageExtras(
gv2UpdateDescription =
GV2UpdateDescription(groupChangeUpdate = updateMessage.groupChange)
).encode()
)
typeFlags = MessageTypes.GROUP_V2_BIT or MessageTypes.GROUP_UPDATE_BIT
}
}
this.put(MessageTable.TYPE, getAsLong(MessageTable.TYPE) or typeFlags)
}

View file

@ -47,7 +47,8 @@ fun MessageTable.getMessagesForBackup(): ChatItemExportIterator {
MessageTable.READ,
MessageTable.NETWORK_FAILURES,
MessageTable.MISMATCHED_IDENTITIES,
"${MessageTable.TYPE} & ${MessageTypes.BASE_TYPE_MASK} AS ${ChatItemExportIterator.COLUMN_BASE_TYPE}"
"${MessageTable.TYPE} & ${MessageTypes.BASE_TYPE_MASK} AS ${ChatItemExportIterator.COLUMN_BASE_TYPE}",
MessageTable.MESSAGE_EXTRAS
)
.from(MessageTable.TABLE_NAME)
.where(

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

View file

@ -35,6 +35,7 @@ import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.database.ThreadTable;
import org.thoughtcrime.securesms.database.model.GroupRecord;
import org.thoughtcrime.securesms.database.model.databaseprotos.DecryptedGroupV2Context;
import org.thoughtcrime.securesms.database.model.databaseprotos.GV2UpdateDescription;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.groups.v2.GroupCandidateHelper;
import org.thoughtcrime.securesms.groups.v2.GroupInviteLinkUrl;
@ -1295,10 +1296,10 @@ final class GroupManagerV2 {
@Nullable GroupChange signedGroupChange,
boolean sendToMembers)
{
GroupId.V2 groupId = GroupId.v2(masterKey);
Recipient groupRecipient = Recipient.externalGroupExact(groupId);
DecryptedGroupV2Context decryptedGroupV2Context = GroupProtoUtil.createDecryptedGroupV2Context(masterKey, groupMutation, signedGroupChange);
OutgoingMessage outgoingMessage = OutgoingMessage.groupUpdateMessage(groupRecipient, decryptedGroupV2Context, System.currentTimeMillis());
GroupId.V2 groupId = GroupId.v2(masterKey);
Recipient groupRecipient = Recipient.externalGroupExact(groupId);
GV2UpdateDescription updateDescription = GroupProtoUtil.createOutgoingGroupV2UpdateDescription(masterKey, groupMutation, signedGroupChange);
OutgoingMessage outgoingMessage = OutgoingMessage.groupUpdateMessage(groupRecipient, updateDescription, System.currentTimeMillis());
DecryptedGroupChange plainGroupChange = groupMutation.getGroupChange();

View file

@ -10,7 +10,13 @@ import org.signal.storageservice.protos.groups.local.DecryptedGroup;
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
import org.signal.storageservice.protos.groups.local.DecryptedMember;
import org.signal.storageservice.protos.groups.local.DecryptedPendingMember;
import org.thoughtcrime.securesms.backup.v2.proto.GroupChangeChatUpdate;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.database.model.GroupsV2UpdateMessageConverter;
import org.thoughtcrime.securesms.database.model.databaseprotos.DecryptedGroupV2Context;
import org.thoughtcrime.securesms.database.model.databaseprotos.GV2UpdateDescription;
import org.thoughtcrime.securesms.database.model.databaseprotos.MessageExtras;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.whispersystems.signalservice.api.groupsv2.PartialDecryptedGroup;
@ -45,6 +51,19 @@ public final class GroupProtoUtil {
throw new GroupNotAMemberException();
}
public static GV2UpdateDescription createOutgoingGroupV2UpdateDescription(@NonNull GroupMasterKey masterKey,
@NonNull GroupMutation groupMutation,
@Nullable GroupChange signedServerChange)
{
DecryptedGroupV2Context groupV2Context = createDecryptedGroupV2Context(masterKey, groupMutation, signedServerChange);
GroupChangeChatUpdate update = GroupsV2UpdateMessageConverter.translateDecryptedChange(SignalStore.account().getServiceIds(), groupV2Context);
return new GV2UpdateDescription.Builder()
.gv2ChangeDescription(groupV2Context)
.groupChangeUpdate(update)
.build();
}
public static DecryptedGroupV2Context createDecryptedGroupV2Context(@NonNull GroupMasterKey masterKey,
@NonNull GroupMutation groupMutation,
@Nullable GroupChange signedServerChange)

View file

@ -18,13 +18,16 @@ import org.signal.storageservice.protos.groups.local.DecryptedGroup;
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
import org.signal.storageservice.protos.groups.local.DecryptedMember;
import org.signal.storageservice.protos.groups.local.DecryptedPendingMember;
import org.thoughtcrime.securesms.backup.v2.proto.GroupChangeChatUpdate;
import org.thoughtcrime.securesms.database.GroupTable;
import org.thoughtcrime.securesms.database.MessageTable;
import org.thoughtcrime.securesms.database.RecipientTable;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.database.ThreadTable;
import org.thoughtcrime.securesms.database.model.GroupRecord;
import org.thoughtcrime.securesms.database.model.GroupsV2UpdateMessageConverter;
import org.thoughtcrime.securesms.database.model.databaseprotos.DecryptedGroupV2Context;
import org.thoughtcrime.securesms.database.model.databaseprotos.GV2UpdateDescription;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.groups.GroupDoesNotExistException;
import org.thoughtcrime.securesms.groups.GroupId;
@ -573,8 +576,8 @@ public class GroupsV2StateProcessor {
.deleteMembers(Collections.singletonList(serviceIds.getAci().toByteString()))
.build();
DecryptedGroupV2Context decryptedGroupV2Context = GroupProtoUtil.createDecryptedGroupV2Context(masterKey, new GroupMutation(decryptedGroup, simulatedGroupChange, simulatedGroupState), null);
OutgoingMessage leaveMessage = OutgoingMessage.groupUpdateMessage(groupRecipient, decryptedGroupV2Context, System.currentTimeMillis());
GV2UpdateDescription updateDescription = GroupProtoUtil.createOutgoingGroupV2UpdateDescription(masterKey, new GroupMutation(decryptedGroup, simulatedGroupChange, simulatedGroupState), null);
OutgoingMessage leaveMessage = OutgoingMessage.groupUpdateMessage(groupRecipient, updateDescription, System.currentTimeMillis());
try {
MessageTable mmsDatabase = SignalDatabase.messages();
@ -803,13 +806,18 @@ public class GroupsV2StateProcessor {
boolean outgoing = !editor.isPresent() || aci.equals(editor.get());
GV2UpdateDescription updateDescription = new GV2UpdateDescription.Builder()
.gv2ChangeDescription(decryptedGroupV2Context)
.groupChangeUpdate(GroupsV2UpdateMessageConverter.translateDecryptedChange(SignalStore.account().getServiceIds(), decryptedGroupV2Context))
.build();
if (outgoing) {
try {
MessageTable mmsDatabase = SignalDatabase.messages();
ThreadTable threadTable = SignalDatabase.threads();
RecipientId recipientId = recipientTable.getOrInsertFromGroupId(groupId);
Recipient recipient = Recipient.resolved(recipientId);
OutgoingMessage outgoingMessage = OutgoingMessage.groupUpdateMessage(recipient, decryptedGroupV2Context, timestamp);
OutgoingMessage outgoingMessage = OutgoingMessage.groupUpdateMessage(recipient, updateDescription, timestamp);
long threadId = threadTable.getOrCreateThreadIdFor(recipient);
long messageId = mmsDatabase.insertMessageOutbox(outgoingMessage, threadId, false, null);

View file

@ -8,7 +8,9 @@ import org.thoughtcrime.securesms.database.model.ParentStoryId
import org.thoughtcrime.securesms.database.model.StoryType
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList
import org.thoughtcrime.securesms.database.model.databaseprotos.DecryptedGroupV2Context
import org.thoughtcrime.securesms.database.model.databaseprotos.GV2UpdateDescription
import org.thoughtcrime.securesms.database.model.databaseprotos.GiftBadge
import org.thoughtcrime.securesms.database.model.databaseprotos.MessageExtras
import org.thoughtcrime.securesms.groups.GroupId
import org.thoughtcrime.securesms.linkpreview.LinkPreview
import org.thoughtcrime.securesms.recipients.RecipientId
@ -36,7 +38,8 @@ class IncomingMessage(
sharedContacts: List<Contact> = emptyList(),
linkPreviews: List<LinkPreview> = emptyList(),
mentions: List<Mention> = emptyList(),
val giftBadge: GiftBadge? = null
val giftBadge: GiftBadge? = null,
val messageExtras: MessageExtras? = null
) {
val attachments: List<Attachment> = ArrayList(attachments)
@ -104,9 +107,8 @@ class IncomingMessage(
serverTimeMillis = timestamp,
groupId = groupId,
groupContext = messageGroupContext,
serverGuid = serverGuid,
body = messageGroupContext.encodedGroupContext,
type = MessageType.GROUP_UPDATE
type = MessageType.GROUP_UPDATE,
messageExtras = MessageExtras(gv2UpdateDescription = GV2UpdateDescription(gv2ChangeDescription = groupContext, groupChangeUpdate = null))
)
}
}

View file

@ -11,6 +11,7 @@ import org.signal.storageservice.protos.groups.local.DecryptedGroup;
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
import org.signal.storageservice.protos.groups.local.DecryptedMember;
import org.thoughtcrime.securesms.database.model.databaseprotos.DecryptedGroupV2Context;
import org.thoughtcrime.securesms.database.model.databaseprotos.MessageExtras;
import org.thoughtcrime.securesms.messages.SignalServiceProtoUtil;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
@ -30,7 +31,6 @@ import java.util.List;
*/
public final class MessageGroupContext {
@NonNull private final String encodedGroupContext;
@NonNull private final GroupProperties group;
@Nullable private final GroupV1Properties groupV1;
@Nullable private final GroupV2Properties groupV2;
@ -38,7 +38,6 @@ public final class MessageGroupContext {
public MessageGroupContext(@NonNull String encodedGroupContext, boolean v2)
throws IOException
{
this.encodedGroupContext = encodedGroupContext;
if (v2) {
this.groupV1 = null;
this.groupV2 = new GroupV2Properties(DecryptedGroupV2Context.ADAPTER.decode(Base64.decode(encodedGroupContext)));
@ -50,15 +49,25 @@ public final class MessageGroupContext {
}
}
public MessageGroupContext(@NonNull MessageExtras messageExtras, boolean v2) {
if (v2) {
this.groupV1 = null;
this.groupV2 = new GroupV2Properties(messageExtras.gv2UpdateDescription.gv2ChangeDescription);
this.group = groupV2;
} else {
this.groupV1 = new GroupV1Properties(messageExtras.gv1Context);
this.groupV2 = null;
this.group = groupV1;
}
}
public MessageGroupContext(@NonNull GroupContext group) {
this.encodedGroupContext = Base64.encodeWithPadding(group.encode());
this.groupV1 = new GroupV1Properties(group);
this.groupV2 = null;
this.group = groupV1;
}
public MessageGroupContext(@NonNull DecryptedGroupV2Context group) {
this.encodedGroupContext = Base64.encodeWithPadding(group.encode());
this.groupV1 = null;
this.groupV2 = new GroupV2Properties(group);
this.group = groupV2;
@ -82,10 +91,6 @@ public final class MessageGroupContext {
return groupV2 != null;
}
public @NonNull String getEncodedGroupContext() {
return encodedGroupContext;
}
public String getName() {
return group.getName();
}

View file

@ -9,8 +9,9 @@ import org.thoughtcrime.securesms.database.model.Mention
import org.thoughtcrime.securesms.database.model.ParentStoryId
import org.thoughtcrime.securesms.database.model.StoryType
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList
import org.thoughtcrime.securesms.database.model.databaseprotos.DecryptedGroupV2Context
import org.thoughtcrime.securesms.database.model.databaseprotos.GV2UpdateDescription
import org.thoughtcrime.securesms.database.model.databaseprotos.GiftBadge
import org.thoughtcrime.securesms.database.model.databaseprotos.MessageExtras
import org.thoughtcrime.securesms.linkpreview.LinkPreview
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.sms.GroupV2UpdateMessageUtil
@ -52,7 +53,8 @@ data class OutgoingMessage(
val scheduledDate: Long = -1,
val messageToEdit: Long = 0,
val isReportSpam: Boolean = false,
val isMessageRequestAccept: Boolean = false
val isMessageRequestAccept: Boolean = false,
val messageExtras: MessageExtras? = null
) {
val isV2Group: Boolean = messageGroupContext != null && GroupV2UpdateMessageUtil.isGroupV2(messageGroupContext)
@ -228,17 +230,18 @@ data class OutgoingMessage(
* Helper for creating a group update message when a state change occurs and needs to be sent to others.
*/
@JvmStatic
fun groupUpdateMessage(threadRecipient: Recipient, group: DecryptedGroupV2Context, sentTimeMillis: Long): OutgoingMessage {
val groupContext = MessageGroupContext(group)
fun groupUpdateMessage(threadRecipient: Recipient, update: GV2UpdateDescription, sentTimeMillis: Long): OutgoingMessage {
val messageExtras = MessageExtras(gv2UpdateDescription = update)
val groupContext = MessageGroupContext(update.gv2ChangeDescription!!)
return OutgoingMessage(
threadRecipient = threadRecipient,
body = groupContext.encodedGroupContext,
sentTimeMillis = sentTimeMillis,
messageGroupContext = groupContext,
isGroup = true,
isGroupUpdate = true,
isSecure = true
isSecure = true,
messageExtras = messageExtras
)
}
@ -260,7 +263,6 @@ data class OutgoingMessage(
): OutgoingMessage {
return OutgoingMessage(
threadRecipient = threadRecipient,
body = groupContext.encodedGroupContext,
isGroup = true,
isGroupUpdate = true,
messageGroupContext = groupContext,

View file

@ -52,7 +52,7 @@ message AccountData {
bool linkPreviews = 5;
bool notDiscoverableByPhoneNumber = 6;
bool preferContactAvatars = 7;
uint32 universalExpireTimer = 8;
uint32 universalExpireTimer = 8; // 0 means no universal expire timer.
repeated string preferredReactionEmoji = 9;
bool displayBadgesOnProfile = 10;
bool keepMutedChatsArchived = 11;
@ -132,7 +132,7 @@ message Chat {
uint64 recipientId = 2;
bool archived = 3;
uint32 pinnedOrder = 4; // 0 = unpinned, otherwise chat is considered pinned and will be displayed in ascending order
uint64 expirationTimerMs = 5;
uint64 expirationTimerMs = 5; // 0 = no expire timer.
uint64 muteUntilMs = 6;
bool markedUnread = 7;
bool dontNotifyForMentionsIfMuted = 8;
@ -537,8 +537,10 @@ message SimpleChatUpdate {
Type type = 1;
}
// For 1:1 chat updates only.
// For group thread updates use GroupExpirationTimerUpdate.
message ExpirationTimerChatUpdate {
uint32 expiresInMs = 1;
uint32 expiresInMs = 1; // 0 means the expiration timer was disabled
}
message ProfileChangeChatUpdate {
@ -591,6 +593,7 @@ message GroupChangeChatUpdate {
GroupV2MigrationInvitedMembersUpdate groupV2MigrationInvitedMembersUpdate = 31;
GroupV2MigrationDroppedMembersUpdate groupV2MigrationDroppedMembersUpdate = 32;
GroupSequenceOfRequestsAndCancelsUpdate groupSequenceOfRequestsAndCancelsUpdate = 33;
GroupExpirationTimerUpdate groupExpirationTimerUpdate = 34;
}
}
@ -794,6 +797,12 @@ message GroupV2MigrationDroppedMembersUpdate {
int32 droppedMembersCount = 1;
}
// For 1:1 timer updates, use ExpirationTimerChatUpdate.
message GroupExpirationTimerUpdate {
uint32 expiresInMs = 1; // 0 means the expiration timer was disabled
optional bytes updaterAci = 2;
}
message StickerPack {
bytes id = 1;
bytes key = 2;

View file

@ -376,7 +376,10 @@ message ExternalLaunchTransactionState {
}
message MessageExtras {
GV2UpdateDescription gv2UpdateDescription = 1;
oneof extra {
GV2UpdateDescription gv2UpdateDescription = 1;
signalservice.GroupContext gv1Context = 2;
}
}
message GV2UpdateDescription {

View file

@ -24,6 +24,8 @@ import org.signal.storageservice.protos.groups.local.DecryptedGroup;
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
import org.signal.storageservice.protos.groups.local.DecryptedMember;
import org.signal.storageservice.protos.groups.local.DecryptedPendingMember;
import org.thoughtcrime.securesms.backup.v2.proto.GroupChangeChatUpdate;
import org.thoughtcrime.securesms.database.model.databaseprotos.DecryptedGroupV2Context;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.whispersystems.signalservice.api.push.ServiceId;
@ -34,6 +36,7 @@ import org.whispersystems.signalservice.api.push.ServiceIds;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.ListIterator;
import java.util.UUID;
import java.util.stream.Collectors;
@ -62,6 +65,8 @@ public final class GroupsV2UpdateMessageProducerTest {
private ACI alice;
private ACI bob;
private ServiceIds selfIds;
private GroupsV2UpdateMessageProducer producer;
@Rule
@ -79,6 +84,8 @@ public final class GroupsV2UpdateMessageProducerTest {
alice = ACI.from(UUID.randomUUID());
bob = ACI.from(UUID.randomUUID());
selfIds = new ServiceIds(you, PNI.from(UUID.randomUUID()));
recipientIdMockedStatic.when(() -> RecipientId.from(anyLong())).thenCallRealMethod();
RecipientId aliceId = RecipientId.from(1);
@ -87,7 +94,7 @@ public final class GroupsV2UpdateMessageProducerTest {
Recipient aliceRecipient = recipientWithName(aliceId, "Alice");
Recipient bobRecipient = recipientWithName(bobId, "Bob");
producer = new GroupsV2UpdateMessageProducer(ApplicationProvider.getApplicationContext(), new ServiceIds(you, PNI.from(UUID.randomUUID())), null);
producer = new GroupsV2UpdateMessageProducer(ApplicationProvider.getApplicationContext(), selfIds, null);
recipientIdMockedStatic.when(() -> RecipientId.from(alice)).thenReturn(aliceId);
recipientIdMockedStatic.when(() -> RecipientId.from(bob)).thenReturn(bobId);
@ -1422,6 +1429,27 @@ public final class GroupsV2UpdateMessageProducerTest {
assertEquals("Alice said hello to Bob, and Bob said hello back to Alice.", result.toString());
}
private @NonNull String describeConvertedNewGroup(@NonNull DecryptedGroup groupState, @NonNull DecryptedGroupChange groupChange) {
GroupChangeChatUpdate update = GroupsV2UpdateMessageConverter.translateDecryptedChangeNewGroup(selfIds, new DecryptedGroupV2Context.Builder()
.change(groupChange)
.groupState(groupState)
.build());
return producer.describeChanges(update.updates).get(0).getSpannable().toString();
}
private @NonNull List<String> describeConvertedChange(@Nullable DecryptedGroup previousGroupState, @NonNull DecryptedGroupChange change) {
GroupChangeChatUpdate update = GroupsV2UpdateMessageConverter.translateDecryptedChangeUpdate(selfIds, new DecryptedGroupV2Context.Builder()
.change(change)
.previousGroupState(previousGroupState)
.build());
return Stream.of(producer.describeChanges(update.updates))
.map(UpdateDescription::getSpannable)
.map(Spannable::toString)
.toList();
}
private @NonNull List<String> describeChange(@NonNull DecryptedGroupChange change) {
return describeChange(null, change);
}
@ -1429,10 +1457,20 @@ public final class GroupsV2UpdateMessageProducerTest {
private @NonNull List<String> describeChange(@Nullable DecryptedGroup previousGroupState,
@NonNull DecryptedGroupChange change)
{
return Stream.of(producer.describeChanges(previousGroupState, change))
.map(UpdateDescription::getSpannable)
.map(Spannable::toString)
.toList();
List<String> convertedChange = describeConvertedChange(previousGroupState, change);
List<String> describedChange = Stream.of(producer.describeChanges(previousGroupState, change))
.map(UpdateDescription::getSpannable)
.map(Spannable::toString)
.toList();
assertEquals(describedChange.size(), convertedChange.size());
ListIterator<String> convertedIterator = convertedChange.listIterator();
ListIterator<String> describedIterator = describedChange.listIterator();
while (convertedIterator.hasNext()) {
assertEquals(describedIterator.next(), convertedIterator.next());
}
return describedChange;
}
private @NonNull String describeNewGroup(@NonNull DecryptedGroup group) {
@ -1440,7 +1478,12 @@ public final class GroupsV2UpdateMessageProducerTest {
}
private @NonNull String describeNewGroup(@NonNull DecryptedGroup group, @NonNull DecryptedGroupChange groupChange) {
return producer.describeNewGroup(group, groupChange).getSpannable().toString();
String newGroupString = producer.describeNewGroup(group, groupChange).getSpannable().toString();
String convertedGroupString = describeConvertedNewGroup(group, groupChange);
assertEquals(newGroupString, convertedGroupString);
return newGroupString;
}
private static GroupStateBuilder newGroupBy(ACI foundingMember, int revision) {