Synchronization workflow
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 1,
|
||||
"identityHash": "866bd337c819985dae51a248851b3dd3",
|
||||
"identityHash": "75e5c80b2c3183c8a4fa300627ffb5db",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "BroadcastNostrEventReceipt",
|
||||
@@ -871,6 +871,202 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "SynchronizeNostrEventRequest",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `status` TEXT NOT NULL, `relayURL` TEXT NOT NULL, `eventIds` TEXT, `authorPublicKeys` TEXT, `kinds` TEXT, `tagName` TEXT, `since` INTEGER, `until` INTEGER, `limit` INTEGER, `nostrEventId` TEXT, `unsignedNostrEventId` INTEGER, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`nostrEventId`) REFERENCES `NostrEvent`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`unsignedNostrEventId`) REFERENCES `UnsignedNostrEvent`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "status",
|
||||
"columnName": "status",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "relayURL",
|
||||
"columnName": "relayURL",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "eventIds",
|
||||
"columnName": "eventIds",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "authorPublicKeys",
|
||||
"columnName": "authorPublicKeys",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "kinds",
|
||||
"columnName": "kinds",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "tagName",
|
||||
"columnName": "tagName",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "since",
|
||||
"columnName": "since",
|
||||
"affinity": "INTEGER"
|
||||
},
|
||||
{
|
||||
"fieldPath": "until",
|
||||
"columnName": "until",
|
||||
"affinity": "INTEGER"
|
||||
},
|
||||
{
|
||||
"fieldPath": "limit",
|
||||
"columnName": "limit",
|
||||
"affinity": "INTEGER"
|
||||
},
|
||||
{
|
||||
"fieldPath": "nostrEventId",
|
||||
"columnName": "nostrEventId",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "unsignedNostrEventId",
|
||||
"columnName": "unsignedNostrEventId",
|
||||
"affinity": "INTEGER"
|
||||
},
|
||||
{
|
||||
"fieldPath": "createdAt",
|
||||
"columnName": "createdAt",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "updatedAt",
|
||||
"columnName": "updatedAt",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_SynchronizeNostrEventRequest_nostrEventId",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"nostrEventId"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_SynchronizeNostrEventRequest_nostrEventId` ON `${TABLE_NAME}` (`nostrEventId`)"
|
||||
},
|
||||
{
|
||||
"name": "index_SynchronizeNostrEventRequest_status",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"status"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_SynchronizeNostrEventRequest_status` ON `${TABLE_NAME}` (`status`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "NostrEvent",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"nostrEventId"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
{
|
||||
"table": "UnsignedNostrEvent",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"unsignedNostrEventId"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "SynchronizeNostrEventResult",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `relayURL` TEXT NOT NULL, `nostrEventId` TEXT, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`nostrEventId`) REFERENCES `NostrEvent`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "relayURL",
|
||||
"columnName": "relayURL",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "nostrEventId",
|
||||
"columnName": "nostrEventId",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "createdAt",
|
||||
"columnName": "createdAt",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "updatedAt",
|
||||
"columnName": "updatedAt",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_SynchronizeNostrEventResult_nostrEventId",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"nostrEventId"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_SynchronizeNostrEventResult_nostrEventId` ON `${TABLE_NAME}` (`nostrEventId`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "NostrEvent",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"nostrEventId"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "UnsignedNostrEvent",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `pubKey` TEXT NOT NULL, `kind` INTEGER NOT NULL, `tags` TEXT NOT NULL, `content` TEXT NOT NULL, `signedAt` INTEGER, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL, `savedAt` INTEGER NOT NULL, `deletedAt` INTEGER, `broadcastedAt` INTEGER)",
|
||||
@@ -1134,7 +1330,7 @@
|
||||
],
|
||||
"setupQueries": [
|
||||
"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, '866bd337c819985dae51a248851b3dd3')"
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '75e5c80b2c3183c8a4fa300627ffb5db')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,8 @@ import ac.aux.compose.database.dao.PostDao
|
||||
import ac.aux.compose.database.dao.ProfileDao
|
||||
import ac.aux.compose.database.dao.ReactionDao
|
||||
import ac.aux.compose.database.dao.RepostDao
|
||||
import ac.aux.compose.database.dao.SynchronizeNostrEventRequestDao
|
||||
import ac.aux.compose.database.dao.SynchronizeNostrEventResultDao
|
||||
import ac.aux.compose.database.dao.UnsignedNostrEventDao
|
||||
import ac.aux.compose.database.dao.ZapDao
|
||||
import ac.aux.compose.database.model.BroadcastNostrEventReceipt
|
||||
@@ -18,6 +20,8 @@ import ac.aux.compose.database.model.Post
|
||||
import ac.aux.compose.database.model.Profile
|
||||
import ac.aux.compose.database.model.Reaction
|
||||
import ac.aux.compose.database.model.Repost
|
||||
import ac.aux.compose.database.model.SynchronizeNostrEventRequest
|
||||
import ac.aux.compose.database.model.SynchronizeNostrEventResult
|
||||
import ac.aux.compose.database.model.UnsignedNostrEvent
|
||||
import ac.aux.compose.database.model.Zap
|
||||
import androidx.room.Database
|
||||
@@ -35,6 +39,8 @@ import androidx.room.useWriterConnection
|
||||
Profile::class,
|
||||
Reaction::class,
|
||||
Repost::class,
|
||||
SynchronizeNostrEventRequest::class,
|
||||
SynchronizeNostrEventResult::class,
|
||||
UnsignedNostrEvent::class,
|
||||
Zap::class
|
||||
],
|
||||
@@ -52,6 +58,10 @@ abstract class AuxDatabase: RoomDatabase() {
|
||||
abstract fun reactionDao(): ReactionDao
|
||||
abstract fun repostDao(): RepostDao
|
||||
|
||||
abstract fun synchronizeNostrEventRequestDao(): SynchronizeNostrEventRequestDao
|
||||
|
||||
abstract fun synchronizeNostrEventResultDao(): SynchronizeNostrEventResultDao
|
||||
|
||||
abstract fun unsignedNostrEventDao(): UnsignedNostrEventDao
|
||||
abstract fun zapDao(): ZapDao
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@ import ac.aux.compose.database.model.BroadcastNostrEventRequest
|
||||
import ac.aux.compose.database.model.NostrEvent
|
||||
import ac.aux.compose.database.model.Post
|
||||
import ac.aux.compose.database.model.Profile
|
||||
import ac.aux.compose.database.model.SynchronizeNostrEventRequest
|
||||
import ac.aux.compose.database.model.SynchronizeNostrEventResult
|
||||
import ac.aux.compose.database.model.UnsignedNostrEvent
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Embedded
|
||||
@@ -59,13 +61,87 @@ abstract class NostrDao(
|
||||
|
||||
database.nostrEventDao().upsert(nostrEvent)
|
||||
|
||||
indexNostrEvent(
|
||||
nostrEvent = nostrEvent,
|
||||
synchronizationRelayURLs = relayURLs
|
||||
)
|
||||
|
||||
relayURLs.forEach { relayURL ->
|
||||
database.broadcastNostrEventRequestDao().upsert(
|
||||
BroadcastNostrEventRequest(
|
||||
nostrEventId = nostrEvent.id,
|
||||
unsignedNostrEventId = nostrEvent.unsignedNostrEventId,
|
||||
relayURL = relayURL
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Transaction
|
||||
open suspend fun storeNostrEvent(
|
||||
nostrEvent: NostrEvent,
|
||||
synchronizeNostrEventRequest: SynchronizeNostrEventRequest,
|
||||
synchronizationRelayURLs: List<String>
|
||||
) {
|
||||
logger.i("Store Nostr Event: $nostrEvent")
|
||||
// Find or create profile with the pubKey... if not found submit a sync request...
|
||||
val profile = database.profileDao().getProfileByPublicKey(nostrEvent.pubKey)
|
||||
|
||||
if (profile == null) {
|
||||
val placeHolderProfile = Profile(
|
||||
publicKey = nostrEvent.pubKey,
|
||||
nostrEventId = nostrEvent.pubKey // This is illegal... because the nostrEventId should be the one with all the profile information
|
||||
)
|
||||
val placeHolderProfileNostrEvent = NostrEvent(
|
||||
id = placeHolderProfile.publicKey,
|
||||
kind = 30024,
|
||||
pubKey = placeHolderProfile.publicKey,
|
||||
content = placeHolderProfile.publicKey,
|
||||
createdAt = Clock.System.now(),
|
||||
sig = placeHolderProfile.publicKey,
|
||||
tags = emptyArray(),
|
||||
)
|
||||
database.nostrEventDao().upsert(placeHolderProfileNostrEvent)
|
||||
database.profileDao().upsert(placeHolderProfile)
|
||||
|
||||
synchronizationRelayURLs.forEach { synchronizationRelayURL ->
|
||||
database.synchronizeNostrEventRequestDao().upsert(
|
||||
SynchronizeNostrEventRequest(
|
||||
authorPublicKeys = listOf<String>(nostrEvent.pubKey).joinToString(","),
|
||||
relayURL = synchronizationRelayURL
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
database.nostrEventDao().upsert(nostrEvent)
|
||||
|
||||
// Index nostrEvent
|
||||
indexNostrEvent(
|
||||
nostrEvent = nostrEvent,
|
||||
synchronizationRelayURLs = synchronizationRelayURLs
|
||||
)
|
||||
|
||||
// Save synchronizationResult
|
||||
database.synchronizeNostrEventResultDao().upsert(
|
||||
SynchronizeNostrEventResult(
|
||||
nostrEventId = nostrEvent.id,
|
||||
relayURL = synchronizeNostrEventRequest.relayURL
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun indexNostrEvent(
|
||||
nostrEvent: NostrEvent,
|
||||
synchronizationRelayURLs: List<String>
|
||||
) {
|
||||
nostrEvent.toPost()?.let { post ->
|
||||
if (post.replyToId != null) {
|
||||
// Find or create placeHolder note that is replyTo
|
||||
val replyingToPost = database.postDao().getPostById(post.replyToId)
|
||||
|
||||
if (replyingToPost == null) {
|
||||
// PlaceHolder and sync
|
||||
// Persist PlaceHolder and sync
|
||||
database.postDao().insert(
|
||||
Post(
|
||||
id = post.replyToId,
|
||||
@@ -74,6 +150,16 @@ abstract class NostrDao(
|
||||
profilePublicKey = nostrEvent.pubKey // Will get overwriting by sync,
|
||||
)
|
||||
)
|
||||
|
||||
// Request a sync for the post being replied to
|
||||
synchronizationRelayURLs.forEach { synchronizationRelayURL ->
|
||||
database.synchronizeNostrEventRequestDao().upsert(
|
||||
SynchronizeNostrEventRequest(
|
||||
eventIds = listOf(post.replyToId).joinToString(","),
|
||||
relayURL = synchronizationRelayURL
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,7 +176,15 @@ abstract class NostrDao(
|
||||
profilePublicKey = nostrEvent.pubKey // Will get overwriting by sync,
|
||||
)
|
||||
)
|
||||
// Sync repostId
|
||||
// Request a sync for the post being reposted
|
||||
synchronizationRelayURLs.forEach { synchronizationRelayURL ->
|
||||
database.synchronizeNostrEventRequestDao().upsert(
|
||||
SynchronizeNostrEventRequest(
|
||||
eventIds = listOf(post.repostId).joinToString(","),
|
||||
relayURL = synchronizationRelayURL
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
database.postDao().insert(post)
|
||||
@@ -111,15 +205,5 @@ abstract class NostrDao(
|
||||
nostrEvent.toZap()?.let { zap ->
|
||||
database.zapDao().upsert(zap)
|
||||
}
|
||||
|
||||
relayURLs.forEach { relayURL ->
|
||||
database.broadcastNostrEventRequestDao().upsert(
|
||||
BroadcastNostrEventRequest(
|
||||
nostrEventId = nostrEvent.id,
|
||||
unsignedNostrEventId = nostrEvent.unsignedNostrEventId,
|
||||
relayURL = relayURL
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package ac.aux.compose.database.dao
|
||||
|
||||
import ac.aux.compose.database.model.SynchronizeNostrEventRequest
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Query
|
||||
import androidx.room.Upsert
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
interface SynchronizeNostrEventRequestDao {
|
||||
@Query("SELECT * FROM SynchronizeNostrEventRequest WHERE status = :status")
|
||||
fun observeSynchronizeNostrEventRequestsByStatus(status: String): Flow<SynchronizeNostrEventRequest?>
|
||||
|
||||
@Upsert
|
||||
fun upsert(synchronizeNostrEventRequest: SynchronizeNostrEventRequest)
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package ac.aux.compose.database.dao
|
||||
|
||||
import ac.aux.compose.database.model.SynchronizeNostrEventRequest
|
||||
import ac.aux.compose.database.model.SynchronizeNostrEventResult
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Query
|
||||
import androidx.room.Upsert
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
interface SynchronizeNostrEventResultDao {
|
||||
@Upsert
|
||||
fun upsert(synchronizeNostrEventResult: SynchronizeNostrEventResult)
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package ac.aux.compose.database.model
|
||||
|
||||
import ac.aux.compose.database.model.traits.NostrEventEntity
|
||||
import ac.aux.compose.database.model.traits.OptionalNostrEventEntity
|
||||
import ac.aux.compose.database.model.traits.TimestampedEntity
|
||||
import ac.aux.compose.database.model.traits.UnsignedNostrEventEntity
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import androidx.room.Index
|
||||
import androidx.room.PrimaryKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import kotlin.time.Clock
|
||||
import kotlin.time.Instant
|
||||
import kotlin.uuid.ExperimentalUuidApi
|
||||
import kotlin.uuid.Uuid
|
||||
|
||||
@OptIn(ExperimentalUuidApi::class)
|
||||
@Entity(
|
||||
foreignKeys = [
|
||||
ForeignKey(
|
||||
entity = NostrEvent::class,
|
||||
parentColumns = ["id"],
|
||||
childColumns = ["nostrEventId"],
|
||||
onDelete = ForeignKey.CASCADE,
|
||||
),
|
||||
ForeignKey(
|
||||
entity = UnsignedNostrEvent::class,
|
||||
parentColumns = ["id"],
|
||||
childColumns = ["unsignedNostrEventId"],
|
||||
onDelete = ForeignKey.CASCADE,
|
||||
),
|
||||
],
|
||||
indices = [
|
||||
Index("nostrEventId"),
|
||||
Index("status"),
|
||||
],
|
||||
)
|
||||
data class SynchronizeNostrEventRequest(
|
||||
@PrimaryKey
|
||||
val id: String = Uuid.generateV4().toHexDashString(),
|
||||
val status: String = "pending",
|
||||
val relayURL: String,
|
||||
val eventIds: String? = null, // TODO: EventIdArray
|
||||
val authorPublicKeys: String? = null, // TODO: PublicKeyArray
|
||||
val kinds: String? = null, // TODO: KindArray
|
||||
val tagName: String? = null,
|
||||
val since: Instant? = null,
|
||||
val until: Instant? = null,
|
||||
val limit: Long? = null,
|
||||
|
||||
override val nostrEventId: HexKey? = null,
|
||||
override val unsignedNostrEventId: Long? = null,
|
||||
override val createdAt: Instant = Clock.System.now(),
|
||||
override val updatedAt: Instant = createdAt,
|
||||
): OptionalNostrEventEntity, UnsignedNostrEventEntity, TimestampedEntity
|
||||
@@ -0,0 +1,40 @@
|
||||
package ac.aux.compose.database.model
|
||||
|
||||
import ac.aux.compose.database.model.traits.NostrEventEntity
|
||||
import ac.aux.compose.database.model.traits.OptionalNostrEventEntity
|
||||
import ac.aux.compose.database.model.traits.TimestampedEntity
|
||||
import ac.aux.compose.database.model.traits.UnsignedNostrEventEntity
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import androidx.room.Index
|
||||
import androidx.room.PrimaryKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import kotlin.time.Clock
|
||||
import kotlin.time.Instant
|
||||
import kotlin.uuid.ExperimentalUuidApi
|
||||
import kotlin.uuid.Uuid
|
||||
|
||||
@OptIn(ExperimentalUuidApi::class)
|
||||
@Entity(
|
||||
foreignKeys = [
|
||||
ForeignKey(
|
||||
entity = NostrEvent::class,
|
||||
parentColumns = ["id"],
|
||||
childColumns = ["nostrEventId"],
|
||||
onDelete = ForeignKey.CASCADE,
|
||||
),
|
||||
],
|
||||
indices = [
|
||||
Index("nostrEventId"),
|
||||
],
|
||||
)
|
||||
data class SynchronizeNostrEventResult(
|
||||
@PrimaryKey
|
||||
val id: String = Uuid.generateV4().toHexDashString(),
|
||||
val relayURL: String,
|
||||
|
||||
override val nostrEventId: HexKey? = null,
|
||||
|
||||
override val createdAt: Instant = Clock.System.now(),
|
||||
override val updatedAt: Instant = createdAt,
|
||||
): OptionalNostrEventEntity, TimestampedEntity
|
||||
@@ -0,0 +1,7 @@
|
||||
package ac.aux.compose.database.model.traits
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
|
||||
interface OptionalNostrEventEntity {
|
||||
val nostrEventId: HexKey?
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import ac.aux.compose.database.model.BroadcastNostrEventReceipt
|
||||
import ac.aux.compose.database.model.BroadcastNostrEventRequest
|
||||
import ac.aux.compose.database.model.NostrEvent
|
||||
import ac.aux.compose.database.model.Profile
|
||||
import ac.aux.compose.database.model.SynchronizeNostrEventRequest
|
||||
import ac.aux.compose.database.model.UnsignedNostrEvent
|
||||
import ac.aux.compose.database.model.intermdiate.LocalBroadcastNostrEventRequest
|
||||
import ac.aux.compose.database.model.intermdiate.LocalProfile
|
||||
@@ -39,6 +40,10 @@ class DatabaseNostrRepository(
|
||||
return database.broadcastNostrEventRequestDao().observeBroadcastNostrEventRequestsByStatus("pending")
|
||||
}
|
||||
|
||||
override suspend fun observePendingSynchronizeNostrEventRequests(): Flow<SynchronizeNostrEventRequest?> {
|
||||
return database.synchronizeNostrEventRequestDao().observeSynchronizeNostrEventRequestsByStatus("pending")
|
||||
}
|
||||
|
||||
override suspend fun createNewProfile(
|
||||
publicKey: HexKey,
|
||||
name: String?,
|
||||
@@ -144,10 +149,33 @@ class DatabaseNostrRepository(
|
||||
logger.d("Processed: $broadcastNostrEventRequest")
|
||||
}
|
||||
|
||||
override suspend fun synchronizeNostrEventRequestProcessed(synchronizeNostrEventRequest: SynchronizeNostrEventRequest) {
|
||||
logger.i("Update local reference: $synchronizeNostrEventRequest")
|
||||
database.synchronizeNostrEventRequestDao().upsert(
|
||||
synchronizeNostrEventRequest.copy(
|
||||
status = "sent",
|
||||
updatedAt = Clock.System.now()
|
||||
)
|
||||
)
|
||||
logger.i("Processed: $synchronizeNostrEventRequest")
|
||||
}
|
||||
|
||||
override suspend fun saveBroadcastReceipt(broadcastNostrEventReceipt: BroadcastNostrEventReceipt) {
|
||||
database.broadcastNostrEventReceiptDao().upsert(
|
||||
broadcastNostrEventReceipt
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun saveNostrEvent(
|
||||
nostrEvent: NostrEvent,
|
||||
synchronizeNostrEventRequest: SynchronizeNostrEventRequest,
|
||||
synchronizationRelayURLs: List<String>
|
||||
) {
|
||||
database.nostrDao().storeNostrEvent(
|
||||
nostrEvent,
|
||||
synchronizeNostrEventRequest,
|
||||
synchronizationRelayURLs = synchronizationRelayURLs
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import ac.aux.compose.database.model.BroadcastNostrEventReceipt
|
||||
import ac.aux.compose.database.model.BroadcastNostrEventRequest
|
||||
import ac.aux.compose.database.model.NostrEvent
|
||||
import ac.aux.compose.database.model.Profile
|
||||
import ac.aux.compose.database.model.SynchronizeNostrEventRequest
|
||||
import ac.aux.compose.database.model.UnsignedNostrEvent
|
||||
import ac.aux.compose.database.model.intermdiate.LocalBroadcastNostrEventRequest
|
||||
import ac.aux.compose.database.model.intermdiate.LocalProfile
|
||||
@@ -17,6 +18,8 @@ interface NostrRepository {
|
||||
|
||||
suspend fun observePendingBroadcastNostrEventRequests(): Flow<List<LocalBroadcastNostrEventRequest>>
|
||||
|
||||
suspend fun observePendingSynchronizeNostrEventRequests(): Flow<SynchronizeNostrEventRequest?>
|
||||
|
||||
suspend fun createNewProfile(
|
||||
publicKey: HexKey,
|
||||
name: String?,
|
||||
@@ -46,8 +49,16 @@ interface NostrRepository {
|
||||
|
||||
suspend fun broadcastProcessed(broadcastNostrEventRequest: BroadcastNostrEventRequest)
|
||||
|
||||
suspend fun synchronizeNostrEventRequestProcessed(synchronizeNostrEventRequest: SynchronizeNostrEventRequest)
|
||||
|
||||
suspend fun saveBroadcastReceipt(broadcastNostrEventReceipt: BroadcastNostrEventReceipt)
|
||||
|
||||
suspend fun saveNostrEvent(
|
||||
nostrEvent: NostrEvent,
|
||||
synchronizeNostrEventRequest: SynchronizeNostrEventRequest,
|
||||
synchronizationRelayURLs: List<String>
|
||||
)
|
||||
|
||||
companion object {
|
||||
val NO_OP_NOSTR_REPOSITORY = object : NostrRepository {
|
||||
override suspend fun observeProfile(publicKey: HexKey): Flow<LocalProfile?> {
|
||||
@@ -62,6 +73,10 @@ interface NostrRepository {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override suspend fun observePendingSynchronizeNostrEventRequests(): Flow<SynchronizeNostrEventRequest?> {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override suspend fun createNewProfile(
|
||||
publicKey: HexKey,
|
||||
name: String?,
|
||||
@@ -99,9 +114,21 @@ interface NostrRepository {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override suspend fun synchronizeNostrEventRequestProcessed(synchronizeNostrEventRequest: SynchronizeNostrEventRequest) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override suspend fun saveBroadcastReceipt(broadcastNostrEventReceipt: BroadcastNostrEventReceipt) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override suspend fun saveNostrEvent(
|
||||
nostrEvent: NostrEvent,
|
||||
synchronizeNostrEventRequest: SynchronizeNostrEventRequest,
|
||||
synchronizationRelayURLs: List<String>
|
||||
) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
package ac.aux.compose.ui.view.model
|
||||
|
||||
import ac.aux.compose.database.model.BroadcastNostrEventReceipt
|
||||
import ac.aux.compose.database.model.NostrEvent
|
||||
import ac.aux.compose.managers.SeedManager
|
||||
import ac.aux.compose.network.NostrEventBroadcaster
|
||||
@@ -35,6 +34,10 @@ import kotlinx.coroutines.flow.getAndUpdate
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import kotlin.collections.set
|
||||
import kotlin.time.Instant
|
||||
|
||||
class NavigationViewModel(
|
||||
@@ -63,6 +66,8 @@ class NavigationViewModel(
|
||||
|
||||
private val logger = Logger.withTag(TAG)
|
||||
|
||||
private val syncingJobs = mutableMapOf<String, Job>()
|
||||
|
||||
val httpClient = HttpClient() {
|
||||
install(WebSockets) {
|
||||
contentConverter = KotlinxWebsocketSerializationConverter(Json {
|
||||
@@ -129,14 +134,125 @@ class NavigationViewModel(
|
||||
}
|
||||
|
||||
private fun observeSyncNostrEventRequests() {
|
||||
logger.i { "observeSyncNostrEventRequests" }
|
||||
|
||||
scope.launch(Dispatchers.IO) {
|
||||
nostrRepository.observePendingSynchronizeNostrEventRequests().collect { synchronizeNostrEventRequestOrNull ->
|
||||
synchronizeNostrEventRequestOrNull?.let { synchronizeNostrEventRequest ->
|
||||
val filterContent = mutableMapOf<String, JsonElement>()
|
||||
|
||||
synchronizeNostrEventRequest.eventIds?.let { eventIds ->
|
||||
filterContent.put("ids", JsonPrimitive(eventIds))
|
||||
}
|
||||
synchronizeNostrEventRequest.authorPublicKeys?.let { authorPublicKeys ->
|
||||
filterContent.put("authors", JsonPrimitive(authorPublicKeys))
|
||||
}
|
||||
synchronizeNostrEventRequest.kinds?.let { kinds ->
|
||||
filterContent.put("kinds", JsonPrimitive(kinds))
|
||||
}
|
||||
synchronizeNostrEventRequest.since?.let { since ->
|
||||
filterContent.put("since", JsonPrimitive(since.toEpochMilliseconds()))
|
||||
}
|
||||
synchronizeNostrEventRequest.until?.let { until ->
|
||||
filterContent.put("until", JsonPrimitive(until.toEpochMilliseconds()))
|
||||
}
|
||||
synchronizeNostrEventRequest.limit?.let { limit ->
|
||||
filterContent.put("limit", JsonPrimitive(limit))
|
||||
}
|
||||
|
||||
val filter = JsonObject(
|
||||
filterContent
|
||||
)
|
||||
logger.d("Syncing: $filter")
|
||||
scope.launch {
|
||||
val webSocketSession = httpClient.webSocketSession(
|
||||
urlString = synchronizeNostrEventRequest.relayURL
|
||||
)
|
||||
|
||||
syncingJobs[synchronizeNostrEventRequest.relayURL] = scope.launch {
|
||||
webSocketSession.send(
|
||||
frame = Frame.Text("[\"REQ\",${synchronizeNostrEventRequest.id},${filter}]")
|
||||
)
|
||||
|
||||
nostrRepository.synchronizeNostrEventRequestProcessed(synchronizeNostrEventRequest)
|
||||
|
||||
webSocketSession.incoming.consumeAsFlow().collect { frame ->
|
||||
when(frame) {
|
||||
is Frame.Text -> {
|
||||
frame.readText().let { text ->
|
||||
try {
|
||||
logger.d("WebSocket Read Text: $text")
|
||||
val result = Json.decodeFromString<JsonArray>(text)
|
||||
|
||||
if (result.firstOrNull()?.text == "EVENT") {
|
||||
result.getOrNull(1)?.text?.let { subscriptionId ->
|
||||
if (subscriptionId == synchronizeNostrEventRequest.id) {
|
||||
|
||||
result.getOrNull(2)?.text?.let { jsonText ->
|
||||
val event = Event.fromJson(
|
||||
jsonText
|
||||
)
|
||||
|
||||
nostrRepository.saveNostrEvent(
|
||||
nostrEvent = NostrEvent(
|
||||
id = event.id,
|
||||
pubKey = event.pubKey,
|
||||
kind = event.kind,
|
||||
content = event.content,
|
||||
tags = event.tags,
|
||||
createdAt = Instant.fromEpochMilliseconds(event.createdAt),
|
||||
sig = event.sig
|
||||
),
|
||||
synchronizeNostrEventRequest,
|
||||
synchronizationRelayURLs = Relays.eventPublishRelaySet.map { normalizedRelayUrl -> normalizedRelayUrl.url }
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logger.d("Unsupported: $text")
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
logger.e("Error processing receipt: $text", e)
|
||||
}
|
||||
}
|
||||
// Syncing doesn't necessary have to close after result..
|
||||
webSocketSession.close()
|
||||
syncingJobs.remove(synchronizeNostrEventRequest.relayURL)?.cancel()
|
||||
}
|
||||
is Frame.Close -> {
|
||||
logger.d("Close $frame")
|
||||
}
|
||||
is Frame.Ping -> {
|
||||
logger.d("Ping: $frame")
|
||||
}
|
||||
is Frame.Pong -> {
|
||||
logger.d("Pong: $frame")
|
||||
}
|
||||
is Frame.Binary -> {
|
||||
logger.d("Binary Message: $frame")
|
||||
}
|
||||
else -> {
|
||||
logger.d("Unsupported frame: $frame")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.i("Websocket exit")
|
||||
}
|
||||
|
||||
syncingJobs[synchronizeNostrEventRequest.relayURL]?.join()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun observePendingBroadcastNostrEventRequests() {
|
||||
logger.i { "observePendingBroadcastNostrEventRequests" }
|
||||
|
||||
scope.launch(Dispatchers.IO) {
|
||||
|
||||
nostrRepository.observePendingBroadcastNostrEventRequests().collect { localBroadcastNostrEventRequests ->
|
||||
localBroadcastNostrEventRequests.forEach { localBroadcastNostrEventRequest ->
|
||||
nostrEventBroadcaster.broadcastEvent(
|
||||
|
||||
Reference in New Issue
Block a user