sync last verified group name hash
This commit is contained in:
parent
719a3ee0bd
commit
b5d12fcab0
@ -1429,6 +1429,7 @@ private extension CVComponentState.Builder {
|
||||
|
||||
return CVComponentThreadDetails.buildComponentState(
|
||||
thread: thread,
|
||||
threadAssociatedData: threadAssociatedData,
|
||||
transaction: transaction,
|
||||
avatarBuilder: avatarBuilder,
|
||||
)
|
||||
|
||||
@ -432,6 +432,7 @@ public class CVComponentThreadDetails: CVComponentBase, CVRootComponent {
|
||||
|
||||
static func buildComponentState(
|
||||
thread: TSThread,
|
||||
threadAssociatedData: ThreadAssociatedData,
|
||||
transaction: DBReadTransaction,
|
||||
avatarBuilder: CVAvatarBuilder,
|
||||
) -> CVComponentState.ThreadDetails {
|
||||
@ -444,6 +445,7 @@ public class CVComponentThreadDetails: CVComponentBase, CVRootComponent {
|
||||
} else if let groupThread = thread as? TSGroupThread {
|
||||
return buildComponentState(
|
||||
groupThread: groupThread,
|
||||
threadAssociatedData: threadAssociatedData,
|
||||
transaction: transaction,
|
||||
avatarBuilder: avatarBuilder,
|
||||
)
|
||||
@ -519,6 +521,7 @@ public class CVComponentThreadDetails: CVComponentBase, CVRootComponent {
|
||||
|
||||
private static func buildComponentState(
|
||||
groupThread: TSGroupThread,
|
||||
threadAssociatedData: ThreadAssociatedData,
|
||||
transaction: DBReadTransaction,
|
||||
avatarBuilder: CVAvatarBuilder,
|
||||
) -> CVComponentState.ThreadDetails {
|
||||
@ -541,6 +544,7 @@ public class CVComponentThreadDetails: CVComponentBase, CVRootComponent {
|
||||
|
||||
let safetySection = Self.buildGroupsSafetySection(
|
||||
from: groupThread,
|
||||
threadAssociatedData: threadAssociatedData,
|
||||
tx: transaction,
|
||||
)
|
||||
let descriptionText: String? = {
|
||||
@ -815,6 +819,7 @@ public class CVComponentThreadDetails: CVComponentBase, CVRootComponent {
|
||||
extension CVComponentThreadDetails {
|
||||
private static func buildGroupsSafetySection(
|
||||
from groupThread: TSGroupThread,
|
||||
threadAssociatedData: ThreadAssociatedData,
|
||||
tx: DBReadTransaction,
|
||||
) -> CVComponentState.ThreadDetails.SafetySection {
|
||||
let accountManager = DependenciesBridge.shared.tsAccountManager
|
||||
@ -932,7 +937,7 @@ extension CVComponentThreadDetails {
|
||||
.color(Self.mutualGroupsTextColor),
|
||||
)
|
||||
|
||||
let shouldShowUnknownThreadWarning = SSKEnvironment.shared.contactManagerImplRef.isLowTrustGroup(groupThread: groupThread, tx: tx)
|
||||
let shouldShowUnknownThreadWarning = !threadAssociatedData.isGroupNameVerified(groupName: groupThread.groupNameOrDefault)
|
||||
|
||||
return .init(
|
||||
shouldShowProfileNamesEducation: shouldShowUnknownThreadWarning,
|
||||
|
||||
@ -163,6 +163,7 @@ public final class BackupArchiveThreadStore {
|
||||
isMarkedUnread: isMarkedUnread,
|
||||
mutedUntilTimestamp: mutedUntilTimestamp ?? 0,
|
||||
audioPlaybackRate: 1,
|
||||
lastVerifiedGroupNameHash: nil,
|
||||
)
|
||||
try threadAssociatedData.insert(context.tx.database)
|
||||
}
|
||||
|
||||
@ -262,28 +262,6 @@ private class SystemContactsCache {
|
||||
// MARK: -
|
||||
|
||||
extension OWSContactsManager: ContactManager {
|
||||
|
||||
public func isLowTrustGroup(groupThread: TSGroupThread, tx: DBReadTransaction) -> Bool {
|
||||
if groupIdsNotNeedingLowTrustWarningCache.contains(groupThread.groupId) {
|
||||
return false
|
||||
}
|
||||
|
||||
if SSKEnvironment.shared.profileManagerRef.isThread(inProfileWhitelist: groupThread, transaction: tx) {
|
||||
groupIdsNotNeedingLowTrustWarningCache.insert(groupThread.groupId)
|
||||
return false
|
||||
}
|
||||
|
||||
if !groupThread.hasPendingMessageRequest(transaction: tx) {
|
||||
return false
|
||||
}
|
||||
// We can skip "unknown thread warnings" if a group has members which are trusted.
|
||||
if hasWhitelistedGroupMember(groupThread: groupThread, tx: tx) {
|
||||
groupIdsNotNeedingLowTrustWarningCache.insert(groupThread.groupId)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private func isInWhitelistedGroupsWithLocalUser(
|
||||
otherAddress: SignalServiceAddress,
|
||||
requireMultipleMutualGroups: Bool,
|
||||
@ -318,12 +296,6 @@ extension OWSContactsManager: ContactManager {
|
||||
return false
|
||||
}
|
||||
|
||||
private func hasWhitelistedGroupMember(groupThread: TSGroupThread, tx: DBReadTransaction) -> Bool {
|
||||
groupThread.groupMembership.fullMembers.contains { member in
|
||||
SSKEnvironment.shared.profileManagerRef.isUser(inProfileWhitelist: member, transaction: tx)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Avatar Blurring
|
||||
|
||||
public func didTapToUnblurAvatar(for thread: TSThread) {
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
import CryptoKit
|
||||
import Foundation
|
||||
public import GRDB
|
||||
|
||||
@ -18,6 +19,10 @@ public class ThreadAssociatedData: NSObject, Codable, FetchableRecord, Persistab
|
||||
public private(set) var mutedUntilTimestamp: UInt64 = 0
|
||||
public private(set) var audioPlaybackRate: Float = 1
|
||||
|
||||
// The last group name that was set by the local user.
|
||||
// Nil if it has never been set by the local user.
|
||||
public private(set) var lastVerifiedGroupNameHash: Data?
|
||||
|
||||
public var isMuted: Bool { mutedUntilTimestamp > Date.ows_millisecondTimestamp() }
|
||||
|
||||
public var mutedUntilDate: Date? {
|
||||
@ -25,6 +30,19 @@ public class ThreadAssociatedData: NSObject, Codable, FetchableRecord, Persistab
|
||||
return Date(millisecondsSince1970: mutedUntilTimestamp)
|
||||
}
|
||||
|
||||
static func groupNameVerificationHash(groupName: String?) -> Data? {
|
||||
guard let groupName, let groupNameData = groupName.data(using: .utf8) else {
|
||||
return nil
|
||||
}
|
||||
var sha = SHA256()
|
||||
sha.update(data: groupNameData)
|
||||
return Data(sha.finalize())
|
||||
}
|
||||
|
||||
public func isGroupNameVerified(groupName: String) -> Bool {
|
||||
return lastVerifiedGroupNameHash == Self.groupNameVerificationHash(groupName: groupName)
|
||||
}
|
||||
|
||||
public static var alwaysMutedTimestamp: UInt64 { UInt64(LLONG_MAX) }
|
||||
|
||||
public static func fetchOrDefault(
|
||||
@ -97,6 +115,7 @@ public class ThreadAssociatedData: NSObject, Codable, FetchableRecord, Persistab
|
||||
case isMarkedUnread
|
||||
case mutedUntilTimestamp
|
||||
case audioPlaybackRate
|
||||
case lastVerifiedGroupNameHash
|
||||
}
|
||||
|
||||
public required init(from decoder: any Decoder) throws {
|
||||
@ -115,6 +134,9 @@ public class ThreadAssociatedData: NSObject, Codable, FetchableRecord, Persistab
|
||||
if let audioPlaybackRate = try container.decodeIfPresent(Float.self, forKey: .audioPlaybackRate) {
|
||||
self.audioPlaybackRate = audioPlaybackRate
|
||||
}
|
||||
if let lastVerifiedGroupNameHash = try container.decodeIfPresent(Data.self, forKey: .lastVerifiedGroupNameHash) {
|
||||
self.lastVerifiedGroupNameHash = lastVerifiedGroupNameHash
|
||||
}
|
||||
}
|
||||
|
||||
public func encode(to encoder: any Encoder) throws {
|
||||
@ -125,6 +147,7 @@ public class ThreadAssociatedData: NSObject, Codable, FetchableRecord, Persistab
|
||||
try container.encode(self.isMarkedUnread, forKey: .isMarkedUnread)
|
||||
try container.encode(Int64(bitPattern: self.mutedUntilTimestamp), forKey: .mutedUntilTimestamp)
|
||||
try container.encode(self.audioPlaybackRate, forKey: .audioPlaybackRate)
|
||||
try container.encodeIfPresent(lastVerifiedGroupNameHash, forKey: .lastVerifiedGroupNameHash)
|
||||
}
|
||||
|
||||
public func didInsert(with rowID: Int64, for column: String?) {
|
||||
@ -137,12 +160,14 @@ public class ThreadAssociatedData: NSObject, Codable, FetchableRecord, Persistab
|
||||
isMarkedUnread: Bool,
|
||||
mutedUntilTimestamp: UInt64,
|
||||
audioPlaybackRate: Float,
|
||||
lastVerifiedGroupNameHash: Data?,
|
||||
) {
|
||||
self.threadUniqueId = threadUniqueId
|
||||
self.isArchived = isArchived
|
||||
self.isMarkedUnread = isMarkedUnread
|
||||
self.mutedUntilTimestamp = mutedUntilTimestamp
|
||||
self.audioPlaybackRate = audioPlaybackRate
|
||||
self.lastVerifiedGroupNameHash = lastVerifiedGroupNameHash
|
||||
super.init()
|
||||
}
|
||||
|
||||
@ -151,6 +176,7 @@ public class ThreadAssociatedData: NSObject, Codable, FetchableRecord, Persistab
|
||||
isMarkedUnread: Bool? = nil,
|
||||
mutedUntilTimestamp: UInt64? = nil,
|
||||
audioPlaybackRate: Float? = nil,
|
||||
lastVerifiedGroupNameHash: Data? = nil,
|
||||
updateStorageService: Bool,
|
||||
transaction: DBWriteTransaction,
|
||||
) {
|
||||
@ -159,6 +185,7 @@ public class ThreadAssociatedData: NSObject, Codable, FetchableRecord, Persistab
|
||||
|| isMarkedUnread != nil
|
||||
|| mutedUntilTimestamp != nil
|
||||
|| audioPlaybackRate != nil
|
||||
|| lastVerifiedGroupNameHash != nil
|
||||
else {
|
||||
return owsFailDebug("You must set one value")
|
||||
}
|
||||
@ -176,6 +203,9 @@ public class ThreadAssociatedData: NSObject, Codable, FetchableRecord, Persistab
|
||||
if let audioPlaybackRate {
|
||||
associatedData.audioPlaybackRate = audioPlaybackRate
|
||||
}
|
||||
if let lastVerifiedGroupNameHash {
|
||||
associatedData.lastVerifiedGroupNameHash = lastVerifiedGroupNameHash
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -108,6 +108,9 @@ final class ThreadMerger {
|
||||
? valuePair.intoValue.audioPlaybackRate
|
||||
: valuePair.fromValue.audioPlaybackRate,
|
||||
|
||||
// Only group threads track the last verified name hash.
|
||||
lastVerifiedGroupNameHash: nil,
|
||||
|
||||
)
|
||||
|
||||
func newValueIfChanged<T: Equatable>(_ keyPath: KeyPath<ThreadAssociatedData, T>) -> T? {
|
||||
@ -122,6 +125,7 @@ final class ThreadMerger {
|
||||
isMarkedUnread: newValueIfChanged(\.isMarkedUnread),
|
||||
mutedUntilTimestamp: newValueIfChanged(\.mutedUntilTimestamp),
|
||||
audioPlaybackRate: newValueIfChanged(\.audioPlaybackRate),
|
||||
lastVerifiedGroupNameHash: nil,
|
||||
updateStorageService: true,
|
||||
tx: tx,
|
||||
)
|
||||
@ -421,6 +425,7 @@ protocol _ThreadMerger_ThreadAssociatedDataManagerShim {
|
||||
isMarkedUnread: Bool?,
|
||||
mutedUntilTimestamp: UInt64?,
|
||||
audioPlaybackRate: Float?,
|
||||
lastVerifiedGroupNameHash: Data?,
|
||||
updateStorageService: Bool,
|
||||
tx: DBWriteTransaction,
|
||||
)
|
||||
@ -433,6 +438,7 @@ class _ThreadMerger_ThreadAssociatedDataManagerWrapper: _ThreadMerger_ThreadAsso
|
||||
isMarkedUnread: Bool?,
|
||||
mutedUntilTimestamp: UInt64?,
|
||||
audioPlaybackRate: Float?,
|
||||
lastVerifiedGroupNameHash: Data?,
|
||||
updateStorageService: Bool,
|
||||
tx: DBWriteTransaction,
|
||||
) {
|
||||
@ -444,6 +450,7 @@ class _ThreadMerger_ThreadAssociatedDataManagerWrapper: _ThreadMerger_ThreadAsso
|
||||
isMarkedUnread: isMarkedUnread,
|
||||
mutedUntilTimestamp: mutedUntilTimestamp,
|
||||
audioPlaybackRate: audioPlaybackRate,
|
||||
lastVerifiedGroupNameHash: lastVerifiedGroupNameHash,
|
||||
updateStorageService: updateStorageService,
|
||||
transaction: tx,
|
||||
)
|
||||
@ -528,6 +535,7 @@ class ThreadMerger_MockThreadAssociatedDataManager: ThreadMerger.Shims.ThreadAss
|
||||
isMarkedUnread: Bool?,
|
||||
mutedUntilTimestamp: UInt64?,
|
||||
audioPlaybackRate: Float?,
|
||||
lastVerifiedGroupNameHash: Data?,
|
||||
updateStorageService: Bool,
|
||||
tx: DBWriteTransaction,
|
||||
) {
|
||||
@ -537,6 +545,7 @@ class ThreadMerger_MockThreadAssociatedDataManager: ThreadMerger.Shims.ThreadAss
|
||||
isMarkedUnread: isMarkedUnread ?? threadAssociatedData.isMarkedUnread,
|
||||
mutedUntilTimestamp: mutedUntilTimestamp ?? threadAssociatedData.mutedUntilTimestamp,
|
||||
audioPlaybackRate: audioPlaybackRate ?? threadAssociatedData.audioPlaybackRate,
|
||||
lastVerifiedGroupNameHash: lastVerifiedGroupNameHash ?? threadAssociatedData.lastVerifiedGroupNameHash,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -146,6 +146,12 @@ public class GroupManager: NSObject {
|
||||
groupV2Snapshot: snapshotResponse.groupSnapshot,
|
||||
transaction: tx,
|
||||
)
|
||||
|
||||
// Since local user created the group, it's name is verified.
|
||||
let lastVerifiedGroupNameHash = ThreadAssociatedData.groupNameVerificationHash(
|
||||
groupName: snapshotResponse.groupSnapshot.title,
|
||||
)
|
||||
|
||||
let groupModel = try builder.buildAsV2()
|
||||
|
||||
let thread = self.insertGroupThreadInDatabaseAndCreateInfoMessage(
|
||||
@ -155,6 +161,7 @@ public class GroupManager: NSObject {
|
||||
infoMessagePolicy: .insert,
|
||||
localIdentifiers: localIdentifiers,
|
||||
spamReportingMetadata: .createdByLocalAction,
|
||||
lastVerifiedGroupNameHash: lastVerifiedGroupNameHash,
|
||||
transaction: tx,
|
||||
)
|
||||
SSKEnvironment.shared.profileManagerRef.addGroupId(
|
||||
@ -243,6 +250,7 @@ public class GroupManager: NSObject {
|
||||
infoMessagePolicy: infoMessagePolicy,
|
||||
localIdentifiers: localIdentifiers,
|
||||
spamReportingMetadata: .unreportable,
|
||||
updatedLastVerifiedGroupNameHash: nil,
|
||||
transaction: transaction,
|
||||
)
|
||||
}
|
||||
@ -714,6 +722,7 @@ public class GroupManager: NSObject {
|
||||
//
|
||||
// newDisappearingMessageToken is nil because we don't want to change DM
|
||||
// state.
|
||||
// updatedLastVerifiedGroupNameHash is nil because this is not a change-name action.
|
||||
updateExistingGroupThreadInDatabaseAndCreateInfoMessage(
|
||||
groupThread: groupThread,
|
||||
newGroupModel: newGroupModel,
|
||||
@ -723,6 +732,7 @@ public class GroupManager: NSObject {
|
||||
infoMessagePolicy: .insert,
|
||||
localIdentifiers: localIdentifiers,
|
||||
spamReportingMetadata: .createdByLocalAction,
|
||||
updatedLastVerifiedGroupNameHash: nil,
|
||||
transaction: tx,
|
||||
)
|
||||
} catch {
|
||||
@ -827,8 +837,10 @@ public class GroupManager: NSObject {
|
||||
infoMessagePolicy: InfoMessagePolicy,
|
||||
localIdentifiers: LocalIdentifiers,
|
||||
spamReportingMetadata: GroupUpdateSpamReportingMetadata,
|
||||
lastVerifiedGroupNameHash: Data?,
|
||||
transaction: DBWriteTransaction,
|
||||
) -> TSGroupThread {
|
||||
let threadAssociatedDataStore = DependenciesBridge.shared.threadAssociatedDataStore
|
||||
|
||||
if let groupThread = TSGroupThread.fetch(groupId: groupModel.groupId, transaction: transaction) {
|
||||
owsFail("Inserting existing group thread: \(groupThread.logString).")
|
||||
@ -839,6 +851,14 @@ public class GroupManager: NSObject {
|
||||
tx: transaction,
|
||||
)
|
||||
|
||||
if let lastVerifiedGroupNameHash {
|
||||
if let threadAssociatedData = threadAssociatedDataStore.fetch(for: groupThread.uniqueId, tx: transaction) {
|
||||
threadAssociatedData.updateWith(lastVerifiedGroupNameHash: lastVerifiedGroupNameHash, updateStorageService: true, transaction: transaction)
|
||||
} else {
|
||||
owsFailDebug("missing threadAssociatedData for group")
|
||||
}
|
||||
}
|
||||
|
||||
let newDisappearingMessageToken = disappearingMessageToken ?? DisappearingMessageToken.disabledToken
|
||||
_ = updateDisappearingMessageConfiguration(
|
||||
newToken: newDisappearingMessageToken,
|
||||
@ -894,6 +914,7 @@ public class GroupManager: NSObject {
|
||||
infoMessagePolicy: InfoMessagePolicy,
|
||||
localIdentifiers: LocalIdentifiers,
|
||||
spamReportingMetadata: GroupUpdateSpamReportingMetadata,
|
||||
updatedLastVerifiedGroupNameHash: Data?,
|
||||
transaction: DBWriteTransaction,
|
||||
) -> TSGroupThread {
|
||||
if DebugFlags.internalLogging {
|
||||
@ -911,6 +932,7 @@ public class GroupManager: NSObject {
|
||||
infoMessagePolicy: infoMessagePolicy,
|
||||
localIdentifiers: localIdentifiers,
|
||||
spamReportingMetadata: spamReportingMetadata,
|
||||
updatedLastVerifiedGroupNameHash: updatedLastVerifiedGroupNameHash,
|
||||
transaction: transaction,
|
||||
)
|
||||
return groupThread
|
||||
@ -948,6 +970,7 @@ public class GroupManager: NSObject {
|
||||
infoMessagePolicy: infoMessagePolicy,
|
||||
localIdentifiers: localIdentifiers,
|
||||
spamReportingMetadata: spamReportingMetadata,
|
||||
lastVerifiedGroupNameHash: updatedLastVerifiedGroupNameHash,
|
||||
transaction: transaction,
|
||||
)
|
||||
}
|
||||
@ -969,6 +992,7 @@ public class GroupManager: NSObject {
|
||||
infoMessagePolicy: InfoMessagePolicy = .insert,
|
||||
localIdentifiers: LocalIdentifiers,
|
||||
spamReportingMetadata: GroupUpdateSpamReportingMetadata,
|
||||
updatedLastVerifiedGroupNameHash: Data?,
|
||||
transaction: DBWriteTransaction,
|
||||
) {
|
||||
guard
|
||||
@ -1106,6 +1130,12 @@ public class GroupManager: NSObject {
|
||||
transaction: transaction,
|
||||
)
|
||||
|
||||
if let updatedLastVerifiedGroupNameHash {
|
||||
let threadAssociatedDataStore = DependenciesBridge.shared.threadAssociatedDataStore
|
||||
let threadAssociatedData = threadAssociatedDataStore.fetch(for: groupThread.uniqueId, tx: transaction)
|
||||
threadAssociatedData?.updateWith(lastVerifiedGroupNameHash: updatedLastVerifiedGroupNameHash, updateStorageService: true, transaction: transaction)
|
||||
}
|
||||
|
||||
let shouldInsertInfoMessages: Bool
|
||||
switch infoMessagePolicy {
|
||||
case .insert:
|
||||
@ -1441,6 +1471,9 @@ extension GroupManager {
|
||||
title != existingGroupModel.groupName
|
||||
{
|
||||
groupChangeSet.setTitle(title)
|
||||
|
||||
// Updated verified group name hash in storage service.
|
||||
SSKEnvironment.shared.storageServiceManagerRef.recordPendingUpdates(groupModel: existingGroupModel)
|
||||
}
|
||||
|
||||
if
|
||||
|
||||
@ -148,6 +148,10 @@ public class GroupV2UpdatesImpl: GroupV2Updates {
|
||||
// The prior method throws if the revisions don't match.
|
||||
owsAssertDebug(changedGroupModel.newGroupModel.revision == changedGroupModel.oldGroupModel.revision + 1)
|
||||
|
||||
var updatedLastVerifiedGroupNameHash: Data?
|
||||
if changedGroupModel.shouldUpdateLastVerifiedGroupNameHash {
|
||||
updatedLastVerifiedGroupNameHash = ThreadAssociatedData.groupNameVerificationHash(groupName: changedGroupModel.newGroupModel.groupName)
|
||||
}
|
||||
GroupManager.updateExistingGroupThreadInDatabaseAndCreateInfoMessage(
|
||||
groupThread: groupThread,
|
||||
newGroupModel: changedGroupModel.newGroupModel,
|
||||
@ -156,6 +160,7 @@ public class GroupV2UpdatesImpl: GroupV2Updates {
|
||||
groupUpdateSource: changedGroupModel.updateSource,
|
||||
localIdentifiers: localIdentifiers,
|
||||
spamReportingMetadata: spamReportingMetadata,
|
||||
updatedLastVerifiedGroupNameHash: updatedLastVerifiedGroupNameHash,
|
||||
transaction: transaction,
|
||||
)
|
||||
// The prior method always updates the revision because we've confirmed it's newer.
|
||||
@ -181,7 +186,6 @@ public class GroupV2UpdatesImpl: GroupV2Updates {
|
||||
tx: transaction,
|
||||
)
|
||||
}
|
||||
|
||||
return groupThread
|
||||
}
|
||||
|
||||
@ -416,6 +420,35 @@ public extension GroupV2UpdatesImpl {
|
||||
try await SSKEnvironment.shared.groupsV2Ref.downloadAndApplyGroupAvatarIfSkipped(secretParams)
|
||||
}
|
||||
|
||||
private func lastVerifiedHashIfLocalUserCreatedGroup(secretParams: GroupSecretParams, localIdentifiers: LocalIdentifiers) async -> Data? {
|
||||
var lastVerifiedHash: Data?
|
||||
do {
|
||||
let groupsV2 = SSKEnvironment.shared.groupsV2Ref
|
||||
let groupV2Params = try GroupV2Params(groupSecretParams: secretParams)
|
||||
|
||||
let firstChangeAction = try await groupsV2.fetchRevisionZeroGroupChangeAction(secretParams: secretParams)
|
||||
let author = try firstChangeAction.author(groupV2Params: groupV2Params, localIdentifiers: localIdentifiers)
|
||||
switch author {
|
||||
case .localUser:
|
||||
if let verificationHash = ThreadAssociatedData.groupNameVerificationHash(groupName: firstChangeAction.snapshot?.title) {
|
||||
lastVerifiedHash = verificationHash
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
return lastVerifiedHash
|
||||
} catch GroupsV2Error.localUserNotInGroup {
|
||||
return nil
|
||||
} catch {
|
||||
owsFailDebug("Failed to fetch change action for revision 0 with an unexpected error: \(error)")
|
||||
// This is a best-effort check to determine if the local user created the group
|
||||
// in case the device that created the group was not on an updated build.
|
||||
// If it fails with an unexpected error, return nil and if the creating device
|
||||
// is up to date we will get the updated value from storage service.
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
private func tryToApplyGroupChangesFromService(
|
||||
secretParams: GroupSecretParams,
|
||||
spamReportingMetadata: GroupUpdateSpamReportingMetadata,
|
||||
@ -423,18 +456,33 @@ public extension GroupV2UpdatesImpl {
|
||||
groupSendEndorsementsResponse: GroupSendEndorsementsResponse?,
|
||||
options: TSGroupModelOptions,
|
||||
) async throws {
|
||||
guard
|
||||
let localIdentifiers = SSKEnvironment.shared.databaseStorageRef.read(block: { tx in
|
||||
let tsAccountManager = DependenciesBridge.shared.tsAccountManager
|
||||
return tsAccountManager.localIdentifiers(tx: tx)
|
||||
})
|
||||
else {
|
||||
throw OWSAssertionError("Missing localIdentifiers.")
|
||||
}
|
||||
|
||||
let groupV2Params = try GroupV2Params(groupSecretParams: secretParams)
|
||||
let groupId = try groupV2Params.groupPublicParams.getGroupIdentifier()
|
||||
|
||||
let threadExists = SSKEnvironment.shared.databaseStorageRef.read { transaction in
|
||||
TSGroupThread.fetch(forGroupId: groupId, tx: transaction) != nil
|
||||
}
|
||||
|
||||
var lastVerifiedGroupNameHash: Data?
|
||||
if !threadExists {
|
||||
// Only check rev0 if we're about to insert a new thread.
|
||||
lastVerifiedGroupNameHash = await lastVerifiedHashIfLocalUserCreatedGroup(
|
||||
secretParams: secretParams,
|
||||
localIdentifiers: localIdentifiers,
|
||||
)
|
||||
}
|
||||
try await SSKEnvironment.shared.databaseStorageRef.awaitableWrite { transaction in
|
||||
let tsAccountManager = DependenciesBridge.shared.tsAccountManager
|
||||
guard let localIdentifiers = tsAccountManager.localIdentifiers(tx: transaction) else {
|
||||
throw OWSAssertionError("Missing localIdentifiers.")
|
||||
}
|
||||
|
||||
let groupV2Params = try GroupV2Params(groupSecretParams: secretParams)
|
||||
let groupId = try groupV2Params.groupPublicParams.getGroupIdentifier()
|
||||
|
||||
let groupThread: TSGroupThread
|
||||
var localUserWasAddedBy: GroupUpdateSource?
|
||||
|
||||
let groupThread: TSGroupThread
|
||||
if let existingThread = TSGroupThread.fetch(forGroupId: groupId, tx: transaction) {
|
||||
groupThread = existingThread
|
||||
localUserWasAddedBy = nil
|
||||
@ -446,6 +494,7 @@ public extension GroupV2UpdatesImpl {
|
||||
groupChanges: groupChanges,
|
||||
groupModelOptions: options,
|
||||
localIdentifiers: localIdentifiers,
|
||||
lastVerifiedGroupNameHash: lastVerifiedGroupNameHash,
|
||||
transaction: transaction,
|
||||
)
|
||||
}
|
||||
@ -553,6 +602,7 @@ public extension GroupV2UpdatesImpl {
|
||||
groupChanges: [GroupV2Change],
|
||||
groupModelOptions: TSGroupModelOptions,
|
||||
localIdentifiers: LocalIdentifiers,
|
||||
lastVerifiedGroupNameHash: Data?,
|
||||
transaction: DBWriteTransaction,
|
||||
) throws -> (TSGroupThread, addedToNewThreadBy: GroupUpdateSource?) {
|
||||
if TSGroupThread.fetch(forGroupId: groupId, tx: transaction) != nil {
|
||||
@ -595,6 +645,7 @@ public extension GroupV2UpdatesImpl {
|
||||
infoMessagePolicy: .insert,
|
||||
localIdentifiers: localIdentifiers,
|
||||
spamReportingMetadata: spamReportingMetadata,
|
||||
updatedLastVerifiedGroupNameHash: lastVerifiedGroupNameHash,
|
||||
transaction: transaction,
|
||||
)
|
||||
|
||||
@ -669,7 +720,10 @@ public extension GroupV2UpdatesImpl {
|
||||
} else if let snapshot = groupChange.snapshot {
|
||||
logger.info("Applying snapshot.")
|
||||
|
||||
var builder = try TSGroupModelBuilder.builderForSnapshot(groupV2Snapshot: snapshot, transaction: transaction)
|
||||
var builder = try TSGroupModelBuilder.builderForSnapshot(
|
||||
groupV2Snapshot: snapshot,
|
||||
transaction: transaction,
|
||||
)
|
||||
builder.apply(options: options)
|
||||
newGroupModel = try builder.build()
|
||||
newDisappearingMessageToken = snapshot.disappearingMessageToken
|
||||
@ -682,7 +736,6 @@ public extension GroupV2UpdatesImpl {
|
||||
// not a single revision update.
|
||||
throw GroupsV2Error.groupChangeProtoForIncompatibleRevision
|
||||
}
|
||||
|
||||
GroupManager.updateExistingGroupThreadInDatabaseAndCreateInfoMessage(
|
||||
groupThread: groupThread,
|
||||
newGroupModel: newGroupModel,
|
||||
@ -691,6 +744,7 @@ public extension GroupV2UpdatesImpl {
|
||||
groupUpdateSource: groupUpdateSource,
|
||||
localIdentifiers: localIdentifiers,
|
||||
spamReportingMetadata: spamReportingMetadata,
|
||||
updatedLastVerifiedGroupNameHash: nil, // Nothing to update.
|
||||
transaction: transaction,
|
||||
)
|
||||
|
||||
@ -775,6 +829,7 @@ public extension GroupV2UpdatesImpl {
|
||||
infoMessagePolicy: .insert,
|
||||
localIdentifiers: localIdentifiers,
|
||||
spamReportingMetadata: spamReportingMetadata,
|
||||
updatedLastVerifiedGroupNameHash: nil,
|
||||
transaction: transaction,
|
||||
)
|
||||
|
||||
|
||||
@ -154,6 +154,8 @@ public protocol GroupsV2 {
|
||||
transaction: DBWriteTransaction,
|
||||
)
|
||||
|
||||
func fetchRevisionZeroGroupChangeAction(secretParams: GroupSecretParams) async throws -> GroupV2Change
|
||||
|
||||
func fetchSomeGroupChangeActions(
|
||||
secretParams: GroupSecretParams,
|
||||
source: GroupChangeActionFetchSource,
|
||||
@ -614,6 +616,10 @@ public class MockGroupsV2: GroupsV2 {
|
||||
owsFail("Not implemented")
|
||||
}
|
||||
|
||||
public func fetchRevisionZeroGroupChangeAction(secretParams: LibSignalClient.GroupSecretParams) async throws -> GroupV2Change {
|
||||
owsFail("not implemented")
|
||||
}
|
||||
|
||||
public func fetchSomeGroupChangeActions(secretParams: GroupSecretParams, source: GroupChangeActionFetchSource) async throws -> GroupChangesResponse {
|
||||
owsFail("not implemented")
|
||||
}
|
||||
|
||||
@ -254,8 +254,6 @@ public extension GroupsV2Impl {
|
||||
return true
|
||||
}
|
||||
|
||||
// This will try to update the group using incremental "changes" but
|
||||
// failover to using a "snapshot".
|
||||
do {
|
||||
try await SSKEnvironment.shared.groupV2UpdatesRef.fetchAndApplyCurrentGroupV2SnapshotFromService(
|
||||
secretParams: groupContextInfo.groupSecretParams,
|
||||
|
||||
@ -644,6 +644,37 @@ public class GroupsV2Impl: GroupsV2 {
|
||||
|
||||
// MARK: - Fetch Group Change Actions
|
||||
|
||||
/// Fetches change actions starting at revision 0.
|
||||
/// Checking the author of the first change can determine in most cases if the local user was the creator of the group.
|
||||
/// If a GroupsV2.localUserNotInGroup error is thrown fetching the first change, the local user either did not create the group, or has since left
|
||||
/// and rejoined.
|
||||
public func fetchRevisionZeroGroupChangeAction(secretParams: GroupSecretParams) async throws -> GroupV2Change {
|
||||
let groupV2Params = try GroupV2Params(groupSecretParams: secretParams)
|
||||
let groupId = try groupV2Params.groupPublicParams.getGroupIdentifier().serialize()
|
||||
let gseExpiration: UInt64
|
||||
|
||||
let databaseStorage = SSKEnvironment.shared.databaseStorageRef
|
||||
gseExpiration = databaseStorage.read { tx in
|
||||
let groupThread = TSGroupThread.fetch(groupId: groupId, transaction: tx)
|
||||
let groupThreadId = groupThread?.sqliteRowId!
|
||||
let endorsementRecord = groupThreadId.flatMap({ try? groupSendEndorsementStore.fetchCombinedEndorsement(groupThreadId: $0, tx: tx) })
|
||||
return endorsementRecord?.expirationTimestamp ?? 0
|
||||
}
|
||||
|
||||
let groupChangesResponse = try await _fetchSomeGroupChangeActions(
|
||||
secretParams: secretParams,
|
||||
startingAtRevision: 0,
|
||||
upThroughRevision: 0,
|
||||
includeFirstState: true,
|
||||
gseExpiration: gseExpiration,
|
||||
)
|
||||
|
||||
guard let first = groupChangesResponse.groupChanges.first else {
|
||||
throw OWSAssertionError("Missing first group change action")
|
||||
}
|
||||
return first
|
||||
}
|
||||
|
||||
/// Fetches some group changes (and a snapshot, if needed).
|
||||
public func fetchSomeGroupChangeActions(
|
||||
secretParams: GroupSecretParams,
|
||||
|
||||
@ -18,6 +18,9 @@ public struct ChangedGroupModel {
|
||||
/// group change.
|
||||
public let newlyLearnedPniToAciAssociations: [Pni: Aci]
|
||||
|
||||
/// Whether we should update the last verified name hash because the local user changed it.
|
||||
public let shouldUpdateLastVerifiedGroupNameHash: Bool
|
||||
|
||||
public init(
|
||||
oldGroupModel: TSGroupModelV2,
|
||||
newGroupModel: TSGroupModelV2,
|
||||
@ -25,6 +28,7 @@ public struct ChangedGroupModel {
|
||||
updateSource: GroupUpdateSource,
|
||||
profileKeys: [Aci: Data],
|
||||
newlyLearnedPniToAciAssociations: [Pni: Aci],
|
||||
shouldUpdateLastVerifiedGroupNameHash: Bool,
|
||||
) {
|
||||
self.oldGroupModel = oldGroupModel
|
||||
self.newGroupModel = newGroupModel
|
||||
@ -32,6 +36,7 @@ public struct ChangedGroupModel {
|
||||
self.updateSource = updateSource
|
||||
self.profileKeys = profileKeys
|
||||
self.newlyLearnedPniToAciAssociations = newlyLearnedPniToAciAssociations
|
||||
self.shouldUpdateLastVerifiedGroupNameHash = shouldUpdateLastVerifiedGroupNameHash
|
||||
}
|
||||
}
|
||||
|
||||
@ -533,6 +538,7 @@ public class GroupsV2IncomingChanges {
|
||||
groupMembershipBuilder.removeBannedMember(aci)
|
||||
}
|
||||
|
||||
var shouldUpdateLastVerifiedGroupNameHash = false
|
||||
if let action = changeActionsProto.modifyTitle {
|
||||
if !canEditAttributes {
|
||||
owsFailDebug("Cannot modify title.")
|
||||
@ -540,6 +546,10 @@ public class GroupsV2IncomingChanges {
|
||||
|
||||
// Change clears or updates the group title.
|
||||
newGroupName = groupV2Params.decryptGroupName(action.title)
|
||||
|
||||
if changeAuthor == localIdentifiers.aci {
|
||||
shouldUpdateLastVerifiedGroupNameHash = true
|
||||
}
|
||||
}
|
||||
|
||||
if let action = changeActionsProto.modifyDescription {
|
||||
@ -687,6 +697,7 @@ public class GroupsV2IncomingChanges {
|
||||
updateSource: updateSource,
|
||||
profileKeys: profileKeys,
|
||||
newlyLearnedPniToAciAssociations: newlyLearnedPniToAciAssociations,
|
||||
shouldUpdateLastVerifiedGroupNameHash: shouldUpdateLastVerifiedGroupNameHash,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -87,7 +87,10 @@ public struct TSGroupModelBuilder {
|
||||
self.isTerminated = groupV2Snapshot.isTerminated
|
||||
}
|
||||
|
||||
static func builderForSnapshot(groupV2Snapshot: GroupV2Snapshot, transaction: DBWriteTransaction) throws -> TSGroupModelBuilder {
|
||||
static func builderForSnapshot(
|
||||
groupV2Snapshot: GroupV2Snapshot,
|
||||
transaction: DBWriteTransaction,
|
||||
) throws -> TSGroupModelBuilder {
|
||||
return try TSGroupModelBuilder(groupV2Snapshot: groupV2Snapshot)
|
||||
}
|
||||
|
||||
|
||||
@ -638,6 +638,9 @@ struct StorageServiceProtos_GroupV2Record: Sendable {
|
||||
/// Clears the value of `avatarColor`. Subsequent reads from it will return its default value.
|
||||
mutating func clearAvatarColor() {self._avatarColor = nil}
|
||||
|
||||
/// SHA-256 of last verified group name
|
||||
var verifiedNameHash: Data = Data()
|
||||
|
||||
var unknownFields = SwiftProtobuf.UnknownStorage()
|
||||
|
||||
enum StorySendMode: SwiftProtobuf.Enum, Swift.CaseIterable {
|
||||
@ -1901,7 +1904,7 @@ extension StorageServiceProtos_GroupV1Record: SwiftProtobuf.Message, SwiftProtob
|
||||
|
||||
extension StorageServiceProtos_GroupV2Record: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
|
||||
static let protoMessageName: String = _protobuf_package + ".GroupV2Record"
|
||||
static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}masterKey\0\u{1}blocked\0\u{1}whitelisted\0\u{1}archived\0\u{1}markedUnread\0\u{1}mutedUntilTimestamp\0\u{1}dontNotifyForMentionsIfMuted\0\u{1}hideStory\0\u{2}\u{2}storySendMode\0\u{1}avatarColor\0")
|
||||
static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}masterKey\0\u{1}blocked\0\u{1}whitelisted\0\u{1}archived\0\u{1}markedUnread\0\u{1}mutedUntilTimestamp\0\u{1}dontNotifyForMentionsIfMuted\0\u{1}hideStory\0\u{2}\u{2}storySendMode\0\u{1}avatarColor\0\u{1}verifiedNameHash\0")
|
||||
|
||||
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
|
||||
while let fieldNumber = try decoder.nextFieldNumber() {
|
||||
@ -1919,6 +1922,7 @@ extension StorageServiceProtos_GroupV2Record: SwiftProtobuf.Message, SwiftProtob
|
||||
case 8: try { try decoder.decodeSingularBoolField(value: &self.hideStory) }()
|
||||
case 10: try { try decoder.decodeSingularEnumField(value: &self.storySendMode) }()
|
||||
case 11: try { try decoder.decodeSingularEnumField(value: &self._avatarColor) }()
|
||||
case 12: try { try decoder.decodeSingularBytesField(value: &self.verifiedNameHash) }()
|
||||
default: break
|
||||
}
|
||||
}
|
||||
@ -1959,6 +1963,9 @@ extension StorageServiceProtos_GroupV2Record: SwiftProtobuf.Message, SwiftProtob
|
||||
try { if let v = self._avatarColor {
|
||||
try visitor.visitSingularEnumField(value: v, fieldNumber: 11)
|
||||
} }()
|
||||
if !self.verifiedNameHash.isEmpty {
|
||||
try visitor.visitSingularBytesField(value: self.verifiedNameHash, fieldNumber: 12)
|
||||
}
|
||||
try unknownFields.traverse(visitor: &visitor)
|
||||
}
|
||||
|
||||
@ -1973,6 +1980,7 @@ extension StorageServiceProtos_GroupV2Record: SwiftProtobuf.Message, SwiftProtob
|
||||
if lhs.hideStory != rhs.hideStory {return false}
|
||||
if lhs.storySendMode != rhs.storySendMode {return false}
|
||||
if lhs._avatarColor != rhs._avatarColor {return false}
|
||||
if lhs.verifiedNameHash != rhs.verifiedNameHash {return false}
|
||||
if lhs.unknownFields != rhs.unknownFields {return false}
|
||||
return true
|
||||
}
|
||||
|
||||
@ -2258,6 +2258,16 @@ public struct StorageServiceProtoGroupV2Record: Codable, CustomDebugStringConver
|
||||
return proto.hasAvatarColor
|
||||
}
|
||||
|
||||
public var verifiedNameHash: Data? {
|
||||
guard hasVerifiedNameHash else {
|
||||
return nil
|
||||
}
|
||||
return proto.verifiedNameHash
|
||||
}
|
||||
public var hasVerifiedNameHash: Bool {
|
||||
return !proto.verifiedNameHash.isEmpty
|
||||
}
|
||||
|
||||
public var hasUnknownFields: Bool {
|
||||
return !proto.unknownFields.data.isEmpty
|
||||
}
|
||||
@ -2322,6 +2332,9 @@ extension StorageServiceProtoGroupV2Record {
|
||||
if let _value = avatarColor {
|
||||
builder.setAvatarColor(_value)
|
||||
}
|
||||
if let _value = verifiedNameHash {
|
||||
builder.setVerifiedNameHash(_value)
|
||||
}
|
||||
if let _value = unknownFields {
|
||||
builder.setUnknownFields(_value)
|
||||
}
|
||||
@ -2386,6 +2399,16 @@ public struct StorageServiceProtoGroupV2RecordBuilder {
|
||||
proto.avatarColor = StorageServiceProtoAvatarColorUnwrap(valueParam)
|
||||
}
|
||||
|
||||
@available(swift, obsoleted: 1.0)
|
||||
public mutating func setVerifiedNameHash(_ valueParam: Data?) {
|
||||
guard let valueParam = valueParam else { return }
|
||||
proto.verifiedNameHash = valueParam
|
||||
}
|
||||
|
||||
public mutating func setVerifiedNameHash(_ valueParam: Data) {
|
||||
proto.verifiedNameHash = valueParam
|
||||
}
|
||||
|
||||
public mutating func setUnknownFields(_ unknownFields: SwiftProtobuf.UnknownStorage) {
|
||||
proto.unknownFields = unknownFields
|
||||
}
|
||||
|
||||
@ -171,6 +171,7 @@ message GroupV2Record {
|
||||
bool hideStory = 8;
|
||||
StorySendMode storySendMode = 10;
|
||||
optional AvatarColor avatarColor = 11;
|
||||
bytes verifiedNameHash = 12; // SHA-256 of last verified group name
|
||||
}
|
||||
|
||||
message AccountRecord {
|
||||
|
||||
@ -324,6 +324,7 @@ public class GRDBSchemaMigrator {
|
||||
case addDevice
|
||||
case addAttachmentBackfillRequestTable
|
||||
case wipeBackupAttachmentUploadQueueForLinkedDevices
|
||||
case addLastVerifiedGroupNameHashColumn
|
||||
|
||||
// NOTE: Every time we add a migration id, consider
|
||||
// incrementing grdbSchemaVersionLatest.
|
||||
@ -5075,6 +5076,13 @@ public class GRDBSchemaMigrator {
|
||||
return .success(())
|
||||
}
|
||||
|
||||
migrator.registerMigration(.addLastVerifiedGroupNameHashColumn) { transaction in
|
||||
try transaction.database.alter(table: "thread_associated_data") { table in
|
||||
table.add(column: "lastVerifiedGroupNameHash", .blob)
|
||||
}
|
||||
return .success(())
|
||||
}
|
||||
|
||||
// MARK: - Schema Migration Insertion Point
|
||||
}
|
||||
|
||||
@ -5261,8 +5269,10 @@ public class GRDBSchemaMigrator {
|
||||
isArchived: thread.isArchivedObsolete,
|
||||
isMarkedUnread: thread.isMarkedUnreadObsolete,
|
||||
mutedUntilTimestamp: thread.mutedUntilTimestampObsolete,
|
||||
// this didn't exist pre-migration, just write the default
|
||||
// audioPlaybackRate and lastVerifiedGroupNameHash didn't exist pre-migration,
|
||||
// just write the default
|
||||
audioPlaybackRate: 1,
|
||||
lastVerifiedGroupNameHash: nil,
|
||||
).insert(transaction.database)
|
||||
} catch {
|
||||
thrownError = error
|
||||
|
||||
@ -988,6 +988,10 @@ class StorageServiceGroupV2RecordUpdater: StorageServiceRecordUpdater {
|
||||
builder.setMarkedUnread(threadAssociatedData.isMarkedUnread)
|
||||
builder.setMutedUntilTimestamp(threadAssociatedData.mutedUntilTimestamp)
|
||||
|
||||
if let lastVerifiedGroupNameHash = threadAssociatedData.lastVerifiedGroupNameHash {
|
||||
builder.setVerifiedNameHash(lastVerifiedGroupNameHash)
|
||||
}
|
||||
|
||||
let groupThread = TSGroupThread.fetch(forGroupId: groupId, tx: transaction)
|
||||
switch groupThread?.mentionNotificationMode {
|
||||
case .none, .default:
|
||||
@ -1129,6 +1133,10 @@ class StorageServiceGroupV2RecordUpdater: StorageServiceRecordUpdater {
|
||||
needsUpdate = true
|
||||
}
|
||||
|
||||
if let verifiedHash = record.verifiedNameHash, verifiedHash != localThreadAssociatedData.lastVerifiedGroupNameHash {
|
||||
localThreadAssociatedData.updateWith(lastVerifiedGroupNameHash: verifiedHash, updateStorageService: false, transaction: transaction)
|
||||
}
|
||||
|
||||
return .merged(needsUpdate: needsUpdate, masterKey)
|
||||
}
|
||||
|
||||
|
||||
@ -70,6 +70,7 @@ class PhoneNumberChangedMessageInserterTest: XCTestCase {
|
||||
isMarkedUnread: false,
|
||||
mutedUntilTimestamp: 0,
|
||||
audioPlaybackRate: 1,
|
||||
lastVerifiedGroupNameHash: nil,
|
||||
)
|
||||
|
||||
let interactionStore = MockInteractionStore()
|
||||
|
||||
@ -203,6 +203,7 @@ final class ThreadMergerTest: XCTestCase {
|
||||
isMarkedUnread: isMarkedUnread,
|
||||
mutedUntilTimestamp: mutedUntilTimestamp,
|
||||
audioPlaybackRate: audioPlaybackRate,
|
||||
lastVerifiedGroupNameHash: nil,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -58,6 +58,7 @@ class ThreadFinderTests: XCTestCase {
|
||||
isMarkedUnread: isMarkedUnread,
|
||||
mutedUntilTimestamp: 0,
|
||||
audioPlaybackRate: 1,
|
||||
lastVerifiedGroupNameHash: nil,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user