TM-SGNL-iOS/SignalServiceKit/Groups/GroupMembership.swift
TeleMessage developers dde0620daf initial commit
2025-05-03 12:28:28 -07:00

946 lines
30 KiB
Swift

//
// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import Foundation
public import LibSignalClient
// MARK: - GroupMemberState
private enum GroupMemberState: Equatable {
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
}
}
}
extension GroupMemberState: Codable {
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)
}
}
}
extension GroupMemberState: CustomStringConvertible {
public var description: String {
switch self {
case .fullMember: return ".fullMember"
case .invited: return ".invited"
case .requesting: return ".requesting"
}
}
}
// MARK: -
@objc
public class GroupMembership: MTLModel {
// MARK: Types
public typealias BannedAtTimestampMillis = UInt64
public typealias BannedMembersMap = [Aci: BannedAtTimestampMillis]
fileprivate typealias MemberStateMap = [SignalServiceAddress: GroupMemberState]
fileprivate typealias InvalidInviteMap = [Data: InvalidInviteModel]
private typealias LegacyMemberStateMap = [SignalServiceAddress: LegacyMemberState]
// MARK: Init
fileprivate var memberStates: MemberStateMap
public fileprivate(set) var bannedMembers: BannedMembersMap
private var invalidInviteMap: InvalidInviteMap
public var invalidInviteUserIds: [Data] {
return Array(invalidInviteMap.keys)
}
@objc
public override init() {
self.memberStates = [:]
self.bannedMembers = [:]
self.invalidInviteMap = [:]
super.init()
}
@objc
required public init?(coder aDecoder: NSCoder) {
if let invalidInviteMap = aDecoder.decodeObject(forKey: Self.invalidInviteMapKey) as? InvalidInviteMap {
self.invalidInviteMap = invalidInviteMap
} else {
// invalidInviteMap is optional.
self.invalidInviteMap = [:]
}
if let memberStatesData = aDecoder.decodeObject(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 = aDecoder.decodeObject(forKey: Self.legacyMemberStatesKey) as? LegacyMemberStateMap {
self.memberStates = Self.convertLegacyMemberStateMap(legacyMemberStateMap)
} else {
owsFailDebug("Could not decode legacy member states.")
return nil
}
if let bannedMembers = aDecoder.decodeObject(forKey: Self.bannedMembersKey) as? [UUID: BannedAtTimestampMillis] {
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 = [:]
}
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" }
public override 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)
}
@objc
public required init(dictionary dictionaryValue: [String: Any]!) throws {
fatalError("init(dictionary:) has not been implemented")
}
fileprivate init(
memberStates: MemberStateMap,
bannedMembers: BannedMembersMap,
invalidInviteMap: InvalidInviteMap
) {
self.memberStates = memberStates
self.bannedMembers = bannedMembers
self.invalidInviteMap = invalidInviteMap
super.init()
}
@objc
init(v1Members: [SignalServiceAddress]) {
var builder = Builder()
builder.addFullMembers(Set(v1Members), role: .normal)
self.memberStates = builder.memberStates
self.bannedMembers = [:]
self.invalidInviteMap = [:]
super.init()
}
#if TESTABLE_BUILD
/// Construction for tests is functionally equivalent to construction of a
/// group membership for a legacy, V1 group model.
convenience init(membersForTest: [SignalServiceAddress]) {
self.init(v1Members: membersForTest)
}
#endif
// MARK: - Equality
@objc
public override 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
}
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: -
@objc
public static var empty: GroupMembership {
return Builder().build()
}
@objc
public static func normalize(_ addresses: [SignalServiceAddress]) -> [SignalServiceAddress] {
return Array(Set(addresses))
.sorted(by: { (l, r) in l.compare(r) == .orderedAscending })
}
public var asBuilder: Builder {
return Builder(
memberStates: memberStates,
bannedMembers: bannedMembers,
invalidInviteMap: invalidInviteMap
)
}
public override var debugDescription: String {
var result = "[\n"
for address in GroupMembership.normalize(Array(allMembersOfAnyKind)) {
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: - Accessors
public extension GroupMembership {
var fullMemberAdministrators: Set<SignalServiceAddress> {
return Set(memberStates.lazy.filter { $0.value.isAdministrator && $0.value.isFullMember }.map { $0.key })
}
var fullMembers: Set<SignalServiceAddress> {
return Set(memberStates.lazy.filter { $0.value.isFullMember }.map { $0.key })
}
var invitedMembers: Set<SignalServiceAddress> {
return Set(memberStates.lazy.filter { $0.value.isInvited }.map { $0.key })
}
var requestingMembers: Set<SignalServiceAddress> {
return Set(memberStates.lazy.filter { $0.value.isRequesting }.map { $0.key })
}
var fullOrInvitedMembers: Set<SignalServiceAddress> {
return Set(memberStates.lazy.filter {
$0.value.isFullMember || $0.value.isInvited
}.map { $0.key })
}
var invitedOrRequestMembers: Set<SignalServiceAddress> {
return Set(memberStates.lazy.filter {
$0.value.isInvited || $0.value.isRequesting
}.map { $0.key })
}
var allMembersOfAnyKind: Set<SignalServiceAddress> {
return Set(memberStates.keys)
}
var allMembersOfAnyKindServiceIds: Set<ServiceId> {
return Set(memberStates.keys.lazy.compactMap { $0.serviceId })
}
}
public extension GroupMembership {
func role(for serviceId: ServiceId) -> TSGroupMemberRole? {
return role(for: SignalServiceAddress(serviceId))
}
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
}
switch memberState {
case .fullMember(let role, _, _):
return role == .administrator
case .invited(let role, _):
return role == .administrator
case .requesting:
return false
}
}
func isFullOrInvitedAdministrator(_ serviceId: ServiceId) -> Bool {
return isFullOrInvitedAdministrator(SignalServiceAddress(serviceId))
}
@objc
func isFullMemberAndAdministrator(_ address: SignalServiceAddress) -> Bool {
guard let memberState = memberStates[address] else {
return false
}
return memberState.isAdministrator && memberState.isFullMember
}
func isFullMemberAndAdministrator(_ serviceId: ServiceId) -> Bool {
return isFullMemberAndAdministrator(SignalServiceAddress(serviceId))
}
@objc
func isFullMember(_ address: SignalServiceAddress) -> Bool {
guard let memberState = memberStates[address] else {
return false
}
return memberState.isFullMember
}
func isFullMember(_ serviceId: ServiceId) -> Bool {
return isFullMember(SignalServiceAddress(serviceId))
}
@objc
func isInvitedMember(_ address: SignalServiceAddress) -> Bool {
guard let memberState = memberStates[address] else {
return false
}
return memberState.isInvited
}
func isInvitedMember(_ serviceId: ServiceId) -> Bool {
return isInvitedMember(SignalServiceAddress(serviceId))
}
func isRequestingMember(_ address: SignalServiceAddress) -> Bool {
guard let memberState = memberStates[address] else {
return false
}
return memberState.isRequesting
}
func isRequestingMember(_ serviceId: ServiceId) -> Bool {
return isRequestingMember(SignalServiceAddress(serviceId))
}
func isMemberOfAnyKind(_ address: SignalServiceAddress) -> Bool {
return memberStates[address] != nil
}
func isMemberOfAnyKind(_ serviceId: ServiceId) -> Bool {
return isMemberOfAnyKind(SignalServiceAddress(serviceId))
}
func isBannedMember(_ aci: Aci) -> Bool {
return bannedMembers[aci] != nil
}
func hasInvalidInvite(forUserId userId: Data) -> Bool {
return invalidInviteMap[userId] != nil
}
/// This method should only be called on invited members.
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
}
}
func addedByAci(forInvitedMember serviceId: ServiceId) -> Aci? {
return addedByAci(forInvitedMember: SignalServiceAddress(serviceId))
}
/// This method should only be called for full members.
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.
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
}
}
/// Is this user's profile key exposed to the group?
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?
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: - Builder
public extension GroupMembership {
struct Builder {
fileprivate var memberStates = MemberStateMap()
private var bannedMembers = BannedMembersMap()
private var invalidInviteMap = InvalidInviteMap()
public init() {}
fileprivate init(
memberStates: MemberStateMap,
bannedMembers: BannedMembersMap,
invalidInviteMap: InvalidInviteMap
) {
self.memberStates = memberStates
self.bannedMembers = bannedMembers
self.invalidInviteMap = invalidInviteMap
}
// 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: 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
)
}
}
}
// MARK: - Local user accessors
public extension GroupMembership {
/// 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
}
@objc
var isLocalUserMemberOfAnyKind: Bool {
guard let localIdentifiers = DependenciesBridge.shared.tsAccountManager.localIdentifiersWithMaybeSneakyTransaction else {
return false
}
if isMemberOfAnyKind(localIdentifiers.aciAddress) {
return true
}
return localPniAsInvitedMember(localIdentifiers: localIdentifiers) != nil
}
@objc
var isLocalUserFullMember: Bool {
guard let localAci = DependenciesBridge.shared.tsAccountManager.localIdentifiersWithMaybeSneakyTransaction?.aci else {
return false
}
return isFullMember(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.
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.
var isLocalUserInvitedMember: Bool {
guard let localIdentifiers = DependenciesBridge.shared.tsAccountManager.localIdentifiersWithMaybeSneakyTransaction else {
return false
}
return localUserInvitedAtServiceId(localIdentifiers: localIdentifiers) != nil
}
var isLocalUserRequestingMember: Bool {
guard let localAci = DependenciesBridge.shared.tsAccountManager.localIdentifiersWithMaybeSneakyTransaction?.aci else {
return false
}
return isRequestingMember(localAci)
}
@objc
var isLocalUserFullOrInvitedMember: Bool {
return isLocalUserFullMember || isLocalUserInvitedMember
}
var isLocalUserFullMemberAndAdministrator: Bool {
guard let localAci = DependenciesBridge.shared.tsAccountManager.localIdentifiersWithMaybeSneakyTransaction?.aci else {
return false
}
return isFullMemberAndAdministrator(localAci)
}
}
// MARK: - InvalidInviteModel
@objc(GroupMembershipInvalidInviteModel)
private class InvalidInviteModel: MTLModel {
@objc
var userId: Data?
@objc
var addedByUserId: Data?
init(userId: Data?, addedByUserId: Data? = nil) {
self.userId = userId
self.addedByUserId = addedByUserId
super.init()
}
@objc
public override init() {
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: - LegacyMemberState
@objc(_TtCC16SignalServiceKit15GroupMembership11MemberState)
private 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
}
}