Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,11 @@ public class InitiateResetMLSConversationUseCase: InitiateResetMLSConversationUs
return
}

attributes = [.conversationId: qualifiedID.safeForLoggingDescription]
attributes = [
.public: true,
.conversationId: qualifiedID.safeForLoggingDescription,
.mlsGroupID: groupID.safeForLoggingDescription
]

lockRepository.setInitiatedReset(conversationID: qualifiedID)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,15 @@
// sourcery: AutoMockable
/// Repairs conversations with faulty removal keys
public protocol RepairRemovalKeysUseCaseProtocol {
func invoke() async throws
@discardableResult
func invoke() async throws -> RepairRemovalKeysResult
}

public struct RepairRemovalKeysResult {

public var faultyConversationsFound = 0
public var conversationsRepaired = 0

}

public struct RepairRemovalKeysUseCase: RepairRemovalKeysUseCaseProtocol {
Expand Down Expand Up @@ -52,7 +60,8 @@
self.initiateResetUseCase = initiateResetUseCase
}

public func invoke() async throws {
@discardableResult
public func invoke() async throws -> RepairRemovalKeysResult {
WireLogger.mls.info(
"initiating repair of faulty removal keys",
attributes: .safePublic
Expand All @@ -63,24 +72,39 @@
"no faulty removal keys to repair, aborting",
attributes: .safePublic
)
return
return RepairRemovalKeysResult()
}

var resultsByDomain: [String: RepairRemovalKeysResult] = [:]

// Process each domain
for (domain, faultyKeyHexStrings) in faultyMLSRemovalKeysByDomain {
try await processDomain(
let domainResult = try await processDomain(
domain: domain,
faultyKeyHexStrings: faultyKeyHexStrings
)
resultsByDomain[domain] = domainResult
}

let totalFaultyConversationsFound = resultsByDomain.values.reduce(0) {
$0 + $1.faultyConversationsFound
}
let totalConversationsRepaired = resultsByDomain.values.reduce(0) {
$0 + $1.conversationsRepaired
}

return RepairRemovalKeysResult(
faultyConversationsFound: totalFaultyConversationsFound,
conversationsRepaired: totalConversationsRepaired
)
}

// MARK: - Private

private func processDomain(
domain: String,
faultyKeyHexStrings: [String]
) async throws {
) async throws -> RepairRemovalKeysResult {
WireLogger.mls.info(
"checking domain for \(faultyKeyHexStrings.count) faulty key(s)",
attributes: .safePublic
Expand All @@ -93,7 +117,7 @@
"failed to decode some faulty removal key hex strings",
attributes: .safePublic
)
return
return RepairRemovalKeysResult()
}

let allMLSConversations = try await conversationLocalStore.fetchAllMLSConversations(
Expand All @@ -106,22 +130,29 @@
faultyKeys: faultyKeyDataList
)

let faultyConversationsFound = faultyConversations.count

WireLogger.mls.info(
"detected \(faultyConversations.count)/\(allMLSConversations.count) affected conversations",
"detected \(faultyConversationsFound)/\(allMLSConversations.count) affected conversations",
attributes: .safePublic
)

// Repair each faulty conversation in parallel
await withTaskGroup(of: Void.self) { group in
for (groupID, qualifiedID) in faultyConversations {
group.addTask {
await repairConversation(
groupID: groupID,
qualifiedID: qualifiedID
)
}
// Repair each faulty conversation serially
var conversationsRepaired = 0
for (groupID, qualifiedID) in faultyConversations {
let success = await repairConversation(
groupID: groupID,
qualifiedID: qualifiedID
)
if success {
conversationsRepaired += 1
}
}

return RepairRemovalKeysResult(
faultyConversationsFound: faultyConversationsFound,
conversationsRepaired: conversationsRepaired
)
}

private func findFaultyConversations(
Expand All @@ -131,7 +162,7 @@
var faultyConversations: [(MLSGroupID, WireDataModel.QualifiedID)] = []

for conversation in conversations {
let (groupID, qualifiedID) = await context.perform {

Check warning on line 165 in WireDomain/Sources/WireDomain/UseCases/RepairRemovalKeysUseCase.swift

View workflow job for this annotation

GitHub Actions / Test Results

Capture of 'conversation' with non-Sendable type 'ZMConversation' in a '@Sendable' closure

Capture of 'conversation' with non-Sendable type 'ZMConversation' in a '@sendable' closure
(conversation.mlsGroupID, conversation.qualifiedID)
}

Expand Down Expand Up @@ -165,7 +196,13 @@
private func repairConversation(
groupID: MLSGroupID,
qualifiedID: WireDataModel.QualifiedID
) async {
) async -> Bool {
let logAttributes: LogAttributes = [
.public: true,
.conversationId: qualifiedID.safeForLoggingDescription,
.mlsGroupID: groupID.safeForLoggingDescription
]

let remoteConversation: WireNetwork.Conversation?
do {
remoteConversation = try await conversationsAPI.getConversations(
Expand All @@ -174,26 +211,27 @@
} catch {
WireLogger.mls.error(
"failed to get epoch for a group, skipping: \(String(describing: error))",
attributes: .safePublic, [.conversationId: qualifiedID.safeForLoggingDescription]
attributes: logAttributes
)
return
return false
}

guard let remoteConversation else {
WireLogger.mls.error(
"remote conversation for a group not found, skipping",
attributes: .safePublic, [.conversationId: qualifiedID.safeForLoggingDescription]
attributes: logAttributes
)
return
return false
}

WireLogger.mls.info(
"initiating reset for faulty conversation: \(qualifiedID)",
attributes: .safePublic, [.conversationId: qualifiedID.safeForLoggingDescription]
attributes: logAttributes
)

let epoch = UInt64(remoteConversation.epoch ?? 0)
await initiateResetUseCase.invoke(groupID: groupID, epoch: epoch)
return true
}

}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ final class DeveloperDebugActionsViewModel: ObservableObject {
.init(title: "Invalidate all conversations", action: invalidateAllConversations),
.init(title: "Set last app version migration", action: requestAppVersionInput),
.init(title: "Initiate reset of first from top MLS", action: initiateResetBrokenMLSConversation),
.init(title: "Repair faulty removal key", action: initiateRepairRemovalKeys),
.init(title: "Initiate reset of affected MLS groups", action: initiateRepairRemovalKeys),
.init(title: "Logout", action: logout)

]
Expand Down Expand Up @@ -211,7 +211,11 @@ final class DeveloperDebugActionsViewModel: ObservableObject {
attributes: .safePublic
)
do {
try await useCase.invoke()
let result = try await useCase.invoke()
WireLogger.mls.info(
"manual trigger to initiate repair removal keys compete. Repaired initiated for \(result.conversationsRepaired)/\(result.faultyConversationsFound) affected conversations.",
attributes: .safePublic
)
} catch {
WireLogger.mls.error(
"manual trigger to repair removal keys failed: \(String(describing: error))",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,9 @@ enum DebugActions {

case .resyncResources:
DebugActions.triggerResyncResources()

case .repairFaultyMLSRemovalKeys:
DebugActions.repairFaultyMLSRemovalKeys()
}
}
}
Expand Down Expand Up @@ -274,6 +277,35 @@ enum DebugActions {
}
}

static func repairFaultyMLSRemovalKeys() {
guard let userSession = ZMUserSession.shared() else {
alert("Error: No user session available")
return
}

guard let useCase = userSession.clientSessionComponent?.repairFaultyRemovalKeysUsecase else {
alert("Error: Repair use case not available")
return
}

Task {
do {
let result = try await useCase.invoke()
await MainActor.run {
let message = """
Found: \(result.faultyConversationsFound) faulty conversation(s)
Repaired: \(result.conversationsRepaired) conversation(s)
"""
alert(message, title: "Faulty MLS Removal Keys Repair")
}
} catch {
await MainActor.run {
alert("Error: \(error.localizedDescription)", title: "Repair Failed")
}
}
}
}

static func appendMessagesToDatabase(count: Int) {
guard let userSession = ZMUserSession.shared() else { return }
let conversation = ConversationList.conversations(inUserSession: userSession).items.first!
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ enum DebugCommand {

case resyncResources

/// Repair conversations with faulty MLS removal keys based
/// on the configuration in SessionManagerConfiguration.

case repairFaultyMLSRemovalKeys

init?(string: String) {
// We may want to have commands that accept arguments, which means
// we'd have to do the parsing of the command here.
Expand All @@ -40,6 +45,9 @@ enum DebugCommand {
case "resync resources":
self = .resyncResources

case "initiateResetMLSGroups":
self = .repairFaultyMLSRemovalKeys

default:
return nil
}
Expand Down
Loading