Model changes for group links.

This commit is contained in:
Matthew Chen 2020-08-20 16:42:41 -03:00
parent a0cd13ea38
commit aa88bb712c
36 changed files with 1231 additions and 615 deletions

View File

@ -103,11 +103,11 @@ extension ConversationViewController: MessageRequestDelegate {
return
}
let groupMembership = groupThread.groupModel.groupMembership
guard groupMembership.isPendingMember(localAddress) else {
guard groupMembership.isPendingProfileKeyMember(localAddress) else {
owsFailDebug("Can't reject invite if not pending.")
return
}
guard let addedByUuid = groupMembership.addedByUuid(forPendingMember: localAddress) else {
guard let addedByUuid = groupMembership.addedByUuid(forPendingProfileKeyMember: localAddress) else {
owsFailDebug("Missing addedByUuid.")
return
}
@ -193,7 +193,7 @@ extension ConversationViewController: MessageRequestDelegate {
var isMemberOfGroup = false
if let groupThread = thread as? TSGroupThread {
isMemberOfGroup = groupThread.isLocalUserInGroup
isMemberOfGroup = groupThread.isLocalUserMemberOfAnyKind
}
if isMemberOfGroup {
@ -232,7 +232,7 @@ extension ConversationViewController: MessageRequestDelegate {
}
guard let groupThread = thread as? TSGroupThread,
groupThread.isLocalUserPendingOrNonPendingMember else {
groupThread.isLocalUserFullOrInvitedMemberOfGroup else {
// If we don't need to leave the group, finish up immediately.
return completion()
}

View File

@ -557,7 +557,7 @@ typedef enum : NSUInteger {
}
TSGroupThread *groupThread = (TSGroupThread *)self.thread;
return !groupThread.isLocalUserInGroup;
return !groupThread.isLocalUserFullMemberOfGroup;
}
- (void)updateInputVisibility

View File

@ -56,7 +56,7 @@ class MessageRequestView: UIStackView {
if let groupThread = thread as? TSGroupThread,
groupThread.isGroupV2Thread {
self.mode = (groupThread.isLocalUserPendingMember
self.mode = (groupThread.isLocalUserPendingProfileKeyMember
? .groupInviteRequest
: .contactOrGroupRequest)
} else {
@ -262,7 +262,7 @@ class MessageRequestView: UIStackView {
return UILabel()
}
let groupMembership = groupThread.groupModel.groupMembership
guard let addedByUuid = groupMembership.addedByUuid(forPendingMember: localAddress) else {
guard let addedByUuid = groupMembership.addedByUuid(forPendingProfileKeyMember: localAddress) else {
owsFailDebug("missing addedByUuid")
return UILabel()
}

View File

@ -172,7 +172,7 @@ class DebugUIGroupsV2: DebugUIPage {
var defaultModelBuilder = TSGroupModelBuilder()
defaultModelBuilder.groupsVersion = groupsVersion
var defaultMembershipBuilder = GroupMembership.Builder()
defaultMembershipBuilder.addNonPendingMember(localAddress, role: .administrator)
defaultMembershipBuilder.addFullMember(localAddress, role: .administrator)
defaultModelBuilder.groupMembership = defaultMembershipBuilder.build()
let defaultModel = try defaultModelBuilder.build(transaction: transaction)
let defaultDMToken = DisappearingMessageToken.disabledToken
@ -240,10 +240,10 @@ class DebugUIGroupsV2: DebugUIPage {
modelBuilder2.name = "name 2"
modelBuilder2.avatarData = "avatar 2".data(using: .utf8)
var groupMembershipBuilder1 = model1.groupMembership.asBuilder
groupMembershipBuilder1.addNonPendingMember(otherAddress0, role: .normal)
groupMembershipBuilder1.addFullMember(otherAddress0, role: .normal)
if groupsVersion == .V2,
let updaterUuid = updaterAddress?.uuid {
groupMembershipBuilder1.addPendingMember(otherAddress1,
groupMembershipBuilder1.addPendingProfileKeyMember(otherAddress1,
role: .normal,
addedByUuid: updaterUuid)
}
@ -371,7 +371,7 @@ class DebugUIGroupsV2: DebugUIPage {
var groupMembershipBuilder1 = defaultModel.groupMembership.asBuilder
for member in members {
groupMembershipBuilder1.remove(member)
groupMembershipBuilder1.addNonPendingMember(member, role: .normal)
groupMembershipBuilder1.addFullMember(member, role: .normal)
}
modelBuilder1.groupMembership = groupMembershipBuilder1.build()
let model1 = try modelBuilder1.build(transaction: transaction)
@ -380,7 +380,7 @@ class DebugUIGroupsV2: DebugUIPage {
var groupMembershipBuilder2 = defaultModel.groupMembership.asBuilder
for member in members {
groupMembershipBuilder2.remove(member)
groupMembershipBuilder2.addNonPendingMember(member, role: .administrator)
groupMembershipBuilder2.addFullMember(member, role: .administrator)
}
modelBuilder2.groupMembership = groupMembershipBuilder2.build()
let model2 = try modelBuilder2.build(transaction: transaction)
@ -425,7 +425,7 @@ class DebugUIGroupsV2: DebugUIPage {
var groupMembershipBuilder2 = defaultModel.groupMembership.asBuilder
for member in members {
groupMembershipBuilder2.remove(member)
groupMembershipBuilder2.addNonPendingMember(member, role: .administrator)
groupMembershipBuilder2.addFullMember(member, role: .administrator)
}
modelBuilder2.groupMembership = groupMembershipBuilder2.build()
let model2 = try modelBuilder2.build(transaction: transaction)
@ -483,7 +483,7 @@ class DebugUIGroupsV2: DebugUIPage {
var groupMembershipBuilder2 = defaultModel.groupMembership.asBuilder
for member in members {
groupMembershipBuilder2.remove(member)
groupMembershipBuilder2.addPendingMember(member, role: .normal, addedByUuid: inviterUuid)
groupMembershipBuilder2.addPendingProfileKeyMember(member, role: .normal, addedByUuid: inviterUuid)
}
modelBuilder2.groupMembership = groupMembershipBuilder2.build()
let model2 = try modelBuilder2.build(transaction: transaction)
@ -493,7 +493,7 @@ class DebugUIGroupsV2: DebugUIPage {
var groupMembershipBuilder3 = defaultModel.groupMembership.asBuilder
for member in members {
groupMembershipBuilder3.remove(member)
groupMembershipBuilder3.addNonPendingMember(member, role: .administrator)
groupMembershipBuilder3.addFullMember(member, role: .administrator)
}
modelBuilder3.groupMembership = groupMembershipBuilder3.build()
let model3 = try modelBuilder3.build(transaction: transaction)
@ -540,7 +540,7 @@ class DebugUIGroupsV2: DebugUIPage {
let oldGroupMembership = oldGroupModel.groupMembership
var groupMembershipBuilder = oldGroupMembership.asBuilder
for address in oldGroupMembership.allUsers {
for address in oldGroupMembership.allMembersOfAnyKind {
if address != localAddress {
groupMembershipBuilder.remove(address)
}
@ -609,7 +609,7 @@ class DebugUIGroupsV2: DebugUIPage {
guard let groupModel = groupThread.groupModel as? TSGroupModelV2 else {
throw OWSAssertionError("Invalid groupModel.")
}
guard groupModel.groupMembership.isNonPendingMember(otherUserAddress) else {
guard groupModel.groupMembership.isFullMember(otherUserAddress) else {
throw OWSAssertionError("Other user is not a full member.")
}
// Last admin (local user) can't leave group, so first

View File

@ -121,12 +121,12 @@ public extension DebugUIStress {
var groupMembershipBuilder = oldGroupMembership.asBuilder
for address in membersToAdd {
assert(address.isValid)
guard !oldGroupMembership.isPendingOrNonPendingMember(address) else {
guard !oldGroupMembership.isMemberOfAnyKind(address) else {
Logger.warn("Recipient is already in group.")
continue
}
// GroupManager will separate out members as pending if necessary.
groupMembershipBuilder.addNonPendingMember(address, role: .normal)
groupMembershipBuilder.addFullMember(address, role: .normal)
}
builder.groupMembership = groupMembershipBuilder.build()
return try builder.build(transaction: transaction)

View File

@ -1671,7 +1671,7 @@ NSString *const kArchiveButtonPseudoGroup = @"kArchiveButtonPseudoGroup";
DatabaseStorageWrite(self.databaseStorage, ^(SDSAnyWriteTransaction *transaction) {
if ([thread isKindOfClass:[TSGroupThread class]]) {
TSGroupThread *groupThread = (TSGroupThread *)thread;
if (groupThread.isLocalUserInGroup || groupThread.isGroupV2Thread) {
if (groupThread.isLocalUserMemberOfAnyKind || groupThread.isGroupV2Thread) {
[groupThread softDeleteThreadWithTransaction:transaction];
} else {
[groupThread anyRemoveWithTransaction:transaction];

View File

@ -305,7 +305,7 @@ public class NewGroupConfirmViewController: OWSViewController {
self.presentingViewController?.dismiss(animated: true, completion: completion)
}
let pendingMembers = groupThread.groupModel.groupMembership.pendingMembers
let pendingMembers = groupThread.groupModel.groupMembership.pendingProfileKeyMembers
guard let firstPendingMember = pendingMembers.first else {
// No pending members.
return navigateToNewGroup(nil)

View File

@ -137,13 +137,13 @@ private extension AddGroupMembersViewController {
return nil
}
for address in addresses {
guard !oldGroupMembership.isNonPendingMember(address) else {
guard !oldGroupMembership.isFullMember(address) else {
owsFailDebug("Recipient is already in group.")
continue
}
// GroupManager will separate out members as pending if necessary.
groupMembershipBuilder.remove(address)
groupMembershipBuilder.addNonPendingMember(address, role: .normal)
groupMembershipBuilder.addFullMember(address, role: .normal)
}
builder.groupMembership = groupMembershipBuilder.build()
return try builder.build(transaction: transaction)
@ -248,7 +248,7 @@ extension AddGroupMembersViewController: GroupMemberViewDelegate {
}
func groupMemberViewGroupMemberCountForDisplay() -> Int {
return (oldGroupModel.groupMembership.pendingAndNonPendingMemberCount +
return (oldGroupModel.groupMembership.allMembersOfAnyKind.count +
newRecipientSet.count)
}
@ -270,11 +270,13 @@ extension AddGroupMembersViewController: GroupMemberViewDelegate {
owsFailDebug("Invalid recipient.")
return false
}
if oldGroupModel.groupMembership.isNonPendingMember(address) {
let groupMembership = oldGroupModel.groupMembership
if groupMembership.isFullMember(address) {
return true
}
if oldGroupModel.groupMembership.isPendingMember(address) {
// We can "add" pending members if they support gv2
if groupMembership.isPendingProfileKeyMember(address) ||
groupMembership.isRequestingMember(address) {
// We can "add" pending or requesting members if they support gv2
// and we know their profile key credential.
let canAddMember = databaseStorage.read { transaction -> Bool in
guard GroupManager.doesUserSupportGroupsV2(address: address, transaction: transaction) else {

View File

@ -147,7 +147,7 @@ public class AddToGroupViewController: OWSTableViewController {
return Environment.shared.contactsManager.shortDisplayName(for: self.address, transaction: transaction)
}
guard !groupThread.groupModel.groupMembership.isPendingOrNonPendingMember(address) else {
guard !groupThread.groupModel.groupMembership.isMemberOfAnyKind(address) else {
let toastFormat = NSLocalizedString(
"ADD_TO_GROUP_ALREADY_MEMBER_TOAST_FORMAT",
comment: "A toast on the 'add to group' view indicating the user is already a member. Embeds {contact name} and {group name}"
@ -220,12 +220,12 @@ public class AddToGroupViewController: OWSTableViewController {
let oldGroupMembership = oldGroupModel.groupMembership
var groupMembershipBuilder = oldGroupMembership.asBuilder
guard !oldGroupMembership.isPendingOrNonPendingMember(self.address) else {
guard !oldGroupMembership.isMemberOfAnyKind(self.address) else {
owsFailDebug("Recipient is already in group.")
return nil
}
// GroupManager will separate out members as pending if necessary.
groupMembershipBuilder.addNonPendingMember(self.address, role: .normal)
groupMembershipBuilder.addFullMember(self.address, role: .normal)
builder.groupMembership = groupMembershipBuilder.build()
return try builder.build(transaction: transaction)

View File

@ -199,7 +199,7 @@ extension ConversationSettingsViewController {
return
}
let isLocalUserInConversation = self.isLocalUserInConversation
let isLocalUserFullMemberOfGroup = self.isLocalUserFullMemberOfGroup
// For pre-message request threads, allow manually sharing your profile if the thread is not whitelisted.
let (isPreMessageRequestsThread, isThreadInProfileWhitelist) = databaseStorage.uiRead { transaction -> (Bool, Bool) in
@ -225,7 +225,7 @@ extension ConversationSettingsViewController {
let cell = OWSTableItem.buildDisclosureCell(name: title,
icon: .settingsProfile,
accessibilityIdentifier: UIView.accessibilityIdentifier(in: self, name: "share_profile"))
cell.isUserInteractionEnabled = isLocalUserInConversation
cell.isUserInteractionEnabled = isLocalUserFullMemberOfGroup
return cell
},
actionBlock: { [weak self] in
@ -472,7 +472,7 @@ extension ConversationSettingsViewController {
: NSLocalizedString("CONVERSATION_SETTINGS_BLOCK_AND_LEAVE_SECTION_CONTACT_FOOTER",
comment: "Footer text for the 'block and leave' section of contact conversation settings view.")
if isGroupThread, isLocalUserInConversation {
if isGroupThread, isLocalUserFullOrInvitedMemberOfGroup {
section.add(OWSTableItem(customCellBlock: { [weak self] in
guard let self = self else {
owsFailDebug("Missing self")
@ -592,7 +592,7 @@ extension ConversationSettingsViewController {
private func accessoryLabel(forAccess access: GroupV2Access) -> String {
switch access {
case .unknown, .any, .member:
case .any, .member:
if access != .member {
owsFailDebug("Invalid attributes access: \(access.rawValue)")
}
@ -600,7 +600,11 @@ extension ConversationSettingsViewController {
comment: "Label indicating that all group members can update the group's attributes: name, avatar, etc.")
case .administrator:
return NSLocalizedString("CONVERSATION_SETTINGS_ATTRIBUTES_ACCESS_ADMINISTRATOR",
comment: "Label indicating that only administrators can update the group's attributes: name, avatar, etc.")
comment: "Label indicating that only administrators can update the group's attributes: name, avatar, etc.")
case .unknown, .unsatisfiable:
owsFailDebug("Invalid access")
return NSLocalizedString("CONVERSATION_SETTINGS_ATTRIBUTES_ACCESS_NONE",
comment: "Label indicating that no member can update the group's attributes: name, avatar, etc.")
}
}
@ -650,7 +654,7 @@ extension ConversationSettingsViewController {
}
let groupMembership = groupModel.groupMembership
let allMembers = groupMembership.nonPendingMembers
let allMembers = groupMembership.fullMembers
var allMembersSorted = [SignalServiceAddress]()
var verificationStateMap = [SignalServiceAddress: OWSVerificationState]()
databaseStorage.uiRead { transaction in
@ -663,15 +667,15 @@ extension ConversationSettingsViewController {
}
var membersToRender = [SignalServiceAddress]()
if groupMembership.isNonPendingMember(localAddress) {
if groupMembership.isFullMember(localAddress) {
// Make sure local user is first.
membersToRender.insert(localAddress, at: 0)
}
// Admin users are second.
let adminMembers = allMembersSorted.filter { $0 != localAddress && groupMembership.isAdministrator($0) }
let adminMembers = allMembersSorted.filter { $0 != localAddress && groupMembership.isFullMemberAndAdministrator($0) }
membersToRender += adminMembers
// Non-admin users are third.
let nonAdminMembers = allMembersSorted.filter { $0 != localAddress && !groupMembership.isAdministrator($0) }
let nonAdminMembers = allMembersSorted.filter { $0 != localAddress && !groupMembership.isFullMemberAndAdministrator($0) }
membersToRender += nonAdminMembers
if membersToRender.count > 1 {
@ -708,7 +712,7 @@ extension ConversationSettingsViewController {
let cell = ContactTableViewCell()
cell.setUseSmallAvatars()
let isGroupAdmin = groupMembership.isAdministrator(memberAddress)
let isGroupAdmin = groupMembership.isFullMemberAndAdministrator(memberAddress)
let isVerified = verificationState == .verified
let isNoLongerVerified = verificationState == .noLongerVerified
let isBlocked = helper.isSignalServiceAddressBlocked(memberAddress)
@ -794,6 +798,7 @@ extension ConversationSettingsViewController {
return section
}
// TODO: Add requesting members section.
private func buildPendingMembersSection(groupModel: TSGroupModel,
localAddress: SignalServiceAddress) -> OWSTableSection {
let section = OWSTableSection()
@ -803,7 +808,7 @@ extension ConversationSettingsViewController {
// Only admins can revoke invites.
let canRevokeInvites = groupViewHelper.canRevokePendingInvites
let pendingMembers = groupModel.groupMembership.pendingMembers
let pendingMembers = groupModel.groupMembership.pendingProfileKeyMembers
let pendingInviteCount = (canRevokeInvites
? pendingMembers.count + groupModel.groupMembership.invalidInvites.count
: pendingMembers.count)
@ -815,7 +820,7 @@ extension ConversationSettingsViewController {
accessoryText = NSLocalizedString("CONVERSATION_SETTINGS_PENDING_MEMBER_INVITES_NONE",
comment: "Indicates that there are no pending member invites in the group.")
}
let isLocalUserFullMember = groupModel.groupMembership.isNonPendingMember(localAddress)
let isLocalUserFullMember = groupModel.groupMembership.isFullMember(localAddress)
section.add(OWSTableItem(customCellBlock: { [weak self] in
guard let self = self else {

View File

@ -172,7 +172,7 @@ extension ConversationSettingsViewController {
private func buildHeaderForGroup(groupThread: TSGroupThread) -> UIView {
var builder = HeaderBuilder(viewController: self)
let memberCount = groupThread.groupModel.groupMembership.nonPendingMembers.count
let memberCount = groupThread.groupModel.groupMembership.fullMembers.count
var groupMembersText = GroupViewUtils.formatGroupMembersLabel(memberCount: memberCount)
if RemoteConfig.groupsV2GoodCitizen,
groupThread.isGroupV1Thread {

View File

@ -135,8 +135,12 @@ class ConversationSettingsViewController: OWSTableViewController {
return groupViewHelper.canEditConversationAccess
}
var isLocalUserInConversation: Bool {
return groupViewHelper.isLocalUserInConversation
var isLocalUserFullMemberOfGroup: Bool {
return groupViewHelper.isLocalUserFullMemberOfGroup
}
var isLocalUserFullOrInvitedMemberOfGroup: Bool {
return groupViewHelper.isLocalUserFullOrInvitedMemberOfGroup
}
var isGroupThread: Bool {
@ -318,7 +322,7 @@ class ConversationSettingsViewController: OWSTableViewController {
// Thread no longer exists.
return false
}
guard latestThread.isLocalUserInGroup else {
guard latestThread.isLocalUserMemberOfAnyKind else {
// Local user is no longer in group, e.g. perhaps they just blocked it.
return false
}
@ -690,9 +694,8 @@ class ConversationSettingsViewController: OWSTableViewController {
return false
}
let groupMembership = groupModelV2.groupMembership
guard groupMembership.isNonPendingMember(localAddress),
groupMembership.isAdministrator(localAddress),
groupMembership.nonPendingAdministrators.count == 1 else {
guard groupMembership.isFullMemberAndAdministrator(localAddress),
groupMembership.fullMemberAdministrators.count == 1 else {
return false
}
return true
@ -710,7 +713,7 @@ class ConversationSettingsViewController: OWSTableViewController {
owsFailDebug("missing local address")
return []
}
var candidates = groupModelV2.groupMembership.nonPendingMembers
var candidates = groupModelV2.groupMembership.fullMembers
candidates.remove(localAddress)
return candidates
}

View File

@ -50,10 +50,10 @@ extension GroupViewHelper {
owsFailDebug("Missing localAddress.")
return false
}
let isLocalUserAdmin = groupThread.groupModel.groupMembership.isAdministrator(localAddress)
let isLocalUserAdmin = groupThread.groupModel.groupMembership.isFullMemberAndAdministrator(localAddress)
let groupMembership = groupThread.groupModel.groupMembership
let canBecomeAdmin = (groupMembership.isNonPendingMember(address) &&
!groupMembership.isAdministrator(address))
let canBecomeAdmin = (groupMembership.isFullMember(address) &&
!groupMembership.isFullMemberAndAdministrator(address))
return (canEditConversationMembership && isLocalUserAdmin && canBecomeAdmin)
}
@ -73,7 +73,7 @@ extension GroupViewHelper {
guard let oldGroupModel = delegate?.currentGroupModel as? TSGroupModelV2 else {
return Promise(error: OWSAssertionError("Missing group model."))
}
guard oldGroupModel.groupMembership.isPendingOrNonPendingMember(address) else {
guard oldGroupModel.groupMembership.isMemberOfAnyKind(address) else {
return Promise(error: OWSAssertionError("Not a group member."))
}
guard let uuid = address.uuid else {
@ -98,10 +98,9 @@ extension GroupViewHelper {
owsFailDebug("Missing localAddress.")
return false
}
let isLocalUserAdmin = groupThread.groupModel.groupMembership.isAdministrator(localAddress)
let groupMembership = groupThread.groupModel.groupMembership
let canRevokeAdmin = (groupMembership.isNonPendingMember(address) &&
groupMembership.isAdministrator(address))
let isLocalUserAdmin = groupMembership.isFullMemberAndAdministrator(localAddress)
let canRevokeAdmin = groupMembership.isFullMemberAndAdministrator(address)
return (canEditConversationMembership && isLocalUserAdmin && canRevokeAdmin)
}
@ -121,7 +120,7 @@ extension GroupViewHelper {
guard let oldGroupModel = delegate?.currentGroupModel as? TSGroupModelV2 else {
return Promise(error: OWSAssertionError("Missing group model."))
}
guard oldGroupModel.groupMembership.isPendingOrNonPendingMember(address) else {
guard oldGroupModel.groupMembership.isMemberOfAnyKind(address) else {
return Promise(error: OWSAssertionError("Not a group member."))
}
guard let uuid = address.uuid else {
@ -148,9 +147,9 @@ extension GroupViewHelper {
return false
}
// Only admins can kick out other members.
let isLocalUserAdmin = groupThread.groupModel.groupMembership.isAdministrator(localAddress)
let groupMembership = groupThread.groupModel.groupMembership
let isAddressInGroup = groupMembership.isPendingOrNonPendingMember(address)
let isLocalUserAdmin = groupMembership.isFullMemberAndAdministrator(localAddress)
let isAddressInGroup = groupMembership.isMemberOfAnyKind(address)
return canEditConversationMembership && isLocalUserAdmin && isAddressInGroup
}
@ -170,7 +169,7 @@ extension GroupViewHelper {
guard let oldGroupModel = delegate?.currentGroupModel as? TSGroupModelV2 else {
return Promise(error: OWSAssertionError("Missing group model."))
}
guard oldGroupModel.groupMembership.isPendingOrNonPendingMember(address) else {
guard oldGroupModel.groupMembership.isMemberOfAnyKind(address) else {
return Promise(error: OWSAssertionError("Not a group member."))
}
guard let uuid = address.uuid else {

View File

@ -61,7 +61,7 @@ class GroupViewHelper: NSObject {
if threadViewModel.hasPendingMessageRequest {
return false
}
guard isLocalUserInConversation else {
guard isLocalUserFullMemberOfGroup else {
return false
}
guard let groupThread = thread as? TSGroupThread else {
@ -85,12 +85,15 @@ class GroupViewHelper: NSObject {
case .unknown:
owsFailDebug("Unknown access.")
return false
case .unsatisfiable:
owsFailDebug("Invalid access.")
return false
case .any:
return true
case .member:
return groupModelV2.groupMembership.isNonPendingMember(localAddress)
return groupModelV2.groupMembership.isFullMember(localAddress)
case .administrator:
return (groupModelV2.groupMembership.isNonPendingMember(localAddress) && groupModelV2.groupMembership.isAdministrator(localAddress))
return (groupModelV2.groupMembership.isFullMemberAndAdministrator(localAddress))
}
}
@ -117,7 +120,7 @@ class GroupViewHelper: NSObject {
if threadViewModel.hasPendingMessageRequest {
return false
}
guard isLocalUserInConversation else {
guard isLocalUserFullMemberOfGroup else {
return false
}
guard let groupThread = thread as? TSGroupThread else {
@ -132,7 +135,7 @@ class GroupViewHelper: NSObject {
owsFailDebug("Missing localAddress.")
return false
}
return groupModelV2.groupMembership.isAdministrator(localAddress)
return groupModelV2.groupMembership.isFullMemberAndAdministrator(localAddress)
}
var canRevokePendingInvites: Bool {
@ -145,20 +148,24 @@ class GroupViewHelper: NSObject {
}
let groupMembership = groupThread.groupModel.groupMembership
return (!threadViewModel.hasPendingMessageRequest &&
groupMembership.isPendingOrNonPendingMember(localAddress) &&
groupMembership.isAdministrator(localAddress))
groupMembership.isFullMemberAndAdministrator(localAddress))
}
var canResendInvites: Bool {
return (!threadViewModel.hasPendingMessageRequest &&
isLocalUserInConversation)
return (!threadViewModel.hasPendingMessageRequest && isLocalUserFullMemberOfGroup)
}
var isLocalUserInConversation: Bool {
var isLocalUserFullMemberOfGroup: Bool {
guard let groupThread = thread as? TSGroupThread else {
return true
}
return groupThread.isLocalUserFullMemberOfGroup
}
return groupThread.isLocalUserInGroup
var isLocalUserFullOrInvitedMemberOfGroup: Bool {
guard let groupThread = thread as? TSGroupThread else {
return true
}
return groupThread.isLocalUserFullOrInvitedMemberOfGroup
}
}

View File

@ -69,7 +69,7 @@ public class PendingGroupMembersViewController: OWSTableViewController {
let groupMembership = groupModel.groupMembership
let allPendingMembersSorted = databaseStorage.uiRead { transaction in
self.contactsManager.sortSignalServiceAddresses(Array(groupMembership.pendingMembers),
self.contactsManager.sortSignalServiceAddresses(Array(groupMembership.pendingProfileKeyMembers),
transaction: transaction)
}
@ -77,7 +77,7 @@ public class PendingGroupMembersViewController: OWSTableViewController {
var membersInvitedByLocalUser = [SignalServiceAddress]()
var membersInvitedByOtherUsers = [SignalServiceAddress: [SignalServiceAddress]]()
for invitedAddress in allPendingMembersSorted {
guard let inviterUuid = groupMembership.addedByUuid(forPendingMember: invitedAddress) else {
guard let inviterUuid = groupMembership.addedByUuid(forPendingProfileKeyMember: invitedAddress) else {
owsFailDebug("Missing inviter.")
continue
}

View File

@ -802,6 +802,9 @@
/* Label indicating that all group members can update the group's attributes: name, avatar, etc. */
"CONVERSATION_SETTINGS_ATTRIBUTES_ACCESS_MEMBER" = "All";
/* Label indicating that no member can update the group's attributes: name, avatar, etc. */
"CONVERSATION_SETTINGS_ATTRIBUTES_ACCESS_NONE" = "Disabled";
/* Footer for the 'attributes access' section in conversation settings view. */
"CONVERSATION_SETTINGS_ATTRIBUTES_ACCESS_SECTION_FOOTER" = "Choose who can edit the group name, photo, and disappearing messages timer.";
@ -1480,6 +1483,9 @@
/* Description of the 'unknown' access level. */
"GROUP_ACCESS_LEVEL_UNKNOWN" = "Unknown";
/* Description of the 'unsatisfiable' access level. */
"GROUP_ACCESS_LEVEL_UNSATISFIABLE" = "Disabled";
/* Message indicating that the access to the group's members was changed by the local user. Embeds {{new access level}}. */
"GROUP_ACCESS_MEMBERS_UPDATED_BY_LOCAL_USER_FORMAT" = "You changed who can edit group membership to “%@“.";
@ -1591,6 +1597,15 @@
/* Message indicating that the local user was removed from the group by an unknown user. */
"GROUP_LOCAL_USER_REMOVED_BY_UNKNOWN_USER" = "You were removed from the group.";
/* Message indicating that the local user's request to join the group was approved. */
"GROUP_LOCAL_USER_REQUEST_APPROVED" = "GROUP_LOCAL_USER_REQUEST_APPROVED";
/* Message indicating that the local user's request to join the group was rejected. */
"GROUP_LOCAL_USER_REQUEST_REJECTED" = "GROUP_LOCAL_USER_REQUEST_REJECTED";
/* Message indicating that the local user requested to join the group. */
"GROUP_LOCAL_USER_REQUESTED_TO_JOIN_TO_THE_GROUP" = "GROUP_LOCAL_USER_REQUESTED_TO_JOIN_TO_THE_GROUP";
/* Message indicating that the local user had their administrator role revoked. */
"GROUP_LOCAL_USER_REVOKED_ADMINISTRATOR" = "Your admin privileges were revoked.";
@ -1714,6 +1729,27 @@
/* Message indicating that the remote user was removed from the group. Embeds {{ %1$@ user who removed the user, %2$@ user who was removed}}. */
"GROUP_REMOTE_USER_REMOVED_FROM_GROUP_BY_REMOTE_USER_FORMAT" = "%1$@ removed %2$@.";
/* Message indicating that a remote user's request to join the group was approved by the local user. Embeds {{requesting user name}}. */
"GROUP_REMOTE_USER_REQUEST_APPROVED_BY_LOCAL_USER_FORMAT" = "GROUP_REMOTE_USER_REQUEST_APPROVED_BY_LOCAL_USER_FORMAT";
/* Message indicating that a remote user's request to join the group was approved by another user. Embeds {{ %1$@ requesting user name, %2$@ approving user name }}. */
"GROUP_REMOTE_USER_REQUEST_APPROVED_BY_REMOTE_USER_FORMAT" = "GROUP_REMOTE_USER_REQUEST_APPROVED_BY_REMOTE_USER_FORMAT";
/* Message indicating that a remote user's request to join the group was approved. Embeds {{requesting user name}}. */
"GROUP_REMOTE_USER_REQUEST_APPROVED_FORMAT" = "GROUP_REMOTE_USER_REQUEST_APPROVED_FORMAT";
/* Message indicating that a remote user's request to join the group was rejected by the local user. Embeds {{requesting user name}}. */
"GROUP_REMOTE_USER_REQUEST_REJECTED_BY_LOCAL_USER_FORMAT" = "GROUP_REMOTE_USER_REQUEST_REJECTED_BY_LOCAL_USER_FORMAT";
/* Message indicating that a remote user's request to join the group was rejected by another user. Embeds {{ %1$@ requesting user name, %2$@ approving user name }}. */
"GROUP_REMOTE_USER_REQUEST_REJECTED_BY_REMOTE_USER_FORMAT" = "GROUP_REMOTE_USER_REQUEST_REJECTED_BY_REMOTE_USER_FORMAT";
/* Message indicating that a remote user's request to join the group was rejected. Embeds {{requesting user name}}. */
"GROUP_REMOTE_USER_REQUEST_REJECTED_FORMAT" = "GROUP_REMOTE_USER_REQUEST_REJECTED_FORMAT";
/* Message indicating that a remote user requested to join the group. Embeds {{requesting user name}}. */
"GROUP_REMOTE_USER_REQUESTED_TO_JOIN_THE_GROUP_FORMAT" = "GROUP_REMOTE_USER_REQUESTED_TO_JOIN_THE_GROUP_FORMAT";
/* Message indicating that a remote user had their administrator role revoked. Embeds {{remote user name}}. */
"GROUP_REMOTE_USER_REVOKED_ADMINISTRATOR" = "%@ had their admin privileges revoked.";

View File

@ -44,8 +44,9 @@ public struct GroupV2SnapshotImpl: GroupV2Snapshot {
private let pendingMembers: [PendingMember]
private let invalidInvites: [InvalidInvite]
public let accessControlForAttributes: GroupsProtoAccessControlAccessRequired
public let accessControlForMembers: GroupsProtoAccessControlAccessRequired
public let groupAccess: GroupAccess
public let inviteLinkPassword: Data?
public let disappearingMessageToken: DisappearingMessageToken
@ -64,8 +65,8 @@ public struct GroupV2SnapshotImpl: GroupV2Snapshot {
members: [Member],
pendingMembers: [PendingMember],
invalidInvites: [InvalidInvite],
accessControlForAttributes: GroupsProtoAccessControlAccessRequired,
accessControlForMembers: GroupsProtoAccessControlAccessRequired,
groupAccess: GroupAccess,
inviteLinkPassword: Data?,
disappearingMessageToken: DisappearingMessageToken,
profileKeys: [UUID: Data]) {
@ -78,8 +79,8 @@ public struct GroupV2SnapshotImpl: GroupV2Snapshot {
self.members = members
self.pendingMembers = pendingMembers
self.invalidInvites = invalidInvites
self.accessControlForAttributes = accessControlForAttributes
self.accessControlForMembers = accessControlForMembers
self.groupAccess = groupAccess
self.inviteLinkPassword = inviteLinkPassword
self.disappearingMessageToken = disappearingMessageToken
self.profileKeys = profileKeys
}
@ -91,7 +92,7 @@ public struct GroupV2SnapshotImpl: GroupV2Snapshot {
owsFailDebug("Invalid value: \(member.role.rawValue)")
continue
}
builder.addNonPendingMember(member.address, role: role)
builder.addFullMember(member.address, role: role)
}
for member in pendingMembers {
@ -99,7 +100,7 @@ public struct GroupV2SnapshotImpl: GroupV2Snapshot {
owsFailDebug("Invalid value: \(member.role.rawValue)")
continue
}
builder.addPendingMember(member.address, role: role, addedByUuid: member.addedByUuid)
builder.addPendingProfileKeyMember(member.address, role: role, addedByUuid: member.addedByUuid)
}
for invalidInvite in invalidInvites {
@ -108,9 +109,4 @@ public struct GroupV2SnapshotImpl: GroupV2Snapshot {
return builder.build()
}
public var groupAccess: GroupAccess {
return GroupAccess(members: GroupAccess.groupV2Access(forProtoAccess: accessControlForMembers),
attributes: GroupAccess.groupV2Access(forProtoAccess: accessControlForAttributes))
}
}

View File

@ -262,7 +262,7 @@ public class GroupV2UpdatesImpl: NSObject, GroupV2UpdatesSwift {
owsFailDebug("Missing localAddress.")
return false
}
let isLocalUserInGroup = groupThread.groupModel.groupMembership.isPendingOrNonPendingMember(localAddress)
let isLocalUserInGroup = groupThread.isLocalUserFullOrInvitedMemberOfGroup
// Auth errors are expected if we've left the group,
// but we should still try to refresh so we can learn
// if we've been re-added.

View File

@ -58,23 +58,27 @@ public class GroupsV2ChangeSetImpl: NSObject, GroupsV2ChangeSet {
// Non-nil if the title changed.
// When clearing the title, this will be the empty string.
private var title: String?
private var newTitle: String?
public var newAvatarData: Data?
public var newAvatarUrlPath: String?
private var shouldUpdateAvatar = false
private var membersToAdd = [UUID: TSGroupMemberRole]()
// Pending or non-pending members to remove.
// Full, pending profile key or pending request members to remove.
private var membersToRemove = [UUID]()
private var membersToChangeRole = [UUID: TSGroupMemberRole]()
private var pendingMembersToAdd = [UUID: TSGroupMemberRole]()
private var pendingProfileKeyMembersToAdd = [UUID: TSGroupMemberRole]()
private var invalidInvitesToRemove = [Data: InvalidInvite]()
private var pendingMembersToPromote = [UUID]()
private var pendingProfileKeyMembersToPromote = [UUID]()
// These access properties should only be set if the value is changing.
private var accessForMembers: GroupV2Access?
private var accessForAttributes: GroupV2Access?
private var accessForAddFromInviteLink: GroupV2Access?
public var newInviteLinkPassword: Data?
private var shouldUpdateInviteLinkPassword = false
private var shouldLeaveGroupDeclineInvite = false
private var shouldRevokeInvalidInvites = false
@ -135,15 +139,19 @@ public class GroupsV2ChangeSetImpl: NSObject, GroupsV2ChangeSet {
let oldGroupMembership = oldGroupModel.groupMembership
let newGroupMembership = newGroupModel.groupMembership
let oldUserUuids = Set(oldGroupMembership.allUsers.compactMap { $0.uuid })
let newUserUuids = Set(newGroupMembership.allUsers.compactMap { $0.uuid })
let oldUserUuids = oldGroupMembership.allMemberUuidsOfAnyKind
let newUserUuids = newGroupMembership.allMemberUuidsOfAnyKind
for uuid in newUserUuids.subtracting(oldUserUuids) {
let isAdministrator = newGroupMembership.isAdministrator(uuid)
let isPending = newGroupMembership.isPendingMember(uuid)
guard !newGroupMembership.isRequestingMember(uuid) else {
owsFailDebug("Pending request members should never be added by diffing models.")
continue
}
let isAdministrator = newGroupMembership.isFullOrInvitedAdministrator(uuid)
let isPending = newGroupMembership.isPendingProfileKeyMember(uuid)
let role: TSGroupMemberRole = isAdministrator ? .administrator : .normal
if isPending {
addPendingMember(uuid, role: role)
addPendingProfileKeyMember(uuid, role: role)
} else {
addMember(uuid, role: role)
}
@ -160,17 +168,22 @@ public class GroupsV2ChangeSetImpl: NSObject, GroupsV2ChangeSet {
}
for uuid in oldUserUuids.intersection(newUserUuids) {
if oldGroupMembership.isPendingMember(uuid),
newGroupMembership.isNonPendingMember(uuid) {
if oldGroupMembership.isPendingProfileKeyMember(uuid),
newGroupMembership.isFullMember(uuid) {
addMember(uuid, role: .normal)
} else if oldGroupMembership.isRequestingMember(uuid),
newGroupMembership.isFullMember(uuid) {
// We only currently support accepting join requests
// with "normal" role.
addMember(uuid, role: .normal)
}
}
let oldMemberUuids = Set(oldGroupMembership.nonPendingMembers.compactMap { $0.uuid })
let newMemberUuids = Set(newGroupMembership.nonPendingMembers.compactMap { $0.uuid })
let oldMemberUuids = Set(oldGroupMembership.fullMembers.compactMap { $0.uuid })
let newMemberUuids = Set(newGroupMembership.fullMembers.compactMap { $0.uuid })
for uuid in oldMemberUuids.intersection(newMemberUuids) {
let oldIsAdministrator = oldGroupMembership.isAdministrator(uuid)
let newIsAdministrator = newGroupMembership.isAdministrator(uuid)
let oldIsAdministrator = oldGroupMembership.isFullMemberAndAdministrator(uuid)
let newIsAdministrator = newGroupMembership.isFullMemberAndAdministrator(uuid)
guard oldIsAdministrator != newIsAdministrator else {
continue
}
@ -178,13 +191,20 @@ public class GroupsV2ChangeSetImpl: NSObject, GroupsV2ChangeSet {
changeRoleForMember(uuid, role: role)
}
if oldGroupModel.inviteLinkPassword != newGroupModel.inviteLinkPassword {
setInviteLinkPassword(newGroupModel.inviteLinkPassword)
}
let oldAccess = oldGroupModel.access
let newAccess = newGroupModel.access
if oldAccess.members != newAccess.members {
self.accessForMembers = newAccess.members
setAccessForMembers(newAccess.members)
}
if oldAccess.attributes != newAccess.attributes {
self.accessForAttributes = newAccess.attributes
setAccessForAttributes(newAccess.attributes)
}
if oldAccess.addFromInviteLink != newAccess.addFromInviteLink {
setAccessForAddFromInviteLink(newAccess.addFromInviteLink)
}
let oldDisappearingMessageToken = oldDMConfiguration.asToken
@ -196,9 +216,17 @@ public class GroupsV2ChangeSetImpl: NSObject, GroupsV2ChangeSet {
@objc
public func setTitle(_ value: String?) {
assert(self.title == nil)
assert(self.newTitle == nil)
// Non-nil if the title changed.
self.title = value ?? ""
self.newTitle = value ?? ""
}
@objc
public func setInviteLinkPassword(_ value: Data?) {
assert(self.newInviteLinkPassword == nil)
assert(!shouldUpdateInviteLinkPassword)
newInviteLinkPassword = value
shouldUpdateInviteLinkPassword = true
}
@objc
@ -234,9 +262,9 @@ public class GroupsV2ChangeSetImpl: NSObject, GroupsV2ChangeSet {
}
@objc
public func promotePendingMember(_ uuid: UUID) {
assert(!pendingMembersToPromote.contains(uuid))
pendingMembersToPromote.append(uuid)
public func promotePendingProfileKeyMember(_ uuid: UUID) {
assert(!pendingProfileKeyMembersToPromote.contains(uuid))
pendingProfileKeyMembersToPromote.append(uuid)
}
public func changeRoleForMember(_ uuid: UUID, role: TSGroupMemberRole) {
@ -244,9 +272,9 @@ public class GroupsV2ChangeSetImpl: NSObject, GroupsV2ChangeSet {
membersToChangeRole[uuid] = role
}
public func addPendingMember(_ uuid: UUID, role: TSGroupMemberRole) {
assert(pendingMembersToAdd[uuid] == nil)
pendingMembersToAdd[uuid] = role
public func addPendingProfileKeyMember(_ uuid: UUID, role: TSGroupMemberRole) {
assert(pendingProfileKeyMembersToAdd[uuid] == nil)
pendingProfileKeyMembersToAdd[uuid] = role
}
public func setShouldLeaveGroupDeclineInvite() {
@ -269,6 +297,11 @@ public class GroupsV2ChangeSetImpl: NSObject, GroupsV2ChangeSet {
accessForAttributes = value
}
public func setAccessForAddFromInviteLink(_ value: GroupV2Access) {
assert(accessForAddFromInviteLink == nil)
accessForAddFromInviteLink = value
}
public func setNewDisappearingMessageToken(_ newDisappearingMessageToken: DisappearingMessageToken) {
assert(self.newDisappearingMessageToken == nil)
self.newDisappearingMessageToken = newDisappearingMessageToken
@ -313,7 +346,7 @@ public class GroupsV2ChangeSetImpl: NSObject, GroupsV2ChangeSet {
// NOTE: We don't (and can't) gather profile key credentials for pending members.
var uuidsForProfileKeyCredentials = Set<UUID>()
uuidsForProfileKeyCredentials.formUnion(membersToAdd.keys)
uuidsForProfileKeyCredentials.formUnion(pendingMembersToPromote)
uuidsForProfileKeyCredentials.formUnion(pendingProfileKeyMembersToPromote)
// This should be redundant, but we'll also double-check that we have
// the local profile key credential.
uuidsForProfileKeyCredentials.insert(localUuid)
@ -383,16 +416,16 @@ public class GroupsV2ChangeSetImpl: NSObject, GroupsV2ChangeSet {
Logger.verbose("Revision: \(oldRevision) -> \(newRevision)")
actionsBuilder.setRevision(newRevision)
var nonPendingAdministratorCount: Int = currentGroupModel.groupMembership.nonPendingAdministrators.count
var allMemberCount = currentGroupModel.groupMembership.pendingAndNonPendingMemberCount
var fullMemberAdministratorCount: Int = currentGroupModel.groupMembership.fullMemberAdministrators.count
var allMemberCount = currentGroupModel.groupMembership.allMembersOfAnyKind.count
var didChange = false
if let title = self.title {
if title == currentGroupModel.groupName {
if let newTitle = self.newTitle {
if newTitle == currentGroupModel.groupName {
// Redundant change, not a conflict.
} else {
let encryptedData = try groupV2Params.encryptGroupName(title)
let encryptedData = try groupV2Params.encryptGroupName(newTitle)
let actionBuilder = GroupsProtoGroupChangeActionsModifyTitleAction.builder()
actionBuilder.setTitle(encryptedData)
actionsBuilder.setModifyTitle(try actionBuilder.build())
@ -416,48 +449,76 @@ public class GroupsV2ChangeSetImpl: NSObject, GroupsV2ChangeSet {
}
}
if shouldUpdateInviteLinkPassword {
if newInviteLinkPassword == currentGroupModel.inviteLinkPassword {
// Redundant change, not a conflict.
} else {
let actionBuilder = GroupsProtoGroupChangeActionsModifyInviteLinkPasswordAction.builder()
if let newInviteLinkPassword = self.newInviteLinkPassword {
actionBuilder.setInviteLinkPassword(newInviteLinkPassword)
}
actionsBuilder.setModifyInviteLinkPassword(try actionBuilder.build())
didChange = true
}
}
let currentGroupMembership = currentGroupModel.groupMembership
for (uuid, role) in self.membersToAdd {
guard !currentGroupMembership.isNonPendingMember(uuid) else {
for (uuid, role) in membersToAdd {
guard !currentGroupMembership.isFullMember(uuid) else {
// Another user has already added this member.
// They may have been added with a different role.
// We don't treat that as a conflict.
continue
}
guard let profileKeyCredential = profileKeyCredentialMap[uuid] else {
throw OWSAssertionError("Missing profile key credential: \(uuid)")
if currentGroupMembership.isRequestingMember(uuid) {
let actionBuilder = GroupsProtoGroupChangeActionsPromoteRequestingMemberAction.builder()
let userId = try groupV2Params.userId(forUuid: uuid)
actionBuilder.setUserID(userId)
actionBuilder.setRole(role.asProtoRole)
actionsBuilder.addPromoteRequestingMembers(try actionBuilder.build())
} else {
guard let profileKeyCredential = profileKeyCredentialMap[uuid] else {
throw OWSAssertionError("Missing profile key credential: \(uuid)")
}
let actionBuilder = GroupsProtoGroupChangeActionsAddMemberAction.builder()
actionBuilder.setAdded(try GroupsV2Protos.buildMemberProto(profileKeyCredential: profileKeyCredential,
role: role.asProtoRole,
groupV2Params: groupV2Params))
actionsBuilder.addAddMembers(try actionBuilder.build())
}
let actionBuilder = GroupsProtoGroupChangeActionsAddMemberAction.builder()
actionBuilder.setAdded(try GroupsV2Protos.buildMemberProto(profileKeyCredential: profileKeyCredential,
role: role.asProtoRole,
groupV2Params: groupV2Params))
actionsBuilder.addAddMembers(try actionBuilder.build())
didChange = true
if role == .administrator {
nonPendingAdministratorCount += 1
fullMemberAdministratorCount += 1
}
}
for uuid in self.membersToRemove {
if currentGroupMembership.isNonPendingMember(uuid) {
if currentGroupMembership.isFullMember(uuid) {
let actionBuilder = GroupsProtoGroupChangeActionsDeleteMemberAction.builder()
let userId = try groupV2Params.userId(forUuid: uuid)
actionBuilder.setDeletedUserID(userId)
actionsBuilder.addDeleteMembers(try actionBuilder.build())
didChange = true
if currentGroupMembership.isAdministrator(uuid) {
nonPendingAdministratorCount -= 1
if currentGroupMembership.isFullMemberAndAdministrator(uuid) {
fullMemberAdministratorCount -= 1
}
allMemberCount -= 1
} else if currentGroupMembership.isPendingMember(uuid) {
} else if currentGroupMembership.isPendingProfileKeyMember(uuid) {
let actionBuilder = GroupsProtoGroupChangeActionsDeletePendingMemberAction.builder()
let userId = try groupV2Params.userId(forUuid: uuid)
actionBuilder.setDeletedUserID(userId)
actionsBuilder.addDeletePendingMembers(try actionBuilder.build())
didChange = true
allMemberCount -= 1
} else if currentGroupMembership.isRequestingMember(uuid) {
let actionBuilder = GroupsProtoGroupChangeActionsDeleteRequestingMemberAction.builder()
let userId = try groupV2Params.userId(forUuid: uuid)
actionBuilder.setDeletedUserID(userId)
actionsBuilder.addDeleteRequestingMembers(try actionBuilder.build())
didChange = true
allMemberCount -= 1
} else {
// Another user has already removed this member or revoked their
// invitation.
@ -466,8 +527,8 @@ public class GroupsV2ChangeSetImpl: NSObject, GroupsV2ChangeSet {
}
}
for (uuid, role) in self.pendingMembersToAdd {
guard !currentGroupMembership.isPendingOrNonPendingMember(uuid) else {
for (uuid, role) in self.pendingProfileKeyMembersToAdd {
guard !currentGroupMembership.isMemberOfAnyKind(uuid) else {
// Another user has already added or invited this member.
// They may have been added with a different role.
// We don't treat that as a conflict.
@ -516,7 +577,7 @@ public class GroupsV2ChangeSetImpl: NSObject, GroupsV2ChangeSet {
}
for (uuid, newRole) in self.membersToChangeRole {
guard currentGroupMembership.isNonPendingMember(uuid) else {
guard currentGroupMembership.isFullMember(uuid) else {
// User is no longer a member.
throw GroupsV2Error.conflictingChange
}
@ -534,9 +595,9 @@ public class GroupsV2ChangeSetImpl: NSObject, GroupsV2ChangeSet {
didChange = true
if currentRole == .administrator {
nonPendingAdministratorCount -= 1
fullMemberAdministratorCount -= 1
} else if newRole == .administrator {
nonPendingAdministratorCount += 1
fullMemberAdministratorCount += 1
}
}
@ -546,7 +607,7 @@ public class GroupsV2ChangeSetImpl: NSObject, GroupsV2ChangeSet {
// Redundant change, not a conflict.
} else {
let actionBuilder = GroupsProtoGroupChangeActionsModifyMembersAccessControlAction.builder()
actionBuilder.setMembersAccess(GroupAccess.protoAccess(forGroupV2Access: access))
actionBuilder.setMembersAccess(access.protoAccess)
actionsBuilder.setModifyMemberAccess(try actionBuilder.build())
didChange = true
}
@ -556,15 +617,25 @@ public class GroupsV2ChangeSetImpl: NSObject, GroupsV2ChangeSet {
// Redundant change, not a conflict.
} else {
let actionBuilder = GroupsProtoGroupChangeActionsModifyAttributesAccessControlAction.builder()
actionBuilder.setAttributesAccess(GroupAccess.protoAccess(forGroupV2Access: access))
actionBuilder.setAttributesAccess(access.protoAccess)
actionsBuilder.setModifyAttributesAccess(try actionBuilder.build())
didChange = true
}
}
if let access = self.accessForAddFromInviteLink {
if currentAccess.addFromInviteLink == access {
// Redundant change, not a conflict.
} else {
let actionBuilder = GroupsProtoGroupChangeActionsModifyAddFromInviteLinkAccessControlAction.builder()
actionBuilder.setAddFromInviteLinkAccess(access.protoAccess)
actionsBuilder.setModifyAddFromInviteLinkAccess(try actionBuilder.build())
didChange = true
}
}
for uuid in pendingMembersToPromote {
for uuid in pendingProfileKeyMembersToPromote {
// Check that pending member is still invited.
guard currentGroupMembership.isPendingMember(uuid) else {
guard currentGroupMembership.isPendingProfileKeyMember(uuid) else {
throw GroupsV2Error.redundantChange
}
guard let profileKeyCredential = profileKeyCredentialMap[uuid] else {
@ -578,9 +649,8 @@ public class GroupsV2ChangeSetImpl: NSObject, GroupsV2ChangeSet {
}
if self.shouldLeaveGroupDeclineInvite {
let isLastAdminInV2Group = (currentGroupMembership.isNonPendingMember(localUuid) &&
currentGroupMembership.isAdministrator(localUuid) &&
nonPendingAdministratorCount == 1)
let isLastAdminInV2Group = (currentGroupMembership.isFullMemberAndAdministrator(localUuid) &&
fullMemberAdministratorCount == 1)
guard !isLastAdminInV2Group else {
// This could happen if the last two admins leave at the same time
// and race.
@ -588,14 +658,14 @@ public class GroupsV2ChangeSetImpl: NSObject, GroupsV2ChangeSet {
}
// Check that we are still invited or in group.
if currentGroupMembership.isPendingMember(localUuid) {
if currentGroupMembership.isPendingProfileKeyMember(localUuid) {
// Decline invite
let actionBuilder = GroupsProtoGroupChangeActionsDeletePendingMemberAction.builder()
let localUserId = try groupV2Params.userId(forUuid: localUuid)
actionBuilder.setDeletedUserID(localUserId)
actionsBuilder.addDeletePendingMembers(try actionBuilder.build())
didChange = true
} else if currentGroupMembership.isNonPendingMember(localUuid) {
} else if currentGroupMembership.isFullMember(localUuid) {
// Leave group
let actionBuilder = GroupsProtoGroupChangeActionsDeleteMemberAction.builder()
let localUserId = try groupV2Params.userId(forUuid: localUuid)

View File

@ -29,6 +29,7 @@ public struct ChangedGroupModel {
// MARK: -
// TODO: Rename to GroupsV2IncomingChanges
public class GroupsV2Changes {
// GroupsV2Changes has one responsibility: applying incremental
@ -75,6 +76,7 @@ public class GroupsV2Changes {
var newGroupName: String? = oldGroupModel.groupName
var newAvatarData: Data? = oldGroupModel.groupAvatarData
var newAvatarUrlPath = oldGroupModel.avatarUrlPath
var newInviteLinkPassword: Data? = oldGroupModel.inviteLinkPassword
let oldGroupMembership = oldGroupModel.groupMembership
var groupMembershipBuilder = oldGroupMembership.asBuilder
@ -82,13 +84,13 @@ public class GroupsV2Changes {
let oldGroupAccess: GroupAccess = oldGroupModel.access
var newMembersAccess = oldGroupAccess.members
var newAttributesAccess = oldGroupAccess.attributes
var newAddFromInviteLinkAccess = oldGroupAccess.addFromInviteLink
if !oldGroupMembership.isPendingOrNonPendingMember(changeAuthorUuid) {
if !oldGroupMembership.isMemberOfAnyKind(changeAuthorUuid) {
owsFailDebug("changeAuthorUuid not a full member of the group.")
}
let isChangeAuthorMember = oldGroupMembership.isNonPendingMember(changeAuthorUuid)
let isChangeAuthorAdmin = (oldGroupMembership.isNonPendingMember(changeAuthorUuid) &&
oldGroupMembership.isAdministrator(changeAuthorUuid))
let isChangeAuthorMember = oldGroupMembership.isFullMember(changeAuthorUuid)
let isChangeAuthorAdmin = oldGroupMembership.isFullMemberAndAdministrator(changeAuthorUuid)
let canAddMembers: Bool
switch oldGroupAccess.members {
case .unknown:
@ -100,6 +102,8 @@ public class GroupsV2Changes {
case .any:
// We no longer honor the "any" level.
canAddMembers = false
case .unsatisfiable:
canAddMembers = false
}
let canRemoveMembers = isChangeAuthorAdmin
let canModifyRoles = isChangeAuthorAdmin
@ -114,8 +118,11 @@ public class GroupsV2Changes {
case .any:
// We no longer honor the "any" level.
canEditAttributes = false
case .unsatisfiable:
canEditAttributes = false
}
let canEditAccess = isChangeAuthorAdmin
let canEditInviteLinks = isChangeAuthorAdmin
// This client can learn of profile keys from parsing group state protos.
// After parsing, we should fill in profileKeys in the profile manager.
@ -146,12 +153,12 @@ public class GroupsV2Changes {
// the service. This is one.
let uuid = try groupV2Params.uuid(forUserId: userId)
guard !oldGroupMembership.isNonPendingMember(uuid) else {
guard !oldGroupMembership.isFullMember(uuid) else {
throw OWSAssertionError("Invalid membership.")
}
groupMembershipBuilder.removeInvalidInvite(userId: userId)
groupMembershipBuilder.remove(uuid)
groupMembershipBuilder.addNonPendingMember(uuid, role: role)
groupMembershipBuilder.addFullMember(uuid, role: role)
guard let profileKeyCiphertextData = member.profileKey else {
throw OWSAssertionError("Missing profileKeyCiphertext.")
@ -176,7 +183,7 @@ public class GroupsV2Changes {
// Any member can leave the group.
owsFailDebug("Cannot kick member.")
}
if !oldGroupMembership.isNonPendingMember(uuid) {
if !oldGroupMembership.isFullMember(uuid) {
owsFailDebug("Invalid membership.")
}
groupMembershipBuilder.removeInvalidInvite(userId: userId)
@ -206,14 +213,14 @@ public class GroupsV2Changes {
// the service. This is one.
let uuid = try groupV2Params.uuid(forUserId: userId)
guard oldGroupMembership.isNonPendingMember(uuid) else {
guard oldGroupMembership.isFullMember(uuid) else {
throw OWSAssertionError("Invalid membership.")
}
if oldGroupMembership.role(for: uuid) == role {
owsFailDebug("Member already has that role.")
}
groupMembershipBuilder.remove(uuid)
groupMembershipBuilder.addNonPendingMember(uuid, role: role)
groupMembershipBuilder.addFullMember(uuid, role: role)
}
for action in changeActionsProto.modifyMemberProfileKeys {
@ -224,7 +231,7 @@ public class GroupsV2Changes {
let uuidCiphertext = try presentation.getUuidCiphertext()
let uuid = try groupV2Params.uuid(forUuidCiphertext: uuidCiphertext)
guard oldGroupMembership.isNonPendingMember(uuid) else {
guard oldGroupMembership.isFullMember(uuid) else {
throw OWSAssertionError("Invalid membership.")
}
@ -284,12 +291,12 @@ public class GroupsV2Changes {
}
continue
}
guard !oldGroupMembership.isPendingOrNonPendingMember(uuid) else {
guard !oldGroupMembership.isMemberOfAnyKind(uuid) else {
throw OWSAssertionError("Invalid membership.")
}
groupMembershipBuilder.removeInvalidInvite(userId: userId)
groupMembershipBuilder.remove(uuid)
groupMembershipBuilder.addPendingMember(uuid, role: role, addedByUuid: addedByUuid)
groupMembershipBuilder.addPendingProfileKeyMember(uuid, role: role, addedByUuid: addedByUuid)
}
for action in changeActionsProto.deletePendingMembers {
@ -309,7 +316,7 @@ public class GroupsV2Changes {
}
guard oldGroupMembership.hasInvalidInvite(forUserId: userId) ||
oldGroupMembership.isPendingMember(uuid) else {
oldGroupMembership.isPendingProfileKeyMember(uuid) else {
throw OWSAssertionError("Invalid membership.")
}
groupMembershipBuilder.removeInvalidInvite(userId: userId)
@ -336,10 +343,10 @@ public class GroupsV2Changes {
let userId = uuidCiphertext.serialize().asData
let uuid = try groupV2Params.uuid(forUuidCiphertext: uuidCiphertext)
guard oldGroupMembership.isPendingMember(uuid) else {
guard oldGroupMembership.isPendingProfileKeyMember(uuid) else {
throw OWSAssertionError("Invalid membership.")
}
guard !oldGroupMembership.isNonPendingMember(uuid) else {
guard !oldGroupMembership.isFullMember(uuid) else {
throw OWSAssertionError("Invalid membership.")
}
guard let role = oldGroupMembership.role(for: uuid) else {
@ -347,7 +354,7 @@ public class GroupsV2Changes {
}
groupMembershipBuilder.removeInvalidInvite(userId: userId)
groupMembershipBuilder.remove(uuid)
groupMembershipBuilder.addNonPendingMember(uuid, role: role)
groupMembershipBuilder.addFullMember(uuid, role: role)
if uuid != changeAuthorUuid {
// Only the invitee can accept an invitation.
@ -410,7 +417,7 @@ public class GroupsV2Changes {
guard let protoAccess = action.attributesAccess else {
throw OWSAssertionError("Missing access.")
}
newAttributesAccess = GroupAccess.groupV2Access(forProtoAccess: protoAccess)
newAttributesAccess = GroupV2Access.access(forProtoAccess: protoAccess)
if newAttributesAccess == .unknown {
owsFailDebug("Unknown attributes access.")
@ -425,15 +432,99 @@ public class GroupsV2Changes {
guard let protoAccess = action.membersAccess else {
throw OWSAssertionError("Missing access.")
}
newMembersAccess = GroupAccess.groupV2Access(forProtoAccess: protoAccess)
newMembersAccess = GroupV2Access.access(forProtoAccess: protoAccess)
if newMembersAccess == .unknown {
owsFailDebug("Unknown member access.")
}
}
if let action = changeActionsProto.modifyAddFromInviteLinkAccess {
if !canEditInviteLinks {
owsFailDebug("Cannot edit addFromInviteLink access.")
}
guard let protoAccess = action.addFromInviteLinkAccess else {
throw OWSAssertionError("Missing access.")
}
newAddFromInviteLinkAccess = GroupV2Access.access(forProtoAccess: protoAccess)
if newAddFromInviteLinkAccess == .unknown {
owsFailDebug("Unknown addFromInviteLink access.")
}
}
for action in changeActionsProto.addRequestingMembers {
if !canAddMembers {
owsFailDebug("Cannot add members.")
}
guard let requestingMember = action.added else {
throw OWSAssertionError("Missing requestingMember.")
}
// TODO: Modify this parsing to reflect action contents.
guard let userId = requestingMember.userID else {
throw OWSAssertionError("Missing userID.")
}
// Some userIds/uuidCiphertexts can be validated by
// the service. This is one.
//
// TODO: Is this one?
let uuid = try groupV2Params.uuid(forUserId: userId)
guard let profileKeyCiphertextData = requestingMember.profileKey else {
throw OWSAssertionError("Missing profileKeyCiphertext.")
}
let profileKeyCiphertext = try ProfileKeyCiphertext(contents: [UInt8](profileKeyCiphertextData))
let profileKey = try groupV2Params.profileKey(forProfileKeyCiphertext: profileKeyCiphertext,
uuid: uuid)
guard let presentationData = requestingMember.presentation else {
throw OWSAssertionError("Missing presentation.")
}
let presentation = try ProfileKeyCredentialPresentation(contents: [UInt8](presentationData))
let uuidCiphertext = try presentation.getUuidCiphertext()
let uuidFromPresentation = try groupV2Params.uuid(forUuidCiphertext: uuidCiphertext)
// TODO:
guard uuid == uuidFromPresentation else {
throw OWSAssertionError("uuids do not match.")
}
guard oldGroupMembership.isMemberOfAnyKind(uuid) else {
throw OWSAssertionError("Invalid membership.")
}
let profileKeyCiphertextFromPresentation = try presentation.getProfileKeyCiphertext()
let profileKeyFromPresentation = try groupV2Params.profileKey(forProfileKeyCiphertext: profileKeyCiphertextFromPresentation,
uuid: uuid)
// TODO:
guard profileKey == profileKeyFromPresentation else {
throw OWSAssertionError("profileKeys do not match.")
}
guard !oldGroupMembership.isMemberOfAnyKind(uuid) else {
throw OWSAssertionError("Invalid membership.")
}
groupMembershipBuilder.removeInvalidInvite(userId: userId)
groupMembershipBuilder.remove(uuid)
groupMembershipBuilder.addRequestingMember(uuid)
profileKeys[uuid] = profileKey
}
if let action = changeActionsProto.modifyInviteLinkPassword {
if !canEditInviteLinks {
owsFailDebug("Cannot modify inviteLinkPassword.")
}
// Change clears or updates the group inviteLinkPassword.
newInviteLinkPassword = action.inviteLinkPassword
}
let newGroupMembership = groupMembershipBuilder.build()
let newGroupAccess = GroupAccess(members: newMembersAccess, attributes: newAttributesAccess)
let newGroupAccess = GroupAccess(members: newMembersAccess, attributes: newAttributesAccess, addFromInviteLink: newAddFromInviteLinkAccess)
GroupsV2Protos.validateInviteLinkState(inviteLinkPassword: newInviteLinkPassword, groupAccess: newGroupAccess)
var builder = oldGroupModel.asBuilder
builder.name = newGroupName
@ -442,6 +533,7 @@ public class GroupsV2Changes {
builder.groupAccess = newGroupAccess
builder.groupV2Revision = newRevision
builder.avatarUrlPath = newAvatarUrlPath
builder.inviteLinkPassword = newInviteLinkPassword
let newGroupModel = try builder.buildAsV2(transaction: transaction)
return ChangedGroupModel(oldGroupModel: oldGroupModel,

View File

@ -118,7 +118,7 @@ class GroupsV2ProfileKeyUpdater {
let groupMembership = groupThread.groupModel.groupMembership
// We only need to update v2 groups of which we are a full member.
guard groupThread.isGroupV2Thread,
groupMembership.isNonPendingMember(localAddress) else {
groupMembership.isFullMember(localAddress) else {
return
}
let groupId = groupThread.groupModel.groupId
@ -254,7 +254,7 @@ class GroupsV2ProfileKeyUpdater {
}
return self.groupsV2.fetchCurrentGroupV2Snapshot(groupModel: groupModel)
}.map(on: .global()) { (groupV2Snapshot: GroupV2Snapshot) throws -> TSGroupThread in
guard groupV2Snapshot.groupMembership.isNonPendingMember(localAddress) else {
guard groupV2Snapshot.groupMembership.isFullMember(localAddress) else {
// We're not a full member, no need to update profile key.
throw GroupsV2Error.redundantChange
}

View File

@ -101,16 +101,20 @@ public class GroupsV2Protos {
groupBuilder.setAvatar(avatarUrl)
}
groupBuilder.setAccessControl(try buildAccessProto(groupAccess: groupModel.access))
let groupAccess = groupModel.access
groupBuilder.setAccessControl(try buildAccessProto(groupAccess: groupAccess))
if let inviteLinkPassword = groupModel.inviteLinkPassword,
!inviteLinkPassword.isEmpty {
groupBuilder.setInviteLinkPassword(inviteLinkPassword)
}
// * You will be member 0 and the only admin.
// * Other members will be non-admin members.
//
// Add local user first to ensure that they are user 0.
let groupMembership = groupModel.groupMembership
let localAddress = SignalServiceAddress(uuid: localUuid)
assert(groupMembership.isAdministrator(localAddress))
assert(!groupMembership.isPendingMember(localAddress))
assert(groupMembership.isFullMemberAndAdministrator(localUuid))
groupBuilder.addMembers(try buildMemberProto(profileKeyCredential: localProfileKeyCredential,
role: .administrator,
groupV2Params: groupV2Params))
@ -118,25 +122,24 @@ public class GroupsV2Protos {
guard uuid != localUuid else {
continue
}
let address = SignalServiceAddress(uuid: uuid)
let isAdministrator = groupMembership.isAdministrator(address)
let isPending = groupMembership.isPendingMember(address)
let role: GroupsProtoMemberRole = isAdministrator ? .administrator : .`default`
guard !isPending else {
let isInvited = groupMembership.isPendingProfileKeyMember(uuid)
guard !isInvited else {
continue
}
let isAdministrator = groupMembership.isFullMemberAndAdministrator(uuid)
let role: GroupsProtoMemberRole = isAdministrator ? .administrator : .`default`
groupBuilder.addMembers(try buildMemberProto(profileKeyCredential: profileKeyCredential,
role: role,
groupV2Params: groupV2Params))
}
for address in groupMembership.pendingMembers {
for address in groupMembership.pendingProfileKeyMembers {
guard let uuid = address.uuid else {
throw OWSAssertionError("Missing uuid.")
}
guard uuid != localUuid else {
continue
}
let isAdministrator = groupMembership.isAdministrator(address)
let isAdministrator = groupMembership.isFullOrInvitedAdministrator(uuid)
let role: GroupsProtoMemberRole = isAdministrator ? .administrator : .`default`
groupBuilder.addPendingMembers(try buildPendingMemberProto(uuid: uuid,
role: role,
@ -144,13 +147,26 @@ public class GroupsV2Protos {
groupV2Params: groupV2Params))
}
validateInviteLinkState(inviteLinkPassword: groupModel.inviteLinkPassword, groupAccess: groupAccess)
return try groupBuilder.build()
}
public class func validateInviteLinkState(inviteLinkPassword: Data?, groupAccess: GroupAccess) {
let canJoinFromInviteLink = groupAccess.addFromInviteLink != .unsatisfiable
let hasInviteLinkPassword = inviteLinkPassword?.count ?? 0 > 0
if canJoinFromInviteLink, !hasInviteLinkPassword {
owsFailDebug("Invite links enabled without inviteLinkPassword.")
} else if !canJoinFromInviteLink, hasInviteLinkPassword {
owsFailDebug("inviteLinkPassword set but invite links not enabled.")
}
}
public class func buildAccessProto(groupAccess: GroupAccess) throws -> GroupsProtoAccessControl {
let builder = GroupsProtoAccessControl.builder()
builder.setAttributes(GroupAccess.protoAccess(forGroupV2Access: groupAccess.attributes))
builder.setMembers(GroupAccess.protoAccess(forGroupV2Access: groupAccess.members))
builder.setAttributes(groupAccess.attributes.protoAccess)
builder.setMembers(groupAccess.members.protoAccess)
builder.setAddFromInviteLink(groupAccess.addFromInviteLink.protoAccess)
return try builder.build()
}
@ -316,6 +332,8 @@ public class GroupsV2Protos {
pendingMembers.append(pendingMember)
}
let inviteLinkPassword = groupProto.inviteLinkPassword
guard let accessControl = groupProto.accessControl else {
throw OWSAssertionError("Missing accessControl.")
}
@ -325,11 +343,20 @@ public class GroupsV2Protos {
guard let accessControlForMembers = accessControl.members else {
throw OWSAssertionError("Missing accessControl.members.")
}
// If group state does not have "invite link" access specified,
// assume invite links are disabled.
let accessControlForAddFromInviteLink = accessControl.addFromInviteLink ?? .unsatisfiable
// If the timer blob is not populated or has zero duration,
// disappearing messages should be disabled.
let disappearingMessageToken = groupV2Params.decryptDisappearingMessagesTimer(groupProto.disappearingMessagesTimer)
let groupAccess = GroupAccess(members: GroupV2Access.access(forProtoAccess: accessControlForMembers),
attributes: GroupV2Access.access(forProtoAccess: accessControlForAttributes),
addFromInviteLink: GroupV2Access.access(forProtoAccess: accessControlForAddFromInviteLink))
validateInviteLinkState(inviteLinkPassword: inviteLinkPassword, groupAccess: groupAccess)
let revision = groupProto.revision
let groupSecretParamsData = groupV2Params.groupSecretParamsData
return GroupV2SnapshotImpl(groupSecretParamsData: groupSecretParamsData,
@ -341,8 +368,8 @@ public class GroupsV2Protos {
members: members,
pendingMembers: pendingMembers,
invalidInvites: invalidInvites,
accessControlForAttributes: accessControlForAttributes,
accessControlForMembers: accessControlForMembers,
groupAccess: groupAccess,
inviteLinkPassword: inviteLinkPassword,
disappearingMessageToken: disappearingMessageToken,
profileKeys: profileKeys)
}

View File

@ -21,7 +21,7 @@ public extension GroupManager {
replacementAdminUuid: UUID? = nil,
success: (() -> Void)?) {
guard groupThread.isLocalUserPendingOrNonPendingMember else {
guard groupThread.isLocalUserMemberOfAnyKind else {
owsFailDebug("unexpectedly trying to leave group for which we're not a member.")
return
}

View File

@ -6,24 +6,39 @@ import Foundation
@objc
public extension TSGroupThread {
var isLocalUserInGroup: Bool {
var isLocalUserMemberOfAnyKind: Bool {
guard let localAddress = TSAccountManager.localAddress else {
return false
}
return groupModel.groupMembership.isNonPendingMember(localAddress)
return groupModel.groupMembership.isMemberOfAnyKind(localAddress)
}
var isLocalUserPendingOrNonPendingMember: Bool {
var isLocalUserFullMemberOfGroup: Bool {
guard let localAddress = TSAccountManager.localAddress else {
return false
}
return groupModel.groupMembership.isPendingOrNonPendingMember(localAddress)
return groupModel.groupMembership.isFullMember(localAddress)
}
var isLocalUserPendingMember: Bool {
var isLocalUserPendingProfileKeyMember: Bool {
guard let localAddress = TSAccountManager.localAddress else {
return false
}
return groupModel.groupMembership.isPendingMember(localAddress)
return groupModel.groupMembership.isPendingProfileKeyMember(localAddress)
}
var isLocalUserRequestingMember: Bool {
guard let localAddress = TSAccountManager.localAddress else {
return false
}
return groupModel.groupMembership.isRequestingMember(localAddress)
}
var isLocalUserFullOrInvitedMemberOfGroup: Bool {
guard let localAddress = TSAccountManager.localAddress else {
return false
}
return (groupModel.groupMembership.isFullMember(localAddress) ||
groupModel.groupMembership.isPendingProfileKeyMember(localAddress))
}
}

View File

@ -346,6 +346,10 @@ extension GroupUpdateCopy {
case .administrator:
return NSLocalizedString("GROUP_ACCESS_LEVEL_ADMINISTRATORS",
comment: "Description of the 'admins only' access level.")
case .unsatisfiable:
// TODO:
return NSLocalizedString("GROUP_ACCESS_LEVEL_UNSATISFIABLE",
comment: "Description of the 'unsatisfiable' access level.")
}
}
@ -415,7 +419,7 @@ extension GroupUpdateCopy {
mutating func addMembershipUpdates(oldGroupMembership: GroupMembership) {
var membershipCounts = MembershipCounts()
let allUsersUnsorted = oldGroupMembership.allUsers.union(newGroupMembership.allUsers)
let allUsersUnsorted = oldGroupMembership.allMembersOfAnyKind.union(newGroupMembership.allMembersOfAnyKind)
var allUsersSorted = allUsersUnsorted.sorted { (left, right) -> Bool in
// Use an arbitrary sort to ensure the output is deterministic.
return left.stringForDisplay > right.stringForDisplay
@ -443,6 +447,9 @@ extension GroupUpdateCopy {
addMemberRoleUpdates(for: address, oldGroupMembership: oldGroupMembership)
case .invited:
addUserLeftOrWasKickedOutOfGroupThenWasInvitedToTheGroup(for: address)
case .requesting:
owsFailDebug("This transition is unexpected")
addUserRequestedToJoinGroup(for: address)
case .none:
addUserLeftOrWasKickedOutOfGroup(for: address)
}
@ -468,11 +475,26 @@ extension GroupUpdateCopy {
case .invited:
// Membership status didn't change.
break
case .requesting:
addUserRequestedToJoinGroup(for: address)
case .none:
addUserInviteWasDeclinedOrRevoked(for: address,
oldGroupMembership: oldGroupMembership,
membershipCounts: &membershipCounts)
}
case .requesting:
switch newMembershipStatus {
case .normalMember:
addUserRequestWasApproved(for: address)
case .invited:
addUserWasInvitedToTheGroup(for: address,
membershipCounts: &membershipCounts)
case .requesting:
// Membership status didn't change.
break
case .none:
addUserRequestWasRejected(for: address)
}
case .none:
switch newMembershipStatus {
case .normalMember:
@ -480,6 +502,8 @@ extension GroupUpdateCopy {
case .invited:
addUserWasInvitedToTheGroup(for: address,
membershipCounts: &membershipCounts)
case .requesting:
addUserRequestedToJoinGroup(for: address)
case .none:
// Membership status didn't change.
break
@ -579,8 +603,8 @@ extension GroupUpdateCopy {
mutating func addMemberRoleUpdates(for address: SignalServiceAddress,
oldGroupMembership: GroupMembership) {
let oldIsAdministrator = oldGroupMembership.isAdministrator(address)
let newIsAdministrator = newGroupMembership.isAdministrator(address)
let oldIsAdministrator = oldGroupMembership.isFullMemberAndAdministrator(address)
let newIsAdministrator = newGroupMembership.isFullMemberAndAdministrator(address)
guard oldIsAdministrator != newIsAdministrator else {
// Role didn't change.
@ -826,7 +850,7 @@ extension GroupUpdateCopy {
var inviterName: String?
var inviterAddress: SignalServiceAddress?
if let inviterUuid = oldGroupMembership.addedByUuid(forPendingMember: address) {
if let inviterUuid = oldGroupMembership.addedByUuid(forPendingProfileKeyMember: address) {
inviterAddress = SignalServiceAddress(uuid: inviterUuid)
inviterName = contactsManager.displayName(for: SignalServiceAddress(uuid: inviterUuid),
transaction: transaction)
@ -924,7 +948,7 @@ extension GroupUpdateCopy {
var inviterName: String?
var inviterAddress: SignalServiceAddress?
if let inviterUuid = oldGroupMembership.addedByUuid(forPendingMember: address) {
if let inviterUuid = oldGroupMembership.addedByUuid(forPendingProfileKeyMember: address) {
inviterAddress = SignalServiceAddress(uuid: inviterUuid)
inviterName = contactsManager.displayName(for: SignalServiceAddress(uuid: inviterUuid),
transaction: transaction)
@ -1198,6 +1222,77 @@ extension GroupUpdateCopy {
}
}
// MARK: - Requesting Members
mutating func addUserRequestedToJoinGroup(for address: SignalServiceAddress) {
let isLocalUser = localAddress == address
if isLocalUser {
addItem(.userMembershipState,
address: address,
copy: NSLocalizedString("GROUP_LOCAL_USER_REQUESTED_TO_JOIN_TO_THE_GROUP",
comment: "Message indicating that the local user requested to join the group."))
} else {
let requesterName = contactsManager.displayName(for: address, transaction: transaction)
let format = NSLocalizedString("GROUP_REMOTE_USER_REQUESTED_TO_JOIN_THE_GROUP_FORMAT",
comment: "Message indicating that a remote user requested to join the group. Embeds {{requesting user name}}.")
addItem(.userMembershipState, address: address, format: format, requesterName)
}
}
mutating func addUserRequestWasApproved(for address: SignalServiceAddress) {
let isLocalUser = localAddress == address
if isLocalUser {
// TODO: Should we embed the name of the user who approved us?
addItem(.userMembershipState,
address: address,
copy: NSLocalizedString("GROUP_LOCAL_USER_REQUEST_APPROVED",
comment: "Message indicating that the local user's request to join the group was approved."))
} else {
let requesterName = contactsManager.displayName(for: address, transaction: transaction)
switch updater {
case .localUser:
let format = NSLocalizedString("GROUP_REMOTE_USER_REQUEST_APPROVED_BY_LOCAL_USER_FORMAT",
comment: "Message indicating that a remote user's request to join the group was approved by the local user. Embeds {{requesting user name}}.")
addItem(.userMembershipState, address: address, format: format, requesterName)
case .otherUser(let updaterName, _):
let format = NSLocalizedString("GROUP_REMOTE_USER_REQUEST_APPROVED_BY_REMOTE_USER_FORMAT",
comment: "Message indicating that a remote user's request to join the group was approved by another user. Embeds {{ %1$@ requesting user name, %2$@ approving user name }}.")
addItem(.userMembershipState, address: address, format: format, requesterName, updaterName)
case .unknown:
let format = NSLocalizedString("GROUP_REMOTE_USER_REQUEST_APPROVED_FORMAT",
comment: "Message indicating that a remote user's request to join the group was approved. Embeds {{requesting user name}}.")
addItem(.userMembershipState, address: address, format: format, requesterName)
}
}
}
mutating func addUserRequestWasRejected(for address: SignalServiceAddress) {
let isLocalUser = localAddress == address
if isLocalUser {
// TODO: Should we embed the name of the user who rejected us?
addItem(.userMembershipState,
address: address,
copy: NSLocalizedString("GROUP_LOCAL_USER_REQUEST_REJECTED",
comment: "Message indicating that the local user's request to join the group was rejected."))
} else {
let requesterName = contactsManager.displayName(for: address, transaction: transaction)
switch updater {
case .localUser:
let format = NSLocalizedString("GROUP_REMOTE_USER_REQUEST_REJECTED_BY_LOCAL_USER_FORMAT",
comment: "Message indicating that a remote user's request to join the group was rejected by the local user. Embeds {{requesting user name}}.")
addItem(.userMembershipState, address: address, format: format, requesterName)
case .otherUser(let updaterName, _):
let format = NSLocalizedString("GROUP_REMOTE_USER_REQUEST_REJECTED_BY_REMOTE_USER_FORMAT",
comment: "Message indicating that a remote user's request to join the group was rejected by another user. Embeds {{ %1$@ requesting user name, %2$@ approving user name }}.")
addItem(.userMembershipState, address: address, format: format, requesterName, updaterName)
case .unknown:
let format = NSLocalizedString("GROUP_REMOTE_USER_REQUEST_REJECTED_FORMAT",
comment: "Message indicating that a remote user's request to join the group was rejected. Embeds {{requesting user name}}.")
addItem(.userMembershipState, address: address, format: format, requesterName)
}
}
}
// MARK: - Disappearing Messages
mutating func addDisappearingMessageUpdates(oldToken: DisappearingMessageToken?,
@ -1311,7 +1406,7 @@ extension GroupUpdateCopy {
comment: "Message indicating that the local user has joined the group."))
case .invited:
if let localAddress = Self.tsAccountManager.localAddress,
let inviterUuid = newGroupMembership.addedByUuid(forPendingMember: localAddress) {
let inviterUuid = newGroupMembership.addedByUuid(forPendingProfileKeyMember: localAddress) {
let inviterAddress = SignalServiceAddress(uuid: inviterUuid)
let inviterName = contactsManager.displayName(for: inviterAddress, transaction: transaction)
let format = NSLocalizedString("GROUP_LOCAL_USER_INVITED_BY_REMOTE_USER_FORMAT",
@ -1325,6 +1420,12 @@ extension GroupUpdateCopy {
copy: NSLocalizedString("GROUP_LOCAL_USER_INVITED_TO_THE_GROUP",
comment: "Message indicating that the local user was invited to the group."))
}
case .requesting:
if let localAddress = Self.tsAccountManager.localAddress {
addUserRequestedToJoinGroup(for: localAddress)
} else {
owsFailDebug("Missing localAddress.")
}
case .none:
if !DebugFlags.permissiveGroupUpdateInfoMessages {
owsFailDebug("Learned of group without any membership status.")
@ -1336,9 +1437,10 @@ extension GroupUpdateCopy {
// MARK: - Membership Status
enum MembershipStatus {
fileprivate enum MembershipStatus {
case normalMember
case invited
case requesting
case none
}
@ -1348,10 +1450,12 @@ extension GroupUpdateCopy {
func membershipStatus(of address: SignalServiceAddress,
in groupMembership: GroupMembership) -> MembershipStatus {
if groupMembership.isNonPendingMember(address) {
if groupMembership.isFullMember(address) {
return .normalMember
} else if groupMembership.isPendingMember(address) {
} else if groupMembership.isPendingProfileKeyMember(address) {
return .invited
} else if groupMembership.isRequestingMember(address) {
return .requesting
} else {
return .none
}

View File

@ -486,7 +486,8 @@ NSString *const kOWSBlockingManager_SyncedBlockedGroupIdsKey = @"kOWSBlockingMan
TSGroupThread *_Nullable groupThread = [TSGroupThread fetchWithGroupId:groupId transaction:transaction];
// Quit the group if we're a member
if (blockMode == BlockMode_LocalShouldLeaveGroups && groupThread.isLocalUserPendingOrNonPendingMember) {
BOOL isInGroup = groupThread.isLocalUserMemberOfAnyKind;
if (blockMode == BlockMode_LocalShouldLeaveGroups && isInGroup) {
[GroupManager leaveGroupOrDeclineInviteAsyncWithoutUIWithGroupThread:groupThread
transaction:transaction
success:nil];

View File

@ -719,7 +719,7 @@ NS_ASSUME_NONNULL_BEGIN
return nil;
}
if (groupThread) {
if (!groupThread.isLocalUserInGroup) {
if (!groupThread.isLocalUserFullMemberOfGroup) {
OWSLogInfo(@"Ignoring messages for left group.");
return nil;
}
@ -806,21 +806,16 @@ NS_ASSUME_NONNULL_BEGIN
return nil;
}
if (!groupThread.isLocalUserInGroup) {
// We don't want to process messages for groups in which we are a pending member.
OWSLogInfo(@"Ignoring messages for left group.");
return nil;
}
if (!envelope.sourceAddress) {
OWSFailDebug(@"Missing sender address.");
return nil;
}
if (!groupThread.isLocalUserInGroup) {
if (!groupThread.isLocalUserFullMemberOfGroup) {
// We don't want to process messages for groups in which we are a pending member.
OWSLogInfo(@"Ignoring messages for left group.");
return nil;
}
if (![groupModel.groupMembership isNonPendingMember:envelope.sourceAddress]) {
if (![groupModel.groupMembership isFullMember:envelope.sourceAddress]) {
// We don't want to process group messages for non-members.
OWSLogInfo(@"Ignoring messages for user not in group: %@.", envelope.sourceAddress);
return nil;
@ -1138,7 +1133,7 @@ NS_ASSUME_NONNULL_BEGIN
if (typingMessage.hasGroupID) {
TSGroupThread *_Nullable groupThread = [TSGroupThread fetchWithGroupId:typingMessage.groupID
transaction:transaction];
if (groupThread != nil && !groupThread.isLocalUserInGroup) {
if (groupThread != nil && !groupThread.isLocalUserFullOrInvitedMemberOfGroup) {
OWSLogInfo(@"Ignoring messages for left group.");
return;
}
@ -1243,7 +1238,7 @@ NS_ASSUME_NONNULL_BEGIN
OWSFailDebug(@"Group update for invalid group version.");
return;
}
if (oldGroupThread.isLocalUserInGroup) {
if (oldGroupThread.isLocalUserFullMemberOfGroup) {
// If the local user had left the group we couldn't trust our local group state - we'd
// have to trust the remote membership.
//
@ -1952,7 +1947,7 @@ NS_ASSUME_NONNULL_BEGIN
}
// Ensure we are in the group.
if (!gThread.isLocalUserInGroup) {
if (!gThread.isLocalUserFullOrInvitedMemberOfGroup) {
OWSLogWarn(@"Ignoring 'Group Info Request' message for group we no longer belong to.");
return;
}

View File

@ -629,13 +629,14 @@ extension MessageSending {
// If a member has left the group since this message was enqueued,
// they should not receive the message.
let groupMembership = groupThread.groupModel.groupMembership
var currentValidRecipients = groupMembership.nonPendingMembers
var currentValidRecipients = groupMembership.fullMembers
// ...or latest known list of "additional recipients".
//
// This is used to send group update messages for v2 groups to
// pending members who are not included in .sendingRecipientAddresses().
if GroupManager.shouldMessageHaveAdditionalRecipients(message, groupThread: groupThread) {
currentValidRecipients.formUnion(groupMembership.pendingMembers)
currentValidRecipients.formUnion(groupMembership.pendingProfileKeyOrRequestMembers)
}
currentValidRecipients.remove(localAddress)
recipientAddresses.formIntersection(currentValidRecipients)

View File

@ -345,14 +345,14 @@ class IncomingGroupsV2MessageQueue: NSObject, MessageProcessingPipelineStage {
owsFailDebug("Missing thread.")
return true
}
guard groupThread.groupModel.groupMembership.isNonPendingMember(localAddress) else {
guard groupThread.groupModel.groupMembership.isFullMember(localAddress) else {
// * Local user might have just left the group.
// * Local user may have just learned that we were removed from the group.
// * Local user might be a pending member with an invite.
Logger.warn("Discarding envelope; local user is not an active group member.")
return true
}
guard groupThread.groupModel.groupMembership.isNonPendingMember(sourceAddress) else {
guard groupThread.groupModel.groupMembership.isFullMember(sourceAddress) else {
// * The sender might have just left the group.
Logger.warn("Discarding envelope; sender is not an active group member.")
return true

View File

@ -162,6 +162,14 @@ struct YAPDBThreadFinder: ThreadFinder {
@objc
public class GRDBThreadFinder: NSObject, ThreadFinder {
// MARK: - Dependencies
private class var tsAccountManager: TSAccountManager {
return .sharedInstance()
}
// MARK: -
public typealias ReadTransaction = GRDBReadTransaction
static let cn = ThreadRecord.columnName
@ -271,9 +279,10 @@ public class GRDBThreadFinder: NSObject, ThreadFinder {
@objc
public class func hasPendingMessageRequest(thread: TSThread, transaction: GRDBReadTransaction) -> Bool {
// TODO: Should we consult isRequestingMember() here?
if let groupThread = thread as? TSGroupThread,
groupThread.isGroupV2Thread,
groupThread.isLocalUserPendingMember {
groupThread.isLocalUserPendingProfileKeyMember {
return true
}
@ -285,7 +294,7 @@ public class GRDBThreadFinder: NSObject, ThreadFinder {
if OWSBlockingManager.shared().isThreadBlocked(thread) { return true }
let isGroupThread = thread is TSGroupThread
let isLocalUserInGroup = (thread as? TSGroupThread)?.isLocalUserInGroup == true
let isLocalUserInGroup = (thread as? TSGroupThread)?.isLocalUserFullOrInvitedMemberOfGroup == true
// If this is a group thread and we're not a member, never show the message request.
if isGroupThread, !isLocalUserInGroup { return false }

View File

@ -10,6 +10,7 @@ public enum GroupV2Access: UInt, Codable {
case any
case member
case administrator
case unsatisfiable
var description: String {
get {
@ -22,9 +23,39 @@ public enum GroupV2Access: UInt, Codable {
return "member"
case .administrator:
return "administrator"
case .unsatisfiable:
return "unsatisfiable"
}
}
}
public static func access(forProtoAccess value: GroupsProtoAccessControlAccessRequired) -> GroupV2Access {
switch value {
case .member:
return .member
case .administrator:
return .administrator
case .unsatisfiable:
return .unsatisfiable
default:
return .unknown
}
}
public var protoAccess: GroupsProtoAccessControlAccessRequired {
switch self {
case .any:
return .unknown
case .member:
return .member
case .administrator:
return .administrator
case .unsatisfiable:
return .unsatisfiable
default:
return .unknown
}
}
}
// MARK: -
@ -36,11 +67,15 @@ public class GroupAccess: MTLModel {
public var members: GroupV2Access = .unknown
@objc
public var attributes: GroupV2Access = .unknown
@objc
public var addFromInviteLink: GroupV2Access = .unknown
public init(members: GroupV2Access,
attributes: GroupV2Access) {
attributes: GroupV2Access,
addFromInviteLink: GroupV2Access) {
self.members = members
self.attributes = attributes
self.addFromInviteLink = addFromInviteLink
super.init()
}
@ -62,12 +97,12 @@ public class GroupAccess: MTLModel {
@objc
public static var allAccess: GroupAccess {
return GroupAccess(members: .any, attributes: .any)
return GroupAccess(members: .any, attributes: .any, addFromInviteLink: .any)
}
@objc
public static var adminOnly: GroupAccess {
return GroupAccess(members: .administrator, attributes: .administrator)
return GroupAccess(members: .administrator, attributes: .administrator, addFromInviteLink: .administrator)
}
@objc
@ -77,34 +112,10 @@ public class GroupAccess: MTLModel {
@objc
public static var defaultForV2: GroupAccess {
return GroupAccess(members: .member, attributes: .member)
}
public class func groupV2Access(forProtoAccess value: GroupsProtoAccessControlAccessRequired) -> GroupV2Access {
switch value {
case .member:
return .member
case .administrator:
return .administrator
default:
return .unknown
}
}
public class func protoAccess(forGroupV2Access value: GroupV2Access) -> GroupsProtoAccessControlAccessRequired {
switch value {
case .any:
return .unknown
case .member:
return .member
case .administrator:
return .administrator
default:
return .unknown
}
return GroupAccess(members: .member, attributes: .member, addFromInviteLink: .unsatisfiable)
}
public override var debugDescription: String {
return "[members: \(members.description), attributes: \(attributes.description), ]"
return "[members: \(members), attributes: \(attributes), addFromInviteLink: \(addFromInviteLink), ]"
}
}

View File

@ -246,9 +246,9 @@ public class GroupManager: NSObject {
// The group creator is an administrator;
// the other members are normal users.
var builder = GroupMembership.Builder()
builder.addNonPendingMembers(Set(membersParam), role: .normal)
builder.addFullMembers(Set(membersParam), role: .normal)
builder.remove(localAddress)
builder.addNonPendingMember(localAddress, role: .administrator)
builder.addFullMember(localAddress, role: .administrator)
return builder.build()
}.then(on: .global()) { (groupMembership: GroupMembership) -> Promise<GroupMembership> in
// If we might create a v2 group,
@ -258,7 +258,7 @@ public class GroupManager: NSObject {
return Promise.value(groupMembership)
}
return firstly { () -> Promise<Void> in
self.groupsV2.tryToEnsureProfileKeyCredentials(for: Array(groupMembership.allUsers))
self.groupsV2.tryToEnsureProfileKeyCredentials(for: Array(groupMembership.allMembersOfAnyKind))
}.map(on: .global()) { (_) -> GroupMembership in
return groupMembership
}
@ -268,11 +268,11 @@ public class GroupManager: NSObject {
// Before we create a v2 group, we need to separate out the
// pending and non-pending members. If we already know we're
// going to create a v1 group, we shouldn't separate them.
let groupMembership = self.separatePendingMembers(in: proposedGroupMembership,
oldGroupModel: nil,
transaction: transaction)
let groupMembership = self.separatePendingProfileKeyMembers(in: proposedGroupMembership,
oldGroupModel: nil,
transaction: transaction)
guard groupMembership.nonPendingMembers.contains(localAddress) else {
guard groupMembership.isFullMember(localAddress) else {
throw OWSAssertionError("Missing localAddress.")
}
@ -379,9 +379,9 @@ public class GroupManager: NSObject {
// * We have a profile key credential for them.
// * Their account has the "groups v2" capability
// (e.g. all of their clients support groups v2.
private static func separatePendingMembers(in newGroupMembership: GroupMembership,
oldGroupModel: TSGroupModel?,
transaction: SDSAnyReadTransaction) -> GroupMembership {
private static func separatePendingProfileKeyMembers(in newGroupMembership: GroupMembership,
oldGroupModel: TSGroupModel?,
transaction: SDSAnyReadTransaction) -> GroupMembership {
guard let localUuid = tsAccountManager.localUuid else {
owsFailDebug("Missing localUuid.")
return newGroupMembership
@ -396,15 +396,19 @@ public class GroupManager: NSObject {
builder.copyInvalidInvites(from: oldGroupMembership)
assert(oldGroupModel.groupsVersion == .V2)
newMembers = newGroupMembership.allUsers.subtracting(oldGroupMembership.allUsers)
newMembers = newGroupMembership.allMembersOfAnyKind.subtracting(oldGroupMembership.allMembersOfAnyKind)
// Carry over existing members as they stand.
let existingMembers = oldGroupMembership.allUsers.intersection(newGroupMembership.allUsers)
let existingMembers = oldGroupMembership.allMembersOfAnyKind.intersection(newGroupMembership.allMembersOfAnyKind)
for address in existingMembers {
if oldGroupMembership.isPendingMember(address),
newGroupMembership.isNonPendingMember(address) {
if oldGroupMembership.isPendingProfileKeyMember(address),
newGroupMembership.isFullMember(address) {
// If we're adding a pending member, treat them as a new member.
newMembers.insert(address)
} else if oldGroupMembership.isRequestingMember(address),
newGroupMembership.isFullMember(address) {
// If we're adding a requesting member, treat them as a new member.
newMembers.insert(address)
} else {
builder.copyMember(address, from: oldGroupMembership)
}
@ -413,14 +417,14 @@ public class GroupManager: NSObject {
// Creating new group
// First, skip separation when creating v1 groups.
guard canUseV2(for: newGroupMembership.allUsers, transaction: transaction) else {
guard canUseV2(for: newGroupMembership.allMembersOfAnyKind, transaction: transaction) else {
// If any member of a new group doesn't support groups v2,
// we're going to create a v1 group. In that case, we
// don't want to separate out pending members.
return newGroupMembership
}
newMembers = newGroupMembership.allUsers
newMembers = newGroupMembership.allMembersOfAnyKind
}
// We only need to separate new members.
@ -451,11 +455,11 @@ public class GroupManager: NSObject {
// instead of adding them.
if address != localAddress &&
DebugFlags.groupsV2forceInvites {
builder.addPendingMember(address, role: role, addedByUuid: localUuid)
builder.addPendingProfileKeyMember(address, role: role, addedByUuid: localUuid)
} else if isPending {
builder.addPendingMember(address, role: role, addedByUuid: localUuid)
builder.addPendingProfileKeyMember(address, role: role, addedByUuid: localUuid)
} else {
builder.addNonPendingMember(address, role: role)
builder.addFullMember(address, role: role)
}
}
return builder.build()
@ -574,7 +578,7 @@ public class GroupManager: NSObject {
let groupMembership = GroupMembership(v1Members: Set(members))
// GroupsV2 TODO: Let tests specify access levels.
// GroupsV2 TODO: Fill in avatarUrlPath when we test v2 groups.
let groupAccess = GroupAccess.allAccess
let groupAccess = GroupAccess.defaultForV1
// Use buildGroupModel() to fill in defaults, like it was a new group.
var builder = TSGroupModelBuilder()
@ -654,7 +658,7 @@ public class GroupManager: NSObject {
throw OWSAssertionError("Missing localAddress.")
}
return (groupModel.groupsVersion == .V1 &&
groupModel.groupMembership.isPendingOrNonPendingMember(localAddress))
groupModel.groupMembership.isMemberOfAnyKind(localAddress))
}
// MARK: - Update Existing Group (Remote)
@ -667,10 +671,6 @@ public class GroupManager: NSObject {
disappearingMessageToken: DisappearingMessageToken?,
groupUpdateSourceAddress: SignalServiceAddress?,
transaction: SDSAnyWriteTransaction) throws -> UpsertGroupResult {
guard let localAddress = tsAccountManager.localAddress else {
throw OWSAssertionError("Missing localAddress.")
}
let groupId = proposedGroupModel.groupId
let updateInfo: UpdateInfo
do {
@ -794,7 +794,7 @@ public class GroupManager: NSObject {
guard RemoteConfig.groupsV2GoodCitizen else {
return Promise.value(())
}
return self.tryToEnableGroupsV2(for: Array(proposedGroupModel.groupMembership.allUsers), isBlocking: true, ignoreErrors: true)
return self.tryToEnableGroupsV2(for: Array(proposedGroupModel.groupMembership.allMembersOfAnyKind), isBlocking: true, ignoreErrors: true)
}.then(on: .global()) { () -> Promise<Void> in
return self.ensureLocalProfileHasCommitmentIfNecessary()
}.then(on: DispatchQueue.global()) { () -> Promise<String?> in
@ -884,7 +884,7 @@ public class GroupManager: NSObject {
// Always ensure we're a member of any v1 group we're updating.
var builder = proposedGroupModel.groupMembership.asBuilder
builder.remove(localAddress)
builder.addNonPendingMember(localAddress, role: .normal)
builder.addFullMember(localAddress, role: .normal)
let groupMembership = builder.build()
var groupModelBuilder = proposedGroupModel.asBuilder
@ -923,23 +923,19 @@ public class GroupManager: NSObject {
let oldDMConfiguration = OWSDisappearingMessagesConfiguration.fetchOrBuildDefault(with: thread, transaction: transaction)
let newDMConfiguration = dmConfiguration ?? oldDMConfiguration
for address in proposedGroupMembership.allUsers {
for address in proposedGroupMembership.allMembersOfAnyKind {
guard address.uuid != nil else {
throw OWSAssertionError("Group v2 member missing uuid.")
}
}
// Before we update a v2 group, we need to separate out the
// pending and non-pending members.
let groupMembership = self.separatePendingMembers(in: proposedGroupMembership,
oldGroupModel: oldGroupModel,
transaction: transaction)
guard groupMembership.nonPendingMembers.contains(localAddress) else {
throw OWSAssertionError("Missing localAddress.")
}
let groupMembership = self.separatePendingProfileKeyMembers(in: proposedGroupMembership,
oldGroupModel: oldGroupModel,
transaction: transaction)
// Don't try to modify a v2 group if we're not a member.
guard groupMembership.nonPendingMembers.contains(localAddress) else {
guard groupMembership.isFullMember(localAddress) else {
throw OWSAssertionError("Missing localAddress.")
}
@ -1108,7 +1104,7 @@ public class GroupManager: NSObject {
}
return updateGroupV2(groupModel: groupModel,
description: "Accept invite") { groupChangeSet in
groupChangeSet.promotePendingMember(localUuid)
groupChangeSet.promotePendingProfileKeyMember(localUuid)
}
}
}
@ -1135,7 +1131,7 @@ public class GroupManager: NSObject {
}
let oldGroupModel = groupThread.groupModel
// Note that we consult allUsers which includes pending members.
guard oldGroupModel.groupMembership.allUsers.contains(localAddress) else {
guard oldGroupModel.groupMembership.isMemberOfAnyKind(localAddress) else {
throw OWSAssertionError("Local user is not a member of the group.")
}
@ -1185,7 +1181,7 @@ public class GroupManager: NSObject {
transaction: SDSAnyWriteTransaction,
success: (() -> Void)?) {
guard groupThread.isLocalUserPendingOrNonPendingMember else {
guard groupThread.isLocalUserMemberOfAnyKind else {
owsFailDebug("unexpectedly trying to leave group for which we're not a member.")
return
}
@ -1535,7 +1531,8 @@ public class GroupManager: NSObject {
// We need to send v2 group updates to pending members
// as well. Normal group sends only include "full members".
assert(messageBuilder.additionalRecipients == nil)
let additionalRecipients = groupThread.groupModel.groupMembership.pendingMembers.filter { address in
let groupMembership = groupThread.groupModel.groupMembership
let additionalRecipients = groupMembership.pendingProfileKeyOrRequestMembers.filter { address in
return doesUserSupportGroupsV2(address: address,
transaction: transaction)
}
@ -1585,7 +1582,8 @@ public class GroupManager: NSObject {
// We need to send v2 group updates to pending members
// as well. Normal group sends only include "full members".
assert(messageBuilder.additionalRecipients == nil)
let additionalRecipients = groupThread.groupModel.groupMembership.pendingMembers.filter { address in
let groupMembership = groupThread.groupModel.groupMembership
let additionalRecipients = groupMembership.pendingProfileKeyOrRequestMembers.filter { address in
return doesUserSupportGroupsV2(address: address,
transaction: transaction)
}
@ -1931,7 +1929,7 @@ public class GroupManager: NSObject {
// if we're a member of the group. We don't want to do this if we're just a
// pending member or are leaving/have already left the group.
let groupMembership = groupThread.groupModel.groupMembership
guard groupMembership.isPendingOrNonPendingMember(localAddress) else {
guard groupMembership.isMemberOfAnyKind(localAddress) else {
return
}
profileManager.addThread(toProfileWhitelist: groupThread, transaction: transaction)

View File

@ -29,251 +29,217 @@ public extension TSGroupMemberRole {
// MARK: -
// NOTE: We only use this class for backwards compatibility.
@objc(_TtCC16SignalServiceKit15GroupMembership11MemberState)
class LegacyMemberState: MTLModel {
@objc
var role: TSGroupMemberRole = .normal
@objc
var isPending: Bool = false
// Only applies for pending members.
@objc
var addedByUuid: UUID?
@objc
public override init() {
super.init()
}
init(role: TSGroupMemberRole,
isPending: Bool,
addedByUuid: UUID? = nil) {
self.role = role
self.isPending = isPending
self.addedByUuid = addedByUuid
super.init()
}
@objc
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
@objc
public required init(dictionary dictionaryValue: [String: Any]!) throws {
try super.init(dictionary: dictionaryValue)
}
@objc
public var isAdministrator: Bool {
return role == .administrator
}
}
// MARK: -
@objc
public enum GroupMemberType: UInt {
case fullMember = 0
case pendingProfileKey = 1
case pendingRequest = 2
}
// MARK: -
extension GroupMemberType: CustomStringConvertible {
public var description: String {
switch self {
case .fullMember:
return ".fullMember"
case .pendingProfileKey:
return ".pendingProfileKey"
case .pendingRequest:
return ".pendingRequest"
}
}
}
// MARK: -
// This class is immutable.
@objc(GroupMembershipMemberState)
class MemberState: MTLModel {
@objc
var role: TSGroupMemberRole = .normal
func memberType() -> GroupMemberType {
.fullMember
}
@objc
public override init() {
super.init()
}
init(role: TSGroupMemberRole) {
self.role = role
super.init()
}
@objc
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
@objc
public required init(dictionary dictionaryValue: [String: Any]!) throws {
try super.init(dictionary: dictionaryValue)
}
@objc
public var isAdministrator: Bool {
return role == .administrator
}
}
// MARK: -
// This class is immutable.
@objc(GroupMembershipFullMemberState)
class FullMemberState: MemberState {
override func memberType() -> GroupMemberType {
.fullMember
}
}
// MARK: -
// This class is immutable.
@objc(GroupMembershipPendingProfileKeyMemberState)
class PendingProfileKeyMemberState: MemberState {
override func memberType() -> GroupMemberType {
.pendingProfileKey
}
@objc
var addedByUuid: UUID?
init(role: TSGroupMemberRole, addedByUuid: UUID? = nil) {
self.addedByUuid = addedByUuid
super.init(role: role)
}
@objc
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
@objc
public required init(dictionary dictionaryValue: [String: Any]!) throws {
try super.init(dictionary: dictionaryValue)
}
}
// MARK: -
// This class is immutable.
@objc(GroupMembershipPendingRequestMemberState)
class PendingRequestMemberState: MemberState {
override func memberType() -> GroupMemberType {
.pendingRequest
}
}
// MARK: -
// This class is immutable.
@objc(GroupMembershipInvalidInviteModel)
class InvalidInviteModel: MTLModel {
@objc
var userId: Data?
@objc
var addedByUserId: Data?
@objc
public override init() {
super.init()
}
init(userId: Data?, addedByUserId: Data? = nil) {
self.userId = userId
self.addedByUserId = addedByUserId
super.init()
}
@objc
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
@objc
public required init(dictionary dictionaryValue: [String: Any]!) throws {
try super.init(dictionary: dictionaryValue)
}
}
// MARK: -
// This class is immutable.
@objc
public class GroupMembership: MTLModel {
// This class is immutable.
@objc(_TtCC16SignalServiceKit15GroupMembership11MemberState)
class MemberState: MTLModel {
@objc
var role: TSGroupMemberRole = .normal
@objc
var isPending: Bool = false
// Only applies for pending members.
@objc
var addedByUuid: UUID?
@objc
public override init() {
super.init()
}
init(role: TSGroupMemberRole,
isPending: Bool,
addedByUuid: UUID? = nil) {
self.role = role
self.isPending = isPending
self.addedByUuid = addedByUuid
super.init()
}
@objc
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
@objc
public required init(dictionary dictionaryValue: [String: Any]!) throws {
try super.init(dictionary: dictionaryValue)
}
@objc
public var isAdministrator: Bool {
return role == .administrator
}
}
// This class is immutable.
@objc(GroupMembershipInvalidInviteModel)
class InvalidInviteModel: MTLModel {
@objc
var userId: Data?
@objc
var addedByUserId: Data?
@objc
public override init() {
super.init()
}
init(userId: Data?, addedByUserId: Data? = nil) {
self.userId = userId
self.addedByUserId = addedByUserId
super.init()
}
@objc
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
@objc
public required init(dictionary dictionaryValue: [String: Any]!) throws {
try super.init(dictionary: dictionaryValue)
}
}
typealias LegacyMemberStateMap = [SignalServiceAddress: LegacyMemberState]
// By using a single dictionary we ensure that no address has more than one state.
typealias MemberStateMap = [SignalServiceAddress: MemberState]
@objc
var memberStateMap: MemberStateMap
var memberStates: MemberStateMap
typealias InvalidInviteMap = [Data: InvalidInviteModel]
@objc
var invalidInviteMap: InvalidInviteMap
@objc
public var nonAdminMembers: Set<SignalServiceAddress> {
return Set(memberStateMap.filter { !$0.value.isAdministrator && !$0.value.isPending }.keys)
}
@objc
public var nonPendingAdministrators: Set<SignalServiceAddress> {
return Set(memberStateMap.filter { $0.value.isAdministrator && !$0.value.isPending }.keys)
}
@objc
public var nonPendingMembers: Set<SignalServiceAddress> {
return Set(memberStateMap.filter { !$0.value.isPending }.keys)
}
@objc
public var pendingNonAdminMembers: Set<SignalServiceAddress> {
return Set(memberStateMap.filter { !$0.value.isAdministrator && $0.value.isPending }.keys)
}
@objc
public var pendingAdministrators: Set<SignalServiceAddress> {
return Set(memberStateMap.filter { $0.value.isAdministrator && $0.value.isPending }.keys)
}
// pendingMembers includes normal and administrator "pending members".
@objc
public var pendingMembers: Set<SignalServiceAddress> {
return Set(memberStateMap.filter { $0.value.isPending }.keys)
}
@objc
public var pendingAndNonPendingMemberCount: Int {
return memberStateMap.count
}
// allUsers includes _all_ users:
//
// * Normal and administrator.
// * Pending and non-pending.
@objc
public var allUsers: Set<SignalServiceAddress> {
return Set(memberStateMap.keys)
}
public struct Builder {
private var memberStateMap = MemberStateMap()
private var invalidInviteMap = InvalidInviteMap()
public init() {}
internal init(memberStateMap: MemberStateMap, invalidInviteMap: InvalidInviteMap) {
self.memberStateMap = memberStateMap
self.invalidInviteMap = invalidInviteMap
}
public mutating func remove(_ uuid: UUID) {
remove(SignalServiceAddress(uuid: uuid))
}
public mutating func remove(_ address: SignalServiceAddress) {
memberStateMap.removeValue(forKey: address)
}
public mutating func remove(_ addresses: Set<SignalServiceAddress>) {
for address in addresses {
remove(address)
}
}
public mutating func addNonPendingMember(_ uuid: UUID,
role: TSGroupMemberRole) {
addNonPendingMember(SignalServiceAddress(uuid: uuid), role: role)
}
public mutating func addNonPendingMember(_ address: SignalServiceAddress,
role: TSGroupMemberRole) {
addNonPendingMembers([address], role: role)
}
public mutating func addNonPendingMembers(_ addresses: Set<SignalServiceAddress>,
role: TSGroupMemberRole) {
for address in addresses {
if memberStateMap[address] != nil {
owsFailDebug("Duplicate address.")
}
memberStateMap[address] = MemberState(role: role, isPending: false, addedByUuid: nil)
}
}
public mutating func addPendingMember(_ uuid: UUID,
role: TSGroupMemberRole,
addedByUuid: UUID) {
addPendingMember(SignalServiceAddress(uuid: uuid), role: role, addedByUuid: addedByUuid)
}
public mutating func addPendingMember(_ address: SignalServiceAddress,
role: TSGroupMemberRole,
addedByUuid: UUID) {
addPendingMembers([address], role: role, addedByUuid: addedByUuid)
}
public mutating func addPendingMembers(_ addresses: Set<SignalServiceAddress>,
role: TSGroupMemberRole,
addedByUuid: UUID) {
for address in addresses {
if memberStateMap[address] != nil {
Logger.error("Duplicate address.")
continue
// owsFailDebug("Duplicate address.")
}
memberStateMap[address] = MemberState(role: role, isPending: true, addedByUuid: addedByUuid)
}
}
public mutating func copyMember(_ address: SignalServiceAddress,
from oldGroupMembership: GroupMembership) {
guard let memberState = oldGroupMembership.memberStateMap[address] else {
owsFailDebug("Unknown address")
return
}
if memberStateMap[address] != nil {
owsFailDebug("Duplicate address.")
}
memberStateMap[address] = memberState
}
public mutating func addInvalidInvite(userId: Data, addedByUserId: Data) {
invalidInviteMap[userId] = InvalidInviteModel(userId: userId, addedByUserId: addedByUserId)
}
public mutating func removeInvalidInvite(userId: Data) {
invalidInviteMap.removeValue(forKey: userId)
}
public mutating func copyInvalidInvites(from other: GroupMembership) {
assert(invalidInviteMap.isEmpty)
invalidInviteMap = other.invalidInviteMap
}
internal func asMemberStateMap() -> MemberStateMap {
return memberStateMap
}
public func build() -> GroupMembership {
var memberStateMap = self.memberStateMap
let localProfileInvariantAddress = SignalServiceAddress(phoneNumber: kLocalProfileInvariantPhoneNumber)
if memberStateMap[localProfileInvariantAddress] != nil {
owsFailDebug("Removing localProfileInvariantAddress.")
memberStateMap.removeValue(forKey: localProfileInvariantAddress)
}
return GroupMembership(memberStateMap: memberStateMap,
invalidInviteMap: invalidInviteMap)
}
}
@objc
public override init() {
self.memberStateMap = MemberStateMap()
self.memberStates = MemberStateMap()
self.invalidInviteMap = [:]
super.init()
@ -281,22 +247,47 @@ public class GroupMembership: MTLModel {
@objc
required public init?(coder aDecoder: NSCoder) {
self.memberStateMap = MemberStateMap()
self.invalidInviteMap = [:]
if let invalidInviteMap = aDecoder.decodeObject(forKey: "invalidInviteMap") as? InvalidInviteMap {
self.invalidInviteMap = invalidInviteMap
} else {
// invalidInviteMap is optional.
self.invalidInviteMap = [:]
}
super.init(coder: aDecoder)
if let memberStates = aDecoder.decodeObject(forKey: "memberStates") as? MemberStateMap {
self.memberStates = memberStates
} else if let legacyMemberStateMap = aDecoder.decodeObject(forKey: "memberStateMap") as? LegacyMemberStateMap {
self.memberStates = Self.convertLegacyMemberStateMap(legacyMemberStateMap)
} else {
owsFailDebug("Could not decode.")
return nil
}
super.init()
}
@objc
public required init(dictionary dictionaryValue: [String: Any]!) throws {
self.memberStateMap = MemberStateMap()
self.invalidInviteMap = [:]
if let invalidInviteMap = dictionaryValue["invalidInviteMap"] as? InvalidInviteMap {
self.invalidInviteMap = invalidInviteMap
} else {
// invalidInviteMap is optional.
self.invalidInviteMap = [:]
}
try super.init(dictionary: dictionaryValue)
if let memberStates = dictionaryValue["memberStates"] as? MemberStateMap {
self.memberStates = memberStates
} else if let legacyMemberStateMap = dictionaryValue["memberStateMap"] as? LegacyMemberStateMap {
self.memberStates = Self.convertLegacyMemberStateMap(legacyMemberStateMap)
} else {
throw OWSAssertionError("Could not decode.")
}
super.init()
}
internal init(memberStateMap: MemberStateMap, invalidInviteMap: InvalidInviteMap) {
self.memberStateMap = memberStateMap
internal init(memberStates: MemberStateMap, invalidInviteMap: InvalidInviteMap) {
self.memberStates = memberStates
self.invalidInviteMap = invalidInviteMap
super.init()
@ -305,89 +296,38 @@ public class GroupMembership: MTLModel {
@objc
public init(v1Members: Set<SignalServiceAddress>) {
var builder = Builder()
builder.addNonPendingMembers(v1Members, role: .normal)
self.memberStateMap = builder.asMemberStateMap()
builder.addFullMembers(v1Members, role: .normal)
self.memberStates = builder.asMemberStateMap()
self.invalidInviteMap = [:]
super.init()
}
private static func convertLegacyMemberStateMap(_ legacyMemberStateMap: LegacyMemberStateMap) -> MemberStateMap {
var result = MemberStateMap()
for (address, legacyMemberState) in legacyMemberStateMap {
let memberState: MemberState
if legacyMemberState.isPending {
if let addedByUuid = legacyMemberState.addedByUuid {
memberState = PendingProfileKeyMemberState(role: legacyMemberState.role,
addedByUuid: addedByUuid)
} else {
owsFailDebug("Missing addedByUuid.")
continue
}
} else {
memberState = FullMemberState(role: legacyMemberState.role)
}
result[address] = memberState
}
return result
}
@objc
public static var empty: GroupMembership {
return Builder().build()
}
public func role(for uuid: UUID) -> TSGroupMemberRole? {
return role(for: SignalServiceAddress(uuid: uuid))
}
public func role(for address: SignalServiceAddress) -> TSGroupMemberRole? {
guard let memberState = memberStateMap[address] else {
return nil
}
return memberState.role
}
public func isAdministrator(_ address: SignalServiceAddress) -> Bool {
guard let memberState = memberStateMap[address] else {
return false
}
return memberState.isAdministrator
}
public func isAdministrator(_ uuid: UUID) -> Bool {
return isAdministrator(SignalServiceAddress(uuid: uuid))
}
@objc
public func isNonPendingMember(_ address: SignalServiceAddress) -> Bool {
guard let memberState = memberStateMap[address] else {
return false
}
return !memberState.isPending
}
public func isNonPendingMember(_ uuid: UUID) -> Bool {
return isNonPendingMember(SignalServiceAddress(uuid: uuid))
}
@objc
public func isPendingMember(_ address: SignalServiceAddress) -> Bool {
guard let memberState = memberStateMap[address] else {
return false
}
return memberState.isPending
}
public func isPendingMember(_ uuid: UUID) -> Bool {
isPendingMember(SignalServiceAddress(uuid: uuid))
}
// When we check "is X a member?" we might mean...
//
// * Is X a "full" member or a pending member?
// * Is X a "full" member and not a pending member?
// * Is X a "normal" member and not an administrator member?
// * Is X a "normal" member or an administrator member?
// * Some combination thereof.
//
// This method is intended tests the inclusive case: pending
// or non-pending, any role.
@objc
public func isPendingOrNonPendingMember(_ address: SignalServiceAddress) -> Bool {
return memberStateMap[address] != nil
}
public func isPendingOrNonPendingMember(_ uuid: UUID) -> Bool {
return isPendingOrNonPendingMember(SignalServiceAddress(uuid: uuid))
}
public func addedByUuid(forPendingMember address: SignalServiceAddress) -> UUID? {
assert(isPendingMember(address))
return memberStateMap[address]?.addedByUuid
}
@objc
public static func normalize(_ addresses: [SignalServiceAddress]) -> [SignalServiceAddress] {
return Array(Set(addresses))
@ -415,19 +355,309 @@ public class GroupMembership: MTLModel {
}
public var asBuilder: Builder {
return Builder(memberStateMap: memberStateMap, invalidInviteMap: invalidInviteMap)
return Builder(memberStates: memberStates, invalidInviteMap: invalidInviteMap)
}
public override var debugDescription: String {
var result = "["
for address in GroupMembership.normalize(Array(allUsers)) {
guard let memberState = memberStateMap[address] else {
for address in GroupMembership.normalize(Array(allMembersOfAnyKind)) {
guard let memberState = memberStates[address] else {
owsFailDebug("Missing memberState.")
continue
}
result += "\(address), isPending: \(memberState.isPending), isAdministrator: \(memberState.isAdministrator)\n"
result += "\(address), memberType: \(memberState.memberType()), role: \(memberState.role)\n"
}
result += "]"
return result
}
}
// MARK: - Swift Accessors
public extension GroupMembership {
var fullMemberAdministrators: Set<SignalServiceAddress> {
return Set(memberStates.filter { $0.value.isAdministrator && $0.value.memberType() == .fullMember }.keys)
}
var fullMembers: Set<SignalServiceAddress> {
return Set(memberStates.filter { $0.value.memberType() == .fullMember }.keys)
}
var pendingProfileKeyMembers: Set<SignalServiceAddress> {
return Set(memberStates.filter { $0.value.memberType() == .pendingProfileKey }.keys)
}
var pendingRequestMembers: Set<SignalServiceAddress> {
return Set(memberStates.filter { $0.value.memberType() == .pendingRequest }.keys)
}
var fullOrPendingProfileKeyMembers: Set<SignalServiceAddress> {
let memberTypes: [GroupMemberType] = [ .fullMember, .pendingProfileKey ]
return Set(memberStates.filter { memberTypes.contains($0.value.memberType()) }.keys)
}
var pendingProfileKeyOrRequestMembers: Set<SignalServiceAddress> {
let memberTypes: [GroupMemberType] = [ .pendingProfileKey, .pendingRequest ]
return Set(memberStates.filter { memberTypes.contains($0.value.memberType()) }.keys)
}
// allMembersOfAnyKind includes _all_ members:
//
// * Normal and administrator.
// * Normal, pending profile key, requesting.
var allMembersOfAnyKind: Set<SignalServiceAddress> {
return Set(memberStates.keys)
}
// allUsers includes _all_ members:
//
// * Normal and administrator.
// * Normal, pending profile key, requesting.
var allMemberUuidsOfAnyKind: Set<UUID> {
return Set(memberStates.keys.compactMap { $0.uuid })
}
func role(for uuid: UUID) -> TSGroupMemberRole? {
return role(for: SignalServiceAddress(uuid: uuid))
}
func role(for address: SignalServiceAddress) -> TSGroupMemberRole? {
guard let memberState = memberStates[address] else {
return nil
}
return memberState.role
}
func isFullOrInvitedAdministrator(_ address: SignalServiceAddress) -> Bool {
guard let memberState = memberStates[address] else {
return false
}
guard memberState.isAdministrator else {
return false
}
switch memberState.memberType() {
case .fullMember, .pendingProfileKey:
return true
case .pendingRequest:
return false
}
}
func isFullOrInvitedAdministrator(_ uuid: UUID) -> Bool {
return isFullOrInvitedAdministrator(SignalServiceAddress(uuid: uuid))
}
func isFullMemberAndAdministrator(_ address: SignalServiceAddress) -> Bool {
guard let memberState = memberStates[address] else {
return false
}
return memberState.isAdministrator && memberState.memberType() == .fullMember
}
func isFullMemberAndAdministrator(_ uuid: UUID) -> Bool {
return isFullMemberAndAdministrator(SignalServiceAddress(uuid: uuid))
}
@objc
func isFullMember(_ address: SignalServiceAddress) -> Bool {
guard let memberState = memberStates[address] else {
return false
}
return memberState.memberType() == .fullMember
}
func isFullMember(_ uuid: UUID) -> Bool {
isFullMember(SignalServiceAddress(uuid: uuid))
}
@objc
func isPendingProfileKeyMember(_ address: SignalServiceAddress) -> Bool {
guard let memberState = memberStates[address] else {
return false
}
return memberState.memberType() == .pendingProfileKey
}
func isPendingProfileKeyMember(_ uuid: UUID) -> Bool {
isPendingProfileKeyMember(SignalServiceAddress(uuid: uuid))
}
func isRequestingMember(_ address: SignalServiceAddress) -> Bool {
guard let memberState = memberStates[address] else {
return false
}
return memberState.memberType() == .pendingRequest
}
func isRequestingMember(_ uuid: UUID) -> Bool {
isRequestingMember(SignalServiceAddress(uuid: uuid))
}
// Returns true for...
//
// * Any type: Full members, "pending invite" members, "requesting" members.
// * Any role: admin, non-admin.
//
// This method is intended tests the inclusive case: pending
// or non-pending, any role.
//
// This method does NOT return true for invalid invites,
// which don't have a UUID or address associated with them.
func isMemberOfAnyKind(_ address: SignalServiceAddress) -> Bool {
return memberStates[address] != nil
}
func isMemberOfAnyKind(_ uuid: UUID) -> Bool {
return isMemberOfAnyKind(SignalServiceAddress(uuid: uuid))
}
// This method should only be called for "pending profile key" members.
func addedByUuid(forPendingProfileKeyMember address: SignalServiceAddress) -> UUID? {
assert(isPendingProfileKeyMember(address))
guard let memberState = memberStates[address] else {
return nil
}
guard let pendingProfileKeyMemberState = memberState as? PendingProfileKeyMemberState else {
owsFailDebug("Unexpected member type.")
return nil
}
return pendingProfileKeyMemberState.addedByUuid
}
}
// MARK: - Builder
@objc
public extension GroupMembership {
struct Builder {
private var memberStates = MemberStateMap()
private var invalidInviteMap = InvalidInviteMap()
public init() {}
internal init(memberStates: MemberStateMap, invalidInviteMap: InvalidInviteMap) {
self.memberStates = memberStates
self.invalidInviteMap = invalidInviteMap
}
public mutating func remove(_ uuid: UUID) {
remove(SignalServiceAddress(uuid: uuid))
}
public mutating func remove(_ address: SignalServiceAddress) {
memberStates.removeValue(forKey: address)
}
public mutating func remove(_ addresses: Set<SignalServiceAddress>) {
for address in addresses {
remove(address)
}
}
public mutating func addFullMember(_ uuid: UUID,
role: TSGroupMemberRole) {
addFullMember(SignalServiceAddress(uuid: uuid), role: role)
}
public mutating func addFullMember(_ address: SignalServiceAddress,
role: TSGroupMemberRole) {
addFullMembers([address], role: role)
}
public mutating func addFullMembers(_ addresses: Set<SignalServiceAddress>,
role: TSGroupMemberRole) {
for address in addresses {
if memberStates[address] != nil {
owsFailDebug("Duplicate address.")
}
memberStates[address] = FullMemberState(role: role)
}
}
public mutating func addPendingProfileKeyMember(_ uuid: UUID,
role: TSGroupMemberRole,
addedByUuid: UUID) {
addPendingProfileKeyMember(SignalServiceAddress(uuid: uuid), role: role, addedByUuid: addedByUuid)
}
public mutating func addPendingProfileKeyMember(_ address: SignalServiceAddress,
role: TSGroupMemberRole,
addedByUuid: UUID) {
addPendingProfileKeyMembers([address], role: role, addedByUuid: addedByUuid)
}
public mutating func addPendingProfileKeyMembers(_ addresses: Set<SignalServiceAddress>,
role: TSGroupMemberRole,
addedByUuid: UUID) {
for address in addresses {
if memberStates[address] != nil {
owsFailDebug("Duplicate address.")
continue
}
memberStates[address] = PendingProfileKeyMemberState(role: role, addedByUuid: addedByUuid)
}
}
public mutating func addRequestingMember(_ uuid: UUID) {
addRequestingMember(SignalServiceAddress(uuid: uuid))
}
public mutating func addRequestingMember(_ address: SignalServiceAddress) {
addRequestingMembers([address])
}
public mutating func addRequestingMembers(_ addresses: Set<SignalServiceAddress>) {
for address in addresses {
if memberStates[address] != nil {
owsFailDebug("Duplicate address.")
continue
}
memberStates[address] = PendingRequestMemberState()
}
}
public mutating func copyMember(_ address: SignalServiceAddress,
from oldGroupMembership: GroupMembership) {
guard let memberState = oldGroupMembership.memberStates[address] else {
owsFailDebug("Unknown address")
return
}
if memberStates[address] != nil {
owsFailDebug("Duplicate address.")
}
memberStates[address] = memberState
}
public mutating func addInvalidInvite(userId: Data, addedByUserId: Data) {
invalidInviteMap[userId] = InvalidInviteModel(userId: userId, addedByUserId: addedByUserId)
}
public mutating func removeInvalidInvite(userId: Data) {
invalidInviteMap.removeValue(forKey: userId)
}
public mutating func copyInvalidInvites(from other: GroupMembership) {
assert(invalidInviteMap.isEmpty)
invalidInviteMap = other.invalidInviteMap
}
internal func asMemberStateMap() -> MemberStateMap {
return memberStates
}
public func build() -> GroupMembership {
var memberStates = self.memberStates
let localProfileInvariantAddress = SignalServiceAddress(phoneNumber: kLocalProfileInvariantPhoneNumber)
if memberStates[localProfileInvariantAddress] != nil {
owsFailDebug("Removing localProfileInvariantAddress.")
memberStates.removeValue(forKey: localProfileInvariantAddress)
}
return GroupMembership(memberStates: memberStates,
invalidInviteMap: invalidInviteMap)
}
}
}

View File

@ -99,6 +99,7 @@ public protocol GroupsV2Swift: GroupsV2 {
// MARK: -
// TODO: Rename to GroupsV2ProposedChanges
public protocol GroupsV2ChangeSet: AnyObject {
var groupId: Data { get }
var groupSecretParamsData: Data { get }
@ -116,7 +117,7 @@ public protocol GroupsV2ChangeSet: AnyObject {
func setAccessForAttributes(_ value: GroupV2Access)
func promotePendingMember(_ uuid: UUID)
func promotePendingProfileKeyMember(_ uuid: UUID)
func setShouldLeaveGroupDeclineInvite()
@ -216,12 +217,11 @@ public protocol GroupV2Snapshot {
var groupAccess: GroupAccess { get }
var accessControlForAttributes: GroupsProtoAccessControlAccessRequired { get }
var accessControlForMembers: GroupsProtoAccessControlAccessRequired { get }
var disappearingMessageToken: DisappearingMessageToken { get }
var profileKeys: [UUID: Data] { get }
var inviteLinkPassword: Data? { get }
}
// MARK: -

View File

@ -24,6 +24,8 @@ public class TSGroupModelV2: TSGroupModel {
public var revision: UInt32 = 0
@objc
public var avatarUrlPath: String?
@objc
public var inviteLinkPassword: Data?
@objc
public required init(groupId: Data,
@ -33,7 +35,8 @@ public class TSGroupModelV2: TSGroupModel {
groupAccess: GroupAccess,
revision: UInt32,
secretParamsData: Data,
avatarUrlPath: String?) {
avatarUrlPath: String?,
inviteLinkPassword: Data?) {
assert(secretParamsData.count > 0)
self.membership = groupMembership
@ -41,11 +44,12 @@ public class TSGroupModelV2: TSGroupModel {
self.access = groupAccess
self.revision = revision
self.avatarUrlPath = avatarUrlPath
self.inviteLinkPassword = inviteLinkPassword
super.init(groupId: groupId,
name: name,
avatarData: avatarData,
members: Array(groupMembership.nonPendingMembers))
members: Array(groupMembership.fullMembers))
}
// MARK: - MTLModel
@ -74,7 +78,7 @@ public class TSGroupModelV2: TSGroupModel {
@objc
public override var groupMembers: [SignalServiceAddress] {
return Array(groupMembership.nonPendingMembers)
return Array(groupMembership.fullMembers)
}
public override func isEqual(to model: TSGroupModel,
@ -100,6 +104,9 @@ public class TSGroupModelV2: TSGroupModel {
guard other.avatarUrlPath == avatarUrlPath else {
return false
}
guard other.inviteLinkPassword == inviteLinkPassword else {
return false
}
return true
}
@ -115,6 +122,7 @@ public class TSGroupModelV2: TSGroupModel {
result += "secretParamsData: \(secretParamsData.hexadecimalString.prefix(32)),\n"
result += "revision: \(revision),\n"
result += "avatarUrlPath: \(String(describing: avatarUrlPath)),\n"
result += "inviteLinkPassword: \(inviteLinkPassword?.hexadecimalString ?? "None"),\n"
result += "]"
return result
}

View File

@ -24,6 +24,7 @@ public struct TSGroupModelBuilder {
public var groupSecretParamsData: Data?
public var newGroupSeed: NewGroupSeed?
public var avatarUrlPath: String?
public var inviteLinkPassword: Data?
public init() {}
@ -39,11 +40,12 @@ public struct TSGroupModelBuilder {
self.groupV2Revision = groupV2Snapshot.revision
self.groupSecretParamsData = groupV2Snapshot.groupSecretParamsData
self.avatarUrlPath = groupV2Snapshot.avatarUrlPath
self.inviteLinkPassword = groupV2Snapshot.inviteLinkPassword
}
public func build(transaction: SDSAnyReadTransaction) throws -> TSGroupModel {
let allUsers = groupMembership.allUsers
let allUsers = groupMembership.allMembersOfAnyKind
for recipientAddress in allUsers {
guard recipientAddress.isValid else {
throw OWSAssertionError("Invalid address.")
@ -70,13 +72,16 @@ public struct TSGroupModelBuilder {
switch groupsVersion {
case .V1:
if groupMembership.pendingMembers.count > 0 {
owsFailDebug("v1 group has pending members.")
if !groupMembership.pendingProfileKeyMembers.isEmpty {
owsFailDebug("v1 group has pending profile key members.")
}
if !groupMembership.pendingRequestMembers.isEmpty {
owsFailDebug("v1 group has pending request members.")
}
return TSGroupModel(groupId: groupId,
name: name,
avatarData: avatarData,
members: Array(groupMembership.nonPendingMembers))
members: Array(groupMembership.fullMembers))
case .V2:
let groupAccess = buildGroupAccess(groupsVersion: groupsVersion)
let groupSecretParamsData = try buildGroupSecretParamsData(newGroupSeed: newGroupSeed)
@ -90,7 +95,8 @@ public struct TSGroupModelBuilder {
groupAccess: groupAccess,
revision: groupV2Revision,
secretParamsData: groupSecretParamsData,
avatarUrlPath: avatarUrlPath)
avatarUrlPath: avatarUrlPath,
inviteLinkPassword: inviteLinkPassword)
}
}
@ -182,6 +188,7 @@ public extension TSGroupModel {
builder.groupV2Revision = v2.revision
builder.groupSecretParamsData = v2.secretParamsData
builder.avatarUrlPath = v2.avatarUrlPath
builder.inviteLinkPassword = v2.inviteLinkPassword
}
return builder
}