Path: blob/main/SignalServiceKit/Threads/TSGroupThread+OWS.swift
1 views
//
// 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
}
}