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

Implement Persistent Preferences #175

Merged
merged 11 commits into from
Oct 26, 2023
4 changes: 2 additions & 2 deletions Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -149,8 +149,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/xmtp/xmtp-rust-swift",
"state" : {
"revision" : "4a76e5401fa780c40610e2f0d248f695261d08dd",
"version" : "0.3.1-beta0"
"branch" : "main",
"revision" : "e857176b7e368c51e1dadcbbcce648bb20432f26"
}
}
],
Expand Down
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ let package = Package(
.package(url: "https://github.com/1024jp/GzipSwift", from: "5.2.0"),
.package(url: "https://github.com/bufbuild/connect-swift", from: "0.3.0"),
.package(url: "https://github.com/apple/swift-docc-plugin.git", from: "1.0.0"),
.package(url: "https://github.com/xmtp/xmtp-rust-swift", from: "0.3.5-beta0"),
.package(url: "https://github.com/xmtp/xmtp-rust-swift", branch: "main"),
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
Expand Down
118 changes: 118 additions & 0 deletions Sources/XMTP/Contacts.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,98 @@
//

import Foundation
import XMTPRust

public enum AllowState: String, Codable {
case allowed, blocked, unknown
}

struct AllowListEntry: Codable, Hashable {
enum EntryType: String, Codable {
case address
}

static func address(_ address: String, type: AllowState = .unknown) -> AllowListEntry {
AllowListEntry(value: address, entryType: .address, permissionType: type)
}

var value: String
var entryType: EntryType
var permissionType: AllowState

var key: String {
"\(entryType)-\(value)"
}
}

class AllowList {
var entries: [String: AllowState] = [:]

static func load(from client: Client) async throws -> AllowList {
let publicKey = client.privateKeyBundleV1.identityKey.publicKey.secp256K1Uncompressed.bytes
let privateKey = client.privateKeyBundleV1.identityKey.secp256K1.bytes

let identifier = try XMTPRust.generate_private_preferences_topic_identifier(RustVec(privateKey)).toString()
let envelopes = try await client.query(topic: .allowList(identifier))
let allowList = AllowList()

for envelope in envelopes.envelopes {


let payload = try XMTPRust.ecies_decrypt_k256_sha3_256(
RustVec(publicKey),
RustVec(privateKey),
RustVec(envelope.message)
)

let entry = try JSONDecoder().decode(AllowListEntry.self, from: Data(payload))
nplasterer marked this conversation as resolved.
Show resolved Hide resolved

allowList.entries[entry.key] = entry.permissionType
}

return allowList
}

static func publish(entry: AllowListEntry, to client: Client) async throws {
let payload = try JSONEncoder().encode(entry)

let publicKey = client.privateKeyBundleV1.identityKey.publicKey.secp256K1Uncompressed.bytes
let privateKey = client.privateKeyBundleV1.identityKey.secp256K1.bytes
let identifier = try XMTPRust.generate_private_preferences_topic_identifier(RustVec(privateKey)).toString()
nplasterer marked this conversation as resolved.
Show resolved Hide resolved

let message = try XMTPRust.ecies_encrypt_k256_sha3_256(
RustVec(publicKey),
RustVec(privateKey),
RustVec(payload)
)

let envelope = Envelope(
topic: Topic.allowList(identifier),
timestamp: Date(),
message: Data(message)
)

try await client.publish(envelopes: [envelope])
}

func allow(address: String) -> AllowListEntry {
entries[AllowListEntry.address(address).key] = .allowed

return .address(address, type: .allowed)
}

func block(address: String) -> AllowListEntry {
entries[AllowListEntry.address(address).key] = .blocked

return .address(address, type: .blocked)
}

func state(address: String) -> AllowState {
let state = entries[AllowListEntry.address(address).key]

return state ?? .unknown
}
}

/// Provides access to contact bundles.
public actor Contacts {
Expand All @@ -17,10 +109,36 @@ public actor Contacts {
// Whether or not we have sent invite/intro to this contact
var hasIntroduced: [String: Bool] = [:]

var allowList = AllowList()

init(client: Client) {
self.client = client
}

public func refreshAllowList() async throws {
self.allowList = try await AllowList.load(from: client)
}

public func isAllowed(_ address: String) -> Bool {
return allowList.state(address: address) == .allowed
}

public func isBlocked(_ address: String) -> Bool {
return allowList.state(address: address) == .blocked
}

public func allow(addresses: [String]) async throws {
for address in addresses {
try await AllowList.publish(entry: allowList.allow(address: address), to: client)
}
}

public func block(addresses: [String]) async throws {
for address in addresses {
try await AllowList.publish(entry: allowList.block(address: address), to: client)
}
}

func markIntroduced(_ peerAddress: String, _ isIntroduced: Bool) {
hasIntroduced[peerAddress] = isIntroduced
}
Expand Down
39 changes: 26 additions & 13 deletions Sources/XMTP/Conversation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,19 @@ public enum Conversation: Sendable {
case v1, v2
}

public func allowState() async -> AllowState {
let client: Client

switch self {
case .v1(let conversationV1):
client = conversationV1.client
case .v2(let conversationV2):
client = conversationV2.client
}

return await client.contacts.allowList.state(address: peerAddress)
}

public var version: Version {
switch self {
case .v1:
Expand Down Expand Up @@ -82,7 +95,7 @@ public enum Conversation: Sendable {
/// See Conversations.importTopicData()
public func toTopicData() -> Xmtp_KeystoreApi_V1_TopicMap.TopicData {
Xmtp_KeystoreApi_V1_TopicMap.TopicData.with {
$0.createdNs = UInt64(createdAt.timeIntervalSince1970 * 1_000) * 1_000_000
$0.createdNs = UInt64(createdAt.timeIntervalSince1970 * 1000) * 1_000_000
$0.peerAddress = peerAddress
if case let .v2(cv2) = self {
$0.invitation = Xmtp_MessageContents_InvitationV1.with {
Expand Down Expand Up @@ -123,16 +136,16 @@ public enum Conversation: Sendable {
}
}

// This is a convenience for invoking the underlying `client.publish(prepared.envelopes)`
// If a caller has a `Client` handy, they may opt to do that directly instead.
@discardableResult public func send(prepared: PreparedMessage) async throws -> String {
switch self {
case let .v1(conversationV1):
return try await conversationV1.send(prepared: prepared)
case let .v2(conversationV2):
return try await conversationV2.send(prepared: prepared)
}
}
// This is a convenience for invoking the underlying `client.publish(prepared.envelopes)`
// If a caller has a `Client` handy, they may opt to do that directly instead.
@discardableResult public func send(prepared: PreparedMessage) async throws -> String {
switch self {
case let .v1(conversationV1):
return try await conversationV1.send(prepared: prepared)
case let .v2(conversationV2):
return try await conversationV2.send(prepared: prepared)
}
}
Comment on lines -126 to +148
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Disregard formatting changes here.


@discardableResult public func send<T>(content: T, options: SendOptions? = nil, fallback _: String? = nil) async throws -> String {
switch self {
Expand Down Expand Up @@ -199,10 +212,10 @@ public enum Conversation: Sendable {
}

/// List messages in the conversation
public func messages(limit: Int? = nil, before: Date? = nil, after: Date? = nil, direction: PagingInfoSortDirection? = .descending) async throws -> [DecodedMessage] {
public func messages(limit: Int? = nil, before: Date? = nil, after: Date? = nil, direction: PagingInfoSortDirection? = .descending) async throws -> [DecodedMessage] {
switch self {
case let .v1(conversationV1):
return try await conversationV1.messages(limit: limit, before: before, after: after, direction: direction)
return try await conversationV1.messages(limit: limit, before: before, after: after, direction: direction)
case let .v2(conversationV2):
return try await conversationV2.messages(limit: limit, before: before, after: after, direction: direction)
}
Comment on lines -202 to 221
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Disregard formatting changes here.

Expand Down
2 changes: 2 additions & 0 deletions Sources/XMTP/Conversations.swift
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,8 @@ public actor Conversations {
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
Expand Down
5 changes: 4 additions & 1 deletion Sources/XMTP/Messages/Topic.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ public enum Topic {
userIntro(String),
userInvite(String),
directMessageV1(String, String),
directMessageV2(String)
directMessageV2(String),
allowList(String)

var description: String {
switch self {
Expand All @@ -30,6 +31,8 @@ public enum Topic {
return wrap("dm-\(addresses)")
case let .directMessageV2(randomString):
return wrap("m-\(randomString)")
case let .allowList(identifier):
return wrap("privatestore-\(identifier)/allowlist")
nplasterer marked this conversation as resolved.
Show resolved Hide resolved
}
}

Expand Down
28 changes: 28 additions & 0 deletions Tests/XMTPTests/ContactsTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,32 @@ class ContactsTests: XCTestCase {
let hasContact = await fixtures.aliceClient.contacts.has(fixtures.bob.walletAddress)
XCTAssert(hasContact)
}

func testAllowAddress() async throws {
let fixtures = await fixtures()

let contacts = fixtures.bobClient.contacts
var result = await contacts.isAllowed(fixtures.alice.address)

XCTAssertFalse(result)

try await contacts.allow(addresses: [fixtures.alice.address])

result = await contacts.isAllowed(fixtures.alice.address)
XCTAssertTrue(result)
}

func testBlockAddress() async throws {
let fixtures = await fixtures()

let contacts = fixtures.bobClient.contacts
var result = await contacts.isAllowed(fixtures.alice.address)

XCTAssertFalse(result)

try await contacts.block(addresses: [fixtures.alice.address])

result = await contacts.isBlocked(fixtures.alice.address)
XCTAssertTrue(result)
}
}
Loading