Path: blob/main/SignalServiceKit/Groups/GroupMembership.swift
1 views
//
// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import Foundation
public import LibSignalClient
// MARK: - GroupMemberState
private enum GroupMemberState: Equatable, Codable, CustomStringConvertible {
case fullMember(
role: TSGroupMemberRole,
didJoinFromInviteLink: Bool,
didJoinFromAcceptedJoinRequest: Bool,
)
case invited(role: TSGroupMemberRole, addedByAci: Aci)
case requesting
var role: TSGroupMemberRole {
switch self {
case .fullMember(let role, _, _):
return role
case .invited(let role, _):
return role
case .requesting:
return .`normal`
}
}
var isAdministrator: Bool {
role == .administrator
}
var isFullMember: Bool {
switch self {
case .fullMember: return true
default: return false
}
}
var isInvited: Bool {
switch self {
case .invited: return true
default: return false
}
}
var isRequesting: Bool {
switch self {
case .requesting: return true
default: return false
}
}
// MARK: -
private enum TypeKey: UInt, Codable {
case fullMember = 0
case invited = 1
case requesting = 2
}
private enum CodingKeys: String, CodingKey {
case typeKey
case role
case addedByAci = "addedByUuid"
case didJoinFromInviteLink
case didJoinFromAcceptedJoinRequest
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let typeKey = try container.decode(TypeKey.self, forKey: .typeKey)
switch typeKey {
case .fullMember:
let role = try container.decode(TSGroupMemberRole.self, forKey: .role)
let didJoinFromInviteLink = try container.decodeIfPresent(Bool.self, forKey: .didJoinFromInviteLink) ?? false
let didJoinFromAcceptedJoinRequest = try container.decodeIfPresent(
Bool.self,
forKey: .didJoinFromAcceptedJoinRequest,
) ?? false
self = .fullMember(
role: role,
didJoinFromInviteLink: didJoinFromInviteLink,
didJoinFromAcceptedJoinRequest: didJoinFromAcceptedJoinRequest,
)
case .invited:
let role = try container.decode(TSGroupMemberRole.self, forKey: .role)
let addedByAci = try container.decode(UUID.self, forKey: .addedByAci)
self = .invited(role: role, addedByAci: Aci(fromUUID: addedByAci))
case .requesting:
self = .requesting
}
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch self {
case .fullMember(let role, let didJoinFromInviteLink, let didJoinFromAcceptedJoinRequest):
try container.encode(TypeKey.fullMember, forKey: .typeKey)
try container.encode(role, forKey: .role)
try container.encode(didJoinFromInviteLink, forKey: .didJoinFromInviteLink)
try container.encode(
didJoinFromAcceptedJoinRequest,
forKey: .didJoinFromAcceptedJoinRequest,
)
case .invited(let role, let addedByAci):
try container.encode(TypeKey.invited, forKey: .typeKey)
try container.encode(role, forKey: .role)
try container.encode(addedByAci.rawUUID, forKey: .addedByAci)
case .requesting:
try container.encode(TypeKey.requesting, forKey: .typeKey)
}
}
// MARK: -
var description: String {
switch self {
case .fullMember: return ".fullMember"
case .invited: return ".invited"
case .requesting: return ".requesting"
}
}
}
// MARK: -
@objc
public class GroupMembership: NSObject, NSSecureCoding {
// MARK: Types
public typealias BannedAtTimestampMillis = UInt64
public typealias BannedMembersMap = [Aci: BannedAtTimestampMillis]
fileprivate typealias MemberStateMap = [SignalServiceAddress: GroupMemberState]
fileprivate typealias InvalidInviteMap = [Data: InvalidInviteModel]
fileprivate typealias MemberLabelsMap = [Aci: MemberLabel]
private typealias LegacyMemberStateMap = [SignalServiceAddress: LegacyMemberState]
// MARK: Init
fileprivate var memberStates: MemberStateMap
public fileprivate(set) var bannedMembers: BannedMembersMap
private var invalidInviteMap: InvalidInviteMap
private var memberLabels: MemberLabelsMap
public var invalidInviteUserIds: [Data] {
return Array(invalidInviteMap.keys)
}
@objc
override public init() {
self.memberStates = [:]
self.bannedMembers = [:]
self.invalidInviteMap = [:]
self.memberLabels = [:]
super.init()
}
public static var supportsSecureCoding: Bool { true }
@objc
public required init?(coder: NSCoder) {
self.invalidInviteMap = coder.decodeDictionary(
withKeyClass: NSData.self,
objectClass: InvalidInviteModel.self,
forKey: Self.invalidInviteMapKey,
) as [Data: InvalidInviteModel]? ?? [:]
if let memberStatesData = coder.decodeObject(of: NSData.self, forKey: Self.memberStatesKey) as Data? {
let decoder = JSONDecoder()
do {
self.memberStates = try decoder.decode(MemberStateMap.self, from: memberStatesData)
} catch {
owsFailDebug("Could not decode member states: \(error)")
return nil
}
} else if
let legacyMemberStateMap = coder.decodeDictionary(
withKeyClass: SignalServiceAddress.self,
objectClass: LegacyMemberState.self,
forKey: Self.legacyMemberStatesKey,
) as LegacyMemberStateMap?
{
self.memberStates = Self.convertLegacyMemberStateMap(legacyMemberStateMap)
} else {
owsFailDebug("Could not decode legacy member states.")
return nil
}
if
let bannedMembers = coder.decodeDictionary(
withKeyClass: NSUUID.self,
objectClass: NSNumber.self,
forKey: Self.bannedMembersKey,
) as [UUID: NSNumber]? as? [UUID: UInt64]
{
self.bannedMembers = bannedMembers.mapKeys(injectiveTransform: { Aci(fromUUID: $0) })
} else {
// TODO: (Group Abuse) we should debug assert here eventually.
// However, while clients are learning about banned members this is
// a normal path to hit.
self.bannedMembers = [:]
}
if let memberLabelsData = coder.decodeObject(of: NSData.self, forKey: Self.memberLabelsMapKey) as Data? {
let decoder = JSONDecoder()
do {
let memberLabelServiceIdBinaryMap = try decoder.decode(Dictionary<UUID, MemberLabel>.self, from: memberLabelsData)
self.memberLabels = memberLabelServiceIdBinaryMap.mapKeys(injectiveTransform: { Aci(fromUUID: $0) })
} catch {
owsFailDebug("Could not decode member labels: \(error)")
return nil
}
} else {
self.memberLabels = [:]
}
super.init()
}
private static var memberStatesKey: String { "memberStates" }
private static var legacyMemberStatesKey: String { "memberStateMap" }
private static var bannedMembersKey: String { "bannedMembers" }
private static var invalidInviteMapKey: String { "invalidInviteMap" }
private static var memberLabelsMapKey: String { "memberLabelsMapV3" }
public func encode(with aCoder: NSCoder) {
let encoder = JSONEncoder()
do {
let memberStatesData = try encoder.encode(self.memberStates)
aCoder.encode(memberStatesData, forKey: Self.memberStatesKey)
} catch {
owsFailDebug("Error: \(error)")
}
aCoder.encode(bannedMembers.mapKeys(injectiveTransform: { $0.rawUUID }), forKey: Self.bannedMembersKey)
aCoder.encode(invalidInviteMap, forKey: Self.invalidInviteMapKey)
do {
let memberLabelsData = try encoder.encode(self.memberLabels.mapKeys(injectiveTransform: { $0.rawUUID }))
aCoder.encode(memberLabelsData, forKey: Self.memberLabelsMapKey)
} catch {
owsFailDebug("Error: \(error)")
}
}
fileprivate init(
memberStates: MemberStateMap,
bannedMembers: BannedMembersMap,
invalidInviteMap: InvalidInviteMap,
memberLabels: MemberLabelsMap,
) {
self.memberStates = memberStates
self.bannedMembers = bannedMembers
self.invalidInviteMap = invalidInviteMap
self.memberLabels = memberLabels
super.init()
}
@objc
init(v1Members: [SignalServiceAddress]) {
var builder = Builder()
builder.addFullMembers(Set(v1Members), role: .normal)
self.memberStates = builder.memberStates
self.bannedMembers = [:]
self.invalidInviteMap = [:]
self.memberLabels = [:]
super.init()
}
public func showInfoMessageForChangeComparedTo(
to other: GroupMembership,
) -> Bool {
guard
Self.memberStates(
self.memberStates,
areEqualTo: other.memberStates,
)
else {
return true
}
guard self.bannedMembers == other.bannedMembers else {
return true
}
let invalidlyInvitedUserIdsSet = Set(invalidInviteUserIds)
let otherInvalidlyInvitedUserIdsSet = Set(other.invalidInviteUserIds)
return invalidlyInvitedUserIdsSet != otherInvalidlyInvitedUserIdsSet
}
#if TESTABLE_BUILD
/// Construction for tests is functionally equivalent to construction of a
/// group membership for a legacy, V1 group model.
public convenience init(membersForTest: [SignalServiceAddress]) {
self.init(v1Members: membersForTest)
}
#endif
// MARK: - Equality
@objc
override public func isEqual(_ object: Any!) -> Bool {
guard let other = object as? GroupMembership else {
return false
}
guard
Self.memberStates(
self.memberStates,
areEqualTo: other.memberStates,
)
else {
return false
}
guard self.bannedMembers == other.bannedMembers else {
return false
}
guard self.memberLabels == other.memberLabels else {
return false
}
let invalidlyInvitedUserIdsSet = Set(invalidInviteUserIds)
let otherInvalidlyInvitedUserIdsSet = Set(other.invalidInviteUserIds)
return invalidlyInvitedUserIdsSet == otherInvalidlyInvitedUserIdsSet
}
/// When comparing member states, ignore the ``didJoinFromInviteLink`` and
/// ``didJoinFromAcceptedJoinRequest`` fields.
/// These fields are not stored as part of memberships in group snapshots from
/// the service, and are only computed when a member joins a group and we add
/// them locally. If our local membership differs from a group snapshot's
/// only in these fields, we want to consider them equal to avoid clobbering our local state.
private static func memberStates(
_ memberStates: MemberStateMap,
areEqualTo otherMemberStates: MemberStateMap,
) -> Bool {
func hardcodeDidJoinViaInviteLink(for groupMemberState: GroupMemberState) -> GroupMemberState {
switch groupMemberState {
case .fullMember(let role, _, _):
return .fullMember(
role: role,
didJoinFromInviteLink: false,
didJoinFromAcceptedJoinRequest: false,
)
default:
return groupMemberState
}
}
guard memberStates.count == otherMemberStates.count else {
return false
}
return memberStates.allSatisfy { key, value -> Bool in
guard let otherValue = otherMemberStates[key] else { return false }
return hardcodeDidJoinViaInviteLink(for: value) == hardcodeDidJoinViaInviteLink(for: otherValue)
}
}
// MARK: -
private static func convertLegacyMemberStateMap(_ legacyMemberStateMap: LegacyMemberStateMap) -> MemberStateMap {
var result = MemberStateMap()
for (address, legacyMemberState) in legacyMemberStateMap {
let memberState: GroupMemberState
if legacyMemberState.isPending {
if let addedByUuid = legacyMemberState.addedByUuid {
memberState = .invited(role: legacyMemberState.role, addedByAci: Aci(fromUUID: addedByUuid))
} else {
owsFailDebug("Missing addedByUuid.")
continue
}
} else {
memberState = .fullMember(
role: legacyMemberState.role,
didJoinFromInviteLink: false,
didJoinFromAcceptedJoinRequest: false,
)
}
result[address] = memberState
}
return result
}
// MARK: -
public static var empty: GroupMembership {
return Builder().build()
}
public var asBuilder: Builder {
return Builder(
memberStates: memberStates,
bannedMembers: bannedMembers,
invalidInviteMap: invalidInviteMap,
memberLabels: memberLabels,
)
}
override public var debugDescription: String {
var result = "[\n"
for address in allMembersOfAnyKind.sorted(by: { ($0.serviceId?.serviceIdString ?? "") < ($1.serviceId?.serviceIdString ?? "") }) {
guard let memberState = memberStates[address] else {
owsFailDebug("Missing memberState.")
continue
}
result += "\(address), memberType: \(memberState)\n"
}
for (aci, bannedAtTimestamp) in bannedMembers {
result += "Banned: \(aci), at \(bannedAtTimestamp)\n"
}
result += "]"
return result
}
// MARK: -
public var allMembersOfAnyKind: Set<SignalServiceAddress> {
return Set(memberStates.keys)
}
public var allMembersOfAnyKindServiceIds: Set<ServiceId> {
return Set(memberStates.keys.lazy.compactMap { $0.serviceId })
}
public func isMemberOfAnyKind(_ address: SignalServiceAddress) -> Bool {
return memberStates[address] != nil
}
public func isMemberOfAnyKind(_ serviceId: ServiceId) -> Bool {
return isMemberOfAnyKind(SignalServiceAddress(serviceId))
}
// MARK: -
public func role(for serviceId: ServiceId) -> TSGroupMemberRole? {
return role(for: SignalServiceAddress(serviceId))
}
public func role(for address: SignalServiceAddress) -> TSGroupMemberRole? {
guard let memberState = memberStates[address] else {
return nil
}
return memberState.role
}
// MARK: -
public var fullMemberAdministrators: Set<SignalServiceAddress> {
return Set(memberStates.lazy.filter { $0.value.isAdministrator && $0.value.isFullMember }.map { $0.key })
}
public var fullMembers: Set<SignalServiceAddress> {
return Set(memberStates.lazy.filter { $0.value.isFullMember }.map { $0.key })
}
public func isFullMemberAndAdministrator(_ address: SignalServiceAddress) -> Bool {
guard let memberState = memberStates[address] else {
return false
}
return memberState.isAdministrator && memberState.isFullMember
}
public func isFullMemberAndAdministrator(_ serviceId: ServiceId) -> Bool {
return isFullMemberAndAdministrator(SignalServiceAddress(serviceId))
}
@objc
public func isFullMember(_ address: SignalServiceAddress) -> Bool {
guard let memberState = memberStates[address] else {
return false
}
return memberState.isFullMember
}
public func isFullMember(_ serviceId: ServiceId) -> Bool {
return isFullMember(SignalServiceAddress(serviceId))
}
/// This method should only be called for full members.
public func didJoinFromInviteLink(forFullMember address: SignalServiceAddress) -> Bool {
guard let memberState = memberStates[address] else {
owsFailDebug("Missing member: \(address)")
return false
}
switch memberState {
case .fullMember(_, let didJoinFromInviteLink, _):
return didJoinFromInviteLink
default:
owsFailDebug("Not a full member.")
return false
}
}
/// this method should only be called for full members.
public func didJoinFromAcceptedJoinRequest(forFullMember address: SignalServiceAddress) -> Bool {
guard let memberState = memberStates[address] else {
owsFailDebug("Missing member: \(address)")
return false
}
switch memberState {
case .fullMember(_, _, let didJoinFromAcceptedJoinRequest):
return didJoinFromAcceptedJoinRequest
default:
owsFailDebug("Not a full member.")
return false
}
}
// MARK: -
public var invitedMembers: Set<SignalServiceAddress> {
return Set(memberStates.lazy.filter { $0.value.isInvited }.map { $0.key })
}
public func isInvitedMember(_ address: SignalServiceAddress) -> Bool {
guard let memberState = memberStates[address] else {
return false
}
return memberState.isInvited
}
public func isInvitedMember(_ serviceId: ServiceId) -> Bool {
return isInvitedMember(SignalServiceAddress(serviceId))
}
/// This method should only be called on invited members.
public func addedByAci(forInvitedMember address: SignalServiceAddress) -> Aci? {
guard let memberState = memberStates[address] else {
return nil
}
switch memberState {
case .invited(_, let addedByAci):
return addedByAci
default:
owsFailDebug("Not a pending profile key member.")
return nil
}
}
public func addedByAci(forInvitedMember serviceId: ServiceId) -> Aci? {
return addedByAci(forInvitedMember: SignalServiceAddress(serviceId))
}
// MARK: -
public var requestingMembers: Set<SignalServiceAddress> {
return Set(memberStates.lazy.filter { $0.value.isRequesting }.map { $0.key })
}
public func isRequestingMember(_ address: SignalServiceAddress) -> Bool {
guard let memberState = memberStates[address] else {
return false
}
return memberState.isRequesting
}
public func isRequestingMember(_ serviceId: ServiceId) -> Bool {
return isRequestingMember(SignalServiceAddress(serviceId))
}
// MARK: -
public func isBannedMember(_ aci: Aci) -> Bool {
return bannedMembers[aci] != nil
}
public func hasInvalidInvite(forUserId userId: Data) -> Bool {
return invalidInviteMap[userId] != nil
}
// MARK: -
public func canLocalUserLeaveGroupWithoutChoosingNewAdmin(localAci: Aci) -> Bool {
let fullMembers = Set(self.fullMembers.compactMap { $0.serviceId as? Aci })
let fullMemberAdmins = Set(self.fullMemberAdministrators.compactMap { $0.serviceId as? Aci })
return Self.canLocalUserLeaveGroupWithoutChoosingNewAdmin(
localAci: localAci,
fullMembers: fullMembers,
admins: fullMemberAdmins,
)
}
static func canLocalUserLeaveGroupWithoutChoosingNewAdmin(
localAci: Aci,
fullMembers: Set<Aci>,
admins: Set<Aci>,
) -> Bool {
// If there's already another admin or we're the only member, we can leave
// without selecting a new admin.
return Set([localAci]) != admins || Set([localAci]) == fullMembers
}
// MARK: -
/// Is this user's profile key exposed to the group?
public func hasProfileKeyInGroup(serviceId: ServiceId) -> Bool {
guard let memberState = memberStates[SignalServiceAddress(serviceId)] else {
return false
}
switch memberState {
case .fullMember, .requesting:
return true
case .invited:
return false
}
}
/// Can this user view the profile keys in the group?
public func canViewProfileKeys(serviceId: ServiceId) -> Bool {
guard let memberState = memberStates[SignalServiceAddress(serviceId)] else {
return false
}
switch memberState {
case .fullMember, .invited:
return true
case .requesting:
return false
}
}
// MARK: -
public enum AddableResult {
case alreadyInGroup
case addableWithProfileKeyCredential
case addableOrInvitable
}
public func canTryToAddToGroup(serviceId: ServiceId) -> AddableResult {
if self.isFullMember(serviceId) {
return .alreadyInGroup
}
if self.isRequestingMember(serviceId) {
return .addableOrInvitable
}
if self.isInvitedMember(serviceId) {
return .addableWithProfileKeyCredential
}
return .addableOrInvitable
}
public static func canTryToAddWithProfileKeyCredential(
serviceId: ServiceId,
groupsV2: any GroupsV2 = SSKEnvironment.shared.groupsV2Ref,
profileManager: any ProfileManager = SSKEnvironment.shared.profileManagerRef,
tsAccountManager: any TSAccountManager = DependenciesBridge.shared.tsAccountManager,
udManager: any OWSUDManager = SSKEnvironment.shared.udManagerRef,
tx: DBReadTransaction,
) -> Bool {
// We can add invited members if we have...
if let aci = serviceId as? Aci, groupsV2.hasProfileKeyCredential(for: aci, transaction: tx) {
return true
}
// ...or can get a credential for them.
return ProfileFetcherJob.canTryToFetchCredential(
serviceId: serviceId,
localIdentifiers: tsAccountManager.localIdentifiers(tx: tx)!,
profileManager: profileManager,
udManager: udManager,
tx: tx,
)
}
// MARK:
public func memberLabel(for aci: Aci) -> MemberLabel? {
return memberLabels[aci]
}
// MARK: - Builder
public struct Builder {
fileprivate var memberStates = MemberStateMap()
private var bannedMembers = BannedMembersMap()
private var invalidInviteMap = InvalidInviteMap()
private var memberLabels = MemberLabelsMap()
public init() {}
fileprivate init(
memberStates: MemberStateMap,
bannedMembers: BannedMembersMap,
invalidInviteMap: InvalidInviteMap,
memberLabels: MemberLabelsMap,
) {
self.memberStates = memberStates
self.bannedMembers = bannedMembers
self.invalidInviteMap = invalidInviteMap
self.memberLabels = memberLabels
}
// MARK: Member states
public mutating func remove(_ serviceId: ServiceId) {
remove(SignalServiceAddress(serviceId))
}
public mutating func remove(_ address: SignalServiceAddress) {
remove([address])
}
public mutating func remove(_ addresses: Set<SignalServiceAddress>) {
for address in addresses {
memberStates.removeValue(forKey: address)
}
}
public mutating func addFullMember(
_ aci: Aci,
role: TSGroupMemberRole,
didJoinFromInviteLink: Bool = false,
didJoinFromAcceptedJoinRequest: Bool = false,
) {
addFullMember(
SignalServiceAddress(aci),
role: role,
didJoinFromInviteLink: didJoinFromInviteLink,
didJoinFromAcceptedJoinRequest: didJoinFromAcceptedJoinRequest,
)
}
public mutating func addFullMember(
_ address: SignalServiceAddress,
role: TSGroupMemberRole,
didJoinFromInviteLink: Bool = false,
didJoinFromAcceptedJoinRequest: Bool = false,
) {
addFullMembers(
[address],
role: role,
didJoinFromInviteLink: didJoinFromInviteLink,
didJoinFromAcceptedJoinRequest: didJoinFromAcceptedJoinRequest,
)
}
public mutating func addFullMembers(
_ addresses: Set<SignalServiceAddress>,
role: TSGroupMemberRole,
didJoinFromInviteLink: Bool = false,
didJoinFromAcceptedJoinRequest: Bool = false,
) {
// Dupe is not necessarily an error; you might know of the UUID
// mapping for a user that another group member doesn't know about.
addMembers(
addresses,
withState: .fullMember(
role: role,
didJoinFromInviteLink: didJoinFromInviteLink,
didJoinFromAcceptedJoinRequest: didJoinFromAcceptedJoinRequest,
),
failOnDupe: false,
)
}
public mutating func addInvitedMember(_ serviceId: ServiceId, role: TSGroupMemberRole, addedByAci: Aci) {
addInvitedMember(SignalServiceAddress(serviceId), role: role, addedByAci: addedByAci)
}
public mutating func addInvitedMember(
_ address: SignalServiceAddress,
role: TSGroupMemberRole,
addedByAci: Aci,
) {
addInvitedMembers([address], role: role, addedByAci: addedByAci)
}
public mutating func addInvitedMembers(
_ addresses: Set<SignalServiceAddress>,
role: TSGroupMemberRole,
addedByAci: Aci,
) {
addMembers(addresses, withState: .invited(role: role, addedByAci: addedByAci))
}
public mutating func addRequestingMember(_ aci: Aci) {
addRequestingMember(SignalServiceAddress(aci))
}
public mutating func addRequestingMember(_ address: SignalServiceAddress) {
addRequestingMembers([address])
}
public mutating func addRequestingMembers(_ addresses: Set<SignalServiceAddress>) {
addMembers(addresses, withState: .requesting)
}
private mutating func addMembers(
_ addresses: Set<SignalServiceAddress>,
withState memberState: GroupMemberState,
failOnDupe: Bool = true,
) {
for address in addresses {
guard memberStates[address] == nil else {
let errorMessage = "Duplicate address."
if failOnDupe {
owsFailDebug(errorMessage)
} else {
Logger.warn(errorMessage)
}
continue
}
memberStates[address] = memberState
}
}
public func hasMemberOfAnyKind(_ address: SignalServiceAddress) -> Bool {
nil != memberStates[address]
}
// MARK: Banned members
public mutating func addBannedMember(_ aci: Aci, bannedAtTimestamp: BannedAtTimestampMillis) {
guard bannedMembers[aci] == nil else {
owsFailDebug("Duplicate banned member!")
return
}
bannedMembers[aci] = bannedAtTimestamp
}
public mutating func removeBannedMember(_ aci: Aci) {
guard bannedMembers[aci] != nil else {
owsFailDebug("Removing not-currently-banned member!")
return
}
bannedMembers.removeValue(forKey: aci)
}
// MARK: Invalid invites
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 func hasInvalidInvite(userId: Data) -> Bool {
nil != invalidInviteMap[userId]
}
// MARK: Member labels
public mutating func setMemberLabel(label: MemberLabel?, aci: Aci) {
memberLabels[aci] = label
}
// MARK: Build
public func build() -> GroupMembership {
owsAssertDebug(
Set(bannedMembers.keys.lazy.map { SignalServiceAddress($0) })
.isDisjoint(with: Set(memberStates.keys)),
)
// TODO: Why is this here? Uggh.
let memberStates = self.memberStates.filter {
$0.key.phoneNumber != OWSUserProfile.Constants.localProfilePhoneNumber
}
return GroupMembership(
memberStates: memberStates,
bannedMembers: bannedMembers,
invalidInviteMap: invalidInviteMap,
memberLabels: memberLabels,
)
}
}
// MARK: - Local user accessors
/// The local PNI, if it is present and an invited member.
///
/// - Note
/// PNIs can only be invited members. Further note that profile keys are
/// required for full and requesting members, and PNIs have no associated
/// profile or profile key.
private func localPniAsInvitedMember(localIdentifiers: LocalIdentifiers) -> Pni? {
if let localPni = localIdentifiers.pni, isInvitedMember(localPni) {
return localPni
}
return nil
}
public var isLocalUserMemberOfAnyKind: Bool {
guard let localIdentifiers = DependenciesBridge.shared.tsAccountManager.localIdentifiersWithMaybeSneakyTransaction else {
return false
}
if isMemberOfAnyKind(localIdentifiers.aciAddress) {
return true
}
return localPniAsInvitedMember(localIdentifiers: localIdentifiers) != nil
}
public var isLocalUserFullMember: Bool {
guard let localAci = DependenciesBridge.shared.tsAccountManager.localIdentifiersWithMaybeSneakyTransaction?.aci else {
return false
}
return isFullMember(localAci)
}
public var localUserMemberLabel: MemberLabel? {
guard let localAci = DependenciesBridge.shared.tsAccountManager.localIdentifiersWithMaybeSneakyTransaction?.aci else {
return nil
}
return memberLabel(for: localAci)
}
/// The ID at which the local user is invited, if at all.
///
/// Checks membership for the local ACI first. If none is available, falls
/// back to checking membership for the local PNI.
public func localUserInvitedAtServiceId(localIdentifiers: LocalIdentifiers) -> ServiceId? {
if isMemberOfAnyKind(localIdentifiers.aci) {
// If our ACI is any kind of member, return that membership rather
// than falling back to the PNI.
if isInvitedMember(localIdentifiers.aci) {
return localIdentifiers.aci
}
return nil
}
return localPniAsInvitedMember(localIdentifiers: localIdentifiers)
}
/// Whether the local user is an invited member.
///
/// Checks membership for the local ACI first. If none is available, falls
/// back to checking membership for the local PNI.
public var isLocalUserInvitedMember: Bool {
guard let localIdentifiers = DependenciesBridge.shared.tsAccountManager.localIdentifiersWithMaybeSneakyTransaction else {
return false
}
return localUserInvitedAtServiceId(localIdentifiers: localIdentifiers) != nil
}
public var isLocalUserRequestingMember: Bool {
guard let localAci = DependenciesBridge.shared.tsAccountManager.localIdentifiersWithMaybeSneakyTransaction?.aci else {
return false
}
return isRequestingMember(localAci)
}
public var isLocalUserFullOrInvitedMember: Bool {
return isLocalUserFullMember || isLocalUserInvitedMember
}
public var isLocalUserFullMemberAndAdministrator: Bool {
guard let localAci = DependenciesBridge.shared.tsAccountManager.localIdentifiersWithMaybeSneakyTransaction?.aci else {
return false
}
return isFullMemberAndAdministrator(localAci)
}
}
// MARK: - InvalidInviteModel
@objc(GroupMembershipInvalidInviteModel)
private final class InvalidInviteModel: NSObject, NSSecureCoding {
static var supportsSecureCoding: Bool { true }
init?(coder: NSCoder) {
self.addedByUserId = coder.decodeObject(of: NSData.self, forKey: "addedByUserId") as Data?
self.userId = coder.decodeObject(of: NSData.self, forKey: "userId") as Data?
}
func encode(with coder: NSCoder) {
if let addedByUserId {
coder.encode(addedByUserId, forKey: "addedByUserId")
}
if let userId {
coder.encode(userId, forKey: "userId")
}
}
override var hash: Int {
var hasher = Hasher()
hasher.combine(addedByUserId)
hasher.combine(userId)
return hasher.finalize()
}
override func isEqual(_ object: Any?) -> Bool {
guard let object = object as? Self else { return false }
guard type(of: self) == type(of: object) else { return false }
guard self.addedByUserId == object.addedByUserId else { return false }
guard self.userId == object.userId else { return false }
return true
}
let userId: Data?
let addedByUserId: Data?
init(userId: Data?, addedByUserId: Data? = nil) {
self.userId = userId
self.addedByUserId = addedByUserId
}
}
// MARK: - LegacyMemberState
@objc(_TtCC16SignalServiceKit15GroupMembership11MemberState)
private final class LegacyMemberState: NSObject, NSSecureCoding {
static var supportsSecureCoding: Bool { true }
init?(coder: NSCoder) {
self.addedByUuid = coder.decodeObject(of: NSUUID.self, forKey: "addedByUuid") as UUID?
self.isPending = coder.decodeObject(of: NSNumber.self, forKey: "isPending")?.boolValue ?? false
self.role = (coder.decodeObject(of: NSNumber.self, forKey: "role")?.uintValue).flatMap(TSGroupMemberRole.init(rawValue:)) ?? .normal
}
func encode(with coder: NSCoder) {
if let addedByUuid {
coder.encode(addedByUuid, forKey: "addedByUuid")
}
coder.encode(NSNumber(value: self.isPending), forKey: "isPending")
coder.encode(NSNumber(value: self.role.rawValue), forKey: "role")
}
override var hash: Int {
var hasher = Hasher()
hasher.combine(addedByUuid)
hasher.combine(isPending)
hasher.combine(role)
return hasher.finalize()
}
override func isEqual(_ object: Any?) -> Bool {
guard let object = object as? Self else { return false }
guard type(of: self) == type(of: object) else { return false }
guard self.addedByUuid == object.addedByUuid else { return false }
guard self.isPending == object.isPending else { return false }
guard self.role == object.role else { return false }
return true
}
let role: TSGroupMemberRole
let isPending: Bool
// Only applies for pending members.
let addedByUuid: UUID?
init(role: TSGroupMemberRole, isPending: Bool, addedByUuid: UUID? = nil) {
self.role = role
self.isPending = isPending
self.addedByUuid = addedByUuid
}
@objc
var isAdministrator: Bool {
return role == .administrator
}
}