Skip to content

Commit

Permalink
In line code comment documentation (#142)
Browse files Browse the repository at this point in the history
Creation of the documentation for the know code

Co-authored-by: Naomi Plasterer <[email protected]>
  • Loading branch information
giovasdistillery and nplasterer authored Dec 16, 2023
1 parent 1cf6d5e commit 1eb7b1f
Show file tree
Hide file tree
Showing 10 changed files with 305 additions and 60 deletions.
5 changes: 5 additions & 0 deletions library/src/main/java/org/xmtp/android/library/ApiClient.kt
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,11 @@ data class GRPCApiClient(
return client.query(request, headers = headers)
}

/**
* This is a helper for paginating through a full query.
* It yields all the envelopes in the query using the paging info
* from the prior response to fetch the next page.
*/
override suspend fun envelopes(topic: String, pagination: Pagination?): List<Envelope> {
var envelopes: MutableList<Envelope> = mutableListOf()
var hasNextPage = true
Expand Down
50 changes: 38 additions & 12 deletions library/src/main/java/org/xmtp/android/library/Client.kt
Original file line number Diff line number Diff line change
Expand Up @@ -114,17 +114,17 @@ class Client() {
EnvelopeBuilder.buildFromTopic(
topic = Topic.userPrivateStoreKeyBundle(v1Key.walletAddress),
timestamp = Date(),
message = encryptedKeys.toByteArray()
)
)
message = encryptedKeys.toByteArray(),
),
),
)
}

fun canMessage(peerAddress: String, options: ClientOptions? = null): Boolean {
val clientOptions = options ?: ClientOptions()
val api = GRPCApiClient(
environment = clientOptions.api.env,
secure = clientOptions.api.isSecure
secure = clientOptions.api.isSecure,
)
return runBlocking {
val topics = api.queryTopic(Topic.contact(peerAddress)).envelopesList
Expand Down Expand Up @@ -184,6 +184,20 @@ class Client() {
return Client(address = address, privateKeyBundleV1 = v1Bundle, apiClient = apiClient)
}

/**
* This authenticates using [account] acquired from network storage
* encrypted using the [wallet].
*
* e.g. this might be called the first time a user logs in from a new device.
* The next time they launch the app they can [buildFromV1Key].
*
* If there are stored keys then this asks the [wallet] to
* [encrypted] so that we can decrypt the stored [keys].
*
* If there are no stored keys then this generates a new identityKey
* and asks the [wallet] to both [createIdentity] and enable Identity Saving
* so we can then store it encrypted for the next time.
*/
private suspend fun loadOrCreateKeys(
account: SigningKey,
apiClient: ApiClient,
Expand All @@ -201,6 +215,11 @@ class Client() {
}
}

/**
* This authenticates with [keys] directly received.
* e.g. this might be called on subsequent app launches once we
* have already stored the keys from a previous session.
*/
private suspend fun loadPrivateKeys(
account: SigningKey,
apiClient: ApiClient,
Expand Down Expand Up @@ -291,7 +310,7 @@ class Client() {
val authorized = AuthorizedIdentity(
address = address,
authorized = privateKeyBundleV1.identityKey.publicKey,
identity = privateKeyBundleV1.identityKey
identity = privateKeyBundleV1.identityKey,
)
val authToken = authorized.createAuthToken()
apiClient.setAuthToken(authToken)
Expand All @@ -312,14 +331,14 @@ class Client() {
val gson = GsonBuilder().create()
val v2Export = gson.fromJson(
conversationData.toString(StandardCharsets.UTF_8),
ConversationV2Export::class.java
ConversationV2Export::class.java,
)
return try {
importV2Conversation(export = v2Export)
} catch (e: java.lang.Exception) {
val v1Export = gson.fromJson(
conversationData.toString(StandardCharsets.UTF_8),
ConversationV1Export::class.java
ConversationV1Export::class.java,
)
try {
importV1Conversation(export = v1Export)
Expand All @@ -337,12 +356,12 @@ class Client() {
keyMaterial = keyMaterial,
context = InvitationV1ContextBuilder.buildFromConversation(
conversationId = export.context?.conversationId ?: "",
metadata = export.context?.metadata ?: mapOf()
metadata = export.context?.metadata ?: mapOf(),
),
peerAddress = export.peerAddress,
client = this,
header = SealedInvitationHeaderV1.newBuilder().build()
)
header = SealedInvitationHeaderV1.newBuilder().build(),
),
)
}

Expand All @@ -358,11 +377,18 @@ class Client() {
ConversationV1(
client = this,
peerAddress = export.peerAddress,
sentAt = sentAt
)
sentAt = sentAt,
),
)
}

/**
* Whether or not we can send messages to [address].
* @param peerAddress is the address of the client that you want to send messages
*
* @return false when [peerAddress] has never signed up for XMTP
* or when the message is addressed to the sender (no self-messaging).
*/
fun canMessage(peerAddress: String): Boolean {
return runBlocking { query(Topic.contact(peerAddress)).envelopesList.size > 0 }
}
Expand Down
49 changes: 41 additions & 8 deletions library/src/main/java/org/xmtp/android/library/Conversation.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,20 @@ import org.xmtp.proto.message.contents.Invitation.InvitationV1.Aes256gcmHkdfsha2
import org.xmtp.android.library.messages.DecryptedMessage
import java.util.Date

/**
* This represents an ongoing conversation.
* It can be provided to [Client] to [messages] and [send].
* The [Client] also allows you to [streamMessages] from this [Conversation].
*
* It attempts to give uniform shape to v1 and v2 conversations.
*/
sealed class Conversation {
data class V1(val conversationV1: ConversationV1) : Conversation()
data class V2(val conversationV2: ConversationV2) : Conversation()

enum class Version {
V1,
V2
}
enum class Version { V1, V2 }

// This indicates whether this a v1 or v2 conversation.
val version: Version
get() {
return when (this) {
Expand All @@ -30,6 +35,7 @@ sealed class Conversation {
}
}

// When the conversation was first created.
val createdAt: Date
get() {
return when (this) {
Expand All @@ -38,6 +44,7 @@ sealed class Conversation {
}
}

// This is the address of the peer that I am talking to.
val peerAddress: String
get() {
return when (this) {
Expand All @@ -46,6 +53,8 @@ sealed class Conversation {
}
}

// This distinctly identifies between two addresses.
// Note: this will be empty for older v1 conversations.
val conversationId: String?
get() {
return when (this) {
Expand All @@ -70,6 +79,11 @@ sealed class Conversation {
return client.contacts.consentList.state(address = peerAddress)
}

/**
* This method is to create a TopicData object
* @return [TopicData] that contains all the information about the Topic, the conversation
* context and the necessary encryption data for it.
*/
fun toTopicData(): TopicData {
val data = TopicData.newBuilder()
.setCreatedNs(createdAt.time * 1_000_000)
Expand All @@ -82,8 +96,8 @@ sealed class Conversation {
.setContext(conversationV2.context)
.setAes256GcmHkdfSha256(
Aes256gcmHkdfsha256.newBuilder()
.setKeyMaterial(conversationV2.keyMaterial.toByteString())
)
.setKeyMaterial(conversationV2.keyMaterial.toByteString()),
),
).build()
}
}
Expand Down Expand Up @@ -149,6 +163,7 @@ sealed class Conversation {
return client.address
}

// Is the topic of the conversation depending of the version
val topic: String
get() {
return when (this) {
Expand All @@ -157,6 +172,19 @@ sealed class Conversation {
}
}

/**
* This lists messages sent to the [Conversation].
* @param before initial date to filter
* @param after final date to create a range of dates and filter
* @param limit is the number of result that will be returned
* @param direction is the way of srting the information, by default is descending, you can
* know more about it in class [MessageApiOuterClass].
* @see MessageApiOuterClass.SortDirection
* @return The list of messages sent. If [before] or [after] are specified then this will only list messages
* sent at or [after] and at or [before].
* If [limit] is specified then results are pulled in pages of that size.
* If [direction] is specified then that will control the sort order of te messages.
*/
fun messages(
limit: Int? = null,
before: Date? = null,
Expand All @@ -168,15 +196,15 @@ sealed class Conversation {
limit = limit,
before = before,
after = after,
direction = direction
direction = direction,
)

is V2 ->
conversationV2.messages(
limit = limit,
before = before,
after = after,
direction = direction
direction = direction,
)
}
}
Expand All @@ -202,6 +230,7 @@ sealed class Conversation {
}
}

// Get the client according to the version
val client: Client
get() {
return when (this) {
Expand All @@ -210,6 +239,10 @@ sealed class Conversation {
}
}

/**
* This exposes a stream of new messages sent to the [Conversation].
* @return Stream of messages according to the version
*/
fun streamMessages(): Flow<DecodedMessage> {
return when (this) {
is V1 -> conversationV1.streamMessages()
Expand Down
55 changes: 49 additions & 6 deletions library/src/main/java/org/xmtp/android/library/ConversationV1.kt
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,32 @@ data class ConversationV1(
val topic: Topic
get() = Topic.directMessageV1(client.address, peerAddress)

/**
* Get the stream of all messages of the current [Client]
* @return Flow object of [DecodedMessage] that represents all the messages of the
* current [Client] as userInvite and userIntro
* @see Conversations.streamAllMessages
*/
fun streamMessages(): Flow<DecodedMessage> = flow {
client.subscribe(listOf(topic.description)).collect {
emit(decode(envelope = it))
}
}

/**
* This lists messages sent to the [Conversation].
* @param before initial date to filter
* @param after final date to create a range of dates and filter
* @param limit is the number of result that will be returned
* @param direction is the way of srting the information, by default is descending, you can
* know more about it in class [MessageApiOuterClass].
* @see MessageApiOuterClass.SortDirection
* @return The list of messages sent. If [before] or [after] are specified then this will only list messages
* sent at or [after] and at or [before].
* If [limit] is specified then results are pulled in pages of that size.
* If [direction] is specified then that will control the sort order of te messages.
* @see Conversation.messages
*/
fun messages(
limit: Int? = null,
before: Date? = null,
Expand All @@ -57,6 +77,19 @@ data class ConversationV1(
}
}

/**
* This lists decrypted messages sent to the [Conversation].
* @param before initial date to filter
* @param after final date to create a range of dates and filter
* @param limit is the number of result that will be returned
* @param direction is the way of srting the information, by default is descending, you can
* know more about it in class [MessageApiOuterClass].
* @see MessageApiOuterClass.SortDirection
* @return The list of messages sent. If [before] or [after] are specified then this will only list messages
* sent at or [after] and at or [before].
* If [limit] is specified then results are pulled in pages of that size.
* If [direction] is specified then that will control the sort order of te messages.
*/
fun decryptedMessages(
limit: Int? = null,
before: Date? = null,
Expand All @@ -69,13 +102,18 @@ data class ConversationV1(
val envelopes = runBlocking {
client.apiClient.envelopes(
topic = Topic.directMessageV1(client.address, peerAddress).description,
pagination = pagination
pagination = pagination,
)
}

return envelopes.map { decrypt(it) }
}

/**
* This decrypts a message
* @param envelope Object that contains all the information of the encrypted message
* @return [DecryptedMessage] object
*/
fun decrypt(envelope: Envelope): DecryptedMessage {
try {
val message = Message.parseFrom(envelope.message)
Expand All @@ -88,13 +126,18 @@ data class ConversationV1(
id = generateId(envelope),
encodedContent = encodedMessage,
senderAddress = header.sender.walletAddress,
sentAt = message.v1.sentAt
sentAt = message.v1.sentAt,
)
} catch (e: Exception) {
throw XMTPException("Error decrypting message", e)
}
}

/**
* This encrypts a message
* @param envelope Object that contains all the information of the decrypted message
* @return [DecodedMessage] object
*/
fun decode(envelope: Envelope): DecodedMessage {
try {
val decryptedMessage = decrypt(envelope)
Expand All @@ -105,7 +148,7 @@ data class ConversationV1(
topic = envelope.contentTopic,
encodedContent = decryptedMessage.encodedContent,
senderAddress = decryptedMessage.senderAddress,
sent = decryptedMessage.sentAt
sent = decryptedMessage.sentAt,
)
} catch (e: Exception) {
throw XMTPException("Error decoding message", e)
Expand Down Expand Up @@ -193,7 +236,7 @@ data class ConversationV1(
sender = client.privateKeyBundleV1,
recipient = recipient,
message = encodedContent.toByteArray(),
timestamp = date
timestamp = date,
)

val isEphemeral: Boolean = options != null && options.ephemeral
Expand All @@ -202,7 +245,7 @@ data class ConversationV1(
EnvelopeBuilder.buildFromString(
topic = if (isEphemeral) ephemeralTopic else topic.description,
timestamp = date,
message = MessageBuilder.buildFromMessageV1(v1 = message).toByteArray()
message = MessageBuilder.buildFromMessageV1(v1 = message).toByteArray(),
)

val envelopes = mutableListOf(env)
Expand All @@ -215,7 +258,7 @@ data class ConversationV1(
env.toBuilder().apply {
contentTopic = Topic.userIntro(client.address).description
}.build(),
)
),
)
client.contacts.hasIntroduced[peerAddress] = true
}
Expand Down
Loading

0 comments on commit 1eb7b1f

Please sign in to comment.