// // Copyright 2020 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only // import LibSignalClient @objc public extension TSGroupThread { var groupId: Data { groupModel.groupId } var groupMembership: GroupMembership { groupModel.groupMembership } // MARK: - static let groupThreadUniqueIdPrefix = "g" @nonobjc private static let uniqueIdMappingStore = KeyValueStore(collection: "TSGroupThread.uniqueIdMappingStore") private static func mappingKey(forGroupId groupId: Data) -> String { groupId.hexadecimalString } private static func existingThreadId( forGroupId groupId: Data, transaction: DBReadTransaction, ) -> String? { owsAssertDebug(!groupId.isEmpty) let mappingKey = self.mappingKey(forGroupId: groupId) return uniqueIdMappingStore.getString(mappingKey, transaction: transaction) } /// Returns the uniqueId for the ``TSGroupThread`` with the given group ID, /// if one exists. /// /// We've historically stored a mapping of `[GroupId: ThreadUniqueId]`, /// which facilitated things like V1 -> V2 migration. We'll still check the /// mapping to find the correct unique ID for old threads who had an entry /// there, but for new threads going forward we'll deterministically derive /// a unique ID from the group ID. /// /// We've actually been doing a deterministic unique ID derivation for new /// threads for some time; we'd then also store that mapping, which is not /// necessary. static func threadId( forGroupId groupId: Data, transaction tx: DBReadTransaction, ) -> String { owsAssertDebug(!groupId.isEmpty) if let threadUniqueId = existingThreadId( forGroupId: groupId, transaction: tx, ) { return threadUniqueId } return defaultThreadId(forGroupId: groupId) } static func defaultThreadId(forGroupId groupId: Data) -> String { owsAssertDebug(!groupId.isEmpty) return groupThreadUniqueIdPrefix + groupId.base64EncodedString() } /// Sets a `[GroupId: ThreadUniqueId]` mapping for a legacy thread. /// /// All newly-created threads use a deterministic mapping from group ID to /// thread unique ID, so this is unnecessary except for legacy threads for /// whom the mapping does not exist. /// /// - SeeAlso ``threadId(forGroupId:transaction:)`` static func setGroupIdMappingForLegacyThread( threadUniqueId: String, groupId: Data, tx: DBWriteTransaction, ) { setGroupIdMapping(threadUniqueId: threadUniqueId, groupId: groupId, tx: tx) if GroupManager.isV1GroupId(groupId) { do { let v2GroupId = try self.v2GroupId(forV1GroupId: groupId) setGroupIdMapping(threadUniqueId: threadUniqueId, groupId: v2GroupId, tx: tx) } catch { Logger.warn("Couldn't set GV2 mapping for legacy thread") } } } private static func v2GroupId(forV1GroupId v1GroupId: Data) throws -> Data { owsPrecondition(GroupManager.isV1GroupId(v1GroupId)) let keyBytes = try hkdf( outputLength: GroupMasterKey.SIZE, inputKeyMaterial: v1GroupId, salt: [], info: Data("GV2 Migration".utf8), ) let contextInfo = try GroupV2ContextInfo.deriveFrom(masterKeyData: keyBytes) return contextInfo.groupId.serialize() } private static func setGroupIdMapping( threadUniqueId: String, groupId: Data, tx: DBWriteTransaction, ) { let mappingKey = mappingKey(forGroupId: groupId) uniqueIdMappingStore.setString(threadUniqueId, key: mappingKey, transaction: tx) } // MARK: - /// Posted when the group associated with this thread adds or removes members. /// /// The object is the group's unique ID as a string. Note that NotificationCenter dispatches by /// object identity rather than equality, so any observer should register for *all* membership /// changes and then filter the notifications they receive as needed. static let membershipDidChange = Notification.Name("TSGroupThread.membershipDidChange") func updateGroupMemberRecords(transaction: DBWriteTransaction) { let groupMemberUpdater = DependenciesBridge.shared.groupMemberUpdater groupMemberUpdater.updateRecords(groupThread: self, transaction: transaction) } } // MARK: - @objc public extension TSThread { var isLocalUserFullMemberOfThread: Bool { guard let groupThread = self as? TSGroupThread else { return true } return groupThread.groupModel.groupMembership.isLocalUserFullMember } var isTerminatedGroup: Bool { guard let groupThread = self as? TSGroupThread, let groupModelV2 = groupThread.groupModel as? TSGroupModelV2 else { return false } return groupModelV2.isTerminated } }