diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/database/AuxDatabase.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/AuxDatabase.kt index 250d6f24..d6552251 100644 --- a/composeApp/src/commonMain/kotlin/ac/aux/compose/database/AuxDatabase.kt +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/AuxDatabase.kt @@ -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 diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/database/dao/BroadcastNostrEventReceiptDao.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/dao/BroadcastNostrEventReceiptDao.kt new file mode 100644 index 00000000..cab796e1 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/dao/BroadcastNostrEventReceiptDao.kt @@ -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 + + @Query("SELECT * FROM BroadcastNostrEventReceipt WHERE id = :id") + fun getBroadcastNostrEventReceiptById(id: Long): Flow + + @Query("SELECT * FROM BroadcastNostrEventReceipt WHERE nostrEventId = :nostrEventId") + fun getBroadcastNostrEventReceiptByNostrEventId(nostrEventId: String): Flow + + @Upsert + suspend fun upsert(broadcastNostrEventReceipt: BroadcastNostrEventReceipt) +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/database/dao/BroadcastNostrEventRequestDao.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/dao/BroadcastNostrEventRequestDao.kt new file mode 100644 index 00000000..d5ce4d86 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/dao/BroadcastNostrEventRequestDao.kt @@ -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 + + @Query("SELECT * FROM BroadcastNostrEventRequest WHERE id = :id") + fun getBroadcastNostrEventRequestById(id: Long): Flow + + @Query("SELECT * FROM BroadcastNostrEventRequest WHERE nostrEventId = :nostrEventId") + fun getBroadcastNostrEventRequestByNostrEventId(nostrEventId: String): Flow + + @Upsert + suspend fun upsert(broadcastNostrEventRequest: BroadcastNostrEventRequest) +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/database/dao/NostrDao.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/dao/NostrDao.kt index d9aaa53d..b7d93aef 100644 --- a/composeApp/src/commonMain/kotlin/ac/aux/compose/database/dao/NostrDao.kt +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/dao/NostrDao.kt @@ -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 = 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( diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/database/dao/ProfileDao.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/dao/ProfileDao.kt index b4ff11cc..0071ff1d 100644 --- a/composeApp/src/commonMain/kotlin/ac/aux/compose/database/dao/ProfileDao.kt +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/dao/ProfileDao.kt @@ -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 - @Insert + @Insert( + onConflict = OnConflictStrategy.IGNORE + ) suspend fun insert(profile: Profile) + + @Upsert + suspend fun upsert(profile: Profile) } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/database/dao/ReactionDao.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/dao/ReactionDao.kt index 572d5448..fe4d979f 100644 --- a/composeApp/src/commonMain/kotlin/ac/aux/compose/database/dao/ReactionDao.kt +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/dao/ReactionDao.kt @@ -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 - @Insert - suspend fun insert(reaction: Reaction) + @Upsert + suspend fun upsert(reaction: Reaction) } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/database/dao/RepostDao.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/dao/RepostDao.kt index f8c2251b..9562ec3d 100644 --- a/composeApp/src/commonMain/kotlin/ac/aux/compose/database/dao/RepostDao.kt +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/dao/RepostDao.kt @@ -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 - @Insert - suspend fun insert(repost: Repost) + @Upsert + suspend fun upsert(repost: Repost) } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/database/dao/ZapDao.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/dao/ZapDao.kt index 1578bf66..022b19b0 100644 --- a/composeApp/src/commonMain/kotlin/ac/aux/compose/database/dao/ZapDao.kt +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/dao/ZapDao.kt @@ -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 - @Insert - suspend fun insert(zap: Zap) + @Upsert + suspend fun upsert(zap: Zap) } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/database/model/BroadcastNostrEventReceipt.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/model/BroadcastNostrEventReceipt.kt index bbb13283..6f9bc79c 100644 --- a/composeApp/src/commonMain/kotlin/ac/aux/compose/database/model/BroadcastNostrEventReceipt.kt +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/model/BroadcastNostrEventReceipt.kt @@ -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, diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/database/model/BroadcastNostrEventRequest.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/model/BroadcastNostrEventRequest.kt index cc1a14b6..62d89845 100644 --- a/composeApp/src/commonMain/kotlin/ac/aux/compose/database/model/BroadcastNostrEventRequest.kt +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/model/BroadcastNostrEventRequest.kt @@ -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, diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/database/model/NostrEvent.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/model/NostrEvent.kt index edbbd448..f937957a 100644 --- a/composeApp/src/commonMain/kotlin/ac/aux/compose/database/model/NostrEvent.kt +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/model/NostrEvent.kt @@ -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( + 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( + 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( + 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( + 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( + 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 + } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/database/model/Repost.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/model/Repost.kt index 1abe7ed1..0a0efc9a 100644 --- a/composeApp/src/commonMain/kotlin/ac/aux/compose/database/model/Repost.kt +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/model/Repost.kt @@ -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?