Skip to content

Commit

Permalink
Merge pull request #559 from xmtp/np/codec-tests
Browse files Browse the repository at this point in the history
Add back custom content type ability.
  • Loading branch information
cameronvoell authored Dec 12, 2024
2 parents 4a20699 + cacd7ed commit eda35c7
Show file tree
Hide file tree
Showing 11 changed files with 339 additions and 20 deletions.
5 changes: 5 additions & 0 deletions .changeset/add-custom-content-type-ability.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@xmtp/react-native-sdk": patch
---

Add back custom content types.
6 changes: 3 additions & 3 deletions android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ repositories {
dependencies {
implementation project(':expo-modules-core')
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${getKotlinVersion()}"
implementation "org.xmtp:android:3.0.13"
implementation "org.xmtp:android:3.0.14"
implementation 'com.google.code.gson:gson:2.10.1'
implementation 'com.facebook.react:react-native:0.71.3'
implementation "com.daveanthonythomas.moshipack:moshipack:1.0.1"
Expand All @@ -109,8 +109,8 @@ dependencies {
// implementation 'io.grpc:grpc-okhttp:1.62.2'
// implementation 'io.grpc:grpc-protobuf-lite:1.62.2'
// implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0'
// implementation 'org.web3j:crypto:5.0.0'
// implementation 'org.web3j:crypto:4.9.4'
// implementation "net.java.dev.jna:jna:5.14.0@aar"
// api 'com.google.protobuf:protobuf-kotlin-lite:3.22.3'
// api 'org.xmtp:proto-kotlin:3.62.1'
// api 'org.xmtp:proto-kotlin:3.71.0'
}
Original file line number Diff line number Diff line change
Expand Up @@ -655,6 +655,26 @@ class XMTPModule : Module() {
}
}

AsyncFunction("sendEncodedContent") Coroutine { installationId: String, conversationId: String, encodedContentData: List<Int> ->
withContext(Dispatchers.IO) {
logV("sendEncodedContent")
val client = clients[installationId] ?: throw XMTPException("No client")
val conversation = client.findConversation(conversationId)
?: throw XMTPException("no conversation found for $conversationId")
val encodedContentDataBytes =
encodedContentData.foldIndexed(ByteArray(encodedContentData.size)) { i, a, v ->
a.apply {
set(
i,
v.toByte()
)
}
}
val encodedContent = EncodedContent.parseFrom(encodedContentDataBytes)
conversation.send(encodedContent)
}
}

AsyncFunction("sendMessage") Coroutine { installationId: String, id: String, contentJson: String ->
withContext(Dispatchers.IO) {
logV("sendMessage")
Expand Down
8 changes: 4 additions & 4 deletions example/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -448,7 +448,7 @@ PODS:
- SQLCipher/standard (4.5.7):
- SQLCipher/common
- SwiftProtobuf (1.28.2)
- XMTP (3.0.14):
- XMTP (3.0.15):
- Connect-Swift (= 1.0.0)
- CryptoSwift (= 1.8.3)
- CSecp256k1 (~> 0.2)
Expand All @@ -459,7 +459,7 @@ PODS:
- ExpoModulesCore
- MessagePacker
- SQLCipher (= 4.5.7)
- XMTP (= 3.0.14)
- XMTP (= 3.0.15)
- Yoga (1.14.0)

DEPENDENCIES:
Expand Down Expand Up @@ -762,8 +762,8 @@ SPEC CHECKSUMS:
RNSVG: d00c8f91c3cbf6d476451313a18f04d220d4f396
SQLCipher: 5e6bfb47323635c8b657b1b27d25c5f1baf63bf5
SwiftProtobuf: 4dbaffec76a39a8dc5da23b40af1a5dc01a4c02d
XMTP: 3b586fa3703640bb5fec8a64daba9e157d9e5fdc
XMTPReactNative: f3e1cbf80b7278b817bd42982703a95a9250497d
XMTP: 8b0c84096edf74642c5780e4fca9ebbc848fdcf2
XMTPReactNative: fa98630d85a3947eccfde6062916bcf3de9c32e2
Yoga: e71803b4c1fff832ccf9b92541e00f9b873119b9

PODFILE CHECKSUM: 0e6fe50018f34e575d38dc6a1fdf1f99c9596cdd
Expand Down
191 changes: 191 additions & 0 deletions example/src/tests/conversationTests.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { content } from '@xmtp/proto'
import { Wallet } from 'ethers'
import ReactNativeBlobUtil from 'react-native-blob-util'
import RNFS from 'react-native-fs'

import { Test, assert, createClients, delayToPropogate } from './test-utils'
Expand All @@ -8,6 +10,7 @@ import {
Conversation,
ConversationId,
ConversationVersion,
JSContentCodec,
} from '../../../src/index'

export const conversationTests: Test[] = []
Expand All @@ -19,6 +22,194 @@ function test(name: string, perform: () => Promise<boolean>) {
})
}

type EncodedContent = content.EncodedContent
type ContentTypeId = content.ContentTypeId

const { fs } = ReactNativeBlobUtil

const ContentTypeNumber: ContentTypeId = {
authorityId: 'org',
typeId: 'number',
versionMajor: 1,
versionMinor: 0,
}

const ContentTypeNumberWithUndefinedFallback: ContentTypeId = {
authorityId: 'org',
typeId: 'number_undefined_fallback',
versionMajor: 1,
versionMinor: 0,
}

const ContentTypeNumberWithEmptyFallback: ContentTypeId = {
authorityId: 'org',
typeId: 'number_empty_fallback',
versionMajor: 1,
versionMinor: 0,
}

export type NumberRef = {
topNumber: {
bottomNumber: number
}
}

class NumberCodec implements JSContentCodec<NumberRef> {
contentType = ContentTypeNumber

// a completely absurd way of encoding number values
encode(content: NumberRef): EncodedContent {
return {
type: ContentTypeNumber,
parameters: {
test: 'test',
},
content: new TextEncoder().encode(JSON.stringify(content)),
}
}

decode(encodedContent: EncodedContent): NumberRef {
if (encodedContent.parameters.test !== 'test') {
throw new Error(`parameters should parse ${encodedContent.parameters}`)
}
const contentReceived = JSON.parse(
new TextDecoder().decode(encodedContent.content)
) as NumberRef
return contentReceived
}

fallback(content: NumberRef): string | undefined {
return 'a billion'
}
}

class NumberCodecUndefinedFallback extends NumberCodec {
contentType = ContentTypeNumberWithUndefinedFallback
fallback(content: NumberRef): string | undefined {
return undefined
}
}

class NumberCodecEmptyFallback extends NumberCodec {
contentType = ContentTypeNumberWithEmptyFallback
fallback(content: NumberRef): string | undefined {
return ''
}
}

test('register and use custom content types', async () => {
const keyBytes = new Uint8Array([
233, 120, 198, 96, 154, 65, 132, 17, 132, 96, 250, 40, 103, 35, 125, 64,
166, 83, 208, 224, 254, 44, 205, 227, 175, 49, 234, 129, 74, 252, 135, 145,
])
const bob = await Client.createRandom({
env: 'local',
codecs: [new NumberCodec()],
dbEncryptionKey: keyBytes,
})
const alice = await Client.createRandom({
env: 'local',
codecs: [new NumberCodec()],
dbEncryptionKey: keyBytes,
})

bob.register(new NumberCodec())
alice.register(new NumberCodec())

await delayToPropogate()

const bobConvo = await bob.conversations.newConversation(alice.address)
await delayToPropogate()
await bobConvo.send(
{ topNumber: { bottomNumber: 12 } },
{ contentType: ContentTypeNumber }
)

await alice.conversations.syncAllConversations()
const aliceConvo = await alice.conversations.findConversation(bobConvo.id)

const messages = await aliceConvo!.messages()
assert(messages.length === 1, 'did not get messages')

const message = messages[0]
const messageContent = message.content()

assert(
typeof messageContent === 'object' &&
'topNumber' in messageContent &&
messageContent.topNumber.bottomNumber === 12,
'did not get content properly: ' + JSON.stringify(messageContent)
)

return true
})

test('handle fallback types appropriately', async () => {
const keyBytes = new Uint8Array([
233, 120, 198, 96, 154, 65, 132, 17, 132, 96, 250, 40, 103, 35, 125, 64,
166, 83, 208, 224, 254, 44, 205, 227, 175, 49, 234, 129, 74, 252, 135, 145,
])
const bob = await Client.createRandom({
env: 'local',
codecs: [
new NumberCodecEmptyFallback(),
new NumberCodecUndefinedFallback(),
],
dbEncryptionKey: keyBytes,
})
const alice = await Client.createRandom({
env: 'local',
dbEncryptionKey: keyBytes,
})
bob.register(new NumberCodecEmptyFallback())
bob.register(new NumberCodecUndefinedFallback())
const bobConvo = await bob.conversations.newConversation(alice.address)

// @ts-ignore
await bobConvo.send(12, { contentType: ContentTypeNumberWithEmptyFallback })

// @ts-ignore
await bobConvo.send(12, {
contentType: ContentTypeNumberWithUndefinedFallback,
})

await alice.conversations.syncAllConversations()
const aliceConvo = await alice.conversations.findConversation(bobConvo.id)

const messages = await aliceConvo!.messages()
assert(messages.length === 2, 'did not get messages')

const messageUndefinedFallback = messages[0]
const messageWithDefinedFallback = messages[1]

let message1Content = undefined
try {
message1Content = messageUndefinedFallback.content()
} catch {
message1Content = messageUndefinedFallback.fallback
}

assert(
message1Content === undefined,
'did not get content properly when empty fallback: ' +
JSON.stringify(message1Content)
)

let message2Content = undefined
try {
message2Content = messageWithDefinedFallback.content()
} catch {
message2Content = messageWithDefinedFallback.fallback
}

assert(
message2Content === '',
'did not get content properly: ' + JSON.stringify(message2Content)
)

return true
})

test('can find a conversations by id', async () => {
const [alixClient, boClient] = await createClients(2)
const alixGroup = await alixClient.conversations.newGroup([boClient.address])
Expand Down
23 changes: 23 additions & 0 deletions ios/XMTPModule.swift
Original file line number Diff line number Diff line change
Expand Up @@ -721,6 +721,29 @@ public class XMTPModule: Module {
return nil
}
}

AsyncFunction("sendEncodedContent") {
(
installationId: String, conversationId: String,
encodedContentData: [UInt8]
) -> String in
guard
let client = await clientsManager.getClient(key: installationId)
else {
throw Error.noClient
}
guard
let conversation = try client.findConversation(
conversationId: conversationId)
else {
throw Error.conversationNotFound(
"no conversation found for \(conversationId)")
}
let encodedContent = try EncodedContent(
serializedBytes: Data(encodedContentData))

return try await conversation.send(encodedContent: encodedContent)
}

AsyncFunction("sendMessage") {
(installationId: String, id: String, contentJson: String) -> String
Expand Down
2 changes: 1 addition & 1 deletion ios/XMTPReactNative.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ Pod::Spec.new do |s|
s.source_files = "**/*.{h,m,swift}"

s.dependency "MessagePacker"
s.dependency "XMTP", "= 3.0.14"
s.dependency "XMTP", "= 3.0.15"
s.dependency 'CSecp256k1', '~> 0.2'
s.dependency "SQLCipher", "= 4.5.7"
end
30 changes: 30 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { content } from '@xmtp/proto'
import { EventEmitter, NativeModulesProxy } from 'expo-modules-core'

import { Client } from '.'
import XMTPModule from './XMTPModule'
import { Address, InboxId, InstallationId, XMTPEnvironment } from './lib/Client'
import { ConsentRecord, ConsentState, ConsentType } from './lib/ConsentRecord'
import {
ContentCodec,
DecryptedLocalAttachment,
EncryptedLocalAttachment,
} from './lib/ContentCodec'
Expand Down Expand Up @@ -39,6 +41,8 @@ export { StaticAttachmentCodec } from './lib/NativeCodecs/StaticAttachmentCodec'
export { TextCodec } from './lib/NativeCodecs/TextCodec'
export * from './lib/Signer'

const EncodedContent = content.EncodedContent

export function address(): string {
return XMTPModule.address()
}
Expand Down Expand Up @@ -530,6 +534,32 @@ export async function findDmByAddress<
return new Dm(client, dm)
}

export async function sendWithContentType<T>(
installationId: InboxId,
conversationId: ConversationId,
content: T,
codec: ContentCodec<T>
): Promise<MessageId> {
if ('contentKey' in codec) {
const contentJson = JSON.stringify(content)
return await XMTPModule.sendMessage(
installationId,
conversationId,
contentJson
)
} else {
const encodedContent = codec.encode(content)
encodedContent.fallback = codec.fallback(content)
const encodedContentData = EncodedContent.encode(encodedContent).finish()

return await XMTPModule.sendEncodedContent(
installationId,
conversationId,
Array.from(encodedContentData)
)
}
}

export async function sendMessage(
installationId: InstallationId,
conversationId: ConversationId,
Expand Down
Loading

0 comments on commit eda35c7

Please sign in to comment.