Merge pull request #150 from ouchadam/release-candidate

[Auto] Release Candidate
This commit is contained in:
Adam Brown 2022-09-19 22:19:02 +01:00 committed by GitHub
commit b3d4f79d8c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 181 additions and 91 deletions

View File

@ -25,7 +25,7 @@ jobs:
- name: Create pip requirements - name: Create pip requirements
run: | run: |
echo "matrix-synapse==v1.60.0" > requirements.txt echo "matrix-synapse" > requirements.txt
- name: Set up Python 3.8 - name: Set up Python 3.8
uses: actions/setup-python@v2 uses: actions/setup-python@v2

View File

@ -68,7 +68,7 @@ import java.time.Clock
internal class AppModule(context: Application, logger: MatrixLogger) { internal class AppModule(context: Application, logger: MatrixLogger) {
private val buildMeta = BuildMeta(BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE) private val buildMeta = BuildMeta(BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE, isDebug = BuildConfig.DEBUG)
private val deviceMeta = DeviceMeta(Build.VERSION.SDK_INT) private val deviceMeta = DeviceMeta(Build.VERSION.SDK_INT)
private val trackingModule by unsafeLazy { private val trackingModule by unsafeLazy {
TrackingModule( TrackingModule(
@ -94,7 +94,7 @@ internal class AppModule(context: Application, logger: MatrixLogger) {
private val workModule = WorkModule(context) private val workModule = WorkModule(context)
private val imageLoaderModule = ImageLoaderModule(context) private val imageLoaderModule = ImageLoaderModule(context)
private val matrixModules = MatrixModules(storeModule, trackingModule, workModule, logger, coroutineDispatchers, context.contentResolver) private val matrixModules = MatrixModules(storeModule, trackingModule, workModule, logger, coroutineDispatchers, context.contentResolver, buildMeta)
val domainModules = DomainModules(matrixModules, trackingModule.errorTracker, workModule, storeModule, context, coroutineDispatchers) val domainModules = DomainModules(matrixModules, trackingModule.errorTracker, workModule, storeModule, context, coroutineDispatchers)
val coreAndroidModule = CoreAndroidModule( val coreAndroidModule = CoreAndroidModule(
@ -232,6 +232,7 @@ internal class MatrixModules(
private val logger: MatrixLogger, private val logger: MatrixLogger,
private val coroutineDispatchers: CoroutineDispatchers, private val coroutineDispatchers: CoroutineDispatchers,
private val contentResolver: ContentResolver, private val contentResolver: ContentResolver,
private val buildMeta: BuildMeta,
) { ) {
val matrix by unsafeLazy { val matrix by unsafeLazy {
@ -240,7 +241,7 @@ internal class MatrixModules(
MatrixClient( MatrixClient(
KtorMatrixHttpClientFactory( KtorMatrixHttpClientFactory(
credentialsStore, credentialsStore,
includeLogging = true includeLogging = buildMeta.isDebug,
), ),
logger logger
).also { ).also {

View File

@ -132,7 +132,7 @@ ext.kotlinTest = { dependencies ->
dependencies.testImplementation Dependencies.mavenCentral.kluent dependencies.testImplementation Dependencies.mavenCentral.kluent
dependencies.testImplementation Dependencies.mavenCentral.kotlinTest dependencies.testImplementation Dependencies.mavenCentral.kotlinTest
dependencies.testImplementation "org.jetbrains.kotlin:kotlin-test-junit:1.6.10" dependencies.testImplementation "org.jetbrains.kotlin:kotlin-test-junit:1.6.10"
dependencies.testImplementation 'io.mockk:mockk:1.12.7' dependencies.testImplementation 'io.mockk:mockk:1.12.8'
dependencies.testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4' dependencies.testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4'
dependencies.testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.0' dependencies.testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.0'

View File

@ -3,4 +3,5 @@ package app.dapk.st.core
data class BuildMeta( data class BuildMeta(
val versionName: String, val versionName: String,
val versionCode: Int, val versionCode: Int,
val isDebug: Boolean,
) )

View File

@ -143,7 +143,7 @@ ext.Dependencies.with {
junit = "junit:junit:4.13.2" junit = "junit:junit:4.13.2"
kluent = "org.amshove.kluent:kluent:1.68" kluent = "org.amshove.kluent:kluent:1.68"
mockk = 'io.mockk:mockk:1.12.7' mockk = 'io.mockk:mockk:1.12.8'
matrixOlm = "org.matrix.android:olm-sdk:3.2.12" matrixOlm = "org.matrix.android:olm-sdk:3.2.12"
} }

View File

@ -8,3 +8,7 @@ fun OlmAccount.readIdentityKeys(): Pair<Ed25519, Curve25519> {
val identityKeys = this.identityKeys() val identityKeys = this.identityKeys()
return Ed25519(identityKeys["ed25519"]!!) to Curve25519(identityKeys["curve25519"]!!) return Ed25519(identityKeys["ed25519"]!!) to Curve25519(identityKeys["curve25519"]!!)
} }
fun OlmAccount.oneTimeCurveKeys(): List<Pair<String, Curve25519>> {
return this.oneTimeKeys()["curve25519"]?.map { it.key to Curve25519(it.value) } ?: emptyList()
}

View File

@ -67,7 +67,7 @@ class OlmWrapper(
private suspend fun accountCrypto(deviceCredentials: DeviceCredentials): AccountCryptoSession? { private suspend fun accountCrypto(deviceCredentials: DeviceCredentials): AccountCryptoSession? {
return olmStore.read()?.let { olmAccount -> return olmStore.read()?.let { olmAccount ->
createAccountCryptoSession(deviceCredentials, olmAccount) createAccountCryptoSession(deviceCredentials, olmAccount, isNew = false)
} }
} }
@ -80,12 +80,12 @@ class OlmWrapper(
val olmAccount = this.olmAccount as OlmAccount val olmAccount = this.olmAccount as OlmAccount
olmAccount.generateOneTimeKeys(count) olmAccount.generateOneTimeKeys(count)
val oneTimeKeys = DeviceService.OneTimeKeys(olmAccount.oneTimeKeys()["curve25519"]!!.map { val oneTimeKeys = DeviceService.OneTimeKeys(olmAccount.oneTimeCurveKeys().map { (key, value) ->
DeviceService.OneTimeKeys.Key.SignedCurve( DeviceService.OneTimeKeys.Key.SignedCurve(
keyId = it.key, keyId = key,
value = it.value, value = value.value,
signature = DeviceService.OneTimeKeys.Key.SignedCurve.Ed25519Signature( signature = DeviceService.OneTimeKeys.Key.SignedCurve.Ed25519Signature(
value = it.value.toSignedJson(olmAccount), value = value.value.toSignedJson(olmAccount),
deviceId = credentials.deviceId, deviceId = credentials.deviceId,
userId = credentials.userId, userId = credentials.userId,
) )
@ -98,20 +98,21 @@ class OlmWrapper(
private suspend fun createAccountCrypto(deviceCredentials: DeviceCredentials, action: suspend (AccountCryptoSession) -> Unit): AccountCryptoSession { private suspend fun createAccountCrypto(deviceCredentials: DeviceCredentials, action: suspend (AccountCryptoSession) -> Unit): AccountCryptoSession {
val olmAccount = OlmAccount() val olmAccount = OlmAccount()
return createAccountCryptoSession(deviceCredentials, olmAccount).also { return createAccountCryptoSession(deviceCredentials, olmAccount, isNew = true).also {
action(it) action(it)
olmStore.persist(olmAccount) olmStore.persist(olmAccount)
} }
} }
private fun createAccountCryptoSession(credentials: DeviceCredentials, olmAccount: OlmAccount): AccountCryptoSession { private fun createAccountCryptoSession(credentials: DeviceCredentials, olmAccount: OlmAccount, isNew: Boolean): AccountCryptoSession {
val (identityKey, senderKey) = olmAccount.readIdentityKeys() val (identityKey, senderKey) = olmAccount.readIdentityKeys()
return AccountCryptoSession( return AccountCryptoSession(
fingerprint = identityKey, fingerprint = identityKey,
senderKey = senderKey, senderKey = senderKey,
deviceKeys = deviceKeyFactory.create(credentials.userId, credentials.deviceId, identityKey, senderKey, olmAccount), deviceKeys = deviceKeyFactory.create(credentials.userId, credentials.deviceId, identityKey, senderKey, olmAccount),
olmAccount = olmAccount, olmAccount = olmAccount,
maxKeys = olmAccount.maxOneTimeKeys().toInt() maxKeys = olmAccount.maxOneTimeKeys().toInt(),
hasKeys = !isNew,
) )
} }
@ -136,6 +137,7 @@ class OlmWrapper(
singletonFlows.update("room-${roomId.value}", rotatedSession) singletonFlows.update("room-${roomId.value}", rotatedSession)
} }
} }
else -> this else -> this
} }
} }
@ -277,10 +279,12 @@ class OlmWrapper(
} }
} }
} }
OlmMessage.MESSAGE_TYPE_MESSAGE -> { OlmMessage.MESSAGE_TYPE_MESSAGE -> {
logger.crypto("decrypting olm message type") logger.crypto("decrypting olm message type")
session.decryptMessage(olmMessage)?.let { JsonString(it) } session.decryptMessage(olmMessage)?.let { JsonString(it) }
} }
else -> throw IllegalArgumentException("Unknown message type: $type") else -> throw IllegalArgumentException("Unknown message type: $type")
} }
}.onFailure { }.onFailure {
@ -297,7 +301,7 @@ class OlmWrapper(
} }
private suspend fun AccountCryptoSession.updateAccountInstance(olmAccount: OlmAccount) { private suspend fun AccountCryptoSession.updateAccountInstance(olmAccount: OlmAccount) {
singletonFlows.update("account-crypto", this.copy(olmAccount = olmAccount)) singletonFlows.update("account-crypto", this.copy(olmAccount = olmAccount, hasKeys = true))
olmStore.persist(olmAccount) olmStore.persist(olmAccount)
} }

View File

@ -99,6 +99,19 @@ class HomeViewModel(
state = when (val current = state) { state = when (val current = state) {
Loading -> current Loading -> current
is SignedIn -> { is SignedIn -> {
when (page) {
current.page -> current
else -> current.copy(page = page).also {
pageChangeSideEffects(page)
}
}
}
SignedOut -> current
}
}
private fun pageChangeSideEffects(page: Page) {
when (page) { when (page) {
Page.Directory -> { Page.Directory -> {
// do nothing // do nothing
@ -106,11 +119,6 @@ class HomeViewModel(
Page.Profile -> profileViewModel.reset() Page.Profile -> profileViewModel.reset()
} }
current.copy(page = page)
}
SignedOut -> current
}
} }
fun stop() { fun stop() {

View File

@ -18,7 +18,7 @@ private const val ENABLED_MATERIAL_YOU = true
class SettingsItemFactoryTest { class SettingsItemFactoryTest {
private val buildMeta = BuildMeta(versionName = "a-version-name", versionCode = 100) private val buildMeta = BuildMeta(versionName = "a-version-name", versionCode = 100, isDebug = false)
private val deviceMeta = DeviceMeta(apiVersion = 31) private val deviceMeta = DeviceMeta(apiVersion = 31)
private val fakePushTokenRegistrars = FakePushRegistrars() private val fakePushTokenRegistrars = FakePushRegistrars()
private val fakeThemeStore = FakeThemeStore() private val fakeThemeStore = FakeThemeStore()

View File

@ -72,6 +72,7 @@ interface Olm {
val fingerprint: Ed25519, val fingerprint: Ed25519,
val senderKey: Curve25519, val senderKey: Curve25519,
val deviceKeys: DeviceKeys, val deviceKeys: DeviceKeys,
val hasKeys: Boolean,
val maxKeys: Int, val maxKeys: Int,
val olmAccount: Any, val olmAccount: Any,
) )

View File

@ -15,18 +15,30 @@ internal class MaybeCreateAndUploadOneTimeKeysUseCaseImpl(
private val credentialsStore: CredentialsStore, private val credentialsStore: CredentialsStore,
private val deviceService: DeviceService, private val deviceService: DeviceService,
private val logger: MatrixLogger, private val logger: MatrixLogger,
): MaybeCreateAndUploadOneTimeKeysUseCase { ) : MaybeCreateAndUploadOneTimeKeysUseCase {
override suspend fun invoke(currentServerKeyCount: ServerKeyCount) { override suspend fun invoke(currentServerKeyCount: ServerKeyCount) {
val ensureCryptoAccount = fetchAccountCryptoUseCase.invoke() val cryptoAccount = fetchAccountCryptoUseCase.invoke()
val keysDiff = (ensureCryptoAccount.maxKeys / 2) - currentServerKeyCount.value when {
if (keysDiff > 0) { currentServerKeyCount.value == 0 && cryptoAccount.hasKeys -> {
logger.crypto("Server has no keys but a crypto instance exists, waiting for next update")
}
else -> {
val keysDiff = (cryptoAccount.maxKeys / 2) - currentServerKeyCount.value
when {
keysDiff > 0 -> {
logger.crypto("current otk: $currentServerKeyCount, creating: $keysDiff") logger.crypto("current otk: $currentServerKeyCount, creating: $keysDiff")
ensureCryptoAccount.createAndUploadOneTimeKeys(countToCreate = keysDiff + (ensureCryptoAccount.maxKeys / 4)) cryptoAccount.createAndUploadOneTimeKeys(countToCreate = keysDiff + (cryptoAccount.maxKeys / 4))
} else { }
else -> {
logger.crypto("current otk: $currentServerKeyCount, not creating new keys") logger.crypto("current otk: $currentServerKeyCount, not creating new keys")
} }
} }
}
}
}
private suspend fun Olm.AccountCryptoSession.createAndUploadOneTimeKeys(countToCreate: Int) { private suspend fun Olm.AccountCryptoSession.createAndUploadOneTimeKeys(countToCreate: Int) {
with(olm) { with(olm) {

View File

@ -22,12 +22,11 @@ class MaybeCreateAndUploadOneTimeKeysUseCaseTest {
private val fakeDeviceService = FakeDeviceService() private val fakeDeviceService = FakeDeviceService()
private val fakeOlm = FakeOlm() private val fakeOlm = FakeOlm()
private val fakeCredentialsStore = FakeCredentialsStore().also { private val fakeCredentialsStore = FakeCredentialsStore().also { it.givenCredentials().returns(A_USER_CREDENTIALS) }
it.givenCredentials().returns(A_USER_CREDENTIALS) private val fakeFetchAccountCryptoUseCase = FakeFetchAccountCryptoUseCase()
}
private val maybeCreateAndUploadOneTimeKeysUseCase = MaybeCreateAndUploadOneTimeKeysUseCaseImpl( private val maybeCreateAndUploadOneTimeKeysUseCase = MaybeCreateAndUploadOneTimeKeysUseCaseImpl(
FakeFetchAccountCryptoUseCase().also { it.givenFetch().returns(AN_ACCOUNT_CRYPTO_SESSION) }, fakeFetchAccountCryptoUseCase.also { it.givenFetch().returns(AN_ACCOUNT_CRYPTO_SESSION) },
fakeOlm, fakeOlm,
fakeCredentialsStore, fakeCredentialsStore,
fakeDeviceService, fakeDeviceService,
@ -43,6 +42,16 @@ class MaybeCreateAndUploadOneTimeKeysUseCaseTest {
fakeDeviceService.verifyDidntUploadOneTimeKeys() fakeDeviceService.verifyDidntUploadOneTimeKeys()
} }
@Test
fun `given account has keys and server count is 0 then does nothing`() = runTest {
fakeFetchAccountCryptoUseCase.givenFetch().returns(AN_ACCOUNT_CRYPTO_SESSION.copy(hasKeys = true))
val zeroServiceKeys = ServerKeyCount(0)
maybeCreateAndUploadOneTimeKeysUseCase.invoke(zeroServiceKeys)
fakeDeviceService.verifyDidntUploadOneTimeKeys()
}
@Test @Test
fun `given 0 current keys than generates and uploads 75 percent of the max key capacity`() = runTest { fun `given 0 current keys than generates and uploads 75 percent of the max key capacity`() = runTest {
fakeDeviceService.expect { it.uploadOneTimeKeys(GENERATED_ONE_TIME_KEYS) } fakeDeviceService.expect { it.uploadOneTimeKeys(GENERATED_ONE_TIME_KEYS) }

View File

@ -10,8 +10,9 @@ fun anAccountCryptoSession(
senderKey: Curve25519 = aCurve25519(), senderKey: Curve25519 = aCurve25519(),
deviceKeys: DeviceKeys = aDeviceKeys(), deviceKeys: DeviceKeys = aDeviceKeys(),
maxKeys: Int = 5, maxKeys: Int = 5,
hasKeys: Boolean = false,
olmAccount: Any = mockk(), olmAccount: Any = mockk(),
) = Olm.AccountCryptoSession(fingerprint, senderKey, deviceKeys, maxKeys, olmAccount) ) = Olm.AccountCryptoSession(fingerprint, senderKey, deviceKeys, hasKeys, maxKeys, olmAccount)
fun aRoomCryptoSession( fun aRoomCryptoSession(
creationTimestampUtc: Long = 0L, creationTimestampUtc: Long = 0L,

View File

@ -56,6 +56,12 @@ internal data class ApiSyncRoom(
@SerialName("state") val state: ApiSyncRoomState, @SerialName("state") val state: ApiSyncRoomState,
@SerialName("account_data") val accountData: ApiAccountData? = null, @SerialName("account_data") val accountData: ApiAccountData? = null,
@SerialName("ephemeral") val ephemeral: ApiEphemeral? = null, @SerialName("ephemeral") val ephemeral: ApiEphemeral? = null,
@SerialName("summary") val summary: ApiRoomSummary? = null,
)
@Serializable
internal data class ApiRoomSummary(
@SerialName("m.heroes") val heroes: List<UserId>? = null
) )
@Serializable @Serializable

View File

@ -21,7 +21,13 @@ internal sealed class ApiTimelineEvent {
@Serializable @Serializable
internal data class Content( internal data class Content(
@SerialName("type") val type: String? = null @SerialName("type") val type: String? = null
) ) {
object Type {
const val SPACE = "m.space"
}
}
} }
@Serializable @Serializable
@ -50,6 +56,18 @@ internal sealed class ApiTimelineEvent {
) )
} }
@Serializable
@SerialName("m.room.canonical_alias")
internal data class CanonicalAlias(
@SerialName("event_id") val id: EventId,
@SerialName("content") val content: Content,
) : ApiTimelineEvent() {
@Serializable
internal data class Content(
@SerialName("alias") val alias: String
)
}
@Serializable @Serializable
@SerialName("m.room.avatar") @SerialName("m.room.avatar")

View File

@ -27,6 +27,7 @@ internal class SyncSideEffects(
response.deviceLists?.changed?.ifEmpty { null }?.let { response.deviceLists?.changed?.ifEmpty { null }?.let {
notifyDevicesUpdated.notifyChanges(it, requestToken) notifyDevicesUpdated.notifyChanges(it, requestToken)
} }
oneTimeKeyProducer.onServerKeyCount(response.oneTimeKeysCount["signed_curve25519"] ?: ServerKeyCount(0)) oneTimeKeyProducer.onServerKeyCount(response.oneTimeKeysCount["signed_curve25519"] ?: ServerKeyCount(0))
val decryptedToDeviceEvents = decryptedToDeviceEvents(response) val decryptedToDeviceEvents = decryptedToDeviceEvents(response)

View File

@ -12,19 +12,22 @@ internal class RoomOverviewProcessor(
private val roomMembersService: RoomMembersService, private val roomMembersService: RoomMembersService,
) { ) {
suspend fun process(roomToProcess: RoomToProcess, previousState: RoomOverview?, lastMessage: LastMessage?): RoomOverview { suspend fun process(roomToProcess: RoomToProcess, previousState: RoomOverview?, lastMessage: LastMessage?): RoomOverview? {
val combinedEvents = roomToProcess.apiSyncRoom.state.stateEvents + roomToProcess.apiSyncRoom.timeline.apiTimelineEvents val combinedEvents = roomToProcess.apiSyncRoom.state.stateEvents + roomToProcess.apiSyncRoom.timeline.apiTimelineEvents
val isEncrypted = combinedEvents.any { it is ApiTimelineEvent.Encryption } val isEncrypted = combinedEvents.any { it is ApiTimelineEvent.Encryption }
val readMarker = roomToProcess.apiSyncRoom.accountData?.events?.filterIsInstance<ApiAccountEvent.FullyRead>()?.firstOrNull()?.content?.eventId val readMarker = roomToProcess.apiSyncRoom.accountData?.events?.filterIsInstance<ApiAccountEvent.FullyRead>()?.firstOrNull()?.content?.eventId
return when (previousState) { return when (previousState) {
null -> combinedEvents.filterIsInstance<ApiTimelineEvent.RoomCreate>().first().let { roomCreate -> null -> combinedEvents.filterIsInstance<ApiTimelineEvent.RoomCreate>().first().let { roomCreate ->
val roomName = roomDisplayName(combinedEvents) when (roomCreate.content.type) {
ApiTimelineEvent.RoomCreate.Content.Type.SPACE -> null
else -> {
val roomName = roomDisplayName(roomToProcess, combinedEvents)
val isGroup = roomToProcess.directMessage == null val isGroup = roomToProcess.directMessage == null
RoomOverview( val processedName = roomName ?: roomToProcess.directMessage?.let {
roomName = roomName ?: roomToProcess.directMessage?.let {
roomMembersService.find(roomToProcess.roomId, it)?.let { it.displayName ?: it.id.value } roomMembersService.find(roomToProcess.roomId, it)?.let { it.displayName ?: it.id.value }
}, }
RoomOverview(
roomName = processedName,
roomCreationUtc = roomCreate.utcTimestamp, roomCreationUtc = roomCreate.utcTimestamp,
lastMessage = lastMessage, lastMessage = lastMessage,
roomId = roomToProcess.roomId, roomId = roomToProcess.roomId,
@ -40,9 +43,12 @@ internal class RoomOverviewProcessor(
isEncrypted = isEncrypted, isEncrypted = isEncrypted,
) )
} }
}
}
else -> { else -> {
previousState.copy( previousState.copy(
roomName = previousState.roomName ?: roomDisplayName(combinedEvents), roomName = previousState.roomName ?: roomDisplayName(roomToProcess, combinedEvents),
lastMessage = lastMessage ?: previousState.lastMessage, lastMessage = lastMessage ?: previousState.lastMessage,
roomAvatarUrl = previousState.roomAvatarUrl ?: roomAvatar( roomAvatarUrl = previousState.roomAvatarUrl ?: roomAvatar(
roomToProcess.roomId, roomToProcess.roomId,
@ -58,9 +64,13 @@ internal class RoomOverviewProcessor(
} }
} }
private fun roomDisplayName(combinedEvents: List<ApiTimelineEvent>): String? { private suspend fun roomDisplayName(roomToProcess: RoomToProcess, combinedEvents: List<ApiTimelineEvent>): String? {
val roomName = combinedEvents.filterIsInstance<ApiTimelineEvent.RoomName>().lastOrNull() val roomName = combinedEvents.filterIsInstance<ApiTimelineEvent.RoomName>().lastOrNull()?.content?.name
return roomName?.content?.name ?: combinedEvents.filterIsInstance<ApiTimelineEvent.CanonicalAlias>().lastOrNull()?.content?.alias
?: roomToProcess.heroes?.let {
roomMembersService.find(roomToProcess.roomId, it).joinToString { it.displayName ?: it.id.value }
}
return roomName?.takeIf { it.isNotEmpty() }
} }
private suspend fun roomAvatar( private suspend fun roomAvatar(
@ -75,6 +85,7 @@ internal class RoomOverviewProcessor(
val filterIsInstance = combinedEvents.filterIsInstance<ApiTimelineEvent.RoomAvatar>() val filterIsInstance = combinedEvents.filterIsInstance<ApiTimelineEvent.RoomAvatar>()
filterIsInstance.lastOrNull()?.content?.url?.convertMxUrToUrl(homeServerUrl)?.let { AvatarUrl(it) } filterIsInstance.lastOrNull()?.content?.url?.convertMxUrToUrl(homeServerUrl)?.let { AvatarUrl(it) }
} }
else -> membersService.find(roomId, dmUser)?.avatarUrl else -> membersService.find(roomId, dmUser)?.avatarUrl
} }
} }

View File

@ -17,7 +17,7 @@ internal class RoomProcessor(
private val ephemeralEventsUseCase: EphemeralEventsUseCase, private val ephemeralEventsUseCase: EphemeralEventsUseCase,
) { ) {
suspend fun processRoom(roomToProcess: RoomToProcess, isInitialSync: Boolean): RoomState { suspend fun processRoom(roomToProcess: RoomToProcess, isInitialSync: Boolean): RoomState? {
val members = roomToProcess.apiSyncRoom.collectMembers(roomToProcess.userCredentials) val members = roomToProcess.apiSyncRoom.collectMembers(roomToProcess.userCredentials)
roomMembersService.insert(roomToProcess.roomId, members) roomMembersService.insert(roomToProcess.roomId, members)
@ -28,16 +28,17 @@ internal class RoomProcessor(
previousState?.events ?: emptyList(), previousState?.events ?: emptyList(),
) )
val overview = createRoomOverview(distinctEvents, roomToProcess, previousState) return createRoomOverview(distinctEvents, roomToProcess, previousState)?.let {
unreadEventsProcessor.processUnreadState(overview, previousState?.roomOverview, newEvents, roomToProcess.userCredentials.userId, isInitialSync) unreadEventsProcessor.processUnreadState(it, previousState?.roomOverview, newEvents, roomToProcess.userCredentials.userId, isInitialSync)
return RoomState(overview, distinctEvents).also { RoomState(it, distinctEvents).also {
roomDataSource.persist(roomToProcess.roomId, previousState, it) roomDataSource.persist(roomToProcess.roomId, previousState, it)
ephemeralEventsUseCase.processEvents(roomToProcess) ephemeralEventsUseCase.processEvents(roomToProcess)
} }
} }
}
private suspend fun createRoomOverview(distinctEvents: List<RoomEvent>, roomToProcess: RoomToProcess, previousState: RoomState?): RoomOverview { private suspend fun createRoomOverview(distinctEvents: List<RoomEvent>, roomToProcess: RoomToProcess, previousState: RoomState?): RoomOverview? {
val lastMessage = distinctEvents.sortedByDescending { it.utcTimestamp }.findLastMessage() val lastMessage = distinctEvents.sortedByDescending { it.utcTimestamp }.findLastMessage()
return roomOverviewProcessor.process(roomToProcess, previousState?.roomOverview, lastMessage) return roomOverviewProcessor.process(roomToProcess, previousState?.roomOverview, lastMessage)
} }
@ -56,6 +57,7 @@ private fun ApiSyncRoom.collectMembers(userCredentials: UserCredentials): List<R
avatarUrl = it.content.avatarUrl?.convertMxUrToUrl(userCredentials.homeServer)?.let { AvatarUrl(it) }, avatarUrl = it.content.avatarUrl?.convertMxUrToUrl(userCredentials.homeServer)?.let { AvatarUrl(it) },
) )
} }
else -> null else -> null
} }
} }

View File

@ -10,4 +10,5 @@ internal data class RoomToProcess(
val apiSyncRoom: ApiSyncRoom, val apiSyncRoom: ApiSyncRoom,
val directMessage: UserId?, val directMessage: UserId?,
val userCredentials: UserCredentials, val userCredentials: UserCredentials,
val heroes: List<UserId>?,
) )

View File

@ -30,7 +30,6 @@ internal class SyncReducer(
suspend fun reduce(isInitialSync: Boolean, sideEffects: SideEffectResult, response: ApiSyncResponse, userCredentials: UserCredentials): ReducerResult { suspend fun reduce(isInitialSync: Boolean, sideEffects: SideEffectResult, response: ApiSyncResponse, userCredentials: UserCredentials): ReducerResult {
val directMessages = response.directMessages() val directMessages = response.directMessages()
val invites = response.rooms?.invite?.map { roomInvite(it, userCredentials) } ?: emptyList() val invites = response.rooms?.invite?.map { roomInvite(it, userCredentials) } ?: emptyList()
val roomsLeft = findRoomsLeft(response, userCredentials) val roomsLeft = findRoomsLeft(response, userCredentials)
val newRooms = response.rooms?.join?.keys?.filterNot { roomDataSource.contains(it) } ?: emptyList() val newRooms = response.rooms?.join?.keys?.filterNot { roomDataSource.contains(it) } ?: emptyList()
@ -46,6 +45,7 @@ internal class SyncReducer(
apiSyncRoom = apiRoom, apiSyncRoom = apiRoom,
directMessage = directMessages[roomId], directMessage = directMessages[roomId],
userCredentials = userCredentials, userCredentials = userCredentials,
heroes = apiRoom.summary?.heroes,
), ),
isInitialSync = isInitialSync isInitialSync = isInitialSync
) )

View File

@ -38,6 +38,7 @@ internal class TimelineEventsProcessor(
is ApiTimelineEvent.RoomMember -> null is ApiTimelineEvent.RoomMember -> null
is ApiTimelineEvent.RoomName -> null is ApiTimelineEvent.RoomName -> null
is ApiTimelineEvent.RoomTopic -> null is ApiTimelineEvent.RoomTopic -> null
is ApiTimelineEvent.CanonicalAlias -> null
ApiTimelineEvent.Ignored -> null ApiTimelineEvent.Ignored -> null
} }
roomEvent roomEvent

View File

@ -70,4 +70,5 @@ private fun aRoomToProcess(ephemeral: ApiEphemeral? = null) = RoomToProcess(
anApiSyncRoom(ephemeral = ephemeral), anApiSyncRoom(ephemeral = ephemeral),
directMessage = null, directMessage = null,
userCredentials = aUserCredentials(), userCredentials = aUserCredentials(),
heroes = null,
) )

View File

@ -101,4 +101,4 @@ internal fun aRoomToProcess(
apiSyncRoom: ApiSyncRoom = anApiSyncRoom(), apiSyncRoom: ApiSyncRoom = anApiSyncRoom(),
directMessage: UserId? = null, directMessage: UserId? = null,
userCredentials: UserCredentials = aUserCredentials(), userCredentials: UserCredentials = aUserCredentials(),
) = RoomToProcess(roomId, apiSyncRoom, directMessage, userCredentials) ) = RoomToProcess(roomId, apiSyncRoom, directMessage, userCredentials, heroes = null)

View File

@ -9,7 +9,7 @@ test {
dependencies { dependencies {
kotlinTest(it) kotlinTest(it)
testImplementation 'app.cash.turbine:turbine:0.9.0' testImplementation 'app.cash.turbine:turbine:0.10.0'
testImplementation Dependencies.mavenCentral.kotlinSerializationJson testImplementation Dependencies.mavenCentral.kotlinSerializationJson

View File

@ -20,10 +20,7 @@ import org.junit.jupiter.api.MethodOrderer
import org.junit.jupiter.api.Order import org.junit.jupiter.api.Order
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestMethodOrder import org.junit.jupiter.api.TestMethodOrder
import test.MatrixTestScope import test.*
import test.TestMatrix
import test.flowTest
import test.restoreLoginAndInitialSync
import java.nio.file.Paths import java.nio.file.Paths
import java.util.* import java.util.*
@ -35,8 +32,8 @@ class SmokeTest {
@Test @Test
@Order(1) @Order(1)
fun `can register accounts`() = runTest { fun `can register accounts`() = runTest {
SharedState._alice = createAndRegisterAccount() SharedState._alice = createAndRegisterAccount("alice")
SharedState._bob = createAndRegisterAccount() SharedState._bob = createAndRegisterAccount("bob")
} }
@Test @Test
@ -94,7 +91,7 @@ class SmokeTest {
@Test @Test
fun `can import E2E room keys file`() = runTest { fun `can import E2E room keys file`() = runTest {
val ignoredUser = TestUser("ignored", RoomMember(UserId("ignored"), null, null), "ignored") val ignoredUser = TestUser("ignored", RoomMember(UserId("ignored"), null, null), "ignored", "ignored")
val cryptoService = TestMatrix(ignoredUser, includeLogging = true).client.cryptoService() val cryptoService = TestMatrix(ignoredUser, includeLogging = true).client.cryptoService()
val stream = loadResourceStream("element-keys.txt") val stream = loadResourceStream("element-keys.txt")
@ -133,10 +130,10 @@ class SmokeTest {
} }
} }
private suspend fun createAndRegisterAccount(): TestUser { private suspend fun createAndRegisterAccount(testUsername: String): TestUser {
val aUserName = "${UUID.randomUUID()}" val aUserName = "${UUID.randomUUID()}"
val userId = UserId("@$aUserName:localhost:8080") val userId = UserId("@$aUserName:localhost:8080")
val aUser = TestUser("aaaa11111zzzz", RoomMember(userId, aUserName, null), HTTPS_TEST_SERVER_URL) val aUser = TestUser("aaaa11111zzzz", RoomMember(userId, aUserName, null), HTTPS_TEST_SERVER_URL, testUsername)
val result = TestMatrix(aUser, includeLogging = true, includeHttpLogging = true) val result = TestMatrix(aUser, includeLogging = true, includeHttpLogging = true)
.client .client
@ -167,26 +164,35 @@ private suspend fun login(user: TestUser) {
} }
object SharedState { object SharedState {
val alice: TestUser val alice: TestUser
get() = _alice!! get() = _alice!!
var _alice: TestUser? = null var _alice: TestUser? = null
set(value) {
field = value!!
TestUsers.users.add(value)
}
val bob: TestUser val bob: TestUser
get() = _bob!! get() = _bob!!
var _bob: TestUser? = null var _bob: TestUser? = null
set(value) {
field = value!!
TestUsers.users.add(value)
}
val sharedRoom: RoomId val sharedRoom: RoomId
get() = _sharedRoom!! get() = _sharedRoom!!
var _sharedRoom: RoomId? = null var _sharedRoom: RoomId? = null
} }
data class TestUser(val password: String, val roomMember: RoomMember, val homeServer: String) data class TestUser(val password: String, val roomMember: RoomMember, val homeServer: String, val testName: String)
data class TestMessage(val content: String, val author: RoomMember) data class TestMessage(val content: String, val author: RoomMember)
fun String.from(roomMember: RoomMember) = TestMessage("$this - ${UUID.randomUUID()}", roomMember) fun String.from(roomMember: RoomMember) = TestMessage("$this - ${UUID.randomUUID()}", roomMember)
fun testAfterInitialSync(block: suspend MatrixTestScope.(TestMatrix, TestMatrix) -> Unit) { fun testAfterInitialSync(block: suspend MatrixTestScope.(TestMatrix, TestMatrix) -> Unit) {
restoreLoginAndInitialSync(TestMatrix(SharedState.alice, includeLogging = false), TestMatrix(SharedState.bob, includeLogging = false), block) restoreLoginAndInitialSync(TestMatrix(SharedState.alice, includeLogging = true), TestMatrix(SharedState.bob, includeLogging = false), block)
} }
private fun Flow<Verification.State>.automaticVerification(testMatrix: TestMatrix) = this.onEach { private fun Flow<Verification.State>.automaticVerification(testMatrix: TestMatrix) = this.onEach {

View File

@ -5,17 +5,13 @@ package test
import TestMessage import TestMessage
import TestUser import TestUser
import app.dapk.st.core.extensions.ifNull import app.dapk.st.core.extensions.ifNull
import app.dapk.st.matrix.common.MxUrl
import app.dapk.st.matrix.common.RoomId import app.dapk.st.matrix.common.RoomId
import app.dapk.st.matrix.common.RoomMember import app.dapk.st.matrix.common.RoomMember
import app.dapk.st.matrix.common.convertMxUrToUrl
import app.dapk.st.matrix.http.MatrixHttpClient
import app.dapk.st.matrix.message.MessageService import app.dapk.st.matrix.message.MessageService
import app.dapk.st.matrix.message.messageService import app.dapk.st.matrix.message.messageService
import app.dapk.st.matrix.sync.RoomEvent import app.dapk.st.matrix.sync.RoomEvent
import app.dapk.st.matrix.sync.syncService import app.dapk.st.matrix.sync.syncService
import io.ktor.client.* import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.request.* import io.ktor.client.request.*
import io.ktor.client.statement.* import io.ktor.client.statement.*
import io.ktor.util.cio.* import io.ktor.util.cio.*
@ -28,7 +24,6 @@ import org.amshove.kluent.fail
import org.amshove.kluent.shouldBeEqualTo import org.amshove.kluent.shouldBeEqualTo
import java.io.File import java.io.File
import java.math.BigInteger import java.math.BigInteger
import java.net.URL
import java.security.MessageDigest import java.security.MessageDigest
import java.util.* import java.util.*

View File

@ -46,6 +46,12 @@ import java.io.File
import java.time.Clock import java.time.Clock
import javax.imageio.ImageIO import javax.imageio.ImageIO
object TestUsers {
val users = mutableSetOf<TestUser>()
}
class TestMatrix( class TestMatrix(
private val user: TestUser, private val user: TestUser,
temporaryDatabase: Boolean = false, temporaryDatabase: Boolean = false,
@ -53,10 +59,11 @@ class TestMatrix(
includeLogging: Boolean = false, includeLogging: Boolean = false,
) { ) {
private val errorTracker = PrintingErrorTracking(prefix = user.roomMember.id.value.split(":")[0]) private val errorTracker = PrintingErrorTracking(prefix = user.testName)
private val logger: MatrixLogger = { tag, message -> private val logger: MatrixLogger = { tag, message ->
if (includeLogging) { if (includeLogging) {
println("${user.roomMember.id.value.split(":")[0]} $tag $message") val messageWithIdReplaceByName = TestUsers.users.fold(message) { acc, user -> acc.replace(user.roomMember.id.value, "*${user.testName}") }
println("${user.testName} $tag $messageWithIdReplaceByName")
} }
} }

View File

@ -1,4 +1,4 @@
{ {
"code": 16, "code": 17,
"name": "15/09/2022-V1" "name": "19/09/2022-V1"
} }