Skip to content

Commit 7b484c2

Browse files
authored
Merge pull request #485 from code-payments/feat/reveal-identity
feat: add reveal identity support to chat if previously established
2 parents fa5bb3e + 6fd2280 commit 7b484c2

20 files changed

Lines changed: 545 additions & 214 deletions

File tree

api/src/main/java/com/getcode/db/ConversationDao.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,13 @@ interface ConversationDao {
4545
return hasInteracted(conversationId.base58)
4646
}
4747

48+
@Query("UPDATE conversations SET hasRevealedIdentity = 1 WHERE idBase58 = :conversationId")
49+
suspend fun revealIdentity(conversationId: String)
50+
51+
suspend fun revealIdentity(conversationId: ID) {
52+
revealIdentity(conversationId.base58)
53+
}
54+
4855
// @Query("SELECT EXISTS (SELECT * FROM messages WHERE conversationIdBase58 = :messageId AND content LIKE '%4|%')")
4956
// suspend fun hasRevealedIdentity(messageId: String): Boolean
5057
//

api/src/main/java/com/getcode/mapper/ConversationMapper.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package com.getcode.mapper
22

33
import com.getcode.model.Conversation
44
import com.getcode.model.chat.Chat
5+
import com.getcode.model.chat.self
6+
import com.getcode.network.TipController
57
import com.getcode.network.localized
68
import com.getcode.network.repository.base58
79
import com.getcode.util.resources.ResourceHelper
@@ -12,12 +14,13 @@ class ConversationMapper @Inject constructor(
1214
) : Mapper<Chat, Conversation> {
1315
override fun map(from: Chat): Conversation {
1416

17+
val self = from.self?.identity
1518
val identity = from.members.filterNot { it.isSelf }.firstNotNullOfOrNull { it.identity }
1619

1720
return Conversation(
1821
idBase58 = from.id.base58,
1922
title = from.title.localized(resources),
20-
hasRevealedIdentity = identity != null,
23+
hasRevealedIdentity = self != null,
2124
lastActivity = null, // TODO: ?
2225
user = identity?.username,
2326
userImage = null,

api/src/main/java/com/getcode/model/chat/Platform.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,9 @@ enum class Platform {
1010
operator fun invoke(proto: ChatService.Platform): Platform {
1111
return runCatching { entries[proto.ordinal] }.getOrNull() ?: Unknown
1212
}
13+
14+
fun named(name: String): Platform {
15+
return entries.firstOrNull { it.name.lowercase() == name.lowercase() } ?: Unknown
16+
}
1317
}
1418
}

api/src/main/java/com/getcode/network/ConversationController.kt

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,16 @@ import com.getcode.model.ConversationWithLastPointers
1515
import com.getcode.model.ID
1616
import com.getcode.model.MessageStatus
1717
import com.getcode.model.chat.ChatType
18+
import com.getcode.model.chat.MessageContent
1819
import com.getcode.model.chat.OutgoingMessageContent
20+
import com.getcode.model.chat.Platform
21+
import com.getcode.model.chat.isConversation
1922
import com.getcode.model.chat.selfId
20-
import com.getcode.model.uuid
2123
import com.getcode.network.client.ChatMessageStreamReference
2224
import com.getcode.network.exchange.Exchange
2325
import com.getcode.network.repository.base58
2426
import com.getcode.network.service.ChatServiceV2
2527
import com.getcode.utils.ErrorUtils
26-
import com.getcode.utils.timestamp
2728
import kotlinx.coroutines.CoroutineScope
2829
import kotlinx.coroutines.Dispatchers
2930
import kotlinx.coroutines.flow.Flow
@@ -38,7 +39,7 @@ interface ConversationController {
3839
fun openChatStream(scope: CoroutineScope, conversation: Conversation)
3940
fun closeChatStream()
4041
suspend fun hasInteracted(messageId: ID): Boolean
41-
suspend fun revealIdentity(messageId: ID)
42+
suspend fun revealIdentity(conversationId: ID, platform: Platform, username: String): Result<Unit>
4243
suspend fun advanceReadPointer(conversationId: ID, messageId: ID, status: MessageStatus)
4344
suspend fun sendMessage(conversationId: ID, message: String): Result<ID>
4445
fun conversationPagingData(conversationId: ID): Flow<PagingData<ConversationMessageWithContent>>
@@ -136,6 +137,21 @@ class ConversationStreamController @Inject constructor(
136137
messageWithContentMapper.map(chat.id to it)
137138
}
138139

140+
val identityRevealed = messages
141+
.flatMap { it.contents }
142+
.filterIsInstance<MessageContent.IdentityRevealed>()
143+
.firstOrNull()
144+
.takeIf { chat.isConversation }
145+
146+
if (identityRevealed != null && conversation.user == null) {
147+
scope.launch(Dispatchers.IO) {
148+
db.conversationDao()
149+
.upsertConversations(
150+
conversation.copy(user = identityRevealed.identity.username)
151+
)
152+
}
153+
}
154+
139155
println("chat messages: ${messages.count()}, pointers=${pointers.count()}")
140156

141157
scope.launch(Dispatchers.IO) {
@@ -169,7 +185,19 @@ class ConversationStreamController @Inject constructor(
169185
return false
170186
}
171187

172-
override suspend fun revealIdentity(messageId: ID) {
188+
override suspend fun revealIdentity(conversationId: ID, platform: Platform, username: String): Result<Unit> {
189+
val owner = SessionManager.getOrganizer()?.ownerKeyPair ?: return Result.failure(Throwable("owner not found"))
190+
val chat = historyController.chats.value?.firstOrNull {
191+
it.id == conversationId
192+
} ?: return Result.failure(Throwable("Chat not found"))
193+
194+
val memberId = chat.selfId ?: return Result.failure(Throwable("Not member of chat"))
195+
196+
return chatService.revealIdentity(owner, chat, memberId, platform, username)
197+
.map { }
198+
.onSuccess {
199+
db.conversationDao().revealIdentity(conversationId)
200+
}
173201
}
174202

175203
override suspend fun advanceReadPointer(conversationId: ID, messageId: ID, status: MessageStatus) {

api/src/main/java/com/getcode/network/HistoryController.kt

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ class HistoryController @Inject constructor(
9999
} else {
100100
it
101101
}
102-
}
102+
}?.sortedByDescending { it.lastMessageMillis }
103103
_chats.update { chats }
104104
}
105105

@@ -246,6 +246,21 @@ class HistoryController @Inject constructor(
246246
private suspend fun fetchChatsWithoutMessages(): List<Chat> {
247247
val owner = owner() ?: return emptyList()
248248
val result = client.fetchChats(owner)
249+
.map { chats ->
250+
chats.map { chat ->
251+
// map revealed identity as title if known
252+
if (chat.isConversation) {
253+
val conversation = conversationMapper.map(chat)
254+
if (conversation.user != null) {
255+
chat.copy(title = Title.Localized(conversation.user))
256+
} else {
257+
chat
258+
}
259+
} else {
260+
chat
261+
}
262+
}
263+
}
249264
.onSuccess { result ->
250265
result.filter { it.isConversation }
251266
.let { chats ->
@@ -257,11 +272,8 @@ class HistoryController @Inject constructor(
257272
chats
258273
}
259274
.onEach {
260-
if (db.conversationDao().findConversation(it.id) == null) {
261-
trace("adding conversation for chat ${it.id.description}")
262-
val conversation = conversationMapper.map(it)
263-
db.conversationDao().upsertConversations(conversation)
264-
}
275+
val conversation = conversationMapper.map(it)
276+
db.conversationDao().upsertConversations(conversation)
265277
}
266278
}
267279
return result.getOrNull().orEmpty()

api/src/main/java/com/getcode/network/api/ChatApiV2.kt

Lines changed: 31 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,20 @@
11
package com.getcode.network.api
22

33
import com.codeinc.gen.chat.v2.ChatService
4+
import com.codeinc.gen.chat.v2.ChatService.ChatMemberIdentity
45
import com.codeinc.gen.chat.v2.ChatService.Content
56
import com.codeinc.gen.chat.v2.ChatService.PointerType
7+
import com.codeinc.gen.chat.v2.ChatService.RevealIdentityRequest
8+
import com.codeinc.gen.chat.v2.ChatService.RevealIdentityResponse
69
import com.codeinc.gen.chat.v2.ChatService.SendMessageRequest
710
import com.codeinc.gen.chat.v2.ChatService.SendMessageResponse
811
import com.codeinc.gen.common.v1.Model
912
import com.getcode.ed25519.Ed25519.KeyPair
1013
import com.getcode.model.Cursor
1114
import com.getcode.model.ID
15+
import com.getcode.model.chat.Chat
1216
import com.getcode.model.chat.OutgoingMessageContent
17+
import com.getcode.model.chat.Platform
1318
import com.getcode.model.chat.StartChatRequest
1419
import com.getcode.model.chat.StartChatResponse
1520
import com.getcode.model.description
@@ -225,31 +230,32 @@ class ChatApiV2 @Inject constructor(
225230
.apply { setSignature(sign(owner)) }
226231
.build()
227232

228-
229-
// val observer = object : StreamObserver<SendMessageResponse> {
230-
// override fun onNext(value: SendMessageResponse?) {
231-
// val result = value?.result
232-
// if (result == null) {
233-
// trace(
234-
// message = "SendMessage Server sent empty message. This is unexpected.",
235-
// type = TraceType.Error
236-
// )
237-
// onResult(Result.failure(Throwable()))
238-
// return
239-
// }
240-
//
241-
// onResult(Res)
242-
// }
243-
//
244-
// override fun onError(t: Throwable?) {
245-
// TODO("Not yet implemented")
246-
// }
247-
//
248-
// override fun onCompleted() {
249-
// TODO("Not yet implemented")
250-
// }
251-
//
252-
// }
253233
api.sendMessage(request, observer)
254234
}
235+
236+
fun revealIdentity(
237+
owner: KeyPair,
238+
chatId: ID,
239+
memberId: UUID,
240+
platform: Platform,
241+
username: String,
242+
observer: StreamObserver<RevealIdentityResponse>
243+
) {
244+
val request = RevealIdentityRequest.newBuilder()
245+
.setChatId(ChatId.newBuilder()
246+
.setValue(chatId.toByteArray().toByteString())
247+
)
248+
.setMemberId(ChatService.ChatMemberId.newBuilder()
249+
.setValue(memberId.bytes.toByteString())
250+
)
251+
.setIdentity(ChatMemberIdentity.newBuilder()
252+
.setPlatformValue(platform.ordinal)
253+
.setUsername(username)
254+
)
255+
.setOwner(owner.publicKeyBytes.toSolanaAccount())
256+
.apply { setSignature(sign(owner)) }
257+
.build()
258+
259+
api.revealIdentity(request, observer)
260+
}
255261
}

api/src/main/java/com/getcode/network/repository/SendTransactionRepository.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,12 @@ class SendTransactionRepository @Inject constructor(
2727
private lateinit var organizer: Organizer
2828
private lateinit var owner: Ed25519.KeyPair
2929
private lateinit var payload: CodePayload
30-
lateinit var payloadData: List<Byte>
30+
private lateinit var payloadData: List<Byte>
3131

3232
private lateinit var rendezvousKey: Ed25519.KeyPair
3333
private var receivingAccount: PublicKey? = null
3434

35-
fun init(amount: KinAmount, organizer: Organizer, owner: Ed25519.KeyPair) {
35+
fun init(amount: KinAmount, organizer: Organizer, owner: Ed25519.KeyPair): List<Byte> {
3636
this.amount = amount
3737
this.organizer = organizer
3838
this.owner = owner
@@ -46,6 +46,8 @@ class SendTransactionRepository @Inject constructor(
4646
this.payloadData = payload.codeData.toList()
4747
this.rendezvousKey = payload.rendezvous
4848
this.receivingAccount = null
49+
50+
return payloadData
4951
}
5052

5153
fun startTransaction(): Flowable<IntentMetadata> {

api/src/main/java/com/getcode/network/service/ChatServiceV2.kt

Lines changed: 84 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import com.codeinc.gen.chat.v2.ChatService
44
import com.codeinc.gen.chat.v2.ChatService.ChatMemberId
55
import com.codeinc.gen.chat.v2.ChatService.OpenChatEventStream
66
import com.codeinc.gen.chat.v2.ChatService.PointerType
7+
import com.codeinc.gen.chat.v2.ChatService.RevealIdentityResponse
78
import com.codeinc.gen.chat.v2.ChatService.SendMessageResponse
89
import com.codeinc.gen.chat.v2.ChatService.StreamChatEventsRequest
910
import com.codeinc.gen.chat.v2.ChatService.StreamChatEventsResponse
@@ -21,6 +22,7 @@ import com.getcode.model.chat.Chat
2122
import com.getcode.model.chat.ChatStreamEventUpdate
2223
import com.getcode.model.chat.ChatType
2324
import com.getcode.model.chat.OutgoingMessageContent
25+
import com.getcode.model.chat.Platform
2426
import com.getcode.model.description
2527
import com.getcode.network.api.ChatApiV2
2628
import com.getcode.network.client.ChatMessageStreamReference
@@ -237,8 +239,8 @@ class ChatServiceV2 @Inject constructor(
237239

238240
val type = when (status) {
239241
MessageStatus.Sent -> PointerType.SENT
240-
MessageStatus.Delivered -> PointerType.DELIVERED
241-
MessageStatus.Read -> PointerType.READ
242+
MessageStatus.Delivered -> PointerType.DELIVERED
243+
MessageStatus.Read -> PointerType.READ
242244
MessageStatus.Unknown -> return Result.failure(Throwable("Can't update a pointer to Unknown"))
243245
}
244246
return try {
@@ -531,6 +533,86 @@ class ChatServiceV2 @Inject constructor(
531533
Timber.e(t = error)
532534
Result.failure(error)
533535
}
536+
537+
else -> {
538+
val error = Throwable("Error: Unknown")
539+
Timber.e(t = error)
540+
Result.failure(error)
541+
}
542+
}
543+
544+
cont.resume(result)
545+
}
546+
547+
override fun onError(t: Throwable?) {
548+
val error = t ?: Throwable("Error: Hit a snag")
549+
ErrorUtils.handleError(error)
550+
cont.resume(Result.failure(error))
551+
}
552+
553+
override fun onCompleted() {
554+
555+
}
556+
557+
}
558+
)
559+
} catch (e: Exception) {
560+
ErrorUtils.handleError(e)
561+
cont.resume(Result.failure(e))
562+
}
563+
}
564+
565+
suspend fun revealIdentity(
566+
owner: KeyPair,
567+
chat: Chat,
568+
memberId: UUID,
569+
platform: Platform,
570+
username: String,
571+
): Result<ChatMessage> = suspendCancellableCoroutine { cont ->
572+
val chatId = chat.id
573+
try {
574+
api.revealIdentity(
575+
owner,
576+
chatId,
577+
memberId,
578+
platform,
579+
username,
580+
observer = object : StreamObserver<RevealIdentityResponse> {
581+
override fun onNext(value: RevealIdentityResponse?) {
582+
val requestResult = value?.result
583+
if (requestResult == null) {
584+
trace(
585+
message = "Chat SendMessage Server returned empty message. This is unexpected.",
586+
type = TraceType.Error
587+
)
588+
return
589+
}
590+
591+
val result = when (requestResult) {
592+
RevealIdentityResponse.Result.OK -> {
593+
trace("Chat message sent =: ${value.message.messageId.value.toList().description}")
594+
val message = messageMapper.map(chat to value.message)
595+
Result.success(message)
596+
}
597+
598+
RevealIdentityResponse.Result.DENIED -> {
599+
val error = Throwable("Error: Send Message: Denied")
600+
Timber.e(t = error)
601+
Result.failure(error)
602+
}
603+
604+
RevealIdentityResponse.Result.CHAT_NOT_FOUND -> {
605+
val error = Throwable("Error: Send Message: chat not found $chatId")
606+
Timber.e(t = error)
607+
Result.failure(error)
608+
}
609+
610+
RevealIdentityResponse.Result.UNRECOGNIZED -> {
611+
val error = Throwable("Error: Send Message: Unrecognized request.")
612+
Timber.e(t = error)
613+
Result.failure(error)
614+
}
615+
534616
else -> {
535617
val error = Throwable("Error: Unknown")
536618
Timber.e(t = error)

0 commit comments

Comments
 (0)