Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Renaming Conversation -> DirectMessage #230

Closed
wants to merge 11 commits into from
6 changes: 2 additions & 4 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,12 @@ let package = Package(
dependencies: [
// Dependencies declare other packages that this package depends on.
// .package(url: /* package url */, from: "1.0.0"),

.package(url: "https://github.com/GigaBitcoin/secp256k1.swift.git", exact: "0.10.0"),
.package(url: "https://github.com/argentlabs/web3.swift", from: "1.1.0"),
.package(url: "https://github.com/1024jp/GzipSwift", from: "5.2.0"),
.package(url: "https://github.com/bufbuild/connect-swift", exact: "0.3.0"),
.package(url: "https://github.com/apple/swift-docc-plugin.git", from: "1.0.0"),
.package(url: "https://github.com/xmtp/libxmtp-swift", revision: "92274fe"),
// .package(path: "../libxmtp-swift")
.package(url: "https://github.com/xmtp/libxmtp-swift", revision: "1d7068f181a402b2cd8b9520ccb768e65a55ec32"),
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
Expand All @@ -39,7 +37,7 @@ let package = Package(
"web3.swift",
.product(name: "Gzip", package: "GzipSwift"),
.product(name: "Connect", package: "connect-swift"),
.product(name: "LibXMTP", package: "libxmtp-swift"),
.product(name: "LibXMTP", package: "libxmtp-swift")
]
),
.target(
Expand Down
117 changes: 101 additions & 16 deletions Sources/XMTPiOS/Client.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@ public struct ClientOptions {
/// Specify which XMTP network to connect to. Defaults to ``.dev``
public var env: XMTPEnvironment = .dev

/// Optional: Specify self-reported version e.g. XMTPInbox/v1.0.0.
/// Specify whether the API client should use TLS security. In general this should only be false when using the `.local` environment.
public var isSecure: Bool = true

/// Specify whether the API client should use TLS security. In general this should only be false when using the `.local` environment.
/// /// Optional: Specify self-reported version e.g. XMTPInbox/v1.0.0.
public var appVersion: String?

public init(env: XMTPEnvironment = .dev, isSecure: Bool = true, appVersion: String? = nil) {
Expand All @@ -44,11 +44,20 @@ public struct ClientOptions {
/// `preCreateIdentityCallback` will be called immediately before a Create Identity wallet signature is requested from the user.
public var preCreateIdentityCallback: PreEventCallback?

public init(api: Api = Api(), codecs: [any ContentCodec] = [], preEnableIdentityCallback: PreEventCallback? = nil, preCreateIdentityCallback: PreEventCallback? = nil) {
public var enableAlphaMLS: Bool = false

public init(
api: Api = Api(),
codecs: [any ContentCodec] = [],
preEnableIdentityCallback: PreEventCallback? = nil,
preCreateIdentityCallback: PreEventCallback? = nil,
enableAlphaMLS: Bool = false
) {
self.api = api
self.codecs = codecs
self.preEnableIdentityCallback = preEnableIdentityCallback
self.preCreateIdentityCallback = preCreateIdentityCallback
self.enableAlphaMLS = enableAlphaMLS
}
}

Expand All @@ -60,11 +69,12 @@ public struct ClientOptions {
/// 2. To sign a random salt used to encrypt the key bundle in storage. This happens every time the client is started, including the very first time).
///
/// > Important: The client connects to the XMTP `dev` environment by default. Use ``ClientOptions`` to change this and other parameters of the network connection.
public final class Client: Sendable {
public final class Client {
/// The wallet address of the ``SigningKey`` used to create this Client.
public let address: String
let privateKeyBundleV1: PrivateKeyBundleV1
let apiClient: ApiClient
let v3Client: LibXMTP.FfiXmtpClient?

/// Access ``Conversations`` for this Client.
public lazy var conversations: Conversations = .init(client: self)
Expand All @@ -83,6 +93,8 @@ public final class Client: Sendable {
codecRegistry.register(codec: codec)
}

var enableAlphaMLS: Bool

/// Creates a client.
public static func create(account: SigningKey, options: ClientOptions? = nil) async throws -> Client {
let options = options ?? ClientOptions()
Expand All @@ -100,23 +112,54 @@ public final class Client: Sendable {
}

static func create(account: SigningKey, apiClient: ApiClient, options: ClientOptions? = nil) async throws -> Client {
let privateKeyBundleV1 = try await loadOrCreateKeys(for: account, apiClient: apiClient, options: options)
let (privateKeyBundleV1, source) = try await loadOrCreateKeys(for: account, apiClient: apiClient, options: options)

let v3Client: FfiXmtpClient?

if options?.enableAlphaMLS == true && options?.api.env == .local {
let dbURL = URL.documentsDirectory.appendingPathComponent("xmtp-\(account.address).db3")
v3Client = try await LibXMTP.createClient(
logger: XMTPLogger(),
host: GRPCApiClient.envToUrl(env: apiClient.environment),
isSecure: apiClient.environment != .local,
db: dbURL.path,
encryptionKey: nil,
accountAddress: account.address,
legacyIdentitySource: source,
legacySignedPrivateKeyProto: try privateKeyBundleV1.toV2().identityKey.serializedData()
)

let client = try Client(address: account.address, privateKeyBundleV1: privateKeyBundleV1, apiClient: apiClient)
if let textToSign = v3Client?.textToSign() {
let signature = try await account.sign(message: textToSign).rawData
try await v3Client?.registerIdentity(recoverableWalletSignature: signature)
}
} else {
v3Client = nil
}

let client = try Client(
address: account.address,
privateKeyBundleV1: privateKeyBundleV1,
apiClient: apiClient,
v3Client: v3Client,
enableAlphaMLS: options?.enableAlphaMLS == true
)
try await client.ensureUserContactPublished()

for codec in (options?.codecs ?? []) {
client.register(codec: codec)
}

return client
}

static func loadOrCreateKeys(for account: SigningKey, apiClient: ApiClient, options: ClientOptions? = nil) async throws -> PrivateKeyBundleV1 {
// swiftlint:disable no_optional_try
static func loadOrCreateKeys(for account: SigningKey, apiClient: ApiClient, options: ClientOptions? = nil) async throws -> (PrivateKeyBundleV1, LegacyIdentitySource) {
if let keys = try await loadPrivateKeys(for: account, apiClient: apiClient, options: options) {
// swiftlint:enable no_optional_try
print("loading existing private keys.")
#if DEBUG
print("Loaded existing private keys.")
#endif
return keys
return (keys, .network)
} else {
#if DEBUG
print("No existing keys found, creating new bundle.")
Expand All @@ -133,7 +176,7 @@ public final class Client: Sendable {
Envelope(topic: .userPrivateStoreKeyBundle(account.address), timestamp: Date(), message: encryptedKeys.serializedData()),
])

return keys
return (keys, .keyGenerator)
}
}

Expand All @@ -155,6 +198,14 @@ public final class Client: Sendable {
return nil
}

public func canMessageV3(address: String) async throws -> Bool {
guard let v3Client else {
return false
}

return try await v3Client.canMessage(accountAddresses: [address]) == [true]
}

public static func from(bundle: PrivateKeyBundle, options: ClientOptions? = nil) async throws -> Client {
return try await from(v1Bundle: bundle.v1, options: options)
}
Expand All @@ -172,13 +223,47 @@ public final class Client: Sendable {
rustClient: client
)

return try Client(address: address, privateKeyBundleV1: v1Bundle, apiClient: apiClient)
let v3Client: FfiXmtpClient?

if options.enableAlphaMLS == true && options.api.env == .local {
let dbURL = URL.documentsDirectory.appendingPathComponent("xmtp-\(address).db3")
v3Client = try await LibXMTP.createClient(
logger: XMTPLogger(),
host: GRPCApiClient.envToUrl(env: apiClient.environment),
isSecure: apiClient.environment != .local,
db: dbURL.path,
encryptionKey: nil,
accountAddress: address,
legacyIdentitySource: .static,
legacySignedPrivateKeyProto: try v1Bundle.toV2().identityKey.serializedData()
)

try await v3Client?.registerIdentity(recoverableWalletSignature: nil)
} else {
v3Client = nil
}

let result = try Client(
address: address,
privateKeyBundleV1: v1Bundle,
apiClient: apiClient,
v3Client: v3Client,
enableAlphaMLS: options.enableAlphaMLS == true
)

for codec in options.codecs {
result.register(codec: codec)
}

return result
}

init(address: String, privateKeyBundleV1: PrivateKeyBundleV1, apiClient: ApiClient) throws {
init(address: String, privateKeyBundleV1: PrivateKeyBundleV1, apiClient: ApiClient, v3Client: LibXMTP.FfiXmtpClient?, enableAlphaMLS: Bool) throws {
self.address = address
self.privateKeyBundleV1 = privateKeyBundleV1
self.apiClient = apiClient
self.v3Client = v3Client
self.enableAlphaMLS = enableAlphaMLS
}

public var privateKeyBundle: PrivateKeyBundle {
Expand Down Expand Up @@ -213,7 +298,7 @@ public final class Client: Sendable {
return try await apiClient.query(topic: Topic.contact(peerAddress)).envelopes.count > 0
}

public func importConversation(from conversationData: Data) throws -> Conversation? {
public func importConversation(from conversationData: Data) throws -> DirectMessage? {
let jsonDecoder = JSONDecoder()

do {
Expand All @@ -229,7 +314,7 @@ public final class Client: Sendable {
}
}

func importV2Conversation(export: ConversationV2Export) throws -> Conversation {
func importV2Conversation(export: ConversationV2Export) throws -> DirectMessage {
guard let keyMaterial = Data(base64Encoded: Data(export.keyMaterial.utf8)) else {
throw ConversationImportError.invalidData
}
Expand All @@ -247,7 +332,7 @@ public final class Client: Sendable {
))
}

func importV1Conversation(export: ConversationV1Export) throws -> Conversation {
func importV1Conversation(export: ConversationV1Export) throws -> DirectMessage {
let formatter = ISO8601DateFormatter()
formatter.formatOptions.insert(.withFractionalSeconds)

Expand Down
4 changes: 3 additions & 1 deletion Sources/XMTPiOS/CodecRegistry.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
import Foundation

struct CodecRegistry {
var codecs: [String: any ContentCodec] = [TextCodec().id: TextCodec()]
var codecs: [String: any ContentCodec] = [
TextCodec().id: TextCodec(),
]

mutating func register(codec: any ContentCodec) {
codecs[codec.id] = codec
Expand Down
40 changes: 40 additions & 0 deletions Sources/XMTPiOS/Codecs/GroupMembershipChanged.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
//
// File.swift
//
//
// Created by Pat Nakajima on 2/1/24.
//

import Foundation
import LibXMTP

public typealias GroupMembershipChanges = Xmtp_Mls_MessageContents_GroupMembershipChanges

public let ContentTypeGroupMembershipChanged = ContentTypeID(authorityID: "xmtp.org", typeID: "group_membership_change", versionMajor: 1, versionMinor: 0)

public struct GroupMembershipChangedCodec: ContentCodec {

public typealias T = GroupMembershipChanges

public init() { }

public var contentType = ContentTypeGroupMembershipChanged

public func encode(content: GroupMembershipChanges, client _: Client) throws -> EncodedContent {
var encodedContent = EncodedContent()

encodedContent.type = ContentTypeGroupMembershipChanged
encodedContent.content = try content.serializedData()

return encodedContent
}

public func decode(content: EncodedContent, client _: Client) throws -> GroupMembershipChanges {
return try GroupMembershipChanges(serializedData: content.content)
}

public func fallback(content: GroupMembershipChanges) throws -> String? {
return nil
}
}

Loading
Loading