diff --git a/composeApp/schemas/ac.cord.auxiliary.compose.database.AuxDatabase/1.json b/composeApp/schemas/ac.cord.auxiliary.compose.database.AuxDatabase/1.json index 9c86b76c..6ba25f0d 100644 --- a/composeApp/schemas/ac.cord.auxiliary.compose.database.AuxDatabase/1.json +++ b/composeApp/schemas/ac.cord.auxiliary.compose.database.AuxDatabase/1.json @@ -2,7 +2,7 @@ "formatVersion": 1, "database": { "version": 1, - "identityHash": "37219e58733785a0530005387bc4e1f4", + "identityHash": "4e55adbd5b9ce2c2de417983fce1b078", "entities": [ { "tableName": "BroadcastNostrEventReceipt", @@ -179,6 +179,89 @@ } ] }, + { + "tableName": "Connection", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sourcePublicKey` TEXT NOT NULL, `destinationPublicKey` TEXT NOT NULL, `nostrEventId` TEXT NOT NULL, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL, `savedAt` INTEGER NOT NULL, `deletedAt` INTEGER, `broadcastedAt` INTEGER, PRIMARY KEY(`sourcePublicKey`, `destinationPublicKey`), FOREIGN KEY(`sourcePublicKey`) REFERENCES `Profile`(`publicKey`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`destinationPublicKey`) REFERENCES `Profile`(`publicKey`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "sourcePublicKey", + "columnName": "sourcePublicKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "destinationPublicKey", + "columnName": "destinationPublicKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "nostrEventId", + "columnName": "nostrEventId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "updatedAt", + "columnName": "updatedAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "savedAt", + "columnName": "savedAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "deletedAt", + "columnName": "deletedAt", + "affinity": "INTEGER" + }, + { + "fieldPath": "broadcastedAt", + "columnName": "broadcastedAt", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "sourcePublicKey", + "destinationPublicKey" + ] + }, + "foreignKeys": [ + { + "table": "Profile", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "sourcePublicKey" + ], + "referencedColumns": [ + "publicKey" + ] + }, + { + "table": "Profile", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "destinationPublicKey" + ], + "referencedColumns": [ + "publicKey" + ] + } + ] + }, { "tableName": "NostrEvent", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `pubKey` TEXT NOT NULL, `kind` INTEGER NOT NULL, `tags` TEXT NOT NULL, `content` TEXT NOT NULL, `sig` TEXT NOT NULL, `taggedNostrEventId` TEXT, `taggedNostrEventRelayUrl` TEXT, `taggedPublicKey` TEXT, `taggedPublicKeyRelayUrl` TEXT, `unsignedNostrEventId` INTEGER, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL, `savedAt` INTEGER NOT NULL, `deletedAt` INTEGER, `broadcastedAt` INTEGER, PRIMARY KEY(`id`))", @@ -1453,7 +1536,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, '37219e58733785a0530005387bc4e1f4')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '4e55adbd5b9ce2c2de417983fce1b078')" ] } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/AuxDatabase.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/AuxDatabase.kt index 2b3a7fa3..a6d04588 100644 --- a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/AuxDatabase.kt +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/AuxDatabase.kt @@ -3,6 +3,7 @@ package ac.cord.auxiliary.compose.database import ac.cord.auxiliary.compose.database.converters.AuxConverters import ac.cord.auxiliary.compose.database.dao.BroadcastNostrEventReceiptDao import ac.cord.auxiliary.compose.database.dao.BroadcastNostrEventRequestDao +import ac.cord.auxiliary.compose.database.dao.ConnectionDao import ac.cord.auxiliary.compose.database.dao.NostrDao import ac.cord.auxiliary.compose.database.dao.NostrEventDao import ac.cord.auxiliary.compose.database.dao.PostDao @@ -17,6 +18,7 @@ import ac.cord.auxiliary.compose.database.dao.UnsignedNostrEventDao import ac.cord.auxiliary.compose.database.dao.ZapDao import ac.cord.auxiliary.compose.database.model.BroadcastNostrEventReceipt import ac.cord.auxiliary.compose.database.model.BroadcastNostrEventRequest +import ac.cord.auxiliary.compose.database.model.Connection import ac.cord.auxiliary.compose.database.model.NostrEvent import ac.cord.auxiliary.compose.database.model.Post import ac.cord.auxiliary.compose.database.model.Profile @@ -41,6 +43,7 @@ val GENESIS_AT = Instant.fromEpochMilliseconds(1231006505000L) entities = [ BroadcastNostrEventReceipt::class, BroadcastNostrEventRequest::class, + Connection::class, NostrEvent::class, Post::class, Profile::class, @@ -59,6 +62,8 @@ val GENESIS_AT = Instant.fromEpochMilliseconds(1231006505000L) abstract class AuxDatabase: RoomDatabase() { abstract fun broadcastNostrEventReceiptDao(): BroadcastNostrEventReceiptDao abstract fun broadcastNostrEventRequestDao(): BroadcastNostrEventRequestDao + abstract fun connectionDao(): ConnectionDao + abstract fun nostrDao(): NostrDao abstract fun nostrEventDao(): NostrEventDao diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/dao/ConnectionDao.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/dao/ConnectionDao.kt new file mode 100644 index 00000000..33067152 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/dao/ConnectionDao.kt @@ -0,0 +1,31 @@ +package ac.cord.auxiliary.compose.database.dao + +import ac.cord.auxiliary.compose.database.model.Connection +import ac.cord.auxiliary.compose.database.model.Profile +import ac.cord.auxiliary.compose.database.model.intermdiate.LocalFollowers +import ac.cord.auxiliary.compose.database.model.intermdiate.LocalFollowing +import ac.cord.auxiliary.compose.database.model.intermdiate.LocalProfile +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.Query +import androidx.room.Upsert +import fr.acinq.bitcoin.PublicKey + +@Dao +interface ConnectionDao { + @Query("SELECT * FROM Connection LIMIT :limit") + fun getAllConnections(limit: Int = 21): List + + @Query("SELECT * FROM Connection WHERE sourcePublicKey = :sourcePublicKey") + fun getPublicKeyFollowing(sourcePublicKey: String): LocalFollowing? + + @Query("SELECT * FROM Connection WHERE destinationPublicKey = :destinationPublicKey") + fun getPublicKeyFollowers(destinationPublicKey: String): LocalFollowers? + + @Insert + suspend fun insertPlaceholderProfile(connection: Connection) + + @Upsert + suspend fun upsert(connection: Connection) + +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/dao/NostrDao.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/dao/NostrDao.kt index ff55d1aa..9ec1a2fa 100644 --- a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/dao/NostrDao.kt +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/dao/NostrDao.kt @@ -3,6 +3,7 @@ package ac.cord.auxiliary.compose.database.dao import ac.cord.auxiliary.compose.database.AuxDatabase import ac.cord.auxiliary.compose.database.GENESIS_AT import ac.cord.auxiliary.compose.database.model.BroadcastNostrEventRequest +import ac.cord.auxiliary.compose.database.model.Connection import ac.cord.auxiliary.compose.database.model.NostrEvent import ac.cord.auxiliary.compose.database.model.Post import ac.cord.auxiliary.compose.database.model.Profile @@ -359,5 +360,66 @@ abstract class NostrDao( database.zapDao().upsert(zap) } + + if (nostrEvent.kind == ContactListEvent.KIND) { + val contactListEvent = ContactListEvent( + id = nostrEvent.id, + tags = nostrEvent.tags, + pubKey = nostrEvent.pubKey, + createdAt = nostrEvent.createdAt.epochSeconds, + content = nostrEvent.content, + sig = nostrEvent.sig + ) + logger.i("Store contact list: $contactListEvent") + + contactListEvent.verifiedFollowKeySet().forEach { followedHexKEys -> + + // Find or create profile with the pubKey... if not found submit a sync request... + val profile = database.profileDao().getProfileByPublicKey(followedHexKEys) + + if (profile == null) { + val placeHolderProfile = Profile( + publicKey = nostrEvent.pubKey, + createdAt = GENESIS_AT, + nostrEventId = nostrEvent.id, // Will get overwriting by sync, + ) + database.profileDao().insertPlaceholderProfile(placeHolderProfile) + + database.synchronizeNostrEventRequestDao().insert( + synchronizationRelayURLs.map { synchronizationRelayURL -> + SynchronizeNostrEventRequest( + purpose = "synchronization", + synchronizationFilters = arrayOf( + SynchronizationFilter( + authors = arrayOf(nostrEvent.pubKey), + kinds = arrayOf( + MetadataEvent.KIND, + ContactListEvent.KIND, + AdvertisedRelayListEvent.KIND, + ChatMessageRelayListEvent.KIND, + SearchRelayListEvent.KIND, + IndexerRelayListEvent.KIND, + ChannelListEvent.KIND, + RelayFeedsListEvent.KIND + ) + ) + ), + relayURL = synchronizationRelayURL + ) + } + ) + } + + // Save profile connections... + database.connectionDao().upsert( + Connection( + sourcePublicKey = nostrEvent.pubKey, + destinationPublicKey = followedHexKEys, + nostrEventId = nostrEvent.id + ) + ) + } + // TODO: Delete connections where sourcePublicKey == nostr.pubKey but destinationPublickKey !in verifiedFollowKeySet + } } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/model/Connection.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/model/Connection.kt new file mode 100644 index 00000000..133765d3 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/model/Connection.kt @@ -0,0 +1,42 @@ +package ac.cord.auxiliary.compose.database.model + +import ac.cord.auxiliary.compose.database.model.traits.BroadcastableEntity +import ac.cord.auxiliary.compose.database.model.traits.LocalStoreEntity +import ac.cord.auxiliary.compose.database.model.traits.NostrEventEntity +import ac.cord.auxiliary.compose.database.model.traits.SoftDeletableEntity +import ac.cord.auxiliary.compose.database.model.traits.TimestampedEntity +import androidx.room.Entity +import androidx.room.ForeignKey +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import kotlin.time.Clock +import kotlin.time.Instant + +@Entity( + foreignKeys = [ + ForeignKey( + entity = Profile::class, + parentColumns = ["publicKey"], + childColumns = ["sourcePublicKey"], + onDelete = ForeignKey.CASCADE, + ), + ForeignKey( + entity = Profile::class, + parentColumns = ["publicKey"], + childColumns = ["destinationPublicKey"], + onDelete = ForeignKey.CASCADE, + ), + ], + primaryKeys = ["sourcePublicKey", "destinationPublicKey"] +) +data class Connection( + val sourcePublicKey: HexKey, + val destinationPublicKey: HexKey, + + override val nostrEventId: HexKey, // This is the contactListEvent with this following... + + override val createdAt: Instant = Clock.System.now(), + override val updatedAt: Instant = Clock.System.now(), + override val savedAt: Instant = Clock.System.now(), + override val deletedAt: Instant? = null, + override val broadcastedAt: Instant? = null, +): NostrEventEntity, TimestampedEntity, LocalStoreEntity, BroadcastableEntity, SoftDeletableEntity diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/model/intermdiate/LocalFollowers.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/model/intermdiate/LocalFollowers.kt new file mode 100644 index 00000000..201a7842 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/model/intermdiate/LocalFollowers.kt @@ -0,0 +1,17 @@ +package ac.cord.auxiliary.compose.database.model.intermdiate + +import ac.cord.auxiliary.compose.database.model.Profile +import ac.cord.auxiliary.compose.database.model.Connection +import androidx.room.Embedded +import androidx.room.Junction +import androidx.room.Relation + +data class LocalFollowers( + @Embedded val connection: Connection, + + @Relation( + parentColumn = "destinationPublicKey", + entityColumn = "publicKey", + ) + val followers: List +) diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/model/intermdiate/LocalFollowing.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/model/intermdiate/LocalFollowing.kt new file mode 100644 index 00000000..7e476c66 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/model/intermdiate/LocalFollowing.kt @@ -0,0 +1,17 @@ +package ac.cord.auxiliary.compose.database.model.intermdiate + +import ac.cord.auxiliary.compose.database.model.Profile +import ac.cord.auxiliary.compose.database.model.Connection +import androidx.room.Embedded +import androidx.room.Junction +import androidx.room.Relation + +data class LocalFollowing( + @Embedded val connection: Connection, + + @Relation( + parentColumn = "sourcePublicKey", + entityColumn = "publicKey", + ) + val following: List +)