diff --git a/Sources/XMTP/Conversation.swift b/Sources/XMTP/Conversation.swift index a408169f..16e1956f 100644 --- a/Sources/XMTP/Conversation.swift +++ b/Sources/XMTP/Conversation.swift @@ -118,6 +118,15 @@ public enum Conversation: Sendable { } } + public func decrypt(_ envelope: Envelope) throws -> DecryptedMessage { + switch self { + case let .v1(conversationV1): + return try conversationV1.decrypt(envelope: envelope) + case let .v2(conversationV2): + return try conversationV2.decrypt(envelope: envelope) + } + } + public func encode(codec: Codec, content: T) async throws -> Data where Codec.T == T { switch self { case let .v1: @@ -211,6 +220,15 @@ public enum Conversation: Sendable { } } + public func streamDecryptedMessages() -> AsyncThrowingStream { + switch self { + case let .v1(conversation): + return conversation.streamDecryptedMessages() + case let .v2(conversation): + return conversation.streamDecryptedMessages() + } + } + /// List messages in the conversation public func messages(limit: Int? = nil, before: Date? = nil, after: Date? = nil, direction: PagingInfoSortDirection? = .descending) async throws -> [DecodedMessage] { switch self { diff --git a/Sources/XMTP/ConversationV1.swift b/Sources/XMTP/ConversationV1.swift index d01510c3..282a11e6 100644 --- a/Sources/XMTP/ConversationV1.swift +++ b/Sources/XMTP/ConversationV1.swift @@ -162,6 +162,17 @@ public struct ConversationV1 { } } + public func streamDecryptedMessages() -> AsyncThrowingStream { + AsyncThrowingStream { continuation in + Task { + for try await envelope in client.subscribe(topics: [topic.description]) { + let decoded = try decrypt(envelope: envelope) + continuation.yield(decoded) + } + } + } + } + var ephemeralTopic: String { topic.description.replacingOccurrences(of: "/xmtp/0/dm-", with: "/xmtp/0/dmE-") } diff --git a/Sources/XMTP/ConversationV2.swift b/Sources/XMTP/ConversationV2.swift index c637f61c..05847157 100644 --- a/Sources/XMTP/ConversationV2.swift +++ b/Sources/XMTP/ConversationV2.swift @@ -137,11 +137,15 @@ public struct ConversationV2 { let envelopes = try await client.apiClient.envelopes(topic: topic.description, pagination: pagination) return try envelopes.map { envelope in - let message = try Message(serializedData: envelope.message) - return try MessageV2.decrypt(generateID(from: envelope), topic, message.v2, keyMaterial: keyMaterial, client: client) + try decrypt(envelope: envelope) } } + func decrypt(envelope: Envelope) throws -> DecryptedMessage { + let message = try Message(serializedData: envelope.message) + return try MessageV2.decrypt(generateID(from: envelope), topic, message.v2, keyMaterial: keyMaterial, client: client) + } + var ephemeralTopic: String { topic.replacingOccurrences(of: "/xmtp/0/m", with: "/xmtp/0/mE") } @@ -172,6 +176,18 @@ public struct ConversationV2 { } } + public func streamDecryptedMessages() -> AsyncThrowingStream { + AsyncThrowingStream { continuation in + Task { + for try await envelope in client.subscribe(topics: [topic.description]) { + let decoded = try decrypt(envelope: envelope) + + continuation.yield(decoded) + } + } + } + } + public var createdAt: Date { Date(timeIntervalSince1970: Double(header.createdNs / 1_000_000) / 1000) } diff --git a/Sources/XMTP/Conversations.swift b/Sources/XMTP/Conversations.swift index 9b9bd3ea..0694ee3a 100644 --- a/Sources/XMTP/Conversations.swift +++ b/Sources/XMTP/Conversations.swift @@ -1,321 +1,392 @@ import Foundation public enum ConversationError: Error { - case recipientNotOnNetwork, recipientIsSender, v1NotSupported(String) + case recipientNotOnNetwork, recipientIsSender, v1NotSupported(String) } /// Handles listing and creating Conversations. public actor Conversations { - var client: Client - var conversationsByTopic: [String: Conversation] = [:] - - init(client: Client) { - self.client = client - } - - /// Import a previously seen conversation. - /// See Conversation.toTopicData() - public func importTopicData(data: Xmtp_KeystoreApi_V1_TopicMap.TopicData) -> Conversation { - let conversation: Conversation; - if (!data.hasInvitation) { - let sentAt = Date(timeIntervalSince1970: TimeInterval(data.createdNs / 1_000_000_000)) - conversation = .v1(ConversationV1(client: client, peerAddress: data.peerAddress, sentAt: sentAt)) - } else { - conversation = .v2(ConversationV2( - topic: data.invitation.topic, - keyMaterial: data.invitation.aes256GcmHkdfSha256.keyMaterial, - context: data.invitation.context, - peerAddress: data.peerAddress, - client: client - )) - } - conversationsByTopic[conversation.topic] = conversation - return conversation - } - - public func listBatchMessages(topics: [String: Pagination?]) async throws -> [DecodedMessage] { - let requests = topics.map { (topic, page) in - makeQueryRequest(topic: topic, pagination: page) - } - /// The maximum number of requests permitted in a single batch call. - let maxQueryRequestsPerBatch = 50 - let batches = requests.chunks(maxQueryRequestsPerBatch) - .map { (requests) in BatchQueryRequest.with { $0.requests = requests } } - var messages: [DecodedMessage] = [] - // TODO: consider using a task group here for parallel batch calls - for batch in batches { - messages += try await client.apiClient.batchQuery(request: batch) - .responses.flatMap { (res) in - res.envelopes.compactMap { (envelope) in - let conversation = conversationsByTopic[envelope.contentTopic] - if conversation == nil { - print("discarding message, unknown conversation \(envelope)") - return nil - } - do { - return try conversation?.decode(envelope) - } catch { - print("discarding message, unable to decode \(envelope)") - return nil - } - } - } - } - return messages - } - - public func streamAllMessages() async throws -> AsyncThrowingStream { - return AsyncThrowingStream { continuation in - Task { - while true { - var topics: [String] = [ - Topic.userInvite(client.address).description, - Topic.userIntro(client.address).description, - ] - - for conversation in try await list() { - topics.append(conversation.topic) - } - - do { - for try await envelope in client.subscribe(topics: topics) { - if let conversation = conversationsByTopic[envelope.contentTopic] { - let decoded = try conversation.decode(envelope) - continuation.yield(decoded) - } else if envelope.contentTopic.hasPrefix("/xmtp/0/invite-") { - let conversation = try fromInvite(envelope: envelope) - conversationsByTopic[conversation.topic] = conversation - break // Break so we can resubscribe with the new conversation - } else if envelope.contentTopic.hasPrefix("/xmtp/0/intro-") { - let conversation = try fromIntro(envelope: envelope) - conversationsByTopic[conversation.topic] = conversation - let decoded = try conversation.decode(envelope) - continuation.yield(decoded) - break // Break so we can resubscribe with the new conversation - } else { - print("huh \(envelope)") - } - } - } catch { - continuation.finish(throwing: error) - } - } - } - } - } - - public func fromInvite(envelope: Envelope) throws -> Conversation { - let sealedInvitation = try SealedInvitation(serializedData: envelope.message) - let unsealed = try sealedInvitation.v1.getInvitation(viewer: client.keys) - - return .v2(try ConversationV2.create(client: client, invitation: unsealed, header: sealedInvitation.v1.header)) - } - - public func fromIntro(envelope: Envelope) throws -> Conversation { - let messageV1 = try MessageV1.fromBytes(envelope.message) - let senderAddress = try messageV1.header.sender.walletAddress - let recipientAddress = try messageV1.header.recipient.walletAddress - - let peerAddress = client.address == senderAddress ? recipientAddress : senderAddress - let conversationV1 = ConversationV1(client: client, peerAddress: peerAddress, sentAt: messageV1.sentAt) - - return .v1(conversationV1) - } - - private func findExistingConversation(with peerAddress: String, conversationID: String?) -> Conversation? { - return conversationsByTopic.first(where: { $0.value.peerAddress == peerAddress && - (($0.value.conversationID ?? "") == (conversationID ?? "")) - })?.value - } - - public func newConversation(with peerAddress: String, context: InvitationV1.Context? = nil) async throws -> Conversation { - if peerAddress.lowercased() == client.address.lowercased() { - throw ConversationError.recipientIsSender - } - print("\(client.address) starting conversation with \(peerAddress)") - if let existing = findExistingConversation(with: peerAddress, conversationID: context?.conversationID) { - return existing - } - - guard let contact = try await client.contacts.find(peerAddress) else { - throw ConversationError.recipientNotOnNetwork - } - - _ = try await list() // cache old conversations and check again - if let existing = findExistingConversation(with: peerAddress, conversationID: context?.conversationID) { - return existing - } - - // We don't have an existing conversation, make a v2 one - let recipient = try contact.toSignedPublicKeyBundle() - let invitation = try InvitationV1.createDeterministic( - sender: client.keys, - recipient: recipient, - context: context) - let sealedInvitation = try await sendInvitation(recipient: recipient, invitation: invitation, created: Date()) - let conversationV2 = try ConversationV2.create(client: client, invitation: invitation, header: sealedInvitation.v1.header) - - try await client.contacts.allow(addresses: [peerAddress]) - - let conversation: Conversation = .v2(conversationV2) - conversationsByTopic[conversation.topic] = conversation - return conversation - } - - public func stream() -> AsyncThrowingStream { - AsyncThrowingStream { continuation in - Task { - var streamedConversationTopics: Set = [] - - for try await envelope in client.subscribe(topics: [.userIntro(client.address), .userInvite(client.address)]) { - if envelope.contentTopic == Topic.userIntro(client.address).description { - let conversationV1 = try fromIntro(envelope: envelope) - - if streamedConversationTopics.contains(conversationV1.topic.description) { - continue - } - - streamedConversationTopics.insert(conversationV1.topic.description) - continuation.yield(conversationV1) - } - - if envelope.contentTopic == Topic.userInvite(client.address).description { - let conversationV2 = try fromInvite(envelope: envelope) - - if streamedConversationTopics.contains(conversationV2.topic) { - continue - } - - streamedConversationTopics.insert(conversationV2.topic) - continuation.yield(conversationV2) - } - } - } - } - } - - private func makeConversation(from sealedInvitation: SealedInvitation) throws -> ConversationV2 { - let unsealed = try sealedInvitation.v1.getInvitation(viewer: client.keys) - let conversation = try ConversationV2.create(client: client, invitation: unsealed, header: sealedInvitation.v1.header) - - return conversation - } - - - public func list() async throws -> [Conversation] { - var newConversations: [Conversation] = [] - let mostRecent = conversationsByTopic.values.max { a, b in - a.createdAt < b.createdAt - } - let pagination = Pagination(after: mostRecent?.createdAt) - do { - let seenPeers = try await listIntroductionPeers(pagination: pagination) - for (peerAddress, sentAt) in seenPeers { - newConversations.append( - Conversation.v1( - ConversationV1( - client: client, - peerAddress: peerAddress, - sentAt: sentAt - ) - ) - ) - } - } catch { - print("Error loading introduction peers: \(error)") - } - - for sealedInvitation in try await listInvitations(pagination: pagination) { - do { - newConversations.append( - Conversation.v2(try makeConversation(from: sealedInvitation)) - ) - } catch { - print("Error loading invitations: \(error)") - } - } - - newConversations - .filter { $0.peerAddress != client.address } - .forEach { conversationsByTopic[$0.topic] = $0 } - - // TODO(perf): use DB to persist + sort - return conversationsByTopic.values.sorted { a, b in - a.createdAt < b.createdAt - } - } - - private func listIntroductionPeers(pagination: Pagination?) async throws -> [String: Date] { - let envelopes = try await client.apiClient.query( - topic: .userIntro(client.address), - pagination: pagination - ).envelopes - - let messages = envelopes.compactMap { envelope in - do { - let message = try MessageV1.fromBytes(envelope.message) - - // Attempt to decrypt, just to make sure we can - _ = try message.decrypt(with: client.privateKeyBundleV1) - - return message - } catch { - return nil - } - } - - var seenPeers: [String: Date] = [:] - for message in messages { - guard let recipientAddress = message.recipientAddress, - let senderAddress = message.senderAddress - else { - continue - } - - let sentAt = message.sentAt - let peerAddress = recipientAddress == client.address ? senderAddress : recipientAddress - - guard let existing = seenPeers[peerAddress] else { - seenPeers[peerAddress] = sentAt - continue - } - - if existing > sentAt { - seenPeers[peerAddress] = sentAt - } - } - - return seenPeers - } - - private func listInvitations(pagination: Pagination?) async throws -> [SealedInvitation] { - var envelopes = try await client.apiClient.envelopes( - topic: Topic.userInvite(client.address).description, - pagination: pagination - ) - - return envelopes.compactMap { envelope in - // swiftlint:disable no_optional_try - try? SealedInvitation(serializedData: envelope.message) - // swiftlint:enable no_optional_try - } - } - - func sendInvitation(recipient: SignedPublicKeyBundle, invitation: InvitationV1, created: Date) async throws -> SealedInvitation { - let sealed = try SealedInvitation.createV1( - sender: client.keys, - recipient: recipient, - created: created, - invitation: invitation - ) - - let peerAddress = try recipient.walletAddress - - try await client.publish(envelopes: [ - Envelope(topic: .userInvite(client.address), timestamp: created, message: try sealed.serializedData()), - Envelope(topic: .userInvite(peerAddress), timestamp: created, message: try sealed.serializedData()), - ]) - - return sealed - } + var client: Client + var conversationsByTopic: [String: Conversation] = [:] + + init(client: Client) { + self.client = client + } + + /// Import a previously seen conversation. + /// See Conversation.toTopicData() + public func importTopicData(data: Xmtp_KeystoreApi_V1_TopicMap.TopicData) -> Conversation { + let conversation: Conversation + if !data.hasInvitation { + let sentAt = Date(timeIntervalSince1970: TimeInterval(data.createdNs / 1_000_000_000)) + conversation = .v1(ConversationV1(client: client, peerAddress: data.peerAddress, sentAt: sentAt)) + } else { + conversation = .v2(ConversationV2( + topic: data.invitation.topic, + keyMaterial: data.invitation.aes256GcmHkdfSha256.keyMaterial, + context: data.invitation.context, + peerAddress: data.peerAddress, + client: client + )) + } + conversationsByTopic[conversation.topic] = conversation + return conversation + } + + public func listBatchMessages(topics: [String: Pagination?]) async throws -> [DecodedMessage] { + let requests = topics.map { topic, page in + makeQueryRequest(topic: topic, pagination: page) + } + /// The maximum number of requests permitted in a single batch call. + let maxQueryRequestsPerBatch = 50 + let batches = requests.chunks(maxQueryRequestsPerBatch) + .map { requests in BatchQueryRequest.with { $0.requests = requests } } + var messages: [DecodedMessage] = [] + // TODO: consider using a task group here for parallel batch calls + for batch in batches { + messages += try await client.apiClient.batchQuery(request: batch) + .responses.flatMap { res in + res.envelopes.compactMap { envelope in + let conversation = conversationsByTopic[envelope.contentTopic] + if conversation == nil { + print("discarding message, unknown conversation \(envelope)") + return nil + } + do { + return try conversation?.decode(envelope) + } catch { + print("discarding message, unable to decode \(envelope)") + return nil + } + } + } + } + return messages + } + + public func listBatchDecryptedMessages(topics: [String: Pagination?]) async throws -> [DecryptedMessage] { + let requests = topics.map { topic, page in + makeQueryRequest(topic: topic, pagination: page) + } + /// The maximum number of requests permitted in a single batch call. + let maxQueryRequestsPerBatch = 50 + let batches = requests.chunks(maxQueryRequestsPerBatch) + .map { requests in BatchQueryRequest.with { $0.requests = requests } } + var messages: [DecryptedMessage] = [] + // TODO: consider using a task group here for parallel batch calls + for batch in batches { + messages += try await client.apiClient.batchQuery(request: batch) + .responses.flatMap { res in + res.envelopes.compactMap { envelope in + let conversation = conversationsByTopic[envelope.contentTopic] + if conversation == nil { + print("discarding message, unknown conversation \(envelope)") + return nil + } + do { + return try conversation?.decrypt(envelope) + } catch { + print("discarding message, unable to decode \(envelope)") + return nil + } + } + } + } + return messages + } + + public func streamAllMessages() async throws -> AsyncThrowingStream { + return AsyncThrowingStream { continuation in + Task { + while true { + var topics: [String] = [ + Topic.userInvite(client.address).description, + Topic.userIntro(client.address).description, + ] + + for conversation in try await list() { + topics.append(conversation.topic) + } + + do { + for try await envelope in client.subscribe(topics: topics) { + if let conversation = conversationsByTopic[envelope.contentTopic] { + let decoded = try conversation.decode(envelope) + continuation.yield(decoded) + } else if envelope.contentTopic.hasPrefix("/xmtp/0/invite-") { + let conversation = try fromInvite(envelope: envelope) + conversationsByTopic[conversation.topic] = conversation + break // Break so we can resubscribe with the new conversation + } else if envelope.contentTopic.hasPrefix("/xmtp/0/intro-") { + let conversation = try fromIntro(envelope: envelope) + conversationsByTopic[conversation.topic] = conversation + let decoded = try conversation.decode(envelope) + continuation.yield(decoded) + break // Break so we can resubscribe with the new conversation + } else { + print("huh \(envelope)") + } + } + } catch { + continuation.finish(throwing: error) + } + } + } + } + } + + public func streamAllDecryptedMessages() async throws -> AsyncThrowingStream { + return AsyncThrowingStream { continuation in + Task { + while true { + var topics: [String] = [ + Topic.userInvite(client.address).description, + Topic.userIntro(client.address).description, + ] + + for conversation in try await list() { + topics.append(conversation.topic) + } + + do { + for try await envelope in client.subscribe(topics: topics) { + if let conversation = conversationsByTopic[envelope.contentTopic] { + let decoded = try conversation.decrypt(envelope) + continuation.yield(decoded) + } else if envelope.contentTopic.hasPrefix("/xmtp/0/invite-") { + let conversation = try fromInvite(envelope: envelope) + conversationsByTopic[conversation.topic] = conversation + break // Break so we can resubscribe with the new conversation + } else if envelope.contentTopic.hasPrefix("/xmtp/0/intro-") { + let conversation = try fromIntro(envelope: envelope) + conversationsByTopic[conversation.topic] = conversation + let decoded = try conversation.decrypt(envelope) + continuation.yield(decoded) + break // Break so we can resubscribe with the new conversation + } else { + print("huh \(envelope)") + } + } + } catch { + continuation.finish(throwing: error) + } + } + } + } + } + + public func fromInvite(envelope: Envelope) throws -> Conversation { + let sealedInvitation = try SealedInvitation(serializedData: envelope.message) + let unsealed = try sealedInvitation.v1.getInvitation(viewer: client.keys) + + return try .v2(ConversationV2.create(client: client, invitation: unsealed, header: sealedInvitation.v1.header)) + } + + public func fromIntro(envelope: Envelope) throws -> Conversation { + let messageV1 = try MessageV1.fromBytes(envelope.message) + let senderAddress = try messageV1.header.sender.walletAddress + let recipientAddress = try messageV1.header.recipient.walletAddress + + let peerAddress = client.address == senderAddress ? recipientAddress : senderAddress + let conversationV1 = ConversationV1(client: client, peerAddress: peerAddress, sentAt: messageV1.sentAt) + + return .v1(conversationV1) + } + + private func findExistingConversation(with peerAddress: String, conversationID: String?) -> Conversation? { + return conversationsByTopic.first(where: { $0.value.peerAddress == peerAddress && + (($0.value.conversationID ?? "") == (conversationID ?? "")) + })?.value + } + + public func newConversation(with peerAddress: String, context: InvitationV1.Context? = nil) async throws -> Conversation { + if peerAddress.lowercased() == client.address.lowercased() { + throw ConversationError.recipientIsSender + } + print("\(client.address) starting conversation with \(peerAddress)") + if let existing = findExistingConversation(with: peerAddress, conversationID: context?.conversationID) { + return existing + } + + guard let contact = try await client.contacts.find(peerAddress) else { + throw ConversationError.recipientNotOnNetwork + } + + _ = try await list() // cache old conversations and check again + if let existing = findExistingConversation(with: peerAddress, conversationID: context?.conversationID) { + return existing + } + + // We don't have an existing conversation, make a v2 one + let recipient = try contact.toSignedPublicKeyBundle() + let invitation = try InvitationV1.createDeterministic( + sender: client.keys, + recipient: recipient, + context: context + ) + let sealedInvitation = try await sendInvitation(recipient: recipient, invitation: invitation, created: Date()) + let conversationV2 = try ConversationV2.create(client: client, invitation: invitation, header: sealedInvitation.v1.header) + + try await client.contacts.allow(addresses: [peerAddress]) + + let conversation: Conversation = .v2(conversationV2) + conversationsByTopic[conversation.topic] = conversation + return conversation + } + + public func stream() -> AsyncThrowingStream { + AsyncThrowingStream { continuation in + Task { + var streamedConversationTopics: Set = [] + + for try await envelope in client.subscribe(topics: [.userIntro(client.address), .userInvite(client.address)]) { + if envelope.contentTopic == Topic.userIntro(client.address).description { + let conversationV1 = try fromIntro(envelope: envelope) + + if streamedConversationTopics.contains(conversationV1.topic.description) { + continue + } + + streamedConversationTopics.insert(conversationV1.topic.description) + continuation.yield(conversationV1) + } + + if envelope.contentTopic == Topic.userInvite(client.address).description { + let conversationV2 = try fromInvite(envelope: envelope) + + if streamedConversationTopics.contains(conversationV2.topic) { + continue + } + + streamedConversationTopics.insert(conversationV2.topic) + continuation.yield(conversationV2) + } + } + } + } + } + + private func makeConversation(from sealedInvitation: SealedInvitation) throws -> ConversationV2 { + let unsealed = try sealedInvitation.v1.getInvitation(viewer: client.keys) + let conversation = try ConversationV2.create(client: client, invitation: unsealed, header: sealedInvitation.v1.header) + + return conversation + } + + public func list() async throws -> [Conversation] { + var newConversations: [Conversation] = [] + let mostRecent = conversationsByTopic.values.max { a, b in + a.createdAt < b.createdAt + } + let pagination = Pagination(after: mostRecent?.createdAt) + do { + let seenPeers = try await listIntroductionPeers(pagination: pagination) + for (peerAddress, sentAt) in seenPeers { + newConversations.append( + Conversation.v1( + ConversationV1( + client: client, + peerAddress: peerAddress, + sentAt: sentAt + ) + ) + ) + } + } catch { + print("Error loading introduction peers: \(error)") + } + + for sealedInvitation in try await listInvitations(pagination: pagination) { + do { + try newConversations.append( + Conversation.v2(makeConversation(from: sealedInvitation)) + ) + } catch { + print("Error loading invitations: \(error)") + } + } + + newConversations + .filter { $0.peerAddress != client.address } + .forEach { conversationsByTopic[$0.topic] = $0 } + + // TODO(perf): use DB to persist + sort + return conversationsByTopic.values.sorted { a, b in + a.createdAt < b.createdAt + } + } + + private func listIntroductionPeers(pagination: Pagination?) async throws -> [String: Date] { + let envelopes = try await client.apiClient.query( + topic: .userIntro(client.address), + pagination: pagination + ).envelopes + + let messages = envelopes.compactMap { envelope in + do { + let message = try MessageV1.fromBytes(envelope.message) + + // Attempt to decrypt, just to make sure we can + _ = try message.decrypt(with: client.privateKeyBundleV1) + + return message + } catch { + return nil + } + } + + var seenPeers: [String: Date] = [:] + for message in messages { + guard let recipientAddress = message.recipientAddress, + let senderAddress = message.senderAddress + else { + continue + } + + let sentAt = message.sentAt + let peerAddress = recipientAddress == client.address ? senderAddress : recipientAddress + + guard let existing = seenPeers[peerAddress] else { + seenPeers[peerAddress] = sentAt + continue + } + + if existing > sentAt { + seenPeers[peerAddress] = sentAt + } + } + + return seenPeers + } + + private func listInvitations(pagination: Pagination?) async throws -> [SealedInvitation] { + var envelopes = try await client.apiClient.envelopes( + topic: Topic.userInvite(client.address).description, + pagination: pagination + ) + + return envelopes.compactMap { envelope in + // swiftlint:disable no_optional_try + try? SealedInvitation(serializedData: envelope.message) + // swiftlint:enable no_optional_try + } + } + + func sendInvitation(recipient: SignedPublicKeyBundle, invitation: InvitationV1, created: Date) async throws -> SealedInvitation { + let sealed = try SealedInvitation.createV1( + sender: client.keys, + recipient: recipient, + created: created, + invitation: invitation + ) + + let peerAddress = try recipient.walletAddress + + try await client.publish(envelopes: [ + Envelope(topic: .userInvite(client.address), timestamp: created, message: sealed.serializedData()), + Envelope(topic: .userInvite(peerAddress), timestamp: created, message: sealed.serializedData()), + ]) + + return sealed + } } diff --git a/Sources/XMTP/Messages/DecryptedMessage.swift b/Sources/XMTP/Messages/DecryptedMessage.swift index 9bf39d78..2109b5b0 100644 --- a/Sources/XMTP/Messages/DecryptedMessage.swift +++ b/Sources/XMTP/Messages/DecryptedMessage.swift @@ -8,9 +8,9 @@ import Foundation public struct DecryptedMessage { - var id: String - var encodedContent: EncodedContent - var senderAddress: String - var sentAt: Date - var topic: String = "" + public var id: String + public var encodedContent: EncodedContent + public var senderAddress: String + public var sentAt: Date + public var topic: String = "" } diff --git a/XMTP.podspec b/XMTP.podspec index edec8162..d4a17258 100644 --- a/XMTP.podspec +++ b/XMTP.podspec @@ -16,7 +16,7 @@ Pod::Spec.new do |spec| # spec.name = "XMTP" - spec.version = "0.6.9-alpha0" + spec.version = "0.6.12-alpha0" spec.summary = "XMTP SDK Cocoapod" # This description is used to generate tags and improve search results. diff --git a/XMTPiOSExample/XMTPiOSExample.xcodeproj/project.pbxproj b/XMTPiOSExample/XMTPiOSExample.xcodeproj/project.pbxproj index 07367f28..3035da8a 100644 --- a/XMTPiOSExample/XMTPiOSExample.xcodeproj/project.pbxproj +++ b/XMTPiOSExample/XMTPiOSExample.xcodeproj/project.pbxproj @@ -34,6 +34,7 @@ A6AE5192297B6270006FDD0F /* Persistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = A65F0703297B5D4E00C3C76E /* Persistence.swift */; }; A6AE5194297B62C8006FDD0F /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = A6AE5193297B62C8006FDD0F /* KeychainAccess */; }; A6D192D0293A7B97006B49F2 /* ConversationListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6D192CF293A7B97006B49F2 /* ConversationListView.swift */; }; + A6E774162B154CF000F01DFF /* XMTP in Frameworks */ = {isa = PBXBuildFile; productRef = A6E774152B154CF000F01DFF /* XMTP */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -78,13 +79,13 @@ A687810629679BC700042FAB /* Account.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Account.swift; sourceTree = ""; }; A687810B29679BFC00042FAB /* WalletConnection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletConnection.swift; sourceTree = ""; }; A687810D29679C0D00042FAB /* WalletConnectionMethod.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletConnectionMethod.swift; sourceTree = ""; }; - A69F33C7292DD3A9005A5556 /* xmtp-ios */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = "xmtp-ios"; path = ..; sourceTree = ""; }; A69F33C9292DD557005A5556 /* LoggedInView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoggedInView.swift; sourceTree = ""; }; A69F33CB292DD568005A5556 /* QRCodeSheetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeSheetView.swift; sourceTree = ""; }; A6AE5180297B61AE006FDD0F /* NotificationService.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = NotificationService.appex; sourceTree = BUILT_PRODUCTS_DIR; }; A6AE518B297B61C8006FDD0F /* NotificationService.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = NotificationService.entitlements; sourceTree = ""; }; A6AE518C297B6210006FDD0F /* NotificationService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = ""; }; A6D192CF293A7B97006B49F2 /* ConversationListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationListView.swift; sourceTree = ""; }; + A6E774192B154D1E00F01DFF /* xmtp-ios */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = "xmtp-ios"; path = ..; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -92,6 +93,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + A6E774162B154CF000F01DFF /* XMTP in Frameworks */, A65F0707297B5E7600C3C76E /* WalletConnectSwift in Frameworks */, 6AEE396E29F330CD0027B657 /* secp256k1 in Frameworks */, A69F33C6292DC992005A5556 /* XMTP in Frameworks */, @@ -114,7 +116,7 @@ A6281986292DC825004B9117 = { isa = PBXGroup; children = ( - A69F33C7292DD3A9005A5556 /* xmtp-ios */, + A6E774192B154D1E00F01DFF /* xmtp-ios */, A6281991292DC825004B9117 /* XMTPiOSExample */, A6AE5181297B61AE006FDD0F /* NotificationService */, A6281990292DC825004B9117 /* Products */, @@ -220,6 +222,7 @@ A65F0706297B5E7600C3C76E /* WalletConnectSwift */, A65F0709297B5E8600C3C76E /* KeychainAccess */, 6AEE396D29F330CD0027B657 /* secp256k1 */, + A6E774152B154CF000F01DFF /* XMTP */, ); productName = XMTPiOSExample; productReference = A628198F292DC825004B9117 /* XMTPiOSExample.app */; @@ -686,6 +689,10 @@ package = A65F0708297B5E8600C3C76E /* XCRemoteSwiftPackageReference "KeychainAccess" */; productName = KeychainAccess; }; + A6E774152B154CF000F01DFF /* XMTP */ = { + isa = XCSwiftPackageProductDependency; + productName = XMTP; + }; /* End XCSwiftPackageProductDependency section */ }; rootObject = A6281987292DC825004B9117 /* Project object */; diff --git a/XMTPiOSExample/XMTPiOSExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/XMTPiOSExample/XMTPiOSExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 38a4515b..9a6c92df 100644 --- a/XMTPiOSExample/XMTPiOSExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/XMTPiOSExample/XMTPiOSExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -30,7 +30,7 @@ { "identity" : "generic-json-swift", "kind" : "remoteSourceControl", - "location" : "https://github.com/zoul/generic-json-swift", + "location" : "https://github.com/iwill/generic-json-swift", "state" : { "revision" : "0a06575f4038b504e78ac330913d920f1630f510", "version" : "2.0.2" @@ -68,8 +68,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-atomics.git", "state" : { - "revision" : "919eb1d83e02121cdb434c7bfc1f0c66ef17febe", - "version" : "1.0.2" + "revision" : "cd142fd2f64be2100422d658e7411e39489da985", + "version" : "1.2.0" } }, { @@ -77,8 +77,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-collections.git", "state" : { - "revision" : "f504716c27d2e5d4144fa4794b12129301d17729", - "version" : "1.0.3" + "revision" : "a902f1823a7ff3c9ab2fba0f992396b948eda307", + "version" : "1.0.5" } }, { @@ -86,8 +86,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-docc-plugin.git", "state" : { - "revision" : "9b1258905c21fc1b97bf03d1b4ca12c4ec4e5fda", - "version" : "1.2.0" + "revision" : "26ac5758409154cc448d7ab82389c520fa8a8247", + "version" : "1.3.0" } }, { @@ -99,13 +99,22 @@ "version" : "1.0.0" } }, + { + "identity" : "swift-http-types", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-http-types", + "state" : { + "revision" : "99d066e29effa8845e4761dd3f2f831edfdf8925", + "version" : "1.0.0" + } + }, { "identity" : "swift-log", "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-log.git", "state" : { - "revision" : "6fe203dc33195667ce1759bf0182975e4653ba1c", - "version" : "1.4.4" + "revision" : "532d8b529501fb73a2455b179e0bbb6d49b652ed", + "version" : "1.5.3" } }, { @@ -113,8 +122,26 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-nio.git", "state" : { - "revision" : "e855380cb5234e96b760d93e0bfdc403e381e928", - "version" : "2.45.0" + "revision" : "702cd7c56d5d44eeba73fdf83918339b26dc855c", + "version" : "2.62.0" + } + }, + { + "identity" : "swift-nio-extras", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio-extras.git", + "state" : { + "revision" : "798c962495593a23fdea0c0c63fd55571d8dff51", + "version" : "1.20.0" + } + }, + { + "identity" : "swift-nio-http2", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio-http2.git", + "state" : { + "revision" : "3bd9004b9d685ed6b629760fc84903e48efec806", + "version" : "1.29.0" } }, { @@ -122,8 +149,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-nio-ssl.git", "state" : { - "revision" : "4fb7ead803e38949eb1d6fabb849206a72c580f3", - "version" : "2.23.0" + "revision" : "320bd978cceb8e88c125dcbb774943a92f6286e9", + "version" : "2.25.0" } }, { @@ -131,8 +158,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-nio-transport-services.git", "state" : { - "revision" : "c0d9a144cfaec8d3d596aadde3039286a266c15c", - "version" : "1.15.0" + "revision" : "ebf8b9c365a6ce043bf6e6326a04b15589bd285e", + "version" : "1.20.0" } }, { @@ -140,8 +167,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-protobuf.git", "state" : { - "revision" : "ab3a58b7209a17d781c0d1dbb3e1ff3da306bae8", - "version" : "1.20.3" + "revision" : "07f7f26ded8df9645c072f220378879c4642e063", + "version" : "1.25.1" } }, { @@ -158,8 +185,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/argentlabs/web3.swift", "state" : { - "revision" : "9da09d639d4e5d06eb59518e636b3ae957e8e9cd", - "version" : "1.3.0" + "revision" : "8ca33e700ed8de6137a0e1471017aa3b3c8de0db", + "version" : "1.6.0" } }, { @@ -167,8 +194,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/vapor/websocket-kit.git", "state" : { - "revision" : "2d9d2188a08eef4a869d368daab21b3c08510991", - "version" : "2.6.1" + "revision" : "53fe0639a98903858d0196b699720decb42aee7b", + "version" : "2.14.0" } }, { @@ -177,7 +204,7 @@ "location" : "https://github.com/xmtp/xmtp-rust-swift", "state" : { "branch" : "main", - "revision" : "e857176b7e368c51e1dadcbbcce648bb20432f26" + "revision" : "eb931c2f467c2a71a621f54d7ae22887b234c13a" } } ],