Storing nostr events and handling their different kinds

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

View File

@@ -1,6 +1,8 @@
package ac.aux.compose.database
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.NostrEventDao
import ac.aux.compose.database.dao.PostDao
@@ -31,6 +33,8 @@ import androidx.room.TypeConverters
)
@TypeConverters(Converters::class)
abstract class AuxDatabase: RoomDatabase() {
abstract fun broadcastNostrEventReceiptDao(): BroadcastNostrEventReceiptDao
abstract fun broadcastNostrEventRequestDao(): BroadcastNostrEventRequestDao
abstract fun nostrDao(): NostrDao
abstract fun nostrEventDao(): NostrEventDao

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -1,8 +1,10 @@
package ac.aux.compose.database.dao
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.Post
import ac.aux.compose.database.model.Profile
import androidx.room.Dao
import androidx.room.Embedded
import androidx.room.Relation
@@ -13,13 +15,48 @@ abstract class NostrDao(
private val database: AuxDatabase
) {
@Transaction
open suspend fun insertNostrEvent(nostrEvent: NostrEvent) {
// TODO: Find or create profile with nostrEvent.pubkey
open suspend fun insertNostrEvent(
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)
// TODO: Get post from nostrEvent
// database.postDao().insert(nostrEvent.post) // TODO: Make use of the nostrEventId...
nostrEvent.toPost()?.let { post ->
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(

View File

@@ -5,7 +5,9 @@ import ac.aux.compose.database.model.Post
import ac.aux.compose.database.model.Profile
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Upsert
import kotlinx.coroutines.flow.Flow
@Dao
@@ -16,6 +18,11 @@ interface ProfileDao {
@Query("SELECT * FROM Profile WHERE publicKey = :publicKey")
fun getProfileByPublicKey(publicKey: String): Flow<Profile?>
@Insert
@Insert(
onConflict = OnConflictStrategy.IGNORE
)
suspend fun insert(profile: Profile)
@Upsert
suspend fun upsert(profile: Profile)
}

View File

@@ -1,12 +1,10 @@
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 androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query
import androidx.room.Upsert
import kotlinx.coroutines.flow.Flow
@Dao
@@ -17,6 +15,6 @@ interface ReactionDao {
@Query("SELECT * FROM Reaction WHERE id = :id")
fun getReactionById(id: String): Flow<Reaction?>
@Insert
suspend fun insert(reaction: Reaction)
@Upsert
suspend fun upsert(reaction: Reaction)
}

View File

@@ -1,11 +1,10 @@
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 androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query
import androidx.room.Upsert
import kotlinx.coroutines.flow.Flow
@Dao
@@ -16,6 +15,6 @@ interface RepostDao {
@Query("SELECT * FROM Repost WHERE id = :id")
fun getRepostById(id: String): Flow<Repost?>
@Insert
suspend fun insert(repost: Repost)
@Upsert
suspend fun upsert(repost: Repost)
}

View File

@@ -1,11 +1,10 @@
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 androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query
import androidx.room.Upsert
import kotlinx.coroutines.flow.Flow
@Dao
@@ -13,6 +12,6 @@ interface ZapDao {
@Query("SELECT * FROM Zap WHERE id = :id")
fun getZapById(id: String): Flow<Zap?>
@Insert
suspend fun insert(zap: Zap)
@Upsert
suspend fun upsert(zap: Zap)
}

View File

@@ -23,7 +23,7 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey
)
data class BroadcastNostrEventReceipt(
@PrimaryKey(autoGenerate = true)
val id: Long,
val id: Long = 0,
override val nostrEventId: HexKey,
val isAccepted: Boolean,
val relayURL: String,

View File

@@ -21,9 +21,9 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey
Index("status"),
],
)
data class BroadcastRequest(
data class BroadcastNostrEventRequest(
@PrimaryKey(autoGenerate = true)
val id: Long,
val id: Long = 0,
override val nostrEventId: HexKey,
val status: String = "pending",
val relayURL: String,

View File

@@ -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.SoftDeletableEntity
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.Fts4
import androidx.room.Index
import androidx.room.PrimaryKey
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.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.Instant
@@ -32,6 +36,8 @@ data class NostrEvent(
override val deletedAt: Instant? = null,
override val broadcastedAt: Instant? = null,
): TimestampedEntity, LocalStoreEntity, BroadcastableEntity, SoftDeletableEntity {
val logger = Logger.withTag("NostrEvent")
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class != other::class) return false
@@ -59,4 +65,163 @@ data class NostrEvent(
result = 31 * result + sig.hashCode()
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
}
}

View File

@@ -12,8 +12,6 @@ import androidx.room.ForeignKey
import androidx.room.Index
import androidx.room.PrimaryKey
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.Instant
@@ -36,20 +34,20 @@ import kotlin.time.Instant
ForeignKey(
entity = Post::class,
parentColumns = ["id"],
childColumns = ["postId"],
childColumns = ["repostedPostId"],
onDelete = ForeignKey.CASCADE,
),
],
indices = [
Index("profilePublicKey"),
Index("nostrEventId"),
Index("postId")
Index("repostedPostId")
],
)
data class Repost(
@PrimaryKey
val id: HexKey,
val postId: HexKey? = null,
val repostedPostId: HexKey? = null,
val content: String,
override val nostrEventId: HexKey, // Might be a replaceable nostr event?