diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt index 0bc89eaa02..4945c69c20 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt @@ -16,7 +16,6 @@ package org.matrix.android.sdk.internal.session.room import io.realm.Realm -import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.crypto.verification.VerificationState import org.matrix.android.sdk.api.session.events.model.AggregatedAnnotation @@ -25,26 +24,18 @@ import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.LocalEcho import org.matrix.android.sdk.api.session.events.model.RelationType import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent -import org.matrix.android.sdk.api.session.events.model.getRelationContent import org.matrix.android.sdk.api.session.events.model.toContent import org.matrix.android.sdk.api.session.events.model.toModel -import org.matrix.android.sdk.api.session.room.getTimelineEvent -import org.matrix.android.sdk.api.session.room.model.PollSummaryContent import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent import org.matrix.android.sdk.api.session.room.model.ReferencesAggregatedContent -import org.matrix.android.sdk.api.session.room.model.VoteInfo -import org.matrix.android.sdk.api.session.room.model.VoteSummary import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconInfoContent import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconLocationDataContent import org.matrix.android.sdk.api.session.room.model.message.MessageContent -import org.matrix.android.sdk.api.session.room.model.message.MessageEndPollContent import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent import org.matrix.android.sdk.api.session.room.model.message.MessagePollResponseContent import org.matrix.android.sdk.api.session.room.model.message.MessageRelationContent import org.matrix.android.sdk.api.session.room.model.relation.ReactionContent import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper -import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent -import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent import org.matrix.android.sdk.internal.SessionManager import org.matrix.android.sdk.internal.crypto.verification.toState import org.matrix.android.sdk.internal.database.helper.findRootThreadEvent @@ -55,7 +46,6 @@ import org.matrix.android.sdk.internal.database.model.EditionOfEvent import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntity import org.matrix.android.sdk.internal.database.model.EventEntity import org.matrix.android.sdk.internal.database.model.EventInsertType -import org.matrix.android.sdk.internal.database.model.PollResponseAggregatedSummaryEntity import org.matrix.android.sdk.internal.database.model.ReactionAggregatedSummaryEntity import org.matrix.android.sdk.internal.database.model.ReactionAggregatedSummaryEntityFields import org.matrix.android.sdk.internal.database.model.ReferencesAggregatedSummaryEntity @@ -164,9 +154,8 @@ internal class EventRelationsAggregationProcessor @Inject constructor( // A replace! handleReplace(realm, event, it, roomId, isLocalEcho, encryptedEventContent.relatesTo.eventId) } else if (event.getClearType() in EventType.POLL_RESPONSE) { - event.getClearContent().toModel(catchError = true)?.let { pollResponseContent -> - Timber.v("###RESPONSE in room $roomId for event ${event.eventId}") - handleResponse(realm, event, pollResponseContent, roomId, isLocalEcho, encryptedEventContent.relatesTo.eventId) + sessionManager.getSessionComponent(sessionId)?.session()?.let { session -> + pollAggregationProcessor.handlePollResponseEvent(session, realm, event) } } } @@ -186,12 +175,16 @@ internal class EventRelationsAggregationProcessor @Inject constructor( } in EventType.POLL_RESPONSE -> { event.getClearContent().toModel(catchError = true)?.let { - handleResponse(realm, event, it, roomId, isLocalEcho, event.getRelationContent()?.eventId) + sessionManager.getSessionComponent(sessionId)?.session()?.let { session -> + pollAggregationProcessor.handlePollResponseEvent(session, realm, event) + } } } in EventType.POLL_END -> { - event.content.toModel(catchError = true)?.let { - handleEndPoll(realm, event, it, roomId, isLocalEcho) + sessionManager.getSessionComponent(sessionId)?.session()?.let { session -> + getPowerLevelsHelper(event.roomId)?.let { + pollAggregationProcessor.handlePollEndEvent(session, it, realm, event) + } } } in EventType.BEACON_LOCATION_DATA -> { @@ -249,12 +242,16 @@ internal class EventRelationsAggregationProcessor @Inject constructor( } in EventType.POLL_RESPONSE -> { event.content.toModel(catchError = true)?.let { - handleResponse(realm, event, it, roomId, isLocalEcho) + sessionManager.getSessionComponent(sessionId)?.session()?.let { session -> + pollAggregationProcessor.handlePollResponseEvent(session, realm, event) + } } } in EventType.POLL_END -> { - event.content.toModel(catchError = true)?.let { - handleEndPoll(realm, event, it, roomId, isLocalEcho) + sessionManager.getSessionComponent(sessionId)?.session()?.let { session -> + getPowerLevelsHelper(event.roomId)?.let { + pollAggregationProcessor.handlePollEndEvent(session, it, realm, event) + } } } in EventType.STATE_ROOM_BEACON_INFO -> { @@ -381,173 +378,10 @@ internal class EventRelationsAggregationProcessor @Inject constructor( } } - private fun handleResponse(realm: Realm, - event: Event, - content: MessagePollResponseContent, - roomId: String, - isLocalEcho: Boolean, - relatedEventId: String? = null) { - val eventId = event.eventId ?: return - val senderId = event.senderId ?: return - val targetEventId = relatedEventId ?: content.relatesTo?.eventId ?: return - val eventTimestamp = event.originServerTs ?: return - - val targetPollContent = getPollContent(roomId, targetEventId) ?: return - - // ok, this is a poll response - var existing = EventAnnotationsSummaryEntity.where(realm, roomId, targetEventId).findFirst() - if (existing == null) { - Timber.v("## POLL creating new relation summary for $targetEventId") - existing = EventAnnotationsSummaryEntity.create(realm, roomId, targetEventId) - } - - // we have it - val existingPollSummary = existing.pollResponseSummary - ?: realm.createObject(PollResponseAggregatedSummaryEntity::class.java).also { - existing.pollResponseSummary = it - } - - val closedTime = existingPollSummary.closedTime - if (closedTime != null && eventTimestamp > closedTime) { - Timber.v("## POLL is closed ignore event poll:$targetEventId, event :${event.eventId}") - return - } - - val currentModel = ContentMapper.map(existingPollSummary.aggregatedContent).toModel() - - if (existingPollSummary.sourceEvents.contains(eventId)) { - // ignore this event, we already know it (??) - Timber.v("## POLL ignoring event for summary, it's known eventId:$eventId") - return - } - val txId = event.unsignedData?.transactionId - // is it a remote echo? - if (!isLocalEcho && existingPollSummary.sourceLocalEchoEvents.contains(txId)) { - // ok it has already been managed - Timber.v("## POLL Receiving remote echo of response eventId:$eventId") - existingPollSummary.sourceLocalEchoEvents.remove(txId) - existingPollSummary.sourceEvents.add(event.eventId) - return - } - - val option = content.getBestResponse()?.answers?.first() ?: return Unit.also { - Timber.d("## POLL Ignoring malformed response no option eventId:$eventId content: ${event.content}") - } - - // Check if this option is in available options - if (!targetPollContent.getBestPollCreationInfo()?.answers?.map { it.id }?.contains(option).orFalse()) { - Timber.v("## POLL $targetEventId doesn't contain option $option") - return - } - - val votes = currentModel?.votes.orEmpty().toMutableList() - - var myVote: String? = null - val existingVoteIndex = votes.indexOfFirst { it.userId == senderId } - if (existingVoteIndex != -1) { - // Is the vote newer? - val existingVote = votes[existingVoteIndex] - if (existingVote.voteTimestamp < eventTimestamp) { - // Take the new one - votes[existingVoteIndex] = VoteInfo(senderId, option, eventTimestamp) - if (userId == senderId) { - myVote = option - } - Timber.v("## POLL adding vote $option for user $senderId in poll :$targetEventId ") - } else { - Timber.v("## POLL Ignoring vote (older than known one) eventId:$eventId ") - } - } else { - votes.add(VoteInfo(senderId, option, eventTimestamp)) - if (userId == senderId) { - myVote = option - } - Timber.v("## POLL adding vote $option for user $senderId in poll :$targetEventId ") - } - - // Precompute the percentage of votes for all options - val totalVotes = votes.size - val newVotesSummary = votes - .groupBy({ it.option }, { it.userId }) - .mapValues { - VoteSummary( - total = it.value.size, - percentage = if (totalVotes == 0 && it.value.isEmpty()) 0.0 else it.value.size.toDouble() / totalVotes - ) - } - val newWinnerVoteCount = newVotesSummary.maxOf { it.value.total } - - if (isLocalEcho) { - existingPollSummary.sourceLocalEchoEvents.add(eventId) - } else { - existingPollSummary.sourceEvents.add(eventId) - } - - val newSumModel = PollSummaryContent( - myVote = myVote, - votes = votes, - votesSummary = newVotesSummary, - totalVotes = totalVotes, - winnerVoteCount = newWinnerVoteCount - ) - - existingPollSummary.aggregatedContent = ContentMapper.map(newSumModel.toContent()) - } - - private fun handleEndPoll(realm: Realm, - event: Event, - content: MessageEndPollContent, - roomId: String, - isLocalEcho: Boolean) { - val pollEventId = content.relatesTo?.eventId ?: return - val pollOwnerId = getPollEvent(roomId, pollEventId)?.root?.senderId - val isPollOwner = pollOwnerId == event.senderId - val powerLevelsHelper = stateEventDataSource.getStateEvent(roomId, EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.NoCondition) + private fun getPowerLevelsHelper(roomId: String): PowerLevelsHelper? { + return stateEventDataSource.getStateEvent(roomId, EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.NoCondition) ?.content?.toModel() ?.let { PowerLevelsHelper(it) } - - if (!isPollOwner && !powerLevelsHelper?.isUserAbleToRedact(event.senderId ?: "").orFalse()) { - Timber.v("## Received poll.end event $pollEventId but user ${event.senderId} doesn't have enough power level in room $roomId") - return - } - - var existingPoll = EventAnnotationsSummaryEntity.where(realm, roomId, pollEventId).findFirst() - if (existingPoll == null) { - Timber.v("## POLL creating new relation summary for $pollEventId") - existingPoll = EventAnnotationsSummaryEntity.create(realm, roomId, pollEventId) - } - - // we have it - val existingPollSummary = existingPoll.pollResponseSummary - ?: realm.createObject(PollResponseAggregatedSummaryEntity::class.java).also { - existingPoll.pollResponseSummary = it - } - - val txId = event.unsignedData?.transactionId - existingPollSummary.closedTime = event.originServerTs - - // is it a remote echo? - if (!isLocalEcho && existingPollSummary.sourceLocalEchoEvents.contains(txId)) { - // ok it has already been managed - Timber.v("## POLL Receiving remote echo of response eventId:$pollEventId") - existingPollSummary.sourceLocalEchoEvents.remove(txId) - existingPollSummary.sourceEvents.add(event.eventId) - } - } - - private fun getPollEvent(roomId: String, eventId: String): TimelineEvent? { - val session = sessionManager.getSessionComponent(sessionId)?.session() - return session?.roomService()?.getRoom(roomId)?.getTimelineEvent(eventId) ?: return null.also { - Timber.v("## POLL target poll event $eventId not found in room $roomId") - } - } - - private fun getPollContent(roomId: String, eventId: String): MessagePollContent? { - val pollEvent = getPollEvent(roomId, eventId) ?: return null - - return pollEvent.getLastMessageContent() as? MessagePollContent ?: return null.also { - Timber.v("## POLL target poll event $eventId content is malformed") - } } private fun handleInitialAggregatedRelations(realm: Realm, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/DefaultPollAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/DefaultPollAggregationProcessor.kt index 5d168b5ec8..0eabf7d387 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/DefaultPollAggregationProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/DefaultPollAggregationProcessor.kt @@ -17,15 +17,31 @@ package org.matrix.android.sdk.internal.session.room.aggregation.poll import io.realm.Realm +import org.matrix.android.sdk.api.extensions.orFalse +import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.LocalEcho import org.matrix.android.sdk.api.session.events.model.RelationType +import org.matrix.android.sdk.api.session.events.model.getRelationContent import org.matrix.android.sdk.api.session.events.model.toContent import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.getTimelineEvent import org.matrix.android.sdk.api.session.room.model.PollSummaryContent +import org.matrix.android.sdk.api.session.room.model.VoteInfo +import org.matrix.android.sdk.api.session.room.model.VoteSummary +import org.matrix.android.sdk.api.session.room.model.message.MessageEndPollContent import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent +import org.matrix.android.sdk.api.session.room.model.message.MessagePollResponseContent +import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent +import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent import org.matrix.android.sdk.internal.database.mapper.ContentMapper import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntity +import org.matrix.android.sdk.internal.database.model.PollResponseAggregatedSummaryEntity +import org.matrix.android.sdk.internal.database.query.create import org.matrix.android.sdk.internal.database.query.getOrCreate +import org.matrix.android.sdk.internal.database.query.where +import timber.log.Timber import javax.inject.Inject class DefaultPollAggregationProcessor @Inject constructor() : PollAggregationProcessor { @@ -59,11 +75,134 @@ class DefaultPollAggregationProcessor @Inject constructor() : PollAggregationPro return true } - override fun handlePollResponseEvent(realm: Realm, event: Event): Boolean { - TODO("Not yet implemented") + override fun handlePollResponseEvent(session: Session, realm: Realm, event: Event): Boolean { + val content = event.getClearContent()?.toModel() ?: return false + val roomId = event.roomId ?: return false + val senderId = event.senderId ?: return false + val targetEventId = event.getRelationContent()?.eventId ?: return false + val targetPollContent = getPollContent(session, roomId, targetEventId) ?: return false + + val annotationsSummaryEntity = getAnnotationsSummaryEntity(realm, roomId, targetEventId) + val aggregatedPollSummaryEntity = getAggregatedPollSummaryEntity(realm, annotationsSummaryEntity) + + val closedTime = aggregatedPollSummaryEntity.closedTime + val responseTime = event.originServerTs ?: return false + if (closedTime != null && responseTime > closedTime) { + return false + } + + if (aggregatedPollSummaryEntity.sourceEvents.contains(event.eventId)) { + return false + } + + val txId = event.unsignedData?.transactionId + val isLocalEcho = LocalEcho.isLocalEchoId(event.eventId ?: "") + if (!isLocalEcho && aggregatedPollSummaryEntity.sourceLocalEchoEvents.contains(txId)) { + aggregatedPollSummaryEntity.sourceLocalEchoEvents.remove(txId) + aggregatedPollSummaryEntity.sourceEvents.add(event.eventId) + return false + } + + val vote = content.getBestResponse()?.answers?.first() ?: return false + if (!targetPollContent.getBestPollCreationInfo()?.answers?.map { it.id }?.contains(vote).orFalse()) { + return false + } + + val pollSummaryModel = ContentMapper.map(aggregatedPollSummaryEntity.aggregatedContent).toModel() + val existingVotes = pollSummaryModel?.votes.orEmpty().toMutableList() + val existingVoteIndex = existingVotes.indexOfFirst { it.userId == senderId } + + if (existingVoteIndex != -1) { + val existingVote = existingVotes[existingVoteIndex] + if (existingVote.voteTimestamp > responseTime) { + return false + } + existingVotes[existingVoteIndex] = VoteInfo(senderId, vote, responseTime) + } else { + existingVotes.add(VoteInfo(senderId, vote, responseTime)) + } + + // Precompute the percentage of votes for all options + val totalVotes = existingVotes.size + val newVotesSummary = existingVotes + .groupBy({ it.option }, { it.userId }) + .mapValues { + VoteSummary( + total = it.value.size, + percentage = if (totalVotes == 0 && it.value.isEmpty()) 0.0 else it.value.size.toDouble() / totalVotes + ) + } + val newWinnerVoteCount = newVotesSummary.maxOf { it.value.total } + + if (isLocalEcho) { + aggregatedPollSummaryEntity.sourceLocalEchoEvents.add(event.eventId) + } else { + aggregatedPollSummaryEntity.sourceEvents.add(event.eventId) + } + + val newSumModel = PollSummaryContent( + myVote = vote, + votes = existingVotes, + votesSummary = newVotesSummary, + totalVotes = totalVotes, + winnerVoteCount = newWinnerVoteCount + ) + aggregatedPollSummaryEntity.aggregatedContent = ContentMapper.map(newSumModel.toContent()) + + return true } - override fun handlePollEndEvent(realm: Realm, event: Event): Boolean { - TODO("Not yet implemented") + override fun handlePollEndEvent(session: Session, powerLevelsHelper: PowerLevelsHelper, realm: Realm, event: Event): Boolean { + val content = event.getClearContent()?.toModel() ?: return false + val roomId = event.roomId ?: return false + val pollEventId = content.relatesTo?.eventId ?: return false + val pollOwnerId = getPollEvent(session, roomId, pollEventId)?.root?.senderId + val isPollOwner = pollOwnerId == event.senderId + + if (!isPollOwner && !powerLevelsHelper.isUserAbleToRedact(event.senderId ?: "")) { + Timber.v("## Received poll.end event $pollEventId but user ${event.senderId} doesn't have enough power level in room $roomId") + return false + } + + val annotationsSummaryEntity = getAnnotationsSummaryEntity(realm, roomId, pollEventId) + val aggregatedPollSummaryEntity = getAggregatedPollSummaryEntity(realm, annotationsSummaryEntity) + + val txId = event.unsignedData?.transactionId + aggregatedPollSummaryEntity.closedTime = event.originServerTs + + val isLocalEcho = LocalEcho.isLocalEchoId(event.eventId ?: "") + if (!isLocalEcho && aggregatedPollSummaryEntity.sourceLocalEchoEvents.contains(txId)) { + aggregatedPollSummaryEntity.sourceLocalEchoEvents.remove(txId) + aggregatedPollSummaryEntity.sourceEvents.add(event.eventId) + } + + return true + } + + private fun getPollEvent(session: Session, roomId: String, eventId: String): TimelineEvent? { + return session.roomService().getRoom(roomId)?.getTimelineEvent(eventId) ?: return null.also { + Timber.v("## POLL target poll event $eventId not found in room $roomId") + } + } + + private fun getPollContent(session: Session, roomId: String, eventId: String): MessagePollContent? { + val pollEvent = getPollEvent(session, roomId, eventId) ?: return null + + return pollEvent.getLastMessageContent() as? MessagePollContent ?: return null.also { + Timber.v("## POLL target poll event $eventId content is malformed") + } + } + + private fun getAnnotationsSummaryEntity(realm: Realm, roomId: String, eventId: String): EventAnnotationsSummaryEntity { + return EventAnnotationsSummaryEntity.where(realm, roomId, eventId).findFirst() + ?: EventAnnotationsSummaryEntity.create(realm, roomId, eventId) + } + + private fun getAggregatedPollSummaryEntity(realm: Realm, + eventAnnotationsSummaryEntity: EventAnnotationsSummaryEntity): PollResponseAggregatedSummaryEntity { + return eventAnnotationsSummaryEntity.pollResponseSummary + ?: realm.createObject(PollResponseAggregatedSummaryEntity::class.java).also { + eventAnnotationsSummaryEntity.pollResponseSummary = it + } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/PollAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/PollAggregationProcessor.kt index e97a85c6b3..848643b435 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/PollAggregationProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/PollAggregationProcessor.kt @@ -17,7 +17,9 @@ package org.matrix.android.sdk.internal.session.room.aggregation.poll import io.realm.Realm +import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper interface PollAggregationProcessor { /** @@ -35,6 +37,7 @@ interface PollAggregationProcessor { * Returns true if the event is aggregated. */ fun handlePollResponseEvent( + session: Session, realm: Realm, event: Event ): Boolean @@ -44,6 +47,8 @@ interface PollAggregationProcessor { * Returns true if the event is aggregated. */ fun handlePollEndEvent( + session: Session, + powerLevelsHelper: PowerLevelsHelper, realm: Realm, event: Event ): Boolean