Storing nostr events and handling their different kinds

This commit is contained in:
Kgothatso Ngako committed 2026-03-25 01:59:19 +02:00
1 parent 1d30ba04d3
commit 4f4db2a9a9
12 files changed
+282 -30

No files matched your search

@@ -1,6 +1,8 @@
package ac.aux.compose.database package ac.aux.compose.database
import ac.aux.compose.database.converters.Converters import ac.aux.compose.database.converters.Converters
import ac.aux.compose.database.dao.BroadcastNostrEventReceiptDao
import ac.aux.compose.database.dao.BroadcastNostrEventRequestDao
import ac.aux.compose.database.dao.NostrDao import ac.aux.compose.database.dao.NostrDao
import ac.aux.compose.database.dao.NostrEventDao import ac.aux.compose.database.dao.NostrEventDao
import ac.aux.compose.database.dao.PostDao import ac.aux.compose.database.dao.PostDao
@@ -31,6 +33,8 @@ import androidx.room.TypeConverters
) )
@TypeConverters(Converters::class) @TypeConverters(Converters::class)
abstract class AuxDatabase: RoomDatabase() { abstract class AuxDatabase: RoomDatabase() {
abstract fun broadcastNostrEventReceiptDao(): BroadcastNostrEventReceiptDao
abstract fun broadcastNostrEventRequestDao(): BroadcastNostrEventRequestDao
abstract fun nostrDao(): NostrDao abstract fun nostrDao(): NostrDao
abstract fun nostrEventDao(): NostrEventDao abstract fun nostrEventDao(): NostrEventDao
@@ -0,0 +1,23 @@
package ac.aux.compose.database.dao
import ac.aux.compose.database.model.BroadcastNostrEventReceipt
import ac.aux.compose.database.model.BroadcastNostrEventRequest
import androidx.room.Dao
import androidx.room.Query
import androidx.room.Upsert
import kotlinx.coroutines.flow.Flow
@Dao
interface BroadcastNostrEventReceiptDao {
@Query("SELECT * FROM BroadcastNostrEventReceipt")
fun getAllBroadcastNostrEventReceipts(): List<BroadcastNostrEventReceipt>
@Query("SELECT * FROM BroadcastNostrEventReceipt WHERE id = :id")
fun getBroadcastNostrEventReceiptById(id: Long): Flow<BroadcastNostrEventReceipt?>
@Query("SELECT * FROM BroadcastNostrEventReceipt WHERE nostrEventId = :nostrEventId")
fun getBroadcastNostrEventReceiptByNostrEventId(nostrEventId: String): Flow<BroadcastNostrEventReceipt?>
@Upsert
suspend fun upsert(broadcastNostrEventReceipt: BroadcastNostrEventReceipt)
}
@@ -0,0 +1,22 @@
package ac.aux.compose.database.dao
import ac.aux.compose.database.model.BroadcastNostrEventRequest
import androidx.room.Dao
import androidx.room.Query
import androidx.room.Upsert
import kotlinx.coroutines.flow.Flow
@Dao
interface BroadcastNostrEventRequestDao {
@Query("SELECT * FROM BroadcastNostrEventRequest")
fun getAllBroadcastNostrEventRequests(): List<BroadcastNostrEventRequest>
@Query("SELECT * FROM BroadcastNostrEventRequest WHERE id = :id")
fun getBroadcastNostrEventRequestById(id: Long): Flow<BroadcastNostrEventRequest?>
@Query("SELECT * FROM BroadcastNostrEventRequest WHERE nostrEventId = :nostrEventId")
fun getBroadcastNostrEventRequestByNostrEventId(nostrEventId: String): Flow<BroadcastNostrEventRequest?>
@Upsert
suspend fun upsert(broadcastNostrEventRequest: BroadcastNostrEventRequest)
}
@@ -1,8 +1,10 @@
package ac.aux.compose.database.dao package ac.aux.compose.database.dao
import ac.aux.compose.database.AuxDatabase import ac.aux.compose.database.AuxDatabase
import ac.aux.compose.database.model.BroadcastNostrEventRequest
import ac.aux.compose.database.model.NostrEvent import ac.aux.compose.database.model.NostrEvent
import ac.aux.compose.database.model.Post import ac.aux.compose.database.model.Post
import ac.aux.compose.database.model.Profile
import androidx.room.Dao import androidx.room.Dao
import androidx.room.Embedded import androidx.room.Embedded
import androidx.room.Relation import androidx.room.Relation
@@ -13,13 +15,48 @@ abstract class NostrDao(
private val database: AuxDatabase private val database: AuxDatabase
) { ) {
@Transaction @Transaction
open suspend fun insertNostrEvent(nostrEvent: NostrEvent) { open suspend fun insertNostrEvent(
// TODO: Find or create profile with nostrEvent.pubkey nostrEvent: NostrEvent,
relayURLs: List<String> = emptyList()
) {
database.profileDao().insert(
Profile(
publicKey = nostrEvent.pubKey,
nostrEventId = nostrEvent.id // This is illegal... because the nostrEventId should be the one with all the profile information
)
)
// TODO: Find note that is replyTo
database.nostrEventDao().insert(nostrEvent) database.nostrEventDao().insert(nostrEvent)
// TODO: Get post from nostrEvent nostrEvent.toPost()?.let { post ->
// database.postDao().insert(nostrEvent.post) // TODO: Make use of the nostrEventId... database.postDao().insert(post)
}
nostrEvent.toProfile()?.let { profile ->
database.profileDao().upsert(profile)
}
nostrEvent.toReaction()?.let { reaction ->
database.reactionDao().upsert(reaction)
}
nostrEvent.toRepost()?.let { repost ->
database.repostDao().upsert(repost)
}
nostrEvent.toZap()?.let { zap ->
database.zapDao().upsert(zap)
}
// Submit a broadcastRequest...
relayURLs.forEach { relayURL ->
database.broadcastNostrEventRequestDao().upsert(
BroadcastNostrEventRequest(
nostrEventId = nostrEvent.id,
relayURL = relayURL
)
)
}
} }
data class NostrEventAndPost( data class NostrEventAndPost(
@@ -5,7 +5,9 @@ import ac.aux.compose.database.model.Post
import ac.aux.compose.database.model.Profile import ac.aux.compose.database.model.Profile
import androidx.room.Dao import androidx.room.Dao
import androidx.room.Insert import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query import androidx.room.Query
import androidx.room.Upsert
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@Dao @Dao
@@ -16,6 +18,11 @@ interface ProfileDao {
@Query("SELECT * FROM Profile WHERE publicKey = :publicKey") @Query("SELECT * FROM Profile WHERE publicKey = :publicKey")
fun getProfileByPublicKey(publicKey: String): Flow<Profile?> fun getProfileByPublicKey(publicKey: String): Flow<Profile?>
@Insert @Insert(
onConflict = OnConflictStrategy.IGNORE
)
suspend fun insert(profile: Profile) suspend fun insert(profile: Profile)
@Upsert
suspend fun upsert(profile: Profile)
} }
@@ -1,12 +1,10 @@
package ac.aux.compose.database.dao package ac.aux.compose.database.dao
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.Reaction import ac.aux.compose.database.model.Reaction
import androidx.room.Dao import androidx.room.Dao
import androidx.room.Insert import androidx.room.Insert
import androidx.room.Query import androidx.room.Query
import androidx.room.Upsert
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@Dao @Dao
@@ -17,6 +15,6 @@ interface ReactionDao {
@Query("SELECT * FROM Reaction WHERE id = :id") @Query("SELECT * FROM Reaction WHERE id = :id")
fun getReactionById(id: String): Flow<Reaction?> fun getReactionById(id: String): Flow<Reaction?>
@Insert @Upsert
suspend fun insert(reaction: Reaction) suspend fun upsert(reaction: Reaction)
} }
@@ -1,11 +1,10 @@
package ac.aux.compose.database.dao package ac.aux.compose.database.dao
import ac.aux.compose.database.model.NostrEvent
import ac.aux.compose.database.model.Post
import ac.aux.compose.database.model.Repost import ac.aux.compose.database.model.Repost
import androidx.room.Dao import androidx.room.Dao
import androidx.room.Insert import androidx.room.Insert
import androidx.room.Query import androidx.room.Query
import androidx.room.Upsert
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@Dao @Dao
@@ -16,6 +15,6 @@ interface RepostDao {
@Query("SELECT * FROM Repost WHERE id = :id") @Query("SELECT * FROM Repost WHERE id = :id")
fun getRepostById(id: String): Flow<Repost?> fun getRepostById(id: String): Flow<Repost?>
@Insert @Upsert
suspend fun insert(repost: Repost) suspend fun upsert(repost: Repost)
} }
@@ -1,11 +1,10 @@
package ac.aux.compose.database.dao package ac.aux.compose.database.dao
import ac.aux.compose.database.model.NostrEvent
import ac.aux.compose.database.model.Post
import ac.aux.compose.database.model.Zap import ac.aux.compose.database.model.Zap
import androidx.room.Dao import androidx.room.Dao
import androidx.room.Insert import androidx.room.Insert
import androidx.room.Query import androidx.room.Query
import androidx.room.Upsert
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@Dao @Dao
@@ -13,6 +12,6 @@ interface ZapDao {
@Query("SELECT * FROM Zap WHERE id = :id") @Query("SELECT * FROM Zap WHERE id = :id")
fun getZapById(id: String): Flow<Zap?> fun getZapById(id: String): Flow<Zap?>
@Insert @Upsert
suspend fun insert(zap: Zap) suspend fun upsert(zap: Zap)
} }
@@ -23,7 +23,7 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey
) )
data class BroadcastNostrEventReceipt( data class BroadcastNostrEventReceipt(
@PrimaryKey(autoGenerate = true) @PrimaryKey(autoGenerate = true)
val id: Long, val id: Long = 0,
override val nostrEventId: HexKey, override val nostrEventId: HexKey,
val isAccepted: Boolean, val isAccepted: Boolean,
val relayURL: String, val relayURL: String,
@@ -21,9 +21,9 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey
Index("status"), Index("status"),
], ],
) )
data class BroadcastRequest( data class BroadcastNostrEventRequest(
@PrimaryKey(autoGenerate = true) @PrimaryKey(autoGenerate = true)
val id: Long, val id: Long = 0,
override val nostrEventId: HexKey, override val nostrEventId: HexKey,
val status: String = "pending", val status: String = "pending",
val relayURL: String, val relayURL: String,
@@ -4,15 +4,19 @@ import ac.aux.compose.database.model.traits.BroadcastableEntity
import ac.aux.compose.database.model.traits.LocalStoreEntity import ac.aux.compose.database.model.traits.LocalStoreEntity
import ac.aux.compose.database.model.traits.SoftDeletableEntity import ac.aux.compose.database.model.traits.SoftDeletableEntity
import ac.aux.compose.database.model.traits.TimestampedEntity import ac.aux.compose.database.model.traits.TimestampedEntity
import ac.aux.compose.database.model.traits.UserViewableEntity
import androidx.room.ColumnInfo
import androidx.room.Entity import androidx.room.Entity
import androidx.room.Fts4
import androidx.room.Index
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import co.touchlab.kermit.Logger
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.core.TagArray import com.vitorpamplona.quartz.nip01Core.core.TagArray
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
import com.vitorpamplona.quartz.nip18Reposts.RepostEvent
import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
import com.vitorpamplona.quartz.utils.EventFactory
import kotlin.String
import kotlin.time.Clock import kotlin.time.Clock
import kotlin.time.Instant import kotlin.time.Instant
@@ -32,6 +36,8 @@ data class NostrEvent(
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 {
val logger = Logger.withTag("NostrEvent")
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
if (other == null || this::class != other::class) return false if (other == null || this::class != other::class) return false
@@ -59,4 +65,163 @@ data class NostrEvent(
result = 31 * result + sig.hashCode() result = 31 * result + sig.hashCode()
return result return result
} }
fun toPost(): Post? = try {
if (kind == TextNoteEvent.KIND) {
val textNote = EventFactory.create<TextNoteEvent>(
id = id,
pubKey = pubKey,
createdAt = createdAt.toEpochMilliseconds(),
kind = kind,
tags = tags,
content = content,
sig = sig
)
return Post(
id = textNote.id,
repostId = null,
quote = null,
content = textNote.content,
nostrEventId = id,
replyToId = textNote.replyingTo(),
profilePublicKey = textNote.pubKey,
createdAt = Instant.fromEpochMilliseconds(textNote.createdAt),
updatedAt = Instant.fromEpochMilliseconds(textNote.createdAt),
)
}
return null
} catch (e: Throwable) {
logger.e("Failed to return Post: ", e)
return null
}
fun toProfile(): Profile? = try {
if (kind == MetadataEvent.KIND) {
val metadataEvent = EventFactory.create<MetadataEvent>(
id = id,
pubKey = pubKey,
createdAt = createdAt.toEpochMilliseconds(),
kind = kind,
tags = tags,
content = content,
sig = sig
)
return metadataEvent.contactMetaData()?.let { userMetadata ->
return Profile(
publicKey = pubKey,
nostrEventId = id,
createdAt = Instant.fromEpochMilliseconds(metadataEvent.createdAt),
updatedAt = Instant.fromEpochMilliseconds(metadataEvent.createdAt),
userName = userMetadata.name,
displayName = userMetadata.displayName,
picture = userMetadata.picture,
banner = userMetadata.banner,
website = userMetadata.website,
about = userMetadata.about,
bot = userMetadata.bot,
pronouns = userMetadata.pronouns,
nip05 = userMetadata.nip05,
domain = userMetadata.domain,
lud06 = userMetadata.lud06,
lud16 = userMetadata.lud16,
twitter = userMetadata.twitter
)
}
}
return null
} catch (e: Throwable) {
logger.e("Failed to return Profile: ", e)
return null
}
fun toReaction(): Reaction? = try {
if (kind == ReactionEvent.KIND) {
val reactionEvent = EventFactory.create<ReactionEvent>(
id = id,
pubKey = pubKey,
createdAt = createdAt.toEpochMilliseconds(),
kind = kind,
tags = tags,
content = content,
sig = sig
)
return reactionEvent.originalPost().firstOrNull()?.let { reactedTo ->
Reaction(
id = reactionEvent.id,
content = reactionEvent.content,
nostrEventId = id,
replyToId = reactedTo,
profilePublicKey = reactionEvent.pubKey,
createdAt = Instant.fromEpochMilliseconds(reactionEvent.createdAt),
updatedAt = Instant.fromEpochMilliseconds(reactionEvent.createdAt),
)
}
}
return null
} catch (e: Throwable) {
logger.e("Failed to return Reaction: ", e)
return null
}
fun toRepost(): Repost? = try {
if (kind == RepostEvent.KIND) {
val repostEvent = EventFactory.create<RepostEvent>(
id = id,
pubKey = pubKey,
createdAt = createdAt.toEpochMilliseconds(),
kind = kind,
tags = tags,
content = content,
sig = sig
)
return repostEvent.boostedEventId()?.let { repostedPostId ->
Repost(
id = repostEvent.id,
repostedPostId = repostedPostId,
content = repostEvent.content,
nostrEventId = id,
profilePublicKey = repostEvent.pubKey,
createdAt = Instant.fromEpochMilliseconds(repostEvent.createdAt),
updatedAt = Instant.fromEpochMilliseconds(repostEvent.createdAt),
)
}
}
return null
} catch (e: Throwable) {
logger.e("Failed to return Repost: ", e)
return null
}
fun toZap(): Zap? = try {
if (kind == LnZapEvent.KIND) {
val zapEvent = EventFactory.create<LnZapEvent>(
id = id,
pubKey = pubKey,
createdAt = createdAt.toEpochMilliseconds(),
kind = kind,
tags = tags,
content = content,
sig = sig
)
// TODO: Return Zap...
return null
}
return null
} catch (e: Throwable) {
logger.e("Failed to return Zap: ", e)
return null
}
} }
@@ -12,8 +12,6 @@ import androidx.room.ForeignKey
import androidx.room.Index import androidx.room.Index
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
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.TagArray
import kotlin.time.Clock import kotlin.time.Clock
import kotlin.time.Instant import kotlin.time.Instant
@@ -36,20 +34,20 @@ import kotlin.time.Instant
ForeignKey( ForeignKey(
entity = Post::class, entity = Post::class,
parentColumns = ["id"], parentColumns = ["id"],
childColumns = ["postId"], childColumns = ["repostedPostId"],
onDelete = ForeignKey.CASCADE, onDelete = ForeignKey.CASCADE,
), ),
], ],
indices = [ indices = [
Index("profilePublicKey"), Index("profilePublicKey"),
Index("nostrEventId"), Index("nostrEventId"),
Index("postId") Index("repostedPostId")
], ],
) )
data class Repost( data class Repost(
@PrimaryKey @PrimaryKey
val id: HexKey, val id: HexKey,
val postId: HexKey? = null, val repostedPostId: HexKey? = null,
val content: String, val content: String,
override val nostrEventId: HexKey, // Might be a replaceable nostr event? override val nostrEventId: HexKey, // Might be a replaceable nostr event?