Skip to content

Commit

Permalink
Implement Persistent Preferences (#175)
Browse files Browse the repository at this point in the history
* Start on v2 allow state stuff

* encrypt

* Derive topic identifier instead of using wallet address

* generate the latest proto code

* update contacts to use proto code

* pull the identifier and keys out to the init

* remove proto code that breaks the build

* bump the pod specs for RN

* update the topic

* name it preference list and confirm tests still pass

* fix the linter issue

---------

Co-authored-by: Naomi Plasterer <[email protected]>
  • Loading branch information
nakajima and nplasterer authored Oct 26, 2023
1 parent 8fac49c commit 7e69703
Show file tree
Hide file tree
Showing 19 changed files with 1,453 additions and 106 deletions.
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
160 changes: 159 additions & 1 deletion Sources/XMTP/Contacts.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,137 @@
//

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
// swiftlint:disable no_optional_try
self.identifier = try? XMTPRust.generate_private_preferences_topic_identifier(RustVec(privateKey)).toString()
// swiftlint:enable no_optional_try
}

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

let envelopes = try await client.query(topic: .preferenceList(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.preferenceList(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 +148,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)
}
}

@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)
}
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),
preferenceList(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 .preferenceList(identifier):
return wrap("pppp-\(identifier)")
}
}

Expand Down
Loading

0 comments on commit 7e69703

Please sign in to comment.