Process group member changes on commit
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 1,
|
||||
"identityHash": "6a176cdf1ee35e271c2a38b5955a7440",
|
||||
"identityHash": "7e418251d31c1dd20c0ea92e14f2711c",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "BroadcastNostrEventReceipt",
|
||||
@@ -2196,7 +2196,7 @@
|
||||
},
|
||||
{
|
||||
"tableName": "Participant",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `participantPublicKey` TEXT NOT NULL, `chatRoomId` TEXT NOT NULL, `relayHint` TEXT, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL, `savedAt` INTEGER NOT NULL, `deletedAt` INTEGER, `broadcastedAt` INTEGER, FOREIGN KEY(`participantPublicKey`) REFERENCES `Profile`(`publicKey`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`chatRoomId`) REFERENCES `ChatRoom`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `participantPublicKey` TEXT NOT NULL, `chatRoomId` TEXT NOT NULL, `relayHint` TEXT, `adminAt` INTEGER, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL, `savedAt` INTEGER NOT NULL, `deletedAt` INTEGER, `broadcastedAt` INTEGER, FOREIGN KEY(`participantPublicKey`) REFERENCES `Profile`(`publicKey`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`chatRoomId`) REFERENCES `ChatRoom`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
@@ -2221,6 +2221,11 @@
|
||||
"columnName": "relayHint",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "adminAt",
|
||||
"columnName": "adminAt",
|
||||
"affinity": "INTEGER"
|
||||
},
|
||||
{
|
||||
"fieldPath": "createdAt",
|
||||
"columnName": "createdAt",
|
||||
@@ -3462,7 +3467,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, '6a176cdf1ee35e271c2a38b5955a7440')"
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '7e418251d31c1dd20c0ea92e14f2711c')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,7 @@ import at.torch.compose.exceptions.MarmotWelcomeEventMissingKeyPackageEventIdExc
|
||||
import at.torch.compose.extensions.toHex
|
||||
import at.torch.compose.managers.MarmotInboundManager
|
||||
import co.touchlab.kermit.Logger
|
||||
import com.vitorpamplona.quartz.marmot.GroupEventResult
|
||||
import com.vitorpamplona.quartz.marmot.mip00KeyPackages.KeyPackageEvent
|
||||
import com.vitorpamplona.quartz.marmot.mip00KeyPackages.KeyPackageRelayListEvent
|
||||
import com.vitorpamplona.quartz.marmot.mip02Welcome.WelcomeEvent
|
||||
@@ -43,6 +44,7 @@ import com.vitorpamplona.quartz.nip01Core.core.toHexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
|
||||
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.normalizedUrls
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.events.taggedEvents
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.people.taggedUsers
|
||||
import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent
|
||||
@@ -398,6 +400,20 @@ abstract class NostrDao(
|
||||
groupEvent = groupEvent
|
||||
)?.let { groupEventResult ->
|
||||
logger.d("groupEventResult: $groupEventResult")
|
||||
|
||||
when(groupEventResult) {
|
||||
is GroupEventResult.CommitProcessed -> {
|
||||
MarmotInboundManager.processGroupMembershipChanges(
|
||||
database = database,
|
||||
mlsGroup = mlsGroup,
|
||||
localChatRoom = localChatRoom
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
logger.i("No app logic to handle: $groupEventResult")
|
||||
}
|
||||
}
|
||||
|
||||
// Save the mls chatRoom state...
|
||||
database.chatRoomDao().upsert(
|
||||
localChatRoom.chatRoom.copy(
|
||||
@@ -752,23 +768,11 @@ abstract class NostrDao(
|
||||
|
||||
val normalizedRelayUrl = NormalizedRelayUrl(relayURL)
|
||||
|
||||
|
||||
val participants = group.members().mapNotNull { (leafIndex, leafNode) ->
|
||||
val pubkey = when (val cred = leafNode.credential) {
|
||||
is Credential.Basic -> cred.identity.toHexKey()
|
||||
else -> null
|
||||
}
|
||||
|
||||
if (pubkey != null) {
|
||||
Participant(
|
||||
participantPublicKey = pubkey,
|
||||
chatRoomId = nostrGroupId,
|
||||
relayHint = group.currentMarmotData()?.relays?.firstOrNull() ?: normalizedRelayUrl.url
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
val participants = Participant.fromMlsGroup(
|
||||
mlsGroup = group,
|
||||
nostrGroupId = nostrGroupId,
|
||||
relayHint = normalizedRelayUrl.url
|
||||
)
|
||||
|
||||
logger.d("Participants: $participants")
|
||||
// Sync missing participant profiles...
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package at.torch.compose.database.dao
|
||||
|
||||
import androidx.room3.Dao
|
||||
import androidx.room3.Delete
|
||||
import androidx.room3.Query
|
||||
import androidx.room3.Upsert
|
||||
import at.torch.compose.database.model.Participant
|
||||
@@ -16,4 +17,7 @@ interface ParticipantDao {
|
||||
@Upsert
|
||||
suspend fun upsert(participants: List<Participant>)
|
||||
|
||||
@Delete
|
||||
suspend fun delete(participants: List<Participant>)
|
||||
|
||||
}
|
||||
@@ -3,7 +3,14 @@ package at.torch.compose.database.model
|
||||
import androidx.room3.Entity
|
||||
import androidx.room3.ForeignKey
|
||||
import androidx.room3.PrimaryKey
|
||||
import at.torch.compose.database.model.traits.BroadcastableEntity
|
||||
import at.torch.compose.database.model.traits.LocalStoreEntity
|
||||
import at.torch.compose.database.model.traits.SoftDeletableEntity
|
||||
import at.torch.compose.database.model.traits.TimestampedEntity
|
||||
import com.vitorpamplona.quartz.marmot.mls.group.MlsGroup
|
||||
import com.vitorpamplona.quartz.marmot.mls.tree.Credential
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
|
||||
import kotlin.time.Clock
|
||||
import kotlin.time.Instant
|
||||
|
||||
@@ -41,13 +48,36 @@ data class Participant(
|
||||
val chatRoomId: String,
|
||||
|
||||
val relayHint: String?,
|
||||
val adminAt: Instant? = null,
|
||||
|
||||
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,
|
||||
): at.torch.compose.database.model.traits.TimestampedEntity,
|
||||
at.torch.compose.database.model.traits.LocalStoreEntity,
|
||||
at.torch.compose.database.model.traits.BroadcastableEntity,
|
||||
at.torch.compose.database.model.traits.SoftDeletableEntity
|
||||
): TimestampedEntity, LocalStoreEntity, BroadcastableEntity, SoftDeletableEntity {
|
||||
companion object {
|
||||
fun fromMlsGroup(
|
||||
mlsGroup: MlsGroup,
|
||||
nostrGroupId: HexKey,
|
||||
relayHint: String?
|
||||
): List<Participant> {
|
||||
return mlsGroup.members().mapNotNull { (leafIndex, leafNode) ->
|
||||
val pubkey = when (val cred = leafNode.credential) {
|
||||
is Credential.Basic -> cred.identity.toHexKey()
|
||||
else -> null
|
||||
}
|
||||
|
||||
if (pubkey != null) {
|
||||
Participant(
|
||||
participantPublicKey = pubkey,
|
||||
chatRoomId = nostrGroupId,
|
||||
relayHint = mlsGroup.currentMarmotData()?.relays?.firstOrNull() ?: relayHint
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package at.torch.compose.managers
|
||||
|
||||
import at.torch.compose.database.TorchDatabase
|
||||
import at.torch.compose.database.model.Participant
|
||||
import at.torch.compose.database.model.intermdiate.LocalChatRoom
|
||||
import at.torch.compose.exceptions.MarmotMissingChatGroupException
|
||||
import at.torch.compose.exceptions.MarmotUnsupportedWireFormatException
|
||||
@@ -27,6 +28,7 @@ import com.vitorpamplona.quartz.marmot.mls.schedule.SecretTree
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
|
||||
import kotlin.time.Clock
|
||||
|
||||
object MarmotInboundManager {
|
||||
private const val TAG = "MarmotInboundManager"
|
||||
@@ -50,6 +52,66 @@ object MarmotInboundManager {
|
||||
|
||||
private val commitTracker = CommitOrdering.EpochCommitTracker()
|
||||
|
||||
suspend fun processGroupMembershipChanges(
|
||||
database: TorchDatabase,
|
||||
localChatRoom: LocalChatRoom,
|
||||
mlsGroup: MlsGroup,
|
||||
) {
|
||||
logger.d("processGroupMembershipChanges")
|
||||
val adminPublicKeys = mlsGroup.currentMarmotData()?.adminPubkeys
|
||||
|
||||
val commitedGroupMembers = Participant.fromMlsGroup(
|
||||
mlsGroup = mlsGroup,
|
||||
nostrGroupId = localChatRoom.chatRoom.id,
|
||||
relayHint = mlsGroup.currentMarmotData()?.relays?.firstOrNull()
|
||||
)
|
||||
val committedGroupMembersPublicKeys = commitedGroupMembers.map { it.participantPublicKey }
|
||||
val excludedParticipants = localChatRoom.localParticipants.filter { localParticipant ->
|
||||
committedGroupMembersPublicKeys.contains(localParticipant.participant.participantPublicKey).not()
|
||||
}.map { localParticipant ->
|
||||
localParticipant.participant.copy(
|
||||
deletedAt = Clock.System.now()
|
||||
)
|
||||
}
|
||||
logger.d("excludedParticipants: $excludedParticipants")
|
||||
val existingParticipantPublicKeys = localChatRoom.localParticipants.map { it.participant.participantPublicKey }
|
||||
val newParticipants = commitedGroupMembers.filter { committedGroupMember ->
|
||||
existingParticipantPublicKeys.contains(committedGroupMember.participantPublicKey)
|
||||
}.map { newParticipant ->
|
||||
newParticipant.copy(
|
||||
adminAt = if (adminPublicKeys?.contains(newParticipant.participantPublicKey) == null) {
|
||||
Clock.System.now()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
)
|
||||
}
|
||||
logger.d("newParticipants: $newParticipants")
|
||||
val removedAdmins = localChatRoom.localParticipants.filter { localParticipant ->
|
||||
localParticipant.participant.adminAt != null && adminPublicKeys?.contains(localParticipant.participant.participantPublicKey) == true
|
||||
}.map { localParticipant ->
|
||||
localParticipant.participant.copy(
|
||||
adminAt = null
|
||||
)
|
||||
}
|
||||
logger.d("removedAdmins: $removedAdmins")
|
||||
|
||||
val graduatedAdmins = localChatRoom.localParticipants.filter { localParticipant ->
|
||||
localParticipant.participant.adminAt == null && adminPublicKeys?.contains(localParticipant.participant.participantPublicKey) == true
|
||||
}.map { localParticipant ->
|
||||
localParticipant.participant.copy(
|
||||
adminAt = Clock.System.now()
|
||||
)
|
||||
}
|
||||
logger.d("graduatedAdmins: $graduatedAdmins")
|
||||
|
||||
// Handle membership changes...
|
||||
database.participantDao().upsert(removedAdmins)
|
||||
database.participantDao().delete(excludedParticipants)
|
||||
database.participantDao().upsert(newParticipants)
|
||||
database.participantDao().upsert(graduatedAdmins)
|
||||
}
|
||||
|
||||
suspend fun processGroupEvent(
|
||||
database: TorchDatabase,
|
||||
activeKeyPair: KeyPair,
|
||||
|
||||
Reference in New Issue
Block a user