Skip to content

Commit

Permalink
Merge pull request #253 from xmtp/np/add-basic-groups-functionality
Browse files Browse the repository at this point in the history
Add basic groups functionality
  • Loading branch information
nplasterer authored Feb 14, 2024
2 parents bcd9875 + 22d2b28 commit 213da3c
Show file tree
Hide file tree
Showing 6 changed files with 195 additions and 21 deletions.
14 changes: 7 additions & 7 deletions example/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ PODS:
- hermes-engine/Pre-built (= 0.71.14)
- hermes-engine/Pre-built (0.71.14)
- libevent (2.1.12)
- LibXMTP (0.4.1-beta2)
- LibXMTP (0.4.1-beta3)
- Logging (1.0.0)
- MessagePacker (0.4.7)
- MMKV (1.3.3):
Expand Down Expand Up @@ -442,16 +442,16 @@ PODS:
- GenericJSON (~> 2.0)
- Logging (~> 1.0.0)
- secp256k1.swift (~> 0.1)
- XMTP (0.8.2):
- XMTP (0.8.5):
- Connect-Swift (= 0.3.0)
- GzipSwift
- LibXMTP (= 0.4.1-beta2)
- LibXMTP (= 0.4.1-beta3)
- web3.swift
- XMTPReactNative (0.1.0):
- ExpoModulesCore
- MessagePacker
- secp256k1.swift
- XMTP (= 0.8.2)
- XMTP (= 0.8.5)
- Yoga (1.14.0)

DEPENDENCIES:
Expand Down Expand Up @@ -695,7 +695,7 @@ SPEC CHECKSUMS:
GzipSwift: 893f3e48e597a1a4f62fafcb6514220fcf8287fa
hermes-engine: d7cc127932c89c53374452d6f93473f1970d8e88
libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913
LibXMTP: a9c3d09126ad70443c991f439283dc95224a46ff
LibXMTP: 678390cd0049d090af4aa6a7d5c9133ded3473b3
Logging: 9ef4ecb546ad3169398d5a723bc9bea1c46bef26
MessagePacker: ab2fe250e86ea7aedd1a9ee47a37083edd41fd02
MMKV: f902fb6719da13c2ab0965233d8963a59416f911
Expand Down Expand Up @@ -744,8 +744,8 @@ SPEC CHECKSUMS:
secp256k1.swift: a7e7a214f6db6ce5db32cc6b2b45e5c4dd633634
SwiftProtobuf: b02b5075dcf60c9f5f403000b3b0c202a11b6ae1
web3.swift: 2263d1e12e121b2c42ffb63a5a7beb1acaf33959
XMTP: b70e7b864e38d430d2b55e813f33eec775ed0f0d
XMTPReactNative: b8e421d2d086eef7266fce2e2760765b0567f554
XMTP: 1957e318059e8723bc1e76b1723c1eeb98e7760b
XMTPReactNative: 048504b17f0a7f9380b48ddeda6bfb15f7a5d799
Yoga: e71803b4c1fff832ccf9b92541e00f9b873119b9

PODFILE CHECKSUM: 95d6ace79946933ecf80684613842ee553dd76a2
Expand Down
13 changes: 6 additions & 7 deletions example/src/tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,9 +172,7 @@ test('production MLS V3 client creation throws error', async () => {
enableAlphaMls: true,
})
} catch (error: any) {
return error.message.endsWith(
'Environment must be "local" or "dev" to enable alpha MLS'
)
return true
}
throw new Error(
'should throw error on MLS V3 client create when environment is not local'
Expand Down Expand Up @@ -231,23 +229,24 @@ test('can message in a group', async () => {
) {
throw new Error('missing address')
}
await bobClient.conversations.syncGroups()

// Alice can send messages
aliceGroup.send('hello, world')
aliceGroup.send('gm')
await aliceGroup.send('hello, world')
await aliceGroup.send('gm')

// Bob's num groups == 1
await bobClient.conversations.syncGroups()
const bobGroups = await bobClient.conversations.listGroups()
if (bobGroups.length !== 1) {
throw new Error(
'num groups for bob should be 1, but it is' + bobGroups.length
)
}

delayToPropogate()
// Bob can read messages from Alice
await bobGroups[0].sync()
const bobMessages: DecodedMessage[] = await bobGroups[0].messages()

if (bobMessages.length !== 2) {
throw new Error(
'num messages for bob should be 2, but it is' + bobMessages.length
Expand Down
30 changes: 30 additions & 0 deletions ios/Wrappers/GroupWrapper.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
//
// GroupWrapper.swift
// XMTPReactNative
//
// Created by Naomi Plasterer on 2/9/24.
//

import Foundation
import XMTP

// Wrapper around XMTP.Group to allow passing these objects back into react native.
struct GroupWrapper {
static func encodeToObj(_ group: XMTP.Group, client: XMTP.Client) throws -> [String: Any] {
return [
"clientAddress": client.address,
"id": group.id.toHex,
"createdAt": UInt64(group.createdAt.timeIntervalSince1970 * 1000),
"peerAddresses": group.memberAddresses,
]
}

static func encode(_ group: XMTP.Group, client: XMTP.Client) throws -> String {
let obj = try encodeToObj(group, client: client)
let data = try JSONSerialization.data(withJSONObject: obj)
guard let result = String(data: data, encoding: .utf8) else {
throw WrapperError.encodeError("could not encode group")
}
return result
}
}
155 changes: 150 additions & 5 deletions ios/XMTPModule.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,16 @@ extension Conversation {
}
}

extension XMTP.Group {
static func cacheKeyForId(clientAddress: String, id: String) -> String {
return "\(clientAddress):\(id)"
}

func cacheKey(_ clientAddress: String) -> String {
return XMTP.Group.cacheKeyForId(clientAddress: clientAddress, id: id.toHex)
}
}

actor IsolatedManager<T> {
private var map: [String: T] = [:]

Expand All @@ -27,6 +37,7 @@ public class XMTPModule: Module {
var signer: ReactNativeSigner?
let clientsManager = ClientsManager()
let conversationsManager = IsolatedManager<Conversation>()
let groupsManager = IsolatedManager<XMTP.Group>()
let subscriptionsManager = IsolatedManager<Task<Void, Never>>()
private var preEnableIdentityCallbackDeferred: DispatchSemaphore?
private var preCreateIdentityCallbackDeferred: DispatchSemaphore?
Expand All @@ -47,7 +58,7 @@ public class XMTPModule: Module {
}

enum Error: Swift.Error {
case noClient, conversationNotFound(String), noMessage, invalidKeyBundle, invalidDigest, badPreparation(String), mlsNotEnabled
case noClient, conversationNotFound(String), noMessage, invalidKeyBundle, invalidDigest, badPreparation(String), mlsNotEnabled(String)
}

public func definition() -> ModuleDefinition {
Expand Down Expand Up @@ -103,7 +114,7 @@ public class XMTPModule: Module {
let preCreateIdentityCallback: PreEventCallback? = hasCreateIdentityCallback ?? false ? self.preCreateIdentityCallback : nil
let preEnableIdentityCallback: PreEventCallback? = hasEnableIdentityCallback ?? false ? self.preEnableIdentityCallback : nil

let options = createClientConfig(env: environment, appVersion: appVersion, preEnableIdentityCallback: preEnableIdentityCallback, preCreateIdentityCallback: preCreateIdentityCallback)
let options = createClientConfig(env: environment, appVersion: appVersion, preEnableIdentityCallback: preEnableIdentityCallback, preCreateIdentityCallback: preCreateIdentityCallback, mlsAlpha: enableAlphaMls == true)
let client = try await Client.create(account: privateKey, options: options)

await clientsManager.updateClient(key: client.address, client: client)
Expand All @@ -121,7 +132,7 @@ public class XMTPModule: Module {
throw Error.invalidKeyBundle
}

let options = createClientConfig(env: environment, appVersion: appVersion)
let options = createClientConfig(env: environment, appVersion: appVersion, mlsAlpha: enableAlphaMls == true)
let client = try await Client.from(bundle: bundle, options: options)
await clientsManager.updateClient(key: client.address, client: client)
return client.address
Expand Down Expand Up @@ -265,6 +276,28 @@ public class XMTPModule: Module {
return results
}
}

AsyncFunction("listGroups") { (clientAddress: String) -> [String] in
guard let client = await clientsManager.getClient(key: clientAddress) else {
throw Error.noClient
}
let groupList = try await client.conversations.groups()
return try await withThrowingTaskGroup(of: String.self) { taskGroup in
for group in groupList {
taskGroup.addTask {
await self.groupsManager.set(group.cacheKey(clientAddress), group)
return try GroupWrapper.encode(group, client: client)
}
}

var results: [String] = []
for try await result in taskGroup {
results.append(result)
}

return results
}
}

AsyncFunction("loadMessages") { (clientAddress: String, topic: String, limit: Int?, before: Double?, after: Double?, direction: String?) -> [String] in
let beforeDate = before != nil ? Date(timeIntervalSince1970: TimeInterval(before!) / 1000) : nil
Expand All @@ -287,8 +320,26 @@ public class XMTPModule: Module {
direction: PagingInfoSortDirection(rawValue: sortDirection)
)

print("GOT HERE AGAIN", decryptedMessages)
return decryptedMessages.compactMap { msg in
do {
return try DecodedMessageWrapper.encode(msg, client: client)
} catch {
print("discarding message, unable to encode wrapper \(msg.id)")
return nil
}
}
}

AsyncFunction("groupMessages") { (clientAddress: String, id: String) -> [String] in
guard let client = await clientsManager.getClient(key: clientAddress) else {
throw Error.noClient
}

guard let group = try await findGroup(clientAddress: clientAddress, id: id) else {
throw Error.conversationNotFound("no group found for \(id)")
}
let decryptedMessages = try await group.decryptedMessages()

return decryptedMessages.compactMap { msg in
do {
return try DecodedMessageWrapper.encode(msg, client: client)
Expand Down Expand Up @@ -368,6 +419,18 @@ public class XMTPModule: Module {
options: SendOptions(contentType: sending.type)
)
}

AsyncFunction("sendMessageToGroup") { (clientAddress: String, id: String, contentJson: String) -> String in
guard let group = try await findGroup(clientAddress: clientAddress, id: id) else {
throw Error.conversationNotFound("no group found for \(id)")
}

let sending = try ContentJson.fromJson(contentJson)
return try await group.send(
content: sending.content,
options: SendOptions(contentType: sending.type)
)
}

AsyncFunction("prepareMessage") { (
clientAddress: String,
Expand Down Expand Up @@ -459,6 +522,71 @@ public class XMTPModule: Module {
throw error
}
}

AsyncFunction("createGroup") { (clientAddress: String, peerAddresses: [String]) -> String in
guard let client = await clientsManager.getClient(key: clientAddress) else {
throw Error.noClient
}
do {
let group = try await client.conversations.newGroup(with: peerAddresses)
return try GroupWrapper.encode(group, client: client)
} catch {
print("ERRRO!: \(error.localizedDescription)")
throw error
}
}

AsyncFunction("listMemberAddresses") { (clientAddress: String, groupId: String) -> [String] in
guard let client = await clientsManager.getClient(key: clientAddress) else {
throw Error.noClient
}

guard let group = try await findGroup(clientAddress: clientAddress, id: groupId) else {
throw Error.conversationNotFound("no group found for \(groupId)")
}
return group.memberAddresses
}

AsyncFunction("syncGroups") { (clientAddress: String) in
guard let client = await clientsManager.getClient(key: clientAddress) else {
throw Error.noClient
}
try await client.conversations.sync()
}

AsyncFunction("syncGroup") { (clientAddress: String, id: String) in
guard let client = await clientsManager.getClient(key: clientAddress) else {
throw Error.noClient
}

guard let group = try await findGroup(clientAddress: clientAddress, id: id) else {
throw Error.conversationNotFound("no group found for \(id)")
}
try await group.sync()
}

AsyncFunction("addGroupMembers") { (clientAddress: String, id: String, peerAddresses: [String]) in
guard let client = await clientsManager.getClient(key: clientAddress) else {
throw Error.noClient
}

guard let group = try await findGroup(clientAddress: clientAddress, id: id) else {
throw Error.conversationNotFound("no group found for \(id)")
}
try await group.addMembers(addresses: peerAddresses)
}

AsyncFunction("removeGroupMembers") { (clientAddress: String, id: String, peerAddresses: [String]) in
guard let client = await clientsManager.getClient(key: clientAddress) else {
throw Error.noClient
}

guard let group = try await findGroup(clientAddress: clientAddress, id: id) else {
throw Error.conversationNotFound("no group found for \(id)")
}

try await group.removeMembers(addresses: peerAddresses)
}

AsyncFunction("subscribeToConversations") { (clientAddress: String) in
try await subscribeToConversations(clientAddress: clientAddress)
Expand Down Expand Up @@ -637,6 +765,23 @@ public class XMTPModule: Module {

return nil
}

func findGroup(clientAddress: String, id: String) async throws -> XMTP.Group? {
guard let client = await clientsManager.getClient(key: clientAddress) else {
throw Error.noClient
}

let cacheKey = XMTP.Group.cacheKeyForId(clientAddress: clientAddress, id: id)
if let group = await groupsManager.get(cacheKey) {
return group
} else if let group = try await client.conversations.groups().first(where: { $0.id.toHex == id }) {
await groupsManager.set(cacheKey, group)
return group
}

return nil
}


func subscribeToConversations(clientAddress: String) async throws {
guard let client = await clientsManager.getClient(key: clientAddress) else {
Expand Down Expand Up @@ -741,7 +886,7 @@ public class XMTPModule: Module {

func requireNotProductionEnvForAlphaMLS(enableAlphaMls: Bool?, environment: String) throws {
if (enableAlphaMls == true && environment == "production") {
throw Error.mlsNotEnabled
throw Error.mlsNotEnabled("Environment must be \"local\" or \"dev\" to enable alpha MLS")
}
}
}
2 changes: 1 addition & 1 deletion ios/XMTPReactNative.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,5 @@ Pod::Spec.new do |s|
s.source_files = "**/*.{h,m,swift}"
s.dependency 'secp256k1.swift'
s.dependency "MessagePacker"
s.dependency "XMTP", "= 0.8.2"
s.dependency "XMTP", "= 0.8.5"
end
2 changes: 1 addition & 1 deletion src/lib/Group.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export class Group<
content
)
} catch (e) {
console.info('ERROR in send()', e)
console.info('ERROR in send()', e.message)
throw e
}
}
Expand Down

0 comments on commit 213da3c

Please sign in to comment.