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

Group Preferences Actions #272

Merged
merged 2 commits into from
Mar 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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}
Comment on lines +128 to +135
Copy link

@tuddman tuddman Mar 1, 2024

Choose a reason for hiding this comment

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

Is this related to this PR? I'm curious why this is needed here.

Copy link
Contributor Author

@zombieobject zombieobject Mar 1, 2024

Choose a reason for hiding this comment

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

That was included in the required protobuf bump: 1eab635


/// 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
Loading