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

In line code comment documentation #142

Merged
merged 2 commits into from
Dec 16, 2023
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
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
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
Loading