Foundation for creating direct message chat...
This commit is contained in:
@@ -6,25 +6,34 @@ import at.torch.compose.database.TorchDatabase
|
||||
import at.torch.compose.database.model.BroadcastNostrEventRequest
|
||||
import at.torch.compose.database.model.ChatMessageBroadcastNostrEventRequestRelation
|
||||
import at.torch.compose.database.model.ChatMessageNostrEventRelation
|
||||
import at.torch.compose.database.model.ChatRoom
|
||||
import at.torch.compose.database.model.MarmotGroupEvent
|
||||
import at.torch.compose.database.model.MarmotInnerEvent
|
||||
import at.torch.compose.database.model.MarmotInnerEvent.Companion.disappearingExpiration
|
||||
import at.torch.compose.database.model.MarmotKeyPackage
|
||||
import at.torch.compose.database.model.NostrEvent
|
||||
import at.torch.compose.database.model.intermdiate.LocalChatRoom
|
||||
import at.torch.compose.extensions.exporterSecret
|
||||
import at.torch.compose.extensions.toHex
|
||||
import at.torch.compose.nostr.Relays
|
||||
import co.touchlab.kermit.Logger
|
||||
import com.vitorpamplona.quartz.marmot.mip01Groups.MarmotGroupData
|
||||
import com.vitorpamplona.quartz.marmot.mip03GroupMessages.GroupEvent
|
||||
import com.vitorpamplona.quartz.marmot.mip03GroupMessages.GroupEventEncryption
|
||||
import com.vitorpamplona.quartz.marmot.mls.codec.TlsReader
|
||||
import com.vitorpamplona.quartz.marmot.mls.group.MlsGroup
|
||||
import com.vitorpamplona.quartz.marmot.mls.messages.MlsKeyPackage
|
||||
import com.vitorpamplona.quartz.marmot.mls.tree.Credential
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync
|
||||
import com.vitorpamplona.quartz.nip40Expiration.expiration
|
||||
import com.vitorpamplona.quartz.nip59Giftwrap.rumors.RumorAssembler
|
||||
import com.vitorpamplona.quartz.utils.RandomInstance
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import kotlin.time.Instant
|
||||
|
||||
@@ -34,6 +43,108 @@ abstract class MarmotOutboundDao(
|
||||
) {
|
||||
val logger = Logger.withTag(TAG)
|
||||
|
||||
@Transaction
|
||||
open suspend fun createMlsDirectMessage(
|
||||
userPublicKey: HexKey,
|
||||
peerPublicKey: HexKey,
|
||||
peerKeyPackage: MarmotKeyPackage,
|
||||
relays: List<String> = Relays.DefaultDMRelayList.map { it.url }
|
||||
): String {
|
||||
// TODO: Might move this logic to a marmotOutboudDao for consistency...
|
||||
val groupId = RandomInstance.bytes(32).toHexKey()
|
||||
val mlsGroup = MlsGroup.create(
|
||||
identity = userPublicKey.hexToByteArray(),
|
||||
initialExtensions = listOf(
|
||||
MarmotGroupData.bootstrap(
|
||||
nostrGroupId = groupId,
|
||||
creatorPubKey = userPublicKey,
|
||||
outboxRelays = relays,
|
||||
).toExtension()
|
||||
)
|
||||
)
|
||||
|
||||
// Invite peer...
|
||||
val keyPackage = MlsKeyPackage.decodeTls(
|
||||
TlsReader(
|
||||
peerKeyPackage.tlsEncodedMarmotKeyPackage
|
||||
)
|
||||
)
|
||||
|
||||
val credential = keyPackage.leafNode.credential
|
||||
require(credential is Credential.Basic) {
|
||||
"KeyPackage must use BasicCredential"
|
||||
}
|
||||
require(credential.identity.toHexKey() == peerPublicKey) {
|
||||
"KeyPackage credential identity does not match memberPubKey"
|
||||
}
|
||||
|
||||
val retainedBefore = mlsGroup.retainedSecrets()
|
||||
val commitResult = mlsGroup.addMember(
|
||||
peerKeyPackage.tlsEncodedMarmotKeyPackage
|
||||
)
|
||||
|
||||
// pushRetainedEpoch(nostrGroupId, retainedBefore)
|
||||
// persistGroup(nostrGroupId)
|
||||
|
||||
// CommitEvent...
|
||||
val encryptedContent = encryptedCommitEvent(
|
||||
mlsGroup,
|
||||
commitBytes = commitResult.framedCommitBytes,
|
||||
exporterKey = commitResult.preCommitExporterSecret
|
||||
)
|
||||
|
||||
val template = GroupEvent.build(
|
||||
encryptedContentBase64 = encryptedContent,
|
||||
nostrGroupId = groupId
|
||||
)
|
||||
|
||||
val ephemeralSigner = NostrSignerInternal(KeyPair())
|
||||
val commitEvent: GroupEvent = ephemeralSigner.sign(template)
|
||||
|
||||
// TODO: Welcome event?
|
||||
// TODO: Save c0mmitEvent nostrEvent and broadcast it...
|
||||
database.nostrEventDao().upsert(
|
||||
NostrEvent(
|
||||
id = commitEvent.id,
|
||||
createdAt = Instant.fromEpochSeconds(commitEvent.createdAt),
|
||||
content = commitEvent.content,
|
||||
sig = commitEvent.sig,
|
||||
tags = commitEvent.tags,
|
||||
kind = commitEvent.kind,
|
||||
pubKey = commitEvent.pubKey
|
||||
)
|
||||
)
|
||||
|
||||
// Save Chat Room
|
||||
database.chatRoomDao().upsert(
|
||||
ChatRoom(
|
||||
id = groupId,
|
||||
userPublicKey = userPublicKey,
|
||||
mlsGroupState = mlsGroup.saveState().encodeTls().toHex(),
|
||||
subject = null,
|
||||
)
|
||||
)
|
||||
|
||||
database.broadcastNostrEventRequestDao().insert(
|
||||
relays.map {
|
||||
BroadcastNostrEventRequest(
|
||||
nostrEventId = commitEvent.id,
|
||||
relayURL = it
|
||||
)
|
||||
}
|
||||
)
|
||||
return groupId
|
||||
}
|
||||
|
||||
private fun encryptedCommitEvent(
|
||||
mlsGroup: MlsGroup,
|
||||
commitBytes: ByteArray,
|
||||
exporterKey: ByteArray? = null,
|
||||
): String {
|
||||
val outerKey = exporterKey ?: mlsGroup.exporterSecret()
|
||||
|
||||
return GroupEventEncryption.encrypt(commitBytes, outerKey)
|
||||
}
|
||||
@Transaction
|
||||
open suspend fun encryptAndSendMarmotInnerEvent(
|
||||
localChatRoom: LocalChatRoom,
|
||||
|
||||
@@ -1,18 +1,26 @@
|
||||
package at.torch.compose.database.repository
|
||||
|
||||
import at.torch.compose.database.model.ChatMessage
|
||||
import at.torch.compose.database.model.ChatRoom
|
||||
import at.torch.compose.database.model.GiftWrapPayload
|
||||
import at.torch.compose.database.model.MarmotInnerEvent
|
||||
import at.torch.compose.database.model.MarmotKeyPackage
|
||||
import at.torch.compose.database.model.Participant
|
||||
import at.torch.compose.database.model.Profile
|
||||
import at.torch.compose.database.model.intermdiate.LocalProfile
|
||||
import at.torch.compose.extensions.toHex
|
||||
import at.torch.compose.nostr.Relays
|
||||
import co.touchlab.kermit.Logger
|
||||
import com.vitorpamplona.quartz.marmot.mip01Groups.MarmotGroupData
|
||||
import com.vitorpamplona.quartz.marmot.mls.codec.TlsReader
|
||||
import com.vitorpamplona.quartz.marmot.mls.group.MlsGroup
|
||||
import com.vitorpamplona.quartz.marmot.mls.group.MlsGroupState
|
||||
import com.vitorpamplona.quartz.marmot.mls.messages.MlsKeyPackage
|
||||
import com.vitorpamplona.quartz.marmot.mls.tree.Credential
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Kind
|
||||
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.crypto.EventHasher
|
||||
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
@@ -23,6 +31,7 @@ import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent
|
||||
import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent
|
||||
import com.vitorpamplona.quartz.nipC7Chats.ChatEvent
|
||||
import com.vitorpamplona.quartz.utils.RandomInstance
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
@@ -113,6 +122,18 @@ class DatabaseChatRepository(
|
||||
return database.profileDao().getProfileByPublicKey(publicKey)
|
||||
}
|
||||
|
||||
override suspend fun createMlsDirectMessage(
|
||||
userPublicKey: HexKey,
|
||||
peerPublicKey: HexKey,
|
||||
peerKeyPackage: MarmotKeyPackage
|
||||
): String {
|
||||
return database.marmotOutboundDao().createMlsDirectMessage(
|
||||
userPublicKey = userPublicKey,
|
||||
peerPublicKey = peerPublicKey,
|
||||
peerKeyPackage = peerKeyPackage
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun sendChatMessage(
|
||||
text: String,
|
||||
localChatRoom: at.torch.compose.database.model.intermdiate.LocalChatRoom,
|
||||
|
||||
@@ -11,6 +11,7 @@ import com.vitorpamplona.quartz.nip01Core.core.Kind
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync
|
||||
import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent
|
||||
import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent
|
||||
import fr.acinq.secp256k1.Hex
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
|
||||
@@ -41,6 +42,12 @@ interface ChatRepository {
|
||||
|
||||
suspend fun getProfileWithPublicKey(publicKey: HexKey): Profile?
|
||||
|
||||
suspend fun createMlsDirectMessage(
|
||||
userPublicKey: HexKey,
|
||||
peerPublicKey: HexKey,
|
||||
peerKeyPackage: MarmotKeyPackage
|
||||
): String
|
||||
|
||||
suspend fun sendChatMessage(
|
||||
text: String,
|
||||
localChatRoom: LocalChatRoom,
|
||||
@@ -102,6 +109,14 @@ interface ChatRepository {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override suspend fun createMlsDirectMessage(
|
||||
userPublicKey: HexKey,
|
||||
peerPublicKey: HexKey,
|
||||
peerKeyPackage: MarmotKeyPackage
|
||||
): String {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override suspend fun sendChatMessage(
|
||||
text: String,
|
||||
localChatRoom: LocalChatRoom,
|
||||
|
||||
@@ -68,7 +68,7 @@ fun ChatRoomDetailScreen(
|
||||
)
|
||||
|
||||
when (val chatRoomDetailUIState = chatRoomDetailViewModel.chatRoomDetailUIState) {
|
||||
ChatRoomDetailUIState.Error -> {
|
||||
is ChatRoomDetailUIState.Error -> {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
@@ -77,7 +77,7 @@ fun ChatRoomDetailScreen(
|
||||
modifier = Modifier.height(50.dp)
|
||||
)
|
||||
Text(
|
||||
text = "Something went wrong",
|
||||
text = chatRoomDetailUIState.message,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,15 +102,26 @@ class ChatRoomDetailViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
val localAccount = nostrRepository.observeProfile(chatRoomId).firstOrNull()
|
||||
val profile = chatRepository.getProfileWithPublicKey(chatRoomId)
|
||||
|
||||
if (localAccount != null) {
|
||||
logger.d("Create chat with: $localAccount")
|
||||
if (profile != null) {
|
||||
logger.d("Create chat with: $profile")
|
||||
// Check if we have a keyPackageEvent...
|
||||
chatRepository.getMarmotKeyPackageForPublicKey(chatRoomId)
|
||||
val keyPackageEvent = chatRepository.getMarmotKeyPackageForPublicKey(chatRoomId)
|
||||
|
||||
if (keyPackageEvent == null) {
|
||||
chatRoomDetailUIState = ChatRoomDetailUIState.Error("Missing Key Package") // TODO: Introduce missing key package UI State...
|
||||
} else {
|
||||
// TODO: Create new chat and invite local
|
||||
chatRepository.createMlsDirectMessage(
|
||||
userPublicKey = activeUserPublicKey,
|
||||
peerPublicKey = chatRoomId,
|
||||
peerKeyPackage = keyPackageEvent
|
||||
)
|
||||
}
|
||||
|
||||
} else {
|
||||
chatRoomDetailUIState = ChatRoomDetailUIState.Error
|
||||
chatRoomDetailUIState = ChatRoomDetailUIState.Error("Couldn't find profile...")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,9 @@ sealed interface ChatRoomDetailUIState {
|
||||
val localChatRoom: at.torch.compose.database.model.intermdiate.LocalChatRoom
|
||||
): ChatRoomDetailUIState
|
||||
|
||||
data object Error: ChatRoomDetailUIState
|
||||
data class Error(
|
||||
val message: String
|
||||
): ChatRoomDetailUIState
|
||||
data object Loading: ChatRoomDetailUIState
|
||||
data object InitiatingNewChat: ChatRoomDetailUIState
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user