Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(offline-backends): users and conversations without metadata refe…
Browse files Browse the repository at this point in the history
…tch pt1. (AR-3123) (#1736)

* feat: adjust query to hide 1:1 convos without metadata

* feat: adjustment query to consider deleted users logic as it is now

* feat: tests for query conversations details

* feat: persistence for getting users out of sync

* feat: persistence for getting users out of sync

* feat: pr comments single quotes

* feat: invok operator

* feat: persist failed convos

* feat: cleanup

* feat: tests cov

* feat: tests cov

* feat: tests cov

* feat: tests cov

* feat: refactor, persist users withoutmetadata with dedicated field

* feat: refactor, relay on missing metadata field for refetch usres

* feat: refactor, relay on missing metadata field for refetch conversations

* feat: refactor, relay on missing metadata field for refetch conversations

* feat: refactor, relay on missing metadata field for refetch conversations

* feat: refactor, fixing tests

* chore: add migration tests
yamilmedina authored May 19, 2023

Verified

This commit was signed with the committer’s verified signature.
bkueng Beat Küng
1 parent 2c3998d commit 53114bd
Showing 18 changed files with 373 additions and 69 deletions.
Original file line number Diff line number Diff line change
@@ -20,6 +20,7 @@ package com.wire.kalium.logic.data.conversation

import com.wire.kalium.logic.data.connection.ConnectionStatusMapper
import com.wire.kalium.logic.data.id.IdMapper
import com.wire.kalium.logic.data.id.NetworkQualifiedId
import com.wire.kalium.logic.data.id.TeamId
import com.wire.kalium.logic.data.id.toApi
import com.wire.kalium.logic.data.id.toDao
@@ -75,6 +76,7 @@ interface ConversationMapper {
fun toApiModel(name: String?, members: List<UserId>, teamId: String?, options: ConversationOptions): CreateConversationRequest

fun fromMigrationModel(conversation: Conversation): ConversationEntity
fun fromFailedGroupConversationToEntity(conversationId: NetworkQualifiedId): ConversationEntity
}

@Suppress("TooManyFunctions", "LongParameterList")
@@ -108,7 +110,8 @@ internal class ConversationMapperImpl(
lastModifiedDate = apiModel.lastEventTime.toInstant(),
access = apiModel.access.map { it.toDAO() },
accessRole = apiModel.accessRole.map { it.toDAO() },
receiptMode = receiptModeMapper.fromApiToDaoModel(apiModel.receiptMode)
receiptMode = receiptModeMapper.fromApiToDaoModel(apiModel.receiptMode),
hasIncompleteMetadata = false
)

override fun fromApiModelToDaoModel(apiModel: ConvProtocol): Protocol = when (apiModel) {
@@ -347,6 +350,29 @@ internal class ConversationMapperImpl(
)
}

/**
* Default values and marked as [ConversationEntity.hasIncompleteMetadata] = true.
* So later we can re-fetch them.
*/
override fun fromFailedGroupConversationToEntity(conversationId: NetworkQualifiedId): ConversationEntity = ConversationEntity(
id = conversationId.toDao(),
name = null,
type = ConversationEntity.Type.GROUP,
teamId = null,
protocolInfo = ProtocolInfo.Proteus,
mutedStatus = ConversationEntity.MutedStatus.ALL_ALLOWED,
mutedTime = 0,
removedBy = null,
creatorId = "",
lastNotificationDate = "1970-01-01T00:00:00.000Z".toInstant(),
lastModifiedDate = "1970-01-01T00:00:00.000Z".toInstant(),
lastReadDate = "1970-01-01T00:00:00.000Z".toInstant(),
access = emptyList(),
accessRole = emptyList(),
receiptMode = ConversationEntity.ReceiptMode.DISABLED,
hasIncompleteMetadata = true
)

private fun ConversationResponse.getProtocolInfo(mlsGroupState: GroupState?): ProtocolInfo {
return when (protocol) {
ConvProtocol.MLS -> ProtocolInfo.MLS(
Original file line number Diff line number Diff line change
@@ -26,6 +26,7 @@ import com.wire.kalium.logic.data.client.MLSClientProvider
import com.wire.kalium.logic.data.id.ConversationId
import com.wire.kalium.logic.data.id.GroupID
import com.wire.kalium.logic.data.id.IdMapper
import com.wire.kalium.logic.data.id.NetworkQualifiedId
import com.wire.kalium.logic.data.id.QualifiedID
import com.wire.kalium.logic.data.id.toApi
import com.wire.kalium.logic.data.id.toCrypto
@@ -245,7 +246,8 @@ internal class ConversationDataSource internal constructor(
}.onSuccess { conversations ->
if (conversations.conversationsFailed.isNotEmpty()) {
kaliumLogger.withFeatureId(CONVERSATIONS)
.d("Skipping ${conversations.conversationsFailed.size} conversations failed")
.d("Handling ${conversations.conversationsFailed.size} conversations failed")
handleFailedConversations(conversations.conversationsFailed)
}
if (conversations.conversationsNotFound.isNotEmpty()) {
kaliumLogger.withFeatureId(CONVERSATIONS)
@@ -599,6 +601,7 @@ internal class ConversationDataSource internal constructor(
conversationDAO.deleteConversationByQualifiedID(conversationId.toDao())
}
}

is Conversation.ProtocolInfo.Proteus -> wrapStorageRequest {
conversationDAO.deleteConversationByQualifiedID(conversationId.toDao())
}
@@ -686,6 +689,18 @@ internal class ConversationDataSource internal constructor(
}
}

private suspend fun handleFailedConversations(
conversationsFailed: List<NetworkQualifiedId>
): Either<CoreFailure, Unit> {
return wrapStorageRequest {
if (conversationsFailed.isNotEmpty()) {
conversationDAO.insertConversations(conversationsFailed.map { conversationId ->
conversationMapper.fromFailedGroupConversationToEntity(conversationId)
})
}
}
}

companion object {
const val DEFAULT_MEMBER_ROLE = "wire_member"
}
Original file line number Diff line number Diff line change
@@ -22,6 +22,7 @@ import com.wire.kalium.logic.data.client.ClientMapper
import com.wire.kalium.logic.data.client.OtherUserClient
import com.wire.kalium.logic.data.event.Event
import com.wire.kalium.logic.data.id.IdMapper
import com.wire.kalium.logic.data.id.NetworkQualifiedId
import com.wire.kalium.logic.data.id.TeamId
import com.wire.kalium.logic.data.id.toDao
import com.wire.kalium.logic.data.id.toModel
@@ -88,6 +89,8 @@ interface UserMapper {

fun apiToEntity(user: UserProfileDTO, member: TeamsApi.TeamMemberDTO?, teamId: String?, selfUser: QualifiedID): UserEntity
fun toUpdateDaoFromEvent(event: Event.User.Update, userEntity: UserEntity): UserEntity

fun fromFailedUserToEntity(userId: NetworkQualifiedId): UserEntity
}

internal class UserMapperImpl(
@@ -131,7 +134,8 @@ internal class UserMapperImpl(
availabilityStatus = UserAvailabilityStatusEntity.NONE,
userType = userTypeEntity ?: UserTypeEntity.STANDARD,
botService = userProfileDTO.service?.let { BotEntity(it.id, it.provider) },
deleted = userProfileDTO.deleted ?: false
deleted = userProfileDTO.deleted ?: false,
hasIncompleteMetadata = false
)
}

@@ -304,4 +308,28 @@ internal class UserMapperImpl(
)
}
}

/**
* Default values and marked as [UserEntity.hasIncompleteMetadata] = true.
* So later we can re-fetch them.
*/
override fun fromFailedUserToEntity(userId: NetworkQualifiedId): UserEntity {
return UserEntity(
id = userId.toDao(),
name = null,
handle = null,
email = null,
phone = null,
accentId = 1,
team = null,
connectionStatus = ConnectionEntity.State.ACCEPTED,
previewAssetId = null,
completeAssetId = null,
availabilityStatus = UserAvailabilityStatusEntity.NONE,
userType = UserTypeEntity.STANDARD,
botService = null,
deleted = false,
hasIncompleteMetadata = true
)
}
}
Original file line number Diff line number Diff line change
@@ -26,6 +26,7 @@ import com.wire.kalium.logic.data.conversation.Recipient
import com.wire.kalium.logic.data.event.Event
import com.wire.kalium.logic.data.id.ConversationId
import com.wire.kalium.logic.data.id.IdMapper
import com.wire.kalium.logic.data.id.NetworkQualifiedId
import com.wire.kalium.logic.data.id.QualifiedIdMapper
import com.wire.kalium.logic.data.id.toApi
import com.wire.kalium.logic.data.id.toDao
@@ -117,6 +118,11 @@ internal interface UserRepository {
* otherwise [Either.Left] with [NetworkFailure]
*/
suspend fun updateSelfEmail(email: String): Either<NetworkFailure, Boolean>

/**
* Updates users without metadata from the server.
*/
suspend fun syncUsersWithoutMetadata(): Either<CoreFailure, Unit>
}

@Suppress("LongParameterList", "TooManyFunctions")
@@ -196,41 +202,36 @@ internal class UserDataSource internal constructor(
return fetchUsersByIds(ids.toSet())
}

override suspend fun fetchUsersByIds(qualifiedUserIdList: Set<UserId>): Either<CoreFailure, Unit> {
val selfUserDomain = selfUserId.domain
qualifiedUserIdList.groupBy { it.domain }
.filter { it.value.isNotEmpty() }
.map { (domain: String, usersOnDomain: List<UserId>) ->
when (selfUserDomain == domain) {
true -> fetchMultipleUsers(usersOnDomain)
false -> {
usersOnDomain.forEach { userId ->
fetchUserInfo(userId).fold({
kaliumLogger.w("Ignoring external users details")
}) { kaliumLogger.d("External users details saved") }
}
Either.Right(Unit)
}
}
}

return Either.Right(Unit)
}

private suspend fun fetchMultipleUsers(qualifiedUsersOnSameDomainList: List<UserId>) = wrapApiRequest {
userDetailsApi.getMultipleUsers(
ListUserRequest.qualifiedIds(qualifiedUsersOnSameDomainList.map { userId -> userId.toApi() })
)
}.flatMap { listUserProfileDTO -> persistUsers(listUserProfileDTO.usersFound) }

override suspend fun fetchUserInfo(userId: UserId) =
wrapApiRequest { userDetailsApi.getUserInfo(userId.toApi()) }
.flatMap { userProfileDTO -> persistUsers(listOf(userProfileDTO)) }

override suspend fun fetchUsersByIds(qualifiedUserIdList: Set<UserId>): Either<CoreFailure, Unit> {
if (qualifiedUserIdList.isEmpty()) {
return Either.Right(Unit)
}

return wrapApiRequest {
userDetailsApi.getMultipleUsers(
ListUserRequest.qualifiedIds(qualifiedUserIdList.map { userId -> userId.toApi() })
)
}.flatMap { listUserProfileDTO ->
if (listUserProfileDTO.usersFailed.isNotEmpty()) {
kaliumLogger.d("Handling ${listUserProfileDTO.usersFailed.size} failed users")
persistIncompleteUsers(listUserProfileDTO.usersFailed)
}
persistUsers(listUserProfileDTO.usersFound)
}
}

override suspend fun updateSelfEmail(email: String): Either<NetworkFailure, Boolean> = wrapApiRequest {
selfApi.updateEmailAddress(email)
}

private suspend fun persistIncompleteUsers(usersFailed: List<NetworkQualifiedId>) = wrapStorageRequest {
userDAO.insertOrIgnoreUsers(usersFailed.map { userMapper.fromFailedUserToEntity(it) })
}

private suspend fun persistUsers(listUserProfileDTO: List<UserProfileDTO>) = wrapStorageRequest {
val selfUserDomain = selfUserId.domain
val selfUserTeamId = selfTeamIdProvider().getOrNull()?.value
@@ -463,6 +464,14 @@ internal class UserDataSource internal constructor(
)
}

override suspend fun syncUsersWithoutMetadata(): Either<CoreFailure, Unit> = wrapStorageRequest {
userDAO.getUsersWithoutMetadata()
}.flatMap { usersWithoutMetadata ->
kaliumLogger.d("Numbers of users refreshed: ${usersWithoutMetadata.size}")
val userIds = usersWithoutMetadata.map { it.id.toModel() }.toSet()
fetchUsersByIds(userIds)
}

companion object {
internal const val SELF_USER_ID_KEY = "selfUserID"
internal val FEDERATED_USER_TTL = 5.minutes
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* Wire
* Copyright (C) 2023 Wire Swiss GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see http://www.gnu.org/licenses/.
*/
package com.wire.kalium.logic.feature.publicuser

import com.wire.kalium.logic.data.user.UserRepository
import com.wire.kalium.logic.functional.fold
import com.wire.kalium.logic.kaliumLogger
import com.wire.kalium.util.KaliumDispatcher
import com.wire.kalium.util.KaliumDispatcherImpl
import kotlinx.coroutines.withContext

/**
* Refresh users without metadata, only if necessary.
*/
interface RefreshUsersWithoutMetadataUseCase {
suspend operator fun invoke()
}

internal class RefreshUsersWithoutMetadataUseCaseImpl(
private val userRepository: UserRepository,
private val dispatchers: KaliumDispatcher = KaliumDispatcherImpl
) : RefreshUsersWithoutMetadataUseCase {

override suspend fun invoke() = withContext(dispatchers.io) {
kaliumLogger.d("Started syncing users without metadata")
userRepository.syncUsersWithoutMetadata()
.fold({
kaliumLogger.w("Error while syncing users without metadata $it")
}) {
kaliumLogger.d("Finished syncing users without metadata")
}
}

}
Original file line number Diff line number Diff line change
@@ -45,6 +45,8 @@ import com.wire.kalium.logic.feature.publicuser.GetAllContactsUseCase
import com.wire.kalium.logic.feature.publicuser.GetAllContactsUseCaseImpl
import com.wire.kalium.logic.feature.publicuser.GetKnownUserUseCase
import com.wire.kalium.logic.feature.publicuser.GetKnownUserUseCaseImpl
import com.wire.kalium.logic.feature.publicuser.RefreshUsersWithoutMetadataUseCase
import com.wire.kalium.logic.feature.publicuser.RefreshUsersWithoutMetadataUseCaseImpl
import com.wire.kalium.logic.feature.publicuser.search.SearchKnownUsersUseCase
import com.wire.kalium.logic.feature.publicuser.search.SearchKnownUsersUseCaseImpl
import com.wire.kalium.logic.feature.publicuser.search.SearchPublicUsersUseCase
@@ -98,6 +100,7 @@ class UserScope internal constructor(
val getAllKnownUsers: GetAllContactsUseCase get() = GetAllContactsUseCaseImpl(userRepository)
val getKnownUser: GetKnownUserUseCase get() = GetKnownUserUseCaseImpl(userRepository)
val getUserInfo: GetUserInfoUseCase get() = GetUserInfoUseCaseImpl(userRepository, teamRepository)
val refreshUsersWithoutMetadata: RefreshUsersWithoutMetadataUseCase get() = RefreshUsersWithoutMetadataUseCaseImpl(userRepository)
val updateSelfAvailabilityStatus: UpdateSelfAvailabilityStatusUseCase
get() = UpdateSelfAvailabilityStatusUseCase(userRepository, messageSender, clientIdProvider, selfUserId)
val getAllContactsNotInConversation: GetAllContactsNotInConversationUseCase
Original file line number Diff line number Diff line change
@@ -199,6 +199,16 @@ class ConversationRepositoryTest {
conversationRepository.fetchConversations()

// then
verify(arrangement.conversationDAO)
.suspendFunction(arrangement.conversationDAO::insertConversations)
.with(
matching { conversations ->
conversations.any { entity ->
entity.id.value == CONVERSATION_RESPONSE_DTO.conversationsFailed.first().value
}
}
).wasInvoked(exactly = once)

verify(arrangement.conversationDAO)
.suspendFunction(arrangement.conversationDAO::insertConversations)
.with(
Loading

0 comments on commit 53114bd

Please sign in to comment.