Add deterministic id on MLS related models
This commit is contained in:
1 parent
0957f710d9
commit
aad97bdd9e
27 files changed
+633
-397
No files matched your search
@@ -2,7 +2,7 @@
|
|||||||
"formatVersion": 1,
|
"formatVersion": 1,
|
||||||
"database": {
|
"database": {
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"identityHash": "f33937ebefe679fe03b71b3930472139",
|
"identityHash": "8dd191b21e518a19f39b50a5f5b4a432",
|
||||||
"entities": [
|
"entities": [
|
||||||
{
|
{
|
||||||
"tableName": "BroadcastNostrEventReceipt",
|
"tableName": "BroadcastNostrEventReceipt",
|
||||||
@@ -282,7 +282,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"tableName": "ChatMessage",
|
"tableName": "ChatMessage",
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `senderPublicKey` TEXT NOT NULL, `isUserMessage` INTEGER NOT NULL, `giftWrapPayloadId` INTEGER NOT NULL, `chatRoomId` TEXT NOT NULL, `replyToMessageId` INTEGER, `quotedMessageId` INTEGER, `content` TEXT NOT NULL, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL, `savedAt` INTEGER NOT NULL, `viewedAt` INTEGER, `deletedAt` INTEGER, FOREIGN KEY(`chatRoomId`) REFERENCES `ChatRoom`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`giftWrapPayloadId`) REFERENCES `GiftWrapPayload`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `senderPublicKey` TEXT NOT NULL, `isUserMessage` INTEGER NOT NULL, `giftWrapPayloadId` TEXT NOT NULL, `chatRoomId` TEXT NOT NULL, `replyToMessageId` INTEGER, `quotedMessageId` INTEGER, `content` TEXT NOT NULL, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL, `savedAt` INTEGER NOT NULL, `viewedAt` INTEGER, `deletedAt` INTEGER, FOREIGN KEY(`chatRoomId`) REFERENCES `ChatRoom`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`giftWrapPayloadId`) REFERENCES `GiftWrapPayload`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"fieldPath": "id",
|
"fieldPath": "id",
|
||||||
@@ -305,7 +305,7 @@
|
|||||||
{
|
{
|
||||||
"fieldPath": "giftWrapPayloadId",
|
"fieldPath": "giftWrapPayloadId",
|
||||||
"columnName": "giftWrapPayloadId",
|
"columnName": "giftWrapPayloadId",
|
||||||
"affinity": "INTEGER",
|
"affinity": "TEXT",
|
||||||
"notNull": true
|
"notNull": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -608,7 +608,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"tableName": "ChatRoom",
|
"tableName": "ChatRoom",
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `userPublicKey` TEXT NOT NULL, `subject` TEXT, `mlsGroupState` TEXT, `initialGiftWrapPayloadId` INTEGER, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL, `savedAt` INTEGER NOT NULL, `viewedAt` INTEGER, `deletedAt` INTEGER, PRIMARY KEY(`id`), FOREIGN KEY(`userPublicKey`) REFERENCES `Profile`(`publicKey`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`initialGiftWrapPayloadId`) REFERENCES `GiftWrapPayload`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `userPublicKey` TEXT NOT NULL, `subject` TEXT, `mlsGroupState` TEXT, `initialGiftWrapPayloadId` TEXT, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL, `savedAt` INTEGER NOT NULL, `viewedAt` INTEGER, `deletedAt` INTEGER, PRIMARY KEY(`id`), FOREIGN KEY(`userPublicKey`) REFERENCES `Profile`(`publicKey`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`initialGiftWrapPayloadId`) REFERENCES `GiftWrapPayload`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"fieldPath": "id",
|
"fieldPath": "id",
|
||||||
@@ -635,7 +635,7 @@
|
|||||||
{
|
{
|
||||||
"fieldPath": "initialGiftWrapPayloadId",
|
"fieldPath": "initialGiftWrapPayloadId",
|
||||||
"columnName": "initialGiftWrapPayloadId",
|
"columnName": "initialGiftWrapPayloadId",
|
||||||
"affinity": "INTEGER"
|
"affinity": "TEXT"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldPath": "createdAt",
|
"fieldPath": "createdAt",
|
||||||
@@ -893,12 +893,12 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"tableName": "GiftWrapPayload",
|
"tableName": "GiftWrapPayload",
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `publicKey` TEXT NOT NULL, `chatRoomId` TEXT NOT NULL, `kind` INTEGER NOT NULL, `tags` TEXT NOT NULL, `content` TEXT NOT NULL, `quotedEventId` TEXT, `giftWrapSealId` TEXT, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL, `savedAt` INTEGER NOT NULL, `viewedAt` INTEGER, `deletedAt` INTEGER, FOREIGN KEY(`giftWrapSealId`) REFERENCES `GiftWrapSeal`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `publicKey` TEXT NOT NULL, `chatRoomId` TEXT NOT NULL, `kind` INTEGER NOT NULL, `tags` TEXT NOT NULL, `content` TEXT NOT NULL, `quotedEventId` TEXT, `giftWrapSealId` TEXT, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL, `savedAt` INTEGER NOT NULL, `viewedAt` INTEGER, `deletedAt` INTEGER, PRIMARY KEY(`id`), FOREIGN KEY(`giftWrapSealId`) REFERENCES `GiftWrapSeal`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"fieldPath": "id",
|
"fieldPath": "id",
|
||||||
"columnName": "id",
|
"columnName": "id",
|
||||||
"affinity": "INTEGER",
|
"affinity": "TEXT",
|
||||||
"notNull": true
|
"notNull": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -971,7 +971,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"primaryKey": {
|
"primaryKey": {
|
||||||
"autoGenerate": true,
|
"autoGenerate": false,
|
||||||
"columnNames": [
|
"columnNames": [
|
||||||
"id"
|
"id"
|
||||||
]
|
]
|
||||||
@@ -1117,12 +1117,12 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"tableName": "MarmotKeyPackage",
|
"tableName": "MarmotKeyPackage",
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `publicKey` TEXT NOT NULL, `tlsEncodedMarmotKeyPackage` BLOB NOT NULL, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL, `savedAt` INTEGER NOT NULL, `deletedAt` INTEGER, `broadcastedAt` INTEGER, FOREIGN KEY(`publicKey`) REFERENCES `Profile`(`publicKey`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `publicKey` TEXT NOT NULL, `tlsEncodedMarmotKeyPackage` BLOB NOT NULL, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL, `savedAt` INTEGER NOT NULL, `deletedAt` INTEGER, `broadcastedAt` INTEGER, PRIMARY KEY(`id`), FOREIGN KEY(`publicKey`) REFERENCES `Profile`(`publicKey`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"fieldPath": "id",
|
"fieldPath": "id",
|
||||||
"columnName": "id",
|
"columnName": "id",
|
||||||
"affinity": "INTEGER",
|
"affinity": "TEXT",
|
||||||
"notNull": true
|
"notNull": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -1167,7 +1167,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"primaryKey": {
|
"primaryKey": {
|
||||||
"autoGenerate": true,
|
"autoGenerate": false,
|
||||||
"columnNames": [
|
"columnNames": [
|
||||||
"id"
|
"id"
|
||||||
]
|
]
|
||||||
@@ -1188,12 +1188,12 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"tableName": "MarmotKeyPackageBundle",
|
"tableName": "MarmotKeyPackageBundle",
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `publicKey` TEXT NOT NULL, `tlsEncodedMarmotKeyPackage` TEXT NOT NULL, `ncryptsecInitPrivateKey` TEXT NOT NULL, `ncryptsecEncryptionPrivateKey` TEXT NOT NULL, `ncryptsecSignaturePrivateKey` TEXT NOT NULL, `consumed` INTEGER NOT NULL, `rotated` INTEGER NOT NULL, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL, `savedAt` INTEGER NOT NULL, `deletedAt` INTEGER, `broadcastedAt` INTEGER)",
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `publicKey` TEXT NOT NULL, `tlsEncodedMarmotKeyPackage` BLOB NOT NULL, `ncryptsecInitPrivateKey` TEXT NOT NULL, `ncryptsecEncryptionPrivateKey` TEXT NOT NULL, `ncryptsecSignaturePrivateKey` TEXT NOT NULL, `consumed` INTEGER NOT NULL, `rotated` INTEGER NOT NULL, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL, `savedAt` INTEGER NOT NULL, `deletedAt` INTEGER, `broadcastedAt` INTEGER, PRIMARY KEY(`id`))",
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"fieldPath": "id",
|
"fieldPath": "id",
|
||||||
"columnName": "id",
|
"columnName": "id",
|
||||||
"affinity": "INTEGER",
|
"affinity": "TEXT",
|
||||||
"notNull": true
|
"notNull": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -1205,7 +1205,7 @@
|
|||||||
{
|
{
|
||||||
"fieldPath": "tlsEncodedMarmotKeyPackage",
|
"fieldPath": "tlsEncodedMarmotKeyPackage",
|
||||||
"columnName": "tlsEncodedMarmotKeyPackage",
|
"columnName": "tlsEncodedMarmotKeyPackage",
|
||||||
"affinity": "TEXT",
|
"affinity": "BLOB",
|
||||||
"notNull": true
|
"notNull": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -1268,7 +1268,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"primaryKey": {
|
"primaryKey": {
|
||||||
"autoGenerate": true,
|
"autoGenerate": false,
|
||||||
"columnNames": [
|
"columnNames": [
|
||||||
"id"
|
"id"
|
||||||
]
|
]
|
||||||
@@ -3044,7 +3044,7 @@
|
|||||||
],
|
],
|
||||||
"setupQueries": [
|
"setupQueries": [
|
||||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'f33937ebefe679fe03b71b3930472139')"
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '8dd191b21e518a19f39b50a5f5b4a432')"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4,6 +4,7 @@ import androidx.room3.Dao
|
|||||||
import androidx.room3.Query
|
import androidx.room3.Query
|
||||||
import androidx.room3.Transaction
|
import androidx.room3.Transaction
|
||||||
import androidx.room3.Upsert
|
import androidx.room3.Upsert
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
@@ -17,7 +18,7 @@ interface ChatMessageDao {
|
|||||||
fun getChatMessagesByChatRoomId(chatRoomId: String): List<at.torch.compose.database.model.intermdiate.LocalChatMessage>
|
fun getChatMessagesByChatRoomId(chatRoomId: String): List<at.torch.compose.database.model.intermdiate.LocalChatMessage>
|
||||||
|
|
||||||
@Query("SELECT * FROM ChatMessage WHERE giftWrapPayloadId = :giftWrapPayloadId ORDER BY createdAt DESC")
|
@Query("SELECT * FROM ChatMessage WHERE giftWrapPayloadId = :giftWrapPayloadId ORDER BY createdAt DESC")
|
||||||
fun getChatMessagesByGiftWrapPayloadId(giftWrapPayloadId: Long): at.torch.compose.database.model.ChatMessage?
|
fun getChatMessagesByGiftWrapPayloadId(giftWrapPayloadId: HexKey): at.torch.compose.database.model.ChatMessage?
|
||||||
|
|
||||||
@Upsert
|
@Upsert
|
||||||
suspend fun upsert(chatMessage: at.torch.compose.database.model.ChatMessage): Long
|
suspend fun upsert(chatMessage: at.torch.compose.database.model.ChatMessage): Long
|
||||||
|
|||||||
+1
-1
@@ -14,5 +14,5 @@ interface GiftWrapPayloadDao {
|
|||||||
fun observeUnsealedGiftWrapPayloads(publicKey: String): Flow<at.torch.compose.database.model.GiftWrapPayload?>
|
fun observeUnsealedGiftWrapPayloads(publicKey: String): Flow<at.torch.compose.database.model.GiftWrapPayload?>
|
||||||
|
|
||||||
@Upsert
|
@Upsert
|
||||||
suspend fun upsert(giftWrapPayload: at.torch.compose.database.model.GiftWrapPayload): Long
|
suspend fun upsert(giftWrapPayload: at.torch.compose.database.model.GiftWrapPayload)
|
||||||
}
|
}
|
||||||
+2
-2
@@ -7,8 +7,8 @@ import at.torch.compose.database.model.MarmotKeyPackageBundle
|
|||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
interface MarmotKeyPackageBundleDao {
|
interface MarmotKeyPackageBundleDao {
|
||||||
@Query("SELECT * FROM MarmotKeyPackageBundle WHERE id = :keyPackageEventId ORDER BY createdAt DESC")
|
@Query("SELECT * FROM MarmotKeyPackageBundle WHERE id = :id ORDER BY createdAt DESC")
|
||||||
fun getMarmotKeyPackageBundleByKeyPackageEventIdId(keyPackageEventId: String): MarmotKeyPackageBundle?
|
fun getMarmotKeyPackageBundleById(id: String): MarmotKeyPackageBundle?
|
||||||
|
|
||||||
@Upsert
|
@Upsert
|
||||||
suspend fun upsert(marmotKeyPackageBundle: MarmotKeyPackageBundle)
|
suspend fun upsert(marmotKeyPackageBundle: MarmotKeyPackageBundle)
|
||||||
|
|||||||
@@ -18,10 +18,8 @@ import at.torch.compose.database.model.types.SynchronizationFilter
|
|||||||
import at.torch.compose.exceptions.GiftWrapImpersonationException
|
import at.torch.compose.exceptions.GiftWrapImpersonationException
|
||||||
import at.torch.compose.exceptions.GiftWrapSealDecryptionException
|
import at.torch.compose.exceptions.GiftWrapSealDecryptionException
|
||||||
import at.torch.compose.exceptions.GiftWrapUnsealException
|
import at.torch.compose.exceptions.GiftWrapUnsealException
|
||||||
import at.torch.compose.exceptions.MarmotMissingKeyPackageEventIdException
|
import at.torch.compose.exceptions.MarmotMissingKeyPackageBundleException
|
||||||
import at.torch.compose.exceptions.MarmotMissingNostrGroupDataExtension
|
import at.torch.compose.exceptions.MarmotWelcomeEventMissingKeyPackageEventIdException
|
||||||
import at.torch.compose.exceptions.MarmotNoMatchingKeyPackageBundleException
|
|
||||||
import at.torch.compose.exceptions.MarmotWelcomeMismatchedGroupIdException
|
|
||||||
import at.torch.compose.extensions.toHex
|
import at.torch.compose.extensions.toHex
|
||||||
import co.touchlab.kermit.Logger
|
import co.touchlab.kermit.Logger
|
||||||
import com.vitorpamplona.quartz.marmot.mip00KeyPackages.KeyPackageEvent
|
import com.vitorpamplona.quartz.marmot.mip00KeyPackages.KeyPackageEvent
|
||||||
@@ -39,6 +37,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
|||||||
import com.vitorpamplona.quartz.nip01Core.tags.events.taggedEvents
|
import com.vitorpamplona.quartz.nip01Core.tags.events.taggedEvents
|
||||||
import com.vitorpamplona.quartz.nip01Core.tags.people.taggedUsers
|
import com.vitorpamplona.quartz.nip01Core.tags.people.taggedUsers
|
||||||
import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent
|
import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent
|
||||||
|
import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent
|
||||||
import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent
|
import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent
|
||||||
import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent
|
import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent
|
||||||
import kotlin.io.encoding.Base64
|
import kotlin.io.encoding.Base64
|
||||||
@@ -127,7 +126,8 @@ abstract class NostrDao(
|
|||||||
logger.i("This is a placeholder profile might need to get synced...: ${profile.publicKey}")
|
logger.i("This is a placeholder profile might need to get synced...: ${profile.publicKey}")
|
||||||
// Setup the sync here...
|
// Setup the sync here...
|
||||||
} else {
|
} else {
|
||||||
return
|
// TODO: Check if event got indexed...
|
||||||
|
// return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -388,257 +388,397 @@ abstract class NostrDao(
|
|||||||
logger.e("Impersonation", giftWrapImpersonation)
|
logger.e("Impersonation", giftWrapImpersonation)
|
||||||
throw giftWrapImpersonation // Once this is thrown the database transaction fails and nothing gets saved to the DB...
|
throw giftWrapImpersonation // Once this is thrown the database transaction fails and nothing gets saved to the DB...
|
||||||
}
|
}
|
||||||
val result = database.giftWrapPayloadDao().upsert(
|
|
||||||
|
database.giftWrapPayloadDao().upsert(
|
||||||
decryptedGiftWrapPayload
|
decryptedGiftWrapPayload
|
||||||
)
|
)
|
||||||
|
|
||||||
val giftWrapPayload = if (result != -1L) {
|
if (decryptedGiftWrapPayload.kind == ChatMessageEvent.KIND) {
|
||||||
decryptedGiftWrapPayload.copy(
|
logger.d("chatMessageEvent: $decryptedGiftWrapPayload")
|
||||||
id = result
|
// Handle normal nip-17 messages...
|
||||||
|
val localChatRoom = database.chatRoomDao().findChatRoomById(
|
||||||
|
decryptedGiftWrapPayload.chatRoomId
|
||||||
)
|
)
|
||||||
} else {
|
|
||||||
decryptedGiftWrapPayload
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Handle MLS message/groups
|
if (localChatRoom == null) {
|
||||||
if (giftWrapPayload.kind == WelcomeEvent.KIND) {
|
val userPublicKey = activeKeyPair.pubKey.toHex()
|
||||||
logger.d("Welcome Event")
|
database.chatRoomDao().upsert(
|
||||||
|
ChatRoom(
|
||||||
|
id = decryptedGiftWrapPayload.chatRoomId,
|
||||||
|
userPublicKey = userPublicKey,
|
||||||
|
subject = decryptedGiftWrapPayload.parseSubject(),
|
||||||
|
createdAt = decryptedGiftWrapPayload.createdAt,
|
||||||
|
initialGiftWrapPayloadId = decryptedGiftWrapPayload.id,
|
||||||
|
mlsGroupState = null
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Add participants...
|
||||||
|
val participants = decryptedGiftWrapPayload.participantPTags(
|
||||||
|
NormalizedRelayUrl(relayURL) // TODO: Get relayURL for publicKey profile...
|
||||||
|
).map {
|
||||||
|
Participant(
|
||||||
|
participantPublicKey = it.pubKey,
|
||||||
|
chatRoomId = decryptedGiftWrapPayload.chatRoomId,
|
||||||
|
relayHint = it.relayHint?.url
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sync missing participant profiles...
|
||||||
|
participants.forEach { participant ->
|
||||||
|
val giftWrapParticipantProfile =
|
||||||
|
database.profileDao()
|
||||||
|
.getProfileByPublicKey(participant.participantPublicKey)
|
||||||
|
|
||||||
|
if (giftWrapParticipantProfile == null) {
|
||||||
|
// Save a placeholder
|
||||||
|
database.profileDao().insertPlaceholderProfile(
|
||||||
|
Profile(
|
||||||
|
displayName = "LOADING...",
|
||||||
|
publicKey = participant.participantPublicKey,
|
||||||
|
createdAt = GENESIS_AT,
|
||||||
|
nostrEventId = nostrEvent.id, // Will get overwriting by sync,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val recommendRelayUrl =
|
||||||
|
giftWrapMessage.receiverRelayHit
|
||||||
|
|
||||||
|
if (recommendRelayUrl != null) {
|
||||||
|
if (profilePublicKeysToSync[recommendRelayUrl] == null) {
|
||||||
|
profilePublicKeysToSync[recommendRelayUrl] =
|
||||||
|
mutableSetOf()
|
||||||
|
}
|
||||||
|
profilePublicKeysToSync[recommendRelayUrl]?.add(
|
||||||
|
participant.participantPublicKey
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
if (profilePublicKeysToSync[relayURL] == null) {
|
||||||
|
profilePublicKeysToSync[relayURL] =
|
||||||
|
mutableSetOf()
|
||||||
|
}
|
||||||
|
profilePublicKeysToSync[relayURL]?.add(
|
||||||
|
participant.participantPublicKey
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
database.participantDao().upsert(
|
||||||
|
participants
|
||||||
|
)
|
||||||
|
|
||||||
|
participants.filter { it.participantPublicKey != userPublicKey }
|
||||||
|
.forEach { participant ->
|
||||||
|
val publicKey = participant.participantPublicKey
|
||||||
|
|
||||||
|
val chatMessageRelayListEvent =
|
||||||
|
database.nostrEventDao()
|
||||||
|
.getAuthoredNostrEvents(
|
||||||
|
kinds = arrayOf(
|
||||||
|
ChatMessageRelayListEvent.KIND
|
||||||
|
),
|
||||||
|
authors = arrayOf(
|
||||||
|
publicKey
|
||||||
|
),
|
||||||
|
since = GENESIS_AT,
|
||||||
|
limit = 5
|
||||||
|
).firstOrNull()?.let { nostrEvent ->
|
||||||
|
if (nostrEvent.kind != ChatMessageRelayListEvent.KIND) {
|
||||||
|
logger.e("getAuthoredNostrEvents returned invalid ChatMessageRelayListEvent for ${publicKey}: $nostrEvent")
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
ChatMessageRelayListEvent(
|
||||||
|
id = nostrEvent.id,
|
||||||
|
tags = nostrEvent.tags,
|
||||||
|
pubKey = nostrEvent.pubKey,
|
||||||
|
content = nostrEvent.content,
|
||||||
|
createdAt = nostrEvent.createdAt.epochSeconds,
|
||||||
|
sig = nostrEvent.sig
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chatMessageRelayListEvent != null) {
|
||||||
|
// Sync messages from this relay that were sent by us
|
||||||
|
val synchronizationFilter =
|
||||||
|
SynchronizationFilter(
|
||||||
|
kinds = arrayOf(
|
||||||
|
GiftWrapEvent.KIND,
|
||||||
|
),
|
||||||
|
authors = arrayOf(
|
||||||
|
userPublicKey
|
||||||
|
),
|
||||||
|
tags = mapOf(
|
||||||
|
Pair(
|
||||||
|
"p",
|
||||||
|
listOf(participant.participantPublicKey)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
limit = 50
|
||||||
|
)
|
||||||
|
database.negentropySynchronizeRequestDao()
|
||||||
|
.insert(
|
||||||
|
chatMessageRelayListEvent.relays()
|
||||||
|
.map { normalizedRelayUrl ->
|
||||||
|
NegentropySynchronizeRequest(
|
||||||
|
id = NegentropySynchronizeRequest.computeId(
|
||||||
|
relayURL = normalizedRelayUrl.url,
|
||||||
|
synchronizationFilter = synchronizationFilter
|
||||||
|
),
|
||||||
|
purpose = "sent-messages",
|
||||||
|
synchronizationFilter = synchronizationFilter,
|
||||||
|
relayURL = normalizedRelayUrl.url,
|
||||||
|
level = 0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
logger.w("We don't have a chatMessageRelayListEvent for the pubkey $publicKey")
|
||||||
|
|
||||||
|
// Sync ChatMessageRelayListEvent publicKey...
|
||||||
|
// TODO: Get relayHint form participant...
|
||||||
|
if (profilePublicKeysToSync.containsKey(
|
||||||
|
relayURL
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
profilePublicKeysToSync[relayURL] =
|
||||||
|
mutableSetOf()
|
||||||
|
}
|
||||||
|
profilePublicKeysToSync[relayURL]?.add(
|
||||||
|
participant.participantPublicKey
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
decryptedGiftWrapPayload.parseSubject()?.let { subject ->
|
||||||
|
database.chatRoomDao().upsert(
|
||||||
|
localChatRoom.chatRoom.copy(
|
||||||
|
subject = subject,
|
||||||
|
updatedAt = decryptedGiftWrapPayload.createdAt
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if (decryptedGiftWrapPayload.kind == WelcomeEvent.KIND) {
|
||||||
|
|
||||||
val welcomeEvent = WelcomeEvent(
|
val welcomeEvent = WelcomeEvent(
|
||||||
id = "", // TODO: Need the ID as string in decryptedGiftWrapPayload...
|
id = decryptedGiftWrapPayload.id, // TODO: Need the ID as string in decryptedGiftWrapPayload...
|
||||||
pubKey = decryptedGiftWrapPayload.publicKey,
|
pubKey = decryptedGiftWrapPayload.publicKey,
|
||||||
content = decryptedGiftWrapPayload.content,
|
content = decryptedGiftWrapPayload.content,
|
||||||
createdAt = decryptedGiftWrapPayload.createdAt.epochSeconds,
|
createdAt = decryptedGiftWrapPayload.createdAt.epochSeconds,
|
||||||
tags = decryptedGiftWrapPayload.tags,
|
tags = decryptedGiftWrapPayload.tags,
|
||||||
sig = ""
|
sig = ""
|
||||||
)
|
)
|
||||||
|
logger.d("welcomeEvent: $welcomeEvent")
|
||||||
|
|
||||||
welcomeEvent.nostrGroupId()?.let { chatRoomId ->
|
// TODO: Find keypackage and decrypt welcome...
|
||||||
val localChatRoom = database.chatRoomDao().findChatRoomById(
|
// TODO: Get keyPackage from
|
||||||
chatRoomId
|
val keyPackageEventId = welcomeEvent.keyPackageEventId() ?: throw MarmotWelcomeEventMissingKeyPackageEventIdException(
|
||||||
|
"Couldn't find keyPackageEventId for $welcomeEvent"
|
||||||
|
)
|
||||||
|
val marmotKeyPackageBundle = database.marmotKeyPackageBundleDao().getMarmotKeyPackageBundleById(
|
||||||
|
keyPackageEventId
|
||||||
|
) ?: throw MarmotMissingKeyPackageBundleException(
|
||||||
|
"Couldn't find marmotKeyPackageBundle for $welcomeEvent"
|
||||||
|
)
|
||||||
|
|
||||||
|
val welcomeBytes = Base64.decode(
|
||||||
|
welcomeEvent.welcomeBase64()
|
||||||
|
)
|
||||||
|
|
||||||
|
val initPrivateKey = marmotKeyPackageBundle.ncryptsecInitPrivateKey.hexToByteArray() // TODO: Decrypt
|
||||||
|
val signaturePrivateKey = marmotKeyPackageBundle.ncryptsecSignaturePrivateKey.hexToByteArray() // TODO: Decrypt
|
||||||
|
val encryptionPrivateKey = marmotKeyPackageBundle.ncryptsecEncryptionPrivateKey.hexToByteArray() // TODO: Decrypt
|
||||||
|
|
||||||
|
val tlsReader = TlsReader(
|
||||||
|
data = marmotKeyPackageBundle.tlsEncodedMarmotKeyPackage
|
||||||
|
)
|
||||||
|
|
||||||
|
val group = MlsGroup.processWelcome(
|
||||||
|
welcomeBytes,
|
||||||
|
bundle = KeyPackageBundle(
|
||||||
|
keyPackage = MlsKeyPackage.decodeTls(
|
||||||
|
tlsReader
|
||||||
|
),
|
||||||
|
initPrivateKey = initPrivateKey,
|
||||||
|
signaturePrivateKey = signaturePrivateKey,
|
||||||
|
encryptionPrivateKey = encryptionPrivateKey
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val nostrGroupId = group.groupId.toHex()
|
||||||
|
|
||||||
|
val localChatRoom = database.chatRoomDao().findChatRoomById(
|
||||||
|
nostrGroupId
|
||||||
|
)
|
||||||
|
|
||||||
|
if (localChatRoom == null) {
|
||||||
|
|
||||||
|
logger.d("processWelcome: welcomeBytes=${welcomeBytes.size}B looking up KeyPackage by ref=${keyPackageEventId.take(8)}…")
|
||||||
|
|
||||||
|
|
||||||
|
// TODO: Save the chat room We can now s
|
||||||
|
val userPublicKey = activeKeyPair.pubKey.toHex()
|
||||||
|
database.chatRoomDao().upsert(
|
||||||
|
ChatRoom(
|
||||||
|
id = nostrGroupId,
|
||||||
|
userPublicKey = userPublicKey,
|
||||||
|
subject = group.currentMarmotData()?.name,
|
||||||
|
initialGiftWrapPayloadId = decryptedGiftWrapPayload.id,
|
||||||
|
createdAt = decryptedGiftWrapPayload.createdAt,
|
||||||
|
mlsGroupState = group.saveState().encodeTls().toHex()
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if (localChatRoom == null) {
|
database.marmotKeyPackageBundleDao().upsert(
|
||||||
|
marmotKeyPackageBundle.copy(
|
||||||
val welcomeBytes = Base64.decode(
|
consumed = true
|
||||||
welcomeEvent.welcomeBase64()
|
|
||||||
)
|
)
|
||||||
val keyPackageEventId = welcomeEvent.keyPackageEventId()
|
)
|
||||||
if (keyPackageEventId == null) {
|
|
||||||
throw MarmotMissingKeyPackageEventIdException(
|
// TODO: Add participants... by processing group.members()
|
||||||
"WelcomeEvent ${welcomeEvent.id} missing KeyPackage event ID tag"
|
val normalizedRelayUrl = NormalizedRelayUrl(relayURL)
|
||||||
|
|
||||||
|
val participants = group.members().mapNotNull { (leafIndex, leafNode) ->
|
||||||
|
val pubkey =
|
||||||
|
when (val cred = leafNode.credential) {
|
||||||
|
is Credential.Basic -> cred.identity.toHexKey()
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
if (pubkey != null) {
|
||||||
|
Participant(
|
||||||
|
participantPublicKey = pubkey,
|
||||||
|
chatRoomId = nostrGroupId,
|
||||||
|
relayHint = normalizedRelayUrl.url
|
||||||
)
|
)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
logger.d("processWelcome: welcomeBytes=${welcomeBytes.size}B looking up KeyPackage by ref=${keyPackageEventId.take(8)}…")
|
// Sync missing participant profiles...
|
||||||
|
participants.forEach { participant ->
|
||||||
|
val giftWrapParticipantProfile = database.profileDao().getProfileByPublicKey(participant.participantPublicKey)
|
||||||
|
|
||||||
// TODO: Check if we have already joined group with keyPackage
|
if (giftWrapParticipantProfile == null) {
|
||||||
val marmotKeyPackageBundle = database.marmotKeyPackageBundleDao().getMarmotKeyPackageBundleByKeyPackageEventIdId(
|
// Save a placeholder
|
||||||
keyPackageEventId
|
database.profileDao().insertPlaceholderProfile(
|
||||||
)
|
Profile(
|
||||||
|
displayName = "LOADING...",
|
||||||
if (marmotKeyPackageBundle == null) {
|
publicKey = participant.participantPublicKey,
|
||||||
// We were unable to find the keyPackageBundle for this invite...
|
createdAt = GENESIS_AT,
|
||||||
throw MarmotNoMatchingKeyPackageBundleException(
|
nostrEventId = nostrEvent.id, // Will get overwriting by sync,
|
||||||
"NO matching KeyPackageBundle for eventId=${keyPackageEventId.take(8)}…"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val initPrivateKey = marmotKeyPackageBundle.ncryptsecInitPrivateKey.hexToByteArray() // TODO: Decrypt
|
|
||||||
val signaturePrivateKey = marmotKeyPackageBundle.ncryptsecSignaturePrivateKey.hexToByteArray() // TODO: Decrypt
|
|
||||||
val encryptionPrivateKey = marmotKeyPackageBundle.ncryptsecEncryptionPrivateKey.hexToByteArray() // TODO: Decrypt
|
|
||||||
|
|
||||||
val tlsReader = TlsReader(
|
|
||||||
data = marmotKeyPackageBundle.tlsEncodedMarmotKeyPackage.hexToByteArray()
|
|
||||||
)
|
|
||||||
|
|
||||||
val group = MlsGroup.processWelcome(
|
|
||||||
welcomeBytes,
|
|
||||||
bundle = KeyPackageBundle(
|
|
||||||
keyPackage = MlsKeyPackage.decodeTls(
|
|
||||||
tlsReader
|
|
||||||
),
|
|
||||||
initPrivateKey = initPrivateKey,
|
|
||||||
signaturePrivateKey = signaturePrivateKey,
|
|
||||||
encryptionPrivateKey = encryptionPrivateKey
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
val derivedId = group.currentMarmotData()?.nostrGroupId
|
|
||||||
|
|
||||||
if (derivedId == null) {
|
|
||||||
throw MarmotMissingNostrGroupDataExtension(
|
|
||||||
"The WelcomeEvent ${welcomeEvent.id} is missing the nostrGroupData extension"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (derivedId != chatRoomId) {
|
|
||||||
throw MarmotWelcomeMismatchedGroupIdException(
|
|
||||||
"The hint group Id $chatRoomId doesn't match the derived group id $derivedId"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Save the chat room We can now s
|
|
||||||
val userPublicKey = activeKeyPair.pubKey.toHex()
|
|
||||||
database.chatRoomDao().upsert(
|
|
||||||
ChatRoom(
|
|
||||||
id = chatRoomId,
|
|
||||||
userPublicKey = userPublicKey,
|
|
||||||
subject = group.currentMarmotData()?.name,
|
|
||||||
initialGiftWrapPayloadId = giftWrapPayload.id,
|
|
||||||
createdAt = decryptedGiftWrapPayload.createdAt,
|
|
||||||
mlsGroupState = group.saveState().encodeTls().toHex()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
database.marmotKeyPackageBundleDao().upsert(
|
|
||||||
marmotKeyPackageBundle.copy(
|
|
||||||
consumed = true
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
// TODO: Add participants... by processing group.members()
|
|
||||||
val normalizedRelayUrl = NormalizedRelayUrl(relayURL)
|
|
||||||
|
|
||||||
val participants = group.members().mapNotNull { (leafIndex, leafNode) ->
|
|
||||||
val pubkey =
|
|
||||||
when (val cred = leafNode.credential) {
|
|
||||||
is Credential.Basic -> cred.identity.toHexKey()
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
if (pubkey != null) {
|
|
||||||
Participant(
|
|
||||||
participantPublicKey = pubkey,
|
|
||||||
chatRoomId = chatRoomId,
|
|
||||||
relayHint = normalizedRelayUrl.url
|
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val recommendRelayUrl = giftWrapMessage.receiverRelayHit
|
||||||
|
|
||||||
|
if (recommendRelayUrl != null) {
|
||||||
|
if (profilePublicKeysToSync[recommendRelayUrl] == null) {
|
||||||
|
profilePublicKeysToSync[recommendRelayUrl] = mutableSetOf()
|
||||||
|
}
|
||||||
|
profilePublicKeysToSync[recommendRelayUrl]?.add(participant.participantPublicKey)
|
||||||
} else {
|
} else {
|
||||||
null
|
if (profilePublicKeysToSync[relayURL] == null) {
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sync missing participant profiles...
|
|
||||||
participants.forEach { participant ->
|
|
||||||
val giftWrapParticipantProfile = database.profileDao().getProfileByPublicKey(participant.participantPublicKey)
|
|
||||||
|
|
||||||
if (giftWrapParticipantProfile == null) {
|
|
||||||
// Save a placeholder
|
|
||||||
database.profileDao().insertPlaceholderProfile(
|
|
||||||
Profile(
|
|
||||||
displayName = "LOADING...",
|
|
||||||
publicKey = participant.participantPublicKey,
|
|
||||||
createdAt = GENESIS_AT,
|
|
||||||
nostrEventId = nostrEvent.id, // Will get overwriting by sync,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
val recommendRelayUrl = giftWrapMessage.receiverRelayHit
|
|
||||||
|
|
||||||
if (recommendRelayUrl != null) {
|
|
||||||
if (profilePublicKeysToSync[recommendRelayUrl] == null) {
|
|
||||||
profilePublicKeysToSync[recommendRelayUrl] = mutableSetOf()
|
|
||||||
}
|
|
||||||
profilePublicKeysToSync[recommendRelayUrl]?.add(participant.participantPublicKey)
|
|
||||||
} else {
|
|
||||||
if (profilePublicKeysToSync[relayURL] == null) {
|
|
||||||
profilePublicKeysToSync[relayURL] = mutableSetOf()
|
|
||||||
}
|
|
||||||
profilePublicKeysToSync[relayURL]?.add(
|
|
||||||
participant.participantPublicKey
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
database.participantDao().upsert(
|
|
||||||
participants
|
|
||||||
)
|
|
||||||
|
|
||||||
participants.filter { it.participantPublicKey != userPublicKey }.forEach { participant ->
|
|
||||||
val publicKey = participant.participantPublicKey
|
|
||||||
|
|
||||||
val chatMessageRelayListEvent = database.nostrEventDao().getAuthoredNostrEvents(
|
|
||||||
kinds = arrayOf(
|
|
||||||
ChatMessageRelayListEvent.KIND
|
|
||||||
),
|
|
||||||
authors = arrayOf(
|
|
||||||
publicKey
|
|
||||||
),
|
|
||||||
since = GENESIS_AT,
|
|
||||||
limit = 5
|
|
||||||
).firstOrNull()?.let { nostrEvent ->
|
|
||||||
if (nostrEvent.kind != ChatMessageRelayListEvent.KIND) {
|
|
||||||
logger.e("getAuthoredNostrEvents returned invalid ChatMessageRelayListEvent for ${publicKey}: $nostrEvent")
|
|
||||||
null
|
|
||||||
} else {
|
|
||||||
ChatMessageRelayListEvent(
|
|
||||||
id = nostrEvent.id,
|
|
||||||
tags = nostrEvent.tags,
|
|
||||||
pubKey = nostrEvent.pubKey,
|
|
||||||
content = nostrEvent.content,
|
|
||||||
createdAt = nostrEvent.createdAt.epochSeconds,
|
|
||||||
sig = nostrEvent.sig
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (chatMessageRelayListEvent != null) {
|
|
||||||
// Sync messages from this relay that were sent by us
|
|
||||||
val synchronizationFilter = SynchronizationFilter(
|
|
||||||
kinds = arrayOf(
|
|
||||||
GiftWrapEvent.KIND,
|
|
||||||
),
|
|
||||||
authors = arrayOf(
|
|
||||||
userPublicKey
|
|
||||||
),
|
|
||||||
tags = mapOf(
|
|
||||||
Pair("p", listOf(participant.participantPublicKey))
|
|
||||||
),
|
|
||||||
limit = 50
|
|
||||||
)
|
|
||||||
database.negentropySynchronizeRequestDao().insert(
|
|
||||||
chatMessageRelayListEvent.relays().map { normalizedRelayUrl ->
|
|
||||||
NegentropySynchronizeRequest(
|
|
||||||
id = NegentropySynchronizeRequest.computeId(
|
|
||||||
relayURL = normalizedRelayUrl.url,
|
|
||||||
synchronizationFilter = synchronizationFilter
|
|
||||||
),
|
|
||||||
purpose = "sent-messages",
|
|
||||||
synchronizationFilter = synchronizationFilter,
|
|
||||||
relayURL = normalizedRelayUrl.url,
|
|
||||||
level = 0
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
logger.w("We don't have a chatMessageRelayListEvent for the pubkey $publicKey")
|
|
||||||
|
|
||||||
// Sync ChatMessageRelayListEvent publicKey...
|
|
||||||
// TODO: Get relayHint form participant...
|
|
||||||
if (profilePublicKeysToSync.containsKey(relayURL)) {
|
|
||||||
profilePublicKeysToSync[relayURL] = mutableSetOf()
|
profilePublicKeysToSync[relayURL] = mutableSetOf()
|
||||||
}
|
}
|
||||||
profilePublicKeysToSync[relayURL]?.add(participant.participantPublicKey)
|
profilePublicKeysToSync[relayURL]?.add(
|
||||||
|
participant.participantPublicKey
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Where is the decoded welcome message...
|
database.participantDao().upsert(
|
||||||
// TODO: Sync GroupEvents/Messages...
|
participants
|
||||||
database.chatMessageDao().upsert(
|
|
||||||
ChatMessage(
|
|
||||||
giftWrapPayloadId = giftWrapPayload.id,
|
|
||||||
senderPublicKey = giftWrapPayload.publicKey,
|
|
||||||
isUserMessage = activeKeyPair.pubKey.toHex() == giftWrapPayload.publicKey,
|
|
||||||
chatRoomId = chatRoomId,
|
|
||||||
createdAt = giftWrapPayload.createdAt,
|
|
||||||
content = "Coming soon.", // TODO: Figure out what to do here...
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
participants.filter { it.participantPublicKey != userPublicKey }.forEach { participant ->
|
||||||
|
val publicKey = participant.participantPublicKey
|
||||||
|
|
||||||
|
val chatMessageRelayListEvent = database.nostrEventDao().getAuthoredNostrEvents(
|
||||||
|
kinds = arrayOf(
|
||||||
|
ChatMessageRelayListEvent.KIND
|
||||||
|
),
|
||||||
|
authors = arrayOf(
|
||||||
|
publicKey
|
||||||
|
),
|
||||||
|
since = GENESIS_AT,
|
||||||
|
limit = 5
|
||||||
|
).firstOrNull()?.let { nostrEvent ->
|
||||||
|
if (nostrEvent.kind != ChatMessageRelayListEvent.KIND) {
|
||||||
|
logger.e("getAuthoredNostrEvents returned invalid ChatMessageRelayListEvent for ${publicKey}: $nostrEvent")
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
ChatMessageRelayListEvent(
|
||||||
|
id = nostrEvent.id,
|
||||||
|
tags = nostrEvent.tags,
|
||||||
|
pubKey = nostrEvent.pubKey,
|
||||||
|
content = nostrEvent.content,
|
||||||
|
createdAt = nostrEvent.createdAt.epochSeconds,
|
||||||
|
sig = nostrEvent.sig
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chatMessageRelayListEvent != null) {
|
||||||
|
// Sync messages from this relay that were sent by us
|
||||||
|
val synchronizationFilter = SynchronizationFilter(
|
||||||
|
kinds = arrayOf(
|
||||||
|
GiftWrapEvent.KIND,
|
||||||
|
),
|
||||||
|
authors = arrayOf(
|
||||||
|
userPublicKey
|
||||||
|
),
|
||||||
|
tags = mapOf(
|
||||||
|
Pair("p", listOf(participant.participantPublicKey))
|
||||||
|
),
|
||||||
|
limit = 50
|
||||||
|
)
|
||||||
|
database.negentropySynchronizeRequestDao().insert(
|
||||||
|
chatMessageRelayListEvent.relays().map { normalizedRelayUrl ->
|
||||||
|
NegentropySynchronizeRequest(
|
||||||
|
id = NegentropySynchronizeRequest.computeId(
|
||||||
|
relayURL = normalizedRelayUrl.url,
|
||||||
|
synchronizationFilter = synchronizationFilter
|
||||||
|
),
|
||||||
|
purpose = "sent-messages",
|
||||||
|
synchronizationFilter = synchronizationFilter,
|
||||||
|
relayURL = normalizedRelayUrl.url,
|
||||||
|
level = 0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
logger.w("We don't have a chatMessageRelayListEvent for the pubkey $publicKey")
|
||||||
|
|
||||||
|
// Sync ChatMessageRelayListEvent publicKey...
|
||||||
|
// TODO: Get relayHint form participant...
|
||||||
|
if (profilePublicKeysToSync.containsKey(relayURL)) {
|
||||||
|
profilePublicKeysToSync[relayURL] = mutableSetOf()
|
||||||
|
}
|
||||||
|
profilePublicKeysToSync[relayURL]?.add(participant.participantPublicKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Where is the decoded welcome message...
|
||||||
|
// TODO: Sync GroupEvents/Messages...
|
||||||
|
database.chatMessageDao().upsert(
|
||||||
|
ChatMessage(
|
||||||
|
giftWrapPayloadId = decryptedGiftWrapPayload.id,
|
||||||
|
senderPublicKey = decryptedGiftWrapPayload.publicKey,
|
||||||
|
isUserMessage = activeKeyPair.pubKey.toHex() == decryptedGiftWrapPayload.publicKey,
|
||||||
|
chatRoomId = nostrGroupId,
|
||||||
|
createdAt = decryptedGiftWrapPayload.createdAt,
|
||||||
|
content = "Coming soon.", // TODO: Figure out what to do here...
|
||||||
|
)
|
||||||
|
)
|
||||||
// TODO: Check encoding...
|
// TODO: Check encoding...
|
||||||
} else {
|
} else {
|
||||||
logger.w("Unsupported event: $giftWrapPayload")
|
logger.w("Unsupported event: $decryptedGiftWrapPayload")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import at.torch.compose.database.model.traits.LocalStoreEntity
|
|||||||
import at.torch.compose.database.model.traits.SoftDeletableEntity
|
import at.torch.compose.database.model.traits.SoftDeletableEntity
|
||||||
import at.torch.compose.database.model.traits.TimestampedEntity
|
import at.torch.compose.database.model.traits.TimestampedEntity
|
||||||
import at.torch.compose.database.model.traits.UserViewableEntity
|
import at.torch.compose.database.model.traits.UserViewableEntity
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||||
import kotlin.time.Clock
|
import kotlin.time.Clock
|
||||||
import kotlin.time.Instant
|
import kotlin.time.Instant
|
||||||
|
|
||||||
@@ -33,7 +34,7 @@ data class ChatMessage(
|
|||||||
val senderPublicKey: String,
|
val senderPublicKey: String,
|
||||||
val isUserMessage: Boolean,
|
val isUserMessage: Boolean,
|
||||||
|
|
||||||
val giftWrapPayloadId: Long,
|
val giftWrapPayloadId: HexKey,
|
||||||
val chatRoomId: String,
|
val chatRoomId: String,
|
||||||
|
|
||||||
val replyToMessageId: Long? = null,
|
val replyToMessageId: Long? = null,
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ data class ChatRoom(
|
|||||||
// TODO: Add a LastChatMessageRelation
|
// TODO: Add a LastChatMessageRelation
|
||||||
// val lastChatMessageId: String? = null,
|
// val lastChatMessageId: String? = null,
|
||||||
// Might want to focus on the other last messages...
|
// Might want to focus on the other last messages...
|
||||||
val initialGiftWrapPayloadId: Long? = null,
|
val initialGiftWrapPayloadId: HexKey? = null,
|
||||||
|
|
||||||
override val createdAt: Instant = Clock.System.now(),
|
override val createdAt: Instant = Clock.System.now(),
|
||||||
override val updatedAt: Instant = createdAt,
|
override val updatedAt: Instant = createdAt,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import androidx.room3.ForeignKey
|
|||||||
import androidx.room3.Ignore
|
import androidx.room3.Ignore
|
||||||
import androidx.room3.PrimaryKey
|
import androidx.room3.PrimaryKey
|
||||||
import co.touchlab.kermit.Logger
|
import co.touchlab.kermit.Logger
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.Kind
|
import com.vitorpamplona.quartz.nip01Core.core.Kind
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||||
import com.vitorpamplona.quartz.nip01Core.tags.people.PTag
|
import com.vitorpamplona.quartz.nip01Core.tags.people.PTag
|
||||||
@@ -24,8 +25,8 @@ import kotlin.time.Instant
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
data class GiftWrapPayload( // TODO: Rename this to GiftWrapPayload...
|
data class GiftWrapPayload( // TODO: Rename this to GiftWrapPayload...
|
||||||
@PrimaryKey(autoGenerate = true)
|
@PrimaryKey
|
||||||
val id: Long = 0,
|
val id: HexKey,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sender Public Key
|
* Sender Public Key
|
||||||
@@ -34,7 +35,7 @@ data class GiftWrapPayload( // TODO: Rename this to GiftWrapPayload...
|
|||||||
|
|
||||||
val chatRoomId: String,
|
val chatRoomId: String,
|
||||||
|
|
||||||
/**
|
/**`
|
||||||
* Payload Event Kind
|
* Payload Event Kind
|
||||||
*/
|
*/
|
||||||
val kind: Kind,
|
val kind: Kind,
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ data class GiftWrapSeal(
|
|||||||
logger.d("giftWrapPayload: ${giftWrapPayload.toJson()}")
|
logger.d("giftWrapPayload: ${giftWrapPayload.toJson()}")
|
||||||
|
|
||||||
val giftWrapPayload = GiftWrapPayload(
|
val giftWrapPayload = GiftWrapPayload(
|
||||||
|
id = id,
|
||||||
giftWrapSealId = id,
|
giftWrapSealId = id,
|
||||||
publicKey = giftWrapPayload.pubKey,
|
publicKey = giftWrapPayload.pubKey,
|
||||||
kind = giftWrapPayload.kind,
|
kind = giftWrapPayload.kind,
|
||||||
|
|||||||
+2
-2
@@ -22,8 +22,8 @@ import kotlin.time.Instant
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
data class MarmotKeyPackage(
|
data class MarmotKeyPackage(
|
||||||
@PrimaryKey(autoGenerate = true)
|
@PrimaryKey
|
||||||
val id: Long = 0,
|
val id: HexKey,
|
||||||
|
|
||||||
val publicKey: HexKey,
|
val publicKey: HexKey,
|
||||||
|
|
||||||
|
|||||||
+45
-5
@@ -26,13 +26,13 @@ import kotlin.time.Instant
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
data class MarmotKeyPackageBundle(
|
data class MarmotKeyPackageBundle(
|
||||||
@PrimaryKey(autoGenerate = true)
|
@PrimaryKey
|
||||||
val id: Long = 0,
|
val id: HexKey,
|
||||||
|
|
||||||
val publicKey: HexKey,
|
val publicKey: HexKey,
|
||||||
// TODO: have the keyPackageEventId
|
|
||||||
|
|
||||||
val tlsEncodedMarmotKeyPackage: String,
|
val tlsEncodedMarmotKeyPackage: ByteArray,
|
||||||
|
|
||||||
val ncryptsecInitPrivateKey: String,
|
val ncryptsecInitPrivateKey: String,
|
||||||
val ncryptsecEncryptionPrivateKey: String,
|
val ncryptsecEncryptionPrivateKey: String,
|
||||||
val ncryptsecSignaturePrivateKey: String,
|
val ncryptsecSignaturePrivateKey: String,
|
||||||
@@ -45,4 +45,44 @@ data class MarmotKeyPackageBundle(
|
|||||||
override val savedAt: Instant = Clock.System.now(),
|
override val savedAt: Instant = Clock.System.now(),
|
||||||
override val deletedAt: Instant? = null,
|
override val deletedAt: Instant? = null,
|
||||||
override val broadcastedAt: Instant? = null,
|
override val broadcastedAt: Instant? = null,
|
||||||
): TimestampedEntity, LocalStoreEntity, BroadcastableEntity, SoftDeletableEntity
|
): TimestampedEntity, LocalStoreEntity, BroadcastableEntity, SoftDeletableEntity {
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (other == null || this::class != other::class) return false
|
||||||
|
|
||||||
|
other as MarmotKeyPackageBundle
|
||||||
|
|
||||||
|
if (consumed != other.consumed) return false
|
||||||
|
if (rotated != other.rotated) return false
|
||||||
|
if (id != other.id) return false
|
||||||
|
if (publicKey != other.publicKey) return false
|
||||||
|
if (!tlsEncodedMarmotKeyPackage.contentEquals(other.tlsEncodedMarmotKeyPackage)) return false
|
||||||
|
if (ncryptsecInitPrivateKey != other.ncryptsecInitPrivateKey) return false
|
||||||
|
if (ncryptsecEncryptionPrivateKey != other.ncryptsecEncryptionPrivateKey) return false
|
||||||
|
if (ncryptsecSignaturePrivateKey != other.ncryptsecSignaturePrivateKey) return false
|
||||||
|
if (createdAt != other.createdAt) return false
|
||||||
|
if (updatedAt != other.updatedAt) return false
|
||||||
|
if (savedAt != other.savedAt) return false
|
||||||
|
if (deletedAt != other.deletedAt) return false
|
||||||
|
if (broadcastedAt != other.broadcastedAt) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = consumed.hashCode()
|
||||||
|
result = 31 * result + rotated.hashCode()
|
||||||
|
result = 31 * result + id.hashCode()
|
||||||
|
result = 31 * result + publicKey.hashCode()
|
||||||
|
result = 31 * result + tlsEncodedMarmotKeyPackage.contentHashCode()
|
||||||
|
result = 31 * result + ncryptsecInitPrivateKey.hashCode()
|
||||||
|
result = 31 * result + ncryptsecEncryptionPrivateKey.hashCode()
|
||||||
|
result = 31 * result + ncryptsecSignaturePrivateKey.hashCode()
|
||||||
|
result = 31 * result + createdAt.hashCode()
|
||||||
|
result = 31 * result + updatedAt.hashCode()
|
||||||
|
result = 31 * result + savedAt.hashCode()
|
||||||
|
result = 31 * result + (deletedAt?.hashCode() ?: 0)
|
||||||
|
result = 31 * result + (broadcastedAt?.hashCode() ?: 0)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -212,6 +212,7 @@ data class NostrEvent(
|
|||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
|
logger.e("GiftWrapMessage error: ", e)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+19
-3
@@ -1,9 +1,12 @@
|
|||||||
package at.torch.compose.database.repository
|
package at.torch.compose.database.repository
|
||||||
|
|
||||||
|
import at.torch.compose.database.model.ChatMessage
|
||||||
|
import at.torch.compose.database.model.GiftWrapPayload
|
||||||
import co.touchlab.kermit.Logger
|
import co.touchlab.kermit.Logger
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.Kind
|
import com.vitorpamplona.quartz.nip01Core.core.Kind
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.crypto.EventHasher
|
||||||
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
|
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync
|
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync
|
||||||
@@ -14,6 +17,8 @@ import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent
|
|||||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlin.time.Clock
|
||||||
|
import kotlin.time.Instant
|
||||||
|
|
||||||
class DatabaseChatRepository(
|
class DatabaseChatRepository(
|
||||||
private val database: at.torch.compose.database.TorchDatabase,
|
private val database: at.torch.compose.database.TorchDatabase,
|
||||||
@@ -104,17 +109,28 @@ class DatabaseChatRepository(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val giftWrapPayloadId = database.giftWrapPayloadDao().upsert(
|
val createdAt = Clock.System.now().epochSeconds
|
||||||
_root_ide_package_.at.torch.compose.database.model.GiftWrapPayload(
|
val giftWrapPayloadId = EventHasher.hashId(
|
||||||
|
pubKey = localChatRoom.chatRoom.userPublicKey,
|
||||||
|
createdAt = createdAt,
|
||||||
|
tags = receiverTags.toTypedArray(),
|
||||||
|
content = text,
|
||||||
|
kind = messageType
|
||||||
|
)
|
||||||
|
|
||||||
|
database.giftWrapPayloadDao().upsert(
|
||||||
|
GiftWrapPayload(
|
||||||
|
id = giftWrapPayloadId,
|
||||||
kind = messageType,
|
kind = messageType,
|
||||||
tags = receiverTags.toTypedArray(),
|
tags = receiverTags.toTypedArray(),
|
||||||
|
createdAt = Instant.fromEpochSeconds(createdAt),
|
||||||
content = text,
|
content = text,
|
||||||
chatRoomId = localChatRoom.chatRoom.id,
|
chatRoomId = localChatRoom.chatRoom.id,
|
||||||
publicKey = localChatRoom.chatRoom.userPublicKey
|
publicKey = localChatRoom.chatRoom.userPublicKey
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
database.chatMessageDao().upsert(
|
database.chatMessageDao().upsert(
|
||||||
_root_ide_package_.at.torch.compose.database.model.ChatMessage(
|
ChatMessage(
|
||||||
content = text,
|
content = text,
|
||||||
chatRoomId = localChatRoom.chatRoom.id,
|
chatRoomId = localChatRoom.chatRoom.id,
|
||||||
senderPublicKey = localChatRoom.chatRoom.userPublicKey,
|
senderPublicKey = localChatRoom.chatRoom.userPublicKey,
|
||||||
|
|||||||
+147
-12
@@ -2,13 +2,31 @@ package at.torch.compose.database.repository
|
|||||||
|
|
||||||
import at.torch.compose.database.TorchDatabase
|
import at.torch.compose.database.TorchDatabase
|
||||||
import at.torch.compose.database.model.MarmotKeyPackageBundle
|
import at.torch.compose.database.model.MarmotKeyPackageBundle
|
||||||
|
import at.torch.compose.database.model.UnsignedNostrEvent
|
||||||
import at.torch.compose.extensions.toHex
|
import at.torch.compose.extensions.toHex
|
||||||
|
import at.torch.compose.nostr.Relays
|
||||||
import at.torch.compose.repository.MarmotRepository
|
import at.torch.compose.repository.MarmotRepository
|
||||||
|
import com.vitorpamplona.quartz.marmot.mip00KeyPackages.KeyPackageEvent
|
||||||
|
import com.vitorpamplona.quartz.marmot.mip00KeyPackages.KeyPackageRotationManager.Companion.KEY_PACKAGE_LIFETIME_SECONDS
|
||||||
|
import com.vitorpamplona.quartz.marmot.mip00KeyPackages.KeyPackageUtils
|
||||||
import com.vitorpamplona.quartz.marmot.mls.codec.TlsWriter
|
import com.vitorpamplona.quartz.marmot.mls.codec.TlsWriter
|
||||||
|
import com.vitorpamplona.quartz.marmot.mls.crypto.Ed25519
|
||||||
|
import com.vitorpamplona.quartz.marmot.mls.crypto.MlsCryptoProvider
|
||||||
|
import com.vitorpamplona.quartz.marmot.mls.crypto.X25519
|
||||||
import com.vitorpamplona.quartz.marmot.mls.messages.KeyPackageBundle
|
import com.vitorpamplona.quartz.marmot.mls.messages.KeyPackageBundle
|
||||||
|
import com.vitorpamplona.quartz.marmot.mls.messages.MlsKeyPackage
|
||||||
|
import com.vitorpamplona.quartz.marmot.mls.tree.Capabilities
|
||||||
|
import com.vitorpamplona.quartz.marmot.mls.tree.Credential
|
||||||
|
import com.vitorpamplona.quartz.marmot.mls.tree.Extension
|
||||||
|
import com.vitorpamplona.quartz.marmot.mls.tree.LeafNode
|
||||||
|
import com.vitorpamplona.quartz.marmot.mls.tree.LeafNodeSource
|
||||||
|
import com.vitorpamplona.quartz.marmot.mls.tree.Lifetime
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||||
import com.vitorpamplona.quartz.nip49PrivKeyEnc.Nip49
|
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.crypto.EventHasher
|
||||||
|
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlin.io.encoding.Base64
|
||||||
|
|
||||||
class DatabaseMarmotRepository(
|
class DatabaseMarmotRepository(
|
||||||
val database: TorchDatabase,
|
val database: TorchDatabase,
|
||||||
@@ -16,24 +34,141 @@ class DatabaseMarmotRepository(
|
|||||||
): MarmotRepository {
|
): MarmotRepository {
|
||||||
override suspend fun saveMarmotKeyPackageBundle(
|
override suspend fun saveMarmotKeyPackageBundle(
|
||||||
publicKey: HexKey,
|
publicKey: HexKey,
|
||||||
keyPackageBundle: KeyPackageBundle,
|
|
||||||
nsecPassword: String
|
nsecPassword: String
|
||||||
) {
|
) {
|
||||||
val tlsWriter = TlsWriter()
|
// KeyPackage...
|
||||||
|
val dTag = KeyPackageUtils.generateRandomDTag() // TODO: Persist this...
|
||||||
|
val identity = publicKey.hexToByteArray()
|
||||||
|
val bundle = generateKeyPackage(identity, dTag)
|
||||||
|
|
||||||
keyPackageBundle.keyPackage.encodeTls(tlsWriter)
|
val keyPackageBytes = bundle.keyPackage.toTlsBytes()
|
||||||
|
val keyPackageBase64 = Base64.encode(keyPackageBytes)
|
||||||
|
val keyPackageRef = bundle.keyPackage.reference().toHexKey()
|
||||||
|
|
||||||
val nip49 = Nip49()
|
val keyPackageTemplate = KeyPackageEvent.build(
|
||||||
|
keyPackageBase64 = keyPackageBase64,
|
||||||
|
dTagSlot = dTag,
|
||||||
|
keyPackageRef = keyPackageRef,
|
||||||
|
relays = Relays.DefaultDMRelayList,
|
||||||
|
)
|
||||||
|
|
||||||
database.marmotKeyPackageBundleDao().upsert(
|
database.unsignedNostrEventDao().upsert(
|
||||||
MarmotKeyPackageBundle(
|
UnsignedNostrEvent(
|
||||||
publicKey = publicKey,
|
pubKey = publicKey,
|
||||||
tlsEncodedMarmotKeyPackage = tlsWriter.toByteArray().toHex(),
|
kind = keyPackageTemplate.kind,
|
||||||
ncryptsecInitPrivateKey = keyPackageBundle.initPrivateKey.toHex(), // TODO: ncryptSec this using the private key
|
tags = keyPackageTemplate.tags,
|
||||||
ncryptsecEncryptionPrivateKey = keyPackageBundle.encryptionPrivateKey.toHex(), // TODO: ncryptSec this using the private key
|
content = keyPackageTemplate.content
|
||||||
ncryptsecSignaturePrivateKey = keyPackageBundle.signaturePrivateKey.toHex() // TODO: ncryptSec this using the private key
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val id = EventHasher.hashIdBytes(
|
||||||
|
pubKey =publicKey,
|
||||||
|
createdAt = keyPackageTemplate.createdAt,
|
||||||
|
kind = keyPackageTemplate.kind,
|
||||||
|
tags = keyPackageTemplate.tags,
|
||||||
|
content = keyPackageTemplate.content
|
||||||
|
)
|
||||||
|
|
||||||
|
database.marmotKeyPackageBundleDao().upsert(
|
||||||
|
MarmotKeyPackageBundle(
|
||||||
|
id = id.toHexKey(),
|
||||||
|
publicKey = publicKey,
|
||||||
|
tlsEncodedMarmotKeyPackage = bundle.keyPackage.toTlsBytes(),
|
||||||
|
ncryptsecInitPrivateKey = bundle.initPrivateKey.toHex(), // TODO: ncryptSec this using the private key
|
||||||
|
ncryptsecEncryptionPrivateKey = bundle.encryptionPrivateKey.toHex(), // TODO: ncryptSec this using the private key
|
||||||
|
ncryptsecSignaturePrivateKey = bundle.signaturePrivateKey.toHex() // TODO: ncryptSec this using the private key
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun generateKeyPackage(
|
||||||
|
identity: ByteArray,
|
||||||
|
dTagSlot: String = KeyPackageUtils.PRIMARY_SLOT,
|
||||||
|
): KeyPackageBundle {
|
||||||
|
val initKp = X25519.generateKeyPair()
|
||||||
|
val encKp = X25519.generateKeyPair()
|
||||||
|
val sigKp = Ed25519.generateKeyPair()
|
||||||
|
|
||||||
|
val leafNode =
|
||||||
|
buildKeyPackageLeafNode(
|
||||||
|
encryptionKey = encKp.publicKey,
|
||||||
|
signatureKey = sigKp.publicKey,
|
||||||
|
identity = identity,
|
||||||
|
signingKey = sigKp.privateKey,
|
||||||
|
)
|
||||||
|
|
||||||
|
val unsigned =
|
||||||
|
MlsKeyPackage(
|
||||||
|
initKey = initKp.publicKey,
|
||||||
|
leafNode = leafNode,
|
||||||
|
// LastResort (0x000A) marks this as a last-resort KeyPackage per MLS Extensions draft.
|
||||||
|
// MDK always marks KeyPackages as last-resort via .mark_as_last_resort().
|
||||||
|
extensions = listOf(
|
||||||
|
Extension(
|
||||||
|
extensionType = 0x000A,
|
||||||
|
extensionData = ByteArray(0)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
signature = ByteArray(0),
|
||||||
|
)
|
||||||
|
val keyPackage =
|
||||||
|
unsigned.copy(
|
||||||
|
signature =
|
||||||
|
MlsCryptoProvider.signWithLabel(
|
||||||
|
sigKp.privateKey,
|
||||||
|
"KeyPackageTBS",
|
||||||
|
unsigned.encodeTbs(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
val bundle = KeyPackageBundle(
|
||||||
|
keyPackage,
|
||||||
|
initPrivateKey = initKp.privateKey,
|
||||||
|
encryptionPrivateKey = encKp.privateKey,
|
||||||
|
signaturePrivateKey = sigKp.privateKey
|
||||||
|
)
|
||||||
|
|
||||||
|
return bundle
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun buildKeyPackageLeafNode(
|
||||||
|
encryptionKey: ByteArray,
|
||||||
|
signatureKey: ByteArray,
|
||||||
|
identity: ByteArray,
|
||||||
|
signingKey: ByteArray,
|
||||||
|
): LeafNode {
|
||||||
|
val now = TimeUtils.now()
|
||||||
|
val unsigned =
|
||||||
|
LeafNode(
|
||||||
|
encryptionKey = encryptionKey,
|
||||||
|
signatureKey = signatureKey,
|
||||||
|
credential = Credential.Basic(identity),
|
||||||
|
capabilities =
|
||||||
|
Capabilities(
|
||||||
|
extensions =
|
||||||
|
listOf(
|
||||||
|
0x000A, // LastResort (required by OpenMLS validation)
|
||||||
|
0xF2EE, // NostrGroupData (required by group's RequiredCapabilities)
|
||||||
|
),
|
||||||
|
proposals =
|
||||||
|
listOf(
|
||||||
|
0x000A, // SelfRemove (required by group's RequiredCapabilities)
|
||||||
|
),
|
||||||
|
),
|
||||||
|
leafNodeSource = LeafNodeSource.KEY_PACKAGE,
|
||||||
|
lifetime = Lifetime(
|
||||||
|
notBefore = now,
|
||||||
|
notAfter = now + KEY_PACKAGE_LIFETIME_SECONDS
|
||||||
|
),
|
||||||
|
extensions = emptyList(),
|
||||||
|
signature = ByteArray(0),
|
||||||
|
)
|
||||||
|
|
||||||
|
val tbs = unsigned.encodeTbs(groupId = null, leafIndex = null)
|
||||||
|
val signature = MlsCryptoProvider.signWithLabel(signingKey, "LeafNodeTBS", tbs)
|
||||||
|
return unsigned.copy(signature = signature)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
+1
-108
@@ -114,7 +114,7 @@ class DatabaseNostrRepository(
|
|||||||
publicKey: HexKey,
|
publicKey: HexKey,
|
||||||
name: String?,
|
name: String?,
|
||||||
biography: String?
|
biography: String?
|
||||||
): KeyPackageBundle {
|
) {
|
||||||
val profileEventTemplate = MetadataEvent.createNew(
|
val profileEventTemplate = MetadataEvent.createNew(
|
||||||
name = name,
|
name = name,
|
||||||
about = biography,
|
about = biography,
|
||||||
@@ -202,113 +202,6 @@ class DatabaseNostrRepository(
|
|||||||
content = ""
|
content = ""
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
// KeyPackage...
|
|
||||||
KeyPackageUtils.PRIMARY_SLOT
|
|
||||||
val dTag = KeyPackageUtils.generateRandomDTag() // TODO: Persist this...
|
|
||||||
val identity = publicKey.hexToByteArray()
|
|
||||||
val bundle = generateKeyPackage(identity, dTag)
|
|
||||||
|
|
||||||
val keyPackageBytes = bundle.keyPackage.toTlsBytes()
|
|
||||||
val keyPackageBase64 = Base64.encode(keyPackageBytes)
|
|
||||||
val keyPackageRef = bundle.keyPackage.reference().toHexKey()
|
|
||||||
|
|
||||||
val keyPackageTemplate = KeyPackageEvent.build(
|
|
||||||
keyPackageBase64 = keyPackageBase64,
|
|
||||||
dTagSlot = dTag,
|
|
||||||
keyPackageRef = keyPackageRef,
|
|
||||||
relays = Relays.DefaultDMRelayList,
|
|
||||||
)
|
|
||||||
|
|
||||||
unsignedNostrEvents.add(
|
|
||||||
UnsignedNostrEvent(
|
|
||||||
pubKey = publicKey,
|
|
||||||
kind = keyPackageTemplate.kind,
|
|
||||||
tags = keyPackageTemplate.tags,
|
|
||||||
content = keyPackageTemplate.content
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
saveUnsignedNostrEvents(unsignedNostrEvents)
|
|
||||||
|
|
||||||
return bundle
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun generateKeyPackage(
|
|
||||||
identity: ByteArray,
|
|
||||||
dTagSlot: String = KeyPackageUtils.PRIMARY_SLOT,
|
|
||||||
): KeyPackageBundle {
|
|
||||||
val initKp = X25519.generateKeyPair()
|
|
||||||
val encKp = X25519.generateKeyPair()
|
|
||||||
val sigKp = Ed25519.generateKeyPair()
|
|
||||||
|
|
||||||
val leafNode =
|
|
||||||
buildKeyPackageLeafNode(
|
|
||||||
encryptionKey = encKp.publicKey,
|
|
||||||
signatureKey = sigKp.publicKey,
|
|
||||||
identity = identity,
|
|
||||||
signingKey = sigKp.privateKey,
|
|
||||||
)
|
|
||||||
|
|
||||||
val unsigned =
|
|
||||||
MlsKeyPackage(
|
|
||||||
initKey = initKp.publicKey,
|
|
||||||
leafNode = leafNode,
|
|
||||||
// LastResort (0x000A) marks this as a last-resort KeyPackage per MLS Extensions draft.
|
|
||||||
// MDK always marks KeyPackages as last-resort via .mark_as_last_resort().
|
|
||||||
extensions = listOf(Extension(extensionType = 0x000A, extensionData = ByteArray(0))),
|
|
||||||
signature = ByteArray(0),
|
|
||||||
)
|
|
||||||
val keyPackage =
|
|
||||||
unsigned.copy(
|
|
||||||
signature =
|
|
||||||
MlsCryptoProvider.signWithLabel(
|
|
||||||
sigKp.privateKey,
|
|
||||||
"KeyPackageTBS",
|
|
||||||
unsigned.encodeTbs(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
val bundle = KeyPackageBundle(keyPackage, initKp.privateKey, encKp.privateKey, sigKp.privateKey)
|
|
||||||
// TODO: Persist this...
|
|
||||||
|
|
||||||
return bundle
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private fun buildKeyPackageLeafNode(
|
|
||||||
encryptionKey: ByteArray,
|
|
||||||
signatureKey: ByteArray,
|
|
||||||
identity: ByteArray,
|
|
||||||
signingKey: ByteArray,
|
|
||||||
): LeafNode {
|
|
||||||
val now = TimeUtils.now()
|
|
||||||
val unsigned =
|
|
||||||
LeafNode(
|
|
||||||
encryptionKey = encryptionKey,
|
|
||||||
signatureKey = signatureKey,
|
|
||||||
credential = Credential.Basic(identity),
|
|
||||||
capabilities =
|
|
||||||
Capabilities(
|
|
||||||
extensions =
|
|
||||||
listOf(
|
|
||||||
0x000A, // LastResort (required by OpenMLS validation)
|
|
||||||
0xF2EE, // NostrGroupData (required by group's RequiredCapabilities)
|
|
||||||
),
|
|
||||||
proposals =
|
|
||||||
listOf(
|
|
||||||
0x000A, // SelfRemove (required by group's RequiredCapabilities)
|
|
||||||
),
|
|
||||||
),
|
|
||||||
leafNodeSource = LeafNodeSource.KEY_PACKAGE,
|
|
||||||
lifetime = Lifetime(notBefore = now, notAfter = now + KEY_PACKAGE_LIFETIME_SECONDS),
|
|
||||||
extensions = emptyList(),
|
|
||||||
signature = ByteArray(0),
|
|
||||||
)
|
|
||||||
|
|
||||||
val tbs = unsigned.encodeTbs(groupId = null, leafIndex = null)
|
|
||||||
val signature = MlsCryptoProvider.signWithLabel(signingKey, "LeafNodeTBS", tbs)
|
|
||||||
return unsigned.copy(signature = signature)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun signInToProfile(
|
override suspend fun signInToProfile(
|
||||||
|
|||||||
+4
@@ -0,0 +1,4 @@
|
|||||||
|
package at.torch.compose.exceptions
|
||||||
|
|
||||||
|
class MarmotMissingKeyPackageBundleException(message: String? = null, cause: Throwable? = null) : Exception(message, cause) {
|
||||||
|
}
|
||||||
-4
@@ -1,4 +0,0 @@
|
|||||||
package at.torch.compose.exceptions
|
|
||||||
|
|
||||||
class MarmotNoMatchingKeyPackageBundleException(message: String? = null, cause: Throwable? = null) : Exception(message, cause) {
|
|
||||||
}
|
|
||||||
+4
@@ -0,0 +1,4 @@
|
|||||||
|
package at.torch.compose.exceptions
|
||||||
|
|
||||||
|
class MarmotWelcomeEventMissingKeyPackageEventIdException(message: String? = null, cause: Throwable? = null) : Exception(message, cause) {
|
||||||
|
}
|
||||||
+4
@@ -0,0 +1,4 @@
|
|||||||
|
package at.torch.compose.exceptions
|
||||||
|
|
||||||
|
class MarmotWelcomeMissingGroupIdException(message: String? = null, cause: Throwable? = null) : Exception(message, cause) {
|
||||||
|
}
|
||||||
@@ -1,13 +1,11 @@
|
|||||||
package at.torch.compose.repository
|
package at.torch.compose.repository
|
||||||
|
|
||||||
import com.vitorpamplona.quartz.marmot.mls.messages.KeyPackageBundle
|
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||||
|
|
||||||
interface MarmotRepository {
|
interface MarmotRepository {
|
||||||
|
|
||||||
suspend fun saveMarmotKeyPackageBundle(
|
suspend fun saveMarmotKeyPackageBundle(
|
||||||
publicKey: HexKey,
|
publicKey: HexKey,
|
||||||
keyPackageBundle: KeyPackageBundle,
|
|
||||||
nsecPassword: String
|
nsecPassword: String
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -15,7 +13,6 @@ interface MarmotRepository {
|
|||||||
val NO_OP_MARMOT_KEY_PACKAGE_BUNDLE = object: MarmotRepository {
|
val NO_OP_MARMOT_KEY_PACKAGE_BUNDLE = object: MarmotRepository {
|
||||||
override suspend fun saveMarmotKeyPackageBundle(
|
override suspend fun saveMarmotKeyPackageBundle(
|
||||||
publicKey: HexKey,
|
publicKey: HexKey,
|
||||||
keyPackageBundle: KeyPackageBundle,
|
|
||||||
nsecPassword: String
|
nsecPassword: String
|
||||||
) {
|
) {
|
||||||
TODO("Not yet implemented")
|
TODO("Not yet implemented")
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package at.torch.compose.repository
|
package at.torch.compose.repository
|
||||||
|
|
||||||
import at.torch.compose.database.model.intermdiate.LocalAccount
|
import at.torch.compose.database.model.intermdiate.LocalAccount
|
||||||
import com.vitorpamplona.quartz.marmot.mls.messages.KeyPackageBundle
|
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.Kind
|
import com.vitorpamplona.quartz.nip01Core.core.Kind
|
||||||
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
|
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
|
||||||
@@ -28,7 +27,7 @@ interface NostrRepository {
|
|||||||
publicKey: HexKey,
|
publicKey: HexKey,
|
||||||
name: String?,
|
name: String?,
|
||||||
biography: String?
|
biography: String?
|
||||||
): KeyPackageBundle
|
)
|
||||||
|
|
||||||
suspend fun signInToProfile(
|
suspend fun signInToProfile(
|
||||||
publicKey: HexKey
|
publicKey: HexKey
|
||||||
@@ -163,7 +162,7 @@ interface NostrRepository {
|
|||||||
publicKey: HexKey,
|
publicKey: HexKey,
|
||||||
name: String?,
|
name: String?,
|
||||||
biography: String?
|
biography: String?
|
||||||
): KeyPackageBundle {
|
) {
|
||||||
TODO("")
|
TODO("")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -287,7 +287,7 @@ private fun ChatRoomDetailScreenPreview() {
|
|||||||
id = "",
|
id = "",
|
||||||
userPublicKey = "",
|
userPublicKey = "",
|
||||||
subject = "Message title",
|
subject = "Message title",
|
||||||
initialGiftWrapPayloadId = 0,
|
initialGiftWrapPayloadId = "sdfaer",
|
||||||
mlsGroupState = null
|
mlsGroupState = null
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
+6
-4
@@ -25,6 +25,7 @@ import at.torch.compose.ui.composable.navigation.routes.ChatRoomCreationRoute
|
|||||||
import at.torch.compose.ui.composable.navigation.routes.LoadingRoute
|
import at.torch.compose.ui.composable.navigation.routes.LoadingRoute
|
||||||
import at.torch.compose.ui.composable.navigation.routes.NostrEventDetailRoute
|
import at.torch.compose.ui.composable.navigation.routes.NostrEventDetailRoute
|
||||||
import at.torch.compose.ui.composable.navigation.routes.ShareProfileRoute
|
import at.torch.compose.ui.composable.navigation.routes.ShareProfileRoute
|
||||||
|
import at.torch.compose.ui.composable.navigation.routes.SocialPreconditionRoute
|
||||||
import at.torch.compose.ui.composable.navigation.routes.SovereignWalletStartupRoute
|
import at.torch.compose.ui.composable.navigation.routes.SovereignWalletStartupRoute
|
||||||
import at.torch.compose.ui.composable.navigation.routes.WriteNewNoteRoute
|
import at.torch.compose.ui.composable.navigation.routes.WriteNewNoteRoute
|
||||||
import at.torch.compose.ui.view.state.NavigationUIState
|
import at.torch.compose.ui.view.state.NavigationUIState
|
||||||
@@ -101,6 +102,7 @@ fun TorchNavHost(
|
|||||||
scope = applicationIOScope
|
scope = applicationIOScope
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
// TODO: Produce a notary UI Element...
|
// TODO: Produce a notary UI Element...
|
||||||
val synchronizationViewModel: at.torch.compose.ui.view.model.SynchronizationViewModel = viewModel(
|
val synchronizationViewModel: at.torch.compose.ui.view.model.SynchronizationViewModel = viewModel(
|
||||||
factory = at.torch.compose.ui.view.model.SynchronizationViewModel.factory(
|
factory = at.torch.compose.ui.view.model.SynchronizationViewModel.factory(
|
||||||
@@ -203,7 +205,7 @@ fun TorchNavHost(
|
|||||||
}
|
}
|
||||||
is NavigationUIState.ProfileLoaded -> {
|
is NavigationUIState.ProfileLoaded -> {
|
||||||
navController.navigate(
|
navController.navigate(
|
||||||
route = at.torch.compose.ui.composable.navigation.routes.SocialPreconditionRoute(
|
route = SocialPreconditionRoute(
|
||||||
activeUserPubkey = state.publicKey
|
activeUserPubkey = state.publicKey
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
@@ -339,7 +341,7 @@ fun TorchNavHost(
|
|||||||
},
|
},
|
||||||
onNavigateToNostrEvent = { hexKey ->
|
onNavigateToNostrEvent = { hexKey ->
|
||||||
navController.navigate(
|
navController.navigate(
|
||||||
route = at.torch.compose.ui.composable.navigation.routes.NostrEventDetailRoute(
|
route = NostrEventDetailRoute(
|
||||||
activeUserPublicKey = route.activeUserPublicKey,
|
activeUserPublicKey = route.activeUserPublicKey,
|
||||||
nostrEventId = hexKey
|
nostrEventId = hexKey
|
||||||
)
|
)
|
||||||
@@ -383,8 +385,8 @@ fun TorchNavHost(
|
|||||||
|
|
||||||
at.torch.compose.ui.composable.UnannouncedProfileScreen()
|
at.torch.compose.ui.composable.UnannouncedProfileScreen()
|
||||||
}
|
}
|
||||||
composable<at.torch.compose.ui.composable.navigation.routes.SocialPreconditionRoute> { backStackEntry ->
|
composable<SocialPreconditionRoute> { backStackEntry ->
|
||||||
val route = backStackEntry.toRoute<at.torch.compose.ui.composable.navigation.routes.SocialPreconditionRoute>()
|
val route = backStackEntry.toRoute<SocialPreconditionRoute>()
|
||||||
at.torch.compose.ui.composable.SocialPreconditionScreen(
|
at.torch.compose.ui.composable.SocialPreconditionScreen(
|
||||||
onNavigateToSkipForNow = {
|
onNavigateToSkipForNow = {
|
||||||
navController.navigate(
|
navController.navigate(
|
||||||
|
|||||||
+2
-2
@@ -105,7 +105,8 @@ class CreateProfileViewModel(
|
|||||||
)
|
)
|
||||||
|
|
||||||
val pubkey = localKeyManager.nostrPublicKey()
|
val pubkey = localKeyManager.nostrPublicKey()
|
||||||
val keyPackageBundle = nostrRepository.createNewProfile(
|
|
||||||
|
nostrRepository.createNewProfile(
|
||||||
pubkey,
|
pubkey,
|
||||||
name = createProfileFormState.nameField.textFieldState.text.toString(),
|
name = createProfileFormState.nameField.textFieldState.text.toString(),
|
||||||
biography = createProfileFormState.biographyField.textFieldState.text.toString()
|
biography = createProfileFormState.biographyField.textFieldState.text.toString()
|
||||||
@@ -113,7 +114,6 @@ class CreateProfileViewModel(
|
|||||||
|
|
||||||
marmotRepository.saveMarmotKeyPackageBundle(
|
marmotRepository.saveMarmotKeyPackageBundle(
|
||||||
publicKey = pubkey,
|
publicKey = pubkey,
|
||||||
keyPackageBundle,
|
|
||||||
nsecPassword = localKeyManager.nsecPassword()
|
nsecPassword = localKeyManager.nsecPassword()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ class NotaryViewModel(
|
|||||||
init {
|
init {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
activeWalletStateFlow.collectLatest { activeWallet ->
|
activeWalletStateFlow.collectLatest { activeWallet ->
|
||||||
logger.d("activeWallet: $activeWallet")
|
logger.d("Notary ActiveWallet: $activeWallet")
|
||||||
activeWallet?.business?.walletManager?.keyManager?.value?.nostrPrivateKey()?.let { nostrPrivateKey ->
|
activeWallet?.business?.walletManager?.keyManager?.value?.nostrPrivateKey()?.let { nostrPrivateKey ->
|
||||||
|
|
||||||
val keyPair = KeyPair(
|
val keyPair = KeyPair(
|
||||||
|
|||||||
+1
@@ -74,6 +74,7 @@ class SynchronizationViewModel(
|
|||||||
init {
|
init {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
activeWalletStateFlow.collectLatest { activeWallet ->
|
activeWalletStateFlow.collectLatest { activeWallet ->
|
||||||
|
logger.d("Synchronization ActiveWallet: $activeWallet")
|
||||||
activeWallet?.business?.walletManager?.keyManager?.value?.nostrPrivateKey()?.let { nostrPrivateKey ->
|
activeWallet?.business?.walletManager?.keyManager?.value?.nostrPrivateKey()?.let { nostrPrivateKey ->
|
||||||
val keyPair = KeyPair(
|
val keyPair = KeyPair(
|
||||||
privKey = nostrPrivateKey.value.toByteArray()
|
privKey = nostrPrivateKey.value.toByteArray()
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ okhttp = "5.3.2"
|
|||||||
okio = "3.17.0"
|
okio = "3.17.0"
|
||||||
pagingCommon = "3.5.0"
|
pagingCommon = "3.5.0"
|
||||||
qrose = "1.1.2"
|
qrose = "1.1.2"
|
||||||
quartz = "1.10.0"
|
quartz = "1.12.6"
|
||||||
room3 = "3.0.0-alpha06"
|
room3 = "3.0.0-alpha06"
|
||||||
sqldelight = "2.3.2"
|
sqldelight = "2.3.2"
|
||||||
sqlite = "2.6.2"
|
sqlite = "2.6.2"
|
||||||
|
|||||||
Reference in new issue
Block a user