Signal-iOS/SignalServiceKit/src/groups/GroupUpdateInfoMessageInserter+FoldIntoExistingMessage.swift
2023-08-14 14:05:49 -05:00

249 lines
9.9 KiB
Swift

//
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import Foundation
import LibSignalClient
extension GroupUpdateInfoMessageInserterImpl {
/// Represents the result of collapsing updates into existing messages.
enum CollapsibleMembershipChangeResult {
/// Membership changes were collapsed into an existing, now-updated,
/// info message.
case updatesCollapsedIntoExistingMessage
/// Update messages pertaining to the membership change are available
/// and a new info message should be inserted containing them. Existing
/// messages may have been updated while computing these updates.
case updateMessageForNewMessage(TSInfoMessage.UpdateMessage)
}
func handlePossiblyCollapsibleMembershipChange(
precomputedUpdateType: PrecomputedUpdateType,
localAci: Aci,
groupThread: TSGroupThread,
oldGroupModel: TSGroupModel,
newGroupModel: TSGroupModel,
oldDisappearingMessageToken: DisappearingMessageToken,
newDisappearingMessageToken: DisappearingMessageToken,
transaction: SDSAnyWriteTransaction
) -> CollapsibleMembershipChangeResult? {
guard
let (mostRecentInfoMsg, secondMostRecentInfoMsgMaybe) = mostRecentVisibleInteractionsAsInfoMessages(
forGroupThread: groupThread,
withTransaction: transaction
)
else {
return nil
}
owsAssertDebug(mostRecentInfoMsg.newGroupModel == oldGroupModel)
switch precomputedUpdateType {
case .newJoinRequestFromSingleUser(let requestingAci):
guard localAci != requestingAci else {
return nil
}
return maybeUpdate(
mostRecentInfoMsg: mostRecentInfoMsg,
withNewJoinRequestFrom: requestingAci,
transaction: transaction
)
case .canceledJoinRequestFromSingleUser(let cancelingAci):
guard localAci != cancelingAci else {
return nil
}
return maybeUpdate(
mostRecentInfoMsg: mostRecentInfoMsg,
andSecondMostRecentInfoMsg: secondMostRecentInfoMsgMaybe,
withCanceledJoinRequestFrom: cancelingAci,
newGroupModel: newGroupModel,
transaction: transaction
)
case .bannedMemberChange:
// If we know only banned members changed we don't want to make a
// new info message, and should simply update the most recent info
// message with the new group model so it accurately reflects the
// latest group state, i.e. is aware of the now-banned members.
mostRecentInfoMsg.setNewGroupModel(newGroupModel)
mostRecentInfoMsg.anyUpsert(transaction: transaction)
return .updatesCollapsedIntoExistingMessage
case
.invitedPnisPromotedToFullMemberAcis,
.invitesRemoved:
owsFail("Should never get here with a non-collapsible group update type!")
}
}
private func maybeUpdate(
mostRecentInfoMsg: TSInfoMessage,
withNewJoinRequestFrom requestingAci: Aci,
transaction: SDSAnyWriteTransaction
) -> CollapsibleMembershipChangeResult? {
// For a new join request we always want a new info message. However,
// if the new request matches collapsed request/cancel events on the
// most recent message we should make a note on the new message that
// it is no longer the tail of the sequence.
//
// Note that the new message might get collapsed further (into the
// most recent message) in the future.
guard
let mostRecentUpdateMessage = mostRecentInfoMsg.updateMessages?.asSingleMessage,
case let .sequenceOfInviteLinkRequestAndCancels(count, isTail) = mostRecentUpdateMessage,
requestingAci == mostRecentInfoMsg.groupUpdateSourceAddress?.serviceId
else {
return nil
}
owsAssertDebug(isTail)
mostRecentInfoMsg.setSingleUpdateMessage(
singleUpdateMessage: .sequenceOfInviteLinkRequestAndCancels(count: count, isTail: false)
)
mostRecentInfoMsg.anyUpsert(transaction: transaction)
return .updateMessageForNewMessage(
.sequenceOfInviteLinkRequestAndCancels(count: 0, isTail: true)
)
}
private func maybeUpdate(
mostRecentInfoMsg: TSInfoMessage,
andSecondMostRecentInfoMsg secondMostRecentInfoMsg: TSInfoMessage?,
withCanceledJoinRequestFrom cancelingAci: Aci,
newGroupModel: TSGroupModel,
transaction: SDSAnyWriteTransaction
) -> CollapsibleMembershipChangeResult? {
// If the most recent message represents the join request that's being
// canceled, we want to collapse into it.
//
// Further, if the second-most-recent message represents already-
// collapsed join/cancel events from the same address, we can simply
// increment that message's collapse counter and delete the most recent
// message.
guard
let mostRecentInfoMsgJoiner = mostRecentInfoMsg.representsSingleRequestToJoin(),
cancelingAci == mostRecentInfoMsgJoiner
else {
return nil
}
if
let secondMostRecentInfoMsg = secondMostRecentInfoMsg,
let updateMessage = secondMostRecentInfoMsg.updateMessages?.asSingleMessage,
case let .sequenceOfInviteLinkRequestAndCancels(count, isTail) = updateMessage,
cancelingAci == secondMostRecentInfoMsg.groupUpdateSourceAddress?.serviceId
{
switch mostRecentInfoMsg.updateMessages?.asSingleMessage {
case .sequenceOfInviteLinkRequestAndCancels(0, true)?: break
default: owsFailDebug("Unexpected state for most recent info message")
}
mostRecentInfoMsg.anyRemove(transaction: transaction)
owsAssertDebug(!isTail)
secondMostRecentInfoMsg.setNewGroupModel(newGroupModel)
secondMostRecentInfoMsg.setSingleUpdateMessage(
singleUpdateMessage: .sequenceOfInviteLinkRequestAndCancels(count: count + 1, isTail: true)
)
secondMostRecentInfoMsg.anyUpsert(transaction: transaction)
return .updatesCollapsedIntoExistingMessage
} else {
mostRecentInfoMsg.setNewGroupModel(newGroupModel)
mostRecentInfoMsg.setSingleUpdateMessage(
singleUpdateMessage: .sequenceOfInviteLinkRequestAndCancels(count: 1, isTail: true)
)
mostRecentInfoMsg.anyUpsert(transaction: transaction)
return .updatesCollapsedIntoExistingMessage
}
}
private func mostRecentVisibleInteractionsAsInfoMessages(
forGroupThread groupThread: TSGroupThread,
withTransaction transaction: SDSAnyReadTransaction
) -> (first: TSInfoMessage, second: TSInfoMessage?)? {
var mostRecentVisibleInteraction: TSInteraction?
var secondMostRecentVisibleInteraction: TSInteraction?
do {
try InteractionFinder(threadUniqueId: groupThread.uniqueId)
.enumerateRecentInteractions(
excludingPlaceholders: !DebugFlags.showFailedDecryptionPlaceholders.get(), // This matches how messages are loaded in MessageLoader
transaction: transaction,
block: { interaction, shouldStop in
if mostRecentVisibleInteraction == nil {
mostRecentVisibleInteraction = interaction
} else if secondMostRecentVisibleInteraction == nil {
secondMostRecentVisibleInteraction = interaction
shouldStop.pointee = true
}
})
} catch let error {
Logger.warn("Failed to get most recent interactions for thread: \(error.localizedDescription)")
return nil
}
guard let mostRecentInfoMessage = mostRecentVisibleInteraction as? TSInfoMessage else {
Logger.debug("Most recent visible interaction not found as info message")
return nil
}
guard let secondMostRecentInfoMessage = secondMostRecentVisibleInteraction as? TSInfoMessage else {
Logger.debug("Second most recent visible interaction not found as info message")
return (mostRecentInfoMessage, nil)
}
return (mostRecentInfoMessage, secondMostRecentInfoMessage)
}
}
// MARK: TSInfoMessage extension
private extension TSInfoMessage.UpdateMessagesWrapper {
var asSingleMessage: TSInfoMessage.UpdateMessage? {
guard updateMessages.count == 1 else {
return nil
}
return updateMessages.first
}
}
private extension TSInfoMessage {
func setSingleUpdateMessage(singleUpdateMessage: UpdateMessage) {
setUpdateMessages(UpdateMessagesWrapper([singleUpdateMessage]))
}
func representsSingleRequestToJoin() -> Aci? {
guard oldDisappearingMessageToken == newDisappearingMessageToken else {
return nil
}
guard
let oldGroupModel = oldGroupModel,
let newGroupModel = newGroupModel,
let groupUpdateSourceAddress = groupUpdateSourceAddress,
let membershipEvent = GroupUpdateInfoMessageInserterImpl.PrecomputedUpdateType.from(
oldGroupMembership: oldGroupModel.groupMembership,
newGroupMembership: newGroupModel.groupMembership,
newlyLearnedPniToAciAssociations: [:]
),
case .newJoinRequestFromSingleUser(let requestingAci) = membershipEvent,
requestingAci == groupUpdateSourceAddress.serviceId
else {
return nil
}
return requestingAci
}
}