Put all necessary metadata on PersistableGroupUpdateItem, never rely on separate metadata

This commit is contained in:
Harry 2024-01-12 05:46:20 -08:00 committed by GitHub
parent 9a9476e925
commit ebab35b4ba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 1792 additions and 499 deletions

View File

@ -896,6 +896,10 @@
667664362A43BBCD00716B84 /* OWSFingerprint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 667664352A43BBCD00716B84 /* OWSFingerprint.swift */; };
667664382A44B6C200716B84 /* OWSFingerprintBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 667664372A44B6C200716B84 /* OWSFingerprintBuilder.swift */; };
66783C2B29CA4F6C00FC4E4E /* RegistrationTransferQRCodeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66783C2A29CA4F6C00FC4E4E /* RegistrationTransferQRCodeViewController.swift */; };
667AF9DA2B48A3F3008AEE5D /* TSInfoMessage+PersistableGroupUpdateItemUpdater.swift in Sources */ = {isa = PBXBuildFile; fileRef = 667AF9D92B48A3F3008AEE5D /* TSInfoMessage+PersistableGroupUpdateItemUpdater.swift */; };
667AF9DE2B4C5824008AEE5D /* PersistableGroupUpdateItem+CVComponentSystemMessageAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 667AF9DD2B4C5824008AEE5D /* PersistableGroupUpdateItem+CVComponentSystemMessageAction.swift */; };
667AF9E02B4C6377008AEE5D /* TSInfoMessage+LegacyPersistablegroupUpdateItemTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 667AF9DF2B4C6377008AEE5D /* TSInfoMessage+LegacyPersistablegroupUpdateItemTest.swift */; };
667AF9E22B4DC5EE008AEE5D /* GroupUpdateSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 667AF9E12B4DC5EE008AEE5D /* GroupUpdateSource.swift */; };
667E90D028E799D1005FE603 /* MyStorySettingsLearnMoreSheetViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 667E90CF28E799D1005FE603 /* MyStorySettingsLearnMoreSheetViewController.swift */; };
667EDE6428F8D6B7001FB487 /* YYAnimatedImage+Duration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 667EDE6328F8D6B7001FB487 /* YYAnimatedImage+Duration.swift */; };
667EDE6628FA0372001FB487 /* StoryBadgeCountManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 667EDE6528FA0372001FB487 /* StoryBadgeCountManager.swift */; };
@ -3541,6 +3545,10 @@
667664352A43BBCD00716B84 /* OWSFingerprint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OWSFingerprint.swift; sourceTree = "<group>"; };
667664372A44B6C200716B84 /* OWSFingerprintBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OWSFingerprintBuilder.swift; sourceTree = "<group>"; };
66783C2A29CA4F6C00FC4E4E /* RegistrationTransferQRCodeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegistrationTransferQRCodeViewController.swift; sourceTree = "<group>"; };
667AF9D92B48A3F3008AEE5D /* TSInfoMessage+PersistableGroupUpdateItemUpdater.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TSInfoMessage+PersistableGroupUpdateItemUpdater.swift"; sourceTree = "<group>"; };
667AF9DD2B4C5824008AEE5D /* PersistableGroupUpdateItem+CVComponentSystemMessageAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PersistableGroupUpdateItem+CVComponentSystemMessageAction.swift"; sourceTree = "<group>"; };
667AF9DF2B4C6377008AEE5D /* TSInfoMessage+LegacyPersistablegroupUpdateItemTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TSInfoMessage+LegacyPersistablegroupUpdateItemTest.swift"; sourceTree = "<group>"; };
667AF9E12B4DC5EE008AEE5D /* GroupUpdateSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupUpdateSource.swift; sourceTree = "<group>"; };
667E90CF28E799D1005FE603 /* MyStorySettingsLearnMoreSheetViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MyStorySettingsLearnMoreSheetViewController.swift; sourceTree = "<group>"; };
667EDE6328F8D6B7001FB487 /* YYAnimatedImage+Duration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "YYAnimatedImage+Duration.swift"; sourceTree = "<group>"; };
667EDE6528FA0372001FB487 /* StoryBadgeCountManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryBadgeCountManager.swift; sourceTree = "<group>"; };
@ -5795,6 +5803,7 @@
347C384C252D0FDC00F3D941 /* CVComponentTypingIndicator.swift */,
347C384A252D085900F3D941 /* CVComponentUnreadIndicator.swift */,
348815BF2553291200D4F4C4 /* CVComponentViewOnce.swift */,
667AF9DD2B4C5824008AEE5D /* PersistableGroupUpdateItem+CVComponentSystemMessageAction.swift */,
);
path = CVComponents;
sourceTree = "<group>";
@ -9097,6 +9106,7 @@
isa = PBXGroup;
children = (
D92C57542A2925AD00A03BB7 /* TSInfoMessage+DisplayableGroupUpdateItemTest.swift */,
667AF9DF2B4C6377008AEE5D /* TSInfoMessage+LegacyPersistablegroupUpdateItemTest.swift */,
D9CD40612A155C4800545803 /* TSInfoMessage+PersistableGroupUpdateItemTest.swift */,
F9426221289B1B5500460798 /* TSMessageTest.swift */,
F9426220289B1B5500460798 /* TSOutgoingMessageTest.swift */,
@ -9449,6 +9459,7 @@
F9C5C8D8289453B100548EEE /* Interactions */ = {
isa = PBXGroup;
children = (
667AF9E12B4DC5EE008AEE5D /* GroupUpdateSource.swift */,
50468F2829EE130A00948E02 /* InteractionStore.swift */,
F9C5C8EB289453B100548EEE /* MentionFinder.swift */,
763A15FF2AEC3A490081D7E5 /* OWSContact.swift */,
@ -9479,6 +9490,7 @@
D9B0AC7329EF42960070F31C /* TSInfoMessage+DisplayableGroupUpdateItem.swift */,
F9C5C8F3289453B100548EEE /* TSInfoMessage+DisplayableGroupUpdateItemBuilder.swift */,
F9A042C5289C7468007D08B6 /* TSInfoMessage+PersistableGroupUpdateItem.swift */,
667AF9D92B48A3F3008AEE5D /* TSInfoMessage+PersistableGroupUpdateItemUpdater.swift */,
F9C5C8FA289453B100548EEE /* TSInfoMessage+ProfileChanges.swift */,
F9C5C8EE289453B100548EEE /* TSInfoMessage+SDS.swift */,
F9C5C8E2289453B100548EEE /* TSInfoMessage+Swift.swift */,
@ -12443,6 +12455,7 @@
3495FF0F25F9538900959D6E /* PaymentsViewPassphraseSplashViewController.swift in Sources */,
34FB6A5325D2D10400E599B1 /* PaymentsViewUtils.swift in Sources */,
D96BE42E292EF04200E4FE1A /* PaypalButton.swift in Sources */,
667AF9DE2B4C5824008AEE5D /* PersistableGroupUpdateItem+CVComponentSystemMessageAction.swift in Sources */,
C176B48A299DA25500B1900D /* PhoneNumberPrivacySettingsViewController.swift in Sources */,
4C5250D221E7BD7D00CE3D95 /* PhoneNumberValidator.swift in Sources */,
3496955D219B605E00DCFE74 /* PhotoAlbumPickerViewController.swift in Sources */,
@ -12883,6 +12896,7 @@
D99A0F6229F35CE1002E02E3 /* GroupUpdateInfoMessageInserter+FoldIntoExistingMessage.swift in Sources */,
D9FC835029F71CC300878BC8 /* GroupUpdateInfoMessageInserter+PrecomputedUpdateType.swift in Sources */,
D99A0F6029F34FDA002E02E3 /* GroupUpdateInfoMessageInserter.swift in Sources */,
667AF9E22B4DC5EE008AEE5D /* GroupUpdateSource.swift in Sources */,
F9C5CD94289453B300548EEE /* HTMLMetadata.swift in Sources */,
F9C5CDC2289453B400548EEE /* HTTPEntities.swift in Sources */,
F9C5CDBC289453B400548EEE /* HTTPUtils.m in Sources */,
@ -13435,6 +13449,7 @@
D9B0AC7429EF42960070F31C /* TSInfoMessage+DisplayableGroupUpdateItem.swift in Sources */,
F9C5CBE7289453B300548EEE /* TSInfoMessage+DisplayableGroupUpdateItemBuilder.swift in Sources */,
F9A042C6289C7468007D08B6 /* TSInfoMessage+PersistableGroupUpdateItem.swift in Sources */,
667AF9DA2B48A3F3008AEE5D /* TSInfoMessage+PersistableGroupUpdateItemUpdater.swift in Sources */,
F9C5CBEE289453B300548EEE /* TSInfoMessage+ProfileChanges.swift in Sources */,
F9C5CBE2289453B300548EEE /* TSInfoMessage+SDS.swift in Sources */,
F9C5CBD6289453B300548EEE /* TSInfoMessage+Swift.swift in Sources */,
@ -13678,6 +13693,7 @@
F908AA8028CE7F8D00472E68 /* TSGroupThreadTest.swift in Sources */,
D92C57552A2925AD00A03BB7 /* TSInfoMessage+DisplayableGroupUpdateItemTest.swift in Sources */,
D9AD1D9528B9955C00B42E6F /* TSInfoMessage+GroupUpdateType+NSAttributedStringTest.swift in Sources */,
667AF9E02B4C6377008AEE5D /* TSInfoMessage+LegacyPersistablegroupUpdateItemTest.swift in Sources */,
D9CD40622A155C4800545803 /* TSInfoMessage+PersistableGroupUpdateItemTest.swift in Sources */,
F9426258289B1B5500460798 /* TSMessageStorageTests.m in Sources */,
F942628A289B1B5600460798 /* TSMessageTest.swift in Sources */,

View File

@ -1146,125 +1146,44 @@ extension CVComponentSystemMessage {
owsFailDebug("TSInfoMessageAddGroupToProfileWhitelistOffer")
return nil
case .typeGroupUpdate:
guard let newGroupModel = infoMessage.newGroupModel else {
return nil
}
guard
let displayableGroupUpdateItems = infoMessage.displayableGroupUpdateItems(tx: transaction),
!displayableGroupUpdateItems.isEmpty
else {
return nil
}
for updateItem in displayableGroupUpdateItems {
switch updateItem {
case .inviteFriendsToNewlyCreatedGroup:
return Action(
title: OWSLocalizedString(
"GROUPS_INVITE_FRIENDS_BUTTON",
comment: "Label for 'invite friends to group' button."
),
accessibilityIdentifier: "group_invite_friends",
action: .didTapGroupInviteLinkPromotion(groupModel: newGroupModel)
)
case .wasMigrated:
return Action(
title: CommonStrings.learnMore,
accessibilityIdentifier: "group_migration_learn_more",
action: .didTapGroupMigrationLearnMore
)
case
.descriptionChangedByLocalUser(let newGroupDescription),
.descriptionChangedByOtherUser(let newGroupDescription, _, _),
.descriptionChangedByUnknownUser(let newGroupDescription):
return Action(
title: CommonStrings.viewButton,
accessibilityIdentifier: "group_description_view",
action: .didTapViewGroupDescription(newGroupDescription: newGroupDescription)
)
case let .sequenceOfInviteLinkRequestAndCancels(_, _, _, isTail):
guard isTail else { return nil }
guard
let requesterAddress = infoMessage.groupUpdateSourceAddress,
let requesterAci = requesterAddress.serviceId as? Aci
else {
owsFailDebug("Missing parameters for join request sequence")
return nil
}
guard
let mostRecentGroupModel = TSGroupThread.fetch(
groupId: newGroupModel.groupId,
transaction: transaction
)?.groupModel as? TSGroupModelV2
else {
owsFailDebug("Missing group thread for join request sequence")
return nil
}
// Only show the option to ban if we are an admin, and they are
// not already banned. We want to use the most up-to-date group
// model here instead of the one on the info message, since
// group state may have changed since that message.
guard
mostRecentGroupModel.groupMembership.isLocalUserFullMemberAndAdministrator,
!mostRecentGroupModel.groupMembership.isBannedMember(requesterAci)
else {
return nil
}
return Action(
title: OWSLocalizedString(
"GROUPS_BLOCK_REQUEST_BUTTON",
comment: "Label for button that lets the user block a request to join the group."
),
accessibilityIdentifier: "block_join_request_button",
action: .didTapBlockRequest(
groupModel: mostRecentGroupModel,
requesterName: contactsManager.shortDisplayName(
for: requesterAddress,
transaction: transaction
),
requesterAci: requesterAci
)
)
default:
break
}
}
guard let oldGroupModel = infoMessage.oldGroupModel else {
return nil
}
let newlyRequestingMembers = newGroupModel.groupMembership.requestingMembers
.subtracting(oldGroupModel.groupMembership.requestingMembers)
guard !newlyRequestingMembers.isEmpty else {
return nil
}
let title: String = {
if newlyRequestingMembers.count > 1 {
return OWSLocalizedString(
"GROUPS_VIEW_REQUESTS_BUTTON",
comment: "Label for button that lets the user view the requests to join the group."
)
} else {
return OWSLocalizedString(
"GROUPS_VIEW_REQUEST_BUTTON",
comment: "Label for button that lets the user view the request to join the group."
)
}
}()
return Action(
title: title,
accessibilityIdentifier: "show_group_requests_button",
action: .didTapShowConversationSettingsAndShowMemberRequests
let localIdentifiers = DependenciesBridge.shared.tsAccountManager.localIdentifiers(
tx: transaction.asV2Read
)
switch infoMessage.groupUpdateMetadata(localIdentifiers: localIdentifiers) {
case .legacyRawString, .nonGroupUpdate:
return nil
case .precomputed(let precomputedItems):
let thread = { infoMessage.thread(tx: transaction) as? TSGroupThread }
return precomputedItems.updateItems
.lazy
.compactMap {
$0.cvComponentAction(
groupThread: thread,
contactsManager: contactsManager,
tx: transaction
)
}
.first
case .newGroup(let newGroupModel, let updateMetadata):
return action(
forOldGroupModel: nil,
newGroupModel: newGroupModel.groupModel,
infoMessage: infoMessage,
changeAuthor: updateMetadata.source,
tx: transaction
)
case .modelDiff(let oldGroupModel, let newGroupModel, let updateMetadata):
return action(
forOldGroupModel: oldGroupModel.groupModel,
newGroupModel: newGroupModel.groupModel,
infoMessage: infoMessage,
changeAuthor: updateMetadata.source,
tx: transaction
)
}
case .typeGroupQuit:
return nil
case .unknownProtocolVersion:
@ -1402,6 +1321,102 @@ extension CVComponentSystemMessage {
}
}
private static func action(
forOldGroupModel oldGroupModel: TSGroupModel?,
newGroupModel: TSGroupModel,
infoMessage: TSInfoMessage,
changeAuthor: GroupUpdateSource,
tx: SDSAnyReadTransaction
) -> Action? {
guard
let displayableGroupUpdateItems = infoMessage.displayableGroupUpdateItems(tx: tx),
!displayableGroupUpdateItems.isEmpty
else {
return nil
}
for updateItem in displayableGroupUpdateItems {
switch updateItem {
case .inviteFriendsToNewlyCreatedGroup:
return Action(
title: OWSLocalizedString(
"GROUPS_INVITE_FRIENDS_BUTTON",
comment: "Label for 'invite friends to group' button."
),
accessibilityIdentifier: "group_invite_friends",
action: .didTapGroupInviteLinkPromotion(groupModel: newGroupModel)
)
case .wasMigrated:
return Action(
title: CommonStrings.learnMore,
accessibilityIdentifier: "group_migration_learn_more",
action: .didTapGroupMigrationLearnMore
)
case
.descriptionChangedByLocalUser(let newGroupDescription),
.descriptionChangedByOtherUser(let newGroupDescription, _, _),
.descriptionChangedByUnknownUser(let newGroupDescription):
return Action(
title: CommonStrings.viewButton,
accessibilityIdentifier: "group_description_view",
action: .didTapViewGroupDescription(newGroupDescription: newGroupDescription)
)
case let .sequenceOfInviteLinkRequestAndCancels(_, _, _, isTail):
switch changeAuthor {
case .unknown, .legacyE164, .rejectedInviteToPni:
owsFailDebug("Missing parameters for join request sequence")
return nil
case .aci(let requesterAci):
return .sequenceOfInviteLinkRequestAndCancelsAction(
requester: requesterAci,
isTail: isTail,
groupThread: {
TSGroupThread.fetch(
groupId: newGroupModel.groupId,
transaction: tx
)
},
contactsManager: contactsManager,
tx: tx
)
}
default:
break
}
}
guard let oldGroupModel else {
return nil
}
let newlyRequestingMembers = newGroupModel.groupMembership.requestingMembers
.subtracting(oldGroupModel.groupMembership.requestingMembers)
guard !newlyRequestingMembers.isEmpty else {
return nil
}
let title: String = {
if newlyRequestingMembers.count > 1 {
return OWSLocalizedString(
"GROUPS_VIEW_REQUESTS_BUTTON",
comment: "Label for button that lets the user view the requests to join the group."
)
} else {
return OWSLocalizedString(
"GROUPS_VIEW_REQUEST_BUTTON",
comment: "Label for button that lets the user view the request to join the group."
)
}
}()
return Action(
title: title,
accessibilityIdentifier: "show_group_requests_button",
action: .didTapShowConversationSettingsAndShowMemberRequests
)
}
private static func action(forCall call: TSCall,
thread: TSThread,
transaction: SDSAnyReadTransaction) -> Action? {

View File

@ -0,0 +1,103 @@
//
// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import Foundation
import LibSignalClient
import SignalServiceKit
extension TSInfoMessage.PersistableGroupUpdateItem {
func cvComponentAction(
groupThread: () -> TSGroupThread?,
contactsManager: ContactsManagerProtocol,
tx: SDSAnyReadTransaction
) -> CVComponentSystemMessage.Action? {
switch self {
case let .sequenceOfInviteLinkRequestAndCancels(requester, _, isTail):
return .sequenceOfInviteLinkRequestAndCancelsAction(
requester: requester.wrappedValue,
isTail: isTail,
groupThread: groupThread,
contactsManager: contactsManager,
tx: tx
)
case .invitedPniPromotedToFullMemberAci:
return nil
case .localUserDeclinedInviteFromInviter:
return nil
case .localUserDeclinedInviteFromUnknownUser:
return nil
case .otherUserDeclinedInviteFromLocalUser:
return nil
case .otherUserDeclinedInviteFromInviter:
return nil
case .otherUserDeclinedInviteFromUnknownUser:
return nil
case .localUserInviteRevoked:
return nil
case .localUserInviteRevokedByUnknownUser:
return nil
case .otherUserInviteRevokedByLocalUser:
return nil
case .unnamedUserInvitesWereRevokedByLocalUser:
return nil
case .unnamedUserInvitesWereRevokedByOtherUser:
return nil
case .unnamedUserInvitesWereRevokedByUnknownUser:
return nil
case .unnamedUserDeclinedInviteFromInviter:
return nil
case .unnamedUserDeclinedInviteFromUnknownUser:
return nil
}
}
}
extension CVComponentSystemMessage.Action {
static func sequenceOfInviteLinkRequestAndCancelsAction(
requester: Aci,
isTail: Bool,
groupThread: () -> TSGroupThread?,
contactsManager: ContactsManagerProtocol,
tx: SDSAnyReadTransaction
) -> Self? {
guard isTail else { return nil }
guard
let mostRecentGroupModel = groupThread()?.groupModel as? TSGroupModelV2
else {
owsFailDebug("Missing group thread for join request sequence")
return nil
}
// Only show the option to ban if we are an admin, and they are
// not already banned. We want to use the most up-to-date group
// model here instead of the one on the info message, since
// group state may have changed since that message.
guard
mostRecentGroupModel.groupMembership.isLocalUserFullMemberAndAdministrator,
!mostRecentGroupModel.groupMembership.isBannedMember(requester)
else {
return nil
}
return CVComponentSystemMessage.Action(
title: OWSLocalizedString(
"GROUPS_BLOCK_REQUEST_BUTTON",
comment: "Label for button that lets the user block a request to join the group."
),
accessibilityIdentifier: "block_join_request_button",
action: .didTapBlockRequest(
groupModel: mostRecentGroupModel,
requesterName: contactsManager.shortDisplayName(
for: SignalServiceAddress(requester),
transaction: tx
),
requesterAci: requester
)
)
}
}

View File

@ -1031,9 +1031,31 @@ public class NotificationPresenter: NSObject, NotificationsProtocolSwift {
}
if let infoMessage = tsInteraction as? TSInfoMessage {
let localIdentifiers = DependenciesBridge.shared.tsAccountManager.localIdentifiers(
tx: transaction.asV2Read
)
switch infoMessage.messageType {
case .typeGroupUpdate:
if let groupUpdateAuthor = infoMessage.groupUpdateSourceAddress,
let groupUpdateAuthor: SignalServiceAddress?
switch infoMessage.groupUpdateMetadata(localIdentifiers: localIdentifiers) {
case .legacyRawString, .nonGroupUpdate:
groupUpdateAuthor = nil
case .newGroup(_, let updateMetadata), .modelDiff(_, _, let updateMetadata):
switch updateMetadata.source {
case .unknown:
groupUpdateAuthor = nil
case .legacyE164(let e164):
groupUpdateAuthor = .init(e164)
case .aci(let aci):
groupUpdateAuthor = .init(aci)
case .rejectedInviteToPni(let pni):
groupUpdateAuthor = .init(pni)
}
case .precomputed(let persistableGroupUpdateItemsWrapper):
groupUpdateAuthor = persistableGroupUpdateItemsWrapper
.asSingleUpdateItem?.senderForNotification
}
if let groupUpdateAuthor,
let intent = thread.generateSendMessageIntent(context: .senderAddress(groupUpdateAuthor), transaction: transaction) {
wrapIntent(intent)
}

View File

@ -184,12 +184,12 @@ extension GroupV2UpdatesImpl: GroupV2UpdatesSwift {
newGroupModel: changedGroupModel.newGroupModel,
newDisappearingMessageToken: changedGroupModel.newDisappearingMessageToken,
newlyLearnedPniToAciAssociations: changedGroupModel.newlyLearnedPniToAciAssociations,
groupUpdateSource: changedGroupModel.changeAuthor,
groupUpdateSource: changedGroupModel.updateSource,
localIdentifiers: localIdentifiers,
transaction: transaction
).groupThread
let authoritativeProfileKeys = changedGroupModel.profileKeys.filter { $0.key == changedGroupModel.changeAuthor }
let authoritativeProfileKeys = changedGroupModel.profileKeys.filter { $0.key == changedGroupModel.updateSource.serviceId() }
GroupManager.storeProfileKeysFromGroupProtos(
allProfileKeysByAci: changedGroupModel.profileKeys,
authoritativeProfileKeysByAci: authoritativeProfileKeys
@ -692,14 +692,13 @@ private extension GroupV2UpdatesImpl {
if
let applyResult = applyResult,
let changeAuthor = applyResult.changeAuthor,
applyResult.wasLocalUserAddedByChange
{
owsAssertDebug(
localUserWasAddedBy == nil || (index == 0 && localUserWasAddedBy == changeAuthor),
localUserWasAddedBy == .unknown || (index == 0 && localUserWasAddedBy == applyResult.changeAuthor),
"Multiple change actions added the user to the group"
)
localUserWasAddedBy = changeAuthor
localUserWasAddedBy = applyResult.changeAuthor
}
}
@ -708,10 +707,28 @@ private extension GroupV2UpdatesImpl {
authoritativeProfileKeysByAci: authoritativeProfileKeysByAci
)
if
let localUserWasAddedBy = localUserWasAddedBy,
self.blockingManager.isAddressBlocked(SignalServiceAddress(localUserWasAddedBy), transaction: transaction)
{
let localUserWasAddedByBlockedUser: Bool
switch localUserWasAddedBy {
case .unknown:
localUserWasAddedByBlockedUser = false
case .legacyE164(let e164):
localUserWasAddedByBlockedUser = self.blockingManager.isAddressBlocked(
.init(e164),
transaction: transaction
)
case .aci(let aci):
localUserWasAddedByBlockedUser = self.blockingManager.isAddressBlocked(
.init(aci),
transaction: transaction
)
case .rejectedInviteToPni(let pni):
localUserWasAddedByBlockedUser = self.blockingManager.isAddressBlocked(
.init(pni),
transaction: transaction
)
}
if localUserWasAddedByBlockedUser {
// If we have been added to the group by a blocked user, we
// should automatically leave the group. To that end, enqueue
// a leave action after we've finished processing messages.
@ -753,10 +770,10 @@ private extension GroupV2UpdatesImpl {
groupModelOptions: TSGroupModelOptions,
localIdentifiers: LocalIdentifiers,
transaction: SDSAnyWriteTransaction
) -> (TSGroupThread, addedToNewThreadBy: ServiceId?)? {
) -> (TSGroupThread, addedToNewThreadBy: GroupUpdateSource)? {
if let groupThread = TSGroupThread.fetch(groupId: groupId, transaction: transaction) {
return (groupThread, addedToNewThreadBy: nil)
return (groupThread, addedToNewThreadBy: .unknown)
}
do {
@ -801,7 +818,7 @@ private extension GroupV2UpdatesImpl {
return (
result.groupThread,
addedToNewThreadBy: didAddLocalUserToV2Group ? groupUpdateSource : nil
addedToNewThreadBy: didAddLocalUserToV2Group ? groupUpdateSource : .unknown
)
} catch {
owsFailDebug("Error: \(error)")
@ -810,7 +827,7 @@ private extension GroupV2UpdatesImpl {
}
private struct ApplySingleChangeFromServiceResult {
let changeAuthor: ServiceId?
let changeAuthor: GroupUpdateSource
let wasLocalUserAddedByChange: Bool
}
@ -862,7 +879,7 @@ private extension GroupV2UpdatesImpl {
let newDisappearingMessageToken: DisappearingMessageToken?
let newProfileKeys: [Aci: Data]
let newlyLearnedPniToAciAssociations: [Pni: Aci]
let groupUpdateSource: ServiceId?
let groupUpdateSource: GroupUpdateSource
// We should prefer to update models using the change action if we can,
// since it contains information about the change author.
@ -883,7 +900,7 @@ private extension GroupV2UpdatesImpl {
newDisappearingMessageToken = changedGroupModel.newDisappearingMessageToken
newProfileKeys = changedGroupModel.profileKeys
newlyLearnedPniToAciAssociations = changedGroupModel.newlyLearnedPniToAciAssociations
groupUpdateSource = changedGroupModel.changeAuthor
groupUpdateSource = changedGroupModel.updateSource
} else if let snapshot = groupChange.snapshot {
logger.info("Applying snapshot.")
@ -895,7 +912,7 @@ private extension GroupV2UpdatesImpl {
newProfileKeys = snapshot.profileKeys
newlyLearnedPniToAciAssociations = [:]
// Snapshots don't have a single author, so we don't know the source.
groupUpdateSource = nil
groupUpdateSource = .unknown
} else if groupChange.changeActionsProto != nil {
logger.info("Change action proto was not a single revision update.")
@ -945,11 +962,13 @@ private extension GroupV2UpdatesImpl {
transaction: transaction
).groupThread
if
let groupUpdateSourceAci = groupUpdateSource as? Aci,
let groupUpdateProfileKey = newProfileKeys[groupUpdateSourceAci]
{
authoritativeProfileKeysByAci[groupUpdateSourceAci] = groupUpdateProfileKey
switch groupUpdateSource {
case .unknown, .legacyE164, .rejectedInviteToPni:
break
case .aci(let groupUpdateSourceAci):
if let groupUpdateProfileKey = newProfileKeys[groupUpdateSourceAci] {
authoritativeProfileKeysByAci[groupUpdateSourceAci] = groupUpdateProfileKey
}
}
// Merge known profile keys, always taking latest.
@ -1040,9 +1059,9 @@ private extension GroupV2UpdatesImpl {
let newGroupModel = try builder.buildAsV2()
let newDisappearingMessageToken = groupV2Snapshot.disappearingMessageToken
// groupUpdateSourceAddress is nil because we don't know the
// groupUpdateSource is unknown because we don't know the
// author(s) of changes reflected in the snapshot.
let groupUpdateSource: ServiceId? = nil
let groupUpdateSource: GroupUpdateSource = .unknown
let result = try GroupManager.tryToUpsertExistingGroupThreadInDatabaseAndCreateInfoMessage(
newGroupModel: newGroupModel,
newDisappearingMessageToken: newDisappearingMessageToken,
@ -1272,16 +1291,38 @@ extension GroupsV2Error: IsRetryableProvider {
}
private extension GroupV2Change {
func author(groupV2Params: GroupV2Params) throws -> ServiceId? {
func author(groupV2Params: GroupV2Params) throws -> GroupUpdateSource {
if let changeActionsProto = changeActionsProto {
guard let changeAuthorUuidData = changeActionsProto.sourceUuid else {
owsFailDebug("Explicit changes should always have authors")
return nil
}
return try groupV2Params.serviceId(for: changeAuthorUuidData)
return try changeActionsProto.updateSource(groupV2Params: groupV2Params)
}
return .unknown
}
}
public extension GroupsProtoGroupChangeActions {
func updateSource(groupV2Params: GroupV2Params) throws -> GroupUpdateSource {
guard let changeAuthorUuidData = self.sourceUuid else {
owsFailDebug("Explicit changes should always have authors")
return .unknown
}
let serviceId = try groupV2Params.serviceId(for: changeAuthorUuidData)
switch serviceId.concreteType {
case .aci(let aci):
return .aci(aci)
case .pni(let pni):
// As of now, the only update with a pni author is
// declining a pni invite. If this changes, differentiate
// state here and split which enum case this becomes.
// This may not be the BEST place to do that differentiation;
// you may need to pass in new params to be able to tell,
// or even just push this up to the callsite. In any case,
// the time to differentiate is when looking at the group updates
// or before/after model we get from the server.
owsAssertDebug(self.deletePendingMembers.count == 1)
owsAssertDebug(self.deletePendingMembers.first?.deletedUserID == Data(pni.serviceIdBinary))
return .rejectedInviteToPni(pni)
}
return nil
}
}

View File

@ -2042,7 +2042,7 @@ public class GroupsV2Impl: GroupsV2Swift, GroupsV2, Dependencies {
oldDisappearingMessageToken: dmToken,
newDisappearingMessageToken: dmToken,
newlyLearnedPniToAciAssociations: [:],
groupUpdateSource: localIdentifiers.aci,
groupUpdateSource: .aci(localIdentifiers.aci),
localIdentifiers: localIdentifiers,
transaction: transaction
)
@ -2082,15 +2082,12 @@ public class GroupsV2Impl: GroupsV2Swift, GroupsV2, Dependencies {
let dmConfigurationStore = DependenciesBridge.shared.disappearingMessagesConfigurationStore
let dmToken = dmConfigurationStore.fetchOrBuildDefault(for: .thread(groupThread), tx: transaction.asV2Read).asToken
GroupManager.insertGroupUpdateInfoMessage(
groupThread: groupThread,
oldGroupModel: nil,
newGroupModel: groupModel,
oldDisappearingMessageToken: nil,
newDisappearingMessageToken: dmToken,
newlyLearnedPniToAciAssociations: [:],
groupUpdateSource: localIdentifiers.aci,
GroupManager.insertGroupUpdateInfoMessageForNewGroup(
localIdentifiers: localIdentifiers,
groupThread: groupThread,
groupModel: groupModel,
disappearingMessageToken: dmToken,
groupUpdateSource: .aci(localIdentifiers.aci),
transaction: transaction
)
@ -2240,7 +2237,7 @@ public class GroupsV2Impl: GroupsV2Swift, GroupsV2, Dependencies {
oldDisappearingMessageToken: dmToken,
newDisappearingMessageToken: dmToken,
newlyLearnedPniToAciAssociations: [:],
groupUpdateSource: localIdentifiers.aci,
groupUpdateSource: .aci(localIdentifiers.aci),
localIdentifiers: localIdentifiers,
transaction: transaction
)
@ -2387,7 +2384,7 @@ public class GroupsV2Impl: GroupsV2Swift, GroupsV2, Dependencies {
let dmConfigurationStore = DependenciesBridge.shared.disappearingMessagesConfigurationStore
let dmToken = dmConfigurationStore.fetchOrBuildDefault(for: .thread(groupThread), tx: transaction.asV2Read).asToken
// groupUpdateSourceAddress is nil; we don't know who did the update.
// groupUpdateSource is unknown; we don't know who did the update.
GroupManager.insertGroupUpdateInfoMessage(
groupThread: groupThread,
oldGroupModel: oldGroupModel,
@ -2395,7 +2392,7 @@ public class GroupsV2Impl: GroupsV2Swift, GroupsV2, Dependencies {
oldDisappearingMessageToken: dmToken,
newDisappearingMessageToken: dmToken,
newlyLearnedPniToAciAssociations: [:],
groupUpdateSource: nil,
groupUpdateSource: .unknown,
localIdentifiers: localIdentifiers,
transaction: transaction
)

View File

@ -12,7 +12,7 @@ public struct ChangedGroupModel {
public let newGroupModel: TSGroupModelV2
// newDisappearingMessageToken is only set of DM state changed.
public let newDisappearingMessageToken: DisappearingMessageToken?
public let changeAuthor: ServiceId
public let updateSource: GroupUpdateSource
public let profileKeys: [Aci: Data]
/// Associations between a PNI and ACI that we learned as a part of this
@ -23,14 +23,14 @@ public struct ChangedGroupModel {
oldGroupModel: TSGroupModelV2,
newGroupModel: TSGroupModelV2,
newDisappearingMessageToken: DisappearingMessageToken?,
changeAuthor: ServiceId,
updateSource: GroupUpdateSource,
profileKeys: [Aci: Data],
newlyLearnedPniToAciAssociations: [Pni: Aci]
) {
self.oldGroupModel = oldGroupModel
self.newGroupModel = newGroupModel
self.newDisappearingMessageToken = newDisappearingMessageToken
self.changeAuthor = changeAuthor
self.updateSource = updateSource
self.profileKeys = profileKeys
self.newlyLearnedPniToAciAssociations = newlyLearnedPniToAciAssociations
}
@ -70,14 +70,18 @@ public class GroupsV2IncomingChanges {
throw GroupsV2Error.cantApplyChangesToPlaceholder
}
let groupV2Params = try oldGroupModel.groupV2Params()
// Many change actions have author info, e.g. addedByUserID. But we can
// safely assume that all actions in the "change actions" have the same author.
guard let changeAuthorUuidData = changeActionsProto.sourceUuid else {
let updateSource = try changeActionsProto.updateSource(groupV2Params: groupV2Params)
let changeAuthor: ServiceId
switch updateSource {
case .unknown, .legacyE164:
// Many change actions have author info, e.g. addedByUserID. But we can
// safely assume that all actions in the "change actions" have the same author.
throw OWSAssertionError("Missing changeAuthorUuid.")
case .aci(let aci):
changeAuthor = aci
case .rejectedInviteToPni(let pni):
changeAuthor = pni
}
// Some userIds/uuidCiphertexts can be validated by
// the service. This is one.
let changeAuthor = try groupV2Params.serviceId(for: changeAuthorUuidData)
guard changeActionsProto.hasRevision else {
throw OWSAssertionError("Missing revision.")
@ -655,7 +659,7 @@ public class GroupsV2IncomingChanges {
oldGroupModel: oldGroupModel,
newGroupModel: newGroupModel,
newDisappearingMessageToken: newDisappearingMessageToken,
changeAuthor: changeAuthor,
updateSource: updateSource,
profileKeys: profileKeys,
newlyLearnedPniToAciAssociations: newlyLearnedPniToAciAssociations
)

View File

@ -0,0 +1,142 @@
//
// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import Foundation
import LibSignalClient
public enum GroupUpdateSource {
/// No source found.
case unknown
/// Legacy update (pre-gv2) with only an e164.
case legacyE164(E164)
/// Standard case. Most updates come from an aci.
case aci(Aci)
/// Only case at time of writing where an update comes
/// from a pni; a user invited by pni rejected that invite.
case rejectedInviteToPni(Pni)
// If there are new cases where updates are authored by
// a pni, add them here. This is because pni authors are
// the SUPER rare exception to the rule, and forcing
// every other case to wonder "when is this a Pni?" is
// more onerous than explicitly typing all such cases.
}
extension GroupUpdateSource {
public func serviceId() -> ServiceId? {
switch self {
case .unknown:
return nil
case .legacyE164:
return nil
case .aci(let aci):
return aci
case .rejectedInviteToPni(let pni):
return pni
}
}
}
extension GroupUpdateSource: Equatable {}
extension TSInfoMessage {
// MARK: - Serialization
private enum GroupUpdateSourceRaw: Int {
/// Historically we serialized the updater as a SignalServiceAddress,
/// so it could be an e164, aci, pni, or unknown.
/// If it is a pni, it _must_ be from an update
/// where a pni invitee rejected the invite. All other
/// cases use aci, or were created after the introduction
/// of this type and therefore would not use the legacy case.
case legacy = 0
case aci = 1
case rejectedInviteToPni = 2
}
internal static func insertGroupUpdateSource(
_ groupUpdateSource: GroupUpdateSource,
intoInfoMessageUserInfoDict userInfoDict: inout [InfoMessageUserInfoKey: Any],
localIdentifiers: LocalIdentifiers
) {
switch groupUpdateSource {
case .unknown:
// Don't insert anything.
return
case .legacyE164(let e164):
// We really shouldn't be inserting these in the modern day...
owsFailDebug("Serializing e164 group updater!")
userInfoDict[.groupUpdateSourceType] = GroupUpdateSourceRaw.legacy.rawValue
userInfoDict[.groupUpdateSourceLegacyAddress] = SignalServiceAddress(e164)
userInfoDict[.legacyUpdaterKnownToBeLocalUser] = localIdentifiers.contains(phoneNumber: e164)
case .aci(let aci):
userInfoDict[.groupUpdateSourceType] = GroupUpdateSourceRaw.aci.rawValue
userInfoDict[.groupUpdateSourceAciData] = Data(aci.serviceIdBinary)
case .rejectedInviteToPni(let pni):
userInfoDict[.groupUpdateSourceType] = GroupUpdateSourceRaw.rejectedInviteToPni.rawValue
userInfoDict[.groupUpdateSourcePniData] = Data(pni.serviceIdBinary)
}
}
internal static func groupUpdateSource(
infoMessageUserInfoDict: [InfoMessageUserInfoKey: Any]?
) -> GroupUpdateSource {
guard let infoMessageUserInfoDict else {
return .unknown
}
guard let rawType = infoMessageUserInfoDict[.groupUpdateSourceType] as? Int else {
return legacyGroupUpdateSource(infoMessageUserInfoDict: infoMessageUserInfoDict)
}
switch GroupUpdateSourceRaw(rawValue: rawType) {
case .none:
owsFailDebug("Unknown group update source")
return legacyGroupUpdateSource(infoMessageUserInfoDict: infoMessageUserInfoDict)
case .legacy:
return legacyGroupUpdateSource(infoMessageUserInfoDict: infoMessageUserInfoDict)
case .aci:
guard
let aciData = infoMessageUserInfoDict[.groupUpdateSourceAciData] as? Data,
let aci = try? Aci.parseFrom(serviceIdBinary: aciData)
else {
return .unknown
}
return .aci(aci)
case .rejectedInviteToPni:
guard
let pniData = infoMessageUserInfoDict[.groupUpdateSourcePniData] as? Data,
let pni = try? Pni.parseFrom(serviceIdBinary: pniData)
else {
return .unknown
}
return .rejectedInviteToPni(pni)
}
}
private static func legacyGroupUpdateSource(
infoMessageUserInfoDict: [InfoMessageUserInfoKey: Any]
) -> GroupUpdateSource {
guard let address = infoMessageUserInfoDict[.groupUpdateSourceLegacyAddress] as? SignalServiceAddress else {
return .unknown
}
if let aci = address.serviceId as? Aci {
return .aci(aci)
} else if let pni = address.serviceId as? Pni {
// When GroupUpdateSource was introduced, the _only_ way to have
// a Pni (and not an aci) be the source address was when the update
// came from someone invited by Pni rejecting that invitation.
// Maybe other cases got added in the future, but if they did they'd
// not use the legacy address storage, so if we find a legacy address
// with a Pni, it _must_ be from the pni invite rejection case.
return .rejectedInviteToPni(pni)
} else if let e164 = address.e164 {
return .legacyE164(e164)
} else {
return .unknown
}
}
}

View File

@ -16,10 +16,7 @@ public protocol DisplayableGroupUpdateItemBuilder {
/// preferred over all others.
func displayableUpdateItemsForPrecomputed(
precomputedUpdateItems: [TSInfoMessage.PersistableGroupUpdateItem],
oldGroupModel: TSGroupModel?,
localIdentifiers: LocalIdentifiers,
groupUpdateSourceAddress: SignalServiceAddress?,
updaterKnownToBeLocalUser: Bool,
tx: DBReadTransaction
) -> [DisplayableGroupUpdateItem]
@ -32,7 +29,7 @@ public protocol DisplayableGroupUpdateItemBuilder {
newGroupModel: TSGroupModel,
newDisappearingMessageToken: DisappearingMessageToken?,
localIdentifiers: LocalIdentifiers,
groupUpdateSourceAddress: SignalServiceAddress?,
groupUpdateSource: GroupUpdateSource,
updaterKnownToBeLocalUser: Bool,
tx: DBReadTransaction
) -> [DisplayableGroupUpdateItem]
@ -49,7 +46,7 @@ public protocol DisplayableGroupUpdateItemBuilder {
oldDisappearingMessageToken: DisappearingMessageToken?,
newDisappearingMessageToken: DisappearingMessageToken?,
localIdentifiers: LocalIdentifiers,
groupUpdateSourceAddress: SignalServiceAddress?,
groupUpdateSource: GroupUpdateSource,
updaterKnownToBeLocalUser: Bool,
tx: DBReadTransaction
) -> [DisplayableGroupUpdateItem]
@ -57,7 +54,7 @@ public protocol DisplayableGroupUpdateItemBuilder {
/// Get a default group update item, if the models required to build more
/// specific group updates are not available.
func defaultDisplayableUpdateItem(
groupUpdateSourceAddress: SignalServiceAddress?,
groupUpdateSource: GroupUpdateSource,
localIdentifiers: LocalIdentifiers?,
tx: DBReadTransaction
) -> DisplayableGroupUpdateItem
@ -74,13 +71,13 @@ public struct DisplayableGroupUpdateItemBuilderImpl: DisplayableGroupUpdateItemB
newGroupModel: TSGroupModel,
newDisappearingMessageToken: DisappearingMessageToken?,
localIdentifiers: LocalIdentifiers,
groupUpdateSourceAddress: SignalServiceAddress?,
groupUpdateSource: GroupUpdateSource,
updaterKnownToBeLocalUser: Bool,
tx: DBReadTransaction
) -> [DisplayableGroupUpdateItem] {
let updater: Updater = .build(
let updater: GroupUpdater = .build(
localIdentifiers: localIdentifiers,
groupUpdateSourceAddress: groupUpdateSourceAddress,
groupUpdateSource: groupUpdateSource,
updaterKnownToBeLocalUser: updaterKnownToBeLocalUser,
contactsManager: contactsManager,
tx: tx
@ -104,30 +101,27 @@ public struct DisplayableGroupUpdateItemBuilderImpl: DisplayableGroupUpdateItemB
public func displayableUpdateItemsForPrecomputed(
precomputedUpdateItems: [TSInfoMessage.PersistableGroupUpdateItem],
oldGroupModel: TSGroupModel?,
localIdentifiers: LocalIdentifiers,
groupUpdateSourceAddress: SignalServiceAddress?,
updaterKnownToBeLocalUser: Bool,
tx: DBReadTransaction
) -> [DisplayableGroupUpdateItem] {
let updater: Updater = .build(
localIdentifiers: localIdentifiers,
groupUpdateSourceAddress: groupUpdateSourceAddress,
updaterKnownToBeLocalUser: updaterKnownToBeLocalUser,
contactsManager: contactsManager,
tx: tx
)
let items = PrecomputedGroupUpdateItemBuilder(
contactsManager: contactsManager
).buildGroupUpdateItems(
precomputedUpdateItems: precomputedUpdateItems,
oldGroupMembership: oldGroupModel?.groupMembership,
updater: updater,
localIdentifiers: localIdentifiers,
tx: tx
)
let updater: GroupUpdater
if let precomputedUpdateItem = precomputedUpdateItems.first {
updater = precomputedUpdateItem.updater(
localIdentifiers: localIdentifiers,
contactsManager: contactsManager,
tx: tx
)
} else {
updater = .unknown
}
return validateUpdateItemsNotEmpty(
tentativeUpdateItems: items,
updater: updater
@ -140,13 +134,13 @@ public struct DisplayableGroupUpdateItemBuilderImpl: DisplayableGroupUpdateItemB
oldDisappearingMessageToken: DisappearingMessageToken?,
newDisappearingMessageToken: DisappearingMessageToken?,
localIdentifiers: LocalIdentifiers,
groupUpdateSourceAddress: SignalServiceAddress?,
groupUpdateSource: GroupUpdateSource,
updaterKnownToBeLocalUser: Bool,
tx: DBReadTransaction
) -> [DisplayableGroupUpdateItem] {
let updater: Updater = .build(
let updater: GroupUpdater = .build(
localIdentifiers: localIdentifiers,
groupUpdateSourceAddress: groupUpdateSourceAddress,
groupUpdateSource: groupUpdateSource,
updaterKnownToBeLocalUser: updaterKnownToBeLocalUser,
contactsManager: contactsManager,
tx: tx
@ -159,7 +153,7 @@ public struct DisplayableGroupUpdateItemBuilderImpl: DisplayableGroupUpdateItemB
newDisappearingMessageToken: newDisappearingMessageToken,
updater: updater,
localIdentifiers: localIdentifiers,
groupUpdateSourceAddress: groupUpdateSourceAddress,
groupUpdateSource: groupUpdateSource,
contactsManager: contactsManager,
tx: tx
).itemList
@ -171,12 +165,12 @@ public struct DisplayableGroupUpdateItemBuilderImpl: DisplayableGroupUpdateItemB
}
public func defaultDisplayableUpdateItem(
groupUpdateSourceAddress: SignalServiceAddress?,
groupUpdateSource: GroupUpdateSource,
localIdentifiers: LocalIdentifiers?,
tx: DBReadTransaction
) -> DisplayableGroupUpdateItem {
return DefaultGroupUpdateItemBuilder().buildGroupUpdateItem(
groupUpdateSourceAddress: groupUpdateSourceAddress,
groupUpdateSource: groupUpdateSource,
localIdentifiers: localIdentifiers,
contactsManager: contactsManager,
tx: tx
@ -185,7 +179,7 @@ public struct DisplayableGroupUpdateItemBuilderImpl: DisplayableGroupUpdateItemB
private func validateUpdateItemsNotEmpty(
tentativeUpdateItems: [DisplayableGroupUpdateItem],
updater: Updater,
updater: GroupUpdater,
file: String = #file,
function: String = #function,
line: Int = #line
@ -212,7 +206,7 @@ public struct DisplayableGroupUpdateItemBuilderImpl: DisplayableGroupUpdateItemB
// MARK: -
private enum Updater {
public enum GroupUpdater {
case localUser
case otherUser(updaterName: String, updaterAddress: SignalServiceAddress)
case unknown
@ -221,19 +215,27 @@ private enum Updater {
///
/// - Parameter updaterKnownToBeLocalUser
/// Whether we know ahead of time that the updater is the local user.
static func build(
internal static func build(
localIdentifiers: LocalIdentifiers?,
groupUpdateSourceAddress: SignalServiceAddress?,
groupUpdateSource: GroupUpdateSource,
updaterKnownToBeLocalUser: Bool,
contactsManager: Shims.ContactsManager,
tx: DBReadTransaction
) -> Updater {
) -> GroupUpdater {
if updaterKnownToBeLocalUser {
return .localUser
}
guard let updaterAddress = groupUpdateSourceAddress else {
let updaterAddress: SignalServiceAddress
switch groupUpdateSource {
case .unknown:
return .unknown
case .legacyE164(let e164):
updaterAddress = .init(e164)
case .aci(let aci):
updaterAddress = .init(aci)
case .rejectedInviteToPni(let pni):
updaterAddress = .init(pni)
}
if let localIdentifiers, localIdentifiers.contains(address: updaterAddress) {
@ -333,58 +335,103 @@ private struct PrecomputedGroupUpdateItemBuilder {
func buildGroupUpdateItems(
precomputedUpdateItems: [TSInfoMessage.PersistableGroupUpdateItem],
oldGroupMembership: GroupMembership?,
updater: Updater,
localIdentifiers: LocalIdentifiers,
tx: DBReadTransaction
) -> [DisplayableGroupUpdateItem] {
return precomputedUpdateItems.compactMap { persistableGroupUpdateItem -> DisplayableGroupUpdateItem? in
let updater = persistableGroupUpdateItem.updater(
localIdentifiers: localIdentifiers,
contactsManager: contactsManager,
tx: tx
)
switch persistableGroupUpdateItem {
case let .sequenceOfInviteLinkRequestAndCancels(count, isTail):
case let .sequenceOfInviteLinkRequestAndCancels(_, count, isTail):
return sequenceOfInviteLinkRequestAndCancelsItem(
updater: updater,
count: count,
isTail: isTail
)
case let .invitedPniPromotedToFullMemberAci(pni, aci):
let invitedPniAddress = SignalServiceAddress(pni.wrappedValue)
let fullMemberAciAddress = SignalServiceAddress(aci.wrappedValue)
case let .invitedPniPromotedToFullMemberAci(newMember, inviter):
return DiffingGroupUpdateItemBuilder.userInviteWasAcceptedItem(
invitedAsAddress: invitedPniAddress,
acceptedAsAddress: fullMemberAciAddress,
oldGroupMembership: oldGroupMembership,
acceptedAsAddress: .init(newMember.wrappedValue),
inviterAci: inviter?.wrappedValue,
updater: updater,
// Promotions are only authored by the invitee; otherwise
// it would be an add member not a promotion.
updaterKnownToBeInvitee: true,
isLocalUserBlock: { localIdentifiers.isLocalUser(address: $0) },
contactsManager: contactsManager,
tx: tx
)
case let .inviteRemoved(invitee, wasLocalUser):
var unnamedInviteCounts = UnnamedInviteCounts()
if let item = DiffingGroupUpdateItemBuilder.userInviteWasDeclinedOrRevokedItem(
inviteeAddress: SignalServiceAddress(invitee.wrappedValue),
inviteeKnownToBeLocalUser: wasLocalUser,
oldGroupMembership: oldGroupMembership,
unnamedInviteCounts: &unnamedInviteCounts,
updater: updater,
isLocalUserBlock: { localIdentifiers.isLocalUser(address: $0) },
contactsManager: contactsManager,
tx: tx
) {
return item
} else {
return DiffingGroupUpdateItemBuilder.unnamedUserInvitesWereRevokedItem(
count: unnamedInviteCounts.revokedInviteCount,
updater: updater
)
}
case .localUserDeclinedInviteFromInviter(let inviterAci):
return .localUserDeclinedInviteFromInviter(
inviterName: contactsManager.displayName(
address: .init(inviterAci.wrappedValue),
tx: tx
),
inviterAddress: .init(inviterAci.wrappedValue)
)
case .localUserDeclinedInviteFromUnknownUser:
return .localUserDeclinedInviteFromUnknownUser
case .otherUserDeclinedInviteFromLocalUser(let invitee):
return .otherUserDeclinedInviteFromLocalUser(
userName: contactsManager.displayName(
address: .init(invitee.wrappedValue),
tx: tx
),
userAddress: .init(invitee.wrappedValue)
)
case let .otherUserDeclinedInviteFromInviter(_, inviterAci),
let .unnamedUserDeclinedInviteFromInviter(inviterAci):
return .otherUserDeclinedInviteFromInviter(
inviterName: contactsManager.displayName(
address: .init(inviterAci.wrappedValue),
tx: tx
),
inviterAddress: .init(inviterAci.wrappedValue)
)
case .otherUserDeclinedInviteFromUnknownUser,
.unnamedUserDeclinedInviteFromUnknownUser:
return .otherUserDeclinedInviteFromUnknownUser
case .localUserInviteRevoked(let revokerAci):
return .localUserInviteRevoked(
revokerName: contactsManager.displayName(
address: .init(revokerAci.wrappedValue),
tx: tx
),
revokerAddress: .init(revokerAci.wrappedValue)
)
case .localUserInviteRevokedByUnknownUser:
return .localUserInviteRevokedByUnknownUser
case .otherUserInviteRevokedByLocalUser(let invitee):
return .otherUserInviteRevokedByLocalUser(
userName: contactsManager.displayName(
address: .init(invitee.wrappedValue),
tx: tx
),
userAddress: .init(invitee.wrappedValue)
)
case .unnamedUserInvitesWereRevokedByLocalUser(let count):
return .unnamedUserInvitesWereRevokedByLocalUser(count: count)
case let .unnamedUserInvitesWereRevokedByOtherUser(updaterAci, count):
return .unnamedUserInvitesWereRevokedByOtherUser(
updaterName: contactsManager.displayName(
address: .init(updaterAci.wrappedValue),
tx: tx
),
updaterAddress: .init(updaterAci.wrappedValue),
count: count
)
case .unnamedUserInvitesWereRevokedByUnknownUser(let count):
return .unnamedUserInvitesWereRevokedByUnknownUser(count: count)
}
}
}
private func sequenceOfInviteLinkRequestAndCancelsItem(
updater: Updater,
updater: GroupUpdater,
count: UInt,
isTail: Bool
) -> DisplayableGroupUpdateItem? {
@ -432,7 +479,7 @@ private struct NewGroupUpdateItemBuilder {
func buildGroupUpdateItems(
newGroupModel: TSGroupModel,
newDisappearingMessageToken: DisappearingMessageToken?,
updater: Updater,
updater: GroupUpdater,
localIdentifiers: LocalIdentifiers,
tx: DBReadTransaction
) -> [DisplayableGroupUpdateItem] {
@ -474,7 +521,7 @@ private struct NewGroupUpdateItemBuilder {
}
private func groupWasInsertedItem(
updater: Updater,
updater: GroupUpdater,
localIdentifiers: LocalIdentifiers,
newGroupModel: TSGroupModel,
newGroupMembership: GroupMembership,
@ -565,7 +612,7 @@ private struct DiffingGroupUpdateItemBuilder {
private let contactsManager: Shims.ContactsManager
private let localIdentifiers: LocalIdentifiers
private let updater: Updater
private let updater: GroupUpdater
private let isReplacingJoinRequestPlaceholder: Bool
/// The update items, in order.
@ -573,21 +620,21 @@ private struct DiffingGroupUpdateItemBuilder {
/// Create a ``GroupUpdateCopy``.
///
/// - Parameter groupUpdateSourceAddress
/// - Parameter groupUpdateSource
/// The address to whom this update should be attributed, if known.
/// - Parameter updaterKnownToBeLocalUser
/// Whether we know, ahead of time, that this update should be attributed to
/// the local user. Necessary if we cannot reliably determine attribution
/// via ``groupUpdateSourceAddress`` alone. For example, the update address
/// via ``groupUpdateSource`` alone. For example, the update address
/// may refer to a PNI that has moved to another owner.
init(
oldGroupModel: TSGroupModel,
newGroupModel: TSGroupModel,
oldDisappearingMessageToken: DisappearingMessageToken?,
newDisappearingMessageToken: DisappearingMessageToken?,
updater: Updater,
updater: GroupUpdater,
localIdentifiers: LocalIdentifiers,
groupUpdateSourceAddress: SignalServiceAddress?,
groupUpdateSource: GroupUpdateSource,
contactsManager: Shims.ContactsManager,
tx: DBReadTransaction
) {
@ -607,7 +654,7 @@ private struct DiffingGroupUpdateItemBuilder {
newGroupModel: newGroupModel,
oldDisappearingMessageToken: oldDisappearingMessageToken,
newDisappearingMessageToken: newDisappearingMessageToken,
groupUpdateSourceAddress: groupUpdateSourceAddress,
groupUpdateSource: groupUpdateSource,
tx: tx
)
@ -642,7 +689,7 @@ private struct DiffingGroupUpdateItemBuilder {
newGroupModel: TSGroupModel,
oldDisappearingMessageToken: DisappearingMessageToken?,
newDisappearingMessageToken: DisappearingMessageToken?,
groupUpdateSourceAddress: SignalServiceAddress?,
groupUpdateSource: GroupUpdateSource,
tx: DBReadTransaction
) {
if isReplacingJoinRequestPlaceholder {
@ -650,7 +697,7 @@ private struct DiffingGroupUpdateItemBuilder {
oldGroupMembership: oldGroupModel.groupMembership,
newGroupMembership: newGroupModel.groupMembership,
newGroupModel: newGroupModel,
groupUpdateSourceAddress: groupUpdateSourceAddress,
groupUpdateSource: groupUpdateSource,
forLocalUserOnly: true,
tx: tx
)
@ -670,7 +717,7 @@ private struct DiffingGroupUpdateItemBuilder {
oldGroupMembership: oldGroupModel.groupMembership,
newGroupMembership: newGroupModel.groupMembership,
newGroupModel: newGroupModel,
groupUpdateSourceAddress: groupUpdateSourceAddress,
groupUpdateSource: groupUpdateSource,
forLocalUserOnly: false,
tx: tx
)
@ -861,7 +908,7 @@ private struct DiffingGroupUpdateItemBuilder {
oldGroupMembership: GroupMembership,
newGroupMembership: GroupMembership,
newGroupModel: TSGroupModel,
groupUpdateSourceAddress: SignalServiceAddress?,
groupUpdateSource: GroupUpdateSource,
forLocalUserOnly: Bool,
tx: DBReadTransaction
) {
@ -886,7 +933,18 @@ private struct DiffingGroupUpdateItemBuilder {
// If the updater has changed their membership status, ensure it appears _last_.
// This trumps the re-ordering of the local user above.
if let updaterAddress = groupUpdateSourceAddress {
let updaterAddress: SignalServiceAddress?
switch groupUpdateSource {
case .unknown:
updaterAddress = nil
case .legacyE164(let e164):
updaterAddress = .init(e164)
case .aci(let aci):
updaterAddress = .init(aci)
case .rejectedInviteToPni(let pni):
updaterAddress = .init(pni)
}
if let updaterAddress {
allUsersSorted = allUsersSorted.filter { $0 != updaterAddress } + [updaterAddress]
}
@ -1290,11 +1348,23 @@ private struct DiffingGroupUpdateItemBuilder {
oldGroupMembership: GroupMembership,
tx: DBReadTransaction
) {
let inviterAci = oldGroupMembership.addedByAci(forInvitedMember: invitedAsAddress)
let updaterKnownToBeInvitee: Bool
switch updater {
case .localUser:
updaterKnownToBeInvitee = localIdentifiers.contains(address: invitedAsAddress)
case .otherUser(_, let updaterAddress):
updaterKnownToBeInvitee = updaterAddress == invitedAsAddress
case .unknown:
updaterKnownToBeInvitee = false
}
addItem(Self.userInviteWasAcceptedItem(
invitedAsAddress: invitedAsAddress,
acceptedAsAddress: acceptedAsAddress,
oldGroupMembership: oldGroupMembership,
inviterAci: inviterAci,
updater: updater,
updaterKnownToBeInvitee: updaterKnownToBeInvitee,
isLocalUserBlock: { isLocalUser(address: $0) },
contactsManager: contactsManager,
tx: tx
@ -1302,10 +1372,10 @@ private struct DiffingGroupUpdateItemBuilder {
}
static func userInviteWasAcceptedItem(
invitedAsAddress: SignalServiceAddress,
acceptedAsAddress: SignalServiceAddress,
oldGroupMembership: GroupMembership?,
updater: Updater,
inviterAci: Aci?,
updater: GroupUpdater,
updaterKnownToBeInvitee: Bool,
isLocalUserBlock: IsLocalUserBlock,
contactsManager: Shims.ContactsManager,
tx: DBReadTransaction
@ -1313,7 +1383,7 @@ private struct DiffingGroupUpdateItemBuilder {
var inviterName: String?
var inviterAddress: SignalServiceAddress?
if let inviterAci = oldGroupMembership?.addedByAci(forInvitedMember: invitedAsAddress) {
if let inviterAci {
inviterAddress = SignalServiceAddress(inviterAci)
inviterName = contactsManager.displayName(address: SignalServiceAddress(inviterAci), tx: tx)
}
@ -1348,10 +1418,10 @@ private struct DiffingGroupUpdateItemBuilder {
userAddress: acceptedAsAddress
)
case let .otherUser(updaterName, updaterAddress):
if invitedAsAddress == updaterAddress || acceptedAsAddress == updaterAddress {
if updaterKnownToBeInvitee || acceptedAsAddress == updaterAddress {
// The update came from the person who was invited.
if isLocalUserBlock(invitedAsAddress) {
if let inviterAddress, isLocalUserBlock(inviterAddress) {
return .otherUserAcceptedInviteFromLocalUser(
userName: acceptedAsName,
userAddress: acceptedAsAddress
@ -1394,10 +1464,12 @@ private struct DiffingGroupUpdateItemBuilder {
unnamedInviteCounts: inout UnnamedInviteCounts,
tx: DBReadTransaction
) {
let inviterAci = oldGroupMembership.addedByAci(forInvitedMember: inviteeAddress)
Self.userInviteWasDeclinedOrRevokedItem(
inviteeAddress: inviteeAddress,
inviteeKnownToBeLocalUser: inviteeKnownToBeLocalUser,
oldGroupMembership: oldGroupMembership,
inviterAci: inviterAci,
unnamedInviteCounts: &unnamedInviteCounts,
updater: updater,
isLocalUserBlock: { isLocalUser(address: $0) },
@ -1418,16 +1490,16 @@ private struct DiffingGroupUpdateItemBuilder {
static func userInviteWasDeclinedOrRevokedItem(
inviteeAddress: SignalServiceAddress,
inviteeKnownToBeLocalUser: Bool,
oldGroupMembership: GroupMembership?,
inviterAci: Aci?,
unnamedInviteCounts: inout UnnamedInviteCounts,
updater: Updater,
updater: GroupUpdater,
isLocalUserBlock: IsLocalUserBlock,
contactsManager: Shims.ContactsManager,
tx: DBReadTransaction
) -> DisplayableGroupUpdateItem? {
var inviterName: String?
var inviterAddress: SignalServiceAddress?
if let inviterAci = oldGroupMembership?.addedByAci(forInvitedMember: inviteeAddress) {
if let inviterAci {
inviterAddress = SignalServiceAddress(inviterAci)
inviterName = contactsManager.displayName(address: SignalServiceAddress(inviterAci), tx: tx)
}
@ -1638,7 +1710,7 @@ private struct DiffingGroupUpdateItemBuilder {
static func unnamedUserInvitesWereRevokedItem(
count: UInt,
updater: Updater
updater: GroupUpdater
) -> DisplayableGroupUpdateItem? {
guard count > 0 else {
return nil
@ -1816,7 +1888,7 @@ private struct DiffingGroupUpdateItemBuilder {
}
static func disappearingMessageUpdateItem(
updater: Updater,
updater: GroupUpdater,
oldToken: DisappearingMessageToken?,
newToken: DisappearingMessageToken?,
forceUnknownAttribution: Bool
@ -2066,14 +2138,14 @@ private struct DefaultGroupUpdateItemBuilder {
init() {}
func buildGroupUpdateItem(
groupUpdateSourceAddress: SignalServiceAddress?,
groupUpdateSource: GroupUpdateSource,
localIdentifiers: LocalIdentifiers?,
contactsManager: Shims.ContactsManager,
tx: DBReadTransaction
) -> DisplayableGroupUpdateItem {
let updater: Updater = .build(
let updater: GroupUpdater = .build(
localIdentifiers: localIdentifiers,
groupUpdateSourceAddress: groupUpdateSourceAddress,
groupUpdateSource: groupUpdateSource,
updaterKnownToBeLocalUser: false,
contactsManager: contactsManager,
tx: tx
@ -2104,7 +2176,7 @@ private extension LocalIdentifiers {
// MARK: - Dependencies
private typealias Shims = DisplayableGroupUpdateItemBuilderImpl.Shims
internal typealias Shims = DisplayableGroupUpdateItemBuilderImpl.Shims
private typealias IsLocalUserBlock = (SignalServiceAddress) -> Bool
extension DisplayableGroupUpdateItemBuilderImpl {

View File

@ -8,6 +8,60 @@ import LibSignalClient
extension TSInfoMessage {
@objc(TSInfoMessageUpdateMessages)
public class LegacyPersistableGroupUpdateItemsWrapper: NSObject, NSCopying, NSSecureCoding {
public let updateItems: [LegacyPersistableGroupUpdateItem]
public init(_ updateItems: [LegacyPersistableGroupUpdateItem]) {
self.updateItems = updateItems
}
// MARK: NSCopying
public func copy(with _: NSZone? = nil) -> Any {
self
}
// MARK: NSSecureCoding
public static var supportsSecureCoding: Bool { true }
private static let messagesKey = "messagesKey"
public func encode(with aCoder: NSCoder) {
let jsonEncoder = JSONEncoder()
do {
let messagesData = try jsonEncoder.encode(updateItems)
aCoder.encode(messagesData, forKey: Self.messagesKey)
} catch let error {
owsFailDebug("Failed to encode updateItems data: \(error)")
return
}
}
public required init?(coder aDecoder: NSCoder) {
guard let updateItemsData = aDecoder.decodeObject(
forKey: Self.messagesKey
) as? Data else {
owsFailDebug("Failed to decode updateItems data")
return nil
}
let jsonDecoder = JSONDecoder()
do {
updateItems = try jsonDecoder.decode(
[LegacyPersistableGroupUpdateItem].self,
from: updateItemsData
)
} catch let error {
owsFailDebug("Failed to decode updateItems data: \(error)")
return nil
}
super.init()
}
}
@objc(TSInfoMessageUpdateMessagesV2)
public class PersistableGroupUpdateItemsWrapper: NSObject, NSCopying, NSSecureCoding {
public let updateItems: [PersistableGroupUpdateItem]
@ -65,7 +119,8 @@ extension TSInfoMessage {
// MARK: -
extension TSInfoMessage {
public enum PersistableGroupUpdateItem: Codable {
public enum LegacyPersistableGroupUpdateItem: Codable {
enum CodingKeys: String, CodingKey {
case sequenceOfInviteLinkRequestAndCancels
case invitedPniPromotedToFullMemberAci
@ -75,5 +130,143 @@ extension TSInfoMessage {
case sequenceOfInviteLinkRequestAndCancels(count: UInt, isTail: Bool)
case invitedPniPromotedToFullMemberAci(pni: PniUuid, aci: AciUuid)
case inviteRemoved(invitee: ServiceIdUppercaseString, wasLocalUser: Bool)
func toNewItem(
updater: GroupUpdateSource,
oldGroupModel: TSGroupModel?,
localIdentifiers: LocalIdentifiers?
) -> PersistableGroupUpdateItem? {
switch self {
case .sequenceOfInviteLinkRequestAndCancels(let count, let isTail):
switch updater {
case .unknown, .legacyE164, .rejectedInviteToPni:
owsFailDebug("How did we get one of these without an updater? that should be impossible")
return nil
case .aci(let aci):
return .sequenceOfInviteLinkRequestAndCancels(
requester: aci.codableUuid,
count: count,
isTail: isTail
)
}
case .invitedPniPromotedToFullMemberAci(let pni, let aci):
return .invitedPniPromotedToFullMemberAci(
newMember: aci,
inviter: oldGroupModel?.groupMembership.addedByAci(
forInvitedMember: SignalServiceAddress(pni.wrappedValue)
)?.codableUuid
)
case .inviteRemoved(let invitee, let wasLocalUser):
let remover: ServiceId
var wasRejectedInvite = false
switch updater {
case .unknown, .legacyE164:
owsFailDebug("Only acis or pnis can remove an invite")
return nil
case .aci(let aci):
remover = aci
case .rejectedInviteToPni(let pni):
remover = pni
wasRejectedInvite = true
}
let inviterAci = oldGroupModel?.groupMembership.addedByAci(
forInvitedMember: SignalServiceAddress(invitee.wrappedValue)
)
if wasLocalUser {
if wasRejectedInvite || (localIdentifiers?.contains(serviceId: remover) ?? false) {
// Local user invite that was rejected.
if let inviterAci {
return .localUserDeclinedInviteFromInviter(
inviterAci: inviterAci.codableUuid
)
} else {
return .localUserDeclinedInviteFromUnknownUser
}
} else {
// Local user invite that was removed by another user.
if let removerAci = remover as? Aci {
return .localUserInviteRevoked(revokerAci: removerAci.codableUuid)
} else {
return .localUserInviteRevokedByUnknownUser
}
}
} else {
if wasRejectedInvite || invitee.wrappedValue == remover {
// Other user rejected an invite.
if let inviterAci {
if inviterAci == localIdentifiers?.aci {
return .otherUserDeclinedInviteFromLocalUser(invitee: invitee)
} else {
return .otherUserDeclinedInviteFromInviter(
invitee: invitee,
inviterAci: inviterAci.codableUuid
)
}
} else {
return .otherUserDeclinedInviteFromUnknownUser(invitee: invitee)
}
} else {
// Other user's invite was revoked.
if let removerAci = remover as? Aci {
if removerAci == localIdentifiers?.aci {
return .otherUserInviteRevokedByLocalUser(invitee: invitee)
} else {
return .unnamedUserInvitesWereRevokedByOtherUser(
updaterAci: removerAci.codableUuid,
count: 1
)
}
} else {
return .unnamedUserInvitesWereRevokedByUnknownUser(count: 1)
}
}
}
}
}
}
public enum PersistableGroupUpdateItem: Codable {
enum CodingKeys: String, CodingKey {
case sequenceOfInviteLinkRequestAndCancels
case invitedPniPromotedToFullMemberAci
case localUserDeclinedInviteFromInviter
case localUserDeclinedInviteFromUnknownUser
case otherUserDeclinedInviteFromLocalUser
case otherUserDeclinedInviteFromInviter
case otherUserDeclinedInviteFromUnknownUser
case unnamedUserDeclinedInviteFromInviter
case unnamedUserDeclinedInviteFromUnknownUser
case localUserInviteRevoked
case localUserInviteRevokedByUnknownUser
case otherUserInviteRevokedByLocalUser
case unnamedUserInvitesWereRevokedByLocalUser
case unnamedUserInvitesWereRevokedByOtherUser
case unnamedUserInvitesWereRevokedByUnknownUser
}
case sequenceOfInviteLinkRequestAndCancels(requester: AciUuid, count: UInt, isTail: Bool)
case invitedPniPromotedToFullMemberAci(newMember: AciUuid, inviter: AciUuid?)
case localUserDeclinedInviteFromInviter(inviterAci: AciUuid)
case localUserDeclinedInviteFromUnknownUser
case otherUserDeclinedInviteFromLocalUser(invitee: ServiceIdUppercaseString)
case otherUserDeclinedInviteFromInviter(invitee: ServiceIdUppercaseString, inviterAci: AciUuid)
case otherUserDeclinedInviteFromUnknownUser(invitee: ServiceIdUppercaseString)
case unnamedUserDeclinedInviteFromInviter(inviterAci: AciUuid)
case unnamedUserDeclinedInviteFromUnknownUser
case localUserInviteRevoked(revokerAci: AciUuid)
case localUserInviteRevokedByUnknownUser
// For a single invite we revoked we keep the invitee.
// For many, or if someone else revoked, we just keep the number.
case otherUserInviteRevokedByLocalUser(invitee: ServiceIdUppercaseString)
case unnamedUserInvitesWereRevokedByLocalUser(count: UInt)
case unnamedUserInvitesWereRevokedByOtherUser(updaterAci: AciUuid, count: UInt)
case unnamedUserInvitesWereRevokedByUnknownUser(count: UInt)
}
}

View File

@ -0,0 +1,107 @@
//
// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import Foundation
import LibSignalClient
extension TSInfoMessage.PersistableGroupUpdateItem {
func updater(
localIdentifiers: LocalIdentifiers,
contactsManager: Shims.ContactsManager,
tx: DBReadTransaction
) -> GroupUpdater {
let groupUpdateSource: GroupUpdateSource
switch self {
case .sequenceOfInviteLinkRequestAndCancels(let requester, _, _):
groupUpdateSource = .aci(requester.wrappedValue)
case .invitedPniPromotedToFullMemberAci(let newMember, _):
// Only the invited member themselves can issue this update.
if localIdentifiers.aci == newMember.wrappedValue {
return .localUser
} else {
groupUpdateSource = .aci(newMember.wrappedValue)
}
case .localUserDeclinedInviteFromInviter:
return .localUser
case .localUserDeclinedInviteFromUnknownUser:
return .localUser
case
let .otherUserDeclinedInviteFromLocalUser(invitee),
let .otherUserDeclinedInviteFromInviter(invitee, _),
let .otherUserDeclinedInviteFromUnknownUser(invitee):
switch invitee.wrappedValue.concreteType {
case .aci(let aci):
groupUpdateSource = .aci(aci)
case .pni(let pni):
groupUpdateSource = .rejectedInviteToPni(pni)
}
case .localUserInviteRevoked(revokerAci: let revokerAci):
groupUpdateSource = .aci(revokerAci.wrappedValue)
case .localUserInviteRevokedByUnknownUser:
return .unknown
case .otherUserInviteRevokedByLocalUser:
return .localUser
case .unnamedUserInvitesWereRevokedByLocalUser:
return .localUser
case let .unnamedUserInvitesWereRevokedByOtherUser(updaterAci, _):
groupUpdateSource = .aci(updaterAci.wrappedValue)
case .unnamedUserInvitesWereRevokedByUnknownUser:
return .unknown
case .unnamedUserDeclinedInviteFromInviter:
return .unknown
case .unnamedUserDeclinedInviteFromUnknownUser:
return .unknown
}
return GroupUpdater.build(
localIdentifiers: localIdentifiers,
groupUpdateSource: groupUpdateSource,
updaterKnownToBeLocalUser: false,
contactsManager: contactsManager,
tx: tx
)
}
/// When rendering a notification for a group upate, if the update has a specific
/// "sender", we render the notification UI as if sent from that sender.
public var senderForNotification: SignalServiceAddress? {
let serviceId: ServiceId? = {
switch self {
case .sequenceOfInviteLinkRequestAndCancels(let requester, _, _):
return requester.wrappedValue
case .invitedPniPromotedToFullMemberAci(let newMember, _):
return newMember.wrappedValue
case .localUserDeclinedInviteFromInviter:
return nil
case .localUserDeclinedInviteFromUnknownUser:
return nil
case .otherUserDeclinedInviteFromLocalUser(let invitee):
return invitee.wrappedValue
case .otherUserDeclinedInviteFromInviter(let invitee, _):
return invitee.wrappedValue
case .otherUserDeclinedInviteFromUnknownUser(let invitee):
return invitee.wrappedValue
case .localUserInviteRevoked(let revokerAci):
return revokerAci.wrappedValue
case .localUserInviteRevokedByUnknownUser:
return nil
case .otherUserInviteRevokedByLocalUser:
return nil
case .unnamedUserInvitesWereRevokedByLocalUser:
return nil
case .unnamedUserInvitesWereRevokedByOtherUser(let updaterAci, _):
return updaterAci.wrappedValue
case .unnamedUserInvitesWereRevokedByUnknownUser:
return nil
case .unnamedUserDeclinedInviteFromInviter:
return nil
case .unnamedUserDeclinedInviteFromUnknownUser:
return nil
}
}()
return serviceId.map { SignalServiceAddress($0) }
}
}

View File

@ -9,42 +9,52 @@ import LibSignalClient
// MARK: - Group updates
public extension TSInfoMessage {
private var displayableGroupUpdateIemBuilder: DisplayableGroupUpdateItemBuilder {
DisplayableGroupUpdateItemBuilderImpl(
@objc
func groupUpdateDescription(transaction tx: SDSAnyReadTransaction) -> NSAttributedString {
let displayableGroupUpdateItemBuilder = DisplayableGroupUpdateItemBuilderImpl(
contactsManager: DisplayableGroupUpdateItemBuilderImpl.Wrappers.ContactsManager(
contactsManager
)
)
}
private var tsAccountManager: TSAccountManager {
DependenciesBridge.shared.tsAccountManager
}
let localIdentifiers: LocalIdentifiers? =
DependenciesBridge.shared.tsAccountManager.localIdentifiers(tx: tx.asV2Read)
@objc
func groupUpdateDescription(transaction tx: SDSAnyReadTransaction) -> NSAttributedString {
// for legacy group updates we persisted a pre-rendered string, rather than the details
// to generate that string
if let customMessage = self.customMessage {
return NSAttributedString(string: customMessage)
}
guard
let newGroupModel = self.newGroupModel,
let localIdentifiers = tsAccountManager.localIdentifiers(tx: tx.asV2Read)
else {
return displayableGroupUpdateIemBuilder.defaultDisplayableUpdateItem(
groupUpdateSourceAddress: groupUpdateSourceAddress,
localIdentifiers: nil,
func fallback() -> NSAttributedString {
return displayableGroupUpdateItemBuilder.defaultDisplayableUpdateItem(
groupUpdateSource: .unknown,
localIdentifiers: localIdentifiers,
tx: tx.asV2Read
).localizedText
}
let updateItems = buildGroupUpdateItems(
newGroupModel: newGroupModel,
localIdentifiers: localIdentifiers,
tx: tx.asV2Read
)
let updateItems: [DisplayableGroupUpdateItem]
switch groupUpdateMetadata(localIdentifiers: localIdentifiers) {
case .nonGroupUpdate:
return fallback()
case .legacyRawString(let string):
return NSAttributedString(string: string)
case .newGroup, .modelDiff, .precomputed:
guard let localIdentifiers else {
return fallback()
}
guard let items = buildGroupUpdateItems(
localIdentifiers: localIdentifiers,
displayableGroupUpdateItemBuilder: displayableGroupUpdateItemBuilder,
tx: tx.asV2Read
) else {
return fallback()
}
updateItems = items
}
guard let firstUpdateItem = updateItems.first else {
owsFailBeta("Should never have an empty update items list!")
@ -63,74 +73,71 @@ public extension TSInfoMessage {
}
func displayableGroupUpdateItems(tx: SDSAnyReadTransaction) -> [DisplayableGroupUpdateItem]? {
guard
self.customMessage == nil,
let newGroupModel = self.newGroupModel
else {
// Legacy group updates persisted a pre-rendered string.
return nil
}
guard let localIdentifiers = tsAccountManager.localIdentifiers(
tx: tx.asV2Read
) else {
owsFailDebug("Missing local identifiers!")
return nil
}
return buildGroupUpdateItems(
newGroupModel: newGroupModel,
localIdentifiers: localIdentifiers,
let localIdentifiers = DependenciesBridge.shared.tsAccountManager.localIdentifiers(
tx: tx.asV2Read
)
switch groupUpdateMetadata(localIdentifiers: localIdentifiers) {
case .legacyRawString, .nonGroupUpdate:
return nil
case .newGroup, .modelDiff, .precomputed:
guard let localIdentifiers = DependenciesBridge.shared.tsAccountManager.localIdentifiers(
tx: tx.asV2Read
) else {
owsFailDebug("Missing local identifiers!")
return nil
}
let displayableGroupUpdateItemBuilder = DisplayableGroupUpdateItemBuilderImpl(
contactsManager: DisplayableGroupUpdateItemBuilderImpl.Wrappers.ContactsManager(
contactsManager
)
)
return buildGroupUpdateItems(
localIdentifiers: localIdentifiers,
displayableGroupUpdateItemBuilder: displayableGroupUpdateItemBuilder,
tx: tx.asV2Read
)
}
}
private func buildGroupUpdateItems(
newGroupModel: TSGroupModel,
localIdentifiers: LocalIdentifiers,
displayableGroupUpdateItemBuilder: DisplayableGroupUpdateItemBuilder,
tx: DBReadTransaction
) -> [DisplayableGroupUpdateItem] {
let oldGroupModel = self.oldGroupModel
let oldDisappearingMessageToken = self.oldDisappearingMessageToken
let newDisappearingMessageToken = self.newDisappearingMessageToken
let groupUpdateSourceAddress = self.groupUpdateSourceAddress
let updaterKnownToBeLocalUser = self.updaterWasLocalUser
) -> [DisplayableGroupUpdateItem]? {
switch groupUpdateMetadata(localIdentifiers: localIdentifiers) {
if let precomputedUpdateItems = self.precomputedGroupUpdateItems {
/// If we have precomputed group update items, we should always
/// prefer displaying them.
return displayableGroupUpdateIemBuilder.displayableUpdateItemsForPrecomputed(
case .nonGroupUpdate, .legacyRawString:
return nil
case .precomputed(let precomputedUpdateItems):
return displayableGroupUpdateItemBuilder.displayableUpdateItemsForPrecomputed(
precomputedUpdateItems: precomputedUpdateItems.updateItems,
oldGroupModel: oldGroupModel,
localIdentifiers: localIdentifiers,
groupUpdateSourceAddress: groupUpdateSourceAddress,
updaterKnownToBeLocalUser: updaterKnownToBeLocalUser,
tx: tx
)
} else if let oldGroupModel {
/// If we don't have precomputed update items, but we do have group
/// models from before/after the group update, we can diff them to
/// compute the update items now.
return displayableGroupUpdateIemBuilder.displayableUpdateItemsByDiffingModels(
oldGroupModel: oldGroupModel,
newGroupModel: newGroupModel,
oldDisappearingMessageToken: oldDisappearingMessageToken,
newDisappearingMessageToken: newDisappearingMessageToken,
case let .newGroup(newGroupModel, updateMetadata):
return displayableGroupUpdateItemBuilder.displayableUpdateItemsForNewGroup(
newGroupModel: newGroupModel.groupModel,
newDisappearingMessageToken: newGroupModel.dmToken,
localIdentifiers: localIdentifiers,
groupUpdateSourceAddress: groupUpdateSourceAddress,
updaterKnownToBeLocalUser: updaterKnownToBeLocalUser,
groupUpdateSource: updateMetadata.source,
updaterKnownToBeLocalUser: updateMetadata.updaterWasLocalUser,
tx: tx
)
} else {
/// If we don't have precomputed update items, and there's no
/// "before" group models, this must be a new group and we can
/// compute update items based on the new models.
return displayableGroupUpdateIemBuilder.displayableUpdateItemsForNewGroup(
newGroupModel: newGroupModel,
newDisappearingMessageToken: newDisappearingMessageToken,
case let .modelDiff(oldGroupModel, newGroupModel, updateMetadata):
return displayableGroupUpdateItemBuilder.displayableUpdateItemsByDiffingModels(
oldGroupModel: oldGroupModel.groupModel,
newGroupModel: newGroupModel.groupModel,
oldDisappearingMessageToken: oldGroupModel.dmToken,
newDisappearingMessageToken: newGroupModel.dmToken,
localIdentifiers: localIdentifiers,
groupUpdateSourceAddress: groupUpdateSourceAddress,
updaterKnownToBeLocalUser: updaterWasLocalUser,
groupUpdateSource: updateMetadata.source,
updaterKnownToBeLocalUser: updateMetadata.updaterWasLocalUser,
tx: tx
)
}
@ -384,39 +391,109 @@ extension TSInfoMessage {
return groupModel
}
public var precomputedGroupUpdateItems: PersistableGroupUpdateItemsWrapper? {
return infoMessageValue(forKey: .groupUpdateItems)
public enum GroupUpdateMetadata {
public struct UpdateMetadata {
public let source: GroupUpdateSource
/// Whether we determined, at the time we created this info message, that
/// the updater was the local user.
/// - Returns
/// `true` if we knew conclusively that the updater was the local user, and
/// `false` otherwise.
public let updaterWasLocalUser: Bool
}
public struct GroupModel {
public let groupModel: TSGroupModel
public let dmToken: DisappearingMessageToken?
}
/// For legacy group updates we persisted a pre-rendered string, rather than the details
/// to generate that string.
case legacyRawString(String)
// For some time after we would persist the group state before and after
// (or just the new group state) along with other optional metadata.
// This will be soon unused at write time, but can still be present
// in the database as there was no migration done.
case newGroup(GroupModel, updateMetadata: UpdateMetadata)
case modelDiff(old: GroupModel, new: GroupModel, updateMetadata: UpdateMetadata)
/// Modern group updates are precomputed into an enum and stored with all necessary metadata
/// strongly typed, whether its a new group or an update to an existing group.
/// Group state is NOT stored with these types.
case precomputed(PersistableGroupUpdateItemsWrapper)
/// This is not a group update.
case nonGroupUpdate
}
public var oldGroupModel: TSGroupModel? {
return infoMessageValue(forKey: .oldGroupModel)
public func groupUpdateMetadata(localIdentifiers: LocalIdentifiers?) -> GroupUpdateMetadata {
if let precomputed: PersistableGroupUpdateItemsWrapper =
infoMessageValue(forKey: .groupUpdateItems)
{
return .precomputed(precomputed)
} else if let legacyPrecomputed: LegacyPersistableGroupUpdateItemsWrapper =
infoMessageValue(forKey: .legacyGroupUpdateItems)
{
let updateMetadata = self.updateMetadata
// Convert the legacy items into new items.
let mappedItems: [PersistableGroupUpdateItem] = legacyPrecomputed
.updateItems
.compactMap { legacyItem in
return legacyItem.toNewItem(
updater: updateMetadata.source,
oldGroupModel: infoMessageValue(forKey: .oldGroupModel),
localIdentifiers: localIdentifiers
)
}
return .precomputed(.init(mappedItems))
} else if
let newGroupModel: TSGroupModel = infoMessageValue(forKey: .newGroupModel)
{
let updateMetadata = self.updateMetadata
if let oldGroupModel: TSGroupModel = infoMessageValue(forKey: .oldGroupModel) {
return .modelDiff(
old: .init(
groupModel: oldGroupModel,
dmToken: infoMessageValue(forKey: .oldDisappearingMessageToken)
),
new: .init(
groupModel: newGroupModel,
dmToken: infoMessageValue(forKey: .newDisappearingMessageToken)
),
updateMetadata: updateMetadata
)
} else {
return .newGroup(
.init(
groupModel: newGroupModel,
dmToken: infoMessageValue(forKey: .newDisappearingMessageToken)
),
updateMetadata: updateMetadata
)
}
} else if let customMessage {
return .legacyRawString(customMessage)
} else {
if messageType == .typeGroupUpdate {
owsFailDebug("Group update should contain some metadata!")
}
return .nonGroupUpdate
}
}
public var newGroupModel: TSGroupModel? {
return infoMessageValue(forKey: .newGroupModel)
}
private var updateMetadata: GroupUpdateMetadata.UpdateMetadata {
let source = Self.groupUpdateSource(infoMessageUserInfoDict: infoMessageUserInfo)
// We grab this legacy value if we have it; its irrelevant for new persistable
// update items which know if they are from the local user or not.
let updaterWasLocalUser: Bool =
infoMessageValue(forKey: .legacyUpdaterKnownToBeLocalUser) ?? false
public var oldDisappearingMessageToken: DisappearingMessageToken? {
return infoMessageValue(forKey: .oldDisappearingMessageToken)
}
public var newDisappearingMessageToken: DisappearingMessageToken? {
return infoMessageValue(forKey: .newDisappearingMessageToken)
}
/// The address of the user to whom this update should be attributed, if
/// known.
public var groupUpdateSourceAddress: SignalServiceAddress? {
return infoMessageValue(forKey: .groupUpdateSourceAddress)
}
/// Whether we determined, at the time we created this info message, that
/// the updater was the local user.
/// - Returns
/// `true` if we knew conclusively that the updater was the local user, and
/// `false` otherwise.
public var updaterWasLocalUser: Bool {
return infoMessageValue(forKey: .updaterKnownToBeLocalUser) ?? false
return GroupUpdateMetadata.UpdateMetadata(
source: source,
updaterWasLocalUser: updaterWasLocalUser
)
}
fileprivate var profileChanges: ProfileChanges? {
@ -451,11 +528,17 @@ extension TSInfoMessage {
setInfoMessageValue(updateItemsWrapper, forKey: .groupUpdateItems)
}
public func setNewGroupModel(_ newGroupModel: TSGroupModel) {
setInfoMessageValue(newGroupModel, forKey: .newGroupModel)
}
public func setNewDisappearingMessageToken(_ newDisappearingMessageToken: DisappearingMessageToken) {
setInfoMessageValue(newDisappearingMessageToken, forKey: .newDisappearingMessageToken)
/// New group update info messages do not keep a copy of the group model on themselves. Older
/// instances do; this method updates the group model for older instances and leaves newer ones untouched.
public func setNewGroupModelForLegacyMessage(_ newGroupModel: TSGroupModel) {
// No need to pass local identifiers because we don't care about the
// contents, just what type of update metadata it is.
switch groupUpdateMetadata(localIdentifiers: nil) {
case .legacyRawString, .nonGroupUpdate, .precomputed:
owsFailDebug("Should not be updating group model!")
return
case .newGroup, .modelDiff:
setInfoMessageValue(newGroupModel, forKey: .newGroupModel)
}
}
}

View File

@ -38,13 +38,17 @@ typedef NS_CLOSED_ENUM(NSInteger, TSInfoMessageType) {
typedef NSString *InfoMessageUserInfoKey NS_STRING_ENUM;
extern InfoMessageUserInfoKey const InfoMessageUserInfoKeyLegacyGroupUpdateItems;
extern InfoMessageUserInfoKey const InfoMessageUserInfoKeyGroupUpdateItems;
extern InfoMessageUserInfoKey const InfoMessageUserInfoKeyOldGroupModel;
extern InfoMessageUserInfoKey const InfoMessageUserInfoKeyNewGroupModel;
extern InfoMessageUserInfoKey const InfoMessageUserInfoKeyOldDisappearingMessageToken;
extern InfoMessageUserInfoKey const InfoMessageUserInfoKeyNewDisappearingMessageToken;
extern InfoMessageUserInfoKey const InfoMessageUserInfoKeyGroupUpdateSourceAddress;
extern InfoMessageUserInfoKey const InfoMessageUserInfoKeyUpdaterKnownToBeLocalUser;
extern InfoMessageUserInfoKey const InfoMessageUserInfoKeyGroupUpdateSourceLegacyAddress;
extern InfoMessageUserInfoKey const InfoMessageUserInfoKeyGroupUpdateSourceType;
extern InfoMessageUserInfoKey const InfoMessageUserInfoKeyGroupUpdateSourceAciData;
extern InfoMessageUserInfoKey const InfoMessageUserInfoKeyGroupUpdateSourcePniData;
extern InfoMessageUserInfoKey const InfoMessageUserInfoKeyLegacyUpdaterKnownToBeLocalUser;
extern InfoMessageUserInfoKey const InfoMessageUserInfoKeyProfileChanges;
extern InfoMessageUserInfoKey const InfoMessageUserInfoKeyChangePhoneNumberAciString;
extern InfoMessageUserInfoKey const InfoMessageUserInfoKeyChangePhoneNumberOld;

View File

@ -10,16 +10,23 @@
NS_ASSUME_NONNULL_BEGIN
const InfoMessageUserInfoKey InfoMessageUserInfoKeyGroupUpdateItems = @"InfoMessageUserInfoKeyUpdateMessages";
const InfoMessageUserInfoKey InfoMessageUserInfoKeyLegacyGroupUpdateItems = @"InfoMessageUserInfoKeyUpdateMessages";
const InfoMessageUserInfoKey InfoMessageUserInfoKeyGroupUpdateItems = @"InfoMessageUserInfoKeyUpdateMessagesV2";
const InfoMessageUserInfoKey InfoMessageUserInfoKeyOldGroupModel = @"InfoMessageUserInfoKeyOldGroupModel";
const InfoMessageUserInfoKey InfoMessageUserInfoKeyNewGroupModel = @"InfoMessageUserInfoKeyNewGroupModel";
const InfoMessageUserInfoKey InfoMessageUserInfoKeyOldDisappearingMessageToken
= @"InfoMessageUserInfoKeyOldDisappearingMessageToken";
const InfoMessageUserInfoKey InfoMessageUserInfoKeyNewDisappearingMessageToken
= @"InfoMessageUserInfoKeyNewDisappearingMessageToken";
const InfoMessageUserInfoKey InfoMessageUserInfoKeyGroupUpdateSourceAddress
const InfoMessageUserInfoKey InfoMessageUserInfoKeyGroupUpdateSourceLegacyAddress
= @"InfoMessageUserInfoKeyGroupUpdateSourceAddress";
const InfoMessageUserInfoKey InfoMessageUserInfoKeyUpdaterKnownToBeLocalUser
const InfoMessageUserInfoKey InfoMessageUserInfoKeyGroupUpdateSourceType
= @"InfoMessageUserInfoKeyGroupUpdateSourceType";
const InfoMessageUserInfoKey InfoMessageUserInfoKeyGroupUpdateSourceAciData
= @"InfoMessageUserInfoKeyGroupUpdateSourceAci";
const InfoMessageUserInfoKey InfoMessageUserInfoKeyGroupUpdateSourcePniData
= @"InfoMessageUserInfoKeyGroupUpdateSourcePni";
const InfoMessageUserInfoKey InfoMessageUserInfoKeyLegacyUpdaterKnownToBeLocalUser
= @"InfoMessageUserInfoKeyUpdaterWasLocalUser";
const InfoMessageUserInfoKey InfoMessageUserInfoKeyProfileChanges = @"InfoMessageUserInfoKeyProfileChanges";
const InfoMessageUserInfoKey InfoMessageUserInfoKeyChangePhoneNumberAciString

View File

@ -348,6 +348,14 @@ extension OWSUserProfileBadgeInfo: DeepCopyable {
// MARK: -
@objc
extension TSInfoMessage.LegacyPersistableGroupUpdateItemsWrapper: DeepCopyable {
public func deepCopy() throws -> AnyObject {
// This class can use shallow copies.
return try DeepCopies.shallowCopy(self)
}
}
@objc
extension TSInfoMessage.PersistableGroupUpdateItemsWrapper: DeepCopyable {
public func deepCopy() throws -> AnyObject {
@ -364,6 +372,9 @@ extension NSObject {
if let string = value as? String {
return string
}
if let data = value as? Data {
return data
}
guard let deepCopyable = value as? DeepCopyable else {
owsFailDebug("Could not copy: \(value)")
return nil

View File

@ -7,6 +7,26 @@ import Foundation
import LibSignalClient
extension GroupManager {
/// Inserts an info message into the given thread for a new group.
public static func insertGroupUpdateInfoMessageForNewGroup(
localIdentifiers: LocalIdentifiers,
groupThread: TSGroupThread,
groupModel: TSGroupModel,
disappearingMessageToken: DisappearingMessageToken,
groupUpdateSource: GroupUpdateSource,
transaction: SDSAnyWriteTransaction
) {
DependenciesBridge.shared.groupUpdateInfoMessageInserter.insertGroupUpdateInfoMessageForNewGroup(
localIdentifiers: localIdentifiers,
groupThread: groupThread,
groupModel: groupModel,
disappearingMessageToken: disappearingMessageToken,
groupUpdateSource: groupUpdateSource,
transaction: transaction.asV2Write
)
}
/// Inserts an info message into the given thread describing how the thread
/// has been updated, given before/after models for the thread.
///
@ -15,12 +35,12 @@ extension GroupManager {
/// group update.
public static func insertGroupUpdateInfoMessage(
groupThread: TSGroupThread,
oldGroupModel: TSGroupModel?,
oldGroupModel: TSGroupModel,
newGroupModel: TSGroupModel,
oldDisappearingMessageToken: DisappearingMessageToken?,
oldDisappearingMessageToken: DisappearingMessageToken,
newDisappearingMessageToken: DisappearingMessageToken,
newlyLearnedPniToAciAssociations: [Pni: Aci],
groupUpdateSource: ServiceId?,
groupUpdateSource: GroupUpdateSource,
localIdentifiers: LocalIdentifiers,
transaction: SDSAnyWriteTransaction
) {

View File

@ -285,7 +285,7 @@ public class GroupManager: NSObject {
let thread = self.insertGroupThreadInDatabaseAndCreateInfoMessage(
groupModel: groupModel,
disappearingMessageToken: disappearingMessageToken,
groupUpdateSource: localIdentifiers.aci,
groupUpdateSource: .aci(localIdentifiers.aci),
localIdentifiers: localIdentifiers,
transaction: transaction
)
@ -416,7 +416,7 @@ public class GroupManager: NSObject {
return try remoteUpsertExistingGroupForTests(
groupModel: groupModel,
disappearingMessageToken: nil,
groupUpdateSource: localIdentifiers.aci,
groupUpdateSource: .aci(localIdentifiers.aci),
localIdentifiers: localIdentifiers,
transaction: transaction
).groupThread
@ -426,7 +426,7 @@ public class GroupManager: NSObject {
private static func remoteUpsertExistingGroupForTests(
groupModel: TSGroupModel,
disappearingMessageToken: DisappearingMessageToken?,
groupUpdateSource: ServiceId?,
groupUpdateSource: GroupUpdateSource,
infoMessagePolicy: InfoMessagePolicy = .always,
localIdentifiers: LocalIdentifiers,
transaction: SDSAnyWriteTransaction
@ -859,7 +859,7 @@ public class GroupManager: NSObject {
groupModelBuilder.groupMembership = groupMembershipBuilder.build()
let newGroupModel = try groupModelBuilder.build()
// groupUpdateSource is nil because we don't (and can't) know who removed
// groupUpdateSource is unknown because we don't (and can't) know who removed
// us or revoked our invite.
//
// newDisappearingMessageToken is nil because we don't want to change DM
@ -868,7 +868,7 @@ public class GroupManager: NSObject {
newGroupModel: newGroupModel,
newDisappearingMessageToken: nil,
newlyLearnedPniToAciAssociations: [:],
groupUpdateSource: nil,
groupUpdateSource: .unknown,
infoMessagePolicy: .always,
localIdentifiers: localIdentifiers,
transaction: transaction
@ -968,7 +968,7 @@ public class GroupManager: NSObject {
public static func insertGroupThreadInDatabaseAndCreateInfoMessage(
groupModel: TSGroupModel,
disappearingMessageToken: DisappearingMessageToken?,
groupUpdateSource: ServiceId?,
groupUpdateSource: GroupUpdateSource,
infoMessagePolicy: InfoMessagePolicy = .always,
localIdentifiers: LocalIdentifiers,
transaction: SDSAnyWriteTransaction
@ -991,7 +991,7 @@ public class GroupManager: NSObject {
token: newDisappearingMessageToken,
thread: groupThread,
shouldInsertInfoMessage: false,
changeAuthor: groupUpdateSource,
changeAuthor: groupUpdateSource.serviceId(),
localIdentifiers: localIdentifiers,
transaction: transaction
)
@ -1006,15 +1006,12 @@ public class GroupManager: NSObject {
switch infoMessagePolicy {
case .always, .insertsOnly:
insertGroupUpdateInfoMessage(
groupThread: groupThread,
oldGroupModel: nil,
newGroupModel: groupModel,
oldDisappearingMessageToken: nil,
newDisappearingMessageToken: newDisappearingMessageToken,
newlyLearnedPniToAciAssociations: [:],
groupUpdateSource: groupUpdateSource,
insertGroupUpdateInfoMessageForNewGroup(
localIdentifiers: localIdentifiers,
groupThread: groupThread,
groupModel: groupModel,
disappearingMessageToken: newDisappearingMessageToken,
groupUpdateSource: groupUpdateSource,
transaction: transaction
)
default:
@ -1039,7 +1036,7 @@ public class GroupManager: NSObject {
newGroupModel: TSGroupModel,
newDisappearingMessageToken: DisappearingMessageToken?,
newlyLearnedPniToAciAssociations: [Pni: Aci],
groupUpdateSource: ServiceId?,
groupUpdateSource: GroupUpdateSource,
canInsert: Bool,
didAddLocalUserToV2Group: Bool,
infoMessagePolicy: InfoMessagePolicy = .always,
@ -1075,7 +1072,7 @@ public class GroupManager: NSObject {
let thread = insertGroupThreadInDatabaseAndCreateInfoMessage(
groupModel: newGroupModel,
disappearingMessageToken: newDisappearingMessageToken,
groupUpdateSource: shouldAttributeAuthor ? groupUpdateSource : nil,
groupUpdateSource: shouldAttributeAuthor ? groupUpdateSource : .unknown,
infoMessagePolicy: infoMessagePolicy,
localIdentifiers: localIdentifiers,
transaction: transaction
@ -1106,7 +1103,7 @@ public class GroupManager: NSObject {
newGroupModel: TSGroupModel,
newDisappearingMessageToken: DisappearingMessageToken?,
newlyLearnedPniToAciAssociations: [Pni: Aci],
groupUpdateSource: ServiceId?,
groupUpdateSource: GroupUpdateSource,
infoMessagePolicy: InfoMessagePolicy = .always,
localIdentifiers: LocalIdentifiers,
transaction: SDSAnyWriteTransaction
@ -1133,7 +1130,7 @@ public class GroupManager: NSObject {
token: newDisappearingMessageToken,
thread: groupThread,
shouldInsertInfoMessage: false,
changeAuthor: groupUpdateSource,
changeAuthor: groupUpdateSource.serviceId(),
localIdentifiers: localIdentifiers,
transaction: transaction
)
@ -1349,11 +1346,11 @@ public class GroupManager: NSObject {
private static func autoWhitelistGroupIfNecessary(
oldGroupModel: TSGroupModel?,
newGroupModel: TSGroupModel,
groupUpdateSource: ServiceId?,
groupUpdateSource: GroupUpdateSource,
localIdentifiers: LocalIdentifiers,
tx: SDSAnyWriteTransaction
) {
guard let groupUpdateSource else {
guard let groupUpdateSource = groupUpdateSource.serviceId() else {
return
}

View File

@ -20,7 +20,7 @@ extension GroupUpdateInfoMessageInserterImpl {
func handlePossiblyCollapsibleMembershipChange(
precomputedUpdateType: PrecomputedUpdateType,
localAci: Aci,
localIdentifiers: LocalIdentifiers,
groupThread: TSGroupThread,
oldGroupModel: TSGroupModel,
newGroupModel: TSGroupModel,
@ -37,21 +37,20 @@ extension GroupUpdateInfoMessageInserterImpl {
return nil
}
owsAssertDebug(mostRecentInfoMsg.newGroupModel == oldGroupModel)
switch precomputedUpdateType {
case .newJoinRequestFromSingleUser(let requestingAci):
guard localAci != requestingAci else {
guard localIdentifiers.aci != requestingAci else {
return nil
}
return maybeUpdate(
mostRecentInfoMsg: mostRecentInfoMsg,
withNewJoinRequestFrom: requestingAci,
localIdentifiers: localIdentifiers,
transaction: transaction
)
case .canceledJoinRequestFromSingleUser(let cancelingAci):
guard localAci != cancelingAci else {
guard localIdentifiers.aci != cancelingAci else {
return nil
}
@ -60,16 +59,23 @@ extension GroupUpdateInfoMessageInserterImpl {
andSecondMostRecentInfoMsg: secondMostRecentInfoMsgMaybe,
withCanceledJoinRequestFrom: cancelingAci,
newGroupModel: newGroupModel,
localIdentifiers: localIdentifiers,
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)
switch mostRecentInfoMsg.groupUpdateMetadata(localIdentifiers: localIdentifiers) {
case .legacyRawString, .nonGroupUpdate, .precomputed:
// Nothing to do for these types; they don't keep the group
// model on themselves.
return .updatesCollapsedIntoExistingMessage
case .newGroup, .modelDiff:
// 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.setNewGroupModelForLegacyMessage(newGroupModel)
mostRecentInfoMsg.anyUpsert(transaction: transaction)
}
return .updatesCollapsedIntoExistingMessage
case
@ -82,6 +88,7 @@ extension GroupUpdateInfoMessageInserterImpl {
private func maybeUpdate(
mostRecentInfoMsg: TSInfoMessage,
withNewJoinRequestFrom requestingAci: Aci,
localIdentifiers: LocalIdentifiers,
transaction: SDSAnyWriteTransaction
) -> CollapsibleMembershipChangeResult? {
@ -93,10 +100,18 @@ extension GroupUpdateInfoMessageInserterImpl {
// Note that the new message might get collapsed further (into the
// most recent message) in the future.
let mostRecentUpdateItem: TSInfoMessage.PersistableGroupUpdateItem?
switch mostRecentInfoMsg.groupUpdateMetadata(localIdentifiers: localIdentifiers) {
case .precomputed(let precomputedItems):
mostRecentUpdateItem = precomputedItems.asSingleUpdateItem
case .modelDiff, .legacyRawString, .newGroup, .nonGroupUpdate:
return nil
}
guard
let mostRecentUpdateItem = mostRecentInfoMsg.precomputedGroupUpdateItems?.asSingleUpdateItem,
case let .sequenceOfInviteLinkRequestAndCancels(count, isTail) = mostRecentUpdateItem,
requestingAci == mostRecentInfoMsg.groupUpdateSourceAddress?.serviceId
let mostRecentUpdateItem,
case let .sequenceOfInviteLinkRequestAndCancels(requester, count, isTail) = mostRecentUpdateItem,
requestingAci == requester.wrappedValue
else {
return nil
}
@ -104,12 +119,20 @@ extension GroupUpdateInfoMessageInserterImpl {
owsAssertDebug(isTail)
mostRecentInfoMsg.setSingleUpdateItem(
singleUpdateItem: .sequenceOfInviteLinkRequestAndCancels(count: count, isTail: false)
singleUpdateItem: .sequenceOfInviteLinkRequestAndCancels(
requester: requestingAci.codableUuid,
count: count,
isTail: false
)
)
mostRecentInfoMsg.anyUpsert(transaction: transaction)
return .updateItemForNewMessage(
.sequenceOfInviteLinkRequestAndCancels(count: 0, isTail: true)
.sequenceOfInviteLinkRequestAndCancels(
requester: requestingAci.codableUuid,
count: 0,
isTail: true
)
)
}
@ -118,6 +141,7 @@ extension GroupUpdateInfoMessageInserterImpl {
andSecondMostRecentInfoMsg secondMostRecentInfoMsg: TSInfoMessage?,
withCanceledJoinRequestFrom cancelingAci: Aci,
newGroupModel: TSGroupModel,
localIdentifiers: LocalIdentifiers,
transaction: SDSAnyWriteTransaction
) -> CollapsibleMembershipChangeResult? {
@ -130,37 +154,48 @@ extension GroupUpdateInfoMessageInserterImpl {
// message.
guard
let mostRecentInfoMsgJoiner = mostRecentInfoMsg.representsSingleRequestToJoin(),
let mostRecentInfoMsgJoiner = mostRecentInfoMsg.representsSingleRequestToJoin(
localIdentifiers: localIdentifiers
),
cancelingAci == mostRecentInfoMsgJoiner
else {
return nil
}
if
let secondMostRecentInfoMsg = secondMostRecentInfoMsg,
let updateItem = secondMostRecentInfoMsg.precomputedGroupUpdateItems?.asSingleUpdateItem,
case let .sequenceOfInviteLinkRequestAndCancels(count, isTail) = updateItem,
cancelingAci == secondMostRecentInfoMsg.groupUpdateSourceAddress?.serviceId
{
switch mostRecentInfoMsg.precomputedGroupUpdateItems?.asSingleUpdateItem {
case .sequenceOfInviteLinkRequestAndCancels(0, true)?: break
default: owsFailDebug("Unexpected state for most recent info message")
}
let secondMostRecentUpdateItem: TSInfoMessage.PersistableGroupUpdateItem?
switch secondMostRecentInfoMsg?.groupUpdateMetadata(localIdentifiers: localIdentifiers) {
case .precomputed(let precomputedItems):
secondMostRecentUpdateItem = precomputedItems.asSingleUpdateItem
case .modelDiff, .legacyRawString, .newGroup, .nonGroupUpdate, .none:
secondMostRecentUpdateItem = nil
}
if
let secondMostRecentInfoMsg,
let secondMostRecentUpdateItem,
case let .sequenceOfInviteLinkRequestAndCancels(requester, count, isTail) = secondMostRecentUpdateItem,
cancelingAci == requester.wrappedValue
{
mostRecentInfoMsg.anyRemove(transaction: transaction)
owsAssertDebug(!isTail)
secondMostRecentInfoMsg.setNewGroupModel(newGroupModel)
secondMostRecentInfoMsg.setSingleUpdateItem(
singleUpdateItem: .sequenceOfInviteLinkRequestAndCancels(count: count + 1, isTail: true)
singleUpdateItem: .sequenceOfInviteLinkRequestAndCancels(
requester: cancelingAci.codableUuid,
count: count + 1,
isTail: true
)
)
secondMostRecentInfoMsg.anyUpsert(transaction: transaction)
return .updatesCollapsedIntoExistingMessage
} else {
mostRecentInfoMsg.setNewGroupModel(newGroupModel)
mostRecentInfoMsg.setSingleUpdateItem(
singleUpdateItem: .sequenceOfInviteLinkRequestAndCancels(count: 1, isTail: true)
singleUpdateItem: .sequenceOfInviteLinkRequestAndCancels(
requester: cancelingAci.codableUuid,
count: 1,
isTail: true
)
)
mostRecentInfoMsg.anyUpsert(transaction: transaction)
@ -208,7 +243,7 @@ extension GroupUpdateInfoMessageInserterImpl {
// MARK: TSInfoMessage extension
private extension TSInfoMessage.PersistableGroupUpdateItemsWrapper {
public extension TSInfoMessage.PersistableGroupUpdateItemsWrapper {
var asSingleUpdateItem: TSInfoMessage.PersistableGroupUpdateItem? {
guard updateItems.count == 1 else {
return nil
@ -223,26 +258,53 @@ private extension TSInfoMessage {
setGroupUpdateItemsWrapper(PersistableGroupUpdateItemsWrapper([singleUpdateItem]))
}
func representsSingleRequestToJoin() -> Aci? {
guard oldDisappearingMessageToken == newDisappearingMessageToken else {
func representsSingleRequestToJoin(localIdentifiers: LocalIdentifiers) -> Aci? {
switch groupUpdateMetadata(localIdentifiers: localIdentifiers) {
case .newGroup, .nonGroupUpdate, .legacyRawString:
return nil
}
case .precomputed(let precomputedItems):
switch precomputedItems.asSingleUpdateItem {
case .none,
.invitedPniPromotedToFullMemberAci,
.localUserDeclinedInviteFromInviter,
.localUserDeclinedInviteFromUnknownUser,
.otherUserDeclinedInviteFromLocalUser,
.otherUserDeclinedInviteFromInviter,
.otherUserDeclinedInviteFromUnknownUser,
.unnamedUserDeclinedInviteFromInviter,
.unnamedUserDeclinedInviteFromUnknownUser,
.localUserInviteRevoked,
.localUserInviteRevokedByUnknownUser,
.otherUserInviteRevokedByLocalUser,
.unnamedUserInvitesWereRevokedByLocalUser,
.unnamedUserInvitesWereRevokedByOtherUser,
.unnamedUserInvitesWereRevokedByUnknownUser:
return nil
case let .sequenceOfInviteLinkRequestAndCancels(requester, count, isTail):
guard isTail, count == 0 else {
return nil
}
return requester.wrappedValue
}
case let .modelDiff(oldGroupModel, newGroupModel, updateMetadata):
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
}
guard oldGroupModel.dmToken == newGroupModel.dmToken else {
return nil
}
return requestingAci
guard
let membershipEvent = GroupUpdateInfoMessageInserterImpl.PrecomputedUpdateType.from(
oldGroupMembership: oldGroupModel.groupModel.groupMembership,
newGroupMembership: newGroupModel.groupModel.groupMembership,
newlyLearnedPniToAciAssociations: [:]
),
case .newJoinRequestFromSingleUser(let requestingAci) = membershipEvent,
requestingAci == updateMetadata.source.serviceId()
else {
return nil
}
return requestingAci
}
}
}

View File

@ -7,15 +7,24 @@ import Foundation
import LibSignalClient
public protocol GroupUpdateInfoMessageInserter {
func insertGroupUpdateInfoMessageForNewGroup(
localIdentifiers: LocalIdentifiers,
groupThread: TSGroupThread,
groupModel: TSGroupModel,
disappearingMessageToken: DisappearingMessageToken,
groupUpdateSource: GroupUpdateSource,
transaction: DBWriteTransaction
)
func insertGroupUpdateInfoMessage(
localIdentifiers: LocalIdentifiers,
groupThread: TSGroupThread,
oldGroupModel: TSGroupModel?,
oldGroupModel: TSGroupModel,
newGroupModel: TSGroupModel,
oldDisappearingMessageToken: DisappearingMessageToken?,
oldDisappearingMessageToken: DisappearingMessageToken,
newDisappearingMessageToken: DisappearingMessageToken,
newlyLearnedPniToAciAssociations: [Pni: Aci],
groupUpdateSource: ServiceId?,
groupUpdateSource: GroupUpdateSource,
transaction: DBWriteTransaction
)
}
@ -27,7 +36,52 @@ class GroupUpdateInfoMessageInserterImpl: GroupUpdateInfoMessageInserter {
self.notificationsManager = notificationsManager
}
func insertGroupUpdateInfoMessage(
public func insertGroupUpdateInfoMessageForNewGroup(
localIdentifiers: LocalIdentifiers,
groupThread: TSGroupThread,
groupModel: TSGroupModel,
disappearingMessageToken: DisappearingMessageToken,
groupUpdateSource: GroupUpdateSource,
transaction v2Transaction: DBWriteTransaction
) {
_insertGroupUpdateInfoMessage(
localIdentifiers: localIdentifiers,
groupThread: groupThread,
oldGroupModel: nil,
newGroupModel: groupModel,
oldDisappearingMessageToken: nil,
newDisappearingMessageToken: disappearingMessageToken,
newlyLearnedPniToAciAssociations: [:],
groupUpdateSource: groupUpdateSource,
transaction: v2Transaction
)
}
public func insertGroupUpdateInfoMessage(
localIdentifiers: LocalIdentifiers,
groupThread: TSGroupThread,
oldGroupModel: TSGroupModel,
newGroupModel: TSGroupModel,
oldDisappearingMessageToken: DisappearingMessageToken,
newDisappearingMessageToken: DisappearingMessageToken,
newlyLearnedPniToAciAssociations: [Pni: Aci],
groupUpdateSource: GroupUpdateSource,
transaction v2Transaction: DBWriteTransaction
) {
_insertGroupUpdateInfoMessage(
localIdentifiers: localIdentifiers,
groupThread: groupThread,
oldGroupModel: oldGroupModel,
newGroupModel: newGroupModel,
oldDisappearingMessageToken: oldDisappearingMessageToken,
newDisappearingMessageToken: newDisappearingMessageToken,
newlyLearnedPniToAciAssociations: newlyLearnedPniToAciAssociations,
groupUpdateSource: groupUpdateSource,
transaction: v2Transaction
)
}
private func _insertGroupUpdateInfoMessage(
localIdentifiers: LocalIdentifiers,
groupThread: TSGroupThread,
oldGroupModel: TSGroupModel?,
@ -35,7 +89,7 @@ class GroupUpdateInfoMessageInserterImpl: GroupUpdateInfoMessageInserter {
oldDisappearingMessageToken: DisappearingMessageToken?,
newDisappearingMessageToken: DisappearingMessageToken,
newlyLearnedPniToAciAssociations: [Pni: Aci],
groupUpdateSource: ServiceId?,
groupUpdateSource: GroupUpdateSource,
transaction v2Transaction: DBWriteTransaction
) {
let transaction = SDSDB.shimOnlyBridge(v2Transaction)
@ -59,7 +113,7 @@ class GroupUpdateInfoMessageInserterImpl: GroupUpdateInfoMessageInserter {
.bannedMemberChange:
switch handlePossiblyCollapsibleMembershipChange(
precomputedUpdateType: precomputedUpdateType,
localAci: localIdentifiers.aci,
localIdentifiers: localIdentifiers,
groupThread: groupThread,
oldGroupModel: oldGroupModel,
newGroupModel: newGroupModel,
@ -77,17 +131,62 @@ class GroupUpdateInfoMessageInserterImpl: GroupUpdateInfoMessageInserter {
case .invitedPnisPromotedToFullMemberAcis(let promotions):
for (pni, aci) in promotions {
updateItemsForNewMessage.append(
.invitedPniPromotedToFullMemberAci(pni: pni.codableUuid, aci: aci.codableUuid)
.invitedPniPromotedToFullMemberAci(
newMember: aci.codableUuid,
inviter: oldGroupModel.groupMembership.addedByAci(
forInvitedMember: .init(pni)
)?.codableUuid
)
)
}
case .invitesRemoved(let inviteeServiceIds):
for removedInviteServiceId in inviteeServiceIds {
// Maps from the remover's aci (or unknown) to count.
var unnamedInvitesRemoved = [Aci?: Int]()
var inviteesRemovedByLocalUser = [ServiceId]()
for inviteeServiceId in inviteeServiceIds {
let item = Self.persistableUpdateForRemovedInvite(
inviteeServiceId: inviteeServiceId,
groupUpdateSource: groupUpdateSource,
oldGroupMembership: oldGroupModel.groupMembership,
localIdentifiers: localIdentifiers,
unnamedInvitesRemoved: &unnamedInvitesRemoved,
inviteesRemovedByLocalUser: &inviteesRemovedByLocalUser
)
if let item {
updateItemsForNewMessage.append(item)
}
}
if inviteesRemovedByLocalUser.count == 1 {
updateItemsForNewMessage.append(
.inviteRemoved(
invitee: removedInviteServiceId.codableUppercaseString,
wasLocalUser: localIdentifiers.contains(serviceId: removedInviteServiceId)
.otherUserInviteRevokedByLocalUser(
invitee: inviteesRemovedByLocalUser[0].codableUppercaseString
)
)
} else if inviteesRemovedByLocalUser.count > 1 {
updateItemsForNewMessage.append(
.unnamedUserInvitesWereRevokedByLocalUser(
count: UInt(inviteesRemovedByLocalUser.count)
)
)
}
for (removerAci, removedInviteCount) in unnamedInvitesRemoved {
guard removedInviteCount > 0 else {
continue
}
if let removerAci {
updateItemsForNewMessage.append(
.unnamedUserInvitesWereRevokedByOtherUser(
updaterAci: removerAci.codableUuid,
count: UInt(removedInviteCount)
)
)
} else {
updateItemsForNewMessage.append(
.unnamedUserInvitesWereRevokedByUnknownUser(
count: UInt(removedInviteCount)
)
)
}
}
}
}
@ -105,18 +204,11 @@ class GroupUpdateInfoMessageInserterImpl: GroupUpdateInfoMessageInserter {
userInfoForNewMessage[.oldDisappearingMessageToken] = oldDisappearingMessageToken
}
if let groupUpdateSource {
// If we know to whom this update should be attributed, record it.
// Additionally record whether, while processing, we know that the
// local user is the updater. This works around scenarios in which
// the updating address may refer to a different user in the future,
// such as a PNI moving from account to account.
userInfoForNewMessage[.groupUpdateSourceAddress] = SignalServiceAddress(groupUpdateSource)
userInfoForNewMessage[.updaterKnownToBeLocalUser] = localIdentifiers.contains(serviceId: groupUpdateSource)
} else {
userInfoForNewMessage[.updaterKnownToBeLocalUser] = false
}
TSInfoMessage.insertGroupUpdateSource(
groupUpdateSource,
intoInfoMessageUserInfoDict: &userInfoForNewMessage,
localIdentifiers: localIdentifiers
)
if !updateItemsForNewMessage.isEmpty {
userInfoForNewMessage[.groupUpdateItems] = TSInfoMessage.PersistableGroupUpdateItemsWrapper(updateItemsForNewMessage)
@ -132,7 +224,18 @@ class GroupUpdateInfoMessageInserterImpl: GroupUpdateInfoMessageInserter {
let wasLocalUserInGroup = oldGroupModel?.groupMembership.isLocalUserMemberOfAnyKind ?? false
let isLocalUserInGroup = newGroupModel.groupMembership.isLocalUserMemberOfAnyKind
if let groupUpdateSource, localIdentifiers.contains(serviceId: groupUpdateSource) {
let isLocalUserUpdate: Bool
switch groupUpdateSource {
case .unknown:
isLocalUserUpdate = false
case .legacyE164(let e164):
isLocalUserUpdate = localIdentifiers.contains(phoneNumber: e164)
case .aci(let aci):
isLocalUserUpdate = localIdentifiers.contains(serviceId: aci)
case .rejectedInviteToPni(let pni):
isLocalUserUpdate = localIdentifiers.contains(serviceId: pni)
}
if isLocalUserUpdate {
infoMessage.markAsRead(
atTimestamp: NSDate.ows_millisecondTimeStamp(),
thread: groupThread,
@ -150,4 +253,103 @@ class GroupUpdateInfoMessageInserterImpl: GroupUpdateInfoMessageInserter {
)
}
}
private static func persistableUpdateForRemovedInvite(
inviteeServiceId: ServiceId,
groupUpdateSource: GroupUpdateSource,
oldGroupMembership: GroupMembership,
localIdentifiers: LocalIdentifiers,
unnamedInvitesRemoved: inout [Aci?: Int],
inviteesRemovedByLocalUser: inout [ServiceId]
) -> TSInfoMessage.PersistableGroupUpdateItem? {
let inviteeIsLocalUser = localIdentifiers.contains(
serviceId: inviteeServiceId
)
let inviterAci = oldGroupMembership.addedByAci(
forInvitedMember: .init(inviteeServiceId)
)
let inviterIsLocalUser = inviterAci == localIdentifiers.aci
switch groupUpdateSource {
case .unknown:
if inviteeIsLocalUser {
return .localUserInviteRevokedByUnknownUser
} else {
unnamedInvitesRemoved[nil] = (unnamedInvitesRemoved[nil] ?? 0) + 1
return nil
}
case .legacyE164:
owsFailDebug("Cannot remove invites from an e164")
return nil
case .aci(let revokerAci):
let revokerIsLocalUser = revokerAci == localIdentifiers.aci
if inviteeIsLocalUser {
if revokerIsLocalUser {
if let inviterAci {
return .localUserDeclinedInviteFromInviter(
inviterAci: inviterAci.codableUuid
)
} else {
return .localUserDeclinedInviteFromUnknownUser
}
} else {
return .localUserInviteRevoked(
revokerAci: revokerAci.codableUuid
)
}
} else if revokerAci == inviteeServiceId {
if inviterIsLocalUser {
return .otherUserDeclinedInviteFromLocalUser(
invitee: inviteeServiceId.codableUppercaseString
)
} else if let inviterAci {
return .otherUserDeclinedInviteFromInviter(
invitee: inviteeServiceId.codableUppercaseString,
inviterAci: inviterAci.codableUuid
)
} else {
return .otherUserDeclinedInviteFromUnknownUser(
invitee: inviteeServiceId.codableUppercaseString
)
}
} else {
if revokerIsLocalUser {
inviteesRemovedByLocalUser.append(inviteeServiceId)
return nil
} else {
unnamedInvitesRemoved[revokerAci] =
(unnamedInvitesRemoved[revokerAci] ?? 0) + 1
return nil
}
}
case .rejectedInviteToPni(let revokerPni):
// Only two options: we rejected our own pni invite,
// or another user rejected their own pni invite.
if revokerPni == localIdentifiers.pni {
if let inviterAci {
return .localUserDeclinedInviteFromInviter(
inviterAci: inviterAci.codableUuid
)
} else {
return.localUserDeclinedInviteFromUnknownUser
}
} else {
if let inviterAci {
if inviterAci == localIdentifiers.aci {
return .otherUserDeclinedInviteFromLocalUser(
invitee: inviteeServiceId.codableUppercaseString
)
} else {
return.otherUserDeclinedInviteFromInviter(
invitee: inviteeServiceId.codableUppercaseString,
inviterAci: inviterAci.codableUuid
)
}
} else {
return .otherUserDeclinedInviteFromUnknownUser(
invitee: inviteeServiceId.codableUppercaseString
)
}
}
}
}
}

View File

@ -0,0 +1,169 @@
//
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import LibSignalClient
import XCTest
@testable import SignalServiceKit
class TSInfoMessageLegacyPersistableGroupUpdateItemTest: XCTestCase {
private typealias UpdateItem = TSInfoMessage.LegacyPersistableGroupUpdateItem
// MARK: - Hardcoded constant data
enum HardcodedDataTestMode {
case runTest
case printStrings
/// Toggle this to use ``testHardcodedJsonDataDecodes()`` to print
/// hardcoded strings, for example when adding new constants.
static let mode: Self = .runTest
}
func testHardcodedJsonDataDecodes() {
switch HardcodedDataTestMode.mode {
case .printStrings:
UpdateItem.printHardcodedJsonDataForConstants()
case .runTest:
for (idx, (constant, jsonData)) in UpdateItem.constants.enumerated() {
do {
let decoded = try JSONDecoder().decode(UpdateItem.self, from: jsonData)
try constant.validate(against: decoded)
} catch let error where error is DecodingError {
XCTFail("Failed to decode JSON model for constant \(idx): \(error)")
} catch ValidatableModelError.failedToValidate {
XCTFail("Failed to validate JSON-decoded model for constant \(idx)")
} catch {
XCTFail("Unexpected error for constant \(idx)")
}
}
}
}
func testConvertToNewUpdateItem() throws {
let updaterAci = Aci.randomForTesting()
for (constant, _) in UpdateItem.constants {
let expectedNewValue: TSInfoMessage.PersistableGroupUpdateItem
switch constant {
case let .sequenceOfInviteLinkRequestAndCancels(count, isTail):
expectedNewValue = .sequenceOfInviteLinkRequestAndCancels(
requester: updaterAci.codableUuid,
count: count,
isTail: isTail
)
case let .inviteRemoved(_, wasLocalUser):
if wasLocalUser {
expectedNewValue = .localUserInviteRevoked(
revokerAci: updaterAci.codableUuid
)
} else {
expectedNewValue = .unnamedUserInvitesWereRevokedByOtherUser(
updaterAci: updaterAci.codableUuid,
count: 1
)
}
case let .invitedPniPromotedToFullMemberAci(_, aci):
expectedNewValue = .invitedPniPromotedToFullMemberAci(
newMember: aci,
inviter: nil
)
}
guard let newValue = constant.toNewItem(
updater: .aci(updaterAci),
oldGroupModel: nil,
localIdentifiers: .init(
aci: .randomForTesting(),
pni: Pni.constantForTesting("PNI:7CE80DE3-6243-4AD5-AE60-0D1F205391DA"),
e164: .init("+15555555555")!
)
) else {
XCTFail("Should always be able to convert!")
return
}
try newValue.validate(against: expectedNewValue)
}
}
}
extension TSInfoMessage.LegacyPersistableGroupUpdateItem: ValidatableModel {
static var constants: [(TSInfoMessage.LegacyPersistableGroupUpdateItem, base64JsonData: Data)] {
[
(
.sequenceOfInviteLinkRequestAndCancels(count: 12, isTail: true),
Data(base64Encoded: "eyJzZXF1ZW5jZU9mSW52aXRlTGlua1JlcXVlc3RBbmRDYW5jZWxzIjp7ImNvdW50IjoxMiwiaXNUYWlsIjp0cnVlfX0=")!
),
(
.sequenceOfInviteLinkRequestAndCancels(count: 0, isTail: false),
Data(base64Encoded: "eyJzZXF1ZW5jZU9mSW52aXRlTGlua1JlcXVlc3RBbmRDYW5jZWxzIjp7ImNvdW50IjowLCJpc1RhaWwiOmZhbHNlfX0=")!
),
(
.invitedPniPromotedToFullMemberAci(
pni: Pni.constantForTesting("PNI:7CE80DE3-6243-4AD5-AE60-0D1F205391DA").codableUuid,
aci: Aci.constantForTesting("56EE0EF4-A7DF-4B52-BFAF-C637F15B4FEC").codableUuid
),
Data(base64Encoded: "eyJpbnZpdGVkUG5pUHJvbW90ZWRUb0Z1bGxNZW1iZXJBY2kiOnsicG5pIjoiN0NFODBERTMtNjI0My00QUQ1LUFFNjAtMEQxRjIwNTM5MURBIiwiYWNpIjoiNTZFRTBFRjQtQTdERi00QjUyLUJGQUYtQzYzN0YxNUI0RkVDIn19")!
),
(
.inviteRemoved(
invitee: Aci.constantForTesting("56EE0EF4-A7DF-4B52-BFAF-C637F15B4FEC").codableUppercaseString,
wasLocalUser: false
),
Data(base64Encoded: "eyJpbnZpdGVSZW1vdmVkIjp7Imludml0ZWUiOiI1NkVFMEVGNC1BN0RGLTRCNTItQkZBRi1DNjM3RjE1QjRGRUMiLCJ3YXNMb2NhbFVzZXIiOmZhbHNlfX0=")!
),
(
.inviteRemoved(
invitee: Pni.constantForTesting("PNI:7CE80DE3-6243-4AD5-AE60-0D1F205391DA").codableUppercaseString,
wasLocalUser: true
),
Data(base64Encoded: "eyJpbnZpdGVSZW1vdmVkIjp7Indhc0xvY2FsVXNlciI6dHJ1ZSwiaW52aXRlZSI6IlBOSTo3Q0U4MERFMy02MjQzLTRBRDUtQUU2MC0wRDFGMjA1MzkxREEifX0=")!
),
]
}
func validate(against: TSInfoMessage.LegacyPersistableGroupUpdateItem) throws {
var validated: Bool = false
switch (self, against) {
case let (
.sequenceOfInviteLinkRequestAndCancels(selfCount, selfIsTail),
.sequenceOfInviteLinkRequestAndCancels(againstCount, againstIsTail)
):
if
selfCount == againstCount,
selfIsTail == againstIsTail
{
validated = true
}
case let (
.invitedPniPromotedToFullMemberAci(selfPni, selfAci),
.invitedPniPromotedToFullMemberAci(againstPni, againstAci)
):
if
selfPni == againstPni,
selfAci == againstAci
{
validated = true
}
case let (
.inviteRemoved(selfInvitee, selfWasLocalUser),
.inviteRemoved(againstInvitee, againstWasLocalUser)
):
if
selfInvitee == againstInvitee,
selfWasLocalUser == againstWasLocalUser
{
validated = true
}
default:
break
}
guard validated else {
throw ValidatableModelError.failedToValidate
}
}
}

View File

@ -47,19 +47,27 @@ extension TSInfoMessage.PersistableGroupUpdateItem: ValidatableModel {
static var constants: [(TSInfoMessage.PersistableGroupUpdateItem, base64JsonData: Data)] {
[
(
.sequenceOfInviteLinkRequestAndCancels(count: 12, isTail: true),
Data(base64Encoded: "eyJzZXF1ZW5jZU9mSW52aXRlTGlua1JlcXVlc3RBbmRDYW5jZWxzIjp7ImNvdW50IjoxMiwiaXNUYWlsIjp0cnVlfX0=")!
.sequenceOfInviteLinkRequestAndCancels(
requester: Aci.constantForTesting("56EE0EF4-A7DF-4B52-BFAF-C637F15B4FEC").codableUuid,
count: 12,
isTail: true
),
Data(base64Encoded: "eyJzZXF1ZW5jZU9mSW52aXRlTGlua1JlcXVlc3RBbmRDYW5jZWxzIjp7ImNvdW50IjoxMiwiaXNUYWlsIjp0cnVlLCJyZXF1ZXN0ZXIiOiI1NkVFMEVGNC1BN0RGLTRCNTItQkZBRi1DNjM3RjE1QjRGRUMifX0=")!
),
(
.sequenceOfInviteLinkRequestAndCancels(count: 0, isTail: false),
Data(base64Encoded: "eyJzZXF1ZW5jZU9mSW52aXRlTGlua1JlcXVlc3RBbmRDYW5jZWxzIjp7ImNvdW50IjowLCJpc1RhaWwiOmZhbHNlfX0=")!
.sequenceOfInviteLinkRequestAndCancels(
requester: Aci.constantForTesting("56EE0EF4-A7DF-4B52-BFAF-C637F15B4FEC").codableUuid,
count: 0,
isTail: false),
Data(base64Encoded: "eyJzZXF1ZW5jZU9mSW52aXRlTGlua1JlcXVlc3RBbmRDYW5jZWxzIjp7ImlzVGFpbCI6ZmFsc2UsImNvdW50IjowLCJyZXF1ZXN0ZXIiOiI1NkVFMEVGNC1BN0RGLTRCNTItQkZBRi1DNjM3RjE1QjRGRUMifX0=")!
),
(
.invitedPniPromotedToFullMemberAci(
pni: Pni.constantForTesting("PNI:7CE80DE3-6243-4AD5-AE60-0D1F205391DA").codableUuid,
aci: Aci.constantForTesting("56EE0EF4-A7DF-4B52-BFAF-C637F15B4FEC").codableUuid
newMember: Aci.constantForTesting("56EE0EF4-A7DF-4B52-BFAF-C637F15B4FEC").codableUuid,
inviter: Aci.constantForTesting("56EE0EF4-A7DF-4B52-BFAF-C637F15B5FEE").codableUuid
),
Data(base64Encoded: "eyJpbnZpdGVkUG5pUHJvbW90ZWRUb0Z1bGxNZW1iZXJBY2kiOnsicG5pIjoiN0NFODBERTMtNjI0My00QUQ1LUFFNjAtMEQxRjIwNTM5MURBIiwiYWNpIjoiNTZFRTBFRjQtQTdERi00QjUyLUJGQUYtQzYzN0YxNUI0RkVDIn19")!
Data(base64Encoded: "eyJpbnZpdGVkUG5pUHJvbW90ZWRUb0Z1bGxNZW1iZXJBY2kiOnsibmV3TWVtYmVyIjoiNTZFRTBFRjQtQTdERi00QjUyLUJGQUYtQzYzN0YxNUI0RkVDIiwiaW52aXRlciI6IjU2RUUwRUY0LUE3REYtNEI1Mi1CRkFGLUM2MzdGMTVCNUZFRSJ9fQ==")!
)
]
}
@ -69,22 +77,40 @@ extension TSInfoMessage.PersistableGroupUpdateItem: ValidatableModel {
switch (self, against) {
case let (
.sequenceOfInviteLinkRequestAndCancels(selfCount, selfIsTail),
.sequenceOfInviteLinkRequestAndCancels(againstCount, againstIsTail)
.sequenceOfInviteLinkRequestAndCancels(selfRequester, selfCount, selfIsTail),
.sequenceOfInviteLinkRequestAndCancels(againstRequester, againstCount, againstIsTail)
):
if
selfRequester == againstRequester,
selfCount == againstCount,
selfIsTail == againstIsTail
{
validated = true
}
case let (
.invitedPniPromotedToFullMemberAci(selfPni, selfAci),
.invitedPniPromotedToFullMemberAci(againstPni, againstAci)
.invitedPniPromotedToFullMemberAci(selfNewMemberAci, selfInviter),
.invitedPniPromotedToFullMemberAci(againstNewMemberAci, againstInviter)
):
if
selfPni == againstPni,
selfAci == againstAci
selfNewMemberAci == againstNewMemberAci,
selfInviter == againstInviter
{
validated = true
}
case let (
.localUserInviteRevoked(selfRevokerAci),
.localUserInviteRevoked(againstRevokerAci)
):
if selfRevokerAci == againstRevokerAci {
validated = true
}
case let (
.unnamedUserInvitesWereRevokedByOtherUser(selfUpdaterAci, selfCount),
.unnamedUserInvitesWereRevokedByOtherUser(againstUpdaterAci, againstCount)
):
if
selfUpdaterAci == againstUpdaterAci,
selfCount == againstCount
{
validated = true
}