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
158 changes: 157 additions & 1 deletion Sources/XMTP/Contacts.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,135 @@
//

import Foundation
import XMTPRust


public typealias PrivatePreferencesAction = Xmtp_MessageContents_PrivatePreferencesAction

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)"
}
}

public enum ContactError: Error {
case invalidIdentifier
}

class AllowList {
var entries: [String: AllowState] = [:]
var publicKey: Data
var privateKey: Data
var identifier: String?

var client: Client

init(client: Client) {
self.client = client
self.privateKey = client.privateKeyBundleV1.identityKey.secp256K1.bytes
self.publicKey = client.privateKeyBundleV1.identityKey.publicKey.secp256K1Uncompressed.bytes
self.identifier = try? XMTPRust.generate_private_preferences_topic_identifier(RustVec(privateKey)).toString()
}

func load() async throws -> AllowList {
guard let identifier = identifier else {
throw ContactError.invalidIdentifier
}

let envelopes = try await client.query(topic: .allowList(identifier))

let allowList = AllowList(client: client)

var preferences: [PrivatePreferencesAction] = []

for envelope in envelopes.envelopes {


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

preferences.append(try PrivatePreferencesAction(contiguousBytes: Data(payload).bytes))
}

preferences.forEach { preference in
preference.allow.walletAddresses.forEach { address in
allowList.allow(address: address)
}
preference.block.walletAddresses.forEach { address in
allowList.block(address: address)
}
}

return allowList
}

func publish(entry: AllowListEntry) async throws {
guard let identifier = identifier else {
throw ContactError.invalidIdentifier
}

var payload = PrivatePreferencesAction()
switch entry.permissionType {
case .allowed:
payload.allow.walletAddresses = [entry.value]
case .blocked:
payload.block.walletAddresses = [entry.value]
case .unknown:
payload.unknownFields
}

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

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,8 +146,35 @@ public actor Contacts {
// Whether or not we have sent invite/intro to this contact
var hasIntroduced: [String: Bool] = [:]

init(client: Client) {
var allowList: AllowList

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

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

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(client: client).publish(entry: allowList.allow(address: address))
}
}

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

func markIntroduced(_ peerAddress: String, _ isIntroduced: Bool) {
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
Loading
Loading