From ff69eafc0efb842868c7a3c07f5126bd2e622548 Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Mon, 25 Nov 2024 13:25:29 -0800 Subject: [PATCH] Consent Streaming (#435) * bump the pod * update to latest libxmtp * clean up the consent code and add streaming * add a test for it * get the tests passing * fix up the flakiness in the test --- Package.swift | 4 +- Sources/XMTPiOS/Client.swift | 4 - Sources/XMTPiOS/Extensions/Ffi.swift | 16 ++++ Sources/XMTPiOS/PrivatePreferences.swift | 93 ++++++++++++++++-------- Tests/XMTPTests/ConversationTests.swift | 64 +++++++++++++++- Tests/XMTPTests/DmTests.swift | 10 +-- Tests/XMTPTests/GroupTests.swift | 24 +++--- XMTP.podspec | 2 +- 8 files changed, 159 insertions(+), 58 deletions(-) diff --git a/Package.swift b/Package.swift index 6216537f..640e61de 100644 --- a/Package.swift +++ b/Package.swift @@ -20,8 +20,8 @@ let package = Package( .package(url: "https://github.com/tesseract-one/CSecp256k1.swift.git", from: "0.2.0"), .package(url: "https://github.com/bufbuild/connect-swift", exact: "1.0.0"), .package(url: "https://github.com/apple/swift-docc-plugin.git", from: "1.4.3"), - .package(url: "https://github.com/xmtp/libxmtp-swift.git", exact: "3.0.7"), - .package(url: "https://github.com/krzyzanowskim/CryptoSwift.git", exact: "1.8.3") + .package(url: "https://github.com/krzyzanowskim/CryptoSwift.git", exact: "1.8.3"), + .package(url: "https://github.com/xmtp/libxmtp-swift.git", exact: "3.0.7") ], targets: [ .target( diff --git a/Sources/XMTPiOS/Client.swift b/Sources/XMTPiOS/Client.swift index 97f9c46e..738b768f 100644 --- a/Sources/XMTPiOS/Client.swift +++ b/Sources/XMTPiOS/Client.swift @@ -474,10 +474,6 @@ public final class Client { try await ffiClient.sendSyncRequest(kind: .messages) } - public func syncConsent() async throws { - try await ffiClient.sendSyncRequest(kind: .consent) - } - public func inboxState(refreshFromNetwork: Bool) async throws -> InboxState { return InboxState( diff --git a/Sources/XMTPiOS/Extensions/Ffi.swift b/Sources/XMTPiOS/Extensions/Ffi.swift index 743a1a4c..7158547f 100644 --- a/Sources/XMTPiOS/Extensions/Ffi.swift +++ b/Sources/XMTPiOS/Extensions/Ffi.swift @@ -45,6 +45,16 @@ extension FfiConsentState { } } +extension FfiConsentEntityType { + var fromFFI: EntryType { + switch self { + case .inboxId: return EntryType.inbox_id + case .address: return EntryType.address + case .conversationId: return EntryType.conversation_id + } + } +} + extension EntryType { var toFFI: FfiConsentEntityType { switch self { @@ -62,3 +72,9 @@ extension ConsentListEntry { ) } } + +extension FfiConsent { + var fromFfi: ConsentListEntry { + ConsentListEntry(value: self.entity, entryType: self.entityType.fromFFI, consentType: self.state.fromFFI) + } +} diff --git a/Sources/XMTPiOS/PrivatePreferences.swift b/Sources/XMTPiOS/PrivatePreferences.swift index 703c54e5..2362d7fd 100644 --- a/Sources/XMTPiOS/PrivatePreferences.swift +++ b/Sources/XMTPiOS/PrivatePreferences.swift @@ -9,12 +9,13 @@ public enum EntryType: String, Codable { } public struct ConsentListEntry: Codable, Hashable { - public init(value: String, entryType: EntryType, consentType: ConsentState) { + public init(value: String, entryType: EntryType, consentType: ConsentState) + { self.value = value self.entryType = entryType self.consentType = consentType } - + static func address(_ address: String, type: ConsentState = .unknown) -> ConsentListEntry { @@ -25,7 +26,8 @@ public struct ConsentListEntry: Codable, Hashable { conversationId: String, type: ConsentState = ConsentState.unknown ) -> ConsentListEntry { ConsentListEntry( - value: conversationId, entryType: .conversation_id, consentType: type) + value: conversationId, entryType: .conversation_id, + consentType: type) } static func inboxId(_ inboxId: String, type: ConsentState = .unknown) @@ -44,25 +46,8 @@ public struct ConsentListEntry: Codable, Hashable { } } -public enum ContactError: Error { - case invalidIdentifier -} - -public actor EntriesManager { - public var map: [String: ConsentListEntry] = [:] - - func set(_ key: String, _ object: ConsentListEntry) { - map[key] = object - } - - func get(_ key: String) -> ConsentListEntry? { - map[key] - } -} - -public class ConsentList { - public let entriesManager = EntriesManager() - var lastFetched: Date? +/// Provides access to contact bundles. +public actor PrivatePreferences { var client: Client var ffiClient: FfiXmtpClient @@ -82,7 +67,9 @@ public class ConsentList { ).fromFFI } - public func conversationState(conversationId: String) async throws -> ConsentState { + public func conversationState(conversationId: String) async throws + -> ConsentState + { return try await ffiClient.getConsentState( entityType: .conversationId, entity: conversationId @@ -95,17 +82,61 @@ public class ConsentList { entity: inboxId ).fromFFI } + + public func syncConsent() async throws { + try await ffiClient.sendSyncRequest(kind: .consent) + } + + public func streamConsent() + -> AsyncThrowingStream + { + AsyncThrowingStream { continuation in + let ffiStreamActor = FfiStreamActor() + + let consentCallback = ConsentCallback(client: self.client) { + records in + guard !Task.isCancelled else { + continuation.finish() + Task { + await ffiStreamActor.endStream() + } + return + } + for consent in records { + continuation.yield(consent.fromFfi) + } + } + + let task = Task { + let stream = await ffiClient.conversations().streamConsent( + callback: consentCallback) + await ffiStreamActor.setFfiStream(stream) + } + + continuation.onTermination = { _ in + task.cancel() + Task { + await ffiStreamActor.endStream() + } + } + } + } } -/// Provides access to contact bundles. -public actor PrivatePreferences { - var client: Client - var ffiClient: FfiXmtpClient - public var consentList: ConsentList +final class ConsentCallback: FfiConsentCallback { + let client: Client + let callback: ([FfiConsent]) -> Void - init(client: Client, ffiClient: FfiXmtpClient) { + init(client: Client, _ callback: @escaping ([FfiConsent]) -> Void) { self.client = client - self.ffiClient = ffiClient - consentList = ConsentList(client: client, ffiClient: ffiClient) + self.callback = callback + } + + func onConsentUpdate(consent: [FfiConsent]) { + callback(consent) + } + + func onError(error: FfiSubscribeError) { + print("Error ConsentCallback \(error)") } } diff --git a/Tests/XMTPTests/ConversationTests.swift b/Tests/XMTPTests/ConversationTests.swift index 7eefe69e..10eae2be 100644 --- a/Tests/XMTPTests/ConversationTests.swift +++ b/Tests/XMTPTests/ConversationTests.swift @@ -200,7 +200,7 @@ class ConversationTests: XCTestCase { try await boDm?.sync() try await alixClient.conversations.sync() try await alixClient2.conversations.sync() - try await alixClient2.syncConsent() + try await alixClient2.preferences.syncConsent() try await alixClient.conversations.syncAllConversations() sleep(2) try await alixClient2.conversations.syncAllConversations() @@ -209,7 +209,7 @@ class ConversationTests: XCTestCase { if let dm2 = try alixClient2.findConversation(conversationId: dm.id) { XCTAssertEqual(try dm2.consentState(), .denied) - try await alixClient2.preferences.consentList.setConsentState( + try await alixClient2.preferences.setConsentState( entries: [ ConsentListEntry( value: dm2.id, @@ -218,12 +218,70 @@ class ConversationTests: XCTestCase { ) ] ) - let convoState = try await alixClient2.preferences.consentList + let convoState = try await alixClient2.preferences .conversationState( conversationId: dm2.id) XCTAssertEqual(convoState, .allowed) XCTAssertEqual(try dm2.consentState(), .allowed) } } + + func testStreamConsent() async throws { + let fixtures = try await fixtures() + + let key = try Crypto.secureRandomBytes(count: 32) + let alix = try PrivateKey.generate() + + let alixClient = try await Client.create( + account: alix, + options: .init( + api: .init(env: .local, isSecure: false), + dbEncryptionKey: key, + dbDirectory: "xmtp_db" + ) + ) + + let alixGroup = try await alixClient.conversations.newGroup(with: [fixtures.bo.walletAddress]) + + let alixClient2 = try await Client.create( + account: alix, + options: .init( + api: .init(env: .local, isSecure: false), + dbEncryptionKey: key, + dbDirectory: "xmtp_db2" + ) + ) + + try await alixGroup.send(content: "Hello") + try await alixClient.conversations.sync() + try await alixClient2.conversations.sync() + try await alixClient.conversations.syncAllConversations() + try await alixClient2.conversations.syncAllConversations() + let alixGroup2 = try alixClient2.findGroup(groupId: alixGroup.id)! + + var consentList = [ConsentListEntry]() + let expectation = XCTestExpectation(description: "Stream Consent") + expectation.expectedFulfillmentCount = 3 + + Task(priority: .userInitiated) { + for try await entry in await alixClient.preferences.streamConsent() { + consentList.append(entry) + expectation.fulfill() + } + } + sleep(1) + try await alixGroup2.updateConsentState(state: .denied) + let dm = try await alixClient2.conversations.newConversation(with: fixtures.caro.walletAddress) + try await dm.updateConsentState(state: .denied) + + sleep(5) + try await alixClient.conversations.syncAllConversations() + try await alixClient2.conversations.syncAllConversations() + + await fulfillment(of: [expectation], timeout: 3) + print(consentList) + XCTAssertEqual(try alixGroup.consentState(), .denied) + } + } diff --git a/Tests/XMTPTests/DmTests.swift b/Tests/XMTPTests/DmTests.swift index a33c9e66..f391b87b 100644 --- a/Tests/XMTPTests/DmTests.swift +++ b/Tests/XMTPTests/DmTests.swift @@ -82,7 +82,7 @@ class DmTests: XCTestCase { _ = try await dm.send(content: "gm") try await dm.sync() - let dmState = try await fixtures.boClient.preferences.consentList + let dmState = try await fixtures.boClient.preferences .conversationState(conversationId: dm.id) XCTAssertEqual(dmState, .allowed) XCTAssertEqual(try dm.consentState(), .allowed) @@ -248,24 +248,24 @@ class DmTests: XCTestCase { let dm = try await fixtures.boClient.conversations.findOrCreateDm( with: fixtures.alix.walletAddress) - let isDm = try await fixtures.boClient.preferences.consentList + let isDm = try await fixtures.boClient.preferences .conversationState(conversationId: dm.id) XCTAssertEqual(isDm, .allowed) XCTAssertEqual(try dm.consentState(), .allowed) - try await fixtures.boClient.preferences.consentList.setConsentState( + try await fixtures.boClient.preferences.setConsentState( entries: [ ConsentListEntry( value: dm.id, entryType: .conversation_id, consentType: .denied) ]) - let isDenied = try await fixtures.boClient.preferences.consentList + let isDenied = try await fixtures.boClient.preferences .conversationState(conversationId: dm.id) XCTAssertEqual(isDenied, .denied) XCTAssertEqual(try dm.consentState(), .denied) try await dm.updateConsentState(state: .allowed) - let isAllowed = try await fixtures.boClient.preferences.consentList + let isAllowed = try await fixtures.boClient.preferences .conversationState(conversationId: dm.id) XCTAssertEqual(isAllowed, .allowed) XCTAssertEqual(try dm.consentState(), .allowed) diff --git a/Tests/XMTPTests/GroupTests.swift b/Tests/XMTPTests/GroupTests.swift index 83ef0ea6..119a14f3 100644 --- a/Tests/XMTPTests/GroupTests.swift +++ b/Tests/XMTPTests/GroupTests.swift @@ -99,7 +99,7 @@ class GroupTests: XCTestCase { XCTAssertEqual(boConsentResult, ConsentState.allowed) let alixConsentResult = try await fixtures.alixClient.preferences - .consentList.conversationState(conversationId: alixGroup.id) + .conversationState(conversationId: alixGroup.id) XCTAssertEqual(alixConsentResult, ConsentState.unknown) try await boGroup.addMembers(addresses: [fixtures.caro.address]) @@ -793,7 +793,7 @@ class GroupTests: XCTestCase { XCTAssertEqual(try group.consentState(), .allowed) try await group.updateConsentState(state: .denied) - let isDenied = try await fixtures.boClient.preferences.consentList + let isDenied = try await fixtures.boClient.preferences .conversationState(conversationId: group.id) XCTAssertEqual(isDenied, .denied) XCTAssertEqual(try group.consentState(), .denied) @@ -806,12 +806,12 @@ class GroupTests: XCTestCase { let fixtures = try await fixtures() let boGroup = try await fixtures.boClient.conversations.newGroup( with: [fixtures.alix.address]) - let inboxState = try await fixtures.boClient.preferences.consentList + let inboxState = try await fixtures.boClient.preferences .inboxIdState( inboxId: fixtures.alixClient.inboxID) XCTAssertEqual(inboxState, .unknown) - try await fixtures.boClient.preferences.consentList.setConsentState( + try await fixtures.boClient.preferences.setConsentState( entries: [ ConsentListEntry( value: fixtures.alixClient.inboxID, entryType: .inbox_id, @@ -822,12 +822,12 @@ class GroupTests: XCTestCase { }) XCTAssertEqual(alixMember?.consentState, .allowed) - let inboxState2 = try await fixtures.boClient.preferences.consentList + let inboxState2 = try await fixtures.boClient.preferences .inboxIdState( inboxId: fixtures.alixClient.inboxID) XCTAssertEqual(inboxState2, .allowed) - try await fixtures.boClient.preferences.consentList.setConsentState( + try await fixtures.boClient.preferences.setConsentState( entries: [ ConsentListEntry( value: fixtures.alixClient.inboxID, entryType: .inbox_id, @@ -838,22 +838,22 @@ class GroupTests: XCTestCase { }) XCTAssertEqual(alixMember?.consentState, .denied) - let inboxState3 = try await fixtures.boClient.preferences.consentList + let inboxState3 = try await fixtures.boClient.preferences .inboxIdState( inboxId: fixtures.alixClient.inboxID) XCTAssertEqual(inboxState3, .denied) - try await fixtures.boClient.preferences.consentList.setConsentState( + try await fixtures.boClient.preferences.setConsentState( entries: [ ConsentListEntry( value: fixtures.alixClient.address, entryType: .address, consentType: .allowed) ]) - let inboxState4 = try await fixtures.boClient.preferences.consentList + let inboxState4 = try await fixtures.boClient.preferences .inboxIdState( inboxId: fixtures.alixClient.inboxID) XCTAssertEqual(inboxState4, .allowed) - let addressState = try await fixtures.boClient.preferences.consentList + let addressState = try await fixtures.boClient.preferences .addressState(address: fixtures.alixClient.address) XCTAssertEqual(addressState, .allowed) } @@ -892,12 +892,12 @@ class GroupTests: XCTestCase { try await fixtures.alixClient.conversations.sync() let alixGroup = try fixtures.alixClient.findGroup(groupId: boGroup.id)! let isGroupAllowed = try await fixtures.alixClient.preferences - .consentList.conversationState(conversationId: boGroup.id) + .conversationState(conversationId: boGroup.id) XCTAssertEqual(isGroupAllowed, .unknown) let preparedMessageId = try await alixGroup.prepareMessage( content: "Test text") let isGroupAllowed2 = try await fixtures.alixClient.preferences - .consentList.conversationState(conversationId: boGroup.id) + .conversationState(conversationId: boGroup.id) XCTAssertEqual(isGroupAllowed2, .allowed) let messageCount = try await alixGroup.messages().count XCTAssertEqual(messageCount, 1) diff --git a/XMTP.podspec b/XMTP.podspec index 48efd7d1..fb6e1ea0 100644 --- a/XMTP.podspec +++ b/XMTP.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |spec| spec.name = "XMTP" - spec.version = "3.0.8" + spec.version = "3.0.9" spec.summary = "XMTP SDK Cocoapod" spec.description = <<-DESC