Skip to content

Commit

Permalink
Group Preferences Actions (#272)
Browse files Browse the repository at this point in the history
* bump the protos to the latest

* Group Preferences Actions

---------

Co-authored-by: Naomi Plasterer <naomi@xmtp.com>
  • Loading branch information
zombieobject and nplasterer authored Mar 1, 2024
1 parent d6a719d commit ca76d64
Show file tree
Hide file tree
Showing 13 changed files with 370 additions and 107 deletions.
1 change: 1 addition & 0 deletions .swiftlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ disabled_rules: # rule identifiers turned on by default to exclude from running
- redundant_optional_initialization
- operator_whitespace
- comma
- no_optional_try

opt_in_rules:
- force_unwrapping
Expand Down
111 changes: 90 additions & 21 deletions Sources/XMTPiOS/Contacts.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,16 @@ public enum ConsentState: String, Codable {

public struct ConsentListEntry: Codable, Hashable {
public enum EntryType: String, Codable {
case address
case address, groupId
}

static func address(_ address: String, type: ConsentState = .unknown) -> ConsentListEntry {
ConsentListEntry(value: address, entryType: .address, consentType: type)
}

static func groupId(groupId: String, type: ConsentState = ConsentState.unknown) -> ConsentListEntry {
ConsentListEntry(value: groupId, entryType: .groupId, consentType: type)
}

public var value: String
public var entryType: EntryType
Expand All @@ -48,17 +52,14 @@ public class ConsentList {
self.client = client
privateKey = client.privateKeyBundleV1.identityKey.secp256K1.bytes
publicKey = client.privateKeyBundleV1.identityKey.publicKey.secp256K1Uncompressed.bytes
// swiftlint:disable no_optional_try
identifier = try? LibXMTP.generatePrivatePreferencesTopicIdentifier(privateKey: privateKey)
// swiftlint:enable no_optional_try
}

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


let envelopes = try await client.apiClient.envelopes(topic: Topic.preferenceList(identifier).description, pagination: Pagination(direction: .ascending))
let consentList = ConsentList(client: client)

Expand All @@ -71,13 +72,21 @@ public class ConsentList {
}

for preference in preferences {
for address in preference.allow.walletAddresses {
for address in preference.allowAddress.walletAddresses {
_ = consentList.allow(address: address)
}

for address in preference.block.walletAddresses {
for address in preference.denyAddress.walletAddresses {
_ = consentList.deny(address: address)
}

for groupId in preference.allowGroup.groupIds {
_ = consentList.allowGroup(groupId: groupId)
}

for groupId in preference.denyGroup.groupIds {
_ = consentList.denyGroup(groupId: groupId)
}
}

return consentList
Expand All @@ -89,13 +98,31 @@ public class ConsentList {
}

var payload = PrivatePreferencesAction()
switch entry.consentType {
case .allowed:
payload.allow.walletAddresses = [entry.value]
case .denied:
payload.block.walletAddresses = [entry.value]
case .unknown:
payload.messageType = nil
switch entry.entryType {

case .address:
switch entry.consentType {
case .allowed:
payload.allowAddress.walletAddresses = [entry.value]
case .denied:
payload.denyAddress.walletAddresses = [entry.value]
case .unknown:
payload.messageType = nil
}

case .groupId:
switch entry.consentType {
case .allowed:
if let valueData = entry.value.data(using: .utf8) {
payload.allowGroup.groupIds = [valueData]
}
case .denied:
if let valueData = entry.value.data(using: .utf8) {
payload.denyGroup.groupIds = [valueData]
}
case .unknown:
payload.messageType = nil
}
}

let message = try LibXMTP.userPreferencesEncrypt(
Expand Down Expand Up @@ -127,12 +154,36 @@ public class ConsentList {
return entry
}

func allowGroup(groupId: Data) -> ConsentListEntry {
let groupIdString = groupId.toHex
let entry = ConsentListEntry.groupId(groupId: groupIdString, type: ConsentState.allowed)
entries[ConsentListEntry.groupId(groupId: groupIdString).key] = entry

return entry
}

func denyGroup(groupId: Data) -> ConsentListEntry {
let groupIdString = groupId.toHex
let entry = ConsentListEntry.groupId(groupId: groupIdString, type: ConsentState.denied)
entries[ConsentListEntry.groupId(groupId: groupIdString).key] = entry

return entry
}

func state(address: String) -> ConsentState {
let entry = entries[ConsentListEntry.address(address).key]
guard let entry = entries[ConsentListEntry.address(address).key] else {
return .unknown
}

return entry.consentType
}

func groupState(groupId: Data) -> ConsentState {
guard let entry = entries[ConsentListEntry.groupId(groupId: groupId.toHex).key] else {
return .unknown
}

// swiftlint:disable no_optional_try
return entry?.consentType ?? .unknown
// swiftlint:enable no_optional_try
return entry.consentType
}
}

Expand Down Expand Up @@ -166,6 +217,14 @@ public actor Contacts {
return consentList.state(address: address) == .denied
}

public func isGroupAllowed(groupId: Data) -> Bool {
return consentList.groupState(groupId: groupId) == .allowed
}

public func isGroupDenied(groupId: Data) -> Bool {
return consentList.groupState(groupId: groupId) == .denied
}

public func allow(addresses: [String]) async throws {
for address in addresses {
try await ConsentList(client: client).publish(entry: consentList.allow(address: address))
Expand All @@ -178,6 +237,20 @@ public actor Contacts {
}
}

public func allowGroup(groupIds: [Data]) async throws {
for groupId in groupIds {
let entry = consentList.allowGroup(groupId: groupId)
try await ConsentList(client: client).publish(entry: entry)
}
}

public func denyGroup(groupIds: [Data]) async throws {
for groupId in groupIds {
let entry = consentList.denyGroup(groupId: groupId)
try await ConsentList(client: client).publish(entry: entry)
}
}

func markIntroduced(_ peerAddress: String, _ isIntroduced: Bool) {
hasIntroduced[peerAddress] = isIntroduced
}
Expand All @@ -198,15 +271,11 @@ public actor Contacts {
let response = try await client.query(topic: .contact(peerAddress))

for envelope in response.envelopes {
// swiftlint:disable no_optional_try
if let contactBundle = try? ContactBundle.from(envelope: envelope) {
knownBundles[peerAddress] = contactBundle

return contactBundle
}
// swiftlint:enable no_optional_try
}

return nil
}
}
6 changes: 5 additions & 1 deletion Sources/XMTPiOS/Conversations.swift
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,11 @@ public actor Conversations {
throw GroupError.memberNotRegistered(erroredAddresses)
}

return try await v3Client.conversations().createGroup(accountAddresses: addresses, permissions: permissions).fromFFI(client: client)
let group = try await v3Client.conversations().createGroup(accountAddresses: addresses, permissions: permissions).fromFFI(client: client)

try await client.contacts.allowGroup(groupIds: [group.id])

return group
}

/// Import a previously seen conversation.
Expand Down
6 changes: 6 additions & 0 deletions Sources/XMTPiOS/Group.swift
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,12 @@ public struct Group: Identifiable, Equatable, Hashable {
}

public func send(encodedContent: EncodedContent) async throws -> String {
let groupState = await client.contacts.consentList.groupState(groupId: id)

if groupState == ConsentState.unknown {
try await client.contacts.allowGroup(groupIds: [id])
}

try await ffiGroup.send(contentBytes: encodedContent.serializedData())
return id.toHex
}
Expand Down
40 changes: 28 additions & 12 deletions Sources/XMTPiOS/Proto/message_contents/message.pb.swift
Original file line number Diff line number Diff line change
Expand Up @@ -125,16 +125,32 @@ public struct Xmtp_MessageContents_MessageV2 {
public mutating func clearCiphertext() {self._ciphertext = nil}

/// HMAC of the message ciphertext, with the HMAC key derived from the topic key
public var senderHmac: Data = Data()
public var senderHmac: Data {
get {return _senderHmac ?? Data()}
set {_senderHmac = newValue}
}
/// Returns true if `senderHmac` has been explicitly set.
public var hasSenderHmac: Bool {return self._senderHmac != nil}
/// Clears the value of `senderHmac`. Subsequent reads from it will return its default value.
public mutating func clearSenderHmac() {self._senderHmac = nil}

/// Flag indicating whether the message should be pushed from a notification server
public var shouldPush: Bool = false
public var shouldPush: Bool {
get {return _shouldPush ?? false}
set {_shouldPush = newValue}
}
/// Returns true if `shouldPush` has been explicitly set.
public var hasShouldPush: Bool {return self._shouldPush != nil}
/// Clears the value of `shouldPush`. Subsequent reads from it will return its default value.
public mutating func clearShouldPush() {self._shouldPush = nil}

public var unknownFields = SwiftProtobuf.UnknownStorage()

public init() {}

fileprivate var _ciphertext: Xmtp_MessageContents_Ciphertext? = nil
fileprivate var _senderHmac: Data? = nil
fileprivate var _shouldPush: Bool? = nil
}

/// Versioned Message
Expand Down Expand Up @@ -432,8 +448,8 @@ extension Xmtp_MessageContents_MessageV2: SwiftProtobuf.Message, SwiftProtobuf._
switch fieldNumber {
case 1: try { try decoder.decodeSingularBytesField(value: &self.headerBytes) }()
case 2: try { try decoder.decodeSingularMessageField(value: &self._ciphertext) }()
case 3: try { try decoder.decodeSingularBytesField(value: &self.senderHmac) }()
case 4: try { try decoder.decodeSingularBoolField(value: &self.shouldPush) }()
case 3: try { try decoder.decodeSingularBytesField(value: &self._senderHmac) }()
case 4: try { try decoder.decodeSingularBoolField(value: &self._shouldPush) }()
default: break
}
}
Expand All @@ -450,20 +466,20 @@ extension Xmtp_MessageContents_MessageV2: SwiftProtobuf.Message, SwiftProtobuf._
try { if let v = self._ciphertext {
try visitor.visitSingularMessageField(value: v, fieldNumber: 2)
} }()
if !self.senderHmac.isEmpty {
try visitor.visitSingularBytesField(value: self.senderHmac, fieldNumber: 3)
}
if self.shouldPush != false {
try visitor.visitSingularBoolField(value: self.shouldPush, fieldNumber: 4)
}
try { if let v = self._senderHmac {
try visitor.visitSingularBytesField(value: v, fieldNumber: 3)
} }()
try { if let v = self._shouldPush {
try visitor.visitSingularBoolField(value: v, fieldNumber: 4)
} }()
try unknownFields.traverse(visitor: &visitor)
}

public static func ==(lhs: Xmtp_MessageContents_MessageV2, rhs: Xmtp_MessageContents_MessageV2) -> Bool {
if lhs.headerBytes != rhs.headerBytes {return false}
if lhs._ciphertext != rhs._ciphertext {return false}
if lhs.senderHmac != rhs.senderHmac {return false}
if lhs.shouldPush != rhs.shouldPush {return false}
if lhs._senderHmac != rhs._senderHmac {return false}
if lhs._shouldPush != rhs._shouldPush {return false}
if lhs.unknownFields != rhs.unknownFields {return false}
return true
}
Expand Down
Loading

0 comments on commit ca76d64

Please sign in to comment.