Merge pull request #6074 from vector-im/feature/ons/poll_refactoring
Poll refactoring with unit tests (PSF-1020)
This commit is contained in:
commit
da764d7c9a
1
changelog.d/6074.bugfix
Normal file
1
changelog.d/6074.bugfix
Normal file
@ -0,0 +1 @@
|
|||||||
|
Poll refactoring with unit tests
|
@ -87,6 +87,8 @@ import org.matrix.android.sdk.internal.session.integrationmanager.IntegrationMan
|
|||||||
import org.matrix.android.sdk.internal.session.openid.DefaultOpenIdService
|
import org.matrix.android.sdk.internal.session.openid.DefaultOpenIdService
|
||||||
import org.matrix.android.sdk.internal.session.permalinks.DefaultPermalinkService
|
import org.matrix.android.sdk.internal.session.permalinks.DefaultPermalinkService
|
||||||
import org.matrix.android.sdk.internal.session.room.EventRelationsAggregationProcessor
|
import org.matrix.android.sdk.internal.session.room.EventRelationsAggregationProcessor
|
||||||
|
import org.matrix.android.sdk.internal.session.room.aggregation.poll.DefaultPollAggregationProcessor
|
||||||
|
import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollAggregationProcessor
|
||||||
import org.matrix.android.sdk.internal.session.room.create.RoomCreateEventProcessor
|
import org.matrix.android.sdk.internal.session.room.create.RoomCreateEventProcessor
|
||||||
import org.matrix.android.sdk.internal.session.room.prune.RedactionEventProcessor
|
import org.matrix.android.sdk.internal.session.room.prune.RedactionEventProcessor
|
||||||
import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor
|
import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor
|
||||||
@ -385,4 +387,7 @@ internal abstract class SessionModule {
|
|||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
abstract fun bindEventSenderProcessor(processor: EventSenderProcessorCoroutine): EventSenderProcessor
|
abstract fun bindEventSenderProcessor(processor: EventSenderProcessorCoroutine): EventSenderProcessor
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
abstract fun bindPollAggregationProcessor(processor: DefaultPollAggregationProcessor): PollAggregationProcessor
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,6 @@
|
|||||||
package org.matrix.android.sdk.internal.session.room
|
package org.matrix.android.sdk.internal.session.room
|
||||||
|
|
||||||
import io.realm.Realm
|
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.query.QueryStringValue
|
||||||
import org.matrix.android.sdk.api.session.crypto.verification.VerificationState
|
import org.matrix.android.sdk.api.session.crypto.verification.VerificationState
|
||||||
import org.matrix.android.sdk.api.session.events.model.AggregatedAnnotation
|
import org.matrix.android.sdk.api.session.events.model.AggregatedAnnotation
|
||||||
@ -28,23 +27,16 @@ import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventCon
|
|||||||
import org.matrix.android.sdk.api.session.events.model.getRelationContent
|
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.toContent
|
||||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
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.PowerLevelsContent
|
||||||
import org.matrix.android.sdk.api.session.room.model.ReferencesAggregatedContent
|
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.MessageBeaconInfoContent
|
||||||
import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconLocationDataContent
|
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.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.MessagePollContent
|
||||||
import org.matrix.android.sdk.api.session.room.model.message.MessagePollResponseContent
|
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.message.MessageRelationContent
|
||||||
import org.matrix.android.sdk.api.session.room.model.relation.ReactionContent
|
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.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.SessionManager
|
||||||
import org.matrix.android.sdk.internal.crypto.verification.toState
|
import org.matrix.android.sdk.internal.crypto.verification.toState
|
||||||
import org.matrix.android.sdk.internal.database.helper.findRootThreadEvent
|
import org.matrix.android.sdk.internal.database.helper.findRootThreadEvent
|
||||||
@ -55,7 +47,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.EventAnnotationsSummaryEntity
|
||||||
import org.matrix.android.sdk.internal.database.model.EventEntity
|
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.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.ReactionAggregatedSummaryEntity
|
||||||
import org.matrix.android.sdk.internal.database.model.ReactionAggregatedSummaryEntityFields
|
import org.matrix.android.sdk.internal.database.model.ReactionAggregatedSummaryEntityFields
|
||||||
import org.matrix.android.sdk.internal.database.model.ReferencesAggregatedSummaryEntity
|
import org.matrix.android.sdk.internal.database.model.ReferencesAggregatedSummaryEntity
|
||||||
@ -68,6 +59,7 @@ import org.matrix.android.sdk.internal.di.SessionId
|
|||||||
import org.matrix.android.sdk.internal.di.UserId
|
import org.matrix.android.sdk.internal.di.UserId
|
||||||
import org.matrix.android.sdk.internal.session.EventInsertLiveProcessor
|
import org.matrix.android.sdk.internal.session.EventInsertLiveProcessor
|
||||||
import org.matrix.android.sdk.internal.session.room.aggregation.livelocation.LiveLocationAggregationProcessor
|
import org.matrix.android.sdk.internal.session.room.aggregation.livelocation.LiveLocationAggregationProcessor
|
||||||
|
import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollAggregationProcessor
|
||||||
import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource
|
import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource
|
||||||
import org.matrix.android.sdk.internal.util.time.Clock
|
import org.matrix.android.sdk.internal.util.time.Clock
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
@ -79,6 +71,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
|
|||||||
@SessionId private val sessionId: String,
|
@SessionId private val sessionId: String,
|
||||||
private val sessionManager: SessionManager,
|
private val sessionManager: SessionManager,
|
||||||
private val liveLocationAggregationProcessor: LiveLocationAggregationProcessor,
|
private val liveLocationAggregationProcessor: LiveLocationAggregationProcessor,
|
||||||
|
private val pollAggregationProcessor: PollAggregationProcessor,
|
||||||
private val clock: Clock,
|
private val clock: Clock,
|
||||||
) : EventInsertLiveProcessor {
|
) : EventInsertLiveProcessor {
|
||||||
|
|
||||||
@ -162,9 +155,8 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
|
|||||||
// A replace!
|
// A replace!
|
||||||
handleReplace(realm, event, it, roomId, isLocalEcho, encryptedEventContent.relatesTo.eventId)
|
handleReplace(realm, event, it, roomId, isLocalEcho, encryptedEventContent.relatesTo.eventId)
|
||||||
} else if (event.getClearType() in EventType.POLL_RESPONSE) {
|
} else if (event.getClearType() in EventType.POLL_RESPONSE) {
|
||||||
event.getClearContent().toModel<MessagePollResponseContent>(catchError = true)?.let { pollResponseContent ->
|
sessionManager.getSessionComponent(sessionId)?.session()?.let { session ->
|
||||||
Timber.v("###RESPONSE in room $roomId for event ${event.eventId}")
|
pollAggregationProcessor.handlePollResponseEvent(session, realm, event)
|
||||||
handleResponse(realm, event, pollResponseContent, roomId, isLocalEcho, encryptedEventContent.relatesTo.eventId)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -184,12 +176,16 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
|
|||||||
}
|
}
|
||||||
in EventType.POLL_RESPONSE -> {
|
in EventType.POLL_RESPONSE -> {
|
||||||
event.getClearContent().toModel<MessagePollResponseContent>(catchError = true)?.let {
|
event.getClearContent().toModel<MessagePollResponseContent>(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 -> {
|
in EventType.POLL_END -> {
|
||||||
event.content.toModel<MessageEndPollContent>(catchError = true)?.let {
|
sessionManager.getSessionComponent(sessionId)?.session()?.let { session ->
|
||||||
handleEndPoll(realm, event, it, roomId, isLocalEcho)
|
getPowerLevelsHelper(event.roomId)?.let {
|
||||||
|
pollAggregationProcessor.handlePollEndEvent(session, it, realm, event)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
in EventType.BEACON_LOCATION_DATA -> {
|
in EventType.BEACON_LOCATION_DATA -> {
|
||||||
@ -245,12 +241,16 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
|
|||||||
}
|
}
|
||||||
in EventType.POLL_RESPONSE -> {
|
in EventType.POLL_RESPONSE -> {
|
||||||
event.content.toModel<MessagePollResponseContent>(catchError = true)?.let {
|
event.content.toModel<MessagePollResponseContent>(catchError = true)?.let {
|
||||||
handleResponse(realm, event, it, roomId, isLocalEcho)
|
sessionManager.getSessionComponent(sessionId)?.session()?.let { session ->
|
||||||
|
pollAggregationProcessor.handlePollResponseEvent(session, realm, event)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
in EventType.POLL_END -> {
|
in EventType.POLL_END -> {
|
||||||
event.content.toModel<MessageEndPollContent>(catchError = true)?.let {
|
sessionManager.getSessionComponent(sessionId)?.session()?.let { session ->
|
||||||
handleEndPoll(realm, event, it, roomId, isLocalEcho)
|
getPowerLevelsHelper(event.roomId)?.let {
|
||||||
|
pollAggregationProcessor.handlePollEndEvent(session, it, realm, event)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
in EventType.STATE_ROOM_BEACON_INFO -> {
|
in EventType.STATE_ROOM_BEACON_INFO -> {
|
||||||
@ -318,22 +318,6 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ContentMapper
|
|
||||||
.map(eventAnnotationsSummaryEntity.pollResponseSummary?.aggregatedContent)
|
|
||||||
?.toModel<PollSummaryContent>()
|
|
||||||
?.let { existingPollSummaryContent ->
|
|
||||||
eventAnnotationsSummaryEntity.pollResponseSummary?.aggregatedContent = ContentMapper.map(
|
|
||||||
PollSummaryContent(
|
|
||||||
myVote = existingPollSummaryContent.myVote,
|
|
||||||
votes = emptyList(),
|
|
||||||
votesSummary = emptyMap(),
|
|
||||||
totalVotes = 0,
|
|
||||||
winnerVoteCount = 0,
|
|
||||||
)
|
|
||||||
.toContent()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val txId = event.unsignedData?.transactionId
|
val txId = event.unsignedData?.transactionId
|
||||||
// is it a remote echo?
|
// is it a remote echo?
|
||||||
if (!isLocalEcho && existingSummary.editions.any { it.eventId == txId }) {
|
if (!isLocalEcho && existingSummary.editions.any { it.eventId == txId }) {
|
||||||
@ -363,6 +347,10 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (event.getClearType() in EventType.POLL_START) {
|
||||||
|
pollAggregationProcessor.handlePollStartEvent(realm, event)
|
||||||
|
}
|
||||||
|
|
||||||
if (!isLocalEcho) {
|
if (!isLocalEcho) {
|
||||||
val replaceEvent = TimelineEventEntity
|
val replaceEvent = TimelineEventEntity
|
||||||
.where(realm, roomId, eventId)
|
.where(realm, roomId, eventId)
|
||||||
@ -392,173 +380,10 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleResponse(realm: Realm,
|
private fun getPowerLevelsHelper(roomId: String): PowerLevelsHelper? {
|
||||||
event: Event,
|
return stateEventDataSource.getStateEvent(roomId, EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.NoCondition)
|
||||||
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<PollSummaryContent>()
|
|
||||||
|
|
||||||
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)
|
|
||||||
?.content?.toModel<PowerLevelsContent>()
|
?.content?.toModel<PowerLevelsContent>()
|
||||||
?.let { PowerLevelsHelper(it) }
|
?.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,
|
private fun handleInitialAggregatedRelations(realm: Realm,
|
||||||
|
@ -0,0 +1,203 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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 javax.inject.Inject
|
||||||
|
|
||||||
|
class DefaultPollAggregationProcessor @Inject constructor() : PollAggregationProcessor {
|
||||||
|
|
||||||
|
override fun handlePollStartEvent(realm: Realm, event: Event): Boolean {
|
||||||
|
val content = event.getClearContent()?.toModel<MessagePollContent>()
|
||||||
|
if (content?.relatesTo?.type != RelationType.REPLACE) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
val roomId = event.roomId ?: return false
|
||||||
|
val targetEventId = content.relatesTo.eventId ?: return false
|
||||||
|
|
||||||
|
EventAnnotationsSummaryEntity.getOrCreate(realm, roomId, targetEventId).let { eventAnnotationsSummaryEntity ->
|
||||||
|
ContentMapper
|
||||||
|
.map(eventAnnotationsSummaryEntity.pollResponseSummary?.aggregatedContent)
|
||||||
|
?.toModel<PollSummaryContent>()
|
||||||
|
?.let { existingPollSummaryContent ->
|
||||||
|
eventAnnotationsSummaryEntity.pollResponseSummary?.aggregatedContent = ContentMapper.map(
|
||||||
|
PollSummaryContent(
|
||||||
|
myVote = existingPollSummaryContent.myVote,
|
||||||
|
votes = emptyList(),
|
||||||
|
votesSummary = emptyMap(),
|
||||||
|
totalVotes = 0,
|
||||||
|
winnerVoteCount = 0,
|
||||||
|
)
|
||||||
|
.toContent()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun handlePollResponseEvent(session: Session, realm: Realm, event: Event): Boolean {
|
||||||
|
val content = event.getClearContent()?.toModel<MessagePollResponseContent>() ?: return false
|
||||||
|
val roomId = event.roomId ?: return false
|
||||||
|
val senderId = event.senderId ?: return false
|
||||||
|
val targetEventId = (event.getRelationContent() ?: content.relatesTo)?.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<PollSummaryContent>()
|
||||||
|
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 myVote = existingVotes.find { it.userId == session.myUserId }?.option
|
||||||
|
|
||||||
|
val newSumModel = PollSummaryContent(
|
||||||
|
myVote = myVote,
|
||||||
|
votes = existingVotes,
|
||||||
|
votesSummary = newVotesSummary,
|
||||||
|
totalVotes = totalVotes,
|
||||||
|
winnerVoteCount = newWinnerVoteCount
|
||||||
|
)
|
||||||
|
aggregatedPollSummaryEntity.aggregatedContent = ContentMapper.map(newSumModel.toContent())
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun handlePollEndEvent(session: Session, powerLevelsHelper: PowerLevelsHelper, realm: Realm, event: Event): Boolean {
|
||||||
|
val content = event.getClearContent()?.toModel<MessageEndPollContent>() ?: 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 ?: "")) {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getPollContent(session: Session, roomId: String, eventId: String): MessagePollContent? {
|
||||||
|
val pollEvent = getPollEvent(session, roomId, eventId)
|
||||||
|
return pollEvent?.getLastMessageContent() as? MessagePollContent
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,55 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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 {
|
||||||
|
/**
|
||||||
|
* Poll start events don't need to be processed by the aggregator.
|
||||||
|
* This function will only handle if the poll is edited and will update the poll summary entity.
|
||||||
|
* Returns true if the event is aggregated.
|
||||||
|
*/
|
||||||
|
fun handlePollStartEvent(
|
||||||
|
realm: Realm,
|
||||||
|
event: Event
|
||||||
|
): Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Aggregates poll response event after many conditional checks like if the poll is ended, if the user is changing his/her vote etc.
|
||||||
|
* Returns true if the event is aggregated.
|
||||||
|
*/
|
||||||
|
fun handlePollResponseEvent(
|
||||||
|
session: Session,
|
||||||
|
realm: Realm,
|
||||||
|
event: Event
|
||||||
|
): Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates poll summary entity and mark it is ended after many conditional checks like if the poll is already ended etc.
|
||||||
|
* Returns true if the event is aggregated.
|
||||||
|
*/
|
||||||
|
fun handlePollEndEvent(
|
||||||
|
session: Session,
|
||||||
|
powerLevelsHelper: PowerLevelsHelper,
|
||||||
|
realm: Realm,
|
||||||
|
event: Event
|
||||||
|
): Boolean
|
||||||
|
}
|
@ -0,0 +1,162 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.matrix.android.sdk.internal.session.room.aggregation.poll
|
||||||
|
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import io.realm.RealmList
|
||||||
|
import io.realm.RealmModel
|
||||||
|
import io.realm.RealmQuery
|
||||||
|
import org.amshove.kluent.shouldBeFalse
|
||||||
|
import org.amshove.kluent.shouldBeTrue
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import org.matrix.android.sdk.api.session.Session
|
||||||
|
import org.matrix.android.sdk.api.session.getRoom
|
||||||
|
import org.matrix.android.sdk.api.session.room.Room
|
||||||
|
import org.matrix.android.sdk.api.session.room.getTimelineEvent
|
||||||
|
import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
|
||||||
|
import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntity
|
||||||
|
import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntityFields
|
||||||
|
import org.matrix.android.sdk.internal.database.model.PollResponseAggregatedSummaryEntity
|
||||||
|
import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsTestData.AN_EVENT_ID
|
||||||
|
import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsTestData.AN_INVALID_POLL_RESPONSE_EVENT
|
||||||
|
import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsTestData.A_BROKEN_POLL_REPLACE_EVENT
|
||||||
|
import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsTestData.A_POLL_END_EVENT
|
||||||
|
import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsTestData.A_POLL_REFERENCE_EVENT
|
||||||
|
import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsTestData.A_POLL_REPLACE_EVENT
|
||||||
|
import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsTestData.A_POLL_RESPONSE_EVENT
|
||||||
|
import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsTestData.A_POLL_RESPONSE_EVENT_WITH_A_WRONG_REFERENCE
|
||||||
|
import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsTestData.A_POLL_START_EVENT
|
||||||
|
import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsTestData.A_ROOM_ID
|
||||||
|
import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsTestData.A_TIMELINE_EVENT
|
||||||
|
import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsTestData.A_USER_ID_1
|
||||||
|
import org.matrix.android.sdk.test.fakes.FakeRealm
|
||||||
|
|
||||||
|
class PollAggregationProcessorTest {
|
||||||
|
|
||||||
|
private val pollAggregationProcessor: PollAggregationProcessor = DefaultPollAggregationProcessor()
|
||||||
|
private val realm = FakeRealm()
|
||||||
|
private val session = mockk<Session>()
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setup() {
|
||||||
|
mockEventAnnotationsSummaryEntity()
|
||||||
|
mockRoom(A_ROOM_ID, AN_EVENT_ID)
|
||||||
|
every { session.myUserId } returns A_USER_ID_1
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given a poll start event, when processing, then is ignored and returns false`() {
|
||||||
|
pollAggregationProcessor.handlePollStartEvent(realm.instance, A_POLL_START_EVENT).shouldBeFalse()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given a poll start event with a reference, when processing, then is ignored and returns false`() {
|
||||||
|
pollAggregationProcessor.handlePollStartEvent(realm.instance, A_POLL_REFERENCE_EVENT).shouldBeFalse()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given a poll start event with a replace relation but without a target event id, when processing, then is ignored and returns false`() {
|
||||||
|
pollAggregationProcessor.handlePollStartEvent(realm.instance, A_BROKEN_POLL_REPLACE_EVENT).shouldBeFalse()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given a poll start event with a replace, when processing, then is processed and returns true`() {
|
||||||
|
pollAggregationProcessor.handlePollStartEvent(realm.instance, A_POLL_REPLACE_EVENT).shouldBeTrue()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given a poll response event with a broken reference, when processing, then is ignored and returns false`() {
|
||||||
|
pollAggregationProcessor.handlePollResponseEvent(session, realm.instance, A_POLL_RESPONSE_EVENT_WITH_A_WRONG_REFERENCE).shouldBeFalse()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given a poll response event with a reference, when processing, then is processed and returns true`() {
|
||||||
|
every { realm.instance.createObject(PollResponseAggregatedSummaryEntity::class.java) } returns PollResponseAggregatedSummaryEntity()
|
||||||
|
pollAggregationProcessor.handlePollResponseEvent(session, realm.instance, A_POLL_RESPONSE_EVENT).shouldBeTrue()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given a poll response event after poll is closed, when processing, then is ignored and returns false`() {
|
||||||
|
every { realm.instance.createObject(PollResponseAggregatedSummaryEntity::class.java) } returns PollResponseAggregatedSummaryEntity().apply {
|
||||||
|
closedTime = (A_POLL_RESPONSE_EVENT.originServerTs ?: 0) - 1
|
||||||
|
}
|
||||||
|
pollAggregationProcessor.handlePollResponseEvent(session, realm.instance, A_POLL_RESPONSE_EVENT).shouldBeFalse()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given a poll response event which is already processed, when processing, then is ignored and returns false`() {
|
||||||
|
every { realm.instance.createObject(PollResponseAggregatedSummaryEntity::class.java) } returns PollResponseAggregatedSummaryEntity().apply {
|
||||||
|
sourceEvents = RealmList(A_POLL_RESPONSE_EVENT.eventId)
|
||||||
|
}
|
||||||
|
pollAggregationProcessor.handlePollResponseEvent(session, realm.instance, A_POLL_RESPONSE_EVENT).shouldBeFalse()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given a poll response event which is not one of the options, when processing, then is ignored and returns false`() {
|
||||||
|
every { realm.instance.createObject(PollResponseAggregatedSummaryEntity::class.java) } returns PollResponseAggregatedSummaryEntity()
|
||||||
|
pollAggregationProcessor.handlePollResponseEvent(session, realm.instance, AN_INVALID_POLL_RESPONSE_EVENT).shouldBeFalse()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given a poll end event, when processing, then is processed and return true`() {
|
||||||
|
every { realm.instance.createObject(PollResponseAggregatedSummaryEntity::class.java) } returns PollResponseAggregatedSummaryEntity()
|
||||||
|
val powerLevelsHelper = mockRedactionPowerLevels(A_USER_ID_1, true)
|
||||||
|
pollAggregationProcessor.handlePollEndEvent(session, powerLevelsHelper, realm.instance, A_POLL_END_EVENT).shouldBeTrue()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given a poll end event for my own poll without enough redaction power level, when processing, then is processed and returns true`() {
|
||||||
|
every { realm.instance.createObject(PollResponseAggregatedSummaryEntity::class.java) } returns PollResponseAggregatedSummaryEntity()
|
||||||
|
val powerLevelsHelper = mockRedactionPowerLevels(A_USER_ID_1, false)
|
||||||
|
pollAggregationProcessor.handlePollEndEvent(session, powerLevelsHelper, realm.instance, A_POLL_END_EVENT).shouldBeTrue()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given a poll end event without enough redaction power level, when is processed, then is ignored and return false`() {
|
||||||
|
every { realm.instance.createObject(PollResponseAggregatedSummaryEntity::class.java) } returns PollResponseAggregatedSummaryEntity()
|
||||||
|
val powerLevelsHelper = mockRedactionPowerLevels("another-sender-id", false)
|
||||||
|
val event = A_POLL_END_EVENT.copy(senderId = "another-sender-id")
|
||||||
|
pollAggregationProcessor.handlePollEndEvent(session, powerLevelsHelper, realm.instance, event).shouldBeFalse()
|
||||||
|
}
|
||||||
|
|
||||||
|
private inline fun <reified T : RealmModel> RealmQuery<T>.givenEqualTo(fieldName: String, value: String, result: RealmQuery<T>) {
|
||||||
|
every { equalTo(fieldName, value) } returns result
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun mockEventAnnotationsSummaryEntity() {
|
||||||
|
val queryResult = realm.givenWhereReturns(result = EventAnnotationsSummaryEntity())
|
||||||
|
queryResult.givenEqualTo(EventAnnotationsSummaryEntityFields.ROOM_ID, A_POLL_REPLACE_EVENT.roomId!!, queryResult)
|
||||||
|
queryResult.givenEqualTo(EventAnnotationsSummaryEntityFields.EVENT_ID, A_POLL_REPLACE_EVENT.eventId!!, queryResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun mockRoom(
|
||||||
|
roomId: String,
|
||||||
|
eventId: String
|
||||||
|
) {
|
||||||
|
val room = mockk<Room>()
|
||||||
|
every { session.getRoom(roomId) } returns room
|
||||||
|
every { room.getTimelineEvent(eventId) } returns A_TIMELINE_EVENT
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun mockRedactionPowerLevels(userId: String, isAbleToRedact: Boolean): PowerLevelsHelper {
|
||||||
|
val powerLevelsHelper = mockk<PowerLevelsHelper>()
|
||||||
|
every { powerLevelsHelper.isUserAbleToRedact(userId) } returns isAbleToRedact
|
||||||
|
return powerLevelsHelper
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,171 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.matrix.android.sdk.internal.session.room.aggregation.poll
|
||||||
|
|
||||||
|
import org.matrix.android.sdk.api.session.events.model.Event
|
||||||
|
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||||
|
import org.matrix.android.sdk.api.session.events.model.RelationType
|
||||||
|
import org.matrix.android.sdk.api.session.events.model.toContent
|
||||||
|
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.PollAnswer
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.message.PollCreationInfo
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.message.PollQuestion
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.message.PollResponse
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
|
||||||
|
import org.matrix.android.sdk.api.session.room.sender.SenderInfo
|
||||||
|
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||||
|
|
||||||
|
object PollEventsTestData {
|
||||||
|
internal const val A_USER_ID_1 = "@user_1:matrix.org"
|
||||||
|
internal const val A_ROOM_ID = "!sUeOGZKsBValPTUMax:matrix.org"
|
||||||
|
internal const val AN_EVENT_ID = "\$vApgexcL8Vfh-WxYKsFKCDooo67ttbjm3TiVKXaWijU"
|
||||||
|
|
||||||
|
internal val A_POLL_CONTENT = MessagePollContent(
|
||||||
|
unstablePollCreationInfo = PollCreationInfo(
|
||||||
|
question = PollQuestion(
|
||||||
|
unstableQuestion = "What is your favourite coffee?"
|
||||||
|
),
|
||||||
|
maxSelections = 1,
|
||||||
|
answers = listOf(
|
||||||
|
PollAnswer(
|
||||||
|
id = "5ef5f7b0-c9a1-49cf-a0b3-374729a43e76",
|
||||||
|
unstableAnswer = "Double Espresso"
|
||||||
|
),
|
||||||
|
PollAnswer(
|
||||||
|
id = "ec1a4db0-46d8-4d7a-9bb6-d80724715938",
|
||||||
|
unstableAnswer = "Macchiato"
|
||||||
|
),
|
||||||
|
PollAnswer(
|
||||||
|
id = "3677ca8e-061b-40ab-bffe-b22e4e88fcad",
|
||||||
|
unstableAnswer = "Iced Coffee"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
internal val A_POLL_RESPONSE_CONTENT = MessagePollResponseContent(
|
||||||
|
unstableResponse = PollResponse(
|
||||||
|
answers = listOf("5ef5f7b0-c9a1-49cf-a0b3-374729a43e76")
|
||||||
|
),
|
||||||
|
relatesTo = RelationDefaultContent(
|
||||||
|
type = RelationType.REFERENCE,
|
||||||
|
eventId = AN_EVENT_ID
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
internal val A_POLL_END_CONTENT = MessageEndPollContent(
|
||||||
|
relatesTo = RelationDefaultContent(
|
||||||
|
type = RelationType.REFERENCE,
|
||||||
|
eventId = AN_EVENT_ID
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
internal val AN_INVALID_POLL_RESPONSE_CONTENT = MessagePollResponseContent(
|
||||||
|
unstableResponse = PollResponse(
|
||||||
|
answers = listOf("fake-option-id")
|
||||||
|
),
|
||||||
|
relatesTo = RelationDefaultContent(
|
||||||
|
type = RelationType.REFERENCE,
|
||||||
|
eventId = AN_EVENT_ID
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
internal val A_POLL_START_EVENT = Event(
|
||||||
|
type = EventType.POLL_START.first(),
|
||||||
|
eventId = AN_EVENT_ID,
|
||||||
|
originServerTs = 1652435922563,
|
||||||
|
senderId = A_USER_ID_1,
|
||||||
|
roomId = A_ROOM_ID,
|
||||||
|
content = A_POLL_CONTENT.toContent()
|
||||||
|
)
|
||||||
|
|
||||||
|
internal val A_POLL_RESPONSE_EVENT = Event(
|
||||||
|
type = EventType.POLL_RESPONSE.first(),
|
||||||
|
eventId = AN_EVENT_ID,
|
||||||
|
originServerTs = 1652435922563,
|
||||||
|
senderId = A_USER_ID_1,
|
||||||
|
roomId = A_ROOM_ID,
|
||||||
|
content = A_POLL_RESPONSE_CONTENT.toContent()
|
||||||
|
)
|
||||||
|
|
||||||
|
internal val A_POLL_END_EVENT = Event(
|
||||||
|
type = EventType.POLL_END.first(),
|
||||||
|
eventId = AN_EVENT_ID,
|
||||||
|
originServerTs = 1652435922563,
|
||||||
|
senderId = A_USER_ID_1,
|
||||||
|
roomId = A_ROOM_ID,
|
||||||
|
content = A_POLL_END_CONTENT.toContent()
|
||||||
|
)
|
||||||
|
|
||||||
|
internal val A_TIMELINE_EVENT = TimelineEvent(
|
||||||
|
root = A_POLL_START_EVENT,
|
||||||
|
localId = 1234,
|
||||||
|
eventId = AN_EVENT_ID,
|
||||||
|
displayIndex = 0,
|
||||||
|
senderInfo = SenderInfo(A_USER_ID_1, "A_USER_ID_1", true, null)
|
||||||
|
)
|
||||||
|
|
||||||
|
internal val A_POLL_RESPONSE_EVENT_WITH_A_WRONG_REFERENCE = A_POLL_RESPONSE_EVENT.copy(
|
||||||
|
content = A_POLL_RESPONSE_CONTENT
|
||||||
|
.copy(
|
||||||
|
relatesTo = RelationDefaultContent(
|
||||||
|
type = RelationType.REPLACE,
|
||||||
|
eventId = null
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.toContent()
|
||||||
|
)
|
||||||
|
|
||||||
|
internal val A_POLL_REPLACE_EVENT = A_POLL_START_EVENT.copy(
|
||||||
|
content = A_POLL_CONTENT
|
||||||
|
.copy(
|
||||||
|
relatesTo = RelationDefaultContent(
|
||||||
|
type = RelationType.REPLACE,
|
||||||
|
eventId = AN_EVENT_ID
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.toContent()
|
||||||
|
)
|
||||||
|
|
||||||
|
internal val A_BROKEN_POLL_REPLACE_EVENT = A_POLL_START_EVENT.copy(
|
||||||
|
content = A_POLL_CONTENT
|
||||||
|
.copy(
|
||||||
|
relatesTo = RelationDefaultContent(
|
||||||
|
type = RelationType.REPLACE,
|
||||||
|
eventId = null
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.toContent()
|
||||||
|
)
|
||||||
|
|
||||||
|
internal val A_POLL_REFERENCE_EVENT = A_POLL_START_EVENT.copy(
|
||||||
|
content = A_POLL_CONTENT
|
||||||
|
.copy(
|
||||||
|
relatesTo = RelationDefaultContent(
|
||||||
|
type = RelationType.REFERENCE,
|
||||||
|
eventId = AN_EVENT_ID
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.toContent()
|
||||||
|
)
|
||||||
|
|
||||||
|
internal val AN_INVALID_POLL_RESPONSE_EVENT = A_POLL_RESPONSE_EVENT.copy(
|
||||||
|
content = AN_INVALID_POLL_RESPONSE_CONTENT.toContent()
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,36 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.matrix.android.sdk.test.fakes
|
||||||
|
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import io.realm.Realm
|
||||||
|
import io.realm.RealmModel
|
||||||
|
import io.realm.RealmQuery
|
||||||
|
import io.realm.kotlin.where
|
||||||
|
|
||||||
|
internal class FakeRealm {
|
||||||
|
|
||||||
|
val instance = mockk<Realm>(relaxed = true)
|
||||||
|
|
||||||
|
inline fun <reified T : RealmModel> givenWhereReturns(result: T?): RealmQuery<T> {
|
||||||
|
val queryResult = mockk<RealmQuery<T>>()
|
||||||
|
every { queryResult.findFirst() } returns result
|
||||||
|
every { instance.where<T>() } returns queryResult
|
||||||
|
return queryResult
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user